ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

C++算法 滑动窗口篇(上)

C++算法 滑动窗口篇(上)

刷数组和字符串题目时,滑动窗口是绕不开的经典算法。

很多初学者容易把它和暴力双循环混淆,误以为内层 while 循环会带来平方级复杂度;实际上只要左指针不回退,滑动窗口就能做到线性时间。它的精髓在于维护一个动态变化的窗口,不断扩张右边界探索新元素,窗口不满足约束时收缩左边界,始终维持窗口的合法性。有些题目不会直接给出窗口目标,需要我们转换问题视角。接下来将通过四道典型真题,一步步带你理解滑动窗口的套路、模板和使用前提。

目录

一、长度最小的子数组

1.题目

2.核心思路

3.举例模拟

4.代码参考

​5.复杂度

二、无重复字符的最长子串

1.题目

2.核心思路

3.举例模拟

4.代码参考

​5.复杂度

三、最大连续1的个数

1.题目

2.核心思路

3.举例模拟

4.代码参考

​5.复杂度

四、将x减到0的最小操作数

1.题目

2.核心思路

3.举例模拟

4.代码参考

​5.复杂度

五、滑动窗口分类复盘


一、长度最小的子数组

1.题目

2.核心思路

数组全部是正整数,所以窗口右边界右移,和一定变大;左边界右移,和一定变小,可以用滑动窗口。

fast:窗口右指针,不断向右扩展窗口,把当前元素加到sum。

slow:窗口左指针。

每当窗口内总和sum >= target,说明窗口满足条件:

计算当前窗口长度fast-slow+1,更新最小长度len;

左指针向右收缩窗口(sum减去左边界的值,slow++),尝试找更小的满足条件窗口;

遍历结束:

如果len仍然是INT_MAX,代表没有找到满足条件的子数组,返回 0;

否则返回最小长度len。

前提:nums 元素都是正数,才能保证收缩窗口时 sum 单调减小。

3.举例模拟

示例:target = 7,nums = [2,3,1,2,4,3]初始:fast=0, slow=0, sum=0, len=INT_MAX

fast=0:sum=2,sum<7,不进 while

fast=1:sum=5,sum<7,不进 while

fast=2:sum=6,sum<7,不进 while

fast=3:sum=8 ≥7

len = min(INT_MAX,3-0+1)=4

sum=8-2=6,slow=1,sum<7,退出内层 while

fast=4:sum=6+4=10 ≥7

len=min(4,4-1+1)=4

sum=10-3=7,slow=2,sum≥7

len=min(4,4-2+1)=3

sum=7-1=6,slow=3,sum<7,退出内层 while

fast=5:sum=6+3=9 ≥7

len=min(3,5-3+1)=3

sum=9-2=7,slow=4,sum≥7

len=min(3,5-4+1)=2

sum=7-4=3,slow=5,sum<7,退出内层 while

遍历结束,len=2,返回 2。 答案:2

4.代码参考

代码需要引入头文件:

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


5.复杂度

时间复杂度:(O(n)) 每个元素最多被fast指针访问一次,被slow指针访问一次,总共最多 2n 次操作,不是双重循环(O(n^2))。

空间复杂度:(O(1)) 只使用了常数个变量,没有开辟额外数组。


二、无重复字符的最长子串

1.题目

2.核心思路

求无重复字符的最长子串,使用左右指针left、right维护一个窗口[left, right],窗口内永远保证没有重复字符:

right作为右边界不断向右扩张,把当前字符加入哈希数组hash,hash[c]记录字符c在窗口内出现次数;

如果当前字符s[right]的计数大于 1,说明窗口出现重复,循环右移left,把窗口左边字符移出窗口、计数减一,直到该字符不再重复;

窗口合法后,更新最长长度ret = max(ret, right-left+1);

right++继续向右扫描,直到遍历完整个字符串。

哈希数组大小 180 是为容纳 ASCII 字符,代替unordered_map,访问更快。

3.举例模拟

举例模拟:s = "abcabcbb"

初始:left=0, right=0, ret=0, hash全0

right=0,s[0]='a',hash['a']=1;hash['a']≤1;ret=1;right=1

right=1,s[1]='b',hash['b']=1;hash['b']≤1;ret=2;right=2

right=2,s[2]='c',hash['c']=1;hash['c']≤1;ret=3;right=3

right=3,s [3]='a',hash ['a']=2 → 进入内层循环: hash [s [0]]-- → hash ['a']=0,left=1;此时 hash ['a']=1,退出循环;窗口 [1,3] 长度 3;ret 保持 3;right=4

right=4,s [4]='b',hash ['b']=2 → 内层循环: hash [s [1]]-- → hash ['b']=0,left=2;hash ['b']=1,退出;窗口 [2,4] 长度 3;ret=3;right=5

right=5,s [5]='c',hash ['c']=2 → 内层循环: hash [s [2]]-- → hash ['c']=0,left=3;hash ['c']=1,退出;窗口 [3,5] 长度 3;ret=3;right=6

right=6,s [6]='b',hash ['b']=2 → 内层循环: hash [s [3]]--,left=4;hash ['b'] 依旧 2;继续 hash [s [4]]--,left=5;hash ['b']=1;窗口 [5,6] 长度 2;ret=3;right=7

right=7,s [7]='b',hash ['b']=2 → 内层循环: hash [s [5]]--,left=6;hash ['b']=1;窗口 [6,7] 长度 2;ret=3;right=8,循环结束 返回结果:3

4.代码参考


5.复杂度

时间复杂度:O (n),n 是字符串长度。right从 0 走到 n-1 只会走一次;left只会向右移动,不会回退,总共最多移动 n 次,两层循环合计每个元素最多被访问两次。

空间复杂度:O (1)。哈希数组固定大小 180,和输入字符串长度无关,属于常数空间。


三、最大连续1的个数

1.题目

2.核心思路

这是可变长度滑动窗口(双指针),题目:最多可以把 k 个 0 翻转为 1,求最长连续 1 的子数组长度。

右指针right不断向右扩张窗口;遇到 0,窗口内 0 计数器count++。

当窗口内 0 的数量count > k,窗口非法:移动左指针left收缩窗口:

nums[left++] == 0:如果左边移出的元素是 0,count--。

收缩到count ≤ k时,窗口合法,更新窗口最大长度ret = max(ret, right-left+1)。

right++,继续向右探索。

窗口定义:[left, right],窗口内 0 的个数 ≤ k,记录合法窗口的最大长度。

3.举例模拟

样例:nums = [1,1,1,0,0,0,1,1,1,1,0], k = 2初始:left=0, right=0, count=0, ret=0

right=0,num=1 → count=0;窗口 [0,0],len=1,ret=1;right=1

right=1,num=1 → count=0;窗口 [0,1],len=2,ret=2;right=2

right=2,num=1 → count=0;窗口 [0,2],len=3,ret=3;right=3

right=3,num=0 → count=1;窗口 [0,3],len=4,ret=4;right=4

right=4,num=0 → count=2;窗口 [0,4],len=5,ret=5;right=5

right=5,num=0 → count=3,count>k=2,进入收缩:

nums [left=0]=1,left 变成 1,count 不变

nums [left=1]=1,left 变成 2,count 不变

nums [left=2]=1,left 变成 3,count 不变

nums [left=3]=0,left 变成 4,count=2 ✔退出内层循环 窗口 [4,5],长度 2,ret 保持 5;right=6

right=6,num=1,count=2;窗口 [4,6] len=3;right=7

right=7,num=1,窗口 [4,7] len=4;right=8

right=8,num=1,窗口 [4,8] len=5;right=9

right=9,num=1,窗口 [4,9] len=6 → ret=6;right=10

right=10,num=0 → count=3>2,收缩: nums [left=4]=0 → left=5,count=2 ✔退出循环 窗口 [5,10] len=6,ret 依旧 6;right=11,循环结束 最终返回6

4.代码参考


5.复杂度

  • 时间复杂度 O (n):left 和 right 都只会从 0 走到 n-1,每个元素最多被左右指针各访问一次,没有嵌套循环叠加。
  • 空间复杂度 O (1):只使用了 left、right、count、ret 几个临时变量,额外空间常数级,不随数组大小变化。

四、将x减到0的最小操作数

1.题目

2.核心思路

只能从数组最左边、最右边拿数字,拿出来数字总和等于x,求最少拿多少次;做不到返回 -1。

直接拿左右两边不好枚举,做转化:

拿左右若干数和为 x ⇨ 中间剩下一段连续子数组,这段子数组的和 =sum(nums) - x想要拿的次数最少⇨ 中间保留的连续子数组尽可能长

前提:本题元素全是正整数,所以可以滑动窗口(双指针)。

先算数组总和sum,目标子数组和tar = sum - x

如果tar < 0:说明总和都不够 x,直接返回 - 1

滑动窗口找和等于 tar 的最长连续子数组

找到最长长度ret,答案就是n - ret(总长度减去中间保留长度 = 左右拿走的数量)

遍历完都没找到和为 tar 的子数组,返回 - 1

3.举例模拟

样例:nums = [1,1,4,2,3], x = 5

sum = 1+1+4+2+3 = 11

tar = 11 - 5 = 6,我们要找和 = 6 的最长连续子数组

n = 5

窗口初始:left=0, tmp=0

right=0,tmp +=1 → tmp=1,tmp<6

right=1,tmp +=1 → tmp=2,tmp<6

right=2,tmp +=4 → tmp=6 ,窗口 [0,2],长度 3,ret=3

right=3,tmp +=2 → tmp=8 >6

tmp -= nums [left=0] → tmp=7,left=1,仍然 > 6

tmp -= nums [left=1] → tmp=6 ,窗口 [2,3],长度 2,ret 保持 max (3,2)=3

right=4,tmp +=3 → tmp=9>6

tmp -= nums[left=2] → tmp=5,left=3,tmp<6

遍历结束,ret=3 答案 = 5 - 3 = 2 解释:中间保留 [1,4,2](和 6),左右拿走1、3,一共 2 次,符合题意。

补充另一个样例:nums=[5,6,7,8,9],x=4,sum=35, tar=31,找不到和 31 的子数组,返回 - 1

4.代码参考


5.复杂度

时间复杂度:(O(n)) left 和 right 都只会向右移动,每个元素最多被加入窗口一次、移出窗口一次,没有嵌套循环。

空间复杂度:(O(1)) 只用了几个 int 变量,没有额外数组 / 哈希。


五、滑动窗口分类复盘

最短窗口模板:右指针扩窗口,满足条件就持续收缩左指针,不断更新最小值(长度最小的子数组)

最长窗口模板:右指针扩窗口,窗口非法时收缩左指针,合法后更新最大值(无重复最长子串、最多翻转 k 个 0、最小操作数)

关键限制:纯正数组才可以用双指针滑动窗口做子数组和类题目;有负数时不能直接双指针,要改用前缀和 + 哈希

复杂度本质:left、right 单向向右,每个元素最多被遍历两次,不是(O(n^2))

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进