第一章--核心套路篇 之 滑动窗口算法框架

滑动窗口算法的主要代码框架如下

// 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.最小覆盖子串

LeetCode 76题

如果我们使用暴力解法,伪代码如下

for i:=0;i<len(s);i++{
  for j:=i+1;j<len(t);j++{
    if s[i:j] 包含 t中的所有字母{
      更新答案
    }
  }
}

但是上述的这个算法时间复杂度比较高,如果采用滑动窗口进行解题,时间复杂度会大大降低

滑动窗口的思路如下:

  1. 在字符串S中使用左右指针,初始化left=right=0,把索引左闭右开区间[left, right)称为一个窗口
  2. 不断增加right指针扩大窗口[left, right),直到窗口中的字符串符合要求(包含了T中的所有字符)
  3. 停止增加right,增加left不断缩小窗口,更新数据,直到窗口中的字符串不再符合要求
  4. 重复第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.字符串排列

LeetCode

一种最简单的方法就是将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题等题目都可以按照滑动窗口进行求解

总结

滑动窗口算法适合解决最小子串问题,这个算法最主要的是要明确:

  • 右指针向右移动的时候如何更新数据
  • 什么时候窗口中的数据满足题目的要求,进行窗口压缩
  • 左指针向左移动的时候如何更新数据
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值