介绍
快慢指针是链表中经常用到的一种技巧,借助该技巧能够一次遍历就可以确定链表中点,判断链表是否含有环等。
实例
先介绍一个非常经典的问题,LeetCode 142 返回链表的入口节点。
给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。说明:不允许修改给定的链表。
在这之前有一个简单的问题,就是如何判断链表是否有环,那么很简单,我定义两个指针,一个走的快,一个走的慢,如果有环那么它们一定会相遇。那么如果没有环呢?那肯定就有终点,因此只要走到链表的末尾,那就是没有环。代码如下:
public boolean findCycle(ListNode head) {
if(head==null||head.next==null||head.next.next==null) return false;
ListNode first = head.next;
ListNode second = head.next.next;
while(first!=second){
if(second==null||second.next==null) return false;
first = first.next;
second = second.next.next;
}
return true;
}
回过头来,这道题可以看成是链表有环问题的拓展,找到环的入口。同样的,这里我们还是用快慢指针。如果有环,那么快慢指针一定会相遇,那么问题来了,在哪里相遇呢?相遇的时候快指针走了多久?慢指针呢?
假设链表的非环长度为L,环的长度为C,那么链表的总长度为L+C;
假设相遇地点在离入口节点长度为X的地方相遇(这里假设指针是顺时针移动),那么有以下公式:
2
∗
(
L
+
(
C
−
X
)
)
=
L
+
n
∗
C
+
(
C
−
X
)
2*(L+(C-X)) = L + n*C + (C-X)
2∗(L+(C−X))=L+n∗C+(C−X)
这个怎么理解呢,就是快指针走过的路程是慢指针的两倍,这里n表示走了n圈环的长度。
展开得:
L
−
X
+
C
=
n
∗
C
L-X +C= n*C
L−X+C=n∗C
L
=
X
+
(
n
−
1
)
∗
C
L= X + (n-1)*C
L=X+(n−1)∗C
这个公式代表的含义就是只要头指针(此时可以令快指针指向头结点)和慢指针再各自前进L步,那么它们一定在环的入口节点相遇,此时返回头指针即可。
public ListNode detectCycle(ListNode head) {
if(head==null||head.next==null||head.next.next==null) return null;
ListNode first = head.next;
ListNode second = head.next.next;
while(first!=second){
if(second==null||second.next==null) return null;
first = first.next;
second = second.next.next;
}
second = head;
while(second!=first){
second = second.next;
first = first.next;
}
return second;
}
除了可以判断链表是否有环,还有LeetCode 876,确定链表的中点。通常做法是遍历两次,一次确定链表长度,一次在链表的中间点停下:
public ListNode middleNode(ListNode head) {
int count = 1;
ListNode copyHead = head;
while(head.next!=null){
head = head.next;
count++;
}
count=count/2;
while(count>0){
copyHead = copyHead.next;
count--;
}
return copyHead;
}
但如果用快慢指针,可以一次遍历就能得到结果:
public ListNode middleNode(ListNode head) {
ListNode fast = head, slow = head;
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
}
return slow;
}
确定中点只是一种特殊情况,可以一次遍历更一般地确定倒数第K个节点:
先让快指针往前走K步,然后停下;然后慢指针和快指针往前走,直到快指针走到节点末尾。(这里就不加代码了)
总结
双指针是非常实用的技巧,不管是滑动窗口中的左右指针还是用户链表中的快慢指针。除了判断链表是否有环,还可以只用一遍扫描就能确定链表的终点、确定链表的倒数第K个节点等。