☰
LeetCode热题100基础精讲:五道题覆盖哈希表、双指针、滑动窗口、栈与链表
2026/10/17 8:21:27 网站建设 项目流程

刷LeetCode最焦虑的时刻,往往不是遇到难题,而是面对上千道题不知道从哪下手。热题100这份题单在程序员圈子里几乎是无人不知的,它按出现频率、面试考察率和社区讨论热度综合筛选出100道题,从数组、链表、哈希表、树到动态规划、图论,几乎覆盖了核心考点。我当年准备跳槽面试时,就是把这份题单当主菜单,配合按知识点分类的专项练习,前后刷了两轮,最后拿到的offer里算法面基本都在这份题单的射程范围之内。作为“LeetCode热题100”系列的第一篇,我想先把刷题策略说清楚,再带大家用五道题把“读题→AC→面试讲题”的完整链路走一遍。这五道题分别涉及哈希表、双指针、滑动窗口、栈、链表,难度从Easy到Medium,正好适合热身。

1. 热题100该怎么定位:题单只是起点,不是全部

不少朋友拿到热题100之后的第一反应是“按顺序刷”,从第1题刷到第100题。我的看法是:如果你是第一次刷,可以按顺序快速过一遍Easy题找感觉;但如果你想靠它准备面试,更合理的做法是把题单当作知识点的索引,而不是一本从头读到尾的习题册。

先看这份题单的整体结构。数组和哈希表相关题目占了相当大的比重,接着是链表、字符串、二叉树、回溯、动态规划,最后是堆、图、并查集等相对冷门的考点。这种分布本身就在传递一个信号:面试官最常考的还是基础数据结构与常见算法范式,冷门题的命中率其实不高。所以我的策略是:第一轮按题号顺序刷,每题最多思考三十分钟,没思路直接看题解,然后用自己语言重写一遍;第二轮按知识点分组刷,例如连续几天只做双指针、只做动态规划,让大脑在同一个思维模式下密集训练。

关于时间分配,我自己比较推荐的节奏是每天3到5题,周末复盘本周错题。这样一个月左右能过完第一轮,再花两周做第二轮专项和错题重刷。如果你是在职准备,通勤时间看题、午休时间想思路、晚上集中写代码,这个节奏也能基本维持。

还有一个容易被忽略的点:热题100里的题面基本都是原题,但面试中会出现大量的变体。比如“两数之和”可能变成“三数之和”、“四数之和”、“三个数相乘最接近某个值”,它们的核心考点仍然落在哈希表和双指针上。所以刷题的时候不要只盯着AC,要主动问自己:这题如果改个条件,解法还成立吗?这是一份题单真正开始发挥价值的地方。

2. 两数之和与哈希表:一道题看清“用空间换时间”

两数之和是LeetCode的第1题,也几乎是所有人算法之旅的第一站。题目很朴素:给定一个整数数组和一个目标值,找出数组中两个数,使得它们的和等于目标值,返回它们的下标。

最直观的解法是暴力枚举,两层循环遍历所有数对,判断是否满足条件。时间复杂度是O(n²),对于小数组没有任何问题。但在面试场景下,这通常是面试官希望你给出的“起步答案”,然后他一定会追问一句:“能不能优化?”

这里就牵扯到一个关键思维:我们真正要找的是什么?对于当前数num,我们需要知道数组里是否存在target - num,并且拿到它的下标。判断“存在”这件事,哈希表能做到平均O(1)的查询。于是我们只需要遍历一遍数组,把已经访问过的数值和下标存进哈希表,一边查一边存,整体时间复杂度降到O(n)。

def twoSum(nums, target): hashtable = {} for i, num in enumerate(nums): complement = target - num if complement in hashtable: return [hashtable[complement], i] hashtable[num] = i return []

这段代码里有几个细节值得拿出来单独说。第一个细节是“先查后存”的顺序。如果把当前值先存进哈希表,再查找target - num,会出现一个问题:当target恰好等于2 * num时,会把自己当成答案。比如数组[3, 2, 4],目标值为6,遍历到3的时候如果先存入,再查找3,就会错误地返回[0, 0]。先查后存能保证找到的另一个下标一定是之前遍历过的,不存在自配对的场景。

第二个细节是题目保证有且只有一个答案,所以找到一个结果就可以直接返回。如果没有这个前提,就需要收集所有满足条件的下标组合,代码逻辑会有变化。

第三个细节是哈希表里为什么存“值→下标”,而不存“下标→值”。因为我们查的是target - num这个值,要返回的是它的下标,所以必须以值为键、下标为值。很多人第一次写会搞反,这属于很正常的事情,写一次就记住了。

这道题的扩展价值在于:它揭示了一个通用范式——当暴力解法需要反复“查找历史元素”时,哈希表永远是第一考虑方向。后面的“字母异位词分组”“最长连续序列”等题,本质上都是这个思路在不同数据结构包装下的变体。

3. 三数之和与双指针:从嵌套循环到“升维思考”

两数之和 AC 之后再去看三数之和,很多人第一反应是“在两数之和外面套一层循环”,也就是固定第一个数,然后对剩下两个数做两数之和。这个思路本身是对的,但如果你用哈希表做内层查找,就需要额外处理去重问题,代码写起来容易乱。更经典的解法是排序加双指针,这也是这道题真正想训练的东西。

题目要求:在一个整数数组中,找出所有不重复的三元组,使得三个数之和为0。首先要明确“不重复”是什么意思,它指的是三元组之间不能重复,比如[-1, 0, 1]和[1, 0, -1]视为同一组,不是指数组中的元素不能重复使用。

先对数组排序。排序在这道题里的作用有两个:一是让双指针的移动变得有方向感,二是有利于去重。排序之后,我们固定第一个数nums[i],然后用左指针指向i+1、右指针指向数组末尾,根据当前三数之和与0的大小关系移动指针:和小于0说明需要更大的数,左指针右移;和大于0说明需要更小的数,右指针左移;等于0则记录答案并继续收缩。

def threeSum(nums): nums.sort() n = len(nums) res = [] for i in range(n - 2): if i > 0 and nums[i] == nums[i - 1]: continue if nums[i] > 0: break left, right = i + 1, n - 1 while left < right: total = nums[i] + nums[left] + nums[right] if total < 0: left += 1 elif total > 0: right -= 1 else: res.append([nums[i], nums[left], nums[right]]) while left < right and nums[left] == nums[left + 1]: left += 1 while left < right and nums[right] == nums[right - 1]: right -= 1 left += 1 right -= 1 return res

这段代码里最值得留意的是三处去重。第一处是固定元素nums[i]的去重:如果当前值和前一个值相同,说明以它为固定元素的所有组合已经被前一个元素覆盖过了,直接跳过。第二处和第三处是找到一组答案之后,左指针和右指针分别跳过重复值,避免在同一轮循环中产生相同三元组。

为什么要用排序加双指针,而不是在两数之和的哈希表解法上直接扩展?关键在于“去重”成本。哈希表解法找到二元组之后,要判断这个组合是否已经出现过,而三元组可能出现的组合数远大于双指针的遍历次数,使用集合去重的内存和时间开销都不划算。排序加双指针的做法天然利用了“有序性”,让去重逻辑融入移动规则之中,整体时间复杂度是O(n²),空间复杂度O(1)(忽略排序和结果存储)。

这道题在面试中还有一个高频追问:如果原始数组里有重复元素,双指针方案是否还能保证三元组不重复?答案是能,因为所有去重逻辑都不依赖数组元素本身的唯一性,只依赖相邻位置的比较。这一点值得你在讲题时主动提出来,面试官会认为你对边界条件想得很清楚。

4. 无重复字符的最长子串:滑动窗口从直觉到模板

“无重复字符的最长子串”是一道Medium题,也是滑动窗口类题目的入门必做题。题面很简单:给定一个字符串,找出其中不含有重复字符的最长子串的长度。

很多人第一次拿到这题会想到暴力枚举所有子串,再逐个检查是否有重复字符,复杂度O(n³),当然能跑,但面试官会立刻追问“能不能线性”。这里就得引入滑动窗口的思想。

滑动窗口可以理解成一条在字符串上游走的“可伸缩区间”。我们用左指针和右指针维护窗口左右边界,窗口内的子串始终保持“无重复字符”的状态。右指针每次向右移动一格,把新字符纳入窗口;如果新字符和窗口内已有字符冲突,就移动左指针,把冲突字符从窗口里挤出去,直到窗口重新满足条件。每移动一次右指针,就计算一次当前窗口长度并更新答案。

def lengthOfLongestSubstring(s): window = set() left = 0 ans = 0 for right in range(len(s)): while s[right] in window: window.remove(s[left]) left += 1 window.add(s[right]) ans = max(ans, right - left + 1) return ans

这段代码的核心是while循环。当s[right]已经在窗口集合中时,说明出现了重复,此时需要不断移除左指针指向的字符,直到重复字符被移出窗口。注意移除和移动的顺序:先移除left位置的字符,再让left自增,这个顺序不能反。举个例子,s = "abcba",当右指针走到最后一个'a'时,窗口里已经有'a'、'b'、'c',此时需要把左指针从位置0移动到位置3,也就是依次移除'a'、'b'、'c',最后窗口只剩下新的'a',窗口左边界来到下标3。如果移除顺序反了,会导致窗口状态和下标对应不上一周目。

为什么用集合而不是字典?因为这里只需要知道窗口内是否存在某个字符,不需要知道它的具体下标。如果你要优化到一步到位把左指针跳到重复字符上次出现位置的下一个位置,那就需要字典记录“字符→最近下标”,这是本题的一个进阶版本,建议基础不错的读者试试。

面试中这道题的加分表达包括三点:第一,问清楚题目输入是否只包含ASCII字符,这影响能否直接用256大小的数组替代哈希表;第二,说明窗口的“不变量”是窗口内无重复字符,每一次移动右指针都必须维护这个不变量;第三,主动分析复杂度:每个字符最多被移入窗口一次、移出窗口一次,所以整体是O(n),这也是滑动窗口类题目时间复杂度的通用证明方法。

5. 有效的括号与合并有序链表:栈和链表的基础盘

两数之和给了哈希表,三数之和给了双指针,最长子串给了滑动窗口,接下来这两道题适合放在一起讲,因为它们考察的是数据结构最基础的操作习惯:栈的入栈出栈规则,以及链表的指针操作。

有效的括号是一道Easy题,题面:给定一个只包含'(', ')', '{', '}', '[', ']'的字符串,判断括号是否有效。有效条件有三个:左括号必须用相同类型的右括号闭合,左括号必须以正确的顺序闭合,每个右括号都有一个对应的同类型左括号。

栈的天然优势在于它“后进先出”的特性恰好匹配括号匹配的顺序。遇到左括号就入栈,遇到右括号就检查栈顶是否是对应的左括号,如果是就弹出,如果不是或者栈为空则直接判定无效。遍历结束后,如果栈为空,说明所有左括号都有匹配的右括号,否则无效。

def isValid(s): stack = [] mapping = {')': '(', ']': '[', '}': '{'} for ch in s: if ch in mapping: if not stack or stack[-1] != mapping[ch]: return False stack.pop() else: stack.append(ch) return not stack

一个小小的经验:用字典存储右括号到左括号的映射,比写一堆if-else更清晰。判断右括号时直接查映射拿到期望的左括号,省去多分支判断。另外,很多人在写的时候容易忘记“栈为空”这个条件,比如输入是“)]}”时,第一个字符就是右括号,此时栈空,如果直接访问stack[-1]会报错,所以必须先判断not stack。

这道题的扩展方向很多,比如括号的嵌套深度计算、括号字符串的最长有效长度,后者就是一道Hard题。但基础思维都是栈,把基础版本吃透,后面遇到变形不至于完全懵。

合并两个有序链表同样是Easy题,题面:将两个升序链表合并为一个新的升序链表并返回。这里要强调“新链表不能共用一个节点导致串值混乱”,实际上标准解法可以直接复用原链表的节点,通过修改next指针串联成一个新链表,不需要额外创建节点。

递归解法很优雅:比较两个头节点,较小的那个作为新链表的头,它的next指针指向“剩下两个链表合并后的结果”。

def mergeTwoLists(l1, l2): if not l1: return l2 if not l2: return l1 if l1.val < l2.val: l1.next = mergeTwoLists(l1.next, l2) return l1 else: l2.next = mergeTwoLists(l1, l2.next) return l2

迭代解法用哨兵节点(dummy node)简化空指针处理,核心是一个while循环,每次比较两个链表当前节点值,把较小者接到结果链表的尾部,同时让对应链表指针后移。循环结束后,某个链表可能还有剩余节点,直接整体接上即可。

def mergeTwoLists(l1, l2): dummy = ListNode(-1) cur = dummy while l1 and l2: if l1.val < l2.val: cur.next = l1 l1 = l1.next else: cur.next = l2 l2 = l2.next cur = cur.next cur.next = l1 if l1 else l2 return dummy.next

递归和迭代怎么选?我的建议是面试时优先讲递归,代码短、语义清晰,适合解释;但如果面试官要求主动写迭代版本,也要能流畅写出来,因为迭代避免递归栈,在大链表下不会栈溢出。链表类题目有一个非常实用的调试技巧:画“三节点图”,画当前指针、next指针和下一个节点的位置。链表题为什么容易写错?因为节点的指针关系是线性的、单向的,脑子里的状态和纸面上的代码经常对不上。画图能快速定位是先动next还是先动当前指针,尤其是涉及到“暂存下一个节点”的场景,这个习惯能少走很多弯路。

6. 第一轮刷题最容易踩的坑:从代码到面试表达

五道题全部过完之后,回到刷题这件事本身。热题100第一轮最常见的坑大概有三个,我逐个说。

第一个坑是“看懂了但是写不出来”。很多人看题解的时候觉得逻辑很简单,合上答案自己写,手指僵在原地。这是正常的,因为“看懂”是输入过程,“写出来”是输出过程,两者用的脑区不同。我的建议是:看完题解后不要马上照抄,先关掉题解,用自己的话把思路写在注释里,再照着注释写代码。这样至少能保证你不是在背答案,而是在理解思路之后重建代码。

第二个坑是“AC之后就再也不回头”。一道题今天能写出来,不代表两周后还能写出来。人类的遗忘曲线在算法题上体现得尤为明显,特别是双指针边界条件和去重逻辑这类细节。我自己的做法是给每道题标一个状态:新题、已AC、需复盘、已掌握。第一轮结束后的错题和“需复盘”题,是第二轮专项训练的主角。那些第一遍就AC的简单题,反而不用反复刷。

第三个坑是“只关注代码,不关注表达”。很多面试场景下,面试官并不真正关心你是否记得住解法,因为你可以现场推导,他更关心你的推导过程是否清晰、边界条件是否考虑到位。我有一个练习方法:刷完一道题后,用手机录音给自己讲一遍思路,控制在三分钟以内。讲的时候必须覆盖几个要素:题目在问什么、我选择什么算法、为什么选它、复杂度是多少、最容易被忽略的边界条件在哪。这个习惯坚持一个月,面试讲题时的流畅度会有质的提升。

另外补充一点关于题量的焦虑。热题100听起来不多,但每一道都值得咀嚼,尤其是那些Medium题,它们的解题套路往往是多个基础知识的组合。比如稍后讲到的“最长连续序列”要用哈希集合,“二叉树的层序遍历”要用队列做层间分隔,“最大子数组和”是动态规划入门,等等。第一轮不求快,但求稳。你每搞懂一道题,面试时其实就多一类题型的底气。

这一篇从两数之和的哈希表讲到三数之和的双指针,从滑动窗口讲到栈和链表,算是把最常考的几个基础方向热了身。接下来的第二篇,我会把重点放到二叉树和回溯这两大块,这也是热题100中题量最多、变体最丰富的两个板块,到时候我还会拿出几道典型的“看题解都会、合上书就废”的题目做详细拆解。如果你正处在第一轮刷题的过程中,这篇提到的“先查后存”“先判空再取栈顶”“画三节点图”这些小习惯,可以先在明天的刷题里用起来。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询