最小覆盖子串

 

题目描述

给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 “” 。
注意: 对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。 如果 s 中存在这样的子串,我们保证它是唯一的答案。

示例 1:
输入:s = “ADOBECODEBANC”, t = “ABC”
输出:”BANC”

示例 2:
输入:s = “a”, t = “a”
输出:”a”

示例 3:
输入: s = “a”, t = “aa”
输出: “”
解释: t 中两个字符 ‘a’ 均应包含在 s 的子串中,因此没有符合条件的子字符串,返回空字符串。

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

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

解题思路

滑动窗口

我们可以使用滑动窗口的思想解决这个问题,在滑动窗口题型中会有两个指针,一个用于延伸现有窗口的r指针,一个用于收缩窗口的l指针。在任意时刻,只有一个指针在运行,另一个保持静止。
我们在s上滑动窗口,通过移动r指针不断扩张窗口,直至窗口中包含t的所有所需的字符。如果满足要求的窗口能收缩,我们就保持r不动移动l收缩窗口得到最小窗口。

在代码实现中我们需要两个map去记录t的字符(charMap)和滑动窗口出现的字符的个数(countMap),通过check方法去检查当前窗口的countMap是否已包含t所需的所有字符。如果已经包含了t所需的所有字符,我们就保持r指针不动,再缩小l指针看是否能找到更小的窗口,当然在缩小滑动窗口的时候我们要始终满足l<=r的前提条件。

代码

golang实现:

import "math"

func minWindow(s string, t string) string {
	charMap, countMap := map[byte]int{}, map[byte]int{}
	tLen := len(t)
	// 记录t的字符
	for i := 0; i < tLen; i++ {
		charMap[t[i]]++
	}
	sLen := len(s)
	// 记录最小结果长度
	len := math.MaxInt32
	ansL, ansR := -1, -1
	// 检查滑动窗口包含t所有字符
	check := func() bool {
		for k, v := range charMap {
			if countMap[k] < v {
				return false
			}
		}
		return true
	}
	// 这里两层循环可以看作r移动时l不移动,l不移动时r不移动,所以实际上还是遍历一遍
	for l, r := 0, 0; r < sLen; r++ {
		if r < sLen && charMap[s[r]] > 0 { // 记录在t中且在s出现的字符
			countMap[s[r]]++
		}
		for check() && l <= r { // 所有t的字符都在滑动窗口内了,缩小len
			if r-l+1 < len { // 小于最小len时更新len
				len = r - l + 1
				ansL, ansR = l, l+len
			}
			if _, ok := charMap[s[l]]; ok { // 逐渐扩大l,观察是否可以缩短len
				countMap[s[l]]--
			}
			l++
		}
	}
	if ansL == -1 {
		return ""
	}
	return s[ansL:ansR]
}

时间和空间复杂度

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