LeetCode - Hot 100 - 找到字符串中所有字母异位词

6 阅读4分钟

算法每日一题 | LeetCode 438:找到字符串中所有字母异位词

📌 题目描述

给定两个字符串 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" 的异位词。

🧠 思路分析

拿到这题,第一反应是暴力——遍历 s 里每个长度为 p.length() 的子串,挨个判断是不是 p 的异位词。思路没问题,但问题在于怎么判断两个字符串是异位词?排序比较一下?那每比一次就是 O(LlogL)O(L \log L),数据量一大直接拉胯。

换个角度想:两个字符串要是异位词,说白了就是每个字符出现的次数一模一样。题目又限定了只有小写字母,那直接用个长度为 26 的 int 数组记频次就行了,两个数组一比较,完全相等就是异位词。

然后就是怎么高效遍历的问题了。因为 p 的长度是固定的(假设叫 LL),所以我们在 s 上只需要看所有长度为 LL 的连续子串。这不就是滑动窗口嘛——维护一个固定大小的窗口在 s 上滑过去就行了:

  1. 先统计 p 里每个字符出现了多少次,存进 pCount 数组
  2. 再搞一个同样大小的 windowCount 数组,用来记当前窗口里的字符频次
  3. 右指针 right 往右走,把新字符加进窗口
  4. 窗口一旦超过 LL,左指针 left 就得跟着往右缩,把最左边的字符从窗口里踢出去
  5. 窗口刚好等于 LL 的时候,拿 windowCountpCount 比一下,一样就把 left 记下来

s = "cbaebabacd"p = "abc" 来手动走一遍(L=3L = 3):

  • pCount = {a:1, b:1, c:1}
  • 窗口滑到 [c,b,a](索引 0~2):频次是 {a:1, b:1, c:1},跟 pCount 一样,记 0
  • 滑到 [b,a,e](索引 1~3):多了个 e,不对
  • 继续滑...中间都不匹配
  • 滑到 [b,a,c](索引 6~8):频次又是 {a:1, b:1, c:1},记 6
  • 最终结果 [0, 6]

🖼️ 图解与执行流程

┌─────────────────────────────────────────────────────┐
│              字符串 s = "cbaebabacd"                   │
│              目标   p = "abc"  (L = 3)                │
└─────────────────────────────────────────────────────┘

 Step 1: 初始化频次数组
 ┌──────────────────────────────────────┐
 │ pCount     = [a:1, b:1, c:1, ...]    │
 │ windowCount= [0, 0, 0, 0, ...]       │
 └──────────────────────────────────────┘

 Step 2: 窗口滑动过程

 right=0  窗口: [c]              → 窗口没满,继续
 right=1  窗口: [c,b]            → 还没满,继续
 right=2  窗口: [c,b,a]          → 满了!比一下 → 匹配!记 left=0
 right=3  窗口: [c,b,a,e]        → 超了!左边缩 → [b,a,e] → 不行
 right=4  窗口: [b,a,e,b]        → 超了!左边缩 → [a,e,b] → 不行
 right=5  窗口: [a,e,b,a]        → 超了!左边缩 → [e,b,a] → 不行
 right=6  窗口: [e,b,a,b]        → 超了!左边缩 → [b,a,b] → 不行
 right=7  窗口: [b,a,b,a]        → 超了!左边缩 → [a,b,a] → 不行
 right=8  窗口: [a,b,a,c]        → 超了!左边缩 → [b,a,c] → 匹配!记 left=6
 right=9  窗口: [b,a,c,d]        → 超了!左边缩 → [a,c,d] → 不行

 最终结果: [0, 6]

💻 核心代码实现

class Solution {

    public List<Integer> findAnagrams(String s, String p) {
        // 结果列表
        List<Integer> res = new ArrayList<>();

        // p 的长度,也就是窗口的固定大小
        int length = p.length();

        // 滑动窗口的左右指针
        int left = 0, right = 0;

        // pCount 记 p 的字符频次,windowCount 记当前窗口的字符频次
        int[] pCount = new int[26];
        int[] windowCount = new int[26];

        // 先把 p 里每个字符出现几次统计好
        for (int i = 0; i < length; i++) {
            char c = p.charAt(i);
            pCount[c - 'a']++;
        }

        // 右指针从头到尾扫一遍 s
        while (right < s.length()) {
            // 右指针指的字符加入窗口
            windowCount[s.charAt(right) - 'a']++;
            right++;

            // 窗口超长了,左指针往右缩
            while (right - left > length) {
                windowCount[s.charAt(left) - 'a']--;
                left++;
            }

            // 窗口刚好等于 p 的长度,比一下频次
            if (right - left == length) {
                if (Arrays.equals(pCount, windowCount)) {
                    // 匹配上了,把起始位置记下来
                    res.add(left);
                }
            }
        }

        return res;
    }
}

🔗 LeetCode 原题链接

LeetCode 438. 找到字符串中所有字母异位词