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

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

## 总结

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

* 固定长度窗口
* 滑动更新频次
* 实时判断是否匹配
相关推荐
智驱力人工智能14 小时前
仓库园区无人机烟雾识别:构建立体化、智能化的早期火灾预警体系 无人机烟雾检测 无人机动态烟雾分析AI系统 无人机辅助火灾救援系统
人工智能·opencv·算法·目标检测·架构·无人机·边缘计算
Christo314 小时前
2022-《Deep Clustering: A Comprehensive Survey》
人工智能·算法·机器学习·数据挖掘
天真小巫15 小时前
2025.12.22总结(边工作,边强大)
职场和发展
Yzzz-F15 小时前
牛客周赛round123 G小红出千[补题][滑动窗口]
算法
肆悟先生15 小时前
3.16 含有可变参数的函数
c++·算法
步步为营DotNet15 小时前
深度解析.NET中属性(Property)的幕后机制:优化数据访问与封装
java·算法·.net
Swift社区15 小时前
LeetCode 454 - 四数相加 II
java·算法·leetcode
tokepson15 小时前
反向传播
深度学习·算法·ai·反向传播
Xの哲學15 小时前
Linux AQM 深度剖析: 拥塞控制
linux·服务器·算法·架构·边缘计算
艾醒16 小时前
大模型原理剖析——突破LLM效率瓶颈:多标记预测(MTP)技术深度解析与实战
算法