LeetCode 高频题:双指针不是模板,是单调关系
LeetCode 高频题:双指针不是模板,是单调关系
一、别把双指针背成口诀
双指针是高频技巧,但很多人只会背“左右指针往中间走”“快慢指针找环”。背模板能做几道题,遇到变形就懵。双指针真正成立的前提,是问题里存在某种单调关系:移动某个指针后,答案空间能被安全排除一部分。
比如有序数组两数之和,左小右大,和太小就左移,和太大就右移。这不是玄学,是单调性保证了被排除的组合不可能成为答案。理解这个,模板才不会乱用。
二、推导链路:先找单调性
flowchart TD A[题目约束] --> B[是否有序或可排序] B --> C[移动指针会排除哪些答案] C --> D[证明排除安全] D --> E[写代码]不要一看到数组就双指针。先问:指针移动后,为什么不会漏答案?如果回答不上来,说明还没理解。
三、代码示例:有序数组两数之和
def two_sum_sorted(nums, target): l, r = 0, len(nums) - 1 while l < r: s = nums[l] + nums[r] if s == target: return [l, r] if s < target: l += 1 else: r -= 1 return [-1, -1]复杂度是 O(n),空间 O(1)。关键不在代码短,而在证明:当 s < target 时,固定 l 搭配更小的右端只会更小,所以 l 可以安全右移。这个证明比代码更重要。
四、工程边界:排序会改变原始索引
很多双指针题需要排序,但排序会改变原数组索引。如果题目要求返回原始下标,就要保存值和索引。刷题时很容易在这里翻车。工程里也一样,优化前要确认输出语义有没有被破坏。
取舍方面,排序后双指针通常是 O(n log n),哈希表可能是 O(n),但双指针空间更省,也更容易处理去重和区间问题。不要迷信某一种方法,要看题目要求。
双指针还常用于滑动窗口。滑动窗口和左右夹逼不一样,它维护的是一个区间,并通过条件收缩左边界。它也依赖单调性:右指针扩展后,某些条件只会变强或变弱。理解这一点,才能写对窗口收缩。
再举一个反例:如果数组里有负数,很多“和不超过 k”的滑动窗口就不成立,因为右指针扩展后窗口和可能变小,左指针收缩后也可能变大,单调性被破坏。这个时候还硬套模板,就会漏答案。算法学习最怕只记成功案例,不记失败条件。
写题解时建议加一段“为什么不能用别的方法”。比如这题能不能哈希,能不能二分,为什么双指针更合适。比较方法能帮助读者建立选择能力,而不是只学一份代码。
最后,双指针代码要特别注意循环条件。l < r、l <= r、移动后是否跳过重复值,都会影响边界。很多题不是思路错,而是边界错。边界检查应该进入题解,而不是留给读者自己撞墙。
去重问题也很典型。三数之和里,排序后固定一个数,再用双指针找另外两个数。找到答案后,需要跳过相同的左值和右值,否则结果重复;固定点也要跳过重复。这里的去重不是代码装饰,而是输出语义要求。
还有一类题需要“同向双指针”,比如删除有序数组重复项。慢指针维护已处理区域,快指针扫描新元素。它和左右夹逼完全不同。题解里把类型分清,读者才不会把所有双指针混成一锅粥。
分类清楚,模板才有用。
否则模板会反噬你自己本身。
五、总结
双指针不是模板,而是建立在单调关系上的搜索空间剪枝。写代码前先证明移动指针不会漏答案,才能在高频题里稳住。
