力扣解题-202. 快乐数
编写一个算法来判断一个数 n 是不是快乐数。
「快乐数」 定义为: 对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。 然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。 如果这个过程 结果为 1,那么这个数就是快乐数。 如果 n 是 快乐数 就返回 true ;不是,则返回 false 。
示例 1:
输入:n = 19
输出:true
解释:
1² + 9² = 82
8² + 2² = 68
6² + 8² = 100
1² + 0² + 0² = 1
示例 2:
输入:n = 2
输出:false
提示:
1 <= n <= 2³¹ - 1
Related Topics
哈希表、数学、双指针
第一次解答(哈希集合检测循环)
解题思路
核心方法:哈希集合记录已出现数字 + 逐位计算平方和,通过HashSet存储过程中出现过的数字,若数字重复出现则说明进入循环(非快乐数),逻辑直观但字符串操作导致性能低效。
核心逻辑拆解
判断快乐数的核心是“是否进入循环且无法到1”:
- 边界处理:若n=1,直接返回true(1是快乐数);
- 循环检测:创建HashSet记录已出现的数字,避免无限循环;
- 平方和计算:
- 将数字转为字符串,遍历每个字符(数字位);
- 转为整数后计算平方和,更新n为该平方和;
- 终止条件:
- 若n=1,返回true;
- 若n已在Set中(循环),返回false;
- 否则将n加入Set,继续循环。
性能损耗分析
- 时间复杂度:O(logn)(每次计算平方和的数字位数为log₁₀n,循环次数取决于是否进入循环),但字符串操作增加常数因子;
- 空间复杂度:O(logn)(Set存储的数字数量等于循环次数);
- 核心损耗点:
- 数字转字符串(
n+""):生成新字符串,且涉及类型转换开销; - 字符转整数(
Integer.parseInt(String.valueOf(...))):多层方法调用,效率远低于数学运算; - HashSet的哈希计算、自动装箱(int→Integer):增加额外开销;
- 数字转字符串(
- 性能表现:耗时3ms仅击败5.24%用户,内存42.8MB击败5.06%用户,正是字符串和HashSet的双重损耗导致。
public boolean isHappy(int n) {
if(n==1){
return true;
}
Set<Integer> set = new HashSet<>();
while (n!=1){
if(set.contains(n)){
return false;
}
set.add(n);
String number=n+"";
if(number.length()==1){
n=n*n;
}else {
int sum=0;
for(int i=0;i<number.length();i++){
int a=Integer.parseInt(String.valueOf(number.charAt(i)));
sum=sum+a*a;
}
n=sum;
}
}
return true;
}
第二次解答(快慢指针法/弗洛伊德循环检测)
解题思路
核心方法:快慢指针法(无额外空间检测循环),用慢指针每次计算一次平方和、快指针每次计算两次平方和,若存在循环则快慢指针必会相遇,无需HashSet存储,时间复杂度O(logn)、空间复杂度O(1),是本题的最优解。
核心原理铺垫
快乐数的计算过程只有两种结果:
- 最终得到1(快乐数);
- 进入无限循环(非快乐数)。
快慢指针法利用“循环检测”的经典思路:
- 慢指针(乌龟):每次走一步(计算一次平方和);
- 快指针(兔子):每次走两步(计算两次平方和);
- 若存在循环,快指针最终会追上慢指针(两者相等);若快指针先到1,则是快乐数。
核心逻辑拆解
- 初始化指针:慢指针
slow初始为n,快指针fast初始为getNext(n)(先走一步); - 循环检测:
- 若快指针到1,返回true;
- 若快慢指针相遇(循环),返回false;
- 否则慢指针走一步,快指针走两步;
- 辅助函数
getNext:用数学运算(取模+除法)计算平方和,避免字符串操作,效率极高。
具体步骤(以n=19为例)
- slow=19,fast=getNext(19)=82;
- 第一次循环:slow=getNext(19)=82,fast=getNext(getNext(82))=getNext(68)=100;
- 第二次循环:slow=getNext(82)=68,fast=getNext(getNext(100))=getNext(1)=1;
- fast=1,循环终止,返回true。
性能优势
- 时间复杂度:O(logn)(与HashSet法相同,但无额外常数因子),耗时0ms击败100%用户;
- 空间复杂度:O(1)(仅使用几个变量,无集合/数组开销),内存41.5MB击败99.24%用户;
- 核心优化点:
- 数学运算替代字符串操作:
n%10取最后一位,n/10去掉最后一位,无类型转换开销; - 无额外空间:无需HashSet存储,彻底消除哈希计算和装箱开销;
- 提前终止:快指针先到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;
}
示例解答
解题思路
解法1:优化版哈希集合法(性能提升)
核心方法:HashSet + 数学运算计算平方和,保留HashSet的循环检测逻辑,但将字符串操作替换为数学运算,大幅提升性能,适合理解循环检测的基础思路。
代码实现
public boolean isHappy(int n) {
Set<Integer> seen = new HashSet<>();
while (n != 1 && !seen.contains(n)) {
seen.add(n);
n = getNext(n); // 复用快慢指针法的getNext函数
}
return n == 1;
}
private int getNext(int n) {
int sum = 0;
while (n > 0) {
int digit = n % 10;
sum += digit * digit;
n /= 10;
}
return sum;
}
性能说明
- 时间复杂度:O(logn),数学运算替代字符串操作后,耗时可降至1ms左右;
- 空间复杂度:O(logn)(仍需Set存储);
- 优势:逻辑比快慢指针更直观,新手易理解,性能远优于原HashSet法。
解法2:数学规律法(极致优化)
核心方法:利用快乐数的数学规律,非快乐数的循环必然包含4(已被数学证明),因此只需检测过程中是否出现4,出现则返回false,逻辑更简洁。
代码实现
public boolean isHappy(int n) {
while (n != 1 && n != 4) {
n = getNext(n);
}
return n == 1;
}
private int getNext(int n) {
int sum = 0;
while (n > 0) {
int digit = n % 10;
sum += digit * digit;
n /= 10;
}
return sum;
}
核心原理
数学证明:所有非快乐数的计算过程最终都会进入包含4的循环(如2→4→16→37→58→89→145→42→20→4),因此只需检测是否出现4即可终止循环。
性能优势
- 时间复杂度:O(logn),循环次数更少(最快到1或4);
- 空间复杂度:O(1),无任何额外存储;
- 适用场景:追求极致简洁的代码,且无需理解快慢指针的循环检测逻辑。
总结
- 基础HashSet法(第一次解答):逻辑直观但字符串操作导致性能差,仅适合理解核心思路;
- 快慢指针法(第二次解答):最优解,O(logn)时间+O(1)空间,无额外存储且运算高效,工程首选;
- 优化版HashSet法:平衡可读性和性能,新手易理解且性能大幅提升;
- 数学规律法:极致简洁,利用数学结论减少循环次数,适合面试快速编写;
- 关键技巧:
- 数字位运算优先用数学方法(取模+除法),避免字符串转换;
- 循环检测问题优先考虑快慢指针法,可消除HashSet的空间开销;
- 利用题目隐含的数学规律(如非快乐数必到4),可进一步简化逻辑。