题目: 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.
这道题其实还有一点没有说得太清楚,导致开始的解得不对。T里面是允许重复字母的。一个符合条件的Window至少要包含和T中一样数目的相应字母。例如例子中如果T是AABC,那Minimum Window 就是整个S了。
搞清了这一点再去做,其实还是想了很久的。主要需要想清楚下面几点。
- 扫描S的过程中,如何判断已经收集到所有目标字母,当前还差哪些字母要收集?正如我们在把妹的过程中mm经常问的: Where are we? 这里要注意,就是有可能T的某些字母还没有收集完毕,另一些字母可能远远超过了T需要的数目。
- 当我们知道已经收集到所有目标字母后,如何知道当前window的起点和终点,用以和以前记录下来的最小window做比较?
- 在确认出现一个window以后,再次继续扫描S(如果S还没有扫描完毕的话),如何更新当前已经收集到的目标字母集合?因为我们不可能一下跳过这个window,因为它的右边部分可能还能和未扫描的S部分组合成更小的window,那么肯定需要想办法把window做最小的右移。
这里有三个解法(本娃就figure out出来复杂度最高的),复杂度依次是O(N*M), O(NlogM)和O(N),其实都是用不同的方式来解决上面三点。
解法一:O(N*M)
我们用一个hash table(名字叫needToFill)来记录T中每一个字母出现的次数;一个hash table(名字叫charAppearenceRecorder)来存储扫描到当前位置 so far在S中出现的T字母的位置。因为可能一个字母需要出现多次,charAppearenceRecorder以T中的每个字母为key,value是一个LinkedList,每一个节点是一个整型的index,表示该字母在S中的位置;最后用一个hash table来为T中每一个字母表示成一个整数,例如T=“ABC”, 那么A=0,B=1,C=2,这样在扫描过程中,我们利用位操作一个整数表示当前某个字母是否已经收集完毕了。
在遍历S的过程中,如果某个字母c属于T,那么我们把它加入到charAppearenceRecorder对应字母的链表(尾)中。如果链表的长度等于了needToFill中记录的T要求的该字母的数目,那么记录c字母收集完毕(例如如果c 是A,我们利用位操作把第0为置1)。而如果链表的长度大于needToFill中记录的T要求的该字母的数目,我们删除对应字母的链表的头节点,也就是最早遇到的该字母的index。这样,charAppearenceRecorder始终保持合法数目的字母,同时,在超过要求数目的字母出现时候,总是选择靠右的合法数目的字母,以缩短window长度。
当发现一个合法的window时,我们可以通过遍历charAppearenceRecorder的所有链表的头节点,找出start index(here就是O(N*M)中的M来历了),更新最小window的起始点。
代码如下:
1 public String minWindow(String S, String T) { 2 //记录T中每一个字母出现的次数 3 HashMap<Character, Integer> needToFill = new HashMap<Character, Integer>(); 4 //记录S中出现的T字母的位置 5 HashMap<Character, LinkedList<Integer>> charAppearenceRecorder = new HashMap<Character, LinkedList<Integer>>(); 6 HashMap<Character, Integer> charBit = new HashMap<Character, Integer>(); 7 int bit_cnt = 0; 8 for(int i = 0; i < T.length(); i++){ 9 if(needToFill.containsKey(T.charAt(i))){ 10 needToFill.put(T.charAt(i), needToFill.get(T.charAt(i)) + 1); 11 }else { 12 needToFill.put(T.charAt(i), 1); 13 charBit.put(T.charAt(i), bit_cnt++); 14 charAppearenceRecorder.put(T.charAt(i), new LinkedList<Integer>()); 15 } 16 } 17 long upper = (1 << bit_cnt) - 1;//当bit_status == upper时,表示收集完所有的字母 18 long bit_status = 0; 19 int minWinStart = -1; 20 int minWinEnd = S.length(); 21 for(int i = 0; i < S.length(); i++){ 22 char c = S.charAt(i); 23 if(needToFill.containsKey(c)){ 24 LinkedList<Integer> charList = charAppearenceRecorder.get(c); 25 charList.add(i); 26 if(charList.size() == needToFill.get(c)){ 27 //字母c已经收集完毕,那么我们设置c对应的位 28 bit_status |= (1 << charBit.get(c)); 29 } 30 if(charList.size() > needToFill.get(c) && bit_status != upper){ 31 charList.removeFirst(); 32 } 33 if(bit_status == upper){//收集到了合法的一个window 34 int start = startIndex(charAppearenceRecorder); 35 if(i - start <= minWinEnd - minWinStart){ 36 minWinEnd = i; 37 minWinStart = start; 38 } 39 char charToShift = S.charAt(start); 40 charList = charAppearenceRecorder.get(charToShift); 41 charList.removeFirst(); 42 bit_status -= (1 << charBit.get(charToShift)); 43 } 44 } 45 } 46 47 return minWinStart == -1 ? "" : S.substring(minWinStart, minWinEnd + 1); 48 }
举个栗子。
S=“acbbaca“ T=“aba”
当扫描到i=3的时候,遇到一个b。我们还没有遇到足够数量的a,但是b的数量,当加入当前的b以后,就超过了要求的数目。
于是我们删除charAppearenceRecorder中对应的b的第一个节点2,继续扫描。
这时候我们再次遇到a,这样,所有的T中的字母收集完毕,红色部分覆盖了一个合法的window,通过找到charAppearenceRecorder中的最小元素(蓝色部分),可以知道当前找到的window的长度4 - 0 + 1 = 5.因为之前没有合法window,所以当前最短就是5了。
在指针再次递进的之前,我们需要更新bit_status状态和charAppearenceRecorder。对charAppearenceRecorder,其实就是简单删除起始索引节点,同时在bit_status中重置对应的bit位。这样,我们表示在期待下一个a了,而且window总是最短的。
最后,我们移动到了6. 这也是一个合法的window。对比之前的长度,6-3+1 = 4明显小于5,所以最短的覆盖T中所有字母的window就是从3到6的这个window。
总结下这个方法:
1.使用bit位来表示收集到足够数目的字母;
2.合理的hash table和linkedlist运用。
3.不足的地方是每次需要在charAppearenceRecorder里面寻找最小的index,来计算window的长度,造成O(N*M)的时间复杂度。
下面一个系列,我们来讨论O(NlogM)的解法。