跳转至

双指针:具体场景与手把手推演

覆盖:三种双指针的具体场景、算法 vs SQL 工程差异、逐步推演、代码模板与刷题清单 适用:单独搞懂双指针,不混谈滑动窗口和数组历史 前置:知道数组和下标即可;细节见 数组

← 返回 总览 | 相关 数组 | 7 天冲刺 Day 1


读前 30 秒:双指针到底是什么?

在数组、字符串或链表上放 两个下标(常叫 left/rightslow/fast),按固定规则一起移动。

它解决的一类问题:本来要两重循环试所有组合 → O(n²),利用 有序、单调、或原地写入 等性质,每走一步 排除一大批不可能 → O(n)。

暴力:试每一对、每一个起点终点  →  n²
双指针:两个指头有方向地动      →  n

本文按 三种姿势 展开,每种都配 具体业务场景 + 纸上推演 + 对应 LeetCode


一、三种双指针,一张表先分清

类型 两个指针怎么放 怎么动 典型业务 代表题
对撞指针 一左一右 向中间靠 对账、回文、盛水 LC 167, 11, 125
快慢指针 同侧,一前一后 都往右,快的多走 去重、压缩、链表环 LC 26, 283, 141
同向 / 逆向双指针 各扫一个有序序列 谁小/谁大谁先动 归并日志、MERGE JOIN LC 88, 21, 977

和滑动窗口的区别(避免混):滑动窗口强调 框住连续一段 并统计窗口内状态;双指针更宽,不一定在维护「连续区间长度」。详见 数组 § 滑动窗口


二、算法思维 vs 工程实践(数据开发为什么「日常感觉不到」)

做计费、对账、数仓的同学常问:我天天写 SQL JOIN,从没写过双指针,这玩意到底有啥用?
下面分四点拆开——这也是 面试算法线上 SQL 之间的那层窗户纸。

2.1 双指针是为了更快「找到某个元素」吗?

不完全是。

双指针的核心价值是:在 有序或单调 的结构上,用 O(n) 解决原本要 O(n²) 的嵌套循环问题,从而做 比较、匹配、去重、合并 这类结构性操作

典型场景 在干什么 是不是「查找某一个元素」
有序数组两数之和 找一对和为 target 否,是 配对
合并两个有序数组 归并成一条 否,是 合并
链表判环 快慢相遇 否,是 结构检测
原地去重 压缩有效区 否,是 原地写

所以:别把它理解成「二分那种找一个下标」,而是 两个游标协同扫一遍,省掉内层循环


2.2 为什么计费 / 数据开发里「好像没见过」?

因为日常工作在 SQL 引擎之上。SQL 是 声明式 的:你写「要什么」,优化器决定用 Hash Join、Nested Loop 还是 Sort-Merge Join

Sort-Merge Join 的底层,就是两个有序扫描指针一左一右对齐匹配——逻辑上就是双指针(或等价的双向遍历),只是:

  • 发生在 数据库进程里
  • C++/Java 引擎 实现
  • 你在 SQL 里 看不见 left++right--
-- 示意:两表按 join key 排序后做 Merge Join
SELECT *
FROM A
JOIN B ON A.amount = B.amount;
-- 若优化器选 Merge Join:先排序,再双路扫描对齐 amount

你没写过双指针,不等于线上没用双指针思想——是引擎替你做了。


2.3 计费对账为什么多半「SQL 关联 + 差异条件」就够?

计费场景常见特点:

特点 带来的做法
数据量 TB 级 必须落在 DB / 湖仓,不能全拉进 Python 内存
交易号、订单号、批次号 等主键 JOIN ON idFULL OUTER JOIN 找丢单、重复
差异类型多是 丢单、重复、金额不等、时间窗偏移 WHERE a.id IS NULL OR a.amt <> b.amt 即可

这类问题 天然有唯一键或业务键,引擎用 哈希或排序合并 做关联,比你在应用层手写双指针合适得多。

而 LeetCode 式双指针常假设:

  • 数据在 内存int[]、链表节点)
  • 没有 稳定唯一键,只能按 排序后的位置 / 值 配对
  • 数据量 小到 一台机器装得下

所以:日常批量对账用 SQL 是对的;双指针题考的是「没有数据库替你干时,你怎么 O(n) 扫」。


2.4 什么时候双指针会比 SQL 更合适?

当你 离开声明式引擎,或问题 不适合用主键 JOIN 时,双指针会很好用。

真实排错场景(一次性、小数据)

银行 CSV 流水 + 内部计费导出 CSV,两边都没有唯一交易号,只能按 金额 + 时间 近似对齐。金额已排序,怀疑有「错位配对」导致差额 700 元。

用 SQL 也能做,但往往要:

  • 建临时表、导入
  • 窗口函数 LAG/LEAD 比相邻行
  • 处理排序、去重、边界

用 Python / Java 读两个排序后的列表,双指针扫金额差

  • 逻辑直观(和本文 §三 场景 1 同一套)
  • 代码短,适合 adhoc 排查
  • 不必为几百行 CSV 搭一套 SQL 管道
维度 SQL 关联(日常对账) 双指针(内存 / 脚本)
适用数据量 大(TB 级) 小(内存能放下)
执行位置 DB / Spark / Flink 引擎 应用进程、Notebook、脚本
是否依赖唯一键 通常 需要,否则 SQL 很绕 可按 排序 + 位置 比较
你是否「看见」算法 优化器黑盒 自己写循环,完全可见
典型计费场景 日批对账、差异报表 CSV 排错、Kafka 双流实时对齐、面试题

一句话:没遇到过,是因为 引擎替你做掉了,或 场景有主键不需要它;一旦在内存里处理两个有序序列,双指针是随手可用的工具。


三、对撞指针:从两端往中间夹

场景 1:财务对账——两笔流水之和等于差额

业务背景
出纳要对账:系统显示差额 700 元,怀疑是两笔 已按金额排序 的流水一出一进配错了。

流水(已排序): [100, 300, 500, 800, 1200]
要找:两笔 a + b = 700

暴力怎么做
两重循环,试 (100,300)、(100,500)… 共约 n²/2 对。10 万条流水就崩。

双指针怎么做

left → 最小    right → 最大
[100,  300,  500,  800, 1200]
  ↑                        ↑
left right 动作 排除了什么
1 100 1200 1300 太大 → right-- 100 配 1200、800… 都不可能(和都更大)
2 100 800 900 太大 → right--
3 100 500 600 太小 → left++ 300 配 800、500… 要试
4 300 500 700 找到

规则(背三句)

  • 太小left++(换更大的左边)
  • 太大right--(换更小的右边)
  • 相等 → 结束

对应题目167. 两数之和 II
前提:数据 有序。无序用哈希表(LC 1)。


场景 2:盛最多水的容器(LC 11)

对应题目11. 盛最多水的容器

文档里曾用「两仓库货位」作类比;若类比干扰理解,直接记「两根竖线夹水」 即可。下面从原题讲起。


题目在画什么?

有一排 竖线,第 i 根高度是 height[i]。选 两根 当左右边界,中间可以「装水」:

高度
 8 |       █
 6 |   █   █       █
 4 |   █   █   █   █
 2 |   █   █   █   █   █
   +---+---+---+---+---+---→ 下标
       0   1   2   3   4   5   6   7
     height = [1, 8, 6, 2, 5, 4, 8, 3, 7]

选下标 left=1(高 8)和 right=6(高 8):

        █               █
        █               █
        █   ~ ~ ~ ~ ~   █    ← 水的高度 = 较矮那根 = 8
        █   ~ ~ ~ ~ ~   █
        █   ~ ~ ~ ~ ~   █

容量公式

容量 = 宽度 × min(左高, 右高)
     = (right - left) × min(height[left], height[right])
  • 宽度:两根线之间的水平距离
  • 高度:水装不满较高的那根,会溢出,所以取 较矮 的那侧

上例:宽度 = 6−1 = 5,高度 = min(8,8) = 8 → 容量 = 40


暴力在干什么?

任选两个下标 (i, j),算 min(h[i], h[j]) × (j - i),取最大。
大约要试 n²/2 对,n = 10⁵ 时会超时。


双指针怎么做?

left 从最左,right 从最右,算当前容量,然后 只移动较矮的那一侧

int area = Math.min(height[left], height[right]) * (right - left);
ans = Math.max(ans, area);
if (height[left] < height[right]) left++;
else right--;

为什么动「较矮」的那边?

left 矮,right 高

  矮|              |高
    |   ~~~~~     |
    +--------------+
    ←── 宽度 W ──→
  • 动较高的 right(往左缩):宽度 一定变小,高度仍受 矮的 left 限制 → 容量 只会更小或不变,不可能变大。
  • 动较矮的 left(往右移):宽度变小,但 left 可能变高 → 高度有机会变大,容量 还有可能变大

所以:只有动矮边才值得继续试;动高边是在白试。


手把手推演(前几步)

height = [1, 8, 6, 2, 5, 4, 8, 3, 7],初始 left=0, right=8

left right 高左 高右 容量 = min×宽 动谁
1 0 8 1 7 8 1×8=8 left(1<7)
2 1 8 8 7 7 7×7=49 right(8>7)
3 1 7 8 3 6 3×6=18 right
每次动较矮端

全程维护 ans 的最大值;本题最优容量为 49
不必手算全程,记住:每次 O(1) 算面积,只动矮边,两指针各最多走 n 步 → O(n)


和「两数之和 II」对撞指针的异同

两数之和 II 盛最多水的容器
指针 两端往中间 两端往中间
比较什么 sumtarget 容量,取最大
移动规则 和太小动 left,太大动 right 永远动较矮的那边
目标 找到等于 target 扫描过程中维护 最大值

都是 对撞双指针,移动规则不同:一个看「和与 target」,一个看「哪边是瓶颈」。


业务联想:「宽 × 矮」在真实系统里长什么样?

LC 11 是 在一条线上选两个点使面积最大;业务里很少有人手写 left++/right-- 去算带宽,但 「两个边界之间能承载多少 = 宽度 × 较弱那侧的上限」 极其常见——这就是流水线里的 瓶颈(bottleneck) 思想。

下面每个例子都按同一套模板写:

容量/吞吐 ≈ 宽度(时间、距离、区间长度)× min(左端能力, 右端能力)
                                              ↑
                                         「较矮的那根线」

例子 1:跨机房 / 跨区数据传输(带宽)

场景
要把对象存储里的大文件从北京机房传到上海机房,中间走专线。
两端网卡、专线入口的 额定带宽不同

北京出口上限: 10 Gbps  ←── 较「高」
上海入口上限:  5 Gbps  ←── 较「矮」 → 整条链路瓶颈 5 Gbps
传输持续:      2 小时   ←── 「宽度」

怎么算
实际可传数据量 ≈ 5 Gbps × 2h(取 min(10, 5),不是 10)。
你加再宽的北京出口,在上海入口仍是 5 Gbps 之前,总传输量不会变——就像 动「较高的 right」无助于增大盛水量

和题目的对应

LC 11 带宽场景
height[i] 某节点/链路段的带宽上限
right - left 传输持续时长、或路径上覆盖的跳数
min(左高, 右高) 端到端有效带宽(瓶颈段)
动较矮边 扩容时先找 最窄的那一段 加带宽,而不是盲目加宽已经够宽的一端

例子 2:CDN / 用户下载(双段链路)

场景
用户从 CDN 边缘节点拉取视频:
用户 ↔ 边缘 一段,边缘 ↔ 源站 一段。

用户家宽:     100 Mbps
边缘↔源站:     20 Mbps  ←── 瓶颈
播放时长:      1 小时

用户感知到的稳定下载速度min(100, 20) = 20 Mbps,不会因为是 100M 宽带就变成 100M 下载。
「水的高度」由 较矮的那段链路 决定;「宽度」可以是时长或文件大小对应的传输窗口。

运维在干什么
排障时看 min(各段);优化时 先抬矮边(加回源带宽、换更近源站),而不是只给用户升家庭带宽。


例子 3:仓储通道堆货(和「货位高度」最贴近的类比)

场景
仓库里两条平行货架之间有一条通道,要在通道上方做 统高堆货(例如托盘层)。
通道 左端 已有货堆高 3m,右端 已有货堆高 5m,通道水平长度 20m。

左端堆高: 3m  ←── 较矮 → 通道内能堆的统一高度 ≤ 3m
右端堆高: 5m
通道长度: 20m ←── 宽度

可堆体积(简化)3m × 20m × 通道宽——再高会被左端挡住。
要想多装货:要么 加高左端能力(挪走左侧低位货),要么 缩短通道(缩小宽度)但换更高的左端——和「动矮边、看能不能遇到更高的线」是同一类权衡。


例子 4:流水线双工位产能(制造业 / 计费批处理)

场景
一条流水线两个环节:
- 环节 A(上游):最多 100 条/分钟
- 环节 B(下游):最多 60 条/分钟
连续跑 8 小时

有效产出 ≈ min(100, 60) × 8h = 60 条/分钟 × 8h
           ↑ 瓶颈工位

整条线 再快也超不过 60——多雇人加 A 的产能,B 不动,总产量不变。
Lean 里叫 瓶颈工序;和 min(左高, 右高) 完全一致。

数据开发里的同类
Spark 作业里 Shuffle 写磁盘 500MB/s、读磁盘 200MB/s,Stage 间有效吞吐按 200 算;优化先找 最慢的那一段(慢磁盘、小 partition、热点 key)。


例子 5:主从库同步 / 双端写入上限

场景
MySQL 主库写入峰值 5000 TPS,从库回放 binlog 能力 3000 TPS,高峰持续 30 分钟

可安全同步的事务量 ≈ 3000 TPS × 30min
                      ↑ 从库是「较矮边」

主库再飙到 8000 TPS,从库仍 3000,积压只会涨——除非 抬高从库(动矮边)拉长时间窗口换别的策略,而不是只扩主库。


例子 6:广告 / 计费——双端配额与填充率

场景
一次投放同时在 媒体 A媒体 B 上跑 7 天(宽度 = 7 天)。
- A 的库存/填充能力:日均 100 万次曝光
- B 的填充能力:日均 40 万次

若策略要求 两端同步放量、按 min 对齐(简化模型):

有效日均曝光 ≈ min(100万, 40万) = 40万
7 天总量    ≈ 40万 × 7

只加 A 的预算,B 仍 40 万,总有效曝光不变——要先抬 B 的 fill rate 或换量更大的 B。


例子 7:API 网关双端限流

场景
- 客户端合同:1000 QPS
- 下游核心服务:200 QPS
- 促销持续 1 小时

实际可持续调用 ≈ min(1000, 200) = 200 QPS
总可处理      ≈ 200 × 3600

网关再放宽客户端配额,核心仍是 200,只会排队/超时——扩容应 先动矮边(核心或缓存)


和 LC 11 双指针算法 的关系(别混)
层面 业务里 LC 11 题里
公式 min × 宽 带宽、产能、限流、堆货高度——天天见 竖线间盛水面积
双指针 O(n) 一般 不会 手写去扫「哪两个机房配对最大」 高度数组 上找最优区间
动矮边 的直觉 优化瓶颈段、先扩最窄环 证明双指针不漏最优解

业务上记住 瓶颈在 min 那一侧 就够应付 90% 的对话;面试再记 竖线夹水 + 动矮边 的代码。

口诀:夹水,容量 = 宽 × 矮;动矮边,别动高边。业务里:先找 min 那段,再谈加宽


场景 3:验证码 / 基因序列——是不是回文

业务背景
用户输入 A man, a plan, a canal: Panama,系统要去掉空格和标点,判断是否回文(正反读一样)。

双指针
left 从头,right 从尾,跳过非字母数字,比较 s[left]s[right],不等则否,相等则 left++right--

清理后:  a m a n a p l a n a c a n a l p a n a m a
         ↑                                   ↑
       left                               right

对应题目125. 验证回文串
业务同类:对称结构校验、DNA 回文片段、回文 ID 规则。


场景 4:风控——三笔交易之和为可疑金额

业务背景
sorted 交易金额,找 三笔 之和 = 0(或某阈值),上报可疑。

做法
固定最左边一笔 i,在 [i+1, n-1] 上用 对撞指针 找两数之和 = -nums[i]
外层 i 从 0 到 n-3,内层对撞 → 总体 O(n²),比 O(n³) 暴力三省一层。

对应题目15. 三数之和


对撞指针小结

什么时候用 信号词
数组 已排序 sorted、升序、两数之和
两端 向中间逼近 盛水、回文
固定一端 + 内层对撞 三数之和

四、快慢指针:同侧,快的探路、慢的落笔

场景 5:ETL 去重——排序后的用户 ID 流

业务背景
上游 Kafka topic 里的 userId 已排序,写入下游前要去重,且 尽量原地写、少占内存。

输入: [1001, 1001, 1002, 1002, 1003]

角色

  • slow:有效区最后一个位置(「打包员」)
  • fast:逐个读新 ID(「质检员」)
步 1:  [1001, 1001, ...]   slow=0, fast=1  → 相同,fast++
步 2:  [1001, 1002, ...]   不同 → slow++,写 nums[slow]=nums[fast]
...
结果:  [1001, 1002, 1003]   长度 slow+1 = 3

对应题目26. 删除有序数组中的重复项


场景 6:日志清洗——把无效记录挪到后面(移动零)

业务背景
一批状态码里,要把 0(失败/无效) 都挪到数组末尾,保持非零元素的相对顺序,原地、O(1) 额外空间。

输入: [0, 1, 0, 3, 12]
      fast 扫,见到非零就交给 slow 写
输出: [1, 3, 12, 0, 0]

和去重的同一套框架:fast 探路,slow 写「要保留的」。

对应题目283. 移动零
业务同类:compaction、过滤后 compact 内存池、稀疏数组整理。


场景 7:依赖有没有环——链表 / 任务引用

业务背景
任务 A → B → C → B,形成环,调度器会死循环。链表 不知道长度,不能先数一遍再判环。

Floyd 判圈(龟兔赛跑)

  • slow 每次走 1 步
  • fast 每次走 2 步
  • 若有环,fast 一定会在环里追上 slow
     A → B → C
         ↑   ↓
         E ← D

slow 和 fast 进环后,fast 像操场跑圈多跑,必相遇

对应题目141. 环形链表
业务同类:循环依赖检测、引用环、GC 可达性(思想类似)。


场景 8:大文件切半——找链表中点

业务背景
单链表要 分片 给两个 worker,需要找 中点 切开,且只能扫一遍。

做法
slow 走 1 步,fast 走 2 步;fast 到尾时 slow 在中点。

对应题目876. 链表的中间结点


快慢指针小结

什么时候用 信号词
原地 修改数组 去重、移动零、过滤
链表 不知长度 环、中点
写指针 + 读指针 单遍扫描、O(1) 空间

五、同向 / 逆向双指针:两个有序序列归并

场景 9:合并两个按时间排序的日志流

业务背景
服务 A、服务 B 各产出 按 timestamp 排序 的日志,要合成一条时间线给检索用。

A: [1:01, 1:05, 1:09]
B: [1:03, 1:07, 1:08]

同向双指针
i 指 A,j 指 B,每次取 较小 timestamp 的那条写入结果,对应指针 +1。

结果: [1:01, 1:03, 1:05, 1:07, 1:08, 1:09]

对应题目977. 有序数组的平方(负数平方后两端大、中间小,也可双指针)、21. 合并两个有序链表


场景 10:数据库 MERGE JOIN

业务背景
表 R、S 在 join key 上都已排序(或有有序索引)。DB 不用嵌套循环 O(n×m),而是两个扫描指针:
key 相等 → 输出匹配行;R 小 → R 指针前进;S 小 → S 指针前进。

R: [10, 20, 30, 40]
S: [15, 20, 20, 35]

匹配: 20-20, 20-20

这就是归并的双指针,和合并两个有序数组 同一套逻辑

对应题目88. 合并两个有序数组


场景 11:合并到 A 数组末尾(逆向填,避免覆盖)

业务背景
A 末尾有空位,要把 B 合并进 A,不能另开一个大数组。若从前往后填,会把 A 里还没处理的数盖住。

逆向双指针
i 指 A 有效区最后,j 指 B 最后,k 指 A 物理最后;谁大写谁到 k,从后往前填。

A: [1, 3, 5, _, _, _]   m=3
B: [2, 4, 6]             n=3
从尾部填: 6, 5, 4, 3, 2, 1

对应题目88. 合并两个有序数组
业务同类:Git 合并两个有序 commit 线、版本 diff 归并。


同向 / 逆向小结

什么时候用 信号词
两个有序 序列 merge、归并、JOIN
结果写进 其中一个数组尾部 逆向双指针
单链表合并 每次取较小节点

六、场景 → 题目速查表

# 业务场景 双指针类型 LeetCode
1 两笔 sorted 流水之和 = 差额 对撞 167
2 两边界之间最大容量/效益 对撞 11
3 回文验证码 / 对称串 对撞 125
4 三笔交易之和可疑 对撞 + 固定一端 15
5 sorted ID 流去重 快慢 26
6 无效记录挪末尾 快慢 283
7 任务/引用环 快慢 141
8 链表找中点分片 快慢 876
9 两路 sorted 日志归并 同向 21
10 有序索引 JOIN 同向 (思想同 88)
11 原地合并进 A 尾部 逆向 88

七、拿到题怎么判断用哪种?

数组/字符串/链表题
        │
        ├─ 两个 **有序** 序列要合成一个?
        │       └─ 同向双指针(或逆向填,见 [§五 场景 11](#场景-11合并到-a-数组末尾逆向填避免覆盖))
        │
        ├─ **已排序** + 找两个/三个数和?
        │       └─ 对撞(三数之和:固定一端 + 内层对撞)
        │
        ├─ **两端** 向中间、回文、盛水?
        │       └─ 对撞
        │
        ├─ **原地** 去重 / 过滤 / 挪零?
        │       └─ 快慢
        │
        ├─ 链表 + 环 / 中点 / 不知长度?
        │       └─ 快慢
        │
        └─ 无序 + 任意两数之和?
                └─ 哈希表(LC 1),不是双指针

八、代码模板(Java)

// ========== 1. 对撞:有序两数之和 ==========
public int[] twoSumSorted(int[] nums, int target) {
    int left = 0, right = nums.length - 1;
    while (left < right) {
        int sum = nums[left] + nums[right];
        if (sum == target) return new int[]{left + 1, right + 1};
        if (sum < target) left++;
        else right--;
    }
    return new int[]{-1, -1};
}

// ========== 2. 对撞:盛最多水的容器 ==========
public int maxArea(int[] h) {
    int left = 0, right = h.length - 1, ans = 0;
    while (left < right) {
        ans = Math.max(ans, Math.min(h[left], h[right]) * (right - left));
        if (h[left] < h[right]) left++;
        else right--;
    }
    return ans;
}

// ========== 3. 快慢:原地去重 ==========
public int removeDuplicates(int[] nums) {
    if (nums.length == 0) return 0;
    int slow = 0;
    for (int fast = 1; fast < nums.length; fast++) {
        if (nums[fast] != nums[slow]) {
            nums[++slow] = nums[fast];
        }
    }
    return slow + 1;
}

// ========== 4. 快慢:移动零 ==========
public void moveZeroes(int[] nums) {
    int slow = 0;
    for (int fast = 0; fast < nums.length; fast++) {
        if (nums[fast] != 0) {
            int tmp = nums[slow];
            nums[slow++] = nums[fast];
            nums[fast] = tmp;
        }
    }
}

// ========== 5. 快慢:链表环 ==========
public boolean hasCycle(ListNode head) {
    ListNode slow = head, fast = head;
    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next.next;
        if (slow == fast) return true;
    }
    return false;
}

// ========== 6. 逆向:合并两个有序数组 ==========
public void merge(int[] nums1, int m, int[] nums2, int n) {
    int i = m - 1, j = n - 1, k = m + n - 1;
    while (i >= 0 && j >= 0) {
        if (nums1[i] > nums2[j]) nums1[k--] = nums1[i--];
        else nums1[k--] = nums2[j--];
    }
    while (j >= 0) nums1[k--] = nums2[j--];
}

九、必刷题目

优先级 题目 类型 场景
⭐⭐⭐ 167 两数之和 II 对撞 对账
⭐⭐⭐ 15 三数之和 对撞 风控
⭐⭐⭐ 11 盛最多水的容器 对撞 边界优化
⭐⭐⭐ 26 删除有序重复项 快慢 ETL 去重
⭐⭐⭐ 88 合并两个有序数组 逆向 日志/MERGE JOIN
⭐⭐ 283 移动零 快慢 原地过滤
⭐⭐ 125 验证回文串 对撞 校验
⭐⭐ 141 环形链表 快慢 环检测
⭐⭐ 21 合并两个有序链表 同向 双路归并
⭐⭐⭐ 42 接雨水 对撞/双指针 进阶

十、小结

类型 一句话 业务联想
对撞 有序数组两端往中间夹,和太大动右、太小动左 对账、回文、盛水
快慢 同侧,快探路慢写入;链表快走两步 去重、挪零、判环、中点
同向/逆向 两个 sorted 序列,取小归并;原地合并则逆向填 日志归并、MERGE JOIN
工程侧 SQL Merge Join 底层即双路扫描;日常对账用 JOIN,排错 CSV 用脚本双指针 §二

练习建议:每个类型至少 手写推演 一题(167 / 26 / 88),再在 LeetCode 各刷 2 题巩固。