题目
标题和出处
标题:快乐数
出处:202. 快乐数
难度
2 级
题目描述
要求
编写一个算法来判断一个数 n \texttt{n} n 是不是快乐数。
「快乐数」定义为:
- 对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
- 然后重复这个过程直到这个数变为 1 \texttt{1} 1,也可能是无限循环但始终变不到 1 \texttt{1} 1。
- 如果可以变为 1 \texttt{1} 1,那么这个数就是快乐数。
如果 n \texttt{n} n 是快乐数就返回 true \texttt{true} true;不是,则返回 false \texttt{false} false。
示例
示例 1:
输入:
n
=
19
\texttt{n = 19}
n = 19
输出:
true
\texttt{true}
true
解释:
1
2
+
9
2
=
82
\texttt{1}^\texttt{2} + \texttt{9}^\texttt{2} = \texttt{82}
12+92=82
8
2
+
2
2
=
68
\texttt{8}^\texttt{2} + \texttt{2}^\texttt{2} = \texttt{68}
82+22=68
6
2
+
8
2
=
100
\texttt{6}^\texttt{2} + \texttt{8}^\texttt{2} = \texttt{100}
62+82=100
1
2
+
0
2
+
0
2
=
1
\texttt{1}^\texttt{2} + \texttt{0}^\texttt{2} + \texttt{0}^\texttt{2} = \texttt{1}
12+02+02=1
示例 2:
输入:
n
=
2
\texttt{n = 2}
n = 2
输出:
false
\texttt{false}
false
数据范围
- 1 ≤ n ≤ 2 31 − 1 \texttt{1} \le \texttt{n} \le \texttt{2}^\texttt{31} - \texttt{1} 1≤n≤231−1
解法一
思路和算法
将一个正整数替换为它每个位置上的数字的平方和,称为一次操作。
根据快乐数的定义,如果从正整数 n n n 开始,经过若干次操作(可以是零次)之后变为 1 1 1,则 n n n 是快乐数。
-
如果正整数 n n n 是快乐数,则当 n n n 变为 1 1 1 之后,每次操作将停留在 1 1 1。
-
如果正整数 n n n 不是快乐数,则每次操作得到的数形成无限循环,该无限循环不包含 1 1 1,一定存在某一次操作,该次操作之后变为的数是已经出现过的数。
是否可能每次操作得到的数趋向于无穷大?答案是否定的,理由如下。
-
由于正整数 n n n 的最大值是 2 31 − 1 2^{31} - 1 231−1, n n n 最多是 10 10 10 位数,因此对 n n n 进行一次操作之后得到的最大可能数是 9 2 × 10 = 810 9^2 \times 10 = 810 92×10=810(其实 810 810 810 也是不可能达到的,因为此时 n = 1 0 10 − 1 > 2 31 − 1 n = 10^{10} - 1 > 2^{31} - 1 n=1010−1>231−1,为了简化逻辑假设最大可能数是 810 810 810),最大可能数是三位数。
-
由于最大的三位数是 999 999 999(同上,其实 999 999 999 也是不可能达到的,为了简化逻辑故考虑最大的三位数),对任意三位数进行一次操作之后得到的最大可能数是 9 2 × 3 = 243 9^2 \times 3 = 243 92×3=243。
-
对任意不超过 243 243 243 的正整数进行一次操作之后得到的最大可能数是 163 163 163(当正整数是 199 199 199 时可得到 163 163 163), 163 < 243 163 < 243 163<243。
根据上述分析可知,对于任意不超过 2 31 − 1 2^{31} - 1 231−1 的正整数,经过两次操作之后得到的数一定不超过 243 243 243,之后经过任意次操作得到的数也一定不超过 243 243 243,因此每次操作得到的数不可能趋向于无穷大,而是一定会出现循环。
实现方面,需要考虑两个部分。
-
对于一个数进行一次操作,可以将数转换成字符串之后遍历字符串的每一位计算平方和,也可以直接对给定的数做数位分离计算每一位的平方和,这里使用的是数位分离的做法。
-
判断每次操作之后变为的数是否已经出现过,需要使用哈希集合存储每次操作之后的数。
由此可以得到判断正整数 n n n 是不是快乐数的方法。
-
从正整数 n n n 开始,每次将 n n n 加入哈希集合,然后对 n n n 进行一次操作并用操作后的数替换 n n n,直到 n n n 已经在哈希集合中,此时出现循环。
-
当出现循环时,如果 n = 1 n = 1 n=1 则返回 true \text{true} true,否则返回 false \text{false} false。
代码
class Solution {
public boolean isHappy(int n) {
Set<Integer> set = new HashSet<Integer>();
while (set.add(n)) {
n = getNextNumber(n);
}
return n == 1;
}
public int getNextNumber(int n) {
int sum = 0;
while (n != 0) {
int digit = n % 10;
n /= 10;
sum += digit * digit;
}
return sum;
}
}
复杂度分析
-
时间复杂度: O ( log n ) O(\log n) O(logn)。这道题的时间复杂度分析颇为困难,以下给出近似分析。
每次操作需要遍历数的每一位,时间复杂度是 O ( log n ) O(\log n) O(logn)。
由于对于任意 n n n 经过至多两次操作之后得到的数一定不超过 243 243 243,之后经过任意次操作得到的数也一定不超过 243 243 243,因此允许的操作次数是有限的。
由于允许的操作次数有限,因此时间复杂度是 O ( 243 log n ) = O ( log n ) O(243 \log n) = O(\log n) O(243logn)=O(logn)。 -
空间复杂度: O ( log n ) O(\log n) O(logn)。空间复杂度主要取决于哈希表,哈希表存储原始正整数 n n n 和每次操作变成的数,哈希表大小为经过的数的个数。
解法二
思路和算法
根据解法一可知,无论 n n n 是不是快乐数,对 n n n 进行足够多次数的操作之后都会出现循环。如果 n n n 是快乐数,则循环将出现在 1 1 1 的位置;如果 n n n 不是快乐数,则循环将不包含 1 1 1。
对于循环问题,一个常用的方法是快慢指针。快慢指针一般用于判断是否存在循环,由于这道题中一定存在循环,因此快慢指针用于判断出现循环的数,只需要判断出现循环的数是不是 1 1 1 即可。
从起点开始,每次将快指针移动两步,慢指针移动一步,在至少移动一次的情况下,当快慢指针相遇时停止,此时判断快慢指针所在的数字是不是 1 1 1,即可判断 n n n 是不是快乐数。
代码
class Solution {
public boolean isHappy(int n) {
int fast = n, slow = n;
do {
fast = getNextNumber(fast);
fast = getNextNumber(fast);
slow = getNextNumber(slow);
} while (fast != slow);
return fast == 1;
}
public int getNextNumber(int n) {
int sum = 0;
while (n != 0) {
int digit = n % 10;
n /= 10;
sum += digit * digit;
}
return sum;
}
}
复杂度分析
-
时间复杂度: O ( log n ) O(\log n) O(logn)。
-
空间复杂度: O ( 1 ) O(1) O(1)。只需要维护快慢指针。