1. 概述

编程中我们经常处理数学问题。判断一个数是否是快乐数是一项有趣的任务。

在这篇教程中,我们将理解什么是快乐数,并探讨如何编写一个Java程序来检查给定的数是否为快乐数。

2. 理解快乐数

一个快乐数通过反复替换其各位数字的平方和达到1。 相反,非快乐(悲伤)数会陷入无限循环,永远不会达到1。

通常,几个例子可以帮助我们快速理解快乐数的定义:

Given number: 19

 19 -> 1^2 + 9^2 = 82
 82 -> 8^2 + 2^2 = 68
 68 -> 6^2 + 8^2 = 100
100 -> 1^2 + 0^2 + 0^2 = 1
  1

如上例所示,对于输入19,最终我们达到了1。因此,19是一个快乐数。同样,其他快乐数的例子包括7、10、13、23等。

然而,如果输入是15,我们将永远无法达到1:

Given number: 15                  
                                  
       15 -> 1^2 + 5^2 = 26       
       26 -> 2^2 + 6^2 = 40       
       40 -> 4^2 + 0^2 = 16       
+--->  16 -> 1^2 + 6^2 = 37       
|      37 -> 3^2 + 7^2 = 58       
|      58 -> 5^2 + 8^2 = 89       
|      89 -> 8^2 + 9^2 = 145      
|     145 -> 1^2 + 4^2 + 5^2 = 42 
|      42 -> 4^2 + 2^2 = 20       
|      20 -> 2^2 + 0^2 = 4        
|       4 -> 4^2 = 16             
+------16                         
                                  

我们可以看到,这个过程在16和4之间无限循环,永远不会到达1。所以,15是一个悲伤的数。

遵循这个规则,我们可以找到更多悲伤的数,如4、6、11、20等。

3. 实现检查方法

现在我们了解了快乐数的定义,让我们实现Java方法来检查给定的数是否为快乐数。

如果一个数形成序列的每个和的平方结果中存在循环,那么它就是悲伤的。换句话说,对于一个数,如果一步的计算结果已经在序列中存在,那么它就是一个悲伤的数

我们可以利用Java中的HashSet数据结构来实现这个逻辑。这允许我们存储每一步的结果,并高效地检查新计算的结果是否已存在于集合中:

class HappyNumberDecider {
    public static boolean isHappyNumber(int n) {
        Set<Integer> checkedNumbers = new HashSet<>();
        while (true) {
            n = sumDigitsSquare(n);
            if (n == 1) {
                return true;
            }
            if (checkedNumbers.contains(n)) {
                return false;
            }
            checkedNumbers.add(n);
        }
    }

    // ...
}

如上代码所示,sumDigitsSquare()方法执行实际的计算并返回结果:

private static int sumDigitsSquare(int n) {
    int squareSum = 0;
    while (n != 0) {
        squareSum += (n % 10) * (n % 10);
        n /= 10;
    }
    return squareSum;
}

接下来,让我们创建一个测试来验证我们的isHappyNumber()方法对不同输入报告预期结果:

assertTrue(HappyNumberDecider.isHappyNumber(7));
assertTrue(HappyNumberDecider.isHappyNumber(10));
assertTrue(HappyNumberDecider.isHappyNumber(13));
assertTrue(HappyNumberDecider.isHappyNumber(19));
assertTrue(HappyNumberDecider.isHappyNumber(23));
 
assertFalse(HappyNumberDecider.isHappyNumber(4));
assertFalse(HappyNumberDecider.isHappyNumber(6));
assertFalse(HappyNumberDecider.isHappyNumber(11));
assertFalse(HappyNumberDecider.isHappyNumber(15));
assertFalse(HappyNumberDecider.isHappyNumber(20));

4. 性能分析

首先,让我们分析解决方案的时间复杂度。

假设我们有一个有k位数的数n。所以我们需要k次迭代来计算数字之和的平方。进一步地,因为k = log n(以10为底的对数),所以计算第一次结果的时间复杂度为O(log n)。我们称其为result1由于最大的数字是9,所以result1的最大值是*9^2 * log n

如果result1不等于1,我们必须重复调用sumDigitsSquare()。然后,到目前为止的时间复杂度是*O(log n) + O(log (9^2 * log n)) = O(log n) + O(log 81 + log log n)*。**去掉常数部分后,我们得到O(log n) + O(log log n)**。

因此,我们的总时间复杂度变为O(log n) + O(log log n) + O(log log log n) + O(log log log log n) + …直到结果达到1或我们检测到循环。**由于这个表达式中log n是主导部分,所以解决方案的时间复杂度为O(log n)`。

在进行空间复杂性分析之前,让我们列出最大位数为n的数以及对应的result1

k     Largest n        result1
1     9                81
2     99               162
3     999              243
4     9999             324
5     99999            405
6     999999           486
7     9999999          567
8     99999999         648
9     999999999        729
10    9999999999       810
11    99999999999      891
12    999999999999     972
13    9999999999999    1053
       ...
1m    9..a million..9  81000000

如我们所见,对于超过三位数的数n反复应用sumDigitsSquare()操作可以在几步内迅速将n减少到三位数以下

例如,当k=1m时,n远大于Java的Long.MAX_VALUE。只需两步就能达到三位数以下的数:9..(1m)..9 -> 81000000 (9^2 * 1m = 81000000) -> 65 (8^2 + 1^2 = 65)

因此,在Java的intlong范围内,我们可以合理地假设n需要常数C步来达到三位数或更少的数字。这样,我们的HashSet最多包含C+243个结果,**导致空间复杂度为O(1)**。

虽然这种方法的空间复杂度为O(1),但它仍然需要空间来存储检测循环的结果。

现在,让我们努力改进isHappyNumber()方法,无需额外空间。

5. 改进isHappyNumber()方法

弗洛伊德的循环检测算法可以检测序列中的循环。例如,我们可以应用此算法检查链表是否包含环

思路是维护两个指针,slowfastslow每次更新一步,而fast每两步替换一次。如果它们在1处相遇,给定的数是快乐的。否则,如果它们相遇但值不为1,就检测到了循环。因此,给定的数是悲伤的。

接下来,让我们在Java中实现慢快指针逻辑:

public static boolean isHappyNumberFloyd(int n) {
    int slow = n;
    int fast = n;
    do {
        slow = sumDigitsSquare(slow);
        fast = sumDigitsSquare(sumDigitsSquare(fast));
    } while (slow != fast);
 
    return slow == 1;
}

让我们使用相同的输入测试isHappyNumberFloyd()方法:

assertTrue(HappyNumberDecider.isHappyNumberFloyd(7));
assertTrue(HappyNumberDecider.isHappyNumberFloyd(10));
assertTrue(HappyNumberDecider.isHappyNumberFloyd(13));
assertTrue(HappyNumberDecider.isHappyNumberFloyd(19));
assertTrue(HappyNumberDecider.isHappyNumberFloyd(23));
                                                   
assertFalse(HappyNumberDecider.isHappyNumberFloyd(4));
assertFalse(HappyNumberDecider.isHappyNumberFloyd(6));
assertFalse(HappyNumberDecider.isHappyNumberFloyd(11));
assertFalse(HappyNumberDecider.isHappyNumberFloyd(15));
assertFalse(HappyNumberDecider.isHappyNumberFloyd(20));

最后,这个解决方案的时间复杂性仍为O(log n)。由于它不需要额外空间,其空间复杂度为O(1)。

6. 总结

在这篇文章中,我们学习了快乐数的概念,并实现了Java方法来判断给定的数是否为快乐数。

如往常一样,示例的完整源代码可以在GitHub上找到。