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

前言

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

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

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

一、题目说明

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

字母异位词由相同字母重新排列形成。例如,abc、cba 和 bac 互为字母异位词;abc 和 abb 则不是,因为字符出现次数不同。

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

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

二、核心思路

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

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

  2. 使用 left 和 right 表示窗口左右边界;

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

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

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

  6. 同时将 left 和 right 向右移动一位。

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

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

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

复制代码
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. 处理 p 比 s 长的情况

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

如果 p 比 s 还长,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 的长度为 n,p 的长度为 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)。

相关推荐
Interview Aid11214 小时前
OpenAI SWE 面试流程
面试·职场和发展
我命由我1234514 小时前
Photoshop - Photoshop 使用对齐功能定位元素
学习·职场和发展·产品运营·求职招聘·职场发展·产品经理·学习方法
飘尘15 小时前
从"算子"到"AI Infra":大模型背后看不见的那群人在忙什么
人工智能·算法·面试
ychqsq15 小时前
212.涟漪
经验分享·职场和发展
ychqsq15 小时前
213.取舍
经验分享·职场和发展
夏幻灵16 小时前
每日一题:LeetCode 3:无重复字符的最长子串——滑动窗口思路详解
算法·leetcode·职场和发展
CarIise16 小时前
Java实训阶段查漏补缺复习笔记2
java·linux·算法
无忧.芙桃16 小时前
C++ 并查集详解:原理、实现与应用
c语言·c++·算法·leetcode
蛋蛋的就会顺顺的17 小时前
hot100——二叉树
java·数据结构·算法·leetcode·力扣
Code_Solitude17 小时前
c语言:二分法的完整补充
c语言·开发语言·算法