LeetCode快乐数题解:双指针快慢指针与Floyd判圈算法实战
2026/9/18 20:55:55 网站建设 项目流程

刷LeetCode热题100的时候,我卡在这道“快乐数”上纠结了很久。倒不是题目本身多难,而是它放在双指针专题里,第一眼看上去完全不像能用双指针解决的问题——既没有数组,也没有链表,就是一个数字在那里反复求平方和。直到我把这个过程在纸上画出来,才反应过来这玩意本质上和链表判圈是同一个问题,快慢指针照样能玩得转。这篇就把我的完整思路、代码实现和调试过程中踩过的坑都记录下来,希望能帮到正在刷这道题的朋友。

1. 题目理解与思路拆解

1.1 快乐数的定义与题目本质

先看题目本身:对于一个正整数,每一次把它替换为每个位置数字的平方和,然后重复这个过程,如果最终能变成1,那这个数就是快乐数;如果陷入无限循环始终变不到1,就不是快乐数。比如19,1²+9²=82,8²+2²=68,6²+8²=100,1²+0²+0²=1,所以19是快乐数。再比如2,2²=4,4²=16,1²+6²=37,3²+7²=58,5²+8²=89,8²+9²=145,1²+4²+5²=42,4²+2²=20,2²+0²=4,你会发现又绕回了4,形成一个循环,所以2不是快乐数。

这里要抓住一个关键点:除了最终得到1的情况,其他所有可能都不是无限增大,而是必然落入一个循环。题目描述中说得很含蓄,只说了“也可能是无限循环但始终变不到1”,但很多初学者没深想,会觉得万一这个数越变越大怎么办?其实不会。我们可以简单论证一下:对于任意一个不超过10位数的正整数,每一位最大是9,平方和最大也就是81×10=810,所以计算一次平方和后,结果无论如何都不会超过810(实际三位数以上的数会迅速掉到三位数以内)。也就是说,这个变化过程一定在有限范围内打转,要么碰到1,要么重复出现某个数,而一旦重复,就说明进入了循环。

1.2 暴力思路为什么不行

最直觉的解法当然是模拟,用一个集合(哈希表)把所有出现过的数记下来,每算出一个新的数,就先看看集合里有没有。如果有,说明循环了,返回false;如果算出来是1,返回true。这个解法没有任何问题,时间复杂度也不差,LeetCode官方题解里也给出了这个方案。

但既然这题被归到双指针专题,就需要考虑能不能不借助额外的存储空间。哈希法的空间复杂度是O(log n)级别——因为数字变化范围有限,实际上是一个常数级别的上限,但理论分析上我们按位数来算。如果能把空间复杂度压到O(1),也就是只使用若干个变量而不使用集合,这题就多了一个值得学习的维度。快慢指针恰好能做到这一点。

2. 双指针解法的核心原理

2.1 把数字变化过程看成链表

我第一次看题解里说“把每个数看成一个节点,平方和的结果就是它的next指针”时,有种恍然大悟的感觉。比如19这个数,可以把19看成链表头节点,82是它的next,68是82的next,100是68的next,1是100的next,而1的next还是1。对于一个会循环的数比如2,它的轨迹上会出现一个环:4 → 16 → 37 → 58 → 89 → 145 → 42 → 20 → 4,最后一个4又指回之前的4,这就是一个典型的环形链表。

到这里,问题就完全转换成了“判断链表是否有环,并且找到环的入口是否等于1”。具体到快乐数,其实只需要判断是否有环即可——因为如果有环,环里既可能有1也可能没有,但需要特别注意:如果某个数在计算过程中出现了1,那1的下一个结果还是1,相当于在1这个节点上自环。所以更准确的判断是:快慢指针相遇时,如果相遇点是1,说明它被“困”在1这个自环里了,是快乐数;如果相遇点不是1,说明掉进了别的死循环,不是快乐数。

2.2 快慢指针为什么能检测循环

快慢指针检测循环的原理,和跑步套圈是一个道理。两个人在圆形跑道上跑,一个人速度快,一个人速度慢,只要跑道是环形的,速度快的人迟早会追上速度慢的人,两人相遇。但如果跑道是直线,速度快的人会先跑到终点,永远不会掉头回来追慢的人。

写代码时慢指针每次走一步,也就是计算一次平方和;快指针每次走两步,也就是连续计算两次平方和。如果存在循环,快指针最终会在环里追上慢指针;如果不存在循环(也就是最终停在1),快指针会先“到达”1,然后每次算出来的结果还是1,本质上也是在1这个节点上打转,所以最终两个指针都会停在1,也算是一种“相遇”。

关键在于:如果这是个快乐数,快指针会比慢指针更早进入1的循环里,然后在1的位置等慢指针,所以两者相遇时位置必然是1。如果这不是快乐数,两个指针都会掉进同一个非1的循环,最终在环里的某个点相遇。因此,循环结束的条件就是快指针追上慢指针,然后检查相遇点是否为1即可。

这里还有一个隐含的数学细节:不管是不是快乐数,整个过程最终都会进入循环,区别只在循环里有没有1。这是快慢指针能直接判断的根本前提。

3. 完整代码实现与逐步解析

3.1 Python实现(快慢指针法)

class Solution: def isHappy(self, n: int) -> bool: def get_next(num: int) -> int: total = 0 while num > 0: digit = num % 10 total += digit * digit num //= 10 return total slow = n fast = get_next(n) while slow != fast: slow = get_next(slow) fast = get_next(get_next(fast)) return slow == 1

这段代码短小精悍,但每一个细节都有讲究。

先看辅助函数get_next,它的作用是算出一个数的各位平方和。实现逻辑不复杂:循环取模得到最低位数字,累加它的平方,然后整除10去掉最低位,直到num变成0。这里要注意的是,先取模再整除,顺序不能乱——我在初学阶段经常写成先整除后取模,导致结果完全错误。

再看主流程。slow初始化为n本身,fast初始化为get_next(n),也就是让快指针先走一步。为什么要这样初始化?因为快慢指针一开始不能在同一个位置,否则while循环压根不会进入,直接返回slow == 1,对于快乐数来说这碰巧是对的(1的next还是1),但对于非快乐数比如2,slow=2,fast=4,此时slow != fast,循环正常进入,如果slow和fast都初始化为n=2,循环条件直接不成立,会返回2 == 1即False——碰巧也对了,但数字更大时结果可能就没这么幸运了。为了避免这个逻辑漏洞,标准做法是先走一步让两者错开。

进入while循环后,慢指针每次移动一步,快指针每次移动两步,直到两者相遇。注意fast = get_next(get_next(fast))这里有两层调用,代表连续算两次平方和,也就是快指针走两步。

循环结束后,判断相遇点的值是否等于1。如果等于1,说明快慢指针在1这个自环节点处相遇了,是快乐数;如果不等于1,说明掉进了非1的循环里,不是快乐数。这个判断准确无误吗?这里有个边界情况需确认:如果某个非快乐数,它的循环里恰好包含1(即某一步算出了1,但1的下一个结果还是1,所以一旦到1就会停住),1之后不会再跳出去,所以如果轨迹上碰到过1,最终数字就会变成1固定下来,那它就是快乐数。因此,有1出现的轨迹不可能是“非快乐数循环”,所以慢指针和快指针相遇于非1的节点,必然意味着这个数整个过程永远没到过1。

3.2 C++实现与哈希法的对比

用C++写一遍,加深理解:

class Solution { public: bool isHappy(int n) { auto getNext = [](int num) { int total = 0; while (num > 0) { int digit = num % 10; total += digit * digit; num /= 10; } return total; }; int slow = n; int fast = getNext(n); while (slow != fast) { slow = getNext(slow); fast = getNext(getNext(fast)); } return slow == 1; } };

C++的lambda表达式用在这里很合适,代码结构和Python版本一一对应。

再放一个哈希法的版本,两者对比看:

class Solution: def isHappy(self, n: int) -> bool: seen = set() while n != 1 and n not in seen: seen.add(n) n = sum(int(d) ** 2 for d in str(n)) return n == 1

两套解法都能通过,差异主要在空间复杂度上。哈希法需要一个集合记录出现过的所有数字,在实际运行中,这个集合的元素个数最多也就几百个(因为数字被限制在一个很小的范围内),但理论分析时会认为是O(log n)甚至更精确一点是O(位数)级别。快慢指针只用了两个变量,空间复杂度严格O(1),这是它最大的优势。

3.3 复杂度分析的完整推导

先分析时间复杂度。这里比较微妙,因为get_next函数每次要遍历数字的每一位,所以一次计算的复杂度是O(log n)(n代表当前数字,log n代表位数)。快慢指针需要走多少步?由于数字被限制在小范围内,实际上无论输入的n有多大,最终进入循环的步数都有一个很小的上界。网上有人实际测试过,对于int范围内的所有正整数,最多不超过几十步就能检测出结果。所以严格来说,时间复杂度和输入数字的位数有关,可以认为是O(log n)量级,但实际上因为循环长度极短,运行起来非常快。空间复杂度上,快慢指针法只用了常数个额外变量,O(1);哈希法最坏情况下要存储循环出现过的所有数字,理论上是O(log n)。

感性地解释一下:快慢指针法本质上是用“时间”换“空间”,多跑了几步计算,省掉了整个集合的存储。在很多实际场景下,如果内存敏感,这种空间O(1)的方案更有吸引力;如果是比赛环境追求代码简洁明快,哈希法也完全够用。

4. 常见问题与调试心得

4.1 快慢指针会不会错过循环入口

有朋友可能会问:快指针每次走两步,会不会刚好“跳过”慢指针,导致永远不相遇?事实上不会。在环里,快指针每走两步、慢指针走一步,相当于快指针相对慢指针每次靠近一步,因为两者的速度差是1步/轮,不存在“跳过去”的情况。这个结论和链表判圈那道题完全一致。就算快指针某时刻在慢指针前面一格,下一步快指针移动两格后会到慢指针后面一格,再下一步就会相遇。相对速度是1,不会出现跨越。

4.2 平方和计算的两个经典错误

第一个错误是get_next里对0的处理。有些同学会把while条件写成while num >= 1,或者漏掉num等于0的情况,导致循环提前退出。实际上while num > 0这个条件已经把所有正整数位都处理完了,不需要额外处理0。不过要注意,如果函数外部传入0,比如isHappy(0),那0不是正整数,题目没要求处理,可以忽略。

第二个错误是取余和整除的顺序。正确的操作顺序是:先digit = num % 10取到最低位,累加平方,再num //= 10去掉最低位。我见过有人写成先把num除以10再去模,那就会漏掉个位的数字。还有一种写法是直接用字符串转换:sum(int(c) ** 2 for c in str(n)),Python这么写很简洁,但在C++里用字符串处理反而更麻烦,而且性能不如直接取模。

4.3 测试用例与边界情况

刷题时养成一个好习惯:提交前先在本地跑几个典型用例。这道题我建议至少测这几组:

  • isHappy(1) = True,循环直接不进入,slow和fast初始分别是1和1,fast = get_next(1) = 1,所以slow == fast成立,跳出循环后判断slow == 1返回True。
  • isHappy(19) = True,这是题目给的示例,可以手动推演一下,确认快慢指针都能在1处相遇。
  • isHappy(2) = False,前面推演过会陷入4的循环,返回False。
  • isHappy(7) = True,7²=49,4²+9²=97,9²+7²=130,1²+3²+0²=10,1²+0²=1,最终确实能到1,而且步数比19还多,容易误判。
  • isHappy(1111111) 这种大数,用来验证get_next对大数的处理是否正确。这种测试不是为了逻辑正确性(逻辑一样),主要是确认不会出现整数溢出——Python和C++的int都能轻松装下中间结果,因为平方和不会超过几百,所以没有溢出风险。

4.4 关于刷题顺序和专题训练的一点体会

我是在刷LeetCode热题100的时候遇到这道双指针题的。刚开始不理解为什么把它归为双指针,后来把链表判圈和这题放在一起对比,才真正理解了“状态机+循环检测”这一大类问题的通用解法模板。如果你也在按专题刷题,建议把这几题放一起做:环形链表、环形链表II、寻找重复数、快乐数。它们的内核都是Floyd判圈算法,只是表现形式完全不同——有链表、有数组、有纯数字。把这四题对照着看,你会形成一种条件反射:看到“重复出现”“循环”“陷入死循环”等字眼,第一反应就是能不能用快慢指针检测。

另外一个心得是:哈希法并不是“低级解法”。在面试中,如果时间紧张,先给出哈希法的版本并分析它的复杂度,然后补充说“如果面试官要求优化空间,可以用快慢指针把空间压到O(1)”,这反而展示了你的知识深度。在实际刷题过程中,我不建议一开始就追求最优解,而是先保证能AC,再思考能不能优化。很多题目的最优解都是建立在朴素解法的理解之上的,跳过朴素解直接看最优解,反而容易消化不良。

最后再分享一个我踩过的坑:有一版代码我把fast初始值写成了n而不是get_next(n),导致while循环的条件一开始就不成立,直接返回slow == 1,而这个判断在n=2时恰好返回False,在n=19时返回False——因为19 != 1,但19明明是快乐数。这种“碰巧能过一部分用例但逻辑不全对”的写法最危险,因为LeetCode上有些测试用例恰好能过,提交后会在某组数据上翻车。所以无论代码多简单,一定要在纸上推演一遍完整流程,确认所有分支都符合预期。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询