LeetCode Hot 100|滑动窗口专题零基础学习笔记

LeetCode Hot 100|滑动窗口专题零基础学习笔记

学习范围:LeetCode 3《无重复字符的最长子串》 、LeetCode 438《找到字符串中所有字母异位词》 、LeetCode 76《最小覆盖子串》

学习阶段:从暴力枚举过渡到滑动窗口;既理解原理,也能独立手写 Python 代码。

适用人群:刚学 Python、已经接触过 for / while / set / list / ord() 的学习者。


目录

  1. 学习成果与总体路线
  2. 滑动窗口到底是什么
  3. [LeetCode 3|无重复字符的最长子串](#LeetCode 3|无重复字符的最长子串)
  4. [LeetCode 438|找到字符串中所有字母异位词](#LeetCode 438|找到字符串中所有字母异位词)
  5. [LeetCode 76|最小覆盖子串](#LeetCode 76|最小覆盖子串)
  6. 三道题的核心区别与通用模板
  7. [Python 基础与常见错误汇总](#Python 基础与常见错误汇总)
  8. 自测练习与答案
  9. 复习清单与后续学习建议

1. 学习成果与总体路线

本专题已完成三道 Hot 100 题:

题目 类型 暴力方法 优化方法 优化时间
3. 无重复字符的最长子串 可变长度窗口 枚举起点,向右逐个试 set + 左右指针 平均 O(n)
438. 找到字符串中所有字母异位词 固定长度窗口 截取每个子串后排序比较 两个长度 26 的计数数组 O(n)
76. 最小覆盖子串 覆盖型可变长度窗口 枚举起点并逐个扩张终点 need / window / formed 平均 O(n+m)

共同思想: 不要为每个起点重新做一遍相同的工作,尽可能复用"上一个窗口"已经获得的信息。

专题进度: 三道滑动窗口核心题均已学习完成。第 76 题属于困难题,重点复习合法条件与 formed 的精确增减。


2. 滑动窗口到底是什么

滑动窗口就是用两个边界,描述字符串或数组中的一段连续区间。

text 复制代码
s = "a b c a b c b b"
     0 1 2 3 4 5 6 7

窗口 [left, right] = [1, 3]
当前连续子串 = "bca"
窗口长度 = right - left + 1 = 3
  • left:左边界。
  • right:右边界。
  • 向右扩张:新的字符进入窗口。
  • 左边收缩:旧的字符离开窗口。
  • 根据题目要求,维护窗口中的辅助信息,例如字符集合、频率数组、和、最大值等。

2.1 可变长度窗口

适用例子:无重复字符的最长子串。

窗口不是固定大小;右指针不断向右,遇到不合法的情况(出现重复)就移动左指针,直到重新合法。

text 复制代码
右指针扩张 → 发现重复 → 左指针收缩直到不重复 → 更新最优答案

2.2 固定长度窗口

适用例子:找到字符串中所有字母异位词。

如果 p 有 m 个字符,那么候选子串必须恰好有 m 个字符。先建立第一个长度为 m 的窗口;之后每一步都让一个字符进入、另一个字符离开。

text 复制代码
初始化前 m 个字符 → 比较一次
右侧进 1 个字符 → 左侧出 1 个字符 → 比较一次 → 重复

2.3 覆盖型可变长度窗口

适用例子:最小覆盖子串。

右边界扩张直到窗口覆盖目标的字符次数;合法之后连续缩小左边界,每次记录更短的窗口;一旦缩小导致缺少必需字符,就重新扩大右边界。

text 复制代码
右指针扩张,补齐需求 → 合法时记录答案并连续收缩 → 不合法时停止收缩 → 重复

先判断题目要求的区间长度是否固定,再决定使用哪种窗口模式;可变长度窗口又要根据题目目标确定什么时候收缩。


3. LeetCode 3|无重复字符的最长子串

3.1 题意理解

python 复制代码
s = "abcabcbb"
# 最长无重复连续子串之一是 "abc"
# 答案:3

s = "pwwkew"
# "wke" 与 "kew" 均合法,长度为 3
# 答案:3

子串必须连续。 例如 "pwwkew" 中依次选出的 "pwke" 虽然没有重复字符,却不是连续子串,不能算。

再区分两件事:

  • 返回的是最长长度,不是子串内容,也不是起点。
  • "没有重复字符"表示窗口中的同一个字符最多出现一次。

3.2 暴力解法:枚举起点 + set 判断重复

思路

  1. 用外层 for i 枚举子串起点。
  2. 每换一个起点,就创建一个空集合 seen。
  3. 内层 for j 从这个起点往右扫描。
  4. 遇到重复字符,当前起点后面的更长子串一定也重复,因此可以 break。
  5. 若不重复,就加入集合,并更新最长长度。

暴力解法完整 Python 源码

python 复制代码
class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        max_length = 0

        for i in range(len(s)):
            seen = set()

            for j in range(i, len(s)):
                if s[j] in seen:
                    break

                seen.add(s[j])
                length = j - i + 1
                max_length = max(max_length, length)

        return max_length

逐行理解

代码 作用
max_length = 0 没找到合法非空子串时,答案为 0
for i in range(len(s)): 枚举所有起点
seen = set() 每个新起点都从空集合开始
for j in range(i, len(s)): 从起点一直向右试
if s[j] in seen: 当前字符是否已经出现
break 一旦重复,停止当前起点的扫描
seen.add(s[j]) 记录没有重复的新字符
j - i + 1 闭区间 [i, j] 的长度
max(max_length, length) 保存历史最长长度

手算:s = "abba"

起点 i=0:

j 新字符 seen(加入成功后) 是否可继续 当前/最长长度
0 a {a} 是 1 / 1
1 b {a, b} 是 2 / 2
2 b 不添加 否,遇重复并 break --- / 2

起点 i=1 时,"b" 合法、继续遇到 "b" 重复;起点 i=2 可以取 "ba",最长仍是 2。所以最终输出 2。

暴力复杂度

  • 时间:最坏 O(n²)。例如字符互不重复,每个起点都会往后扫描较长一段。
  • 额外空间:O(min(n, k)),其中 k 是字符种类上限;不限制字符集时可写为 O(n)。

问题在哪里? 例如检查完 "abcde",换起点后又从 "bcde" 重新检查。大量字符被重复处理。

3.3 优化思路:可变长度滑动窗口

我们不再为每个起点创建一个新的集合,而是只维护一个窗口 和一个集合。

关键不变量(必须始终成立):

seen 中的字符,恰好是当前窗口 [left, right] 内的所有字符,而且互不重复。

当右指针想加入 s[right] 时:

  • 如果 s[right] 不在集合里:直接加入,窗口增长。
  • 如果已经在集合里:反复删除 s[left] 并移动 left,直到重复消除。
  • 然后加入新字符,更新窗口长度。

为什么必须用 while 而不是 if?

python 复制代码
s = "abba"
# 当前窗口是 "ab",left = 0
# right = 2,准备加入第二个 b

此时 seen = {'a', 'b'}。

第一次删除 a,集合中仍然有 b ;必须再删除原来的 b。如果用 if 只删一次,新窗口就可能保留两个 b,违反条件。使用 while 才能持续收缩到合法。

3.4 最优解:set + 双指针

完整 Python 源码(与本次练习写法一致)

python 复制代码
class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        left = 0
        ar = set()
        max_length = 0

        for right in range(len(s)):
            while s[right] in ar:
                ar.remove(s[left])
                left += 1

            ar.add(s[right])

            length = right - left + 1
            max_length = max(max_length, length)

        return max_length

ar 是你自己起的变量名。完全正确。也可以把它改名为 seen 或 window_chars,只要前后一致即可。

从上到下解释

  1. left = 0:从字符串最左边开始。
  2. ar = set():集合只保存当前窗口里已有的字符。
  3. for right in range(len(s)):右指针每轮向右处理一个新字符。
  4. while s[right] in ar:新字符在窗口中已经存在,说明发生重复。
  5. ar.remove(s[left]):先删除原左端字符。
  6. left += 1:再向右移动左边界;顺序不能交换,否则可能删错字符。
  7. ar.add(s[right]):窗口再次合法后,才加入新字符。
  8. right - left + 1:计算当前闭区间长度。
  9. max(...):维护所有合法窗口中出现过的最大长度。

逐轮手算:s = "pwwkew"

right 当前新字符 收缩动作 最终 left 最终合法窗口 max_length
0 p 无 0 p 1
1 w 无 0 pw 2
2 w 移出 p、旧 w 2 w 2
3 k 无 2 wk 2
4 e 无 2 wke 3
5 w 移出旧 w 3 kew 3

最终输出:3。

为什么两层循环仍是 O(n)?

虽然代码包含一个 for 和一个 while,但两个指针都只向右移动,从不回退:

  • right 遍历 n 个字符。
  • left 在整个算法中最多累计移动 n 次。

所以总移动次数不超过线性数量,集合操作平均 O(1):

  • 平均时间复杂度:O(n)。
  • 额外空间:O(min(n, k));不限制字符集规模时是 O(n)。

边界测试

s 结果 说明
"abcabcbb" 3 最长 abc 等
"bbbbb" 1 每个字符都是 b
"pwwkew" 3 wke 或 kew
"abba" 2 要用 while 连续移除
"" 0 循环不执行
"abcdef" 6 全部不重复
"dvdf" 3 最长 vdf

3.5 本题常见错误

错误 A:用 if 代替 while。 只删一次不保证新字符不重复;考虑 "abba"。

错误 B:在循环里写 seen = set()。 这样每轮都清空窗口信息,丧失滑动窗口的意义。

错误 C:先移动 left,再删旧字符。 正确顺序是先 remove(s[left]),再 left += 1。

错误 D:用 right - left 求长度。 因为左右端点都包含,必须 + 1。

错误 E:把 range(s) 当作遍历下标。 range() 需要整数;本题应写 range(len(s))。


4. LeetCode 438|找到字符串中所有字母异位词

4.1 题意理解

python 复制代码
s = "cbaebabacd"
p = "abc"
# "cba" 和 "bac" 都与 "abc" 互为异位词
# 输出:[0, 6]

字母异位词的三个条件:

  1. 长度相同。
  2. 字符种类相同。
  3. 每种字符出现的次数相同;顺序可以不同。

例如 "aab" 与 "aba" 是异位词,但 "aab" 与 "abb" 不是。

因为长度必须与 p 一致,所以窗口大小一开始就能确定:m = len(p)。

4.2 暴力解法:枚举固定长度子串 + 排序

思路

  1. 提前算出 target = sorted(p)。
  2. 枚举 s 中每个足够容纳 m 个字符的起点 i。
  3. 截取 window = s[i:i+m]。
  4. 若 sorted(window) == target,就记录 i。

暴力解法完整 Python 源码

python 复制代码
class Solution:
    def findAnagrams(self, s: str, p: str) -> list[int]:
        result = []
        m = len(p)
        target = sorted(p)

        for i in range(len(s) - m + 1):
            window = s[i:i + m]

            if sorted(window) == target:
                result.append(i)

        return result

逐行解释与关键下标

  • sorted(p):将 p 的字符排序后保留作参照。
  • range(len(s) - m + 1):枚举所有合法窗口起点,防止子串长度不足。
  • s[i:i+m]:截取从 i 开始的 m 个字符;切片不包含右端下标 i+m。
  • sorted(window) == target:排序后完全一样,才是异位词。
  • result.append(i):添加的是起始下标,不是当前子串或长度。

举例:s = "abab",p = "ab"。

i s[i:i+2] 排序结果 是否匹配
0 "ab" ['a','b'] 是
1 "ba" ['a','b'] 是
2 "ab" ['a','b'] 是

返回 [0, 1, 2]。

暴力复杂度

设 n = len(s)、m = len(p),并假设 n >= m:

  • sorted(p):O(m log m)。
  • 最多有 n-m+1 个窗口,每次切片 O(m)、排序 O(m log m)。
  • 总时间:O(m log m + (n-m+1) · m log m),通常简写为 O((n-m+1) · m log m)(当 m ≥ 2)。
  • 额外空间:O(m),用于切片、排序和目标排序表;不计返回答案。

问题: 相邻窗口大量字符相同,但我们仍然从头排序。

4.3 优化思路:固定长度窗口 + 字符频次

题目规定只有 a~z 26 个小写英文字母,因此可以用一个长度为 26 的列表计数。

python 复制代码
count = [0] * 26

如何将字母转换为下标?

python 复制代码
ord('a') - ord('a')  # 0
ord('b') - ord('a')  # 1
ord('c') - ord('a')  # 2
# ...
ord('z') - ord('a')  # 25

建立两个频次数组:

python 复制代码
m_count = [0] * 26        # p 的字符频次
windows_count = [0] * 26  # s 当前窗口的字符频次

判断异位词,无须排序:

python 复制代码
if windows_count == m_count:
    # 当前窗口与 p 的字符次数完全一致

列表 == 会逐个比较对应位置;两个列表都只含 26 个数字,因此一次比较是 O(26),可以视为常数时间。

4.4 为什么分两个 for?------最适合当前学习阶段的写法

你提出过一个很重要的问题:为什么初始化窗口和后续滑动要分开写?

答案:这是为了区分建立窗口 和移动已建立好的窗口两个阶段,逻辑更直观。不是语法强制要求,也可以合并;但我们选你理解更顺手的分开写法。

第一阶段:建立第一个窗口

python 复制代码
for i in range(m):
    m_index = ord(p[i]) - ord('a')
    windows_index = ord(s[i]) - ord('a')
    m_count[m_index] += 1
    windows_count[windows_index] += 1

这个循环结束时,窗口恰好容纳了 s 的前 m 个字符。此时需要单独检查一次下标 0:

python 复制代码
if windows_count == m_count:
    result.append(0)

第二阶段:从第 m 个下标开始滑动

python 复制代码
for i in range(m, n):
    in_index = ord(s[i]) - ord('a')
    windows_count[in_index] += 1

    out_index = ord(s[i - m]) - ord('a')
    windows_count[out_index] -= 1

    if windows_count == m_count:
        result.append(i - m + 1)

每轮固定做三件事:进一个 → 出一个 → 比较一次。

为什么移出的是 i-m?

假设 m=3、当前 i=5:

text 复制代码
移动之前:窗口覆盖下标 2、3、4
移动之后:窗口覆盖下标 3、4、5

新进入:s[5]
旧离开:s[2] = s[i-m]
新窗口起点:3 = i-m+1

别混淆三个下标公式:

操作 对应表达式
进入窗口的字符 s[i]
离开窗口的字符 s[i-m]
当前窗口起始下标 i-m+1

特别注意: 如果窗口还未建立,就从 i=0 执行 s[i-m],Python 会把负数下标当成"从末尾索引",导致错误地移除字符,而不一定报错。因此分开初始化也能规避这类失误。

4.5 最优解:两个计数数组 + 两个 for

完整 Python 源码(与这次独立练习写法一致)

python 复制代码
class Solution:
    def findAnagrams(self, s: str, p: str) -> list[int]:
        result = []
        m = len(p)
        n = len(s)

        m_count = [0] * 26
        windows_count = [0] * 26

        if m > n:
            return result

        # 第一个 for:初始化目标词和第一个窗口
        for i in range(m):
            m_index = ord(p[i]) - ord('a')
            windows_index = ord(s[i]) - ord('a')

            m_count[m_index] += 1
            windows_count[windows_index] += 1

        # 别漏掉第一个窗口
        if windows_count == m_count:
            result.append(0)

        # 第二个 for:窗口每次右移一格
        for i in range(m, n):
            # 右边新字符进入
            in_index = ord(s[i]) - ord('a')
            windows_count[in_index] += 1

            # 左边旧字符离开
            out_index = ord(s[i - m]) - ord('a')
            windows_count[out_index] -= 1

            # 当前窗口匹配,则记录起点
            if windows_count == m_count:
                result.append(i - m + 1)

        return result

这就是你已经独立写对的算法,只调整了空格和注释以方便复习。变量名 m_count、windows_count 都可以继续沿用。

注:这里用的是 Python 3.9+ 的 list[int] 类型标注。若使用旧版 Python 的 List[int],在本地通常需要 from typing import List。

逐行逻辑总览

关键语句 含义
m = len(p) 固定窗口长度
if m > n: return result 原字符串太短,直接返回 []
m_count = [0] * 26 保存 p 的 26 字母频次
windows_count = [0] * 26 保存窗口的 26 字母频次
for i in range(m) 初始化 p 和 s 的首个窗口
result.append(0) 首个窗口匹配时,起点是 0
for i in range(m, n) 后续每次让一个新字符进入
windows_count[in_index] += 1 进入字符计数增加
windows_count[out_index] -= 1 离开字符计数减少
result.append(i-m+1) 记录当前窗口的起始下标

手算:s = "cbaebabacd",p = "abc"

m=3,目标频次为 a:1, b:1, c:1。

起点 当前长度 3 的子串 匹配? result
0 cba 是 [0]
1 bae 否 [0]
2 aeb 否 [0]
3 eba 否 [0]
4 bab 否 [0]
5 aba 否 [0]
6 bac 是 [0, 6]
7 acd 否 [0, 6]

最终返回 [0, 6]。

优化后复杂度

  • 初始化频次:O(m)。
  • 每个后续字符只进入一次、移出一个;两份长度 26 的列表比较最多 26 次:O(26(n-m))。
  • 因为 26 是常数,n >= m 时总时间为 O(n)。
  • 两个固定长度 26 的计数数组:额外空间 O(1)(不计输出 result)。

4.6 可选思考:一个循环也能实现,但无需强行改写

下面只给出核心处理 s 的逻辑,不是要求现在背的模板:

python 复制代码
for right in range(n):
    # 先让新字符进入
    windows_count[ord(s[right]) - ord('a')] += 1

    # 当加入第 m+1 个字符时,开始移出最左侧字符
    if right >= m:
        windows_count[ord(s[right-m]) - ord('a')] -= 1

    # 窗口至少有 m 个字符时,才能检查是否匹配
    if right >= m - 1 and windows_count == m_count:
        result.append(right-m+1)

使用这一写法时,m_count 要先单独统计完毕。这只说明"两段式"并非唯一方法;当前优先掌握上面的两个 for 版本即可。

4.7 本题常见错误

错误 A:漏掉第一个窗口。 后续 for i in range(m,n) 从第 m 个下标开始,因此必须提前比较一次下标 0 的窗口。

错误 B:窗口进一个字符后忘了出一个。 窗口长度会不断增长,无法匹配固定长度 m。

错误 C:写反 +=1 与 -=1。 新进入必须 +1,旧离开必须 -1。

错误 D:把旧字符下标写成 i-m+1。 旧字符在 i-m,而 i-m+1 才是新窗口起点。

错误 E:只比较字符是否出现,不比较出现次数。 "aab" 与 "abb" 有相同字符种类,但不是异位词。

错误 F:直接 for i in range(m) 前不检查 m>n。 原字符串太短时,访问 s[i] 会越界;需要提前返回。

边界测试

s p 结果
"cbaebabacd" "abc" [0, 6]
"abab" "ab" [0, 1, 2]
"baa" "aa" [1]
"abc" "abcd" []
"aaaa" "aa" [0, 1, 2]
"a" "a" [0]
"a" "b" []

5. LeetCode 76|最小覆盖子串

5.1 题目到底要什么

给定字符串 s 和目标字符串 t,在 s 中寻找最短的连续子串 ,要求这个子串包含 t 中每一种字符以及对应的出现次数 。找不到就返回空字符串 ""。

python 复制代码
s = "ADOBECODEBANC"
t = "ABC"
# 答案:"BANC"

"ADOBEC" 也包含 A、B、C,长度为 6;"BANC" 更短,长度为 4,因此选择后者。

三个不能混淆的条件:

  • 子串必须连续,不能隔几个字符随意挑选。
  • 覆盖意味着次数至少达到要求,允许多出来的字符。
  • 返回的是字符串,不是起点,也不是最短长度。

举个更容易忽略的例子:

python 复制代码
t = "AABC"
# 需要至少 2 个 A、1 个 B、1 个 C
# "ABC"   不合法:少 1 个 A
# "BAAC"  合法:次数恰好满足
# "AAABC" 合法:多 1 个 A 也没关系

本题区分大小写:"A" 和 "a" 是不同字符。

5.1.1 你学过的两道题如何帮助理解?

题目 你已经会的能力 在本题中的用途
3. 无重复字符的最长子串 left、right 构成可变窗口,while 连续收缩 窗口合法后,尽可能缩短
438. 找到所有字母异位词 字符计数、需求与当前窗口比较 确认每种目标字符出现次数足够
76. 最小覆盖子串 把以上两个知识点组合 频次达标后缩小窗口,记录最短字符串

5.2 从暴力解法开始

5.2.1 最直观的思路

假设:

python 复制代码
s = "ADOBECODEBANC"
t = "ABC"
  1. 固定起点 left = 0,让终点 right 从左向右移动。
  2. 维护当前窗口里每个字符的出现次数。
  3. 一旦当前窗口覆盖 t,记录合法窗口长度;对这个固定起点而言,再向右扩大只会更长,所以停止这一轮内层搜索。
  4. 将 left 改成 1、2、3......,重复上述过程。
  5. 所有起点检查完后,返回找到的最短子串。

为了表示目标需求,先创建字典:

python 复制代码
need = {}
for ch in t:
    need[ch] = need.get(ch, 0) + 1

# 如果 t = "AABC",则 need = {'A': 2, 'B': 1, 'C': 1}

其中 need.get(ch, 0) 表示:ch 不在字典里时使用默认值 0。

5.2.2 暴力解法完整 Python 源码

python 复制代码
class Solution:
    def minWindow(self, s: str, t: str) -> str:
        if not s or not t or len(t) > len(s):
            return ""

        need = {}
        for ch in t:
            need[ch] = need.get(ch, 0) + 1

        min_length = float('inf')
        answer = ""

        # 枚举每个子串的起点
        for left in range(len(s)):
            window = {}

            # 从当前起点向右枚举终点
            for right in range(left, len(s)):
                ch = s[right]
                window[ch] = window.get(ch, 0) + 1

                # 判断是否覆盖 t 的所有字符和对应次数
                valid = True
                for key in need:
                    if window.get(key, 0) < need[key]:
                        valid = False
                        break

                if valid:
                    length = right - left + 1
                    if length < min_length:
                        min_length = length
                        answer = s[left:right + 1]

                    # 起点固定,继续扩大只会更长
                    break

        return answer

为什么 break 正确?

break 只结束当前的内层 for right。对于固定的 left,第一个合法的 right 已经是这个起点能得到的最短合法窗口。后面扩大窗口,不可能更短。但我们仍然继续尝试新的左边界,因为新的起点可能得到更好的答案。

为什么这里 window 每次都要重新建?

暴力解法对每个新起点都从头统计。这正是它效率不高的原因,也是滑动窗口要优化的地方。

5.2.3 暴力复杂度

设 n = len(s),m = len(t),k 为 t 中不同字符的种类数。

  • 建立 need:O(m)。
  • 枚举起点和终点:最坏 O(n²) 次。
  • 每个候选窗口检查最多 k 种必需字符:O(k)。
  • 因此最坏时间:O(m + n²k) ;字符种类上限固定时可以写作 O(m+n²)。
  • 额外空间:需求字典和窗口字典最多存储 O(k+n) 种字符(暴力代码会统计所有进入窗口的字符)。在固定英文字符集下为 O(1);若不固定字符集,可界定为 O(m+n),不计答案字符串。

暴力解法的重要性:明确"我要维护什么"和"哪里在重复计算",再谈优化。


5.3 为什么要改用滑动窗口

对于窗口 [left, right],如果我们已经统计了它的字符次数:

  • 扩大窗口 :right 前进一步,只需要统计刚进入的 s[right]。
  • 缩小窗口 :left 前进一步,只需要扣掉刚离开的 s[left]。
  • 不再 因为起点变化就清空 window 并把中间字符重新统计一次。

目标不同,收缩规则也不同:

text 复制代码
无重复字符的最长子串(3):
右侧加入后发生重复 → 不合法 → 收缩至合法 → 更新最大长度

最小覆盖子串(76):
右侧加入直到覆盖全部需求 → 已经合法 → 尽可能收缩并更新最小长度

不要死记"所有可变窗口都是窗口不合法才收缩"。 不同题目的合法性与最优化目标不同。本题恰好在合法时持续收缩。


5.4 真正的难点:formed 是什么

只使用 need 与 window 两个字典,当然也能每次逐个检查目标字符是否足够。但这会反复扫字典。

我们新建两个变量:

python 复制代码
required = len(need)  # 一共有多少种不同的目标字符
formed = 0             # 已经有多少种目标字符的数量达标

当且仅当:

python 复制代码
formed == required

当前窗口才覆盖了目标 t。

最重要的提醒:formed 统计"满足需求的字符种类数",不是窗口里字符的数量,也不是出现次数的总和。

5.4.1 用 t = "AABC" 手算

目标:

python 复制代码
need = {'A': 2, 'B': 1, 'C': 1}
required = 3
当前窗口 A 达标? B 达标? C 达标? formed
"A" 否(仅 1 个) 否 否 0
"AA" 是 否 否 1
"AAB" 是 是 否 2
"AABC" 是 是 是 3
"AAABC" 是(多一个无妨) 是 是 3

"A" 虽然出现了,但目标要求两个 A,所以它还不能贡献一个 formed。

5.4.2 右边进入一个字符:什么时候 formed += 1?

python 复制代码
ch = s[right]

if ch in need:
    window[ch] = window.get(ch, 0) + 1

    if window[ch] == need[ch]:
        formed += 1

必须刚好达到目标次数,才把这种字符计为达标。

例如 need['A'] = 2:

  • 第一个 A:window['A'] = 1,未达标,formed 不动。
  • 第二个 A:window['A'] = 2,刚刚达标 ,formed += 1。
  • 第三个 A:window['A'] = 3,早已达标,不得再次加 1。

为什么不能写 >=?因为第三个、第四个 A 都会再次增加 formed,使"已满足种类数"失真。

5.4.3 左边移出一个字符:什么时候 formed -= 1?

python 复制代码
left_ch = s[left]

if left_ch in need:
    window[left_ch] -= 1

    if window[left_ch] < need[left_ch]:
        formed -= 1

left += 1

只有某种字符的次数从"足够"降到"不足",formed 才减 1。

例如 need['A'] = 2、当前有 3 个 A:

  • 删除一个 A → 还剩 2 个,仍达标,formed 不变。
  • 再删除一个 A → 只剩 1 个,不达标,formed -= 1。

这里先减少 window[left_ch],再判断是否 < need[left_ch],最后才移动 left;顺序很重要。

5.4.4 为什么 while formed == required?

假设:

python 复制代码
s = "XAABCA"
t = "AABC"

从左往右扩张,首次覆盖需求的窗口是 "XAABC":

text 复制代码
X A A B C A
[-------]       XAABC:合法,长度 5
  [-----]       AABC:移除 X 后仍合法,长度 4(更优)
    [---]       ABC:再移除 A 后不合法,缺一个 A

因此,每当窗口合法,就用 while 尽量缩小,并且在删除左字符之前记录当前合法答案。一旦缩小到不合法,就继续让右指针扩张。


5.5 滑动窗口最优解完整源码

这是你通过练习并修正两个小错误之后应当能够独立写出的版本。保持熟悉的 for right / while formed 结构。

python 复制代码
class Solution:
    def minWindow(self, s: str, t: str) -> str:
        if not s or not t or len(t) > len(s):
            return ""

        # 阶段 1:统计目标字符串每种字符需要多少个
        need = {}
        for ch in t:
            need[ch] = need.get(ch, 0) + 1

        # 阶段 2:初始化窗口状态
        window = {}
        required = len(need)
        formed = 0

        left = 0
        start = 0
        min_length = float('inf')

        # 阶段 3:右指针向右,逐个加入新字符
        for right in range(len(s)):
            ch = s[right]

            if ch in need:
                window[ch] = window.get(ch, 0) + 1

                # 这一种字符刚好达到要求
                if window[ch] == need[ch]:
                    formed += 1

            # 阶段 4:窗口合法,尽可能收缩左边
            while formed == required:
                length = right - left + 1

                if length < min_length:
                    min_length = length
                    start = left

                left_ch = s[left]

                if left_ch in need:
                    window[left_ch] -= 1

                    # 这一种字符从达标变为不足
                    if window[left_ch] < need[left_ch]:
                        formed -= 1

                left += 1

        # 阶段 5:返回最短子串,而不是它的长度
        if min_length == float('inf'):
            return ""

        return s[start:start + min_length]

5.5.1 分段解释

代码 具体作用
if not s or not t or len(t) > len(s) 空输入或 t 更长时,不可能有合法窗口
need[ch] = need.get(ch, 0) + 1 统计所需字符的次数;重复字符不能丢
window = {} 只记录当前窗口里出现过的目标字符的次数
required = len(need) 目标中不同字符的种类数
formed = 0 当前已满足次数要求的种类数
min_length = float('inf') 用无穷大表示"尚未找到答案"
for right in range(len(s)) 右指针一步步向右扩张
if ch in need 非目标字符无需维护计数,但仍占据子串长度
if window[ch] == need[ch] 新进入字符刚刚达标,增加 formed
while formed == required 当前覆盖需求了,尽可能缩小左边界
length = right - left + 1 当前窗口包含左右两个端点,所以要加 1
start = left 保存目前最优窗口的起点
window[left_ch] -= 1 旧字符离开窗口
if window[left_ch] < need[left_ch] 移出导致该字符不再达标,减少 formed
left += 1 收缩完成后继续向右移动左边界
s[start:start + min_length] 切片返回真正的字符串答案;右端点不包含

5.5.2 算法运行时的两个关键不变量

  1. 每轮 for 执行完右侧加入操作时,window 中记录的是当前窗口 [left, right] 的目标字符频次;窗口收缩时每移走左端字符,也会同步维护频次。
  2. formed 始终等于当前频次已经达到 need 要求的字符种类数 ;所以 formed == required 等价于"当前窗口合法"。

只要这两个不变量不被破坏,算法就能在窗口合法时收缩寻找最短答案。


5.6 逐步手算与正确性理解

5.6.1 官方示例 s = "ADOBECODEBANC"、t = "ABC"

目标需要 A、B、C 各一个:required = 3。

观察关键时刻(省略不影响结论的中间扩张步骤):

时刻 当前合法窗口或当前局面 操作结果 最优答案
right=5(字符 C) "ADOBEC",formed=3 首次合法,记录长度 6 "ADOBEC"
随后左侧收缩 删除最左侧 A 后不足 formed 变 2,停止收缩 "ADOBEC"
right=10(字符 A) 重新拥有 A、B、C 又可以连续收缩 仍 "ADOBEC"
收缩到左边界为 5 "CODEBA",长度 6 已合法,但没更短 仍 "ADOBEC"
继续删除 C 失去必需的 C formed 变 2,停止收缩 仍 "ADOBEC"
right=12(字符 C) 再次满足 A、B、C 不断删除左端多余字符 逐渐缩短
收缩到左边界为 9 "BANC",长度 4 更新最短窗口 "BANC"
再删除 B 不再有 B,窗口失效 结束当前收缩 "BANC"

最终:

python 复制代码
"BANC"

5.6.2 为什么这个算法不会漏掉更短答案?

  • 对每个 right,算法只在右端加入一个新字符后检查当前窗口是否合法。
  • 一旦合法,就不断右移 left,把不必要的左端字符尽量去掉,期间的所有合法候选窗口都与历史最短比较。
  • 当移走某种必需字符造成数量不足时,继续缩小只会更不满足需求;此时正确操作是扩大右端,等待补足缺失字符。
  • 两指针不会回退,但也不必回退:已经扔掉的较左起点,不会比本轮已经检查的合法窗口更短。

5.6.3 为什么最优答案只保存起点与长度?

python 复制代码
min_length = float('inf')
start = 0

当发现更优窗口时只更新这两个数字,最后才截取:

python 复制代码
return s[start:start + min_length]

这样避免每次窗口一变短就复制一段字符串,也使最优解时间复杂度的论证更清晰。


5.7 复杂度与两种窗口对比

设 n = len(s)、m = len(t)、k = t 中不同字符的种类数。

5.7.1 最优解复杂度

  • 建立 need:O(m)。
  • 右指针 right 最多处理 n 个字符:O(n)。
  • 左指针 left 总共最多向右移动 n 次,不会回头:所有内层 while 的总次数也是 O(n)。
  • 字典查询、插入、更新在通常情况下平均 O(1)。
  • 最后只截取一次答案,最多复制 O(n) 个字符。

因此:平均时间 O(n+m)。

空间:need 和 window 只为目标字符分配字典键,最多 k 个键,额外空间 O(k) ,不计返回的答案字符串。在题目规定的固定英文字符集里,k 有常数上界,也可写作 O(1)。

5.7.2 三道题比较

题目 窗口什么时候收缩 保存什么状态 求什么答案
3. 无重复字符的最长子串 右侧新字符造成重复时 set,当前窗口有哪些字符 最长长度
438. 找到所有字母异位词 每轮滑动移出一个旧字符 两个频次数组 所有合法起点
76. 最小覆盖子串 窗口已覆盖目标时,持续收缩 need、window、formed 最短子串本身

5.8 你亲自遇到的两个错误

你在独立写代码时,主体结构、缩进、右指针与左指针移动都写对了,只剩下两个非常典型的逻辑错误。一定要把它们当成复习重点。

5.8.1 错误一:用整个字典比较来更新 formed

你第一次写的是:

python 复制代码
if window == need:
    formed += 1

为什么不对?

formed 记录的是"多少种目标字符已经达标",不是"整个字典有没有恰好一样"。

例如:

python 复制代码
need = {'A': 2, 'B': 1, 'C': 1}
window = {'A': 2}

A 已经达标,正确的 formed 是 1,但 window == need 是 False。另外,窗口里允许出现某些目标字符超过所需次数,整个字典也可能不相等。

正确写法:

python 复制代码
if window[ch] == need[ch]:
    formed += 1

只关注刚加入的那个字符是否"恰好跨过达标线"。

5.8.2 错误二:返回了最短长度,不是最短子串

你第一次写的是:

python 复制代码
return min_length

但题目的函数声明是:

python 复制代码
def minWindow(self, s: str, t: str) -> str:

题目要求返回字符串。比如 "BANC",而不是整数 4。

正确写法:

python 复制代码
return s[start:start + min_length]

切片左闭右开 ,比如 s[9:13] 得到 "BANC"。如果整个过程中从未找到合法窗口,那么 min_length 保持 float('inf'),必须在切片前先返回 ""。

5.8.3 再补充四个容易误写的地方

  1. window[ch] >= need[ch] 就每次 formed += 1。 错!第 3、4 个 A 不应重复贡献种类数;加入时只有 == 才加。
  2. 左侧每删一个目标字符,就直接 formed -= 1。 错!只有删除后 < need[left_ch] 才说明刚刚从达标变为不达标。
  3. 忘记 left += 1。 while 可能永远停在同一个左端点,发生死循环。
  4. 把 length = right - left + 1 写成 right - left。 闭区间长度必须加 1。

5.9 边界用例、练习与默写

5.9.1 必测用例

s t 预期结果 检查什么
"ADOBECODEBANC" "ABC" "BANC" 官方标准例子
"a" "a" "a" 单字符正好匹配
"a" "aa" "" 需求次数多于原串
"aa" "aa" "aa" 同一个字符需要多次
"XAABCA" "AABC" "AABC" 重复需求、收缩窗口
"abc" "d" "" 根本不存在目标字符
"AAABC" "AABC" "AABC" 多余字符可以被收缩
"aAbBc" "ABC" "" 大小写有区别
"" "A" "" 空原字符串(鲁棒性测试)
"ABC" "" "" 空目标(鲁棒性测试)

注:最后两项是为了练习鲁棒性,题目原始约束通常限定字符串非空。

5.9.2 口头练习

  1. t="AABC" 时,当前窗口只有一个 A,formed 能增加吗?为什么?
  2. 如果目标需要 2 个 A,窗口有 3 个 A,移除一个后 formed 要不要减 1?
  3. 本题为什么合法时用 while 收缩,而第 3 题通常是重复时收缩?
  4. start=9、min_length=4,返回的切片怎么写?
  5. 为什么 for right 嵌套 while 仍然是线性级别?
  6. 目标字符之外的字符(如目标 ABC,窗口中有 D)是否需要统计?它是否占据窗口长度?

展开口头练习答案

  1. 不能,A 尚未满足需要的 2 次。
  2. 不减,移除后剩 2 个,仍然达标。
  3. 第 3 题要保证"不重复",重复出现使窗口非法;本题要尽可能短,只有合法后才可以放心尝试缩小。
  4. s[start:start+min_length],即 s[9:13]。
  5. right 最多走 n 步,left 在所有 while 中累计最多走 n 步,互不回退。
  6. 可以不计入 window 字典,但它仍在 [left,right] 内,计算长度时不能假装它不存在。

5.9.3 默写框架(不要先看标准答案)

python 复制代码
class Solution:
    def minWindow(self, s: str, t: str) -> str:
        if not s or not t or len(t) > len(s):
            return ""

        need = {}
        for ch in t:
            need[ch] = TODO_1

        window = {}
        required = len(need)
        formed = 0
        left = 0
        start = 0
        min_length = float('inf')

        for right in range(len(s)):
            ch = s[right]
            if ch in need:
                window[ch] = window.get(ch, 0) + 1
                if TODO_2:
                    formed += 1

            while TODO_3:
                length = right - left + 1
                if length < min_length:
                    min_length = length
                    start = TODO_4

                left_ch = s[left]
                if left_ch in need:
                    window[left_ch] -= 1
                    if TODO_5:
                        formed -= 1
                TODO_6

        if min_length == float('inf'):
            return ""
        return TODO_7

展开填空答案

  • TODO_1 → need.get(ch, 0) + 1
  • TODO_2 → window[ch] == need[ch]
  • TODO_3 → formed == required
  • TODO_4 → left
  • TODO_5 → window[left_ch] < need[left_ch]
  • TODO_6 → left += 1
  • TODO_7 → s[start:start + min_length]

5.10 一分钟速记卡片

需求字典 :need[ch] = need.get(ch, 0) + 1。

目标种类数 :required = len(need)。

已达标种类数 :formed。

右侧进入 :window[ch] += 1;只有刚好等于 need[ch] 才 formed += 1。

合法条件 :formed == required。

合法后收缩 :先记录更短答案;左侧退出,只有低于 need[left_ch] 才 formed -= 1;再 left += 1。

返回 :若无合法窗口返回 "";否则返回 s[start:start+min_length]。

最后一句:第 76 题不是死记 formed,而是理解它代表"已经满足数量要求的字符种类数"。


6. 三道题的核心区别与通用模板

6.1 对照表

对比维度 LeetCode 3 LeetCode 438 LeetCode 76
需要找什么 最长无重复子串的长度 所有异位词子串的起点 最短覆盖子串的内容
窗口类型 可变长度 固定长度 可变长度(覆盖型)
窗口合法条件 任意字符最多出现一次 与目标 p 的每种字符次数一致 每种所需字符次数至少达到目标要求
维护信息 一个 set 两个 26 长度计数数组 need、window、formed
左边何时移动 发现重复时,可能连续多次 每加入一个字符,就移出一个旧字符 已覆盖所有需求时尽量收缩
更新答案 每次窗口合法后更新最大长度 两份频次数组相等时记录起点 每次窗口合法时更新最短长度与起点
优化后时间 平均 O(n) O(n) 平均 O(n+m)
优化后额外空间 最坏 O(n) O(1)(26 个小写字母) O(k),固定英文字符集下 O(1)

6.2 模板一:可变长度窗口(理解型伪代码)

python 复制代码
left = 0
# 创建窗口维护数据

for right in range(n):
    # 检查/考虑右侧新元素

    while 当前窗口不合法:
        # 移除 s[left] 对窗口的影响
        left += 1

    # 把新元素纳入合法窗口(视题目维护方式而定)
    # 更新答案

这是理解型框架 ,不是可以直接运行的 Python;不同题目中"先加入新元素"还是"先检查重复后加入",可能不同。LeetCode 3 采用先检查重复、后加入。

6.3 模板二:固定长度窗口(你喜欢的分开写法)

python 复制代码
m = len(p)
n = len(s)

if n < m:
    return []

# 1. 初始化第一个长度为 m 的窗口
for i in range(m):
    # 将 s[i] 纳入窗口,同时初始化目标信息
    pass

# 2. 检查第一个窗口
# 如果符合条件,记录起点 0

# 3. 后续窗口每轮向右移动一步
for i in range(m, n):
    # s[i] 进入
    # s[i-m] 离开
    # 判断窗口,若符合条件则记录 i-m+1
    pass

同样,这是帮助回忆的伪代码骨架;要真正提交,需要填入具体的计数、判定及返回逻辑。

6.4 模板三:覆盖型可变长度窗口(LeetCode 76)

python 复制代码
# 这段是帮助回忆的模板,不是完整可运行代码
need = ...
window = {}
required = len(need)
formed = 0
left = 0

for right in range(len(s)):
    # 让右边新字符进入,并同步维护 window、formed
    while formed == required:
        # 记录当前合法区间:争取更短
        # 让最左侧字符离开,并同步维护 window、formed
        left += 1

牢记:formed 不是已加入字符数,而是已达标字符的种类数。

6.5 何时使用 set,何时使用计数数组?

  • 只关心某字符是否存在 ,且窗口要求互不重复:优先考虑 set。
  • 关心每种字符具体出现多少次 :考虑 dict、Counter 或计数数组。
  • 字符集明确、很小且固定,例如只有 a-z:长度 26 的数组通常简单高效。
  • 字符集可能任意(例如 Unicode 字符),不能想当然用 26 长度的计数数组。
  • 需要判断"至少有多少个"的覆盖需求:字典 + formed 更适合灵活维护。

7. Python 基础与常见错误汇总

这一节尤其重要:你前面已经能够理解算法,接下来要提高独立敲代码的准确率。

7.1 set() 不是 {}

python 复制代码
seen = set()  # 空集合
other = {}    # 空字典,不是集合

7.2 set.add() 和 set.remove()

python 复制代码
seen = set()
seen.add('a')      # 添加 a
seen.remove('a')   # 删除已存在的 a

注意 remove() 删除一个不存在的元素会抛出 KeyError。LeetCode 3 的代码保证了当前 s[left] 确实在合法窗口集合中,所以不会误删。

7.3 range() 与 len()

python 复制代码
s = "abc"
for i in range(len(s)):
    print(i)  # 0、1、2

错误示范:range(s),因为 s 是字符串而不是整数。

7.4 break、continue 和 while

  • break:退出当前这一层循环。
  • continue:跳过当前轮剩余代码,进入下一轮。
  • while 条件::条件成立就一直重复;LeetCode 3 用它持续清除重复字符。

7.5 字符串切片与窗口长度

python 复制代码
s = "abcde"
print(s[1:4])  # "bcd",右端下标 4 不包含

left = 1
right = 3
length = right - left + 1  # 3

切片区间 [起点, 终点) 与滑动窗口常用闭区间 [left, right] 不要混淆。

7.6 ord() 转换字母下标

python 复制代码
index = ord('c') - ord('a')  # 2
count = [0] * 26
count[index] += 1

7.7 Python 的下划线是变量名的一部分

python 复制代码
left = 0
_left = 0
left____ = 0

它们是三个不同的变量。做代码填空时,替换整个 TODO 或 _____ 占位符,不要把答案嵌入下划线中间。

7.8 List[int] 与 list[int]

python 复制代码
# Python 3.9+
def f(nums: list[int]) -> list[int]:
    return nums

# 需要 typing.List 的写法
from typing import List

def g(nums: List[int]) -> List[int]:
    return nums

7.9 dict.get() 与 formed 的达标判断

python 复制代码
need = {}
for ch in "AABC":
    need[ch] = need.get(ch, 0) + 1
# {'A': 2, 'B': 1, 'C': 1}

# 右边进入时:次数刚好达标,才 formed += 1
if window[ch] == need[ch]:
    formed += 1

# 左边退出时:次数刚好跌破目标,才 formed -= 1
if window[left_ch] < need[left_ch]:
    formed -= 1

7.10 缩进决定逻辑归属

python 复制代码
for right in range(len(s)):
    while s[right] in seen:
        seen.remove(s[left])
        left += 1

    seen.add(s[right])       # 与 while 平级:去重结束后才执行

如果 seen.add(s[right]) 错误地缩进进 while,程序逻辑就会被破坏。


8. 自测练习与答案

建议先只看题目,尝试手算或在纸上写代码,再展开后面的答案。

练习 A:判断子串与滑动窗口(LeetCode 3)

  1. s = "abba",最长无重复连续子串长度是多少?
  2. s = "dvdf",最长无重复连续子串长度是多少?
  3. 在 s = "abba" 中,right=2 遇到第二个 b。收缩窗口结束后 left 等于多少?
  4. 为什么 LeetCode 3 的去重语句使用 while 而不是 if?

练习 B:固定长度滑动窗口(LeetCode 438)

  1. s = "abab",p = "ab",返回哪些起始下标?
  2. s = "aaba",p = "aa",返回哪些起始下标?
  3. m=3,i=5,进入和离开的字符下标分别是多少?新窗口起点是多少?
  4. 为什么分两个 for 的写法,需要单独检查第一个窗口?
  5. s 长度比 p 短时,应该返回什么?
  6. 为什么频次数组只有 26 个元素时,比较两份数组在复杂度分析中算 O(1)?

练习 C:最小覆盖子串(LeetCode 76)

  1. t="AABC",当前窗口 "AA":required 和 formed 分别是多少?
  2. t="AABC",窗口从 "AAABC" 移除一个 A,formed 应不应该减 1?
  3. 为什么 LeetCode 76 使用 while formed == required?
  4. 目标需要 2 个 A,第三个 A 进入时,formed 应不应该再加 1?
  5. s="ADOBECODEBANC"、t="ABC",最终返回 "BANC" 还是整数 4?

练习 D:默写挑战

不看答案,独立写出:

  • LeetCode 3:set() + for right + while 收缩 + 更新 max_length。
  • LeetCode 438:m_count / windows_count + 初始化首窗口 + 检查起点 0 + 后续进一出一 + i-m+1。
  • LeetCode 76:need / window / formed + 右扩张 + 合法时 while 收缩 + 返回切片。

写完后分别测试空/短字符串、重复字符、大量相同字母和一般样例。
点击展开:自测参考答案

  1. 2("ab" 或 "ba")。
  2. 3("vdf")。
  3. 2;需要依次移出 a 和原来的 b。
  4. 一次移除不一定消除重复,因此需要持续收缩。
  5. [0, 1, 2]。
  6. [0]。
  7. 进入 s[5],离开 s[2],窗口起点 3。
  8. 第二个循环从 i=m 开始,首次窗口对应的下标 0 不会在第二轮被检查。
  9. []。
  10. 要检查的元素个数固定为 26,不随 n 增长。
  11. required=3(A、B、C 三种),formed=1(只有 A 已达标)。
  12. 不减;从 3 个 A 变为 2 个,仍满足需要 2 个的要求。
  13. 为了在仍然合法的前提下尽可能缩短窗口,并检查所有可能的更短候选。
  14. 不加;只有从 1 个 A 增加到 2 个 A 时才刚好达标。
  15. 返回字符串 "BANC"。

9. 复习清单与后续学习建议

9.1 完成打卡

  • 理解连续子串与子序列的区别。
  • 能解释 LeetCode 3 的暴力两层循环思路。
  • 理解滑动窗口如何复用上一个窗口的信息。
  • 能用 set、while 实现可变长度窗口。
  • 能解释为什么嵌套 while 仍然是平均 O(n)。
  • 理解字母异位词必须满足字符频次相同。
  • 能用 ord() 和长度 26 的列表统计字符频次。
  • 能使用两个 for 完成先初始化、再移动的固定长度窗口。
  • 能解释 i、i-m 和 i-m+1 的不同用途。
  • 理解覆盖题中"至少满足需要的次数"和异位词的"次数完全相等"的区别。
  • 能解释 formed 的含义,以及为什么进入用 ==、退出用 <。
  • 能使用合法窗口的 while 收缩,维护 start、min_length。
  • 知道最终返回 s[start:start+min_length],而不是整数长度。
  • 已完成三道滑动窗口题的代码练习和纠错。

9.2 复习时的三个层次

第一遍:只看思路。 能否说清楚窗口何时扩张、何时收缩、如何更新答案?

第二遍:手算。 手动写出 left、right、集合或频次数组在几轮关键迭代中的状态。

第三遍:独立写代码。 合上笔记,从 class Solution 开始完整默写,再对照样例与边界情况检查。

9.3 下一步推荐

本专题三道题已完成。 下一步建议进入 Hot 100「子串」专题,先学习 LeetCode 560《和为 K 的子数组》,逐步掌握前缀和与哈希表的结合。


最后一页:考试前速记

无重复字符的最长子串(3) :set 记录窗口字符,right 向右,重复时用 while 移除 s[left] 并 left += 1,最后更新 right-left+1。

找到所有字母异位词(438) :窗口长度固定为 m=len(p),先建立首窗口、检查下标 0,再从 i=m 开始循环;s[i] 进入、s[i-m] 离开,频次相同时记录 i-m+1。

最小覆盖子串(76) :need 保存需求,window 计数,formed 记录已达标的字符种类;右扩张至合法、左收缩找最短;最后返回 s[start:start+min_length]。

滑动窗口的本质:复用已知窗口的信息,而不是每换一个起点就从头计算。

本笔记整合了已完成的三道滑动窗口题;代码采用 Python 3,其中第 438 题使用题目限定的非空 p 和小写英文字母条件。第 76 题附完整暴力解法、优化解法,以及独立练习时的两处典型纠错。

相关推荐
小雪崩1 小时前
嵌入式学习 day68:驱动——SPI总线
学习
其实防守也摸鱼2 小时前
CTF Web 6 题全解 Writeup
linux·sql·学习·算法·安全·ctf·xss
Xudde.2 小时前
DC-1 靶机渗透测试(Vulnhub Writeup)
笔记·学习·安全·web安全
浪子不回头4152 小时前
同花顺一面
学习
拾饵9422 小时前
面试回答整理(Q1–Q3)
leetcode
小王师傅662 小时前
LangChain 系统化学习入门
学习·langchain
dualven_in_csdn2 小时前
【网络安全】fscan的学习与使用
网络·学习
海绵宝宝转agent2 小时前
2026-10-14算法+面经总结
java·笔记·算法·缓存
虚无的纽扣2 小时前
【力扣刷题】第四天:三数之和、四数之和
算法·leetcode·职场和发展