本文目录
1 简介
快慢指针法是链表算法中常用的一种方法,通过两个步长不同的移动指针,可以帮助解决一些链表中的问题。
通常,慢指针每次移动一个节点,快指针每次移动两个节点,因此,二者的路径存在一个二倍的数学关系(偶数节点情况下)。
2 链表中点
通过快慢指针,可以确定链表中间节点,可用于二分查找等算法。
// head为头部节点,head.next表示链表第一个节点。
ListNode slowNode = head, fastNode = head;
while (fastNode != null && fastNode.next != null) {
slowNode = slowNode.next;
fastNode = fastNode.next.next;
}
/*
* 运行至此,各节点情况:
* 奇数节点:fastNode为null,slowNode为中间节点;
* 偶数节点:fastNode.next为null,slowNode为中前节点(前半段最后一个节点)。
*/
3 判断有无环
(1)无环:当快指针检查到无下一节点时,说明该链表无环。
当链表中不存在环时,快指针终将优先到达链表尾部。
(2)有环:当快指针与慢指针相遇(指向同一个节点)时,说明该链表有环。
当链表中存在环时,快指针终将进入一个环中,无限循环的移动下去,而慢指针也终将进入这个环中;
由于快指针每次比慢指针多移动一个节点,因此,在环中,快指针将“追逐”慢指针,并最终会在某个节点“追上(相遇)”。
public boolean existLoop(ListNode head) {
ListNode slowNode = head, fastNode = head;
while (fastNode != null && fastNode.next != null) {
slowNode = slowNode.next;
fastNode = fastNode.next.next;
if (slowNode == fastNode) {
return true;
}
}
return false;
}
4 确定环入口
假设一个链表中存在环,如何找到环的入口位置?
由 3 判断有无环 可知,快慢指针必将在环中某个节点相遇,而相遇时,存在如下关系:
- 设 到达环入口之前的距离为 L(L ≥ 0),环长度(周长)为 P(P ≥ 2),相遇位置距离环入口长度为 S(0 ≤ S ≤ P)。
- 慢指针路径长度 = L + S(相遇时,若非在环入口相遇,慢指针必定未走满一个环)
- 快指针路径长度 = L + n * P + S(相遇时,快指针已经走满n个环)
- 由于快慢指针路径二倍关系可得 L + S = n * P,则 L = n * P - S
当前相遇位置距离环入口为 S,若从该位置移动 L 距离,将正好停在环的入口位置,数学关系如下:
- S + L = S + n * P - S = n * P
因此,当快慢指针首次相遇时,启动两个新指针p1和p2,分别从头部节点和相遇节点开始移动,且每次均移动一步,当两个指针相遇时,相遇节点即为环的入口节点,验证如下:
- p1移动 L 步之后,处于环的入口位置;
- p2移动 L 步之后,也处于环的入口位置;
- 因此,两个指针首次相遇必定在环入口位置。
public ListNode findLoopBeginNode(ListNode head) {
ListNode slowNode = head, fastNode = head;
do {
slowNode = slowNode.next;
fastNode = fastNode.next.next;
} while (slowNode != fastNode);
ListNode p1 = head, p2 = fastNode;
while (p1 != p2) {
p1 = p1.next;
p2 = p2.next;
}
return p1;
}
5 判断是否交叉
(1)方法一:
分别遍历两个链表,记录两个链表长度及末端节点;
若末端节点相同,则表示存在交叉,若末端节点不同,则不存在交叉;
存在交叉情况下,长链表指针先移动 长度差值 步数(此时剩余链表节点数相同),此后,两个链表指针一起移动,首次相遇的节点即为交叉节点。
public ListNode findCrossNode(ListNode head1, ListNode head2) {
ListNode node1 = head1, node2 = head2;
int count1 = 0, count2 = 0;
while (node1.next != null) {
node1 = node1.next;
count1++;
}
while (node2.next != null) {
node2 = node2.next;
count2++;
}
if (node1 != node2) {
return null; // 无交叉
}
node1 = head1;
node2 = head2;
while (count1 != count2) {
if (count1 > count2) {
node1 = node1.next;
count1--;
} else {
node2 = node2.next;
count2--;
}
}
while (node1 != node2) {
node1 = node1.next;
node2 = node2.next;
}
return node1; // 交叉点
}
(2)方法二:(仅提供思路,不再赘述代码)
将其中一个链表首尾相连,然后通过 3 判断有无环 方法来判断另一个链表是否存在环;
若存在环,则存在交叉点,若不存在环,则不存在交叉点;
若存在环,则通过 4 确定环入口 方法来找到交叉点位置。