什麼是滑動視窗(Sliding Window)
The Sliding Problem contains a sliding window which is a sub – list that runs over a Large Array which is an underlying collection of elements.
滑動視窗演算法可以用以解決陣列/字串的子元素問題,它可以將巢狀的迴圈問題,轉換為單迴圈問題,降低時間複雜度。
比如找最長的全為1的子陣列長度。滑動視窗一般從第一個元素開始,一直往右邊一個一個元素挪動。當然了,根據題目要求,我們可能有固定視窗大小的情況,也有視窗的大小變化的情況。
如何判斷使用滑動視窗演算法
如果題目中求的結果有以下情況時可使用滑動視窗演算法:
- 最小值 Minimum value
- 最大值 Maximum value
- 最長值 Longest value
- 最短值 Shortest value
- K值 K-sized value
演算法模板與思路
/* 滑動視窗演算法框架 */
void slidingWindow(string s, string t) {
unordered_map<char, int> need, window;
for (char c : t) need[c]++;
int left = 0, right = 0;
int valid = 0;
while (right < s.size()) {
// c 是將移入視窗的字元
char c = s[right];
// 右移視窗
right++;
// 進行視窗內資料的一系列更新
...
/*** debug 輸出的位置 ***/
printf("window: [%d, %d)\n", left, right);
/********************/
// 判斷左側視窗是否要收縮
while (window needs shrink) {
// d 是將移出視窗的字元
char d = s[left];
// 左移視窗
left++;
// 進行視窗內資料的一系列更新
...
}
}
}
滑動視窗演算法的思路:
- 在字串 S 中使用雙指標中的左右指標技巧,初始化 left = right = 0 ,把索引左閉右開區間 [left, right) 稱為一個「視窗」。
- 不斷地增加 right 指標擴大視窗 [left, right) ,直到視窗中的字串符合要求(包含了 T 中的所有字元)。
- 此時停止增加 right ,轉而不斷增加 left 指標縮小視窗 [left, right) ,直到視窗中的字串不再符合要求(不包含 T 中的所有字元了)。同時,每次增加 left ,都要更新一輪結果。
- 重複第2和第3步,直到 right 到達字串 S 的盡頭。
needs 和 window 相當於計數器,分別記錄 T 中字元出現次數和「視窗」中的相應字元的出現次數。
開始套模板之前,要思考以下四個問題:
- 當移動 right 擴大視窗,即加入字元時,應該更新哪些資料?
- 什麼條件下,視窗應該暫停擴大,開始移動_left_ 縮小視窗?
- 當移動 left 縮小視窗,即移出字元時,應該更新哪些資料?
- 我們要的結果應該在擴大視窗時還是縮小視窗時進行更新?
滑動視窗問題例項
最小覆蓋子串
LeetCode題目:76.最小覆蓋子串
1、閱讀且分析題目
題目中包含關鍵字:時間複雜度O(n)、字串、最小子串。可使用滑動視窗演算法解決。
2. 思考滑動視窗演算法四個問題
1、當移動 right 擴大視窗,即加入字元時,應該更新哪些資料?
更新 window 中加入字元的個數,判斷 need 與 window 中的字元個數是否相等,相等則 valid++ 。
2、什麼條件下,視窗應該暫停擴大,開始移動_left_ 縮小視窗?
當 window 包含 need 中的字元及個數時,即 valid == len(need) 。
3、當移動 left 縮小視窗,即移出字元時,應該更新哪些資料?
更新 window 中移出字元的個數,且判斷 need 與 window 中的移出字元個數是否相等,相等則 valid-- 。
4、我們要的結果應該在擴大視窗時還是縮小視窗時進行更新?
在縮小視窗時,因為求的是最小子串。
3. 程式碼實現
func minWindow(s string, t string) string {
need, window := make(map[byte]int), make(map[byte]int)
for i := 0; i < len(t); i++ { // 初始化 need
if _, ok := need[t[i]]; ok {
need[t[i]]++
} else {
need[t[i]] = 1
}
}
left, right, valid := 0, 0, 0
start, slen := 0, len(s)+1 // 設定長度為 len(s) + 1 表示此時沒有符合條件的子串
for right < len(s) { // 滑動視窗向右擴大
c := s[right]
right++
if _, ok := need[c]; ok { // 向右擴大時,更新資料
if _, ok := window[c]; ok {
window[c]++
} else {
window[c] = 1
}
if window[c] == need[c] {
valid++
}
}
for valid == len(need) { // 當視窗包括 need 中所有字元及個數時,縮小視窗
if right-left < slen { // 縮小前,判斷是否最小子串
start = left
slen = right - left
}
d := s[left]
left++
if v, ok := need[d]; ok { // 向左縮小時,更新資料
if window[d] == v {
valid--
}
window[d]--
}
}
}
if slen == len(s)+1 { // 長度 len(s) + 1 表示此時沒有符合條件的子串
return ""
} else {
return s[start : start+slen]
}
}
4. 複雜度分析
- 時間複雜度:O(n),n 表示字串 s 的長度。遍歷一次字串。
- 空間複雜度:O(m),m 表示字串 t 的長度。使用了兩個雜湊表,儲存字串 t 中的字元個數。
字串排列
LeetCode題目:567.字串的排列
1、閱讀且分析題目
題目中包含關鍵字:字串、子串,且求 s2 中是否包含 s1 的排列,即求是否包含長度 k 的子串。可使用滑動視窗演算法解決。
2. 思考滑動視窗演算法四個問題
1、當移動 right 擴大視窗,即加入字元時,應該更新哪些資料?
更新 window 中加入字元的個數,判斷 need 與 window 中的字元個數是否相等,相等則 valid++ 。
2、什麼條件下,視窗應該暫停擴大,開始移動_left_ 縮小視窗?
當 window 包含 need 中的字元及個數時,即 valid == len(need) 。
3、當移動 left 縮小視窗,即移出字元時,應該更新哪些資料?
更新 window 中移出字元的個數,且判斷 need 與 window 中的移出字元個數是否相等,相等則 valid-- 。
4、我們要的結果應該在擴大視窗時還是縮小視窗時進行更新?
無論在擴大時或縮小視窗時都可以,因為求的是固定長度的子串。選擇在縮小視窗時更新。
3. 程式碼實現
func checkInclusion(s1 string, s2 string) bool {
if s1 == s2 {
return true
}
need, window := make(map[byte]int), make(map[byte]int)
for i := 0; i < len(s1); i++ {
if _, ok := need[s1[i]]; ok {
need[s1[i]]++
} else {
need[s1[i]] = 1
}
}
left, right := 0, 0
valid := 0
for right < len(s2) {
c := s2[right]
right++
if _, ok := need[c]; ok {
if _, ok := window[c]; ok {
window[c]++
} else {
window[c] = 1
}
if window[c] == need[c] {
valid++
}
}
for valid == len(need) {
if right-left == len(s1) {
return true
}
d := s2[left]
left++
if _, ok := need[d]; ok {
if _, ok := window[d]; ok {
if window[d] == need[d] {
valid--
}
window[d]--
}
}
}
}
return false
}
4. 複雜度分析
- 時間複雜度:O(n),n 表示字串 s2 的長度。遍歷一次字串。
- 空間複雜度:O(m),m 表示字串 s1 的長度。使用了兩個雜湊表,儲存字串 s1 中的字元個數。
找所有字母異位詞
LeetCode題目:438.找到字串中所有字母異位詞
1、閱讀且分析題目
題目中包含關鍵字:字串,且求 s 中的所有 p 的字母異位詞的子串。可使用滑動視窗演算法解決。
2. 思考滑動視窗演算法四個問題
1、當移動 right 擴大視窗,即加入字元時,應該更新哪些資料?
更新 window 中加入字元的個數,判斷 need 與 window 中的字元個數是否相等,相等則 valid++ 。
2、什麼條件下,視窗應該暫停擴大,開始移動_left_ 縮小視窗?
當 window 包含 need 中的字元及個數時,即 valid == len(need) 。
3、當移動 left 縮小視窗,即移出字元時,應該更新哪些資料?
更新 window 中移出字元的個數,且判斷 need 與 window 中的移出字元個數是否相等,相等則 valid-- 。
4、我們要的結果應該在擴大視窗時還是縮小視窗時進行更新?
無論在擴大時或縮小視窗時都可以,因為求的是固定長度的子串。選擇在縮小視窗時更新。
3. 程式碼實現
func findAnagrams(s string, p string) []int {
need, window := make(map[byte]int), make(map[byte]int)
for i := 0; i < len(p); i++ { // 初始化
if _, ok := need[p[i]]; ok {
need[p[i]]++
} else {
need[p[i]] = 1
}
}
left, right := 0, 0
valid := 0
ans := make([]int, 0)
for right < len(s) {
c := s[right]
right++
if _, ok := need[c]; ok {
if _, ok := window[c]; ok {
window[c]++
} else {
window[c] = 1
}
if need[c] == window[c] {
valid++
}
}
for valid == len(need) {
if right-left == len(p) {
ans = append(ans, left)
}
d := s[left]
left++
if _, ok := need[d]; ok {
if _, ok := window[d]; ok {
if need[d] == window[d] {
valid--
}
window[d]--
}
}
}
}
return ans
}
4. 複雜度分析
- 時間複雜度:O(n),n 表示字串 s 的長度。遍歷一次字串。
- 空間複雜度:O(m),m 表示字串 p 的長度。使用了兩個雜湊表,儲存字串 p 中的字元個數。
最長無重複子串
LeetCode題目:3. 無重複字元的最長子串
1、閱讀且分析題目
題目中包含關鍵字:時間複雜度O(n)、字串、最小子串。可使用滑動視窗演算法解決。
2. 思考滑動視窗演算法四個問題
1、當移動 right 擴大視窗,即加入字元時,應該更新哪些資料?
更新 window 中加入字元的個數,及當 window 中的某個字元個數 == 2時,更新 valid == false 。
2、什麼條件下,視窗應該暫停擴大,開始移動_left_ 縮小視窗?
當 window 中的字元及個數 == 2時,即 valid == false 。
3、當移動 left 縮小視窗,即移出字元時,應該更新哪些資料?
更新 window 中移出字元的個數,且判斷 window 中移出字元個數是否 == 2 ,相等則 valid == true 。
4、我們要的結果應該在擴大視窗時還是縮小視窗時進行更新?
在擴大視窗時,因為求的是最大子串。
3. 程式碼實現
func lengthOfLongestSubstring(s string) int {
if s == "" { // 當字串為空時,返回0
return 0
}
window := make(map[byte]int)
left, right, max := 0, 0, 0
valid := true
for right < len(s) {
c := s[right]
right++
if _, ok := window[c]; !ok { // 初始化
window[c] = 0
}
window[c]++ // 累加
if window[c] == 2 { // 當出現重複字元時
valid = false
} else { // 否則累加不重複子串長度,並且判斷是否當前最長
if max < right-left {
max = right - left
}
}
for valid == false {
d := s[left]
left++
if window[d] == 2 {
valid = true
}
window[d]--
}
}
return max
}
4. 複雜度分析
- 時間複雜度:O(n),n 表示字串 s 的長度。遍歷一次字串。
- 空間複雜度:O(n),n 表示字串 s 的長度。使用了雜湊表,儲存不重複的字元個數。
總結
- 滑動視窗演算法可以用以解決陣列/字串的子元素問題,它可以將巢狀的迴圈問題,轉換為單迴圈問題,降低時間複雜度。
- 問題中包含字串子元素、最大值、最小值、最長、最短、K值等關鍵字時,可使用滑動視窗演算法。
- 模板中的向左和向右時的處理是對稱的。
- 套模板前思考四個問題:
- 當移動 right 擴大視窗,即加入字元時,應該更新哪些資料?
- 什麼條件下,視窗應該暫停擴大,開始移動_left_ 縮小視窗?
- 當移動 left 縮小視窗,即移出字元時,應該更新哪些資料?
- 我們要的結果應該在擴大視窗時還是縮小視窗時進行更新?