第04篇-双指针算法-从有序数组到回文判断的高频解法

概述:为什么双指针是基础算法题里的高频武器

学完数组和字符串之后,很多人会发现一类题特别常见:

  • 在有序数组里找两个满足条件的数
  • 原地删除重复元素或移动元素
  • 反转字符串
  • 判断一个字符串是不是回文

这些题表面上长得不一样,但背后经常指向同一个核心技巧:双指针

双指针的价值在于,它常常能把"看起来要枚举很多次"的问题,优化成"一次扫描"或"从两端收缩"的过程。

所以这篇文章的目标,不是只让你记住几个题,而是帮你建立双指针最基础的套路意识。

你至少要掌握下面两种模型:

  1. 左右指针:一头一尾向中间靠拢
  2. 快慢指针:两个指针同向移动,但速度或职责不同

学完这篇,你应该能判断一道题是否适合用双指针,以及该选哪一种双指针模型。

核心概念:双指针到底是什么

所谓双指针,可以简单理解为:

不只用一个位置变量,而是同时维护两个位置,让它们配合完成搜索、比较、交换或筛选。

双指针最常见有两种写法。

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]

扫描过程可以理解为:

  1. slow 先停在第一个有效元素位置
  2. fast 向后找下一个和它不同的值
  3. 找到后,把这个新值放到 slow + 1 的位置
  4. 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']

这个模板的价值非常大,因为很多题虽然不叫"反转",但本质上都在考你会不会:

  • 两端比较
  • 两端交换
  • 指针向中间逼近

后面你会遇到很多变形:

  • 只反转部分区间
  • 只反转元音字母
  • 先反转整体,再反转局部单词

这些题的底层骨架,基本都还是这套左右指针模板。

第四类高频题:回文判断几乎就是双指针送分题

所谓回文,就是一个字符串正着读和反着读都一样,比如:

  • level
  • abba
  • racecar

这类题之所以特别适合双指针,是因为你根本不需要真的把字符串反转一遍再比较。

你只要同时比较两端字符是否相等即可。

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)

这也是为什么双指针会成为面试里的高频技巧:

它往往代码不长,但能明显体现你是否会利用数据特征优化复杂度。

总结

双指针不是一种题型,而是一种思考方式

双指针的本质,是用两个位置的配合,减少无效枚举,提高扫描效率。

当你真正建立起这种意识后,你会发现很多原本看着分散的题,其实都能归到同一套模板里。

相关推荐
CC数学建模1 小时前
2026年江西省研究生数学建模竞赛1题:空间数据分析中的过拟合识别完整思路、代码、模型、文章,全网首发高质量分享!
python·算法·数学建模
matlabgoodboy1 小时前
计算机java程序代写python代码编写c/c++代做qt设计php开发matlab
java·c语言·python
leo__5202 小时前
MATLAB实现牧羊人算法
开发语言·算法·matlab
视觉小萌新2 小时前
C++利用libmicrohttpd制作交互网页端——C1
java·c++·交互
Gauss松鼠会2 小时前
【GaussDB】GaussDB SMP特性调优详解
java·服务器·前端·数据库·sql·算法·gaussdb
Tisfy2 小时前
LeetCode 3689.最大子数组总值 I:What The Medium
算法·leetcode·题解·贪心·模拟·脑筋急转弯
葬送的代码人生2 小时前
JavaScript 数组完全指南:从入门到实战
前端·javascript·算法
格发许可优化管理系统2 小时前
Mentor许可证使用规定全解析
java·大数据·c语言·开发语言·c++
JAVA面经实录9172 小时前
Redis 知识体系(完整版)
java·redis·nosql数据库·nosql