在刷题的过程中,你可能会遇到这样一些题,这些题中的单链表的尾节点的next不是指向NULL,而是指向这个链表的某一个节点,这些题目可能会让你证明这个链表的尾节点的next不为NULL(成环),甚至可能会问你尾节点的next到底指向哪个节点(环的入口)。如果你对这些问题毫无思路的话,希望这篇文章会对你有所帮助。
环形链表的概念
如果链表中有某个节点,可以通过连续跟踪 next
指针再次到达,则链表中存在环。
这段简单的文字就是环形链表的概念,光看文字会觉得很抽象,但是我们可以结合图来看。
上面的链表都为环形链表,即链表的最后一个节点的next不为空,而是指向链表中的任意一个节点,甚至指向自己本身。这样的链表叫做环形链表。
反之就不算环形链表,如平常我们所遇见、学习的链表。
链表成环的证明方法及其原理
证明方法
我们先说结论,想要证明链表成环很简单,我们只需要运用快慢指针的方法,定义一个指针fast,一个指针slow,两个指针都从头节点开始运动,fast一次走两步而slow一次走一步,只要链表成环那么两个指针一定会相遇。
bool hasCycle(struct ListNode *head) {
struct ListNode* slow, *fast;
slow = head;
fast = head;
while(fast && fast->next)
{
fast = fast->next->next;
slow = slow->next;
if(fast == slow)
{
return true;
}
}
return false;
}
你可能会好奇循环条件为什么是fast && fast->next(fast != NULL || fast->next != NULL),其实这个循环条件是为链表不成环准备的。如果链表不成环,fast一定是最先走到末尾的,所以我们可以以fast的状态为结束条件,那么和fast->next有什么关系呢?那是因为根据链表节点数目的不同,访问链表结束时fast所在的位置是不同的。
从图中我们可以看见,当链表节点个数为奇数时,在fast访问完链表后,fast的最后位置应为链表的最后一个节点,所以我们需要将fast->next == NULL为循环的结束条件。
当链表节点个数为偶数个时,fast访问完链表时所在的位置为NULL,所以我们应将fast == NULL为结束条件。
综上所述,循环的条件应为fast && fast->next。
方法原理
如果你只是需要用来做题,那么记住上面的结论是已经足够了,但是如果去面试,只知道结论是远远不够的,我们还需要知道其中的原理。
我们先来思考两个问题:
1、当fast走两步,slow走一步时,为什么slow和fast一定会在环中相遇?会不会在环里面错过,永远遇不上?
2、为什么slow走一步,fast走两步呢?能不能fast走一次走n步(n > 2)?
我先来回答一下两个问题:
1、只要带环,fast和slow在此条件下一定会相遇。
2、当n > 2时,两者不一定会相遇。
证明原理其实就是第一个问题的答案,但第二个问题也值得我们思考。我们两个问题都来解决一下。
第一个问题其实和我们物理中追及相遇问题很像,为什么这么说呢?为了方便,我们不妨设这个链表成环,因为fast一次运动的距离要大于slow,那么fast一定会在slow前面进环,在slow进环前,fast会一直在环中运动(运动的距离与未成环部分的长度有关)。我们不管fast运动了多少,我们假设当slow进环时,fast与slow的距离(fast的运动方向上的距离)为N。
slow进环后会和fast一起一直绕着环运动,此时追及相遇就开始了。因为运动的速度不同,fast一次走两步,slow一次走一步,所以在每次运动过后,fast一定会比slow多运动一步(fast追及slow),fast和slow间的距离就会减少1,fast和slow之间的距离会随着运动次数的增大而不断缩短,直到相遇。
运动次数 | fast与slow的相对距离 |
0 | N |
1 | N-1 |
...... | ...... |
N-1 | 1 |
N | 0(相遇) |
所以第一个问题我们就解决完毕了,接下来我们来思考第二个问题。
我们不妨假设slow一次走一步而fast一次走三步,这样fast和slow间的距离在每次后都会减去二,同样的,我们假设slow进环后,fast和slow间的距离为N,我们来看一下每次运动过后,fast和slow间距离的变化。
fast和slow间的相对距离(N为奇数) | fast和slow间的相对距离(N为偶数) |
N | N |
N-2 | N-2 |
N-4 | N-4 |
N-6 | N-6 |
...... | ...... |
1 | 2 |
-1(fast超过了slow) | 0(相遇) |
我们发现了当N为偶数时,slow与fast会相遇,而当N为奇数时,经过一次追及运动后并不会相遇,我们需要再来看第二次追及运动。
我们假设环的长度为C,当fast和slow的距离为-1时意味着N = C-1。
接下来我们再次观察一下fast和slow间距离的变化。
fast和slow间的相对距离(C为奇数,N为偶数) | fast和slow间的相对距离(C为偶数,N为奇数) |
N(C-1) | N(C-1) |
N-2 | N-2 |
N-4 | N-4 |
...... | ...... |
2 | 1 |
0(相遇) | -1(fast超过slow) |
我们观察到,当C为奇数时,fast和slow在第二次追及中相遇,而当C为偶数时,fast会再一次超越slow,那么有没有必要进行第三次追及呢?答案是否定的,因为第三次追及的结果与第二次是一样的。
我们是以fast一次走三步举例,其实不管走四步、五步还是n步都是一样的,不过是把每次相减的距离发生了改变而已。所以我们就得到了答案当n > 2时,两者不一定会相遇,这与环的长度C有关。
如何寻找环的入口
寻找方法
老样子,我们先说结论:
一个指针从相遇点开始走,一个指针从链表头开始走,他们会在环的入口点相遇。
struct ListNode *detectCycle(struct ListNode *head) {
struct ListNode* slow = head, *fast = head;
//验证是否成环
while(fast && fast->next)
{
fast = fast->next->next;
slow = slow->next;
//如果相等,说明成环了
if(fast == slow)
{
struct ListNode* meet = slow;
struct ListNode* curhead = head;
//查找成环的入口节点
//如果成环的话,从相遇点开始走和从链表头开始走的指针一定会相遇(以同一速度)
while(meet != curhead)
{
meet = meet->next;
curhead = curhead->next;
}
return meet;
}
}
return NULL;
}
方法原理
我们设在相遇时,slow走过的距离为L+X,fast走过的距离为L+X+n*C(n>=1)。
因为fast的速度为slow的距离的两倍,所以在相同运动时间内,fast运动的总距离为slow运动总距离的2倍。
故:
2*(L+X)= L+X+n*C
L+X = n*C
L = n*C-X
L = (n-1)*C + C - X
因为fast和slow的运动以C为一个周期(从一个点开始走C个长度后,还是该点),所以我们可以直接将C直接省略,即最后的式子为:
L = C - X
所以我们就证明出了: 一个指针从相遇点开始走,一个指针从链表头开始走,他们会在环的入口点相遇。
结语及练手题目
以上就是我总结的环形链表一些基础题目的解题方法和相关证明。希望对你能够有所帮助,如果有错误还请指正。
经过学习后,我们可以用以下题目练手: