算法:双指针:从 O(n²) 到 O(n) 的核心武器

双指针:从 O(n²) 到 O(n) 的核心武器

面试官让你优化一个 O(n²) 的暴力双层循环,你的第一反应是什么?

如果问题涉及"在数组/字符串中找满足条件的一对或一段",大概率答案是双指针。它是面试中把 O(n²) 压到 O(n) 最常用的武器,出现频率极高,且往往是 follow-up 的第一步:"能不能优化?"

本篇的目标:先讲清楚双指针为什么能降复杂度(而不只是"怎么移动"),然后用面试高频题验证这个理解。

一、三种双指针模式

面试中所有双指针题,按指针运动方向分三类:

  • 对撞指针(左右相向)------两个指针从数组两端出发,向中间收拢。适用于有序数组上的配对问题:两数之和、盛最多水、接雨水。
  • 同向指针(同方向移动)------两个指针同方向前进,维护一段区间 [i, j)。外层 i 每前进一步,内层 j 只增不减。适用于子数组/子串问题:连续序列和、最长无重复子串。滑动窗口是它的进阶形态。
  • 快慢指针(同向不同速)------两个指针从同一起点出发,一快一慢。主要用于链表结构:判断有环、找环入口、找中点。它的原理是追击/倍速关系,与前两类的"单调性排除"机制不同。

本篇聚焦前两类------对撞指针和同向指针。它们共享同一个核心:利用单调性批量排除不可能的候选。
#mermaid-svg-bEJ6skCbQLBxoWLt{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-bEJ6skCbQLBxoWLt .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-bEJ6skCbQLBxoWLt .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-bEJ6skCbQLBxoWLt .error-icon{fill:#552222;}#mermaid-svg-bEJ6skCbQLBxoWLt .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-bEJ6skCbQLBxoWLt .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-bEJ6skCbQLBxoWLt .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-bEJ6skCbQLBxoWLt .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-bEJ6skCbQLBxoWLt .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-bEJ6skCbQLBxoWLt .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-bEJ6skCbQLBxoWLt .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-bEJ6skCbQLBxoWLt .marker{fill:#333333;stroke:#333333;}#mermaid-svg-bEJ6skCbQLBxoWLt .marker.cross{stroke:#333333;}#mermaid-svg-bEJ6skCbQLBxoWLt svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-bEJ6skCbQLBxoWLt p{margin:0;}#mermaid-svg-bEJ6skCbQLBxoWLt .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-bEJ6skCbQLBxoWLt .cluster-label text{fill:#333;}#mermaid-svg-bEJ6skCbQLBxoWLt .cluster-label span{color:#333;}#mermaid-svg-bEJ6skCbQLBxoWLt .cluster-label span p{background-color:transparent;}#mermaid-svg-bEJ6skCbQLBxoWLt .label text,#mermaid-svg-bEJ6skCbQLBxoWLt span{fill:#333;color:#333;}#mermaid-svg-bEJ6skCbQLBxoWLt .node rect,#mermaid-svg-bEJ6skCbQLBxoWLt .node circle,#mermaid-svg-bEJ6skCbQLBxoWLt .node ellipse,#mermaid-svg-bEJ6skCbQLBxoWLt .node polygon,#mermaid-svg-bEJ6skCbQLBxoWLt .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-bEJ6skCbQLBxoWLt .rough-node .label text,#mermaid-svg-bEJ6skCbQLBxoWLt .node .label text,#mermaid-svg-bEJ6skCbQLBxoWLt .image-shape .label,#mermaid-svg-bEJ6skCbQLBxoWLt .icon-shape .label{text-anchor:middle;}#mermaid-svg-bEJ6skCbQLBxoWLt .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-bEJ6skCbQLBxoWLt .rough-node .label,#mermaid-svg-bEJ6skCbQLBxoWLt .node .label,#mermaid-svg-bEJ6skCbQLBxoWLt .image-shape .label,#mermaid-svg-bEJ6skCbQLBxoWLt .icon-shape .label{text-align:center;}#mermaid-svg-bEJ6skCbQLBxoWLt .node.clickable{cursor:pointer;}#mermaid-svg-bEJ6skCbQLBxoWLt .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-bEJ6skCbQLBxoWLt .arrowheadPath{fill:#333333;}#mermaid-svg-bEJ6skCbQLBxoWLt .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-bEJ6skCbQLBxoWLt .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-bEJ6skCbQLBxoWLt .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-bEJ6skCbQLBxoWLt .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-bEJ6skCbQLBxoWLt .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-bEJ6skCbQLBxoWLt .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-bEJ6skCbQLBxoWLt .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-bEJ6skCbQLBxoWLt .cluster text{fill:#333;}#mermaid-svg-bEJ6skCbQLBxoWLt .cluster span{color:#333;}#mermaid-svg-bEJ6skCbQLBxoWLt div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-bEJ6skCbQLBxoWLt .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-bEJ6skCbQLBxoWLt rect.text{fill:none;stroke-width:0;}#mermaid-svg-bEJ6skCbQLBxoWLt .icon-shape,#mermaid-svg-bEJ6skCbQLBxoWLt .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-bEJ6skCbQLBxoWLt .icon-shape p,#mermaid-svg-bEJ6skCbQLBxoWLt .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-bEJ6skCbQLBxoWLt .icon-shape .label rect,#mermaid-svg-bEJ6skCbQLBxoWLt .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-bEJ6skCbQLBxoWLt .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-bEJ6skCbQLBxoWLt .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-bEJ6skCbQLBxoWLt :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 双指针三大模式
双指针
对撞指针

两端向中间
同向指针

同方向前进
快慢指针

同向不同速
配对 (i, j)

有序数组 + 两数之和/盛水/接雨水
区间 [i, j)

子数组/子串/连续序列
链表结构

环检测/中点/相交

二、为什么双指针能把 O(n²) 降到 O(n)

很多人会套双指针模板做题,但说不清"为什么这样移动指针是对的"。这一节解决这个问题。

2.1 暴力的本质

拿"有序数组两数之和等于 target"举例。暴力做法:双层循环枚举所有 (i, j) 配对,检查 arr[i] + arr[j] == target

搜索空间是什么?把所有 (i, j) 画成一个 n×n 的矩阵(取上三角),每个格子是一个候选配对。暴力就是逐格遍历,O(n²)。

但这时候回想优化三问:我在重复计算什么?有什么东西可以预存?我在遍历不可能的区域吗?

答案就在第三个问题上。数组是有序的,但暴力算法完全没有利用这个信息------它对无序数组和有序数组的搜索方式一模一样。换句话说,在"有序"这个前提下,你遍历了大量不可能成为解的配对。既然如此,能不能利用有序性把这些不可能的区域一次性跳过?

2.2 对撞指针:每步排除一整行/列

数组有序带来一个关键性质:

左指针 i 指向小端,右指针 j 指向大端。

  • arr[i] + arr[j] > target:说明 j 太大了。而且 j 与任何比 i 更大的值配对只会更大。所以 j 这一整列全部排除j--
  • arr[i] + arr[j] < target:说明 i 太小了。i 与任何比 j 更小的值配对只会更小。所以 i 这一整行全部排除i++

每移动一次指针,就在搜索矩阵中划掉一整行或一整列。最多 n 步搞定。
#mermaid-svg-BKlxOYTJZpMSRBuK{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-BKlxOYTJZpMSRBuK .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-BKlxOYTJZpMSRBuK .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-BKlxOYTJZpMSRBuK .error-icon{fill:#552222;}#mermaid-svg-BKlxOYTJZpMSRBuK .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-BKlxOYTJZpMSRBuK .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-BKlxOYTJZpMSRBuK .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-BKlxOYTJZpMSRBuK .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-BKlxOYTJZpMSRBuK .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-BKlxOYTJZpMSRBuK .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-BKlxOYTJZpMSRBuK .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-BKlxOYTJZpMSRBuK .marker{fill:#333333;stroke:#333333;}#mermaid-svg-BKlxOYTJZpMSRBuK .marker.cross{stroke:#333333;}#mermaid-svg-BKlxOYTJZpMSRBuK svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-BKlxOYTJZpMSRBuK p{margin:0;}#mermaid-svg-BKlxOYTJZpMSRBuK .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-BKlxOYTJZpMSRBuK .cluster-label text{fill:#333;}#mermaid-svg-BKlxOYTJZpMSRBuK .cluster-label span{color:#333;}#mermaid-svg-BKlxOYTJZpMSRBuK .cluster-label span p{background-color:transparent;}#mermaid-svg-BKlxOYTJZpMSRBuK .label text,#mermaid-svg-BKlxOYTJZpMSRBuK span{fill:#333;color:#333;}#mermaid-svg-BKlxOYTJZpMSRBuK .node rect,#mermaid-svg-BKlxOYTJZpMSRBuK .node circle,#mermaid-svg-BKlxOYTJZpMSRBuK .node ellipse,#mermaid-svg-BKlxOYTJZpMSRBuK .node polygon,#mermaid-svg-BKlxOYTJZpMSRBuK .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-BKlxOYTJZpMSRBuK .rough-node .label text,#mermaid-svg-BKlxOYTJZpMSRBuK .node .label text,#mermaid-svg-BKlxOYTJZpMSRBuK .image-shape .label,#mermaid-svg-BKlxOYTJZpMSRBuK .icon-shape .label{text-anchor:middle;}#mermaid-svg-BKlxOYTJZpMSRBuK .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-BKlxOYTJZpMSRBuK .rough-node .label,#mermaid-svg-BKlxOYTJZpMSRBuK .node .label,#mermaid-svg-BKlxOYTJZpMSRBuK .image-shape .label,#mermaid-svg-BKlxOYTJZpMSRBuK .icon-shape .label{text-align:center;}#mermaid-svg-BKlxOYTJZpMSRBuK .node.clickable{cursor:pointer;}#mermaid-svg-BKlxOYTJZpMSRBuK .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-BKlxOYTJZpMSRBuK .arrowheadPath{fill:#333333;}#mermaid-svg-BKlxOYTJZpMSRBuK .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-BKlxOYTJZpMSRBuK .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-BKlxOYTJZpMSRBuK .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-BKlxOYTJZpMSRBuK .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-BKlxOYTJZpMSRBuK .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-BKlxOYTJZpMSRBuK .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-BKlxOYTJZpMSRBuK .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-BKlxOYTJZpMSRBuK .cluster text{fill:#333;}#mermaid-svg-BKlxOYTJZpMSRBuK .cluster span{color:#333;}#mermaid-svg-BKlxOYTJZpMSRBuK div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-BKlxOYTJZpMSRBuK .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-BKlxOYTJZpMSRBuK rect.text{fill:none;stroke-width:0;}#mermaid-svg-BKlxOYTJZpMSRBuK .icon-shape,#mermaid-svg-BKlxOYTJZpMSRBuK .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-BKlxOYTJZpMSRBuK .icon-shape p,#mermaid-svg-BKlxOYTJZpMSRBuK .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-BKlxOYTJZpMSRBuK .icon-shape .label rect,#mermaid-svg-BKlxOYTJZpMSRBuK .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-BKlxOYTJZpMSRBuK .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-BKlxOYTJZpMSRBuK .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-BKlxOYTJZpMSRBuK :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 排除过程
n×n 搜索矩阵
暴力枚举所有 (i, j) 配对

n² 个候选
arri + arrj > target

→ j 这列全部排除

j--
arri + arrj < target

→ i 这行全部排除

i++
i 不变, j--

列减少
i++, j 不变

行减少
最多 n 步

i, j 相遇 → 搜索空间清空

对撞指针在搜索矩阵中排除整列。

这就是对撞指针 O(n) 的原因:单调性保证了被跳过的那一行/列中不可能存在答案。

2.3 同向指针:j 只增不减,总移动 O(n)

再看"找和为 S 的连续正整数序列":维护区间 [i, j) 的和 sum。

  • sum < target:j 右移扩大区间
  • sum > target:i 右移缩小区间

关键观察:i 右移时,j 绝对不会回退。因为 i 增大导致区间缩短、sum 变小,j 只可能继续右移才能重新凑够 target。

这就是同向指针的经验法则:当暴力解是 O(n²) 的双层循环,且内层变量 j 伴随外层 i 只增不减时,就可以用双指针优化到 O(n)。

i 最多移动 n 次,j 也最多移动 n 次,总移动量 O(n)。
#mermaid-svg-YrsgZuBcnEn174hc{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-YrsgZuBcnEn174hc .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-YrsgZuBcnEn174hc .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-YrsgZuBcnEn174hc .error-icon{fill:#552222;}#mermaid-svg-YrsgZuBcnEn174hc .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-YrsgZuBcnEn174hc .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-YrsgZuBcnEn174hc .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-YrsgZuBcnEn174hc .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-YrsgZuBcnEn174hc .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-YrsgZuBcnEn174hc .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-YrsgZuBcnEn174hc .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-YrsgZuBcnEn174hc .marker{fill:#333333;stroke:#333333;}#mermaid-svg-YrsgZuBcnEn174hc .marker.cross{stroke:#333333;}#mermaid-svg-YrsgZuBcnEn174hc svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-YrsgZuBcnEn174hc p{margin:0;}#mermaid-svg-YrsgZuBcnEn174hc .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-YrsgZuBcnEn174hc .cluster-label text{fill:#333;}#mermaid-svg-YrsgZuBcnEn174hc .cluster-label span{color:#333;}#mermaid-svg-YrsgZuBcnEn174hc .cluster-label span p{background-color:transparent;}#mermaid-svg-YrsgZuBcnEn174hc .label text,#mermaid-svg-YrsgZuBcnEn174hc span{fill:#333;color:#333;}#mermaid-svg-YrsgZuBcnEn174hc .node rect,#mermaid-svg-YrsgZuBcnEn174hc .node circle,#mermaid-svg-YrsgZuBcnEn174hc .node ellipse,#mermaid-svg-YrsgZuBcnEn174hc .node polygon,#mermaid-svg-YrsgZuBcnEn174hc .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-YrsgZuBcnEn174hc .rough-node .label text,#mermaid-svg-YrsgZuBcnEn174hc .node .label text,#mermaid-svg-YrsgZuBcnEn174hc .image-shape .label,#mermaid-svg-YrsgZuBcnEn174hc .icon-shape .label{text-anchor:middle;}#mermaid-svg-YrsgZuBcnEn174hc .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-YrsgZuBcnEn174hc .rough-node .label,#mermaid-svg-YrsgZuBcnEn174hc .node .label,#mermaid-svg-YrsgZuBcnEn174hc .image-shape .label,#mermaid-svg-YrsgZuBcnEn174hc .icon-shape .label{text-align:center;}#mermaid-svg-YrsgZuBcnEn174hc .node.clickable{cursor:pointer;}#mermaid-svg-YrsgZuBcnEn174hc .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-YrsgZuBcnEn174hc .arrowheadPath{fill:#333333;}#mermaid-svg-YrsgZuBcnEn174hc .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-YrsgZuBcnEn174hc .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-YrsgZuBcnEn174hc .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-YrsgZuBcnEn174hc .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-YrsgZuBcnEn174hc .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-YrsgZuBcnEn174hc .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-YrsgZuBcnEn174hc .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-YrsgZuBcnEn174hc .cluster text{fill:#333;}#mermaid-svg-YrsgZuBcnEn174hc .cluster span{color:#333;}#mermaid-svg-YrsgZuBcnEn174hc div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-YrsgZuBcnEn174hc .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-YrsgZuBcnEn174hc rect.text{fill:none;stroke-width:0;}#mermaid-svg-YrsgZuBcnEn174hc .icon-shape,#mermaid-svg-YrsgZuBcnEn174hc .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-YrsgZuBcnEn174hc .icon-shape p,#mermaid-svg-YrsgZuBcnEn174hc .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-YrsgZuBcnEn174hc .icon-shape .label rect,#mermaid-svg-YrsgZuBcnEn174hc .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-YrsgZuBcnEn174hc .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-YrsgZuBcnEn174hc .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-YrsgZuBcnEn174hc :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} i 前进 1 步
i 再前进
i→→
j 继续右移

→→→
数组尾部
i 前进,j 不回退
i
j

→→→
数组尾部
i→
j 从原位置继续

→→→
数组尾部
i 右移后区间和变小,j 只可能继续右移

才能重新凑够 target,绝不回退

2.4 没有单调性就不能用双指针

反例:无序数组的两数之和。移动左端?不知道和会变大还是变小。移动右端?同样不确定。没有单调性来保证"被跳过的候选一定不是答案",双指针就不成立。此时只能用哈希表 O(n)。

一句话:双指针的正确性 = 被跳过的候选一定不是最优解。这个保证来自单调性。

2.5 双指针四步思考法

每次遇到一道可能是双指针的题,按这四步自问:

步骤 自我追问 对应优化三问
1. 暴力是什么 搜索空间长什么样?哪两层循环? 建立基线
2. 单调性在哪 "如果 A 不行,那 A 的整行/列都不行"------这个性质存在吗? 问题三:在遍历不可能的区域吗?
3. 排除了什么 每移动一步,排除了哪些候选?一行?一列?j 不回退? 确认正确性
4. 何时终止 指针相遇/越过 = 搜索空间清空了吗? 确认完备性

#mermaid-svg-4KrFmNqIw0m8u6Dj{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-4KrFmNqIw0m8u6Dj .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-4KrFmNqIw0m8u6Dj .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-4KrFmNqIw0m8u6Dj .error-icon{fill:#552222;}#mermaid-svg-4KrFmNqIw0m8u6Dj .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-4KrFmNqIw0m8u6Dj .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-4KrFmNqIw0m8u6Dj .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-4KrFmNqIw0m8u6Dj .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-4KrFmNqIw0m8u6Dj .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-4KrFmNqIw0m8u6Dj .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-4KrFmNqIw0m8u6Dj .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-4KrFmNqIw0m8u6Dj .marker{fill:#333333;stroke:#333333;}#mermaid-svg-4KrFmNqIw0m8u6Dj .marker.cross{stroke:#333333;}#mermaid-svg-4KrFmNqIw0m8u6Dj svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-4KrFmNqIw0m8u6Dj p{margin:0;}#mermaid-svg-4KrFmNqIw0m8u6Dj .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-4KrFmNqIw0m8u6Dj .cluster-label text{fill:#333;}#mermaid-svg-4KrFmNqIw0m8u6Dj .cluster-label span{color:#333;}#mermaid-svg-4KrFmNqIw0m8u6Dj .cluster-label span p{background-color:transparent;}#mermaid-svg-4KrFmNqIw0m8u6Dj .label text,#mermaid-svg-4KrFmNqIw0m8u6Dj span{fill:#333;color:#333;}#mermaid-svg-4KrFmNqIw0m8u6Dj .node rect,#mermaid-svg-4KrFmNqIw0m8u6Dj .node circle,#mermaid-svg-4KrFmNqIw0m8u6Dj .node ellipse,#mermaid-svg-4KrFmNqIw0m8u6Dj .node polygon,#mermaid-svg-4KrFmNqIw0m8u6Dj .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-4KrFmNqIw0m8u6Dj .rough-node .label text,#mermaid-svg-4KrFmNqIw0m8u6Dj .node .label text,#mermaid-svg-4KrFmNqIw0m8u6Dj .image-shape .label,#mermaid-svg-4KrFmNqIw0m8u6Dj .icon-shape .label{text-anchor:middle;}#mermaid-svg-4KrFmNqIw0m8u6Dj .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-4KrFmNqIw0m8u6Dj .rough-node .label,#mermaid-svg-4KrFmNqIw0m8u6Dj .node .label,#mermaid-svg-4KrFmNqIw0m8u6Dj .image-shape .label,#mermaid-svg-4KrFmNqIw0m8u6Dj .icon-shape .label{text-align:center;}#mermaid-svg-4KrFmNqIw0m8u6Dj .node.clickable{cursor:pointer;}#mermaid-svg-4KrFmNqIw0m8u6Dj .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-4KrFmNqIw0m8u6Dj .arrowheadPath{fill:#333333;}#mermaid-svg-4KrFmNqIw0m8u6Dj .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-4KrFmNqIw0m8u6Dj .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-4KrFmNqIw0m8u6Dj .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4KrFmNqIw0m8u6Dj .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-4KrFmNqIw0m8u6Dj .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4KrFmNqIw0m8u6Dj .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-4KrFmNqIw0m8u6Dj .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-4KrFmNqIw0m8u6Dj .cluster text{fill:#333;}#mermaid-svg-4KrFmNqIw0m8u6Dj .cluster span{color:#333;}#mermaid-svg-4KrFmNqIw0m8u6Dj div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-4KrFmNqIw0m8u6Dj .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-4KrFmNqIw0m8u6Dj rect.text{fill:none;stroke-width:0;}#mermaid-svg-4KrFmNqIw0m8u6Dj .icon-shape,#mermaid-svg-4KrFmNqIw0m8u6Dj .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4KrFmNqIw0m8u6Dj .icon-shape p,#mermaid-svg-4KrFmNqIw0m8u6Dj .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-4KrFmNqIw0m8u6Dj .icon-shape .label rect,#mermaid-svg-4KrFmNqIw0m8u6Dj .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4KrFmNqIw0m8u6Dj .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-4KrFmNqIw0m8u6Dj .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-4KrFmNqIw0m8u6Dj :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 找到单调性
找不到
拿到 O(n²) 题目

  1. 暴力是什么?

搜索空间长什么样?
2. 单调性在哪?

有序?区间和递增?

移动一端后另一端不回退?
3. 排除了什么?

一行?一列?j 不回退?

确认被跳过的候选不可能是答案
换思路

哈希 / 分治 / DP
4. 何时终止?

指针相遇 = 搜索空间清空?
双指针 O(n) 完成

如果第 2 步找不到单调性,这题大概率不是双指针------换哈希或别的思路。

三、高频面试题

带着第二节的四步法看题。每道题我们都回答同一个问题:单调性在哪?为什么这样移动指针是对的?

3.1 盛最多水的容器(对撞指针)

题意 :给你 n 条竖线,第 i 条高度为 hi。选两条线 i 和 j,它们和 x 轴围成一个容器,能装的水量 = min(h[i], h[j]) × (j - i)。求最大水量。

第一步------暴力:枚举所有 (i, j) 对,计算面积取最大值。搜索空间是 n×n 上三角矩阵,O(n²)。

第二步------单调性在哪 :i 从最左端,j 从最右端(宽度最大)。假设 h[i] < h[j](i 是短板)。如果移动 j(较高的那端):

  • 宽度 (j-i) 一定减小
  • 高度 min(h[i], h[j']) ≤ h[i](无论 hj' 多高,瓶颈仍是短板 hi
  • 面积一定不会更大

这意味着:以当前 i 为左端的所有配对(j, j-1, j-2...),最优的就是当前这个最宽的。整行排除。

第三步------排除了什么:每步移动短板那端,排除了短板端的一整行(所有以它为一端的配对)。

第四步------终止:i、j 相遇时所有配对都考察过了。总共 n-1 步,O(n)。

盛最多水的容器------移动较矮端。

启发思考的关键点:不是"移动矮的"这个结论本身,而是**"为什么移动高的一定不会更优"**------想通这一点,做法就自然浮现了。

3.2 接雨水(对撞指针 · 渐进优化链)

题意 :给定 n 个非负整数表示柱子高度图(每个柱子宽度为 1),计算下雨后能接多少水。例如 [0,1,0,2,1,0,1,3,2,1,2,1] 能接 6 个单位。

这道题是面试中"一题多解"的典型代表,面试官经常追问"还有别的方法吗?能优化吗?"。它的价值在于展示如何从暴力一步步想到最优解------每一步优化都是对上一步的自然追问。
#mermaid-svg-EYjhwhIRvtR3UCOk{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-EYjhwhIRvtR3UCOk .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-EYjhwhIRvtR3UCOk .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-EYjhwhIRvtR3UCOk .error-icon{fill:#552222;}#mermaid-svg-EYjhwhIRvtR3UCOk .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-EYjhwhIRvtR3UCOk .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-EYjhwhIRvtR3UCOk .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-EYjhwhIRvtR3UCOk .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-EYjhwhIRvtR3UCOk .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-EYjhwhIRvtR3UCOk .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-EYjhwhIRvtR3UCOk .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-EYjhwhIRvtR3UCOk .marker{fill:#333333;stroke:#333333;}#mermaid-svg-EYjhwhIRvtR3UCOk .marker.cross{stroke:#333333;}#mermaid-svg-EYjhwhIRvtR3UCOk svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-EYjhwhIRvtR3UCOk p{margin:0;}#mermaid-svg-EYjhwhIRvtR3UCOk .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-EYjhwhIRvtR3UCOk .cluster-label text{fill:#333;}#mermaid-svg-EYjhwhIRvtR3UCOk .cluster-label span{color:#333;}#mermaid-svg-EYjhwhIRvtR3UCOk .cluster-label span p{background-color:transparent;}#mermaid-svg-EYjhwhIRvtR3UCOk .label text,#mermaid-svg-EYjhwhIRvtR3UCOk span{fill:#333;color:#333;}#mermaid-svg-EYjhwhIRvtR3UCOk .node rect,#mermaid-svg-EYjhwhIRvtR3UCOk .node circle,#mermaid-svg-EYjhwhIRvtR3UCOk .node ellipse,#mermaid-svg-EYjhwhIRvtR3UCOk .node polygon,#mermaid-svg-EYjhwhIRvtR3UCOk .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-EYjhwhIRvtR3UCOk .rough-node .label text,#mermaid-svg-EYjhwhIRvtR3UCOk .node .label text,#mermaid-svg-EYjhwhIRvtR3UCOk .image-shape .label,#mermaid-svg-EYjhwhIRvtR3UCOk .icon-shape .label{text-anchor:middle;}#mermaid-svg-EYjhwhIRvtR3UCOk .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-EYjhwhIRvtR3UCOk .rough-node .label,#mermaid-svg-EYjhwhIRvtR3UCOk .node .label,#mermaid-svg-EYjhwhIRvtR3UCOk .image-shape .label,#mermaid-svg-EYjhwhIRvtR3UCOk .icon-shape .label{text-align:center;}#mermaid-svg-EYjhwhIRvtR3UCOk .node.clickable{cursor:pointer;}#mermaid-svg-EYjhwhIRvtR3UCOk .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-EYjhwhIRvtR3UCOk .arrowheadPath{fill:#333333;}#mermaid-svg-EYjhwhIRvtR3UCOk .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-EYjhwhIRvtR3UCOk .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-EYjhwhIRvtR3UCOk .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-EYjhwhIRvtR3UCOk .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-EYjhwhIRvtR3UCOk .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-EYjhwhIRvtR3UCOk .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-EYjhwhIRvtR3UCOk .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-EYjhwhIRvtR3UCOk .cluster text{fill:#333;}#mermaid-svg-EYjhwhIRvtR3UCOk .cluster span{color:#333;}#mermaid-svg-EYjhwhIRvtR3UCOk div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-EYjhwhIRvtR3UCOk .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-EYjhwhIRvtR3UCOk rect.text{fill:none;stroke-width:0;}#mermaid-svg-EYjhwhIRvtR3UCOk .icon-shape,#mermaid-svg-EYjhwhIRvtR3UCOk .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-EYjhwhIRvtR3UCOk .icon-shape p,#mermaid-svg-EYjhwhIRvtR3UCOk .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-EYjhwhIRvtR3UCOk .icon-shape .label rect,#mermaid-svg-EYjhwhIRvtR3UCOk .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-EYjhwhIRvtR3UCOk .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-EYjhwhIRvtR3UCOk .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-EYjhwhIRvtR3UCOk :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 双指针从两端逼近




i →

维护 lmax
lmax < rmax ?
结算 i

积水 = lmax - hi

i++
结算 j

积水 = rmax - hj

j--
i < j ?
所有位置结算完毕
lmax < rmax 时

左侧瓶颈确定 → 结算 i

反之结算 j

接雨水------双指针从两端逼近。

第一步:先把题意转化为公式

先别急着写代码。一个格子能接多少水?想象往里面灌水,水会从两边溢出,水位取决于两边"围墙"中较矮的那个:

复制代码
位置 i 的积水 = min(i 左侧最高柱, i 右侧最高柱) - h[i]

(值为负说明该位置本身比两边都高,积水为 0。)

有了这个公式,问题变成:怎么高效地求每个位置的"左侧最高"和"右侧最高"?

第二步:暴力------每个位置都重新扫一遍

最直接的做法:对每个位置 i,向左扫一遍找 lmax,向右扫一遍找 rmax,代入公式。n 个位置每个扫 O(n),总共 O(n²)。

第三步:发现重复------能不能预存?

观察暴力过程:计算位置 5 的 lmax 时扫描了 0, 4;计算位置 6 的 lmax 时扫描了 0, 5。后者完全包含前者,只多看了一个元素!大量重复。

自然追问:既然 lmax[i+1] 只比 lmax[i] 多考虑一个元素,能不能存下来复用?

当然可以。递推:lmax[i] = max(lmax[i-1], h[i-1]),一次正序遍历搞定;rmax[i] = max(rmax[i+1], h[i+1]),一次倒序遍历搞定。第三次遍历用公式结算。

时间 O(n),空间 O(n)。时间已经最优了。

第四步:追问------能不能把空间也优化掉?

lmax[i] 只依赖 lmax[i-1]------正序遍历时一个变量就够。但 rmax 需要倒序算,方向相反,没法同时用单变量。

关键洞察:两个指针从两端同时出发,各自维护 lmax 和 rmax。

但 i 和 j 还没相遇时,位置 i 的右侧真实最大值还不知道------能算吗?

能。推导如下:假设当前 lmax < rmax

  • 位置 i 的左侧最高 = lmax(精确值,一路维护过来的)
  • 位置 i 的右侧最高 ≥ rmax(因为 rmax 只是 j 右侧的最大值,j 到 i 之间可能还有更高的------真实值只会更大或相等)
  • 所以 min(左侧最高, 右侧最高) = min(lmax, 某个 ≥ rmax 的值) = lmax

结论:lmax < rmax 时,位置 i 的积水确定为 lmax - h[i] 不需要知道右侧精确最大值,只需要知道它"至少有 rmax 那么高",而 lmax 比它小,瓶颈在左侧。

放心结算 i,然后 i++。反之 rmax ≤ lmax 时结算 j,j--。相遇时所有位置结算完毕。时间 O(n),空间 O(1)。

回顾这条优化链:

解法 时间 空间 每一步的自然追问
暴力 O(n²) O(1) ---
前缀数组 O(n) O(n) "在重复计算什么?能存下来吗?"
双指针 O(n) O(1) "能不能连存都不存?短板侧能确定性结算吗?"

其他解法:这道题还可以用单调栈(遇到更高柱子时横向按层结算凹槽水量),时间 O(n) 空间 O(n),思路完全不同------不是"逐列纵向"算水高,而是"逐层横向"算水面积。面试被追问"还有别的方法吗"时可以提,展示思维宽度。

3.3 连续正整数和为 S(同向指针)

题意:给定正整数 target,找出所有连续正整数序列(至少两个数),使序列之和等于 target。例如 target = 15,输出 1,2,3,4,54,5,67,8

第一步------暴力:外层枚举起点 i,内层枚举终点 j,累加判断。O(n²)。

第二步------单调性在哪 :正整数序列有一个关键性质:所有元素 > 0。这意味着:

  • 区间 [i, j) 的和 sum 随 j 右移严格递增
  • i 右移时 sum 严格递减

所以对于固定的 i,满足 sum ≥ target 的 j 有一个明确的分界点------j 不需要从头试起。更关键的是:当 i 右移一步,新的起点更大,要凑够 target 需要的长度只会更短或持平,j 绝对不会回退。

第三步------排除了什么:i 前进时,j 不回退。i 的每一步排除了"从更小的 j 开始"的所有不可能区间。

第四步------终止:i 到达 target/2 以上时,连续两个数的和就超过 target 了,可以停止。

为什么是 O(n):i 和 j 各自最多移动 O(target) 次(实际上 O(√target)),总移动量线性。

启发思考的关键点:看到"连续""正整数""子数组和"这些关键词时,想到"所有元素为正 → 区间和关于端点单调 → j 不回退",双指针的正确性就有了。

3.4 三数之和(排序 + 对撞指针)

题意 :给定数组 nums,找出所有满足 nums[a] + nums[b] + nums[c] == 0 的三元组,结果不能有重复。

第一步------暴力:三层循环枚举所有三元组,O(n³)。

第二步------单调性在哪 :三个变量太复杂。先降维:固定 nums[k],问题变成在剩余数组中找两数之和等于 -nums[k]------这就是第二节的经典问题!但无序数组上对撞指针不成立,怎么办?

关键决策:排序。 排序 O(n log n) 之后数组有序,对撞指针的"排除整行/列"逻辑立刻生效。

第三步------排除了什么:外层 k 遍历 O(n),内层对 k+1, n-1 做对撞指针 O(n)。每步内层指针移动排除一整行/列,总 O(n²)。

第四步------去重:排序后相同元素紧挨,去重变得简单:

  • 外层:nums[k] == nums[k-1] 时跳过
  • 内层:找到答案后 i 跳过所有相同值,j 同理
  • 剪枝:nums[k] > 0 时直接 break(三正数不可能和为 0)

启发思考的关键点:原始问题不具备单调性时,主动排序来创造单调性------这是一个重要的思维习惯。排序的代价是 O(n log n),但如果暴力是 O(n³) 或 O(n²),排序后能降一个量级,完全值得。
#mermaid-svg-E1tMWjWbMWRrTRgW{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-E1tMWjWbMWRrTRgW .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-E1tMWjWbMWRrTRgW .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-E1tMWjWbMWRrTRgW .error-icon{fill:#552222;}#mermaid-svg-E1tMWjWbMWRrTRgW .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-E1tMWjWbMWRrTRgW .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-E1tMWjWbMWRrTRgW .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-E1tMWjWbMWRrTRgW .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-E1tMWjWbMWRrTRgW .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-E1tMWjWbMWRrTRgW .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-E1tMWjWbMWRrTRgW .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-E1tMWjWbMWRrTRgW .marker{fill:#333333;stroke:#333333;}#mermaid-svg-E1tMWjWbMWRrTRgW .marker.cross{stroke:#333333;}#mermaid-svg-E1tMWjWbMWRrTRgW svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-E1tMWjWbMWRrTRgW p{margin:0;}#mermaid-svg-E1tMWjWbMWRrTRgW .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-E1tMWjWbMWRrTRgW .cluster-label text{fill:#333;}#mermaid-svg-E1tMWjWbMWRrTRgW .cluster-label span{color:#333;}#mermaid-svg-E1tMWjWbMWRrTRgW .cluster-label span p{background-color:transparent;}#mermaid-svg-E1tMWjWbMWRrTRgW .label text,#mermaid-svg-E1tMWjWbMWRrTRgW span{fill:#333;color:#333;}#mermaid-svg-E1tMWjWbMWRrTRgW .node rect,#mermaid-svg-E1tMWjWbMWRrTRgW .node circle,#mermaid-svg-E1tMWjWbMWRrTRgW .node ellipse,#mermaid-svg-E1tMWjWbMWRrTRgW .node polygon,#mermaid-svg-E1tMWjWbMWRrTRgW .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-E1tMWjWbMWRrTRgW .rough-node .label text,#mermaid-svg-E1tMWjWbMWRrTRgW .node .label text,#mermaid-svg-E1tMWjWbMWRrTRgW .image-shape .label,#mermaid-svg-E1tMWjWbMWRrTRgW .icon-shape .label{text-anchor:middle;}#mermaid-svg-E1tMWjWbMWRrTRgW .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-E1tMWjWbMWRrTRgW .rough-node .label,#mermaid-svg-E1tMWjWbMWRrTRgW .node .label,#mermaid-svg-E1tMWjWbMWRrTRgW .image-shape .label,#mermaid-svg-E1tMWjWbMWRrTRgW .icon-shape .label{text-align:center;}#mermaid-svg-E1tMWjWbMWRrTRgW .node.clickable{cursor:pointer;}#mermaid-svg-E1tMWjWbMWRrTRgW .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-E1tMWjWbMWRrTRgW .arrowheadPath{fill:#333333;}#mermaid-svg-E1tMWjWbMWRrTRgW .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-E1tMWjWbMWRrTRgW .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-E1tMWjWbMWRrTRgW .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-E1tMWjWbMWRrTRgW .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-E1tMWjWbMWRrTRgW .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-E1tMWjWbMWRrTRgW .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-E1tMWjWbMWRrTRgW .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-E1tMWjWbMWRrTRgW .cluster text{fill:#333;}#mermaid-svg-E1tMWjWbMWRrTRgW .cluster span{color:#333;}#mermaid-svg-E1tMWjWbMWRrTRgW div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-E1tMWjWbMWRrTRgW .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-E1tMWjWbMWRrTRgW rect.text{fill:none;stroke-width:0;}#mermaid-svg-E1tMWjWbMWRrTRgW .icon-shape,#mermaid-svg-E1tMWjWbMWRrTRgW .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-E1tMWjWbMWRrTRgW .icon-shape p,#mermaid-svg-E1tMWjWbMWRrTRgW .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-E1tMWjWbMWRrTRgW .icon-shape .label rect,#mermaid-svg-E1tMWjWbMWRrTRgW .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-E1tMWjWbMWRrTRgW .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-E1tMWjWbMWRrTRgW .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-E1tMWjWbMWRrTRgW :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 三数之和 = 排序 + 降维 + 双指针




无序数组
排序 O(n log n)

创造单调性
固定 numsk

问题降为两数之和
k+1, n-1

对撞指针 O(n)
numsa+numsb

== -numsk ?
记录三元组
移动 i/j
跳过重复值

去重
k 遍历完?
输出所有不重复三元组

结尾

代码骨架速查

理解了原理之后,写代码时两种指针的结构是固定的。心里装着这个骨架,面试时只需要填入具体的判断条件。

对撞指针骨架:

python 复制代码
i = 0, j = n - 1
while i < j:
    if 满足条件: 记录答案
    if 应该收缩右端: j--
    else: i++

核心决策点只有一个:当前这一步该移动谁? 判断依据就是移动另一端一定不会更优------找到这个单调性论证,代码就写完了。

同向指针骨架:

python 复制代码
j = 0
for i in range(n):
    while j < n and 区间需要扩大:
        j++  # 扩展区间
    if 区间满足条件: 记录答案
    # i 即将前进,缩小区间------注意 j 不回退

外层 i 每前进一步,内层 j 只增不减。写代码时的自检:j 有没有可能在某次 i 前进后需要回退? 如果需要回退,说明不具备单调性,这个结构不适用。

通用心法

拿到一道"优化 O(n²)"的题,按四步思考法走:

  1. 画出暴力的搜索空间------通常是 n×n 矩阵或双层循环的所有 (i, j) 对
  2. 找单调性------有序?区间和递增?移动一端后另一端不回退?
  3. 确认每步排除了什么------被跳过的候选真的不可能是答案吗?(正确性)
  4. 确认终止时搜索空间清空------所有可能的答案都考察过了吗?(完备性)

第 2 步是核心。找到单调性,做法自然浮现;找不到,果断换思路(哈希、分治、动态规划)。

对撞 vs 同向的速查

特征 对撞指针 同向指针
指针方向 两端向中间 同方向前进
搜索空间 配对 (i, j) 区间 [i, j)
单调性体现 移动一端后,另一端整行/列排除 i 前进时 j 不回退
典型信号 有序数组 + 配对问题 连续子区间 + 元素非负

历史文章(算法解析专栏)

算法:链表题核心技巧与解题框架