使用递归求30个完美数仅得前4个后报错,求算法问题排查
你的递归完美数算法的问题分析
咱们来一步步拆解你代码里的问题,为啥只能算出前4个完美数就报错:
1. 递归导致的栈溢出(StackOverflowError)
这是最直接的触发报错的原因。你的代码用递归方式不断调用getPerfectNumbers,每一次调用都会在JVM的栈内存中创建一个新的栈帧。而完美数的增长速度快得惊人:
- 第1个:6
- 第2个:28
- 第3个:496
- 第4个:8128
- 第5个:33550336
要找到第5个完美数,你需要从8129开始一直循环到33550336,每一次循环都会触发一次递归调用——这意味着栈里要堆积超过3000万个栈帧,远远超出了JVM默认的栈内存容量,直接就会抛出StackOverflowError。递归完全不适合这种需要大量线性迭代的场景,改用循环迭代才是正确的选择,循环不会占用栈内存,可以持续运行下去。
2. 因数计算的效率极低
你的代码计算一个数的真因数和时,循环从i=1跑到i<number,时间复杂度是O(n),对于大数来说这个过程慢到离谱。其实有很多优化空间:
- 只需要循环到
sqrt(number):因为因数是成对出现的(比如6的因数1和6、2和3),找到一个小于等于平方根的因数,就能直接得到对应的另一个因数,时间复杂度可以降到O(√n)。 - 利用偶完美数的数学性质:目前人类还没发现奇完美数,所有已知的完美数都是偶完美数,且符合欧几里得-欧拉定理——可以表示为
2^(p-1)*(2^p - 1),其中2^p - 1是梅森素数。直接通过寻找梅森素数来生成完美数,效率比逐个检查每个数高得多。
3. 递归逻辑的设计瑕疵
另外,Java是值传递,虽然你的代码里out++后传递给下一次递归的逻辑没问题,但这种递归设计本身就很别扭。递归适合处理可以拆分为独立子问题的场景,而找完美数是一个线性的遍历过程,用递归属于南辕北辙,完全发挥不出递归的优势,反而带来了栈溢出的风险。
修复思路示例
把递归改成循环,同时优化因数计算逻辑。比如下面的基础循环版本:
public class PerfectNumbers { public static void main(String[] args) { int count = 0; long number = 2; while (count < 30) { if (isPerfect(number)) { System.out.println("Perfect Number " + number); count++; } number++; } } static boolean isPerfect(long num) { if (num <= 1) return false; long sum = 1; // 1是所有大于1的数的真因数 for (long i = 2; i * i <= num; i++) { if (num % i == 0) { sum += i; if (i != num / i) { // 避免平方数重复累加 sum += num / i; } } } return sum == num; } }
当然,要找到第5个及之后的完美数,这个基础版本还是会很慢,如果要找更多完美数,建议直接基于梅森素数的检测逻辑来实现。
内容的提问来源于stack exchange,提问作者Andres Salazar Lopez
相关产品推荐
相关产品推荐

