- 教程
- 文档
【免费下载链接】LogicStack-LeetCode
公众号「宫水三叶的刷题日记」刷穿 LeetCode 系列文章源码
本篇是「宫水三叶的刷题日记」刷穿 LeetCode 系列第 No.507 篇的深度解读。文章以 LeetCode/501-510/507. 完美数(简单).md 为核心骨架,完整继承原题解中的数学推导、成对因子枚举思路与 Java 参考代码,并结合本仓库的 Tag 归类体系,补充边界情形论证、溢出规避原理与数论背景,帮助你彻底掌握这一类「因数统计类」简单题的通用解法,读完后可直接在 LeetCode 上独立 AC 并迁移到其他因子枚举类题目。
题目描述与输入输出约定
对于一个正整数,如果它和除了它自身以外的所有正因子之和相等,我们称它为「完美数」(Perfect Number)。
给定一个整数n,如果是完美数返回true,否则返回false。
Tag:
模拟、数论、数学难度:简单
数据范围:
1 <= num <= 10^8
示例
示例 1:
输入:num = 28 输出:true 解释:28 = 1 + 2 + 4 + 7 + 14 1, 2, 4, 7, 和 14 是 28 的所有正因子。示例 2:
输入:num = 6 输出:true示例 3:
输入:num = 496 输出:true示例 4:
输入:num = 8128 输出:true示例 5:
输入:num = 2 输出:false从示例可以看到,判断的关键在于两点:一是正确枚举出除自身以外的全部正因子,二是对因子求和并与原数比较。前四个示例恰好就是最小的四个完美数 6、28、496、8128,而 2 的因子只有 1,1 ≠ 2,因此返回false。
核心思路:朴素枚举的缺陷与成对因子优化
拿到题目最容易想到的做法是从1到num - 1逐个判断能否整除并累加,但num上限是10^8,线性枚举在最坏情况下需要执行约10^8次取模运算,虽然勉强能过,但显然不是最优解。
原题解给出的关键洞察是:正因子总是成对出现的。例如28的因子对为(1, 28)、(2, 14)、(4, 7),每一对中较小的因子一定不超过sqrt(num)。因此我们只需要枚举每对正因子中的较小数,即从[1, sqrt(num)]范围内枚举即可(num > 1时成立),枚举到一个小因子i后,配对的大因子num / i可以直接算出并一并累加。
这样一来,枚举量从O(num)直接降到O(sqrt(num)),对10^8规模的数据只需约10^4次迭代。
数学解法:边界、溢出与平方根特判
原题解的参考代码如下(Java):
class Solution { public boolean checkPerfectNumber(int num) { if (num == 1) return false; int ans = 1; for (int i = 2; i <= num / i; i++) { if (num % i == 0) { ans += i; if (i * i != num) ans += num / i; } } return ans == num; } }这段代码非常精简,背后有三个值得细讲的工程细节:
1. 为什么num == 1直接返回false
1除了自身以外没有任何正因子,因子和(空和)为0,0 ≠ 1,因此直接返回false。若不特判,下面的循环i从2开始不会执行,ans初始化为1,ans == num会错误地判定1为完美数,所以这一行是必须的。
2. 用i <= num / i代替i <= sqrt(num)的动机
原题解明确说明:使用i <= num / i作为上界判断,目的是避免调用sqrt库函数以及整数溢出。
- 如果写
i <= Math.sqrt(num),需要引入浮点库函数,浮点开方在精度与性能上都劣于纯整数比较; - 如果写
i * i <= num,当num接近10^8时i最大约10^4,i * i不会溢出;但若题目数据范围进一步扩大,i * i在int下可能溢出为负数,从而破坏循环条件。
而i <= num / i全程使用整数除法,既无浮点误差,也不会溢出,是处理这类「以平方根为上界」问题的通用写法,在仓库其他数学类题解中同样被反复采用(参见 Index/数学.md 收录的 367. 有效的完全平方数、441. 排列硬币等题)。
3.i * i != num的平方根特判
当num是完全平方数时,其平方根i是唯一不与另一个不同因子配对的因子。例如num = 36,因子对为(1, 36)、(2, 18)、(3, 12)、(4, 9)、(6, 6),其中6只应计数一次。
因此当num % i == 0时,先累加i,再判断i * i != num:若i不是平方根,说明num / i是另一个不同的因子,一并累加;若是平方根,则num / i与i相等,跳过避免重复计数。这正是成对枚举中唯一的重复风险点,代码用一行判断干净地化解了它。
累加初值ans = 1的含义
由于循环从i = 2开始,因子1被跳过,因此将ans的初值直接设为1,等价于把1这一因子预先计入。这与题目「除了它自身以外的所有正因子之和」的定义严格对齐——1计入、num自身不计入(因为成对枚举时大因子num / i的最小取值就是i = 2时给出的num / 2,永远不会取到num本身,当num为素数时循环内一个因子都加不进去,ans恰好等于1)。
复杂度分析
- 时间复杂度:
O(sqrt(num))。循环上界为i <= num / i,迭代次数不超过sqrt(num)量级,对num = 10^8上限仅需约10^4次迭代,毫秒级完成。 - 空间复杂度:
O(1)。仅使用常数个整型变量,不依赖任何额外数据结构。
在本题数据范围下,该算法无论时间还是空间都是最优级别的表现;ans的累加值在10^8内也不会超出int表示范围。
边界用例验证与正确性论证
| 输入 | 因子(除自身外) | 因子和 | 结果 |
|---|---|---|---|
1 | 无 | 0 | false(代码特判) |
2 | 1 | 1 | false |
6 | 1, 2, 3 | 6 | true |
28 | 1, 2, 4, 7, 14 | 28 | true |
36 | 1, 2, 3, 4, 6, 9, 12, 18 | 55 | false(验证平方根不重复计数) |
496 | 1, 2, 4, 8, 16, 31, 62, 124, 248 | 496 | true |
8128 | 全部真因子 | 8128 | true |
其中36这个用例特别适合用来检验平方根特判:若把6重复累加两次,因子和会变成61 ≠ 36,但按正确逻辑得到55 ≠ 36,依然返回false,因此特判的正确性在非完美完全平方数上体现得最清楚。
数论背景延伸:完美数家族与欧几里得-欧拉定理
本题虽标记为「简单」,背后却连接着一个经典的数论话题。完美数的研究可追溯到古希腊,最早被确认的几个完美数正是题目示例中的6, 28, 496, 8128。数论中著名的欧几里得-欧拉定理给出结论:偶完美数与梅森素数一一对应——若2^p - 1是素数,则2^(p-1) * (2^p - 1)是偶完美数;反之每个偶完美数都具有该形式。据此可以验证,在本题1 <= num <= 10^8的范围内,恰好存在 5 个完美数:6, 28, 496, 8128, 33550336,这也是另一种理论上可行的「打表」思路(仓库中另有 Index/打表.md 专题可供参考)。
值得说明的是:是否存在奇完美数至今是数论中未解决的问题,目前已知的完美数均为偶数。这些背景能帮助你理解为什么题目给出的示例恰好是这些数字,但在本题的约束下,成对因子枚举的模拟解法才是通用、稳妥且可迁移的首选。
仓库归类与延伸学习
本题在原仓库中被同时归入「数学/数论」与「模拟」两个专题:
- Index/数学.md:收录 507. 完美数 的题解链接,与 367. 有效的完全平方数、441. 排列硬币、633. 平方数之和等因子与平方根类题目归为一类;
- Index/模拟.md:从「按规则逐项统计」的视角将本题与 166. 分数到小数、400. 第 N 位数字 等模拟类简单题并列。
如果你希望进一步巩固本解法中用到的技巧,可以按以下顺序延伸阅读仓库内相关题解:
- 367. 有效的完全平方数(数学/二分):同样围绕
sqrt与整数平方根判断,体会i <= num / i这类整数写法的通用性; - 441. 排列硬币(数学/二分):同样需要以平方根级别上界做枚举/二分的数学题;
- 633. 平方数之和(数学/双指针):继续练习因子与平方根视角的组合使用;
- 若想挑战同类但更复杂的「因子统计」问题,可阅读 Index/数论相关题解 中难度更高的题目。
小结
LeetCode 507「完美数」是一道典型的用数学性质优化模拟的入门题。核心要点可总结为三条:
- 正因子成对出现,只需枚举到
sqrt(num)即可覆盖所有因子,复杂度从O(num)降为O(sqrt(num)); - 用
i <= num / i作为循环上界,避开sqrt库函数与整数乘法溢出两种隐患; - 对完全平方数做
i * i != num特判,避免平方根因子被重复累加,并正确处理num = 1的边界。
掌握这套「成对因子枚举」的写法后,你不仅能在本题轻松 AC,还能将它直接迁移到其他因子统计、因子和计算的题目中。完整的原始题解与仓库全部系列文章,可在 LeetCode/501-510/507. 完美数(简单).md 及 README.md 中查看。
- 教程
- 文档
【免费下载链接】LogicStack-LeetCode
公众号「宫水三叶的刷题日记」刷穿 LeetCode 系列文章源码
相关推荐
LogicStack-LeetCode 题解精读:LeetCode 952 按公因数计算最大组件大小(枚举质因数 + 并查集)
LogicStack LeetCode 题解精读:LeetCode 952 按公因数计算最大组件大小(枚举质因数 + 并查集) 本文基于 LogicStack
教程文档LogicStack-LeetCode 题解精讲:LeetCode 1775 通过最少操作次数使数组的和相等(枚举 + 贪心 + 数学)
LogicStack LeetCode 题解精讲:LeetCode 1775 通过最少操作次数使数组的和相等(枚举 + 贪心 + 数学) 本篇技术指南围绕「宫水
教程文档LogicStack-LeetCode 题解精讲:整数转罗马数字的贪心模拟解法(LeetCode 12 中等)
LogicStack LeetCode 题解精讲:整数转罗马数字的贪心模拟解法(LeetCode 12 中等) 本文以「宫水三叶的刷题日记」刷穿 LeetCod
教程文档
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考