深入理解字母异位词查找算法:从基础到优化
引言
大家好!今天我们来深入探讨一个经典的算法问题:如何在一个字符串中找到所有字母异位词的起始索引。这个问题不仅是常见的面试题,也是理解滑动窗口技术和字符频次统计的绝佳例子。
问题描述
给定两个字符串 s 和 p,找到 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;
}
代码解析
-
初始化:
- 创建
targetMap和windowMap来存储字符频次 - 创建
result数组来存储匹配的起始索引
- 创建
-
处理模式串:
- 遍历
p,统计每个字符的频次存入targetMap
- 遍历
-
处理第一个窗口:
- 遍历
s的前p.length个字符,统计频次存入windowMap - 检查第一个窗口是否匹配
- 遍历
-
滑动窗口处理:
- 遍历剩余的字符
- 移除窗口左边的字符,更新
windowMap - 添加窗口右边的新字符,更新
windowMap - 检查当前窗口是否匹配
-
匹配检查:
- 使用
isEqual函数比较windowMap和targetMap
- 使用
优化过程
在实现过程中,我们进行了几次重要的优化:
-
减少重复操作: 原始代码中,我们在移除字符时有两次
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操作,提高了效率。 -
使用描述性变量名: 使用
removeChar和addChar替代原来的s[i-p.length]和s[i],提高了代码可读性。 -
修复边界条件: 在代码开始添加
if (s.length < p.length) return [];,处理特殊情况。 -
优化比较函数:
isEqual函数首先比较 Map 的大小,快速排除不可能相等的情况。
时间复杂度分析
- 总体时间复杂度:O(n),其中 n 是字符串 s 的长度
- 空间复杂度:O(k),其中 k 是字符集的大小(在这个问题中,通常 k <= 26)
总结
通过这个问题,我们学习了:
- 滑动窗口技术的应用
- 使用 Map 进行高效的字符频次统计
- 如何优化代码以提高效率和可读性
这个算法不仅解决了字母异位词查找问题,也为我们处理类似的字符串匹配问题提供了思路。希望这篇文章对你有所帮助!如果你有任何问题或想法,欢迎在评论区讨论。
进一步思考
- 如何处理大规模数据?
- 如果需要处理 Unicode 字符,我们的算法需要如何调整?
- 这个算法可以如何应用到实际的项目中?
感谢阅读,编码愉快!