"Success is not final, failure is not fatal: It is the courage to continue that counts." - Winston Churchill
1. 题目描述
2. 题目分析与解析
2.1 思路一
还是最简单的,模拟最直观的思路,就是进行一个while循环。比如:
根据上面的示例,可以知道while循环终止条件为各个位上的平方和为1,就可以返回true。但是对于false的情况怎么办呢?
此时就需要注意题目中提到的
它为什么把无限循环加粗?那就是在向你透露一个信息,对于无限循环的情况那就是需要返回false的情况。
所以我们现在的目的就是找到无限循环到底是什么样的。需要找到这个无限循环的规律,那我们就得找一些出现无限循环的数来看看,也就是找到那些返回false的情况。看看题目中的测试用例:
这不是就有一个?因此我们就可以以 2
作为突破口,看看究竟无限循环是什么意思。
上面是以2为用例的一次次计算,发现什么了吗?
有没有发现开始循环了?所以遇见问题不要慌,把能用的信息都用上多试一试就能解决了。
既然发现有循环了,那是不是就很好办了,我们就可以使用一个结构,存储那些循环的部分,如果在后续while过程中发现开始和循环的部分重复了,就可以判断该数不是快乐数了。
存储那些循环的部分,到底是哪部分?
既然要循环,继续看这个图:
两个红色部分有没有看出点什么?2和20,两个就多了几个0,因此我们就可以假设出现循环的位置就是我们的当前位置的值可能加了几个0,这样才能使得他们生成的下一个数字一样。于时我根据这个思路写了代码,但是很遗憾在测试用例为3时:
所以我又根据3作为输入,重新计算每一次的值:
这我发现又冒出来了4这个数字,又开始循环了。直到这时我才知道原来这个数字就是出现循环返回false的条件,也就是说对于那些不是快乐数的n,它肯定会走到4开头的循环。
而我之前运行超时的假设是:
对于任何数,如果不是快乐数,那么就会有一个轮次的固定数,也就是如上图红色一个轮次,蓝色一个轮次,这两轮除了第一个数字不一样以外其它部分全部相同,所以我在之前的代码中就去判断第1次while循环的结果和某一次是否相同,当出现相同值就说明进入了第二轮次循环,就可以返回false。所以我假设错误的结构是这样的:
但是实际上是所有的非快乐数都会进入一个循环:4→16→37→58→89→145→42→20→4
,也就是实际的结构是如下图:
现在规律找到了,那么我们就可以缕一缕代码思路了。
代码思路:
-
定义一个hashMap,存储平方值
-
进行while循环计算每一个sum,并将计算结果在最后赋值给n
-
当发现n等于1时,说明满足条件,返回true
-
当发现n等于4时,说明进入循环,返回false
总结:
遇见问题多尝试,多试一试就能发现规律。开始我也没头绪,但是试着试着就找到规律了,所以还是得多动手。
2.2 思路二——判断是否循环
在这里再讲一下快慢指针法,因为我们已经知道如果某个数字不是快乐数,就肯定会出现循环,因此我们就需要判断是否有循环即可,至于这个循环是什么循环,我们并不关心。而什么是循环,循环就是当前节点连到下一个点,这样一直连下去最后还能回到当前节点,那么是不是就是一个环状结构。
因此判断一个数是不是快乐数就是判断它形成的数字链中是否有环,就是检测一个链表是否有环。这个问题怎么解决呢?
在给答案之前先来思考一下,如何判断是否会进入环?
2.2.1 思路二——1
我们是不是可以用一个定义一个hashMap,保存走过的值,如果发现某一个值开始在之前的hashMap中包含了,那么肯定下一个数字也被包含,因为相同数字的下一个数字肯定还一样。因此只要发现某一个值开始在之前的hashMap中包含了,就说明有循环。
代码思路:
-
定义一个hashMap,保存走过的值,定义一个hashMap,存储平方值(只为了做加速运算的作用)
-
进行循环,条件为如果n等于1,表示成功,那么结束循环
-
将n存入走过的hashMap
-
计算新的值
-
判断之前走过的值中是否有当前的新值,如果有说明进入循环了,就需要返回false
-
将新值赋值给 n
-
2.2.2 思路二——2
除了上述思路以外,还有一种方式就是假想一下两个人现在准备去操场跑步,他们准备一直在操场跑不停止。
假设他们都从宿舍出发,A 8点就到了跑道开始跑步,B 10点才到跑道跑步,并且我们假设A 的速度是5m/s,B的速度是3m/s,那么A和B会相遇嘛?
答案显而易见即使他们不同时到达,而且速度不一样,但是他们肯定会相遇。那如果A的速度是100m/s,B的速度是1m/s呢?
答案还是会相遇,这是显而易见的,因为他们的速度虽然不同,但是速度差就决定了他们彼此相互靠近的速度。
因此把这种思想套用在我们的题目上,我们要判定是否出现环状结构,那么是不是就可以让两个速度不一样的人用不同的时间从宿舍去操场,开始跑步。
-
如果这个数不是快乐数,它就不会出现循环结构,也就是说他们的跑道会变成直线跑道,不是快乐数那就肯定有终点,这个终点就是出现1的情况,那么他们之间的距离肯定会不断拉大,并且跑的快的人肯定比跑的慢的人先到达终点。
-
如果这个数是快乐数,那么它就会出现环状结构,就是有循环,那么他们就是围绕环形跑道进行奔跑:
那他们肯定会在某一个地方相遇,这就是终止条件。
其实上述思路就是 弗洛伊德循环查找算法。
所以对于是快乐数和不是快乐数,两个不同速度的跑步者都能找到终止条件,那么程序不也一样?所以根据以上思路,我们可以写出如下代码思路。
代码思路:
-
假定两个速度不同的跑步者,这里的速度其实就是遍历节点的个数,假设A每次遍历两个,B每次遍历一个。那么A和B要么相遇,要么A先走到1。
-
定义一个hashMap,存储平方值(加速运算)
-
while循环计算每一次的结果,终止条件为速度快的是否到达终点
1
,或者两个跑步者是否相遇-
计算A跑步者每次跑步后的位置
-
计算B跑步者每次跑步后的位置
-
3. 代码实现
3.1 思路一
3.2 思路二
3.2.1 思路二——1
3.2.1 思路二——2
4. 相关复杂度分析
4.1 思路一
时间复杂度
-
初始化HashMap: 初始化操作的时间复杂度为
O(1)
,因为这里只有10个固定的映射关系被添加到HashMap中,与输入大小n
无关。 -
while循环: 这个循环的次数取决于
n
减小到1或者进入循环(如开始重复相同的数值)所需的迭代次数。对于快乐数问题,已知所有非快乐数最终都会进入到4, 16, 37, 58, 89, 145, 42, 20, 4
的循环中。因此,循环的次数是有上限的,但是这个上限如何精确定义对于任意的n
来说是不明确的。尽管如此,我们可以认为这个上限存在,从而可以认为该循环的时间复杂度是O(log n)
,因为每一次迭代n
都会显著减小(至少在多数情况下)。 -
内部while循环: 这个循环用于计算
n
的每一位数的平方和。对于一个k
位数字,这个循环将执行k
次。因为k = log10(n)
,所以这个循环的时间复杂度是O(log n)
。
因此,总的时间复杂度可以被视为O(log n * log n)
,因为外部循环和内部循环的复杂度都是O(log n)
,并且它们是嵌套的。
空间复杂度
-
HashMap: 因为HashMap中存储了10个固定的键值对,所以它的空间复杂度是
O(1)
。 -
临时变量:
sum
和循环中的其他临时变量占用的空间是常数级别的,因此它们的空间复杂度也是O(1)
。
所以该函数空间复杂度是O(1)
,因为所需的额外空间不随输入n
的大小变化而变化。
4.2 思路二
方法一分析 (isHappy2
)
-
时间复杂度:
-
初始化
squareMap
的时间复杂度为O(1),因为它仅包含0到9的平方,是一个常数时间操作。 -
主循环中,对于每个
n
,我们计算其数字平方和,直到n
变为1或进入循环。这个过程的时间复杂度取决于n
的大小和它进入循环的快慢。在最坏的情况下,这可能接近O(log n)到O(log^2 n),因为每次迭代都会减少n
的大小,但是具体取决于数字的分布和循环的检测。 -
使用
numMap
来检查循环的存在,每次插入和查找操作的时间复杂度为O(1)。
-
-
空间复杂度:
-
squareMap
的空间复杂度为O(1)。 -
numMap
的空间复杂度最坏情况下为O(log n)到O(log^2 n),因为它存储了遍历过程中的每个数字,直到找到循环或达到1。
-
方法二分析 (isHappy3
)
-
时间复杂度:
-
初始化
squareMap
与方法一相同,时间复杂度为O(1)。 -
使用快慢指针的方法,时间复杂度主要依赖于找到循环或1所需的步数。快慢指针技巧通常用于检测链表中的循环,其效率高于简单的追踪方法,因为它减少了不必要的迭代。对于快乐数问题,这种方法能够更快地到达重复序列或结束,因此在实践中,它的时间效率通常比线性检查所有元素要好,但最坏情况下的理论时间复杂度仍然是O(log n)到O(log^2 n)。
-
-
空间复杂度:
-
squareMap
的空间复杂度为O(1)。 -
与方法一不同,方法二没有使用额外的哈希映射来存储遍历过的数字,因此它的空间复杂度优于方法一,主要是O(1),因为除了几个变量之外没有存储大量数据。
-
总结
-
方法一(
isHappy2
)的时间复杂度大约是O(log n)到O(log^2 n),空间复杂度是O(log n)到O(log^2 n)。 -
方法二(
isHappy3
)的时间复杂度同样是O(log n)到O(log^2 n),但空间复杂度降低到O(1)。
方法二在空间效率上有显著优势,因为它避免了存储所有遍历过的数字。在时间效率方面,两种方法可能相似,但方法二通常更优,因为快慢指针技巧能更快地发现循环或达到1。