一、滑动窗口的常见问题分析
-
问题
给定一个含有 n 个正整数的数组和一个正整数 target 。
找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [numsl, numsl+1, …, numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。
-
问题分析 (滑动窗口问题的解题思路)
- 当看到题目要求==在数组中找到符合某一条件的连续子数组==时,优先想到使用滑动窗口!!!
- 使用滑动窗口分析问题,弄清楚以下步骤即可
- 首先,弄清楚窗口内的元素需要符合什么条件,当未达到条件时,让窗口一直扩张
- 当窗口内元素符合条件时,就可以获取窗口的大小,同时开始从窗口左侧缩小窗口,从而开始下一个窗口的查找。(难点就在于窗口左侧应该如何放置)
- 找到所有符合条件的窗口,然后比较得到符合题意的窗口,返回即可。
-
本题解决方案
- 窗口内的元素需要满足的条件为:窗口内的元素和>=target
- 当窗口内元素符合条件后,窗口左侧只需要往右移动,直到窗口内元素不满足条件为止即可
func minSubArrayLen(target int, nums []int) int{ left := 0 sum := 0 //用于计算窗口内的元素和 min := len(nums)+1 for j:=0;j<len(nums);j++{ //j为窗口右边界,当条件为满足时,窗口一直扩张 sum += nums[j] for sum >= 7{ // 找到窗口内元素需要满足的条件 if min > j - left + 1{ // 获取窗口长度 min = j - left + 1 } sum -= nums[left] // 缩小窗口 left++ } } if min > len(nums){ // 如果没找到符合条件的窗口 return 0 } return min }
二.进阶问题:限定窗口内的元素种类问题
2.1 水果成篮问题
-
问题:904. 水果成篮
-
问题分析
- 这道题从题目来看,在一数组中找符合条件的最大子数组,显然符合使用滑动窗口法的条件
- 首先确定窗口内元素的条件,窗口内的元素,要满足只有两种类别,即相同的数字只能有两类
- 当第三类元素出现在窗口内部时,就可以获取窗口长度,然后考虑窗口左侧边界的位置了。
- 当窗口内出现第三个元素时,左侧边界移动到什么位置呢,当然是要移动到窗口内仅有两个种类的时候了,因此我们是不是应该用一个数据结构来记录窗口内每种元素的个数,当移动窗口左边界时,让对应种类的个数减一,一直到某个种类的元素个数为0,就可以令下一个位置为窗口的新左边界了
- 每个种类对应一个数量,典型的key-value键值对类型,因此可以使用hashMap结构。
-
解决方案
func totalFruit(fruits []int) int{ left := 0 max := -1 kind := make(map[int]int) for right:=0;right<len(fruits);right++{ kind[fruits[right]]++ // 记录每个种类的水果个数 for len(kind) > 2{ // 如果篮子里大于2种水果了 if max < right-left { // 获取窗口长度 max = right - left } // 调整窗口左边界位置 kind[fruits[left]]-- if kind[fruits[left]] == 0{ // 当某种水果个数为0时,从篮子种删除该水果 delete(kind,fruits[left]) } left++ } } if max < right -left{ // 如果一直到数组尾端都符合条件,则循环中不会进入判断,因此在末尾还要判断一次 max = right-left } return max }
2.2 最小覆盖字串问题
-
问题:76. 最小覆盖子串
-
问题分析
- 字符串可以看成特殊的数组,该题寻找最小子串,显然也可以使用滑动窗口法
- 这题与上面一题有着相同点,那就是其窗口内的元素要满足一定的种类,此外,本题还要满足每种种类的元素个数
- 首先,我们可以定义一个hashMap(mapT)来记录t串的字符种类与每种字符的个数
- 其次,我们可以再定义一个hashMap(mapS)来记录窗口内已经满足条件的字符种类,类似于上一题的篮子
- 接下来就是滑动窗口法解决问题
- 窗口内的元素应该满足的条件为,len(mapS)==len(mapT),即篮子里要包含t串的所有字符
- 找到窗口后,获取窗口元素的长度,随后调整左边界,调整方法去上一题类似
-
解决方案
func minWindow(s string, t string) string{ left,right := 0,0 min := len(s)+1 mapT := make(map[byte]int) // 记录t串的字符种类及个数 mapS := make(map[byte]int) // 将满足条件的字符放入篮子 result := "" for _,v := range t{ // 记录t串的字符种类及个数 mapT[byte(v)]++ } for ;right < len(s);right++{ if _,ok := mapT[s[right]];ok{ // 判断是否是t串中有的字母,如果不是直接跳过 mapT[s[right]]-- if mapT[s[right]] <= 0{ // 判断个数是否满足,如果个数也满足了,说明该字符已经符合条件,放入篮子 mapS[s[right]] = right } } for len(mapS) == len(mapT){ //当窗口内容符合条件 if min > right - left + 1{ // 比较获取最小子串 min = right - left + 1 result = s[left:right+1] // 左闭右开区间 } // 移动左边界,即将其中一个满足条件的字符从篮子里剔除,谁先满足就剔除谁 if _,ok := mapT[s[left]];ok{ //与t串字符相匹配,则数量加1 mapT[s[left]]++ } if mapT[s[left]] > 0{ // 当数量大于0,说明此时篮子里的该字符个数已不满足t串,删除该字符 delete(mapS,s[left]) } left++ } } if min < right - left + 1{ // 如果最后都满足窗口,再判断一次 min = right - left + 1 result = s[left:right+1] } return result }
三、寻找字符串中所有字母的异位词
-
分析:
- 首先,这道题是要在一个字符串中找满足条件的连续子串,满足滑动窗口法的前提
- 那么这里的窗口的条件是什么呢?
- 一个是窗口内的字母必须是p包含的字母
- 二是窗口的长度是固定的,即p的长度
- 我们可以使用一个hash结构来记录p的字母,然后调节窗口右边界扩张窗口,一旦有某个字母不符合条件了,说明当前窗口一定是不符合条件的,因此我们要调节窗口的左边界,把不符合条件的元素排除出去,保证我们窗口内的元素一定是满足条件的
- 当窗口长度变为p的长度时,我们就可以通过左边界获取窗口的起始索引了
-
解决方案
//ps:该解题代码是参考leetcode题解下@Edward Elric用户的评论而写的,我认为这位大佬这道题的题解比官方的简洁的多,respect! func findAnagrams(s string,p string)[]int{ cnt := [26]int{} left,right := 0,0 ans := make([]int,0) for _,ch := range p{ //记录p cnt[ch-'a']++ } for right < len(s){ if cnt[s[right]-'a'] > 0{ // 满足条件1 cnt[s[right]-'a'] -- right++ if right - left == len(p){ ans = append(ans,left) } }else{ // 不满足条件1,立刻调整窗口,保证窗口内元素时刻满足条件1 cnt[s[left]-'a']++ left++ } } return ans }
- 这道题与前面两题的不同之处在于:
- 前面两题我们是在窗口未满足条件时扩张窗口,满足条件后调整窗口,而本题则是在不满足条件时调整窗口。
- 前面两题的窗口长度是未知的,而本题的窗口是定长的
- 这道题与前面两题的不同之处在于:
总结
- 遇到在数组种找一个==满足一定条件的连续子数组==时,考虑使用滑动窗口法。
- 滑动窗口,优先找到窗口内元素需要满足的条件,以此作为判断的入口
- 窗口内元素达到条件后,要想办法调整窗口左边界,更新窗口,使得更新后的窗口不满足条件
- 如果子数组要求的元素种类有限制,即所谓的篮子问题,可以借助hashMap作为篮子,以map的长度作为窗口条件。