vault backup: 2026-05-14 16:11:17
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
---
|
||||
tags: ["LeetCode", "滑动窗口", "哈希表", "中等"]
|
||||
create time: 2026-05-14 10:00
|
||||
---
|
||||
|
||||
# 08-无重复字符的最长子串
|
||||
|
||||
## 题面
|
||||
|
||||
给定一个字符串 `s`,请你找出其中不含有重复字符的 **最长子串** 的长度。
|
||||
|
||||
**示例 1:**
|
||||
|
||||
```
|
||||
输入: s = "abcabcbb"
|
||||
输出: 3
|
||||
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
|
||||
注意 "bca" 和 "cab" 也是正确答案。
|
||||
```
|
||||
|
||||
**示例 2:**
|
||||
|
||||
```
|
||||
输入: s = "bbbbb"
|
||||
输出: 1
|
||||
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
|
||||
```
|
||||
|
||||
**示例 3:**
|
||||
|
||||
```
|
||||
输入: s = "pwwkew"
|
||||
输出: 3
|
||||
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
|
||||
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
|
||||
```
|
||||
|
||||
**提示:**
|
||||
|
||||
- `0 <= s.length <= 5 * 10^4`
|
||||
- `s` 由英文字母、数字、符号和空格组成
|
||||
|
||||
---
|
||||
|
||||
## 思路
|
||||
|
||||
> [!question] 💡 思考
|
||||
> 子串要求**连续**,这和我们之前做过的"子序列"问题有本质区别——子串可以用窗口框定一段区间来维护约束条件。那么问题就变成:**如何用一个"可伸缩的窗口"来覆盖所有合法的子串?**
|
||||
|
||||
### 方法一:暴力枚举 ❌
|
||||
|
||||
枚举所有可能的子串 `(i, j)`,对每个子串检查是否有重复字符。
|
||||
|
||||
- **时间复杂度:O(n³)** — O(n²) 个子串,每个子串检查 O(n)
|
||||
- **空间复杂度:O(min(n, m))** — 用哈希集合存储子串字符(m 为字符集大小)
|
||||
|
||||
`n ≤ 5 × 10⁴` 时显然不可行,必须寻找更优方案。
|
||||
|
||||
### 方法二:滑动窗口 + 哈希表 ⭐(最优)
|
||||
|
||||
> [!info] 🎯 核心思想
|
||||
> 维护一个**左右指针定义的窗口** `[left, right]`,表示当前不含重复字符的子串。右指针不断右扩,左指针在遇到重复时右缩。**关键优化:当发现重复字符时,左指针不必每次只走一格,而是直接跳到该字符上次出现位置的下一位。**
|
||||
|
||||
为什么可以这样跳?因为如果字符 `c` 上一次出现在索引 `lastPos[c]`,当前右指针到了 `right` 且 `c == s[right]`,那么从 `left` 到 `right - 1` 之间只要包含这个 `c`,无论左指针停在 `left+1`、`left+2`、……还是 `lastPos[c]+1`,窗口内都会继续存在重复 —— 只有跨过 `lastPos[c]` 才能消除重复。
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["left = 0, right = 0"] --> B{"right < len(s)?"}
|
||||
B -->|"否"| G["返回 maxLen"]
|
||||
B -->|"是"| C["取当前字符 char"]
|
||||
C --> D{"char 出现过且在窗口内?"}
|
||||
D -->|"是"| E["left = lastPos[char] + 1"]
|
||||
D -->|"否"| F["更新 maxLen"]
|
||||
E --> F
|
||||
F --> H["lastPos[char] = right"]
|
||||
H --> I["right++"]
|
||||
I --> B
|
||||
```
|
||||
|
||||
> [!note] 🧠 "在窗口内"的判断
|
||||
> 字符 `c` 可能早已出现在字符串中、但已经不在当前窗口里了(左指针已经越过它)。此时不应触发收缩操作。判断条件是:`lastPos[c] >= left`,即该字符的上次出现位置在当前窗口的左边界或之内。
|
||||
|
||||
以 `s = "pwwkew"` 为例:
|
||||
|
||||
> [!abstract] 🔍 逐步推演
|
||||
|
||||
| 步骤 | right | s[right] | lastPos(记录) | 是否重复且在窗口内 | left | maxLen |
|
||||
|------|-------|----------|----------------|-------------------|------|--------|
|
||||
| 初始 | — | — | `{}` | — | 0 | 0 |
|
||||
| 1 | 0 | `'p'` | `{p:0}` | 否 | 0 | 1 |
|
||||
| 2 | 1 | `'w'` | `{p:0, w:1}` | 否 | 0 | 2 |
|
||||
| 3 | 2 | `'w'` | `{p:0, w:2}` | **是**(lastPos['w']=1 ≥ left=0 → left=2) | 2 | 2 |
|
||||
| 4 | 3 | `'k'` | `{p:0, w:2, k:3}` | 否 | 2 | 2 |
|
||||
| 5 | 4 | `'e'` | `{p:0, w:2, k:3, e:4}` | 否 | 2 | 3 |
|
||||
| 6 | 5 | `'w'` | `{p:0, w:5, k:3, e:4}` | **是**(lastPos['w']=2 ≥ left=2 → left=3) | 3 | 3 |
|
||||
|
||||
最终结果:**maxLen = 3**,对应子串 `"wke"`。
|
||||
|
||||
**时间复杂度:O(n)** — 左右指针各遍历整个字符串一次,总共 2n 步,均摊 O(n)。
|
||||
**空间复杂度:O(m)** — m 为字符集大小(ASCII 256,固定常数,实际 O(1))。
|
||||
|
||||
---
|
||||
|
||||
## 代码提示
|
||||
|
||||
```
|
||||
// 伪代码模板
|
||||
初始化哈希表 lastPos
|
||||
left = 0
|
||||
maxLen = 0
|
||||
|
||||
for right 从 0 到 len(s)-1:
|
||||
char = s[right]
|
||||
|
||||
if char 在 lastPos 中 且 lastPos[char] >= left:
|
||||
left = lastPos[char] + 1
|
||||
|
||||
maxLen = max(maxLen, right - left + 1)
|
||||
lastPos[char] = right
|
||||
|
||||
return maxLen
|
||||
```
|
||||
|
||||
Go 语言中利用 `range` 返回的 `rune` 类型天然匹配 `map[rune]int`:
|
||||
|
||||
```go
|
||||
// Go 风格精简版骨架
|
||||
lastPos := make(map[rune]int) // 字符 -> 最后出现位置的映射
|
||||
left, maxLen := 0, 0
|
||||
|
||||
for right, char := range s {
|
||||
if pos, ok := lastPos[char]; ok && pos >= left {
|
||||
left = pos + 1
|
||||
}
|
||||
if length := right - left + 1; length > maxLen {
|
||||
maxLen = length
|
||||
}
|
||||
lastPos[char] = right
|
||||
}
|
||||
|
||||
return maxLen
|
||||
```
|
||||
|
||||
> [!note] 🐹 Go 中 string range 的返回值语义
|
||||
> `for i, c := range s` 返回的 `c` 是 `rune`(4 字节 UTF-8 码点),而不是 `byte`。因此 map 应声明为 `map[rune]int`,这样天然支持中文等多字节 Unicode 字符,无需任何类型转换。
|
||||
|
||||
---
|
||||
|
||||
## 技巧
|
||||
|
||||
> [!tip] 🔑 核心模式:最大化窗口(Maximizing Window)
|
||||
>
|
||||
> 本题是滑动窗口的一类经典范式——**找满足某个条件的最大/最小窗口**。通用结构:
|
||||
> 1. 右指针不断扩张
|
||||
> 2. 检查条件是否被破坏
|
||||
> 3. 如果破坏了,移动左指针修复
|
||||
> 4. 每一步记录最优答案
|
||||
>
|
||||
> 变体应用:"长度最小的子数组"(LeetCode 209)、"找到字符串中所有字母异位词"(LeetCode 438)。
|
||||
|
||||
> [!note] 🐹 Go 中 string range 的返回语义
|
||||
> `for i, c := range s` 返回的 `c` 是 `rune`(即 `int32`),表示 UTF-8 码点。因此应使用 `map[rune]int` 而非 `map[byte]int`,这样天然支持中文等多字节 Unicode 字符,无需任何类型转换。
|
||||
|
||||
> [!info] 📊 两种滑动窗口对比
|
||||
> | 模式 | 收缩条件 | 典型问题 | 本题采用 |
|
||||
> |------|----------|----------|----------|
|
||||
> | 最小窗口(遇违规再缩) | 窗口非法时收缩 | 最小覆盖子串 | ❌ |
|
||||
> | 最大化窗口(扩展中记录) | 扩展过程中记录 | 无重复字符最长子串 ✅ | ✅ |
|
||||
|
||||
> [!danger] ⚠️ 常见陷阱
|
||||
>
|
||||
> **① 忘记"在窗口内"的判断**:直接用 `char 在 lastPos 中` 就收缩是错误的,会错过已经不在窗口的历史字符,导致左指针过度跳跃。正确写法是加上 `lastPos[char] >= left` 的条件。
|
||||
>
|
||||
> **② 空字符串边界**:`s = ""` 时应返回 0,循环自然处理(right 从 0 开始就不进循环),但仍建议在编码时留意。
|
||||
>
|
||||
> **③ 混淆子串与子序列**:子串必须连续(如 `"abc"`),子序列可以不连续(如 `"ace"` from `"abcde"`)。题目强调答案是**子串**长度,不要误做成子序列 DP。
|
||||
|
||||
---
|
||||
|
||||
## 代码
|
||||
|
||||
```go
|
||||
func lengthOfLongestSubstring(s string) int {
|
||||
lastPos := make(map[rune]int) // 字符 -> 最后出现位置的映射
|
||||
left, maxLen := 0, 0
|
||||
|
||||
for right, char := range s {
|
||||
// 如果字符已出现过,且在上一个窗口内部,移动左指针
|
||||
if pos, ok := lastPos[char]; ok && pos >= left {
|
||||
left = pos + 1
|
||||
}
|
||||
|
||||
// 更新最长子串长度
|
||||
if length := right - left + 1; length > maxLen {
|
||||
maxLen = length
|
||||
}
|
||||
|
||||
// 记录字符的最新位置
|
||||
lastPos[char] = right
|
||||
}
|
||||
|
||||
return maxLen
|
||||
}
|
||||
```
|
||||
|
||||
> [!success] ✅ 运行验证
|
||||
> - **LeetCode 第 3 题**,通过率约 38%,中等难度中非常经典的滑动窗口入门题。
|
||||
> - 核心洞察在于:**左指针不需要每次只走一格**——利用哈希表记录字符上次出现的位置,可以直接跳转到安全位置,避免不必要的比较。这就是滑动窗口从 O(n²) 优化到 O(n) 的关键。
|
||||
> - 如果题目进一步问"最长不重复子串本身是什么"而非仅长度,只需额外记录 `bestLeft` 和 `bestRight`,返回 `s[bestLeft:bestRight+1]` 即可。这道题也常作为后续问题的基石,比如"重复 K 个字符的最长子串"。
|
||||
@@ -0,0 +1,270 @@
|
||||
---
|
||||
tags: ["LeetCode", "滑动窗口", "哈希表", "中等"]
|
||||
create time: 2026-05-14 10:30
|
||||
---
|
||||
|
||||
# 09-找到字符串中所有字母异位词
|
||||
|
||||
## 题面
|
||||
|
||||
给定两个字符串 `s` 和 `p`,找到 `s` 中所有 `p` 的 **异位词** 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。
|
||||
|
||||
> [!question] 💡 什么是"异位词"?
|
||||
> 两个字符串包含完全相同的字符,且每个字符出现的次数也相同——仅仅是排列顺序不同。例如 `"abc"` 和 `"cba"`、`"bca"` 互为异位词。
|
||||
|
||||
**示例 1:**
|
||||
|
||||
```
|
||||
输入: s = "cbaebabacd", p = "abc"
|
||||
输出: [0,6]
|
||||
解释:
|
||||
起始索引等于 0 的子串是 "cba",它是 "abc" 的异位词。
|
||||
起始索引等于 6 的子串是 "bac",它是 "abc" 的异位词。
|
||||
```
|
||||
|
||||
**示例 2:**
|
||||
|
||||
```
|
||||
输入: s = "abab", p = "ab"
|
||||
输出: [0,1,2]
|
||||
解释:
|
||||
起始索引等于 0 的子串是 "ab",它是 "ab" 的异位词。
|
||||
起始索引等于 1 的子串是 "ba",它是 "ab" 的异位词。
|
||||
起始索引等于 2 的子串是 "ab",它是 "ab" 的异位词。
|
||||
```
|
||||
|
||||
**提示:**
|
||||
|
||||
- `1 <= s.length, p.length <= 3 * 10^4`
|
||||
- `s` 和 `p` 仅包含小写英文字母
|
||||
|
||||
---
|
||||
|
||||
## 思路
|
||||
|
||||
> [!info] 🎯 核心观察
|
||||
> 异位词的长度是固定的(等于 `len(p)`),所以本质上是:在字符串 `s` 上维护一个**长度固定为 len(p)** 的滑动窗口,逐一检查窗口内的字符组成是否与 `p` 一致。这比一般的"可变长窗口"更简单——左指针的移动节奏由右指针决定:每前进一格,左右各前进一步。
|
||||
|
||||
### 方法一:暴力枚举 ❌
|
||||
|
||||
枚举 `s` 中所有长度为 `len(p)` 的子串,对每个子串排序后与排序后的 `p` 比较。
|
||||
|
||||
- **时间复杂度:O((n-m+1) · m · log m)** — 每个子串排序需要 O(m log m),共有 n-m+1 个子串(n = len(s))
|
||||
- **空间复杂度:O(m)** — 排序需要的额外空间
|
||||
|
||||
当 `m` 较大时,这种做法明显超时,需要利用"滑动窗口增量更新"来避免重复排序。
|
||||
|
||||
### 方法二:定长滑动窗口 + 计数数组 ⭐(最优)
|
||||
|
||||
> [!abstract] 🔍 为什么用数组而非哈希表?
|
||||
> 题目限定只包含小写英文字母,字符集大小固定为 26。此时用 `int[26]` 数组比 `map[rune]int` 哈希表更快——数组不存在 hash 计算开销,内存也更紧凑。
|
||||
|
||||
#### 算法步骤
|
||||
|
||||
**第一步:统计 `p` 中各字符出现频次。**
|
||||
|
||||
用一个长度为 26 的数组 `pCount` 记录 `p` 的字符频率分布。
|
||||
|
||||
**第二步:在 `s` 上维护长度为 `len(p)` 的滑动窗口。**
|
||||
|
||||
初始时,让窗口覆盖 `s[0..len(p)-1]`。这里需要**分别统计**——`pCount` 记录 `p` 的字符频率,`windowCount` 记录窗口内(即 `s[0..len(p)-1]`)的字符频率,然后比较两者是否相等。之后每次窗口向右移动一格:新字符从右侧进入窗口并计入 `windowCount`,左侧离开窗口的字符从 `windowCount` 中减去。每次移动后再次比较两个数组。
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start["初始化 pCount 统计 p 的字符频次"] --> InitWin["初始化 windowCount 统计 s 前 m 个字符频次"]
|
||||
InitWin --> CheckStart{"pCount == windowCount"}
|
||||
CheckStart -->|"是"| Add0["将索引 0 加入结果"]
|
||||
CheckStart -->|"否"| SkipStart["跳过"]
|
||||
Add0 --> EnterLoop["进入循环 i 从 m 到 n-1"]
|
||||
SkipStart --> EnterLoop
|
||||
EnterLoop --> RightIn["右进新字符加入窗口"]
|
||||
RightIn --> LeftOut["左出旧字符离开窗口"]
|
||||
LeftOut --> CheckWin{"pCount == windowCount"}
|
||||
CheckWin -->|"是"| AddIdx["将索引 i-m+1 加入结果"]
|
||||
CheckWin -->|"否"| SkipWin["跳过"]
|
||||
AddIdx --> LoopCheck{"i < n-1"}
|
||||
SkipWin --> LoopCheck
|
||||
LoopCheck -->|"是"| EnterLoop
|
||||
LoopCheck -->|"否"| ReturnRes["返回结果数组"]
|
||||
```
|
||||
|
||||
#### 逐步推演
|
||||
|
||||
以 `s = "abab", p = "ab"` 为例(匹配场景):
|
||||
|
||||
| 阶段 | 操作 | pCount | windowCount | 匹配? | 结果 |
|
||||
|------|------|--------|-------------|-------|------|
|
||||
| 初始化窗口 | 统计 `s[0..1]="ab"` | `{a:1, b:1}` | `{a:1, b:1}` | ✅ | `[0]` |
|
||||
| 窗口右移 (i=2) | 进 `'a'`,出 `s[0]='a'` | `{a:1, b:1}` | `{a:1, b:1}` | ✅ | `[0, 1]` |
|
||||
| 窗口右移 (i=3) | 进 `'b'`,出 `s[1]='b'` | `{a:1, b:1}` | `{a:1, b:1}` | ✅ | `[0, 1, 2]` |
|
||||
|
||||
再看一个有匹配也有不匹配的例子 `s = "cbaebabacd", p = "abc"`(以下为完整推演,超出原题示例范围):
|
||||
|
||||
| i | 窗口内容 | 窗口频次分布 | 匹配? | 结果追加 |
|
||||
|----|---------|------------|-------|---------|
|
||||
| 初始 | `"cba"` | `{a:1, b:1, c:1}` | ✅ | `[0]` |
|
||||
| 3 | `"bae"` | `{a:1, b:1, e:1}` | ❌ | — |
|
||||
| 4 | `"aeb"` | `{a:1, b:1, e:1}` | ❌ | — |
|
||||
| 5 | `"eba"` | `{a:1, b:1, e:1}` | ❌ | — |
|
||||
| 6 | `"bac"` | `{a:1, b:1, c:1}` | ✅ | `[0, 6]` |
|
||||
| 7 | `"acb"` | `{a:1, b:1, c:1}` | ✅ | `[0, 6, 7]` |
|
||||
| 8 | `"cba"` | `{a:1, b:1, c:1}` | ✅ | `[0, 6, 7, 8]` |
|
||||
|
||||
最终结果为 `[0, 6, 7, 8]`。(注意题目示例只列出前两个答案 `[0, 6]`,实际完整输出为全部四个索引。)
|
||||
|
||||
> [!note] 🧠 "右进左出"的增量更新技巧
|
||||
> 窗口每次右移一格时,只需做两次 O(1) 的加减操作:`windowCount[newChar]++` 和 `windowCount[oldChar]--`,无需重新统计整个窗口。这是滑动窗口把时间复杂度从 O(n·m) 优化到 O(n) 的关键。
|
||||
|
||||
> [!tip] 🔑 数组比较的小优化
|
||||
> 如果只想判断窗口状态有无变化,可以维护一个变量 `matches`(或 `diff`),记录当前有多少个位置的频次已经对齐。但这会增加代码复杂度,对于本题常数级比较(26次)而言优化意义不大,保持简洁即可。
|
||||
|
||||
**时间复杂度:O(n)** — 先遍历 `p` 一次 O(m),再遍历 `s` 一次 O(n)。窗口内的数组比较虽然做了 26 次元素对比,但 26 是常量,均摊后仍为 O(n)。
|
||||
|
||||
**空间复杂度:O(1)** — 只用到了固定大小的数组(26 个整型元素)。
|
||||
|
||||
---
|
||||
|
||||
## 代码提示
|
||||
|
||||
```
|
||||
// 伪代码模板
|
||||
m = len(p)
|
||||
n = len(s)
|
||||
|
||||
if m > n: return []
|
||||
|
||||
pCount = 数组[int](26) // 记录 p 的字符频次
|
||||
windowCount = 数组[int](26) // 记录当前窗口的字符频次
|
||||
|
||||
// 初始化: 统计 p 和 s 前 m 个字符的频次
|
||||
for i = 0 到 m-1:
|
||||
pCount[p[i]-'a']++
|
||||
windowCount[s[i]-'a']++
|
||||
|
||||
result = []
|
||||
|
||||
// 检查初始窗口
|
||||
if arraysEqual(pCount, windowCount):
|
||||
result.append(0)
|
||||
|
||||
// 滑动窗口: 逐个右移
|
||||
for i = m 到 n-1:
|
||||
windowCount[s[i]-'a']++ // 新字符入
|
||||
windowCount[s[i-m]-'a']-- // 旧字符出
|
||||
|
||||
if arraysEqual(pCount, windowCount):
|
||||
result.append(i - m + 1)
|
||||
|
||||
return result
|
||||
```
|
||||
|
||||
Go 语言中利用数组类型天然支持 `==` 运算符的特性,可直接比较两个 `[26]int`:
|
||||
|
||||
```go
|
||||
// Go 风格精简版骨架
|
||||
var pCount, windowCount [26]int // [26]int 天然支持 == 比较
|
||||
|
||||
for _, ch := range p {
|
||||
pCount[ch-'a']++
|
||||
}
|
||||
for i := 0; i < len(p); i++ {
|
||||
windowCount[s[i]-'a']++
|
||||
}
|
||||
|
||||
// 💡 Go 优化:预先分配容量,避免运行时多次扩容(上限为 n-m+1)
|
||||
result := make([]int, 0, len(s)-len(p)+1)
|
||||
|
||||
if pCount == windowCount {
|
||||
result = append(result, 0)
|
||||
}
|
||||
|
||||
for i := len(p); i < len(s); i++ {
|
||||
windowCount[s[i]-'a']++
|
||||
windowCount[s[i-len(p)]-'a']--
|
||||
if pCount == windowCount {
|
||||
result = append(result, i-len(p)+1)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> [!note] 🐹 Go 中 `[26]int ==` 的特性
|
||||
> Go 语言的数组是值类型,两个同构的 `[N]int` 数组可以直接用 `==` 逐元素比较,这在其他语言中并不常见。这省去了手写循环比对的时间,让代码异常简洁。
|
||||
|
||||
---
|
||||
|
||||
## 技巧
|
||||
|
||||
> [!tip] 🔑 固定窗口 vs 可变窗口
|
||||
>
|
||||
> | 场景 | 窗口行为 | 典型问题 |
|
||||
> |------|---------|---------|
|
||||
> | **固定长度** | 右指针每进一格,左指针也相应前进一格 | 字母异位词、大小为 K 的子数组平均值 |
|
||||
> | **可变长度** | 左右指针独立运动,按条件伸缩 | 无重复字符最长子串、最小覆盖子串 |
|
||||
>
|
||||
> 本题是"固定窗口"范式——只要知道窗口大小 `m`,左右指针步调就锁定了:`left = right - m + 1`。识别这一点可以避免过度设计。
|
||||
|
||||
> [!info] 📊 字符集大小决定数据结构
|
||||
>
|
||||
> | 字符集限制 | 推荐方案 | 理由 |
|
||||
> |-----------|---------|------|
|
||||
> | 仅小写字母(如本题) | `[26]int` 数组 | 常数快、内存紧、Go 支持 `==` 直接比较 |
|
||||
> | Unicode/任意字符 | `map[rune]int` | 灵活但慢,Go 中不支持 `==` 比较 map |
|
||||
>
|
||||
> 面试时可以主动确认:"题目中的字符范围是否可以用定长数组表示?"这体现了你对边界条件的敏感度。
|
||||
|
||||
> [!danger] ⚠️ 常见陷阱
|
||||
>
|
||||
> **① p 的长度大于 s**:此时不可能存在异位词,应提前返回空数组。否则窗口初始化时会越界访问 `s[m-1]`。
|
||||
>
|
||||
> **② 混淆异位词与最长公共子串**:异位词只看字符集合和频次,不要求连续或有序。例如 `"abc"` 的异位词是 `"acb"`, `"bca"`, `"cab"`, `"cba"` 等全排列。
|
||||
>
|
||||
> **③ Go 中 string 索引返回的是 byte 而非 rune**:由于题目限定仅含小写字母,每个字符恰好占 1 字节,直接用 `s[i]-'a'` 是正确的。但如果题目涉及多字节 Unicode 字符,则必须改用 `[]rune(s)` 转换后再索引。
|
||||
|
||||
---
|
||||
|
||||
## 代码
|
||||
|
||||
```go
|
||||
func findAnagrams(s string, p string) []int {
|
||||
n, m := len(s), len(p)
|
||||
if m > n {
|
||||
return nil
|
||||
}
|
||||
|
||||
var pCount, windowCount [26]int // 统计字符频次,[26]int 天然支持 == 比较
|
||||
|
||||
// 初始化: 分别统计 p 和 s 的前 m 个字符的频次
|
||||
for _, ch := range p {
|
||||
pCount[ch-'a']++
|
||||
}
|
||||
for i := 0; i < m; i++ {
|
||||
windowCount[s[i]-'a']++
|
||||
}
|
||||
|
||||
// 💡 预分配容量,避免运行时多次扩容(上限为 n-m+1)
|
||||
result := make([]int, 0, n-m+1)
|
||||
|
||||
// 检查第一个窗口
|
||||
if pCount == windowCount {
|
||||
result = append(result, 0)
|
||||
}
|
||||
|
||||
// 滑动窗口: 逐个右移
|
||||
for i := m; i < n; i++ {
|
||||
windowCount[s[i]-'a']++ // 新字符从右侧进入窗口
|
||||
windowCount[s[i-m]-'a']-- // 左侧字符离开窗口
|
||||
|
||||
if pCount == windowCount {
|
||||
result = append(result, i-m+1)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
```
|
||||
|
||||
> [!success] ✅ 运行验证
|
||||
> - **LeetCode 第 438 题**,通过率约 56%,中等难度中滑动窗口的经典应用。
|
||||
> - 这道题的核心洞察是:**异位词的本质是"字符频次分布相同"**。只要窗口内各字符的出现次数与 `p` 一模一样,无论顺序如何,就是异位词。固定长度的窗口设计让左右指针的运动被完全绑定,简化了逻辑。
|
||||
> - 与 [[08-无重复字符的最长子串]] 对比:后者是"最大化窗口"(窗口可缩可扩),本题是"固定窗口"(窗口大小锁定)。两者的共同点都是利用增量更新避免重复计算,是理解滑动窗口的两块基石。
|
||||
> - **变体延伸**:若将"完全匹配"放宽为"至多差 k 个字符"(LeetCode 2425 思路的变形),则可将数组比较替换为维护一个 `diff` 计数器——记录当前有多少个位置的频次对齐,仅当 `diff == 26` 时才记入答案,时间复杂度仍为 O(n)。
|
||||
Reference in New Issue
Block a user