LeetCode Hot 100 题解 · 哈希篇

LeetCode Hot 100 题解 · 哈希篇

  1. 本专题收录 LeetCode Hot 100 中所有 哈希 相关题目。
  2. 每道题提供最直接、最容易理解的解题思路,包含详细注释的代码实现,方便笔试和面试复习。

📑 目录

题号 题目 难度 核心思路
1 1.两数之和 🟢简单 暴力枚举两层循环是 O(n²),瓶颈在于"找搭档"这一步太慢。哈希表把查找降到 O(1):一边遍历一边问"我需要的那个数出现过没",出现过直接返回下标。
49 49.字母异位词分组 🟠中等 异位词的本质是字母组成相同 ,那就找一个"同组成映射到同 key"的规则:排序后的字符串,或者 26 个字母的计数串。哈希表按 key 收集,一组组捞出来就是答案
128 128.最长连续序列 🟠中等 O(n),排序 O(n log n) 不行。全丢进 HashSet,只从"没有前驱 x-1 的数"开始往后数 ------每个数最多被访问两遍,总代价还是 O(n)

前置知识:刷哈希题要熟的 Map / Set API

写哈希题时真正卡手的往往不是思路,是 Map 这些"取出时顺便处理"的 API 不熟。这里一次讲清。

声明与遍历

java 复制代码
// 声明:接口 Map 声明,实现用 HashMap(刷题 99% 用它)
Map<Integer, Integer> map = new HashMap<>();

// 存 / 取 / 删 / 判存在
map.put(1, 100);              // 存(重复 put 同一个 key 会覆盖旧 value)
Integer v = map.get(1);       // 取,key 不存在返回 null(不是抛异常)
map.remove(1);                // 删
map.containsKey(1);           // 判 key 存在
map.containsValue(100);       // 判 value 存在(O(n),刷题基本不用)
map.size();                   // 键值对个数

// 遍历三种,最常用 entrySet(key 和 value 都要时)
for (Map.Entry<Integer, Integer> e : map.entrySet()) {
    e.getKey(); e.getValue();
}
for (int k : map.keySet()) { }        // 只要 key
for (int val : map.values()) { }      // 只要 value

// Set 同理(Map 只留 key 的版本)
Set<Integer> set = new HashSet<>();
set.add(1); set.contains(1); set.remove(1);

getOrDefault:取出时兜底,不改表

"key 存在就取 value,不存在就返回我给的默认值" ,表本身不动。最典型的场景是计数

java 复制代码
// 统计每个数出现的次数
map.put(num, map.getOrDefault(num, 0) + 1);
// 等价的老写法,对比一下就知道它省了什么:
if (map.containsKey(num)) {
    map.put(num, map.get(num) + 1);
} else {
    map.put(num, 1);
}

computeIfAbsent:取出时顺手建组

"key 存在就取 value;不存在就执行 lambda 算出一个 value,存进去再返回" ------它和 getOrDefault 的区别就一个:没命中时会写入 。所以它专治 Map<K, List<V>> / Map<K, Set<V>> 这种"value 是个容器"的分组结构:

java 复制代码
// 分组:key -> 这组的所有元素
map.computeIfAbsent(key, k -> new ArrayList<>()).add(elem);
//      └ 不存在就建一个空列表存进去 ┘       └ 拿到列表直接 add
// 等价的老写法:
List<String> list = map.get(key);
if (list == null) {
    list = new ArrayList<>();
    map.put(key, list);
}
list.add(elem);

49 题"异位词分组"用的就是它:每组第一次遇到时建列表,之后往里 add

compute:取出旧值,算完写回(不管 key 在不在)

"按 key 取出旧值(可能是 null),执行 lambda 算出新值,写回" 。lambda 收到 (key, oldValue),返回值就是新 value。key 不存在时 oldValuenull,要自己处理 ------返回 null 则整个 entry 被删掉。适合"新值要同时看 key 和旧值"的计算:

java 复制代码
// 计数也能这么写,oldValue 为 null 时当成 0
map.compute(num, (k, old) -> old == null ? 1 : old + 1);

merge:计数的最短写法

"key 存在就把旧值和给定值用函数合并;不存在就存入给定值" 。lambda 收到 (oldValue, value)。它的妙处是没命中时 lambda 根本不执行,直接存 value ,不用像 compute 那样判 null

java 复制代码
// 计数一行流,49 题那个计数串也能用它写
map.merge(num, 1, Integer::sum);
//        │   │    └ 旧值和 1 求和:old + 1
//        │   └ 第一次遇到时直接存的值
//        └ key
// 等价于 getOrDefault 版:map.put(num, map.getOrDefault(num, 0) + 1);

mergecompute 的分工:"不存在时存默认值"用 merge(不碰 null),"新值依赖旧值做计算"用 compute 。计数这种场景 merge 最短,但 getOrDefault 版意图更直白,团队代码里两种都常见。

这几个 API 怎么选

API key 不存在时 会不会写表 典型场景
getOrDefault(k, defaultValue) 返回 defaultValue 不会 计数:put(k, getOrDefault(k,0)+1)
computeIfAbsent(k, fn) 执行 fn,结果存入再返回 分组:computeIfAbsent(k, k->new ArrayList<>()).add(x)
get(k) 返回 null 不会 只取不兜底(记得判 null
merge(k, v, fn) 直接存入 vfn 不执行) 最短计数:merge(k, 1, Integer::sum)
compute(k, fn) fn 收到 oldValue = null,需自判 新值要同时看 key 和旧值的计算

一句话记:计数用 getOrDefault 或更短的 merge,建组用 computeIfAbsent,"旧值 + 计算"用 compute,不确定 key 在不在还直接取的用 get + 手动判 null

三个 API 怎么选

API key 不存在时 会不会写表 典型场景
get(k) 返回 null 不会 只取不兜底(记得判 null
getOrDefault(k, defaultValue) 返回 defaultValue 不会 计数:put(k, getOrDefault(k,0)+1)
computeIfAbsent(k, fn) 执行 fn,结果存入再返回 value为集合时使用,分组:computeIfAbsent(k, k->new ArrayList<>()).add(elem)

一句话记:计数用 getOrDefault,建组用 computeIfAbsent,不确定 key 在不在还直接取的用 get + 手动判 null 。另外 Map 还有 mergecompute 等变体,逻辑是"取出旧值 + 函数计算 + 写回"三合一,刷题用得少,遇到再查即可。


1.两数之和

题目链接1.两数之和

题目描述 :给你一个整数数组 nums 和目标值 target,找出数组中和为 target 的两个整数,返回它们的下标。同一个元素不能重复用,题目保证恰好有一个答案。


思路一:哈希表(一遍遍历,边存边查"搭档")

核心思想

  1. 暴力做法是两层循环固定一个数、再线性找另一个,O(n²)。瓶颈很清楚:"找 target - nums[i] 在不在前面"这个查找动作太慢
  2. 哈希表正好把查找降到 O(1)。遍历到 nums[i] 时先问哈希表:target - nums[i] 出现过吗?出现过直接返回;没出现过把 nums[i] 存进去,留给后面的数当搭档。
  3. 先查再存 ,顺序不能反------先存的话同一个元素会被自己匹配上(比如 target = 6nums = [3, ...],3 会和自己配对)。

时间复杂度 : O ( n ) O(n) O(n),其中 n 为数组长度,一遍遍历,每次查/存哈希表 O(1)

空间复杂度 : O ( n ) O(n) O(n),哈希表最多存 n 个元素。

详细注释版本:

java 复制代码
class Solution {
    /**
     * 两数之和问题:给定一个整数数组 nums 和一个整数目标值 target,
     * 请你在该数组中找出和为目标值 target 的那两个整数,并返回它们的数组下标。
     * 你可以假设每种输入只会对应一个答案,且数组中同一个元素在答案里不能重复出现。
     *
     * 人话:
     * 其实就是要检测数对,因为对于某一个数而言我们有明确的目标,
     * 所以我们可以通过哈希表的方式进行前值的存储,便于后值的查询。
     * 
     * @param nums   输入的整数数组
     * @param target 目标和值
     * @return 两个整数的索引组成的数组,按出现顺序返回(前一个索引小于后一个)
     */
    public int[] twoSum(int[] nums, int target) {
        int n = nums.length; // 获取数组长度,用于循环遍历
        // 创建哈希表,键为数组元素值,值为该元素对应的索引,用于快速查找已遍历过的元素
        Map<Integer, Integer> mp = new HashMap<>();

        // 遍历数组中的每个元素
        for (int i = 0; i < n; ++i) {
            int x = nums[i]; // 将当前元素赋值给变量x,减少数组访问次数,优化性能

            // 计算目标值与当前元素的差值,即需要找到的另一个元素值
            int complement = target - x;
            // 检查哈希表中是否存在该差值:若存在,说明之前已遍历过该元素,直接返回两个索引
            if (mp.containsKey(complement)) {
                return new int[] { mp.get(complement), i };
            }

            // 若哈希表中不存在该差值,则将当前元素及其索引存入哈希表,供后续元素查找
            mp.put(x, i);
        }

        // 题目假设存在唯一解,此处为默认返回(实际不会执行到)
        return new int[] { -1, -1 };
        // retur new int[0];
    }
}

精简注释版本:

java 复制代码
class Solution {
    public int[] twoSum(int[] nums, int target) {
        Map<Integer, Integer> map = new HashMap<>(); // 值 -> 下标,存"已经出现过的数"
        for (int i = 0; i < nums.length; i++) {
            int need = target - nums[i]; // 我需要的搭档
            if (map.containsKey(need)) {
                return new int[]{map.get(need), i}; // 搭档在前面出现过,直接返回
            }
            map.put(nums[i], i); // 没找到,把自己存进去,等后面的数来配对
        }
        return new int[0]; // 题目保证有解,走不到这
    }
}

思路二:暴力枚举(固定一个,线性找另一个)

核心思想 :两层循环,外层固定 nums[i],内层从 i + 1 开始到 nums.length结束,找 nums[j] 使两者相加等于 target。逻辑最直白,但 O(n²) 在 n 大时会超时,面试时作为引出哈希解法的铺垫提一句即可。

时间复杂度 : O ( n 2 ) O(n^2) O(n2),其中 n 为数组长度。

空间复杂度 : O ( 1 ) O(1) O(1)。

java 复制代码
class Solution {
    public int[] twoSum(int[] nums, int target) {
        // 暴力解法:枚举所有可能的两个数
        // 看它们的和是否等于 target

        int n = nums.length; // 数组长度

        // 第一层循环:固定第一个数 nums[i]
        for (int i = 0; i < n; ++i) {

            // 第二层循环:从 i 的后一个位置开始找第二个数 nums[j]
            // 这样可以避免重复枚举,例如 (0,1) 和 (1,0) 只算一次
            for (int j = i + 1; j < n; ++j) {

                // 如果当前这两个数的和正好等于目标值
                if (nums[i] + nums[j] == target) {

                    // 返回这两个数的下标
                    return new int[] { i, j };
                }
            }
        }

        // 如果遍历完所有数对都没有找到
        // 按题意一般不会发生,这里只是为了代码完整性
        return new int[] { -1, -1 };
    }
}

49.字母异位词分组

题目链接49.字母异位词分组

题目描述 :给你一个字符串数组 strs,把字母异位词 (字母相同、顺序可以不同的词,如 eat / tea / ate)组合在一起,返回分组后的列表。不考虑输出顺序和组内顺序。


思路一:排序后的字符串作 key(异位词排序后必相同)

核心思想

  1. 分组问题的关键是找等价关系 :什么样的字符串算"同一类"?异位词的定义是字母组成相同,那把每个字符串的字符排序,异位词排序后的结果必然完全一致eat / tea / ate 都排成 aet)。
  2. 用哈希表:key = 排序后的串,value = 原串列表。遍历每个词,排好序找到自己的组,塞进去。最后把所有组捞出来就是答案。

时间复杂度 : O ( n ⋅ k log ⁡ k ) O(n \cdot k \log k) O(n⋅klogk),其中 n 为字符串个数,k 为字符串的最大长度------每个串排序 O(k log k)

空间复杂度 : O ( n ⋅ k ) O(n \cdot k) O(n⋅k),哈希表要存下所有字符串。

computeIfAbsent写法:

java 复制代码
class Solution {
    public List<List<String>> groupAnagrams(String[] strs) {
        Map<String, List<String>> m = new HashMap<>();
        for (String s : strs) {
            // 把 s 排序,作为哈希表的 key
            char[] sortedS = s.toCharArray();
            Arrays.sort(sortedS);
            // 排序后相同的字符串分到同一组
            // computeIfAbsent:如果 key 不在哈希表中,则插入一个新的 ArrayList
            m.computeIfAbsent(new String(sortedS), _ -> new ArrayList<>()).add(s);
        }
        // 哈希表的 value 保存分组后的结果
        return new ArrayList<>(m.values()); 
    }
}

普通getgetOrDefault写法:

java 复制代码
class Solution {
    /**
     * 可以以排序后的字符串作为key,如果有就加入,如果没有就新开再加入
     * 最后需要从HashMap到List赋值
     */
    public List<List<String>> groupAnagrams(String[] strs) {
        Map<String, List<String>> mp = new HashMap<>(); // 排序后的串 -> 原串列表
        for (String str : strs) {
            char[] arr = str.toCharArray(); // 暂存数组用于排序,因为java不能直接排序字符串
            Arrays.sort(arr); // 异位词排序后必相同,是这组词的"指纹"
            String key = new String(arr); // 作为 新key比较

            // 写法1:
            // if (!mp.containsKey(tmp)) {
            //     mp.put(key, new ArrayList<>());
            // }
            // mp.get(key).add(str);

            // 写法2:
            List<String> list = mp.getOrDefault(key, new ArrayList<String>()); // 有组就进组;没组就建组再进组
            list.add(str);
            mp.put(key, list);

        }
        // 写法1:
        // int n = mp.size();
        // List<List<String>> ans = new ArrayList<>();
        // for (String k : mp.keySet()) {
        //     ans.add(mp.get(k));
        // }
        // return ans;

        // 写法2:
        return new ArrayList<>(mp.values());
        // 等价:return new ArrayList<List<String>>(mp.values());
    }
}

思路二:字母计数作 key(26 维计数串代替排序)

核心思想

  1. 排序的目的是生成"指纹",但排序要 O(k log k)"字母组成相同"还有更直接的刻画:26 个字母各出现几次 ------统计计数,拼成 a3b1c2... 这样的串作 key,异位词的计数串也必然相同。
  2. 生成计数串只要 O(k),比排序省一个 log。但 Java 里要手动拼字符串,常数不小,长度短时实际提升有限,面试时作为"还能怎么优化"的加分项讲。

时间复杂度 : O ( n ⋅ k ) O(n \cdot k) O(n⋅k),其中 n 为字符串个数,k 为字符串的最大长度。

空间复杂度 : O ( n ⋅ k ) O(n \cdot k) O(n⋅k)。

java 复制代码
class Solution {
    public List<List<String>> groupAnagrams(String[] strs) {
        Map<String, List<String>> map = new HashMap<>();
        for (String str : strs) {
            int[] count = new int[26]; // 26 个字母的出现次数
            for (char c : str.toCharArray()) {
                ++count[c - 'a'];
            }
            // 计数拼成串当 key,比如 "1,0,2,...,0" ;异位词的计数串必相同
            StringBuilder sb = new StringBuilder();
            for (int cnt : count) {
                sb.append(cnt).append('#'); // 加分隔符,防止 2,18 和 21,8 拼出同一个串
            }
            map.computeIfAbsent(sb.toString(), k -> new ArrayList<>()).add(str);
        }
        return new ArrayList<>(map.values());
    }
}

注意 # 分隔符不能省:不加的话计数 [2,18][21,8] 会拼成同一个串 218,不同组成的词被错分到一组。


128.最长连续序列

题目链接128.最长连续序列

题目描述 :给你一个未排序的整数数组 nums,找出数字连续的最长序列 (数值上连续,不要求在数组中相邻,如 [100,4,200,1,3,2]1,2,3,4)的长度。要求时间复杂度 O(n)


思路一:HashSet + 只从序列起点数(没有前驱的数才配当起点)

核心思想

  1. O(n) 的要求直接堵死了排序(O(n log n))。数组顺序既然没用,先把所有数丢进 HashSet------去重 + O(1) 判断某个数存不存在,这两点是后面一切的前提。
  2. 朴素的数法:对每个数 x,从它开始 x+1x+2 ... 一直往 set 里查,数出序列长度。但这样有大量重复劳动------[1,2,3,4] 里从 1、2、3 出发各数一遍,总代价最坏 O(n²)
  3. 关键优化:只从"序列的起点"开始数------x 是起点,当且仅当 x - 1 不在 setx - 1 存在说明 x 是某个更长序列的中段,从它数必然是白费,直接跳过。这样每个数只被"它所在序列的一次完整扫描"访问到,每个数最多被访问两遍 (一遍判前驱、一遍被数到),总代价 O(n)

时间复杂度 : O ( n ) O(n) O(n),其中 n 为数组长度,每个数最多被访问两遍。

空间复杂度 : O ( n ) O(n) O(n),HashSet 存所有数。

java写法:

java 复制代码
class Solution {
    public int longestConsecutive(int[] nums) {
        Set<Integer> set = new HashSet<>();
        for (int num : nums) {
            set.add(num); // 去重 + O(1) 判存在
        }
        int longest = 0;
        for (int num : set) { // 遍历 set 而不是 nums,重复数只处理一次
            if (set.contains(num - 1)) {
                continue; // 有前驱说明 num 是序列中段,从这数必然重复劳动,跳过
            }
            // 走到这说明 num 是某段连续序列的起点(x-1 不存在)
            int cur = num;
            while (st.contains(++cur)) {
            }
            longest = Math.max(longest, cur - num);
        }
        return longest;
    }
}

C++写法:

cpp 复制代码
class Solution {
public:
    /*
     * 时间:2025年2月17日13:42:31
     * 位置:哈希计数,
     * 算法思路:若排序则nlogn不满足要求
     *    优化思路:若遍历nums,必定是O(n*n)=>遍历哈希表,O(n*1)
     *       在代码中每个num最多被访问两次["所处num-1的位置"],所以O(n)
     *  ->->->子数组考虑前缀和,子序列考虑哈希
     */
    int longestConsecutive(vector<int>& nums) {
        unordered_set<int> hashSet(nums.begin(), nums.end()); // nums->哈希集合
        int re = 0;
        for (int num : hashSet) {          // 枚举起点
            if (hashSet.contains(num - 1)) // count 起点更换为num-1
                continue;
            // else
            int end = num + 1; // 新起点
            while (hashSet.contains(end))
                ++end;               // 一直找到连续的终点 end-1
            re = max(re, end - num); // end-1 -num +1 = end-num
        }
        return re;
    }
};

面试时常被追问"为什么是 O(n) 不是 O(n²)":内层 while 不是对每个外层数都跑满,而是每个连续段只被完整扫一次 (只有起点能触发扫描)。[1..n] 这种最坏输入里,外层只有 1 是起点,内层扫一遍 n,其余数全部被 continue 跳过------均摊下来每个数 O(1)

思路二:排序后数连续段(好想但不满足题目要求)

核心思想 :排序后相同的数相邻、连续的数也相邻,线性扫一遍:和前一个相等就跳过(去重),大 1 就延长当前段,否则开新段。好写好想,但排序 O(n log n)题目明确要 O(n) 时这版只能当铺垫讲

时间复杂度 : O ( n log ⁡ n ) O(n \log n) O(nlogn),其中 n 为数组长度,瓶颈在排序。

空间复杂度 : O ( 1 ) O(1) O(1)(不计排序本身的开销,取决于语言实现)。

java 复制代码
class Solution {
    public int longestConsecutive(int[] nums) {
        if (nums.length == 0) return 0;
        Arrays.sort(nums); // 排完相等的数相邻、连续的数也相邻
        int longest = 1, cur = 1; // cur:以 nums[i] 结尾的连续段长度
        // 枚举每个位置作为 以 nums[i]结尾的连续段长度
        for (int i = 1; i < nums.length; ++i) {
            if (nums[i] == nums[i - 1]) {
                continue; // 重复数直接跳过,不影响当前段
            }
            if (nums[i] == nums[i - 1] + 1) {
                cur++; // 还在同一个连续段里,延长
            } else {
                cur = 1; // 前面断了,从自己开新段
            }
            longest = Math.max(longest, cur);
        }
        return longest;
    }
}

这版的价值是给你一个"能对拍"的朴素解法:思路一写完可以用它验证。注意 cur 的语义是以当前元素结尾的段长,这样状态转移只有"延长 / 重开"两种,不用回看。


总结:哈希篇三道题是同一个模式的三档难度------"把一个 O(n) 级的查找/判定动作压到 O(1)"。 1.两数之和 靠哈希表即时查"搭档出现过没";49.字母异位词 靠"指纹作 key"(排序串或计数串)把等价类归到一组;128.最长连续序列HashSetO(1) 判存在,再配"只从没有前驱的数起数"避免重复扫描。核心一句话:遇到"要在前面/全体里找某个特定值"或"要按等价关系分组",第一反应就是哈希表;而 O(n) 的硬要求下,还要想清楚每个元素到底会被访问几遍

相关推荐
罗西的思考2 小时前
DreamZero 与 DreamDojo:世界模型与策略的分层协同综合分析与对比
人工智能·算法·机器学习
玖玥拾2 小时前
LeetCode 219 存在重复元素 II
算法·leetcode·哈希算法·散列表
知无不研3 小时前
c语言中循环的介绍与简单应用
c语言·开发语言·算法·循环·for·while
a187927218314 小时前
【算法】动态规划第四篇:背包收官——min 哨兵、计数世界与组合排列分水岭
算法·leetcode·动态规划·dp·01背包·算法讲解·决策合并
2601_962297485 小时前
在python3中、下列输出变量a的正确写法是_2020超星大数据Python免费答案
数据结构·python·算法·编程·字符串操作
辰烨chenye5 小时前
LeetCode Hot 100 题解 · 子串篇
算法·leetcode·职场和发展
辰烨chenye5 小时前
LeetCode Hot 100 题解 · 堆篇
java·算法·leetcode
手写码匠5 小时前
华为云Flexus+DeepSeek征文|DeepSeek 应用监控告警体系实战:从“用户投诉才知道“到“故障前就发现“
人工智能·深度学习·算法·aigc
shehuiyuelaiyuehao6 小时前
算法29,前缀和,除自身以外的数组的乘积
java·数据结构·算法