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

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

## 总结

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

* 固定长度窗口
* 滑动更新频次
* 实时判断是否匹配
相关推荐
Maỿbe1 小时前
暴打力扣之优先级队列(堆)
算法·leetcode·职场和发展
北冥湖畔的燕雀1 小时前
二叉搜索树:高效查找与删除的实现
数据结构·c++·算法
学学学无无止境1 小时前
力扣-位1的个数
leetcode
别学LeetCode1 小时前
#leetcode# 1
leetcode
兩尛1 小时前
矩阵中非1的数量 (2025B卷
线性代数·算法·矩阵
kupeThinkPoem1 小时前
线段树有哪些算法?
数据结构·算法
sheeta19981 小时前
LeetCode 每日一题笔记 日期:2025.11.30 题目:1590.使数组和能被 P 整除
笔记·算法·leetcode
兩尛1 小时前
HJ43 迷宫问题
算法
小龙报1 小时前
【算法通关指南:数据结构与算法篇(五)】树的 “自我介绍”:从递归定义到存储绝技(vector vs 链式前向星)
c语言·数据结构·c++·算法·链表·启发式算法·visual studio