带环单链表的故事
@不了解前尘往事的Reader,烦请阅读——《判断单链表是否有环的算法》
如何找带环单链表的环的入口
这里只说比较可行的算法吧。
思路一:HashSet第一个重复元素就是环的入口
按照查找单链表带环的思路二,我们用一个HashSet维护已经跑过的元素,当重复的时候,那个结点就是环的入口。这法子还算好使,不过还是老问题——空间复杂度大。
思路二:再开一个指针与当前指针相会
我们当前双指针停在交汇处,这里有一个位置。
思来想去我还是给大家画个图吧:
有两个画错的地方:就是其实l1应该由node4画到node7,再就是node6上的线应该把焦点改到node7。
大家也读过上面的文章应该也知道我的测试数据是:
node1 → node2 → node3 → node4 → node5 → node6 → node7 → node8 → node9 → node4
。
环入口在node4处。
我们按照快慢双指针的思路走一遍判断环的流程:
t = 0:prev = node1,rear = node1;
t = 1:prev = node3,rear = node2;
t = 2:prev = node5,rear = node3;
t = 3:prev = node7,rear = node4;
t = 4:prev = node9,rear = node5;
t = 5:prev = node5,rear = node6;
t = 6:prev = node7,rear = node7。
(画错了,其实应该是在node7相遇的……不过不影响推理)
显然,第一轮的慢指针必然跑不完一圈。
我们做一些定义:
初始点(node1)到环入口点(node4)为l0;
环入口点(node4)到交汇点(node7)为l1;
交汇点(node7)到环入口点(node4)为l2;
环入口点(node4)到环入口点(node4)一圈为r;
慢指针跑过的结点路程为s,快指针跑过的路程是2s且在环内(环入口点(node4)顺到环入口点(node4)为环内一圈)跑了n圈。
由于慢指针跑不完一圈,所以说:s = l0 + l1
(1)
由于快指针跑了n圈圈内,多出来的部分正好是l1,所以说:2s = l0 + l1 + nr
(2)
(2)式 -(1)式,得到:s = nr
(3)
由于一圈长度为r,也是l1 + l2,所以说:r = l1 + l2
(4)
由(4)式得:l1 = r - l2
(5)
(5)式带入(1)式得:s = l0 + l1 = l0 + r - l2
(6)
由(3)和(6)得,s = nr = l0 + r - l2
(7)
(7)移项得:l0 = (n-1)r + l2
(8)
(8)式即为所求。
(8)式告诉我们什么呢?
排一个新的指针new从node1出发,另外从prev和rear中抽一个指针出来与new同时出发,每次都移动一个结点,最终必定在环入口点(图中node4)处相遇,相遇的一瞬间,那个点就是环的入口。
这就是完整的算法推导过程。其实网上有很多写的乱七八糟的,看不懂怎么推的。我就自己推了一下,结合着实例给大家讲解到这里,还望满意。
Java编程实现
首先对之前的isCircular()进行了修改,把返回值从boolean改成了Node,便于调用。
private Node<T> isCircular() {
Node<T> prev = first;
Node<T> rear = first;
while (prev.next != null) {
prev = prev.next;
if (prev.next == null) {
return null;
}
prev = prev.next;
rear = rear.next;
if (prev == rear) {
return prev;
}
}
return null;
}
接下来是新的方法getEntry()了:
private Node<T> getEntry() {
Node<T> prev = isCircular();
if (prev == null) {
return null;
}
Node<T> rear = first;
while (prev != rear) {
prev = prev.next;
rear = rear.next;
}
return prev;
}
完整代码(Java语言描述)
public class Main {
private static class Node<T> {
T element;
Node<T> next;
Node(T element) {
this.element = element;
}
}
private static class LinkedList<T> {
Node<T> first;
private Node<T> isCircular() {
Node<T> prev = first;
Node<T> rear = first;
while (prev.next != null) {
prev = prev.next;
if (prev.next == null) {
return null;
}
prev = prev.next;
rear = rear.next;
if (prev == rear) {
return prev;
}
}
return null;
}
private Node<T> getEntry() {
Node<T> prev = isCircular();
if (prev == null) {
return null;
}
Node<T> rear = first;
while (prev != rear) {
prev = prev.next;
rear = rear.next;
}
return prev;
}
}
public static void main(String[] args) {
Node<Integer> node1 = new Node<>(1);
Node<Integer> node2 = new Node<>(2);
Node<Integer> node3 = new Node<>(3);
Node<Integer> node4 = new Node<>(4);
Node<Integer> node5 = new Node<>(5);
Node<Integer> node6 = new Node<>(6);
Node<Integer> node7 = new Node<>(7);
Node<Integer> node8 = new Node<>(8);
Node<Integer> node9 = new Node<>(9);
node1.next = node2;
node2.next = node3;
node3.next = node4;
node4.next = node5;
node5.next = node6;
node6.next = node7;
node7.next = node8;
node8.next = node9;
node9.next = node4;
LinkedList<Integer> list = new LinkedList<>();
list.first = node1;
System.out.println("链表是否有环:" + (list.isCircular() == null ? "false" : "true"));
Node<Integer> entry = list.getEntry();
System.out.println("链表环的入口是:" + (entry == null ? "不存在" : "Node"+entry.element));
}
}