two pointers 思想
two pointers 思想(广义):利用问题本身与序列的特性,使用两个下标 i、j 对序列进行扫描(同向扫描、反向扫描),以较低的复杂度(一般是 O(n) 的复杂度)解决问题。
示例1:
给定一个递增的正整数序列和一个正整数 M,求序列中的两个不同位置的数 a 和 数 b,使得它们的和恰好为 M,输出所有满足条件的方案。
例如: 给定序列 {1, 2, 3, 4, 5, 6} 和正整数 M = 8,就存在 2 + 6 = 8 与 3 + 5 = 8 成立。
分析: 传统做法 二重循环枚举序列中的 a 和 b,判断它们的和是否为 M,如果是输出方案;如果不是,则继续枚举。代码如下:
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
if (a[i] + a[j] == M) {
printf ("%d %d\n", a[i], a[j]);
}
}
}
这种做法的时间复杂度为 O(n^2),对 n 在 10^5 规模时,是不可承受的。
来看看高复杂度的原因是什么:
-
对一个确定的 a[i] 来说,如果当前的 a[i] 满足 a[i] + a[j] > M,显然也会有 a[i] + a[j + 1] > M 成立(因为序列是递增的),因此其实不需要在对 a[j] 之后的数进行枚举。(如果无视这个性质,就会导致 j 进行大量的无效枚举,效率骤降);
-
对某个 a[i] 来说,如果找到一个 a[j],使得 a[i] + a[j] > M 恰好成立,那么对 a[i + 1] 来说,一定也有 a[i +1] + a[j] > M 成立,因此在 a[j] 之后的元素也不必再去枚举。
[上面两似乎体现了一个问题:i 和 j 的枚举似乎是互相牵制的,而这似乎可以给优化算法带来很大的空间。 事实上,本题中 two pointers 将利用有序序列的枚举特性来有效降低复杂度。]
本题的算法过程如下:
令下标 i 的初值为 0,下标 j 的初值为 n-1,即令 i、j 分别指向序列的第一个元素和最后一个元素,接下来根据 a[i] + a[j-1] 与 M 的大小来进行下面三种选择,使 i 不断向右移动、使 j 不断向左移动,直到 i >= j 成立。
- 如果满足 a[i] + a[j] == M,说明找到了其中一组方案。由于序列递增,不等式 a[i + 1] + a[j] > M 与 a[i] + a[j - 1] < M 均成立,但是 a[i + 1] + a[j - 1] 与 M 的大小未知,因此剩余的方案只可能在 [i + 1, j - 1] 区间内产生,令 i = i + 1、j = j - 1(即令 i 向右移动,j 向左移动);
- 如果满足 a[i] + a[j] > M,由于序列递增,不等式 a[i + 1] + a[j] > M 成立,但是 a[i] + a[j - 1] 与 M 的大小未知,因此剩余的方案只可能在 [i, j -1 ]区间产生,令 j = j - 1(即令 j 向左移动);
- 如果满足 a[i] + a[j] < M,由于序列递增,不等式 a[i] + a[j -1] < M 成立,但是 a[i+1] + a[j] 与 M 的大小未知,因此剩余的方案只可能在 [i + 1, j] 区间内产生,令 j = j - 1(即令 j 向左移动。
反复执行上面三个判断,直到 i >= j 成立。代码如下:
while (i < j) {
if (a[i] + a[j] ==M) {
printf ("%d %d\n", i, j);
i++;
j--;
} else if (a[i] + a[j] < M) {
i++;
} else {
j--;
}
}
算法的时间复杂度:由于 i 的初值为 0,j 的初值为 n - 1,而程序中变量 i 只有递增操作、变量 j 只有递减操作,且循环当 i >= j 时停止,因此 i 和 j 的操作次数最多为 n 次,时间复杂度为 O(n)。
(two pointers 的思想充分利用了序列递增的性质,以浅显的思想降低了复杂度。)
示例2:『序列合并问题』
假设有两个递增序列 A 和 B,要求将它们合并为一个递增的序列C。
分析:
可以设置两个下标 i 和 j,初值均为 0,表示分别指向序列 A的第一个元素和序列B的第一个元素,然后根据 A[i] 和 B[j] 的大小来决定哪一个放入序列C。
- 若 A[i] < B[j],说明 A[i] 是当前序列 A 与 序列 B 的剩余元素中最小的那个,因此把 A[i] 加入序列C 中,并让 i 加1(即让 i 右移一位);
- 若 A[i] > B[j],说明 B[j] 是当前序列 A 与 序列 B 的剩余元素中最小的那个,因此把 B[j] 加入序列 C中,并让 j 加 1(即让 j 右移一位);
- 若 A[i] == B[j],则任意选一个加入到序列 C 中,并让对应的下标加 1。
上面的分支操作直到 i、j 中的一个到达序列末端为止,然后将另一个序列的所有元素依次加入序列 C 中。代码如下:
int merge (int A[], int B[], int C[], int n, int m) {
int i = 0, j = 0, index = 0; // i 指向 A[0],j 指向 B[0]
while (i < n && j < m) {
if (A[i] <= B[j]) { //如果 A[i] <= B[j]
C[index++] = A[i++]; //将 A[i] 加入序列 C
} else { //如果 A[i] > B[j]
C[index++] = B[j++]; //将B[j] 加入序列 C
}
}
while (i < n) C[index++] = A[i++]; //将序列 A 的剩余元素加入序列 C
while (j < m) C[index++] = B[j++]; //jiang 序列 B 的剩余元素加入序列 C
return index;
}
在实际编程中,我们要能够有使用这种思想的意思。

浙公网安备 33010602011771号