LeetCode题解:套用模板 解决滑动 窗口问题
自大学开始,我便陆陆续续的学习一些 算法和数据结构 方面的内容,同时也开始在一些平台刷题,也会参加一些大大小小的算法竞赛。但是平时刷题缺少目的性、系统性,最终导致算法方面进步缓慢。最终,为了自己的未来,我决定开始在LeetCode上进行系统的学习和练习,同时将刷题的轨迹整理记录,分享出来与大家共勉。
参考资料: 我写了套框架,把滑动窗口算法变成了默写题
参考资料: LeetCode社区官方提供的思路/题解 以及 评论区/题解区各路大神提供的思路/答案
目录标题
- LeetCode题解:套用模板 解决滑动 窗口问题
- 000.模板框架
- [076. 最小覆盖子串](https://leetcode-cn.com/problems/minimum-window-substring/)
- [567. 字符串的排列](https://leetcode-cn.com/problems/permutation-in-string/)
- [438. 找到字符串中所有字母异位词](https://leetcode-cn.com/problems/find-all-anagrams-in-a-string/)
- [003. 无重复字符的最长子串](https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/)
000.模板框架
Java版本
class Solution{
/* 滑动窗口算法框架 */
void slidingWindow(String s, String t) {
// 字符需要出现的次数
Map<Character, Integer> needs = new HashMap<Character, Integer>();
// 滑动窗口中字符出现的次数
Map<Character, Integer> window = new HashMap<Character, Integer>();
for (char ch : t.toCharArray())
needs.put(ch, needs.getOrDefault(ch, 0) + 1);
int left = 0, right = 0;
int valid = 0;
while (right < s.length()) {
// c 是将移入窗口的字符
char c = s.charAt(right);
// 如果满足条件就将 c移入窗口中,并进行窗口内数据的一系列更新
if (...)
...
// 右移窗口
right++;
/*** debug 输出的位置 ***/
System.out.println("window: [" + left + ", " + right + ")\n");
/********************/
// 判断左侧窗口是否要收缩
while (window needs shrink){
// d 是将移出窗口的字符
char d = s.charAt(left);
// 如果满足一定条件,进行窗口内数据的一系列更新,使得退出这个 while循环
if (...)
...
// 左移窗口
left++;
}
}
}
}
详细参数说明:
初始化window
和need
两个哈希表,记录窗口中的字符和需要凑齐的字符:
unordered_map<char, int> need, window;
for (char c : t) need[c]++;
然后,使用left
和right
变量初始化窗口的两端,不要忘了,区间[left, right)
是左闭右开的,所以初始情况下窗口没有包含任何元素:
int left = 0, right = 0;
int valid = 0;
while (right < s.size()) {
// 开始滑动
}
其中valid
变量表示窗口中满足need
条件的字符个数,如果valid
和need.size
的大小相同,则说明窗口已满足条件,已经完全覆盖了串T
。
现在开始套模板,只需要思考以下四个问题:
**1、**当移动right
扩大窗口,即加入字符时,应该更新哪些数据?
**2、**什么条件下,窗口应该暂停扩大,开始移动left
缩小窗口?
**3、**当移动left
缩小窗口,即移出字符时,应该更新哪些数据?
**4、**我们要的结果应该在扩大窗口时还是缩小窗口时进行更新?
如果一个字符进入窗口,应该增加window
计数器;如果一个字符将移出窗口的时候,应该减少window
计数器;当valid
满足need
时应该收缩窗口;应该在收缩窗口的时候更新最终结果。
076. 最小覆盖子串
难度: 困难
给你一个字符串 s
、一个字符串 t
。返回 s
中涵盖 t
所有字符的最小子串。如果 s
中不存在涵盖 t
所有字符的子串,则返回空字符串 ""
。
**注意:**如果 s
中存在这样的子串,我们保证它是唯一的答案。
示例:
输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"
题解:
套用模板:15ms
class Solution {
public String minWindow(String s, String t) {
HashMap<Character,Integer> need = new HashMap();
HashMap<Character,Integer> window = new HashMap();
//need存放的不重复的字符出现的次数
for(char c:t.toCharArray())
need.put(c,need.getOrDefault(c,0)+1);
//left,right 表示滑动窗口的左右指针
int left = 0 , right = 0;
//valid表示是否满足了t中的字符,不算重复的
int valid = 0;
//记录最小覆盖子串的起始索引及长度
int start = 0 , len = Integer.MAX_VALUE;
while(right < s.length()){
char c = s.charAt(right);
right++;
//判断取出的字符是否在需要的Map中
if(need.containsKey(c)){
window.put(c,window.getOrDefault(c,0)+1);
if(window.get(c).equals(need.get(c)))
valid++;
}
//判断是否需要收缩(即已经找到了合适的覆盖串)
while(valid == need.size()){
//更新最小覆盖子串
if(right - left < len){
start = left;
len = right - left;
}
char c1 = s.charAt(left);
//左移窗口
left++;
//进行窗口内数据的一系列更新
//如果当前要移动的字符是包含在need中,我们需要进行讨论,如果该字符的次数刚好与我们需要的次数相等,则valid--,并同时更新window中这个值出现的次数
if(need.containsKey(c1)){
if(window.get(c1).equals(need.get(c1)))
valid--;
window.put(c1,window.getOrDefault(c1,0)-1);
}
}
}
return len == Integer.MAX_VALUE ? "" : s.substring(start, start + len);
}
}
范例: 2ms
class Solution {
public String minWindow(String s, String t) {
String res="";
if (s.length() < t.length()) return res;
int[] need = new int[128];
char[] S=s.toCharArray();
int cnt=0;
for (int i = 0; i < t.length(); i++) {
need[t.charAt(i)]++;
}
for (int a : need) {
if (a > 0) cnt ++;
}
for(int i=0,j=0,c=0;i<s.length();i++){
if(need[S[i]]==1) c++;
need[S[i]]--;
while(c==cnt&&need[S[j]]<0) need[S[j++]]++;
if(c==cnt){
if(res==""||res.length()>i-j+1)
res=s.substring(j,i+1);
}
}
return res;
}
}
567. 字符串的排列
难度: 中等
给定两个字符串 s1 和 s2,写一个函数来判断 s2 是否包含 s1 的排列。
换句话说,第一个字符串的排列之一是第二个字符串的子串。
示例:
输入: s1 = "ab" s2 = "eidbaooo"
输出: True
解释: s2 包含 s1 的排列之一 ("ba").
对于这道题的解法代码,基本上和最小覆盖子串一模一样,只需要改变两个地方:
**1、**本题移动left
缩小窗口的时机是窗口大小大于t.size()
时,因为排列嘛,显然长度应该是一样的。
**2、**当发现valid == need.size()
时,就说明窗口中就是一个合法的排列,所以立即返回true
。
至于如何处理窗口的扩大和缩小,和最小覆盖子串完全相同。
题解:
套用模板: 30ms
class Solution {
public boolean checkInclusion(String t, String s) {
HashMap<Character, Integer> need = new HashMap<>();
HashMap<Character, Integer> window = new HashMap<>();
for (char c : t.toCharArray()) {
need.put(c, need.getOrDefault(c, 0) + 1);
}
int left=0,right=0;
int valid=0;
while (right<s.length()){
char c=s.charAt(right);
right++;
if (need.containsKey(c)){
window.put(c,window.getOrDefault(c,0)+1);
if (window.get(c).equals(need.get(c)))
valid++;
}
//更改:判断左侧窗口是否收缩
while (right-left>=t.length()){
//更改:判断是否找到了合适的子串
if (valid==need.size())
return true;
char c1=s.charAt(left);
left++;
if (need.containsKey(c1)) {
if (window.get(c1).equals(need.get(c1)))
valid--;
window.put(c1, window.getOrDefault(c1, 0) - 1);
}
}
}
return false;
}
}
范例:2ms
class Solution {
public boolean checkInclusion(String s1, String s2) {
char[] str = s2.toCharArray();
char[] target = s1.toCharArray();
int sn = str.length;
int tn = target.length;
int left = 0;
int right = 0;
int l = -1;
int r = sn;
int[] winFreq = new int[26];
int[] needFreq = new int[26];
for (char c : target){
needFreq[c - 'a']++;
}
while (right < sn){
int index = str[right] - 'a';
if (needFreq[index] == 0){
right++;
left = right;
tn = target.length;
for (int i = 0; i < 26; i++){
winFreq[i] = 0;
}
continue;
}
winFreq[index]++;
tn--;
while (winFreq[index] > needFreq[index]){
winFreq[str[left++] - 'a']--;
tn++;
}
if (tn == 0){
return true;
}
right++;
}
return false;
}
}
438. 找到字符串中所有字母异位词
难度: 中等
给定一个字符串 s 和一个非空字符串 p,找到 s 中所有是 p 的字母异位词的子串,返回这些子串的起始索引。
字符串只包含小写英文字母,并且字符串 s 和 p 的长度都不超过 20100。
说明:
- 字母异位词指字母相同,但排列不同的字符串。
- 不考虑答案输出的顺序。
示例:
输入:
s: "cbaebabacd" p: "abc"
输出:
[0, 6]
解释:
起始索引等于 0 的子串是 "cba", 它是 "abc" 的字母异位词。
起始索引等于 6 的子串是 "bac", 它是 "abc" 的字母异位词。
题解:
套用模板: 34ms
class Solution438{
public List<Integer> findAnagrams(String s, String t) {
HashMap<Character, Integer> need = new HashMap<>();
HashMap<Character, Integer> window = new HashMap<>();
for (char c : t.toCharArray()) {
need.put(c, need.getOrDefault(c, 0) + 1);
}
int left=0,right=0;
int valid=0;
//更改:记录结果集
List<Integer> res=new ArrayList<>();
while (right<s.length()){
char c=s.charAt(right);
right++;
if (need.containsKey(c)){
window.put(c,window.getOrDefault(c,0)+1);
if (window.get(c).equals(need.get(c)))
valid++;
}
while (right-left>=t.length()){
//更改:记录结果
if (valid==need.size()){
res.add(left);
}
char c1=s.charAt(left);
left++;
if (need.containsKey(c1)) {
if (window.get(c1).equals(need.get(c1)))
valid--;
window.put(c1, window.getOrDefault(c1, 0) - 1);
}
}
}
return res;
}
}
范例: 3ms
class Solution {
public List<Integer> findAnagrams(String s, String p) {
/**
* 固定长度滑动窗口,固定长度为p的长度
* 右侧延伸与左侧收缩同时进行
*用哈希存储字符频率以做比较
*/
int n1 = s.length() , n2 = p.length();
List<Integer> ans = new ArrayList<>();
if(n2 > n1){
return ans;
}
int[] charCount = new int[26];
/*初始化,p里所需字符设定为负数,表示需要满足*/
for(int i = 0 ; i < n2 ; i ++){
charCount[p.charAt(i) - 'a'] --;
}
/*设定固定长度滑动窗口*/
int left = 0 , right = left - 1 + n2;
/*由于窗口长度固定而不是慢慢延伸,需要先把初始窗口字符更新进哈希*/
for(int i = left ; i <= right ; i ++){
charCount[s.charAt(i) - 'a'] ++;
}
/*滑动窗口*/
while(right < n1){
/*哈希全是0时即为满足要求,将left索引加入list*/
boolean isFit = true;
for(int count : charCount){
if(count != 0){
isFit = false;
break;
}
}
if(isFit){
ans.add(left);
}
/*移动窗口,左侧收缩与右侧延伸同时进行*/
right ++;
/*右侧延伸后要判断是否越界*/
if(right < n1){
charCount[s.charAt(right) - 'a'] ++;
}
charCount[s.charAt(left) - 'a'] --;
left ++;
}
return ans;
}
}
003. 无重复字符的最长子串
难度中等4978收藏分享切换为英文接收动态反馈
给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。
示例:
输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
题解:
套用模板: 11ms
class Solution003 {
public int lengthOfLongestSubstring(String s) {
HashMap<Character, Integer> window = new HashMap<>();
int left = 0, right = 0;
int res = 0;
while (right < s.length()) {
char c = s.charAt(right);
right++;
window.put(c, window.getOrDefault(c, 0) + 1);
//判断收缩
while (window.get(c) > 1) {
char c1 = s.charAt(left);
left++;
window.put(c1, window.getOrDefault(c1, 0) - 1);
}
//更新答案
res=Math.max(res,right-left);
}
return res;
}
}
这就是变简单了,连need
和valid
都不需要,而且更新窗口内数据也只需要简单的更新计数器window
即可。
当window[c]
值大于 1 时,说明窗口中存在重复字符,不符合条件,就该移动left
缩小窗口了嘛。
唯一需要注意的是,在哪里更新结果res
呢?我们要的是最长无重复子串,哪一个阶段可以保证窗口中的字符串是没有重复的呢?
这里和之前不一样,要在收缩窗口完成后更新res
,因为窗口收缩的 while 条件是存在重复元素,换句话说收缩完成后一定保证窗口中没有重复嘛。
范例: 2ms
class Solution {
public int lengthOfLongestSubstring(String s) {
//解法一:滑动窗口
char[] arr = s.toCharArray();
int start = 0,end = 0,max = 0;
while(end < arr.length){
int index = start;
while(arr[index] != arr[end] && index < end){
index++;
}
if(index != end){
max = max > (end - start) ? max : (end - start);
start = index + 1;
}
end++;
}
return max > (end - start ) ? max : (end - start);
}
}
作者:耿鬼不会笑
时间:2021年2月