题目描述
给你一个整数数组 nums,有一个大小为 k **的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回 滑动窗口中的最大值 。
示例 1:
输入: nums = [1,3,-1,-3,5,3,6,7], k = 3
输出: [3,3,5,5,6,7]
解释:
滑动窗口的位置 最大值
--------------- -----
[1 3 -1] -3 5 3 6 7 3
1 [3 -1 -3] 5 3 6 7 3
1 3 [-1 -3 5] 3 6 7 5
1 3 -1 [-3 5 3] 6 7 5
1 3 -1 -3 [5 3 6] 7 6
1 3 -1 -3 5 [3 6 7] 7
示例 2:
输入: nums = [1], k = 1
输出: [1]
提示:
1 <= nums.length <= 105-104 <= nums[i] <= 1041 <= k <= nums.length
最小覆盖子串解题思路
本题要求在字符串 s 中找到包含字符串的最小子串。这是一个经典的滑动窗口问题,可以使用以下几个方面解决:
- 使用滑动窗口技术,维护一个左右指针
- 使用哈希表记录目标字符串 t 中每个字符的出现次数
- 使用另一个哈希表记录当前窗口中的字符出现次数
- 当窗口包含 t 中所有字符时,尝试缩小窗口
算法步骤
- 创建两个哈希表,一个记录 t 中字符及其出现次数,另一个记录当前窗口中字符及其出现次数
- 初始化左右指针 left= 0,right=0
- 扩展窗口:移动右指针增大窗口,直到窗口包含 t 中所有字符
- 收缩窗口:移动左指针缩小窗口,尝试找到最小覆盖子串(贪心)
- 重复步骤 3-4, 直到右指针到达字符串 s 的末尾
代码实现
func minWindow(s string, t string) string {
// 如果 s 或 t 为空,或 s 的长度小于 t,则不可能找到覆盖子串
if len(s) == 0 || len(t) == 0 || len(s) < len(t) {
return ""
}
// 创建两个哈希表
need := make(map[byte]int) // 记录 t 中每个字符的出现次数
window := make(map[byte]int) // 记录当前窗口中每个字符的出现次数
// 初始化 need 哈希表
for i := 0; i < len(t); i++ {
need[t[i]]++
}
// 初始化窗口的左右边界,以及匹配的字符数量
left, right := 0, 0
valid := 0 // 当前窗口中满足 need 条件的字符个数
// 记录最小覆盖子串的起始位置和长度
start, length := 0, len(s) + 1
// 开始滑动窗口
for right < len(s) {
// c 是将要移入窗口的字符
c := s[right]
right++ // 扩大窗口
// 更新窗口中的数据
if _, ok := need[c]; ok {
window[c]++
// 如果窗口中字符 c 的数量恰好等于 need 中字符 c 的数量,说明该字符已满足条件
if window[c] == need[c] {
valid++
}
}
// 当窗口中包含了 t 中所有字符,开始收缩窗口
for valid == len(need) {
// 更新最小覆盖子串
if right - left < length {
start = left
length = right - left
}
// d 是将要移出窗口的字符
d := s[left]
left++ // 缩小窗口
// 更新窗口中的数据
if _, ok := need[d]; ok {
// 如果窗口中字符 d 的数量等于 need 中字符 d 的数量,移除后就不再满足条件
if window[d] == need[d] {
valid--
}
window[d]--
}
}
}
// 如果没有找到符合条件的子串
if length == len(s) + 1 {
return ""
}
return s[start:start+length]
}
时序图
sequenceDiagram
participant main as 主函数
participant need as 目标字符哈希表
participant window as 窗口字符哈希表
participant left as 左指针
participant right as 右指针
main->>need: 初始化 t 中每个字符的频率
main->>window: 初始化为空
main->>left: 初始化为 0
main->>right: 初始化为 0
loop 右指针未到达字符串s末尾
right->>window: 移入字符s[right]
right->>right: 右指针右移 (right++)
alt 移入的字符是t中的字符
window->>window: 更新窗口中该字符的计数
alt 该字符在窗口中的计数等于在t中的计数
window->>main: valid++
end
end
loop 窗口包含t中所有字符
main->>main: 更新最小子串信息
left->>window: 移出字符s[left]
left->>left: 左指针右移 (left++)
alt 移出的字符是t中的字符
alt 该字符在窗口中的计数等于在t中的计数
window->>main: valid--
end
window->>window: 更新窗口中该字符的计数
end
end
end
main->>main: 返回最小覆盖子串
状态变化图
graph TD
subgraph 初始化
A["need = {A:1, B:1, C:1}"]
B["window = {}"]
C["left = 0, right = 0"]
D["valid = 0, start = 0, length = 14"]
end
subgraph 扩展窗口
E1["right = 0, c = 'A'"]
E2["window = {A:1}, valid = 1"]
E3["right = 1, c = 'D'"]
E4["window = {A:1}, valid = 1"]
E5["right = 2, c = 'O'"]
E6["window = {A:1}, valid = 1"]
E7["right = 3, c = 'B'"]
E8["window = {A:1, B:1}, valid = 2"]
E9["right = 4, c = 'E'"]
E10["window = {A:1, B:1}, valid = 2"]
E11["right = 5, c = 'C'"]
E12["window = {A:1, B:1, C:1}, valid = 3"]
end
subgraph 收缩窗口1
F1["满足条件: valid = 3"]
F2["更新: start = 0, length = 6"]
F3["left = 0, d = 'A'"]
F4["window = {A:0, B:1, C:1}, valid = 2"]
end
subgraph 继续扩展和收缩
G1["继续扩展窗口..."]
G2["right = 9, c = 'B'"]
G3["window = {A:1, B:1, C:1}, valid = 3"]
end
subgraph 收缩窗口2
H1["满足条件: valid = 3"]
H2["更新: start = 9, length = 4"]
H3["再次收缩窗口"]
end
subgraph 最终结果
I["返回 s[9:13] = 'BANC'"]
end
A --> B --> C --> D
D --> E1 --> E2 --> E3 --> E4 --> E5 --> E6 --> E7 --> E8 --> E9 --> E10 --> E11 --> E12
E12 --> F1 --> F2 --> F3 --> F4
F4 --> G1 --> G2 --> G3
G3 --> H1 --> H2 --> H3
H3 --> I
关键过程数据变化表
| 操作 | 窗口状态 | left | right | window | valid | 当前最小长度 |
|---|---|---|---|---|---|---|
| 初始 | [] | 0 | 0 | {} | 0 | 14 |
| 添加'A' | [A] | 0 | 1 | {A:1} | 1 | 14 |
| 添加'D' | [AD] | 0 | 2 | {A:1} | 1 | 14 |
| 添加'O' | [ADO] | 0 | 3 | {A:1} | 1 | 14 |
| 添加'B' | [ADOB] | 0 | 4 | {A:1,B:1} | 2 | 14 |
| 添加'E' | [ADOBE] | 0 | 5 | {A:1,B:1} | 2 | 14 |
| 添加'C' | [ADOBEC] | 0 | 6 | {A:1,B:1,C:1} | 3 | 6 |
| 移除'A' | [DOBEC] | 1 | 6 | {A:0,B:1,C:1} | 2 | 6 |
| ... | ... | ... | ... | ... | ... | ... |
| 添加'B' | [...B] | 8 | 9 | {A:0,B:1,C:0} | 1 | 6 |
| 添加'A' | [...BA] | 8 | 10 | {A:1,B:1,C:0} | 2 | 6 |
| 添加'N' | [...BAN] | 8 | 11 | {A:1,B:1,C:0} | 2 | 6 |
| 添加'C' | [...BANC] | 8 | 12 | {A:1,B:1,C:1} | 3 | 4 |
| 移除'B' | [...ANC] | 9 | 12 | {A:1,B:0,C:1} | 2 | 4 |
最终返回的最小覆盖子串是 "BANC",长度为 4。
考察点
- 滑动窗口技术:经典双指针问题,通过移动左右指针来维护一个符合条件的窗口
- 哈希表:使用哈希表高效存储和查询字符的出现频率
- 贪心思想:收缩窗口尽可能移动左指针,以寻找最优解
- 字符串处理:高效处理字符串,提取子串
- 时间复杂度控制:该算法的时间复杂度为 O(n),其中 n 是字符串 s 的长度。
- 空间复杂度优化:使用哈希表存储必要信息,空间复杂度为 O(k),其中 k 是字符集大小。
这是一个经典的双指针滑动窗口问题,广泛应用于各种字符串匹配、子数组和子序列问题中。