【问题标题】:Improving a prime sieve algorithm改进素筛算法
【发布时间】:2011-03-06 21:38:10
【问题描述】:

我正在尝试制作一个像样的 Java 程序,它可以生成从 1 到 N 的素数(主要用于 Project Euler 问题)。

目前我的算法如下:

初始化一个布尔数组(如果 N 足够大,则初始化一个位数组)以使它们全部为假,并初始化一个整数数组来存储找到的素数。

设置一个整数,s等于最低素数,(即2)

虽然 s 是

在数组/位数组中将 s 的所有倍数(从 s^2 开始)设置为 true。

在数组/位数组中找到下一个为假的最小索引,将其用作 s 的新值。

结束。

遍历数组/位数组,对于每个为假的值,将相应的索引放入素数数组中。

现在,我尝试跳过不是 6k + 1 或 6k + 5 形式的数字,但这只会让我的速度提高约 2 倍,而我看到程序的运行速度比我的快几个数量级(尽管非常复杂的代码),例如here

我可以做些什么来改进?

编辑:好的,这是我的实际代码(N of 1E7):

int l = 10000000, n = 2, sqrt = (int) Math.sqrt(l);
boolean[] nums = new boolean[l + 1];
int[] primes = new int[664579];

while(n <= sqrt){
    for(int i = 2 * n; i <= l; nums[i] = true, i += n);
    for(n++; nums[n]; n++);
}

for(int i = 2, k = 0; i < nums.length; i++) if(!nums[i]) primes[k++] = i;

在我的 2.0GHz 机器上运行大约 350 毫秒。

【问题讨论】:

  • 虽然 java 不是这里最快的选择,但如果您发布(完整但紧凑的)代码,我们可能会发现性能问题。
  • 顺便说一下,实施埃拉托色尼筛法一点也不复杂:)

标签: java algorithm primes


【解决方案1】:

虽然 s 是
人们在此类算法中经常犯的一个错误是没有预先计算平方根。

while (s <= sqrt(N)) {

慢得多
int limit = sqrt(N);
while (s <= limit) {

但总的来说,Eiko 在他的评论中是对的。如果您希望人们提供低级优化,您必须提供代码。

更新好的,现在关于您的代码。

您可能会注意到代码中的迭代次数仅比“l”大一点。 (您可以将计数器放在第一个“for”循环中,它只会大 2-3 倍)而且,显然,您的解决方案的复杂性不能低于 O(l)(你不能少于 'l ' 迭代)。

真正不同的是有效地访问内存。请注意,写那篇文章的人试图减少存储大小,不仅仅是因为他对内存很贪婪。制作紧凑的数组可以让您更好地使用缓存,从而提高速度。

我刚刚用 int[] 替换了 boolean[] 并立即获得了 x2 的速度增益。 (和 8 倍内存)而且我什至没有尝试有效地做到这一点。

更新2
这很简单。您只需将每个分配 a[i] = true 替换为 a[i/32] |= 1 &lt;&lt; (i%32) 并将每个读取操作 a[i] 替换为 (a[i/32] &amp; (1 &lt;&lt; (i%32))) != 0。很明显,boolean[] aint[] a

从第一个替换开始,应该清楚它是如何工作的:如果f(i) 为真,那么在整数a[i/32] 中有一个位1,在位置i%32(Java 中的int 正好如您所知,32 位)。

您可以更进一步,将i/32 替换为i &gt;&gt; 5,将i%32 替换为i&amp;31。您还可以为数组中 0 到 31 之间的每个 j 预先计算所有 1 &lt;&lt; j

但遗憾的是,我认为在 Java 中你无法在这方面接近 C。更不用说,那个家伙还使用了许多其他棘手的优化,我同意如果他制作 cmets,他的价值会更高。

【讨论】:

  • sqrt(N) 可能使用自然对数来计算比 while (ss s 并使用它要慢的值。
  • 不,我预先计算了 N 的平方根
  • 是的,你做到了,但我认为你会稍微快一点 while(n*n
  • 用上面的代码试了一下(N = 1E7);速度没有明显变化
  • 我得到了 3 倍的加速(使用按位破解)。这太棒了:D
【解决方案2】:

使用BitSet 将使用更少的内存。 Sieve 算法相当简单,因此您可以简单地“设置”BitSet 上的位位置,然后迭代以确定素数。

【讨论】:

  • BitSet 实际上更慢。使用 new BitSet(l+1) 进行初始化的时间是 new boolean[l+1] 的 2 倍,并且循环也比使用 boolean[] 慢一些。我认为减速是由方法调用 set(i)、get(i) 和 nextClearBit(i) 的开销引起的,而不是从数组中访问值。
  • 请注意我没有提到速度:P
【解决方案3】:

您是否在跳过非 6k+1 和 6k+5 形式的数字时也使数组更小? 我只测试了忽略 2k 形式的数字,这给了我约 4 倍的速度(440 ms -> 120 ms):

int l = 10000000, n = 1, sqrt = (int) Math.sqrt(l);
int m = l/2;
boolean[] nums = new boolean[m + 1];
int[] primes = new int[664579];
int i, k;

while (n <= sqrt) {
  int x = (n<<1)+1;
  for (i = n+x; i <= m; nums[i] = true, i+=x);
  for (n++; nums[n]; n++);
}

primes[0] = 2;
for (i = 1, k = 1; i < nums.length; i++) {
  if (!nums[i])
    primes[k++] = (i<<1)+1;
}

【讨论】:

  • 有趣...对我来说,它提供了 190ms)但这仍然很好,特别是考虑到它可以与其他优化结合使用。谢谢! ^_^
【解决方案4】:

以下来自我的 Project Euler Library...它是 Eratosthenes 筛的轻微变化...我不确定,但我认为它称为 Euler Sieve。

1) 它使用一个 BitSet(所以只有 1/8 的内存) 2) 仅将位集用于奇数...(另一个 1/2,因此是 1/16)

注意:内循环(对于倍数)从“n*n”而不是“2*n”开始,并且增量“2*n”的倍数只被划掉......因此加快了速度。

private void beginSieve(int mLimit) 
{ 
    primeList = new BitSet(mLimit>>1); 
    primeList.set(0,primeList.size(),true); 

    int sqroot = (int) Math.sqrt(mLimit); 
    primeList.clear(0); 
    for(int num = 3; num <= sqroot; num+=2) 
    { 
        if( primeList.get(num >> 1) ) 
        { 
            int inc = num << 1;
            for(int factor = num * num; factor < mLimit; factor += inc) 
            { 
                //if( ((factor) & 1) == 1) 
                //{ 
                    primeList.clear(factor >> 1); 
                //} 
            } 
        } 
    } 
} 

这是检查数字是否为素数的函数...

public boolean isPrime(int num) 
{ 
    if( num < maxLimit)
    {
        if( (num & 1) == 0) 
            return ( num == 2); 
        else 
            return primeList.get(num>>1);
    }
    return false;
} 

【讨论】:

    【解决方案5】:

    您可以在检测它们时执行“将相应的索引放入素数数组”的步骤,然后遍历数组,但这就是我现在能想到的全部。

    【讨论】:

    • 我不确定这在我上面发布的代码中是否可行。你能详细说明一下吗?
    • 我在你的代码之前发布了这个。所以我不会知道。我只是按照你的描述进行。
    【解决方案6】:

    我最近使用 BitSet 编写了一个简单的筛子实现,以获得乐趣(每个人都说不这样做,但它是有效存储大量数据的最佳现成方法)。性能对我来说似乎相当不错,但我仍在努力改进它。

    public class HelloWorld {
        private static int LIMIT = 2140000000;//Integer.MAX_VALUE broke things.
        private static BitSet marked;
    
        public static void main(String[] args) {
             long startTime = System.nanoTime();
            init();
            sieve();
             long estimatedTime = System.nanoTime() - startTime;
            System.out.println((float)estimatedTime/1000000000); //23.835363 seconds
            System.out.println(marked.size()); //1070000000 ~= 127MB
        }
    
        private static void init()
        {
            double size = LIMIT * 0.5 - 1;
            marked = new BitSet();
            marked.set(0,(int)size, true);
        }
    
        private static void sieve()
        {
            int i = 0;
            int cur = 0; 
            int add = 0;
            int pos = 0;
    
            while(((i<<1)+1)*((i<<1)+1) < LIMIT)
            {
                pos = i;
                if(marked.get(pos++))
                {
                    cur = pos;
                    add = (cur<<1);
                    pos += add*cur + cur - 1;
                    while(pos < marked.length() && pos > 0)
                    {
                        marked.clear(pos++);
                        pos += add;
                    }
                }
                i++;
            }
        }
    
        private static void readPrimes()
        {
            int pos = 0;
            while(pos < marked.length())
            {
                if(marked.get(pos++))
                {
                    System.out.print((pos<<1)+1);
                    System.out.print("-");
                }
            }
        }
    }
    

    使用更小的 LIMIT(比如 10,000,000 耗时 0.077479 秒),我们得到比 OP 快得多的结果。

    【讨论】:

      【解决方案7】:

      我敢打赌,java 在处理位时的性能很糟糕...... 从算法上讲,您指出的链接应该足够了

      【讨论】:

      • 问题是我不知道链接中发生了什么。
      • 具体有什么不明白的?
      • primes_sieve.c 链接中的源代码大约一半似乎几乎无法阅读<.>
      • html 页面包含有用的信息
      • 嗯...任何想法如何有效地实施它在“选择合适的筛子尺寸”子标题下所说的内容。如果 Java 的 VM 决定是否将内容放入 CPU 缓存中,如果“顺序筛选”使用足够小的筛子以适应缓存大小?
      【解决方案8】:

      您是否尝试过谷歌搜索,例如对于“java素数”。我做了并挖掘了这个简单的改进:

      http://www.anyexample.com/programming/java/java_prime_number_check_%28primality_test%29.xml

      当然,您可以在 google 上找到更多信息。

      【讨论】:

      • 有比顺序应用素数检查更有效的生成素数的方法。
      • 当然,但是来吧,“我如何生成前 N 个素数”的问题并不完全是新的,谷歌应该足以找到每一种可能的改进。
      【解决方案9】:

      这是我的 Erastothenes 筛法代码,这实际上是我能做到的最有效的方法:

      final int MAX = 1000000;
      int p[]= new int[MAX];
      p[0]=p[1]=1;
      int prime[] = new int[MAX/10];
      prime[0]=2;
      void sieve()
      {
          int i,j,k=1;
          for(i=3;i*i<=MAX;i+=2)
          {
              if(p[i])
                  continue;
              for(j=i*i;j<MAX;j+=2*i)
                  p[j]=1;
          }
          for(i=3;i<MAX;i+=2)
          {
              if(p[i]==0)
                  prime[k++]=i;
          }
          return;
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2020-07-24
        • 1970-01-01
        • 2011-03-18
        • 1970-01-01
        • 2021-05-25
        • 1970-01-01
        • 1970-01-01
        • 2013-05-12
        相关资源
        最近更新 更多