LeetCode 438 - 找到字符串中所有字母异位词


文章目录

摘要

这题本质是一个非常经典的"滑动窗口"问题:给你两个字符串 sp,要你在 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),非常高效。
![](https://i-blog.csdnimg.cn/direct/a0d338fa17054066953ec43bb8f178a1.png)
## 题解代码分析

下面是完整、可直接运行的 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

这是典型的常数级空间,非常经济。

## 总结

这题是滑动窗口 + 频次统计的最佳练习题之一。重点不是代码技巧,而是掌握一种通用模式:

* 固定长度窗口
* 滑动更新频次
* 实时判断是否匹配
相关推荐
ZPC82106 分钟前
【无标题】
人工智能·pytorch·算法·机器人
2301_764441339 分钟前
使用python构建的STAR实验ΛΛ̄自旋关联完整仿真
开发语言·python·算法
Rainy Blue88312 分钟前
前缀和与差分(蓝桥杯高频考点)
数据结构·算法·蓝桥杯
Dfreedom.12 分钟前
机器学习经典算法全景解析与演进脉络(无监督学习篇)
人工智能·学习·算法·机器学习·无监督学习
kaoshi100app18 分钟前
本周,河南二建报名公布!
开发语言·人工智能·职场和发展·学习方法
421!18 分钟前
ESP32学习笔记之GPIO
开发语言·笔记·单片机·嵌入式硬件·学习·算法·fpga开发
智算菩萨26 分钟前
【How Far Are We From AGI】4 AGI的“生理系统“——从算法架构到算力基座的工程革命
论文阅读·人工智能·深度学习·算法·ai·架构·agi
福赖29 分钟前
《算法:生产车间》
算法
alphaTao37 分钟前
LeetCode 每日一题 2026/3/16-2026/3/22
linux·windows·leetcode
空空潍37 分钟前
LeetCode力扣 hot100一刷完结
算法·leetcode