pre
面试中遇到过,知道解法,但是细节不是很了解,这里重新整理一下思路,通知给出关键部分的理由和证明
问题1:判断链表是否有环
问题分析
首先考虑环出现之后链表的特征,从头开始的链表遍历无法结束,也就是不存在尾节点
这时候第一个想法,一遍遍历链表,直至出现null 或者下一个元素是之前出现过的元素,那么这样时间复杂度为O(n),空间复杂度为O(n)[这里需要缓存之前节点的访问的情况]
下一个问题是这是否能够优化到不需要缓存Node,也就是空间复杂度为O(1)?
这里提出优化算法,声明两个指针,一个指针以步长为1的幅度向后遍历,另外一个指针以步长为2的幅度向后遍历,如果步长为2的指针找到null 或者 与步长为1的指针相撞时,算法结束。算法的时间复杂度为O(N),空间复杂度为O(1)
算法证明
1°如果链表无环,那么快指针必定会访问到null
显得
2°如果链表有环,那么快指针必定会与慢指针相遇
令A到B的长度为X, B到C的长度为Y, B->C->B的长度为N
假设结论不成立,那么对于所有的x(x表示慢指针走的步数)下面的等式不能满足
(x - X) = ( 2*x - X ) (mod N) //含义为 对N同余上面的等式等价于
x = 2*x (mod N)令 x = k *N + b b ∈[ 0 , N );
k*N + b = 2*k*N + 2*b ( mod N)上式等价于
b = 2*b (mod N)当且仅到b = 0 的时候 上式成立
综上,假设不成立,所以,如果链表有环,那么快指针必定会与慢指针相遇
3°算法的时间复杂度为O(N)
通过反推2°中反证法的过程,显得在两指针相遇时满指针走了k*N步, N是大于X的最小的N的倍数
代码
public boolen hasCycle(Node head){
if(head.next == null){
return false;
}
Node fastNode = head
Node slowNode = head;
while(fastNode != null){
if(fastNode.next != null){
fastNode = fastNode.next.next;
}else{
fastNode = null;
}
slowNode = slowNode.next;
if(slowNode == fastNode){
return ture;
}
}
return false;
}
问题2:找出链表中环的入口
问题分析
入口节点有什么特征?入口节点的入度为2,所以我们可以通过一遍遍历节点,第一个被重复访问的节点也就是环的入口节点,那么这样时间复杂度为O(n),空间复杂度为O(n)
前面的解法和第一个问题解法想似,我们能不能通过第一题的算法来解决整个问题。
从问题一种可以提取这样的结论
两个指针第一次相遇的时候慢指针走了k*N步,k*N是比X大的最小的N的倍数
(没理解的同学 可以重新看一下第二题的反证法部分)我们可以得到如下的等式(令a是两指针第一次相遇时满指针在环中循环的圈数)
k*N = X + a * N + Y
X = (k-a)*N - YX + Y = (k-a)*N
X+Y = 0 (mod N )
所以如果指针从C开始走X步,那么必定会回到B处
联想到如果从A点开始走X步,那么指针也会指到B处
另外显得,如果两只指针分别从C处和A处出发,B是他们两个的第一个相遇点。
综上获得算法
1 采用一个步长为1的指针和步长为2的指针遍历数组,直至两指针出现第一个交点
2 从上一步的交点 和 起始节点开始,以步长为1的指针分别开始遍历,直至两指针出现第一个交点
3 返回第二步中得到的交点。
代码
public Node getEntranceOfCycle(Node head){
Node fastNode = head
Node slowNode = head;
while(fastNode != null){
if(fastNode.next != null){
fastNode = fastNode.next.next;
}else{
fastNode = null;
}
slowNode = slowNode.next;
if(slowNode == fastNode){
fastNode = head;
while(fastNode != slowNode){
fastNode = fastNode.next;
slowNode = slowNode.next;
}
return slowNode;
}
}
return null;
}
后记
在和同学的讨论后,我发现其实问题我想复杂了
首先是证明两指针必定能够相遇的问题,由于两个指针都会进入到环中,那么当慢指针进入环时,此时两个指针的相遇问题就可以转化成追击问题,快指针以步长为1的速度追击慢指针,所以快指针必定能够正好追上慢指针
其次是证明两指针第一次相遇时,慢指针走的步数是k的倍数,假设相遇时慢指针走了x步,那么快指针走了2*x步,这时候快指针比慢指针多走了k圈(k>=1), 所以 2x-x = k*N, 即得 x = k*N