
1. 题目定位与刷题价值反转链表这道题在 Leetcode Hot 100 里的编号是 206对准备春招秋招的候选人来说它几乎是链表题的第一道关卡。只要面试考链表大概率就是从反转链表或者它的变体开始问的比如反转前 K 个节点、反转区间、K 个一组翻转链表这些都是 206 题的衍生。把这道题吃透后面那些看起来吓人的变体题本质上就是在你已有的解法框架上加参数、加边界条件而已。我在给朋友做刷题辅导的时候经常说一句“链表题不会反转等于没学链表”。这不是夸张因为链表的指针操作、节点断开与重接、边界处理这些基本功全部浓缩在这一道题里。ACM 模式下的反转链表更是如此它要求你不仅会写核心逻辑还要会自己处理链表构造、输入读取和结果输出这对机试场景的还原度很高。平时在 Leetcode 网页上刷题平台已经把 ListNode 定义好了函数签名也规定好了你只需要填核心逻辑但到了真正的机试、笔试环节题目给的往往是一个完整的程序框架甚至让你自己写 main这时候很多人就懵了不是不会反转而是不知道链表怎么建、怎么读、怎么打印。这篇文章我会把 206 题在 ACM 模式下的完整解法拆开揉碎从链表定义、输入处理到迭代法和递归法的原理与代码再到常见的边界情况排查一次性讲清楚。无论你是刚开始刷 Leetcode 的算法新手还是准备机试的求职者这篇文章都能帮你把反转链表这个基础点彻底搞定。2. ACM 模式与核心代码模式的区别2.1 两种模式对解题能力的要求完全不同很多在校生习惯在 Leetcode 网页上刷题那里是“核心代码模式”你只需要实现一个类的方法比如 ListNode* reverseList(ListNode* head)剩下的输入输出、链表构造、测试用例匹配平台全部帮你处理完了。这种模式的好处是练习算法思路很纯粹缺点是到了真正的笔试现场很多人会栽在“如何把一行数字变成链表”这种看似简单的事情上。ACM 模式也叫竞赛模式或机试模式要求你写出完整可运行的程序定义数据结构如果有需要、写 main 函数、自己处理标准输入、自己构造测试数据、自己打印输出。就拿反转链表来说核心代码模式下你只要写反转逻辑ACM 模式下你需要先读入一个整数表示链表长度再依次读入每个节点的值用这些值构建链表然后调用反转函数最后遍历链表并按格式输出。这中间的每一步都可能出问题而且出问题之后调试起来比核心代码模式麻烦得多。我在实际面试中观察到一个现象同样一道反转链表让候选人用核心代码模式写大多数人都能写对但要求手写整个 ACM 模式的程序框架代码质量就参差不齐了。有的人不会处理输入终止条件有的人忘记释放内存有的人输出格式多了一个空格被扣分。这些都是可以提前练习的东西不需要很高的算法水平只需要在平时刷题的时候多留一个心眼每道题都用 ACM 模式跑一遍。2.2 链表结构与输入输出的完整实现ACM 模式下链表这个数据结构需要自己定义。标准定义一般长这样#include iostream using namespace std; struct ListNode { int val; ListNode* next; ListNode() : val(0), next(nullptr) {} ListNode(int x) : val(x), next(nullptr) {} ListNode(int x, ListNode* next) : val(x), next(next) {} };这里定义了三个构造函数方便不同场景下创建节点。如果你用 C 语言写结构体里不能有构造函数就需要自己写一个 createNode 函数来分配内存并初始化。C 的这个定义是从 Leetcode 上沿用下来的在 ACM 模式里也完全适用。输入处理的逻辑是第一行读入链表长度 n如果 n 为 0链表为空直接输出空行即可。如果 n 大于 0接下来读入 n 个整数依次构建节点并连接到链表尾部。构建链表的常见写法有两种一种是用尾指针不断追加另一种是维护一个 dummy 头节点来简化边界操作。我推荐用 dummy 节点因为当 n 0 时你不需要单独判断代码更干净ListNode* buildList() { int n; cin n; ListNode* dummy new ListNode(-1); ListNode* tail dummy; for (int i 0; i n; i) { int x; cin x; tail-next new ListNode(x); tail tail-next; } return dummy-next; }输出部分没什么难度遍历链表打印每个节点的值节点之间用空格分隔末尾换行。但这里有个细节一般要求行尾不能有多余空格所以需要判断一下当前节点是不是最后一个节点。我用一个 cur ! nullptr cur-next ! nullptr 的判断来决定要不要打印空格这样输出就是从 5 4 3 2 1 而不是 5 4 3 2 1 这种带尾随空格的格式。void printList(ListNode* head) { ListNode* cur head; while (cur ! nullptr) { cout cur-val; if (cur-next ! nullptr) cout ; cur cur-next; } cout endl; }主函数里把这三个步骤串起来构建链表、调用反转函数、输出结果。这里我建议在 main 里把链表构建和反转分开写这样如果要测试多个案例只需要重复调用即可。int main() { ListNode* head buildList(); ListNode* ans reverseList(head); printList(ans); return 0; }ACM 模式的代码写多了以后你会形成一种条件反射看到题目先想数据结构和输入输出格式再想算法逻辑。这种习惯在笔试中非常有用因为真正到了考场上最大的敌人不是算法本身而是各种格式、编译、运行时错误带来的时间消耗。3. 迭代法反转链表三指针步步为营3.1 核心思路与指针的职责划分迭代法是反转链表最直观、最符合直觉的解法。它的核心思想可以概括为遍历链表的过程中把每一个节点的 next 指针从“指向下一个节点”改为“指向前一个节点”。这个操作听起来简单但有一个关键问题当你把 cur 节点的 next 改成指向前一个节点之后原本的下一个节点就找不到了因为只有 cur-next 这一条路可以到达原来的下一个节点。所以我们必须提前把下一个节点保存下来这就是第三个指针存在的意义。三个指针的职责是这样的pre 指针指向当前节点的前一个节点。初始时链表头部的前面是空所以 pre 初始化为 nullptr。cur 指针指向当前正在处理的节点。初始时 cur 指向链表头节点。nxt 指针用于暂存 cur 的下一个节点防止修改 next 指针后找不到路。这个过程有点像排队转身一个人记着前面的人是谁另一个人要转过身去但他转身之前得先让旁边的人帮他记一下他后面是谁。等前面的人都转完身队伍的朝向就完全反过来了。3.2 迭代法代码与逐步演示迭代法的典型代码如下class Solution { public: ListNode* reverseList(ListNode* head) { ListNode* pre nullptr; ListNode* cur head; while (cur ! nullptr) { ListNode* nxt cur-next; cur-next pre; pre cur; cur nxt; } return pre; } };我们用一个具体例子来走一遍整个过程。假设原始链表是 1 - 2 - 3 - 4 - 5 - nullptr。初始状态pre nullptrcur 节点1链表1 - 2 - 3 - 4 - 5 - nullptr第 1 轮循环nxt 节点2cur-next pre即 1 的 next 指向 nullptrpre 节点1cur 节点2此时链表形态nullptr - 12 - 3 - 4 - 5 - nullptr第 2 轮循环nxt 节点3cur-next pre即 2 的 next 指向节点1pre 节点2cur 节点3此时链表形态nullptr - 1 - 23 - 4 - 5 - nullptr后面的循环以此类推。到第 5 轮循环结束时pre 指向节点5cur 指向 nullptr循环终止。返回 pre此时链表变成 5 - 4 - 3 - 2 - 1 - nullptr。注意一个容易混淆的点循环结束后 cur 是 nullptr说明我们已经遍历到了链表末尾之外。此时 pre 正好指向反转后链表的头节点这是巧合吗不是。pre 的含义是“当前节点的前一个节点”当 cur 变成 nullptr 时pre 自然就是原链表最后一个节点也就是反转后链表的第一个节点。3.3 迭代法的边界条件与复杂度分析迭代法的时间复杂度是 O(n)链表的每个节点都被访问一次。空间复杂度是 O(1)只用到了三个指针没有额外开辟与链表长度相关的内存。边界条件方面最关键的是处理空链表和单节点链表空链表head 为 nullptrcur 初始为 nullptrwhile 循环一次都不执行直接返回 nullptr。程序不会崩溃逻辑也是正确的。单节点链表链表只有一个节点循环执行一次后 pre 指向该节点返回它本身。反转单节点链表结果还是它自己。这两个边界情况在面试中经常被问到如果你能主动说出来会显得你想问题比较全面。我在实际调试中还发现一个新手常犯的错忘记保存 nxt直接写 cur-next pre然后试图通过 cur-next 找到下一个节点。这显然是矛盾的因为 next 已经被改成指向前一个节点了。修改完之后就必须用 nxt 这个副本才能继续前进。所以说nxt 指针不是可选项而是必需的。注意迭代法的核心就是记住“先保存后修改”。在任何涉及修改链表指针的操作中只要发现某个节点的 next 要被改写而这个节点后面还有内容就先把这个内容存下来。4. 递归法反转链表换个角度看问题4.1 递归的思考方式与子问题拆解递归法的代码比迭代法短很多但理解起来门槛更高。很多初学者卡在递归这里是因为他们试图在脑子里一层一层地模拟递归调用结果被调用栈搞晕了。我建议换一种思考方式不要跟踪递归的每一层而是假设递归函数已经完成了它的任务重点思考“当前层需要做什么”。对于反转链表这个函数我们定义它的语义是传入一个链表的头节点返回反转之后的新头节点。那么对于当前节点 head我们调用 reverseList(head-next)这个调用会返回什么它会返回“从 head-next 开始到链表末尾这一段反转之后的新头节点”。用一个例子说明。假设原始链表是 1 - 2 - 3 - 4 - 5 - nullptr我们在最外层调用 reverseList(节点1)。它内部会调用 reverseList(节点2)而 reverseList(节点2) 内部会调用 reverseList(节点3)依此类推直到最后一个节点。关键来了reverseList(节点2) 这个调用完成后返回的是反转后的新链表的头节点这个新链表是 5 - 4 - 3 - 2。此时整个链表的内存结构是什么样的节点1仍然指向节点2但节点2的 next 已经被反转操作改成了指向节点3吗不是的反转后节点2的 next 已经被改成 nullptr 了因为节点2在反转后的链表中是尾节点。所以当递归调用返回时当前层的状态是head 节点1head-next 节点2这个指针没有变从节点2开头的子链表已经反转完成即 5 - 4 - 3 - 2 - nullptr我们现在要做的就是把节点1接到这个反转后的子链表末尾。节点2现在是子链表的末尾要让节点2的 next 指向节点1也就是 head-next-next head。然后把节点1的 next 置为 nullptr因为节点1在反转后的链表中是新的尾节点。这样整个链表就变成了 5 - 4 - 3 - 2 - 1 - nullptr。4.2 递归法代码与关键行解析递归法的代码非常简洁class Solution { public: ListNode* reverseList(ListNode* head) { if (head nullptr || head-next nullptr) return head; ListNode* newHead reverseList(head-next); head-next-next head; head-next nullptr; return newHead; } };这段代码里最关键的两行是 head-next-next head 和 head-next nullptr。很多人不理解为什么要写这两行上面我们通过子问题的分析已经解释了。我再从“指针指向”的角度补充一个认知递归调用 reverseList(head-next) 完成之后head-next 这个指针本身并没有改变它仍然指向原来的下一个节点。但在反转后的新链表中head-next 所指向的节点即原链表的第二个节点已经变成了新链表的尾节点它的 next 已经变成了 nullptr。而我们希望新链表的尾节点指向 head所以直接通过 head-next-next head 来修改原第二个节点的 next 指针。这行代码利用了 head-next 这个现成的“入口”巧妙地把当前节点 head 接到了反转后子链表的末尾。至于 head-next nullptr是因为 head 已经成为新链表的尾节点尾节点的 next 必须为 nullptr。如果不设置head 的 next 还指向原第二个节点而原第二个节点的 next 已经指向 head这样就会形成环导致遍历链表时死循环。递归的终止条件是 head nullptr || head-next nullptr也就是链表为空或者只有一个节点。这两种情况都不需要反转直接返回 head 即可。这也是递归的出口确保了递归不会无限进行。4.3 递归调用过程的可视化理解为了帮你彻底理解递归我给出一个非常直观的步骤推演。假设链表为 1 - 2 - 3 - nullptr调用 reverseList(节点1)head 是节点1head-next 不是 nullptr继续递归调用 reverseList(节点2)head 是节点2head-next 是节点3继续递归调用 reverseList(节点3)head 是节点3head-next 是 nullptr命中终止条件返回节点3回到节点2这一层newHead 是节点3head-next-next head即 节点3-next 节点2head-next nullptr即 节点2-next nullptr返回 newHead节点3回到节点1这一层newHead 是节点3head-next-next head即 节点2-next 节点1head-next nullptr即 节点1-next nullptr返回 newHead节点3整个过程结束后链表变成了 3 - 2 - 1 - nullptr确实完成了反转。这个过程中newHead 从最深的递归层一路返回始终是原始链表的最后一个节点也就是反转后链表的头节点。它从第一次确定之后就不再改变后面的递归回溯只是在把其他节点接到它后面。我个人的经验是理解递归最有效的方式就是找一个链表长度是 3 或 4 的例子像上面这样一层一层地推演一遍。推演完一个例子之后递归的结构就清晰了。如果在面试中面试官让你讲递归的思路你也可以用类似的“子问题 当前层操作”的方式来讲而不是从头到尾背代码。注意递归虽然代码短但空间复杂度是 O(n)因为系统栈需要保存每一层调用的现场信息。如果链表很长比如上万个节点递归可能导致栈溢出。实际项目中如果要反转特别长的链表迭代法是更安全的选择。5. 两种实现方式的横向对比与选型建议5.1 时间、空间与代码复杂度对照迭代法和递归法都能正确解决反转链表问题但它们各有特点。我整理了一张对比表方便你直观理解两者的差异。对比维度迭代法递归法时间复杂度O(n)O(n)空间复杂度O(1)O(n)递归栈深度代码长度略长代码量稍多极短逻辑高度浓缩理解难度直观容易上手需要建立递归思维风险点忘记保存 nxt 指针栈溢出、断链形成环面试考察点指针操作的熟练度递归与子问题分解能力时间复杂度方面两种方法都需要遍历或回溯到每一个节点所以都是 O(n)。空间复杂度是两者最显著的区别迭代法只用了三个指针额外空间是常数级递归法每一层递归都会在系统栈中压入一条调用记录空间占用与链表长度成正比。5.2 不同场景下的解法选择我在面试中最常见的画面是面试官先让你写迭代法写完之后问“能不能用递归实现一下”然后让你解释递归的思路。为什么面试官喜欢这么问因为他想同时考察两个维度第一你是否能熟练操作链表指针第二你是否具备递归分解问题的能力。如果你时间紧张只想先拿下一道题我建议你优先掌握迭代法。理由很实在迭代法的思路和代码都是可控的不会出现调不明白的问题而且空间效率更高不容易踩坑。但如果你准备得比较充分递归法一定要掌握因为很多链表的进阶题比如反转区间、K 个一组翻转用递归来思考会简洁很多。还有一个选型建议如果题目明确要求“不能用递归”或者“空间复杂度需要 O(1)”那就只能写迭代。比如某些嵌入式场景或系统级代码对栈空间极其敏感递归往往是被禁止的。反过来如果反问句允许使用递归且链表长度不大两种方法都可以。我从真实面试中得来的经验是候选人如果能主动分析两种解法的时间和空间复杂度并说明为什么在特定场景下选择某一种会给面试官留下很好的印象。这说明你不只是背了一道题而是真正理解了算法设计的取舍。5.3 三个面试追问与应对思路面试官在反转链表这道题后面经常紧跟三个追问如何处理空链表、如何处理只有一个节点的链表、能否用递归再写一遍。前两个问题的答案我们在前面已经提到过空链表和单节点链表都直接返回头节点。第三个问题我们在递归法部分已经详细解答。还有一个经常被延伸到的问题反转链表的递归和迭代本质上有什么区别我的理解是迭代法是“从头到尾边走边改”递归法是“先走到尾再原路返回时逐层修改”。迭代法利用循环来推进递归法利用系统栈来回溯。两者操作的节点数相同只是执行的顺序和依赖的机制不同。如果面试官追问“tail 递归会不会多一层调用”你可以回答reverseList(nullptr) 这种调用会在空链表时发生一次但返回立即生效后续代码不会执行所以逻辑上没有问题。这个细节也能体现你对递归终止条件的理解深度。6. 常见问题与边界情况的排查实录6.1 写好代码后仍报错的四个高频原因我在带人刷题和实际的面试模拟中发现反转链表这道题出错的位置高度集中。整理出来大家写的时候对照自查。原因一迭代时修改 next 之前没有保存后继节点。这是最经典的错误。如果你直接把 cur-next pre 然后想把 cur 移到原链表的下一个节点只能通过 cur-next 去找但此时 cur-next 已经被改成了 pre原后继节点就永久丢失了。正确操作一定先备份 nxt cur-next再修改 next 指针最后 cur nxt。原因二递归时 head-next 被提前置空。在递归代码里如果你先写了 head-next nullptr再写 head-next-next head就会出问题。因为 head-next 已经变成 nullptr你后面再访问 head-next-next 就会造成空指针异常。必须严格遵守先接新链head-next-next head后断开旧链head-next nullptr的顺序。原因三返回了错误的节点。迭代法的返回值应该是 pre而不是 cur 或 head。因为循环结束时 pre 是原链表的最后一个节点也就是反转后链表的第一个节点。递归法的返回值应该是 newHead它是原链表最后一个节点从最深一层递归一路传上来的不能返回 head 或 head-next。原因四ACM 模式下输入输出处理出错。例如漏掉对 n 0 的处理或者链表构建时没有把尾节点的 next 置为 nullptr导致遍历时访问非法内存。还有输出行尾空格被数组或测评系统判错。这些不是算法问题但同样会导致丢分。6.2 排查用的调试技巧如果你手边有调试器或者可以写临时测试代码我推荐用这两种方法快速定位问题。方法一打印指针地址。在关键节点打印 cur、pre、nxt 的地址检查链表是否出现环。如果发现某个指针地址在链表遍历中重复出现说明链表有环大概率是递归时没有断开 head-next。方法二用小规模用例测试。分别用空链表、单节点链表、两个节点的链表、三个节点的链表来做验证。如果两个节点和三个节点的用例都通过而空链表和单节点链表出错那问题基本就在边界条件的处理上。如果只有三个节点以上的用例出错那大概率是循环逻辑或递归回溯逻辑写错了。方法三打印递归的进入和退出。在递归函数开头打印传入的 head-val在返回前打印返回的节点值就能看出递归的调用顺序是否正确。如果发现回溯时 head-next 已经变成 nullptr那问题就出在断链的时机太早了。6.3 ACM 模式下的常见坑位ACM 模式下链表构建是最容易出问题的地方。下面这个代码是很多人在笔试时写出来的版本我标记出其中的问题ListNode* buildList() { int n; cin n; ListNode* head new ListNode(); ListNode* p head; for (int i 0; i n; i) { int x; cin x; p-next new ListNode(x); p p-next; } return head-next; }这段代码本身能工作但有几个隐患。第一head 是一个实际分配的节点dummy 节点如果忘了最后返回 head-next就会把 dummy 节点也当作链表的一部分输出结果头部多了一个默认值 0。第二如果 n 为 0dummy-next 是 nullptr返回 nullptr逻辑上没问题但要注意 main 函数里的反转函数能否接收 nullptr。第三ACM 模式里不要求释放内存所以 new 出来的节点可以不用 delete但我个人习惯在极端注意内存的笔试环境里简单评估一下如果循环创建大量节点而且内存限制很紧可以在反转结束后遍历链表并 delete 所有节点。不过大部分机试不会因为这个卡人核心还是功能正确。6.4 反馈与验证写完代码后正确性的验证不能只靠“感觉对了”。我的习惯是构造几组典型用例跑一遍确保所有分支都覆盖到。对于反转链表至少要跑这六组用例测试用例输入预期输出主要验证点空链表0空行边界处理单节点1 11边界处理两个节点2 1 22 1基本反转三个节点3 1 2 33 2 1基本反转多个节点含重复值5 3 3 2 1 33 1 2 3 3值重复不影响结构已经反转后的链表3 3 2 11 2 3二次反转回到原序跑完这些用例基本可以确定代码在功能上是完整的。在 Leetcode 里提交时平台会自动跑大量隐藏用例但如果你在自己练习时能养成先构造用例的习惯到真实笔试时会少走很多弯路。7. 从反转链表延伸到整个 Hot 100 刷题路线7.1 链表类题目的主线安排反转链表在 Leetcode Hot 100 中看似只是一道小题但它在链表这个专题里的地位非常关键。掌握了这道题后面很多题目都会沿袭类似的处理思路。比如 Leetcode 92 反转区间链表就是反转链表的加参版本先找到要反转的那一段的前驱节点把这一段单独切出来做一次反转再接回去。Leetcode 25 K 个一组翻转链表则是把整条链表切成若干段对每一段做一次反转段与段之间再接好。我做 Hot 100 刷题规划时建议的链表专题主线是这样的先做 206 反转链表再做 21 合并两个有序链表然后是 141 环形链表快慢指针接着 160 相交链表最后挑战 25 K 个一组翻转链表。这条主线从基础指针操作走到复杂链表的区间处理难度平滑上升中间不会让你觉得特别跳跃。很多人一上来就直奔 25 题结果被搞到怀疑人生。这不是你不聪明而是前置知识没到位。25 题的每一段反转本质还是 206 题的迭代法。如果 206 的迭代法能闭着眼睛写出来25 题就只是多了一个“按段切分 段间连接”的步骤。所以我的建议是基础不牢别急着上难度先把 206 练成本能反应。7.2 如何安排速度与质量之间的平衡找工作冲刺阶段时间是最宝贵的资源。我在规划 Hot 100 刷题路线时给定的时间大概在 3 到 5 周左右。很多人会问一天刷几道最合适我的经验是不要贪多每天新题控制在 3 到 4 道留出至少 1 小时的时间做复习和总结。反转链表这种基础题第一天刷完第二天应该无提示重写一遍第三天再做一次变形题。重复三次之后这个知识点才算真正长在你身上。ACM 版的刷题还有一个额外好处它强迫你进行完整的输入输出处理。这个能力在核心代码模式下完全得不到锻炼。很多人在 Leetcode 上刷了几百题到了公司笔试环境比如牛客网、赛码网却一脸茫然就是因为平时根本没有练过 ACM 模式的完整代码。我的建议是Hot 100 中的链表题和二叉树题优先用 ACM 模式练习。这些题的数据结构定义相对固定写起来不费劲还能把你对输入输出的处理能力练扎实。7.3 以反转链表为起点建立自己的知识模板我刷题有个习惯每一类经典题最终都会沉淀出一个“模板代码”。反转链表就是链表类模板中最基础的一段。我的模板里包含三部分ListNode 结构体定义、ACM 模式下的构建与打印函数、反转函数本身。之后做任何链表题都直接复制这个模板再根据题目要求改核心逻辑。这个方法听起来简单但在实际面试中非常占便宜。因为笔试时间有限如果你每次都要从零开始写 buildList 和 printList光这些辅助代码就能耗掉十分钟。而有了模板你只需要专注在算法核心部分。所以我在文末给你留一个小建议今天看完这篇文章立刻动手把你的链表模板整理出来存到本地或者笔记里。以后每天做链表题都用这个模板起手你会慢慢感受到效率的提升。提示模板不是越复杂越好。对于链表题一份干净的代码模板应该做到“拿到就能用注释清晰不依赖非标准库”。C 的话只需要标准输入输出和 vector 就够了。8. 我的一些实操心得反转链表这道题是我在做 Hot 100 刷题路线时当之无愧的“开场题”。它简单但又不那么简单。简单在于代码量很少无论迭代还是递归核心代码都只有几行不简单在于它浓缩了链表题的所有核心操作逻辑指针的保存、节点的断开、方向的改变、边界的处理。你要是能把这道题讲明白就说明你离真正理解链表不远了。我最初刷这道题的时候迭代法写了很多遍才能保证一次通过。不是不会而是总在某些边界条件下出错比如空链表比如只有一个节点。后来我总结了一个笨办法每次写完不管题目要不要我都主动去处理空链表和单节点的情况并且输出测试一遍。几次之后这些边界条件就变成了我写代码时的“默认动作”不会再让它们成为丢分点。递归法我一直觉得是在某天突然开窍的。那天我试着不再追踪每一层递归而是把 reverseList(head-next) 当作一个确定的结果来使用它已经把后面的链表反转好了我只需要把当前节点接到这个新链表的末尾。当思维完成这个转换之后递归的代码就变成了一种很自然的结构甚至比迭代法更容易记忆。如果你现在还在为递归头疼不妨试试这个思考方式从“过程模拟”转变成“结果利用”可能会有意想不到的突破。最后再分享一个小技巧反转链表这种题在面试前晚值得快速在纸上默写一遍不需要跑代码只需要手写核心逻辑。写错了就改改完再写一遍直到能把两种解法都在几分钟内写出来。这比临时刷十道新题更管用。面试考的不是你会不会而是你在有限时间内能不能稳定输出。这种稳定只有通过反复默写和演练才能练出来。