困难
给你一个字符串 s
、一个字符串 t
。返回 s
中涵盖 t
所有字符的最小子串。如果 s
中不存在涵盖 t
所有字符的子串,则返回空字符串 ""
。
注意:
- 对于
t
中重复字符,我们寻找的子字符串中该字符数量必须不少于t
中该字符数量。 - 如果
s
中存在这样的子串,我们保证它是唯一的答案。
示例 1:
输入:s = "ADOBECODEBANC", t = "ABC" 输出:"BANC" 解释:最小覆盖子串 "BANC" 包含来自字符串 t 的 'A'、'B' 和 'C'。
示例 2:
输入:s = "a", t = "a" 输出:"a" 解释:整个字符串 s 是最小覆盖子串。
示例 3:
输入: s = "a", t = "aa" 输出: "" 解释: t 中两个字符 'a' 均应包含在 s 的子串中, 因此没有符合条件的子字符串,返回空字符串。
提示:
m == s.length
n == t.length
1 <= m, n <= 105
s
和t
由英文字母组成
进阶:你能设计一个在 o(m+n)
时间内解决此问题的算法吗?
思路:
这个问题是一个经典的滑动窗口问题,我们可以通过使用双指针(左右指针)的方法来解决。滑动窗口算法的基本思想是,通过移动窗口的边界,来找到满足条件的最小子串。
以下是解决这个问题的步骤:
-
首先,统计字符串
t
中每个字符出现的次数,存储在一个哈希表中。 -
初始化两个指针
left
和right
,分别表示子串的左右边界,以及两个变量count
和min_length
,用来记录当前窗口内满足条件的字符数量和当前找到的最小子串的长度。 -
遍历字符串
s
,将right
指针向右移动,直到窗口内包含了t
中所有字符的至少一个实例。 -
每次移动
right
指针时,检查当前窗口内是否包含了t
中的所有字符。如果是,尝试通过移动left
指针来缩小窗口的大小,直到窗口不再满足条件。 -
在每一步,如果找到了一个更小的满足条件的子串,就更新
min_length
。 -
如果在整个
s
中都没有找到满足条件的子串,返回空字符串""
。
下面是这个问题的Python代码实现:
def min_window(s, t):
from collections import Counter
# 统计t中每个字符的出现次数
counter_t = Counter(t)
# 初始化左右指针和结果
left, right, min_left, min_len = 0, 0, 0, float('inf')
count = len(counter_t)
# 用于存储当前窗口内字符的计数
counter_window = {}
while right < len(s):
# 将right指针的字符加入窗口计数
counter_window[s[right]] = counter_window.get(s[right], 0) + 1
# 如果窗口中的字符在t中,且窗口中的计数满足t中该字符的最小出现次数
if s[right] in counter_t and counter_window[s[right]] == counter_t[s[right]]:
count -= 1
# 当窗口满足条件时,尝试缩小窗口
while count == 0:
if right - left + 1 < min_len:
min_len = right - left + 1
min_left = left
# 移除left指针的字符
counter_window[s[left]] -= 1
if s[left] in counter_t and counter_window[s[left]] < counter_t[s[left]]:
count += 1
left += 1
right += 1
# 如果没有找到满足条件的子串,返回空字符串
return "" if min_len == float('inf') else s[min_left:min_left + min_len]
# 示例
print(min_window("ADOBECODEBANC", "ABC")) # 输出 "BANC"
print(min_window("a", "a")) # 输出 "a"
print(min_window("a", "aa")) # 输出 ""
这个算法的时间复杂度是O(m+n),其中m是字符串s
的长度,n是字符串t
的长度。这是因为我们只需要遍历一次字符串s
,并且在每次迭代中,我们只进行常数时间的操作。空间复杂度是O(min(m, n)),因为我们需要存储s
和t
中不同字符的计数,最坏情况下,这将是s
或t
中不同字符的数量。
下面是对代码的详细解释:
-
导入Counter类:
from collections import Counter
这行代码导入了Python标准库中的Counter
类,它是一个字典的子类,用于计数可哈希对象。
-
函数定义:
def min_window(s, t):
定义了一个名为min_window
的函数,接收两个字符串参数s
和t
。
-
统计
t
中字符的出现次数:counter_t = Counter(t)
使用Counter
类统计字符串t
中每个字符的出现次数。
-
初始化变量:
left, right, min_left, min_len
分别初始化为0和无穷大,这些变量用于记录滑动窗口的左右边界、最小子串的起始索引和长度。count
初始化为len(counter_t)
,即t
中不同字符的数量。
-
滑动窗口计数:
counter_window
初始化为一个空字典,用于存储当前窗口内字符的计数。
-
遍历字符串
s
:- 使用
while right < len(s):
循环遍历字符串s
直到right
指针到达字符串末尾。
- 使用
-
更新窗口计数:
- 在循环内部,首先更新
counter_window
,将right
指针的字符加入到当前窗口的计数中。
- 在循环内部,首先更新
-
检查窗口是否满足条件:
- 如果当前字符在
t
中,并且当前窗口中该字符的计数等于t
中该字符的计数,count
减1。
- 如果当前字符在
-
尝试缩小窗口:
- 当窗口满足条件(即
count
为0)时,尝试通过移动left
指针来缩小窗口,直到窗口不再满足条件。
- 当窗口满足条件(即
-
更新最小子串:
- 如果当前窗口的长度小于之前记录的最小长度
min_len
,则更新min_len
和min_left
。
- 如果当前窗口的长度小于之前记录的最小长度
-
移动
right
指针:right += 1
移动right
指针到下一个字符。
-
检查最小子串:
- 在循环结束后,如果
min_len
仍然是初始值(无穷大),说明没有找到满足条件的子串,返回空字符串""
。 - 否则,根据
min_left
和min_len
返回最小覆盖子串。
- 在循环结束后,如果
这段代码使用了滑动窗口的方法来找到满足条件的最小子串。通过不断调整窗口的大小,直到窗口包含了t
中的所有字符,然后尝试缩小窗口以找到更小的满足条件的子串。这种方法的时间复杂度是O(m+n),其中m是字符串s
的长度,n是字符串t
的长度,因为每个字符最多被访问两次(一次由right
指针,一次可能由left
指针)。空间复杂度是O(min(m, n)),因为counter_t
和counter_window
最多存储s
和t
中不同字符的数量。
时间复杂度
- 初始化阶段:
counter_t
的初始化需要遍历字符串t
,时间复杂度为 O(n)。 - 主循环:
right
指针的循环会遍历整个字符串s
,时间复杂度为 O(m)。对于每个right
指针的位置,left
指针可能会移动,但每次移动left
指针都是在尝试缩小窗口,且每个字符在left
指针处最多被检查一次,因此这部分的时间复杂度仍然是 O(m)。 - 窗口内操作:窗口内的字符计数和比较操作是常数时间的。
综合考虑,时间复杂度主要由主循环决定,为 O(m + n),其中 m 是字符串 s
的长度,n 是字符串 t
的长度。
空间复杂度
- Counter 对象:
counter_t
存储了t
中所有字符的计数,其空间复杂度为 O(k),k 是t
中不同字符的数量,k <= n。 - 滑动窗口计数:
counter_window
存储了当前窗口内字符的计数,最坏情况下,如果s
中包含t
中所有字符,那么counter_window
的大小将接近min(m, k)
。 - 其他变量:
left
,right
,min_left
,min_len
,count
等变量占用的内存空间相对于 m 和 n 来说是常数级别的。
综合考虑,空间复杂度主要由 counter_t
和 counter_window
的大小决定,为 O(k + min(m, k))。在最坏的情况下,如果 s
和 t
包含相同的字符集,那么 k 接近 m,空间复杂度为 O(m)。如果 t
中的字符集合远小于 s
,那么空间复杂度将接近 O(n)。
总结
- 时间复杂度:O(m + n)
- 空间复杂度:O(k + min(m, k)),其中 k 是
t
中不同字符的数量,且 k <= n。在最坏情况下,空间复杂度为 O(m)。
请注意,这里的分析假设字符集的大小(k)相对于 m 和 n 是较小的,这在实际应用中通常是成立的,因为字符集的大小通常是一个常数。如果字符集非常大,那么空间复杂度可能会接近 O(m) 或 O(n)。