【算法之链表(一)】判断单链表中是否有环、环的长度、环的入口节点,单链表的倒数第K个节点等

题目:

给定一个单链表,只给出头指针head:

1、如何判断是否存在环?
2、如何知道环的长度?
3、如何找出环的连接点在哪里?
4、带环链表的长度是多少?

 
解法:

1、对于问题1,使用追赶的方法,设定两个指针slow、fast,从头指针开始,每次分别前进1步、2步。如存在环,则两者相遇;如不存在环,fast遇到NULL退出。

解析:

如果以其它速度前进是否可以呢? 下面我主要讨论这个问题。 假设p和q分别以速度为v1和v2前进。如果有环,设指针p和q第一次进入环时,他们相对于环中第一个节点的偏移地址分别为a和b(可以把偏移地址理解为节点个数)。如上图。 这样,可以看出,链表有环的充要条件就是某一次循环时,指针p和q的值相等,就是它们相对环中首节点的偏移量相等。 我们设环中的结点个数为n,程序循环了m次。 由此可以有下面等式成立:(mod(n)即对n取余) (a+m*v1)mod(n) = (b+m*v2) mod(n) 设等式左边mod(n)的最大整数为k1,等式右边mod(n)的最大整数为k2,则 (a+m*v1)-k1*n = (b+m*v2)-k2*n 整理以上等式: m= |((k2-k1)*n+a-b)/( v2-v1)|       ① 如果是等式①成立,就要使循环次数m为一整数。显然如果v2-v1为1,则等式成立。 这样p和q分别以速度为v1和v2且|v2-v1|为1时,按以上算法就可找出链表中是否有环。

也可以这么理解:

假定单链表的长度为n,并且该单链表是环状的,那么第i次迭代时,p指向元素i mod n,q指向2i mod n。因此当i≡2i(mod n)时,p与q相遇。而i≡2i(mod n) =>(推出) (2i - i) mod n = 0 => i mod n = 0 => 当i=n时,p与q相遇。

这里一个简单的理解是,p和q同时在操场跑步,其中q的速度是p的两倍,当他们两个同时出发时,p跑一圈到达起点,而q此时也刚 好跑完两圈到达起点。


那么当p与q起点不同呢?假定第i次迭代时p指向元素i mod n,q指向k+2i mod n,其中0<k<n。那么i≡(2i+k)(mod n) => (i+k) mod n = 0=> 当i=n-k时,p与q相遇。


2、对于问题2,记录下问题1的碰撞点p,slow、fast从该点开始,再次碰撞所走过的操作数就是环的长度s。

3、问题3:有定理:碰撞点p到连接点的距离=头指针到连接点的距离,因此,分别从碰撞点、头指针开始走,相遇的那个点就是连接点。(证明在后面附注)

4、问题3中已经求出连接点距离头指针的长度,加上问题2中求出的环的长度,二者之和就是带环单链表的长度


判断是否存在环的程序:
  1. bool IsExitsLoop(slist *head)    
  2. {    
  3.     slist *slow = head, *fast = head;    
  4.     while ( fast && fast->next )     
  5.     {    
  6.         slow = slow->next;    
  7.         fast = fast->next->next;    
  8.         if ( slow == fast ) break;    
  9.     }      
  10.     return !(fast == NULL || fast->next == NULL);    
  11. }    

寻找环连接点(入口点)的程序:
  1. slist* FindLoopPort(slist *head)    
  2. {    
  3.     slist *slow = head, *fast = head;      
  4.     while ( fast && fast->next )     
  5.     {    
  6.         slow = slow->next;    
  7.         fast = fast->next->next;    
  8.         if ( slow == fast ) break;    
  9.     }      
  10.     if (fast == NULL || fast->next == NULL)    
  11.         return NULL;    
  12.     slow = head;    
  13.     while (slow != fast)    
  14.     {    
  15.          slow = slow->next;    
  16.          fast = fast->next;    
  17.     }    
  18.     return slow;    
  19. }   

亦可以用类似与hash表的方法,即设立一个数组,将链表结点中的值做数组下标,当赋值冲突时就是环的接入点


证明题:

对于一个顺时针的环,有P,Q两点,且两点相距为N,同时,P每向前移动一步,Q就向前以东两步,已知环的周长是L,问P,Q两点相遇在哪点上?如下图所示:P点是环的入口点


假设P,Q两点相遇时,P点移动的距离是X,则有以下等式:

X-N = 2*X - L;

=>X= L-N

那么我可以从上图看到相遇点离入口点的距离为:L-(L-N)=N


综上所述:
假设单链表的总长度为L,头结点到环入口的距离为a,环入口到快慢指针相遇的结点距离为x,环的长度为r,慢指针总共走了s步,则快指针走了2s步。另外,快指针要追上慢指针的话快指针至少要在环里面转了一圈多(假设转了n圈加x的距离),得到以下关系:
    s = a + x
    2s = a + nr + x
    =>a + x = nr
    =>a = nr - x

由上式可知:若在头结点和相遇结点分别设一指针,同步(单步)前进,则最后一定相遇在环入口结点

通过上面的证明题发现数学知识在编程世界中真的很重要呀~~

也可以这么理解:

假设链表长度是L,前半部分长度为k-1,那么第一个再环里的节点是k,环的长度是 n, 那么当q=2p时, 什么时候第一次相交呢?当q指针走到第k个节点时,q指针已经在环的第 k mod n 的位置。即p和q 相差k个元素,从不同的起点开始,则相交的位置为 n-k, 则有了下面的图:

从图上可以明显看到,当p从交点的位置(n-k) ,向前遍历k个节点就到到达环的第一个几点,节点k.

算法就很简单: 一个指针从p和q 中的第一次相交的位置起(n-k),另外一个指针从链表头开始遍历,其交点就是链表中第一个在环里的交点。


总结:我们看到这里面有一个核心的地方就是第一个问题,判断有没有环的情况,采用了两个指针:快指针和慢指针,这个在处理一些链表的问题中尤其重要,比如下面的两道关于链表的题目:

第一题:已知单链表的头指针,查找到倒数第K个节点

这道题的通俗的解法就是先遍历一边链表,得到链表的长度N,然后再从头开始遍历N-K个节点即可

但是现在如果要求只遍历一遍链表的话,该怎么操作呢?

这时候就可以借助快指针和慢指针了

我们定义一个快指针P和慢指针Q,先让P指针走到K个节点位置,然后Q指针从头指针开始和P一起移动,当P移动到尾部的时候,那么此时Q节点所在的位置就是倒数第K个节点。


第二题:已知单链表的头结点,查找到链表的中间节点

这道题的通俗的解法和上面的方法一样,就是先遍历一边链表,得到链表的长度N,然后再次遍历N/2个节点即可

但是现在同样的如果要求之遍历一边链表的话,该怎么操作呢?

这时候我们同样可以借助快指针和慢指针了

我们定义一个快指针P和慢指针Q,P和Q同时从头指针出发,快指针P每次移动两步,慢指针每次移动一步,当快指针P到尾部的时候,慢指针Q所在的位置就是中间节点的位置。


通过上面的两道题目我们可以看到快慢指针的在解决单链表的相关问题上还是很有用的~~


下面在来看一下还有一个技巧就是在解决第二道题目的时候,那个求环的入口节点,这个当时真的是没有任何头绪,所以这时候就要求我们又很好的数学功底了,能够发现相关的规律,然后总结出定理,这样就可以将问题简化了。



展开阅读全文

没有更多推荐了,返回首页