算法<C++>——双指针 | 滑动窗口

一、背景

定长滑窗套路:

窗口右端点在 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 变量):

  1. 初始化 ansLeft=−1, ansRight=m,用来记录最短子串的左右端点,其中 m 是 s 的长度。
  2. 用一个哈希表(或者数组)cntT 统计 t 中每个字母的出现次数。
  3. 初始化 left=0,以及一个空哈希表(或者数组)cntS,用来统计 s子串中每个字母的出现次数。
  4. 初始化 less 为 t 中的不同字母个数。
  5. 遍历 s,设当前枚举的子串右端点为 right,把字母c=s[right] 的出现次数加一。加一后,如果 cntS[c]=cntT[c],说明 c 的出现次数满足要求,把 less 减一。
  6. 如果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 中该字母的出现次数为止。
  1. 最后,如果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;
    }
};
相关推荐
芥子沫7 小时前
《人工智能基础》[算法篇3]:决策树
人工智能·算法·决策树
mit6.8247 小时前
dfs|位运算
算法
保持低旋律节奏7 小时前
算法——二叉树、dfs、bfs、适配器、队列练习
算法·深度优先·宽度优先
Y200309167 小时前
U-net 系列算法总结
人工智能·算法·目标跟踪
代码不停7 小时前
Java二分算法题目练习
java·算法
等一个自然而然的晴天~7 小时前
晴天小猪历险记之Hill---Dijkstra算法
算法
Brookty7 小时前
【算法】位运算| & ^ ~ -n n-1
学习·算法·leetcode·位运算
.格子衫.7 小时前
023数据结构之线段树——算法备赛
java·数据结构·算法
TT哇7 小时前
【BFS 解决 FloodFill 算法】1. 图像渲染(medium)
算法·宽度优先