一、背景
定长滑窗套路:
窗口右端点在 i 时,由于窗口长度为 k,所以窗口左端点为 i−k+1。
我总结成三步:入-更新-出。
入:下标为 i 的元素进入窗口,更新相关统计量。如果窗口左端点 i−k+1<0,则尚未形成第一个窗口,重复第一步。
更新:更新答案。一般是更新最大值/最小值。 出:下标为 i−k+1 的元素离开窗口,更新相关统计量,为下一个循环做准备。
以上三步适用于所有定长滑窗题目。**
如果用暴力解的话,你需要嵌套 for 循环这样穷举所有子数组,时间复杂度是 O(N2)
cpp
for (int i = 0; i < nums.length; i++) {
for (int j = i; j < nums.length; j++) {
// nums[i, j] 是一个子数组
}
}
滑动窗口算法技巧的思路也不难,就是维护一个窗口,不断滑动,然后更新答案,该算法的大致逻辑如下:
cpp
// 索引区间 [left, right) 是窗口
int left = 0, right = 0;
while (right < nums.size()) {
// 增大窗口
window.addLast(nums[right]);
right++;
while (window needs shrink) {
// 缩小窗口
window.removeFirst(nums[left]);
left++;
}
}
在上述代码中,指针 left, right 不会回退(它们的值只增不减),所以字符串/数组中的每个元素都只会进入窗口一次,然后被移出窗口一次,不会说有某些元素多次进入和离开窗口,所以算法的时间复杂度就和字符串/数组的长度成正比,所以时间复杂度是O(N)阶。
cpp
// 滑动窗口算法伪码框架
void slidingWindow(string s) {
// 用合适的数据结构记录窗口中的数据,根据具体场景变通
// 比如说,我想记录窗口中元素出现的次数,就用 map
// 如果我想记录窗口中的元素和,就可以只用一个 int
auto window = ...
int left = 0, right = 0;
while (right < s.size()) {
// c 是将移入窗口的字符
char c = s[right];
window.add(c);
// 增大窗口
right++;
// 进行窗口内数据的一系列更新
...
// *** debug 输出的位置 ***
printf("window: [%d, %d)\n", left, right);
// 注意在最终的解法代码中不要 print
// 因为 IO 操作很耗时,可能导致超时
// 判断左侧窗口是否要收缩
while (window needs shrink) {
// d 是将移出窗口的字符
char d = s[left];
window.remove(d);
// 缩小窗口
left++;
// 进行窗口内数据的一系列更新
...
}
}
}
二、实操题目
76. 最小覆盖子串 | 力扣 | LeetCode | (hard)
给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 "" 。
注意:
对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。 如果 s 中存在这样的子串,我们保证它是唯一的答案。
示例 1:
cpp
> 输入:s = "ADOBECODEBANC", t = "ABC"
> 输出:"BANC" 解释:最小覆盖子串 "BANC" 包含来自字符串 t
> 的 'A'、'B' 和 'C'。
示例 2:
cpp
> 输入:s = "a", t = "a"
> 输出:"a" 解释:整个字符串 s 是最小覆盖子串。
示例 3:
cpp
> 输入: s = "a", t = "aa"
> 输出: "" 解释: t 中两个字符 'a' 均应包含在 s 的子串中,
> 因此没有符合条件的子字符串,返回空字符串。
提示:
m == s.length n == t.length 1 <= m, n <= 105 s 和 t 由英文字母组成 进阶:你能设计一个在
o(m+n) 时间内解决此问题的算法吗? 题目来源:力扣 76. 最小覆盖子串。
第一次尝试因为没弄清楚涵盖的意思,导致运行错误,初步学习后:
cpp
class Solution {
public:
bool is_cover(int n1[],int n2[]){
for(int i='A';i<='Z';i++){
if(n1[i]<n2[i])
return false;
}
for(int i='a';i<='z';i++){
if(n1[i]<n2[i])
return false;
}
return true;
}
string minWindow(string s, string t) {
int n1[200]{};
int n2[200]{};
int n=s.length(),m=t.length();
for(int i=0;i<m;i++){
n2[t[i]]++;
}
int res_l=-1,res_r=n,l=0;
for(int r=0;r<n;r++){
n1[s[r]]++;
while(is_cover(n1,n2)){
if(r-l<res_r-res_l){
res_l=l;
res_r=r;
}
n1[s[l]]--;
l++;
}
}
return res_l<0?"":s.substr(res_l,res_r-res_l+1);
}
};
灵神优化代码:
用一个变量 less 维护目前子串中有 less 种字母的出现次数小于 t 中字母的出现次数。
具体来说(注意下面算法中的 less 变量):
- 初始化 ansLeft=−1, ansRight=m,用来记录最短子串的左右端点,其中 m 是 s 的长度。
- 用一个哈希表(或者数组)cntT 统计 t 中每个字母的出现次数。
- 初始化 left=0,以及一个空哈希表(或者数组)cntS,用来统计 s子串中每个字母的出现次数。
- 初始化 less 为 t 中的不同字母个数。
- 遍历 s,设当前枚举的子串右端点为 right,把字母c=s[right] 的出现次数加一。加一后,如果 cntS[c]=cntT[c],说明 c 的出现次数满足要求,把 less 减一。
- 如果less=0,说明 cntS 中的每个字母及其出现次数都大于等于 cntT 中的字母出现次数,那么:
- 如果 right−left<ansRight−ansLeft,说明我们找到了更短的子串,更新 ansLeft=left, ansRight=right。
- 把字母 x=s[left] 的出现次数减一。减一前,如果 cntS[x]=cntT[x],说明 x 的出现次数不满足要求,把 less 加一。
- 左端点右移,即 left 加一。
- 重复上述三步,直到 less>0,即 cntS有字母的出现次数小于 cntT 中该字母的出现次数为止。
- 最后,如果ansLeft<0,说明没有找到符合要求的子串,返回空字符串,否则返回下标 ansLeft 到下标 ansRight 之间的子串。
cpp
class Solution {
public:
string minWindow(string s, string t) {
int cnt[128]{};
int less = 0;
for (char c : t) {
if (cnt[c] == 0) {
less++; // 有 less 种字母的出现次数 < t 中的字母出现次数
}
cnt[c]++;
}
int m = s.size();
int ans_left = -1, ans_right = m;
int left = 0;
for (int right = 0; right < m; right++) { // 移动子串右端点
char c = s[right]; // 右端点字母
cnt[c]--; // 右端点字母移入子串
if (cnt[c] == 0) {
// 原来窗口内 c 的出现次数比 t 的少,现在一样多
less--;
}
while (less == 0) { // 涵盖:所有字母的出现次数都是 >=
if (right - left < ans_right - ans_left) { // 找到更短的子串
ans_left = left; // 记录此时的左右端点
ans_right = right;
}
char x = s[left]; // 左端点字母
if (cnt[x] == 0) {
// x 移出窗口之前,检查出现次数,
// 如果窗口内 x 的出现次数和 t 一样,
// 那么 x 移出窗口后,窗口内 x 的出现次数比 t 的少
less++;
}
cnt[x]++; // 左端点字母移出子串
left++;
}
}
return ans_left < 0 ? "" : s.substr(ans_left, ans_right - ans_left + 1);
}
};
作者:灵茶山艾府
567. 字符串的排列 | 力扣 | LeetCode | (medium)
给你两个字符串 s1 和 s2 ,写一个函数来判断 s2 是否包含 s1 的排列。如果是,返回 true ;否则,返回 false 。
换句话说,s1 的排列之一是 s2 的 子串 。
示例 1:
cpp
输入:s1 = "ab" s2 = "eidbaooo"
输出:true 解释:s2 包含 s1 的排列之一 ("ba").
示例 2:
cpp
输入:s1= "ab" s2 = "eidboaoo"
输出:false
提示:
1 <= s1.length, s2.length <= 104 s1 和 s2 仅包含小写字母 题目来源:力扣 567. 字符串的排列。
cpp
class Solution {
public:
bool checkInclusion(string s1, string s2) {
int n = s1.length();
if (n > s2.length())
return false;
array<int, 26> cnt_s1;
for (char c : s1) {
cnt_s1[c - 'a']++;
}
array<int, 26> cnt_s2;
for (int i = 0; i < s2.length(); i++) {
// in
cnt_s2[s2[i] - 'a']++;
if (i + 1 < n)
continue;
//update
if (cnt_s1 == cnt_s2)
return true;
//out
cnt_s2[s2[i - n + 1] - 'a']--;
}
return false;
}
};
灵神优化
把每次循环的 cntS1 == cntT 从 O(∣Σ∣) 优化成 O(1)。
cpp
class Solution {
public:
bool checkInclusion(string s1, string s2) {
int m = s1.size();
if (m > s2.size()) {
return false;
}
int cnt[26]{};
int less = 0;
for (char c : s1) {
if (cnt[c - 'a'] == 0) {
less++;
}
cnt[c - 'a']++;
}
for (int i = 0; i < s2.size(); i++) {
// 1. 进入窗口
int c = s2[i] - 'a';
cnt[c]--;
if (cnt[c] == 0) {
less--;
}
if (i < m - 1) { // 窗口大小不足 m
continue;
}
// 2. 判断子串 t 的每种字母的出现次数是否均与 s1 的相同
if (less == 0) {
return true;
}
// 3. 离开窗口,为下一个循环做准备
int out = s2[i - m + 1] - 'a';
if (cnt[out] == 0) {
less++;
}
cnt[out]++;
}
return false;
}
};
作者:灵茶山艾府
1456. 定长子串中元音的最大数目 | 力扣 | LeetCode | (medium)
给你字符串 s 和整数 k 。
请返回字符串 s 中长度为 k 的单个子字符串中可能包含的最大元音字母数。
英文中的 元音字母 为(a, e, i, o, u)。
示例1:
cpp
输入:s = "abciiidef", k = 3
输出:3
解释:子字符串 "iii" 包含 3 个元音字母。
示例2:
cpp
输入:s = "aeiou", k = 2
输出:2
解释:任意长度为 2 的子字符串都包含 2 个元音字母。
用滑动窗口尝试一下
cpp
class Solution {
public:
int maxVowels(string s, int k) {
int n=s.length(),count=0,ans=INT_MIN;
for(int i=0;i<n;i++){
if(s[i]=='a'||s[i]=='o'||s[i]=='e'||s[i]=='u'||s[i]=='i')
count++;//in
int left=i-k+1;
if(left<0) continue;
ans=max(ans,count);//update
char out=s[left];
if(out=='a'||out=='o'||out=='i'||out=='e'||out=='u')
count--;//out
}
return ans;
}
};
438. 找到字符串中所有字母异位词 | 力扣 | LeetCode | (medium)
给定两个字符串 s 和 p,找到 s 中所有 p 的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。
示例1:
cpp
输入: s = "cbaebabacd", p = "abc"
输出: [0,6]
解释:
起始索引等于 0 的子串是 "cba", 它是 "abc" 的异位词。
起始索引等于 6 的子串是 "bac", 它是 "abc" 的异位词
示例2:
cpp
输入: s = "abab", p = "ab"
输出: [0,1,2]
解释:
起始索引等于 0 的子串是 "ab", 它是 "ab" 的异位词。
起始索引等于 1 的子串是 "ba", 它是 "ab" 的异位词。
起始索引等于 2 的子串是 "ab", 它是 "ab" 的异位词。
代码尝试:
cpp
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
vector<int> res;
int n=s.length(),m=p.length();
array<int,26> n1;
array<int,26> n2;
for(int i=0;i<m;i++){
n1[p[i]-'a']++;
}
for(int i=0;i<n;i++){
n2[s[i]-'a']++;
int left=i-m+1;
if(left<0) continue;
if(n1==n2){
res.push_back(left);
}
n2[s[left]-'a']--;
}
return res;
}
};
3. 无重复字符的最长子串 | 力扣 | LeetCode | (medium)
给定一个字符串 s ,请你找出其中不含有重复字符的 最长 子串 的长度
示例1:
cpp
输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。注意 "bca" 和 "cab" 也是正确答案。
示例2:
cpp
输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
之前没接触哈希表,第一次想到用数组尝试:
cpp
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int n=s.length(),left=0,res=0;
array<int,26> m;
for(int i=0;i<n;i++){
m[s[i]-'a']++;
while((m[s[i]-'a'])>1){
m[s[left]-'a']--;
left++;
}
res=max(res,i-left+1);
}
return res;
}
};
总结到,上面的代码仅限小写字母场景。
使用哈希表后:
cpp
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int n = s.length(), res = 0, left = 0;
unordered_map<char, int> cnt;
for (int right = 0; right < n; right++) {
cnt[s[right]]++;
while (cnt[s[right]] > 1) {
cnt[s[left]]--;
left++;
}
res = max(res, right - left + 1);
}
return res;
}
};