算法滑动窗口

滑动窗口算法可以用以解决数组/字符串的子元素问题,它可以将嵌套的循环问题,转换为单循环问题,降低时间复杂度

 

简而言之,滑动窗口算法在一个特定大小的字符串或数组上进行操作,而不在整个字符串和数组上操作,这样就降低了问题的复杂度,从而也达到降低了循环的嵌套深度。

可以用来解决一些查找满足一定条件的连续区间的性质(长度等)的问题。由于区间连续,因此当区间发生变化时,可以通过旧有的计算结果对搜索空间进行剪枝,这样便减少了重复计算,降低了时间复杂度。往往类似于“ 请找到满足 xx 的最 x 的区间(子串、子数组)的 xx ”这类问题都可以使用该方法进行解决。

转载labuladong:

力扣

fucking-algorithm/滑动窗口技巧.md at master · labuladong/fucking-algorithm · GitHub

适用题型:

遇到子串问题,首先想到用滑动窗口技巧。(意味着一个串长度一定小与另一个串)

题型分为窗口大小不断变化窗口大小固定两种题型。

窗口大小变化

思路:

滑动窗口算法的思路是这样:

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

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

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

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

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

上述过程可以简单地写出如下伪码框架:
string s, t;
// 在 s 中寻找 t 的「最小覆盖子串」
int left = 0, right = 0;
string res = s;

while(right < s.size()) {
    window.add(s[right]);
    right++;
    // 如果符合要求,移动 left 缩小窗口
    while (window 符合要求) {
        // 如果这个窗口的子串更短,则更新 res
        res = minLen(res, window);
        window.remove(s[left]);
        left++;
    }
}
return res;

注:

1,输入两个字符串的需要needs和window两个hash计数器。输入为一个字符串的只需要一个window计数器

2,shrink条件,即更新结果条件两个地方要注意。

一, 76. 最小覆盖子串

可以用两个哈希表当作计数器解决。用一个哈希表 needs 记录字符串 t 中包含的字符及出现次数,用另一个哈希表 window 记录当前「窗口」中包含的字符及出现的次数,如果 window 包含所有 needs 中的键,且这些键对应的值都大于等于 needs 中的值,那么就可以知道当前「窗口」符合要求了,可以开始移动 left 指针了。

思路:

(1) 往右滑,更新使window符合条件的变量(这里是match): 当right所处位置字符在needs中,且window中c1出现次数等于needs中c1出现次数时才增加mach。而window中可以记录[left, right]中所有字符,也可以只记录[left, right]范围内只出现在needs中的字符

(2) 窗口内字符满足条件:匹配到的数量等于needs.size()

(3) 结果更新:用start和minLen记录所要求的s的子串起始pos和长度。

(4) 往左滑,更新使window不符合条件的变量(这里是match)

window滑动的过程中只记录s在[left, right]段中在needs中出现过的字符:

string minWindow(string s, string t) {
    // 记录最短子串的开始位置和长度
    int start = 0, minLen = INT_MAX;
    int left = 0, right = 0;
    
    unordered_map<char, int> window;
    unordered_map<char, int> needs;
    for (char c : t) needs[c]++;
    
    int match = 0;
    
    while (right < s.size()) {
        char c1 = s[right];
        if (needs.count(c1)) {
            window[c1]++;
            if (window[c1] == needs[c1]) 
                match++;
        }
        right++;
        
        while (match == needs.size()) {
            if (right - left < minLen) {
                // 更新最小子串的位置和长度
                start = left;
                minLen = right - left;
            }
            char c2 = s[left];
            if (needs.count(c2)) {
                window[c2]--;
                if (window[c2] < needs[c2])
                    match--;
            }
            left++;
        }
    }
    return minLen == INT_MAX ?
                "" : s.substr(start, minLen);
}

window滑动的过程中会记录s在[left, right]段的所有字符:

class Solution {
public:
    string minWindow(string s, string t) {
        int left = 0;
        int right = 0;
        unordered_map<char, int> needs;
        unordered_map<char, int> window;
        for (auto ch : t) {
            needs[ch]++;
        }

        int match = 0;
        int minLen = INT_MAX;
        int start = 0;
        while (right < s.size()) {
            char c1 = s[right];
            window[c1]++;
            if (needs.count(c1) > 0 && (window[c1] == needs[c1])) {
                match++;
            }
            right++; // 扩大窗口右边界

            while (match == needs.size()) { // 扩大窗口右边界后找到一个解。
                                            // 缩减窗口左边界优化这个解
                //更新
                if (right - left < minLen) {
                    start = left;
                    minLen = right - left;
                }
                char c2 = s[left];
                if (needs.count(c2) > 0 && (window[c2] == needs[c2])) {
                    match--;
                }
                window[c2]--;
                left++;
            }
        }
        string res = minLen != INT_MAX ? s.substr(start, minLen) : "";
        return res;
    }
};

二、找到字符串中所有字母异位词438. 找到字符串中所有字母异位词

思路:

(1) 往右滑,更新使window符合条件的变量(这里是match): 当right所处位置字符在needs中,且window中c1出现次数等于needs中c1出现次数时才增加mach。window只记录[left, right]范围内只出现在needs中的字符。

(2) 窗口内字符满足条件:匹配到的数量等于needs.size()

(3) 结果更新:只有right - left大小等于p串长度,才是目标子串。

(4) 往左滑,更新使window不符合条件的变量(这里是match)

vector<int> findAnagrams(string s, string t) {
    // 用数组记录答案
    vector<int> res;
    int left = 0, right = 0;
    unordered_map<char, int> needs;
    unordered_map<char, int> window;
    for (char c : t) needs[c]++;
    int match = 0;
    
    while (right < s.size()) {
        char c1 = s[right];
        if (needs.count(c1)) {
            window[c1]++;
            if (window[c1] == needs[c1])
                match++;
        }
        right++;

        while (match == needs.size()) {
            // 如果 window 的大小合适
            // 就把起始索引 left 加入结果
            if (right - left == t.size()) {
                res.push_back(left);
            }
            char c2 = s[left];
            if (needs.count(c2)) {
                window[c2]--;
                if (window[c2] < needs[c2])
                    match--;
            }
            left++;
        }
    }
    return res;
}

三、无重复字符的最长子串3. 无重复字符的最长子串

类似之前的思路,使用 window 作为计数器记录窗口中的字符出现次数,然后先向右移动 right,当 window 中出现重复字符时,开始移动 left 缩小窗口,如此往复:

需要注意的是,因为我们要求的是最长子串,所以需要在每次移动 right 增大窗口时更新 res,而不是像之前的题目在移动 left 缩小窗口时更新 res。

// 滑动窗口, 窗口[left, right)和window_map的间接关系,
// window_map中存的就是[left, right)中的元素。
// 增加求子串本身
int lengthOfLongestSubstring(string s) {
    int res = 0;
    int left = 0;
    int right = 0;
    int start = 0;

    // unordered_map<char, int> need;
    unordered_map<char, int> window;
    
    while (right < s.size()) {
        char c1 = s[right];
        window[c1]++;
        right++;

        // 满足条件时:窗口中有重复字符(但不知道此时重复字符ch1处在当前窗口的哪个位置),
        // 缩小窗口, 直到窗口中与ch1重复的字符被从窗口里拿掉
        while (window[c1] > 1) {
            char c2 = s[left];
            window[c2]--;
            left++;
        }
        // 此时window已经滤掉了重复元素
        // res = max(res, right - left);
        if (right - left > res) {
            res = right - left;
            start = left;
        }
    }
    string resStr = s.substr(start, res);
    return res;
}

424. 替换后的最长重复字符

// 方法:滑动窗口

// 思路:

// 右边界先移动找到一个满足题意的可以替换 k 个字符以后,所有字符都变成一样的当前看来最长的子串,直到右边界纳入一个字符以后,不能满足的时候停下;然后考虑左边界向右移动,左边界只须要向右移动一格以后,右边界就又可以开始向右移动了,继续尝试找到更长的目标子串; 替换后的最长重复子串就产生在右边界、左边界交替向右移动的过程中。

class Solution1 {
public:
    int characterReplacement(string s, int k) {
        int res = 0;
        int left = 0;
        int right = 0;
        vector<int> window(26);
        int maxCnt = 0;
        // [left, right) 内最多替换 k 个字符可以得到只有一种字符的子串
        while (right < s.size()) {
            window[s[right] - 'A']++;
            // 在这里维护 maxCount,因为每一次右边界读入一个字符,字符频数增加,才会使得 maxCount 增加
            maxCnt = max(maxCnt, window[s[right] - 'A']);
            right++;
            if (right - left > maxCnt + k) { // if判断,仅缩小一次窗口左边界
              	// 说明此时 k 不够用(把其它不是最多出现的字符替换以后,都不能填满这个滑动的窗口),这个时候须要考虑左边界向右移动
                // 移出滑动窗口的时候,频数数组须要相应地做减法
                window[s[left] - 'A']--;
                left++;
            }
            res = max(res, right - left); // 因为已经缩小了窗口左边界, 因此在这里更新结果
        }
        return res;
    }
};

窗口大小固定

思路:固定长度窗口向前滑动,窗口维护一个total值,每次移动窗口,只需要对窗口加新头、去旧尾两步操作(不要每次滑动都重新把窗口内的值全部计算一遍,这样时间复杂度是O(n*k))保证O(n)时间复杂度。

1343. 大小为 K 且平均值大于等于阈值的子数组数目

class Solution {
public:
    int numOfSubarrays(vector<int>& arr, int k, int threshold) {
        size_t itBound = arr.size() - k;
        int sum = accumulate(arr.begin(), arr.begin() + k, 0);
        int kThreshold = k * threshold;
        int result = sum >= kThreshold ? 1 : 0;
        int headKMinusOneSum = sum - arr[0];
        for (size_t i = 1; i <= itBound; i++) {
            sum = headKMinusOneSum + arr[i + k - 1];
            result = sum >= kThreshold ? result + 1 : result;
            headKMinusOneSum = sum - arr[i];
        }
        return result;
    }
};

567. 字符串的排列

class Solution {
public:
    // 穷举, 超时 O(m! * (n-m))
    bool checkInclusion1(string s1, string s2) {
        if (s1.size() > s2.size()) {
            return false;
        }
        sort(s1.begin(), s1.end());
        do {
            if (s2.find(s1) != string::npos) {
                return true;
            }
        } while (next_permutation(s1.begin(), s1.end()));
        return false;
    }
    // 滑动窗口(固定大小子串),排序
    // 思路:对s1排序,然后一遍遍历s2,每步截取s1.size()长度子串并对子串排序,然后再和
    //      序后的s1比较相等。
    //代码:略
    
    // 滑动窗口(哈希数组), 通过7% + 6%, O(26*m*(n-m))
    // 思路:一遍遍历s2,截取s1.size()长度子串后放入哈希数组2,然后再比较两个哈希数组。
    bool checkInclusion2(string s1, string s2) {
        if (s1.size() > s2.size()) {
            return false;
        }
        vector<int> hashVec1(26);
        size_t s1Len = s1.size();
        for (size_t i = 0; i < s1Len; i++) {
            hashVec1[s1[i] - 'a']++;
        }
        size_t s2Bound = s2.size() - s1.size() + 1;
        for (size_t i = 0; i < s2Bound; i++) {
            string subS2 = s2.substr(i, s1Len);
            vector<int> hashVec2(26);
            for (size_t j = 0; j < s1Len; j++) {
                hashVec2[subS2[j] - 'a']++;
            }
            size_t k = 0;
            for (; k < 26; k++) {
                if (hashVec1[k] != hashVec2[k]) {
                    break;
                }
            }
            if (k == 26) {
                return true;
            }
        }
        return false;
    }
    // 子串问题:滑动窗口(哈希窗口)  89.3%, 96.29% O(26*(n - m))
    // 思路:只维护两个哈希数组,一遍遍历s2,比较两个哈希数组。
    bool checkInclusion(string s1, string s2) {
        if (s1.size() > s2.size()) {
            return false;
        }

        vector<int> needs(26);
        vector<int> window(26);
        for (size_t i = 0; i < s1.size(); i++) {
            needs[s1[i] - 'a']++;
            window[s2[i] - 'a']++;
        }

        size_t s2Bound = s2.size() - s1.size() + 1;
        for (size_t i = 0; i < s2Bound; i++) {
            size_t k = 0;
            for (; k < 26; k++) {
                if (needs[k] != window[k]) {
                    break;
                }
            }
            if (k == 26) {
                return true;
            }
            if (i == s2Bound - 1) {
                break;
            }
            // 窗口只需加头,去尾两步操作
            window[s2[i + s1.size()] - 'a']++;
            window[s2[i] - 'a']--;
        }
        return false;
    }

    // 同上文lc438找到字符串中所有字母异位词: 41ms, 41.5%。用这种解法
    bool checkInclusion(string s, string t) {
        if (s.size() > t.size()) {
            return false;
        }
        int left = 0;
        int right = 0;
        unordered_map<char, int> needs;
        unordered_map<char, int> window;
        for (auto ch : s) {
            needs[ch]++;
        }

        int match = 0;
        while (right < t.size()) {
            char c1 = t[right];
            if (needs.count(c1) != 0) {
                window[c1]++;
                if (window[c1] == needs[c1]) {
                    match++;
                }
            }
            right++;

            while (match == needs.size()) {
                if (right - left == s.size()) {
                    return true;
                }
                char c2 = t[left];
                if (needs.count(c2) != 0) {
                    window[c2]--;
                    if (window[c2] < needs[c2]) {
                        match--;
                    }
                }
                left++;
            }
        }
        return false;
    }
};

783. 二叉搜索树节点最小距离

思路:// 贪心 + 滑动窗口 + 二叉搜索树中序遍历

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    // 贪心 + 滑动窗口 + 二叉树中序遍历
    int minDiffInBST(TreeNode* root) {
        vector<int> inOrderPath;
        InOrderTravel(root, inOrderPath);
        int result = INT_MAX;
        // 窗口大小为2, 相邻两元素
        int windowLeft = inOrderPath[0];
        for (size_t i = 1; i < inOrderPath.size(); i++) {
            result = min(result, inOrderPath[i] - windowLeft);
            windowLeft = inOrderPath[i];
        }
        return result;
    }

    void InOrderTravel(TreeNode* root, vector<int>& inOrderPath) {
        if (root != nullptr) {
            InOrderTravel(root->left, inOrderPath);
            inOrderPath.push_back(root->val);
            InOrderTravel(root->right, inOrderPath);
        }
    }
};

滑动窗口题目:

3. 无重复字符的最长子串   done

30. 串联所有单词的子串   

76. 最小覆盖子串  done

159. 至多包含两个不同字符的最长子串

209. 长度最小的子数组 done

class Solution {
public:
    int minSubArrayLen(int s, vector<int>& nums) {
        int sum = 0;
        int left = 0;
        int right = 0;
        int result = nums.size();
        while (right < nums.size()) {
            sum += nums[right];
            right++;
            while (sum >= s) {
                result = min(result, right - left);
                sum -= nums[left];
                left++;
            }
        }

        return result;
    }
};

方法2:前缀和 + 二分

因为这道题保证了数组中每个元素都为正,所以前缀和一定是递增的,这一点保证了二分的正确性。如果题目没有说明数组中每个元素都为正,这里就不能使用二分来查找这个位置了。

class Solution {
public:
    int minSubArrayLen(int s, vector<int>& nums) {
        int n = nums.size();
        if (n == 0) {
            return 0;
        }
        int ans = INT_MAX;
        vector<int> sums(n + 1, 0); 
        for (int i = 1; i <= n; i++) {
            sums[i] = sums[i - 1] + nums[i - 1];
        }
        for (int i = 1; i <= n; i++) {
            int target = s + sums[i - 1];
            auto bound = lower_bound(sums.begin(), sums.end(), target);
            if (bound != sums.end()) {
                ans = min(ans, static_cast<int>((bound - sums.begin()) - (i - 1)));
            }
        }
        return ans == INT_MAX ? 0 : ans;
    }
};

239. 滑动窗口最大值      done 滑动窗口 + 单调队列(单调递减队列)

    // 滑动窗口 + 单调队列, 固定窗口大小

    // 思路:

    // 单调递减队列中存储数组下标, 下标是递增的, 下标对应的值是递减的

    // 窗口滑动时,当前元素若大于(或大于等于)队尾下标对应的值则循环出队尾, 若当前队首下标在窗口外(队首下标小于left=i-k)则循环队首出队--保证队列中的值始终是滑动中窗口内的值

注:此类题不能用优先队列代替单调队列,因为无法实现优先队列中的值始终是窗口内的值,不能实现窗口外的值及时出优先队列,eg:[9,10,9,-7,-4,-8,2,-6],k=5。

class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        if (nums.size() * k == 0) {
            return {};
        }
        vector<int> res;
        deque<int> q;
        for (size_t i = 0; i < k; i++) {
            while (!q.empty() && nums[q.back()] < nums[i]) {
                q.pop_back();
            }
            q.push_back(i);
        }
        res.push_back(nums[q.front()]);
        for (size_t i = k; i < nums.size(); i++) {
            // 队列头是上一步窗口内最大值的索引值。若队列头不在当前窗口内,则队首出队,看第二大的数处于队首,再继续比较
            while (!q.empty() && q.front() <= i - k) {
                q.pop_front();
            }
            while (!q.empty() && nums[q.back()] < nums[i]) {
                q.pop_back();
            }
            q.push_back(i);
            res.push_back(nums[q.front()]);
        }
        return res;
    }
};

567. 字符串的排列        done

class Solution {
public:
    // 穷举, 超时 O(m! * (n-m))
    bool checkInclusion1(string s1, string s2) {
        if (s1.size() > s2.size()) {
            return false;
        }
        sort(s1.begin(), s1.end());
        do {
            if (s2.find(s1) != string::npos) {
                return true;
            }
        } while (next_permutation(s1.begin(), s1.end()));
        return false;
    }
    // 滑动窗口(固定大小子串),排序
    // 思路:对s1排序,然后一遍遍历s2,每步截取s1.size()长度子串并对子串排序,然后再和
    //      序后的s1比较相等。
    //代码:略
    
    // 滑动窗口(哈希数组), 通过7% + 6%, O(26*m*(n-m))
    // 思路:一遍遍历s2,截取s1.size()长度子串后放入哈希数组2,然后再比较两个哈希数组。
    bool checkInclusion2(string s1, string s2) {
        if (s1.size() > s2.size()) {
            return false;
        }
        vector<int> hashVec1(26);
        size_t s1Len = s1.size();
        for (size_t i = 0; i < s1Len; i++) {
            hashVec1[s1[i] - 'a']++;
        }
        size_t s2Bound = s2.size() - s1.size() + 1;
        for (size_t i = 0; i < s2Bound; i++) {
            string subS2 = s2.substr(i, s1Len);
            vector<int> hashVec2(26);
            for (size_t j = 0; j < s1Len; j++) {
                hashVec2[subS2[j] - 'a']++;
            }
            size_t k = 0;
            for (; k < 26; k++) {
                if (hashVec1[k] != hashVec2[k]) {
                    break;
                }
            }
            if (k == 26) {
                return true;
            }
        }
        return false;
    }
    // 子串问题:滑动窗口(哈希窗口)  89.3%, 96.29% O(26*(n - m))
    // 思路:只维护两个哈希数组,一遍遍历s2,比较两个哈希数组。
    bool checkInclusion(string s1, string s2) {
        if (s1.size() > s2.size()) {
            return false;
        }
 
        vector<int> needs(26);
        vector<int> window(26);
        for (size_t i = 0; i < s1.size(); i++) {
            needs[s1[i] - 'a']++;
            window[s2[i] - 'a']++;
        }
 
        size_t s2Bound = s2.size() - s1.size() + 1;
        for (size_t i = 0; i < s2Bound; i++) {
            size_t k = 0;
            for (; k < 26; k++) {
                if (needs[k] != window[k]) {
                    break;
                }
            }
            if (k == 26) {
                return true;
            }
            if (i == s2Bound - 1) {
                break;
            }
            // 窗口只需加头,去尾两步操作
            window[s2[i + s1.size()] - 'a']++;
            window[s2[i] - 'a']--;
        }
        return false;
    }
// 同上文lc438找到字符串中所有字母异位词: 41ms, 41.5%。用这种解法
    bool checkInclusion3(string s, string t) {
        if (s.size() > t.size()) {
            return false;
        }
        int left = 0;
        int right = 0;
        unordered_map<char, int> needs;
        unordered_map<char, int> window;
        for (auto ch : s) {
            needs[ch]++;
        }
 
        int match = 0;
        while (right < t.size()) {
            char c1 = t[right];
            if (needs.count(c1) != 0) {
                window[c1]++;
                if (window[c1] == needs[c1]) {
                    match++;
                }
            }
            right++;
 
            while (match == needs.size()) {
                if (right - left == s.size()) {
                    return true;
                }
                char c2 = t[left];
                if (needs.count(c2) != 0) {
                    window[c2]--;
                    if (window[c2] < needs[c2]) {
                        match--;
                    }
                }
                left++;
            }
        }
        return false;
    }
};

632. 最小区间

727. 最小窗口子序列

其他子串问题

  • 回文子串

回文子串不用滑动窗口,用中心扩展法

注:回文串有两种:

(1) "aabaa" (2) "aabba"

class Solution {
public:
    string longestPalindrome(string s)
    {
        int sLength = s.size();
        if (sLength == 0) {
            return "";
        }

        int start = 0;
        int end = 0;
        int maxLen = 0;
        int len = 0;
        string result;
        for (int i = 0; i < sLength; i++) {
            int len1 = CenterExpandValid(s, sLength, i, i); // 尝试计算aabaa类型回文串长度
            int len2 = CenterExpandValid(s, sLength, i, i + 1); // 尝试计算aabbaa类型回文长度串
            len = max(len1, len2);
            if (len > maxLen) {
                maxLen = len;
                start = i - (len - 1) / 2;
                end = i + len / 2;
            }
        }
        result = s.substr(start, maxLen);
        return result;
    }
    int  CenterExpandValid(string& s, int sLength, int left, int right)
    {
        while (left >= 0 && right < sLength && s[left] == s[right]) {
            left--;
            right++;
        }
        return right - left - 1;
    }
};

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值