原文链接地址
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)
T里面是允许重复字母的。一个符合条件的Window至少要包含和T中一样数目的相应字母。例如例子中如果T是AABC,那Minimum Window 就是整个S了。
清楚下面几点:
- 扫描S的过程中,如何判断已经收集到所有目标字母,当前还差哪些字母要收集?正如我们在把妹的过程中mm经常问的: Where are we? 这里要注意,就是有可能T的某些字母还没有收集完毕,另一些字母可能远远超过了T需要的数目。
- 当我们知道已经收集到所有目标字母后,如何知道当前window的起点和终点,用以和以前记录下来的最小window做比较?
- 在确认出现一个window以后,再次继续扫描S(如果S还没有扫描完毕的话),如何更新当前已经收集到的目标字母集合?因为我们不可能一下跳过这个window,因为它的右边部分可能还能和未扫描的S部分组合成更小的window,那么肯定需要想办法把window做最小的右移。
我们用一个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的起始点。
代码如下:
public String minWindow(String S, String T) {
//记录T中每一个字母出现的次数
HashMap<Character, Integer> needToFill = new HashMap<Character, Integer>();
//记录S中出现的T字母的位置
HashMap<Character, LinkedList<Integer>> charAppearenceRecorder = new HashMap<Character, LinkedList<Integer>>();
HashMap<Character, Integer> charBit = new HashMap<Character, Integer>();
int bit_cnt = 0;
for(int i = 0; i < T.length(); i++){
if(needToFill.containsKey(T.charAt(i))){
needToFill.put(T.charAt(i), needToFill.get(T.charAt(i)) + 1);
}else {
needToFill.put(T.charAt(i), 1);
charBit.put(T.charAt(i), bit_cnt++);
charAppearenceRecorder.put(T.charAt(i), new LinkedList<Integer>());
}
}
long upper = (1 << bit_cnt) - 1;//当bit_status == upper时,表示收集完所有的字母
long bit_status = 0;
int minWinStart = -1;
int minWinEnd = S.length();
for(int i = 0; i < S.length(); i++){
char c = S.charAt(i);
if(needToFill.containsKey(c)){
LinkedList<Integer> charList = charAppearenceRecorder.get(c);
charList.add(i);
if(charList.size() == needToFill.get(c)){
//字母c已经收集完毕,那么我们设置c对应的位
bit_status |= (1 << charBit.get(c));
}
if(charList.size() > needToFill.get(c) && bit_status != upper){
charList.removeFirst();
}
if(bit_status == upper){//收集到了合法的一个window
int start = startIndex(charAppearenceRecorder);
if(i - start <= minWinEnd - minWinStart){
minWinEnd = i;
minWinStart = start;
}
char charToShift = S.charAt(start);
charList = charAppearenceRecorder.get(charToShift);
charList.removeFirst();
bit_status -= (1 << charBit.get(charToShift));
}
}
}
return minWinStart == -1 ? "" : S.substring(minWinStart, minWinEnd + 1);
}
举个例子:
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(N)
基本思路是在遍历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)。
实现代码:
// Returns false if no valid window is found. Else returns
// true and updates minWindowBegin and minWindowEnd with the
// starting and ending position of the minimum window.
bool minWindow(const char* S, const char *T,
int &minWindowBegin, int &minWindowEnd) {
int sLen = strlen(S);
int tLen = strlen(T);
int needToFind[256] = {0};
for (int i = 0; i < tLen; i++)
needToFind[T[i]]++;
int hasFound[256] = {0};
int minWindowLen = INT_MAX;
int count = 0;
for (int begin = 0, end = 0; end < sLen; end++) {
// skip characters not in T
if (needToFind[S[end]] == 0) continue;
hasFound[S[end]]++;
if (hasFound[S[end]] <= needToFind[S[end]])
count++;
// if window constraint is satisfied
if (count == tLen) {
// advance begin index as far right as possible,
// stop when advancing breaks window constraint.
while (needToFind[S[begin]] == 0 ||
hasFound[S[begin]] > needToFind[S[begin]]) {
if (hasFound[S[begin]] > needToFind[S[begin]])
hasFound[S[begin]]--;
begin++;
}
// update minWindow if a minimum length is met
int windowLen = end - begin + 1;
if (windowLen < minWindowLen) {
minWindowBegin = begin;
minWindowEnd = end;
minWindowLen = windowLen;
} // end if
} // end if
} // end for
return (count == tLen) ? true : false;
}
具体可参考 leetcode 里面的讨论版
该方法类似于我自己写的方法 Minimum Window Substring 。