环形链表 OR 快慢指针
判断环是否存在
如上图所示,在一个单链表中存在一个环,判断这个环是否存在。
一个比较容易想到的方法是哈希表,每遍历一个节点,先判断其是否已经在哈希表中,如果在,说明存在环,结束;否则就加入哈希表,知道判断存在环或者遍历完所有的节点。
但是上面的做法有一定的空间开销,如果链表节点数足够大,容易造成一定的空间开销。有些算法题也要求使用原地算法来解决,因此要使用双指针的方法;
Floyd判圈算法(龟兔赛跑算法)
相像一下【乌龟】和【兔子】在链表上移动,【兔子】跑的快,假定每次走两步,【乌龟】跑得慢,假定每次走一步。
当两者从链表上同一个节点开始移动时,如果没有环的存在,那么【乌龟】永远不可能追上【兔子】;
如果有环的存在,那么【兔子】一定会先于【乌龟】进入环,并且一直在环内移动,等到【乌龟】进入环时,由于【兔子】跑的快,所以会把【乌龟】套圈,在某一个时刻二者在环内相遇;
根据以上思路来解决问题,我们可以定义两个指针,慢指针slow
和快指针fast
,慢指针就是上面的【乌龟】,每次移动一步,快指针就是上面的【兔子】,每次移动两步;
初始时,【乌龟】和【兔子】在同一个出发点,每一轮移动,慢指针走一步,快指针走两步,然后开始遍历指针;如果在移动过程中,快指针反过来追上慢指针,那么就说明该链表中存在环;否则快指针将到达链表尾部,该链表不为环形链表。
ListNode *fast = head;
ListNode *slow = head;
while (true)
{
if (fast == nullptr || fast->next == nullptr)
return nullptr;
slow = slow->next;
fast = fast->next->next;
if (slow == fast)
return true
}
return false;
时间复杂度是O(N),N是节点个数。
- 当链表不存在环时,快指针将先于慢指针到达链表尾部,链表中的每个节点至多被访问两次;
- 当链表中存在环时,每一轮移动后,快慢指针的距离将减小1,二者间的最大距离是环的长度,因此最多移动N轮(N个节点都属于环)
进阶版:找到环的入口节点
最简单的方法还是哈希表,但是和上面所说一样,空间开销大,不符合某些题目要求的原地算法。
在上面的基础上,我们已经可以判断环的存在,以及找到二者相遇的结点。
我们来考虑这样一个问题:
假设环是存在的,其中 a 个节点属于环之前的节点,b 个节点属于环中的节点,以上面的图为例,a=3, b=4;
- 当【乌龟】和【兔子】相遇时,【兔子】所走的步数是【乌龟】的两倍,即
f=2s
- 【兔子】比【乌龟】多走了 n 个环的长度,即
f=s+nb
(两者都走过了 a 步。然后在环内绕圈直到相遇,相遇时【兔子】比【乌龟】多走环的长度的整数倍) - 两式相减得到:
f=2nb
,s=nb
,即【兔子】和【乌龟】分别走了 2n 个环和 n 个环的长度
目前情况分析:
如果让指针从链表头部一直向前走并统计步数 k,那么所有 走到链表入口节点时的步数 是:k=a+nb(先走 a 步到入口节点,之后每绕 1 圈环( b 步)都会再次到入口节点)。
而目前,slow 指针走过的步数为 nb 步。因此,我们只要想办法让 slow 再走 a步停下来,就可以到环的入口。
但是我们不知道 a的值,该怎么办?依然是使用双指针法。我们构建一个指针,此指针需要有以下性质:此指针和 slow 一起向前走 a 步后,两者在入口节点重合。那么从哪里走到入口节点需要 a 步?答案是链表头部 head 。
双指针第二次相遇:
slow指针 位置不变 ,将 fast 指针重新 指向链表头部节点 ;slow 和 fast 同时每轮向前走 1 步;
TIPS:此时 f=0,s=nb ;
当 fast 指针走到f=a 步时,slow 指针走到步s=a+nb ,此时 两指针重合,并同时指向链表环入口 。
第二次相遇后,返回 slow 指针指向的节点即可。
ListNode *detectCycle(ListNode *head)
{
ListNode *fast = head;
ListNode *slow = head;
while (true)
{
if (fast == nullptr || fast->next == nullptr)
return nullptr;
slow = slow->next;
fast = fast->next->next;
if (slow == fast)
break;
}
fast = head;
while (fast != slow)
{
slow = slow->next;
fast = fast->next;
}
return fast;
}