跳到主要内容

双指针

双指针通过两个位置的协调移动减少重复扫描。本文负责有序相向查找、原地压缩和指针距离。

1. 面试核心​

先记住这几条,再读正文
  • 相向双指针。 有序数组可按两端和的大小排除一个端点:和偏小时增大左端,和偏大时减小右端。有序性保证被排除的组合不可能是答案。卡 · 相向移动的排除依据
  • 读写指针。 读写指针可保序压缩数组。read 扫描原数组,write 表示有效前缀长度,写入始终发生在已读取范围内。卡 · 有序数组去重
  • 移动次数。 两个指针各自最多向前 n 次时,总移动量为 O(n)。

2. 相向移动的排除依据​

升序数组求两数之和时,和偏小就移动左端。当前左值与任何更小右值相加,都达不到目标。和偏大时,对称地排除右端。

static int[] twoSumSorted(int[] a, long target) {
int left = 0, right = a.length - 1;
while (left < right) {
long sum = (long) a[left] + a[right];
if (sum == target) return new int[]{left, right};
if (sum < target) left++;
else right--;
}
return new int[]{-1, -1};
}

[1,2,4,7]、target=9 返回下标 [1,3]。left<right 保证使用两个位置。先转 long 再加,结果覆盖两个 int 的和。时间 O(n),额外空间 O(1)。

3. 同向指针与快慢指针​

读写指针分别管理输入与输出。read 逐项读取,write 指向下一个可写位置。有效结果始终保存在 [0,write)。

快慢指针以不同速度沿链移动。判环与入口推导见链表。

3.1 有序数组去重​

static int unique(int[] a) {
int write = 0;
for (int read = 0; read < a.length; read++) {
if (write == 0 || a[read] != a[write - 1]) {
a[write++] = a[read];
}
}
return write;
}

输入 [1,1,2] 返回 2,有效前缀为 [1,2]。尾部内容不属于结果。write 始终不超过 read+1,因此覆盖的是已处理位置。每项只读一次,时间 O(n),空间 O(1)。

3.2 固定间距​

删除链表倒数第 k 项时,可让快指针先走 k 步。两者再同步前进,直到快指针到末端。使用虚拟头可统一删除原头节点的路径。实现需先约定 k 的合法范围和指针初始位置。

4. 使用边界​

无序数据缺少按和排除端点的依据。先排序再搜索需 O(n log n),原下标也需额外保存。

读写压缩要求先读当前值,再覆盖已处理位置。有序去重依赖相同值相邻,无序去重需额外记录已见值。

5 · 面试题卡片​

本页题目与参考回答

5.1 相向双指针求两数之和为何依赖有序性?​

考察重点:排除候选的大小关系。

相关内容:相向移动的排除依据。

参考回答

有序性保证和偏小时,当前左值与更小右值也无解。

因此可以安全排除左值。无序输入缺少这个保证,应排序并保留下标,或使用哈希表。

5.2 原地去重返回的长度代表什么,为何不会覆盖待处理项?​

考察重点:有效前缀与读写位置关系。

相关内容:有序数组去重。

参考回答

返回值是去重后的有效前缀长度,尾部内容可以忽略;write 始终位于已经读取的范围内。

每轮先读取当前项,确定需要保留时才写入并增加 write。这样写入不会覆盖尚未处理的元素。

5.3 如何用固定间距定位链表倒数第 k 项?​

考察重点:同步移动与前驱边界。

相关内容:固定间距。

参考回答

让 fast 先从头走 k 步,再让 fast 和 slow 同步前进;fast 为空时,slow 就在倒数第 k 个节点。

两者始终保持 k 步间距,因此 fast 走到链表末尾时即可确定 slow 的位置。k 必须在 1 到链表长度之间;若要删除该节点,还需保留 slow 的前驱。

6. 参考资料​