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分钟不暴燥,但该技能只能用一次。求能使满意顾客数的最大值。
分析:
- Use a sliding window
win
to record the number of satisfied customers if grumpy technique used forX
minutes. Update the value ofwin
when the window is wider thanX
. - Use
satisfy
to record the number of satistified customers without grumpy technique. - 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; } };