141. 环形链表
思路一:双指针
判断是否有环
可以使用快慢指针法, 分别定义 fast 和 slow指针,从头结点出发,fast指针每次移动两个节点,slow指针每次移动一个节点,如果 fast 和 slow指针在途中相遇 ,说明这个链表有环。
为什么fast 走两个节点,slow走一个节点,有环的话,一定会在环内相遇呢,而不是永远的错开呢
首先第一点: fast指针一定先进入环中,如果fast 指针和slow指针相遇的话,一定是在环中相遇,这是毋庸置疑的。
那么来看一下,为什么fast指针和slow指针一定会相遇呢?
可以画一个环,然后让 fast指针在任意一个节点开始追赶slow指针。
会发现最终都是这种情况, 如下图:
fast和slow各自再走一步, fast和slow就相遇了
这是因为fast是走两步,slow是走一步,其实相对于slow来说,fast是一个节点一个节点的靠近slow的,所以fast一定可以和slow重合。
动画如下:
C++代码如下:
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;
}
};
思路二:哈希表
可以使用哈希表来存储所有已经访问过的节点。每次我们到达一个节点,如果该节点已经存在于哈希表中,则说明该链表是环形链表,否则就将该节点加入哈希表中。重复这一过程,直到我们遍历完整个链表即可。
class Solution {
public:
bool hasCycle(ListNode *head) {
unordered_set<ListNode*> see;
while (head) {
if (see.count(head)) {
return true;
}
see.insert(head);
head = head->next;
}
return false;
}
};
使用集合保存访问过的节点,还要每次查找节点是否存在,时间和空间复杂度都不低
142. 环形链表 II
思路一:双指针
这道题目,不仅考察对链表的操作,而且还需要一些数学运算。
主要考察两知识点:
- 判断链表是否环
- 如果有环,如何找到这个环的入口
判断链表是否环
如果有环,如何找到这个环的入口
此时已经可以判断链表是否有环了,那么接下来要找这个环的入口了。
假设从头结点到环形入口节点 的节点数为x。 环形入口节点到 fast指针与slow指针相遇节点 节点数为y。 从相遇节点 再到环形入口节点节点数为 z。 如图所示:
相遇时,slow走过的节点数量,fast走过的节点数/公式,其中n为fast指针在环内绕的圈数
由于fast的速度是slow的两倍,那么fast和slow相遇时,fast访问的节点个数也是slow的两倍,有:
两边消掉一个得:
因为要找环形的入口,那么要求的是x,因为x表示 头结点到 环形入口节点的的距离。
所以要求x ,将x单独放在左面:
再从中提出一个 来,整理公式之后为如下公式:
n一定是大于等于1的,因为 fast指针至少要多走一圈才能相遇slow指针。
这个公式说明什么?
先拿n为1的情况来举例,意味着fast指针在环形里转了一圈之后,就遇到了slow指针了。
当 n为1的时候,公式就化为
这就意味着,从头结点出发一个指针,从相遇节点也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是 环形入口的节点。
也就是在相遇节点处,定义一个指针index1,在头结点处定一个指针index2。
让index1和index2同时移动,每次移动一个节点, 那么他们相遇的地方就是 环形入口的节点。
动画如下:
那么 n如果大于1是什么情况呢,就是fast指针在环形转n圈之后才遇到 slow指针。
其实这种情况和n为1的时候 效果是一样的,一样可以通过这个方法找到 环形的入口节点,只不过,index1 指针在环里 多转了(n-1)圈,然后再遇到index2,相遇点依然是环形的入口节点。
直接使用slow和fast之一,只定义一个head出发的节点就行了
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
ListNode* fast = head;
ListNode* slow = head;
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
if (slow == fast) { // 有环
ListNode* index = head;// 定义一个节点,让它和slow同时出发
while (slow != index) { // 当slow和index相遇时,就找到了环的入口
slow = slow->next;
index = index->next;
}
return slow;
}
}
return NULL;
}
};
补充
为什么第一次在环中相遇,slow的 步数 是 x+y 而不是 x + 若干环的长度 + y 呢?
首先fast一定是先进入环的,当slow也进环后,slow跑一圈的时间,fast可以跑两圈,fast和slow相对速度为1,相当于fast以每次一个节点的速度接近slow,假设环的长度为A,考虑两种极端情况:
- 环入口就是相遇的节点,那么他俩差正好一圈,也就是slow还没开始走第一圈呢,就被追上了,y=0
- slow第一次达到环的入口,fast刚好在slow的前面,这时候,fast追上slow需要用最多的时间,相当于slow领先了A-1步,由相对速度为1,当slow走A-1步时,fast会追上slow,这时候b = A - 1
也就是说,无论如何,slow进入环的第一圈,一定会被fast追上!!
Carl哥的思路:
首先slow进环的时候,fast一定是先进环来了。
如果slow进环入口,fast也在环入口,那么把这个环展开成直线,就是如下图的样子:
可以看出如果slow 和 fast同时在环入口开始走,一定会在环入口3相遇,slow走了一圈,fast走了两圈。
重点来了,slow进环的时候,fast一定是在环的任意一个位置,如图:
那么fast指针走到环入口3的时候,已经走了k + n 个节点,slow相应的应该走了(k + n) / 2 个节点。
因为k是小于n的(图中可以看出),所以(k + n) / 2 一定小于n。
也就是说slow一定没有走到环入口3,而fast已经到环入口3了。
这说明什么呢?
在slow开始走的那一环已经和fast相遇了。
那有同学又说了,为什么fast不能跳过去呢? 在刚刚已经说过一次了,fast相对于slow是一次移动一个节点,所以不可能跳过去。
剑指offer思路
先通过快慢指针找到相遇的节点,然后统计环中的节点个数
记环长为n,头节点到入口之前共有x个节点,那么链表的总长度为 x + n,现在快指针先走n步,距离环的入口刚还还剩下x个节点,由此:
用两个指针:
- 快指针在头节点开始向前走n个节点
慢指针在头节点开始,和第一个指针同时走,他们相遇的位置就是环的入口
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
// 先找到相遇节点
ListNode* fast = head;
ListNode* slow = head;
ListNode* meeting = new ListNode();
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
// 如果有环
if (fast == slow) {
meeting = fast;
// 统计环中节点个数
int nodeOfLoop = 1;
while (fast->next != meeting) {
fast = fast->next;
nodeOfLoop++;
}
// 找环的入口
// 先移动fast
fast = head;
for (int i = 0; i < nodeOfLoop; i++) {
fast = fast->next;
}
// fast 和 slow 同时移动
slow = head;
while (fast != slow) {
fast = fast->next;
slow = slow->next;
}
return fast;
}
}
return NULL;
}
};
思路二:哈希表
可以在扫描链表的过程中,将链表的节点放入集合中,在放入节点之前检查,集合中是否存在该节点,发现第一个已经存在的节点,返回这个节点,这个节点就是环的入口。
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
unordered_set<ListNode*> visited;
while (head) {
if (visited.count(head))
return head;
visited.insert(head);
head = head->next;
}
return NULL;
}
};