LeetCode 笔记系列16.1 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.

这道题其实还有一点没有说得太清楚,导致开始的解得不对。T里面是允许重复字母的。一个符合条件的Window至少要包含和T中一样数目的相应字母。例如例子中如果T是AABC,那Minimum Window 就是整个S了。

搞清了这一点再去做,其实还是想了很久的。主要需要想清楚下面几点。

  1. 扫描S的过程中,如何判断已经收集到所有目标字母,当前还差哪些字母要收集?正如我们在把妹的过程中mm经常问的: Where are we? 这里要注意,就是有可能T的某些字母还没有收集完毕,另一些字母可能远远超过了T需要的数目。
  2. 当我们知道已经收集到所有目标字母后,如何知道当前window的起点和终点,用以和以前记录下来的最小window做比较?
  3. 在确认出现一个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     }
O(N*M)

举个栗子。

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)的解法。

posted on 2013-07-16 16:48  lichen782  阅读(1245)  评论(0编辑  收藏  举报