Skip to content

滑动窗口

滑动窗口是双指针技巧的一种高级应用,特别适合解决子数组/子字符串的最值问题。其核心思想是维护一个窗口,通过左右指针的移动来调整窗口大小,在满足条件的过程中记录最优解。本模块涵盖无重复字符的最长子串、最小覆盖子串等经典题型,掌握窗口的扩大与收缩时机是解题关键。


Q1: 长度最小的子数组 「🟡 中级」

题目描述:给定一个含有 n 个正整数的数组和一个正整数 target。找出该数组中满足其和 ≥ target 的长度最小的连续子数组,并返回其长度。如果不存在符合条件的子数组,返回 0。

考察点:滑动窗口、前缀和+二分查找、最短子数组。

解题思路

  • 方法1:暴力枚举(时间O(n²),空间O(1))
  • 方法2:前缀和 + 二分查找(时间O(nlogn),空间O(n))
  • 方法3:滑动窗口,因为都是正数,窗口和单调变化(时间O(n),空间O(1))- 最优解

代码实现

java
public int minSubArrayLen(int target, int[] nums) {
    int left = 0;
    int sum = 0;
    int minLen = Integer.MAX_VALUE;
    for (int right = 0; right < nums.length; right++) {
        sum += nums[right];
        while (sum >= target) {
            minLen = Math.min(minLen, right - left + 1);
            sum -= nums[left];
            left++;
        }
    }
    return minLen == Integer.MAX_VALUE ? 0 : minLen;
}

复杂度分析:时间复杂度 O(n),左右指针各移动n次;空间复杂度 O(1)。

变体扩展

  • 变体1:和为K的子数组(有负数,不能用滑动窗口)
  • 变体2:乘积小于K的子数组
  • 变体3:最大子数组和
  • 变体4:最长湍流子数组

Q2: 无重复字符的最长子串 「🟡 中级」

题目描述:给定一个字符串 s,请你找出其中不含有重复字符的最长子串的长度。

考察点:滑动窗口、哈希表/数组计数、最长子串。

解题思路

  • 方法1:暴力枚举(时间O(n²),空间O(1))
  • 方法2:滑动窗口 + 哈希集合(时间O(n),空间O(min(m, n)),m为字符集大小)- 最优解
  • 方法3:滑动窗口 + 哈希表优化,直接记录字符最新位置

代码实现

java
public int lengthOfLongestSubstring(String s) {
    Set<Character> set = new HashSet<>();
    int left = 0;
    int maxLen = 0;
    for (int right = 0; right < s.length(); right++) {
        char c = s.charAt(right);
        while (set.contains(c)) {
            set.remove(s.charAt(left));
            left++;
        }
        set.add(c);
        maxLen = Math.max(maxLen, right - left + 1);
    }
    return maxLen;
}

复杂度分析:时间复杂度 O(n),左右指针各移动n次;空间复杂度 O(min(m, n)),m为字符集大小。

变体扩展

  • 变体1:至多包含两个不同字符的最长子串
  • 变体2:至多包含K个不同字符的最长子串
  • 变体3:最小覆盖子串
  • 变体4:找到字符串中所有字母异位词

Q3: 最小覆盖子串 「🔴 高级」

题目描述:给你一个字符串 s、一个字符串 t。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 ""。

考察点:滑动窗口进阶、最小窗口、need/window计数。

解题思路

  • 方法:滑动窗口,右指针扩大窗口直到满足条件,左指针收缩窗口求最小值(时间O(m+n),空间O(m),m为t的字符种类)- 最优解

代码实现

java
public String minWindow(String s, String t) {
    Map<Character, Integer> need = new HashMap<>();
    Map<Character, Integer> window = new HashMap<>();
    for (char c : t.toCharArray()) {
        need.put(c, need.getOrDefault(c, 0) + 1);
    }
    int left = 0, valid = 0;
    int start = 0, minLen = Integer.MAX_VALUE;
    for (int right = 0; right < s.length(); right++) {
        char c = s.charAt(right);
        if (need.containsKey(c)) {
            window.put(c, window.getOrDefault(c, 0) + 1);
            if (window.get(c).equals(need.get(c))) {
                valid++;
            }
        }
        // 满足条件时收缩窗口
        while (valid == need.size()) {
            if (right - left + 1 < minLen) {
                minLen = right - left + 1;
                start = left;
            }
            char d = s.charAt(left);
            left++;
            if (need.containsKey(d)) {
                if (window.get(d).equals(need.get(d))) {
                    valid--;
                }
                window.put(d, window.get(d) - 1);
            }
        }
    }
    return minLen == Integer.MAX_VALUE ? "" : s.substring(start, start + minLen);
}

复杂度分析:时间复杂度 O(m+n),左右指针各遍历s一次;空间复杂度 O(m),m为t中不同字符的数量。

变体扩展

  • 变体1:无重复字符的最长子串
  • 变体2:找到字符串中所有字母异位词
  • 变体3:字符串的排列
  • 变体4:最小窗口子序列

Q4: 找到字符串中所有字母异位词 「🟡 中级」

题目描述:给定两个字符串 s 和 p,找到 s 中所有 p 的异位词的子串,返回这些子串的起始索引。异位词指由相同字母重排列形成的字符串(包括相同的字符串)。

考察点:滑动窗口、固定窗口大小、字母计数数组。

解题思路

  • 方法:滑动窗口 + 数组计数,窗口大小固定为p的长度(时间O(m+n),空间O(1)因为字符集有限)- 最优解

代码实现

java
public List<Integer> findAnagrams(String s, String p) {
    List<Integer> res = new ArrayList<>();
    if (s.length() < p.length()) return res;
    int[] need = new int[26];
    int[] window = new int[26];
    for (char c : p.toCharArray()) {
        need[c - 'a']++;
    }
    int left = 0, valid = 0;
    for (int right = 0; right < s.length(); right++) {
        char c = s.charAt(right);
        window[c - 'a']++;
        if (window[c - 'a'] == need[c - 'a']) {
            valid++;
        }
        // 窗口超过p长度时收缩
        while (right - left + 1 >= p.length()) {
            if (valid == countDiff(need)) {
                res.add(left);
            }
            char d = s.charAt(left);
            if (window[d - 'a'] == need[d - 'a']) {
                valid--;
            }
            window[d - 'a']--;
            left++;
        }
    }
    return res;
}

private int countDiff(int[] need) {
    int count = 0;
    for (int n : need) if (n > 0) count++;
    return count;
}

复杂度分析:时间复杂度 O(m+n);空间复杂度 O(1),字符集大小固定为26。

变体扩展

  • 变体1:有效的字母异位词
  • 变体2:字符串的排列
  • 变体3:最小覆盖子串
  • 变体4:字母异位词分组

Q5: 滑动窗口最大值 「🔴 高级」

题目描述:给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。返回滑动窗口中的最大值。

考察点:单调队列、滑动窗口、双向队列。

解题思路

  • 方法1:暴力解法(时间O(nk),空间O(1))
  • 方法2:优先队列(大顶堆)(时间O(nlogk),空间O(k))
  • 方法3:单调递减队列,维护可能成为最大值的元素(时间O(n),空间O(k))- 最优解

代码实现

java
public int[] maxSlidingWindow(int[] nums, int k) {
    int n = nums.length;
    int[] res = new int[n - k + 1];
    Deque<Integer> deque = new LinkedList<>(); // 存下标,保证对应值递减
    for (int i = 0; i < n; i++) {
        // 移除队列中比当前元素小的,因为它们不可能成为最大值
        while (!deque.isEmpty() && nums[deque.peekLast()] <= nums[i]) {
            deque.pollLast();
        }
        deque.offerLast(i);
        // 移除超出窗口范围的队首
        if (deque.peekFirst() <= i - k) {
            deque.pollFirst();
        }
        // 窗口形成时记录最大值
        if (i >= k - 1) {
            res[i - k + 1] = nums[deque.peekFirst()];
        }
    }
    return res;
}

复杂度分析:时间复杂度 O(n),每个元素最多入队出队各一次;空间复杂度 O(k),队列最多存k个元素。

变体扩展

  • 变体1:滑动窗口中位数
  • 变体2:最小栈
  • 变体3:每日温度(单调栈)
  • 变体4:柱状图中最大的矩形

Q6: 字符串的排列 「🟡 中级」

题目描述:给你两个字符串 s1 和 s2,写一个函数来判断 s2 是否包含 s1 的排列。换句话说,s1 的排列之一是 s2 的子串。

考察点:滑动窗口、固定窗口、排列判断。

解题思路

  • 方法:滑动窗口 + 数组计数,窗口大小固定为s1长度(时间O(m+n),空间O(1))- 最优解

代码实现

java
public boolean checkInclusion(String s1, String s2) {
    if (s1.length() > s2.length()) return false;
    int[] need = new int[26];
    int[] window = new int[26];
    for (char c : s1.toCharArray()) {
        need[c - 'a']++;
    }
    int left = 0, valid = 0;
    int diffCount = 0;
    for (int n : need) if (n > 0) diffCount++;
    
    for (int right = 0; right < s2.length(); right++) {
        char c = s2.charAt(right);
        window[c - 'a']++;
        if (window[c - 'a'] == need[c - 'a']) {
            valid++;
        }
        while (right - left + 1 >= s1.length()) {
            if (valid == diffCount) {
                return true;
            }
            char d = s2.charAt(left);
            if (window[d - 'a'] == need[d - 'a']) {
                valid--;
            }
            window[d - 'a']--;
            left++;
        }
    }
    return false;
}

复杂度分析:时间复杂度 O(m+n);空间复杂度 O(1),字符集固定大小。

变体扩展

  • 变体1:找到字符串中所有字母异位词
  • 变体2:最小覆盖子串
  • 变体3:无重复字符的最长子串
  • 变体4:字符串的排列(全排列回溯题)