前言
本文将介绍两种环形链表的问题——判断环形链表 和 返回入环节点
对于这两个问题,都将各自介绍两种不同的思路方法。
【暴力法】【双指针】
【哈希遍历】【快慢指针】
一、环形链表是什么?
环形链表是一种特殊的链表结构,其中至少存在一个节点,它的指针指向链表中的其他节点,从而形成一个环状。
二、环形链表Ⅰ(判断环形链表)
在这里我们引入具体的题目,方便大家更好地理解环形链表问题。
题目链接: [点击链接] Leetcode 141. 环形链表
这一类环形链表问题,要求给出头节点,判断是否为环形链表即可。
1.方法一(暴力)
这道题很有趣,并且值得注意的是给出了节点数量的范围。
既然给出了节点数,那么我们就可以利用环形链表的特点(有进无出),来进行循环,环形链表一旦开始循环,就无法自己跳出循环。
所以跳出循环的就不是环形链表。
代码如下:
class Solution {
public:
bool hasCycle(ListNode *head) {
for(int i=0;i<=10000;i++){//这里i<=10000是因为节点最多有10000个
if(head==nullptr){
return false;
}
head=head->next;
}
return true;
}
};
2.方法二(双指针)
可以采用快慢指针的方式,将链表的所有节点全部走过,一旦,两个指针再相遇,说明为环形链表。
快慢指针主要利用,慢指针每次移动慢,快指针每次移动快,如果是非环形的链表,那么快指针会一直比慢指针快,如果最初快指针就在慢指针前面,那么两个指针将永不相遇。
反之,如果两个指针相遇了说明了什么呢?
说明链表是环形的,快指针又回到了慢指针的后面。
代码如下:
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 (slow == fast) {//相遇
return true;
}
}
return false;
}
};
三、环形链表Ⅱ(返回入环节点)
这里我们引入另一种环形链表的题。
题目链接: [点击跳转] Leetcode 142. 环形链表 II
这道题则是在判断环形链表后,求解从哪里开始入环。
1.方法一(哈希遍历)
通过维护一个哈希表,将链表中的节点作为key存入map中,如果这个节点没出现过就是默认的false,如果出现过就是true。
因此,我们可以设置一个循环,这个循环只有false时(新节点)才进行下去,当检测到true(说明之前存过这个节点,此时正是进入环的时刻),跳出循环,返回当前节点。
代码如下:
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
map<ListNode*,bool> maph;
while (maph.find(head) == maph.end()){//找到已有节点
if(head==nullptr||head->next==nullptr){//判断环形
return NULL;
}
maph.insert({head,true});//将节点插入
head=head->next;
}
return head;
}
};
注意:不要忘了判断一下是否为环形链表哦!
2.方法二(快慢指针)
这道题同样可以用快慢指针解决。
观察下面的一个环形链表(链表中的节点数未知):
-------
/ \
(slow) / \
A-----------B -
(fast) \ /
\ /
-------
假设我们在节点A(也就是头节点)设置一个慢指针slow,和一个快指针fast。
当slow走到节点B时,我们可以确定fast一定已经在环上了(因为fast更快)。
(fast)
-----C-
/ \
/ \
A-----------B(slow) -
\ /
\ /
-------
如上图,我们假设slow在节点B时(第一次到节点B),fast在节点C。
但我们不知道slow第一次到B时,fast已经在环上走了几圈(因为AB长度未知,环的周长也未知),我们只能假设fast此时已经在环上走了n圈(n>=0)。
快慢指针一定会在某一时刻相遇(关于为什么相遇,在 环形链表Ⅰ方法二中已经解答),那么会在什么时刻相遇呢?
我们用BC来表示环的左上部分长度,用CB来表示环的右下部分长度
(fast)
BC -----C-
/ \
/ \
A-----------B(slow) -
\ /
\ / CB
-------
如上图,当相遇时一定会有:
2*(AB+slow在环上走的长度)==AB+fast在环上走的长度
AB等于什么呢?
2AB==AB+BC+n(BC+CB)
当slow第一次走到B时,fast走到C,等式左侧是slow走的长度的2倍,右侧是fast走的总长度,二者是相等的,所以能得到这个等式。
可以推断出: AB=BC+n*(BC+CB)
(fast)
-----C-
/ \
/ \
A-----------B(slow) -
\ /
\ /
-------
观察上图,我们可以直观的推断出:
slow再走长度为(CB)的路径后,就会和fast相遇。
因为此时的fast要追上slow,隔着(CB),而fast速度是slow的2倍,所以fast走2*(CB),slow走(CB),二者刚好相遇。
假设,slow和fast相遇再节点D,如下:
-----C-
/ \
/ \
A-----------B -
\ /
BD \ /
------D(slow)(fast)
此时BC+CD就等于(CB)
我们发现CB还可以用CD+DB来表示呀
推出:BC+CD==CD+DB
推出:BC=DB
之前我们曾得到: AB=BC+n*(BC+CB)
那么可以得到:AB=DB+n*(BC+CB)
-----C-
/ \
(rot) / \
A-----------B -
\ /
BD \ /
------D(slow)
重点来了,我们在节点A再定义一个指针rot 指向A,rot的速度与slow是一样的。
我们发现rot走到节点B,路径长度是AB,由于二者速度相同,slow走(DB+n*(BC+CB))长度,刚好slow也走到节点B,这时rot与slow相遇了。
我们返回这个时刻相遇节点即可。
代码如下:
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
ListNode* slow = head;//慢指针,从头开始
ListNode* fast = head;//快指针,从头开始
while (fast!=nullptr) {
slow=slow->next;//每次移动一格
if (fast->next==nullptr) {//判断环形链表
return nullptr;
}
fast=fast->next->next;//每次移动两格
if (slow==fast) {//快慢指针相遇
ListNode* rot=head;//新建节点
while (rot!=slow) {//当没相遇时
rot=rot->next;
slow=slow->next;
}
return rot;//相遇在入环节点
}
}
return nullptr;
}
};