7.双指针算法

7.双指针算法

1. 核心定义与核心思想

  • 定义 :通过两个指针在序列(或两个序列)上移动,协同完成任务的算法,本质是利用问题的单调性优化枚举效率。
  • 核心价值:将朴素枚举的 O(n2) 时间复杂度优化为 O(n)(两个指针总移动次数不超过 2n)。
  • 单调性本质:随着一个指针(如右指针)的向后移动,另一个指针(如左指针)只会向后移动或保持不动,不会向前回溯(可通过反证法证明:若左指针回溯,则与 "当前区间已满足条件" 矛盾)。

2. 算法分类

分类 适用场景 典型案例
指向两个序列 两个有序序列的合并 / 匹配 归并排序的合并步骤、两数之和(有序数组)
指向一个序列 维护区间的特定性质(无重复、和满足条件等) 快排的划分过程、最长无重复子序列、字符串拆分单词

3. 通用模板

C++ 复制代码
// 一维序列通用模板(i为右指针,j为左指针)
for (int i = 0; i < n; i++) 
{
    // 移动j,维护[j, i]区间满足目标性质
    while (j < i && check(j, i)) j++; 
    // 具体业务逻辑(如计算区间长度、输出结果等)
    // ...
}
核心思想:
for(int i = 0; i < n; i++)
{
    for(int j = 0; j < n; j++)
    {
        O(n²)
    }
}
  • 关键:check(j, i) 是区间性质的判断函数,需根据题目自定义。

4. 经典例题解析

例题 1:字符串拆分单词(基础应用)

  • 题目:输入一个字符串(单词间仅一个空格,无首尾空格),按行输出每个单词。
  • 思路:
    1. 左指针 i 指向单词起始位置,右指针 ji 出发,扫描到空格停止(找到单词末尾);
    2. 输出 [i, j-1] 区间的字符,更新 i = j + 1 跳过空格。
  • 代码实现
C++ 复制代码
#include <iostream>
#include <string>
using namespace std;

int main() 
{
    string str;
    getline(cin, str);
    int n = str.size();
    int i = 0;
    while (i < n)
    {
        int j = i;
        // 移动j到单词末尾(空格前)
        while (j < n && str[j] != ' ') j++;
        // 输出当前单词
        for (int k = i; k < j; k++) cout << str[k];
        cout << endl;
        i = j + 1; // 跳过空格,指向next单词起始
    }
    return 0;
}

例题 2:最长无重复字符的连续子序列(核心应用)

朴素做法

C++ 复制代码
for(int i = 0; i < n; i++)
{
    for(int j = 0; j < n; j++)
    {
        if(check(j,i))  res = max(res, i-j+1);
    }
}

双指针做法

C++ 复制代码
for(int i = 0; i < n; i++)
{
    while(j <= i && check(j,i)) j++;
    res = max(res, i-j+1);
}
  • 题目 :给定长度为 n 的整数序列,找出最长的不包含重复数字的连续子序列,输出其长度。
  • 样例 :输入 [1,2,2,3,5],输出 3(对应子序列 [2,3,5])。
  • 思路:
    1. s[] 数组记录当前区间 [j, i] 中每个数字的出现次数;
    2. 右指针 i 遍历序列,将 a[i] 加入区间(s[a[i]]++);
    3. s[a[i]] > 1(出现重复),移动左指针 j 并减少对应数字的计数(s[a[j]]--),直到区间无重复;
    4. 每次更新区间长度的最大值。
  • 代码实现
C++ 复制代码
#include <iostream>
using namespace std;

const int N = 1e5 + 10; 
int a[N],s[N]; 

int main()
{
    int n;
    cin >> n;
    for (int i = 0; i < n; i++) cin >> a[i];
    
    int res = 0;
    for (int i = 0, j = 0; i < n; i++) 
    {
        s[a[i]]++; // 加入当前数字
        
        // 区间有重复,移动j
        while (s[a[i]] > 1)
        {
            //j是起点,s[a[i]]>1,说明j到i-1已经遍历过了「a [i] 加入窗口后重复」,所以接下来是窗口调整是「j 右移,i 不变」
            //「调整的终止条件」s[a[i]] > 1不成立
            s[a[j]]--;
            j++;
        }
        // 更新最长长度
        res = max(res, i - j + 1);
    }
    cout << res << endl;
    return 0;
}
  • 扩展 :若数字范围极大(如 109),可改用哈希表(unordered_map<int, int>)替代数组计数。

5. 补充扩展

  • 双指针的本质是 "减少无效枚举",核心是找到区间的单调性
  • 常见变形:滑动窗口(固定窗口大小 / 动态窗口)、快慢指针(找链表环)等;
  • 课后练习:LeetCode 3. 无重复字符的最长子串、LeetCode 209. 长度最小的子数组。