【題目描述】
給你一個字符串 s 、一個字符串 t 。返回 s 中涵蓋 t 所有字符的最小子串。如果 s 中不存在涵蓋 t 所有字符的子串,則返回空字符串 "" 。
注意:如果 s 中存在這樣的子串,我們保證它是唯一的答案。
示例 1:
輸入:s = "ADOBECODEBANC", t = "ABC" 輸出:"BANC"
示例 2:
輸入:s = "a", t = "a" 輸出:"a"
示例 3:
輸入:s = "a", t = "bb" 輸出:""
來源:力扣(LeetCode)第76題 鏈接:https://leetcode-cn.com/problems/minimum-window-substring 著作權歸領扣網絡所有。商業轉載請聯系官方授權,非商業轉載請注明出處。
熟悉的童靴都知道,這道題屬於雙指針的經典題目:
我們稱之為“滑動窗口”。
【題目分析】
我們先看看題目是什么意思。
本問題要求我們返回字符串 s中包含字符串 t 的全部字符的最小窗口。
我們稱包含 t 的全部字母的窗口為「可行」窗口。
通過示例很容易明白題目意思,只要窗口里包含有目標字符串t中的左右字符,且要求是最短的。
那么什么是“滑動窗口”呢?
我把它比作家中的鋁合金推拉窗。
對就是上圖的這個東東。
在滑動窗口類型的問題中都會有兩個指針。一個用於「延伸」現有窗口的 right 指針(右側),和一個用於「收縮」窗口的 left 指針(左側)。在任意時刻,只有一個指針運動,而另一個保持靜止。我們在 s 上滑動窗口,通過移動 right 指針不斷擴張窗口。當窗口包含 t 中全部所需的字符后,如果左側能收縮,我們就收縮窗口(左側指針 left)直到得到最小窗口。
有人會問,為什么收縮要從左側指針方向?
因為,你擴張是從右側的,當停止擴張時,你想想,為什么會停止擴張?是因為當窗口包含 t 中全部所需的字符了。所有此時最右側的字符一定是必須的,故要從左側縮減,如果能的話。
【圖解示例:參考leetCode】
作者:LeetCode-Solution 鏈接:https://leetcode-cn.com/problems/minimum-window-substring/solution/zui-xiao-fu-gai-zi-chuan-by-leetcode-solution/ 來源:力扣(LeetCode) 著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
假設:
s = ABAACBAB
t = ABC
第一步:
第二步:從第一步到第四步中間少了四步即是從A->B->A->A
第三步:left 縮減至B,仍然是包含了ABC,但不知道是否是最短,只好記錄下來left ,right,len = right - left。
第四步:繼續縮減;
第五步:再次包含ABC,記錄下此時的left ,right,len = len>(right - left) ? (right - left) : len。
第六步:仍然包含ABC,記錄下此時的left ,right,len = len>(right - left) ? (right - left) : len。left 繼續右移。
第七步:
第八步:同上,記錄,比較。不在贅述。
第九步:
第十步:至此后,結束。
思路知道了,那么說一說細節:
1、如何判斷當前的窗口包含所有 t 所需的字符呢?
這涉及到數據結構的運用了,我們知道Java中hashMap查找的時間復雜度是O(1)。
借此,我們可以用一個需要匹配的哈希表 need<char, int> 表示 t 中所有的字符以及它們的個數,用一個窗口匹配哈希表 windows<char, int> 動態維護窗口中所有的字符以及它們的個數,如果這個動態表中包含 t 的哈希表中的所有字符,並且對應的個數都不小於 t 的哈希表中各個字符的個數,那么當前的窗口是「可行」的,即是滿足“包含”的條件的。
2、如何判斷這個動態表windows中是否包含 t 的哈希表中的所有字符,並且對應的個數都不小於 t 的哈希表中各個字符的個數?
可以利用一個int 變量即可,設為kind,我稱之為種類,即當windows中達到need中某字符的數量時,該變量加1。
【代碼實現】
//C++
class Solution { public: string minWindow(string s, string t) { //if(s.size() < t.size()) return ""; unordered_map<char, int> need,windows; for(char c:t) need[c]++; int kind = 0;//windows中的種類 int left = 0; int right = 0; int len = INT_MAX;//返回長度 int start = 0;//返回起始下標 while(right < s.size()){ //current char char c = s[right++]; if(need.count(c)){ windows[c]++; if(windows[c] == need[c]){ kind++; } } //if windows中種類齊全了,縮減左側 while(kind == need.size()){ if(right - left < len){ start = left; len = right - left; } char d = s[left++]; if(need.count(d)){ windows[d]--; if(need[d] > windows[d]){ kind--; } } } } return len == INT_MAX ? "" : s.substr(start,len); } };
【Java】
class Solution { Map<Character, Integer> ori = new HashMap<Character, Integer>(); Map<Character, Integer> cnt = new HashMap<Character, Integer>(); public String minWindow(String s, String t) { int tLen = t.length(); for (int i = 0; i < tLen; i++) { char c = t.charAt(i); ori.put(c, ori.getOrDefault(c, 0) + 1); } int l = 0, r = -1; int len = Integer.MAX_VALUE, ansL = -1, ansR = -1; int sLen = s.length(); while (r < sLen) { ++r; if (r < sLen && ori.containsKey(s.charAt(r))) { cnt.put(s.charAt(r), cnt.getOrDefault(s.charAt(r), 0) + 1); } while (check() && l <= r) { if (r - l + 1 < len) { len = r - l + 1; ansL = l; ansR = l + len; } if (ori.containsKey(s.charAt(l))) { cnt.put(s.charAt(l), cnt.getOrDefault(s.charAt(l), 0) - 1); } ++l; } } return ansL == -1 ? "" : s.substring(ansL, ansR); } public boolean check() { Iterator iter = ori.entrySet().iterator(); while (iter.hasNext()) { Map.Entry entry = (Map.Entry) iter.next(); Character key = (Character) entry.getKey(); Integer val = (Integer) entry.getValue(); if (cnt.getOrDefault(key, 0) < val) { return false; } } return true; } }
【復雜度分析】
- 時間復雜度:最壞情況下左右指針對 s 的每個元素各遍歷一遍,哈希表中對 s 中的每個元素各插入、刪除一次,對 t 中的元素各插入一次。每次檢查是否可行會遍歷整個 t 的哈希表,哈希表的大小與字符集的大小有關,設字符集大小為 C,則漸進時間復雜度為 O(C⋅∣s∣+∣t∣)。
- 空間復雜度:這里用了兩張哈希表作為輔助空間,每張哈希表最多不會存放超過字符集大小的鍵值對,我們設字符集大小為 C ,則漸進空間復雜度為 O(C)。
Over...