76. 最小覆盖子串

1. 题目

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

注意:

  • 对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。
  • 如果 s 中存在这样的子串,我们保证它是唯一的答案。

示例 1:

1
2
3
输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"
解释:最小覆盖子串 "BANC" 包含来自字符串 t 的 'A'、'B' 和 'C'。

示例 2:

1
2
3
输入:s = "a", t = "a"
输出:"a"
解释:整个字符串 s 是最小覆盖子串。

示例 3:

1
2
3
4
输入: s = "a", t = "aa"
输出: ""
解释: t 中两个字符 'a' 均应包含在 s 的子串中,
因此没有符合条件的子字符串,返回空字符串。

提示:

  • m == s.length
  • n == t.length
  • 1 <= m, n <= 10^5
  • st 由英文字母组成

2. 思路

  • 整体利用滑动窗口的做法,配合着哈希表,对区间范围内的字符串进行比对
  • 初始化targetStringCountMap哈希表用于记录目标字符串每个字符出现的次数
  • 初始化左指针l与右指针r,与结果左指针resultLeftIndex和结果右指针resultRightIndex,两个结果指针会实时刷新为最小子串的左右指针
  • 每次对l指针与r指针形成的滑动窗口范围内的字符进行实时统计,并记录到sourceStringCountMap
  • 当滑动窗口范围内未完全包含目标字符串时,r指针持续向右移动
  • 当滑动窗口范围内已完全包含目标字符串时,l指针持续向左移动,并实时更新窗口内的字符统计,直至窗口范围不满足结果要求(因为题目要求的是最小覆盖子串)
  • 滑动窗口范围内满足结果的过程中如果出现比记录的最终左右指针还要小的字符串,更新对应最终的结果左指针resultLeftIndex和结果右指针resultRightIndex
  • 直至r指针到s字符串的最后一位,返回结果,返回结果采用Stringsubstring方法,其中需要注意的是endIndex为不包含字符下标位
  • 整体思路如下图所示:

fig1

图源LeetCode官方题解

3. 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class Solution {
private Map<Character, Integer> sourceStringCountMap = new HashMap<>();
private Map<Character, Integer> targetStringCountMap = new HashMap<>();

public String minWindow(String s, String t) {
if (s.length() < t.length()) {
return "";
}

var targetCharArray = t.toCharArray();
var sourceCharArray = s.toCharArray();

for (var ch : targetCharArray) {
var count = targetStringCountMap.getOrDefault(ch, 0);
targetStringCountMap.put(ch, count + 1);
}

var l = 0;
var r = 0;

var resultLeftIndex = -1;
var resultRightIndex = -1;

while (r < sourceCharArray.length) {
var count = sourceStringCountMap.getOrDefault(sourceCharArray[r], 0);
sourceStringCountMap.put(sourceCharArray[r], count + 1);

while (successMatch()) {
if (resultLeftIndex == -1 || resultRightIndex - resultLeftIndex + 1 > r - l + 1) {
resultLeftIndex = l;
resultRightIndex = r;
}

count = sourceStringCountMap.get(sourceCharArray[l]);
sourceStringCountMap.put(sourceCharArray[l], count - 1);
l++;
}
r++;
}

return resultLeftIndex == -1 && resultRightIndex == -1 ? "" : s.substring(resultLeftIndex, resultRightIndex + 1);
}

private boolean successMatch() {
var targetCharSet = targetStringCountMap.keySet();
for (var targetChar : targetCharSet) {
if (sourceStringCountMap.getOrDefault(targetChar, 0) < targetStringCountMap.get(targetChar)) {
return false;
}
}
return true;
}
}

4. 复杂度

  • 时间复杂度:最坏情况下左右指针对 s 的每个元素各遍历一遍,哈希表中对 s 中的每个元素各插入、删除一次,对 t 中的元素各插入一次。每次检查是否可行会遍历整个 t 的哈希表,哈希表的大小与字符集的大小有关,设字符集大小为 C,则渐进时间复杂度为 O(C · |s| + |t|)
  • 空间复杂度:这里用了两张哈希表作为辅助空间,每张哈希表最多不会存放超过字符集大小的键值对,我们设字符集大小为 C,则渐进空间复杂度为O(C)

image-20230819132929645