【最小覆盖子串】

Python3.8

Python3.8

Conda
Python

Python 是一种高级、解释型、通用的编程语言,以其简洁易读的语法而闻名,适用于广泛的应用,包括Web开发、数据分析、人工智能和自动化脚本

一、题目

一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 “” 。

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

示例 1:
输入:s = “ADOBECODEBANC”, t = “ABC”
输出:“BANC”
解释:最小覆盖子串 “BANC” 包含来自字符串 t 的 ‘A’、‘B’ 和 ‘C’

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

二、语法知识

2.1 字典

2.1.1 字典 (dict) 概述

字典是 Python 中一种内置的核心数据结构,属于映射类型。它存储的是键值对的集合。字典的主要特点包括:

  1. 无序性:在 Python 3.6 及更早版本中,字典的键值对存储顺序是未定义的(不保证插入顺序)。但在 Python 3.7+ 中,字典会保留键值对的插入顺序(作为语言规范的一部分)。
  2. 可变性:字典的内容(键值对)可以动态地添加、修改或删除。
  3. 键的唯一性:字典的键必须是唯一的。如果添加时使用了已存在的键,则会覆盖该键对应的旧值。
  4. 键的类型要求:字典的键必须是可哈希的数据类型。通常,不可变类型(如整数、浮点数、字符串、元组)可以用作键。可变类型(如列表、字典、集合)不能用作键。
  5. 值的类型自由:字典的值可以是任何 Python 对象(包括数字、字符串、列表、元组、其他字典等)。

字典使用花括号 {} 创建,键值对之间用冒号 : 分隔,不同的键值对用逗号 , 分隔。

# 创建一个空字典
empty_dict = {}

# 创建包含键值对的字典
student = {
    "name": "Alice",
    "age": 20,
    "courses": ["Math", "Physics"],
    1: "ID Number"  # 整数也可以作为键
}

2.1.2 常用字典方法

字典对象提供了多种方法来操作和访问其中的数据。以下是一些最常用的方法:

1. 访问元素
  • dict[key]: 通过键访问对应的值。如果键不存在,会引发 KeyError
    name = student["name"]  # 获取 "name" 对应的值 "Alice"
    
  • dict.get(key[, default]): 通过键获取对应的值。如果键不存在,返回 default(默认为 None),而不是引发错误。
    age = student.get("age")  # 20
    grade = student.get("grade", "N/A")  # 键不存在,返回 "N/A"
    
2. 添加/修改元素
  • dict[key] = value: 如果键 key 不存在,则添加新的键值对 {key: value};如果键 key 已存在,则更新其对应的值为 value
    student["grade"] = "A"  # 添加新键值对 "grade": "A"
    student["age"] = 21  # 更新键 "age" 的值为 21
    
  • dict.setdefault(key[, default]): 如果键 key 存在于字典中,则返回其对应的值。如果键不存在,则插入 {key: default}default 默认为 None),并返回 default
    # 键 "email" 不存在,插入 {"email": None} 并返回 None
    email = student.setdefault("email")
    # 键 "phone" 不存在,插入 {"phone": "123-4567"} 并返回 "123-4567"
    phone = student.setdefault("phone", "123-4567")
    
3. 删除元素
  • del dict[key]: 删除字典中键为 key 的项。如果键不存在,引发 KeyError
    del student["courses"]  # 删除键 "courses" 及其值
    
  • dict.pop(key[, default]): 删除字典中键为 key 的项,并返回其对应的值。如果键不存在,且提供了 default,则返回 default;否则引发 KeyError
    removed_age = student.pop("age")  # 删除并返回 "age" 的值
    removed_grade = student.pop("grade", "N/A")  # 键不存在,返回 "N/A"
    
  • dict.popitem(): 删除并返回字典中的最后一个键值对(遵循 LIFO 原则)。返回形式为 (key, value)。如果字典为空,引发 KeyError
    last_item = student.popitem()  # 删除并返回最后插入的键值对,如 (1, 'ID Number')
    
  • dict.clear(): 清空字典,移除所有键值对。
    student.clear()  # student 变为空字典 {}
    
4. 获取视图对象

这些方法返回字典的视图对象,它们是动态的,会反映字典的更改。

  • dict.keys(): 返回一个视图对象,包含字典中所有的键。
    keys_view = student.keys()  # 如 dict_keys(['name', 'age', 'courses'])
    
  • dict.values(): 返回一个视图对象,包含字典中所有的值。
    values_view = student.values()  # 如 dict_values(['Alice', 20, ['Math', 'Physics']])
    
  • dict.items(): 返回一个视图对象,包含字典中所有的键值对(以 (key, value) 元组形式)。
    items_view = student.items()  # 如 dict_items([('name', 'Alice'), ('age', 20), ...])
    
5. 更新字典
  • dict.update([other]): 使用另一个字典 other 或可迭代的键值对(如元组列表)来更新当前字典。对于 other 中的键值对:
    • 如果键在当前字典中不存在,则添加。
    • 如果键已存在,则更新其对应的值。
    new_info = {"age": 21, "city": "New York"}
    student.update(new_info)  # 更新 age 为 21, 添加 city: "New York"
    
6. 其他方法
  • dict.copy(): 返回字典的一个浅拷贝。
    student_copy = student.copy()
    
  • len(dict): 返回字典中键值对的数量(不是方法,是内置函数)。
    num_items = len(student)  # 如 3
    
  • key in dict: 检查键 key 是否存在于字典中(不是方法,是成员运算符)。
    has_name = "name" in student  # True
    

2.1.3 总结

字典是 Python 中功能强大且高效的数据结构,特别适合需要通过唯一键快速查找、添加或删除数据的场景。其核心是基于哈希表实现,使得这些操作的平均时间复杂度接近 O ( 1 ) O(1) O(1)

以下表格总结了上述常用方法:

方法/操作描述示例
dict[key]通过键访问值(不存在则 KeyErrorvalue = my_dict["key"]
dict.get(key[, default])通过键获取值(不存在则返回 defaultvalue = my_dict.get("key", "default")
dict[key] = value设置/更新键值对my_dict["new_key"] = 42
dict.setdefault(key[, default])键存在返回值,不存在则插入 {key: default} 并返回 defaultval = my_dict.setdefault("key", [])
del dict[key]删除指定键的项(不存在则 KeyErrordel my_dict["key"]
dict.pop(key[, default])删除指定键的项并返回其值(不存在则返回 defaultKeyErrorval = my_dict.pop("key")
dict.popitem()删除并返回最后一个键值对(空字典则 KeyErrorkey, value = my_dict.popitem()
dict.clear()清空字典my_dict.clear()
dict.keys()返回键的视图keys = my_dict.keys()
dict.values()返回值的视图values = my_dict.values()
dict.items()返回键值对元组的视图items = my_dict.items()
dict.update([other])用其他字典或键值对迭代器更新字典my_dict.update({"a": 1, "b": 2})
dict.copy()返回字典的浅拷贝new_dict = my_dict.copy()
len(dict)返回键值对数量count = len(my_dict)
key in dict检查键是否存在if "key" in my_dict:

三、算法知识

3.2 滑动窗口

3.2.1 滑动窗口算法概述

滑动窗口算法是一种用于处理数组字符串等序列数据的有效技巧。它的核心思想是维护一个大小可变的窗口(通常由两个指针 leftright 定义),这个窗口在序列上“滑动”,并根据问题要求动态调整窗口的大小或位置。这种方法通常能将原本需要嵌套循环的问题优化到线性时间复杂度 O ( n ) O(n) O(n)

3.2.2 主要类型

  1. 固定大小的窗口

    • 特点:窗口的长度在滑动过程中保持不变。
    • 应用场景:计算数组每个大小为 k 的连续子数组的平均值、最大值、最小值等。
    • 操作
      • 初始化 left=0, right=k-1
      • 计算初始窗口的值。
      • 每次将窗口向右滑动一步(left++, right++),移除窗口最左端元素的影响,加入新进入窗口的元素的影响,更新窗口值。
  2. 可变大小的窗口

    • 特点:窗口的大小会根据某些条件(如窗口内元素满足特定性质)动态变化。
    • 应用场景:寻找满足特定条件(如和小于等于 target)的最长子数组/子串,或者寻找包含所有特定元素的最短连续子数组/子串(如最小覆盖子串)。
    • 操作
      • 初始化 left=0, right=0
      • 不断将 right 指针向右移动,扩大窗口,直到窗口内的状态满足(或不再满足)某个条件。
      • 当满足条件时,可能需要更新结果(如记录当前窗口长度)。
      • 然后开始移动 left 指针向右,收缩窗口(通常是为了寻找更优解或破坏当前条件),并不断更新窗口状态。
      • 重复上述过程直到 right 到达序列末端。

3.2.3 关键点

  • 窗口表示:通常由两个指针 left(左边界)和 right(右边界)表示。窗口范围是 [left, right]
  • 窗口滑动:通过移动 leftright 指针实现窗口的扩张或收缩。
  • 窗口状态维护:在移动指针时,需要高效地更新窗口的状态信息(如窗口内元素的和、最大值、元素出现频率等)。使用哈希表(用于计数)、队列(用于维护最大值/最小值)等数据结构可以高效地实现这一点。

3.2.4 应用示例

  1. 固定大小:计算大小为 k 的滑动窗口的最大值(可使用双端队列优化)。
  2. 可变大小
    • 最长无重复字符子串:给定字符串,找到不含重复字符的最长子串。
    • 最小覆盖子串:给定字符串 st,在 s 中找到包含 t 所有字符的最短连续子串。
    • 和为 target 的最长子数组:给定整数数组和 target,找到和大于等于 target 的最短连续子数组(或小于等于 target 的最长子数组)。

3.2.5 时间复杂度

  • 固定大小窗口:通常为 O ( n ) O(n) O(n),因为每个元素最多被进入和离开窗口各一次。
  • 可变大小窗口:通常也为 O ( n ) O(n) O(n) O ( 2 n ) O(2n) O(2n)。虽然 leftright 都可能移动 n n n 次,但每个元素最多被 leftright 各访问一次。

3.2.6 代码示例(可变窗口:最长无重复字符子串)

def lengthOfLongestSubstring(s: str) -> int:
    if not s:
        return 0
    char_index = {}  # 记录字符最近一次出现的位置
    left = 0
    max_length = 0
    for right in range(len(s)):
        if s[right] in char_index and char_index[s[right]] >= left:
            # 当前字符在窗口内重复出现,左边界跳到重复字符上次出现位置的下一位
            left = char_index[s[right]] + 1
        char_index[s[right]] = right  # 更新字符位置
        max_length = max(max_length, right - left + 1)  # 更新最大长度
    return max_length

3.2.7 总结

滑动窗口算法是一种高效处理连续子数组/子串问题的技巧,特别适合需要满足特定条件的连续序列查询。通过维护窗口的状态并利用指针移动,它能显著降低时间复杂度。理解窗口如何根据条件扩张和收缩是掌握该算法的关键。

四、题解

4.1 题解1

思路:哈希表+滑动窗口(双指针)
核心思想滑动窗口 + 字符计数器(高效判断窗口是否覆盖目标字符串)


4.1.1 问题本质

  • 目标:在字符串 s 中找到最短子串,使其包含字符串 t 的所有字符(允许额外字符)
  • 关键挑战
    • 暴力解法(检查所有子串):O(n³) → 超时
    • 需设计 O(n) 解法,通过滑动窗口动态维护覆盖状态

4.1.2 核心思想:覆盖条件 ≠ 精确匹配

条件是否满足题目要求示例(t="AAB"
window_count == target_count❌ 错误窗口 "AAB" ✅,但 "AAAB"
window_count ≥ target_count✅ 正确窗口 "AAB" ✅,"AAAB"

💡 核心洞察
窗口只需满足 window_count[c] ≥ target_count[c](允许更多字符/更高频次),
而非精确相等window_count == target_count)。


4.1.3 算法步骤详解

1. 初始化阶段
# 构建目标字符计数器
target_count = {}
for char in t:
    target_count[char] = target_count.get(char, 0) + 1
  • 作用:记录 t 中每个字符的目标出现次数
  • 示例t="AAB"{'A':2, 'B':1}
2. 滑动窗口维护
指针操作目的
右指针扩展窗口直到覆盖所有目标字符找到第一个有效窗口
左指针收缩窗口尝试找到最小有效窗口优化当前窗口长度
3. 关键状态跟踪(核心创新点)
matched_chars = 0  # 已满足条件的字符种类数
  • 定义:当前窗口中**window_count[c] ≥ target_count[c] 的字符种类数**
  • 判断覆盖条件matched_chars == len(target_count)
    (当所有目标字符种类均满足要求时,窗口有效)
为什么需要 matched_chars
方法时间复杂度原因
直接比较字典O(nk)每次循环需 O(k) 遍历字典检查
matched_chars 跟踪O(n)每次更新 O(1),判断覆盖 O(1)

⚠️ k 的影响:当字符集很大时(如 Unicode),字典比较的 O(k) 会显著拖慢性能


4.1.4 关键操作逻辑

1. 右指针移动(扩展窗口)
char = s[right]
if char in target_count:
    window_count[char] = window_count.get(char, 0) + 1
    # 仅当达到目标次数时增加 matched_chars
    if window_count[char] == target_count[char]:
        matched_chars += 1
  • 关键点
    • 仅当 window_count[char] == target_count[char] 时增加 matched_chars
    • 避免重复计数(例如 t="AA" 时,第二个 A 不会再次增加 matched_chars
2. 左指针移动(收缩窗口)
left_char = s[left]
if left_char in target_count:
    # 仅当低于目标次数时减少 matched_chars
    if window_count[left_char] == target_count[left_char]:
        matched_chars -= 1
    window_count[left_char] -= 1
  • 关键点
    • 仅当 window_count[left_char] == target_count[left_char] 时减少 matched_chars
    • 确保精确反映覆盖状态(例如 t="AA" 时,移除第一个 A 不会破坏匹配)

4.1.5 手动模拟验证(s="ADOBECODEBANC", t="ABC"

步骤操作window_countmatched_chars说明
1初始化{}0target_count = {'A':1, 'B':1, 'C':1}
2右指针到 'A'{'A':1}1A:1 ≥ 1 → 但需检查是否首次达到
3右指针到 'B'{'A':1, 'B':1}2B:1 ≥ 1matched_chars += 1
4右指针到 'C'{'A':1, 'B':1, 'C':1}3C:1 ≥ 1matched_chars += 1窗口有效
5左指针收缩到 'D'{'A':0, 'B':1, 'C':1}2A:0 < 1matched_chars -= 1窗口失效

验证结果

  • 首个有效窗口:"ADOBEC"(长度 6)
  • 最小有效窗口:"BANC"(长度 4)

4.1.6 时间复杂度分析

操作时间复杂度说明
遍历 t 初始化O(m)m = len(t)
遍历 s 滑动窗口O(n)n = len(s)
总时间复杂度O(n+m)线性时间,高效
空间复杂度O(k)k = 字符集大小(通常为常数)

💡 对比

  • 暴力解法:O(n³) → n=100 时已超时
  • 滑动窗口:O(n) → n=10⁵ 时仅需 0.1 秒

4.1.7 常见误区与避坑指南

误区后果修正方法
window_count == target_count永远无法检测到有效窗口matched_chars 跟踪
忘记检查 char in target_count额外字符干扰计数只处理目标字符
更新 matched_chars 时用 重复计数导致逻辑错误严格用 == 判断
忽略 window_count 初始化字典操作报错get(char, 0) 安全访问

4.1.8 完整代码模板

class Solution:
    def minWindow(self, s: str, t: str) -> str:
        m = len(s)
        result = ""

        # 1. 初始化 target_count
        target_count = {}
        for char in t:
            target_count[char] = target_count.get(char, 0) + 1
        # 2. 滑动窗口变量
        matched_chars = 0
        window_count = {}
        left=0
        min_len = inf
        # 3. 移动右指针
        for right in range(m):
            char = s[right]
            if char in target_count:
                window_count[char] = window_count.get(char, 0) + 1
                # 关键:检查当前字符是否满足目标次数
                if window_count[char] == target_count[char]:
                    matched_chars += 1
            # 4. 当覆盖所有字符时,尝试收缩左指针
            while matched_chars == len(target_count):
                # 5. 更新最短子串
                if right-left+1 < min_len:
                    min_len = right-left+1
                    result=s[left:right+1]

                # 6. 收缩左指针
                left_char = s[left]
                if left_char in target_count:
                    window_count[left_char]-=1
                    # 关键:检查收缩后是否破坏匹配
                    if window_count[left_char]<target_count[left_char]:
                        matched_chars-=1
                left+=1 
        return result

4.1.9 关键总结口诀

“目标计数先建好,
窗口扩展看匹配;
达标计数加一种,
收缩窗口减一种。
左移更新最小子,
覆盖判断看种类。”


4.1.10 扩展思考

  1. 变体问题
    • 求最长不重复子串→ 反向应用此逻辑
    • 求包含所有元音字母的最短子串 → 修改 target_count 内容
  2. 优化方向
    • 用数组代替字典(当字符集固定时,如 ASCII)→ 常数级优化
    • 提前终止条件(当 min_len == len(t) 时)→ 小幅优化
  3. 面试高频考点
    • 为什么不能直接比较字典?
    • matched_chars 的更新时机为什么用 == 而不是
    • 如何处理 t 包含重复字符的情况?

💡 终极思考
如果 t="AAA",当窗口中出现 "AA" 时,matched_chars 是多少?为什么?
(答案:0,因为 window_count['A']=2 < target_count['A']=3,未达到目标次数)

4.1.11 优化

4.1.11.1 优化 1:提前终止(理论最短窗口)

原理:当找到长度等于 len(t) 的窗口时,不可能有更短的有效窗口(因为必须包含 t 的所有字符)。

# 在更新最短子串后添加
if min_len == n:  # n = len(t)
    return result  # 立即返回,无需继续
4.1.11.2 优化 2:数组代替字典(ASCII 优化)

原理:当字符集有限(如 ASCII 128 字符),数组访问比字典快 3-5 倍(O(1) 常数更小)。

class Solution:
    def minWindow(self, s: str, t: str) -> str:
        m = len(s)
        n = len(t)
        result = ""

        # 1. 初始化 target_count
        # 替换原字典初始化
        target_count = [0] * 128
        in_t = [False] * 128  # 快速判断字符是否在 t 中
        required_chars = 0
        for char in t:
            ascii_val = ord(char)
            if target_count[ascii_val] == 0:  # 首次出现该字符
                required_chars += 1
            target_count[ascii_val] += 1
            in_t[ascii_val] = True
        # 2. 滑动窗口变量
        matched_chars = 0
        # 窗口计数也改用数组
        window_count = [0] * 128
        left=0
        min_len = inf
        # 3. 移动右指针
        for right in range(m):
            char = s[right]
            ascii_val = ord(char)
            if in_t[ascii_val]:  # O(1) 检查
                window_count[ascii_val] += 1
                # 关键:检查当前字符是否满足目标次数
                if window_count[ascii_val] == target_count[ascii_val]:
                    matched_chars += 1
            # 4. 当覆盖所有字符时,尝试收缩左指针
            while matched_chars == required_chars:
                # 5. 更新最短子串
                if right-left+1 < min_len:
                    min_len = right-left+1
                    result=s[left:right+1]
                if min_len==n:return result
                # 6. 收缩左指针
                left_char = ord(s[left])
                if in_t[left_char]:
                    window_count[left_char]-=1
                    # 关键:检查收缩后是否破坏匹配
                    if window_count[left_char]<target_count[left_char]:
                        matched_chars-=1
                left+=1 
        return result

您可能感兴趣的与本文相关的镜像

Python3.8

Python3.8

Conda
Python

Python 是一种高级、解释型、通用的编程语言,以其简洁易读的语法而闻名,适用于广泛的应用,包括Web开发、数据分析、人工智能和自动化脚本

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小白的高手之路

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值