Minimum Window Substring [从O(N*M),O(NlogM) 到O(N)]

原文链接地址

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了。

清楚下面几点:

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


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值