一、算法例题
有一个单向链表,链表中可能出现环,如下图所示:
那么,如何用程序来判断该链表是否为环链表呢?
方法一:
首先从头节点开始,依次遍历单链表中的每一个节点。每遍历一个新节点,就从头检查新节点之前的所有节点,用新节点和此节点之前所有节点依次比较。如果发现新节点和之前的某个节点相同,则说明该节点被遍历过两次,链表有环,如果之前的所有节点中不存在与新节点相同的节点,就继续遍历下一个新节点,继续重复刚才的操作。如图中所示,当遍历到节点7的时候,从头方位节点5和节点3,发现已遍历的节点中并不存在节点7,则继续往下遍历。
当第2次遍历到节点2时,从头访问曾经遍历过的节点,发现已经遍历过节点2,说明链表有环。
假设链表的节点数量为n,则该解法的时间复杂度为O(n*n),空间复杂度为O(1).
方法二:
首先创建一个节点Id为Key的 HashSet集合,用来存储曾经遍历过的节点。然后同样从头节点开始。依次遍历单链表中的每一个节点,每遍历一个新节点,都用新节点和集合中存储的节点进行比较,如果发现集合中存在与之相同的节点ID,则说明链表有环,如果集合中不存在与之相同的节点ID,就把这个新节点ID存入集合中,之后进入下一个节点,继续重复刚才的操作。
由此可知该方法的与方法一类似,区别就是使用了集合作为额外的缓存。
假设链表的节点数量为n,则该方法的时间复杂度为o(n)。空间复杂度为O(n).
方法三:
首先创建两个指针p1和p2,让它们同时指向这个链表的头节点,然后开始一个大循环,在循环体中,让指针1每次向后移动一个节点,让指针2每次向后移动2个节点,然后比较两个指针的节点是否相同,如果相同则可以判断出链表有环,如果不同,则继续下一次循环。
理解思路:如同一个环形跑道,速度快的人迟早会第二次追上速度慢的的人。
假设链表的节点数量为N,则该算法的时间复杂度为O(n),除两个指针外,没有额外的存储空间,所以空间复杂度是O(1).
代码实现:
public class isCycle {
/**
* 判断是否有环
* @param head
* @return
*/
public static boolean isCycle(Node head){
Node p1 = head;
Node p2 = head;
while (p2!=null &&p2.next !=null){
p1 = p1.next;
p2 = p2.next.next;
if (p1==p2){
return true;
}
}
return false;
}
private static class Node{
int data;
Node next;
Node(int data){
this.data = data;
}
}
public static void main(String[] args) throws Exception{
Node node1 = new Node(5);
Node node2 = new Node(3);
Node node3 = new Node(7);
Node node4 = new Node(2);
Node node5 = new Node(6);
node1.next=node2;
node2.next=node3;
node3.next=node4;
node4.next=node5;
node5.next=node2;
System.out.println(isCycle(node1));
}
}
结果:
二、问题扩展
1、如果链表有环,求出环的长度?
2、如果链表有环,如何求出入环节点?
当两个指针首次相遇,证明链表有环的时候,让两个指针从相遇点继续循环前进,并统计前进的循环次数,直到两个指针第二次相遇。此时,统计出来的前进次数就是环长。
因为指针1每次走一步,指针2每次走两步,两者的速度差是一步,当两个指针再次相遇时,2比1多走了一圈。因此:
环长=每一次速度差*前进次数=前进次数
假设从链表头节点到入环点的距离时D,从入环点到两个指针首次相遇的距离是S1,从首次相遇点回到入环点的距离是S2.
那么,当两个指针首次相遇时,各自所走的距离时多少呢?
指针1一次只走一步,所走的距离是D+S1.
指针2一次走2步,多走了n(n>-1)整圈,所走的距离是D+S1+N(s1+s2)
由于指针2的速度是指针1的2倍,所以所走的距离也是指针1 的2倍,因此:
2(D+S1)=D+S1+N(S1+S2)
等式经过整理得出:
D=(N-1)(S1+S2)+S2
也就是说,从链表头节点到入环点的距离,等于从首次相遇点绕环n-1圈再回到入环点的距离。
只要把其中一个指针放回到头节点位置,另一个指针保持再首次相遇点,两个指针都是每次向前走一步,那么,它们最终相遇的节点就是入环点。
头大大