在调查给定数字的Hailstone序列(Collatz conjecture)的长度时,我对过去几天(更多来自算法而非数学观点)特别感兴趣。实现递归算法可能是计算长度的最简单方法,但在我看来,这似乎是不必要的计算时间浪费。许多序列重叠;以3的Hailstone序列为例:
3 -> 10 -> 5 -> 16 -> 8 -> 4 -> 2 -> 1
这长度为7;更具体地说,它需要7次操作才能达到1.如果我们再拿6:
6 -> 3 -> ...
我们已经立即注意到我们已经计算过这个,所以我们只需添加3的序列长度,而不是再次遍历所有这些数字,大大减少了计算每个数字的序列长度所需的操作数量
我尝试使用HashMap在Java中实现这一点(考虑到O(1)概率get / put复杂度似乎是合适的):
import java.util.HashMap;
/* NOTE: cache.put(1,0); is called in main to act as the
* 'base case' of sorts.
*/
private static HashMap<Long, Long> cache = new HashMap<>();
/* Returns length of sequence, pulling prerecorded value from
* from cache whenever possible, and saving unrecorded values
* to the cache.
*/
static long seqLen(long n) {
long count = 0, m = n;
while (true) {
if (cache.containsKey(n)) {
count += cache.get(n);
cache.put(m, count);
return count;
}
else if (n % 2 == 0) {
n /= 2;
}
else {
n = 3*n + 1;
}
count++;
}
}
seqLen
基本上做的是从一个给定的数字开始并通过该数字的Hailstone序列,直到它遇到cache
中已有的数字,在这种情况下它会添加转到count
的当前值,然后将值和HashMap中的关联序列长度记录为(key,val)
对。
我还有以下相当标准的递归算法进行比较:
static long recSeqLen(long n) {
if (n == 1) {
return 0;
}
else if (n % 2 == 0) {
return 1 + recSeqLen(n / 2);
}
else return 1 + recSeqLen(3*n + 1);
}
所有帐户的日志记录算法应该比天真的递归方法运行得快得多。但是在大多数情况下,它根本不会运行得那么快,而对于较大的输入,它实际上运行较慢。运行以下代码会产生随n
的大小更改而变化很大的时间:
long n = ... // However many numbers I want to calculate sequence
// lengths for.
long st = System.nanoTime();
// Iterative logging algorithm
for (long i = 2; i < n; i++) {
seqLen(i);
}
long et = System.nanoTime();
System.out.printf("HashMap algorithm: %d ms\n", (et - st) / 1000000);
st = System.nanoTime();
// Using recursion without logging values:
for (long i = 2; i < n; i++) {
recSeqLen(i);
}
et = System.nanoTime();
System.out.printf("Recusive non-logging algorithm: %d ms\n",
(et - st) / 1000000);
n = 1,000
:两种算法都是~2ms n = 100,000
:迭代日志记录为~65ms,递归非日志记录为~75ms n = 1,000,000
:~500ms和~900ms n = 10,000,000
:~14,000ms和~10,000ms 值越高,我就会出现内存错误,因此我无法检查模式是否继续。
所以我的问题是:为什么日志记录算法突然开始花费更长而不是大的n值的朴素递归算法?
完全废弃HashMaps并选择一个简单的数组结构(以及删除检查值是否在数组中的部分开销)会产生所需的效率:
private static final int CACHE_SIZE = 80000000;
private static long[] cache = new long[CACHE_SIZE];
static long seqLen(long n) {
int count = 0;
long m = n;
do {
if (n % 2 == 0) {
n /= 2;
}
else {
n = 3*n + 1;
}
count++;
} while (n > m);
count += cache[(int)n];
cache[(int)m] = count;
return count;
}
迭代整个缓存大小(8000万)现在只需3秒,而使用递归算法则需要93秒。 HashMap算法抛出了内存错误,因此它甚至无法进行比较,但考虑到它在较低值时的行为,我觉得它不能很好地比较。
答案 0 :(得分:1)
关闭袖口,我猜它花了很多时间重新分配哈希图。听起来好像你是空着它并继续添加东西。这意味着随着它的大小增加,它将需要分配更大的内存块来存储您的数据,并重新计算所有元素的哈希值,即O(N)。尝试将大小预先分配到您希望放在那里的大小。有关详细讨论,请参阅https://docs.oracle.com/javase/8/docs/api/java/util/HashMap.html。