滑动窗口

滑动窗口

时间复杂度

算法的时间复杂度O(N),其中 N 是输入字符串/数组的长度。

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

大致逻辑

int left = 0, right = 0;

while (right < s.size()) {
    // 增大窗口
    window.add(s[right]);
    right++;
    
    while (window needs shrink) {
        // 缩小窗口
        window.remove(s[left]);
        left++;
    }
}

滑动窗口算法的代码框架,以后遇到相关的问题,就默写出来如下框架然后改三个地方就行,还不会出 bug

/* 滑动窗口算法框架 */
void slidingWindow(string s) {
    unordered_map<char, int> window;
    
    int left = 0, right = 0;
    while (right < s.size()) {
        // c 是将移入窗口的字符
        char c = s[right];
        // 增大窗口
        right++;
        // 进行窗口内数据的一系列更新
        ...

        /*** debug 输出的位置 ***/
        // 注意在最终的解法代码中不要 print
        // 因为 IO 操作很耗时,可能导致超时
        printf("window: [%d, %d)\n", left, right);
        /********************/
        
        // 判断左侧窗口是否要收缩
        while (window needs shrink) {
            // d 是将移出窗口的字符
            char d = s[left];
            // 缩小窗口
            left++;
            // 进行窗口内数据的一系列更新
            ...
        }
    }
}

其中两处 ... 表示的更新窗口数据的地方,到时候直接往里面填就行了

而且,这两个 ... 处的操作分别是扩大和缩小窗口的更新操作,它们操作是完全对称的。

题目一:最小覆盖子串

​ 给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 "" 。

注意:

  • 对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。
  • 如果 s 中存在这样的子串,我们保证它是唯一的答案。

示例 1:

输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"
解释:最小覆盖子串 "BANC" 包含来自字符串 t 的 'A''B''C'

思路:

1、在字符串 S 中使用双指针中的左右指针技巧,初始化 left = right = 0,把索引左闭右开区间 [left, right) 称为一个「窗口」。

PS:设计为左闭右开区间是最方便处理的。因为这样初始化 left = right = 0 时区间 [0, 0) 中没有元素,但只要让 right 向右移动(扩大)一位,区间 [0, 1) 就包含一个元素 0 了。如果设置为两端都开的区间,那么让 right 向右移动一位后开区间 (0, 1) 仍然没有元素;如果设置为两端都闭的区间,那么初始区间 [0, 0] 就包含了一个元素。这两种情况都会给边界处理带来不必要的麻烦。

2、先不断地增加 right 指针扩大窗口 [left, right),直到窗口中的字符串符合要求(包含了 T 中的所有字符)。

3、此时,我们停止增加 right,转而不断增加 left 指针缩小窗口 [left, right),直到窗口中的字符串不再符合要求(不包含 T 中的所有字符了)。同时,每次增加 left,我们都要更新一轮结果。

4、重复第 2 和第 3 步,直到 right 到达字符串 S 的尽头。

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

下面画图理解一下,needswindow 相当于计数器,分别记录 T 中字符出现次数和「窗口」中的相应字符的出现次数。

初始状态:

增加 right,直到窗口 [left, right) 包含了 T 中所有字符:

现在开始增加 left,缩小窗口 [left, right)

直到窗口中的字符串不再符合要求,left 不再继续移动:

之后重复上述过程,先移动 right,再移动 left…… 直到 right 指针到达字符串 S 的末端,算法结束。

Code:

public static String minWindow(String s, String t) {
		//t的长度大于s的长度,s不可能包含t,直接返回""
		if(t.length()>s.length()) return "";
		//t的长度等于s的长度但t不等于s,s不可能包含t,直接返回""
		if(t.length()==s.length()&&!t.equals(s)) return "";
		
		//记录下t的字符
		HashMap<Character, Integer> need = new HashMap<>();
		//记录[left,right)窗口中有哪些字符
		HashMap<Character, Integer> window = new HashMap<>();
		for(int i=0;i<t.length();i++) {
			need.put(t.charAt(i), need.getOrDefault(t.charAt(i), 0)+1);
		}
		int left=0,right=0;
		int len=s.length();
		int len1=Integer.MAX_VALUE;
		int start=0;
		int count=0;
		while(right<len) {
			//c是将移入窗口的字符
			char c=s.charAt(right);
			//扩大窗口
			right++;
			//进行窗口内数据的一系列跟新操作
			if(need.containsKey(c)) {
				window.put(c, window.getOrDefault(c, 0)+1);
				if (need.get(c)==window.get(c)) {
					count++;
				}
			}
			//判断左窗口是否要收缩
			while(count==need.size()) {
				//判断是否要更新最小覆盖字串
				if(right-left<len1) {
					start=left;
					//因为窗口范围为[left,right),所有长度为right-left
					len1=right-left;
				}
				//d为将移除窗口的字符
				char d=s.charAt(left);
				//缩小窗口
				left++;
				if(need.containsKey(d)) {
					if (need.get(d)==window.get(d)) {
						count--;
					}
					window.put(d, window.getOrDefault(d, 0)-1);
				}
			}
		}
		return len1==Integer.MAX_VALUE?"":s.substring(start,start+len1);
    }

题目二: 字符串的排列

​ 给你两个字符串 s1s2 ,写一个函数来判断 s2 是否包含 s1 的排列。如果是,返回 true ;否则,返回 false

​ 换句话说,s1 的排列之一是 s2 的 子串

思路:

​ 由于排列不会改变字符串中每个字符的个数,所以只有当两个字符串每个字符的个数均相等时,一个字符串才是另一个字符串的排列。

​ 根据这一性质,新建两个数组cnt1cnt2,先用cnt1记录s1中每个字符的个数,再用滑动窗口记录s2中每个字符的个数,遍历过程中如果两数组一样,则返回true,遍历完成也不一样就返回false。

Code:

	public boolean checkInclusion(String s1, String s2) {
		int n = s1.length();
		int m = s2.length();
		if (m < n)
			return false;
		int[] cnt1 = new int[26];
		int[] cnt2 = new int[26];
		for (int i = 0; i < n; i++) {
			cnt1[s1.charAt(i) - 'a']++;
			cnt2[s2.charAt(i) - 'a']++;
		}
//		if(cnt1.equals(cnt2)) 
		if (Arrays.equals(cnt1, cnt2))
			return true;
		int left = 0, right = n;
		while (right < m) {
			cnt2[s2.charAt(right) - 'a']++;
			right++;
			if (right - left > n) {
				cnt2[s2.charAt(left) - 'a']--;
				left++;
//				if(cnt1.equals(cnt2))
				if (Arrays.equals(cnt1, cnt2))
					return true;
			}
		}
		return false;
	}

坑点:

在比较两数组是否相等时,不能用cnt1.equals(cnt2),而要用Arrays.equals(cnt1, cnt2)

数组的三种比较的 ==,a.equals(b),和Arrays.equals(a,b)

若a,b都为数组,则:

  • a==b ,比较的是地址
  • a.equals(b) 比较的是地址
  • Arrays.equals(a, b) 比较的是元素内容

原因:

int没有定义equals,而Object类中的equals方法是用来比较“地址”的。

数组a==数组b,比较的是地址,两个变量的内存地址不一样,也就是说它们指向的对象不 一样。

当比较两个字符串的时候,它使用的是String类下的equals()方法,这个方法比较的是元素内容。

当比较两个数组的值的时候,需要使用Arrays类中的equals()方法。即Arrays.equals(a, b);

题目三:找到字符串中所有字母异位词

​ 给定两个字符串 s 和 p,找到 s 中所有 p 的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。

​ 异位词 指由相同字母重排列形成的字符串(包括相同的字符串)。

思路:

与题目二类似

Code:

	public List<Integer> findAnagrams(String s, String p) {
		ArrayList<Integer> list = new ArrayList<>();
		int n = s.length();
		int m = p.length();
		if (n < m)
			return list;
		int[] cnt1 = new int[26];
		int[] cnt2 = new int[26];
		for (int i = 0; i < m; i++) {
			cnt1[p.charAt(i) - 'a']++;
			cnt2[s.charAt(i) - 'a']++;
		}
		int left = 0, right = m;
		if (Arrays.equals(cnt1, cnt2)) {
			list.add(0);
		}
		while (right < n) {
			cnt2[s.charAt(right) - 'a']++;
			right++;
			if (right - left > m) {
				cnt2[s.charAt(left) - 'a']--;
				left++;
				if (Arrays.equals(cnt1, cnt2)) {
					list.add(left);
				}
			}
		}
		return list;
	}

题目四:无重复字符的最长子串

​ 给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。

Code:

	public int lengthOfLongestSubstring(String s) {
		if (s.length() == 0)
			return 0;
		HashMap<Character, Integer> map = new HashMap<>();
		int res = 0;
		int left = 0, right = 0;
		while (right < s.length()) {
			char c = s.charAt(right);
			right++;
			map.put(c, map.getOrDefault(c, 0) + 1);
			while (map.get(c) > 1) {
				char d = s.charAt(left);
				left++;
				map.put(c, map.getOrDefault(c, 0) - 1);
			}
			res = Math.max(res, right - left);
		}
		return res;
	}

参考文章:

posted @   QING~h  阅读(72)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示

目录导航