概述:为什么双指针是基础算法题里的高频武器
学完数组和字符串之后,很多人会发现一类题特别常见:
- 在有序数组里找两个满足条件的数
- 原地删除重复元素或移动元素
- 反转字符串
- 判断一个字符串是不是回文
这些题表面上长得不一样,但背后经常指向同一个核心技巧:双指针。
双指针的价值在于,它常常能把"看起来要枚举很多次"的问题,优化成"一次扫描"或"从两端收缩"的过程。
所以这篇文章的目标,不是只让你记住几个题,而是帮你建立双指针最基础的套路意识。
你至少要掌握下面两种模型:
- 左右指针:一头一尾向中间靠拢
- 快慢指针:两个指针同向移动,但速度或职责不同
学完这篇,你应该能判断一道题是否适合用双指针,以及该选哪一种双指针模型。
核心概念:双指针到底是什么
所谓双指针,可以简单理解为:
不只用一个位置变量,而是同时维护两个位置,让它们配合完成搜索、比较、交换或筛选。
双指针最常见有两种写法。
1. 左右指针
一个指针在左边,一个指针在右边,然后不断向中间逼近。
最典型的应用包括:
- 有序数组中的两数之和
- 字符串反转
- 回文判断
它的核心动作通常是:
- 比较两端元素
- 根据条件决定移动左边还是右边
- 或者交换两端元素后继续收缩
2. 快慢指针
两个指针从同一侧出发,但分工不同。
常见情况包括:
fast负责扫描原数组slow负责维护有效区间
最典型的应用包括:
- 有序数组去重
- 移动零
- 链表找中点、判断环
在当前阶段,你可以先把它理解成:
一个指针负责"看",另一个指针负责"写"或"保留结果"。
左右指针更像"两边夹逼",快慢指针更像"一边扫描,一边整理结果"。
原理:为什么双指针能比暴力法更快
很多初学者写题时,第一反应往往是枚举所有可能。
比如在数组里找两个数相加等于 target:
- 暴力法:枚举所有数对
- 双指针:利用顺序关系或位置关系,主动排除不可能的情况
双指针之所以高效,核心原因通常有两个:
1. 每一步都在缩小搜索范围
如果数组有序,那么当你发现当前和太小,就没有必要继续保留左边更小的元素;当你发现当前和太大,就没有必要继续保留右边更大的元素。
这意味着:每移动一次指针,都在排除一批无效答案。
2. 每个元素通常只会被访问有限次
很多双指针题里,两个指针都只会单调移动,不会反复回退。
所以整体复杂度通常是:
text
O(n)
而不是暴力法那种:
text
O(n^2)
第一类高频题:有序数组中的左右夹逼
双指针最经典的入门题之一,就是:
给定一个递增数组,判断是否存在两个数之和等于
target。
暴力写法为什么慢
如果你枚举所有数对,写法通常是两层循环:
java
public static boolean twoSumBruteForce(int[] nums, int target) {
int n = nums.length;
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
if (nums[i] + nums[j] == target) {
return true;
}
}
}
return false;
}
时间复杂度是:
text
O(n^2)
双指针写法
如果数组本身有序,就可以让一个指针指向最左边,一个指针指向最右边。
java
public static boolean twoSumSorted(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
while (left < right) {
int sum = nums[left] + nums[right];
if (sum == target) {
return true;
} else if (sum < target) {
left++;
} else {
right--;
}
}
return false;
}
这段代码为什么成立
假设当前:
text
nums[left] + nums[right] < target
因为数组递增,nums[left] 已经是当前左边最小的候选值。
如果此时和还不够大,那么继续保留它意义不大,应该让左指针右移,尝试更大的数。
反过来,如果:
text
nums[left] + nums[right] > target
那就说明当前和太大,需要让右指针左移。
这个过程可以用下面的流程理解:
#mermaid-svg-6Jn2DLou4JbJ8VQT{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-6Jn2DLou4JbJ8VQT .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-6Jn2DLou4JbJ8VQT .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-6Jn2DLou4JbJ8VQT .error-icon{fill:#552222;}#mermaid-svg-6Jn2DLou4JbJ8VQT .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-6Jn2DLou4JbJ8VQT .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-6Jn2DLou4JbJ8VQT .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-6Jn2DLou4JbJ8VQT .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-6Jn2DLou4JbJ8VQT .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-6Jn2DLou4JbJ8VQT .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-6Jn2DLou4JbJ8VQT .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-6Jn2DLou4JbJ8VQT .marker{fill:#333333;stroke:#333333;}#mermaid-svg-6Jn2DLou4JbJ8VQT .marker.cross{stroke:#333333;}#mermaid-svg-6Jn2DLou4JbJ8VQT svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-6Jn2DLou4JbJ8VQT p{margin:0;}#mermaid-svg-6Jn2DLou4JbJ8VQT .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-6Jn2DLou4JbJ8VQT .cluster-label text{fill:#333;}#mermaid-svg-6Jn2DLou4JbJ8VQT .cluster-label span{color:#333;}#mermaid-svg-6Jn2DLou4JbJ8VQT .cluster-label span p{background-color:transparent;}#mermaid-svg-6Jn2DLou4JbJ8VQT .label text,#mermaid-svg-6Jn2DLou4JbJ8VQT span{fill:#333;color:#333;}#mermaid-svg-6Jn2DLou4JbJ8VQT .node rect,#mermaid-svg-6Jn2DLou4JbJ8VQT .node circle,#mermaid-svg-6Jn2DLou4JbJ8VQT .node ellipse,#mermaid-svg-6Jn2DLou4JbJ8VQT .node polygon,#mermaid-svg-6Jn2DLou4JbJ8VQT .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-6Jn2DLou4JbJ8VQT .rough-node .label text,#mermaid-svg-6Jn2DLou4JbJ8VQT .node .label text,#mermaid-svg-6Jn2DLou4JbJ8VQT .image-shape .label,#mermaid-svg-6Jn2DLou4JbJ8VQT .icon-shape .label{text-anchor:middle;}#mermaid-svg-6Jn2DLou4JbJ8VQT .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-6Jn2DLou4JbJ8VQT .rough-node .label,#mermaid-svg-6Jn2DLou4JbJ8VQT .node .label,#mermaid-svg-6Jn2DLou4JbJ8VQT .image-shape .label,#mermaid-svg-6Jn2DLou4JbJ8VQT .icon-shape .label{text-align:center;}#mermaid-svg-6Jn2DLou4JbJ8VQT .node.clickable{cursor:pointer;}#mermaid-svg-6Jn2DLou4JbJ8VQT .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-6Jn2DLou4JbJ8VQT .arrowheadPath{fill:#333333;}#mermaid-svg-6Jn2DLou4JbJ8VQT .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-6Jn2DLou4JbJ8VQT .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-6Jn2DLou4JbJ8VQT .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6Jn2DLou4JbJ8VQT .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-6Jn2DLou4JbJ8VQT .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6Jn2DLou4JbJ8VQT .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-6Jn2DLou4JbJ8VQT .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-6Jn2DLou4JbJ8VQT .cluster text{fill:#333;}#mermaid-svg-6Jn2DLou4JbJ8VQT .cluster span{color:#333;}#mermaid-svg-6Jn2DLou4JbJ8VQT 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-6Jn2DLou4JbJ8VQT .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-6Jn2DLou4JbJ8VQT rect.text{fill:none;stroke-width:0;}#mermaid-svg-6Jn2DLou4JbJ8VQT .icon-shape,#mermaid-svg-6Jn2DLou4JbJ8VQT .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6Jn2DLou4JbJ8VQT .icon-shape p,#mermaid-svg-6Jn2DLou4JbJ8VQT .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-6Jn2DLou4JbJ8VQT .icon-shape .label rect,#mermaid-svg-6Jn2DLou4JbJ8VQT .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6Jn2DLou4JbJ8VQT .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-6Jn2DLou4JbJ8VQT .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-6Jn2DLou4JbJ8VQT :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否,和太小
否,和太大
left 指向最左,right 指向最右
计算 numsleft + numsright
是否等于 target
找到答案,返回 true
left++
right--
时间复杂度:
text
O(n)
空间复杂度:
text
O(1)
只要题目里同时出现"有序"和"找两个位置",你就应该优先想到左右指针。
第二类高频题:快慢指针做原地去重
双指针不一定非得一左一右。
在很多数组题里,更常见的是两个指针从左往右同时移动,但职责不同。
来看一道经典题:
给定一个有序数组,原地删除重复元素,并返回去重后的长度。
为什么有序很重要
如果数组有序,那么重复元素一定挨在一起。
这时我们不需要把所有元素搬来搬去,只需要维护一个"有效区间"。
fast:负责向前扫描slow:负责指向当前有效结果的结尾
代码实现
java
public static 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]) {
slow++;
nums[slow] = nums[fast];
}
}
return slow + 1;
}
这个过程在做什么
假设数组是:
text
[1, 1, 2, 2, 3]
扫描过程可以理解为:
slow先停在第一个有效元素位置fast向后找下一个和它不同的值- 找到后,把这个新值放到
slow + 1的位置 slow向前扩展有效区间
最终数组前半段会变成:
text
[1, 2, 3, ...]
为什么这是双指针
因为这里有两个位置在配合:
- 一个负责读原始数据
- 一个负责写整理后的结果
时间复杂度:
text
O(n)
空间复杂度:
text
O(1)
当题目要求"原地保留有效元素"时,优先考虑快慢指针。
第三类高频题:反转字符串的标准模板
字符串反转是双指针最基础也最重要的模板之一。
如果题目要求你原地反转字符数组,最自然的做法就是:
- 左指针指向开头
- 右指针指向结尾
- 两边交换后继续向中间收缩
java
public static void reverse(char[] chars) {
int left = 0;
int right = chars.length - 1;
while (left < right) {
char temp = chars[left];
chars[left] = chars[right];
chars[right] = temp;
left++;
right--;
}
}
例如:
text
['h', 'e', 'l', 'l', 'o']
反转后会变成:
text
['o', 'l', 'l', 'e', 'h']
这个模板的价值非常大,因为很多题虽然不叫"反转",但本质上都在考你会不会:
- 两端比较
- 两端交换
- 指针向中间逼近
后面你会遇到很多变形:
- 只反转部分区间
- 只反转元音字母
- 先反转整体,再反转局部单词
这些题的底层骨架,基本都还是这套左右指针模板。
第四类高频题:回文判断几乎就是双指针送分题
所谓回文,就是一个字符串正着读和反着读都一样,比如:
levelabbaracecar
这类题之所以特别适合双指针,是因为你根本不需要真的把字符串反转一遍再比较。
你只要同时比较两端字符是否相等即可。
java
public static boolean isPalindrome(String s) {
int left = 0;
int right = s.length() - 1;
while (left < right) {
if (s.charAt(left) != s.charAt(right)) {
return false;
}
left++;
right--;
}
return true;
}
为什么这比"先反转再比较"更自然
因为回文的定义本身就是"两端对称"。
既然如此,直接从两端向中间检查,就是最贴合问题结构的做法。
时间复杂度:
text
O(n)
空间复杂度:
text
O(1)
一个更常见的变形
面试里还经常会把题目加强成:
- 忽略大小写
- 忽略非字母数字字符
这时双指针仍然适用,只不过每次比较前要先跳过无效字符。
java
public static boolean isPalindromeIgnoreNonAlphaNumeric(String s) {
int left = 0;
int right = s.length() - 1;
while (left < right) {
while (left < right && !Character.isLetterOrDigit(s.charAt(left))) {
left++;
}
while (left < right && !Character.isLetterOrDigit(s.charAt(right))) {
right--;
}
char c1 = Character.toLowerCase(s.charAt(left));
char c2 = Character.toLowerCase(s.charAt(right));
if (c1 != c2) {
return false;
}
left++;
right--;
}
return true;
}
这类题提醒你一个重要事实:
双指针不是死板地移动,而是根据题目规则灵活跳过、比较和收缩。
双指针题的识别方法:看到这几类关键词就该警觉
很多人不是不会写双指针,而是做题时根本没想到可以用双指针。
你可以先记下面这张表:
| 题目特征 | 常见模型 | 典型动作 |
|---|---|---|
| 有序数组里找两个位置 | 左右指针 | 左右夹逼 |
| 原地去重、筛选、覆盖 | 快慢指针 | 一边扫一边写 |
| 反转字符串或数组 | 左右指针 | 两端交换 |
| 判断回文 | 左右指针 | 两端比较 |
| 跳过无效字符后比较 | 左右指针 | 边移动边过滤 |
如果你看到题目里出现这些词,应该马上提高警觉:
- 有序数组
- 两个数
- 两端
- 回文
- 原地删除
- 原地移动
- 保留有效元素
双指针往往不是题目明说出来的,而是藏在"位置关系"和"顺序关系"里。
易错点:新手写双指针最容易踩的坑
1. 没想清楚什么时候该移动左指针,什么时候该移动右指针
在有序数组题里,指针移动一定要有依据。
不是"随便试一下",而是基于当前和与目标值的大小关系。
2. 循环条件写错
很多左右指针题常见写法是:
java
while (left < right)
如果条件写成别的形式,容易出现漏比较或越界问题。
3. 交换后忘记移动指针
反转类题里,如果交换完后不写:
java
left++;
right--;
程序就可能死循环。
4. 快慢指针里把"读"和"写"的职责混了
像有序数组去重这类题,fast 是扫描,slow 是维护结果。
如果这两个职责搞混,代码通常会越写越乱。
5. 忽略边界情况
例如:
- 空数组
- 只有一个元素
- 所有元素都相同
- 字符串长度为 0 或 1
这些情况都应该提前在脑子里过一遍。
6. 没利用题目给出的"有序"条件
很多题之所以能用双指针,就是因为"有序"这个条件帮你排除了很多可能。
如果你把它忽略掉,往往就会退回到低效暴力法。
复杂度总结:双指针为什么经常能做到线性
双指针题经常高效,不是因为它神奇,而是因为:
- 指针移动方向通常单调
- 每个元素不会被来回反复处理很多次
下面这张表可以帮助你建立直觉:
| 题型 | 常见时间复杂度 | 常见空间复杂度 |
|---|---|---|
| 有序数组两数之和 | O(n) |
O(1) |
| 有序数组去重 | O(n) |
O(1) |
| 反转字符数组 | O(n) |
O(1) |
| 回文判断 | O(n) |
O(1) |
这也是为什么双指针会成为面试里的高频技巧:
它往往代码不长,但能明显体现你是否会利用数据特征优化复杂度。
总结
双指针不是一种题型,而是一种思考方式
双指针的本质,是用两个位置的配合,减少无效枚举,提高扫描效率。
当你真正建立起这种意识后,你会发现很多原本看着分散的题,其实都能归到同一套模板里。