双指针/滑动窗口—算法总结与教学指南 - 指南

核心思想总结

三种双指针模式

模式特点适用场景经典例题
同向双指针
(滑动窗口)
左右指针同向移动,维护一个窗口子数组/子串问题,满足某条件的最长/最短区间无重复字符的最长子串、长度最小的子数组
对向双指针左右指针从两端向中间移动有序数组查找、两数之和、回文判断两数之和 II、反转字符串
快慢指针快慢指针以不同速度移动链表环检测、中点查找、重复元素环形链表、寻找链表中点

滑动窗口解题框架

通用模板

int slidingWindow(vector<int>& nums, int k) {
  int left = 0;               // 左指针
  int result = 0;             // 结果
  unordered_map<int, int> freq; // 频率统计(根据需要)
    for (int right = 0; right < nums.size(); right++) {
    // 1. 右指针扩张,更新状态
    freq[nums[right]]++;
    // 或 sum += nums[right];
    // 2. 当不满足条件时,收缩左指针
    while (/* 不满足条件 */) {
    // 更新状态
    freq[nums[left]]--;
    // 或 sum -= nums[left];
    left++;
    }
    // 3. 更新结果
    result = max(result, right - left + 1);
    // 或 result += right - left + 1;
    }
    return result;
    }

问题识别技巧

什么时候用滑动窗口?

  1. 关键词:连续子数组、子串、最长/最短
  2. 限制条件:不超过K个某元素、至少包含K个某元素
  3. 数据特征:数组/字符串,通常需要找满足条件的区间

判断依据

// 如果是这些问题,考虑滑动窗口:
- "找到最短的连续子数组,其和 ≥ target"
- "找到最长的子串,其中最多有K个重复字符"
- "找到包含所有字符的最小子串"
- "统计满足条件的子数组个数"

关键决策点

1. 窗口何时扩张?

  • 总是:右指针每次循环向右移动一步
  • 将新元素纳入窗口,更新相关统计

2. 窗口何时收缩?

  • 不满足题目条件时收缩
  • 关键:正确识别"不满足条件"的判断
    // 各种条件的判断:
    while (sum >= target)            // 最小长度:条件达成时收缩
    while (freq[c] > k)              // 最多K个重复:超过时收缩  
    while (unique_chars > k)         // 最多K种字符:超过时收缩
    while (zero_count > k)           // 最多翻转K个0:超过时收缩

3. 如何更新答案?

  • 最长问题ans = max(ans, right-left+1) 在while循环
  • 最短问题ans = min(ans, right-left+1) 在while循环
  • 计数问题ans += right-left+1 在while循环

经典问题分类与解法

类型1:固定条件窗口

// 问题:满足条件的最长/最短窗口
int minSubArrayLen(int target, vector<int>& nums) {
  int left = 0, sum = 0, ans = INT_MAX;
  for (int right = 0; right < nums.size(); right++) {
  sum += nums[right];
  while (sum >= target) {  // 满足条件时尝试收缩
  ans = min(ans, right - left + 1);
  sum -= nums[left];
  left++;
  }
  }
  return ans == INT_MAX ? 0 : ans;
  }

类型2:频率限制窗口

// 问题:最多K个重复字符/最多K种字符
int lengthOfLongestSubstringKDistinct(string s, int k) {
int left = 0, ans = 0;
unordered_map<char, int> freq;
  for (int right = 0; right < s.size(); right++) {
  freq[s[right]]++;
  while (freq.size() > k) {  // 超过K种字符
  freq[s[left]]--;
  if (freq[s[left]] == 0) freq.erase(s[left]);
  left++;
  }
  ans = max(ans, right - left + 1);
  }
  return ans;
  }

类型3:转换思维窗口

// 问题:从两端删除使和等于x → 找中间和为total-x的最长子数组
int minOperations(vector<int>& nums, int x) {
  int total = accumulate(nums.begin(), nums.end(), 0);
  int target = total - x;  // 关键转换
  int left = 0, sum = 0, max_len = -1;
  for (int right = 0; right < nums.size(); right++) {
  sum += nums[right];
  while (sum > target && left <= right) {
  sum -= nums[left];
  left++;
  }
  if (sum == target) {
  max_len = max(max_len, right - left + 1);
  }
  }
  return max_len == -1 ? -1 : nums.size() - max_len;
  }

教学要点

给初学者的建议

  1. 先画图理解

    • 画出数组和指针移动过程
    • 用具体例子手动模拟
  2. 从暴力法思考

    暴力:O(n²) → 枚举所有子数组
    优化:滑动窗口 O(n) → 利用连续性
  3. 记住三个核心问题

    • 窗口什么时候扩张?(右指针右移)
    • 窗口什么时候收缩?(不满足条件时)
    • 什么时候记录答案?(收缩后/收缩时)
  4. 从简单模板开始

    int left = 0;
    for (int right = 0; right < n; right++) {
    // 加入nums[right]
    while (/* 不满足条件 */) {
    // 移除nums[left]
    left++;
    }
    // 更新答案
    }

常见错误与调试

  1. 死循环:确保while循环条件最终能打破
  2. 漏掉答案:检查答案更新位置是否正确
  3. 边界错误:注意数组索引越界
  4. 初始值错误:ans初始化为合适值

练习题进阶路径

Level 1: 固定窗口大小问题
Level 2: 条件简单的可变窗口
Level 3: 需要统计频率的窗口
Level 4: 需要转换思维的问题
Level 5: 多条件复合窗口

一句话总结各类问题

  1. 最长无重复子串:字符频率 >1 时收缩
  2. 长度最小子数组:和 ≥ target 时收缩并记录
  3. 最大连续1的个数III:0的数量 >k 时收缩
  4. 乘积小于K的子数组:乘积 ≥k 时收缩,计数用窗口长度
  5. 水果成篮:水果种类 >2 时收缩
  6. 最小覆盖子串:需要额外记录匹配条件
  7. 替换后的最长重复字符:窗口长度-最大频率 >k 时收缩
  8. 字符串的排列:固定长度窗口+频率匹配

终极心法

滑动窗口 = 右指针探索 + 左指针维持合法性 + 适时记录答案

掌握这个心法,配合足够的练习,就能解决大部分滑动窗口问题。开始时多画图,多模拟,慢慢就会形成直觉。

posted on 2026-01-01 15:34  ljbguanli  阅读(0)  评论(0)    收藏  举报