LeetCode 笔记系列16.3 Minimum Window Substring [从O(N*M), O(NlogM)到O(N),人生就是一场不停的战斗]
题目: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.人生是一场不停的战斗,不断优化你的算法。