一:LeetCode中的环形队列

题目本质 :
判断一个链表中是否存在环。
这是LeetCode中的一个环形链表的题,在这个题目中是让我们进行判断环形链表的,就是在一个链表中是否存在环,第一眼看到这道题的时候,相信大家都和我一样泛起了难,如何判断这个链表存在环,不是环的情况很好解决,只需要我们一直往下遍历,如果能遍历到结尾,就知道这个链表不存在环,但是当我们进行判断存在环的时候,我们不知道判断条件是什么,因为一旦陷入环内,我们的遍历就会一直进行下去,是无法停止的,所以这道题给我们练习算法的小白,简直就是当头一棒,其实这道题看过题解之后,我们就都知道这道题可以使用快慢指针进行解答,真是十分聪明的想法。
直觉解法的困境:
- 没环:一直遍历,能到
NULL✅ - 有环:会无限循环 ❌(根本停不下来)

通过画图之后,我们貌似可以感觉到,让慢指针一次走一步,快指针一次走两步,这样的方式就会让它们两在环中相遇,这样我们就可以证明是否存在环,但是现在有两个问题:
- 让慢指针一次走一步,快指针一次走两步,它们两一定会相遇吗,这是如何证明呢?还有就是有没有一种情况就是快指针是一次走两步的,慢指针一次走一步,有没有可能就是快指针直接就越过慢指针呢?(答案其实是不可能的)。
- 还有就是,一定是慢指针走一步,快指针走两步吗,我钥匙让快指针多走几步(三步,四步。。。。),它们两还有可能相遇吗?

现在我们来回答问题1,答案就是一定会相遇的。现在我们让慢指针slow一次走一步,快指针fast一次走两步,当slow走到环入口时,fast已经进入环内了,所以我们假设此刻环内fast的位置到环入口的距离为L,又因为slow一次走一步,fast一次走两步,所以它们两直接的步数差就是1,所以每走一次,它们两直接的相对位置就会减一,知道距离L减为0时,它们两一定会相遇。不会存在fast直接走过slow的情况。
至于问题2的答案,有了问题1的答案,我们可以很简单的就得到问题2的答案,同样,假如我们让慢指针slow一次走一步,快指针一次走三步,同样假设当slow走到环入口的时候,在环内的fast到环入口的位置为L,这样它们两的步数差就是2,这样只有当L为偶数,fast和slow就会相遇,当L为奇数的时候,就会使得在L就会减为-1,代表着这个时候fast就会越过slow,那么它们接下来会不会遇到呢,这个时候就取决于环的长度,现在我们假设环的长度为C,此刻它们两在环内的距离为C-1,同样当C-1为偶数的时候,由于它们两的步数差为2,所以它们会在下一圈中相遇,但是如果C-1为奇数,这就意味着它们两彻底失去了缘分,这辈子也不会相遇。
了解到这里,我们就知道,判断链表是否存在环,只需要我们使用快慢指针就可以解决了。这样这道LeetCode我们就可以成功通过了。
代码实现
class Solution {
public:
bool hasCycle(ListNode* head) {
ListNode* slow = head;
ListNode* fast = head;
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
if (fast == slow) {
return true;
}
}
return false;
}
};

二:LeetCode中的快乐数(其实是"隐形环")

看到这道题的时候,同样不知道题目快不快乐,反正相信大家拿到这道题的第一时刻肯定时不怎么快乐,甚至都快要有点emo了。
我们先来分析一下这道题,这道题说一个正整数,每次讲这个数的每一个位置上的数进行平方,然后将其相加,得到一个新的正整数,然后继续这样的操作。
在这个题目中说这个数这个数的结果变为1,要么就是无限循环。
看到这句话的时候,我就想我就不信这个邪,就难道没有一种情况就是一直平方下去,他就一定会循环吗?相信大家肯定有人和我一样的想法,但是我们仔细想一想之后,无论怎么样,它确实最后一定会陷入循环。

这里粗略的谈谈我的看法,我们可以看到题目中的正整数是int型的最大值,我们先通过计算器来看一看,结果是2 147 483 647,现在我们假设每一个位置都是9,也就是9 999 999 999,我们可以通过题目的要求计算一下这个数的结果是10个9*9,也就是810,也就意味着通过题目的要求转化之后,这个数的取值范围也就不过是1到810,这还是理想情况,即使恰好有那么一个数,刚好讲1到810这个区间的数字都取了一遍,下一次它的结果还会回到这个区间内,所以这道题不会存在一直平方下去的情况,他会陷入循环。

现在我们就得到了一个结论就是这个题一定就是一个循环,只不过一个循环的时候都是1,而另一个循环内是无限循环到不了1。所以接下来这个问题就转化为了上面环形队列的问题。
这样我们就可以同样使用快慢指针的方式解决这道题。判断它们相遇的时候,该处的值是多少,如果这个值是1,表明这个刚开始的正整数就是快乐数,如果不等于1,就表明这个数不是快乐数。
解法1:快慢指针
class Solution {
public:
int squareNum(int n) {
int num = 0;
while (n > 0) {
int tmp = n % 10;
num += tmp * tmp;
n = n / 10;
}
return num;
}
bool isHappy(int n) {
int slow = n;
int fast = squareNum(n);
while (slow != fast) {
slow = squareNum(slow);
fast = squareNum(squareNum(fast));
}
return slow == 1;
}
};

解法2:哈希表
除了使用这种快慢指针的方式,我们还可以使用哈希集合的方式解决这道题,我们可以从开始就将正整数插入到集合中,这样反复检查新插入的数字是否已经在集合中,将这个作为我们跳出循环的判断,然后跳出循环之后,再次判断这个数是否为1,如果这个数为1,代表是快乐数,否则,代表这个数不是快乐数,并且哈希集合查询一个数是否在集合中的效率接近 O(1),速度也是杠杠的。
class Solution {
public:
int squareNum(int n) {
int num = 0;
while (n > 0) {
int tmp = n % 10;
num += tmp * tmp;
n = n / 10;
}
return num;
}
bool isHappy(int n) {
unordered_set<int> s;
auto ret = s.insert(n);
while (n != 1 && ret.second) {
n = squareNum(n);
ret = s.insert(n);
}
if (*(ret.first) == 1) {
return true;
}
return false;
}
};

两种方法对比
| 方法 | 优点 | 缺点 |
|---|---|---|
| 快慢指针 | 空间O(1),高级 | 不易理解 |
| 哈希表 | 简单直观 | 需要额外空间 |
三:Leetcode中的移动零

这道题也是利用双指针的思想可以快速解决的一道题。

把数组划分成三个区间:
0, dest-1\] → 已处理的非0区间 \[dest, cur-1\] → 已处理的0区间 \[cur, n-1\] → 未处理区间
两个指针:
cur:负责遍历数组(扫描)dest:指向下一个应该放非0元素的位置
核心逻辑(超重要)
遍历数组时:
👉 情况1:当前是非0
swap(nums[cur], nums[dest]);
dest++;
✔ 把非0元素"丢到前面去"
👉 情况2:当前是0
什么都不做
cur++;
✔ 直接跳过
代码实现
class Solution {
public:
void moveZeroes(vector<int>& nums) {
int cur = 0;
int dest = 0;
while (cur < nums.size()) {
if (nums[cur] != 0) {
swap(nums[cur],nums[dest]);
cur++;
dest++;
} else {
cur++;
}
}
}
};
一句话总结
遇到"循环问题" → 想快慢指针
遇到"数组整理" → 想双指针