滑動視窗
- 基本概念
- 最小覆寫子串
- 字母異位詞
- 無重複字元的最長子串
- 滑動視窗技巧總結
- 滑動視窗: 是進階雙指針技巧的算法架構,涉及字元串比對問題
- 滑動視窗使用的資料結構:
- unordered_map : 哈希表
- 包含一個方法count(key), 相當于containsKey(key). 可以判斷key是否存在
- map[key] :
- 可以使用方括号通路鍵對應的值map[key]. 需要注意,如果該key不存在,會自動建立這個key, 并且将map[key] 指派為0
- map[key]++ 相當于Java的map.put(key, map.getOrDefault(key, 0) + 1)
- 題目: 要在S(source) 中找到包含T(target) 中全部字母的一個子串,這個子串一定要是所有子串中最短的
- 直接法求解的代碼:
for (int i = 0; i < s.size(); i++):
for (int j = i + 1; j < s.size(); j++):
if s[i : j] 包含 t 的所有字母:
更新結果
- 思路簡單直接,但是這個算法的時間複雜度肯定大于O(N2) 了
- 滑動視窗算法思路: 尋找一個可行解,然後優化這個可行解,最終找到最優解.左右指針輪流前進,視窗大小增增減減,不斷向右滑動
- 在字元串S中使用雙指針中的左右指針技巧,初始化left = right = 0, 将索引閉區間[left,right]作為一個 [視窗]
- 首先不斷增加right指針來擴大視窗 [left, right], 直到視窗中的字元串符合包含T中的所有字元的要求
- 然後,停止增加right, 通過不斷增加left指針縮小視窗 [left,right], 直到視窗中的字元串不再符合包含T中的所有字元的要求.同時,每次增加left, 都要更新一輪結果
- 重複以上兩個步驟,直到right到達S的盡頭
- 滑動視窗算法僞碼架構:
String s, t;
int left = 0, right = 0;
// res是符合要求的最小覆寫子串
String res = s
/*
* 在s中尋找t的最小覆寫子串
*/
while (right < s.size()) {
// window是對應的滑動視窗,即子串s[left,right]
window.add(s[right]);
right++;
while (window 符合要求) {
// 如果這個視窗的子串更短,則更新res
res = minLen(res, window);
window.remove(s[left]);
}
}
return res;
- 如何判斷window中,也就是子串s[left,right]是否符合要求,是否包含t的所有字段:
- 可以使用兩個哈希表當作計數器解決
- 使用一個哈希表needs記錄字元串t中包含的字元及出現次數
- 使用另一個哈希表window記錄目前 [視窗] 中包含的字元及出現的次數
- 如果window包含所有needs中的鍵,并且這些鍵對應的值都大于等于needs中的值,那麼目前視窗就符合要求,就可以開始移動left指針
- 滑動視窗算法架構優化:
String s, t;
int left = 0, right = 0
String res = s;
/*
* 建立兩個hash表作為字元的計數器
*/
unordered_map<char, int> window;
unordered_map<char, int> needs;
for (Char c : t) needs[c]++;
// 記錄window中符合要求的字元的個數
int match = 0;
/*
* 在s中尋找t的最小覆寫子串
*/
while (right < s.size()) {
char r = s[right]
if (needs.count(r)) {
// 如果r在needs中,則将r加入window
window[r]++;
if (window[r] == needs[r]) {
// 字元串r出現的次數符合要求
match++;
}
}
right++;
// window中的字元串是符合要求的最小覆寫子串
while(match == needs.size()) {
res = minLen(res, window);
char l = s[left];
if (needs.count(l)) {
// 如果l在needs中,則将l移出window
window[l]--;
if (window[l] < needs[l]) {
// 字元串l出現的次數不符合要求
match--;
}
}
left++
}
}
return res;
String minWindow(String s, String t) {
// 記錄最短子串的開始位置和長度
int start = 0, min_len = INIT_MAX;
int left = 0, right = 0;
// 建立兩個Hash表作為字元的計數器
unordered_map<char, int> window;
unordered_map<char, int> needs;
for (char c : t) need[c]++;
// 記錄符合要求的字元個數
int match = 0;
// 在s中尋找t的最小覆寫子串
while (right < s.size()) {
char r = s[right];
if (needs.count(r)) {
window[r]++;
if (window[r] == needs[r]) {
match++;
}
}
right++;
while (match == s.size()) {
if (right - left < minLen) {
// 如果滑動視窗的長度小于最小長度,則更新最小長度
start = left;
minLen = right - left;
}
char l = s[left];
if (needs.count(l)) {
window[l]--;
if (window[l] < needs[l]) {
match--
}
}
left++;
}
}
return minLen == INIT_MAX ? "" : s.substr(start, minLen);
}
- 算法的時間複雜度為O(M + N),M和N分别是字元串S和T的長度
- 因為首先用for循環周遊了字元串T來初始化needs. 時間為O(N)
- 之後的兩個循環最多執行2M次,時間複雜度為O(M). 因為while循環的次數就是雙指針left和right走過的總路程
vector<int> findAnagrams(String s, String t) {
// 使用數組記錄答案
vector<int> res;
int left = 0, right = 0;
// 建立兩個Hash表作為字元的計數器
unordered_map<char, int> needs;
unordered_map<char, int> window;
// 記錄要求比對的字元的個數
int match = 0;
while (right < s.size()) {
char r = s[right];
if (needs.count(r)) {
window[r]++;
if (window[r] == needs[r]) {
match++;
}
}
right++;
while (match == s.size()) {
// 如果比對的字元串和要求的長度相同則加入結果
if (right - left = t.size) {
res.push_back(left)
}
char l = s[left];
if (needs.count(left)) {
window[l]--;
if (window[l] < needs[l]) {
match--;
}
}
left++
}
}
return res;
}
- 子串相關問題,首先想到使用滑動視窗技巧
- 使用window作為計數器記錄視窗中字元出現次數,然後向右移動right, 當window中出現重複字元時,開始移動left縮小視窗.重複執行:
int lengthOfLongestSubstring(String s) {
int left = 0, right = 0;
// 建立一個Hash表作為字元計數器
unordered_map<char, int> window;
// 記錄字元串的長度
int res = 0;
while (right < s.size()) {
char r = s[right];
window[r]++;
right++;
// 如果window中出現重複字元,則開始縮小window視窗
while (window[r] > 1) {
char l = s[left];
window[l]--;
left++;
}
// 每次移動right時,更新res值
res = max(res, right - left);
}
return res;
}
- 因為要求的是最長子串,是以需要在每次移動right增大視窗時更新res
int left = 0, right = 0;
while (righ < s.size()) {
window.add(s[right]);
right++;
while (valid) {
window.remove(s[left]);
left++;
}
}
def minWindow(self, s : str, t : str) -> str:
# 字元串開始位置和長度
start, min_len = 0, float('Inf')
left, right = 0, 0
res = s
# 兩個計數器
needs = Counter(t)
# defaultdict在通路的key不存在的時候會傳回預設值0
window = collections.defaultdict(int)
# 記錄比對要求的字元的個數
match = 0
while right < len(s):
r = s[right]
if needs[r] > 1:
window[r] += 1
if window[r] == needs[r]:
match += 1
right += 1
while match = len(needs):
if right - left < min_len:
start = left
min_len = right - left
l = s[left]
if needs[l] > 0:
window[l] -= 1
if window[l] < needs[l]:
match -= 1
left -= 1
return s[start : start + min_len] if min_len != float("Inf") else ""