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) 使用数组代替哈希表,可能更快 仅适用于字符集有限的情况

相关题目

相关推荐
Ruci ALYS几秒前
SpringBoot Maven快速上手
spring boot·后端·maven
MATLAB代码顾问2 分钟前
混合粒子群-模拟退火算法(HPSO-SA)求解作业车间调度问题——附MATLAB代码
算法·matlab·模拟退火算法
java1234_小锋4 分钟前
谈谈Ribbon和Feign区别?
后端·spring cloud·ribbon
Felven7 分钟前
C. Prefix Min and Suffix Max
算法
加农炮手Jinx7 分钟前
LeetCode 26. Remove Duplicates from Sorted Array 题解
算法·leetcode·力扣
加农炮手Jinx7 分钟前
LeetCode 88. Merge Sorted Array 题解
算法·leetcode·力扣
格林威8 分钟前
线阵工业相机:如何计算线阵相机的行频(Line Rate)?公式+实例
开发语言·人工智能·数码相机·算法·计算机视觉·工业相机·线阵相机
yueyue54311 分钟前
透过现象看本质:以fast_lio架构的整套算法的局部避障改为TEB算法为例深度探讨——如何成为一个合格的算法架构师?
算法·架构
梨花爱跨境11 分钟前
红人视频×A10算法:亚马逊转化率与流量闭环实战
算法
SamDeepThinking14 分钟前
为什么要做性能测试
java·后端·程序员