题目描述

✅ LCR 015. 找到字符串中所有字母异位词

image-20260928234838399

image-20260928234838402

题意分析

找出字符串 s 中所有与 p 互为字母异位词的连续子串,返回它们的起始下标。异位词可以改变字母排列顺序,但每个字母的出现次数必须保持一致,所以候选长度固定为 p.length()。

这里要收集全部位置,找到一个后仍需继续寻找;匹配窗口可以互相重叠,也可以具有完全相同的内容,只要起点不同就分别记录。两串都只含小写英文字母,源串短于目标串时返回空结果。

解法:定长窗口收集匹配起点

核心思路

[!blue]

设源串长度为 m,目标长度为 n。用 cnt2 固定保存 p 的字符频次,用 cnt1 保存 s 中当前长度为 n 的窗口频次。两个数组逐项相等,就等价于当前窗口是目标的一个异位词。

相邻窗口有 n - 1 个位置重合,右移一次只改变两个字符:新右端加入,旧左端离开。初始统计前 n 个字符后,每次增减这两个计数即可维持准确的窗口频次,无需重新扫描窗口内容。

当新右端为 i 时,移出的旧位置是 i - n,新窗口覆盖 [i - n + 1, i],所以匹配时要保存 i - n + 1。它是新左端,不是被移出的下标,也不是当前右端。

初始窗口单独检查并可能记录零;后续每次只移动一位,确保重叠匹配也不会被跳过。找到一个后只是追加答案,不提前返回,不改变频次状态,继续扫描其他起点。最终结果自然按起点递增排列。

解题步骤

  1. 创建答案列表,若 s 比 p 短,直接返回空结果。
  2. 统计 p 与 s 的首个等长窗口的频次。
  3. 比较首窗口,匹配则加入起点零。
  4. 从新右端下标 i = n 开始,每次加入 s[i] 并移除 s[i - n]。
  5. 两次计数修改完成后比较数组,匹配时追加起点 i - n + 1。
  6. 扫描所有窗口后返回全部起点。

代码实现

class Solution {
    public List<Integer> findAnagrams(String s, String p) {
        int m = s.length();
        int n = p.length();
        List<Integer> answer = new ArrayList<>();

        // 长度不够时无解,同时防止建初始窗口越界。
        if (m < n) {
            return answer;
        }

        int[] cnt1 = new int[26];
        int[] cnt2 = new int[26];

        for (int i = 0; i < n; ++i) {
            ++cnt1[s.charAt(i) - 'a'];
            ++cnt2[p.charAt(i) - 'a'];
        }

        // 起点为 0 的窗口也是候选。
        if (Arrays.equals(cnt1, cnt2)) {
            answer.add(0);
        }

        for (int i = n; i < m; ++i) {
            // 右端进、左端出,两次更新后窗口长度回到 n。
            ++cnt1[s.charAt(i) - 'a'];
            --cnt1[s.charAt(i - n) - 'a'];

            if (Arrays.equals(cnt1, cnt2)) {
                // 窗口覆盖 [i - n + 1, i],起点是 i - n + 1。
                answer.add(i - n + 1);
            }
        }

        return answer;
    }
}
func findAnagrams(s string, p string) (answer []int) {
    m, n := len(s), len(p)
    if m < n {
        return
    }
    // 定长数组是值类型,可直接用 == 整体比较。
    var cnt1, cnt2 [26]int
    for i, ch := range p {
        cnt1[s[i]-'a']++
        cnt2[ch-'a']++
    }
    if cnt1 == cnt2 {
        answer = append(answer, 0)
    }
    for i := n; i < m; i++ {
        cnt1[s[i]-'a']++
        cnt1[s[i-n]-'a']--
        if cnt1 == cnt2 {
            answer = append(answer, i-n+1)
        }
    }
    return
}

复杂度分析

  • 时间复杂度:O(n + 26m),即 O(m + n)。初始统计目标长度的字符,每个后续窗口只增减两个计数并比较固定二十六项。
  • 空间复杂度:辅助空间 O(1);答案在最坏情况下包含线性数量的起点,另占 O(m)。

关键点总结

[!green]

  • 精确频次匹配保证异位词成立,固定窗口保证长度一致。
  • 窗口起点由闭区间长度推得 i - n + 1。
  • 每次仅移动一位,既覆盖分离匹配,也覆盖重叠匹配。
  • 收集全部解与判断存在性的区别在于命中后追加并继续。

易错点总结

[!yellow]

  • 记录右端或旧左端:需要的是新窗口起点 i - n + 1。
  • 第一次匹配就返回:后面仍可能有合法起点。
  • 匹配后跨过整个窗口:会跳过与当前窗口重叠的其他答案。
  • 只比较字母集合:各字母数量也必须与目标一致。
  • 忘记首窗口检查或移出旧字符:前者漏掉起点零,后者会把计数变成不断增长的前缀。
  • 对匹配内容去重:题目返回位置,同内容的不同起点仍然是不同答案。

相似题目

题目 难度 关联与区别
567. 字符串的排列 中等 同样比较定长窗口的频次,本题记录全部匹配位置,原题只回答是否存在。
49. 字母异位词分组 中等 同样识别字母频次相同的字符串,原题按完整单词分组,本题滑动截取子串。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/29341602
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!