3、此时,我们停止增加 right,转而不断增加 left 指针缩小窗口 [left, right),直到窗口中的字符串不再符合要求(不包含 T 中的所有字符了)。同时,每次增加 left,我们都要更新一轮结果。
4、重复第 2 和第 3 步,直到 right 到达字符串 S 的尽头。
这个思路其实也不难,第 2 步相当于在寻找一个「可行解」,然后第 3 步在优化这个「可行解」,最终找到最优解,也就是最短的覆盖子串。左右指针轮流前进,窗口大小增增减减,窗口不断向右滑动,这就是「滑动窗口」这个名字的来历。
下面画图理解一下,needs 和 window 相当于计数器,分别记录 T 中字符出现次数和「窗口」中的相应字符的出现次数。
初始状态:
增加 right,直到窗口 [left, right] 包含了 T 中所有字符:
现在开始增加 left,缩小窗口 [left, right]:
直到窗口中的字符串不再符合要求,left 不再继续移动:
之后重复上述过程,先移动 right,再移动 left…… 直到 right 指针到达字符串 S 的末端,算法结束。
如果你能够理解上述过程,恭喜,你已经完全掌握了滑动窗口算法思想。现在我们来看看这个滑动窗口代码框架怎么用:
首先,初始化 window 和 need 两个哈希表,记录窗口中的字符和需要凑齐的字符:
var need,window map[char]int for _,c := range t{ need[c]++ }然后,使用 left 和 right 变量初始化窗口的两端,不要忘了,区间 [left, right) 是左闭右开的,所以初始情况下窗口没有包含任何元素:
left := 0 right := 0 valid := 0 for right < len(s){ // 开始滑动 }其中 valid 变量表示窗口中满足 need 条件的字符个数,如果 valid 和 need.size 的大小相同,则说明窗口已满足条件,已经完全覆盖了串 T。
现在开始套模板,只需要思考以下四个问题:
1、当移动 right 扩大窗口,即加入字符时,应该更新哪些数据?
2、什么条件下,窗口应该暂停扩大,开始移动 left 缩小窗口?
3、当移动 left 缩小窗口,即移出字符时,应该更新哪些数据?
4、我们要的结果应该在扩大窗口时还是缩小窗口时进行更新?
如果一个字符进入窗口,应该增加 window 计数器;如果一个字符将移出窗口的时候,应该减少 window 计数器;当 valid 满足 need 时应该收缩窗口;应该在收缩窗口的时候更新最终结果。
下面是完整代码:
func minWindow(s string, t string) string{ need, window := map[byte]int{}, map[byte]int{} // go中无char.还有注意不能只声明,不创建 for i:=0;i<len(t);i++{ // 使用range遍历得到是rune,使用t[i]得到的是byte need[t[i]]++ } left := 0 right := 0 valid := 0 // 记录最小覆盖子串的起始索引及长度 start := 0 temp := math.MaxInt32 // 保存长度 for right < len(s){ // c是将移入窗口的字符 c := s[right] // 右移窗口 right++ // 进行窗口内数据的一系列更新 if need[c]!=0{ window[c]++ if window[c] == need[c]{ valid++ } } // 判断左侧窗口是否要收缩 for valid == len(need){ // 在这里更新最小覆盖字串 if right - left < temp{ start = left temp = right - left } // d是将移除窗口的字符 d := s[left] // 左移窗口 left++ // 进行窗口内数据的一系列更新 if need[d]!=0{ if window[d]==need[d]{ valid-- } window[d]-- } } } // 返回最小覆盖字串 if temp == math.MaxInt32{ return "" }else{ return s[start:start+temp] } }