记得一定要看到文末,会有惊喜哦哦哦哦!!!
滑动窗口是为了解决字符串中子串相关问题,举几个例子:
s = "ADOBECODEBANC", t = "ABC",寻找s中最小的t覆盖,结果为:BANCs1 = "ab" s2 = "eidbaooo",判断s2中是否包含s1的任一全排列序列s = "cbaebabacd", p = "abc",寻找s中p的所有全排列s = "abcabcbb",寻找s中最长无重复子串上述四个例子分别对应最上面的四个题目,详情可点击题目查看
我们的思路很简单:维护一个窗口window,不断的对窗口进行扩张和收缩,来得到最终的结果。我们接下来针对第一个例子来详细介绍滑动窗口的思想
首先我们利用左右指针来模拟一个窗口,窗口的大小为[left, right),注意是左闭右开
其次我们还需要一个存放我们目标结果的窗口need,只有当window==need的时候,才进行一次结果的更新;既然need是存放目标结果的窗口,那么need始终都不会发生改变
同时我们维护了一个valid变量,根据题意,只有当valid满足相应的条件时,才进行窗口的收缩

如上面的动图所示。下面给出相对于的代码 (建议仔细看注释)。题目最小覆盖子串
public String minWindow(String s, String t) { // 实时记录窗口内的数据 (只存放 need 中需要的字符) Map<Character, Integer> window = new HashMap<>(); // 记录目标字符,始终保持不变 Map<Character, Integer> need = new HashMap<>(); // 初始化 need for (char c : t.toCharArray()) { need.put(c, need.getOrDefault(c, 0) + 1); } int left = 0, right = 0; // 窗口中满足 need 目标需要的字符个数 // 即:valid == need.size() 时满足条件 int valid = 0; // 记录最小覆盖的起始位置及长度 int start = 0, len = Integer.MAX_VALUE; while (right < s.length()) { // 1. 窗口扩张 char c = s.charAt(right); right++; // 2. 更新 valid,window 数据 // 判断此时窗口当前数据是否为 need 需要的数据,即 need 中是否存在 if (need.containsKey(c)) { // 先更新 window window.put(c, window.getOrDefault(c, 0) + 1); // 后更新 valid // 说明字符 c 的数量已经满足要求 if (window.get(c).equals(need.get(c))) { valid++; } } // 当 valid == need.size() 时,窗口开始收缩 while (valid == need.size()) { // 1. 判断结果是否需要更新 if (right - left < len) { start = left; len = right - left; } // 2. 窗口收缩 char d = s.charAt(left); left++; // 3. 更新 valid,window 数据 if (need.containsKey(d)) { // 先更新 valid // 如果窗口内字符 d 的数量和 need 中一样,valid-- // 原因:更新后字符 d 的数量已经不满足要求,所以 valid 需要 -1 // 小技巧:判断 valid++ 和 valid-- 的条件是一样的 if (window.get(d).equals(need.get(d))) { valid--; } // 后更新 window window.put(d, window.get(d) - 1); } } } return len == Integer.MAX_VALUE ? "" : s.substring(start, start + len);}题目 字符串的排列
public boolean checkInclusion(String s1, String s2) { Map<Character, Integer> window = new HashMap<>(); Map<Character, Integer> need = new HashMap<>(); for (char c : s1.toCharArray()) need.put(c, need.getOrDefault(c, 0) + 1); int left = 0, right = 0; int valid = 0; while (right < s2.length()) { char c = s2.charAt(right); 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()) { // 先判断,后收缩 // 如果窗口长度等于 s1 长度,则满足条件 if (right - left == s1.length()) return true; // 否则,进行收缩 char d = s2.charAt(left); left++; // 收缩后,更新 valid,window 数据 if (need.containsKey(d)) { // 先更新 valid if (window.get(d).equals(need.get(d))) valid--; // 后更新 window window.put(d, window.get(d) - 1); } } } return false;}public List<Integer> findAnagrams(String s, String p) { Map<Character, Integer> window = new HashMap<>(); Map<Character, Integer> need = new HashMap<>(); // 初始化 need for (char c : p.toCharArray()) need.put(c, need.getOrDefault(c, 0) + 1); int left = 0, right = 0; int valid = 0; List<Integer> res = new ArrayList<>(); while (right < s.length()) { char c = s.charAt(right); right++; // 更新 valid,window 数据 // 只有当 need 中存在时,才更新 // 记住:window 记录 need 需要的数据 if (need.containsKey(c)) { // 先更新 window,再更新 valid,和收缩刚好相反 window.put(c, window.getOrDefault(c, 0) + 1); if (window.get(c).equals(need.get(c))) valid++; } while (valid == need.size()) { // 先判断 if (right - left == p.length()) res.add(left); // 后收缩 char d = s.charAt(left); left++; // 更新 valid,window 数据 // 只有当 need 中存在时,才更新 // 记住:window 记录 need 需要的数据 if (need.containsKey(d)) { // 先更新 valid,再更新 window if (window.get(d).equals(need.get(d))) valid--; window.put(d, window.get(d) - 1); } } } return res;}题目 无重复字符的最长子串
注意:这个题目和模版有少许的区别,但是核心思想是不变的,只是少了need目标窗口而已。但是我尽量把代码和模版保持了一致
public int lengthOfLongestSubstring(String s) { Map<Character, Integer> window = new HashMap<>(); int left = 0, right = 0; // true: 无重复 // false: 有重复 boolean valid = true; int res = 0; while (right < s.length()) { // 窗口扩张 char c = s.charAt(right); right++; // 更新 window,valid window.put(c, window.getOrDefault(c, 0) + 1); if (window.get(c) > 1) { // 有重复 valid = false; } // 有重复后,开始收缩窗口 while (!valid) { char d = s.charAt(left); left++; // 更新 valid 的条件和扩张时的条件保持一致 if (window.get(d) > 1) { valid = true; } // 更新 window window.put(d, window.get(d) - 1); } // 执行到此处,一定无重复 // 更新结果 res = Math.max(res, right - left); } return res;}好了,现在来总结一下模版吧!!哈哈哈哈哈
public String slideWindow(String s, String t) { // 实时记录窗口内的数据 (只存放 need 中需要的字符) Map<Character, Integer> window = new HashMap<>(); // 记录目标字符,始终保持不变 Map<Character, Integer> need = new HashMap<>(); // 初始化 need 目标窗口 // code... // 左闭右开 [left, right) int left = 0, right = 0; // 存放满足收缩条件的变量 // valid while (right < s.length()) { // 1. 窗口扩张 char c = s.charAt(right); right++; // 2. 更新 valid,window 数据 if (need.containsKey(c)) { // 更新 window // code... // 更新 valid if (条件) { // code... } } // 根据 valid,来判断是否需要收缩 while (valid == need.size()) { // 1. 判断结果是否需要更新 // code... // 2. 窗口收缩 char d = s.charAt(left); left++; // 3. 更新 valid,window 数据 if (need.containsKey(d)) { // 更新 valid if (条件) { // code... } // 后更新 window // code... } } } return res;}