欢迎来到博主的专栏——算法解析
博主ID:代码小豪
leetcode202——快乐数
这题的最大难度就在于理解题目了。我们来看看题目到底要求的是什么?
首先对于一个数n是否是快乐数,我们要求得该数n各个位数上的平方之和,记为s1。如果s不为1。那么求s各个位数上的平方之和,记为s2。如果s2不为1,那么继续求s2各个位数上的平方之和。以此类推,一个数n可能会推出s1,s2,……,sn。如果sn等于1,则n为快乐数
这个定义乍一看那是吓到年幼的我了。这么复杂的数学问题我咋能会啊。后来仔细看了看示例,发现情况并非如此,如果找到规律,那么这道题确实是一道简单题。
我们来看看,示例1,n为19。
经过计算,最后的结果出现了1,因此19是快乐数。
那么示例2,n为2。
2不是快乐数,但那是怎么得出来的呢?我们现在演算纸上计算出s1,s2……sn。算了,这还是太费劲了,既然咱会编程,那么我们就用电脑来生成s1,s2……,s20.代码如下:
void createhappynum20(int n)
{
static int count = 0;
int happysum = 0;
int reminder = 0;
while (n)
{
reminder = n % 10;
happysum += reminder*reminder;
n /= 10;
}
cout << happysum <<endl;
count++;
if (count == 20) {
return;
}
createhappynum20(happysum);
}
我们将2输入至这个函数当中。输出结果为
有没有发现什么规律吗?如果没有那么就将数据排成横向的。
有没有发现,其实这个数组中的数据会在某个节点处开始循环!如果还没有发现,那么也可以将图画成这样
有没有发现,这个数列是循环的。那么也就是说,如果一个数不是快乐数,那么该数的数列一定会在某个节点进入循环。但是我们只有2这么一个例子,其他自然数也会进入循环吗?
首先我们要知道一点,当位数超过3位数,小于13位数时,其各个位数的平方和不会超过3位数,那么也就是说,不存在没有循环的非快乐数。原理如下:
假如现在有5个抽屉,而你又6个球,那么一定会有一个抽屉出现2个以上的球。那么反过来,现在int类型的数据不会超过11位。因此,只要在这个范围内的任何数据,其平方和都不会超过999。如果我们让数列循环求平方和1000次,那么,一定会存在2个以上平方和是相同的,一旦出现一个相同的平方和,那么该数列就相当于进入了死循环,就能证明n是非快乐数了。
那么难题就在于,如何证得该数列进入了一个死循环。
指针的追及问题
博主这里直接给出暴论:在数列的起始位置,设一个指针为慢指针,一次向后移动1位,另一个指针为快指针,一次向后移动两位。如果该数列循环,则快慢指针一定会相遇!
你们大可以在演算纸上试着画一下,你会发现这个结论是正确的,那么博主就这么草率的下了这个定义吗?当然不是了,小人不才,但是这个算法正好略懂。
对于这种需要证明数据循环的问题,快慢双指针当然是一个非常不错的选择,我们想要证明数据循环,那么就要证明快慢指针会相遇。其实这个道理可以用一个例子解决,即一个人坐飞机,一个人开船,他们都从一个地方一直往东走,如果开飞机的转了一圈,遇到了开船的,那么不就说明,地球是圆的(哈哈)。
由于快指针和慢指针都是从头部位置开始,那么,快指针一定会比慢指针先进入循环。我们假设这个环的周长为C,当慢指针进入循环入口时,快指针距离入口为N,那么,快指针与慢指针之间的距离为C-N。
(用这个画图工具来画图有点不顺手,还是CAD用的舒服,在后面会附上一个链接,也是双指针的追及问题,里面的图使用CAD画的,感兴趣可以看看)
已知,快指针fast一次前进2步,slow每次前进一步,那么,每经过一回合,fast距离slow的距离就会减少1。如果fast要追上slow,只需要经历C-N个回合。
这就证明,在循环数据中,快慢指针一定会在循环内部相遇。那么回到快乐数这个问题本身,我们要证明的,不就是这个非快乐数构成的数列会进入循环吗?
代码如下:
class Solution {
public:
int calc_sum_of_bit_squre(int n)//此函数用来计算各个位数的平方和
{
int ret = 0;
while (n) {
int reminder = n % 10;
ret += reminder * reminder;
n /= 10;
}
return ret;
}
bool isHappy(int n) {
vector<int> arr;
size_t size;
while(1)
{
n=calc_sum_of_bit_squre(n);
arr.push_back(n);
size=arr.size();
if(arr.back()==1)
{
return true;
}
if(size>2)
{
if(arr[size/2]==arr[size-1])
{
return false;
}
}
}
}
};
说明:
由于数列是一个位置数量的数列,因此我们可以用一个vector来承载这些数列的数据(因为vector可以变长)。然后这里面没有使用快慢指针,这是因为vector是动态增长的,因此slow永远遇不到fast。但是证明数列循环不需要让指针相等,只需要让指针指向的数据相等即可。
而fast和slow的关系是,fast/2=slow,因此我们可以数组下标来模仿快慢指针。即arr[size]==arr[size/2]。这个关系式就是代表快慢指针指向的数据相等。
当然,还有下面这种方式,就是不好理解,但是效率比上面高得多
class Solution {
public:
int calc_sum_of_bit_squre(int n)//此函数用来计算各个位数的平方和
{
int ret = 0;
while (n) {
int reminder = n % 10;
ret += reminder * reminder;
n /= 10;
}
return ret;
}
bool isHappy(int n) {
int slow=calc_sum_of_bit_squre(n);//slow获得数列上的下一个数
int fast=calc_sum_of_bit_squre(calc_sum_of_bit_squre(n));//fast获得数列上的下两个数
while(fast!=slow)//当快慢指针相遇即结束
{
slow=calc_sum_of_bit_squre(slow);
fast=calc_sum_of_bit_squre(calc_sum_of_bit_squre(fast));
}
return fast==1;//判断循环内部值是否为1
}
};
前面提到了,这种线性动态增长的数列,如果是用普通指针的形式,slow永远追不上fast。但是,我们只需要证明slow指向的值和fast指向的值相等,也能证明这个数列循环,因此我们甚至可以抛弃常用的双指针形式(即数组下标,迭代器,指针类型)。直接使用整型数据来获取数列上的值。slow获取数列上的下一个数据,fast获得数列上的下两个数据。如果fast与slow相等了,那么就证明,这个数列进入了循环。如果循环体内的值都是1,说明这个数就是快乐数,若非1,则非快乐数。
更加典型的双指针追及问题
快乐数用到指针比较抽象,而且追及的图像画的也不够好,也不够形象。实际上在博主以前的博客当中就出现过一个快慢指针算法,来解决的追及问题的示例。博主将链接放在底下,大家感兴趣的可以看看。里面的指针可是正儿八经的指针哦。
leetcode141——环形链表