LeetCode12 - 最小覆盖子串
📝 题目描述
题目链接:最小覆盖子串
给定两个字符串 s 和 t,长度分别是 m 和 n,返回 s 中的最短窗口子串,使得该子串包含 t 中的每一个字符(包括重复字符)。如果没有这样的子串,返回空字符串 ""。
测试用例保证答案唯一。
子串:子字符串 是字符串中连续的 非空 字符序列。
示例:
1 |
|
提示:
m == s.lengthn == t.length1 <= m, n <= 10^5s 和 t 由英文字母组成
💡 解题思路
方法一:滑动窗口
这个题目的思路有点类似于“LeetCode8 - 无重复字符的最长子串”,都是滑动窗口的标准写法:
“右指针主动走,左指针被动收缩”。
这道题也不例外,我们先规划两个128长度的数组(ASCII 共128个字符),用于存储 s的子串 和 t 的每个字母的出现次数,并提前写好一个 check 函数用于判断 s的子串是否包含了 t 中的所有字符。
接下来就是滑动窗口法来不断迭代:
- 首先,right右移,将
s[right]包含进子串中; - 接下来进行检查,看
s的子串是否包含了t中的所有字符:- 是,则检查子串是否更短,更短则更新答案记录,然后收缩
left边界;重复此流程,直到s的子串不再包含t中的所有字符; - 否,则不进行操作,继续等待
right右移。
- 是,则检查子串是否更短,更短则更新答案记录,然后收缩
这样,循环结束时,检查记录即可得到最终答案。
方法二:优化的滑动窗口
上面思路的主要待优化点在于判断逻辑,我们可以参照“LeetCode9 - 找到字符串中所有字母异位词”的方法,只关注目前变化的字符,并计算 needCount 是否为 0 来判断子串是否已包含 t 中所有的字符。
具体来说,我们先初始化 needCount = t.size(),也就是说,我们的 有用字符总需求量 是 needCount。接下来初始化数组 vector<int> need(128, 0),表示 t 中 每个字符的需求量。
接下来进入滑动窗口循环:
- 首先,我们判断
need[s[right]] > 0是否为真,这一步帮助我们判断目前的s[right]是否是我们需要的字符,是则needCount--,然后无论是不是我们需要的字符,都要need[s[right]]--。(这一步主要是为了进行“记账”,如果只对需要的字符进行need[s[right]]--,那么当后续左指针移动时,所有的need都为0,无法判断哪个是我们需要的) - 接下来进行检查,看
needCount是否为0:- 是,则检查子串是否更短,更短则更新答案记录,并判断
need[s[left]] == 0是否为真,是则needCount++,然后无论是不是我们需要的字符,都要need[s[left]]++,然后收缩left边界;重复此流程,直到 ``needCount不再为0`; - 否,则不进行操作,继续等待
right右移。
- 是,则检查子串是否更短,更短则更新答案记录,并判断
接下来解释一下上面说的“记账”思路,随着窗口的滑动,need[c] 的值会产生三种状态:
- 大于 0 (
> 0):表示当前窗口还需要多少个字符c才能满足t的要求。 - 等于 0 (
== 0):表示当前窗口内字符c的数量刚好满足t的要求(或者本来就不需要,且窗口里也没有)。 - 小于 0 (
< 0):表示当前窗口内字符c的数量超标了(即冗余字符)。这包含两种情况:- 这个字符根本不在
t中,但被我们圈进了窗口里。 - 这个字符在
t中,但窗口里包含该字符的数量多于t中要求的数量。
- 这个字符根本不在
也就是说,我们可以利用 need 数组,来记录子串中的字符状态,从而节省了“子串是否包含 t 中所有字符”的判断开销。
🔧 代码实现
1、滑动窗口
1 | class Solution { |
2、优化的滑动窗口
1 | class Solution { |
📊 复杂度分析
1、滑动窗口
- 时间复杂度:,此处 ,每次需要判断子串是否包含了所有的
t中的字符。 - 空间复杂度:,额外需要两个 的数组,不随输入规模变化。
2、优化的滑动窗口
- 时间复杂度:,判断子串时,不每次都去全局检查 128 个字符,而是只关注 needCount,节省了时间开销。
- 空间复杂度:,额外需要一个 的数组,不随输入规模变化。
🎯 总结
- 核心思想:熟悉滑动窗口的公式写法,及时想起来滑动窗口。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 很多时候不懂事!