簡介
在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
給定一個字符串 S 和一個字符串 T,請在 S 中找出包含 T 所有字母的最小子串。(minimum-window-substring)
輸入: S = "ADOBECODEBANC", T = "ABC" 輸出: "BANC"
思路:左右指針滑動窗口
這個問題讓我們無法按照示例 1 中的方法進行查找,因為它不是給定了窗口大小讓你找對應的值,而是給定了對應的值,讓你找最小的窗口。
我們仍然可以使用滑動窗口算法,只不過需要換一個思路。
既然是找最小的窗口,我們先定義一個最小的窗口,也就是長度為 0 的窗口。
我們比較一下當前窗口在的位置的字母,是否是 T 中的一個字母。
很明顯, A 是 ABC 中的一個字母,也就是 T 所有字母的最小子串 可能包含當前位置的 S 的值。
如果包含,我們開始擴大窗口,直到擴大后的窗口能夠包含 T 所有字母。
假設題目是 在 S 中找出包含 T 所有字母的第一個子串,我們就已經解決問題了,但是題目是找到最小的子串,就會存在一些問題。
- 當前窗口內可能包含了一個更小的能滿足題目的窗口
- 窗口沒有滑動到的位置有可能包含了一個更小的能滿足題目的窗口
為了解決可能出現的問題,當我們找到第一個滿足的窗口后,就從左開始縮小窗口。
- 如果縮小后的窗口仍滿足包含 T 所有字母的要求,則當前窗口可能是最小能滿足題目的窗口,儲存下來之后,繼續從左開始縮小窗口。
- 如果縮小后的窗口不能滿足包含 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 的字母異位詞。
方法總結:
具體來說:
- 雙指針begin,end——記錄滑動窗口的左右邊界。
- 一個Hash表——記錄的t中的所有字符(去重)以及每個字符的出現次數。原因:由於t中可能包含重復字符,那么不僅要依次判斷窗口子序列是否包含t中某個字符,還要判斷該字符出現的次數是否與在t中相同。既然字符本身和出現次數相關聯,那么就可以用一對鍵值對來表示,所以可使用Hash表來保存t中的字符和出現頻率。C++中,我們用unordered_map<char, int> map;
- 一個計數器count,記錄t中包含的字符數(去重后),即需要判斷是否存在於t的字符。
- 令begin = 0, end = 0;移動右邊界,每當發現一個字符存在於t中,遞減該字符在Hash表中出現頻次,即<key,value>中value的值,遞減至0時,說明該窗口子序列中至少包含了與t中相同個數的該字符,那么此時遞減count計數器,表示該字符的判斷已完成,需要判斷的字符數-1.
- 以此類推,不斷拓展右邊界,直至count為0,表示窗口序列中已經至少包含了t中所有字符(包括重復的)。
- 分析此時的窗口子序列,t是該序列的子集,條件2已滿足。如果兩者長度相同,即滿足條件3,那么它的左邊界begin就是我們想要的結果之一了。但我們不會一直那么幸運,這時就需要收縮窗口的左邊界,即end不動,begin向右移動遍歷該子序列,直至找到t中包含的字符,此時再次計算end-begin的值,與t長度比較,判斷是否是想要的結果。而找到上述字符后,字符頻次加1,如加1后該字符頻次仍小於0,說明該字符有冗余,而出現頻次大於0,則count加1,這是告訴我們有一個字符需要重新被判斷了,因為無論它是不是我們想要的,都不能再用了,需要繼續向右拓展窗口從新找起。
- 當count != 0時,繼續向右拓展窗口,直至count為0,然后判斷條件3的同時,向右移動begin遍歷子序列,直至count != 0,以此類推。