滑動窗口(Sliding Window)技巧總結


什么是滑動窗口(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++;
            // 進行窗口內數據的一系列更新
            ...
        }
    }
}

滑動窗口算法的思路:

  1. 在字符串 S 中使用雙指針中的左右指針技巧,初始化 left = right = 0 ,把索引左閉右開區間 [left, right) 稱為一個「窗口」。
  2. 不斷地增加 right 指針擴大窗口 [left, right) ,直到窗口中的字符串符合要求(包含了 T 中的所有字符)。
  3. 此時停止增加 right ,轉而不斷增加 left 指針縮小窗口 [left, right) ,直到窗口中的字符串不再符合要求(不包含 T 中的所有字符了)。同時,每次增加 left ,都要更新一輪結果。
  4. 重復第2和第3步,直到 right 到達字符串 S 的盡頭。

needswindow 相當於計數器,分別記錄 T 中字符出現次數和「窗口」中的相應字符的出現次數。

開始套模板之前,要思考以下四個問題:

  1. 當移動 right 擴大窗口,即加入字符時,應該更新哪些數據?
  2. 什么條件下,窗口應該暫停擴大,開始移動_left_ 縮小窗口?
  3. 當移動 left 縮小窗口,即移出字符時,應該更新哪些數據?
  4. 我們要的結果應該在擴大窗口時還是縮小窗口時進行更新?

滑動窗口問題實例

最小覆蓋子串

LeetCode題目:76.最小覆蓋子串

76.最小覆蓋子串

1、閱讀且分析題目

題目中包含關鍵字:時間復雜度O(n)字符串最小子串。可使用滑動窗口算法解決。

2. 思考滑動窗口算法四個問題

1、當移動 right 擴大窗口,即加入字符時,應該更新哪些數據?

更新 window 中加入字符的個數,判斷 needwindow 中的字符個數是否相等,相等則 valid++

2、什么條件下,窗口應該暫停擴大,開始移動_left_ 縮小窗口?

window 包含 need 中的字符及個數時,即 valid == len(need)

3、當移動 left 縮小窗口,即移出字符時,應該更新哪些數據?

更新 window 中移出字符的個數,且判斷 needwindow 中的移出字符個數是否相等,相等則 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.字符串的排列

567.字符串的排列

1、閱讀且分析題目

題目中包含關鍵字:字符串子串,且求 s2 中是否包含 s1 的排列,即求是否包含長度 k 的子串。可使用滑動窗口算法解決。

2. 思考滑動窗口算法四個問題

1、當移動 right 擴大窗口,即加入字符時,應該更新哪些數據?

更新 window 中加入字符的個數,判斷 needwindow 中的字符個數是否相等,相等則 valid++

2、什么條件下,窗口應該暫停擴大,開始移動_left_ 縮小窗口?

window 包含 need 中的字符及個數時,即 valid == len(need)

3、當移動 left 縮小窗口,即移出字符時,應該更新哪些數據?

更新 window 中移出字符的個數,且判斷 needwindow 中的移出字符個數是否相等,相等則 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.找到字符串中所有字母異位詞

438.找到字符串中所有字母異位詞

1、閱讀且分析題目

題目中包含關鍵字:字符串,且求 s 中的所有 p 的字母異位詞的子串。可使用滑動窗口算法解決。

2. 思考滑動窗口算法四個問題

1、當移動 right 擴大窗口,即加入字符時,應該更新哪些數據?

更新 window 中加入字符的個數,判斷 needwindow 中的字符個數是否相等,相等則 valid++

2、什么條件下,窗口應該暫停擴大,開始移動_left_ 縮小窗口?

window 包含 need 中的字符及個數時,即 valid == len(need)

3、當移動 left 縮小窗口,即移出字符時,應該更新哪些數據?

更新 window 中移出字符的個數,且判斷 needwindow 中的移出字符個數是否相等,相等則 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. 無重復字符的最長子串

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值等關鍵字時,可使用滑動窗口算法。
  • 模板中的向左和向右時的處理是對稱的。
  • 套模板前思考四個問題:
    1. 當移動 right 擴大窗口,即加入字符時,應該更新哪些數據?
    2. 什么條件下,窗口應該暫停擴大,開始移動_left_ 縮小窗口?
    3. 當移動 left 縮小窗口,即移出字符時,應該更新哪些數據?
    4. 我們要的結果應該在擴大窗口時還是縮小窗口時進行更新?

參考資料


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM