“快乐数”这道题算是一道经典的入门题——题干简单易懂,但藏着一个容易被忽略的核心考点(无限循环的判断)。今天就结合你提供的两个核心实现函数,从题目理解、思路拆解到代码解析,一步步搞懂背后的逻辑,新手也能轻松吃透~
一、题目解读:什么是“快乐数”?
先明确题干核心定义,避免理解偏差,对应你要实现的算法需求:
对于一个正整数 n,“快乐数”的判断规则如下:
-
每一次将该数替换为它每个位置上数字的平方和(比如 n=19,第一次替换就是 1² + 9² = 1 + 81 = 82);
-
重复这个替换过程,只会出现两种唯一情况:
-
情况1:最终数字变为 1,此时 n 是快乐数,返回 true;
-
情况2:陷入无限循环(数字一直在某个范围内重复,永远变不到 1),此时 n 不是快乐数,返回 false。
核心难点:如何判断“无限循环”?这是这道题最关键的点——如果没有循环判断,代码会陷入死循环,永远无法返回结果。你提供的两个函数,正是围绕“循环判断”给出了两种不同解法。
二、解法一:集合判重法
1. 解题思路
核心逻辑:用“集合(Set)”作为容器,记录每次计算出的“数字平方和”。因为集合的元素具有唯一性,一旦某个数字再次出现,就说明进入了无限循环(重复出现的数字会导致后续平方和计算也重复,形成闭环),直接返回 false 即可。
具体解题步骤:
-
创建一个空集合(Set),用于存储每次计算后的数字,标记已出现的结果,避免重复;
-
开启循环:循环的终止条件有两个——要么 n 变成 1(是快乐数,终止循环),要么 n 已经在集合中(出现循环,终止循环);
-
循环体内操作:先将当前 n 加入集合(标记为已出现),再计算 n 每个位置数字的平方和,用计算结果更新 n;
-
循环终止后,判断 n 是否等于 1:等于则是快乐数,返回 true;否则不是,返回 false。
2. 完整代码解析
结合代码逐行拆解,搞懂每一步的作用,新手也能轻松理解:
// 解法一:集合判重法,判断n是否为快乐数
function isHappy_1(n: number): boolean {
// 1. 创建集合,用于存储已出现的数字,核心作用是判断是否出现循环
const set = new Set<number>();
// 2. 循环终止条件:n≠1(还没找到快乐数)且 n不在集合中(没出现循环)
while (n !== 1 && !set.has(n)) {
// 3. 将当前n加入集合,标记为已出现,避免后续重复判断
set.add(n);
// 4. 计算n每个位置数字的平方和,并用结果更新n
// 拆解:n转字符串→分割成单个字符→遍历求和(字符转数字后平方)
n = n.toString().split('').reduce((acc, cur) => acc + (+cur) ** 2, 0);
}
// 5. 循环终止后,判断n是否为1:是则返回true(快乐数),否则返回false(循环)
return n === 1;
};
3. 解法优势与注意点
-
优势:思路简单直观,代码易写易调试,不需要复杂的算法思维,适合新手入门;
-
注意点:集合会占用一定的内存空间,空间复杂度为 O(log n)(集合存储的数字个数不超过 log n 个,因为每次计算平方和都会减少数字位数);
-
关键细节:(+cur) 是将字符串类型的数字转为数字类型,避免直接用 cur ** 2(此时 cur 是字符串,会报错)。
三、解法二:快慢指针法
1. 解题思路
核心逻辑:无需额外容器(集合/数组),利用“快慢指针”判断是否存在循环,本质和“判断链表是否有环”的思路一致。
结合快乐数的特性:要么最终变到 1(无环,是快乐数),要么无限循环(有环,不是快乐数)。我们把每次计算的“平方和”看作链表的一个节点,那么问题就转化为“判断这个链表是否有环”,具体指针操作如下:
-
慢指针(slow):每次计算 1 次平方和(一步一步走);
-
快指针(fast):每次计算 2 次平方和(两步两步走);
-
终止条件:用 do-while 循环(先执行一次计算,再判断条件),当快慢指针相遇时,循环终止;
-
最终判断:如果相遇时,指针指向的数字是 1,则是快乐数;否则是循环,不是快乐数。
核心优势:无需额外存储容器,空间复杂度优化到 O(1),体现算法思维,适合面试中对空间复杂度有要求的场景。
2. 完整代码解析
逐行拆解代码,重点理解快慢指针的移动逻辑和 do-while 循环的作用:
// 解法二:快慢指针法,空间优化版,判断n是否为快乐数
function isHappy_2(n: number): boolean {
// 1. 初始化快慢指针,都从n开始(起点一致)
let slow = n;
let fast = n;
// 2. do-while循环:先执行一次指针移动,再判断快慢指针是否相遇(避免初始时指针相等直接终止)
do {
// 3. 慢指针走1步:计算1次平方和,更新slow
slow = slow.toString().split('').reduce((acc, cur) => acc + (+cur) ** 2, 0);
// 4. 快指针走2步:计算2次平方和,更新fast(先算一次,再算一次)
fast = fast.toString().split('').reduce((acc, cur) => acc + (+cur) ** 2, 0);
fast = fast.toString().split('').reduce((acc, cur) => acc + (+cur) ** 2, 0);
} while (slow !== fast); // 循环终止条件:快慢指针相遇(有环)
// 5. 最终判断:相遇时指针指向1 → 快乐数(无环);否则不是(有环)
return slow === 1;
};
3. 解法优势与注意点
-
优势:空间复杂度为 O(1),不占用额外内存,是空间最优解法,面试中更易加分;
-
注意点:必须用 do-while 循环(而非 while 循环),因为初始时 slow 和 fast 都等于 n,若用 while 循环会直接终止,无法执行指针移动;
-
关键细节:快指针每次必须计算 2 次平方和(两步),否则无法判断环的存在——快慢指针的“速度差”是判断环的核心。
四、两种解法对比
两种解法各有优劣,根据使用场景选择即可,整理对比表方便快速理解:
| 解法 | 对应函数 | 时间复杂度 | 空间复杂度 | 核心优势 | 适用场景 |
|---|---|---|---|---|---|
| 集合判重法 | isHappy_1 | O(log n) | O(log n) | 思路简单,代码易写,新手易理解、易调试 | 日常刷题、新手练习、面试基础场景(不要求空间优化) |
| 快慢指针法 | isHappy_2 | O(log n) | O(1) | 空间最优,体现算法思维,性能更优 | 面试进阶场景、对空间复杂度有要求的项目开发 |
补充说明:两种解法的时间复杂度一致,都是 O(log n)——因为无论快慢指针还是集合,每次计算平方和都会让数字的位数减少(比如三位数变两位数、两位数变一位数),循环次数不会太多,效率相近。
五、测试案例+避坑指南
1. 测试案例(验证两个函数正确性)
- 案例1:n=19 → 快乐数(两个函数均返回 true)
拆解过程:19 → 1²+9²=82 → 8²+2²=68 → 6²+8²=100 → 1²+0²+0²=1(终止,返回 true);
- 案例2:n=2 → 非快乐数(两个函数均返回 false)
拆解过程:2 → 4 → 16 → 37 → 58 → 89 → 145 → 42 → 20 → 4(重复出现,循环,返回 false)。
2. 避坑注意事项(新手必看)
-
边界条件:题干明确 n 是正整数,无需处理 n≤0 的情况,两个函数均符合这一要求;
-
类型转换:字符串转数字必须用 (+cur) 或 Number(cur),避免直接用 cur ** 2(字符串无法直接进行平方运算,会报错);
-
循环判断:isHappy_1 依赖集合判重,isHappy_2 依赖快慢指针相遇,两者缺一不可,否则会陷入死循环;
-
do-while 用法:isHappy_2 中不能替换为 while 循环,否则初始时 slow === fast,会直接返回结果,导致判断错误。
六、总结
LeetCode 202. 快乐数的核心考点只有一个——无限循环的判断,你提供的两个函数,正是围绕这个考点给出了两种经典解法:
-
isHappy_1(集合判重法):用集合记录已出现的数字,思路简单直观,适合新手入门,重点掌握“集合元素唯一”的特性在判重中的应用;
-
isHappy_2(快慢指针法):用快慢指针判断环的存在,无需额外内存,空间最优,重点掌握“指针速度差”和 do-while 循环的用法。
这道题整体难度不高,适合新手练习——既锻炼了字符串、数组的操作(平方和计算),也掌握了集合、快慢指针两种常用的“判重/判环”思路,为后续刷更难的算法题打下基础。