Skip to content
On this page

滑动窗口技巧

算法思想

滑动窗口算法的思路是这样

_1、_我们在字符串S中使用双指针中的左右指针技巧,初始化left = right = 0把索引左闭右开区间[left, right)称为一个「窗口」

_2、_我们先不断地增加right指针扩大窗口[left, right),直到窗口中的字符串符合要求(包含了T中的所有字符)。

_3、_此时,我们停止增加right,转而不断增加left指针缩小窗口[left, right),直到窗口中的字符串不再符合要求(不包含T中的所有字符了)。同时,每次增加left,我们都要更新一轮结果。

_4、_重复第 2 和第 3 步,直到right到达字符串S的尽头。

现在开始套模板,只需要思考以下四个问题

**1、**当移动right扩大窗口,即加入字符时,应该更新哪些数据?

**2、**什么条件下,窗口应该暂停扩大,开始移动left缩小窗口?

**3、**当移动left缩小窗口,即移出字符时,应该更新哪些数据?

**4、**我们要的结果应该在扩大窗口时还是缩小窗口时进行更新?

一、最小覆盖子串

import java.util.HashMap;

/*
 * @lc app=leetcode.cn id=76 lang=java
 *
 * [76] 最小覆盖子串
 */

// @lc code=start
class Solution {
  public String minWindow(String s, String t) {
    // 首先建立两个哈希表,存储窗口内包含的字符,以及存储需要的包含的字符
    HashMap<Character, Integer> need = new HashMap<>();
    HashMap<Character, Integer> window = new HashMap<>();
    for (int i = 0; i < t.length(); i++) {
      char ch = t.charAt(i);
      need.put(ch, need.getOrDefault(ch, 0) + 1);
    }
    // 建立两个指针,分别指向窗口的前后
    int left = 0, right = 0;
    // valid变量表示窗口中满足need条件的字符个数
    int valid = 0;
    // 记录最小覆盖子串的起始索引及长度
    int start = 0, len = Integer.MAX_VALUE;
    while (right < s.length()) {
      // 获取移入窗口的字符,同时右移窗口
      char ch = s.charAt(right++);
      // 如果需要包含这个字符,对窗口内的数据进行一系列的更新
      if (need.containsKey(ch)) {
        // 更新窗口内的该字符的信息
        window.put(ch, window.getOrDefault(ch, 0) + 1);
        if (window.getOrDefault(ch, 0).equals(need.getOrDefault(ch, 0))) {
          valid++;
        }
      }
      // 当valid和need.size的大小相同,则说明窗口已满足条件,已经完全覆盖了串T。
      while (valid == need.size()) {
        // 当已经满足条件时,更新最小覆盖子串的信息
        if (right - left < len) {
          start = left;
          len = right - left;
        }
        // 移除窗口的字符
        char d = s.charAt(left++);
        // 如果需要包含这个字符
        if (need.containsKey(d)) {
          if (window.getOrDefault(d, 0).equals(need.getOrDefault(d, 0))) {
            // 如果当前窗口下的字符个数只能刚好满足需求,移除字符后会退出循环,
            // 此时的最小覆盖子串已经暂时形成
            valid--;
          }
          // 更新窗口内的该字符的信息
          window.put(d, window.getOrDefault(d, 0) - 1);
        }
      }
    }
    return len == Integer.MAX_VALUE ? "" : s.substring(start, start + len);
  }
}
// @lc code=end

需要注意的是,当我们发现某个字符在window的数量满足了need的需要,就要更新valid,表示有一个字符已经满足要求。而且,你能发现,两次对窗口内数据的更新操作是完全对称的。

valid == need.size()时,说明T中所有字符已经被覆盖,已经得到一个可行的覆盖子串,现在应该开始收缩窗口了,以便得到「最小覆盖子串」。

移动left收缩窗口时,窗口内的字符都是可行解,所以应该在收缩窗口的阶段进行最小覆盖子串的更新,以便从可行解中找到长度最短的最终结果。

二、找所有字母异位词

所谓的字母异位词,不就是排列吗? 相当于,输入一个串S,一个串T,找到S中所有T的排列,返回它们的起始索引

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/*
 * @lc app=leetcode.cn id=567 lang=java
 *
 * [567] 字符串的排列
 */

// @lc code=start
class Solution {
  public List<Integer> findAnagrams(String s1, String s2) {
    Map<Character, Integer> window = new HashMap<>();
    Map<Character, Integer> need = new HashMap<>();

    for (int i = 0; i < s2.length(); i++) {
      char ch = s2.charAt(i);
      need.put(ch, need.getOrDefault(ch, 0) + 1);
    }

    int l = 0, r = 0;
    int valid = 0;
    List<Integer> list = new ArrayList<>();
    while (r < s1.length()) {
      char c = s1.charAt(r++);
      if (need.containsKey(c)) {
        window.put(c, window.getOrDefault(c, 0) + 1);
        if (need.getOrDefault(c, 0).equals(window.getOrDefault(c, 0))) {
          valid++;
        }
      }
      // 改动一:收缩条件
      while (r - l >= s2.length()) {
		// 改动二
        if (valid == need.size()) {
          list.add(l);
        }
        char d = s1.charAt(l++);
        if (need.containsKey(d)) {
          if (need.getOrDefault(d, 0).equals(window.getOrDefault(d, 0))) {
            valid--;
          }
          window.put(d, window.getOrDefault(d, 0) - 1);
        }
      }
    }
    return list;
  }
}
// @lc code=end

三、最长无重复子串

window[c]值大于 1 时,说明窗口中存在重复字符,不符合条件,就该移动left缩小窗口了嘛。

唯一需要注意的是,在哪里更新结果res呢?我们要的是最长无重复子串,哪一个阶段可以保证窗口中的字符串是没有重复的呢?

这里和之前不一样,要在收缩窗口完成后更新res,因为窗口收缩的 while 条件是存在重复元素,换句话说收缩完成后一定保证窗口中没有重复嘛。

import java.util.HashMap;
import java.util.Map;

/*
 * @lc app=leetcode.cn id=3 lang=java
 *
 * [3] 无重复字符的最长子串
 */

// @lc code=start
class Solution {
  public int lengthOfLongestSubstring(String s) {
    Map<Character, Integer> map = new HashMap<>();

    int res = 0;
    int l = 0, r = 0;
    while (r < s.length()) {
      char c = s.charAt(r++);
      map.put(c, map.getOrDefault(c, 0) + 1);
      while (map.getOrDefault(c, 0) > 1) {
        char d = s.charAt(l++);
        map.put(d, map.getOrDefault(d, 0) - 1);
      }
      res = Math.max(res, r - l);
    }
    return res;
  }
}
// @lc code=end

学习链接:https://mp.weixin.qq.com/s/ioKXTMZufDECBUwRRp3zaA