你不用慌,刚开始刷 LeetCode 热题 100 连第一题(两数之和)都不会,是所有编程新手的常态,这不是你能力的问题,而是还没掌握「解算法题的基本思路」和「前端视角的解题方法」。我会从最基础的步骤带你拆解这道题,帮你建立「从 0 到 1 解算法题」的思维,而不是直接给答案。
第一步:先读懂「两数之和」的题目(LeetCode 1. 两数之和)
先把题目拆成「人话」,明确输入、输出、核心要求:
- 输入 :一个整数数组
nums+ 一个目标值target; - 输出 :数组中两个不同位置 的数的下标,这两个数相加等于
target; - 要求:每个输入只有一个答案,同一个元素不能用两次。
- 示例 :输入
nums = [2,7,11,15], target = 9→ 输出[0,1](因为 2+7=9)。
第二步:从「最笨的方法」开始(新手先落地,再优化)
新手不要一上来就想「最优解」,先写「能跑通的代码」,再谈优化。这道题最容易理解的是「暴力枚举法」(两层循环):
思路
- 遍历数组中的每一个数
nums[i]; - 对于每个
nums[i],再遍历它后面的所有数nums[j](j > i,避免重复); - 检查
nums[i] + nums[j]是否等于target,如果是,直接返回[i,j]。
前端(JS)实现代码(带详细注释)
javascript
运行
javascript
/**
* @param {number[]} nums 输入的整数数组
* @param {number} target 目标和
* @return {number[]} 满足条件的两个数的下标
*/
var twoSum = function(nums, target) {
// 第一层循环:遍历每个元素,i是第一个数的下标
for (let i = 0; i < nums.length; i++) {
// 第二层循环:遍历i后面的元素,j是第二个数的下标(避免重复用同一个元素)
for (let j = i + 1; j < nums.length; j++) {
// 核心判断:两数之和等于目标值
if (nums[i] + nums[j] === target) {
// 找到答案,直接返回下标数组
return [i, j];
}
}
}
// 题目说必有解,这里只是兜底(实际不会执行)
return [];
};
// 测试用例(新手一定要写测试,验证自己的代码)
console.log(twoSum([2,7,11,15], 9)); // 输出 [0,1]
console.log(twoSum([3,2,4], 6)); // 输出 [1,2]
console.log(twoSum([3,3], 6)); // 输出 [0,1]
代码解释(新手逐行看)
for (let i = 0; i < nums.length; i++):遍历数组的每一个元素,i是第一个数的下标;for (let j = i + 1; j < nums.length; j++):j从i+1开始,确保不会重复使用同一个元素(比如不会出现i=0,j=0的情况);nums[i] + nums[j] === target:判断两数之和是否等于目标值,满足则返回下标;- 测试用例覆盖了「普通情况」「非相邻元素」「重复值」,验证代码的鲁棒性。
复杂度分析(大厂面试必说)
- 时间复杂度:O(n2)(n 是数组长度),因为两层循环,最坏情况要遍历 n∗(n−1)/2 次;
- 空间复杂度:O(1),只用到了几个变量,没有额外开辟大空间。
第三步:优化解法(前端高频考的「哈希表法」)
暴力法虽然好理解,但效率低(O(n2)),大厂面试会追问「能不能优化?」,这时候要讲「哈希表(Map)」的解法(时间复杂度 O(n))。
思路(新手记这个「核心逻辑」)
- 用一个
Map来「存已经遍历过的数 + 它的下标」(空间换时间); - 遍历数组时,对当前数
nums[i],计算「需要找的补数」:complement = target - nums[i]; - 检查
Map里有没有这个补数:- 如果有,说明之前遍历过这个补数,直接返回「补数的下标」和「当前下标 i」;
- 如果没有,把当前数
nums[i]和下标i存入Map,继续遍历。
JS 实现代码(带注释)
javascript
运行
javascript
var twoSum = function(nums, target) {
// 1. 创建一个Map,用来存「已遍历的数: 下标」
const map = new Map();
// 2. 遍历数组,i是当前下标,num是当前数
for (let i = 0; i < nums.length; i++) {
const num = nums[i];
// 3. 计算需要找的补数
const complement = target - num;
// 4. 检查Map里有没有这个补数
if (map.has(complement)) {
// 有则返回:补数的下标 + 当前下标
return [map.get(complement), i];
}
// 5. 没有则把当前数和下标存入Map
map.set(num, i);
}
// 兜底返回(题目保证有解)
return [];
};
// 测试用例
console.log(twoSum([2,7,11,15], 9)); // [0,1]
console.log(twoSum([3,2,4], 6)); // [1,2]
console.log(twoSum([3,3], 6)); // [0,1]
核心解释(新手重点理解)
Map的作用:把「找补数」的操作从「遍历数组(O (n))」变成「查 Map(O (1))」;- 为什么不会重复?因为先查补数,再存当前数,所以当前数不会和自己匹配(比如
nums=[3,3],遍历第一个 3 时,Map 为空,存入 Map;遍历第二个 3 时,补数是 3,Map 里有第一个 3 的下标 0,直接返回 [0,1]); - 复杂度:时间 O(n)(只遍历一次),空间 O(n)(最坏情况存 n-1 个数)。
第四步:新手刷这道题的「正确姿势」(比会写更重要)
- 先抄再理解:如果完全不会,先把暴力法的代码抄一遍,逐行改测试用例(比如把 nums 改成 [1,2,3],target 改成 5),看输出是否正确,理解每一行的作用;
- 自己写一遍:关掉答案,自己写暴力法,写完后用测试用例验证,遇到 bug(比如 j 从 0 开始导致重复),自己排查;
- 思考优化点:问自己「为什么暴力法慢?」「哪里可以省时间?」,再尝试写哈希表法;
- 复盘总结:这道题的核心是「空间换时间」,哈希表是前端解「数组 / 字符串匹配」的高频工具,记住这个思路(比如后续刷「字母异位词」「两数之和变种题」都能用)。
第五步:给你的「新手刷题小建议」(针对热题 100)
-
不要跳题:热题 100 是按「基础→进阶」排的,第一题是最基础的哈希表应用,吃透它再刷下一题(比如第二题两数相加是链表题,先把数组 / 哈希表的基础打牢);
-
每天只刷 1 道:新手不用贪多,每天把 1 道题的「暴力法 + 最优解」都搞懂,手写 2 遍,比一天刷 5 道半懂不懂的题有用;
-
记「模板」不记答案 :这道题的哈希表模板可以记下来:
javascript
运行
javascript// 哈希表解题模板(数组匹配类) const map = new Map(); for (let i = 0; i < arr.length; i++) { const need = target - arr[i]; if (map.has(need)) { return [map.get(need), i]; } map.set(arr[i], i); } -
用前端语言写:全程用 JS/TS 写,不用纠结 Python/Java 的写法,大厂前端面试只看你用前端语言的实现。
总结
- 新手不会第一题完全正常,核心是先从「暴力法」落地,再理解「最优解」,不要一开始就追求完美;
- 「两数之和」的核心考点是哈希表的空间换时间思想,这是前端算法的高频考点,必须吃透;
- 刷题的关键是「理解 + 手写 + 复盘」,而不是背答案,哪怕一天只搞懂 1 道题,也是有效进步。
什么是哈希表
哈希表(Hash Table)其实是前端开发中天天都在用的东西,只是你可能没意识到它的名字而已。我会用最通俗的比喻 + 前端实际例子,帮你彻底搞懂哈希表,以及它在「两数之和」里的作用。
第一步:先理解「哈希表」的核心概念(用生活例子比喻)
哈希表的本质是「键值对(Key-Value) 存储结构」,核心作用是:通过「键(Key)」快速找到「值(Value)」,就像我们生活中的「字典」或「通讯录」:
- 字典:通过「汉字(Key)」快速查到「释义(Value)」,不用从头翻;
- 通讯录:通过「名字(Key)」快速找到「手机号(Value)」,不用一个个找;
- 哈希表:通过「自定义的键」快速找到「对应的值」,时间复杂度接近 O(1)(瞬间找到)。
对比:为什么哈希表比「遍历数组」快?
- 遍历数组找值:比如在
[2,7,11,15]里找 7,需要从头数(第 2 个),最坏要找完整个数组(O(n)); - 哈希表找值:把数组存成
{2:0, 7:1, 11:2, 15:3},找 7 的话直接查哈希表[7],一步到位(O(1))。
第二步:前端里的「哈希表」就是这些东西
在 JavaScript 中,我们不用自己实现哈希表,直接用现成的「对象(Object)」或「Map」就行 ------ 这两个都是哈希表的具体实现:
| 前端哈希表载体 | 特点 | 常用场景 |
|---|---|---|
| Object(对象) | 键只能是字符串 / 数字 / Symbol | 简单场景(比如存数字、字符串键) |
| Map | 键可以是任意类型(数字、对象、函数等),有专门的 API(has/set/get) | 算法题 / 复杂场景(推荐用) |
举个前端实际例子(先感受用法)
javascript
运行
javascript
// 1. 用Object模拟哈希表(存「数:下标」)
const objMap = {};
// 存值:键是数字2,值是下标0
objMap[2] = 0;
// 取值:通过键2,瞬间拿到值0
console.log(objMap[2]); // 输出 0
// 检查是否有某个键
console.log(2 in objMap); // 输出 true
// 2. 用Map(算法题更推荐,语义更清晰)
const map = new Map();
// 存值:set(键, 值)
map.set(7, 1);
// 取值:get(键)
console.log(map.get(7)); // 输出 1
// 检查是否有某个键:has(键)
console.log(map.has(7)); // 输出 true
// 删除键:delete(键)
map.delete(7);
console.log(map.has(7)); // 输出 false
第三步:回到「两数之和」,理解哈希表的作用
我们再回头看「两数之和」的优化解法,就能明白哈希表到底在干嘛:
问题核心痛点(暴力法的问题)
暴力法要两层循环,是因为对每个数 nums[i],都要「遍历剩下的数」找 target - nums[i](补数),这个找的过程是 O(n),所以整体是 O(n2)。
哈希表的解决思路(空间换时间)
我们把「已经遍历过的数和它的下标」存到哈希表里,这样找补数的过程就从「遍历数组」变成「查哈希表」(O(1)),整体时间复杂度降到 O(n)。
用「通讯录」比喻这一过程
假设:
- 目标和 target = 9;
- 数组 nums = [2,7,11,15];
- 哈希表 = 你的「通讯录」,存的是「数:下标」。
遍历过程:
- 遍历第一个数 2(下标 0):
- 补数 = 9 - 2 = 7;
- 查通讯录:有没有 7?没有;
- 把 2 和下标 0 存进通讯录(通讯录:{2:0});
- 遍历第二个数 7(下标 1):
- 补数 = 9 - 7 = 2;
- 查通讯录:有 2!对应的下标是 0;
- 直接返回 [0,1],结束。
再看代码,就完全懂了
javascript
运行
javascript
var twoSum = function(nums, target) {
// 1. 初始化哈希表(空通讯录)
const map = new Map();
// 2. 遍历数组(一个个看数)
for (let i = 0; i < nums.length; i++) {
const num = nums[i]; // 当前数
const complement = target - num; // 补数(要找的数)
// 3. 查哈希表:有没有这个补数?
if (map.has(complement)) {
// 有就返回:补数的下标 + 当前下标
return [map.get(complement), i];
}
// 4. 没有就把当前数和下标存进哈希表
map.set(num, i);
}
return [];
};
第四步:新手必懂的哈希表核心考点(前端算法)
-
核心优势:「查询 / 插入 / 删除」的平均时间复杂度都是 O(1),这是它能优化算法的关键;
-
前端算法常见用法 :
- 计数(比如统计数组中每个数出现的次数);
- 去重(比如数组去重,存到 Map/Set 里);
- 快速匹配(比如两数之和、字母异位词);
- 缓存(比如存已经计算过的结果,避免重复计算);
-
和数组的区别 :
操作 数组 哈希表(Map) 按值找下标 O(n) O(1) 按下标找值 O(1) 不支持(哈希表没有下标) 存储结构 有序(下标) 无序(ES6 的 Map 是有序的,但算法题不关注)
总结
- 哈希表的核心是「键值对存储」,能通过键快速找到值 (O(1)),前端用
Object或Map实现; - 在「两数之和」中,哈希表的作用是「存已遍历的数和下标」,把找补数的时间从 O(n) 降到 O(1),实现算法优化;
- 哈希表是前端算法的「高频工具」,只要遇到「需要快速查找 / 匹配」的场景,优先想到它。
为什么哈希表比数组快,不应该都是从头开始找吗
你这个问题问到了哈希表的核心原理上!很多新手都会有这个疑问 ------「哈希表看起来也是存了一堆键值对,为什么找 7 不用从头遍历?」其实关键在于哈希表的「哈希函数」和「存储结构」,和数组的遍历完全不是一回事。我用最通俗的方式给你讲明白,不用看复杂的公式,只理解核心逻辑就行。
第一步:先推翻「哈希表需要从头找」的误区
数组的存储是「连续的内存地址」,比如 [2,7,11,15] 存的时候是按顺序放在内存里:
- 下标 0 → 地址 A → 存 2
- 下标 1 → 地址 A+1 → 存 7
- 下标 2 → 地址 A+2 → 存 11所以找 7 的时候,只能从地址 A 开始一个个查,直到找到地址 A+1,这就是「遍历」(O (n))。
而哈希表(比如 JS 的 Object/Map)的存储逻辑完全不同:它不是按顺序存,而是通过「哈希函数」把键(比如 7)直接转换成一个内存地址,存的时候直接放到这个地址,查的时候也直接用这个地址取 ------ 相当于给每个键分配了一个「专属储物柜」,找的时候不用挨个翻,直接去对应的柜子拿就行。
第二步:用「快递柜」比喻哈希表的核心逻辑
我们用生活中的「快递柜」来比喻,你一下就能懂:
- 存快递(存键值对) :
- 你有一个快递(键 = 7,值 = 1),快递柜有个「编码规则」(哈希函数):键的数字 % 10(取余)就是柜号;
- 计算 7 % 10 = 7 → 直接把快递放进 7 号柜;
- 同理,2 → 2 号柜,11 → 1 号柜,15 →5 号柜。
- 取快递(查值) :
- 要找键 7 对应的值,不用从 1 号柜翻到 10 号柜;
- 直接用编码规则算:7%10=7 → 打开 7 号柜,直接拿到快递(值 = 1);
- 整个过程只有「计算柜号 + 开柜」两步,和有多少个快递无关 ------ 这就是 O (1)(常数时间)。
对比数组的「货架」逻辑:
数组是「一排货架」,找 7 的时候要从第一个货架开始数:1 号(2)→2 号(7),数到第二个才找到,货架越长,数的次数越多(O (n))。
第三步:回到 JS 的哈希表(Object/Map),看实际逻辑
JS 里的 Object/Map 底层就是用这个原理实现的(细节比快递柜复杂,但核心一致):
- 当你执行
map.set(7, 1)时:JS 引擎会用「哈希函数」把键 7 转换成一个哈希值(比如一串数字),然后把值 1 存在这个哈希值对应的内存位置; - 当你执行
map.get(7)时:不会遍历所有键,而是再次用同一个哈希函数算出 7 的哈希值,直接去对应的内存位置取值 ------ 不管 Map 里有 10 个还是 1000 个键值对,这个过程都是「一次计算 + 一次取值」,时间复杂度就是 O (1)。
第四步:补充一个小细节(哈希冲突)
你可能会问:如果两个键的哈希值一样怎么办?比如键 7 和 17,按「%10」的规则都对应 7 号柜(这叫「哈希冲突」)。其实哈希表有解决冲突的办法(比如「链表法」):7 号柜里放一个小链表,先存 7→1,再存 17→2,查的时候到 7 号柜后,只需要遍历这个小链表(通常只有 1-2 个元素),整体还是接近 O (1),远快于遍历整个数组。
用代码验证:哈希表查值和数组查值的效率差异
我们用 10 万条数据做对比,你能直观看到区别:
javascript
运行
javascript
// 1. 准备数据:10万个数字的数组 + 对应的哈希表
const nums = [];
const hashMap = new Map();
for (let i = 0; i < 100000; i++) {
nums.push(i);
hashMap.set(i, i);
}
// 2. 数组找值(找99999):需要遍历10万次
console.time('数组查找');
const index = nums.findIndex(item => item === 99999);
console.timeEnd('数组查找'); // 耗时约几毫秒(随数据量增长)
// 3. 哈希表找值(找99999):一次到位
console.time('哈希表查找');
const value = hashMap.get(99999);
console.timeEnd('哈希表查找'); // 耗时约0.01毫秒(几乎不随数据量增长)
运行后你会发现:数组查找的耗时会随着数据量增加而明显变长,而哈希表几乎不变 ------ 这就是 O (n) 和 O (1) 的本质区别。
总结
- 哈希表查值不用从头遍历,核心是通过「哈希函数」把键直接转换成存储地址,查值时直接取对应地址的值;
- 数组是「顺序存储 + 下标访问」,按值找下标必须遍历;哈希表是「哈希地址存储 + 键访问」,按键找值一步到位;
- 「两数之和」用哈希表优化的核心,就是把「遍历数组找补数(O (n))」换成「哈希表查补数(O (1))」,从而把整体时间复杂度从 O (n²) 降到 O (n)。
把哈希函数、哈希冲突这两个哈希表的核心细节,用「快递柜」的比喻继续讲透,还是保持通俗不绕公式,同时补充前端算法题里用得到的知识点,面试被追问也能答上来~
一、哈希函数:哈希表的「快递柜编码规则」
哈希函数(Hash Function)是哈希表的核心 ,作用就是把任意类型的「键(Key)」,转换成一个固定长度的「哈希值(Hash Code)」,这个哈希值就是键在哈希表中的「存储地址标识」。
1. 核心要求(算法题 / 面试只记这 3 点)
- 确定性:同一个键,永远算出同一个哈希值(比如键 7,每次算都是 7,不能这次 7 下次 8);
- 高效性:计算速度要快,不能比遍历数组还慢;
- 均匀性:计算出的哈希值要均匀分布,尽量避免多个键算出同一个值(减少冲突)。
2. 举个前端能懂的「简易哈希函数」例子
算法题里我们接触的键大多是数字 / 字符串,用简单的算法就能做哈希函数,比如:
- 键是数字 :直接取余(
key % 数组长度),比如键 7、17、27,对 10 取余,得到 7、7、7(这就是后面要讲的冲突); - 键是字符串 :比如键
"abc",可以把每个字符的 ASCII 码相加再取余,a(97)+b(98)+c(99)=294,对 10 取余得 4,哈希值就是 4。
3. JS 里的哈希函数(不用自己实现,了解即可)
我们写算法题用的Map/Object,JS 引擎已经帮我们实现了高效的哈希函数:
- 对数字 / 字符串 / Symbol:直接生成唯一哈希值;
- 对对象 / 数组 :根据内存地址生成哈希值(所以两个内容相同的空对象
{} !== {},哈希值不同,Map 里会当成两个键)。✅ 前端算法题小技巧 :如果需要用对象当 Map 的键,先把对象转成字符串(比如JSON.stringify(obj)),避免因内存地址导致的哈希值不同。
二、哈希冲突:多个快递「撞柜」了怎么办?
不管哈希函数设计得多好,必然会出现「不同的键算出同一个哈希值」的情况 ,这就是哈希冲突 ------ 比如按key%10的规则,键 7 和 17 都算出 7,都要放 7 号快递柜,这就是撞柜了。
1. 为什么冲突不可避免?
核心是「鸽巢原理」:比如有 10 个快递柜(哈希值范围 0-9),但要存 11 个快递,必然有至少一个柜子要放 2 个快递,哈希表的哈希值范围是固定的,而键的数量是无限的,冲突是必然的。
2. 哈希表解决冲突的两种核心方法(面试高频,记名字 + 思路)
哈希表的底层实现,主要用这两种方法解决冲突,前端算法题不用实现,但要能说清思路,大厂面试偶尔会追问「哈希表怎么解决冲突?」
方法 1:链地址法(拉链法)------ 快递柜里「加层板」
这是最常用 的方法,也是 JSMap/Object底层用的方法,思路超简单:
- 把哈希表的每个存储位置(快递柜),变成一个链表 / 数组;
- 当多个键哈希值相同时,就把这些键值对依次存到同一个位置的链表中(相当于快递柜里加层板,一层放一个快递)。
用快递柜比喻链地址法:
- 规则:
key%10,柜号 0-9; - 存键 7(值 1):7 号柜空,直接放进去,7 号柜→
[7:1]; - 存键 17(值 2):哈希值也是 7,7 号柜加层板,7 号柜→
[7:1, 17:2]; - 存键 27(值 3):继续加层板,7 号柜→
[7:1, 17:2, 27:3]; - 查值:找 17 时,先算哈希值 7→打开 7 号柜→遍历柜内的小链表,找到 17 对应的值 2。
✅ 关键:链地址法的查询效率依然接近 O (1),因为哈希函数的均匀性,每个柜子里的链表都很短(通常 1-2 个元素),遍历的代价可以忽略。
方法 2:开放寻址法 ------ 撞柜了就「找隔壁空柜子」
思路:当某个位置被占了,就按一定规则依次查找下一个空的存储位置,把键值对存进去,常见的规则是「线性探测」(往后找 1 个,再找 1 个)。
用快递柜比喻开放寻址法:
- 规则:
key%10,柜号 0-9; - 存键 7(值 1):7 号柜空,直接放,7 号柜→
7:1; - 存键 17(值 2):7 号柜被占,找隔壁 8 号柜,空的,存 8 号柜→
17:2; - 存键 27(值 3):7 号、8 号都被占,找 9 号柜,空的,存 9 号柜→
27:3; - 查值:找 17 时,先算 7 号柜→没有→找 8 号柜→找到 17:2。
❌ 缺点:如果哈希表存得很满,会出现「连续占柜」的情况,查询时要找很多次,效率会降到 O (n),所以开放寻址法的哈希表不能存太满。
三、前端算法题里,哈希表的这些细节不用纠结!
讲完哈希函数和冲突,你可能会担心「刷题时要不要考虑这些?」,答案是完全不用,因为:
- JS 的
Map/Object已经帮我们实现了「高效哈希函数 + 链地址法解决冲突」,我们只用调用set/get/has就行,不用关心底层; - 算法题里考察的是哈希表的使用思路(空间换时间),不是底层实现,面试官也不会让前端手写哈希表;
- 我们刷的题中,哈希表的查询效率都可以直接按O(1) 算,不用考虑冲突的遍历代价。
四、哈希表核心知识点总结(面试答题版)
如果面试被追问「哈希表的原理」,把这几句按顺序说,简洁又到位,完全够前端的考察要求:
- 哈希表的核心是键值对存储 ,通过哈希函数把键转换成哈希值,作为存储地址,实现按键的 O (1) 查询;
- 由于鸽巢原理,哈希冲突不可避免,常用链地址法(拉链法) 解决,将冲突的键值对存到同一个地址的链表中;
- 前端中用
Object/Map实现哈希表,Map 支持任意类型的键,解决了 Object 键只能是字符串 / 数字的问题,算法题中更推荐用 Map。
最后再回到「两数之和」,串一遍完整逻辑
现在你懂了底层,再看这道题的哈希表解法,就是完整的逻辑链了:
- 初始化 Map(空的哈希表,有自己的哈希函数和冲突解决机制);
- 遍历数 7,计算补数 2→调用
map.has(2)→Map 内部用哈希函数算出 2 的哈希值→找到对应地址→地址为空,返回 false; - 调用
map.set(7,1)→Map 用哈希函数算出 7 的哈希值→存到对应地址; - 遍历数 2,计算补数 7→调用
map.has(7)→算出 7 的哈希值→找到对应地址→地址里有 7:1,返回 true→调用map.get(7)拿到 1,返回 [1,0]。
到这里,哈希表的核心原理就全懂了,从「怎么用」到「为什么快」再到「底层细节」,完全覆盖前端算法题和面试的要求~