Leetcode 76. 最小覆盖子串

本题链接

题目

给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 “” 。

注意:如果 s 中存在这样的子串,我们保证它是唯一的答案。

示例 1:

输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"

示例 2:

输入:s = "a", t = "a"
输出:"a"

提示:

1 <= s.length, t.length <= 105
s 和 t 由英文字母组成

进阶:你能设计一个在 o(n) 时间内解决此问题的算法吗?


思路

用两个 map<char, int> 分别统计滑动窗口和字符串 t 中各个字符出现的次数,用双指针 leftright 遍历字符串 s,每次先让 right 向右一格,如果窗口中的字母数量符合要求(即窗口中的子串合法)的话尝试让 left 向右收缩窗口,直到收缩到窗口不符合要求为止,继续让 right 向右扩张窗口。

时间复杂度: O ( C s + t ) O(Cs + t) O(Cs+t)
最坏情况下左右指针分别遍历完 s,设字符集长度为 C,每次检查两个 map 是否符合要求需要 C,再加上最开始统计 t 的字符集需要 t
空间复杂度: O ( C ) O(C) O(C)

C++ 代码

class Solution {
public:
    bool checkEqual(map<char, int>& winCnt, map<char, int>& tCnt) {
        for (auto it = tCnt.begin(); it != tCnt.end(); ++it) {
            if (winCnt[it->first] < it->second)
                return false;
        }
        return true;
    }
    string minWindow(string s, string t) {
        int sizeT = t.size(), sizeS = s.size();
        map<char, int> winCnt, tCnt;    // 分别记录滑动窗口和字符串t中各个字符出现的次数
        for (int i = 0; i < sizeT; ++i) {
            ++tCnt[t[i]];
        }
        
        int minStart, minLen = INT_MAX, left = 0, right = -1;
        while (right < sizeS - 1) {
            ++right;
            if (tCnt.count(s[right]))
                ++winCnt[s[right]];
            while (checkEqual(winCnt, tCnt)) {
                if (right - left + 1 < minLen) {
                    minLen = right - left + 1;
                    minStart = left;
                }
                if (tCnt.count(s[left]))
                    --winCnt[s[left]];
                ++left;
            }
        }
        return minLen == INT_MAX ? "" : s.substr(minStart, minLen);
    }
};

最开始错误的思路和 C++ 代码

我觉得这个题表述有模糊的地方,我一开始写了一版代码通过了两个题中的样例,但是提交的时候这个样例:

输入:
"a"
"aa"
输出:
"a"

的意思就是各个字母的出现次数还得匹配,想了一下再用我这个思路改会很复杂,就看了题解。

class Solution {
public:
    string minWindow(string s, string t) {
    /*  u用来记录t中所有不重复字符
        为u中所有字符建立一个map<char, int> win;
        int表示当前窗口中这个char最后出现的位置

        用双指针left, right遍历字符串s
        首先找到第一个符合条件的子串[left, right],顺便初始化win
        并记录win中的最小值minRights,记录minLen=right-left,记录minStart=left
        不断向右移动right,如果遇到t中的字符,按顺序更新leftSet,win
        再尝试尽量右移left(这里要维护一个set<int> leftSet来找到left的下一个位置)
        更新minLen和minStart
        遍历结束后从s[minStart]开始输出minLen个字符即为所求

        因为有这个测试样例,所以找第一个[left, right]的时候还要统计字母次数...
        输入:
            "a"
            "aa"
        输出:
            "a"
    */
        // 构造u,初始化win
        map<char, int> win;
        string u;   // u用来记录t中所有不重复字符
        int sizeT = t.size(), sizeS = s.size();
        for (int i = 0; i < sizeT; ++i) {
            if (win.count(t[i]) == 0) {
                win[t[i]] = 0;
                u.push_back(t[i]);
            }
        }

        // 遍历s
        int left = 0, right = 0, cntU = 0, sizeU = u.size(), minLen, minStart;
        set<int> leftSet;   // 最小堆
        // 定位第一个合法的left,并统计信息
        while (win.count(s[left]) == 0)
            ++left;
        win[s[left]] = left;
        ++cntU;
        leftSet.insert(left);

        // 定位第一个合法的right,并统计信息
        while (cntU < sizeU) {
            ++right;
            if (win.count(s[right]) != 0) {
                win[s[right]] = right;
                leftSet.insert(right);
                ++cntU;
            }
        }
        minLen = right - left + 1;

        // right向右遍历完s
        ++right;
        if (right >= sizeS)
            return s.substr(minStart, minLen);
        for (; right < sizeS; ++right) {
            if (win.count(s[right]) != 0) {
                leftSet.erase(win[s[right]]);
                win[s[right]] = right;
                leftSet.insert(win[s[right]]);
                left = *(leftSet.begin());
                if (right - left + 1 < minLen) {
                    minLen = right - left + 1;
                    minStart = left;
                }
            }
        }
        return s.substr(minStart, minLen);
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值