• Sliding Window


    sliding window: 解决数组/字符串的子元素问题,它可以将嵌套循环问题,转换为单循环问题,降低时间复杂度.

     最简单的sliding window问题:给定一个整数数组,计算长度为 k 的连续子数组的最大总和。

    int maxSum(vector<int>& arr, int k){
        int max_sum = 0;
        for(int i = 0; i < k; ++i)
            max_sum += arr[i];
        int window_sum = max_sum;
        for(int i = k; i < arr.size(); ++i){
            window_sum += arr[i] - arr[i - k];
            max_sum = max(max_sum, window_sum);
        }
        return max_sum;
    }

    总结一下Leetcode上sliding window的一些问题:

    Leetcode 3. Longest Substring Without Repeating Characters

    给定一个字符串,计算不包含重复字母的字串的最大长度。

     分析:用一个map,key是字母,value是字母的位置,两个指针 i, j 指向当前窗口的头和尾,如果map[s[i]] > j,说明当前窗口内已经出现过一次 s[i] 了,此时 j 移动到 map[s[i]],同时,当前字母的位置要存入map中。

    class Solution {
    public:
        int lengthOfLongestSubstring(string s) {
            //时间复杂度O(n), 空间复杂度O(m), m是字符集的大小
            int n = s.size(), res = 0;
            //如果是任意字符,则用map<char, int>, 如果只有字母,可用vector<int>(128)
            vector<int> index(128);
            for(int i = 0, j = 0; i < n; ++i){
                //j是窗口的开始位置,i是窗口的当前位置
                //如果s[i]从未出现过,index[s[i]] = 0, j保持开始位置不变;
                //如果s[i]出现过,且在当前窗口内,则index[s[i]] > j, index[s[i]]表示s[i]第一次出现的位置+1,j = index[s[i]];
                //如果s[i]出现过,但不在当前窗口内,则index[s[i]] < j, j保持开始位置不变
                j = max(index[s[i]], j);
                //i - j - 1是当前窗口大小
                res = max(res, i - j + 1);
                //记录当前字符的位置
                index[s[i]] = i + 1;
            }
            return res;
        }
    };

    Leetcode 76. Minimum Window Substring

    给定一个字符串S和字符串T,在S中找出包含T所有字母的最小字串,要求time O(n)。

    class Solution {
    public:
        string minWindow(string s, string t) {
            //1. Use two pointers: start and end to represent a window.
            //2. Move end to find a valid window.
            //3. When a valid window is found, move start to find a smaller window.
            //To check if a window is valid, we use a map to store (char, count) for chars in t. 
            //And use counter for the number of chars of t to be found in s. The key part is m[s[end]]--;. 
            //We decrease count for each char in s. If it does not exist in t, the count will be negative.
            vector<int> m(128, 0);
            for(char c : t) m[c]++;
            int start = 0, end = 0, d = INT_MAX, head = 0, counter = t.size();
            while(end < s.size()){
                if(m[s[end]] > 0) counter--;
                m[s[end]]--;
                end++;
                while(counter == 0){
                    if(end - start  < d) {
                        d = end - start;
                        head = start;
                    }
                    if(m[s[start]] == 0) counter++;
                    m[s[start]]++;
                    start++;
                }
            }
            return d == INT_MAX ? "" : s.substr(head, d);
        }
    };

    Leetcode 239. Sliding Window Maximum

    给定一个数组,一个大小为 k 的窗口从数组最左端开始滑动,每次向右滑动1,返回每个窗口中的最大值构成的数组。

    分析:用deque存"promising element"

    class Solution {
    public:
        vector<int> maxSlidingWindow(vector<int>& nums, int k) {
            deque<int> dq;
            vector<int> ans;
            for(int i = 0; i < nums.size(); ++i){
                if(!dq.empty() && dq.front() == i - k) dq.pop_front();
                while(!dq.empty() && nums[dq.back()] < nums[i]) dq.pop_back();
                dq.push_back(i);
                if(i >= k - 1) ans.push_back(nums[dq.front()]);
            }
            return ans;
        }
    };

     

    Leetcode 480. Sliding Window Median

    给定一个数组,一个大小为 k 的窗口从数组最左端开始滑动,每次向右滑动1,返回每个窗口中的中位数构成的数组(若窗口大小为偶数,则中位数为中间两个数的平均数)。

    分析:把一个window中的元素存入multiset,迭代器mid指向中间值(k/2)

    time: O(nlgk)

    space:  O(k)

    class Solution {
    public:
        vector<double> medianSlidingWindow(vector<int>& nums, int k) {
            multiset<int> window(nums.begin(), nums.begin() + k);
            auto mid = next(window.begin(), k / 2);
            vector<double> medians;
            for (int i=k; ; i++) {
    
                // Push the current median.
                medians.push_back((double(*mid) + *prev(mid, 1 - k%2)) / 2);
    
                // If all done, return.
                if (i == nums.size())
                    return medians;
    
                // Insert nums[i].
                window.insert(nums[i]);
                if (nums[i] < *mid)
                    mid--;
    
                // Erase nums[i-k].
                if (nums[i-k] <= *mid)
                    mid++;
                window.erase(window.lower_bound(nums[i-k]));
            }
        }
    };

    Leetcode 424. Longest Repeating Character Replacement

    给定一个字符串,只包含大写字母,一次操作可以选择任意一个字母变为另一个字母,最多操作 k 次,求可以得到最长的只包含一种字母的字串的长度。

    分析:curMost表示当前窗口内出现最多次的字母:

        end - start + 1 - curMost <= k 则end可以继续向右扩展

        end - start + 1 - curMost == k + 1 则不能继续向右扩展,start必须向右移动1,此时窗口为[start + 1, end + 1],curMost可能不是正确值且该窗口可能不满足要求,但是res不会大于之前的res,因此无所谓。当有足够多的相同字母进入窗口,curMost会正确更新。 

    class Solution {
    public:
        int characterReplacement(string s, int k) {
            vector<int> m(128, 0);
            int curMost = 0, res = 0;
            for(int i = 0, j = 0; i < s.size(); ++i){
                m[s[i]]++;
                curMost = max(curMost, m[s[i]]);
                if(i - j + 1 - curMost > k){
                    m[s[j]]--;
                    j++;
                }
                res = max(res, i - j + 1);
            }
            return res;
        }
    };

    Leetcode 567. Permutation in String

    给定两个字符串s1和s2,判断s2是否存在某个字串为s1的排列。

    方法1:记录s1每个字母的个数,用滑动窗口法判断每个窗口中是否包含了这些字母和对应个数

    time: O(26 * n2) n2 = s2.size()

    class Solution {
    public:
        bool checkInclusion(string s1, string s2) {
            int len1 = s1.size(), len2 = s2.size();
            if(len1 > len2) return false;
            vector<int> count(26, 0);
            for(int i = 0; i < len1; ++i){
                count[s1[i] - 'a']++;
                count[s2[i] - 'a']--;
            }
            if(allZero(count)) return true;
            for(int i = len1; i < len2; ++i){
                count[s2[i] - 'a']--;
                count[s2[i - len1] - 'a']++;
                if(allZero(count)) return true;
            }
            return false;
        }
        
        bool allZero(vector<int>& count){
            for(int i : count)
                if(i != 0) return false;
            return true;
        }
    };

    方法2:count 表示每个 s1 中的字母需要几个就为负几,每扫描一个字母(s2[r]),count[s2[r]]++,如果大于零,说明要么 s2[r] 没有出现再 s1 中,要么虽然出现再 s1 中,但个数超出了,此时不能继续向右扫描,需要将向 l 右移直到 count[s2[l]] == 0,说明当前窗口有效,继续向右扫描 r.

    class Solution {
    public:
        bool checkInclusion(string s1, string s2) {
            vector<int> count(128, 0);
            for(auto c : s1) count[c]--;
            for(int l = 0, r = 0; r < s2.size(); ++r){
                if(++count[s2[r]] > 0){
                    while(--count[s2[l++]] != 0){};
                }
                else if(r - l + 1 == s1.size()) return true;
            }
            return s1.size() == 0;
        }
    };

    正的其实也行:

    class Solution {
    public:
        bool checkInclusion(string s1, string s2) {
            vector<int> m(128, 0);
            for(char c : s1) m[c]++;
            for(int i = 0, j = 0; i < s2.size(); ++i){
                if(--m[s2[i]] < 0){  
                    while(++m[s2[j++]] != 0){}; //小于零说明s2[j]是多余的
                }
                else if(i - j + 1 == s1.size()) return true;
            }
            return false;
        }
    };

    Leetcode 978. Longest Turbulent Subarray

    给定一个数组,返回最大子波动数组的长度 A[i] < A[i + 1] > A[i + 2] < ....

    分析:滑动窗口法,有个小技巧: 用compare(A[i - 1], A[i]) * compare(A[i], A[i + 1]) 是否等于-1来判断 A[i - 1], A[i], A[i + 1] 是否波动。

    class Solution {
    public:
        int maxTurbulenceSize(vector<int>& A) {
            int n = A.size();
            int ans = 1;
            int anchor = 0;
            for(int i = 1; i < n; ++i){
                int c = compare(A[i - 1], A[i]);
                if(c == 0) anchor = i;
                else if(i == n - 1 || c * compare(A[i], A[i + 1]) != -1){
                    ans = max(ans, i - anchor + 1);
                    anchor = i;
                }
            }
            return ans;
        }
        
        int compare(int& a, int& b){
            if(a < b) return 1;
            if(a == b) return 0;
            if(a > b) return -1;
            return 0;
        }
    };

    Leetcode 992. Subarrays with K Different Integers

    给定一个正整数数组,如果它的一个子数组中不同整数的个数恰好等于 K,则称这个子数组为good。计算这个数组good子数组的个数。

    分析:如果问至多有k个不同整数的子数组,则很好用滑动窗口法,因此,将问题转化成: exactly(K) = atMost(K) - atMost(K - 1)

    想想能不能直接求解?

    class Solution {
    public:
        int subarraysWithKDistinct(vector<int>& A, int K) {
            return atMostK(A, K) - atMostK(A, K - 1);
        }
        
        int atMostK(vector<int>& A, int K){
            int i = 0, res = 0;
            unordered_map<int, int> count; 
            for(int j = 0; j < A.size(); ++j){
                if(!count[A[j]]++) K--;
                while(K < 0){
                    if(!--count[A[i]]) K++;
                    i++;
                }
                res += j - i + 1; // j - i + 1表示以A[j]结尾的所有满足要求的子数组
            }
            return res;
        }
    };

     直接计算的方法:(详细解析

    分析:[j, i] 是包含 K 个不同数字的最小数组,prefix 记录了 j 之前有多少个数字出现在 [j, i] 中,则该窗口中,不同数字等于 K 的个数为 prefix + 1.

    class Solution {
    public:
        int subarraysWithKDistinct(vector<int>& A, int K) {
            int res = 0;
            vector<int> m(A.size() + 1);
            for(int i = 0, j = 0, prefix = 0, cnt = 0; i < A.size(); ++i){
                if(m[A[i]]++ == 0) cnt++;
                if(cnt > K) --m[A[j++]], --cnt, prefix = 0;
                while(m[A[j]] > 1) ++prefix, --m[A[j++]];
                if(cnt == K) res += prefix + 1;
            }
            return res;
        }
    };

    Leetcode 1004. Max Consecutive Ones III

    给定一个数组只包含0和1,可以将 K 个0变成1,求只包含1的最大子数组的长度。

    分析:将问题转化为:找到包含至多 K 个0的最长的子数组。

    For each A[j], try to find the longest subarray.

    If A[i]~A[j] has zeros <= K, we continue to increment j.

    If A[i]~A[j] has zeros > K, we increment i.

    class Solution {
    public:
        int longestOnes(vector<int>& A, int K) {
            int i = 0, j;
            for(j = 0; j < A.size(); ++j){
                if(A[j] == 0) K--;
                if(K < 0 && A[i++] == 0) K++;
            }
            return j - i;
        }
    };

    Leetcode 1040. Moving Stones Until Consecutive II

    第 i 个石头的位置为position[i],每次只能将石头从endpoint移到非endpoint,直到所有石头位置连续,求最大和最小移动次数。

    分析: 有点复杂,懒得写了。看这里

    class Solution {
    public:
        vector<int> numMovesStonesII(vector<int>& A) {
            sort(A.begin(), A.end());
            int i = 0, n = A.size(), low = n;
            int high = max(A[n - 2] - A[0] - n + 2, A[n - 1] - A[1] - n + 2);
            for(int j = 0; j < n; ++j){
                while(A[j] - A[i] >= n) i++;
                if(j - i + 1 == n - 1 && A[j] - A[i] == n - 2)
                    low = min(low, 2);
                else
                    low = min(low, n - (j - i + 1));
            }
            return{low, high};
        }
    };

    Leetcode 1052. Grumpy Bookstore Owner

    书店老板开书店customers.size()分钟,每分钟customers[i]个顾客进入书店,该分钟结束后离开。在某些分钟,书店老板很暴躁,grumpy[i] = 1 表示暴躁。

    老板有一个特殊技能:保持连续X分钟不暴燥,但该技能只能用一次。求能使满意顾客数的最大值。

    分析:

    1. Use a sliding window win to record the number of satisfied customers if grumpy technique used for X minutes. Update the value of win when the window is wider than X.
    2. Use satisfy to record the number of satistified customers without grumpy technique.
    3. by the end of iterations, satisfy + max(win) is the answer.
    class Solution {
    public:
        int maxSatisfied(vector<int>& customers, vector<int>& grumpy, int X) {
            int satisfy = 0, maxwin = 0, win = 0;
            for(int i = 0; i < customers.size(); ++i){
                if(grumpy[i] == 0) satisfy += customers[i];
                else win += customers[i];
                if(i >= X) win -= customers[i - X] * grumpy[i - X];
                maxwin = max(maxwin, win);
            }
            return maxwin + satisfy;
        }
    };

    Leetcode 1208. Get Equal Substrings Within Budget

    给定两个一样长的字符串s, t,将 s[i] 变为 t[i] 的代价为 |s[i] - t[i]|,给定最大代价maxCost,求s最长的字串,可以在不多于maxCost的代价下转换为对应t的字串。

    分析:这道题与1004相似,相同的解法。

    class Solution {
    public:
        int equalSubstring(string s, string t, int maxCost) {
            int i = 0, j = 0;
            while(i < s.size()){
                maxCost -= abs(s[i] - t[i++]);
                if(maxCost < 0) maxCost += abs(s[j] - t[j++]);
            }
            return i - j;
        }
    };

    Leetcode 1234. Replace the Substring for Balanced String

    给定一个字符串s,只包含Q, W, E, R, 计算最小字串长度,修改该子串可以使得 s 中 Q, W, E, R 各出现 n / 4 次。

    分析:滑动窗口法,只需关心窗口外的元素,只要使得窗口外的Q, W, E, R出现次数不超过 n / 4 即可。

    class Solution {
    public:
        int balancedString(string s) {
            unordered_map<int, int> count;
            int n = s.size(), res = n, i = 0;
            for(char c : s) count[c]++;
            for(int j = 0; j < s.size(); ++j){
                count[s[j]]--;
                while(i < n && count['Q'] <= n / 4 && count['W'] <= n / 4 && count['E'] <= n / 4 && count['R'] <= n / 4){
                    res = min(res, j - i + 1);
                    count[s[i++]]++;
                }
            }
            return res;
        }
    };
  • 相关阅读:
    30天敏捷结果(2):用三个故事驱动你的一周
    30天敏捷结果(24):恢复你的精力
    30天敏捷结果(6):周五回顾,找到三件做的好以及三件需要改善的事情
    js 数组循环和迭代
    没有+求和
    js检测数组类型
    redis 在windows 下的安装和使用
    版本控制(一)——初步概念
    苹果树的故事(转发的)
    mongoDB之在windows下的安装
  • 原文地址:https://www.cnblogs.com/betaa/p/11643802.html
Copyright © 2020-2023  润新知