vault backup: 2026-05-14 16:40:36

This commit is contained in:
2026-05-14 16:40:36 +08:00
parent 1a9283a40f
commit e2bd06d982
@@ -49,10 +49,10 @@ create time: 2026-05-14 10:30
枚举 `s` 中所有长度为 `len(p)` 的子串,对每个子串排序后与排序后的 `p` 比较。
- **时间复杂度:O((n-m+1) · m · log m)** — 每个子串排序需要 O(m log m),共有 n-m+1 个子串(n = len(s))
- **空间复杂度:O(m)** — 排序需要的额外空间
- **时间复杂度:O((m-n+1) · n · log n)** — 每个子串排序需要 O(n log n),共有 m-n+1 个子串(m = len(s))
- **空间复杂度:O(n)** — 排序需要的额外空间
当 `m` 较大时,这种做法明显超时,需要利用"滑动窗口增量更新"来避免重复排序。
当 `n` 较大时,这种做法明显超时,需要利用"滑动窗口增量更新"来避免重复排序。
### 方法二:定长滑动窗口 + 计数数组 ⭐(最优)
@@ -71,18 +71,18 @@ create time: 2026-05-14 10:30
```mermaid
flowchart TD
Start["初始化 pCount 统计 p 的字符频次"] --> InitWin["初始化 windowCount 统计 s 前 m 个字符频次"]
Start["初始化 pCount 统计 p 的字符频次"] --> InitWin["初始化 windowCount 统计 s 前 n 个字符频次"]
InitWin --> CheckStart{"pCount == windowCount"}
CheckStart -->|"是"| Add0["将索引 0 加入结果"]
CheckStart -->|"否"| SkipStart["跳过"]
Add0 --> EnterLoop["进入循环 i 从 m 到 n-1"]
Add0 --> EnterLoop["进入循环 i 从 n 到 m-1"]
SkipStart --> EnterLoop
EnterLoop --> RightIn["右进新字符加入窗口"]
RightIn --> LeftOut["左出旧字符离开窗口"]
LeftOut --> CheckWin{"pCount == windowCount"}
CheckWin -->|"是"| AddIdx["将索引 i-m+1 加入结果"]
CheckWin -->|"是"| AddIdx["将索引 i-n+1 加入结果"]
CheckWin -->|"否"| SkipWin["跳过"]
AddIdx --> LoopCheck{"i < n-1"}
AddIdx --> LoopCheck{"i < m"}
SkipWin --> LoopCheck
LoopCheck -->|"是"| EnterLoop
LoopCheck -->|"否"| ReturnRes["返回结果数组"]
@@ -113,12 +113,12 @@ flowchart TD
最终结果为 `[0, 6, 7, 8]`。(注意题目示例只列出前两个答案 `[0, 6]`,实际完整输出为全部四个索引。)
> [!note] 🧠 "右进左出"的增量更新技巧
> 窗口每次右移一格时,只需做两次 O(1) 的加减操作:`windowCount[newChar]++` 和 `windowCount[oldChar]--`,无需重新统计整个窗口。这是滑动窗口把时间复杂度从 O(n·m) 优化到 O(n) 的关键。
> 窗口每次右移一格时,只需做两次 O(1) 的加减操作:`windowCount[newChar]++` 和 `windowCount[oldChar]--`,无需重新统计整个窗口。这是滑动窗口把时间复杂度从 O(m·n) 优化到 O(m) 的关键。
> [!tip] 🔑 数组比较的小优化
> 如果只想判断窗口状态有无变化,可以维护一个变量 `matches`(或 `diff`),记录当前有多少个位置的频次已经对齐。但这会增加代码复杂度,对于本题常数级比较(26次)而言优化意义不大,保持简洁即可。
**时间复杂度:O(n)** — 先遍历 `p` 一次 O(m),再遍历 `s` 一次 O(n)。窗口内的数组比较虽然做了 26 次元素对比,但 26 是常量,均摊后仍为 O(n)。
**时间复杂度:O(m)** — 先遍历 `p` 一次 O(n),再遍历 `s` 一次 O(m)。窗口内的数组比较虽然做了 26 次元素对比,但 26 是常量,均摊后仍为 O(m)。
**空间复杂度:O(1)** — 只用到了固定大小的数组(26 个整型元素)。
@@ -128,16 +128,16 @@ flowchart TD
```
// 伪代码模板
m = len(p)
n = len(s)
m = len(s)
n = len(p)
if m > n: return []
if n > m: return []
pCount = 数组[int](26) // 记录 p 的字符频次
windowCount = 数组[int](26) // 记录当前窗口的字符频次
// 初始化: 统计 p 和 s 前 m 个字符的频次
for i = 0 到 m-1:
// 初始化: 统计 p 和 s 前 n 个字符的频次
for i = 0 到 n-1:
pCount[p[i]-'a']++
windowCount[s[i]-'a']++
@@ -148,12 +148,12 @@ if arraysEqual(pCount, windowCount):
result.append(0)
// 滑动窗口: 逐个右移
for i = m 到 n-1:
for i = n 到 m-1:
windowCount[s[i]-'a']++ // 新字符入
windowCount[s[i-m]-'a']-- // 旧字符出
windowCount[s[i-n]-'a']-- // 旧字符出
if arraysEqual(pCount, windowCount):
result.append(i - m + 1)
result.append(i - n + 1)
return result
```
@@ -171,7 +171,7 @@ for i := 0; i < len(p); i++ {
windowCount[s[i]-'a']++
}
// 💡 Go 优化:预先分配容量,避免运行时多次扩容(上限为 n-m+1)
// 💡 Go 优化:预先分配容量,避免运行时多次扩容(上限为 m-n+1)
result := make([]int, 0, len(s)-len(p)+1)
if pCount == windowCount {
@@ -201,7 +201,7 @@ for i := len(p); i < len(s); i++ {
> | **固定长度** | 右指针每进一格,左指针也相应前进一格 | 字母异位词、大小为 K 的子数组平均值 |
> | **可变长度** | 左右指针独立运动,按条件伸缩 | 无重复字符最长子串、最小覆盖子串 |
>
> 本题是"固定窗口"范式——只要知道窗口大小 `m`,左右指针步调就锁定了:`left = right - m + 1`。识别这一点可以避免过度设计。
> 本题是"固定窗口"范式——只要知道窗口大小 `n`,左右指针步调就锁定了:`left = right - n + 1`。识别这一点可以避免过度设计。
> [!info] 📊 字符集大小决定数据结构
>
@@ -214,7 +214,7 @@ for i := len(p); i < len(s); i++ {
> [!danger] ⚠️ 常见陷阱
>
> **① p 的长度大于 s**:此时不可能存在异位词,应提前返回空数组。否则窗口初始化时会越界访问 `s[m-1]`。
> **① p 的长度大于 s**:此时不可能存在异位词,应提前返回空数组。否则窗口初始化时会越界访问 `s[n-1]`。
>
> **② 混淆异位词与最长公共子串**:异位词只看字符集合和频次,不要求连续或有序。例如 `"abc"` 的异位词是 `"acb"`, `"bca"`, `"cab"`, `"cba"` 等全排列。
>
@@ -226,23 +226,23 @@ for i := len(p); i < len(s); i++ {
```go
func findAnagrams(s string, p string) []int {
n, m := len(s), len(p)
if m > n {
m, n := len(s), len(p)
if n > m {
return nil
}
var pCount, windowCount [26]int // 统计字符频次,[26]int 天然支持 == 比较
// 初始化: 分别统计 p 和 s 的前 m 个字符的频次
// 初始化: 分别统计 p 和 s 的前 n 个字符的频次
for _, ch := range p {
pCount[ch-'a']++
}
for i := 0; i < m; i++ {
for i := 0; i < n; i++ {
windowCount[s[i]-'a']++
}
// 💡 预分配容量,避免运行时多次扩容(上限为 n-m+1)
result := make([]int, 0, n-m+1)
// 💡 预分配容量,避免运行时多次扩容(上限为 m-n+1)
result := make([]int, 0, m-n+1)
// 检查第一个窗口
if pCount == windowCount {
@@ -250,12 +250,12 @@ func findAnagrams(s string, p string) []int {
}
// 滑动窗口: 逐个右移
for i := m; i < n; i++ {
for i := n; i < m; i++ {
windowCount[s[i]-'a']++ // 新字符从右侧进入窗口
windowCount[s[i-m]-'a']-- // 左侧字符离开窗口
windowCount[s[i-n]-'a']-- // 左侧字符离开窗口
if pCount == windowCount {
result = append(result, i-m+1)
result = append(result, i-n+1)
}
}
@@ -267,4 +267,4 @@ func findAnagrams(s string, p string) []int {
> - **LeetCode 第 438 题**,通过率约 56%,中等难度中滑动窗口的经典应用。
> - 这道题的核心洞察是:**异位词的本质是"字符频次分布相同"**。只要窗口内各字符的出现次数与 `p` 一模一样,无论顺序如何,就是异位词。固定长度的窗口设计让左右指针的运动被完全绑定,简化了逻辑。
> - 与 [[08-无重复字符的最长子串]] 对比:后者是"最大化窗口"(窗口可缩可扩),本题是"固定窗口"(窗口大小锁定)。两者的共同点都是利用增量更新避免重复计算,是理解滑动窗口的两块基石。
> - **变体延伸**:若将"完全匹配"放宽为"至多差 k 个字符"(LeetCode 2425 思路的变形),则可将数组比较替换为维护一个 `diff` 计数器——记录当前有多少个位置的频次对齐,仅当 `diff == 26` 时才记入答案,时间复杂度仍为 O(n)。
> - **变体延伸**:若将"完全匹配"放宽为"至多差 k 个字符"(LeetCode 2425 思路的变形),则可将数组比较替换为维护一个 `diff` 计数器——记录当前有多少个位置的频次对齐,仅当 `diff == 26` 时才记入答案,时间复杂度仍为 O(m)。