【LeetCode 热题 100】找到字符串中所有字母异位词


精选专栏链接 🔗


欢迎订阅,点赞+关注,每日精进1%,与百万开发者共攀技术珠峰

更多内容持续更新中~


【LeetCode 热题 100】找到字符串中所有字母异位词

  • 📝题目描述
  • 💡提示信息
  • [📌 字母异位词是什么](#📌 字母异位词是什么)
  • [🔍 解题思路一:暴力枚举](#🔍 解题思路一:暴力枚举)
  • [🔍 解题思路二:滑动窗口 + 数组计数](#🔍 解题思路二:滑动窗口 + 数组计数)

📝题目描述

给定两个字符串 s 和 p,找到 s 中所有 p 的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。

示例 1:

bash 复制代码
输入: s = "cbaebabacd", p = "abc"
输出: [0,6]
解释:
起始索引等于 0 的子串是 "cba", 它是 "abc" 的异位词。
起始索引等于 6 的子串是 "bac", 它是 "abc" 的异位词。

示例 2:

bash 复制代码
输入: s = "abab", p = "ab"
输出: [0,1,2]
解释:
起始索引等于 0 的子串是 "ab", 它是 "ab" 的异位词。
起始索引等于 1 的子串是 "ba", 它是 "ab" 的异位词。
起始索引等于 2 的子串是 "ab", 它是 "ab" 的异位词。

💡提示信息

  • 1 <= s.length, p.length <= 3 * 10 4 10^4 104;
  • s 和 p 仅包含小写字母;

📌 字母异位词是什么

字母异位词指的是两个字符串包含的字符完全相同(每个字符出现次数一致),仅顺序不同。例如 "abc" 和 "bca" 是异位词, "aab" 和 "aba" 也是异位词。


🔍 解题思路一:暴力枚举

既然要找 s 中长度为 p.length() 的子串,最直观的做法是:

  • 截取:从 s 的第 0 位开始,每次截取一段长度等于 p 的子串;
  • 比较:拿着这个子串去跟 p 比,看是不是异位词;
  • 移动:起始位置 +1,重复上述步骤。

暴力枚举法代码实现如下:

java 复制代码
class Solution {
    public List<Integer> findAnagrams(String s, String p) {
        List<Integer> result = new ArrayList<>();
        int sLen = s.length();
        int pLen = p.length();

        // 将 p 转为字符数组并排序,作为比较的标准
        char[] pChars = p.toCharArray();
        Arrays.sort(pChars);
        String sortedP = new String(pChars);

        // 遍历 s 中所有可能的子串起点
        for(int i = 0 ; i <= sLen - pLen ; i++){
            // 截取长度为 pLen 的子串,注意substring字母都是小写
            String sub = s.substring(i, i + pLen);

            // 将子串也排序后与 p 比较
            char[] subChars = sub.toCharArray();
            Arrays.sort(subChars);
            String sortedSub = new String(subChars);

            if(sortedP.equals(sortedSub)){
                result.add(i);
            }
        }
        return result;

    }
}

提交代码,运行结果如下:

可以看出,暴力枚举法效率实际上并不高。

暴力枚举法的缺陷

假设 s 的长度是 N ,p 的长度是 M 。我们要移动约 N 次。每次移动后,都要重新排序。

  • 暴力枚举法的时间复杂度为 𝑂(𝑁⋅𝑀log𝑀) 。每次循环都要对子串进行排序;
  • 暴力枚举法的空间复杂度为 𝑂(𝑀) ,用于存储排序后的字符数组。

知识点补充

在 Java 中,Arrays.sort() 对基本数据类型(如 char[])使用的是双轴快速排序,其平均时间复杂度是 O ( n log ⁡ n ) O(n \log n) O(nlogn)。其中 n 是字符串 p 的长度。


🔍 解题思路二:滑动窗口 + 数组计数

我们需要在 s 中寻找长度恰好等于 p.length() 的子串。如果使用暴力枚举,每次窗口向右移动一格,我们都要重新统计整个新窗口的字符频率,这会造成大量的重复计算而滑动窗口的核心思想是 "增量更新" 。当窗口向右滑动一格时,实际上只发生了两件事:

  • 左边界有一个字符离开了窗口;
  • 右边界有一个新字符进入了窗口。

因此,我们只需要在旧的统计结果上,把离开的字符计数减 1,把进来的字符计数加 1,就能在 O(1) 的时间内得到新窗口的状态。具体例子如下:

bash 复制代码
① 初始窗口 [0, 2]:子串是 "cba"。我们需要遍历这3个字符,统计出频率 {c:1, b:1, a:1};
② 窗口右移一格 [1, 3]:子串变成了 "bae";
③ 注意观察!新窗口 "bae" 和旧窗口 "cba" 中间重叠了 "ba";
④ 此时我们只需把刚离开窗口的左边字符 'c' 的计数减 1。把刚进入窗口的新字符 'e' 的计数加 1;
⑤ 经过两步简单的加减法,sCount 数组变成了 {b:1, a:1, e:1},这正是新窗口 "bae" 的频率!

代码实现如下:

java 复制代码
class Solution {
    public List<Integer> findAnagrams(String s, String p) {
        int sLen = s.length();
        int pLen = p.length();

        // 1. 边界处理:如果 s 比 p 还短,不可能存在异位词,直接返回空列表
        if(sLen < pLen){
            return new ArrayList<Integer>();
        }

        List<Integer> result = new ArrayList<Integer>();
        // 2. 初始化两个频率统计数组,长度为 26 对应 26 个小写英文字母
        int[] sCount = new int[26];
        int[] pCount = new int[26];

        // 3. 构建初始窗口:统计 p 的字母频率,以及 s 中前 pLen 个字母频率
        for(int i =0; i < pLen; i++){
            // s.charAt(i) - 'a',可将字符映射为数组索引。'a'的ASCII 值为 97
            ++sCount[s.charAt(i) - 'a'];
            ++pCount[p.charAt(i) - 'a'];
            // 此时sCount 记录了 s 中第一个窗口内各字母出现的频率,pCount 记录了目标字符串的字母频率
        }

        // 4. 检查 初始窗口 是否匹配
        if (Arrays.equals(sCount, pCount)) {
            result.add(0);
        }

        // 5. 核心滑动过程:窗口从索引 1 开始,一直滑动到 s 的末尾
        for (int i = 0; i < sLen - pLen; i++) {
            // 【出】窗口左边界字符离开:将 s[i] 的计数减 1
            --sCount[s.charAt(i) - 'a'];
            
            // 【进】窗口右边界新字符进入:将 s[i + pLen] 的计数加 1
            ++sCount[s.charAt(i + pLen) - 'a'];

            // 6. 比较当前窗口与目标字符串 p 的频率是否一致
            if (Arrays.equals(sCount, pCount)) {
                // 注意:此时窗口的起始索引是 i + 1
                result.add(i + 1);
            }
        }
        return result;
    }
}

提交代码,运行结果如下:

滑动窗口法 复杂度分析

时间复杂度: O(N) ,其中 N 是字符串 s 的长度,M 为字符串 p 的长度:

  • 初始化两个数组和初始窗口需要 O(M) ;
  • 滑动窗口过程需遍历 O(N−M) 次,每次操作(加减计数 + 比较 26 个元素)都是 O(1) ;
  • 总体来看,时间复杂度与 s 的长度成线性关系。

空间复杂度: O(1)

  • 仅使用了两个固定大小为 26 的整型数组 sCount 和 pCount,以及几个辅助变量,不随输入数据的规模增长。
相关推荐
来一碗刘肉面1 小时前
栈在递归中的应用
数据结构·算法
8K超高清1 小时前
博冠获中国电影电视技术学会科技进步奖
人工智能·科技·算法·安全·接口隔离原则·智能硬件
皓月斯语2 小时前
程序设计语言的特点
开发语言·数据结构·c++
刘小八2 小时前
Spring AI Tool Calling 生产化:参数校验、权限控制与超时隔离
java·人工智能·spring
微石科技2 小时前
长期慢病如何做好居家管理?宁波微石科技星梦云康改善周期数据零散短板
大数据·科技·物联网·算法
奶糖 肥晨2 小时前
一次Spring Boot编译报错排查:三元运算符与包装类型的“隐形陷阱”
java·spring boot·后端
谢栋_2 小时前
设计模式从入门到精通之(七)责任链模式
java·设计模式·责任链模式
愚公移码2 小时前
蓝凌EKP18产品:核心执行流程
java·流程引擎
VortMall2 小时前
全维度打磨细节体验,赋能商城稳定有序运营|VortMall 微服务商城 v1.3.11 版本发布
java·微服务·云原生·架构·商城系统·开源商城·vortmall