链表是最为基本的数据结构之一,常与数组作比较。由于链表相较于数组更加优良的的增删性能,常利用于增删比较频繁场合,并且衍生出了许多特殊的链表结构——静态链表,循环链表,双向链表等等。
而链表操作具有较强的技巧性,双指针最为常见,而双指针中,又以快慢指针最为特殊。这篇文章将根据自己的理解,说明快慢指针的原理与常见应用。
链表的中间结点
题目说明
给定一个带有头结点 head
的非空单链表,返回链表的中间结点。
如果有两个中间结点,则返回第二个中间结点。
示例 1:
1 | 输入:[1,2,3,4,5] |
示例 2:
1 | 输入:[1,2,3,4,5,6] |
提示:
- 给定链表的结点数介于
1
和100
之间。
题目解答
链表的中间结点是快慢指针最为基础的一个例子, java
代码如下
1 | /** |
原理分析:
原理颇为简单,但很精妙:
- 声明两个指针,一个将之称为快指针,一个将之称为慢指针【快指针步长(一次经过的结点)相较慢指针更大,固有其名】
- 此处的快指针步长为2(一次两个结点),慢指针步长为 1 。当快指针到达链表末尾的时候,慢指针刚好到链表中间,由是一次循环便可以解决问题。
快慢指针的一般形式
由上题可见,快慢指针实质上就是两个步长不同的指针,步长大的称之为快指针,小的称之为慢指针。而解决问题的原理,在于利用两个指针步长不同所产生的差异,来进行问题的求解。
环形链表
题目描述
给定一个链表,判断链表中是否有环。环,即链表末尾并非指向 null
,而是指向链表中的任意一个结点。详情可点击超链接。
题目解答
此题依旧可以利用快慢指针的思想来解答,java
代码如下:
1 | /** |
原理分析
- 同样是快慢指针,快指针步长为 2 ,慢指针步长为 1 。
- 如果两指针相遇,便说明链表中有环。
那为快慢指针一定会相遇呢?我看了知乎的同名问题,发现有好多都用了这样一个比喻:两个同学在操场跑步,一个快,一个慢,跑的快的同学总能追的上跑得慢的同学。乍一听,好像颇有道理,但是细思,这个比喻其实是相当不合理的,跑道可以看是连续的,但是环形链表不是,一个个结点不是,它是离散的。
所以,这样的比喻并没有任何说服力,而如要真正说明,便需要严谨的数学证明,也即此问题目前最高赞回答:
- 快指针与慢指针之间差一步:此时继续往后走,慢指针前进一步,快指针前进两步,两者相遇。
- 快指针与慢指针之间差两步:此时继续往后走,慢指针前进一步,快指针前进两步,两者之间相差一步,又变为第一种情况。
- 快指针与慢指针之间差N步:此时继续往后走,慢指针前进一步,快指针前进两步,两者之间相差N-1步。
所以,根据上述并不严谨的数学归纳法,便可清晰的说明原因。
但是若再深究,步长必须一个为 1 ,一个为 2 吗? 那么为了说明这点,就需要一个较为普遍的证明:
- 一有环链表如下图所示:
参数说明:
A B
分别为链表的头结点,环的入口结点L1
为非环部分的链表长度L2
为慢指针在环入口结点时候与快指针间的距离- 链表环的周长为
C
- \(S_s\) 和 \(S_f\) 分别为快指针和慢指针所走的 “路径”(即经过的结点数)
- \(N_1\) 为正整数
\(N_2\) 为一任意实数数
证明:
当慢指针到达环的入口结点(即B点)之时,二者所经过的 “路程” 如下:
\(S_S = L1\)
\(S_f = 2\times L1 = L1 + N_1C + L2\)
则:\(L1 = N_1C + L2\)
假设慢指针与快指针在慢指针走了
i
之后相遇(快指针为慢指针的 \(N_2\) 倍),可得方程如下:\((S_s + i - L1)\ mod\ C =(S_f + N_2i -L1)\ mod\ C\)
代入可得: \(i\ mod\ C =(N_1C + N_2i + L2)\ mod\ C\)
移向化简: \([(N_2 - 1)i + L1]\ mod\ C = 0\)
又因为
NC
是C
的整数倍,可约去,所以最终等式为:\([(N_2-1)i+L1]\ mod\ C = 0\)
则说明,只要 \((N_2-1)i+L1\) 是 \(C\) 的整数倍,那么快慢指针一定能够相遇
\(N_2\) 应该如何取值呢?
- 显然,如果 \(N_2 = 1\) (即两指针步长相同),那么等式就只有在 \(L1\) 是 \(C\) 整数倍的时候才能相遇
- \((N_2-1)i+L1 = KC\) (\(K\) 为任意正整数),显然,\(N_2\) 取任意大于1的实数,均能够满足此等式
所以,有环链表中,理论上只要快指针的步长大于慢指针的步长,两指针就能相遇。
但是,快指针步长为2,慢指针步长为1之时,其时间复杂度是最低的,故一般如此使用。【关于时间复杂度最低问题,链表中,快指针虽然说是一次走两步,但是其仍旧是一个结点一个结点逐次访问的,所以步长越大,其所遍历的多余结点数就越多,当然时间复杂度也会相应提升】
环形链表 II
本题与环形链表不同的是,需要求出环形链表的入环结点。此题所采用的算法好像正规名叫 Floyd 判圈算法 ,是图论里的一种算法。
题目解答
这算法的详细描述是:
- 利用快慢指针判断是否有环
- 如果有,则将相遇的结点标记,同时设新指针指向链表头。
- 两个指针分别从相遇的结点和链表头开始移动,直至相遇,相遇后的结点便是入环结点。
java
代码如下:
1 | /** |
为了清晰起见,重新定义了一个函数。
原理分析
由上题之证明:二指针相遇之时,恰好满足 \([(N_2 - 1)i + L1]\ mod\ C = 0\) ;
又因为i
为慢指针走的步数,L1
为前无环部分的长度,二者之和刚好是 \(C\) 的整数倍。所以,可得 \(C-i = L1\),故此方法定能找到入环结点。
参考文章:
windsmoon的博客:判断单向链表是否有环及求环入口的算法数学证明
知乎问题:为什么用快慢指针找链表的环,快指针和慢指针一定会相遇?
StackOverflow 提问:Proof of detecting the start of cycle in linked list
本文题目均来源于LeetCode