202. 快乐数(Happy Number)
频次 ★★★ · 难度 🟢 · 高频:全厂
题目
编写一个算法判断一个数 n 是不是快乐数。
快乐数定义为:对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和,然后重复这个过程,直到这个数变为 1,也可能无限循环但始终变不到 1。如果可以变为 1,那么这个数就是快乐数。
示例:
输入: n = 19
输出: true
解释:
1² + 9² = 82
8² + 2² = 68
6² + 8² = 100
1² + 0² + 0² = 1
思路
核心问题是检测循环——如果计算过程中出现重复的数字,说明进入了循环,永远不可能到 1。
两种实现方式:
- HashSet 记录:用 HashSet 存储每次计算的结果,如果新结果已在 Set 中出现,说明进入循环,返回
false;如果结果为 1,返回true。空间 O(log n)。 - 快慢指针:将每次计算看作链表的”next”指针,用快慢指针检测环——如果快慢指针相遇且值不为 1,说明有环(不是快乐数);如果快指针到达 1,说明是快乐数。空间 O(1)。
推荐快慢指针法,空间更优,且体现与”环形链表”的解题思路迁移。
代码
// 快慢指针法
public boolean isHappy(int n) {
int slow = n;
int fast = getNext(n);
while (fast != 1 && slow != fast) {
slow = getNext(slow);
fast = getNext(getNext(fast));
}
return fast == 1;
}
private int getNext(int n) {
int sum = 0;
while (n > 0) {
int digit = n % 10;
sum += digit * digit;
n /= 10;
}
return sum;
}// HashSet 法(更直观,空间 O(log n))
public boolean isHappy(int n) {
Set<Integer> seen = new HashSet<>();
while (n != 1 && !seen.contains(n)) {
seen.add(n);
n = getNext(n);
}
return n == 1;
}复杂度
- 时间:O(log n) — 每次
getNext需要 O(log n) 时间(数字位数),循环次数有上限(对于 int 范围,1 的平方和或进入循环的步数是常数级) - 空间:快慢指针 O(1);HashSet 法 O(log n)
边界条件
n = 1:直接返回true,1² = 1,是快乐数- 非快乐数一定进入循环:已知非快乐数最终会进入
4 → 16 → 37 → 58 → 89 → 145 → 42 → 20 → 4的循环,不可能无限增长 - 大数:
getNext按位计算,不受 int 范围影响;最大三位数 999 的平方和为 243,所以计算过程中数字会快速缩小
变式
- 只用一个 HashSet 的早期退出:在循环中检查
n == 1或seen.contains(n),哪个先成立就对应哪种结果 - 数学法:已知唯一的循环是
4 → 16 → 37 → 58 → 89 → 145 → 42 → 20 → 4,可以硬编码这个循环,但面试中不推荐——面试官想看的是”检测循环”的通用思维
易错点
- 快慢指针法中,
fast初始值要调用一次getNext(n),不能和slow一样初始为n——否则while条件slow != fast一开始就不成立 getNext中注意取每一位数字:n % 10取最低位,n /= 10去掉最低位,循环直到n == 0- 不要忘记判断
fast == 1的情况——快指针可能提前到达 1
面试追问
- 为什么非快乐数一定会进入循环? 因为
getNext(n)的结果有上界——对于 k 位数,最大平方和为 81k(9²×k),而 k 位数的最小值是 10^(k-1)。当 k 足够大时,平方和一定小于原数(如 1000 的平方和最大为 81×4=324),所以序列必然被限制在有限范围内,根据鸽巢原理一定会重复。 - 快慢指针法和 HashSet 法怎么选? 快慢指针 O(1) 空间,适合面试展示”问题转化”能力(把数字变换看作链表 next);HashSet 法更直观,适合快速实现。如果面试官追问空间优化,再给快慢指针。
关联题
- 同套路:141. 环形链表 —— 快慢指针检测循环的经典模板
- 知识点:快慢指针(Floyd 判圈算法)的核心思想——如果有环,快指针一定会在环内追上慢指针