本文已参与「新人创作礼」活动,一起开启掘金创作之路。
快慢指针
两个指针从同一侧开始遍历数组,将这两个指针分别定义为快指针(fast)和慢指针(slow),两个指针将以不同的策略(速度)移动,直到两个指针的值相等(或其他条件)为止,例如快指针fast是慢指针slow的两倍速度。
快慢指针主要用于解决链表中的问题:
- 判断链表是否有环:快慢指针从头节点出发,如果链表中存在环,两个指针最终会在环中相遇。
- 寻找链表的中点:快慢指针从头节点出发,每轮迭代中,快指针向前移动两个节点,慢指针向前移动一个节点,最终当快指针到达终点的时候,慢指针刚好在中间的节点。
- 求链表中环的长度:从相遇后一个指针不动,另一个指针继续前进,直到再次相遇,可以算出走了多少步再次相遇。
快慢指针的相关题目
题型1 - 判断链表是否有环
-
题目
141.环形链表
Easy:给定一个链表的头节点head,判断链表中是否有环。 -
解题思路
使用快慢指针
fast和slow,从一侧出发,fast和slow指针初始都指向head,循环链表,每次循环fast向前两步,slow向前走一步。如果没有环,则快指针fast会抵达终点,如果有环,那么快指针会追上慢指针。 -
代码
// @lc code=start
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* @param {ListNode} head
* @return {boolean}
*/
var hasCycle = function(head) {
// 定义快慢指针,初始都指向头结点head
let fast = head
let slow = head;
// 当fast.next为null时说明fast走到了终点,没有环
while (fast && fast.next){
// 慢指针每次1步
slow = slow.next;
// 快指针每次2步
fast = fast.next.next;
//快慢指针相遇,说明含有环
if (slow == fast) {
return true;
}
}
return false
};
// @lc code=end
-
复杂度分析
-
时间复杂度:
O(n),其中n是链表中的节点数。当链表中不存在环时,快指针将先于慢指针到达链表尾部,链表中每个节点至多被访问两次。
当链表中存在环时,每一轮移动后,快慢指针的距离将减小一。而初始距离为环的长度,因此至多移动 n 轮。
-
空间复杂度:
O(1),只使用了两个指针的额外空间。
-
题型2 - 寻找链表的中点
-
题目
876.链表的中间结点
Easy:给定一个头结点为head的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。 -
解题思路
使用快慢指针
fast和slow,从一侧出发,fast和slow指针初始都指向head,每次fast向前两步,slow向前走一步。当快指针fast抵达终点时,慢指针刚好在中间的节点。 -
代码
// @lc code=start
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
var middleNode = function(head) {
// 定义快慢指针, 同侧出发,初始为head
let fast = head
let slow = head
// 快指针每次2步,慢指针每次1步
// 快指针到达终点时,慢指针所在位置极为中点
// 当链表节点为奇数时,快指针会恰好走到终点
// 当链表节点为偶数时,快指针会走到终点前一个节点,
// 但此时不满足fast.next === null, 会继续执行while,让slow走到第二个中间节点
while (fast && fast.next) {
fast = fast.next.next
slow = slow.next
}
return slow
};
// @lc code=end
-
复杂度分析
- 时间复杂度:
O(n),其中n是给定链表的结点数目。 - 空间复杂度:
O(1),只需要常数空间存放slow和fast两个指针。
- 时间复杂度:
题型3 - 求链表中环的入口节点
-
题目
142. 环形链表 II
Medium:给定一个链表的头节点head,返回链表开始入环的第一个节点。 如果链表无环,则返回null。 -
解题思路
- 判断是否有环:使用快慢指针
fast和slow,从一侧出发,fast和slow指针初始都指向head,循环链表,每次循环fast向前两步,slow向前走一步。如果没有环,则快指针fast会抵达终点,如果有环,那么快指针会追上慢指针。 - 有环则寻找入环点:当快指针与慢指针首次相遇时,设置一个新指针
res,初始也指向链表头部。随后,它和慢指针slow每次移动一个位置,它们会在入环点相遇。
- 判断是否有环:使用快慢指针
-
思路解释
- 为什么快指针
fast和慢指针slow一定是在slow的第一圈相遇?
- 为什么快指针
// 假设环长L
// slow进入环的时候,假设fast距离slow的长度为n , n <= L
// 第①种情况: n = L
// 当 n = L 时,说明 fast 此时就和 slow 已经相遇
// 第②种情况:n < L
// 当 n < L 时,fast 每次走两步,slow 每次走一步 ,所以每走一次,它们的距离会缩短1;
// 也就是需要n次,fast就会与slow相遇
// 走n次 slow 走了n步,而 n < L,
// 综上所述,slow 一定走不完一圈就会被追上
- 为什么新指针`res`会和慢指针`slow`在入环点相遇?
// 设环外部分长度为a,slow指针在环内走过的长度为b,环内剩余的长度为c。
// slow走过的路程为
slow = a + b
// fast走过的路程为
fast = a + b + c + b
// fast 走过的路程是 slow 的2倍
fast === 2 * slow
// 将fast和slow代入
a + b + c + b === 2 * (a + b)
//可得
c === a
//即 环外部分长度a === 环内slow走1圈剩余的长度c
- 代码
// @lc code=start
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
var detectCycle = function(head) {
// 定义快慢指针,初始都指向头结点head
let fast = head
let slow = head
while (fast && fast.next) {
// 快指针每次走2步
fast = fast.next.next
// 慢指针每次走1步
slow = slow.next
//快慢指针相遇,说明含有环
if (slow == fast) {
// 定义一个新指针res,初始指向头节点head
let res = head
// 新指针res和慢指针slow会在入环点相遇
while (res != slow) {
res = res.next
slow = slow.next
}
return res
}
}
return null
};
// @lc code=end
-
复杂度分析
- 时间复杂度:
O(n),其中n为链表中节点的数目。在最初判断快慢指针是否相遇时,slow指针走过的距离不会超过链表的总长度;随后寻找入环点时,走过的距离也不会超过链表的总长度。因此,总的执行时间为O(n)+O(n)=O(n)。 - 空间复杂度:
O(1)。只使用了fast、slow、res三个指针。
- 时间复杂度: