废话不多说,今天来看看链表的一个难点,就是环的问题,看下面这个经典问题:
给定一个单向链表,链表如果有环,返回入环的第一个节点(就是形成环形的第一个Node),如果无环返回null。
遇到这样一个题,有个巧妙的方法求解(不知道谁先想出来的,不过真的是天才),就是用快慢指针的解法,具体来说,就是定义两个Node,一快一慢,都从head出发,快的每次走2步,慢的每次走1步,如果相交了,那么相交的两个Node一个不动,另一个从head开始,大家再按相同的步调走(都是一次一步),第二次相交的那个Node就是形成环形的第一个Node!
其实,要是说为什么,我相信能想到这个方法的人不太可能是凭空想象出来,而是通过简单的数学化简,再结合算法来解的,所以嘛归根结底是个不难的数学问题,想清楚过程至关重要。
我们假设,从开始到入环节点距离为L,入环点和相遇点很可能不是一个点,所以我们设入环点到相遇点距离为d,快指针经过相遇点可能会绕这个环n圈,然后在相遇点和慢指针碰上,我们设一圈的周长为r,为了后面好理解,我们定义快指针绕圈的过程是从相遇点开始计算的,这样设定后,当快慢指针相遇时,我们就能表达出快慢指针各自走的路程了:
快:L+d+n*r
慢:L+d
然后的推导可以看下图:
由于快指针是一次2步,慢指针一次1步,相遇时快指针路程=慢指针路程*2。路程的等式简单的约分后,我们得到:L = nr - d
L就是从开始走到入环节点的距离,nr-d就是从相遇点开始绕n圈扣除d的距离,这个距离扣除d那就是走到了入环节点!所以等号左边设一个指针,右边也设一个指针,大家一起走这些距离,就必定相遇在入环节点了!
这样大家应该明白了吧,看下代码吧,代码其实很简单就两个循环,主要还是理解这个过程,不然很难长久记忆的!
package class06;
/**
* @Description
* @Package: class06
* @Author: Ray
* @CreateTime: 2020/8/5 0:18
* @E-mail: 634302021@qq.com
*/
public class Test6 {
//链表定义
public static class Node {
public int value;
public Node next;
public Node(int data) {
this.value = data;
}
}
// 找到链表第一个入环节点,如果无环,返回null
public static Node getFirstLoopNode(Node head) {
if (head == null || head.next == null || head.next.next == null) {
return null;
}
// n1 慢 n2 快
Node n1 = head.next; // n1 -> slow
Node n2 = head.next.next; // n2 -> fast
while (n1 != n2) { //相交退出循环
if (n2.next == null || n2.next == null || n2.next.next == null) {
return null;
}
n2 = n2.next.next;
n1 = n1.next;
}
n2 = head; // n2 从头开始走,n1 从相交点开始走
while (n1 != n2) { //相交的地方就是入环第一个节点,退出循环并返回
n1 = n1.next;
n2 = n2.next;
}
return n1;
}
public static void main(String[] args) {
Node head1 = new Node(1);
head1.next = new Node(2);
Node node3 = new Node(3); //定义一个入环节点
head1.next.next = node3; //入环节点
head1.next.next.next = new Node(4);
head1.next.next.next.next = new Node(5);
head1.next.next.next.next.next = new Node(6);
head1.next.next.next.next.next.next = new Node(7);
head1.next.next.next.next.next.next.next = node3; //入环节点
System.out.println(getFirstLoopNode(head1).value); //3
}
}