引入:双指针到底解决什么问题
我目前对“双指针”的理解是:在遍历数组、数字状态或候选区间时,同时维护两个会移动的位置,并让它们承担不同的职责。这样做的价值不只是代码里出现了两个变量,而是每次移动都能根据题目的规律排除一批不可能的情况,从而减少重复枚举。
很多直接枚举的写法会产生两层、三层甚至四层循环。双指针并不能把所有问题都变成线性复杂度,但在数据有序、结果具有单调性,或者数组需要原地读写时,它经常可以把一部分重复搜索压缩掉。
本文整理八道题:283 移动零、1089 复写零、202 快乐数、11 盛最多水的容器、611 有效三角形的个数、LCR 179 两数之和、15 三数之和、18 四数之和。范围主要集中在四种模式:
- 同向快慢指针:一个指针读,一个指针写;
- 从后向前的读写指针:避免扩展写入时覆盖还没有读取的数据;
- 快慢指针判环:把反复变化的数字状态看成一条路线;
- 排序后相向双指针:根据和的大小、面积上限或不等式关系移动左右边界。
这里的“原地”是指直接修改输入数组,不额外创建一个同等规模的新数组;“去重”是指避免同一个答案因为数组中出现重复数字而被加入多次。下面按题号逐题整理我对指针含义、移动理由和代码细节的理解。
一、283. 移动零
题目描述
给定一个数组 nums,将数组中的所有 0 移动到数组末尾,同时保持非零元素原来的相对顺序。要求直接修改输入数组,也就是原地完成,不能返回一个新的完整数组。
例如,[0, 1, 0, 3, 12] 处理后变为 [1, 3, 12, 0, 0]。
题目链接
LeetCode 283. 移动零
算法思路
这道题适合同向快慢指针。我的判断依据是:题目要求把“满足条件的元素”集中到数组前面,同时保留它们原来的顺序,并且要求原地修改。
可以把任务拆成两步:先把所有非零数按原顺序写到数组前面,再把剩余位置补成零。
- fast 是读指针,从左到右检查每一个元素;
- slow 是写指针,表示下一个非零元素应该放在哪里;
- 每读到一个非零元素,就写到 nums[slow],然后让 slow 向右移动;
- 全部扫描结束后,slow 左侧已经放好了所有非零元素,从 slow 开始到数组末尾全部填零。
这里不需要在每遇到一个零时搬动后面的整段元素。那种做法虽然容易想到,但多个零会反复搬动同一批数据,最坏情况下会达到 O(n²)。快慢指针把“寻找非零元素”和“放置非零元素”分开,每个元素只被扫描和处理有限次。
slow 不会超过 fast,因此写入动作不会覆盖右侧还没有读到的元素。当 slow 和 fast 相等时,当前非零元素相当于写回原处,也不会有问题。
Java代码
class Solution {
public void moveZeroes(int[] nums) {
int slow = 0;
for (int fast = 0; fast < nums.length; fast++) {
if (nums[fast] != 0) {
nums[slow] = nums[fast];
slow++;
}
}
while (slow < nums.length) {
nums[slow] = 0;
slow++;
}
}
}
代码说明
slow 初始为 0,表示第一个非零数应当写入的位置。fast 扫描到非零值时,nums[slow] = nums[fast] 将它写入当前应该保留的区域,然后 slow++ 为下一个非零值预留位置。
扫描结束时,区间 [0, slow) 中已经按原有顺序放入所有非零数。这里的右边界 slow 不包含在已完成区域中,所以从 slow 开始补零即可。
例如输入 [0, 1, 0, 3, 12],非零数依次是 1、3、12,它们会被写到下标 0、1、2,最后下标 3、4 写成零。
复杂度与易错点
- 时间复杂度:O(n)。fast 扫描数组一次,补零最多再扫描一次,整体仍是线性时间。
- 空间复杂度:O(1),只使用了几个额外变量。
易错点主要有以下几个:
这道题让我先认识到一种很常见的双指针职责分工:fast 负责找,slow 负责放。后面的同向数组题也可以先从这个角度判断。
二、1089. 复写零
题目描述
给定一个固定长度的数组 arr,每遇到一个零,就在它后面复制一个零,并将右侧元素依次向右移动。超出数组长度的元素会被丢弃。要求原地修改数组,数组长度保持不变。
例如,[1, 0, 2, 3, 0, 4, 5, 0] 处理后为 [1, 0, 0, 2, 3, 0, 0, 4]。
题目链接
LeetCode 1089. 复写零
算法思路
这道题和移动零的区别在于:元素不是被筛选后占一个位置,而是零会额外占用一个位置。数组仍然是固定长度,因此从前向后直接写会覆盖后面还没有读取的元素。
我把它理解成一个“虚拟扩容数组”:普通数字在虚拟数组中占一个位置,零占两个位置。实际数组并没有真的扩容,只是先按照这个规则计算每个原元素在虚拟数组中的位置。
具体做法如下:
从后向前写的好处是,写入位置位于当前读取位置的右侧,或者与当前读取位置重合,不会破坏左侧尚未读取的内容。若虚拟位置已经超出真实数组边界,只计算位置而不实际赋值。
Java代码
class Solution {
public void duplicateZeros(int[] nums) {
int zeros = 0;
for (int num : nums) {
if (num == 0) {
zeros++;
}
}
int i = nums.length – 1;
int j = nums.length + zeros – 1;
while (i >= 0) {
if (j < nums.length) {
nums[j] = nums[i];
}
if (nums[i] == 0) {
j–;
if (j < nums.length) {
nums[j] = 0;
}
}
i–;
j–;
}
}
}
代码说明
zeros 表示所有零在虚拟数组中多占出来的格数,因此虚拟数组最后一个下标是 nums.length + zeros – 1。
i 始终指向真实数组中还没有处理的原元素,j 指向这个原元素在虚拟布局中应该写入的位置。普通元素只占一格,所以写完后 j–;零占两格,所以先写一份,再额外 j– 写第二份零,最后再统一 j– 进入前一个原元素对应的区域。
边界判断只在写入前进行。比如虚拟数组的末尾可能超过真实数组末尾,这些位置虽然参与了计算,但不能访问 nums[j]。因此 if (j < nums.length) 是避免数组越界的关键。
复杂度与易错点
- 时间复杂度:O(n)。统计零一次,倒序处理一次。
- 空间复杂度:O(1)。没有创建虚拟数组,虚拟数组只是用来推导下标。
易错点包括:
和移动零放在一起看,二者都属于原地读写,但方向相反:如果写入不会增加元素数量,可以从前向后用快慢指针;如果一个元素可能扩展成多个位置,并且前写会覆盖未读数据,就更适合从后向前处理。
三、202. 快乐数
题目描述
给定一个正整数 n,不断将它替换为各位数字的平方和。如果最终得到 1,那么这个数就是快乐数;如果计算过程进入一个不包含 1 的循环,则它不是快乐数。
例如,19 → 82 → 68 → 100 → 1,所以 19 是快乐数。
题目链接
LeetCode 202. 快乐数
算法思路
这道题表面上没有数组下标,但仍然可以使用快慢指针。每个数字都可以看成一个状态,计算“各位数字平方和”就是从当前状态走向下一个状态。
如果一个状态序列最终到达 1,由于 1 的下一个状态仍然是 1,所以它会进入 1 这个长度为一的环。如果不是快乐数,序列也会进入另一个环。问题的关键就变成了:状态序列是否成环,以及相遇时的状态是不是 1。
做法是:
- slow 每次执行一次 getNext;
- fast 每次执行两次 getNext;
- 如果序列进入环,快指针最终会追上慢指针;
- 相遇后判断相遇值是否为 1。
这里将数字状态当成链表节点,getNext 就类似于链表节点的 next。快慢指针不一定只能操作数组或链表,只要对象之间存在“下一步”的关系,就可能用来判断环。
Java代码
class Solution {
public boolean isHappy(int n) {
int slow = n;
int fast = getNext(n);
while (slow != fast) {
slow = getNext(slow);
fast = getNext(getNext(fast));
}
return slow == 1;
}
private int getNext(int n) {
int sum = 0;
while (n > 0) {
int digit = n % 10;
sum += digit * digit;
n /= 10;
}
return sum;
}
}
代码说明
getNext(int n) 负责计算下一个状态。n % 10 取出当前个位数字,n /= 10 去掉个位,循环结束时就得到了所有位数字平方和。
这里将 slow 初始化为 n,将 fast 初始化为 getNext(n),相当于让快指针先走一步。这样可以直接使用普通的 while (slow != fast)。如果两个指针都初始化为 n,则需要采用 do…while 或其他方式保证至少先执行一次移动。
非快乐数 2 的变化过程会出现:2 → 4 → 16 → 37 → 58 → 89 → 145 → 42 → 20 → 4。再次遇到 4 就说明进入了循环,快慢指针会在这个环中相遇,而相遇值不是 1。
复杂度与易错点
- 时间复杂度:每次计算各位平方和需要处理数字的位数,单次是 O(log n);对于 int 范围的输入,状态很快会进入一个有限的小范围,整体可以按 O(log n) 级别理解。
- 空间复杂度:O(1)。
易错点包括:
四、11. 盛最多水的容器
题目描述
给定一个整数数组 height,其中 height[i] 表示位于下标 i 的竖线高度。选择两条不同的竖线,与横轴共同构成一个容器,求这个容器能够盛放的最大水量。
容器面积由两部分决定:两条线之间的宽度,以及两条线中较短的高度。面积公式是 min(height[left], height[right]) * (right – left)。
题目链接
LeetCode 11. 盛最多水的容器
算法思路
这是相向双指针的典型题。开始时让 left 位于最左端、right 位于最右端,此时宽度最大。每一轮先计算当前容器面积,再决定移动哪一侧。
水位受到短板限制,所以如果左边高度较小,继续保留左边而只让右边向内移动时:
- 宽度一定会变小;
- 水位仍然不会超过当前左边的短板;
- 面积不可能超过当前这组边界的面积。
因此,左边较短时,当前左边界对应的其他更窄组合都可以排除,只有换掉左边、寻找更高的左边界才有可能产生更大的面积。右边较短时同理。
这就是“移动短边”的依据,不是一个脱离题目条件的固定口诀。它依赖于面积中的短板限制和宽度单调变小这两个事实。当两边一样高时,移动任意一边都可以,代码中选择移动左边。
Java代码
class Solution {
public int maxArea(int[] height) {
int left = 0;
int right = height.length – 1;
int ret = 0;
while (left < right) {
int width = right – left;
int minHeight = Math.min(height[left], height[right]);
int area = width * minHeight;
ret = Math.max(ret, area);
if (height[left] <= height[right]) {
left++;
} else {
right–;
}
}
return ret;
}
}
代码说明
left 和 right 表示当前还没有排除的两条边。宽度是下标差 right – left,不是元素个数,所以不需要加一。
每轮先通过 Math.min 求出短板高度,再计算面积并更新 ret。更新完当前组合后,根据两边高度决定移动短边。循环条件是 left < right,因为同一个下标不能形成有宽度的容器。
例如数组 [1,8,6,2,5,4,8,3,7],最初两端形成面积 8。左边高度 1 是短板,移动左指针后,边界来到高度 8,此时与右端高度 7 形成面积 7 * 7 = 49,后续搜索不会得到更大结果。
复杂度与易错点
- 时间复杂度:O(n)。每轮至少移动一个指针,两个指针总共只向内移动有限次。
- 空间复杂度:O(1)。
易错点包括:
五、611. 有效三角形的个数
题目描述
给定一个包含非负整数的数组 nums,统计从数组中选择三个下标后,能够组成三角形的组合数量。相同数值如果来自不同下标,仍然属于不同的下标组合,需要分别计数。
三角形成立的条件是:两条较短边之和严格大于最长边。
题目链接
LeetCode 611. 有效三角形的个数
算法思路
直接使用三层循环会得到 O(n³)。排序以后,可以固定最长边,再用左右指针批量统计满足条件的组合。
先对数组升序排序。令 i 从数组末尾向前移动,把 nums[i] 作为当前固定的最长边。剩下的两条边在 [0, i – 1] 中选择,设置:
- left = 0,指向当前最小候选边;
- right = i – 1,指向当前最大的候选边。
判断 nums[left] + nums[right] > nums[i]:
- 如果成立,由于数组已经有序,left 到 right – 1 的所有元素都不小于 nums[left],它们和 nums[right] 组成的组合也都满足条件。因此可以一次增加 right – left 个答案,然后让 right–。
- 如果不成立,说明当前最小边太小。固定 left 时,即使使用最大的另一条边 nums[right] 也不够,只有让 left++ 变大才有机会满足条件。
这里的批量计数是本题最重要的地方。排序带来的单调性让一次判断可以覆盖一整段候选,而不是逐个检查。
Java代码
import java.util.Arrays;
class Solution {
public int triangleNumber(int[] nums) {
Arrays.sort(nums);
int ret = 0;
for (int i = nums.length – 1; i >= 2; i–) {
int left = 0;
int right = i – 1;
while (left < right) {
if (nums[left] + nums[right] > nums[i]) {
ret += right – left;
right–;
} else {
left++;
}
}
}
return ret;
}
}
代码说明
排序后固定的 nums[i] 是最长边,所以只需要判断 nums[left] + nums[right] > nums[i]。另外两组边之和一定不会更小,因为 nums[i] 已经是三者中的最大值。
条件成立时,为什么增加 right – left 而不是 right – left + 1?当前 right 已经被作为第二条边使用,左边只能从 left 到 right – 1 选择,共有 right – left 个下标。之后 right–,继续处理下一条可能的第二长边。
条件不成立时,当前 left 和最大的 right 都无法组成三角形,因此保留这个 left 没有必要,直接 left++。
例如排序后的 [2, 2, 3, 4]:固定最长边 4 时,3 与两个 2 都能组成三角形,一次计入两个组合;固定最长边 3 时,两个 2 又组成一个组合,总数为 3。这里相同的边长来自不同下标,因此不能像三数之和那样按数值去重。
复杂度与易错点
- 时间复杂度:排序为 O(n log n),外层固定最长边并进行双指针扫描的部分为 O(n²),总复杂度是 O(n²)。
- 空间复杂度:除排序实现可能使用的栈空间外,算法只使用常数个变量;Java 中 Arrays.sort(int[]) 的具体辅助空间取决于实现。
易错点包括:
六、LCR 179. 两数之和
题目描述
给定一个已经按非递减顺序排列的数组 price 和目标值 target,找出两个数,使它们的和等于 target,并返回这两个数。题目通常保证存在满足条件的答案,返回的是数值而不是下标。
题目链接
LCR 179. 查找总价格为目标值的两个商品
算法思路
这是有序数组中的标准相向双指针。left 从最小值开始,right 从最大值开始。每轮计算两端之和:
- 和等于 target,直接返回两个数;
- 和小于 target,需要让和变大,所以移动 left,尝试更大的数;
- 和大于 target,需要让和变小,所以移动 right,尝试更小的数。
移动理由依赖于数组有序。如果当前和太小,移动 right 只会让右侧数变小,结果不可能变大,因此当前 left 与这个 right 的组合以及更小的右侧组合都可以排除;同理,和太大时应排除当前 right。
如果数组无序,就不能直接套用这套方向判断。无序数组通常需要哈希表,或者先排序并同时保留原下标;本题已经有序,所以双指针可以用常数级额外空间完成。
Java代码
class Solution {
public int[] twoSum(int[] price, int target) {
int left = 0;
int right = price.length – 1;
while (left < right) {
int sum = price[left] + price[right];
if (sum == target) {
return new int[] {price[left], price[right]};
} else if (sum < target) {
left++;
} else {
right–;
}
}
return new int[0];
}
}
代码说明
left 和 right 始终指向两个不同位置,所以循环条件为 left < right。当 sum == target 时,返回包含两个价格的数组,而不是返回两个下标。
以 price = [2, 7, 11, 15]、target = 9 为例,开始时 2 + 15 = 17,和太大,右指针向左移动;接着 2 + 11 = 13 仍然太大,右指针继续移动;最后 2 + 7 = 9,返回 [2, 7]。
方法末尾的 return new int[0] 是为了覆盖“没有找到答案”的代码路径。题目如果保证答案存在,正常执行时会在循环中返回;这行不会影响核心算法。
复杂度与易错点
- 时间复杂度:O(n)。两个指针都只向内移动,不会反复回退。
- 空间复杂度:O(1),不计返回数组占用的固定空间。
易错点包括:
这道题让我更清楚地看到:相向双指针的关键不是“左右各放一个指针”,而是数组有序后,当前和的大小能够决定哪一侧不再有可能。
七、15. 三数之和
题目描述
给定一个整数数组 nums,找出所有和为 0 且不重复的三元组 [nums[i], nums[left], nums[right]]。三元组中的三个元素必须来自三个不同下标,答案中的组合顺序不重要。
例如,输入 [-1, 0, 1, 2, -1, -4],结果为 [[-1, -1, 2], [-1, 0, 1]]。
题目链接
LeetCode 15. 三数之和
算法思路
三数之和如果使用三层循环,复杂度是 O(n³)。排序后固定第一个数,再在右侧区间用左右指针寻找另外两个数,可以降到 O(n²)。
步骤如下:
- 和小于 0,left++,尝试更大的数;
- 和大于 0,right–,尝试更小的数;
- 和等于 0,加入结果,然后左右指针都移动。
“去重”在这里指的是答案值不能重复,并不是说数组中相同元素只能使用一次。比如 [-1, -1, 2] 是合法答案,因为两个 -1 来自两个不同下标。真正需要跳过的是:当某一层固定的值与上一轮相同,继续使用它只会生成已经处理过的答案。
固定层去重写成 i > 0 && nums[i] == nums[i – 1]。找到一个答案后,先让 left++ 和 right– 离开当前组合,再跳过新的左右指针与刚使用值相同的元素。
排序还有一个额外作用:如果 nums[i] > 0,那么后面的数也都大于零,三数和不可能再回到零,可以提前结束外层循环。
Java代码
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
Arrays.sort(nums);
List<List<Integer>> ret = new ArrayList<>();
for (int i = 0; i < nums.length – 2; i++) {
if (i > 0 && nums[i] == nums[i – 1]) {
continue;
}
if (nums[i] > 0) {
break;
}
int left = i + 1;
int right = nums.length – 1;
while (left < right) {
int sum = nums[i] + nums[left] + nums[right];
if (sum < 0) {
left++;
} else if (sum > 0) {
right–;
} else {
ret.add(Arrays.asList(nums[i], nums[left], nums[right]));
left++;
right–;
while (left < right && nums[left] == nums[left – 1]) {
left++;
}
while (left < right && nums[right] == nums[right + 1]) {
right–;
}
}
}
}
return ret;
}
}
代码说明
排序后,三元组会以非递减顺序出现,因此相同答案中的数字排列也统一了。外层循环只需要保证 i 右边还有两个位置,所以条件是 i < nums.length – 2。
i > 0 && nums[i] == nums[i – 1] 只跳过同一层中连续重复的固定值,不会影响使用重复数字组成合法答案。比如排序后的 [-1, -1, 0, 1, 2],第一个 -1 已经作为固定值搜索过,第二个 -1 作为固定值时会得到同样的答案,因此跳过。
在左右扫描中,和偏小时左指针右移,和偏大时右指针左移。命中答案后必须同时移动两个指针,否则下一轮还会停在同一组数上。移动后再跳过相同值,才能避免 [0, 0, 0, 0] 之类的数据产生重复三元组。
复杂度与易错点
- 时间复杂度:排序为 O(n log n),外层固定一个数、内层双指针扫描为 O(n²),总复杂度为 O(n²)。
- 空间复杂度:不计返回结果时,主要是排序可能使用的栈空间;结果列表占用的空间取决于答案数量。
易错点包括:
八、18. 四数之和
题目描述
给定一个整数数组 nums 和目标值 target,找出所有总和等于 target 且不重复的四元组 [nums[i], nums[j], nums[left], nums[right]]。四个元素必须来自四个不同下标。
例如,输入 nums = [1, 0, -1, 0, -2, 2]、target = 0,结果为 [[-2, -1, 1, 2], [-2, 0, 0, 2], [-1, 0, 0, 1]]。
题目链接
LeetCode 18. 四数之和
算法思路
四数之和可以看成三数之和再增加一层固定下标:先排序,用 i 固定第一个数,用 j 固定第二个数,再用 left 和 right 在剩余区间中进行相向扫描。
流程是:
- 和小于 target,左指针右移;
- 和大于 target,右指针左移;
- 和等于 target,记录答案,左右指针各移动一次,再跳过重复值。
这里必须使用 long 计算 sum。即使方法参数和数组元素是 int,四个接近 1_000_000_000 的数相加也可能超过 int 的最大值。需要在加法开始前转换类型:(long) nums[i] + nums[j] + nums[left] + nums[right]。如果只把最终结果赋值给 long,而加法过程仍全部是 int,溢出已经发生,后续再转换也无法恢复。
四层去重分别对应:
- 固定 i 时跳过同层重复值;
- 固定 j 时跳过同一 i 下的重复值;
- 找到答案后跳过重复的 left 值;
- 找到答案后跳过重复的 right 值。
Java代码
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
class Solution {
public List<List<Integer>> fourSum(int[] nums, int target) {
Arrays.sort(nums);
List<List<Integer>> ret = new ArrayList<>();
for (int i = 0; i < nums.length – 3; i++) {
if (i > 0 && nums[i] == nums[i – 1]) {
continue;
}
for (int j = i + 1; j < nums.length – 2; j++) {
if (j > i + 1 && nums[j] == nums[j – 1]) {
continue;
}
int left = j + 1;
int right = nums.length – 1;
while (left < right) {
long sum = (long) nums[i] + nums[j]
+ nums[left] + nums[right];
if (sum < target) {
left++;
} else if (sum > target) {
right–;
} else {
ret.add(Arrays.asList(
nums[i], nums[j], nums[left], nums[right]));
left++;
right–;
while (left < right && nums[left] == nums[left – 1]) {
left++;
}
while (left < right && nums[right] == nums[right + 1]) {
right–;
}
}
}
}
}
return ret;
}
}
代码说明
外层 i 右侧至少要剩下三个元素,所以循环条件是 i < nums.length – 3。内层 j 右侧至少要剩下两个元素,所以条件是 j < nums.length – 2。
j 的去重条件不能简单写成 j > 0。当 j == i + 1 时,它是当前固定的 i 后面第一个候选位置,即使数值与前面其他位置相同,也不能跨越当前 i 的边界去跳过。正确的判断是 j > i + 1 && nums[j] == nums[j – 1],表示只跳过同一个 i 下已经处理过的第二个数。
相向扫描部分和三数之和相同:和小就需要增大左侧数字,和大就需要减小右侧数字。命中后同时移动左右指针,再跳过重复数值。
四个下标始终满足 i < j < left < right,所以不会重复使用同一个数组位置。排序后加入结果的四个数天然是非递减顺序,答案的表示也统一了。
复杂度与易错点
- 时间复杂度:排序为 O(n log n),两层固定下标约为 O(n²),每组固定下标用双指针扫描为 O(n),总复杂度为 O(n³)。
- 空间复杂度:不计返回结果时,主要是排序可能使用的栈空间;结果列表占用的空间取决于答案数量。
易错点包括:
九、八道题放在一起看:双指针规律的归纳
1. 快慢指针不只是一种写法
283 移动零中的两个指针都从左向右,但一个负责读取、一个负责写入;202 快乐数中的两个指针也都沿着同一条状态路线前进,但一个走一步、一个走两步;1089 复写零则是从右向左完成读写。
所以我现在不再只按变量名记忆“快慢指针”,而是先问两个指针各自代表什么:
- 是不是一个负责读、一个负责写?
- 是否存在会重复出现的状态?
- 写入是否会让数据变长,从而覆盖尚未读取的内容?
指针的移动方向和速度,都是由这个职责决定的。
2. 左右指针的前提是能够排除不可能情况
LCR 179 中,数组有序使得“和小增大左端、和大减小右端”成立;11 盛最多水的容器中,短板和宽度的关系使得移动长边没有机会超过当前面积;611 有效三角形的个数中,排序后的不等式让一次判断能够批量计数。
因此,看到两个边界并不意味着一定能用双指针。需要进一步确认:移动一侧后,是否真的能排除一批候选,并且不会错过答案。
3. 排序既是为了移动,也是为了去重
611、15、18 都先排序,但使用排序的方式不完全相同:
- 三角形个数利用有序关系批量增加答案;
- 三数之和利用和的大小移动左右指针,并对固定数和命中后的左右值去重;
- 四数之和在三数之和基础上多固定一层,因此多了一层 j 的去重。
排序还会改变输入数组原来的顺序,这些题的题意允许这种变化。若题目要求保留原数组顺序,就需要重新考虑是否可以排序。
4. 去重是“同一层不重复”,不是“相同数字不能使用”
在三数之和和四数之和中,相同数值来自不同下标时仍然可能组成合法答案。例如两个 0 可以同时参与一个答案。去重针对的是“同一个值组合已经被记录过”,而不是把重复元素全部删除。
- 外层固定值:与同层前一个值相同就跳过;
- 内层固定值:只在同一个外层固定值范围内跳过;
- 左右扫描命中后:跳过与刚刚使用的数值相同的指针位置。
611 则不同,因为它统计下标组合的数量,相同边长对应不同下标时需要分别计数,不能使用三数之和的去重方式。
5. 数值范围会影响指针题的正确性
18 四数之和中,指针方向本身没有问题,但如果四个 int 先相加并溢出,sum 的大小就会变错,后面的移动也会跟着错。使用 long 时,类型转换必须发生在第一次加法之前。
这提醒我,算法逻辑和 Java 类型范围需要一起检查。代码看起来符合双指针模板,并不代表在所有数值范围下都安全。
十、这一组题形成的判断顺序
现在遇到一个可能使用双指针的问题时,我会按下面的顺序判断:
这八道题放在一起后,双指针对我们来说不再是单独的一组固定代码,而是一种利用职责、顺序、单调性和排除关系来减少重复搜索的方法。代码中的 left++、right– 或 slow++ 只有在移动理由清楚时才真正可靠。
网硕互联帮助中心



评论前必须登录!
注册