方法一:从头结点开始遍历链表,把每次访问到的结点(或其地址)存入一个集合(hashset)或字典(dictionary),如果发现某个结点已经被访问过了,就表示这个链表存在环,并且这个结点就是环的入口点。这需要O(N)空间和O(N)时间,其中N是链表中结点的数目。
/** * 判断是否有环 */ public boolean isLoop(Node head){ Set<Node> set = new HashSet<Node>(); while(head!=null){ if(set.contains(head)){ return true; }else{ set.add(head); } head=head.next; } return false; }
方法二:定义两个指针fast与slow,fast是快指针,slow是慢指针,二者的初始值都指向链表头,slow每次前进一步,fast每次前进2步,两个指针同时向前移动。快指针每走一步都要跟慢指针比较,直到当快指针等于慢指针为止,这就证明这个链表是带环的单向链表,否则,证明这个链表是不带环的(fast先行到达尾部为null,则为无环链表)。
/** * 判断是否有环 */ public boolean isLoop(Node head){ Node fast = head; Node slow = head; while(fast!=null && fast.next!=null){ fast = fast.next.next; slow = slow.next; if(fast==slow){ return true; } } return false; }
这个方法需要O(1)空间和O(N)时间,优于第一种方法。
二、环长
根据上述的分析,以下都采用快慢指针的方式。
快慢指针第一次相遇之后,快指针停止不动,慢指针继续前行,再次相遇时慢指针的步长就是环长。
/** * 获得环长 */ public int getLoopLength(Node head){ Node fast = head; Node slow = head; // 第一次相遇 while(fast!=null && fast.next!=null){ fast = fast.next.next; slow = slow.next; if(fast==slow){ break; } } int loopLength = 0; // 第二次相遇 while(slow!=null){ slow = slow.next; loopLength++; if(fast==slow){ break; } } return loopLength; }
三、子链长、环的入口
假设链表总长S,子链长s1,环长L,S=L+s1。
定义两个慢指针P1和P2(每次前行一步),P1先行L步,然后P1和P2同时移动,两个指针相遇时,P2的步长就是子链长,相遇的结点就是环的入口结点。
/** * 获得入口结点 */ public Node getLoopPort(Node head){ Node p1=head; Node p2=head; int loopLength = getLoopLength(head); // p1前行L步(L是环长) for(int i=1;i<=loopLength;i++){ p1=p1.next; } int childLength=1; // 假设子链长度包括入口结点 while(p1!=p2){ p1=p1.next; p2=p2.next; childLength++; } return p1; }
以上方法需要先取得环的长度,下面介绍一种代码更简洁的方法,但是理解起来有点绕:
1)还是快慢指针的方式,p1每次前进一步,p2每次前进2步,快慢指针第一次相遇在环内的X结点;
2)指针p2移动到链表的头结点;
3)p1和p2同时移动,p1和p2每次移动一步,p1和p2将再次相遇在环的入口。
代码实现:
/** * 获得入口结点 */ public Node getLoopPort(Node head){ Node p1=head; //起初是慢指针 Node p2=head; //起初是快指针 //第一次相遇 while(p2!=null&&p2.next!=null){ p2 = p2.next.next; p1 = p1.next; if(p1==p2){ break; } } //没有环则返回空 if(p2==null || p2.next==null){ return null; } // p2回到头结点 p2=head; int childLength=1; // 子链长 //p1和p2每次一步,再次相遇在环的入口 while(p1!=p2){ p1=p1.next; p2=p2.next; childLength++; } return p1; }
逻辑说明:
假设快慢指针的相遇点距离入口结点X步,环长L,子链长s1,链表总长S,然后两个指针每次一步向前移动,如果它们在入口点相遇,就要证明nL-X=s1(n>=1).
>快慢指针第一次相遇的时候,慢指针必定没有遍历完整个列表,如果慢指针走了s步,那么快指针走了2s步,此时快指针必定在环内循环了n圈,所以满足如下关系式:
2s=s+nL
s=nL
X+s1=nL
s1=nL-X