
大家刷《代码随想录》系列时容易陷入一个节奏误区一开始很猛链表、哈希表一章接一章题目刷完就下一节结果到后面才发现前面的底层逻辑没吃透返工成本极高。我自己的体会是前期某几天的内容一旦压得够扎实后面环形链表、滑动窗口、动态规划都会顺很多。今天这篇就专门复盘代码随想录系列里的第七天也就是哈希表这一整块的学习记录和实操踩坑过程不光是题目解法更多是思路层面怎么真正吃透这一类数据结构题。第七天在整套题单里属于典型的“离散知识点集中爆发”的阶段代码量不大但对哈希思想的理解要求高而且一旦你掌握了判断“需不需要”和“怎么维护一张表”的思维方式后面很多中等、困难题都能降维打击。1. 看待哈希表的方式不是因为语言有字典才用它很多初学者对哈希表的认知停留在“它是一种查得快的数据结构”。第七天题目做完之后你会发现这只是表象。哈希表的本质其实是一个“空间换时间”的映射工具——你愿意花一份额外内存把某个值和另一个值快速关联起来以换取O(1)复杂度的判断和查询。我在实操中习惯把它类比成快递柜快递放进去之前你已经记好了取件码和格子的对应关系。取件时直接刷码不需要一家一家快递站去翻包裹。哈希表做的事情一模一样把某个中间结果存在表里用同一个标准去校验新来的数据。这个思维方式在第七天的三道核心题目里体现得非常彻底判断两个字符串是否为有效字母异位词、求两个数组的交集、以及判断快乐数。它们表面问法各不相同但本质都是“是否存在”或“是否出现过”这一类问题而哈希表正是解“存在性判断”的最优数据结构。用代码随想录里的一个观点来说就是你需要一张表因为你需要知道某个元素之前是否出现过。这句话是第七天最值得记住的一句话。你理解了它后面做两数之和、四数相加、最长连续序列都会顺理成章。我之前在指导A同学时发现一个常见问题他会用哈希表但不知道为什么自己在用哈希表。刷完第七天的题目后他说了一句“原来我用字典就是为了回答一个问题——这个东西我之前见过没有”这句总结比任何教程都到位。实际做题时判断用哪种哈希结构也很好选数据范围小且是连续字符比如字母异位词那道题用数组就够了性能还更好数据范围大或不确定比如数组交集、快乐数用HashSet存“出现过没有”即可需要存值和它的“额外信息”比如出现次数或下标那就用HashMap。其实这三种选择几乎覆盖了日常算法题里90%的哈希场景。第七天特别有价值的点就在这里它用几道题就把哈希表在不同场景下的选择逻辑给你练通了。2. 从字母异位词说起数组有时候比真正的哈希表更哈希第七天开头那道“有效字母异位词”的常规做法是用长度为26的数组统计每个字母出现的次数。这个解法看起来简单但背后藏着哈希表一个核心概念哈希函数设计。你把字母a-z映射到数组下标0-25这本质上就是一次极其精简的哈希过程只不过它没有碰撞、不需要处理冲突因为字母有限且一一对应。实操细节上有一个很基础但容易坑的点如何正确取到相对下标。如果字符是小写字母直接用ord(a)作为基准每个字母减去这个基准值就得到0到25的索引。我在某次代码评审中看到有人直接对字符转ASCII码后跟a的ASCII码比较大小逻辑没错但代码冗余还有人在统计结束后忘了判断两个数组是否相等直接用比较列表却忽略了列表和数组类型差异这类低级错误在笔试题里经常导致超时或者报错。这个题目时间复杂度的优化空间其实不大O(n)基本就是下界。但空间上有一点点讲究你完全可以在一个数组上先加后减最后检查数组是否全零这样就不需要两个数组了。我自己更喜欢这种写法边遍历s边加、边遍历t边减最后一次性判断代码短而且思路清晰。def isAnagram(self, s: str, t: str) - bool: if len(s) ! len(t): return False table [0] * 26 for ch in s: table[ord(ch) - ord(a)] 1 for ch in t: table[ord(ch) - ord(a)] - 1 for cnt in table: if cnt ! 0: return False return True代码随想录里在这道题上给出了一个提醒哈希表不只有HashSet和HashMap数组本身就是一种极简的哈希表适用于“数据范围可以提前确定且不大”的场景。这个提示虽然简短但对后面做题影响很大——比如你在处理字符统计、桶排序、或者某些需要标记状态的题时数组往往比真正的HashMap性能好得多。有的同学可能会问这题不是也可以用Python的collections.Counter直接比较吗当然可以但面试时如果只会用库函数反而容易让人怀疑你对底层原理的理解程度。第七天这个阶段我建议自己显式地写一遍数组计数逻辑这样你才会真正理解为什么Counter能工作、它内部大致是怎么做的。从实际效率说数组方案在时间和空间上都是最优的时间复杂度O(n)空间复杂度O(1)也就是固定的26个元素大小而HashSet或HashMap方案空间上是O(n)级别。虽然n不大时差异可以忽略但这道题作为第七天开胃菜值得用最标准、最底层的写法过一遍。3. 数组交集与HashSet去重与存在性的一次到位第二道核心题“两个数组的交集”是哈希表第二层用法的绝佳示范不仅要判断存在性还要去重输出。这里就需要用到集合的特性也就是HashSet。我在练习时尝试了两种方案用两个集合分别存储两个数组的元素然后遍历小的集合判断每个元素是否也在大的集合中是则加入结果列表用一个集合存储第一个数组的去重结果遍历第二个数组时直接查集合命中就加入结果列表同时从集合中删除该元素避免重复输出。第二种方案时间效率略高一些因为只需要一个集合而且删除操作可以保证去重的效果。但这两种的时间复杂度其实都是O(mn)差别主要是常数级别的。还有一种更符合代码风格的写法是使用Python内置的集合运算符intersection一行搞定。但我在实际练习中并不建议一上来就写这种写法因为它的内部细节都被封装掉了你无法体会“维护一张表去判断是否出现”的过程。面试时也可以用但前提是你已经把底层逻辑讲清楚了再用语法糖来精简代码这才能起到加分效果。这道题有哪些我实际踩过的坑没有考虑输入数组可能为空的情况。虽然空集求交集结果也是空但代码里如果不做防御性判断有时会因为None值报错把结果列表转换成集合又转回列表多此一举。熟悉集合操作后完全可以一步到位遍历时直接修改原数组或集合。比如在遍历第二个数组的过程中从集合里删除元素这个操作在Python里看似没问题但如果遍历对象本身就是那个集合就会抛RuntimeError。从第七天这几道题的内容安排来看一个明显的特点是相同数据结构在不同题目里会用出不同层级。字母异位词里你用数组数组交集里你用Set快乐数里你不仅要Set还要靠它打破循环检测的死结。这个区分度在设计层面是有意为之的。它让你意识到同样是“查重”不同的题目特征对应最合适的那一种实现方式。有些题用数组卡边界有些题用集合做唯一化有些题必须上Map才能保存额外状态。脑子里有这条决策链刷题效率才会真正提上来。4. 快乐数循环检测中哈希表的隐形战场快乐数这道题第一次看到的人十有八九会觉得“好花哨的数学题”。它的题目描述是一个数不断替换成各数位数字的平方和如果最终能变成1就是快乐数否则会进入无限循环。问题的关键就落在这个“无限循环”上你怎么知道它在循环里怎么快速判断并且终止不少人的第一反应是维护一个计数器跑到一定次数就超时退出。这种方法不严谨因为循环周期无法提前预估而且不同数开始循环的节奏也不一样。其实在拿到一个新数的时候你要回答的就是那个熟悉的问题“我之前见过这个数吗”如果见过说明你已经走到之前走过的老路上循环已成立这个数永远不可能到1。所以这道题本质上就是在最合适的数据结构上自然而然地运用哈希表也就是HashSet。每次生成一个新的平方和就丢进集合里看是否已存在存在就直接返回False否则加入并继续迭代。整个判断逻辑一气呵成完全不需要额外的记忆化搜索或状态机。def isHappy(self, n: int) - bool: seen set() while n ! 1 and n not in seen: seen.add(n) n sum(int(d) ** 2 for d in str(n)) return n 1实际操作时还有一个细节容易漏掉计算平方和时先转字符串再逐位处理是最直观也最不容易越界的写法。但如果你追求极致性能完全可以禁用字符串用整数取余的方式逐位拆def get_next(num): total 0 while num 0: digit num % 10 total digit * digit num // 10 return total这种写法避免了str和int的来回转换在面试白板题或是需要高频计算时更耐打。第七天练习时可以两种都写一遍训练一下“从同等功能中选出更合适的实现”的敏感度。快乐数这道题如果搜索题解你会发现还有数学规律非快乐数最终会进入已知的固定循环其中包含数字4。比如从任意非快乐数开始你迟早会得到4、16、37、58、89、145、42、20这些数。理论上你可以直接判断数字是否为该循环中的某几个值来快捷得出结论但在普通算法训练中完全没有必要去背这个规律用Set已经足够清楚可靠了。不做这种优化的原因也很简单哈希表的判重本来就是O(1)性能瓶颈根本不在这一层何必引入一个面试时容易讲不清楚的“黑魔法”这道题实际考察的核心其实是费米估算式的常识判断一个很大很大的数经过一次数位平方和后会急剧变小。比如999999999999一次平方和是972三轮后就是个位数级别。加上Set判断循环用不了几个循环复杂度就收了。很多人害怕“它会一直跑不停”其实完全没有必要——数位平方和天然会把结果压向一个小区域这是快乐数问题真正被归类为“哈希表相关”而不是“数学难题”的原因。5. 两数之和的另一种打开方式Map的键值设计先查后存是精髓第七天常规题单里两数之和往往是正式引入HashMap的第一道题。题目异常经典给定数组和目标值返回两个下标。第一次接触的人最自然的想法肯定是双重循环暴力求解。暴力法当然能做出来但当你写下它的时候其实已经放弃了一个优化机会双重循环的第二层在做的是“查找当前元素是否存在匹配项”而查找这个行为明明可以用哈希表把时间复杂度降到O(1)。这就是第七天讲两数之和的真正意义——把暴力里的“查找”升级成哈希表里的“命中”。代码随想录第七天的两数之和解法里有一个特别有意思、值得反复咀嚼的细节遍历过程中要“先查后存”。def twoSum(self, nums: List[int], target: int) - List[int]: record {} for i, num in enumerate(nums): diff target - num if diff in record: return [record[diff], i] record[num] i return []先查后存意味着当前数字不会和自己匹配。如果不小心先存了再查那对于“两个相同数字恰好组成target”的情况比如nums[3,3], target6会出现“自己等于自己”的错误配对。很多同学第一次写这里都会犯这个错我也犯过。把这个问题想透后你会发现这个“先查后存”的顺序并不是玄学而是一种严谨的工程习惯——先把已有信息用来判断再更新状态避免新状态污染当前决策。在某些变种题里比如“找到数组中和为target的两数并返回其值不是下标”则可以用另一种思路把数组转成Set再遍历查找互补元素是否存在。但是如果数组里有重复元素Set会丢失数量信息这时就必须用Map来存元素和它出现的次数。从两数之和这类基础题里你就能看出哈希表选型的关键不是“哪个更高级”而是“哪个不会丢信息”。从项目实操的角度看这个“先查后存”的思路不止用于两数之和。我后来在做一个模拟项目X的内存键值对模块时也遇到过类似场景需要判断当前批次中的记录是否和之前批次产生冲突。如果我先插入了当前记录再去做冲突检测就会把新记录本身当成历史记录误报冲突。后来就是套用了这道题的“先查后存”逻辑把冲突检测放在插入之前Bug直接消失。这就是刷题与实际编码直接相通的最好证明。6. 什么时候用Set什么时候用Map一个简单但关键的决策模型第七天做完之后一个非常实际的问题会摆在面前那以后遇到新题到底该用HashMap还是HashSet甚至该不该用数组我根据第七天的题目和我自己做过的实际项目总结了下面这套判断流程可以直接抄走如果只关心“某个值是否出现过”不需要关心次数、位置或对应关系那就用Set。比如数组交集、快乐数、链表判环都属于这一类。如果不仅要判断出现过没有还要取回“它出现时生成的额外数据”比如出现次数、数组下标、对应节点那就用Map。两数之和要下标四数相加要计数都是Map的菜。如果值的范围已知而且不大并且只是连续整数、字符这类可枚举数据优先考虑用数组当哈希表。比如字母异位词、固定范围内的频次统计用数组速度最快且不用处理哈希碰撞。如果既要存多个值的关系又需要维护顺序比如最近最少使用等那么单纯的哈希表就不够了要哈希表配合双向链表这类结构。这不是第七天需要掌握的东西但你在后续刷题中会反复遭遇提前知道这条决策链有助于不迷路。这层决策模型比背一百道题重要得多。第七天的题量不算大但你把这套“哈希结构选型思维”建立起来后面碰到需要哈希参与的中等题几乎不会走弯路。另一个经验点是不要把HashMap的value只当成“数字值”来用。它可以是字符下标、频次、状态标记、最后一个位置、父节点索引、颜色标记……什么都能存。你设了什么value就意味着你打算在查中它之后取出什么信息用。这是设计HashMap解题方案时的第一步——先想清楚“如果查到了我需要用什么信息做后续决策”再决定value类型是什么。第七天很多时候被低估因为它题面短、难度低看起来不够“厉害”。但我认真做完之后发现哈希表很多最常见的坑比如先查后存、边界映射、循环检测、重复元素去重恰恰都在这一天里集中练了一遍。这也是我为什么愿意花大量篇幅来写第七天而不是某个更加炫技的章节的原因。7. 第七天实操调试记录我在跑题时常见的报错与误判在带A同学完整跑完第七天所有题目后我把我们两个在该过程中遇到的典型问题整理了出来这些问题在LeetCode讨论区里其实也高频出现值得存一波速查。第一个高频问题字母异位词里用了collections.defaultdict(int)却在对value加减时忘了考虑负数的情况。其实不管是defaultdict还是普通dict计数加加减减完全可行关键在于最后检查时只查有没有非零值。负数也是非零所以不需要特殊处理。真正会出问题的是在中间某个环节试图做“裁剪”或“提前return”导致漏掉了末尾需要判断全零的步骤。第二个高频问题数组交集时直接把set(nums1)和set(nums2)的结果取交集再转list本身没错但如果你先遍历一个集合又要同时从另一个已转换成列表的结构里删除就会出现“迭代时修改”的经典异常。Python对“在遍历集合时修改集合”是直接报错的这一点和Java里的ConcurrentModificationException异曲同工。解决办法就是遍历副本、遍历列表、或者用序列化后的新结果接收。第三个高频问题快乐数里有人会把“n是否已经出现”的判断放在加入Set之后。表面看只是顺序问题实际上会导致一个死循环已经见过的数又被反复加入集合永远在膨胀但代码却无法及时返回False。把if n in seen放在seen.add(n)之前这是必须的写序规范。第四个高频问题两数之和里用Map存值和下标的映射时如果数组里有重复值后出现的值会覆盖前面值的下标。这会导致返回的下标不是我们想要的那一组实际上不会。因为“先查后存”的顺序保证了当后一个重复值出现时我们早就用它在Map中的前一个位置和当前值进行了匹配并直接return了根本不会走到覆盖那一步。但如果你改成“先存后查”重复值就会造成错误覆盖和漏判。这个例子最能说明“先查后存”不是风格偏好而是算法正确性的保证。这些问题我在培训或帮助他人攻克类似题目时几乎每次都遇到。它们看起来是零散的代码细节但底层其实都是对哈希表基本操作时机和语义的不够熟悉造成的。等你连续做过几遍第七天的题型这些问题基本都能根治。8. 第七天里的双指针思路为什么不建议把哈希和双指针割裂开代码随想录第七天虽然主推哈希表但有一个很重要的隐含事实很多哈希表题其实也存在双指针解法。比如字母异位词可以先排序再双指针比较数组交集也可以用排序加双指针归并。那为什么第七天还是要主攻哈希表因为双指针通常只是“省空间”的选择而哈希表的优势是“省时间”。两者没有绝对优劣只有场景适配。刷题训练时最好两个都掌握并且要有意识地在同一道题上比较两种方案的差异。下面用我自己整理的表格展示两种思路在不同维度上的对比便于直观理解。维度哈希表方案排序双指针方案时间复杂度O(n)线性完成O(n log n)受限于排序空间复杂度O(n)需要额外哈希结构O(1)或O(log n)视排序算法而定是否修改原数据不修改通常需要排序可能修改原数组适用场景需要快速判断存在性、查下标或计数数据量极大且对空间敏感、数据可排序典型题目两数之和、四数相加三数之和、四数之和第七天的题目里哈希表方案普遍更直接而双指针方案作为结束后延伸思考也很有价值。我自己练习时的策略是第七天按哈希表思路全部过一遍休息之后再把字母异位词和数组交集用双指针重写一次。这种“一道题两种解法”的训练方式比单纯刷更多新题要有效得多。有意思的是两数之和如果用双指针必须要先排序但排序后原数组下标就丢失了。你可以用元组保存原下标再排序可这样一来空间复杂度也上来了整体复杂度反而不如哈希表方案优雅。所以第七天把它放在哈希表章节是有道理的它就是想让你别把简单问题复杂化。再往后刷三数之和、四数之和时你会发现哈希表方案也不是不行但去重逻辑会变得非常麻烦这时排序双指针才是正解。第七天如果打好了双指针和哈希表的对比意识你在面对三数之和时就能自然地切换思路不会死磕一种解法。9. 做完第七天的自测方法与心态调整题单刷完不意味着彻底掌握。我给自己和A同学都设置了一套快速“检验清单”不靠看一眼题觉得“会了”就跳过而是直接动笔默写确保每个关键点都能闭眼写出来。这套方法在后续的高阶题目里也持续有效。自测维度大致包括高频题、盲写能力、同类新题、复杂场景字母异位词能否在纸上写出“一加一减单数组”版本数组交集能否跟面试官讲清楚Set和Map的选型理由快乐数能否解释为什么HashSet能检测循环且不会无限跑两数之和能否默写“先查后存”的循环代码而不出错哈希结构选择给一个随机题面能否在10秒内说出该用数组、Set还是Map以上这些我当时反复测了三轮直到最后一遍完全通过才觉得第七天真正过关。经过这个阶段我发现刷题快慢根本没意义真正决定后续章节效率的是你对“哈希表为什么这样用”的理解深度。尤其是后续接触设计类题目时很多难点都藏在哈希表的底层结构里比如负载因子、哈希碰撞、扩容机制这些在第七天虽然不深入涉及但如果你连“什么时候用Set什么时候用Map”都没想明白底层优化更是无从谈起。心态上也需要提前打个预防针哈希表这一章学完后见到题就想哈希是正常的但见到什么题都硬套Map就过了。第七天的最后一天我建议你试着脱离“这节学哈希”的条件反射遇到新题先想清楚数据是否需要映射是否关心位置、次数是否需要有序如果都不需要也许双指针或滑动窗口才是更优解。这种主动思考的习惯比按时刷完整个代码随想录系列更有价值。从始至终第七天真正的关键词只有一个把哈希表当作一种条件反射式的思维工具而不是背着API调库。当你看到“判断”“出现过”“重复”“交集”“计数”“下标”这些词脑子里立刻弹出一条路径——该不该用哈希、用哪种哈希、存什么key、存什么value、先查还是先存——这一章就算真正学到手了。