环形链表的常见题型包括:判断链表是否有环、寻找环的入口以及判断环的长度。这类问题都可以使用快慢指针法来解决。
算法的原理来自于弗洛伊德判圈法。原理解析可以看下:https://cloud.tencent.com/developer/article/1999816
这里再补充一点,很多的文章都说若链表有环,则快慢指针必定相遇,并且还类比了龟兔赛跑的例子,说是在一个操场上,a和b以不同的速度沿着相同的方向出发一定会相遇。但是现在讨论的是链表,一个是离散空间,一个是连续空间实际上是不能直接类比的。等价的问题应该是,兔子每隔1米一个脚印,乌龟每隔0.05米一个脚印,问步数相同的情况下,龟兔可不可能在同一个脚印下相遇。
快慢指针法的前提条件是快慢指针在环内必定相遇,这里实际上是可以证明(证伪)的,下面本文会给出一个简单的证明。
问题模型可以简化为下图所示。
问题一:A和B在一个环状链表内从同一个起点出发,A每次走m步,B每次走n步,链表长度为L,请问AB一定会相遇吗?
事实上是一定会相遇的。假设经过k次后AB相遇,那么一定有
问题就转换成了寻找合适的k,事实上直接让k=L这个等式就成立了,并且如果k是L的整数倍,那么这个等式一定成立,所以这个问题一定有解还不止一个。
问题二:A和B在一个环状链表内从不同起点出发,A每次走m步,B每次走n步,链表长度为L,请问AB一定会相遇吗?
假设AB距离为s,那么问题就转换成了:
m和n都是正整数,假设m-n=p,那么上式就转换成了
p/k/s/L都是正整数的情况下,这个等式不一定有解,举个反例,
m=5, n=3, s=31, L=100,带入就变成了:
显而易见的,无论k取什么值,都不可能让等式成立,不信的同学可以写程序验证一下。
所以在题目中,经常可以看到快指针走2步,慢指针走1步,这样差值是1,即使起点不同,两个指针也一定有交集。
下面再证明一下弗洛伊德判圈法的原理,示意图如下图
假设有2个指针,fast和slow,都从start出发。fast每次走2步,slow每次走1步。环的长度是L
第一次相遇在meet点,meet点到enter点的长度是n,start到enter的长度是m,第一次相遇时慢节点走了a圈,a大于等于0,快节点则走了b圈,b大于等于1(为什么大于等于1呢?不明白自己好好品一下)。设慢节点走过的路程为S,那快节点走过的路程就是2S,则有:
最终的目的是求出m的长度,因此需要消去S,第二个公式减去第一个公式x2得出:
这个公式说明了,起点start到环入口enter的距离是环长度的整数倍再减去一个数值,这个数值就是从环入口到两个节点第一次相遇走过的长度。
重点:如果让AB两个指针分别从start/enter出发,那么当A走到enter的时候,B实际上走的距离是(b-2a)L-n。那如果我们先让B指针从enter出发,先走n个节点,然后A再出发,两个指针每次都走一个节点,最后肯定相交于enter。先走n个节点的位置就是前面快慢指针相遇的位置。
因此在快慢指针相遇后,直接让其中一个指针重新指向头节点,然后每个指针每次都移动一个位置,快慢指针相遇的位置就是环的入口!
做题还是简单的,leetcode上经典的两个题目:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
bool hasCycle(ListNode *head) {
ListNode *fast = head;
ListNode *slow = head;
while(fast && fast->next)
{
fast = fast->next->next;
slow = slow->next;
if(fast==slow)
return true;
}
return false;
}
};
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
ListNode *fast = head;
ListNode *slow = head;
while(fast && fast->next)
{
slow = slow->next;
// 为空说明没有环,必须先判断,不判断直接执行下一行代码会报错
if(fast->next == NULL)
return NULL;
fast = fast->next->next;
if(slow==fast)
{
fast=head;
while(fast!=slow)
{
fast=fast->next;
slow=slow->next;
}
return fast;
}
}
return NULL;
}
};
这是一条吃饭博客,由挨踢零声赞助