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的int
或long
范围内,我们可以合理地假设n需要常数C步来达到三位数或更少的数字。这样,我们的HashSet最多包含C+243个结果,**导致空间复杂度为O(1)**。
虽然这种方法的空间复杂度为O(1),但它仍然需要空间来存储检测循环的结果。
现在,让我们努力改进isHappyNumber()
方法,无需额外空间。
5. 改进isHappyNumber()
方法
弗洛伊德的循环检测算法可以检测序列中的循环。例如,我们可以应用此算法来检查链表是否包含环。
思路是维护两个指针,slow和fast。slow每次更新一步,而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上找到。