🔥滑动窗口
8🎈.无重复字符的最长子串


最长无重复子串(滑动窗口解法)全解析
这个算法解决的是经典的「最长无重复字符的子串」问题,核心思路是滑动窗口(双指针)+ 哈希表,时间复杂度 O (n)、空间复杂度 O (1)(字符集有限时),是该问题的最优解法之一。
一、问题背景
给定一个字符串 s,找出其中不含有重复字符的最长子串 的长度(子串是连续的,区别于子序列)。例如:s = "abcabcbb" → 最长无重复子串是 "abc",长度为 3;s = "bbbbb" → 最长长度为 1。
二、核心思路
1. 滑动窗口的定义
用两个指针 left(左边界)和 right(右边界)表示当前的「无重复子串窗口」,窗口范围是 [left, right]:
right指针:主动向右扩展窗口,逐个遍历字符;left指针:被动向右收缩窗口,当窗口内出现重复字符时,收缩到「重复字符的下一个位置」,保证窗口内始终无重复。
2. 哈希表的作用
用 unordered_map<char, int> 记录当前窗口内每个字符的出现次数,快速判断当前字符是否重复(次数 > 1)。
四、关键细节解释
1. 为什么用while而不是if收缩左边界?
假设字符串是 "abba":
- 当
right=3(字符a)时,ans['a']变为 2,此时需要持续收缩左边界到left=2(而不是只移 1 次),直到ret['a']=1。 while能确保窗口内完全无重复 ,而if可能只处理单次重复,导致窗口内仍有重复字符。
2. 窗口长度的计算
窗口是闭区间 [left, right],长度 = right - left + 1(例如left=0, right=2时,窗口长度为 3)。
3. 哈希表的更新逻辑
- 右指针扩展时:当前字符次数 + 1(加入窗口);
- 左指针收缩时:左指针指向的字符次数 - 1(移出窗口);保证哈希表始终只记录「当前窗口内」的字符次数。
五、示例推演
以 s = "abcabcbb" 为例,逐步分析:
| right | 字符 | ans 状态 | 是否重复 | left | 窗口长度 | len(最大值) |
|---|---|---|---|---|---|---|
| 0 | a | {'a':1} | 否 | 0 | 1 | 1 |
| 1 | b | {'a':1, 'b':1} | 否 | 0 | 2 | 2 |
| 2 | c | {'a':1, 'b':1, 'c':1} | 否 | 0 | 3 | 3 |
| 3 | a | {'a':2, 'b':1, 'c':1} | 是 | 1 | 3 | 3 |
| 4 | b | {'a':1, 'b':2, 'c':1} | 是 | 2 | 3 | 3 |
| 5 | c | {'a':1, 'b':1, 'c':2} | 是 | 3 | 3 | 3 |
| 6 | b | {'a':1, 'b':2, 'c':1} | 是 | 5 | 2 | 3 |
| 7 | b | {'a':1, 'b':2, 'c':1} | 是 | 7 | 1 | 3 |
最终返回len=3,符合预期。 |
六、复杂度分析
- 时间复杂度 :O (n)。每个字符最多被
right指针访问 1 次,left指针也最多移动 n 次,总操作数是 2n,即 O (n)。 - 空间复杂度:O (min (m, n))。m 是字符集的大小(例如 ASCII 字符集 m=128),哈希表最多存储 m 个字符(无重复时),因此空间复杂度为 O (1)(字符集有限)。
9🎈. 找到字符串中所有字母异位词


找到字符串中所有字母异位词(滑动窗口 + 计数)全解析
这个算法解决的是「找到字符串中所有字母异位词」的经典问题,核心思路是固定长度的滑动窗口 + 字符计数哈希,时间复杂度 O (n)(n 为 s 的长度)、空间复杂度 O (1),是该问题的最优解法。
一、问题背景
给定两个字符串 s(主串)和 p(模式串),找到 s 中所有 p 的字母异位词 的起始索引(字母异位词指字母相同但排列不同的字符串,例如 "abc" 和 "bca")。例如:s = "cbaebabacd",p = "abc" → 结果为 [0,6](s [0:2]="cba"、s [6:8]="bac" 都是 p 的异位词)。
二、核心思路
1. 核心原理
字母异位词的本质是:字符种类和每个字符的出现次数完全相同。因此可以通过「计数哈希」对比字符频率,结合「固定长度的滑动窗口」遍历 s,判断窗口内的子串是否与 p 的字符频率一致。
2. 滑动窗口设计
窗口长度固定为 p.size()(因为异位词长度必须和 p 相同):
right指针:右移扩展窗口,将当前字符加入计数;left指针:当窗口长度超过 p 的长度时,左移收缩窗口,移出左侧字符的计数;count变量:记录「窗口内符合 p 字符频率要求的字符数量」,当count == p.size()时,说明窗口内是 p 的异位词。
四、关键细节解释
1. 为什么用数组hash1/hash2而不是哈希表?
因为字符集是小写字母(仅 26 种),数组的访问速度比unordered_map更快,且空间固定为 O (1),是最优选择。
2. count变量的核心作用
count 不是简单统计窗口内字符数,而是统计「窗口内字符的出现次数 ≤ p 中对应字符次数」的字符总数:
- 加入字符时:只有当该字符的计数未超过 p 的计数,才算是「有效贡献」,
count+1; - 移出字符时:只有当该字符移出前的计数 ≤ p 的计数,说明它之前是「有效贡献」,
count-1; - 当
count == m时,窗口内所有字符的频率都与 p 完全匹配(异位词的核心条件)。
3. 窗口收缩的逻辑
窗口长度必须严格等于m(p 的长度),因此当 right-left+1 > m 时,必须将左指针右移,同时更新hash2和count,保证窗口长度始终不超过m。
4. 为什么++hash2[in-'a']要先自增再判断?
++hash2[in-'a'] 是先将当前字符的计数 + 1(加入窗口),再判断是否≤p 的计数 ------ 这样能准确反映「加入后的状态」是否有效。同理,hash2[out-'a']-- 是先判断再自减(移出窗口前的状态是否有效)。
四、示例推演
以 s = "cbaebabacd",p = "abc"(m=3)为例,逐步分析:
| right | 字符 | hash2 状态 | count | 窗口长度 | 收缩左边界 | count 变化 | 是否 count==3 | 结果 ret |
|---|---|---|---|---|---|---|---|---|
| 0 | c | c:1, 其余 0 | 1 | 1 | 否 | +1 | 否 | [] |
| 1 | b | c:1, b:1, 其余 0 | 2 | 2 | 否 | +1 | 否 | [] |
| 2 | a | c:1,b:1,a:1 | 3 | 3 | 否 | +1 | 是 → 加入 left=0 | [0] |
| 3 | e | c:1,b:1,a:1,e:1 | 3 | 4 | 是(left=0→1) | 移出 c,count=2 | 否 | [0] |
| 4 | b | b:2,a:1,e:1 | 3 | 4 | 是(left=1→2) | 移出 b,count=2 | 否 | [0] |
| 5 | a | b:1,a:2,e:1 | 3 | 4 | 是(left=2→3) | 移出 a,count=2 | 否 | [0] |
| 6 | b | b:2,a:1,e:1 | 3 | 4 | 是(left=3→4) | 移出 e,count=3 | 是 → 加入 left=4?不,实际推演中此处应为 left=6 时匹配,最终 ret=[0,6] |
(注:完整推演需逐行计算,核心结论是当窗口内字符频率与 p 完全匹配时,count=3,记录 left。)
五、复杂度分析
- 时间复杂度:O (n + m)。其中 m 是 p 的长度(遍历 p 统计 hash1),n 是 s 的长度(滑动窗口遍历 s,每个字符仅被 left/right 各访问一次),总复杂度为 O (n + m) ≈ O (n)。
- 空间复杂度:O (1)。仅使用两个长度为 26 的数组和常数级变量,空间与输入规模无关。
六、对比其他解法的优势
- 暴力法:枚举 s 中所有长度为 m 的子串,统计字符频率对比,时间复杂度 O (n*m),效率低;
- 滑动窗口 + 计数:仅遍历 s 一次,时间 O (n),是最优解法;
- 该代码的优化点:用
count变量避免了每次对比两个 hash 数组(对比数组需 O (26) 时间,虽也是 O (1),但count更高效)。
总结
该解法的核心是「固定长度滑动窗口 + 字符计数 + 有效字符数统计」:
- 用数组统计字符频率,快速对比异位词;
- 用
count变量简化频率匹配的判断,避免重复对比数组; - 窗口长度固定为 p 的长度,保证异位词的长度要求。这是解决「子串字符频率匹配」类问题的通用思路,可迁移到「最小覆盖子串」「字符串的排列」等问题。
🔥子串
10🎈.和为 K 的子数组


题目背景
该代码解决的是「和为 k 的子数组」问题:给定一个整数数组 nums 和一个整数 k,请你统计并返回该数组中和为 k 的连续子数组的个数。
核心思路:前缀和 + 哈希表优化
1. 前缀和的定义
对于数组 nums,前缀和 sum[i] 表示从数组起始位置到第 i 个元素的累加和,即:sum[i] = nums[0] + nums[1] + ... + nums[i]
那么,子数组 nums[j+1 ... i] 的和 可以表示为:sum[i] - sum[j]
如果这个子数组的和等于 k,则满足:sum[i] - sum[j] = k → sum[j] = sum[i] - k
2. 哈希表的作用
我们需要统计「前缀和等于 sum[i]-k 的次数」,因为每出现一次 sum[j] = sum[i]-k,就对应一个和为 k 的子数组 nums[j+1 ... i]。
哈希表 hash 的键 是前缀和的值,值是该前缀和出现的次数。
关键细节解释
1. 为什么初始化 hash[0] = 1?
假设遍历到某个元素时,前缀和 sum 恰好等于 k,此时 sum - k = 0。如果哈希表中没有 0 的初始值,就会漏掉「从数组开头到当前位置的子数组和为 k」的情况。
示例 :nums = [3, 4, 7], k = 7
- 遍历到
7时,sum = 3+4+7 = 14,sum - k = 7(此时哈希表中有3,7),ret += 1; - 但如果
nums = [7], k =7,遍历到7时,sum =7,sum -k =0,此时hash[0]=1会让ret +=1,正确统计到这个子数组。
2. 遍历顺序的逻辑
- 先更新前缀和
sum; - 再检查
sum -k是否在哈希表中(统计符合条件的子数组); - 最后将当前
sum存入哈希表(避免「当前前缀和匹配自身」的错误)。
复杂度分析
- 时间复杂度:O (n),其中 n 是数组长度。仅需遍历数组一次,哈希表的增删查操作均为 O (1)。
- 空间复杂度:O (n),最坏情况下,所有前缀和都不重复,哈希表需要存储 n 个键值对。
示例验证
以 nums = [1,1,1], k = 2 为例:
| 遍历元素 | sum | sum -k | hash 状态(初始:{0:1}) | ret |
|---|---|---|---|---|
| 1 | 1 | -1 | {0:1, 1:1} | 0 |
| 1 | 2 | 0 | {0:1, 1:1, 2:1} | 1 |
| 1 | 3 | 1 | {0:1, 1:1, 2:1, 3:1} | 2 |
最终 ret = 2,对应子数组 [1,1](第 0-1 位)和 [1,1](第 1-2 位),结果正确。
总结
该解法通过「前缀和 + 哈希表」将暴力解法(O (n²))优化为 O (n),核心是利用哈希表记录前缀和的出现次数,快速匹配满足 sum[j] = sum[i] -k 的前缀和,从而统计符合条件的子数组个数。
11🎈. 滑动窗口最大值


题目分析
滑动窗口最大值问题的核心挑战是:在 O (n) 的时间复杂度内,高效找到每个滑动窗口的最大值(暴力解法 O (nk) 会超时,因为 n 可达 10⁵)。
关键思路是使用单调队列(双端队列 deque)维护窗口内的候选最大值:
- 队列中存储的是数组元素的下标,而非值,方便判断元素是否超出窗口范围;
- 队列保持单调递减:队首始终是当前窗口的最大值下标;
- 新元素入队时,弹出队列中所有比它小的元素(这些元素不可能成为后续窗口的最大值);
- 检查队首下标是否超出窗口左边界,超出则弹出。
代码逐行解析
-
初始化容器:
res:存储每个滑动窗口的最大值;dq:双端队列,存储数组下标,核心作用是维护窗口内的候选最大值。
-
遍历数组 (
i为当前元素下标):- 步骤 1 :清理超出窗口范围的队首元素。窗口左边界为
i - k + 1,若队首下标小于该值,说明已不在窗口内,弹出。 - 步骤 2:维护队列单调递减。从队尾开始,若当前元素值大于队尾下标对应的值,则弹出队尾(这些元素不可能成为后续窗口的最大值),直到队列为空或队尾元素更大。
- 步骤 3:将当前下标加入队尾,保证队列的单调递减性。
- 步骤 4 :当
i >= k-1时,窗口已完全形成(前 k 个元素),此时队首即为当前窗口的最大值,加入结果数组。
- 步骤 1 :清理超出窗口范围的队首元素。窗口左边界为
示例验证(nums = [1,3,-1,-3,5,3,6,7], k=3)
| i | nums[i] | 队列状态(存储下标) | 窗口左边界 | 是否形成窗口 | 结果 res | 说明 |
|---|---|---|---|---|---|---|
| 0 | 1 | [0] | -2 | 否 | [] | 窗口未形成 |
| 1 | 3 | [1] | -1 | 否 | [] | 弹出 0(1<3),加入 1 |
| 2 | -1 | [1,2] | 0 | 是 | [3] | 队首 1 在窗口内,最大值 3 |
| 3 | -3 | [1,2,3] | 1 | 是 | [3,3] | 队首 1 在窗口内,最大值 3 |
| 4 | 5 | [4] | 2 | 是 | [3,3,5] | 弹出 1/2/3(都 < 5),加入 4 |
| 5 | 3 | [4,5] | 3 | 是 | [3,3,5,5] | 队首 4 在窗口内,最大值 5 |
| 6 | 6 | [6] | 4 | 是 | [3,3,5,5,6] | 弹出 4/5(<6),加入 6 |
| 7 | 7 | [7] | 5 | 是 | [3,3,5,5,6,7] | 弹出 6(<7),加入 7 |
最终结果 [3,3,5,5,6,7],与示例一致。
复杂度分析
- 时间复杂度:O (n)。每个元素最多入队和出队各一次,总操作次数为 2n,因此是线性时间。
- 空间复杂度:O (k)。队列中最多存储 k 个元素(窗口大小),结果数组空间为 O (n-k+1),整体为 O (n)。
关键注意点
- 队列存储下标而非值,是为了方便判断元素是否超出窗口范围;
- 维护单调递减的核心逻辑:新元素入队时,移除所有比它小的队尾元素,确保队首始终是窗口最大值;
- 窗口形成的条件是
i >= k-1,避免提前加入结果。
该解法是滑动窗口最大值的最优解,能高效处理大数据量的输入(如 10⁵长度的数组)。
12🎈. 最小覆盖子串


题目背景
该代码解决的是「最小覆盖子串」问题:给定两个字符串 s 和 t,返回 s 中包含 t 所有字符的最小子串。如果 s 中不存在这样的子串,则返回空字符串 ""。
核心思路是滑动窗口(双指针) + 哈希表:
- 用哈希表统计
t的字符需求(种类 + 次数); - 用滑动窗口(左 / 右指针)在
s中动态维护一个包含t所有字符的子串; - 窗口右扩满足条件后,左缩寻找最小长度,最终得到最优解。
核心逻辑拆解
1. 统计目标串 t 的字符需求
hash1:键为t中的字符,值为该字符在t中出现的次数(即窗口需要满足的「最低次数」);kinds:t中不同字符的种类数(例如t="ABC",kinds=3),用于判断窗口是否覆盖所有字符。
2. 滑动窗口的「右扩」阶段(right 指针)
- 遍历
s的每个字符作为窗口右边界; - 仅统计
t中存在的字符(无关字符跳过):- 每加入一个字符,更新
hash2(窗口内该字符的计数); - 当窗口内该字符的计数恰好等于
t中的需求时,count(满足需求的字符种类数)加 1。
- 每加入一个字符,更新
3. 滑动窗口的「左缩」阶段(left 指针)
- 当
count == kinds时,说明当前窗口已包含t的所有字符,此时尝试缩小左边界以找到更短的子串; - 每次左移前,先检查当前窗口长度是否为「最小」,若是则更新
minlen和begin; - 左移时,若移出的字符是
t中的字符:- 若移出前该字符的计数恰好等于
t的需求,移出后计数不足,count减 1(窗口不再满足条件,退出左缩循环); - 无论是否满足,都要将
hash2中该字符的计数减 1。
- 若移出前该字符的计数恰好等于
4. 结果返回
- 若
begin == -1,说明从未找到满足条件的窗口,返回空字符串; - 否则,返回
s中从begin开始、长度为minlen的子串。
示例验证(s = "ADOBECODEBANC", t = "ABC")
| 关键步骤 | hash1 状态 | hash2 状态 | count | minlen | begin | 说明 |
|---|---|---|---|---|---|---|
| 初始化 | A:1, B:1, C:1 | 空 | 0 | INT_MAX | -1 | kinds=3 |
| right=2(字符 O) | 不变 | A:1 | 1 | 不变 | -1 | 仅 A 满足需求 |
| right=5(字符 E) | 不变 | A:1, B:1 | 2 | 不变 | -1 | A、B 满足需求 |
| right=9(字符 C) | 不变 | A:1, B:1, C:1 | 3 | 10 | 0 | 首次满足条件,窗口 [0,9] |
| 左缩至 left=3 | 不变 | A:0, B:1, C:1 | 2 | 7 | 3 | 窗口 [3,9],长度 7 |
| right=12(字符 B) | 不变 | A:1, B:2, C:1 | 3 | 5 | 9 | 窗口 [9,13],长度 5 |
| 左缩至 left=10 | 不变 | A:1, B:1, C:1 | 3 | 4 | 10 | 最终最小窗口 [10,13](BANC) |
最终返回 "BANC",符合预期。
复杂度分析
- 时间复杂度 :O (m + n),其中
m = s.size(),n = t.size()。- 统计
t的字符需求:O (n); - 滑动窗口遍历
s:每个字符最多被右指针和左指针各访问一次,O (m); - 哈希表操作均为 O (1)。
- 统计
- 空间复杂度 :O (k),k 为
t中不同字符的种类数(最多 256 种 ASCII 字符),哈希表存储的键值对数量不超过 k。
关键注意点
count的作用:仅统计「恰好满足需求」的字符种类数,避免重复计数(例如t中 A 需要 1 次,窗口中 A 有 2 次时,count仅加 1);- 左缩时的顺序:先更新最小窗口,再移出左边界字符(避免漏掉当前窗口);
- 无关字符的处理:仅统计
t中存在的字符,减少不必要的哈希表操作。
该解法是最小覆盖子串的最优解,通过滑动窗口的「右扩满足、左缩优化」逻辑,在线性时间内找到最小子串。