审题
题目会给我们一个int型的数,如果在经过有限次的迭代后结果为1,则说明该数是快乐数,如果一直迭代都变不成1,则说明不是快乐数,迭代的方式是计算自身各位上的平方的和。
思考方案
乍一看像是要解决著名的不可解问题----停机问题,不是已经证明为不可解的问题吗?难道是leetcode的出题人在整活???
非也非也!我们可以大胆地猜测在迭代的过程中会出现一定的规律,比如出现一个循环......
大胆猜测后就小心地试着来证明一下:
既然n是一个常数范围内的数,每次迭代的结果又与自身各位上的大小有关系,大小?要不试试求迭代结果的最大值和最小值看看,有范围的话可以试着暴力穷举一下;
既然是平方和,那一定是非负数,要使迭代结果最小,那么就是各位上都是最小的即可,比如0,但是n的最小值是1,所以迭代结果的最小值不会是0,那只能是将最高位写成1了,那么迭代后的最小值就是1了;
要使迭代结果的最大,一个数的各个数都是最大的9就好了,n最大是2^31 - 1 ,即一个十位数:2,147,483,647,那么其迭代的最大值也不会超过9^2 * 10,即810,这也就说明,对于一个int型的数,其迭代变化的范围只会在[1,810),即对于任意一个int数,迭代后的所有结果都不会超过810,那么在810次的变化后,一定会出现一个出现过的数并进入循环,为什么出现一个出现过的数后就会进入循环呢?那是因为每个数都对应只有一个迭代结果,对于迭代结果同理也是固定只有一个迭代结果...由此可以推断出每个数都有固定的迭代链,简单举例如下:
对于快乐数,最后的循环肯定都是1,而非快乐数,最后的循环里肯定没有1,这就是很关键的一点,那么在确定已经走进循环后,只需要判断自身的值就可以判断是否为快乐数了。
代码实现
解法一:暴力穷举
不多想,直接暴力穷举,既然810次变化后一定进入循环,那么就直接进行810次循环,判断最后一个迭代结果,即循环中的数是否为1即可:
class Solution {
public:
int sum_digit_double(int n) // 用于计算数字 n 的每一位的平方和
{
int sum = 0;
while (n != 0)
{
int digit = n % 10;
sum += digit*digit;
n /= 10;
}
return sum;
}
bool isHappy(int n) {
int nums[812]; // 由n<=2^31 - 1 可知,经一次变换后最大的数一定小于9^2 * 10, 即810,根据抽屉原理,在第810次变化后一定会出现循环
nums[0] = n;
for (int i = 1; i<812; i++)
{
nums[i] = sum_digit_double(nums[i-1]);
}
if (nums[811] == 1) // 可以确定第810之后一定进入循环,所以判断即可
{
return true;
}
else
{
return false;
}
}
};
解法二:双指针法
看到直线进循环的形状,有读者可能想到了判断链表是否有环用到的快慢指针的解法,快慢指针相遇的地方就是循环的里边了,虽然在这题里面都是常数级的时间复杂度,但是双指针的方法兼容性较前者好,双指针法基本上可以无视n的范围。
要想在这里套用双指针,就需要一定的抽象能力了,这里的指针不再是地址和下标,而是将对象本身抽象为“指针”,因为每个数的迭代链只有固定的唯一的一条,也就是说值的大小就决定了自己所在的位置了,所以这里可以用对象自己来作为指针。
给第一次接触的读者科普一下快慢指针,顾名思义,就是有两个指针,一个走的快,一个走的慢,注意,这里的快慢都是相对而言的,为了能使它俩尽可能快地相遇,普遍是让快指针每次走两个单位长度,慢的走一个单位长度,当然,具体情况具体分析:
class Solution {
public:
int sum_digit_double(int n) // 用于计算数字 n 的每一位的平方和
{
int sum = 0;
while (n != 0)
{
int digit = n % 10;
sum += digit*digit;
n /= 10;
}
return sum;
}
// 双指针法:
bool isHappy(int n) {
int slow = n;
int fast = sum_digit_double(n); // 这是为了让slow一开始与fast来开差距,顺应后续的逻辑
while (slow != fast)
{
slow = sum_digit_double(slow); // 等价于指针向后走一步
fast = sum_digit_double(sum_digit_double(fast));
}
return fast == 1;
}
};