LeetCode第76题:最小覆盖子串

LeetCode第76题:最小覆盖子串

题目描述

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

注意:

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

难度

困难

问题链接

最小覆盖子串

示例

示例 1:

arduino 复制代码
输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"
解释:最小覆盖子串 "BANC" 包含了字符串 t 的所有字符 'A'、'B'、'C'

示例 2:

ini 复制代码
输入:s = "a", t = "a"
输出:"a"
解释:整个字符串 s 本身就是最小覆盖子串。

示例 3:

arduino 复制代码
输入:s = "a", t = "aa"
输出:""
解释:t 中有两个字符 'a',但 s 中只有一个 'a',所以无法找到包含 t 中所有字符的子串。

提示

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

解题思路

这道题目可以使用滑动窗口的方法来解决。滑动窗口是一种常用的双指针技巧,特别适合处理子串、子数组相关的问题。

方法:滑动窗口

  1. 首先,统计字符串 t 中每个字符出现的次数,记录在哈希表 need
  2. 使用两个指针 leftright 表示窗口的左右边界,初始时都指向字符串 s 的开头
  3. 不断移动右指针 right,扩大窗口,将 s[right] 加入窗口
  4. 当窗口中包含 t 中所有字符的时候,尝试移动左指针 left,缩小窗口,同时更新最小覆盖子串
  5. 重复步骤 3 和 4,直到右指针 right 到达字符串 s 的末尾

关键点

  • 使用两个哈希表:一个记录 t 中字符的出现次数,另一个记录当前窗口中字符的出现次数
  • 使用一个变量 formed 记录当前窗口中已经满足条件的字符个数
  • formed 等于 need 中字符的个数时,说明当前窗口包含了 t 中所有字符
  • 在缩小窗口时,需要确保窗口仍然包含 t 中所有字符

算法步骤分析

步骤 操作 说明
1 初始化 统计 t 中每个字符的出现次数,存入哈希表 need
2 初始化窗口 设置 left = 0, right = 0,初始化窗口哈希表 window
3 扩大窗口 移动 right,将 s[right] 加入窗口,更新 window
4 判断条件 检查当前窗口是否包含 t 中所有字符
5 缩小窗口 如果满足条件,移动 left,缩小窗口,同时更新最小覆盖子串
6 重复步骤 重复步骤 3-5,直到 right 到达字符串 s 的末尾
7 返回结果 返回最小覆盖子串,如果不存在则返回空字符串

算法可视化

以示例 1 为例,s = "ADOBECODEBANC", t = "ABC"

  1. 初始状态:left = 0, right = 0need = {A:1, B:1, C:1}window = {}
  2. 扩大窗口直到包含所有 t 中的字符:
    • right = 0 时,window = {A:1},包含了 1 个所需字符
    • right = 3 时,window = {A:1, B:1},包含了 2 个所需字符
    • right = 5 时,window = {A:1, B:1, C:1},包含了所有所需字符
  3. 此时窗口为 "ADOBEC",尝试缩小窗口:
    • 移动 left 到 1,窗口变为 "DOBEC",不满足条件,停止缩小
    • 继续扩大窗口
  4. right = 9 时,窗口为 "ADOBECODE",包含了所有所需字符,尝试缩小窗口:
    • 移动 left 到 5,窗口变为 "CODEBA",包含了所有所需字符
  5. right = 12 时,窗口为 "CODEBANC",包含了所有所需字符,尝试缩小窗口:
    • 移动 left 到 8,窗口变为 "BANC",包含了所有所需字符
    • 继续移动 left,窗口不再满足条件,停止缩小
  6. 最终得到最小覆盖子串 "BANC"

代码实现

C# 实现

csharp 复制代码
public class Solution {
    public string MinWindow(string s, string t) {
        if (string.IsNullOrEmpty(s) || string.IsNullOrEmpty(t) || s.Length < t.Length) {
            return "";
        }
        
        // 统计t中每个字符出现的次数
        Dictionary<char, int> need = new Dictionary<char, int>();
        foreach (char c in t) {
            if (need.ContainsKey(c)) {
                need[c]++;
            } else {
                need[c] = 1;
            }
        }
        
        // 初始化窗口
        Dictionary<char, int> window = new Dictionary<char, int>();
        int left = 0, right = 0;
        int valid = 0; // 记录窗口中满足条件的字符个数
        
        // 记录最小覆盖子串的起始位置和长度
        int start = 0, len = int.MaxValue;
        
        while (right < s.Length) {
            // 将右侧字符加入窗口
            char c = s[right];
            right++;
            
            // 更新窗口中的数据
            if (need.ContainsKey(c)) {
                if (window.ContainsKey(c)) {
                    window[c]++;
                } else {
                    window[c] = 1;
                }
                
                // 如果窗口中字符c的数量等于需要的数量,说明该字符满足条件
                if (window[c] == need[c]) {
                    valid++;
                }
            }
            
            // 当窗口中包含t中所有字符时,尝试缩小窗口
            while (valid == need.Count) {
                // 更新最小覆盖子串
                if (right - left < len) {
                    start = left;
                    len = right - left;
                }
                
                // 移除左侧字符
                char d = s[left];
                left++;
                
                // 更新窗口中的数据
                if (need.ContainsKey(d)) {
                    if (window[d] == need[d]) {
                        valid--;
                    }
                    window[d]--;
                }
            }
        }
        
        return len == int.MaxValue ? "" : s.Substring(start, len);
    }
}

Python 实现

python 复制代码
class Solution:
    def minWindow(self, s: str, t: str) -> str:
        if not s or not t or len(s) < len(t):
            return ""
        
        # 统计t中每个字符出现的次数
        need = {}
        for c in t:
            need[c] = need.get(c, 0) + 1
        
        # 初始化窗口
        window = {}
        left, right = 0, 0
        valid = 0  # 记录窗口中满足条件的字符个数
        
        # 记录最小覆盖子串的起始位置和长度
        start, length = 0, float('inf')
        
        while right < len(s):
            # 将右侧字符加入窗口
            c = s[right]
            right += 1
            
            # 更新窗口中的数据
            if c in need:
                window[c] = window.get(c, 0) + 1
                if window[c] == need[c]:
                    valid += 1
            
            # 当窗口中包含t中所有字符时,尝试缩小窗口
            while valid == len(need):
                # 更新最小覆盖子串
                if right - left < length:
                    start = left
                    length = right - left
                
                # 移除左侧字符
                d = s[left]
                left += 1
                
                # 更新窗口中的数据
                if d in need:
                    if window[d] == need[d]:
                        valid -= 1
                    window[d] -= 1
        
        return "" if length == float('inf') else s[start:start+length]

C++ 实现

cpp 复制代码
class Solution {
public:
    string minWindow(string s, string t) {
        if (s.empty() || t.empty() || s.size() < t.size()) {
            return "";
        }
        
        // 统计t中每个字符出现的次数
        unordered_map<char, int> need;
        for (char c : t) {
            need[c]++;
        }
        
        // 初始化窗口
        unordered_map<char, int> window;
        int left = 0, right = 0;
        int valid = 0;  // 记录窗口中满足条件的字符个数
        
        // 记录最小覆盖子串的起始位置和长度
        int start = 0, len = INT_MAX;
        
        while (right < s.size()) {
            // 将右侧字符加入窗口
            char c = s[right];
            right++;
            
            // 更新窗口中的数据
            if (need.count(c)) {
                window[c]++;
                if (window[c] == need[c]) {
                    valid++;
                }
            }
            
            // 当窗口中包含t中所有字符时,尝试缩小窗口
            while (valid == need.size()) {
                // 更新最小覆盖子串
                if (right - left < len) {
                    start = left;
                    len = right - left;
                }
                
                // 移除左侧字符
                char d = s[left];
                left++;
                
                // 更新窗口中的数据
                if (need.count(d)) {
                    if (window[d] == need[d]) {
                        valid--;
                    }
                    window[d]--;
                }
            }
        }
        
        return len == INT_MAX ? "" : s.substr(start, len);
    }
};

执行结果

C# 执行结果

  • 执行用时:84 ms,击败了 93.75% 的 C# 提交
  • 内存消耗:40.2 MB,击败了 87.50% 的 C# 提交

Python 执行结果

  • 执行用时:96 ms,击败了 91.23% 的 Python3 提交
  • 内存消耗:15.8 MB,击败了 85.71% 的 Python3 提交

C++ 执行结果

  • 执行用时:4 ms,击败了 97.89% 的 C++ 提交
  • 内存消耗:7.8 MB,击败了 89.65% 的 C++ 提交

代码亮点

  1. 滑动窗口技巧:使用双指针实现滑动窗口,有效解决子串问题。
  2. 哈希表优化:使用哈希表快速查找和统计字符出现次数,提高效率。
  3. 条件判断优化 :使用 valid 变量记录满足条件的字符个数,避免重复检查。
  4. 空间优化:只需要两个哈希表,空间复杂度为 O(K),其中 K 是字符集大小。
  5. 提前返回:对于特殊情况(如空字符串)提前返回,避免不必要的计算。

常见错误分析

  1. 窗口更新错误 :在更新窗口时,容易忘记更新 valid 变量,导致条件判断错误。
  2. 边界条件处理:对于空字符串或长度不足的情况,需要特别处理。
  3. 字符计数错误 :在处理重复字符时,需要确保窗口中字符的数量不少于 t 中该字符的数量。
  4. 最小长度更新:在找到满足条件的窗口后,需要正确更新最小长度和起始位置。
  5. 循环条件设置 :内层循环的条件是 valid == need.size(),而不是简单地检查窗口大小。

解法比较

解法 时间复杂度 空间复杂度 优点 缺点
滑动窗口 O(n) O(K) 一次遍历即可找到最小覆盖子串 需要额外空间存储字符计数
暴力枚举 O(n^3) O(K) 思路简单直接 效率极低,会超时
优化的滑动窗口 O(n) O(K) 使用数组代替哈希表,可能更快 仅适用于字符集有限的情况

相关题目

相关推荐
এ旧栎41 分钟前
蓝桥与力扣刷题(蓝桥 星期计算)
java·数据结构·算法·leetcode·职场和发展·蓝桥杯·规律
小杨4041 小时前
springboot框架项目实践应用八(validation自定义校验)
spring boot·后端·架构
Cloud_.1 小时前
Spring Boot整合Sa-Token极简指南
java·后端·springboot·登录校验
mit6.8241 小时前
[Sum] C++STL oj常用API
c++·算法·leetcode
槐月初叁1 小时前
C++洛谷基础练习题及解答
开发语言·c++·算法
冬冬小圆帽1 小时前
防止手机验证码被刷:React + TypeScript 与 Node.js + Express 的全面防御策略
前端·后端·react.js·typescript
陈明勇2 小时前
chromem-go:Go 语言 RAG 应用的高效轻量级向量数据库
后端·go
Archer1942 小时前
C++基础——从C语言快速入门
数据结构·c++·算法
好易学数据结构2 小时前
可视化图解算法:链表中环的入口节点(环形链表 II)
数据结构·算法