java用哈希表实现LeetCode 438的滑动窗口算法

38 阅读3分钟

使用哈希表实现LeetCode 438(找到字符串中所有字母异位词)的滑动窗口算法,适用于字符串包含任意字符(不仅限于小写字母)的场景。核心思路是通过哈希表记录字符频率,结合固定大小的滑动窗口判断子串是否为异位词。

实现代码

import java.util.*;

public class FindAnagramsWithHash {
    public List<Integer> findAnagrams(String s, String p) {
        List<Integer> result = new ArrayList<>();
        int sLen = s.length();
        int pLen = p.length();

        // 边界条件:若s长度小于p,直接返回空列表
        if (sLen < pLen) {
            return result;
        }

        // 哈希表存储p中每个字符的频率
        Map<Character, Integer> pFreq = new HashMap<>();
        // 哈希表存储当前滑动窗口中每个字符的频率
        Map<Character, Integer> windowFreq = new HashMap<>();

        // 初始化p的频率表
        for (char c : p.toCharArray()) {
            pFreq.put(c, pFreq.getOrDefault(c, 0) + 1);
        }

        // 初始化滑动窗口的初始状态(前pLen个字符)
        for (int i = 0; i < pLen; i++) {
            char c = s.charAt(i);
            windowFreq.put(c, windowFreq.getOrDefault(c, 0) + 1);
        }

        // 检查初始窗口是否为p的异位词
        if (isFreqEqual(pFreq, windowFreq)) {
            result.add(0);
        }

        // 滑动窗口:从pLen位置开始向右移动
        for (int right = pLen; right < sLen; right++) {
            // 1. 加入右侧新字符(扩展窗口右边界)
            char newChar = s.charAt(right);
            windowFreq.put(newChar, windowFreq.getOrDefault(newChar, 0) + 1);

            // 2. 移除左侧溢出字符(收缩窗口左边界,保持窗口大小为pLen)
            char leftChar = s.charAt(right - pLen);
            int count = windowFreq.get(leftChar);
            if (count == 1) {
                windowFreq.remove(leftChar); // 频率为1时,移除后频率为0,直接删除键
            } else {
                windowFreq.put(leftChar, count - 1); // 频率减1
            }

            // 3. 检查当前窗口是否与p的频率匹配
            if (isFreqEqual(pFreq, windowFreq)) {
                // 当前窗口的起始索引 = right - pLen + 1
                result.add(right - pLen + 1);
            }
        }

        return result;
    }

    // 辅助函数:判断两个频率哈希表是否完全相等
    private boolean isFreqEqual(Map<Character, Integer> map1, Map<Character, Integer> map2) {
        // 若键的数量不同,直接不匹配
        if (map1.size() != map2.size()) {
            return false;
        }
        // 逐个检查键对应的频率是否相同
        for (char c : map1.keySet()) {
            // 若map2中没有该键,或频率不同,则不匹配
            if (!map2.containsKey(c) || !map1.get(c).equals(map2.get(c))) {
                return false;
            }
        }
        return true;
    }
}

关键思路解析

  1. 哈希表的作用
  • ​pFreq​​ 存储字符串 ​​p​​ 中每个字符的出现次数(频率)。
  • ​windowFreq​​ 存储当前滑动窗口(长度为 ​​pLen​​)中每个字符的频率。
  1. 滑动窗口逻辑
  • 窗口大小固定为 ​​pLen​​(异位词长度必与 ​​p​​ 相同)。
  • 右指针 ​​right​​ 从 ​​pLen​​ 开始移动,每次移动时:
  • 加入右侧新字符 ​​s[right]​​,更新 ​​windowFreq​​。
  • 移除左侧溢出字符 ​​s[right - pLen]​​(保证窗口长度不变),更新 ​​windowFreq​​(若频率为0则直接删除键,避免干扰比较)。
  1. 频率匹配判断
  • 自定义 ​​isFreqEqual​​ 函数,通过对比两个哈希表的键数量和对应频率,判断是否为异位词。

时间复杂度与空间复杂度

  • 时间复杂度:O(sLen * k),其中 ​​sLen​​ 是 ​​s​​ 的长度,​​k​​ 是 ​​p​​ 中不同字符的数量(哈希表键的数量)。每个字符最多被加入和移除窗口各一次,每次频率比较需遍历 ​​k​​ 个键。
  • 空间复杂度:O(k),两个哈希表最多存储 ​​k​​ 个键(​​k​​ 为 ​​p​​ 中不同字符的数量)。

适用场景

当字符串 ​​s​​ 和 ​​p​​ 包含 任意字符(如大写字母、数字、符号等)时,无法用固定大小的数组统计频率,此时哈希表实现更通用。例如:

  • 输入 ​​s = "aBcAbC"​​, ​​p = "abc"​​(包含大写字母)
  • 输入 ​​s = "12321"​​, ​​p = "321"​​(包含数字)

这种实现通过哈希表灵活记录字符频率,结合滑动窗口高效匹配异位词,兼顾了通用性和效率。