滑动窗口
滑动窗口
时间复杂度
算法的时间复杂度是 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 步在优化这个「可行解」,最终找到最优解,也就是最短的覆盖子串。左右指针轮流前进,窗口大小增增减减,窗口不断向右滑动,这就是「滑动窗口」这个名字的来历。
下面画图理解一下,needs
和 window
相当于计数器,分别记录 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);
}
题目二: 字符串的排列
给你两个字符串 s1
和 s2
,写一个函数来判断 s2
是否包含 s1
的排列。如果是,返回 true
;否则,返回 false
。
换句话说,s1
的排列之一是 s2
的 子串
思路:
由于排列不会改变字符串中每个字符的个数,所以只有当两个字符串每个字符的个数均相等时,一个字符串才是另一个字符串的排列。
根据这一性质,新建两个数组cnt1
和cnt2
,先用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;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)