學過計算機網絡的同學,都知道滑動窗口協議(Sliding Window Protocol),該協議是 TCP協議 的一種應用,用於網絡數據傳輸時的流量控制,以避免擁塞的發生。該協議允許發送方在停止並等待確認前發送多個數據分組。由於發送方不必每發一個分組就停下來等待確認。因此該協議可以加速數據的傳輸,提高網絡吞吐量。
滑動窗口算法其實和這個是一樣的,只是用的地方場景不一樣,可以根據需要調整窗口的大小,有時也可以是固定窗口大小。
🍀滑動窗口算法(Sliding Window Algorithm)
Sliding window algorithm is used to perform required operation on specific window size of given large buffer or array.
滑動窗口算法是在給定特定窗口大小的數組或字符串上執行要求的操作。
This technique shows how a nested for loop in few problems can be converted to single for loop and hence reducing the time complexity.
該技術可以將一部分問題中的嵌套循環轉變為一個單循環,因此它可以減少時間復雜度。
簡而言之,滑動窗口算法在一個特定大小的字符串或數組上進行操作,而不在整個字符串和數組上操作,這樣就降低了問題的復雜度,從而也達到降低了循環的嵌套深度。其實這里就可以看出來滑動窗口主要應用在數組和字符串上。
🍁基本示例
如下圖所示,設定滑動窗口(window)大小為 3,當滑動窗口每次划過數組時,計算當前滑動窗口中元素的和,得到結果 res。
可以用來解決一些查找滿足一定條件的連續區間的性質(長度等)的問題。由於區間連續,因此當區間發生變化時,可以通過舊有的計算結果對搜索空間進行剪枝,這樣便減少了重復計算,降低了時間復雜度。往往類似於“ 請找到滿足 xx 的最 x 的區間(子串、子數組)的 xx ”這類問題都可以使用該方法進行解決。
需要注意的是,滑動窗口算法更多的是一種思想,而非某種數據結構的使用。
🍂滑動窗口法的大體框架
在介紹滑動窗口的框架時候,大家先從字面理解下:
-
滑動:說明這個窗口是移動的,也就是移動是按照一定方向來的。
-
窗口:窗口大小並不是固定的,可以不斷擴容直到滿足一定的條件;也可以不斷縮小,直到找到一個滿足條件的最小窗口;當然也可以是固定大小。
為了便於理解,這里采用的是字符串來講解。但是對於數組其實也是一樣的。滑動窗口算法的思路是這樣:
-
我們在字符串 S 中使用雙指針中的左右指針技巧,初始化 left = right = 0,把索引閉區間 [left, right] 稱為一個「窗口」。
-
我們先不斷地增加 right 指針擴大窗口 [left, right],直到窗口中的字符串符合要求(包含了 T 中的所有字符)。
-
此時,我們停止增加 right,轉而不斷增加 left 指針縮小窗口 [left, right],直到窗口中的字符串不再符合要求(不包含 T 中的所有字符了)。同時,每次增加 left,我們都要更新一輪結果。
-
重復第 2 和第 3 步,直到 right 到達字符串 S 的盡頭。
這個思路其實也不難,第 2 步相當於在尋找一個「可行解」,然后第 3 步在優化這個「可行解」,最終找到最優解。左右指針輪流前進,窗口大小增增減減,窗口不斷向右滑動。
下面畫圖理解一下,needs 和 window 相當於計數器,分別記錄 T 中字符出現次數和窗口中的相應字符的出現次數。
初始狀態:
增加 right,直到窗口 [left, right] 包含了 T 中所有字符:
現在開始增加 left,縮小窗口 [left, right]。
直到窗口中的字符串不再符合要求,left 不再繼續移動。
之后重復上述過程,先移動 right,再移動 left…… 直到 right 指針到達字符串 S 的末端,算法結束。
如果你能夠理解上述過程,恭喜,你已經完全掌握了滑動窗口算法思想。至於如何具體到問題,如何得出此題的答案,都是編程問題,等會提供一套模板,理解一下就會了。
上述過程對於非固定大小的滑動窗口,可以簡單地寫出如下偽碼框架:
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;
但是,對於固定窗口大小,可以總結如下:
// 固定窗口大小為 k string s; // 在 s 中尋找窗口大小為 k 時的所包含最大元音字母個數 int right = 0; while (right < s.size()) { window.add(s[right]); right++; // 如果符合要求,說明窗口構造完成, if (right >= k) { // 這是已經是一個窗口了,根據條件做一些事情 // ... 可以計算窗口最大值等 // 最后不要忘記把 right -k 位置元素從窗口里面移除 } } return res;
可以發現此時不需要依賴 left 指針了。因為窗口固定所以其實就沒必要使用left,right 雙指針來控制窗口的大小。
其次是對於窗口是固定的,可以輕易獲取到 left 的位置,此處 left = right-k;
實際上,對於窗口的構造是很重要的。具體可以看下面的實例。
🔥算法實例
1208. 盡可能使字符串相等
給你兩個長度相同的字符串,s 和 t。
將 s 中的第 i 個字符變到 t 中的第 i 個字符需要 |s[i] - t[i]| 的開銷(開銷可能為 0),也就是兩個字符的 ASCII 碼值的差的絕對值。
用於變更字符串的最大預算是 maxCost。在轉化字符串時,總開銷應當小於等於該預算,這也意味着字符串的轉化可能是不完全的。
如果你可以將 s 的子字符串轉化為它在 t 中對應的子字符串,則返回可以轉化的最大長度。
如果 s 中沒有子字符串可以轉化成 t 中對應的子字符串,則返回 0。
示例 1:
輸入:s = "abcd", t = "bcdf", cost = 3 輸出:3 解釋:s 中的 "abc" 可以變為 "bcd"。開銷為 3,所以最大長度為 3。
示例 2:
輸入:s = "abcd", t = "cdef", cost = 3 輸出:1 解釋:s 中的任一字符要想變成 t 中對應的字符,其開銷都是 2。因此,最大長度為 1。
示例 3:
輸入:s = "abcd", t = "acde", cost = 0 輸出:1 解釋:你無法作出任何改動,所以最大長度為 1。
代碼
class Solution { public: int equalSubstring(string s, string t, int maxCost) { int left = 0, right = 0; int sum = 0; int res = 0; // 構造窗口 while (right < s.length()) { sum += abs(s[right] - t[right]); right++; // 窗口構造完成,這時候要根據條件當前的窗口調整窗口大小 while (sum > maxCost) { sum -= abs(s[left] - t[left]); left++; } // 記錄此時窗口的大小 res = max(res, right - left); } return res; } };
這里跟前面總結的框架不一樣的一個點就是,前面的框架是求最小窗口大小,這里是求最大窗口大小,大家要學會靈活變通。
239. 滑動窗口最大值
給定一個數組 nums,有一個大小為 k 的滑動窗口從數組的最左側移動到數組的最右側。你只可以看到在滑動窗口內的 k 個數字。滑動窗口每次只向右移動一位。
返回滑動窗口中的最大值。
進階:
你能在線性時間復雜度內解決此題嗎?
示例:
輸入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3 輸出: [3,3,5,5,6,7] 解釋: 滑動窗口的位置 最大值 --------------- ----- [1 3 -1] -3 5 3 6 7 3 1 [3 -1 -3] 5 3 6 7 3 1 3 [-1 -3 5] 3 6 7 5 1 3 -1 [-3 5 3] 6 7 5 1 3 -1 -3 [5 3 6] 7 6 1 3 -1 -3 5 [3 6 7] 7
提示:
1 <= nums.length <= 10^5 -10^4 <= nums[i] <= 10^4 1 <= k <= nums.length
解答:
class Solution { public: vector<int> maxSlidingWindow(vector<int> nums, int k) { int right = 0, len = nums.size(); vector<int> res(len - k + 1); int index = 0; deque<int> list; // 開始構造窗口 while (right < len) { // 這里的list的首位必須是窗口中最大的那位 while (!list.empty() && nums[right] > list.back()) list.pop_back(); // 不斷添加 list.push_back(nums[right]); right++; // 構造窗口完成,這時候需要根據條件做一些操作 if (right >= k) { res[index++] = list.front(); // 如果發現第一個已經在窗口外面了,就移除 if (list.front() == nums[right - k]) list.pop_front(); } } return res; } };
這道題難度是困難。當然我們也會發現,這道題目和前面的非固定大小滑動窗口還是不一樣的。
看了一道困難的題目后,接下來看一道中等難度的就會發現是小菜一碟。
1456. 定長子串中元音的最大數目
給你字符串 s 和整數 k 。
請返回字符串 s 中長度為 k 的單個子字符串中可能包含的最大元音字母數。
英文中的 元音字母 為(a, e, i, o, u)。
示例 1:
輸入:s = "abciiidef", k = 3 輸出:3 解釋:子字符串 "iii" 包含 3 個元音字母。
示例 2:
輸入:s = "aeiou", k = 2 輸出:2 解釋:任意長度為 2 的子字符串都包含 2 個元音字母。
示例 3:
輸入:s = "leetcode", k = 3 輸出:2 解釋:"lee"、"eet" 和 "ode" 都包含 2 個元音字母。
示例 4:
輸入:s = "rhythms", k = 4 輸出:0 解釋:字符串 s 中不含任何元音字母。
示例 5:
輸入:s = "tryhard", k = 4 輸出:1
提示:
1 <= s.length <= 10^5 s 由小寫英文字母組成 1 <= k <= s.length
解答
class Solution { public: int maxVowels(string s, int k) { int right = 0; int sum = 0; int maxn = 0; while (right < s.length()) { sum += isYuan(s[right]); right++; if (right >= k) { maxn = max(maxn, sum); sum -= isYuan(s[right - k]); } } return maxn; } int isYuan(char s) { return s == 'a' || s == 'e' || s == 'i' || s == 'o' || s == 'u' ? 1 : 0; } };
轉載自:滑動窗口算法基本原理與實踐