LeetCode 202. 快乐数:题解+思路拆解

0 阅读8分钟

“快乐数”这道题算是一道经典的入门题——题干简单易懂,但藏着一个容易被忽略的核心考点(无限循环的判断)。今天就结合你提供的两个核心实现函数,从题目理解、思路拆解到代码解析,一步步搞懂背后的逻辑,新手也能轻松吃透~

一、题目解读:什么是“快乐数”?

先明确题干核心定义,避免理解偏差,对应你要实现的算法需求:

对于一个正整数 n,“快乐数”的判断规则如下:

  1. 每一次将该数替换为它每个位置上数字的平方和(比如 n=19,第一次替换就是 1² + 9² = 1 + 81 = 82);

  2. 重复这个替换过程,只会出现两种唯一情况:

  3. 情况1:最终数字变为 1,此时 n 是快乐数,返回 true;

  4. 情况2:陷入无限循环(数字一直在某个范围内重复,永远变不到 1),此时 n 不是快乐数,返回 false。

核心难点:如何判断“无限循环”?这是这道题最关键的点——如果没有循环判断,代码会陷入死循环,永远无法返回结果。你提供的两个函数,正是围绕“循环判断”给出了两种不同解法。

二、解法一:集合判重法

1. 解题思路

核心逻辑:用“集合(Set)”作为容器,记录每次计算出的“数字平方和”。因为集合的元素具有唯一性,一旦某个数字再次出现,就说明进入了无限循环(重复出现的数字会导致后续平方和计算也重复,形成闭环),直接返回 false 即可。

具体解题步骤:

  1. 创建一个空集合(Set),用于存储每次计算后的数字,标记已出现的结果,避免重复;

  2. 开启循环:循环的终止条件有两个——要么 n 变成 1(是快乐数,终止循环),要么 n 已经在集合中(出现循环,终止循环);

  3. 循环体内操作:先将当前 n 加入集合(标记为已出现),再计算 n 每个位置数字的平方和,用计算结果更新 n;

  4. 循环终止后,判断 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_1O(log n)O(log n)思路简单,代码易写,新手易理解、易调试日常刷题、新手练习、面试基础场景(不要求空间优化)
快慢指针法isHappy_2O(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. 快乐数的核心考点只有一个——无限循环的判断,你提供的两个函数,正是围绕这个考点给出了两种经典解法:

  1. isHappy_1(集合判重法):用集合记录已出现的数字,思路简单直观,适合新手入门,重点掌握“集合元素唯一”的特性在判重中的应用;

  2. isHappy_2(快慢指针法):用快慢指针判断环的存在,无需额外内存,空间最优,重点掌握“指针速度差”和 do-while 循环的用法。

这道题整体难度不高,适合新手练习——既锻炼了字符串、数组的操作(平方和计算),也掌握了集合、快慢指针两种常用的“判重/判环”思路,为后续刷更难的算法题打下基础。