題目:Given a string S and a string T, find the minimum window in S which will contain all the characters in T in complexity O(n).
For example,
S = "ADOBECODEBANC"
T = "ABC"
Minimum window is "BANC"
.
Note:
If there is no such window in S that covers all characters in T, return the emtpy string ""
.
If there are multiple such windows, you are guaranteed that there will always be only one unique minimum window in S.
前面兩個系列講了O(N*M)和O(NlogM)的解法。下面講一下O(N)的。人生可不就是一場不停的戰斗么。。。?
實際上leetcode已經有這個的詳細解法和介紹了,大家可以看看這里。
以下部分我翻譯自leetcode上的解說。
注意到前面的解法(O(NlogM))用到了一個hash table,一個隊列(二娃:C++解法中使用隊列來實現charAppearanceRecorder),一個sorted map,真是要有多復雜有多復雜。在面試中,問題通常來說比較短,代碼也大多會不超過50行,所以一定要和面試官交流,讓他知道你的想法。如果你的思路比較復雜,面試官可能會給出一些提示。如果你半天都找不到好的方法又悶聲不響地在那摳,那就悲劇鳥。
我們下面使用 S = "acbbaca" and T = "aba" 來演示這個算法。基本思路是在遍歷S的過程中,使用兩個指針(合法window的begin和end索引)和兩個table(needToFind和hasFound),needToFind保存T中每個字母的個數(二娃:相當於我們的needToFill),hasFound保存當前收集到的每個字母的個數。我們也同時使用一個count變量,用來保存當前收集到的字母總數,但是那些收集過了的字母數並不算在count里面。這樣的話,當count等於T.length,那我們就知道遇到一個合法的window了。
我們利用end指針來遍歷S,假設當前end指向S中的字母x,如果x是T中的字母,hasFound[x]加一。如果hasFound[x]目前還小於等於needToFind[x](二娃:說明字母x還沒有收集全或者剛剛收集全哦),那么我們同時也增加count。當合法window的條件滿足,也就是count等於T.length,我們立即遞增begin指針,並且保證在遞增的過程中始終有count等於T.length。
在遞增begin指針的過程中,我們怎么樣才能保證“始終有count等於T.length”呢?
假設begin指向字母x,如果hasFound[x]大於了needToFind[x],hasFound[x]減去一,同時遞增begin。(二娃:這里很有畫面感哦。因為當前遇到的x是冗余的靠左的字母,這里的操作其實等價於前面兩個算法中的“刪除charAppearanceRecorder中相應的字母的鏈表頭節點”,有點兒像一個是lazy去重,一個是eager去重)否則的話,當前的begin就是window的起始索引了。
接下來我們就可以通過end - begin + 1得到當前window的長度了。這里便可以更新最小window長度。
算法實際上首先找到第一個合法的window,然后在接下來的掃描過程中保持window的合法性(二娃:其實就是count 始終小於等於(當遇到新window)T.length)。
看下面的圖圖。
i)S = "acbbaca" and T = "aba".
ii)找到第一個合法的window。這里注意我們不能遞增begin指針因為hasFound['a'] 等於 needToFind['a'],即2. 如果我們此時遞增begin,那就不是合法window了。
iii)找到第二個合法的window。begin指針指向第一個a,hasFound['a']等於3,而needToFind['a']等於2,說明這個a是一個冗余的a,我們遞減hasFound['a']同時遞增begin。
iv)我們也需要跳過那些不在T中的字母,例如上面的c。現在beging指向了b,hasFound['b']等於2,大於了needToFind['b'],說明這也是一個冗余的b,我們遞減hasFound['a']同時遞增begin。
v)begin指向b,這時候hasFound['b']等於needToFind['b']。不能再減了,同時begin也不能再次移動了,這里就是一個短window的起點位置。
begin和end都最多前進N次,從而整個算法執行小於2N. 復雜度是O(N)。
上述算法的代碼如下:

1 public String minWindow3(String S, String T){ 2 HashMap<Character, Integer> needToFill = new HashMap<Character, Integer>(); 3 HashMap<Character, Integer> hasFound = new HashMap<Character, Integer>(); 4 int count = 0; 5 for(int i = 0; i < T.length(); i++){ 6 if(!needToFill.containsKey(T.charAt(i))){ 7 needToFill.put(T.charAt(i), 1); 8 hasFound.put(T.charAt(i), 0); 9 }else { 10 needToFill.put(T.charAt(i), needToFill.get(T.charAt(i)) + 1); 11 } 12 } 13 int minWinBegin = -1; 14 int minWinEnd = S.length(); 15 for(int begin = 0, end = 0; end < S.length(); end++){ 16 char c = S.charAt(end); 17 if(needToFill.containsKey(c)){ 18 hasFound.put(c, hasFound.get(c) + 1); 19 if(hasFound.get(c) <= needToFill.get(c)){ 20 count++; 21 } 22 if(count == T.length()){ 23 while(!needToFill.containsKey(S.charAt(begin)) || 24 hasFound.get(S.charAt(begin)) > needToFill.get(S.charAt(begin))) { 25 if(needToFill.containsKey(S.charAt(begin)) 26 && hasFound.get(S.charAt(begin)) > needToFill.get(S.charAt(begin))){ 27 hasFound.put(S.charAt(begin), hasFound.get(S.charAt(begin)) - 1); 28 } 29 begin++; 30 } 31 if(end - begin < minWinEnd - minWinBegin){ 32 minWinEnd = end; 33 minWinBegin = begin; 34 } 35 } 36 } 37 } 38 return minWinBegin == -1 ? "" : S.substring(minWinBegin, minWinEnd + 1);
這個算法的亮點是只用一個整型變量就判斷了是否是一個合法window。
該算法教育我們:
1.做題要有畫面感,而且是精確的畫面感;
2.如果用了超過2個復雜數據結構,應該考慮是否有更簡單的思路。;
3.人生是一場不停的戰斗,不斷優化你的算法。