先解释一个很多新手容易看走眼的地方。完数这个定义里的“因子”,指的是“除它本身以外的正因子”,也叫真因子。如果不注意这个限定,6的全部因子是1、2、3、6,加起来等于12,那所有数都不可能是完数了。题目里那句“例如6=1+2+3”,其实已经把规则写清楚了,但实际写代码时,不少人会不小心把自身加进去,导致判断结果永远是 false,这是第一个隐藏的坑。
这道题我见过太多次了。它经常出现在Java基础面试、大学编程作业、还有各种“经典算法题合集”里。它的价值不在于完数本身有多难,而是这一道题几乎把循环边界、取余判断、因子成对、代码性能这些基本功全考了一遍。我拿它面试过不少候选人,也帮人改过很多版本,先亮结论:1000以内只有三个完数,分别是6、28、496。但这篇文章不只是告诉你答案,我要把整道题从题意理解、暴力实现、数学优化、批量筛法,到常见调试错误全部拆开讲一遍,保证看完你能写出比普通教学示例好得多的代码。
阅读对象主要是三类人:正在学Java基础、想搞懂完数算法的新手;准备面试、怕被追问优化的求职者;以及想拿这道经典题练手、顺便学点底层思路的业余选手。下面我们一步一步来。
1. 先搞懂“完数”到底在考什么:题意与数学底子
1.1 完数的定义与容易踩的概念坑
完数的标准定义是:一个数恰好等于它的真因子之和,这个数就是完数。这里的“真因子”是解题的第一关键。我见过一些网上的解释,直接把因子之和写成“所有因子相加”,这个表述是有歧义的。严谨地说,应该是“除自身以外的所有正因子相加”。所以在判断n是否为完数时,求和范围天然就不应该包含n本身。
另一个容易混淆的概念是“因子”和“质因数”。因子是能整除n的正整数,比如12的因子有1、2、3、4、6、12;质因数是因子中那些本身就是质数的部分,比如2和3。完数题目里用的是因子,不是质因数。如果你按质因数去求和,那6会被判成1+2+3=6,好像也正确,但换到28,质因数只有2和7,加起来显然不对。这个细节在面试时偶尔会被追问,答不好就容易露怯。
我们再看定义里另一个隐含条件:1是不是完数?1的真因子是没有的(空集),真因子和可以认为是0,0不等于1,所以1不是完数。2到5这些数呢,真因子只有1,和是1,显然不等于自身。所以遍历时从1开始没问题,但代码里要单独注意n比较小的情况。
1.2 1000以内为什么只会出现这几类结果
如果不考虑数学结论,直接让程序遍历1到1000,理论上每个数都要判断一次。但完数本身是很少的,1000以内只有6、28、496三个。这背后有个经典数学结论:偶完数都满足 ( 2^{p-1} \times (2^p - 1) ) 的形式,其中 ( 2^p - 1 ) 必须是质数,也就是梅森素数。比如p=2时得到6,p=3时得到28,p=5时得到496,p=7时得到8128。
这个结论对解决这道编程题来说不是必须的,但理解它有几个好处。第一,你知道暴力遍历不会有太多输出,方便验证结果;第二,如果面试官问“1000以内完数有哪些”,你可以直接秒答6、28、496;第三,当题目范围扩大到10万甚至100万时,你可以利用这个数学性质快速生成候选数,而不是傻乎乎地对每个数做因子求和。
不过作为编程练习,我不建议一开始就用数学结论硬套。原因很简单:这道题考察的核心是“因子求和”的代码能力,不是“背诵梅森素数”。先用最朴素的思路把功能做出来,再一步一步优化,才是正确的学习节奏。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Java实现第一版:从最简单暴力的思路出发
2.1 暴力枚举:怎么写最容易理解
第一版代码的思路非常直白:对1到1000的每一个数n,都从1到n-1遍历一遍,把所有能整除n的数累加起来,最后比较累加和是否等于n。这种写法最大的优点是贴近定义,几乎不可能写错,适合作为第一步功能实现。
java复制public class PerfectNumberV1 {
public static void main(String[] args) {
int limit = 1000;
for (int n = 1; n <= limit; n++) {
if (isPerfect(n)) {
System.out.println(n);
}
}
}
// 暴力判断:遍历 1 到 n-1
public static boolean isPerfect(int n) {
int sum = 0;
for (int i = 1; i < n; i++) {
if (n % i == 0) {
