我一直在玩Project Euler挑战,以帮助提高我对Java的了解。特别是,我为problem 14编写了以下代码,要求您找到最长的Collatz链,其开头的数字低于1,000,000。它假设子链很可能不止一次出现,并且通过将它们存储在缓存中,不会进行冗余计算。
Collatz.java:
import java.util.HashMap;
public class Collatz {
private HashMap<Long, Integer> chainCache = new HashMap<Long, Integer>();
public void initialiseCache() {
chainCache.put((long) 1, 1);
}
private long collatzOp(long n) {
if(n % 2 == 0) {
return n/2;
}
else {
return 3*n +1;
}
}
public int collatzChain(long n) {
if(chainCache.containsKey(n)) {
return chainCache.get(n);
}
else {
int count = 1 + collatzChain(collatzOp(n));
chainCache.put(n, count);
return count;
}
}
}
ProjectEuler14.java:
public class ProjectEuler14 {
public static void main(String[] args) {
Collatz col = new Collatz();
col.initialiseCache();
long limit = 1000000;
long temp = 0;
long longestLength = 0;
long index = 1;
for(long i = 1; i < limit; i++) {
temp = col.collatzChain(i);
if(temp > longestLength) {
longestLength = temp;
index = i;
}
}
System.out.println(index + " has the longest chain, with length " + longestLength);
}
}
这很有效。并根据&#34; measure-command&#34;来自Windows Powershell的命令,执行大约需要1708毫秒(1.708秒)。
然而,在通过论坛阅读之后,我注意到有些人编写了看似天真的代码,从头开始计算每个链,似乎比我更好的执行时间。我(从概念上)采用了其中一个答案,并将其翻译成Java:
NaiveProjectEuler14.java:
public class NaiveProjectEuler14 {
public static void main(String[] args) {
int longest = 0;
int numTerms = 0;
int i;
long j;
for (i = 1; i <= 10000000; i++) {
j = i;
int currentTerms = 1;
while (j != 1) {
currentTerms++;
if (currentTerms > numTerms){
numTerms = currentTerms;
longest = i;
}
if (j % 2 == 0){
j = j / 2;
}
else{
j = 3 * j + 1;
}
}
}
System.out.println("Longest: " + longest + " (" + numTerms + ").");
}
}
在我的机器上,这也给出了正确的答案,但它在0.502毫秒内给出了它 - 是原始程序速度的三分之一。起初我认为创建一个HashMap可能有一点点开销,并且所花费的时间太小而无法得出任何结论。但是,如果我在两个程序中将上限从1,000,000增加到10,000,000,NaiveProjectEuler14需要4709毫秒(4.709秒),而ProjectEuler14需要高达25324毫秒(25.324秒)!
为什么ProjectEuler14需要这么长时间?我能理解的唯一解释是在HashMap数据结构中存储大量的对会增加了巨大的开销,但我不明白为什么会出现这种情况。我还尝试记录在程序过程中存储的(键,值)对的数量(1,000,000个案例为2,168,611对,10,000,000个案例为21,730,849对),并为HashMap提供一点点数量。构造函数,以便它最多只需要调整一次,但这似乎不会影响执行时间。
有没有人知道为什么memoized版本要慢很多?
答案 0 :(得分:4)
这个不幸的现实有一些原因:
可比较
public static void main(String[] args) {
int longest = 0;
int numTerms = 0;
int i;
long j;
Map<Long, Integer> map = new HashMap<>();
for (i = 1; i <= 10000000; i++) {
j = i;
Integer terms = map.get(i);
if (terms != null) {
continue;
}
int currentTerms = 1;
while (j != 1) {
currentTerms++;
if (currentTerms > numTerms){
numTerms = currentTerms;
longest = i;
}
if (j % 2 == 0){
j = j / 2;
// Maybe check the map only here
Integer m = map.get(j);
if (m != null) {
currentTerms += m;
break;
}
}
else{
j = 3 * j + 1;
}
}
map.put(j, currentTerms);
}
System.out.println("Longest: " + longest + " (" + numTerms + ").");
}
这并没有真正做好充分的记忆。对于增加参数,不检查3*j+1
会略微减少未命中(但也可能会跳过默认值)。
记忆通过每次通话的繁重计算而存在。如果函数由于深度递归而不是计算而花费很长时间,则每个函数调用的memoization开销都是负数。