万用框架!一篇文章带你学会滑动窗口算法

框架

Map<Character, Integer> window = new HashMap<>();
int size = s.size();
int left = 0, right = 0;
while(right < size) {
   
	// 增大窗口
	char c = s.charAt(right);
	// 表示增加键为 c 的值,如果 map 中没有这个键,则返回默认值 0,然后加上 1
	window.put(c, window.getOrDefault(c, 0) + 1); // 等同于c++的map[key]++
	right++;
	// 进行窗口内的一系列数据更新
	// ...
	
	// 注意最终代码不要留下print,因为IO很费时间
	System.out.printf("window:[%d, %d]\n", left, right);


	while(window needs shrink) {
   
		// 缩小窗口
		char d = s.charAt(left);
		// 移出去的字符对应需要的次数减一
		window.put(d, window.get(d) - 1);
		left++;
		// 进行窗口内的一系列数据更新
		// ...
	}
}

虽然滑动窗口代码框架中有一个嵌套的 while 循环,但算法的时间复杂度依然是 O(N),其中 N 是输入字符串/数组的长度。

为什么呢?简单来说,指针 left, right 不会回退(它们的值只增不减),所以字符串/数组中的每个元素都只会进入窗口一次,然后被移出窗口一次,不会说有某些元素多次进入和离开窗口,所以算法的时间复杂度就和字符串/数组的长度成正比。

另外,Java 中的 Integer 和 String 这种包装类不能直接用 == 进行相等判断,而应该使用类的 equals 方法。

通用思路

在这里插入图片描述
第 2 步相当于在寻找一个「可行解」,然后第 3 步在优化这个「可行解」,最终找到最优解,也就是最短的覆盖子串。左右指针轮流前进,窗口大小增增减减,窗口不断向右滑动,这就是「滑动窗口」这个名字的来历。

框架的使用方法

  1. 初始化window和need两个哈希表,记录窗口中的字符和需要凑齐的字符。
unordered_map<char, int> need, window;
for (char c : t) need[c]++;
  1. 然后,使用 leftright 变量初始化窗口的两端,不要忘了,区间 [left, right) 是左闭右开的,所以初始情况下窗口没有包含任何元素:
int left = 0, right = 0;
int valid = 0; 
while (right < s.size()) {
   
    // 开始滑动
}

其中 valid 变量表示窗口中满足 need 条件的字符个数,如果 validneed.size 的大小相同,则说明窗口已满足条件,已经完全覆盖了串 T

要思考的问题

1、什么时候应该移动 right 扩大窗口?窗口加入字符时,应该更新哪些数据?

2、什么时候窗口应该暂停扩大,开始移动 left 缩小窗口?从窗口移出字符时,应该更新哪些数据?

3、我们要的结果应该在扩大窗口时还是缩小窗口时进行更新?

如果一个字符进入窗口,应该增加 window 计数器;如果一个字符将移出窗口的时候,应该减少 window 计数器;当 valid 满足 need 时应该收缩窗口;应该在收缩窗口的时候更新最终结果。

例题

最小覆盖子串

76. 最小覆盖子串 - 力扣(LeetCode)

public String minWindow(String s, String t) {
   
    // 用于记录需要的字符和窗口中的字符及其出现的次数
    Map<Character, Integer> need = new HashMap<>();
    Map
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值