LeetCode No.76 Minimum Window Substring 题解

15 篇文章 0 订阅

LeetCode No.76 Minimum Window Substring 题解

题目描述


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 empty string “”.

If there are multiple such windows, you are guaranteed that there will always be only one unique minimum window in S.


题目分析

题目大意:找出s的一个最短子串,使得其为T的一个“Window”(即可以利用这个子串的部分字符进行重排得到T),要求时间复杂度为 O(n)
可能是我对Hash比较擅长一些吧,从最后结果来看, 几乎马上就想到Hash + 双指针 的这种大概思路。现在回过头来,试着来分析一下思路。。(口胡)
首先看到线性复杂度, 只能想到 “预处理 + 扫一遍” 的搜索过程。问题是遍历只能在线性复杂度上进行, 而子串个数是平方级的。 那就基本只能是首尾指针来指示扫描了。
其实双重循环的遍历也可以看做是两个指示变量分别指示首尾, 这个的不同之处在于, 这两个变量只会向后移动, 而不会向前重置。能够这样其实是要靠一定的问题性质来保证的,对这道题来说就有如下性质:

对于任何子串s1, 和任何字符c, 如果s1是T的一个window,那么c + s1, s1 + c 都是T的一个window。

这性质看似废话, 但是说明了我们有可能从一个较大的window,来逐步minimize到一个小的window。
现在把上面那个性质反过来看, 这就是这道题的核心(我认为)了:

对于任何子串s1, 和任何字符c, 如果c + s1 是T的一个window, 若s1.count[c] >= T.count[c], 则s1也是T的一个window。

这就给我们提供了一个可以由大window得到小window的方法。很明显这方法中出现了对一段字符串中某个字符出现次数的统计。 到这里想到用 hash 就不奇怪了吧~。~

现在我们重新整理一下整个流程, 看看还缺些啥。

  1. 对T进行预处理。使得hash table 中能够体现T中字符的出现次数。(我的话是每次-1, 在步骤4更新时每次+1)
  2. 对S利用头尾指针i, j指示其当前正在搜索的子串。
  3. 若当前子串满足window要求, 将i向前移动试图缩小window。
  4. 每次循环结束前移动j, 保证继续向后寻找。

对于3,4两步, 要及时更新hash table的值。对于步骤3, 当得到一个不能更小的window,与当前记录的min_window比较并更新。

这道题真正卡住我的是下面的步骤。 注意到上面由于只需要遍历一遍, 貌似复杂度已达到要求了。 实则不然。 注意第三步,需要判断当前子串是否满足window要求。这是一个O(n)复杂度的操作。而反过来看,我们之前的算法显然没法再优化了。 所以只能想办法把判断当前子串是否是window的复杂度降为O(1)了。

再回到刚才那个性质,那是已知window的情况下, 每需要缩小window的一个长度, 就要比较一个条件是否满足。
换句话说, 其实是满足了 T.size()个条件, 才能使一个子串成为window。(这里有点不严谨, 事实上这个数要比T.size()小, 应该是T 中不同字符的个数)。
那么我们可以用一个int变量,来实时记录当前剩余多少条件不满足即可。 如果剩余为0 , 则是一个window。
我的程序中这个变量名叫做rest,在此列举一下rest主要需要更新的几个地方:
* 预处理时, 更新rest为T中不同字符数。
* 每次移动j时, count[s[j]]变化之后,看其是否恰好为0(即恰好因为这个字符的添加使得某个字符个数满足了要求),--rest
* 在minimize window 时,每次移动i并更新count[s[i]]后,若其恰好为 -1(即刚好去掉的这个字符使得这个字符的条件是不满足要求的),++rest

不知不觉写了这么多。。。其实代码还是一如既往地少。 只是解释起来有点费劲罢了。
下面是实现代码:


class Solution {
public:
    string minWindow(string s, string t) {
        if (s == "") return s;
        int i = 0, j = 0, rest = 0;
        map<char, int> count;
        for (int i = 0; i < t.size(); ++i) {
            if (count[t[i]] == 0) rest++;
            --count[t[i]];
        }
        count[s[i]]++;
        int st = -1, ed = -1;
        if (count[s[i]] == 0) rest--;
        while (j <= s.size()) {
            if (rest == 0) {
                while (count[s[i]] > 0) {
                    count[s[i]]--;
                    i++;
                }
                if (j - i  < ed - st || st == -1) {
                    st = i;
                    ed = j;
                }
            }
            ++j; count[s[j]]++;
            if (count[s[j]] == 0) rest--;
        }

        if (st != -1) return s.substr(st, ed - st + 1);
        else return "";
    }
};

其他细节

  • 这题的T中字符是会有重复的(即要计算个数),所以用bool来标识是不行的。
  • st, ed这样写是有点丑。 一开始的版本我是直接把得到window字符串截出来。然后不断更新字符串。但是觉得效率低就改成这样了
  • 上面说的直接保存字符串从测试结果来看两者耗时相等。这就很尴尬。。。只能说这数据没有体现这个优化的优势了。
  • 看了一下耗时的rank, 发现我是在map上吃亏了。。。直接手动弄个hash table才是真正的O(1)。只不过我懒orz。。。

The End.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值