LeetCode 热题 HOT 100(子串)76. 最小覆盖子串

63 阅读4分钟

题目描述

76. 最小覆盖子串

给你一个整数数组 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] <= 104
  • 1 <= k <= nums.length

最小覆盖子串解题思路

本题要求在字符串 s 中找到包含字符串的最小子串。这是一个经典的滑动窗口问题,可以使用以下几个方面解决:

  1. 使用滑动窗口技术,维护一个左右指针
  2. 使用哈希表记录目标字符串 t 中每个字符的出现次数
  3. 使用另一个哈希表记录当前窗口中的字符出现次数
  4. 当窗口包含 t 中所有字符时,尝试缩小窗口

算法步骤

  1. 创建两个哈希表,一个记录 t 中字符及其出现次数,另一个记录当前窗口中字符及其出现次数
  2. 初始化左右指针 left= 0,right=0
  3. 扩展窗口:移动右指针增大窗口,直到窗口包含 t 中所有字符
  4. 收缩窗口:移动左指针缩小窗口,尝试找到最小覆盖子串(贪心)
  5. 重复步骤 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

关键过程数据变化表

操作窗口状态leftrightwindowvalid当前最小长度
初始[]00{}014
添加'A'[A]01{A:1}114
添加'D'[AD]02{A:1}114
添加'O'[ADO]03{A:1}114
添加'B'[ADOB]04{A:1,B:1}214
添加'E'[ADOBE]05{A:1,B:1}214
添加'C'[ADOBEC]06{A:1,B:1,C:1}36
移除'A'[DOBEC]16{A:0,B:1,C:1}26
.....................
添加'B'[...B]89{A:0,B:1,C:0}16
添加'A'[...BA]810{A:1,B:1,C:0}26
添加'N'[...BAN]811{A:1,B:1,C:0}26
添加'C'[...BANC]812{A:1,B:1,C:1}34
移除'B'[...ANC]912{A:1,B:0,C:1}24

最终返回的最小覆盖子串是 "BANC",长度为 4。

考察点

  1. 滑动窗口技术:经典双指针问题,通过移动左右指针来维护一个符合条件的窗口
  2. 哈希表:使用哈希表高效存储和查询字符的出现频率
  3. 贪心思想:收缩窗口尽可能移动左指针,以寻找最优解
  4. 字符串处理:高效处理字符串,提取子串
  5. 时间复杂度控制:该算法的时间复杂度为 O(n),其中 n 是字符串 s 的长度。
  6. 空间复杂度优化:使用哈希表存储必要信息,空间复杂度为 O(k),其中 k 是字符集大小。

这是一个经典的双指针滑动窗口问题,广泛应用于各种字符串匹配、子数组和子序列问题中。