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 中字元出現次數 和 視窗中的相應字元的出現次數
- 初始狀態
- 增加right,直到視窗[left,right]包含了 T 中所有字元
- 現在開始增加 left ,縮小視窗[left,right]
- 直到視窗中的字串不再符合要求,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 中全部字母的一個子串,且這個子串一定是所有可能子串中最短的
思路:
- 使用雙指標中的左右指標技巧,初始化left = right = 0,把索引左閉右開[left,right)稱為一個視窗
- 不斷增加right 指標擴大視窗,直到視窗中的字串符合要求(包含了T中所有字元)
- 此時,停止增加right,轉而不斷增加 left 指標縮小視窗,直到視窗中的字串不再符合要求,同時,每次增加left 都要更新一輪結果
- 重複2、3步,直到right到達字串S盡頭
套模版,思考四個問題
- 當移動 right 擴大視窗,即加入字元時,應該更新哪些資料?
- 什麼條件下,視窗應該暫停擴大,開始移動left縮小視窗?
- 當移動left縮小視窗,即移除字元時,應該更新哪些資料?
- 我們要的結果應該在擴大視窗時還是縮小視窗時進行更新?
當一個字元進入視窗,應該增加 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條件是存在重複元素
換言之收縮完成後一定保證視窗中沒有重複