滑动窗口算法的主要代码框架如下
// s 是所给的字符串或者其他数组
// t 是目标值
func slidingWindow(s, t string) {
need := make(map[byte]int)
window := make(map[byte]int)
for i:=0;i<len(t);i++{
need[t[i]] ++
}
left, right := 0, 0
valid := 0
// s不一定是字符串,还可以是其他类型的数组
// 如果右指针还没有到达s的终点
for right < len(s){
// c 是需要移进窗口的字符
c := s[right]
// 右移窗口
right ++
// 进行窗口内的数据更新
// do something here
// 判断窗口是否需要收缩
// 这种情况下是因为当前窗口满足问题所给需求
// 但是需要获取到最小的值
for window 需要收缩{
d := s[left]
left ++
// 进行窗口内数据的更新
// do something here
}
}
}
下面以例子来进行讲解
1.最小覆盖子串
如果我们使用暴力解法,伪代码如下
for i:=0;i<len(s);i++{
for j:=i+1;j<len(t);j++{
if s[i:j] 包含 t中的所有字母{
更新答案
}
}
}
但是上述的这个算法时间复杂度比较高,如果采用滑动窗口进行解题,时间复杂度会大大降低
滑动窗口的思路如下:
- 在字符串S中使用左右指针,初始化left=right=0,把索引左闭右开区间[left, right)称为一个窗口
- 不断增加right指针扩大窗口[left, right),直到窗口中的字符串符合要求(包含了T中的所有字符)
- 停止增加right,增加left不断缩小窗口,更新数据,直到窗口中的字符串不再符合要求
- 重复第2,3步,直到right到达字符串的尽头
上面思路中,其实第2步在寻找可行解,而第3步中不断优化解,最终找到最优解,也就是最小覆盖子串
needs用来标记T中字符出现次数,也就是我们需要的次数,window标记窗口中相应字符已经出现的个数。
同时要注意left和right所构成的区间是左闭右开的,所以初始条件下窗口中没有任何元素。
valid变量表示窗口中满足条件的字符个数,如果valid和需要的字符种类数相同,则说明窗口已经满足条件,已经完全覆盖目标串
func minWindow(s string, t string) string {
needs := make(map[byte]int)
windows := make(map[byte]int)
left, right := 0, 0
valid := 0
// 长度的最大值
const MAX int = 10e5
// 保存长度
length := MAX
// 保存字符串起始索引
start := 0
// 将需要的次数保存到needs中
for i := 0; i < len(t); i++ {
needs[t[i]]++
}
for right < len(s) {
c := s[right]
right++
// 右指针移动一位
// 并且更新相关数据
if _, ok := needs[c]; ok {
windows[c]++
if windows[c] == needs[c] {
valid++
}
}
// 向右移动一位之后满足题目所给的条件
for valid == len(needs) {
// 更新数据
if right-left < length {
length = right - left
start = left
}
d := s[left]
left++
if _, ok := needs[d]; ok {
if windows[d] == needs[d] {
valid--
}
windows[d]--
}
}
}
if length == MAX {
return ""
} else {
return s[start : start+length]
}
}
2.字符串排列
一种最简单的方法就是将T的排列全部穷举出来,然后去S中寻找,但是这个时间复杂度很高,全排列就需要n!的复杂度,不可取。
这道题目也可以使用滑动窗口进行求解,按照滑动窗口算法框架,我们可以明确当窗口中的字符串包含t中的所有字符串时,我们需要对窗口进行压缩
代码在上个题目上进行改写即可
func checkInclusion(s1 string, s2 string) bool {
needs := make(map[byte]int)
windows := make(map[byte]int)
left, right := 0, 0
valid := 0
// 将需要的次数保存到needs中
for i := 0; i < len(s1); i++ {
needs[s1[i]]++
}
for right < len(s2) {
c := s2[right]
right++
// 右指针移动一位
// 并且更新相关数据
if _, ok := needs[c]; ok {
windows[c]++
if windows[c] == needs[c] {
valid++
}
}
// 向右移动一位之后满足题目所给的条件
for right - left >= len(s1) {
if valid == len(needs){
return true
}
d := s2[left]
left++
if _, ok := needs[d]; ok {
if windows[d] == needs[d] {
valid--
}
windows[d]--
}
}
}
return false
}
当然还有其他的题目,比如LeetCode 438题, LeetCode 剑指offer48题等题目都可以按照滑动窗口进行求解
总结
滑动窗口算法适合解决最小子串问题,这个算法最主要的是要明确:
- 右指针向右移动的时候如何更新数据
- 什么时候窗口中的数据满足题目的要求,进行窗口压缩
- 左指针向左移动的时候如何更新数据