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

LeetCode 438. 找到字符串中所有字母异位词 - 滑动窗口解法详解

一、问题理解

问题描述

给定两个字符串 s 和 p,找到 s 中所有 p 的字母异位词的起始索引,并返回这些索引的列表。

字母异位词定义:由相同字母重排列形成的字符串,包括相同的字符串。

示例

text

输入: s = "cbaebabacd", p = "abc"
输出: [0,6]
解释:
起始索引 0 的子串 "cba" 是 "abc" 的字母异位词。
起始索引 6 的子串 "bac" 是 "abc" 的字母异位词。

二、核心思路:固定窗口滑动 + 字符频率统计

暴力解法的局限性

枚举 s 中所有长度为 len(p) 的子串,逐个检查是否为 p 的字母异位词,时间复杂度为 O(n×m),其中 n 是 s 的长度,m 是 p 的长度。

滑动窗口优化思路

  1. 字符频率统计:用两个长度为 26 的数组(对应 26 个小写字母)分别记录 p 的字符频率和当前窗口的字符频率

  2. 固定窗口滑动:窗口大小固定为 p 的长度,在 s 上滑动

  3. 高效比对:通过比较两个频率数组判断当前窗口是否是 p 的字母异位词

三、代码逐行解析

Java 解法(修正版)

java

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

class Solution {
    public List<Integer> findAnagrams(String s, String p) {
        // 1. 初始化结果列表
        List<Integer> ans = new ArrayList<>();
        
        // 2. 边界处理:如果s的长度小于p的长度,不可能有异位词
        if (s.length() < p.length()) {
            return ans;
        }
        
        // 3. 初始化两个计数数组(26个小写字母)
        int[] cntP = new int[26];
        int[] cntS = new int[26];
        int lenP = p.length();
        int n = s.length();
        
        // 4. 统计p的字符频率
        for (int i = 0; i < lenP; i++) {
            cntP[p.charAt(i) - 'a']++;
        }
        
        // 5. 滑动窗口遍历s
        for (int right = 0; right < n; right++) {
            // 5.1 将当前字符加入窗口(更新cntS)
            cntS[s.charAt(right) - 'a']++;
            
            // 5.2 计算当前窗口的左边界
            int left = right - lenP + 1;
            
            // 5.3 如果左边界小于0,说明窗口还没达到p的长度,继续扩展
            if (left < 0) {
                continue;
            }
            
            // 5.4 窗口大小达到p的长度,比较频率数组
            if (Arrays.equals(cntS, cntP)) {
                ans.add(left);
            }
            
            // 5.5 移除左边界字符的频率(为下一次窗口移动做准备)
            cntS[s.charAt(left) - 'a']--;
        }
        
        // 6. 返回结果
        return ans;
    }
}

Python 解法

python

from typing import List
from collections import Counter

class Solution:
    def findAnagrams(self, s: str, p: str) -> List[int]:
        # 1. 边界处理
        if len(s) < len(p):
            return []
        
        # 2. 初始化结果和计数
        result = []
        len_p = len(p)
        
        # 3. 统计p的字符频率
        p_counter = [0] * 26
        for ch in p:
            p_counter[ord(ch) - ord('a')] += 1
        
        # 4. 初始化窗口计数器
        window_counter = [0] * 26
        
        # 5. 滑动窗口遍历s
        for right in range(len(s)):
            # 5.1 将当前字符加入窗口
            window_counter[ord(s[right]) - ord('a')] += 1
            
            # 5.2 计算左边界
            left = right - len_p + 1
            
            # 5.3 如果窗口还没达到p的长度,继续扩展
            if left < 0:
                continue
            
            # 5.4 窗口大小达到p的长度,比较频率数组
            if window_counter == p_counter:
                result.append(left)
            
            # 5.5 移除左边界字符的频率
            window_counter[ord(s[left]) - ord('a')] -= 1
        
        return result

Python 解法(使用Counter)

python

from typing import List
from collections import Counter

class Solution:
    def findAnagrams(self, s: str, p: str) -> List[int]:
        result = []
        len_p = len(p)
        len_s = len(s)
        
        # 边界处理
        if len_s < len_p:
            return result
        
        # 统计p的字符频率
        p_count = Counter(p)
        
        # 初始化窗口计数器
        window_count = Counter()
        
        # 滑动窗口遍历s
        for right in range(len_s):
            # 将当前字符加入窗口
            window_count[s[right]] += 1
            
            # 计算左边界
            left = right - len_p + 1
            
            # 如果窗口还没达到p的长度,继续扩展
            if left < 0:
                continue
            
            # 窗口大小达到p的长度,比较频率
            if window_count == p_count:
                result.append(left)
            
            # 移除左边界字符
            window_count[s[left]] -= 1
            if window_count[s[left]] == 0:
                del window_count[s[left]]
        
        return result

四、Java 与 Python 语法对比

1. 数组/列表操作

操作JavaPython
创建固定大小数组int[] arr = new int[26];arr = [0] * 26
字符转索引c - 'a'ord(c) - ord('a')
数组比较Arrays.equals(arr1, arr2)arr1 == arr2

2. 字符串处理

操作JavaPython
字符串长度s.length()len(s)
获取字符s.charAt(index)s[index]
字符串转字符数组s.toCharArray()list(s)

3. 集合/计数器操作

操作JavaPython
计数器需要手动实现数组collections.Counter
添加元素map.put(key, value)counter[key] = value
删除元素map.remove(key)del counter[key]

五、实例演示

以测试用例 s = "cbaebabacd"p = "abc" 为例,演示滑动窗口过程:

初始状态

  • lenP = 3cntP = [1, 1, 1, 0, 0, ...] (a:1, b:1, c:1)

  • ans = []

步骤演示

步骤rights[right]cntS更新后left窗口内容频率匹配?结果移除字符
10'c'c:1-2窗口不足[]
21'b'b:1, c:1-1窗口不足[]
32'a'a:1, b:1, c:10"cba"[0]移除s[0]='c'
43'e'a:1, b:1, e:11"bae"[0]移除s[1]='b'
54'b'a:1, b:1, e:12"aeb"[0]移除s[2]='a'
65'a'a:1, b:1, e:13"eba"[0]移除s[3]='e'
76'c'a:1, b:1, c:14"bac"[0,6]移除s[4]='b'
87'a'a:1, c:1, a:15"aca"[0,6]移除s[5]='a'
98'c'a:1, c:1, c:16"cac"[0,6]移除s[6]='c'
109'd'a:1, c:1, d:17"acd"[0,6]移除s[7]='a'

最终结果[0, 6]

六、关键细节解析

1. 为什么使用固定大小为26的数组?

  • 题目假设字符串只包含小写英文字母,共26个

  • 数组索引直接对应字符,访问时间复杂度O(1)

  • 比使用HashMap更高效,不需要哈希计算

2. 滑动窗口的维护

  • 窗口大小固定:始终为 len(p)

  • 扩展窗口:右指针 right 向右移动,将新字符加入窗口

  • 收缩窗口:当窗口达到固定大小时,左指针 left 也需要向右移动(通过移除最左边的字符)

3. 边界条件处理

  • 如果 s.length() < p.length(),直接返回空列表

  • 当 left < 0 时,窗口还未达到固定大小,跳过比较

4. 频率数组比较

  • 使用 Arrays.equals(cntS, cntP) 比较两个频率数组

  • 时间复杂度为O(26),属于常数时间

七、复杂度分析

时间复杂度:O(n + m)

  • 统计 p 的频率:O(m),其中 m 是 p 的长度

  • 滑动窗口遍历 s:O(n),其中 n 是 s 的长度

  • 每次循环中的操作(更新计数、比较数组)都是 O(1)

空间复杂度:O(1)

  • 使用两个固定大小的数组(各26个元素)

  • 与输入字符串长度无关

八、优化技巧与变体

1. 使用变量记录匹配的字符数

java

class Solution {
    public List<Integer> findAnagrams(String s, String p) {
        List<Integer> result = new ArrayList<>();
        if (s.length() < p.length()) return result;
        
        int[] count = new int[26];
        for (char c : p.toCharArray()) {
            count[c - 'a']++;
        }
        
        int left = 0, right = 0;
        int matchCount = 0;
        
        while (right < s.length()) {
            // 右指针扩展窗口
            char rightChar = s.charAt(right);
            if (count[rightChar - 'a'] > 0) {
                matchCount++;
            }
            count[rightChar - 'a']--;
            right++;
            
            // 当窗口大小等于p的长度时
            if (right - left == p.length()) {
                if (matchCount == p.length()) {
                    result.add(left);
                }
                
                // 左指针收缩窗口
                char leftChar = s.charAt(left);
                if (count[leftChar - 'a'] >= 0) {
                    matchCount--;
                }
                count[leftChar - 'a']++;
                left++;
            }
        }
        
        return result;
    }
}

2. 使用单个计数数组(差值统计)

python

class Solution:
    def findAnagrams(self, s: str, p: str) -> List[int]:
        result = []
        if len(s) < len(p):
            return result
        
        # 初始化计数数组(统计p和当前窗口的差值)
        count = [0] * 26
        for ch in p:
            count[ord(ch) - ord('a')] += 1
        
        # 记录有多少个字符的计数不为0
        non_zero_count = sum(1 for c in count if c != 0)
        
        left, right = 0, 0
        
        while right < len(s):
            # 右指针扩展窗口
            right_char = s[right]
            idx = ord(right_char) - ord('a')
            count[idx] -= 1
            
            if count[idx] == 0:
                non_zero_count -= 1
            elif count[idx] == -1:
                non_zero_count += 1
            
            right += 1
            
            # 当窗口大小等于p的长度时
            if right - left == len(p):
                if non_zero_count == 0:
                    result.append(left)
                
                # 左指针收缩窗口
                left_char = s[left]
                idx = ord(left_char) - ord('a')
                count[idx] += 1
                
                if count[idx] == 0:
                    non_zero_count -= 1
                elif count[idx] == 1:
                    non_zero_count += 1
                
                left += 1
        
        return result

九、常用函数积累

Java 常用函数

java

// 字符串操作
String s = "hello";
int len = s.length();                  // 字符串长度
char c = s.charAt(0);                  // 获取指定位置的字符
char[] chars = s.toCharArray();        // 字符串转字符数组

// 数组操作
int[] arr = new int[26];
Arrays.fill(arr, 0);                   // 填充数组
Arrays.equals(arr1, arr2);             // 比较两个数组
Arrays.toString(arr);                  // 数组转字符串

// 列表操作
List<Integer> list = new ArrayList<>();
list.add(element);                     // 添加元素
list.size();                           // 获取列表大小

Python 常用函数

python

# 字符串操作
s = "hello"
length = len(s)                        # 字符串长度
char = s[0]                            # 获取指定位置的字符
char_list = list(s)                    # 字符串转列表

# 列表操作
arr = [0] * 26
arr == other_arr                       # 比较两个列表
str(arr)                               # 列表转字符串

# 内置函数
ord(c)                                 # 字符转ASCII码
chr(code)                              # ASCII码转字符
sum(iterable)                          # 求和

# collections模块
from collections import Counter
counter = Counter("hello")             # 创建计数器
counter == other_counter               # 比较两个计数器

十、总结

核心要点

  1. 固定窗口滑动:窗口大小固定为 p 的长度,在 s 上滑动

  2. 字符频率统计:用数组记录字符出现次数,判断是否为字母异位词

  3. 高效比对:通过比较频率数组而不是排序字符串来判断异位词

面试常见问题

  1. 为什么使用数组而不是HashMap?

    • 字符集固定(26个小写字母)时,数组访问更快(O(1))

    • 数组不需要哈希计算,更高效

  2. 如何处理大写字母或Unicode字符?

    • 对于大写字母,数组大小可以扩展到52(大小写各26)

    • 对于Unicode字符,可以使用HashMap

  3. 这个算法是否会漏掉某些异位词?

    • 不会。滑动窗口检查了 s 中所有长度为 len(p) 的子串

  4. 最坏情况下的时间复杂度?

    • O(n + m),其中 n 是 s 的长度,m 是 p 的长度

  5. 如果 p 很长怎么办?

    • 算法仍然有效,但初始统计 p 的频率需要 O(m) 时间

扩展思考

  1. 类似问题

    • 最小覆盖子串(LeetCode 76)

    • 字符串的排列(LeetCode 567)

    • 找到字符串中所有字母异位词(本题)

  2. 变体问题

    • 允许最多k个不同字符的异位词

    • 寻找最长的异位词子串

  3. 实际应用

    • 文本搜索和模式匹配

    • DNA序列分析

    • 数据压缩和加密

掌握固定窗口滑动+字符频率统计的解法,不仅能够解决字母异位词问题,还能够解决一系列类似的字符串匹配问题,是面试中非常重要的算法技巧。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值