深入理解字母异位词查找算法:从基础到优化

131 阅读4分钟

深入理解字母异位词查找算法:从基础到优化

引言

大家好!今天我们来深入探讨一个经典的算法问题:如何在一个字符串中找到所有字母异位词的起始索引。这个问题不仅是常见的面试题,也是理解滑动窗口技术和字符频次统计的绝佳例子。

问题描述

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

示例:

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

解题思路

1. 理解字母异位词

字母异位词是指两个字符串包含相同的字母,且每个字母出现的次数也相同。例如,"abc" 和 "bca" 是字母异位词。

2. 滑动窗口技术

我们将使用滑动窗口技术来解决这个问题:

  • 窗口大小固定为模式串 p 的长度
  • 在源字符串 s 上滑动这个窗口
  • 对每个窗口内的字符频次进行统计和比较

3. 使用 Map 进行字符频次统计

我们将使用两个 Map:

  • targetMap: 存储模式串 p 的字符频次
  • windowMap: 存储当前窗口的字符频次

代码实现

让我们一步步实现这个算法:

/**
 * @param {string} s
 * @param {string} p
 * @return {number[]}
 */
var findAnagrams = function(s, p) {
    if (s.length < p.length) return [];
    
    const targetMap = new Map();
    const windowMap = new Map();
    const result = [];
    
    // 统计模式串 p 的字符频次
    for (let char of p) {
        targetMap.set(char, (targetMap.get(char) || 0) + 1);
    }
    
    // 处理第一个窗口
    for (let i = 0; i < p.length; i++) {
        const char = s[i];
        windowMap.set(char, (windowMap.get(char) || 0) + 1);
    }
    
    // 检查第一个窗口是否匹配
    if (isEqual(windowMap, targetMap)) {
        result.push(0);
    }
    
    // 处理剩余的窗口
    for (let i = p.length; i < s.length; i++) {
        const removeChar = s[i - p.length];
        const addChar = s[i];
        
        // 移除旧字符
        let count = windowMap.get(removeChar);
        if (count === 1) {
            windowMap.delete(removeChar);
        } else {
            windowMap.set(removeChar, count - 1);
        }
        
        // 添加新字符
        windowMap.set(addChar, (windowMap.get(addChar) || 0) + 1);
        
        // 检查当前窗口是否匹配
        if (isEqual(windowMap, targetMap)) {
            result.push(i - p.length + 1);
        }
    }
    
    return result;
};

function isEqual(map1, map2) {
    if (map1.size !== map2.size) return false;
    
    for (let [key, value] of map1) {
        if (map2.get(key) !== value) {
            return false;
        }
    }
    return true;
}

代码解析

  1. 初始化

    • 创建 targetMapwindowMap 来存储字符频次
    • 创建 result 数组来存储匹配的起始索引
  2. 处理模式串

    • 遍历 p,统计每个字符的频次存入 targetMap
  3. 处理第一个窗口

    • 遍历 s 的前 p.length 个字符,统计频次存入 windowMap
    • 检查第一个窗口是否匹配
  4. 滑动窗口处理

    • 遍历剩余的字符
    • 移除窗口左边的字符,更新 windowMap
    • 添加窗口右边的新字符,更新 windowMap
    • 检查当前窗口是否匹配
  5. 匹配检查

    • 使用 isEqual 函数比较 windowMaptargetMap

优化过程

在实现过程中,我们进行了几次重要的优化:

  1. 减少重复操作: 原始代码中,我们在移除字符时有两次 get 操作:

    windowMap.set(removeChar, windowMap.get(removeChar) - 1);
    if (windowMap.get(removeChar) === 0) {
        windowMap.delete(removeChar);
    }
    

    优化后:

    let count = windowMap.get(removeChar);
    if (count === 1) {
        windowMap.delete(removeChar);
    } else {
        windowMap.set(removeChar, count - 1);
    }
    

    这样我们只需要一次 get 操作,提高了效率。

  2. 使用描述性变量名: 使用 removeCharaddChar 替代原来的 s[i-p.length]s[i],提高了代码可读性。

  3. 修复边界条件: 在代码开始添加 if (s.length < p.length) return [];,处理特殊情况。

  4. 优化比较函数isEqual 函数首先比较 Map 的大小,快速排除不可能相等的情况。

时间复杂度分析

  • 总体时间复杂度:O(n),其中 n 是字符串 s 的长度
  • 空间复杂度:O(k),其中 k 是字符集的大小(在这个问题中,通常 k <= 26)

总结

通过这个问题,我们学习了:

  1. 滑动窗口技术的应用
  2. 使用 Map 进行高效的字符频次统计
  3. 如何优化代码以提高效率和可读性

这个算法不仅解决了字母异位词查找问题,也为我们处理类似的字符串匹配问题提供了思路。希望这篇文章对你有所帮助!如果你有任何问题或想法,欢迎在评论区讨论。

进一步思考

  1. 如何处理大规模数据?
  2. 如果需要处理 Unicode 字符,我们的算法需要如何调整?
  3. 这个算法可以如何应用到实际的项目中?

感谢阅读,编码愉快!