Project Euler的问题10:
该程序针对较小的数字运行,并且减速到数十万的爬行速度。 200万,即使程序看起来仍在运行,答案也无法显示。
我正在尝试实施 Eratosthenes的Sieve 。它应该非常快。我的方法出了什么问题?
import java.util.ArrayList;
public class p010
{
/**
* The sum of the primes below 10 is 2 + 3 + 5 + 7 = 17
* Find the sum of all the primes below two million.
* @param args
*/
public static void main(String[] args)
{
ArrayList<Integer> primes = new ArrayList<Integer>();
int upper = 2000000;
for (int i = 2; i < upper; i++)
{
primes.add(i);
}
int sum = 0;
for (int i = 0; i < primes.size(); i++)
{
if (isPrime(primes.get(i)))
{
for (int k = 2; k*primes.get(i) < upper; k++)
{
if (primes.contains(k*primes.get(i)))
{
primes.remove(primes.indexOf(k*primes.get(i)));
}
}
}
}
for (int i = 0; i < primes.size(); i++)
{
sum += primes.get(i);
}
System.out.println(sum);
}
public static boolean isPrime(int number)
{
boolean returnVal = true;
for (int i = 2; i <= Math.sqrt(number); i ++)
{
if (number % i == 0)
{
returnVal = false;
}
}
return returnVal;
}
}
答案 0 :(得分:5)
您似乎正在尝试实施Eratosthenes的Sieve,它应该表现得更好O(N^2)
(事实上,维基百科说它是O(N log(log N))
...)。
根本问题是您选择的数据结构。您已选择将剩余的主要候选人集合表示为ArrayList
素数。这意味着您的测试以查看数字是否仍在集合中需要进行O(N)
比较...其中N
是剩余素数的数量。然后您使用ArrayList.remove(int)
删除非素数... O(N)
也是。
所有这些都会使您的Sieve实施更糟而不是O(N^2)
。
解决方案是将ArrayList<Integer>
替换为boolean[]
,其中boolean
数组中的位置(索引)表示数字,布尔值表示数字是否为素数/可能是素数,或者不是素数。
(还有其他问题,我没有注意到......见其他答案。)
答案 1 :(得分:3)
这里有一些问题。首先,我们来谈谈算法。您的isPrime
方法实际上是筛子旨在避免的。当您在筛子中找到一个数字时,您已经知道它是主要的,您不需要进行测试。如果它不是素数,它就已经被淘汰作为较低数量的因素。
所以,第1点:
isPrime
方法。它永远不应该返回假。然后,存在实施问题。 primes.contains
和primes.remove
是问题所在。它们在ArrayList
上以线性时间运行,因为它们需要检查每个元素或重写大部分后备数组。
第2点:
boolean[]
,或使用其他更合适的数据结构。)我通常会使用boolean primes = new boolean[upper+1]
之类的内容,并在n
时定义!(primes[n])
。 (我只是忽略元素0和1,所以我不必减去索引。)要“删除”一个元素,我将其设置为true。我想你也可以使用像TreeSet<Integer>
这样的东西。使用boolean[]
,该方法几乎是瞬时的。
第3点:
sum
需要很长时间。答案(大约1.429e11)大于整数(2 ^ 31-1)如果你愿意,我可以发布工作代码,但这里是测试输出,没有剧透:
public static void main(String[] args) {
long value;
long start;
long finish;
start = System.nanoTime();
value = arrayMethod(2000000);
finish = System.nanoTime();
System.out.printf("Value: %.3e, time: %4d ms\n", (double)value, (finish-start)/1000000);
start = System.nanoTime();
value = treeMethod(2000000);
finish = System.nanoTime();
System.out.printf("Value: %.3e, time: %4d ms\n", (double)value, (finish-start)/1000000);
}
输出:
Using boolean[]
Value: 1.429e+11, time: 17 ms
Using TreeSet<Integer>
Value: 1.429e+11, time: 4869 ms
修改强> 由于发布了剧透,这是我的代码:
public static long arrayMethod(int upper) {
boolean[] primes = new boolean[upper+1];
long sum = 0;
for (int i = 2; i <=upper; i++) {
if (!primes[i]) {
sum += i;
for (int k = 2*i; k <= upper; k+=i) {
primes[k] = true;
}
}
}
return sum;
}
public static long treeMethod(int upper) {
TreeSet<Integer> primes = new TreeSet<Integer>();
for (int i = 2; i <= upper; i++) {
primes.add(i);
}
long sum = 0;
for (Integer i = 2; i != null; i=primes.higher(i)) {
sum += i;
for (int k = 2*i; k <= upper; k+=i) {
primes.remove(k);
}
}
return sum;
}
答案 2 :(得分:0)
两件事:
您的代码很难遵循。你有一个名为“primes”的列表,其中包含非素数!
此外,您应该强烈考虑数组列表是否合适。在这种情况下,LinkedList会更有效率。
这是为什么?数组列表必须通过以下方式不断调整数组大小:要求新内存创建数组,然后在新创建的数组中复制旧内存。链接列表只会通过更改指针来调整内存大小。这要快得多!但是,我不认为通过进行此更改可以挽救您的算法。
如果您需要非顺序访问项目,则应使用数组列表,此处(使用合适的算法)您需要按顺序访问项目。
另外,你的算法很慢。接受SJuan76(或gyrogearless)的建议,谢谢sjuan76
答案 3 :(得分:0)
你的程序不是 Eratosthenes的筛子;模运算符将它放弃。你的程序将是O(n ^ 2),其中一个适当的Eratosthenes筛子是O(n log log n),它基本上是n。这是我的版本;我将留给您使用适当的数值数据类型转换为Java:
function sumPrimes(n)
sum := 0
sieve := makeArray(2..n, True)
for p from 2 to n step 1
if sieve[p]
sum := sum + p
for i from p * p to n step p
sieve[i] := False
return sum
如果您对使用素数编程感兴趣,我会在我的博客上谦虚地推荐this essay。
答案 4 :(得分:0)
现代CPU上the sieve of Eratosthenes经典实现效率的关键是直接(即非顺序)内存访问。幸运的是,ArrayList<E>
does implement RandomAccess
。
筛选效率的另一个关键是它与索引和值的混合,就像在integer sorting中一样。实际上从序列中删除任何数字都会破坏这种直接寻址的能力而无需任何计算。我们必须标记,而不是删除任何复合材料,因此任何大于它的数字都将保留在序列中的位置。
ArrayList<Integer>
可以用于此目的(除了占用更多的内存而不是严格必要的内容,但是200万这是无关紧要的。)
因此,您的代码采用最少的修改修补程序(也将sum
更改为long
,而其他人也指出),变为
import java.util.ArrayList;
public class Main
{
/**
* The sum of the primes below 10 is 2 + 3 + 5 + 7 = 17
* Find the sum of all the primes below two million.
* @param args
*/
public static void main(String[] args)
{
ArrayList<Integer> primes = new ArrayList<Integer>();
int upper = 5000;
primes.ensureCapacity(upper);
for (int i = 0; i < upper; i++) {
primes.add(i);
}
long sum = 0;
for (int i = 2; i <= upper / i; i++) {
if ( primes.get(i) > 0 ) {
for (int k = i*i; k < upper ; k+=i) {
primes.set(k, 0);
}
}
}
for (int i = 2; i < upper; i++) {
sum += primes.get(i);
}
System.out.println(sum);
}
}
查找2000000 in half a second on Ideone的结果。 projected run time for your original code there:10到400小时(!)。
要在遇到代码较慢的情况下查找运行时的粗略估计值,您应该始终尝试找出empirical orders of growth:运行它以获得一些小尺寸n1
,然后更大的尺寸{{} 1}},记录运行时间n2
和t1
。如果是t2
,那么t ~ n^a
。
对于原始代码,在a = log(t2/t1) / log(n2/n1)
上限值10k .. 20k .. 40k
范围内衡量的经验增长顺序为N
。对于固定代码,它比~ N^1.7 .. N^1.9 .. N^2.1
(事实上,它在测试范围~ N
中的~ N^0.9
)更快。理论上的复杂性为0.5 mln .. 1 mln .. 2 mln
。