判断链表是否有环,判断环的入口

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 - Y

X + 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

猜你喜欢

转载自blog.csdn.net/u010953266/article/details/82691009