LeetCode 热题 100:哈希

69 阅读4分钟

哈希表(Hash Table)是算法题中最常用、最高效的数据结构之一,尤其适用于快速查找、去重、分组等场景。在 LeetCode 热题 100 中,有三道极具代表性的哈希表题目:两数之和字母异位词分组最长连续序列。本文将逐一解析它们的核心思路,并提供简洁高效的 JavaScript 实现。


1. 两数之和(Two Sum)

题目链接

LeetCode 1. 两数之和

题目描述

给定一个整数数组 nums 和一个目标值 target,请在数组中找出和为目标值的两个整数,并返回它们的下标。
你可以假设每种输入只对应一个答案,且同一个元素不能使用两次

解题思路

暴力解法使用双重循环,时间复杂度为 O(n2)O(n^2)。但借助哈希表,我们可以将查找时间从 O(n)O(n) 优化到 O(1)O(1)

核心思想:在遍历数组的同时,将每个元素 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);
    }
};

💡 注意:由于题目保证有唯一解,循环无需显式终止条件。

复杂度分析

  • 时间复杂度O(n)O(n),仅需一次遍历。
  • 空间复杂度O(n)O(n),哈希表最多存储 nn 个元素。

2. 字母异位词分组(Group Anagrams)

题目链接

LeetCode 49. 字母异位词分组

题目描述

给你一个字符串数组 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('') 更直观且避免逗号干扰。

复杂度分析

  • 时间复杂度O(nklogk)O(n \cdot k \log k),其中 nn 为字符串数量,kk 为平均字符串长度(主要开销在排序)。
  • 空间复杂度O(nk)O(n \cdot k),用于存储所有字符串及其分组。

3. 最长连续序列(Longest Consecutive Sequence)

题目链接

LeetCode 128. 最长连续序列

题目描述

给定一个未排序的整数数组 nums,找出其中数字连续的最长序列的长度。
要求算法时间复杂度为 O(n)O(n),且序列元素在原数组中无需连续

解题思路

若使用排序,时间复杂度为 O(nlogn)O(n \log n),不符合要求。我们转而使用 哈希集合(Set) 实现 O(1)O(1) 的成员判断。

关键优化只从“连续序列的起点”开始扩展。如何判断起点?—— 若 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;
};

🔍 方法二更巧妙,但理解成本略高;方法一逻辑清晰,推荐优先掌握。

复杂度分析

  • 时间复杂度O(n)O(n),每个元素最多被访问常数次。
  • 空间复杂度O(n)O(n),哈希集合或映射存储所有元素。

总结对比

题目核心技巧使用的数据结构
两数之和边遍历边查找补数Map
字母异位词分组排序作为统一 key 进行分组Map
最长连续序列从起点出发 + Set 快速判断连续性Set / Map

这三道题分别体现了哈希表在 快速查找等价类分组区间合并/去重优化 中的强大能力。掌握这些模式,不仅能高效攻克 LeetCode 难题,也能在实际开发中写出更优雅、高性能的代码。


希望这篇解析对你有帮助!如果你喜欢这类深入浅出的算法讲解,欢迎关注我的 LeetCode 系列文章 👋