双指针:从 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²) 题目
- 暴力是什么?
搜索空间长什么样?
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,5、4,5,6、7,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²)"的题,按四步思考法走:
- 画出暴力的搜索空间------通常是 n×n 矩阵或双层循环的所有 (i, j) 对
- 找单调性------有序?区间和递增?移动一端后另一端不回退?
- 确认每步排除了什么------被跳过的候选真的不可能是答案吗?(正确性)
- 确认终止时搜索空间清空------所有可能的答案都考察过了吗?(完备性)
第 2 步是核心。找到单调性,做法自然浮现;找不到,果断换思路(哈希、分治、动态规划)。
对撞 vs 同向的速查
| 特征 | 对撞指针 | 同向指针 |
|---|---|---|
| 指针方向 | 两端向中间 | 同方向前进 |
| 搜索空间 | 配对 (i, j) | 区间 [i, j) |
| 单调性体现 | 移动一端后,另一端整行/列排除 | i 前进时 j 不回退 |
| 典型信号 | 有序数组 + 配对问题 | 连续子区间 + 元素非负 |
历史文章(算法解析专栏)