滑动窗口是一种解题技巧,一句话说明就是维护一个窗口,不断滑动,更新答案。
滑动窗口适合的一维情况,比如数组、字符串;同时,拓展到二维也不是不可能。
根据问题求解的特性,可分为最小滑动窗口和最大滑动窗口两种解题模版。
滑动窗口大致逻辑 1 2 3 4 5 6 7 8 9 10 11 12 13 14 int left = 0 , right = 0 ;while (right < 某个值) { right++; window.add (s[right]); while (window needs shrink) { window.remove (s[left]); left++; } }
通过双指针维护窗口,right指针扩大窗口,left指针缩小窗口。滑动窗口射击两个过程,扩大窗口以及缩小窗口。需要注意一些细节,比如说如何向窗口中添加新元素,如何缩小窗口,在窗口滑动的哪个阶段更新结果。
(1)窗口内状态收集一般通过容器进行,比如map、queue(单调队列)等。
(2)两种模型
如果在扩大阶段收集答案,那么就是最大窗口模型,但注意同时要满足题目特定条件。背后的思想与贪心类似。
如果在窗口缩小阶段收集答案,那么就是最小窗口模型。
那么这两种模型到底有什么区别?滑动窗口法都有窗口扩张和窗口缩小的过程。最大窗口模型,窗口缩小是使条件满足的过程,然后收集答案;最小窗口模型,只有在满足条件时才能进行窗口缩小,窗口缩小的目的是使得条件不满足,在每一次窗口缩小过程中收集答案。
最小滑动窗口 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 void slidingWindow (string s, string t) { int left=0 ,right=0 ; int valid = 0 ; while (right<s.size ()){ char c = s[right]; while (窗口满足条件){ } right++; } }
可以看出来,最小滑动窗口的条件是在while循环内更新的,因为一旦满足了条件就要马上更新,取最小。
最大滑动窗口 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 void slidingWindow (string s, string t) { int left=0 ,right=0 ; int valid = 0 ; while (right<s.size ()){ char c = s[right]; while (窗口不满足条件){ } right++; } }
最大窗口的条件更新是在while外更新的。当我们的窗口满足条件时,我们希望继续扩大窗口,期望得到最大值 。一旦窗口内的数据不满足条件了,我们就缩小窗口,调整满足条件。
滑动窗口法实战 字符串排列 https://leetcode.cn/problems/permutation-in-string/description/
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Solution { public boolean checkInclusion (String s1, String s2) { if (s1.length()>s2.length()) return false ; int [] winodw = new int [26 ]; int [] s1_hash = new int [26 ]; for (int i=0 ; i<s1.length(); ++i){ s1_hash[s1.charAt(i)-'a' ] += 1 ; winodw[s2.charAt(i)-'a' ] += 1 ; } if (Arrays.equals(s1_hash, winodw)) return true ; for (int i=s1.length(); i<s2.length(); ++i){ winodw[s2.charAt(i-s1.length()) -'a' ]--; winodw[s2.charAt(i)-'a' ]++; if (Arrays.equals(s1_hash, winodw)) return true ; } return false ; } }
很简单的思路,用哈希表映射窗口每个字母的个数,然后不断向右滑动。
字符串中的所有字母异位词 https://leetcode.cn/problems/find-all-anagrams-in-a-string/
(1)暴力解法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Solution { public List<Integer> findAnagrams (String s, String p) { List<Integer> ans = new ArrayList <>(); if (p.length() > s.length() ) return ans; int [] p_map = new int [26 ]; int [] win_map = new int [26 ]; for (int i=0 ; i<p.length(); ++i){ p_map[p.charAt(i)-'a' ] +=1 ; win_map[s.charAt(i)-'a' ] +=1 ; } if (Arrays.equals(p_map, win_map)) ans.add(0 ); for (int i=p.length();i<s.length(); ++i){ win_map[s.charAt(i-p.length())-'a' ]--; win_map[s.charAt(i) -'a' ]++; if (Arrays.equals(p_map, win_map)) ans.add(i-p.length()+1 ); } return ans; } }
窗口大小固定,往右滑动。
在比较窗口内字符和p字符时,通过Arrays.equals(p_map, win_map)
线性遍历每一个位置是否相同,代价太大。能否优化?
(2)优化比较
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 class Solution { public List<Integer> findAnagrams (String s, String p) { List<Integer> ans = new ArrayList <>(); if (p.length() > s.length() ) return ans; int [] count = new int [26 ]; int diff = 0 ; for (int i=0 ; i<p.length(); ++i){ count[s.charAt(i)-'a' ] +=1 ; count[p.charAt(i)-'a' ] -=1 ; } for (int cnt:count){ if (cnt!=0 ) diff++; } if (diff==0 ) ans.add(0 ); for (int i=p.length();i<s.length(); ++i){ if (count[s.charAt(i-p.length())-'a' ]==1 ){ diff--; }else if (count[s.charAt(i-p.length())-'a' ]==0 ){ diff++; } count[s.charAt(i-p.length())-'a' ]--; if (count[s.charAt(i)-'a' ]==-1 ){ diff--; }else if (count[s.charAt(i)-'a' ]==0 ){ diff++; } count[s.charAt(i)-'a' ]++; if (diff==0 ) ans.add(i-p.length()+1 ); } return ans; } }
为了减少比较两个数组的遍历次数,我们维护一个diff变量表示两个数组中的差异数量。当diff为零,表示两个数组一摸一样。此时,可用一个数组count作为两个数组的差值,差值为零自然两个数组相同。
当窗口滑动时,我们知道右侧字母增加,左侧字母减少。右侧字母增加时,如果此位置差值为-1,那么窗口滑动的结果是使得此位置的差值为0,那么diff减少。
在收集答案时,只需判断一次diff是否为零。
最小窗口实战 长度最小子数组 https://leetcode.cn/problems/minimum-size-subarray-sum/
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution { public int minSubArrayLen (int target, int [] nums) { int windowCount = 0 ; int ans = Integer.MAX_VALUE; int l = 0 , r=0 ; while (r<nums.length){ windowCount += nums[r]; while (windowCount>=target){ ans = Math.min(ans, r-l+1 ); windowCount -= nums[l]; l++; } r++; } return ans==Integer.MAX_VALUE?0 :ans; } }
最小滑动窗口入门题了,窗口内状态只需要用一个变量保存,然后在缩小窗口的过程中更新答案。
最小覆盖子串 https://leetcode.cn/problems/minimum-window-substring/
(1)暴力解法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 class Solution { public boolean isCovered (int [] s1, int [] s2) { for (int i=0 ; i<s1.length; ++i) { if (s1[i]<s2[i]) return false ; } return true ; } public int [] initMap(String s, int len){ int [] ret = new int [60 ]; for (int i=0 ; i<len; ++i){ ret[s.charAt(i)-'A' ]++; } return ret; } public String minWindow (String s, String t) { if (s.length()<t.length()) return "" ; int l=0 , r=0 , record_l=0 , record_r=s.length()+1 ; int [] tmap = initMap(t, t.length()); int [] window = new int [60 ]; while (r<s.length()){ window[s.charAt(r)-'A' ]++; while (isCovered(window, tmap)){ if (record_r-record_l > r-l){ record_l = l; record_r = r; } window[s.charAt(l)-'A' ]--; l++; } r++; } return record_r==1 +s.length()?"" :s.substring(record_l, record_r+1 ); } }
(2)比较优化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 class Solution { public int [] initMap(String s, int len){ int [] ret = new int [60 ]; for (int i=0 ; i<len; ++i){ ret[s.charAt(i)-'A' ]--; } return ret; } public String minWindow (String s, String t) { if (s.length()<t.length()) return "" ; int l=0 , r=0 , record_l=0 , record_r=s.length()+1 ; int [] window = initMap(t, t.length()); int cover=0 ; for (int win:window){if (win>=0 ) cover++;} while (r<s.length()){ if (window[s.charAt(r)-'A' ]==-1 ) cover++; window[s.charAt(r)-'A' ]++; while (cover==60 ){ if (record_r-record_l > r-l){ record_l = l; record_r = r; } if (window[s.charAt(l)-'A' ]==0 ) cover--; window[s.charAt(l)-'A' ]--; l++; } r++; } return record_r==1 +s.length()?"" :s.substring(record_l, record_r+1 ); } }
利用一个cover表示字母覆盖情况,那么每个位置都需要覆盖到,满足覆盖条件时cover==数组长度。
cover更新:窗口扩张,右侧位置为 -1时,cover增加;窗口缩小,左侧位置为0时,cover减小。
核心思想:维护比较的差异度!而不是每次重新比较,忽略了历史值的利用。
最大窗口实战 无重复字符的最长子串 https://leetcode.cn/problems/longest-substring-without-repeating-characters/
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Solution { public int lengthOfLongestSubstring (String s) { int i=0 ,j=0 ,ans=0 ; Map<Character, Integer> map = new HashMap <>(); while (j<s.length()){ char ch = s.charAt(j); map.put(ch ,map.getOrDefault(ch ,0 )+1 ); while (map.get(ch)>1 ){ char d_ch = s.charAt(i); map.put(d_ch ,map.getOrDefault(d_ch ,0 )-1 ); i++; } ans = Math.max(ans, j-i+1 ); j++; } return ans; } }
参考:
作者:labuladong 链接:https://leetcode.cn/problems/find-all-anagrams-in-a-string/solutions/9749/hua-dong-chuang-kou-tong-yong-si-xiang-jie-jue-zi-/
作者:HelloPGJC 链接:https://leetcode.cn/problems/fruit-into-baskets/solutions/1437444/shen-du-jie-xi-zhe-dao-ti-he-by-linzeyin-6crr/