本文章只对单向链表做出示例
如何理解链表存在环
存在环就说明链表的尾节点不是指向 null
,而是指向了链表中的另一个节点,只有这样才会构成环,如下图所示就是一个存在环的单向链表:
如何判断链表存在环呢
1. Set对象
思路:直接遍历链表,并且判断当前节点是否存在于Set集合中,如果存在,那就说明当前链表存在环,如果不存在,那么我们就将当前链表节点存入Set集合中,并继续往后遍历,如果存在环,那么Set集合中就一定会发生碰撞,如果不存在环,那么就一定有一个节点的 next 指针指向null,所以循环也会终止。
const isCircleBySet (head) {
if (null == head || null == head.next) {
return false;
}
// 判断是否存在闭环
const setBox = new Set();
while (null != head) {
// 判断set集合中是否包含当前节点
if (setBox.has(head.next)) {
return true; // 中断循环并抛出存在闭环
}
setBox.add(head.next);
head = head.next;
};
// 循环结束未发现闭环
return false;
};
2. 快慢双指针
思路:设想一下这个场景,如果说有两个人在圆形跑道上跑步,一个人一秒钟跑一米,另一个人一秒钟跑两米,然后他们两同时开始并且一直跑,只要体力允许,他们两人一定会在某一个节点相遇;相反,如果是直线跑道,那么他们就不可能相遇。
快慢双指针法主要步骤为:定义两个指针,一个
slow
指针,一次走一步;另一个fast
指针,一次走两步(必须是两步后面有注释)。如果可以在某一个点满足slow = fast
,那么就说明存在环,否则 fast指针必然先到到终点。
const isByTwoPorint (head) {
if (null == head || null == head.next) {
return false;
}
// 两个指针
const slow = head;
const fast = head;
// 试着想象一下链表不存在闭环且快指针落在最后两个节点的情况,就可以理解为什么这样判断了
// 只判断快指针是因为如果不存在闭环可以早些退出循环,如果存在闭环则必定会在闭环中与慢指针相遇
while (null != fast && null != fast.next) {
slow = slow.next; // 慢指针一次一步
fast = fast.next.next; // 快指针一次两步
// 如果快慢指针值一致了,则代表存在闭环
if (slow == fast) {
return true;
}
}
return false;
};
那么为什么快指针必须是两步呢?
我们先来看看假如快指针走 3
步会有什么问题?我们还是以文中开头的环形链表为例子,当第一次 fast
走了 3
步,slow
走 1
步之后,快慢指针位置如下图所示:
这时候 fast
继续走 3
步,而 slow
继续走 1
步,快慢指针位置如下图所示:
我们发现,快指针超过了慢指针,又跑到前面去了,虽然最终他们还是会相遇,但是这会导致一个问题,那就是当快慢指针相遇时,我们无法知道快指针在环形里面走了多少圈,也无法知道慢指针在环形里面走了多少圈,这会导致很难推断环的入口位置。
而如果快指针走
2
步,当慢指针也入环之后,每走一次,慢指针都会和快指针拉开1
步距离;而反过来想,相当于是快指针每次都在以缩短1
步的距离来追赶slow
指针,因为每次只缩短1
步,所以快慢指针一定不会出现错过相遇点的情况
快指针任何时候走的距离一定为慢指针的 2 倍
我们都知道 fast
指针在任何时候走的距离一定是 slow
指针的 2
倍,那么我们再来看一下下面这幅图:
上图中以节点为划分,有三段链表,a
,b
,c
分别表示三段的距离,我们假设当快慢指针相遇的时候,快指针已经在环中走了 n
圈,那么就可以得到快指针走过的距离为:a+n*(b+c)+b
,而慢指针走过的距离为 a+b
,根据快指针走的路程一定是慢指针的两倍,可以得到如下等式:
a+n*(b+c)+b = 2*(a+b)
,最终经过转换,得到 a=(n-1)*(b+c) + c
。
到这里可能有人会有疑问,快慢指针相遇的时候,为什么慢指针一定没有走完一圈呢?如果慢指针也走了 m
圈,那么慢指针走过的距离就是 a+m*(b+c)+b
了,但是这其实是不可能的。
假设一个链表15个节点,以第十节点为环的入口,慢指针一次一步,快指针一次两步
则会出现以下情况:
1 1; 2 3; 3 5; 4 7; 5 9; 6 11; 7 13; 8 15; // 快指针未入环时
9 11; 10 13; 11 15; 12 11;13 13; // 快指针入环后
得出:快指针是有可能走 n 圈的,但不会跨过慢指针
假设一个链表20个节点,以第六个节点为环的入口,慢指针一次一步,快指针一次三步
则会出现以下情况:
1 1; 2 4; 3 7; 4 10; 5 13; 6 16; 7 19; // 快指针未入环时
8 7; 9 10; 10 13;11 16; 12 19; 13 7; 14 10; 15 13; 16 16; // 快指针入环后
得出:快指针是有可能走 n 圈的并且会跨过慢指针
总结:存在环的情况下
1.只要慢指针没有入环,快指针永远不可能与慢指针相遇。
2.快指针在一次两步时,一旦慢指针入环则快指针以每步减少一节点的差距向慢指针追
赶,绝对不会在环内有跨过慢指针的情况。
3.快指针在一次 n (n > 2)步时,一旦慢指针入环则快指针以每步减少 n - 1 节点的差距
向慢指针追赶,可能会在环内有跨过慢指针的情况。
为什么快慢指针相遇时慢指针没有走完一圈?(fast 指针一次两步情况下)
我们假设这个环的长度为 x
,那么当 slow
指针走完一圈时,需要走 x
步,而当 slow
指针走完 x
步,fast
指针已经走了 2x
步了,也就是走了两圈了,那么他们一定在某一个点相遇过了(因为他们不可能错过相遇点),所以当快慢指针第一次相遇时,慢指针是不可能走完一圈的。
3. 找环的入口位置
利用第三个指针找到环的位置
继续回到上面的等式:a=(n-1)*(b+c) + c
,然后我们可以发现,其实 b+c
正好等于环的长度,也就是说:从链表头部到入环的距离(a)恰好等于从相遇点到入环点的距离(c)再加上 n-1
圈个环的长度。
这时候就有个有趣的现象了,如果
slow
和fast
相遇了,那么这时候我们再定义一个指针指向链表起点,一次走一步,slow
指针也同步继续往后走,那么这两个指针就一定会在链表的入口位置相遇。
const findCycleHead (head) {
if (null == head || null == head.next) {
return false;
}
// 两个指针
const slow = head;
const fast = head;
// 试着想象一下链表不存在闭环且快指针落在最后两个节点的情况,就可以理解为什么这样判断了
// 只判断快指针是因为如果不存在闭环可以早些退出循环,如果存在闭环则必定会在闭环中与慢指针相遇
while (null != fast && null != fast.next) {
slow = slow.next; // 慢指针一次一步
fast = fast.next.next; // 快指针一次两步
// 如果快慢指针值一致了,则代表存在闭环
if (slow == fast) {
const access = head; //定义一个新指针
while (access != slow){
access = access.next;
slow = slow.next;
}
return access;//返回环的入口位置
}
}
return null;
};