双指针

双指针是指用两个变量在线性结构上遍历,可以对暴力解法进行优化,降低时间复杂度。

滑动窗口

双指针维护一个滑动窗口[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) % rslow指针此刻在环中的位置为(t - k) % r,并且(2*t - k) % r == (t - k) % r,也即tr的倍数,相遇点在环中的位置为(-k) % r

如何寻找环入口:

让两个指针一个从相遇点开始,一个从头结点开始,都是单步移动,当从头结点开始移动的指针走到环入口处时,从相遇点开始移动的指针走到了((-k) % r + k) % r处,化简一下是0,同样也是环入口处。

如何计算环长度:

快慢指针第一次相遇,此后,快指针继续按1次2步的速度走,慢指针按1次1步的速度走,并设置一个计数器cnt = 0,每走1次加1。当快慢指针再次相遇时,快指针刚好比慢指针多走了r步,而计数器cnt == rr为环长度。

对撞指针(反向移动)

两个指针分别从序列的首尾出发相向而行,直到相遇才停止,在此期间可以获得两指针所指元素并进行操作。

对撞指针也可以用于维护两个单向延伸的窗口(首元素-指针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]);
}