Sliding Window Algorithm 滑動窗口算法


簡介

在LeetCode寫題目的時候評論區看到一個方法,一開始沒看懂,后來查了一些資料整理了一下。原題見文中例3

什么是滑動窗口算法?
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.

滑動窗口算法可以用以解決數組/字符串的子元素問題,它可以將嵌套的循環問題,轉換為單循環問題,降低時間復雜度。

假設有數組[a b c d e f g h]
一個大小為3的滑動窗口在其上滑動,則有:
[a b c]
[b c d]
 [c d e]
   [d e f]
     [e f g]
       [f g h]

算法題例子

例1

給定一個整數數組,計算長度為 'k' 的連續子數組的最大總和。

輸入:arr [] = {100,200,300,400}
  k = 2

輸出:700

解釋:300 + 400 = 700

思路1:暴力法

沒啥好說的,直接遍歷,但是時間復雜度很差

C++代碼:

int maxSum(int *arr, int length, int k) {
    int max = INT32_MIN;
    for (int i = 0; i < length - k + 1; i++) {
        int tempSum = 0;
        for (int j = 0; j < k; j++) {
            tempSum += arr[i + j];
        }
        max = tempSum > max ? tempSum : max;
    }
    return max;
}

思路2:滑動窗口

C++代碼如下:

int maxSum(int *arr,int length,int k){
    int max=0;
    for (int i = 0; i < k; ++i) {
        max+=arr[i];
    } // 初始化max
    for (int j = 0; j < length-k; ++j) {
        int temp = max-arr[j]+arr[j+k];
        max = temp>max?temp:max;
    }
    return max;
}

例2

LeetCode原題

給定一個字符串 S 和一個字符串 T,請在 S 中找出包含 T 所有字母的最小子串。(minimum-window-substring)

輸入: S = "ADOBECODEBANC", T = "ABC"
輸出: "BANC"

思路:左右指針滑動窗口

這個問題讓我們無法按照示例 1 中的方法進行查找,因為它不是給定了窗口大小讓你找對應的值,而是給定了對應的值,讓你找最小的窗口。

我們仍然可以使用滑動窗口算法,只不過需要換一個思路。

既然是找最小的窗口,我們先定義一個最小的窗口,也就是長度為 0 的窗口。

我們比較一下當前窗口在的位置的字母,是否是 T 中的一個字母。

很明顯, A 是 ABC 中的一個字母,也就是 T 所有字母的最小子串 可能包含當前位置的 S 的值。

如果包含,我們開始擴大窗口,直到擴大后的窗口能夠包含 T 所有字母。

假設題目是 在 S 中找出包含 T 所有字母的第一個子串,我們就已經解決問題了,但是題目是找到最小的子串,就會存在一些問題。

  • 當前窗口內可能包含了一個更小的能滿足題目的窗口
  • 窗口沒有滑動到的位置有可能包含了一個更小的能滿足題目的窗口

為了解決可能出現的問題,當我們找到第一個滿足的窗口后,就從左開始縮小窗口。

  1. 如果縮小后的窗口仍滿足包含 T 所有字母的要求,則當前窗口可能是最小能滿足題目的窗口,儲存下來之后,繼續從左開始縮小窗口。
  2. 如果縮小后的窗口不能滿足包含 T 所有字母的要求,則縮小窗口停止,從右邊開始擴大窗口。

縮小窗口停止:

向右擴大停止:

不斷重復上面的步驟,直到窗口滑動到最右邊,且找不到合適的窗口為止。最小滿足的窗口就是我們要找的 S 中包含 T 所有字母的最小子串。

C++代碼如下:

string maxSubString(string s,string t){
    map<char,int> rightData;
    for (int i = 0; i < t.length(); ++i) {
        if(rightData.find(t[i])!=rightData.end()){
            rightData[t[i]]++;
        } else{
            rightData[t[i]] = 1;
        }
    }
    int leftPos = 0;
    int rightPos = 0;
    // 窗口的左右指針
    int count = t.length(); // t中不被子串包含的字符數
    int min = INT32_MAX; // 最小長度
    string res;

    while (rightPos < s.length()){
        if(rightData.find(s[rightPos])!=rightData.end()){
            if(rightData[s[rightPos]]>0)
                count--;
            rightData[s[rightPos]]--;
        }
        rightPos++;
        while (count==0) { // 找到子串,左邊向右收縮
            if(rightPos-leftPos<min){
                min = rightPos -leftPos;
                res = s.substr(leftPos,rightPos-leftPos);
            }
            if(rightData.find(s[leftPos])!=rightData.end()){
                rightData[s[leftPos]]++;
                if(rightData[s[leftPos]]>0)
                    count++;
            }
            leftPos++;
        }
    }
    return res;
}

在LeetCode評論區看到一個化簡后的寫法,其實里面的j就是rightPos,i就是leftPos

    int lengthOfLongestSubstring(string s) {
        int  size,i=0,j,k,max=0;
        size = s.size();
        for(j = 0;j<size;j++){
            for(k = i;k<j;k++)
                if(s[k]==s[j]){
                    i = k+1;
                    break;
                }
            if(j-i+1 > max)
                max = j-i+1;
        }
        return max;
    }

例3

給定一個字符串,請你找出其中不含有重復字符的 最長子串 的長度。(longest-substring-without-repeating-characters)

輸入: "abcabcbb"
輸出: 3 
解釋: 因為無重復字符的最長子串是 "abc",所以其長度為 3。

通過例2,我們發現這種滑動窗口的問題可以用一左一右兩個指針來解決。

和例 2 相似,我們不斷的擴大/縮小窗口,把無重復字母的窗口大小保存下來,直到窗口滑動結束,就找到了不含有重復字符的 最長子串 的長度。

思路

leftPos 窗口左指針

rightPos 窗口右指針

只要保證窗口內的子串沒有重復字符即可,用map來記錄

其實這也是遍歷一遍所有符合條件的子串的方法,時間復雜度為O(n)

C++代碼如下:

int lengthOfLongestSubstring(string str){
    map<char,int> strMap;
    int leftPos = 0;
    int rightPos = 0;
    int max = INT32_MIN;
    string res;
    while (rightPos<str.length()){
        if(strMap.find(str[rightPos])==strMap.end()){
            strMap[str[rightPos]] = 1;
            rightPos++;
        } else{
            while (leftPos<rightPos){
                if(str[leftPos] == str[rightPos]){
                    strMap.erase(str[leftPos]);
                    leftPos++;
                    break;
                } else{
                    strMap.erase(str[leftPos]);
                    leftPos++;
                }
            }
        }
        if(rightPos-leftPos>max){
            max = rightPos-leftPos;
            res = str.substr(leftPos,max);
        }
    }
    cout<<res<<endl;
    return max;
}

例4

給定一個字符串 s 和一個非空字符串 p,找到 s 中所有是 p 的字母異位詞的子串,返回這些子串的起始索引。(find-all-anagrams-in-a-string)

輸入:
s: "cbaebabacd" p: "abc"

輸出:
[0, 6]

解釋:
起始索引等於 0 的子串是 "cba", 它是 "abc" 的字母異位詞。
起始索引等於 6 的子串是 "bac", 它是 "abc" 的字母異位詞。

與示例 1 類似,我們維護一個長度為 p 的窗口,然后不斷往右滑動查找當前窗口是否為 p 的字母異位詞。

方法總結:

具體來說:

  1. 雙指針begin,end——記錄滑動窗口的左右邊界。
  2. 一個Hash表——記錄的t中的所有字符(去重)以及每個字符的出現次數。原因:由於t中可能包含重復字符,那么不僅要依次判斷窗口子序列是否包含t中某個字符,還要判斷該字符出現的次數是否與在t中相同。既然字符本身和出現次數相關聯,那么就可以用一對鍵值對來表示,所以可使用Hash表來保存t中的字符和出現頻率。C++中,我們用unordered_map<char, int> map;
  3. 一個計數器count,記錄t中包含的字符數(去重后),即需要判斷是否存在於t的字符。
  4. 令begin = 0, end = 0;移動右邊界,每當發現一個字符存在於t中,遞減該字符在Hash表中出現頻次,即<key,value>中value的值,遞減至0時,說明該窗口子序列中至少包含了與t中相同個數的該字符,那么此時遞減count計數器,表示該字符的判斷已完成,需要判斷的字符數-1.
  5. 以此類推,不斷拓展右邊界,直至count為0,表示窗口序列中已經至少包含了t中所有字符(包括重復的)。
  6. 分析此時的窗口子序列,t是該序列的子集,條件2已滿足。如果兩者長度相同,即滿足條件3,那么它的左邊界begin就是我們想要的結果之一了。但我們不會一直那么幸運,這時就需要收縮窗口的左邊界,即end不動,begin向右移動遍歷該子序列,直至找到t中包含的字符,此時再次計算end-begin的值,與t長度比較,判斷是否是想要的結果。而找到上述字符后,字符頻次加1,如加1后該字符頻次仍小於0,說明該字符有冗余,而出現頻次大於0,則count加1,這是告訴我們有一個字符需要重新被判斷了,因為無論它是不是我們想要的,都不能再用了,需要繼續向右拓展窗口從新找起。
  7. 當count != 0時,繼續向右拓展窗口,直至count為0,然后判斷條件3的同時,向右移動begin遍歷子序列,直至count != 0,以此類推。

參考鏈接

https://www.jianshu.com/p/869f6d00d962

https://www.zhihu.com/topic/20746237/intro


免責聲明!

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



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