双指针是指用两个变量在线性结构上遍历,可以对暴力解法进行优化,降低时间复杂度。
双指针维护一个滑动窗口[l, r],r指针遍历序列一遍,l指针始终在r指针左侧且不会后退,因此复杂度为$O(n)$。
check(l, r)
检查是否是滑动窗口(l不超过r)且是否满足题目要求的滑动窗口需要满足的条件。
for (int r = 0, l = 0; r < n; r++) {
// 假设r已经右移,检查滑动窗口条件
while (check(l, r)) {
// l右移缩小窗口后的操作
l++;
}
// r右移扩大窗口后的操作
// 窗口变为[l, r]后的操作
}
两个指针在环形序列中同向移动,但速度不同,一段时间后它们会相遇。
例如判断链表是否有环,两个指针速度分别为1和2,如果能够相遇说明有环。
假设环的长度为r
,头结点到环入口长度为k
,快慢指针从头结点到相遇经过时间为t
,则fast
指针此刻在环中的位置为(2*t - k) % r
,slow
指针此刻在环中的位置为(t - k) % r
,并且(2*t - k) % r == (t - k) % r
,也即t
为r
的倍数,相遇点在环中的位置为(-k) % r
。
如何寻找环入口:
让两个指针一个从相遇点开始,一个从头结点开始,都是单步移动,当从头结点开始移动的指针走到环入口处时,从相遇点开始移动的指针走到了((-k) % r + k) % r
处,化简一下是0
,同样也是环入口处。
如何计算环长度:
快慢指针第一次相遇,此后,快指针继续按1次2步的速度走,慢指针按1次1步的速度走,并设置一个计数器cnt = 0
,每走1次加1
。当快慢指针再次相遇时,快指针刚好比慢指针多走了r
步,而计数器cnt == r
,r
为环长度。
两个指针分别从序列的首尾出发相向而行,直到相遇才停止,在此期间可以获得两指针所指元素并进行操作。
对撞指针也可以用于维护两个单向延伸的窗口(首元素-指针i、指针j-尾元素),使得窗口内元素满足一定的性质。
例如在有序序列中寻找两个数的和为定值,使用两指针分别从首尾向中间移动,从而使得两个指针指向的数字之和稳定在所求的和定值附近,复杂度为$O(n)$。
原地修改通常是序列需要删除掉一些不需要的元素,用需要保留的元素去覆盖不需要保留的元素。
例如:有序数组去重,i指针始终指向当前需要被修改的位置,j指针遍历序列寻找用来修改a[i]
的元素,找到后a[i] = a[j]
,复杂度为$O(n)$。
原地交换通常是序列的两个部分分别满足不同的性质,用两个指针分别遍历两个部分,将不满足性质的元素交换到另一部分去。
i指针和j指针分别寻找两个要交换的元素,找到后swap(a[i], a[j])
,复杂度为$O(n)$。
例如:快排划分问题,其实是对撞指针和原地交换的综合。
详见快速排序。
再如:荷兰国旗问题,其实是对撞指针、滑动窗口和原地交换的综合。
arr[l...r]
中有三种元素0、1、2若干个,要求将其排序。
int i = l, j = r, k = l;
while (k <= j) {
if (arr[k] == 0)
swap(arr[i++], arr[k++]);
else if (arr[k] == 1)
k++;
else
swap(arr[j--], arr[k]);
}