参考: 双指针技巧直接秒杀五道算法题
双指针是一种时间复杂度 O(n)
,空间复杂度 O(1)
的算法
快慢指针一般都初始化指向链表的头结点head
,前进时快指针fast
在前,慢指针slow
在后,巧妙解决一些链表中的问题
快指针一次前进两步,慢指针一次前进一步,当快指针到达链表尽头时,慢指针就处于链表的中间位置。
- 当链表的长度是奇数时,
slow
恰巧停在中点位置; - 如果长度是偶数,
slow
最终的位置是中间偏右
class Solution {
public:
ListNode* middleNode(ListNode* head) {
ListNode* slow = head;
ListNode* fast = head;
while (fast != nullptr && fast->next != nullptr) {
fast = fast->next->next;
slow = slow->next;
}
return slow;
}
};
例题:876-[快慢双指针]-链表的中间结点
寻找链表中点的一个重要作用是对链表进行归并排序。「还没理解」
回想数组的归并排序:求中点索引递归地把数组二分,最后合并两个有序数组。对于链表,合并两个有序链表是很简单的,难点就在于二分。
参考 1:Cycle detection
参考 2:为什么用快慢指针找链表的环,快指针和慢指针一定会相遇?
Floyd判圈算法(Floyd Cycle Detection Algorithm),又称龟兔赛跑算法(Tortoise and Hare Algorithm),是一个可以在有限状态机、迭代函数或者链表上「判断是否存在环」,以及判断环的起点与长度的算法。
有下面几个结论:
- 如果从「同一个起点」(即使这个起点不在某个环上)同时开始以「不同速度」前进的2个指针最终相遇,那么可以判定存在一个环,且可以求出2者相遇处所在的环的起点与长度。
- 只要满足快慢指针速度差为 1,即可判断是否存在环(不一定慢1快2)
- 如果存在环,那么在某个环上以「不同速度」前进的2个指针必定会在某个时刻相遇(即便不同起点)
应用:
循环检测的应用包括测试伪随机数生成器和密码哈希函数的质量、计算数论算法、检测计算机程序中的无限循环和元胞自动机中的周期性配置、链表数据结构的自动形状分析以及死锁检测用于 DBMS 中的事务管理
例题:141-[环问题-是否有环]-环形链表
经典解法使用快慢两个指针,快指针每次走两步,慢指针每次走一步。
- 如果不含环,快指针会先遇到
null
,说明链表不含环; - 如果含有环,快指针最终会超慢指针一圈,和慢指针相遇,说明链表含有环。
下面的实现,不会错过起点为环入口
class Solution {
public:
bool hasCycle(ListNode *head) {
ListNode *slow = head;
ListNode *fast = head;
while (fast && fast->next) {
fast = fast->next->next;
slow = slow->next;
if (slow == fast) return true;
}
return false;
}
};
同一地点出发,不同速度的指针也可以检测环,slow=2、fast=3
slow=1、fast=3 在141题的 OJ 中测试也行
class Solution {
public:
bool hasCycle(ListNode *head) {
ListNode *slow = head;
ListNode *fast = head;
while (fast && fast->next && fast->next->next) {
fast = fast->next->next->next;
slow = slow->next->next;
if (slow == fast) return true;
}
return false;
}
};
当然还有种空间复杂度为 O(n) 的解法,走过的放入集合
class Solution:
def findDuplicate(self, nums: List[int]) -> int:
s = set()
for n in nums:
if n not in s:
s.add(n)
else:
return True
return False
例题:142-[环问题-环入口]-环形链表
当快慢指针相遇时,让其中任一个指针指向头节点,然后让它俩以相同的速度前进,再次相遇时所在的节点位置就是环开始的位置
下面的算法实现:
- 第一步找到慢指针 slow 和快指针 fast 在环内的相遇点
- 第二步将快指针重新指向head,fast 与 slow 以每次走一步的速度往前走
最后相遇的位置就是环的起始位置
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
bool isLoop = false;
ListNode *slow = head;
ListNode *fast = head;
while (fast && fast->next) {
fast = fast->next->next;
slow = slow->next;
if (slow == fast) {
isLoop = true;
break;
}
}
if (!isLoop) return NULL;
fast = head;
while (fast != slow) {
fast = fast->next;
slow = slow->next;
}
return slow;
}
};
快指针不动,慢指针开始走动,记录走的距离,当遇到快指针的时候就得到环的长度
class Solution {
public:
bool hasCycle(ListNode *head) {
ListNode *slow = head;
ListNode *fast = head;
while (fast && fast->next) {
fast = fast->next->next;
slow = slow->next;
if (slow == fast) break;
}
int length = 1;
slow = slow->next;
while (slow != fast) {
slow = slow->next;
length++;
}
return length;
}
};
数学归纳法
首先,由于链表是个环,所以相遇的过程可以看作是快指针从后边追赶慢指针的过程。那么做如下思考:
-
快指针与慢指针之间差一步。此时继续往后走,慢指针前进一步,快指针前进两步,两者相遇。
-
快指针与慢指针之间差两步。此时唏嘘往后走,慢指针前进一步,快指针前进两步,两者之间相差一步,转化为第一种情况。
-
快指针与慢指针之间差N步。此时继续往后走,慢指针前进一步,快指针前进两步,两者之间相差 N-1步。转化为上一种情况。
因此此题得证。若存在环,快指针必然与慢指针相遇。
数学证明—线性同余等式(没看懂)
结论是可以相遇,但要满足快慢指针速度差为 1,即可判断链表存在环
我们先设置慢指针 slow 和快指针 fast ,慢指针每次走一步,快指针每次走两步,根据「Floyd 判圈算法」两个指针在有环的情况下一定会相遇,此时我们再将 finder 放置起点 0,slow 和 finder 两个指针每次同时移动一步,相遇的点就是答案。(当然也可以将 fast 重新指向 0,再与 slow 同时走)
假设环长为 L,从起点到环的入口的步数是 a,从环的入口继续走 b 步到达相遇位置,从相遇位置继续走 c 步回到环的入口,则有 L=b+c ,其中 L、a、b、c 都是正整数。根据上述定义,慢指针走了 a+b 步,快指针走了 2(a+b) 步。从另一个角度考虑,在相遇位置,快指针比慢指针多走了若干圈,因此快指针走的步数还可以表示成 a+b+kL,其中 k 表示快指针比慢指针在环上多走的圈数。联立等式,可以得到 $$ 2(a + b) = a + b + kL $$
因为 L=b+c,所以 $$ \begin{aligned} a &= -b + kL \ &= c + (1-k)L \end{aligned} $$ 从上述等式可知:
- 「查找指针」从起点出发,「慢指针」从相遇位置同时出发,每次两个指针都移动一步
- 「查找指针」走了a 步之后到达环的入口,「慢指针」在环里从相遇位置走了 c 步和 k−1 圈到环的入口
- 最终「查找指针」和「慢指针」在环的入口相遇,相遇点就是答案
使用前后双指针,前面的指针先走 n 步,后面的指针开始同速前进。这样当前面的指针走到链表末尾null
时,后面的指针所在的位置就是倒数第n
个链表节点
具体实现上,需要先建个哨兵头节点,可以防止很多异常用例。
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* hair = new ListNode(0, head);
ListNode* front = head;
ListNode* back = hair;
for (int i = 0; i < n; i++) {
front = front->next;
} // 此时 front 与 back 之间距离为 n + 2
while (front != nullptr) {
front = front->next;
back = back->next;
}
// 此时 front 为原链表后的 null,back 指向删除节点的前一个
back->next = back->next->next;
return hair->next;
}
};
例题:19-[栈-前后双指针]-删除链表的倒数第N个结点
- 对于前 k 个数字,直接保留
- 对于 k 个后面的数字,与当前写入的位置,从后往前数的第 k 个元素进行比较,不相同则保留
此方法的正确性根源为:「数据有序」
例题:26-80-[数据有序-相同元素最多保留k位]-删除数组中的重复项
左右指针在数组中实际是指两个索引值,一般初始化为left = 0, right = nums.length - 1
一般情况下要求原始数组是有序的,但是无序的数组也可应用
二分算法其实就是左右双指针
在有序数组中,left = 0 指向最小的值,right = n - 1,指向最大的值
- 如果当前值比 target 小,left++
- 如果当前值比 taget 大,right--
class Solution {
public:
vector<int> twoSum(vector<int>& numbers, int target) {
int left = 0;
int right = numbers.size() - 1;
while (left < right) {
int tmp = numbers[left] + numbers[right];
if (tmp == target) {
return {left, right};
} else if (tmp < target) {
left++;
} else if (tmp > target) {
right--;
}
}
return {};
}
};
例题:167-[左右双指针-入门]-两数之和II
- 如果数组长度为奇数,最后跳出循环时 left == right,不用交换
- 如果数组长度为偶数,最后跳出循环时 left > right,不用交换
class Solution {
public:
void reverseString(vector<char>& s) {
int left = 0;
int right = s.size() - 1;
while (left < right) {
swap(s[left], s[right]);
left++;
right--;
}
}
};
例题:344. 反转字符串
滑动窗口也是双指针
- 窗口右边的指针 right 控制元素进入窗口
- 窗口左边的指针 left 控制元素出窗口
算法步骤:
- 将所有集合排序,进行数据预处理
- 每个集合有自己的指针,并指向开头
- 循环条件为:所有的指针都在数据长度的范围内
- 在循环内部,指针指向元素的消耗,引起指针往后移动(要消耗的写在 if 里)
模版:
class Solution {
public:
int findContentChildren(vector<int>& g, vector<int>& s) {
// 尽量小的饼干,刚好满足胃口小的小孩
sort(g.begin(), g.end());
sort(s.begin(), s.end());
int ans = 0;
int ptr1 = 0, ptr2 = 0; // ptr1指向小孩,ptr2指向饼干
while (ptr1 < g.size() && ptr2 < s.size()) {
// cout << ptr1 << " " << ptr2 << endl;
if (s[ptr2] >= g[ptr1]) { // 将必须要消耗的写在 if 里,并计数
ans++;
ptr1++; // 消耗掉一个小孩,往后指一位
}
ptr2++; // 消耗掉一个饼干,或当前饼干小了,都往后指
}
return ans;
}
};
另一种情况
int i = s.length() - 1;
int j = t.length() - 1;
while (true) {
while (i >= 0) {
if (找到想要的值) {
break;
} else {
i--;
}
}
// 此时 i 指向 s 中想要的值
while (j >= 0) {
if (找到想要的值) {
break;
} else {
j--;
}
}
// 此时 j 指向 t 中想要的值
if (i >= 0 && j >= 0) { // 都大于等于0
if (s[i] != t[j]) {
return false;
}
} else { // 其中一个小于零 或 都小于零
if (i == -1 && j == -1) { // 同时到达 -1 时,返回 True
return true;
} else {
return false;
}
}
i--, j--;
}
return true; // 逻辑不会走到这里