1. 程式人生 > 其它 >3.滑動視窗演算法

3.滑動視窗演算法

滑動視窗演算法

子串、子陣列問題

連結串列子串陣列題,用雙指標別猶豫

雙指標家三兄弟,各個都是萬人迷

快慢指標最神奇,連結串列操作無壓力

歸併排序找重點,連結串列成環搞判定

左右指標最常見,左右兩端相向行

反轉陣列要靠它,二分搜尋是弟弟

滑動視窗老猛男,子串問題全靠它

左右指標滑視窗,一前一後齊頭進

滑動視窗演算法框架

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;//表示視窗中滿足 need 條件的字元個數,如果和need.size 大小相同,則視窗滿足條件,已經完全覆蓋子串T
	while(right < s.size()){
		char c = s[right];//c 是將移入視窗的字元
		right++;//右移視窗
		...//進行視窗內資料的一系列更新
		
		printf("window: [%d, %d]\n", left, right);//debug 輸出位置
		
		while(window needs shrink){//判斷左側視窗是否收縮
			char d = s[left];// d是將移除視窗的字元
			left++;//左移視窗
	
			...//進行視窗內資料的一系列更新
		}
	}
}

needs 和 window 相當於計數器,分別記錄 T 中字元出現次數 和 視窗中的相應字元的出現次數

  1. 初始狀態
  1. 增加right,直到視窗[left,right]包含了 T 中所有字元
  1. 現在開始增加 left ,縮小視窗[left,right]
  1. 直到視窗中的字串不再符合要求,left不再繼續移動

5.重複上述過程,先移動right,再移動left....直到right指標到達字串S末端,演算法結束。

[unordered_map](.\3.1 unordered_map.md)

滑動視窗很多時候都是在處理字串相關的問題,java處理字串不方便,所有C++實現

unordered_map 雜湊表(字典),count(key)相當於 java的 containsKey(key)可以判斷鍵key是否存在

可以使用方括號訪問鍵對應的值map[key],key不存在,會自動建立這個key,並把map[key]賦值為0

map[key]++相當於java的 map.put(key,map.getOrDefault(key,0)+1)

最小覆蓋子串

返回 s 涵蓋 t 所有字元的最小子串

在 Source 中找到包含 Target 中全部字母的一個子串,且這個子串一定是所有可能子串中最短的

思路:

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

套模版,思考四個問題

  1. 當移動 right 擴大視窗,即加入字元時,應該更新哪些資料?
  2. 什麼條件下,視窗應該暫停擴大,開始移動left縮小視窗?
  3. 當移動left縮小視窗,即移除字元時,應該更新哪些資料?
  4. 我們要的結果應該在擴大視窗時還是縮小視窗時進行更新?

當一個字元進入視窗,應該增加 window 計數器;

當 valid 滿足 need 時應該收縮視窗

如果一個字元將移出視窗時,應該減少 window 計數器

應該在收縮視窗的時候更新最終結果

string minWindow(string s, string t){
	unordered_map<char, int> need, window;
	for(char c : t) need[c]++;//needs = {A:1,B:1,C:1}
	
	int left = 0, right = 0;//視窗區間:[0,0)
	int valid = 0;//視窗中滿足need條件的字元個數
	int start = 0, len = INT_MAX;
	while(right < s.size()){
		char c = s[right];//將移入視窗的字元
		right++;//右移視窗
		if(need.count(c)){
			window[c]++;//1.如果一個字元進入視窗,應該增加window計數器
			if(window[c] == need[c])//出現次數相同
				valid++;//滿足need條件的 字元個數,目標是與need.size()相同
		}

		while(valid == need.size()){//2.當valid滿足need時應該收縮視窗,說明T中所有字元已經被覆蓋,已經得到一個可行的覆蓋子串,現在開始收縮視窗,以便得到最小覆蓋子串
			if(right - left < len){//此處只做校驗,區別於排列的 right - left >= t.size()
			//縮小視窗前的初始化動作
				start = left;//記錄縮小前視窗最左端
				len = right - left;//以及視窗長度
			}
			//鎖定了符合條件的窗左開端,和長度,即包含最小覆蓋子串的整個視窗

			//直到視窗中的字串不再符合要求,left不再繼續移動
			char d = s[left];//d 是將移除視窗的字元
			left++;//縮小視窗
			if(need.count(d)){
				if(window[d] == need[d])//Java的Integer,String等型別判定相等應該用equals
					valid--;//4.應該在收縮視窗的時候更新最終結果
				window[d]--;//3.如果一個字元移除視窗的時候,應該減少window計數器
			}
		}
	}
	return len == INT_MAX ? "" : s.substr(start,len);//?字元擷取函式,從第len個字元開始擷取後面所有的字串
}

[java版本](.\3.2 java版本.md)

字串排列

s2包含s1的排列之一,s1可以包含重複字元

相當於,S中是否存在一個子串,包含T中所有字元且不包含其他字元?

bool checkInclusion(string t, string s) {
        unordered_map<char,int>need,window;
        for(char c : t) need[c]++;

        int left = 0, right = 0;
        int valid = 0;
        while(right < s.size()){
            char c = s[right];
            right++;
            if(need.count(c)){
                window[c]++;
                if(window[c]==need[c]){
                    valid++;
                }
            }
						//縮小視窗的時機是視窗大小大於t.size(),因為排列長度應該一樣
            while(right - left >= t.size()){
                if(valid == need.size()){
                    return true;
                }
                char d = s[left];
                left++;
                if(need.count(d)){
                    if(window[d]==need[d]){
                        valid--;
                    }
                    window[d]--;
                }
            }
        }
        return false;
    }

找所有字母異位詞

異位詞 指由相同字母重排列形成的字串(包括相同的字串)

相當於,找到S中所有T的排列,返回它們的起始索引

vector<int> findAnagrams(string s, string t) {
        unordered_map<char,int>need,window;
        for(char c : t) need[c]++;
        int left = 0,right = 0;
        int valid = 0;
        vector<int> res;
        while(right < s.size()){
            char c = s[right];
            right++;
            if(need.count(c)){
                window[c]++;
                if(window[c]==need[c]){
                    valid++;
                }
            }

            while(right-left >= t.size()){
                if(valid == need.size())
                    res.push_back(left);//push_back() 在Vector最後新增一個元素(引數為要插入的值)

                char d = s[left];
                left++;
                if(need.count(d)){
                    if(window[d] == need[d])
                        valid--;
                    window[d]--;
                }
            }
        }
        return res;
    }

無重複字元的最長子串

int lengthOfLongestSubstring(string s) {
        unordered_map<char,int>window;
        int left = 0,right = 0;
        int res =0;
        while(right < s.size()){
            char c = s[right];
            right++;
            window[c]++;
            while(window[c]>1){
                char d = s[left];
                left++;
                window[d]--;
            }
            res = max(res,right-left);
        }
        return res;
    }

只需要更新視窗內資料,只需要簡單的更新計數器window即可

當window[c]>1說明視窗中存在重複字元,不符合條件,就該移動left縮小視窗了

要在收縮視窗完成後更新res,因為視窗收縮的while條件是存在重複元素

換言之收縮完成後一定保證視窗中沒有重複