76.最小覆盖子串-LeetCode


题目链接

方法一 暴力求解(Time Limit Exceed)

算法思想

  1. 枚举输入字符串s的所有子串(由于子串要覆盖t中所有字符,因此子串的长度一定大于等于字符串t的长度,此时可以节省一部分时间);
  2. 判断每个子串是否符合要求,子串中元素是否覆盖了字符串t中所有字符;
  3. 在判断每个子串的过程中,对于符合要求的子串检查其长度,记录下长度更短的子串;

时间复杂度:T(n) = O(|S|3 +|T|) 。获取子串是|S|2,检查符合要求是|S|+|T|。
空间复杂度:S(n) = O(|S|+|T|)。

C++实现

class Solution {
public:
    unordered_map<char, int> tFreq; // 字符串t的频数数组

    // 检查子串是否包含t中所有字符
    bool check(const string &str){
        unordered_map<char, int> subFreq; // 当前子串的字符频数数组
        for(const auto &c: str){
            ++subFreq[c];
        }
        for(const auto &p: tFreq){
            if(subFreq[p.first] < p.second){
                return false;
            }
        }
        return true;
    }

    string minWindow(string s, string t) {
        // 初始化tFreq
        int lenT = t.size();
        for(const auto &c: t){
            ++tFreq[c];
        }

        // 初始化最短符合要求的子串
        int len = INT_MAX;
        string curSubstr = "";

        // 双重遍历得到s的子串,检测是否符合题目要求,涵盖了t中所有字符
        int lenS = s.size();
        for(int start = 0; start < lenS; ++start){
            for(int subLen = lenT; start + subLen <= lenS; ++subLen){
                string tmp = s.substr(start,subLen);
                if(check(tmp) && tmp.size() < len){ // 该子串符合要求且长度更小
                    curSubstr = tmp;
                    len = tmp.size();
                }
            }
        }
        return curSubstr;
    }
};

运行结果如下
在这里插入图片描述
题目给出字符串长度最长达到105,按照这个时间复杂度,显然不是短时间能完成的。

方法二 双指针滑动窗口

暴力解法存在的问题:做了很多无用功

  1. 在已经找到了一个符合要求的子串后,以当前子串左边界为起点的更长的子串就不用看了;
  2. 判断每个子串是否覆盖了字符串t在利用滑动窗口时不需要遍历整个子串,详情见下列方法。

算法思想:利用双指针构造一个滑动窗口,滑动窗口在不断移动的过程中找到最短的包含字符串t中字符的子串。

  1. 滑动窗口通过右指针来扩展,每次向右移动一次,完成窗口的扩展,并且检查此时窗口内是否包含了字符串t中所有字符。
    i. 如果包含,那么考虑从窗口左侧窗口减小窗口大小以求得最小子串;
    ii. 如果不包含,那么继续向右扩展;
  2. 滑动窗口规模缩减,左侧指针不断右移,随着窗口规模每减小1,如若当前窗口仍然包含字符串t中所有元素,那么说明当前窗口的子串符合要求,应同之前的子串长度作比较,长度更小则更新子串状态。循环往复,直到不再满足"窗口内元素包含字符串t中所有元素"这一要求,此时说明以r位置为尾部的子串长度已达到最小。应该继续往后扩展寻找是否有新的符合要求的子串。

时间复杂度分析:滑动窗口左右指针各遍历一遍,外加对字符串t的哈希插入,耗时O(|s|+|t|);每一次插入后都进行一次子串覆盖判定耗时O(C)【假定字符串t中字符集大小为C】,因此总的时间复杂度为T(n) = O(C|s|+|t|)。
空间复杂度分析:使用了两张哈希表,根据字符集大小为C,因此消耗空间为S(n) = O(C)

请添加图片描述

C++实现

class Solution {
public:
    // tFreq:给定字符串t中元素的数量
    // winFreq:表示字符串s的子串s[l,r]中包含的t中各元素的数量
    unordered_map<int, int> tFreq, winFreq; 

    // 检查子串s[l,r]中是否涵盖了字符串t中所有字符,即比较tFreq和winFreq的元素数量
    bool check(){ 
        for(auto const &elem: tFreq){
            if(winFreq[elem.first] < elem.second){
                return false;
            }
        }
        return true;
    }

    string minWindow(string s, string t) {
        // 初始化tFreq
        for(const auto &c:t){
            ++tFreq[c]; 
        }
        // 初始化子串的状态
        int len = INT_MAX; // 初始情况满足要求的最短子串的长度设定为正无穷
        int ansL = -1, ansR = -1; // 初始子串的左端和右端索引
        // 初始化滑动窗口
        int left = 0, right = -1; // 表示滑动窗口的左右指针

        int lenS = s.size(); 
        while(right < lenS){
            // 右指针向右扩展一位,更新winFreq
            if(tFreq.find(s[++right]) != tFreq.end()){ // 如果s[++right]的元素是字符串t中的元素,那么子串中该元素的数量+1
                ++winFreq[s[right]]; 
            }

            // 检查并更新最短子串状态
            // 如果此刻的子串s[left,right]是否包含了t中所有字符,那么更新子串状态,同时调整滑动窗口左端位置
            if(check()){
                do{ 
                    // 缩减滑动窗口左端直到不满足"子串包含t中所有字符"的要求
                    if(tFreq.find(s[left]) != tFreq.end()){ // 注意:每次缩减还需要更新子串对应的count表
                        --winFreq[s[left]];
                    }
                    ++left;
                }while(check() && left<=right);

                int curLen = right-left+2; // 当前满足要求的子串的长度
                if(curLen < len){ // 找到了更小的子串->更新最小子串的范围及长度
                    len = curLen;
                    ansL = left-1;
                    ansR = right;
                }
            }
        }
        return len == INT_MAX?string():s.substr(ansL,len);
    }
};

运行结果如下
在这里插入图片描述

方法三 滑动窗口+距离定义

算法思想:在方法二的基础上,定义滑动窗口同字符串t之间的距离,通过距离作为滑动窗口是否覆盖子串的标志,从而减少了判断的时间。定义见代码。
时间复杂度:T(n) = O(|s|+|t|),此处与字符集大小不再相关;
空间复杂度:S(n) = O(C);

C++实现

class Solution {
public:
    unordered_map<int, int> tFreq, winFreq; 
    int distance = 0; // 表示滑动窗口内部包含了字符串t中字符的个数。窗口内增加字符时,当该字符的个数超过了在t中的个数,distance不再增加;窗口内减少字符时,若该字符数量超过了在t中的个数,则distance不减少;当distance==t中字符总个数时,即说明窗口内字符覆盖了t中所有字符。

    // 检查子串s[l,r]中是否涵盖了字符串t中所有字符,即比较count和origin的元素数量
    bool check(const string &t){ 
        return distance == t.size()? true:false;
    }

    string minWindow(string s, string t) {
        // 初始化tFreq
        for(const auto &c:t){
            ++tFreq[c]; 
        }
        // 初始化子串的状态
        int len = INT_MAX; // 初始情况满足要求的最短子串的长度设定为正无穷
        int ansL = -1, ansR = -1; // 初始子串的左端和右端索引
        // 初始化滑动窗口
        int left = 0, right = -1; // 表示滑动窗口的左右指针

        int lenS = s.size(); 
        while(right < lenS){
            // 右指针向右扩展一位,更新winFreq表和distance
            if(tFreq.find(s[++right]) != tFreq.end()){ // 如果s[++r]的元素是字符串t中的元素,那么子串中该元素的数量+1
                ++winFreq[s[right]]; 
                if(winFreq[s[right]] <= tFreq[s[right]]){ // 如果窗口内的该字符超过了字符串t对应字符,则说明本次增加的字符属于多余的字符,则距离保持不变
                    ++distance; // 距离字符串t中对应字符更近一步,
                }
            }

            // 如果此刻的滑动窗口包含了t中所有字符,那么需要缩减调整滑动窗口左端位置,找到以right结尾的最短子串,更新最短子串状态
            if(check(t)){ // 大循环出发小循环
                do{ 
                    // 缩减滑动窗口左端直到不满足"滑动窗口覆盖字符串t"的要求
                    if(tFreq.find(s[left]) != tFreq.end()){ // 如果窗口左端字符是字符串t中字符
                        --winFreq[s[left]];
                        if(winFreq[s[left]] < tFreq[s[left]]){ // 如果除去该字符后窗口内的该字符仍大于等于字符串t对应字符,则说明本次减少的字符属于多余的字符,则距离保持不变
                            --distance;
                        }
                    }
                    ++left;
                }while(check(t) && left<=right);

                // 更新最小覆盖子串
                int curLen = right-left+2; // 当前以right位置字符为结尾的最小覆盖子串的长度
                if(curLen < len){ // 找到了更小的子串->更新最小子串的范围及长度
                    len = curLen;
                    ansL = left-1;
                    ansR = right;
                }
            }
            
            // ====上面小循环也可写成如下这种形式====
            // while(check(t) && left<=right){ 
            //     // 更新最小覆盖子串
            //     int curLen = right-left+1; // 当前以right位置字符为结尾的最小覆盖子串的长度
            //     if(curLen < len){ // 找到了更小的子串->更新最小子串的范围及长度
            //         len = curLen;
            //         ansL = left;
            //         ansR = right;
            //     }

            //     // 缩减滑动窗口左端直到不满足"滑动窗口覆盖字符串t"的要求
            //     if(tFreq.find(s[left]) != tFreq.end()){ // 如果窗口左端字符是字符串t中字符
            //         --winFreq[s[left]];
            //         if(winFreq[s[left]] < tFreq[s[left]]){ // 如果除去该字符后窗口内的该字符仍大于等于字符串t对应字符,则说明本次减少的字符属于多余的字符,则距离保持不变
            //             --distance;
            //         }
            //     }
            //     ++left;
            // }
        }
        return len == INT_MAX?string():s.substr(ansL,len);
    }
};

运行结果如下
在这里插入图片描述
同方法二对比,方法三的优化在时间上显然高效了很多,而空间上仍然保持占用不变。

方法四

未完待续

	看完点个赞呗!(●'◡'●)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Milk_exe

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值