虽然双指针和快慢指针的真正联系只是"Two Pointers"而已,但两种算法的思想还是不太相同的,所以就用本篇博客单独提出来说明。
快慢指针原称Floyd's cycle-finding algorithm,该算法会使用两个在数组(序列/链表)种以不同速度移动的指针。因此也被称为Floyd龟兔算法
说起龟兔,自然而然的就想到龟兔赛跑的故事,而该算法的核心思想就是与这个故事有关。我们可以把两个指针视为龟指针和兔指针,让龟兔以不同的速度向前步进,以方便获取目标。
由上图简单分析可知,乌龟一次爬一步,兔子一次跑两步。直到兔子到达了终点。这样,二者的距离每次移动后加1 。
那么,我们不妨假设链表有n个元素,那么最多移动n/2轮。
当n为奇数时,乌龟就刚好停在中间节点,
当n为偶数时,乌龟刚好停在中间两个节点靠前的一个。 由此,龟兔算法用于处理链表/数组中的循环问题,找寻链表的中点或者需要知道特定元素的位置。以及处理回文的问题
说这么多可能还是蒙蔽,写算法关键还得是实战。那么以下请看例题。
实战1:判断是否为环形链表(LeetCode 141)
解题思路:按照龟兔算法思想,首先将龟兔处于同一起跑线,也就是首结点 3 的位置,在给予龟兔不同的初速度(一般为兔子 = 2 * 乌龟),那么当兔子先一步跑入 2 - 0 - 4 的环形链表中,它就会一直在这里循环跑圈,而当乌龟也慢悠悠的爬进环形链表中,二者的相对速度则是 1,这样在n次循环后,二者就可以在循环链表中相遇。
Java
public boolean hasCycle(ListNode head) {
if (head == null || head.next == null) return false;
// 使用快慢指针 (适用于循环问题,或者中点位置)
// 标记慢指针
ListNode slow = head;
// 标记快指针
ListNode fast = head;
// 当二者不相等时,则进行下一步
while (slow != fast) {
// 当快指针本身位空,或者快指针的下一个指向为空,则说明为单链表
if (fast == null || fast.next == null) {
return false;
}
// 更新快指针
slow = slow.next;
// 更新慢指针
fast = fast.next.next;
}
return true;
}
番外:这一题还有一种思路,就是通过改变经过节点的值(一个特殊的值),当循环到一样值的时候,则意味着有循环链表了。
此时就有人要问了,这只能判断有没有循环呀,那我要求循环链表开始的位置,该怎么做呢?咦,不要急,就等你这一句话呢!下面请看实战2。
实战2:求环链表的第一个结点(LeetCode2)
解题思路:我们先假设 x点是起点(即3),y点为环形链表的起始节点(即2),z点为龟兔相遇的点(未知)。
当龟兔相遇时,此时乌龟刚爬了 (a + b)m,而兔子已经跑了 a + b + n(b + c)m (n表示兔子绕圈次数),而由于兔子的速度是乌龟的两倍,则可以形成等式: 2(a + b) = (a + b)+ n(b + c)。
等式缩减后即可得 a + b = n(b + c);
分析了二者等式之后,我们再来看本题,要求求 y 的位置。
我们就有了两个思路,要么是从 x -> y (a)距离 ,要么是从 z -> y (b 距离)。 第一种思路就可知 a = n(b + c)- b = (n - 1)(b + c) + c ,这样就可以很清晰的得知,当二者相遇时,我们将乌龟重新挪回起点,当它再次爬到 Y点时,这时 兔子刚好也跑到了 Y点。 这种思路可行。 第二种思路就可知 b = n (b + c) - a,此时我们会发现未知数更多了,更加迷糊了,所以这个思路就放弃。那么题解如下:
java
public ListNode detectCycle(ListNode head) {
if (head == null || head.next == null) return null;
// 标记快指针
ListNode fast = head;
// 标记慢指针
ListNode slow = head;
// 使用 do-while,如果使用while则第一次就会跳出
do {
if(fast == null || fast.next == null) {
return null;
}
fast = fast.next.next;
slow = slow.next;
} while(fast != slow);
// 重置指针,这样就相同了
slow = head;
while(fast != slow) {
fast = fast.next;
slow = slow.next;
}
return slow;
}
通过以上两个例题,大致就明白了快慢指针在环链表中的使用。此时也可以散场了,今天又愉快的学到了一个小知识点。