问题

编写一个算法来判断一个数 n 是不是快乐数

快乐数定义为:

  • 对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和
  • 重复这个过程直到这个数变为 1,也可能是无限循环但始终变不到 1
  • 如果可以变为1,那么这个数就是快乐数;如果 n 是快乐数就返回 true ;不是,则返回 false

示例 1:输入:19 输出:true

示例 2:输入:n = 2 输出:false

方法一:用哈希集合检测循环

从 116 开始,通过反复平方和计算下一个数字,最终得到 58,再继续计算之后,又回到 58。由于我们回到了一个已经计算过的数字,可以知道有一个循环,因此不可能达到 1。所以对于 116,函数应该返回 false
leetcode-202:快乐数 - 图1
因此在求解快乐数的过程中可能会存在以下三种情况:

  • 1
  • 循环
  • 值趋于无穷大

趋于无穷大的情况难以检测和处理。我们怎么知道它会继续变大,而不是最终得到 1 呢?我们可以仔细想一想,每一位数的最大数字的下一位数是多少。

Digits Largest Next
1 9 81
2 99 162
3 999 243
4 9999 324
5 99999 405
6 999999 486

对于 3 位数的数字,它不可能大于 243。这意味着它要么被困在 243 以下的循环内,要么跌到 1;4 位或 4 位以上的数字在每一步都会丢失一位,直到降到 3 位为止。所以,最坏的情况下,算法可能会在 243 以下的所有数字上循环,然后回到它已经到过的一个循环或者回到 1。但它不会无限期地进行下去,所以排除第三种选择

算法分为两部分,我们需要设计和编写代码

  • 给一个数字 n,它的下一个数字是什么
    • 我们按照题目的要求做数位分离,求平方和
  • 按照一系列的数字来判断我们是否进入了一个循环
    • 可以使用哈希集合完成。每次生成链中的下一个数字时,我们都会检查它是否已经在哈希集合中
      • 如果它不在哈希集合中,我们应该添加它
      • 如果它在哈希集合中,这意味着我们处于一个循环中,因此应该返回 false

我们使用哈希集合而不是向量、列表或数组的原因是因为我们反复检查其中是否存在某数字
检查数字是否在哈希集合中需要leetcode-202:快乐数 - 图2的时间,而对于其他数据结构,则需要leetcode-202:快乐数 - 图3的时间。选择正确的数据结构是解决这些问题的关键部分

  1. package leetcode;
  2. import java.util.HashSet;
  3. import java.util.Set;
  4. public class leetcode_202 {
  5. private int getNext(int n){
  6. //公共成员用public来修饰,可以被所有类访问
  7. //私有成员用private来修饰,它只允许在自己类的内部被访问
  8. int totalSum = 0;
  9. while(n > 0){
  10. int d = n % 10;
  11. n = n / 10;
  12. totalSum += d * d;
  13. }
  14. return totalSum;
  15. }
  16. public boolean isHappy(int n){
  17. Set<Integer> seen = new HashSet<>();
  18. while (n != 1 && !seen.contains(n)) {
  19. //.contains 用于判断list集合是否包含某个元素
  20. seen.add(n);
  21. n = getNext(n);
  22. }
  23. return n == 1;
  24. }
  25. }
  • 时间复杂度:leetcode-202:快乐数 - 图4
  • 空间复杂度:leetcode-202:快乐数 - 图5

方法二:快慢指针

通过反复调用 getNext(n) 得到的链是一个隐式的链表。隐式意味着我们没有实际的链表节点和指针,但数据仍然形成链表结构。起始数字是链表的头 “节点”,链中的所有其他数字都是节点。next 指针通过调用 getNext(n) 函数获得
意识到我们实际有个链表,那么这个问题就可以转换为检测一个链表是否有环。因此我们在这里可以使用弗洛伊德循环查找算法
我们不是只跟踪链表中的一个值,而是跟踪两个值,即快慢指针。在算法的每一步中,慢指针在链表中前进 1 个节点,快指针前进 2 个节点(对getNext(n)函数的嵌套调用)

  • 如果 n 是一个快乐数,即没有循环,那么快跑者最终会比慢跑者先到达数字 1
  • 如果 n 不是一个快乐的数字,那么最终快跑者和慢跑者将在同一个数字上相遇

    1. class Solution {
    2. public int getNext(int n) {
    3. int totalSum = 0;
    4. while (n > 0) {
    5. int d = n % 10;
    6. n = n / 10;
    7. totalSum += d * d;
    8. }
    9. return totalSum;
    10. }
    11. public boolean isHappy(int n) {
    12. int slowRunner = n;
    13. int fastRunner = getNext(n);
    14. while (fastRunner != 1 && slowRunner != fastRunner) {
    15. slowRunner = getNext(slowRunner);
    16. fastRunner = getNext(getNext(fastRunner));
    17. }
    18. return fastRunner == 1;
    19. }
    20. }
  • 时间复杂度:leetcode-202:快乐数 - 图6

  • 空间复杂度:leetcode-202:快乐数 - 图7