力扣hoT100之找到字符串中所有字母异位词(java版)

一、题目描述

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

示例 1:

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

示例 2:

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

提示:

  • 1 <= s.length, p.length <= 3 * 104

  • sp 仅包含小写字母

二、思路分析

第一步:理解"异位词"的本质
思考1:怎么判断两个字符串是异位词?
  • 长度必须相同

  • 每个字符出现的次数必须完全一样

举例:

  • p = "abc" → 字符频率:a:1, b:1, c:1

  • s 的子串 "bca" → 字符频率:a:1, b:1, c:1 → 是异位词

  • s 的子串 "aab" → a:2, b:1 → 不是

Java知识点1:字符与ASCII码

在Java中,char 类型本质是整数(ASCII码),我们可以用:

复制代码
 char c = 'a';
 int index = c - 'a'; // 'a'→0, 'b'→1, ..., 'z'→25

→ 这样可以用 长度为26的数组 表示26个小写字母的频率!

第二步:暴力法 → 优化方向
暴力思路:
  • 遍历 s 中每个长度为 p.length() 的子串

  • 对每个子串,统计字符频率,和 p 比较

  • 时间复杂度:O(n × m),其中 n 是 s 长度,m 是 p 长度 → 太慢!

优化方向:滑动窗口 + 频率数组复用

我们发现:

相邻的两个子串,只有第一个字符被移除,最后一个字符被加入

我们不需要每次都重新统计整个子串的频率!只需更新两个字符的计数

这就是滑动窗口的核心思想!

第三步:设计滑动窗口结构
步骤分解:
  1. 创建两个数组 pFreq[26]windowFreq[26],分别记录 p 和当前窗口的字符频率。

  2. 初始化:先把 p 的字符频率统计好。

  3. 用双指针 leftright控制窗口:

    • right 向右扩展,加入新字符

    • 当窗口大小 == p.length()时:

      • 比较 pFreqwindowFreq

      • 如果相等 → 记录 left

      • 然后 left++,移除左端字符,继续滑动

第四步:动手写代码 ------ 一步一步来!
Step 1:准备结果容器和边界判断
java 复制代码
import java.util.*;

class Solution {
    public List<Integer> findAnagrams(String s, String p) {
        List<Integer> result = new ArrayList<>(); // 存放结果索引
        // 边界情况:s为空、p为空、s比p短 → 不可能有异位词
        if (s == null || p == null || s.length() < p.length()) {
            return result;
        }
        // 后续代码写在这里...
    }
}

Java知识点2:ArrayList

  • List<Integer> result = new ArrayList<>(); → 动态数组,可自动扩容

  • .add(index) → 添加元素

Step 2:创建频率数组,初始化 pFreq
java 复制代码
    int[] pFreq = new int[26];      // p的字符频率
        int[] windowFreq = new int[26]; // 当前窗口的字符频率

        // 填充 pFreq
        for (char c : p.toCharArray()) {
            pFreq[c - 'a']++;
        }

Java知识点3:for-each 循环 & toCharArray()

  • p.toCharArray() → 把字符串转成字符数组

  • for (char c : array) → 遍历每个字符,简洁安全

算法思想:计数排序/桶排序思想

  • 用数组下标表示字符,值表示出现次数 → O(1) 查找和更新
Step 3:滑动窗口主循环

我们用 right 控制右边界,left 控制左边界。

java 复制代码
  int left = 0;
        for (int right = 0; right < s.length(); right++) {
            // 1. 把 s[right] 加入窗口
            char rightChar = s.charAt(right);
            windowFreq[rightChar - 'a']++;

            // 2. 如果窗口大小等于 p 的长度,开始检查
            if (right - left + 1 == p.length()) {
                // 检查是否是异位词
                if (Arrays.equals(pFreq, windowFreq)) {
                    result.add(left);
                }

                // 3. 移动左边界:移除 s[left]
                char leftChar = s.charAt(left);
                windowFreq[leftChar - 'a']--;
                left++;
            }
        }

Java知识点4:Arrays.equals()

  • 比较两个数组内容是否完全相等 → 非常适合比较频率数组!

算法思想:滑动窗口

  • 窗口大小固定(= p.length())

  • 每次右移一步,左移一步 → 保持窗口大小不变

  • 频率数组只需更新"进"和"出"的字符 → 高效!

Step 4:完整代码整合
java 复制代码
import java.util.*;
 import java.util.Arrays;
 ​
 class Solution {
     public List<Integer> findAnagrams(String s, String p) {
         List<Integer> result = new ArrayList<>();
         if (s == null || p == null || s.length() < p.length()) {
             return result;
         }
         int[] pFreq = new int[26];
         int[] windowFreq = new int[26];
         // 初始化 p 的频率
         for (char c : p.toCharArray()) {
             pFreq[c - 'a']++;
         }
         int left = 0;
         for (int right = 0; right < s.length(); right++) {
             // 加入右端字符
             windowFreq[s.charAt(right) - 'a']++;
             // 窗口大小达到 p 的长度
             if (right - left + 1 == p.length()) {
                 // 检查是否是异位词
                 if (Arrays.equals(pFreq, windowFreq)) {
                     result.add(left);
                 }
                 // 移除左端字符,窗口右移
                 windowFreq[s.charAt(left) - 'a']--;
                 left++;
             }
         }
         return result;
     }
 }
第五步:算法复杂度分析
  • 时间复杂度:O(n)

    • s 只遍历一次,每个字符最多进窗口一次、出窗口一次

    • 每次比较频率数组是 O(26) = O(1)

  • 空间复杂度:O(1)

    • 只用了两个固定长度26的数组 + 一些变量
第六步:举个例子走一遍流程(手动模拟)

假设:

  • s = "cbaebabacd"

  • p = "abc"

pFreq = [1,1,1,0,0,...] (a,b,c 各1次)

滑动过程:

right 窗口内容 windowFreq 是否等于 pFreq left 移动后
0 "c" [0,0,1,...] -
1 "cb" [0,1,1,...] -
2 "cba" [1,1,1,...] 是 → 记录 left=0 left=1
3 "bae" [1,1,0,0,1,...] left=2
... ... ... ... ...
6 "bac" [1,1,1,...] 是 → 记录 left=5 left=6

最终结果:[0, 6]

总结
项目 内容
算法思想 滑动窗口、字符频率统计、异位词判定
数据结构 固定数组(桶)、ArrayList
Java技巧 toCharArray(), charAt(), Arrays.equals(), for-each循环
优化关键 避免重复计算,窗口滑动时只更新"进出"字符
复杂度 时间O(n),空间O(1)
暴力算法解决

Step 1:边界判断

如果 sp 短,直接返回空列表。

java 复制代码
if (s.length() < p.length()) return new ArrayList<>();

Step 2:外层循环 ------ 枚举所有起始位置

i = 0i = s.length() - p.length(),每个 i 作为子串起始点:

java 复制代码
for (int i = 0; i <= s.length() - p.length(); i++) {
 String substring = s.substring(i, i + p.length());
 // 判断 substring 和 p 是否是异位词
}

Java知识点:substring(beginIndex, endIndex)

  • substring(i, i + len) → 截取 [i, i+len) 区间的子串(左闭右开)

  • 时间复杂度 O(len),每次都会创建新字符串对象 → 开销大!

Step 3:判断两个字符串是否是异位词 ------ 写一个 helper 函数

我们可以写一个函数 isAnagram(String a, String b)

方法一:排序法(最直观)

java 复制代码
 private boolean isAnagram(String a, String b) {
     char[] aChars = a.toCharArray();
     char[] bChars = b.toCharArray();
     Arrays.sort(aChars);
     Arrays.sort(bChars);
     return Arrays.equals(aChars, bChars);
 }

Java知识点:Arrays.sort() & Arrays.equals()

  • Arrays.sort(char[]) → 对字符数组排序,O(n log n)

  • Arrays.equals() → 逐个比较数组元素是否相等

举例:

  • "abc" → 排序 → "abc"

  • "bca" → 排序 → "abc" → 相等 → 是异位词!

Step 4:如果判断为异位词,就把起始索引 i 加入结果

java 复制代码
 if (isAnagram(substring, p)) {
     result.add(i);
 }

完整暴力算法代码

java 复制代码
 import java.util.*;
 import java.util.Arrays;
 ​
 class Solution {
     public List<Integer> findAnagrams(String s, String p) {
         List<Integer> result = new ArrayList<>();
         // 边界情况
         if (s == null || p == null || s.length() < p.length()) {
             return result;
         }
         int len = p.length();
         // 遍历所有起始位置
         for (int i = 0; i <= s.length() - len; i++) {
             String substring = s.substring(i, i + len);
             if (isAnagram(substring, p)) {
                 result.add(i);
             }
         }
         return result;
     }
     private boolean isAnagram(String a, String b) {
         char[] aChars = a.toCharArray();
         char[] bChars = b.toCharArray();
         Arrays.sort(aChars);
         Arrays.sort(bChars);
         return Arrays.equals(aChars, bChars);
     }
 }
时间复杂度分析
  • 外层循环次数:n - m + 1 ≈ O(n),其中 n = s.length(), m = p.length()

  • 每次循环:

    • substring() → O(m)

    • toCharArray() → O(m)

    • Arrays.sort() → O(m log m)

  • 总时间复杂度:O(n × m log m)

举例:如果 s 长度是 10000,p 长度是 100 → 每次排序 100 log100 ≈ 700 操作,总共 9900 次 → 700×9900 ≈ 700万次 → 在 Java 中勉强能过(但很慢)

暴力法 vs 滑动窗口法 对比

项目 暴力法 滑动窗口法
思路 简单直接,容易想到 需要理解"窗口滑动"和"频率复用"
时间复杂度 O(n × m log m) 或 O(n × m) O(n)
空间复杂度 O(m) ------ 每次生成子串和数组 O(1) ------ 固定两个数组
是否创建新字符串 每次都 substring 只用 charAt,不创建子串
适合场景 数据量小、快速验证逻辑 数据量大、追求效率
相关推荐
松岛雾奈.2302 小时前
机器学习--KNN算法中的距离、范数、正则化
人工智能·算法·机器学习
兮山与2 小时前
算法33.0
算法
Brduino脑机接口技术答疑2 小时前
支持向量机(SVM)在脑电情绪识别中的学术解析与研究进展
人工智能·算法·机器学习·支持向量机·数据分析
拂晓银砾2 小时前
Java 连接数据库
java
青衫码上行2 小时前
【Java Web学习 | 第九篇】JavaScript(3) 数组+函数
java·开发语言·前端·javascript·学习
浮游本尊2 小时前
Java学习第29天 - 企业级系统架构与实战
java
YoungHong19922 小时前
面试经典150题[063]:删除链表的倒数第 N 个结点(LeetCode 19)
leetcode·链表·面试
程序猿DD2 小时前
探索 Java 中的新 HTTP 客户端
java·后端
xier_ran2 小时前
深度学习:Mini-batch 大小选择与 SGD 和 GD
人工智能·算法·机器学习