应用实例:生产代码中的双指针
双指针不只服务于算法题,它是一种基础的「用相对运动换复杂度」的程序结构。掌握前面三类思路之后,在标准库、运行时、数据库内核里都能反复见到它们。每个例子都回答同一个问题:是哪两个指针、各管什么?
Merge Sort 的合并步
归并排序的核心在「合并两个有序子数组」这一步——和合并有序数组、稀疏向量点积同一手法:两个读指针各指一个子数组的当前最小元,谁小就把谁拷进输出、对应指针前进一格,直到一边耗尽。整个 merge 是 的线性扫描。
数据库 Merge Join / LSM 的多路归并
关系数据库做 merge join 时,若两张表都按 join key 有序,就用双指针对齐:key 相等输出配对、否则推进 key 较小的那一侧——和稀疏向量点积里「谁的 index 小谁前进」一模一样。LSM-tree(LevelDB / RocksDB)合并多个有序 SSTable、Lucene 合并倒排表(posting list)求交,都是这套归并指针的多路推广。
std::remove / Java Arrays 的「保留-压实」
C++ 的 std::remove / std::unique、各语言「原地删除满足条件的元素」,内核都是移动零那对快慢指针:慢指针(写)标记「下一个要保留的元素的写入位置」,快指针(读)向前扫描,凡是该保留的就写到慢指针处、慢指针进一格。一趟扫描、原地完成,最后慢指针的位置就是新长度(std::remove 返回的就是这个「新尾」迭代器)。
垃圾回收的标记-整理(Mark-Compact)
分代 GC 的 compaction 阶段要把存活对象集中到堆的一端、消除碎片。经典做法正是双指针:scan 指针遍历整个堆,free 指针指向「下一个存活对象该搬到的空位」——扫到存活对象就搬到 free 处、free 前移。和擦除-填充同构,只不过「保留」的判定换成了「对象可达」。
滑动窗口:限流、最长子串、TCP 流控
滑动窗口是双指针的近亲:left / right 框出一个窗口,right 不断扩张纳入新元素,一旦窗口违反约束就收缩 left——两个指针都只单向前进,总移动 。它支撑了「无重复字符最长子串」「和 ≥ target 的最短子数组」这类题,也是限流器(sliding-window rate limiter)、TCP 滑动窗口流量控制的底层模型。这一分支的系统展开见相邻的滑动窗口 Sliding Window 系列。
环形缓冲区(Ring Buffer)的读写双指针
生产者-消费者之间的环形队列用两个 offset:write 指针入队时前移、read 指针出队时前移,都对缓冲长度取模绕回。两者相等表示空、写指针追上读指针表示满。音频缓冲、网卡 DMA 环、Disruptor、内核 ring buffer 全是这个结构——双指针在「环」上运行的工程化版本,与快慢指针判圈依赖的是同一套环上追及关系。
1 · 它真实跑在哪里
标准库算法:C++ STL 的 remove / unique / partition / inplace_merge、各语言的 filter 原地版,本质都是快慢或归并双指针。
排序与数据库:merge sort 合并步、归并外排、merge join、LSM compaction、倒排表求交。
运行时与系统:GC mark-compact、ring buffer 读写指针、滑动窗口限流、TCP 流控。
算法面试:两数之和(有序数组对撞)、三数之和、盛最多水的容器、回文判定、链表判环 / 找中点——双指针覆盖了相当大比例的高频题。
简言之:当你看到「两层循环 + 数据本身有序 / 单调」,几乎总能用一两个只单向移动的指针把它压成线性。回看三类——异构同向、左右对撞、快慢速度差——它们覆盖了绝大多数双指针题的骨架。