

文章目录
摘要
这题本质是一个非常经典的"滑动窗口"问题:给你两个字符串 s 和 p,要你在 s 里面找到所有 p 的字母异位词的子串,并给出它们的起始位置。
很多同学第一次做会直接想到"排序后对比",但那一套不仅效率低,而且在大数据量输入下会直接 TLE。真正高效的做法,是维护两个字母频次数组,然后用滑动窗口"一格一格地移动",顺便实时对比即可。
文章将带你完整理解滑动窗口在字符串处理中的优雅之处,并附上可运行的 Swift Demo。

描述
题目要求的是找到 s 中所有字母组成和 p 完全一样的子串。比如:
txt
s = "cbaebabacd", p = "abc"
```txt
你要找的不是字典顺序是否一致,而是字母出现次数是否一致,也就是"异位词"。
例如:
* "cba" 是 "abc" 的异位词
* "bac" 也是 "abc" 的异位词
* "cab" 也是
所以只要字符数量组合一致,这个子串就算匹配。
## 题解答案
最优解法:**滑动窗口 + 字母频次统计**。
我们使用两个长度为 26 的数组:
* `need[26]`:表示字符串 `p` 中每个字母的数量
* `window[26]`:表示当前窗口中每个字母的数量
窗口的大小固定为 `p.count`,窗口从左到右滑动,每滑动一格:
* 移除窗口左端的字符
* 添加窗口右端的字符
* 对比两个数组是否完全一致,如果一致就说明此窗口是 p 的异位词
这种方式的时间复杂度是 O(n),非常高效。

## 题解代码分析
下面是完整、可直接运行的 Swift Demo 代码,包括测试入口:
```swift
import Foundation
class Solution {
func findAnagrams(_ s: String, _ p: String) -> [Int] {
let sArr = Array(s), pArr = Array(p)
let sCount = sArr.count, pCount = pArr.count
if sCount < pCount { return [] }
// 记录 p 的字母频次
var need = [Int](repeating: 0, count: 26)
var window = [Int](repeating: 0, count: 26)
for ch in pArr {
need[Int(ch.asciiValue! - Character("a").asciiValue!)] += 1
}
var result: [Int] = []
// 初始化第一个窗口
for i in 0..<pCount {
let idx = Int(sArr[i].asciiValue! - Character("a").asciiValue!)
window[idx] += 1
}
// 判断第一个窗口是否匹配
if window == need {
result.append(0)
}
// 来回滑动窗口
for i in pCount..<sCount {
// 右边进来一个
let rightIndex = Int(sArr[i].asciiValue! - Character("a").asciiValue!)
window[rightIndex] += 1
// 左边出去一个
let leftIndex = Int(sArr[i - pCount].asciiValue! - Character("a").asciiValue!)
window[leftIndex] -= 1
// 对比两个频次数组是否一致
if window == need {
result.append(i - pCount + 1)
}
}
return result
}
}
// MARK: - Demo 运行
func demo() {
let solution = Solution()
let s = "cbaebabacd"
let p = "abc"
let result = solution.findAnagrams(s, p)
print("异位词起始索引:", result)
}
demo()
```txt
## 题解代码详解
我们来把关键点细细拆开。
### 1. 为什么要用数组而不是字典?
因为只有小写字母,也就是固定 26 个字符。
直接用 `[Int](repeating: 0, count: 26)` 可以做到:
* 定长数组
* 下标访问 O(1)
* 内存布局连续,速度快
这是最适合字符串计数的方式。
### 2. 为什么滑动窗口窗口大小固定?
因为异位词的长度必须跟 `p` 一样。
窗口长度就是 `p.count`,每移动一步:
```txt
- 去掉窗口最左端的字符
- 增加窗口最右端的字符
```txt
既不用重新统计整个窗口,也不用排序子串,始终 O(1) 调整。
### 3. 为什么 window == need 就能判断是异位词?
因为频次数组完全一致,说明窗口里的每个字母数量与 `p` 完全相同。
比如:
need = [a:1, b:1, c:1]
window = [b:1, a:1, c:1]
顺序不重要,本质是出现次数一样。
### 4. 实际开发中滑动窗口有什么用?
这种模式在很多后端、前端甚至移动端业务中都很常用,例如:
* 日志中查找某段频次模式
* 监控系统里查找某段异常行为出现次数一致的时段
* 文本分析场景:查找固定词频匹配段
* 游戏服务器判断玩家是否在短时间内出现特定操作序列(反作弊)
滑动窗口的核心价值是:
**你找的是"连续区间"的特征,而不是任意组合。**
只要是连续、固定长度的检查,滑动窗口永远能省下大量重复计算。
## 示例测试及结果
### 示例 1
```txt
输入:
s = "cbaebabacd"
p = "abc"
输出:
[0, 6]
```txt
解释:
* s[0...2] = "cba" → 是异位词
* s[6...8] = "bac" → 是异位词
运行 Demo 输出:
```txt
异位词起始索引: [0, 6]
```txt
### 示例 2
```txt
s = "abab"
p = "ab"
输出:
[0, 1, 2]
```txt
窗口过程如下:
* "ab" → ✔
* "ba" → ✔
* "ab" → ✔
结果非常符合预期。
## 时间复杂度
算法只遍历一次 `s`:
```txt
O(n)
```txt
窗口更新 + 数组下标访问都是 O(1),不会额外增加复杂度。
## 空间复杂度
只使用 2 个长度 26 的数组:
```txt
O(1)
```txt
这是典型的常数级空间,非常经济。
## 总结
这题是滑动窗口 + 频次统计的最佳练习题之一。重点不是代码技巧,而是掌握一种通用模式:
* 固定长度窗口
* 滑动更新频次
* 实时判断是否匹配