news 2026/10/21 14:28:38

代码随想录第七天:哈希表专题四题入门与实战全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
代码随想录第七天:哈希表专题四题入门与实战全解析

很多刷算法的朋友应该都有这种体验:跟着训练营按天打卡,前六天还在数组、链表、双指针里打转,到了第七天突然迎面撞上一组“哈希表”的题,题目看着都不长,做起来却处处是门道。没错,代码随想录第七天正是进入哈希表专题的第一天,这一天的四道题——有效的字母异位词、两个数组的交集、快乐数、两数之和——可以说是哈希表从理论到实战最经典的一组入门组合。我刷完这一天的感受很直接:前面几天的内容是在教你怎么把数据“摆整齐”,而哈希表这天开始,教的是怎么把数据“存得巧、找得快”。这篇文章就围绕第七天的完整学习过程,把每道题的解题思路、代码实现、易错点和我自己踩过的坑一次说清楚。

不管你是刚跟着训练营走到第七天的新手,还是准备面试想突击哈希表的老手,这四道题都值得静下心来吃透。它们分别对应了哈希表的三种形态:数组当哈希表、set去重、map存键值对,每一题都是一种独立的哈希用法。理解了这四道题,后面再遇到三数之和、四数相加、赎金信这类题目,你会明显感觉到地基是稳的。

1. 哈希表专题的第一天,到底在学什么

1.1 为什么第七天轮到哈希表

先聊一个大家可能都想过的问题:为什么哈希表专题被安排在第七天,而不是更早或者更晚?

前面几天的内容,数组篇解决的是“连续空间里的下标操作”,链表篇解决的是“非连续空间里的指针操作”,双指针法则是通过控制遍历方向来降低复杂度。这些内容有一个共同点:数据本身就是“有序、可定位”的。而哈希表解决的问题完全不同——它面向的是“判断一个元素是否在集合中出现过”这类查找问题。

举个例子,你要检查一段英文文案里有没有重复单词,如果用数组来做,你得把所有字母排序再逐个比较;但如果用哈希表,你只需要把每个字母存进对应的桶里,查询的时候直接算位置,平均时间复杂度是O(1)。第七天这套题的目的,就是把“哈希表能解决什么问题、什么时候用哪种哈希结构”这件事讲清楚。

从训练路线来看,这个位置也安排得很巧妙。前六天你已经熟悉了基本的遍历、指针移动、数组操作,此刻引入哈希表,正好可以用它来重构之前的一些问题,比如用哈希表优化暴力解。这种“旧题新解”的感觉,会让你很快意识到哈希表的实用价值。

1.2 哈希表的三种形态:数组、set、map怎么选

哈希表在代码里的落地形态主要有三种:数组、集合(set)、映射(map)。很多新手在这一天犯迷糊,根本原因就是不知道这三种形态的适用边界。

先说数组。数组本质上就是一张哈希表,下标是键,元素值是存储的内容。它的优势是极快的访问速度和极小的内存开销,但前提是键的范围必须是连续的、有限的、可枚举的。比如字符串里的字符,范围就是26个小写字母(或者ASCII的256个字符),这种情况下用数组做哈希表,既快又省。

再说set。set适合的场景是:我们只关心“某个值存不存在”,不关心它出现了几次。比如两个数组的交集,我们只需要知道哪些元素在两个数组里都出现过,不需要计数。set自动去重的特性在这里非常省心,不用手动写去重逻辑。

最后是map。map存的是“键值对”,也就是我们不仅关心一个元素是否存在,还关心它对应的信息。比如两数之和,我们需要知道“某个数出现过没有”,还要知道“它出现在哪个下标”,这时候就必须用map把值和下标成对存起来。

记住一个简易口诀:要计数用数组,要存在性用set,要存关联信息用map。这个选择逻辑,就是第七天整组题的核心主线。

1.3 第七天在整条训练路线中的坐标

如果只看单天的题目,你可能会觉得第七天就是“四道哈希表题”,但放在整个训练营的坐标系里看,这一天更像是一座桥。

前面的数组、链表题目培养了最基础的遍历思维,但从第七天开始,题目开始强调“空间换时间”。你不再满足于“能解出来”,而是要思考“怎么用更低的复杂度解出来”。两数之和这道题就是最好的例子:暴力解是O(n^2),用哈希表直接降到O(n)。这种思维转变,是为后面动态规划、图论里大量使用缓存表、记忆化搜索打基础的。

另外,第七天涉及的set去重思维,也会在后面的回溯算法、组合问题里反复出现;map的键值对思维,则是后面很多图论题目里邻接表的基础。所以第七天不只是一组孤立题目,它悄悄埋下了好多后续专题的种子。

2. 四道经典题拆解:每一题都在教一种哈希用法

2.1 有效的字母异位词:用数组当哈希表扫清计数

先看第一道题:给定两个字符串s和t,判断它们是不是字母异位词,也就是两个字符串里每个字母出现的次数是否完全一样。

我第一反应当然是排序比较——把两个字符串排序后逐位比较,能解,但排序的时间复杂度是O(n log n)。哈希表的做法是:用一个长度为26的数组,下标对应字母a到z,先遍历第一个字符串,每个字母出现就计数加一;再遍历第二个字符串,每个字母出现就计数减一。最后检查数组里有没有非零值,如果有就说明两个字符串的字母构成不一致。

核心代码逻辑大概是这样的:

bool isAnagram(string s, string t) { int record[26] = {0}; for (char c : s) record[c - 'a']++; for (char c : t) record[c - 'a']--; for (int i = 0; i < 26; i++) { if (record[i] != 0) return false; } return true; }

这段代码里最值得品的是c - 'a'这个操作。字符在计算机里本质是整数,'a'到'z'在ASCII里是连续的,减去'a'就能把字母映射到0到25的下标区间。这种“字符转下标”的套路,在哈希表相关的字符串题里几乎必用。

空间复杂度是O(1),因为数组长度固定为26,不管字符串多长都不变。时间复杂度是O(n),遍历三次字符串,但常数很小。这题的意义在于告诉新手:当键的范围是有限且已知时,数组就是最高效的哈希表,连set和map都不用上。

2.2 两个数组的交集:set自动去重的正确姿势

第二道题要求计算两个数组的交集,输出结果里的每个元素必须是唯一的。看到“唯一”这个词,第一反应就是去重,而set天然就是干这个的。

用set的解法思路很直接:把第一个数组的所有元素放进一个set,然后遍历第二个数组,如果当前元素存在于set中,说明是交集元素,把它放进结果set里。最终结果set自动去重,最后转成数组输出即可。

这里有个新手很容易犯的错误:直接在遍历第一个数组时就用set去重,然后遍历第二个数组时把重复的元素多次加入结果。虽然结果set也能兜底去重,但这样既浪费了操作时间,也说明你还没吃透set的用途。正确的理解是:第一个set负责快速判断存在性,第二个set负责收集结果并去重,一前一后分工明确。

用unordered_set和set有什么区别?性能上,unordered_set底层是哈希表,平均增删查是O(1);set底层是红黑树,操作是O(log n)。所以只要题目不要求有序输出,一律优先用unordered_set。如果你发现结果顺序不稳定导致测试不过,再考虑set。

这题还可以再进一步:如果两个数组都是排序好的,那用双指针也能做,时间复杂度是O(n + m)且不需要额外空间。但哈希表的做法更通用,不依赖于输入是否有序。两种方案各有适用场景,面试时能把两种都说出来,会比只会一种印象分高不少。

2.3 快乐数:用哈希表抓循环,别让程序死转

快乐数这道题,题目描述很简短:一个数,每次用各个位上的数字的平方和替换它自己,如果最终能变成1,就是快乐数;如果陷入无限循环,就不是。

很多新手在这道题上卡住的点不是算平方和,而是“怎么判断不是快乐数”。就一个正整数n,计算过程可能无穷无尽,如果你不设终止条件,程序就会一直跑下去。关键在于:如果一个数不是快乐数,它的变换序列必然会陷入循环,也就是说,某个数字会重复出现。

所以解法就是:用一个set记录已经出现过的数字,每次计算出新的数字后,先检查它是否已经在set里,如果在,说明循环了,直接返回false;如果新数字等于1,返回true;否则把新数字加入set,继续计算。

bool isHappy(int n) { unordered_set<int> seen; while (n != 1) { if (seen.count(n)) return false; seen.insert(n); int sum = 0; while (n) { sum += (n % 10) * (n % 10); n /= 10; } n = sum; } return true; }

这个循环检测的思路,在算法里其实有个更广的应用叫“快慢指针判圈”,但用哈希表实现更直观、更好理解。算平方和的时候,n % 10取出个位数字,n / 10去掉个位,这两个操作组合起来就能逐位分解一个整数。这个“拆数字”的技巧,后面在回文数、水仙花数这类题里还会用到。

还有个小细节:负数可以直接排除吗?题目给的n是正整数,所以不用考虑负数。但如果面试官追问,负数在平方和后可能变成正数,依然需要循环检测,不能简单判负就返回false。

2.4 两数之和:map是天然的“补数查询”工具

两数之和大概是LeetCode上知名度最高的一道题了:给定一个整数数组和一个目标值,找出数组中和为目标值的两个数,返回它们的下标。

暴力解谁都会,两层循环,时间复杂度O(n^2)。但用哈希表,一次遍历就能搞定。

核心思路:遍历数组时,对于每个元素nums[i],我们需要知道“target - nums[i]”这个补数是否在之前出现过,并且还要知道它的下标。这时候就需要map了,因为光是set只告诉我们“有没有”,map还能告诉我们“在哪里”。

vector<int> twoSum(vector<int>& nums, int target) { unordered_map<int, int> map; for (int i = 0; i < nums.size(); i++) { int complement = target - nums[i]; if (map.count(complement)) { return {map[complement], i}; } map[nums[i]] = i; } return {}; }

这题的顺序感很重要:先查补数,再把当前元素插进map。为什么不能先插入再查询?因为如果数组里有重复元素,先插入可能会让同一个下标被匹配两次。比如nums = [3, 3],target = 6,如果你先插入,遍历到第二个3时,map里已经有一个3了,查补数3能查到,返回的下标是{0, 1},这没问题。但如果数组是[3, 2, 4],target = 6,遍历到2时先插入2,再遍历4时查补数2,返回{1, 2},也没问题。真正要注意的是:如果先插入再查询,在极端情况下可能用同一个元素匹配自己,比如nums = [3],target = 6,虽然题目保证有解,但这种细节还是养成“先查后插”的习惯更稳妥。

另一个值得思考的问题:map的key存什么,value存什么?这题的key是元素值,value是下标。因为我们要“根据值查下标”,所以值必须作为key。这个“谁当key”的判断,是map类型题目的核心考点,想清楚这一点,后续很多map题你都能迎刃而解。

3. 实操复现:从读题到AC的完整过程

3.1 做题顺序与时间分配建议

第七天整体题量是四道,我建议按“异位词→交集→快乐数→两数之和”的顺序来做,而不是按题目编号顺序。原因很简单:这个顺序正好是“数组哈希→set哈希→set判环→map哈希”的难度递进,每一道都在前一道的基础上加一点新东西。

时间分配上,新手建议每道题先自己思考15到20分钟,想不出来再看题解。这四道题里,两数之和虽然是压轴,但却是最容易被秒杀的,因为暴力解很容易想,哈希优化也比较自然;反而是快乐数,如果之前没见过“循环检测”的思路,容易卡住。整体上,四道题控制在两到三个小时比较合理。

有一个实操习惯值得养成:每道题AC之后,再用暴力解法写一遍,并对比两种方案的耗时和空间。比如两数之和,你可以先写O(n^2)的暴力解,再去改成哈希表写法。这种对比会让你对“空间换时间”有更具体的感觉,而不是只停留在概念层面。

3.2 关键代码实现与边界处理

四道题的完整代码里,有几个边界细节是必须亲手敲一遍才能记住的。

有效的字母异位词,最容易漏的是字符串长度不等的情况。虽然长度不等一定不是异位词,可以提前return false,但有经验的写法也会先判断长度,因为长度不等时后续遍历没意义。不过即使不判断,数组计数也能正确输出结果,所以这个优化是锦上添花。

两个数组的交集,注意结果要转成vector输出。C++里unordered_set转vector,直接用迭代器构造即可:vector<int>(result.begin(), result.end())。这个语法不常用,容易忘,遇到就记一下。

快乐数的拆位循环,最容易写错的是内层while循环的条件。应该是while (n)而不是while (n > 0),其实两个等价,但要注意n在循环里会被不断除以10,最终变成0,所以循环条件是n非零。这道题如果在拆位时不小心把n提前改了,外层while的判断就会出问题,建议用一个临时变量来保存原始值。

两数之和,返回的是两个下标,顺序无所谓。有些人会纠结返回{map[complement], i}还是{i, map[complement]},测试都能过,但为了语义清楚,建议先返回先出现的下标,也就是补数的下标在前,当前下标在后。

还有一道隐藏的边界问题:如果数组里有负数怎么办?两数之和完全兼容负数,因为补数的计算同样是target - nums[i],负数照样能正确匹配。但快乐数如果输入是0,0的平方和是0,会永远循环在0上,不是快乐数。题目规定n是正整数,所以不用处理,但如果你在做扩展练习时遇到0,要能说出这个结论。

3.3 测试用例与自测方法

很多新手刷题只跑题目给的示例就提交,结果经常在隐藏用例上翻车。正确的做法是自己构造几组边界用例。

异位词这题,至少测三组:两个空字符串(true)、长度不同的字符串(false)、包含重复字母的字符串(如“aabbcc”和“abcabc”)。

交集这题,测一个数组为空的情况,测两个数组完全相同的情况,测有重复元素且交集结果需要去重的情况,比如nums1 = [1, 2, 2, 1],nums2 = [2, 2],期望输出[2]。

快乐数这题,19是标准的快乐数,建议手算一遍它的变换过程:19→82→68→100→1,体会一下什么是“最终收敛到1”。再测一个非快乐数,比如2:2→4→16→37→58→89→145→42→20→4,可以看到4再次出现,说明进入循环。这个手算过程能帮你验证代码逻辑是否正确。

两数之和,测数组里有重复元素的情况,测目标值由两个相同元素组成的情况(如[3, 3]和6),测包含负数和零的情况。

这些用例不用全写到代码里,但在脑子里过一遍,能帮你提前发现自己解法里的漏洞。我自己的习惯是写完代码后在草稿纸上手推一组用例,确认逻辑没有明显问题再提交,实测下来能减少不少无谓的WA(Wrong Answer)。

4. 常见问题排查与避坑实录

4.1 第一梯队报错:数组越界与哈希碰撞

第七天的报错,最常见的是Runtime Error,而且多半是数组越界。异位词这题,如果忘记把字符减去'a',直接用字符的ASCII值当下标,那record数组的长度26就会越界。比如字符'z'的ASCII值是122,直接访问record[122]就崩了。

另一个值得知道的概念是哈希碰撞。不同key映射到同一个哈希桶的情况,就是哈希碰撞。C++的unordered_set和unordered_map底层通过链地址法解决碰撞,所以一般不需要你手动处理。但如果你是自己在数组里做哈希映射,就要特别注意映射函数不要产生大量冲突。比如异位词这题,映射函数是c - 'a',在26个字母范围内是完美的一一映射,没有碰撞,这也是用数组做哈希表的一个隐藏优点。

如果你在交集中用了unordered_set,但发现结果顺序每次都变,不用慌,这是哈希表的正常行为,因为遍历顺序取决于哈希桶的排列。如果题目要求顺序输出,可以用set替代,或者输出前排序。

4.2 第二梯队问题:去重去错、死循环与漏判

交集题的一个典型错误是在遍历第二个数组时,没有判断当前元素是否已经在结果集里,导致重复添加。虽然最终转成vector前可以用set兜底去重,但如果你用的是vector直接收集结果,就会输出重复值。所以要么结果也用set,要么在添加前检查result里是否已经存在该元素。

快乐数最隐蔽的问题是死循环。你的程序看起来一直在跑,不是因为你忘记写循环检测,而是因为你忘记更新n的值,导致每次计算的sum都相同,外层while陷入死循环。这种情况IDE不会报错,只会一直转圈。遇到程序卡住,先检查是不是变量更新位置写错了,再考虑是不是逻辑缺了终止条件。

异位词这题,还有可能因为你用了map但没注意不存在key的默认行为。比如用map做计数时,map[c]++会自动把不存在的key初始化为0再自增,这没问题。但如果你用map.at(c)访问不存在的key,会抛异常。C++的map和unordered_map在访问不存在的key时行为不同,一个会创建默认值,一个会抛异常,这是很容易踩的坑,尤其是从Python转C++的人会特别不习惯。

4.3 哈希表相关的高频面试追问

这四道题如果出现在面试里,面试官大概率会追问几个问题,我提前整理一下。

两数之和的经典追问是:如果数组有序,能不能优化?这时的最优解其实是双指针,两个指针从两端向中间移动,根据和与目标值的大小调整指针位置,时间复杂度O(n),空间复杂度O(1),比哈希表的O(n)空间更优。这个追问考察的是你能不能根据输入特性调整算法,而不是只会一种解法。

快乐数的追问经常是:能不能不用额外空间?这就引出了快慢指针判圈法。一个指针每次走一步(计算一次平方和),另一个指针每次走两步,如果存在循环,快指针一定会追上慢指针。这个方法把空间复杂度降到O(1),是哈希表做法之外的另一种思路。

异位词的追问则是:如果字符集不只包含小写字母,而是包含Unicode字符怎么办?那数组就不适用了,因为字符集范围太大,用数组会浪费大量空间,这时候应该用map来动态记录。这个问题问的就是你能不能判断“什么时候该用哪种哈希结构”。

这些追问本质上都是在考察你对“hash思想”的理解深度,而不仅仅是记忆代码。

5. 第七天之后的路线与个人心得

5.1 从哈希到后续专题的衔接方法

第七天结束之后,我建议别急着往后赶进度,先花半小时做一件事情:把四道题里用到的哈希思想整理成一张自己的知识卡片,写到笔记里。每道题记录三行:题目类型、用的哈希结构、为什么用这个结构。

我当时的笔记大致是这样:异位词,数组哈希,因为字符集有限;交集,set,因为只关心存在性且需要去重;快乐数,set,因为要检测循环重复;两数之和,map,因为既要查存在性还要查下标。

这张卡片的价值,在后续刷三数之和、四数相加、赎金信时会直接体现出来。三数之和看似是双指针题,但去重逻辑里其实也藏着哈希思想;四数相加则是两数之和的map思路的扩展版,把四个数组两两配对,用map统计两数之和的出现次数,时间复杂度O(n^2)。你会发现,第七天打下的基础,在后面的题目里被反复复用。

5.2 我实操中踩过的坑与调整建议

最后分享几个我做这四道题时真实踩过的坑,算是给后来者提个醒。

第一,千万不要因为看懂了题解就跳过手动推演。我一开始觉得快乐数这题太简单了,看完答案直接提交,结果代码里把一个变量名写错了,导致WA。后来我重新手推了一遍2的变换链条,才真正理解了“循环检测”这个操作的本质。看懂和会写之间,差的就是亲手推演这一遍。

第二,C++的unordered_set和set的性能差异,在第七天这四道题的量级下其实感觉不出来,但到了后面的题目,数据规模大了之后,选错容器真的会超时。所以我从第七天开始就强制自己默认用unordered版本,除非明确需要有序性。

第三,两数之和的“先查后插”顺序问题,看起来是个小细节,但我确实见过有朋友在面试时因为写反了顺序,被面试官追问到卡壳。这种细节就属于“你知道吗”和“你理解为什么吗”的区别,建议大家不仅要记住顺序,还要能说出为什么这么写。

第七天这四道题,是我整个训练营里印象最深的一天。不是因为题目有多难,而是因为它是第一次让我体会到“数据结构的选择能直接改变算法复杂度”这件事。如果你今天刚刷完这四道题,不妨再回头看一眼自己写的暴力解和哈希解,感受一下那种“优化前后”的差别。后面还有更多有意思的题目等着你,但地基就在这里——别赶路,去感受路。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 14:28:14

用Python和OpenAI API打造智能聊天机器人:从零到完整项目

智能聊天机器人这个词&#xff0c;听起来总带着一层神秘感&#xff0c;好像只有大厂才能做出来。其实有了大语言模型 API&#xff0c;个人开发者完全可以在一天之内搭出一个能对话、能记住上下文、甚至能扮演固定角色的机器人。这篇文章就带着你从零开始&#xff0c;用 Python …

作者头像 李华
网站建设 2026/10/21 14:27:56

RPM与YUM包管理深度解析:依赖解析原理与实战避坑指南

1. 从一次依赖地狱说起&#xff1a;为什么需要包管理器 如果你在Linux世界里待过哪怕一周&#xff0c;大概率听过RPM和YUM这两个词。但很多人对它们的理解停留在“装软件用的”&#xff0c;具体怎么回事、为什么要有两个东西、什么时候用哪个&#xff0c;往往说不清楚。我自己最…

作者头像 李华
网站建设 2026/10/21 14:27:25

后台弱口令批量检测工具选型:WebCrack 实战配置与避坑指南

简介&#xff1a;WebCrack是一款开源免费的Web后台弱口令与万能密码批量爆破、检测工具&#xff0c;面向网络安全从业者、渗透测试人员及网站管理员&#xff0c;用于安全审计中排查登录凭证隐患。资源包共18个文件&#xff0c;以12个Python脚本为核心&#xff0c;涵盖爆破任务调…

作者头像 李华
网站建设 2026/10/21 14:27:19

大模型微调实战:从LoRA调优到DPO对齐的稳定上线复盘

从 “跑通 demo” 到 “敢上线”&#xff0c;中间隔着的就是风格迭代和微调正反馈。很多人在基础任务里学会的是调用现成接口、拿个小数据集跑通监督微调&#xff0c;但从训练完成到交付一个稳定可用的模型&#xff0c;这一步才是真正的关卡。task05 不谈花哨技巧&#xff0c;它…

作者头像 李华
网站建设 2026/10/21 14:27:04

rea 的多场景实践:React 架构、实时调度、需求分析与资源分配

1. 从“rea”这个标题说起&#xff1a;一个被低估的通用缩写 第一次看到“rea”这个标题&#xff0c;很多人会愣一下——三个字母&#xff0c;没有上下文&#xff0c;没有说明&#xff0c;像是谁随手敲的键盘测试。但如果你在技术社区、设计圈或者项目管理场景里待过一段时间&a…

作者头像 李华
网站建设 2026/10/21 14:26:52

REA建模实战:用资源、事件、参与者统一业务与财务口径

去年我接手了一个内部代号叫“REA”的项目。项目本身一句话就能说清楚&#xff1a;把公司散落多年的订单、库存、财务三套系统&#xff0c;整合成一套口径统一的业务底座。真正值得复盘的是&#xff0c;我们在这个项目里把建模框架也选成了REA——Resources、Events、Agents&am…

作者头像 李华