← 返回博客列表

一、什么是双指针

双指针(Two Pointers)是指在遍历数据结构时使用两个指针变量协同移动的一种技巧。两个指针按特定规则前进,从而避免暴力枚举所有组合,把原本 O(n²) 的双重循环优化到 O(n)。

一句话定义
双指针 = 用两个游标在数组/链表上协同运动,利用"单调性"剪枝,把双层循环压成单层。

双指针并不是一种具体的算法,而是一类思想的总称。根据两个指针的运动方向不同,主要分三种形态:

1.1 三种主要形态

类型 初始位置 运动方向 典型应用
对撞指针 一头一尾 左→←右 相向而行 有序数组求两数之和、回文判定
快慢指针 同一起点 快指针走两步,慢指针走一步 链表找中点、判环
左右指针(滑动窗口) 同一起点 右指针扩,左指针缩 区间求和、最长/最短子串
⚠ 命名约定
不同教材对"双指针"的细分叫法略有差异。本文把"一头一尾相向运动"称为对撞指针,"同向快慢运动"称为快慢指针,"同向左缩右扩"称为滑动窗口(滑动窗口单列为另一篇专门讲,这里只做对照)。

双指针之所以高效,核心是"单调性剪枝":当某对 (i, j) 不满足条件时,我们能根据单调性直接排除掉一整片不可能的组合,而不需要逐一尝试。

二、核心原理与图解

理解双指针的关键,是理解"为什么指针只往一个方向走,不会错过答案"。下面分别用三张图展示对撞、快慢、滑动三种典型过程。

2.1 对撞指针过程图

以有序数组 [2, 3, 4, 6, 8, 11, 15] 找和为 15 的两数为例。两个指针 left、right 分别从两端出发,根据 nums[left] + nums[right] 与目标的大小关系决定移动哪一侧。

对撞指针:有序数组找和为 15 的两数 数组: 2 3 4 6 8 11 15 步骤 1:left=0(值 2),right=6(值 15) 和 = 2+15 = 17 > 15,太大 → right 左移 ↑L ↑R 步骤 2:left=0(值 2),right=5(值 11) 和 = 2+11 = 13 < 15,太小 → left 右移 ↑L ↑R 步骤 3:left=1(值 3),right=5(值 11) 和 = 3+11 = 14 < 15,太小 → left 右移 ↑L ↑R 步骤 4:left=2(值 4),right=5(值 11) 和 = 4+11 = 15 = target 命中!返回 [3, 6](下标从 1 起) 4 11 ↑L ↑R 每次根据"和与目标的大小关系"决定移动哪一侧,两个指针各最多移动 n 次
图 1:对撞指针在有序数组中找两数之和的过程

2.2 快慢指针过程图

快慢指针多用于链表。两个指针同时从头出发,快指针一次走两步,慢指针一次走一步。当快指针到达末尾时,慢指针恰好在链表中点。

快慢指针:找链表中点(6 个节点) 链表: 1 2 3 4 5 6 → null 第 1 轮后:slow 指向 1,fast 指向 2 S F 第 2 轮后:slow 指向 2,fast 指向 4 S F 第 3 轮后:slow 指向 3,fast 已到末尾 S F 3 结果:slow 停在节点 3,正是链表中点 快指针速度是慢指针的两倍,所以快走完全程时慢正好走一半
图 2:快慢指针定位链表中点

2.3 左右指针滑动过程图

左右指针同向而行:右指针 right 主动右扩,左指针 left 在窗口不满足条件时右缩。下面以"和 ≥ 7 的最短子数组"演示这一过程。

左右滑动窗口:在 [2,3,1,2,4,3] 中找和 ≥ 7 的最短子数组 数组: 2 3 1 2 4 3 步骤 1:right 扩到下标 2,窗口 [2,3,1],和 = 6 < 7 L R 步骤 2:right 扩到下标 3,窗口 [2,3,1,2],和 = 8 ≥ 7 记录长度 4,然后 left 右缩看能否更短 L R 步骤 3:left 缩到下标 1,窗口 [3,1,2],和 = 6 < 7 停止缩 L R 步骤 4:right 扩到下标 4,窗口 [3,1,2,4],和 = 10 ≥ 7 记录长度 4(不更优),left 缩到下标 2,窗口 [1,2,4] 和 = 7 命中长度 3 L R 最终结果:最短子数组长度 = 3(子数组 [4,3] 也会被找到,但 [1,2,4] 先到) 右指针单向扩张,左指针只在条件满足时右缩——O(n) 总开销
图 3:左右滑动窗口在数组中找最短子数组

2.4 复杂度分析

⏱ 时间复杂度:O(n)
无论哪种形态,两个指针累计移动次数不超过 2n(对撞是从两端向中各 n 次;快慢各走 n 与 n/2;滑动窗口的 left、right 各最多走 n 次)。所以总复杂度是线性的。

💾 空间复杂度:O(1)
只用了两个指针变量和少数状态量,原地操作。

三、通用模板

双指针没有"一种模板吃所有题"的统一写法,但对撞和快慢各有典型骨架。下面给出两种最常见的模板。

3.1 对撞指针模板

// 对撞指针通用模板:在有序数组中找满足条件的两数
vector<int> twoSumSorted(const vector<int>& nums, int target) {
    int left = 0, right = (int)nums.size() - 1;
    while (left < right) {
        int sum = nums[left] + nums[right];
        if (sum == target) {
            return {left, right};   // 命中
        } else if (sum < target) {
            ++left;     // 和太小,左指针右移让和变大
        } else {
            --right;    // 和太大,右指针左移让和变小
        }
    }
    return {-1, -1};  // 无解
}
对撞模板三要点
1. 数组必须有序——对撞指针依赖"和随指针移动单调变化"的性质
2. 循环条件 left < right——指针重合或越界即结束
3. 一次只移动一侧——根据当前和与目标的关系决定走哪边

3.2 快慢指针模板(链表找中点)

// 快慢指针通用模板:找链表中点
struct ListNode {
    int val;
    ListNode* next;
    ListNode(int x) : val(x), next(nullptr) {}
};

ListNode* findMiddle(ListNode* head) {
    if (!head) return nullptr;
    ListNode* slow = head;   // 慢指针,一次走 1 步
    ListNode* fast = head;   // 快指针,一次走 2 步
    while (fast->next && fast->next->next) {
        slow = slow->next;
        fast = fast->next->next;
    }
    return slow;  // 偶数个节点时返回前半段最后一个
}
快慢模板三要点
1. 判环用 fast != nullptr && fast->next != nullptr——快指针走两步,必须保证两步都非空
2. 找中点用 fast->next && fast->next->next——让偶数链表时 slow 落在前半段末尾
3. 链表题的特殊性——无法回退,所以快慢是处理链表的"标配技巧"

四、示例一:两数之和 II(有序数组)

题目(LeetCode 167):给定一个已升序排序的数组 numbers,找到两个数使其和等于目标 target。返回这两个数的下标(从 1 开始),且下标小的在前。保证恰好有一个解。

4.1 示例

输入:numbers = [2, 7, 11, 15],target = 9
输出:[1, 2](即 2 + 7 = 9)

两数之和 II:在 [2,7,11,15] 中找和为 9 数组: 2 7 11 15 步骤 1:left=0(值 2),right=3(值 15) 和 = 17 > 9,太大 → right 左移到下标 2 L R 步骤 2:left=0(值 2),right=2(值 11) 和 = 13 > 9,太大 → right 左移到下标 1 L R 步骤 3:left=0(值 2),right=1(值 7) 和 = 9 = target 命中!返回 [1, 2](下标从 1 起) 2 7
图 4:两数之和 II 对撞指针执行过程

4.2 C++ 代码

#include <vector>
using namespace std;

vector<int> twoSum(const vector<int>& numbers, int target) {
    int left = 0, right = (int)numbers.size() - 1;
    while (left < right) {
        int sum = numbers[left] + numbers[right];
        if (sum == target) {
            return {left + 1, right + 1};  // 题目要求下标从 1 开始
        } else if (sum < target) {
            ++left;     // 和太小,左指针右移
        } else {
            --right;    // 和太大,右指针左移
        }
    }
    return {-1, -1};  // 不会执行到,题目保证有解
}

这就是对撞模板的直接套用,注意返回时下标加 1 即可。如果题目没说"恰好一个解",要在找到后继续移动指针收集所有解。

五、示例二:三数之和

题目(LeetCode 15):给定数组 nums,找出所有和为 0 的不重复三元组。答案中不能包含重复的三元组。

5.1 思路:固定一个,对撞两个

三数之和是两数之和的扩展:先排序,再枚举第一个数 a = nums[i],然后在 [i+1, n-1] 区间用对撞指针找两数之和等于 -a。去重的关键是相邻相同值跳过。

三数之和:固定 a=-1,在对撞区间找 b+c=1 排序后: -1 -1 0 1 2 2 固定 i=0:a = nums[0] = -1,需找 b + c = 1 i L R b + c = 1 + 2 = 3 > 1,R 左移 b + c = 1 + 2 = 3 > 1,R 左移到 0 b + c = 1 + 0 = 1 = 1 命中!记录 [-1, 0, 1] L R i 右移:i=1 时 nums[1]=-1 重复,跳过 i=2 时 a=0,b+c=0,区间 [3..5] 无解 i 继续右移,所有可能的 a 都已枚举完毕 最终结果:[[-1, 0, 1]](注意 -1 + -1 + 2 = 0 也合法)
图 5:三数之和——固定一个数 + 对撞找两数

5.2 C++ 代码

#include <vector>
#include <algorithm>
using namespace std;

vector<vector<int>> threeSum(vector<int>& nums) {
    vector<vector<int>> res;
    sort(nums.begin(), nums.end());  // 排序是对撞指针的前提
    int n = (int)nums.size();

    for (int i = 0; i < n - 2; ++i) {
        if (nums[i] > 0) break;  // 剪枝:a > 0 则三数之和必 > 0
        if (i > 0 && nums[i] == nums[i - 1]) continue;  // a 去重

        int left = i + 1, right = n - 1;
        while (left < right) {
            int sum = nums[i] + nums[left] + nums[right];
            if (sum == 0) {
                res.push_back({nums[i], nums[left], nums[right]});
                // b、c 去重:跳过相邻相同值
                while (left < right && nums[left] == nums[left + 1]) ++left;
                while (left < right && nums[right] == nums[right - 1]) --right;
                ++left; --right;
            } else if (sum < 0) {
                ++left;
            } else {
                --right;
            }
        }
    }
    return res;
}
💡 去重三个关键点
1. a 去重:if (i > 0 && nums[i] == nums[i-1]) continue;
2. b 去重:命中后 left 右移直到值变化
3. c 去重:命中后 right 左移直到值变化
去重的本质是"同值只让第一次出现的下标产生答案"。

六、示例三:盛最多水的容器

题目(LeetCode 11):给定 n 条垂直线,第 i 条高度为 height[i],两线与 x 轴围成的容器能盛多少水。返回最大盛水量。

6.1 贪心思路:每次移动较矮的一侧

容器水量 = min(h[L], h[R]) * (R - L)。两个指针从两端向中间走,每次只移动较矮的一侧——因为移动较高的一侧不可能让水量增加(高度被较矮一侧限制,宽度反而变小)。

盛最多水的容器:高度 [1,8,6,2,5,4,8,3,7] 1 8 6 2 5 4 8 3 7 L R 初始:L=0(h=1),R=8(h=7),水量 = min(1,7)*8 = 8 移动较矮一侧 → L 右移(左边较矮) 最优:L=1(h=8),R=8(h=7),水量 = min(8,7)*7 = 49(最大)
图 6:盛水容器——每次移动较矮的一侧逼近最优

6.2 C++ 代码

#include <vector>
using namespace std;

int maxArea(const vector<int>& height) {
    int left = 0, right = (int)height.size() - 1;
    int bestArea = 0;
    while (left < right) {
        // 容器水量 = 短板高度 × 宽度
        int h = min(height[left], height[right]);
        int w = right - left;
        bestArea = max(bestArea, h * w);
        // 关键贪心:总是移动较矮的一侧
        if (height[left] < height[right]) {
            ++left;
        } else {
            --right;
        }
    }
    return bestArea;
}
💡 为什么这样贪心是对的
当 height[L] < height[R] 时,水量被 L 限制。如果移动 R,宽度变小、高度仍被 L 限制,水量必减少;只有移动 L 才有可能(虽然不保证)让高度上升从而水量增加。所以移动较矮的一侧是唯一可能找到更优解的方向。

七、总结与适用场景

双指针核心要点

维度 要点
本质 两个游标协同运动,借助单调性剪枝把 O(n²) 优化到 O(n)
时间复杂度 O(n)——两指针累计移动 ≤ 2n 次
空间复杂度 O(1)——原地操作,只用常数变量
识别信号 "有序数组 + 找两数""链表找中点/判环""区间按条件伸缩""有序 + 回文"
常见变体 N 数之和(N≥2)、滑动窗口、快慢指针判环、回文判定

典型题目清单

🎯 记忆口诀
"有序找两数,对撞头尾移;链表找中点,快慢同出发;同向找区间,左缩右扩追。"
这三句话覆盖了双指针的三大主流场景,遇到题目先按"是否有序""是数组还是链表"两步判断选哪种形态即可。