【Hot100|9-LeetCode 438. 找到字符串中所有字母异位词】

这段代码是解决 LeetCode 438. 找到字符串中所有字母异位词 问题的固定大小滑动窗口 + 数组计数解法。核心目标是在字符串s中找到所有是字符串p字母异位词的子串,并返回这些子串的起始索引。字母异位词指字符种类和数量完全相同,但排列顺序不同的字符串。该解法时间复杂度为 O (m + n)(m 为p长度,n 为s长度),空间复杂度为 O (1)(因仅用两个固定大小的 26 位数组)。

注:代码存在一处小笔误(未定义变量n),需补充int n = s.length();才能正常运行,后续解析会修正该问题。

一、问题理解

问题要求

给定两个字符串sp,找到s中所有p的字母异位词的起始索引。

  • 示例:输入s = "cbaebabacd"p = "abc",输出[0,6]。因为s中索引 0 开始的"cba"和索引 6 开始的"bac"都是"abc"的字母异位词。

核心解题逻辑

字母异位词的核心特征是字符种类和对应数量完全一致,因此可通过统计字符频率判断两个字符串是否为异位词。结合固定大小的滑动窗口,在s中滑动出与p长度相同的子串,逐一比对频率,高效找到目标子串。

二、核心思路

  1. 字符频率统计:用两个长度为 26 的数组(对应 26 个小写英文字母)cntPcntS,分别记录p的字符频率和s当前滑动窗口内的字符频率。
  2. 固定窗口滑动:窗口大小固定为p的长度lenP。右指针遍历s时,不断将当前字符加入窗口并更新cntS;当窗口长度达到lenP后,每次右移都移除窗口最左侧的字符频率,保持窗口大小不变。
  3. 频率比对:当窗口长度等于lenP时,对比cntScntP。若两者相等,当前窗口对应的子串是p的异位词,记录窗口起始索引。

三、代码逐行解析(含修正)

先补充笔误修正后的完整代码,再逐行讲解:

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> ans = new ArrayList<>();
        // 初始化两个计数数组,统计26个小写字母的出现次数
        int[] cntP = new int[26];
        int[] cntS = new int[26];
        int lenP = p.length();
        int n = s.length(); // 补充笔误,定义s的长度
        
        // 步骤1:统计字符串p的字符频率
        for (char c : p.toCharArray()) {
            // 字符c - 'a' 映射为0-25的索引(如'a'→0,'b'→1)
            cntP[c - 'a']++;
        }
        
        // 步骤2:右指针遍历s,维护固定大小的滑动窗口
        for (int right = 0; right < n; right++) {
            // 当前字符加入窗口,更新s的窗口频率数组
            cntS[s.charAt(right) - 'a']++;
            
            // 计算当前窗口的左边界:窗口大小固定为lenP,左边界 = 右边界 - lenP + 1
            int left = right - lenP + 1;
            
            // 窗口未达到p的长度(左边界为负),跳过后续比对
            if (left < 0) {
                continue;
            }
            
            // 步骤3:窗口大小达标,比对两个频率数组
            if (Arrays.equals(cntS, cntP)) {
                // 频率一致,记录当前窗口起始索引left
                ans.add(left);
            }
            
            // 步骤4:窗口右移前,移除左边界字符的频率(避免影响下一个窗口)
            cntS[s.charAt(left) - 'a']--;
        }
        
        return ans;
    }
}

关键代码拆解

  1. 初始化与p的频率统计

    • cntPcntS数组:因题目默认字符串为小写字母,用长度 26 的数组足够,通过c - 'a'将字符映射为 0 - 25 的索引,实现 O (1) 的频率更新。
    • 遍历p的字符,填充cntP,完成对p的字符频率记录。
  2. 右指针遍历与窗口维护

    • right从 0 开始遍历s,每遍历一个字符就将其频率加入cntS,相当于窗口向右扩展。
    • 计算leftleft = right - lenP + 1,这是固定窗口的核心,确保窗口长度始终等于lenP。例如lenP=3right=2时,left=0,窗口为[0,2],长度 3。
  3. 窗口有效性判断与结果记录

    • left < 0时,说明窗口还没达到p的长度(如right=1lenP=3时,left=-1),无需比对频率,直接跳过。
    • Arrays.equals(cntS, cntP):比对两个频率数组,若相等则当前窗口是p的异位词,将left加入结果列表。
  4. 窗口右移的预处理比对完成后,将cntSleft对应字符的频率减 1。这是因为下一轮right右移时,窗口会抛弃当前左边界的字符,确保下一个窗口的频率统计准确。

四、实例演示(直观理解过程)

以测试用例s = "cbaebabacd"p = "abc"为例(lenP=3cntPa:1b:1c:1,其余为 0):

步骤right遍历字符cntS 更新后left窗口内容频率是否匹配结果列表cntS 左边界减 1(操作)
10'c'c:1-2无(窗口不足)[]
21'b'c:1、b:1-1无(窗口不足)[]
32'a'c:1、b:1、a:10[0,2]:"cba"是(匹配 cntP)[0]减 'a'→a:0
43'e'c:1、b:1、e:11[1,3]:"bae"[0]减 'b'→b:0
54'b'c:1、b:1、e:12[2,4]:"aeb"[0]减 'a'→a:0
65'a'a:1、b:1、e:13[3,5]:"eba"[0]减 'e'→e:0
76'c'a:1、b:1、c:14[4,6]:"bac"[0,6]减 'b'→b:0
...........................

最终结果列表为[0,6],与预期一致。

五、关键细节与复杂度分析

1. 关键细节

  • 为什么用数组而非 HashMap:26 个小写字母的场景下,数组比 HashMap 更高效,无需哈希计算和处理哈希冲突,且Arrays.equals比对数组的时间为 O (26),属于常数时间。
  • 固定窗口的优势:相比可变滑动窗口,固定窗口无需复杂的左指针收缩逻辑,仅需在每次窗口达标后移除左边界字符频率,逻辑更简洁。
  • 边界处理left < 0的判断避免了窗口未达标时的无效比对,确保代码鲁棒性。

2. 复杂度分析

  • 时间复杂度:O (m + n)。遍历p统计频率耗时 O (m);遍历s耗时 O (n),每次遍历中的频率更新和数组比对均为 O (1)(数组比对固定 26 次),整体时间与字符串长度线性相关。
  • 空间复杂度:O (1)。两个计数数组均为固定长度 26,与输入字符串长度无关,无额外空间开销。

六、总结

该解法的核心是 **“固定滑动窗口 + 字符频率统计”**,精准利用字母异位词的频率特征,通过高效的数组计数和窗口维护,实现线性时间求解。这种思路不仅适用于本题,还可迁移到类似的字符串匹配问题(如判断子串是否包含目标字符集、统计符合字符频率要求的子串数量等),是处理字符串频率类问题的经典范式。

# 力扣hot100刷题记录表 ### 一,哈希部分 - [ ] 1. 两数之和 (简单) - [ ] 2. 字母异位词分组(中等) - [ ] 3. 最长连续序列(中等) ### 二,双指针部分 - [ ] 4. 移动零(简单) - [ ] 5. 盛水最多的容器 (中等) - [ ] 6. 三数之和 (中等) - [ ] 7. 接雨水(困难) ### 三,滑动窗口 - [ ] 8. 无重复字符的最长子串(中等) - [ ] 9. 找到字符中所有的字母异位词(中等) ### 四,子串 - [ ] 10. 和为k的子数组(中等) - [ ] 11. 滑动窗口最大值(困难) - [ ] 12. 最小覆盖子窜(困难) ### 五,普通数组 - [ ] 13. 最大子数组和(中等) - [ ] 14. 合并区间(中等) - [ ] 15. 轮转数组(中等) - [ ] 16. 除自身以外数组的乘积(中等) - [ ] 17. 缺失的第一个正数(困难) ### 六,矩阵 - [ ] 18. 矩阵置零(中等) - [ ] 19. 螺旋矩阵 (中等) - [ ] 20. 旋转图像 (中等) - [ ] 21. 搜索二维矩阵Ⅱ (中等) ### 七,链表 - [ ] 22. 相交链表 (简单) - [ ] 23. 反转链表 (简单) - [ ] 24. 回文链表 (简单) - [ ] 25. 环形链表 (简单) - [ ] 26. 环形链表Ⅱ (中等) - [ ] 27. 合并两个有序链表 (简单) - [ ] 28. 两数相加 (中等) - [ ] 29. 删除链表的倒数第 N 个结点 (中等) - [ ] 30. 两两交换链表中的节点 (中等) - [ ] 31. K个一组翻转链表 (困难) - [ ] 32. 随机链表的复制 (中等) - [ ] 33. 排序链表 (中等) - [ ] 34. 合并 K 个升序链表 (困难) - [ ] 35. LRU 缓存 (中等) ### 八,二叉树 - [ ] 36. 二叉树的中序遍历 (简单) - [ ] 37. 二叉树的最大深度 (简单) - [ ] 38. 翻转二叉树 (简单) - [ ] 39. 对称二叉树 (简单) - [ ] 40. 二叉树的直径 (简单) - [ ] 41. 二叉树的层序遍历 (中等) - [ ] 42. 将有序数组转换为二叉搜索树 (简单) - [ ] 43. 验证二叉搜索树 (中等) - [ ] 44. 二叉搜索树中第 K 小的元素 (中等) - [ ] 45. 二叉树的右视图 (中等) - [ ] 46. 二叉树展开为链表 (中等) - [ ] 47. 从前序与中序遍历序列构造二叉树 (中等) - [ ] 48. 路径总和 III (中等) - [ ] 49. 二叉树的最近公共祖先 (中等) - [ ] 50. 二叉树中的最大路径和 (困难) ### 九,图论 - [ ] 51. 岛屿数量 (中等) - [ ] 52. 腐烂的橘子 (中等) - [ ] 53. 课程表 (中等) - [ ] 54. 实现 Trie(前缀树) (中等) ### 十,回溯 - [ ] 55.全排列(中等) - [ ] 56.子集(中等) - [ ] 57.电话号码的字母组合(中等) - [ ] 58.组合总和(中等) - [ ] 59.括号生成(中等) - [ ] 60.单词搜索(中等) - [ ] 61.分割回文串(中等) - [ ] 62.N 皇后 (困难) ### 十一,二分查找 - [ ] 63. 搜索插入位置 (简单) - [ ] 64. 搜索二维矩阵 (中等) - [ ] 65. 在排序数组中查找元素的第一个和最后一个位置 (中等) - [ ] 66. 搜索旋转排序数组 (中等) - [ ] 67. 寻找旋转排序数组中的最小值 (中等) - [ ] 68. 寻找两个正序数组的中位数 (困难) ### 十二,栈 - [ ] 69. 有效的括号 (简单) - [ ] 70. 最小栈 (中等) - [ ] 71. 字符串解码 (中等) - [ ] 72. 每日温度 (中等) - [ ] 73. 柱状图中最大的矩形 (困难) ### 十三,堆 - [ ] 74. 数组中的第K个最大元素 (中等) - [ ] 75. 前K 个高频元素 (中等) - [ ] 76. 数据流的中位数 (闲难) ### 十四,贪心算法 - [ ] 77. 买卖股票的最佳时机 (简单) - [ ] 78. 跳跃游戏 (中等) - [ ] 79. 跳跃游戏 III (中等) - [ ] 80. 划分字母区间 (中等) ### 十五,动态规划 - [ ] 81. 爬楼梯(简单) - [ ] 82. 杨辉三角 (简单) - [ ] 83. 打家劫舍 (中等) - [ ] 84. 完全平方数 (中等) - [ ] 85. 零钱兑换 (中等) - [ ] 86. 单词拆分 (中等) - [ ] 87. 最长递增子序列 (中等) - [ ] 88. 乘积最大子数组 (中等) ### 十六,多维动态规划 - [ ] 91. 不同路径 (中等) - [ ] 92. 最小路径和 (中等) - [ ] 93. 最长回文子串 (中等) - [ ] 94. 最长公共子序列 (中等) - [ ] 95. 编辑距离 (中等) ### 十七,技巧 - [ ] 96. 只出现一次的数字 (简单) - [ ] 97. 多数元素 (简单) - [ ] 98. 颜色分类 (中等) - [ ] 99. 下一个排列 (中等) - [ ] 100. 寻找重复数 (中等) 如何使用
07-20
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值