java – Memoization效率问题(Collat​​z Hailstone序列)

在调查给定数字的Hailstone序列( Collatz conjecture)的长度时,我对过去几天(更多来自算法而不是数学角度)特别感兴趣.实现递归算法可能是计算长度的最简单方法,但在我看来,这似乎是不必要的计算时间浪费.许多序列重叠;以3’s 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序列,直到它遇到缓存中已有的数字,在这种情况下,它会将其添加到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算法会抛出内存错误,因此甚至无法进行比较,但鉴于它在较低值时的行为,我觉得它不能很好地比较.

最佳答案 关闭袖口,我猜它花了很多时间重新分配哈希映射.听起来好像你是空着它并继续添加东西.这意味着随着它的大小增加,它将需要分配更大的内存块来存储您的数据,并重新计算所有元素的哈希值,即O(N).尝试将大小预先分配到您希望放在那里的大小.有关更多讨论,请参见 https://docs.oracle.com/javase/8/docs/api/java/util/HashMap.html.

点赞