🪟 滑动窗口进阶 — 子串与子数组

最小覆盖子串、无重复最长子串、至多K个不同字符——滑动窗口的精准控制

📖 滑动窗口进阶的核心模式

第3课我们学了滑动窗口的基础——固定窗口大小。但面试中的滑动窗口问题往往是变长窗口,窗口大小由条件动态决定。核心模式有两种:

滑动窗口两大模式: 模式1: 求最长/最大 → 找到不满足条件时收缩 while 窗口不满足条件: 缩小左边界 更新最大长度 模式2: 求最短/最小 → 找到满足条件时收缩 while 窗口满足条件: 缩小左边界 (寻找更优) 更新最小长度 常用辅助数据结构: ├── 哈希表 counter → 计数窗口内字符频率 ├── 变量 distinct → 记录不同字符数 └── 变量 formed → 记录已满足的条件数

滑动窗口通用模板

# 变长滑动窗口通用模板
def sliding_window(s):
    from collections import Counter
    window = Counter()
    left = 0
    result = ...  # 初始化
    
    for right in range(len(s)):
        # 1. 扩大窗口: 加入右端元素
        window[s[right]] += 1
        
        # 2. 收缩窗口: 循环直到不满足条件
        while window_needs_shrink(window):
            window[s[left]] -= 1
            if window[s[left]] == 0:
                del window[s[left]]
            left += 1
        
        # 3. 更新结果
        result = update(result, left, right)
    
    return result

🎯 题目一:最小覆盖子串 (LC 76)

题目描述:给定字符串 st,返回 s 中涵盖 t 所有字符(包括重复字符)的最小子串。如果不存在,返回空字符串。

示例: s = "ADOBECODEBANC", t = "ABC" t的字符需求: {A:1, B:1, C:1} 滑动窗口过程: ┌─────────────────────────────────────────────┐ │ A D O B E C O D E B A N C │ │ └─────────────┘ │ │ ADOBEC → 满足! 长度6 │ │ │ │ └───────────────┘ │ │ CODEBA → 满足! 长度6 │ │ │ │ └─────┘ │ │ BANC → 满足! 长度4 ← 最优│ └─────────────────────────────────────────────┘ 关键: 维护 formed 计数器 need = {A:1, B:1, C:1}, need_cnt = 3 formed = 已满足的字符种数 当 formed == need_cnt 时窗口满足条件

思路分析

双指针 + 计数器:need 记录 t 中每个字符的需求量,window 记录当前窗口中各字符的数量。维护 formed 变量表示"窗口中已满足需求的字符种数"。当 formed == len(need) 时,窗口满足条件,尝试收缩左边界找更小的窗口。

代码实现

from collections import Counter

class Solution:
    def minWindow(self, s: str, t: str) -> str:
        need = Counter(t)
        window = Counter()
        formed = 0           # 已满足需求的字符种数
        need_cnt = len(need) # 需要满足的字符种数
        
        left = 0
        min_len = float('inf')
        min_start = 0
        
        for right, ch in enumerate(s):
            # 扩大窗口
            window[ch] += 1
            if ch in need and window[ch] == need[ch]:
                formed += 1
            
            # 收缩窗口
            while formed == need_cnt:
                # 更新结果
                if right - left + 1 < min_len:
                    min_len = right - left + 1
                    min_start = left
                
                # 移除左端字符
                left_ch = s[left]
                if left_ch in need and window[left_ch] == need[left_ch]:
                    formed -= 1
                window[left_ch] -= 1
                left += 1
        
        return s[min_start:min_start + min_len] if min_len != float('inf') else ""

复杂度分析

时间复杂度:O(|s| + |t|),空间复杂度:O(|字符集|)

LC 76 的精髓在于 formed 计数器。不用每次都遍历 need 检查是否满足,而是当 window[ch] == need[ch] 时 formed++,当 window[ch] < need[ch] 时 formed--。这样判断窗口是否满足只需 O(1)。

🎯 题目二:无重复字符的最长子串 (LC 3)

题目描述:给定一个字符串 s,找到不含重复字符的最长子串的长度。

示例: s = "abcabcbb" ┌────────────────────────────────────────┐ │ a b c a b c b b │ │ └─────┘ │ │ abc → 无重复, len=3 │ │ └─────┘ │ │ bca → 无重复, len=3 │ │ └─────┘ │ │ cab → 无重复, len=3 │ │ └──┘ │ │ abc → 无重复, len=3 │ │ └─┘ │ │ cb → 无重复, len=2 │ │ └ │ │ b → 无重复, len=1 │ └────────────────────────────────────────┘ 最长: 3 收缩策略: 遇到重复时, 左边界跳到重复字符的下一个位置

思路分析

求最长 → 不满足条件时收缩:用哈希表记录每个字符的最新位置。当遇到重复字符时,左边界跳到 max(left, last_pos[ch] + 1),确保窗口内无重复。

代码实现

class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        last_pos = {}  # 字符 → 最新位置
        left = 0
        max_len = 0
        
        for right, ch in enumerate(s):
            if ch in last_pos:
                left = max(left, last_pos[ch] + 1)
            last_pos[ch] = right
            max_len = max(max_len, right - left + 1)
        
        return max_len

复杂度分析

时间复杂度:O(n),空间复杂度:O(min(n, 字符集))

注意 left = max(left, last_pos[ch] + 1) 中的 max!不能直接用 last_pos[ch] + 1,因为 last_pos 中记录的位置可能已经在窗口左边了(已经被跳过)。例如 "abba",当处理第二个 a 时,last_pos['a']=0,但 left 已经是2了。

🎯 题目三:至多包含 K 个不同字符的最长子串 (LC 340)

题目描述:给定一个字符串 s 和整数 k,找到至多包含 k 个不同字符的最长子串的长度。

示例: s = "eceba", k = 2 滑动窗口过程: e → {e:1} 1种 ≤ 2 ✓ len=1 ec → {e:1,c:1} 2种 ≤ 2 ✓ len=2 ece → {e:2,c:1} 2种 ≤ 2 ✓ len=3 ← 最优 eceb → {e:2,c:1,b:1} 3种 > 2 ✗ 收缩: ce → {c:1,e:1} 2种 ✓ len=2 ceb → {c:1,e:1,b:1} 3种 > 2 ✗ 收缩: eb → {e:1,b:1} 2种 ✓ len=2 eba → {e:1,b:1,a:1} 3种 > 2 ✗ 收缩: ba → {b:1,a:1} 2种 ✓ len=2 最长: 3 ("ece")

代码实现

from collections import Counter

class Solution:
    def lengthOfLongestSubstringKDistinct(self, s: str, k: int) -> int:
        if k == 0:
            return 0
        window = Counter()
        left = 0
        max_len = 0
        
        for right, ch in enumerate(s):
            window[ch] += 1
            
            # 收缩: 直到不同字符数 ≤ k
            while len(window) > k:
                left_ch = s[left]
                window[left_ch] -= 1
                if window[left_ch] == 0:
                    del window[left_ch]
                left += 1
            
            max_len = max(max_len, right - left + 1)
        
        return max_len

复杂度分析

时间复杂度:O(n),空间复杂度:O(k)

LC 340 是 LC 3(k=1 时不重复字符)和 LC 76(求最短而非最长)的推广。滑动窗口的模式:求最长时"不满足条件才收缩",求最短时"满足条件就收缩"。

🔬 滑动窗口的完整分类

┌─────────────────────────────────────────────────────┐ │ 滑动窗口问题分类 │ ├─────────────────────────────────────────────────────┤ │ 固定窗口: 窗口大小固定, 滑动求每个窗口的统计值 │ │ LC 239 滑动窗口最大值 (单调队列) │ │ LC 643 子数组最大平均数 │ │ │ │ 变长窗口 - 求最长: 不满足条件时收缩 │ │ LC 3 无重复字符最长子串 │ │ LC 340 至多K个不同字符最长子串 │ │ LC 424 替换后的最长重复字符 │ │ │ │ 变长窗口 - 求最短: 满足条件时收缩 │ │ LC 76 最小覆盖子串 │ │ LC 209 长度最小的子数组 │ │ LC 904 水果成篮 (≈LC 340) │ │ │ │ 技巧总结: │ │ 子串问题 → 滑动窗口 + 哈希计数 │ │ 子数组和 → 滑动窗口 / 前缀和 │ │ 条件判断 → formed计数器 O(1)判断 │ │ 收缩时机 → 求最长: 不满足时, 求最短: 满足时 │ └─────────────────────────────────────────────────────┘

子数组求和问题 vs 子串问题

子串问题(字符频率匹配)用哈希计数器,子数组和问题(和为K)用前缀和+哈希:

# 子数组和 = K 的个数 (LC 560)
# 不能用滑动窗口! (因为有负数)
# 用前缀和 + 哈希表
def subarraySum(nums, k):
    from collections import Counter
    count = Counter({0: 1})  # 前缀和为0出现1次
    prefix = 0
    result = 0
    for num in nums:
        prefix += num
        result += count[prefix - k]  # 前面有多少个prefix-k
        count[prefix] += 1
    return result
滑动窗口处理子数组和问题只适用于正整数数组(窗口扩大和一定增大)。如果有负数,窗口扩大和不一定增大,滑动窗口失效!此时用前缀和+哈希(LC 560)。

📝 练习题单

  1. LC 76 最小覆盖子串(变长窗口-最短)✅
  2. LC 3 无重复字符的最长子串(变长窗口-最长)✅
  3. LC 340 至多K个不同字符的最长子串(变长窗口-最长)✅
  4. LC 209 长度最小的子数组(变长窗口-最短)
  5. LC 424 替换后的最长重复字符(变长窗口+技巧)
  6. LC 904 水果成篮(LC 340变体)
  7. LC 567 字符串的排列(固定长度窗口匹配)
  8. LC 438 找到字符串中所有字母异位词(固定窗口+计数)
  9. LC 560 和为K的子数组(前缀和+哈希,非滑动窗口)
  10. LC 713 乘积小于K的子数组(变长窗口-计数)

🔑 关键知识点总结

成就解锁:滑动窗口专家 — 掌握变长窗口的两种模式(最长/最短),能用formed计数器高效判断窗口条件!🎉
LeetCode AC验证:LC 76 Minimum Window Substring ✅ | LC 3 Longest Substring ✅ | LC 340 K Distinct ✅
思考题:LC 424 "替换后的最长重复字符"中,如何用滑动窗口在O(n)时间内解决?提示:不需要真的模拟替换,只需要知道"窗口长度 - 窗口内最多字符的个数 ≤ k"就说明可以通过替换满足条件。