哈希表(Hash Table)是算法题中最常用、最高效的数据结构之一,尤其适用于快速查找、去重、分组等场景。在 LeetCode 热题 100 中,有三道极具代表性的哈希表题目:两数之和、字母异位词分组 和 最长连续序列。本文将逐一解析它们的核心思路,并提供简洁高效的 JavaScript 实现。
1. 两数之和(Two Sum)
题目链接
题目描述
给定一个整数数组 nums 和一个目标值 target,请在数组中找出和为目标值的两个整数,并返回它们的下标。
你可以假设每种输入只对应一个答案,且同一个元素不能使用两次。
解题思路
暴力解法使用双重循环,时间复杂度为 。但借助哈希表,我们可以将查找时间从 优化到 。
核心思想:在遍历数组的同时,将每个元素 nums[i] 及其下标 i 存入哈希表。对于当前元素,检查 target - nums[i] 是否已存在于表中。若存在,说明找到了答案。
代码实现
/**
* @param {number[]} nums
* @param {number} target
* @return {number[]}
*/
var twoSum = function(nums, target) {
const map = new Map();
for (let i = 0; ; i++) {
const complement = target - nums[i];
if (map.has(complement)) {
return [map.get(complement), i];
}
map.set(nums[i], i);
}
};
💡 注意:由于题目保证有唯一解,循环无需显式终止条件。
复杂度分析
- 时间复杂度:,仅需一次遍历。
- 空间复杂度:,哈希表最多存储 个元素。
2. 字母异位词分组(Group Anagrams)
题目链接
题目描述
给你一个字符串数组 strs,请将字母异位词组合在一起。
字母异位词是指由相同字母以不同顺序组成的单词(如 "eat"、"tea"、"ate")。
解题思路
判断两个字符串是否为异位词,关键在于它们的字符组成是否一致。一种高效的做法是:将字符串排序后作为统一的“签名”(key)。所有异位词排序后结果相同,自然可以归为一组。
我们使用 Map,以排序后的字符串为键,原始字符串列表为值。
代码实现
/**
* @param {string[]} strs
* @return {string[][]}
*/
var groupAnagrams = function(strs) {
const map = new Map();
for (const str of strs) {
const key = str.split('').sort().join(''); // 排序生成 key
if (map.has(key)) {
map.get(key).push(str);
} else {
map.set(key, [str]);
}
}
return Array.from(map.values());
};
✅ 也可写作
Array.from(str).sort().toString(),但join('')更直观且避免逗号干扰。
复杂度分析
- 时间复杂度:,其中 为字符串数量, 为平均字符串长度(主要开销在排序)。
- 空间复杂度:,用于存储所有字符串及其分组。
3. 最长连续序列(Longest Consecutive Sequence)
题目链接
题目描述
给定一个未排序的整数数组 nums,找出其中数字连续的最长序列的长度。
要求算法时间复杂度为 ,且序列元素在原数组中无需连续。
解题思路
若使用排序,时间复杂度为 ,不符合要求。我们转而使用 哈希集合(Set) 实现 的成员判断。
关键优化:只从“连续序列的起点”开始扩展。如何判断起点?—— 若 num - 1 不在集合中,则 num 是一个新序列的起点。
这样,每个数字最多被访问两次(一次作为非起点被跳过,一次作为起点被遍历),整体仍为线性时间。
代码实现
方法一:起点扩展法
/**
* @param {number[]} nums
* @return {number}
*/
var longestConsecutive = function(nums) {
const set = new Set(nums);
let maxLength = 0;
for (const num of set) {
if (!set.has(num - 1)) { // 确保 num 是序列起点
let current = num;
let length = 1;
while (set.has(current + 1)) {
current++;
length++;
}
maxLength = Math.max(maxLength, length);
}
}
return maxLength;
};
方法二:并查思想,动态维护区间长度
var longestConsecutive = function(nums) {
const map = new Map();
let max = 0;
for (const num of nums) {
if (map.has(num)) continue; // 跳过重复元素
const left = map.get(num - 1) || 0; // 左侧连续长度
const right = map.get(num + 1) || 0; // 右侧连续长度
const curLen = left + 1 + right; // 合并后的新长度
map.set(num, curLen);
max = Math.max(max, curLen);
// 更新区间两端的长度(用于后续合并)
map.set(num - left, curLen);
map.set(num + right, curLen);
}
return max;
};
🔍 方法二更巧妙,但理解成本略高;方法一逻辑清晰,推荐优先掌握。
复杂度分析
- 时间复杂度:,每个元素最多被访问常数次。
- 空间复杂度:,哈希集合或映射存储所有元素。
总结对比
| 题目 | 核心技巧 | 使用的数据结构 |
|---|---|---|
| 两数之和 | 边遍历边查找补数 | Map |
| 字母异位词分组 | 排序作为统一 key 进行分组 | Map |
| 最长连续序列 | 从起点出发 + Set 快速判断连续性 | Set / Map |
这三道题分别体现了哈希表在 快速查找、等价类分组 和 区间合并/去重优化 中的强大能力。掌握这些模式,不仅能高效攻克 LeetCode 难题,也能在实际开发中写出更优雅、高性能的代码。
希望这篇解析对你有帮助!如果你喜欢这类深入浅出的算法讲解,欢迎关注我的 LeetCode 系列文章 👋