LeetCode 438 找到字符串中所有字母异位词:滑动窗口与排序解法详解

前言

LeetCode 438 要求从字符串 s 中找出所有与字符串 p 互为字母异位词的子串,并返回这些子串的起始下标。

这道题可以使用固定长度滑动窗口解决:字母异位词的长度一定相同,因此只需遍历 s 中所有长度等于 p.length() 的子串。对于每个窗口,将字符排序后与排序后的 p 比较;如果两个字符数组完全相同,当前窗口就是 p 的字母异位词。

本文严格围绕"固定长度滑动窗口 + 排序比较"的解法展开。

一、题目说明

给定字符串 sp,找到 s 中所有 p 的字母异位词子串,返回这些子串的起始索引。

字母异位词由相同字母重新排列形成。例如,abccbabac 互为字母异位词;abcabb 则不是,因为字符出现次数不同。

复制代码
输入:s = "cbaebabacd", p = "abc"
输出:[0, 6]

从下标 0 开始的子串是 cba,从下标 6 开始的子串是 bac。二者排序后都是 abc,因此返回 [0, 6]

二、核心思路

p 的长度为 m。如果一个子串是 p 的字母异位词,它的长度也必须是 m。因此,可以在 s 中维护一个长度固定为 m 的窗口:

  1. p 转成字符数组并提前排序;

  2. 使用 leftright 表示窗口左右边界;

  3. 截取当前窗口,将其转成字符数组并排序;

  4. 使用 Arrays.equals() 比较两个有序数组;

  5. 如果相同,记录窗口起点 left

  6. 同时将 leftright 向右移动一位。

窗口只改变位置,不改变长度。

三、为什么排序后可以判断异位词

两个字符串互为字母异位词,需要同时满足:长度相同,并且每种字符的出现次数相同。当两个字符串的字符及数量完全一致时,按照相同规则排序后,得到的字符序列必然一致。

复制代码
abc 排序后:abc
cba 排序后:abc
bac 排序后:abc
abb 排序后:abb

因此,只需判断两个排序后的字符数组是否完全相等。

四、滑动窗口如何定义

代码使用两个指针定义窗口:

复制代码
int left = 0, right = p.length() - 1;

区间 [left, right] 的长度是 right - left + 1。代入初始值可得:

复制代码
p.length() - 1 - 0 + 1 = p.length()

所以初始窗口长度正好等于 p 的长度。每次检查结束后执行:

复制代码
left++;
right++;

左右边界同时增加,窗口整体右移一位,长度保持不变。

五、示例演算

s = "cbaebabacd"p = "abc" 为例,p 排序后仍为 abc

left 当前窗口 排序结果 是否匹配 操作
0 cba abc 记录 0
1 bae abe 不记录
2 aeb abe 不记录
3 eba abe 不记录
4 bab abb 不记录
5 aba aab 不记录
6 bac abc 记录 6
7 acd acd 不记录

最终得到 [0, 6]

六、Java 完整代码

复制代码
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

class Solution {
    public List<Integer> findAnagrams(String s, String p) {
        List<Integer> res = new ArrayList<>();
        if (p.length() > s.length()) {
            return res;
        }

        char[] chs = p.toCharArray();
        Arrays.sort(chs);

        // 定义滑动窗口,其长度与 p 的长度相同
        int left = 0, right = p.length() - 1;

        while (right < s.length()) {
            // 截取当前滑动窗口
            String win = s.substring(left, right + 1);
            char[] winArr = win.toCharArray();
            Arrays.sort(winArr);

            // 判断当前窗口是否为 p 的异位词
            if (Arrays.equals(chs, winArr)) {
                res.add(left);
            }

            // 窗口整体向右滑动一位
            left++;
            right++;
        }
        return res;
    }
}

七、代码逐段解析

1. 处理 ps 长的情况

复制代码
if (p.length() > s.length()) {
    return res;
}

如果 ps 还长,s 中不可能存在长度相等的候选子串,可以直接返回空集合。

2. 提前排序 p

复制代码
char[] chs = p.toCharArray();
Arrays.sort(chs);

p 在遍历中不会变化,因此只需要排序一次,后续所有窗口都与这个数组比较。

3. 截取并排序当前窗口

复制代码
String win = s.substring(left, right + 1);
char[] winArr = win.toCharArray();
Arrays.sort(winArr);

substring(beginIndex, endIndex) 采用左闭右开区间:包含 beginIndex,不包含 endIndex。由于 right 是需要包含的右边界,所以第二个参数必须写成 right + 1

4. 判断并记录答案

复制代码
if (Arrays.equals(chs, winArr)) {
    res.add(left);
}

两个有序字符数组完全相同,说明当前窗口和 p 的字符组成一致,记录窗口起始位置即可。

5. 移动窗口

复制代码
left++;
right++;

两个指针同时右移,保证窗口长度始终等于 p.length()

八、复杂度分析

s 的长度为 np 的长度为 m

p 排序需要 O(m log m)。字符串 s 中共有 n - m + 1 个候选窗口。对于每个窗口:

  • 截取子串并转换为字符数组需要 O(m)

  • 对窗口排序需要 O(m log m)

  • 比较两个数组需要 O(m)

总时间复杂度为:

复制代码
O(m log m + (n - m + 1) × m log m)

通常简写为 O(n × m log m)。每次需要创建长度为 m 的子串和字符数组,所以额外空间复杂度为 O(m)

相关推荐
M78佐菲19 分钟前
Linux多线程:创建、回收与互斥同步
linux·笔记·学习·算法
小手cool24 分钟前
使用递归对数组进行反转操作
java·数据结构·算法
兔兔兔兔135 分钟前
记录C++ 13
开发语言·c++·算法
码行山野赴时序归途40 分钟前
从暴力到最优:三道 C 语言入门题的解法思路
c语言·开发语言·数据结构·算法·leetcode·排序算法
土司大王40 分钟前
LeetCode hot100——随机链表的复制
算法·leetcode·链表
叠层归一研究院41 分钟前
螺旋时空:E框架的整合宇宙论
人工智能·经验分享·算法·机器学习·agi
磁场转动100万匹1 小时前
线性回归算法实现训练数据集、测试数据集的缺失值填充
算法·机器学习·线性回归
乐迪信息2 小时前
防爆AI摄像机实时监测船舶异常姿态
大数据·人工智能·算法·安全·目标跟踪
CarIise3 小时前
C语言结构体与链表
数据结构·算法