跳转至

447_滑动窗口面试题


一句话说明

滑动窗口是用双指针维护一个"窗口区间",在 O(n) 时间内解决"连续子数组/子字符串"的极值/满足条件问题,避免暴力 O(n²)。


核心知识点

白话理解

滑动窗口就像用一个移动的"取景框"在数组上滑动,左边界和右边界都只向右移,每次扩张右边界纳入新元素,当窗口不满足条件时收缩左边界。整体每个元素最多进出窗口各一次,所以是 O(n)。

两种窗口类型

  1. 固定大小窗口:窗口大小固定为 k,左右边界同步移动
  2. 可变大小窗口:右边界不断扩展,满足条件后收缩左边界

代码模板

# 可变窗口模板
left = 0
for right in range(len(s)):
    # 1. 将 right 纳入窗口,更新窗口状态
    window.update(s[right])

    # 2. 当窗口不满足条件时,收缩左边界
    while window_is_invalid():
        window.remove(s[left])
        left += 1

    # 3. 更新结果(此时窗口合法)
    ans = max(ans, right - left + 1)

经典题目与解法

题目1:无重复字符的最长子串(LeetCode 3)

题意:找出字符串中不含重复字符的最长子串长度。

def length_of_longest_substring(s: str) -> int:
    """
    可变窗口:右边界不断扩展,遇到重复字符收缩左边界
    用集合记录窗口内的字符
    """
    char_set = set()                       # 窗口内的字符集合
    left = 0                               # 窗口左边界
    max_len = 0                            # 最长子串长度

    for right in range(len(s)):
        # 如果 right 指向的字符已在窗口中,收缩左边界直到无重复
        while s[right] in char_set:
            char_set.remove(s[left])       # 移除左边界字符
            left += 1                      # 左边界右移

        char_set.add(s[right])             # 将新字符加入窗口
        max_len = max(max_len, right - left + 1)  # 更新最大长度

    return max_len

# 优化版:用字典记录字符最后出现的位置,直接跳跃左边界
def length_of_longest_substring_v2(s: str) -> int:
    char_pos = {}                          # {字符: 最后出现的位置}
    left = 0
    max_len = 0

    for right, c in enumerate(s):
        # 如果字符已见且在窗口内,直接跳跃左边界
        if c in char_pos and char_pos[c] >= left:
            left = char_pos[c] + 1         # 跳到重复字符的下一位
        char_pos[c] = right                # 更新字符位置
        max_len = max(max_len, right - left + 1)

    return max_len

# 测试
print(length_of_longest_substring("abcabcbb"))  # 3("abc")
print(length_of_longest_substring("bbbbb"))     # 1("b")
print(length_of_longest_substring("pwwkew"))    # 3("wke")

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


题目2:最小覆盖子串(LeetCode 76,难)

题意:在字符串 s 中找包含 t 的所有字符的最小子串。

from collections import Counter

def min_window(s: str, t: str) -> str:
    """
    可变窗口:右边界扩展,窗口覆盖t后收缩左边界找最小
    用 Counter 记录窗口状态和目标状态的差距
    """
    if not t or not s:
        return ""

    need = Counter(t)                      # 需要的字符及数量
    window = Counter()                     # 当前窗口的字符及数量
    have = 0                               # 窗口中已满足的字符种数
    required = len(need)                   # 需要满足的字符种数

    left = 0
    min_len = float('inf')
    result = ""

    for right in range(len(s)):
        c = s[right]
        window[c] += 1                     # 右边界字符纳入窗口

        # 如果当前字符满足了其在 t 中的需求
        if c in need and window[c] == need[c]:
            have += 1                      # 满足的种数+1

        # 当窗口已覆盖 t 的所有字符时,尝试收缩
        while have == required:
            if right - left + 1 < min_len: # 更新最小窗口
                min_len = right - left + 1
                result = s[left:right + 1]

            # 收缩左边界
            left_c = s[left]
            window[left_c] -= 1
            if left_c in need and window[left_c] < need[left_c]:
                have -= 1                  # 满足的种数-1
            left += 1

    return result

# 测试
print(min_window("ADOBECODEBANC", "ABC"))  # "BANC"
print(min_window("a", "a"))                # "a"
print(min_window("a", "aa"))               # ""(s中没有两个a)

时间复杂度:O(|s| + |t|)
空间复杂度:O(|t|)


题目3:长度最小的子数组(LeetCode 209,固定和)

题意:找和 >= target 的最短连续子数组长度。

def min_sub_array_len(target: int, nums) -> int:
    """
    可变窗口:右边界扩展,当窗口和>=target时收缩左边界找最小
    """
    left = 0
    window_sum = 0                         # 窗口内元素的和
    min_len = float('inf')

    for right in range(len(nums)):
        window_sum += nums[right]          # 纳入右边界

        # 满足条件时收缩左边界
        while window_sum >= target:
            min_len = min(min_len, right - left + 1)  # 更新最小长度
            window_sum -= nums[left]       # 移除左边界
            left += 1                      # 左边界右移

    return min_len if min_len != float('inf') else 0

# 测试
print(min_sub_array_len(7, [2,3,1,2,4,3]))  # 2(子数组[4,3])
print(min_sub_array_len(4, [1,4,4]))        # 1(子数组[4])
print(min_sub_array_len(11, [1,1,1,1,1]))   # 0(不存在)

时间复杂度:O(n) 每个元素最多进出各一次
空间复杂度:O(1)


面试技巧

  1. 识别滑动窗口题型:题目里有"连续子数组/子字符串""最长/最短"等关键词
  2. 先确定窗口合法条件:什么时候窗口合法,什么时候需要收缩——这是核心
  3. 固定窗口更简单:大小固定时左右同步,不用 while
  4. 窗口状态用什么记录:sum(求和)、Counter(字符统计)、set(去重)
  5. 说清楚 O(n) 的原因:每个元素最多进窗口一次,出窗口一次,所以是 O(2n) = O(n)

速查表

# 固定大小窗口(大小为k)
window_sum = sum(nums[:k])
max_sum = window_sum
for i in range(k, len(nums)):
    window_sum += nums[i] - nums[i-k]     # 右边进,左边出
    max_sum = max(max_sum, window_sum)

# 可变窗口模板
left = 0
state = {}                                # 窗口状态
for right in range(len(s)):
    state.update(s[right])                # 纳入右边界
    while not valid(state):               # 不合法时收缩
        state.remove(s[left])
        left += 1
    ans = update(ans, right - left + 1)   # 更新结果
题型关键词窗口类型窗口状态
最长不含重复可变(最大化)set 或 dict
最小覆盖可变(最小化)Counter
和>=目标可变(最小化)sum 变量
K大小的子数组固定大小滑进滑出
至多K种字符可变(最大化)Counter