
两年前我刚开始刷算法题的时候最怕的就是那种“看了解析觉得会了合上答案又写不出来”的题。后来带新人才发现这几乎是所有人的通病。所以陆陆续续写了差不多五十来道题的手写笔记从最简单的数组双指针一路啃到树形DP一边刷一边把每道题的思路拆碎了重写。这篇“算法题-07”算是系列里比较特殊的一篇它没有把某一类特定题型拎出来深挖而是把几道面试里反复出现的经典题放一起用一种“横向对比”的视角去拆。你可能会发现有的题表面上问的是“找某个数”实际考的却是“怎么把O(n²)优化成O(n)”有的题代码就十行但背后的双指针思想能串起七八道题。这篇内容适合正在准备大厂面试、刷LeetCode但总是看完就忘的读者也适合考研408在数据结构部分想建立整体框架的同学。文中的每道题我都会给到完整的推导过程、可运行的代码和踩坑记录尽量做到看完就能自己写出来下次遇见同类题型心里不慌。1. 题目怎么选这几道题背后的“能力考察”刷题的坑我也踩过不少最早是海量刷每天四五道看起来量很足但面试时被问到一个变形题照样卡壳。后来复盘才发现刷题有效果的关键不在“量”而在“题型覆盖”和“解法收敛”。面试官真正想考察的不是你记住多少题而是你在有限时间内能否快速分析问题、拆解条件、选择合适的算法策略。所以这一篇选的题目覆盖面是这样的一道哈希表的空间换时间经典题一道双指针在数组上的收敛问题一道链表的细节处理题一道二叉树的层序变种题外加一道动态规划的入门路径题。这些题难度跨度从简单到中等偏上正好对应面试从热身到压力的全过程。还有一个选题原则每道题的解法至少要能“一题三解”。比如两数之和暴力解能过但没意义哈希解是标准答案排序加双指针则是另一条路。这种题的价值不在于“做对一次”而在于“知道所有解法在什么场景下优先选哪条”。面试时你如果能主动说出“这题我还能用双指针做但空间复杂度会多O(n)所以哈希更好”这一句话比默默写对十道题都加分。1.1 从热搜词看当前算法面试的侧重点现在算法面试的题目来源确实有一些风向变化。字节这类公司喜欢出“原题变形”题目看着眼熟但条件里藏了限制吉利这类偏前端的岗位JS考察的比例在提升很多LeetCode难度不小的题要求你用JavaScript写并且要考虑到原型链、闭包、隐式类型转换这些语言特性考研408的方向则完全不同它更偏数据结构底层原理和手动模拟过程考察的是“这个操作在内存里到底发生了什么”。把这几个方向交叉一下你会发现真正高频被考的东西其实还是那些最基础的算法范式哈希、双指针、递归、分治、动规。背题没有用你得把这些范式的特征吃透知道什么时候该用、什么时候用了就错。1.2 解题不是目的建立“解法-题型”映射才是我记得有一次帮一个师弟改简历他的项目经历里写了“精通数据结构与算法”我就拿了一道“三数之和”问他。他写了二十分钟暴力解能跑通但O(n³)的复杂度自己都看不下去。其实三数之和的考点很明确它检验你对双指针收敛技巧的掌握程度以及你是否理解排序预处理后问题复杂度是怎么降下来的。这件事给我的启发就是刷题的核心目标应该是建立一个“条件反射”看到“有序数组查找类问题”就想到二分或双指针看到“两两配对求和/求差”就想到哈希看到“极值或路径方案统计”就考虑DP。这篇的几道题每一道我都会把“怎么从题干特征定位到解法”的逻辑链讲清楚这个能力才是刷题真正要带走的东西。2. 核心算法范式拆解一题多解背后的思维模型开始讲具体题目之前我想先把贯穿整篇的几种算法范式拿出来说一下。这有点像学功夫前的扎马步看似无聊但所有招式都从这里面长出来。我见过太多人上来就死记代码模板但面试时题目稍微拐个弯就“武功全废”根本原因就是马步没扎稳。2.1 哈希思想空间换时间的核心哈希的思想本质就一句话把“查找”的时间从O(n)降到O(1)。但很多人没想清楚的是哈希表里存储的“键”和“值”应该怎么设计这才是这题的灵魂。以两数之和为例给定一个数组 nums 和一个目标值 target要求找出和为 target 的两个数的下标。暴力法是两层循环外层固定一个数内层找另一个数复杂度O(n²)。哈希解法是遍历一遍数组每遇到一个数就算它的“补数”target - nums[i]然后去哈希表里查这个补数之前有没有出现过。这里的“键”为什么选“数值本身”而不是“下标”因为最频繁的操作是“根据值找下标”哈希表天然就是这个用途。但这里有个很隐蔽的坑如果数组里有重复元素怎么办比如 nums [3, 3]target 6答案是[0, 1]。第一次遍历到第一个3补数是3哈希表为空就把{3: 0}存进去第二次遍历到第二个3补数是3哈希表里已经有{3: 0}了直接返回[0, 1]完美。但如果你在一开始就把所有元素一次性存入哈希表重复键会互相覆盖就会得到错误结果。这一点每次面试都有不少人被问住。我做了一个小对比表格方便你直观感受解法时间复杂度空间复杂度关键点适用场景暴力枚举O(n²)O(1)双重循环数组极小哈希表一次遍历O(n)O(n)边查边存绝大多数场景排序双指针O(n log n)O(1)原地排序需要返回下标时需额外存储有序数组或无需下标还有一个延伸如果题目改成“返回两个数本身的值而不是下标”那排序双指针就是最优解法因为不需要额外空间记录下标了。这也是面试官喜欢追问的点看你有没有“按需选解”的思维。2.2 双指针收敛有序数组的杀手锏双指针是一个大的技巧门类现在这一篇里我先讲最经典的对撞指针也就是两个指针一左一右根据条件向中间移动直到相遇为止。这种解法的前提是数组已经有序或者题目本身就保证有序典型题目就是“两数之和 II - 输入有序数组”、三数之和、盛最多水的容器。核心逻辑其实就两点如果当前指针指向的两个数之和大于 target说明“大数那边太大了”需要让右指针左移如果小于 target说明“小数那边不够大”需要让左指针右移。每一步移动都排除掉了一大批候选组合所以时间复杂度能被压缩到O(n)。我第一次看懂这个思路的时候觉得它简直像“贪吃蛇自己在收缩”——每一步都基于已确认的信息绝对不回头效率自然高。我在写这一题的时候正好遇到一个非常有意思的细节如果题目要求的不是“存在性”而是“所有不重复组合”那去重就是最大的坑。比如[-1, 0, 1, 2, -1, -4]这个例子里如果不去重会输出两组[-1, 0, 1]重复组合。去重的方式也不是排序完了直接跳过重复元素那么简单而是要在双指针移动的过程中每次找到一组答案后左右指针都要跳过重复值。这个细节很多人会漏后面写代码的时候我会特别标出来。2.3 链表细节控制指针操作最容易出错的地方链表题在面试中出现频率极高但很多人对它的态度是“能写但写着写着就乱了”。这很正常因为链表的操作本质上是对一堆节点的指针重新排列任何一步顺序错了结果就全错了。尤其是反转链表这道题代码不超过十行但能一遍写对的人真的不多。反转链表的核心逻辑是维护三个指针prev、curr、next。每次循环先把 curr.next 保存到 next防止后面断链然后把 curr.next 指向 prev再整体往下移动 prev 和 curr。这个步骤里最常见的问题就是顺序很多人先做“curr.next prev”然后再保存 next结果链表直接断在第二步后续遍历变成访问空指针。我自己的习惯是写每一行代码之前都在心里模拟一次“如果我现在把下一个节点丢了后面能不能找回来”。链表的操作安全永远是第一位的。这里补充一个我自己总结的规律凡是涉及链表反转、合并、交换相邻节点这类“结构性变化”的题目最好先在纸上把每个节点的连接关系画出来标好操作顺序再动手写代码。虽然多花了三十秒但避免了反复调试浪费的五分钟更重要的是面试官看重的是你的逻辑是否周密而不是你眼神好不好使能不能一把过。2.4 二叉树的层序变种从“遍历”到“构建”二叉树这块层序遍历本身是基础操作用队列实现每层按顺序输出。但面试里真正的高频考法是把它做成变种题最常见的两种按之字形顺序打印二叉树和根据层序遍历结果反序列化重建二叉树。前者简单只需要在偶数层反转一次列表后者难需要正确处理空节点的占位标记。我遇到的真题是“给定层序遍历序列其中空节点用 null 表示请重建整棵树”。这道题很有意思因为它把“遍历”和“构建”两个方向串起来了。解法核心还是那一个队列把根节点入队然后按层遍历序列每次从队列中弹出节点作为“父节点”从序列里按顺序取出它的左右子树值构造新节点并挂接然后再把新节点入队。这个过程中最关键的点是你必须保证“序列下标推进的速度”和“队列中节点的弹出速度”完全同步否则序列读完了树还没建完或者队列清空了序列还剩一堆数据。这种题的面试价值在于它在考一个很底层的认知层序遍历的序列化格式是唯一的反序列化过程其实就是“遍历过程的逆运算”。如果你理解到这一层不管题目怎么包装比如增加一个“让左右子树交换”之类的操作你都能很快定位到本质。代码写法上用队列做BFS是最自然的但我见过有人非要用递归做反序列化然后被空节点占位绕得晕头转向得不偿失。2.5 动态规划入门路径问题中的状态转移DP是很多人的心魔但其实入门级题目没那么可怕。我先拿一道经典的“不同路径”来带一下。一个机器人位于 m x n 网格的左上角每次只能向下或者向右移动一步问到达右下角有多少条不同的路径。这道题的答案很简单就是组合数 C(mn-2, m-1)但面试时直接甩公式并不会加分你得把状态转移的推导过程讲明白。状态定义dp[i][j] 表示从起点走到坐标(i, j)的路径总数。因为每个格子只能从上面或左边到达所以状态转移方程就是 dp[i][j] dp[i-1][j] dp[i][j-1]。边界条件是第一行和第一列都只有一种走法。这个递推关系看起来简单到不像话但它已经是DP的核心骨架了明确状态、找到转移、确定边界。我在教别人的时候会把这个递推过程比喻成“倒着算账”你想知道到终点有多少种走法不要去直接数而是问“最后一步是从上面来的还是从左边来的”这样层层倒退问题规模就一步步缩小了。等你想清楚这个问题代码基本就是填表的事了。另一个很常考的变种是“带障碍物的不同路径”也就是网格里有障碍物遇到障碍物就置0思路完全一致只是多了一个判断条件。能把这道基础题吃透后面遇到“最小路径和”“爬楼梯”这些题代码模板都是一通百通。3. 实操演示从题目复述到代码落地的完整过程这一节我会把每一道题的完整解题流程写出来包括我对题干的解读、测试样例的设计、代码实现、以及运行时的输出验证。这些代码我都用 Python 写了可运行的版本兼具可读性和面试手写时的简洁度。JavaScript 换个语法就能跑思路完全一样。3.1 两数之和哈希表一次遍历的标准写法题目描述给定一个整数数组 nums 和一个目标值 target请你在该数组中找出和为目标值的那两个整数并返回它们的数组下标。你可以假设每种输入只会对应一个答案。但是数组中同一个元素不能使用两遍。这道题我已经讲过思路直接上代码def two_sum(nums, target): # 哈希表存储数值 - 下标 # 边遍历边存储这样能避免重复元素覆盖问题 seen {} for i, num in enumerate(nums): complement target - num # 如果补数已经在哈希表中说明找到了答案 if complement in seen: return [seen[complement], i] # 没有找到就把当前元素存入哈希表 seen[num] i return []代码非常简单但有几个值得注意的习惯第一不要用 nums.index() 去查下标那本质上又是一次O(n)扫描把哈希省下来的时间又浪费回去了。第二判断补数在不在表里的时候直接if complement in seen就行不要用if complement in seen.keys()后者会额外生成一个视图对象。第三如果你在做算法题的过程中想让代码更严谨可以在for循环结束之后加一句return []虽然题目说了必有解但写明确会让代码风格更完备。运行验证用nums [2, 7, 11, 15], target 9输出结果就是[0, 1]。我建议你把nums [3, 2, 4], target 6也跑一遍这个案例能帮你确认自己写的不是“同一个元素用两次”答案是[1, 2]而不是[0, 0]因为33虽然也等于6但下标0不能重复使用。这种样例设计能力在面试中很加分它说明你在用测试用例反向验证代码逻辑。3.2 三数之和排序双指针去重的全流程题目描述给你一个包含 n 个整数的数组 nums判断 nums 中是否存在三个元素 abc使得 a b c 0请你找出所有满足条件且不重复的三元组。这道题是两数之和的升级版但解法思路变了排序固定一个数再用双指针在剩余区间中找两个数使它们的和等于目标零。我带你完整走一遍def three_sum(nums): # 排序是双指针的前提 nums.sort() n len(nums) res [] # 固定第一个数剩下区间交给双指针 for i in range(n - 2): # 如果第一个数已经大于0后面的数都比它大三数之和不可能为0了 if nums[i] 0: break # 跳过重复的第一个数避免产生重复三元组 if i 0 and nums[i] nums[i - 1]: continue # 左指针从i1开始右指针从数组末尾开始 left, right i 1, n - 1 while left right: total nums[i] nums[left] nums[right] if total 0: 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 elif total 0: # 总和太小左指针右移增大总和 left 1 else: # 总和太大右指针左移减小总和 right - 1 return res这里面有三个细节是我反复测试后觉得必须重点说明的第一排序这个操作是必须的它能把“任意数组找三个数”的问题转成“有序数组用双指针收敛”的问题。排序本身O(n log n)的时间复杂度在所有排序解法里已经算很优秀的了而且它带来的双指针优势会让整体性能远好于暴力解法。第二外层for循环跳过重复值的方式是nums[i] nums[i - 1]注意不是nums[i] nums[i 1]。为什么因为i是当前固定的第一个数我们要保证“同一个值只作为第一个数出现一次”如果用i 1去比较就会误杀[0, 0, 0]这种合法答案在数组为[0,0,0]时期望输出就是[[0,0,0]]如果跳过就会漏解。这个例子是我当时踩过坑才总结出来的。第三找到一组答案之后left和right为什么要同时收缩因为我们已经用while循环把和当前值相等的所有重复值都跳过了所以收缩后的左右指针指向的一定是新值。如果你只缩一个指针那下一轮判断大概率还是会得到相同的组合比如左移后仍是重复值白做无效操作。这里“找到后立即去重收缩”是双指针题型的高频考点每次都必须想清楚。3.3 反转链表三步走与断链防护题目描述定义一个函数输入一个链表的头节点反转该链表并输出反转后链表的头节点。这道题在LeetCode上编号是206在面试中出现频率属于顶级。代码短小精悍但写对的人不多主要问题出在“指针操作的顺序”上。class ListNode: def __init__(self, val0, nextNone): self.val val self.next next def reverse_list(head): prev None curr head while curr is not None: # 第一步先保存当前节点的下一个节点防止断链 next_node curr.next # 第二步反转指针让当前节点指向它的前一个节点 curr.next prev # 第三步整体前进prev和curr都往后移 prev curr curr next_node return prev整个过程可以用一句话记“保存、指向、前进”。我把每一步的心理动机展开说第一步next_node curr.next必须要放在第一句。很多初学者先curr.next prev再想保存curr.next发现原来是后一个节点的引用已经被覆盖了链表从中间断成两截。第二步curr.next prev是反转的核心。经过多次循环之后prev 指向“当前节点之前的那个节点”也就是反转后它应该指向的位置。第三步prev curr; curr next_node顺序不能变一旦顺序变了prev 就会被提前覆盖反转过程就会错乱。这里还可以考虑一下递归写法。递归的本质是“递到链表末尾再从末尾往回指”。代码可以很简洁def reverse_list_recursive(head): if head is None or head.next is None: return head new_head reverse_list_recursive(head.next) head.next.next head head.next None return new_head但我的建议是面试的时候优先写迭代版。递归虽然看起来酷但“head.next.next head”这两行其实更难解释清楚而且如果链表特别长递归还可能导致栈溢出让面试官对你的“工程意识”打折扣。迭代版代码量多两行但每一步都清晰可控反而更容易拿高分。3.4 之字形层序遍历队列BFS的变种实现题目描述给定一棵二叉树返回其节点值的之字形层序遍历即先从左往右再从右往左进行下一层遍历以此类推层与层之间交替进行。这道题算是层序遍历的经典变种LeetCode编号103。它的核心逻辑仍然是用队列做BFS唯一多出来的就是层数的奇偶判断。我用代码说明from collections import deque class TreeNode: def __init__(self, val0, leftNone, rightNone): self.val val self.left left self.right right def zigzag_level_order(root): if not root: return [] res [] queue deque([root]) left_to_right True # 第一层从左往右 while queue: level_size len(queue) level_vals [] for _ in range(level_size): node queue.popleft() level_vals.append(node.val) if node.left: queue.append(node.left) if node.right: queue.append(node.right) # 根据当前层方向决定是否需要反转 if not left_to_right: level_vals.reverse() res.append(level_vals) # 下一层方向取反 left_to_right not left_to_right return res这里有一个稍微高级的优化思路也是我在后面刷“剑指Offer”的时候才想通的不要用level_vals.reverse()而是依然按照原顺序遍历但在放入level_vals的时候根据方向选择“从头部插入”还是“从尾部插入”。比如# 方向为从左到右时正常append if left_to_right: level_vals.append(node.val) else: # 从右到左时改成从头插入 level_vals.insert(0, node.val)不过insert(0, ...)的时间复杂度是O(n)如果每一层都用它效率反而不如append reversereverse也是O(n)但常数更小。我在面试中遇到这道题时会主动跟面试官讲这个权衡然后选择 “append reverse” 作为最终方案。这种“主动讨论时空权衡”的交流往往能让面试官看到你不是在背题而是在真正思考。3.5 不同路径从DFS到DP的递进题目描述一个机器人位于一个 m x n 网格的左上角机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角问总共有多少条不同的路径我前面已经讲了状态转移方程这里直接上代码再用一个样例具体演示。def unique_paths(m, n): # dp[i][j] 表示到达坐标(i, j)的路径数 dp [[1] * n for _ in range(m)] # 第一行和第一列都是1已经在初始化时填好 for i in range(1, m): for j in range(1, n): # 当前格子的路径数 上方格子路径数 左方格子路径数 dp[i][j] dp[i - 1][j] dp[i][j - 1] return dp[m - 1][n - 1]我用 m 3, n 7 手动验算一下最终结果是28代码输出就是28正确。你可能好奇为什么第一行第一列直接初始化为1因为第一行的格子只能靠一直往右走到达路径只有一条第一列同理只能靠一直往下走。这个初始化是DP里最容易被忽略的部分一旦漏了数组里全是0整张表都填不起来。关于这道题还有一个空间优化版本既然每一行只依赖上一行和当前行左边的值完全可以用一维数组滚动更新。def unique_paths_optimized(m, n): dp [1] * n for i in range(1, m): for j in range(1, n): dp[j] dp[j] dp[j - 1] return dp[n - 1]这个优化后的代码行数更少空间从O(m*n)降到O(n)。面试时如果你能先把基础版写出来再主动提一句“其实这里的空间还可以继续优化因为每一行的计算只依赖上一行”面试官通常都会很满意。这道题是DP的一个很好的“示例样本”通过它你可以把后面遇到的大部分“网格路径类”DP题的套路摸透。4. 常见问题与调试实录这些坑我替你踩过了题目本身总会做对但那些“为什么我的代码一跑就超时”“为什么某段逻辑看着没问题但结果就是不对”的问题才是真正让人崩溃的地方。我把这个系列里遇到的高频问题和对应的排查思路整理出来希望能帮你在写题时少走弯路。4.1 哈希表相关覆盖和顺序问题两数之和这道题最容易踩的坑是“把所有元素先一次性存到哈希表再遍历查找”。我前面讲过这样会导致重复元素覆盖的问题但其实还有一个更隐蔽的哈希表的存储顺序。如果你把所有元素先存一遍再遍历数组时去查补数你会遇到[3, 3]这种输入第二个3确实能查到第一个3但第一个3会被第二个3覆盖下标就错了。所以我一再强调正确的姿势是“边遍历边存”查完当前元素再把它放入哈希表这样才能保证不会“自己配自己”。判断哈希表里是不是存在某键时还有一个 Python 特有的坑dict.has_key()在 Python 3 已经废除了很多人还在用会直接报 AttributeError。正确写法是if key in dict。另外如果键可能是整数0而你要判断的是“键是否存在”不要写if dict.get(key)因为get返回0时会被当成False逻辑直接出错。这种细节笔试不报错但面试现场一报错就是灾难。4.2 双指针类的越界与死循环三数之和这类双指针题最常见的 bug 就是 left 或 right 走过头。我见过有人这么写while nums[left] nums[left 1]: left 1这段代码一旦遇到left已经走到n - 1left 1就越界了。正确写法一定要加上区间判断while left right and nums[left] nums[left 1]: left 1left right这个条件既是去重的边界也是安全的边界不能省。同样右指针在while循环中也要检查left right。这个坑在指针类题目里几乎人人都踩过我第一次写三数之和的时候就因为这个越界问题运行直接报 IndexError调试了好几分钟才反应过来。另外一个很隐蔽的问题是“在内层 while 循环中修改了 left 或 right外层循环是否有保护”。比如外层 while 的条件是while left right但如果在一次进入循环后内层去重代码把 left 跳到了 right 甚至越过 right下一次外层循环判断就出问题了。最好的办法是在内层循环里时刻带着left right判断确保指针不会相互交错。4.3 链表操作断链和循环引用链表题调试起来最痛苦因为报错信息往往只是“AttributeError: NoneType object has no attribute next”看起来毫无头绪。我自己总结的排查方法是在每一步指针操作后都问自己三个问题——当前节点的 next 指向哪里我是否丢失了对后续节点的唯一引用如果我把当前节点的 next 修改了后续遍历还能继续吗反转链表里我一再强调先保存后指向就是为了防断链。另一个容易犯的错误是“反转后返回了错误的头节点”比如返回 prev 还是 curr很多人分不清。循环结束时curr 已经是 None说明走到链表末尾了此时 prev 正好停在原链表的最后一个节点上也就是反转后的头节点。如果你返回 curr就相当于返回了一个 None整个算法白写了。4.4 二叉树 BFS层数与空节点的处理之字形遍历这道题很多人在“用不用再建一个队列”这件事上纠结。其实不需要一次入队出队完全够用关键是level_size len(queue)这一步必须在for循环前记录下来因为for循环过程中队列长度一直在变如果直接for i in range(len(queue))循环次数会被动态改变导致层与层之间的数据都混在一起。这是个非常经典的 BFS 陷阱。另一个关于反序列化的坑如果层序遍历序列里面用 0 表示空节点那你可能遇到“节点值为0”和“这个节点是空的”这两种情况。这个时点一定要和面试官确认输入格式到底是 null、None、0 还是 #不要想当然。我见过有人用if node.val 0来判断空节点结果遇到的测试数据里正好有值为0的节点整道题直接崩了。4.5 DP中的数组初始化和索引错位DP题的bug通常比别的题更隐蔽因为代码不报错但答案就是错的。不同路径这道题最常见的两个错误一是数组dp没有正确初始化导致dp[0][0]的值不对后续累加全部出错二是在循环中把行列写反比如用了dp[j][i]而不是dp[i][j]如果 m 和 n 不相等很可能某一次索引越界但如果是方阵mn越界不会发生答案却是错的。这种“方阵掩盖错误”的情况面试时最容易蒙混过关也最容易让你在后续追问中翻车。我的自查习惯是写完dp代码后用两个小样例跑一遍一个 1x1 的边界矩阵期望结果1一个 2x3 或 3x2 的矩阵看结果是否符合手算。如果这两个都通过那逻辑基本就是对的。很多人总觉得测试样例要选大的才有效恰恰相反在算法题里边界值和小矩阵最能把逻辑漏洞暴露出来。5. 面试与应试中的关键技巧从“会做”到“讲明白”我刷了这么久算法题最深的体会是笔试能过的人很多但面试时能把一道题讲得清晰透彻的人真的不多。面试官看的不光是你最终写出来的代码更是你的思维过程。所以这里单独写一节分享几个我觉得很实用的面试表达技巧。5.1 先确认题目条件再动手不管是电话面试还是现场手写我都建议你先花三十秒和面试官确认几个关键信息输入规模是多少数组是否有序数据范围有没有负数返回值是下标还是值链表是否有环这个习惯有两个作用。第一它避免了你误解题意白写二十分钟第二它让你接下来的解法有了充分的“铺垫”比如你知道了数组无序就可以说“因为是无序的所以哈希表是最自然的选择”。这会让面试官觉得你是真的在思考题目而不是一上来就套模板。我记得有一次面试面试官给了一道“搜索旋转排序数组”我没注意到数组里可能包含重复元素直接按无重复的解法写了结果测试数据里出现了重复值答案不一致。面试官提示我“考虑一下重复元素会带来什么问题”我才意识到边界条件没有确认清楚。从那以后我养成了一个习惯拿到题先口述一遍自己的理解等面试官确认后再动手。这个习惯在几乎所有面试里都帮我省了大麻烦。5.2 代码风格影响评分笔试环境里没人管你的代码风格但面试手写代码时面试官是有明确评价的。我建议刻意保持这几个习惯变量名不要用 a、b、c 这种没有语义的单个字母函数名和使用逻辑尽量贴合题目语义在关键步骤旁边加注释说明“这一步在干什么”。别小看这些细节它能帮你争取到“沟通能力”“代码规范”这些维度的加分。举个很简单的例子同样是反转链表的代码如果你写的是def f(head): p None c head while c: n c.next c.next p p c c n return p面试官虽然能看懂但阅读成本很高如果写的是带注释和全称变量的版本观感完全不一样。刷题时你可能追求速度忽略这些但面试时一定要提醒自己放慢节奏写出“可读性强”的代码。5.3 一题多解是回答深度的分水岭写出一道题的答案只是及格线真正拉开差距的是你能不能主动说出“还有没有其他解法”。比如两数之和写完哈希解法之后你可以补一句“这道题如果数组是有序的我会用双指针这样可以把空间复杂度降到O(1)。”再比如不同路径写完二维DP之后你可以提一句“这个还可以用组合数学直接求因为机器人一共要走 m-1 次下和 n-1 次右排列组合的数量就是 C(mn-2, m-1)”。这些延伸的内容不必长篇大论点到为止即可。面试官听到这些基本就能判断出你对题目的理解深度远超“背答案”水平。5.4 时间/空间复杂度的表达不要背模板很多人被问到“这个算法的时间复杂度是多少”的时候脱口而出“O(n)”但面试官紧接着问一句“为什么”就卡住了。我建议你在平时练习时就习惯性地分析每一步逻辑所带来的复杂度而不是背答案。比如两数之和我们遍历了一次数组每次循环只做哈希查找和插入所以时间复杂度是O(n)哈希表最多存储n个键值对所以空间复杂度是O(n)。这样分析信息完整逻辑清晰完全不需要死记硬背。反过来如果你连自己的代码为什么是O(n)都说不清楚那面试官大概率会认为你是在背模板。6. 从算法题到工程思维刷题到底在刷什么说实话进了大厂之后纯算法题在日常业务开发中直接出现的场景并不多。你不太可能天天写“三数之和”也不太可能真的需要手写一棵平衡二叉树的旋转操作但这不代表刷题没有意义。我觉得算法题真正训练的是三样东西分解问题的能力、选择数据结构的能力、以及审慎验证的习惯。第一个分解问题的能力在工程里就是“需求拆解”。给你一个模糊需求你能不能把它拆成若干个子模块每个子模块用什么数据结构实现这就是算法题里“理解题目-设计解法-编码落地”的翻版。第二个选择数据结构的能力更直接了。你写一个功能模块什么时候用哈希表什么时候用队列什么时候必须用栈这些选择在很多场景下直接决定了系统的性能。第三个审慎验证的习惯其实就是“测试思维”。写代码前想好边界条件写完用几个样例验证这个习惯在工程里的价值比任何一门框架都高。我自己带过一个小团队面试候选人的时候会更关注候选人遇到不会的题时的心态和反应。有一种候选人题目刚看完就说“这题我没见过”然后停下来不说话了另一种候选人同样没见过这道题但是会试着从熟悉的问题出发把未知向已知转化比如“这个题很像两数之和我能不能借鉴哈希的思路”。后者往往才是真正能把算法能力迁移到工程里的人。所以刷题本身不是目的它其实是训练你的思维方式让你在面对陌生问题时不慌、有路、能落地。7. 这一篇之外后续刷题路线的建议如果这一篇的题目你都能自己写出代码并讲清楚思路恭喜你你已经具备了继续深入的基础。接下来的路线我根据自己的经历给一个实用建议第一把每个基础范式对应的“代表题”做透。哈希对应两数之和、三数之和、字母异位词分组双指针对应LeetCode 11盛最多水的容器、15三数之和、42接雨水链表对应206反转链表、141环形链表、21合并两个有序链表二叉树对应102层序遍历、103之字形遍历、124二叉树中的最大路径和DP对应70爬楼梯、62不同路径、300最长递增子序列。这些题每个都值得刷两遍第二遍刷的时候尝试不参考任何资料自己在零基础上推一遍思路。第二做“专题周”而不是随机乱刷。建议每周只刷一个主题比如这周只做双指针下周只做二叉树。这样做的优势是你很容易总结出规律原来双指针问题为什么总能O(n)解决因为它把二维穷举压缩成了一维的指针移动原来二叉树层序遍历的模板是 queue while for 三层结构。一旦形成了类似的“模式识别”见到新题就能快速归类。第三不要害怕做不来hard题。很多人的挫败感来自于一上来就刷hard题一个小时过去了还是没思路。我的建议是easy和medium是构建信心的hard是用来见识“原来还能这么想”的。每次做完一道hard题不要停下来立刻去看别人的题解重点看他们的思考路径而不是代码。你会发现再难的题也是由若干个基础范式组合而成的。比如滑动窗口最大值本质就是“单调队列窗口滑动”最大回文子串本质就是“中心扩展双指针”。第四刷题过程中一定要会用调试器或者 print 观察中间结果。我见过很多人在本地 IDE 跑不过就硬看代码其实只要打印一下关键变量逻辑瞬间就清楚了。比如三数之和你可以在内层循环开头打印 left、right、total。打印出来你就知道是去重逻辑写错了还是指针移动条件有问题。调试也是工程能力的一种面试时能用 print 定位问题反而能给面试官留下一个“有实战经验”的印象。