漫画算法-小灰的算法之旅(18)
1. 如何判断链表有环
Q: 有一个单向链表,链表中有可能出现"环",像下图一样。那么,如何用程序来判断该链表是否为有环链表呢?
方法一
首先从头节点开始,依次遍历单链表中的每一个节点。每遍历一个新节点,就从头检查新节点之前的所有节点,用新节点和此节点之前所有节点依次做比较。如果发现新节点和之前的某个节点相同,则说明该节点被遍历过两次,链表有环;如果之前的所有节点中不存在与新节点相同的节点,就继续遍历下一个新节点,继续重复刚才的操作。
就像图中这样,当遍历链表节点7时,从头访问节点5和节点3,发现已遍历的节点中并不存在节点7,则继续往下遍历。
当第二次遍历到节点2时,从头访问曾经遍历过的节点,发现已经遍历过节点2,说明链表有环。
假设链表的节点数量为n,则该解法的时间复杂度为O(n^2)。由于并没有创建额外的存储空间,因此空间复杂度为O(1)。
方法二
首先创建一个以节点ID为key的hashSet集合,用来存储曾经遍历过的节点。然后同样从头节点开始,依次遍历单链表中的每一个节点。每遍历一个新节点,都用新节点和HashSet集合中存储的节点进行比较,如果发现HashSet中存在与之相同的节点ID,则说明链表有环,如果HashSet中不存在与新节点相同的节点ID,就把这个新节点ID存入HashSet中,之后进入下一个节点,继续重复刚才的操作。
遍历过5,3
遍历过5,3,7,2,6,8,1
当再次遍历节点2时,查找HashSet,发现节点已经存在。
由此可见,链表有环。
这个方法在流程上和方法一类似,本质的区别是使用了HashSet作为额外的缓存。假设链表的节点数量为n,则该解法的时间复杂度是O(n),由于使用了额外的存储空间,所以算法的空间复杂度同样是O(n).
方法三(快慢指针追及)
首先创建两个指针slow和fast;让它们同时指向这个链表的头节点。然后开始一个大循环,在循环体中,让slow指针每次向后移动一个节点,让fast指针每次向后移动2个节点,然后比较两个指针指向的节点是否相同。如果相同,则链表有环。如果不相同,则继续下一次循环。
第一步,slow和fast都指向节点5
第二步,slow指针指向节点3,fast指针指向节点7
第三步,slow指针指向节点7,fast指针指向节点6
第四步,slow指针指向节点2,fast指针指向节点1
第五步,slow指针指向节点6,fast指针也指向节点6,slow和fast指针重合,说明链表有环。
此方法就类似于一个追及问题:在一个环形跑道上,两个运动员从同一地点起跑,一个运动员速度快,另一个运动员速度慢。当两人跑了一段时间后,速度快的运动员必然会再次追上并超过速度慢的运动员;原因很简单,因为跑道是环形的。
假设链表的节点数量是N,则该算法的时间复杂度为O(n).除了两个指针外,没有使用任何额外的存储空间,所以空间复杂度是O(1).
代码实现
/**
* 判断是否有环
* param head 链表头节点
*/
public static boolean isCycle(Node head){
Node slow =head;
Node fast =head;
while(fast!=null && fast.next!=null){
slow=slow.next;
fast=fast.next.next;
if(slow==fast){
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));
}
2. 如何求链表环的长度
如果链表有环,如何求出环的长度?
当两个指针首次相遇,证明链表有环的时候,让两个指针从相遇点继续循环前进,并统计前进的循环次数,知道两个指针第二次相遇。此时,统计出来的前进次数就是环长。
因为指针slow每次走1步,指针fast每次走2步,两者的速度差是1步。当两个指针再次相遇时,fast比slow多走了整整1圈。因此,环长=每一次速度差x前进次数=前进次数。
3. 如何求出环形俩表的入环点
如果链表有环,如何求出如环节点?
上图是对有环链表所做的一个抽象示意图。假设从链表头节点到入环点点距离是D,从入环点到两个指针首次相遇点的距离是S1,从首次相遇点回到入环点的距离是S2.
那么,当两个指针首次相遇时,各自所走的距离是多少呢?
指针slow一次只走1步,所走的距离为D+S1
指针fast一次走2步,多走了n(n>=1)整圈,所走的距离是D+S1+n(S1+S2)
由于fast指针的速度是slow指针的2倍,因此所走的距离也是slow的2倍:故存在如下等式
2(D+S1)=D+S1+n(S1+S2)
等式转换之后:
D=(n-1)(S1+S2)+S2
也就是说,从链表头节点到入环点的距离:等于从首次相遇点绕环(n-1)圈再回到入环点的距离。
因此,当发现slow指针和fast指针相遇时,我们再额外使用一个指针ptr.指向链表头部,随后他和slow指针每次向后移动一个位置。最终,它们相遇的节点,就是入环点。