ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

链表翻转:从LeetCode 206到K个一组翻转的完整套路

2026/9/13 20:11:29 拓冰建站 浏览量
链表翻转:从LeetCode 206到K个一组翻转的完整套路 昨天有位读者跑来问我链表翻转这种题Leetcode 上标记成“简单”为什么面试的时候一紧张还是写错我回了一句因为你只是在背代码没有真正搞清楚三个指针之间谁先动、谁后动。链表翻转大概是 Leetcode 里最经典、也最容易被低估的一道题。说它经典是因为从热门 100 题到面试手撕代码几乎绕不开它说它被低估是因为很多刷题的人 AC 一次就再也不看结果遇到“K 个一组翻转”“区间翻转”就卡住。实际上链表翻转是链表类题目里最核心的基础操作理解透它回文链表、反转链表 II、K 个一组翻转这些题都能顺下来。这篇文章不打算只给代码我会把迭代、递归两种解法的每一步拆开把指针顺序“为什么这样走”讲明白再附上区间翻转、K 个一组翻转的套路最后聊聊刷题和面试时怎么把这道题讲出亮点。不管你是刚开始刷 Leetcode 的新手还是准备跳槽想快速过一遍基础题的老手这篇文章应该都能给你一点新的东西。1. 一道简单题为什么值得你重新认真做一遍1.1 链表翻转在 Leetcode 题库中的位置Leetcode 上链表翻转的标准题号是 206英文名 Reverse Linked List。它不光是经典题还是很多进阶题的前提。你去看 Leetcode 热门 100 题清单链表分类下有好几道题的解法核心其实都是翻转操作234 回文链表要翻转后半段92 反转链表 II 是区间内翻转25 K 个一组翻转链表是把长链表分段翻转甚至 143 重排链表也要先找中点、再翻转后半段。所以你看这道题不是“刷过就算”的题而是一个基础工具。就跟学排序先学冒泡一样链表操作里翻转就是最常用的基本功。很多人在每日一题里遇到 206觉得“太简单了吧”直接略过。但如果你追问自己几个问题为什么迭代要三个指针递归的返回值到底是谁循环结束后 prev 和 curr 分别指向哪里如果这些问题能脱口而出才算真正吃透了。1.2 这道题真正想考察的能力面试官爱考链表翻转不是因为题目本身难而是它能快速考察三件事。第一你是否理解链表的结构。链表节点由值和 next 指针组成翻转的实质是让 next 指针反向而不是交换节点里的值。很多初学者下意识想交换值其实是数组思维在作祟。第二你的指针操作是否严谨。链表的指针一旦丢失就找不回来操作顺序错一步链表就断了。这考察的是写代码之前有没有先在脑子里理顺顺序。第三你的边界条件处理是否到位。空链表、单节点、两个节点、长链表这些情况代码能否全都跑通平时不写边界测试的人面试时最容易栽在这里。1.3 本文路线一道题到一类题的完整链条我会先讲迭代解法因为它是理解指针流动最好的方式几乎能解决所有链表翻转变式。然后讲递归解法弄明白函数定义之后你会发现递归代码不超过五句话但背后的回溯逻辑必须彻底理解。接着进入变式题把 92 题和 25 题的固定写法讲透。最后分享工程和面试视角下的复盘方法包括测试用例、面试讲解技巧和刷题安排。这条路线是我自己刷题时验证过的。最开始我也只会背代码直到有一次面试被追问“你讲讲递归到最后一层时发生了什么”当场卡壳才意识到自己根本没懂。所以这篇文章我会把“到底发生了什么”作为重点来讲。2. 迭代解法三个指针为什么缺一不可2.1 指针的流动顺序一段一段拆给你看迭代翻转链表最经典的做法是维护三个指针prev、curr、next。prev已经翻转好的链表部分的头节点初始为 nil。curr当前正在处理、即将改变指向的节点初始为 head。nextcurr 的下一个节点用来在指向被改写之前保存现场。为什么需要三个指针因为你要做两件事让 curr.next 指向前一个节点同时还要保证不会丢失原来的下一个节点。如果只有两个指针一旦执行 curr.next prev原来的后继就找不到了链表就断成两截。我用一个例子走一遍。假设链表是 1 - 2 - 3 - 4 - 5初始 prev nilcurr 1。第一步next curr.next把 2 存下来。第二步curr.next prev1 的 next 指向 nil1 变成新链表的头部。第三步prev curr此时 prev 变为 1。第四步curr nextcurr 变为 2。每一轮循环做的事情就是重复这个四步操作。等到 curr 走到 nil 的时候prev 恰好停在原链表的最后一个节点 5同时 5 的 next 指向 4整个链表已经完成了翻转返回 prev 就是新链表的头节点。关键的思考点是next指针必须在curr.next prev之前保存这是迭代解法不变量中最重要的一条。很多人写错是因为把curr next写在了curr.next prev之后导致 next 丢失。2.2 迭代代码实现与关键注释用 Python 写出来大概是这样class ListNode: def __init__(self, val0, nextNone): self.val val self.next next class Solution: def reverseList(self, head: ListNode) - ListNode: prev None curr head while curr is not None: # 先保存下一个节点防止指向改写后丢失 next_node curr.next # 当前节点的指针指向前一个节点完成一次翻转 curr.next prev # 整体向后移动两个指针 prev curr curr next_node return prev再看一遍 Go 版本逻辑完全一样func reverseList(head *ListNode) *ListNode { var prev *ListNode curr : head for curr ! nil { next : curr.Next curr.Next prev prev curr curr next } return prev }注意 Go 里的var prev *ListNode默认是 nil不需要显式赋值。返回的 prev 指向原链表的尾节点也就是翻转后的头节点。2.3 边界条件空链表和单节点写任何链表题第一件事就是考虑边界。空链表head 为 nilwhile 循环根本进不去直接返回 prev也就是 nil完全正确。单节点1 - nil执行一轮循环后 curr.next nilprev 指向 1curr 为 nil返回 1也没问题。不要轻视这些边界场景。很多面试者写核心流程很流畅但被要求“跑一个空链表”时突然愣住这会给面试官留下基础不扎实的印象。我的习惯是在任何链表操作题里先把节点数量为 0、1、2 的情况在脑子里过一遍再写代码。2.4 空间复杂度和时间复杂度迭代解法的时间复杂度是 O(n)每个节点恰好被访问一次循环 n 次。空间复杂度是 O(1)只用了三个额外指针不随链表长度增加而增加。这是链表翻转里最优的空间表现。面试时如果面试官问“能不能优化”你要能答出 O(1) 空间已经是理论上的最优因为无论如何你都得遍历每一个节点才能改了它的 next 指向。3. 递归解法先想清楚函数定义3.1 递归思路的核心不是“怎么翻”而是“返回什么”递归解法是很多人卡壳的地方。他们习惯用迭代的方式去理解递归试图在每一层把节点一个个翻转结果越想越乱。其实递归解法最需要想清楚的只有一件事递归函数返回什么对于反转链表这个问题递归函数reverseList(head)接收一个链表头节点返回的是一个已经翻转过的新链表头部。比如说原链表是 1 - 2 - 3 - 4 - 5执行reverseList(head)后返回的应该是 5。有了这个函数定义假设我们已经成功调用了reverseList(head.next)对于完整的链表来说head.next传入进去之后返回的是以 5 为头的翻转好的链表。此时这个新链表的尾部恰好指向原来的head.next节点即 2。为什么因为整个以 2 为头的子链表翻转后原先的头变成尾部尾部变成 5而 2 作为原子链表头自然成了翻转后子链表的最后一个节点。接下来的关键操作就两行head.next.next head head.next None第一行让 2 的 next 指向 1这就把节点 1 接上了已经翻转好的新链表第二行把 1 的 next 清空防止形成环。3.2 递归代码的完整版本class Solution: def reverseList(self, head: ListNode) - ListNode: # 递归出口空链表或只有一个节点翻转后还是它自己 if head is None or head.next is None: return head # 翻转以 head.next 为头的链表返回新链表头 new_head self.reverseList(head.next) # 把当前节点接到子链表尾部 head.next.next head head.next None return new_head你用 Go 写也差不多func reverseList(head *ListNode) *ListNode { if head nil || head.Next nil { return head } newHead : reverseList(head.Next) head.Next.Next head head.Next nil return newHead }递归终止条件为什么是head nil || head.next nil因为head nil处理空链表head.next nil处理单节点链表。这两者都不需要翻转直接返回 head 就是正确结果。3.3 递归回溯过程的细节head.next.next 到底发生了什么我见过不少人能写出递归代码但被问到“回溯到第二层时指针是什么状态”时脑子就成了一团浆糊。这里专门解释一下。假设链表 1 - 2 - 3 - 4 - 5。递归一层层进去直到 head 5此时head.next nil返回 5。回到上一层head 4此时4.next还是 5执行4.next.next 4等价于5.next 4同时4.next nil。于是这一段链表变成了 4 - 5 的反转也就是 5 - 4。再回到上一层head 3此时 3.next 是 4执行3.next.next 3即4.next 3同时3.next nil。链表变成 5 - 4 - 3。循环往复最终得到 5 - 4 - 3 - 2 - 1新链表头是 new_head也就是最深层返回的 5所以每个递归层始终返回 new_head不会丢失新的头节点。有个细节值得注意在递归返回过程中每个节点的next其实经历了“先保持不变再被改写最后被清空”的过程。head.next nil这步很多人觉得多余但如果不做原链表的头节点可能会产生环路。比如原链表 1 - 2翻转后 2 - 1如果 1.next 不清空那 1.next 还是 22.next 又指向 1形成循环遍历时会死循环。3.4 迭代和递归怎么选迭代的优点是好理解、空间省递归的代码简洁但空间复杂度是 O(n)因为每层递归都有自己的方法调用栈。当链表很长时递归方法理论上可能触发栈溢出但 Leetcode 的测试数据一般不会给你挂掉的机会。面试时我建议优先写迭代因为空间复杂度更好向面试官解释也更容易保证不出边界问题。但递归解法一定要会讲因为面试官很可能追问“如果用递归怎么写”或者反问“递归的空间复杂度是多少”。这两者不是备选关系而是都需要掌握。4. 从每日一题到一类题三个高频变式的固定套路4.1 反转链表 II区间翻转的核心是“先走到位再局部解开”Leetcode 92 反转链表 II要求翻转从位置 left 到 right 的链表段。比如 1 - 2 - 3 - 4 - 5翻转第 2 到第 4 个节点得到 1 - 4 - 3 - 2 - 5。这个题的套路和 206 的区别在于翻转前需要先“走到”区间起点然后把区间内的节点当作一个独立链表做局部翻转翻转完再接回去。具体做法设置一个 dummy 头节点避免 left 1 时头节点变化的处理麻烦。用 cur 指针走到第 left - 1 个节点这个节点是区间前驱。记录 leftNode它是区间翻转前的头节点。从 leftNode 开始做 left 到 right 区间内的迭代翻转。翻转完成后把前驱节点指向区间的新头把区间的旧头指向 right 之后的后继节点。写出来大致是def reverseBetween(head: ListNode, left: int, right: int) - ListNode: dummy ListNode(0, head) prev dummy # 走到 left 的前一个节点 for _ in range(left - 1): prev prev.next # 记录区间头和区间尾巴开始局部翻转 reverse_start prev.next curr reverse_start before None for _ in range(right - left 1): next_node curr.next curr.next before before curr curr next_node # 接回原链表 prev.next before reverse_start.next curr return dummy.next这里的核心思路是把区间从原链表“摘”出来的动作其实根本没有物理摘除只是通过局部翻转后重新接线。注意reverse_start.next curr这步它把翻转后区间的尾部接到原链表右侧的剩余节点上少了这一步链表会断。区间翻转的时间复杂度也是 O(n)最坏情况下走了一遍链表空间 O(1)。4.2 K 个一组翻转链表分组处理时最容易漏掉“尾部的零头”Leetcode 25 K 个一组翻转链表是 206 的加强版。给定一个链表每 k 个节点一组进行翻转最后不足 k 个节点的部分保持原样。这道题面试频率很高因为它综合考察了链表遍历、分组、局部翻转和边界处理。固定写法可以拆成三个小函数getKthNode(head, k)从当前节点出发数出第 k 个节点如果不足 k 个就返回 nil。主循环里对每一组调用局部翻转函数reverseSegment然后拼接。主循环结束时处理尾巴。伪代码结构如下def reverseKGroup(head: ListNode, k: int) - ListNode: dummy ListNode(0, head) group_prev dummy while True: kth getKthNode(group_prev.next, k) if not kth: break group_next kth.next # 翻转 group_prev.next 到 kth 这一段 prev kth.next curr group_prev.next while curr ! group_next: next_node curr.next curr.next prev prev curr curr next_node # 把翻转好的组接到前驱上 group_prev.next kth group_prev group_prev.next # 此时 group_prev 是翻转后组的尾节点也就是原来的组头这个写法里有个很容易踩的坑翻转完一组后group_prev应该移动到本组翻转后的最后一个节点也就是下一组的前一个节点。很多初学者翻转完之后把group_prev挪错了位置导致下一组拼接错乱。另外最后一组不足 k 个节点时getKthNode直接返回 nil直接 break保留原始顺序。4.3 回文链表和重排链表翻转只是中间步骤234 回文链表的常规做法是先用快慢指针找到中点然后翻转后半段再两边同时遍历比较。这个题的难点不在于翻转本身而在于“找到中点后如何正确截断”和“比较完之后要不要恢复链表”。工程上的做法是比较时不恢复链表反正输出结果后链表不再使用但在面试中如果面试官特别要求“不能改变原链表结构”你需要先用快慢指针找中点再反转后半段比较完后把后半段再反转回去。这个时候你写的翻转代码就是现成的工具函数。143 重排链表也是一样找中点、翻转后半段、然后交错合并。两道题全都离不开放一个reverseList子函数。这就是为什么说 206 是基础中的基础掌握了 206等于拿到了一把钥匙能解锁后面一大串题。5. 刷题与面试中的实操经验测试用例、讲解技巧与复盘方法5.1 自测用例应该怎么设计写完链表翻转的代码光靠 Leetcode 自带的用例直接提交这习惯不够好。我给你一份个人常用的链表自测清单适用于 206 以及所有链表类题目空链表head nil执行完不报错。单节点链表1 - nil翻转后还是 1。双节点链表1 - 2翻转后 2 - 1最容易暴露基础指针问题。奇数长度链表1 - 2 - 3 - 4 - 5检查中间节点对称性。偶数长度链表1 - 2 - 3 - 4检查翻转后中点附近指针不会成环。全相同值的链表1 - 1 - 1确保不会因为值相同而混淆节点身份。建议在本地 IDE 里写一个辅助函数把链表打印成数组形式方便自己对拍验证。Leetcode 的用例通过不代表你理解了本地自己画图推导、再运行验证才是真正掌握。5.2 面试现场如何把这道题讲出亮点面试官让你手写反转链表不是真的想看你五分钟默写代码而是想听你“怎么想”。我建议按下面这个顺序讲先说思路遍历链表把每个节点的 next 指向前一个节点需要暂存原 next 防止丢失最终返回新的头节点。再提边界空链表和单节点直接返回这是天然递归出口。然后写代码同时保持边写边说让面试官看到你不是在背代码。写完代码主动提一句迭代解法的空间复杂度是 O(1)递归解法的空间复杂度是 O(n)因为递归栈。如果需要优化空间我会选迭代。这么做的好处是面试官不需要猜你的思路你自己把每一个关键决策都讲清楚了显得逻辑清晰。哪怕代码出点小问题面试官也更愿意给提示。5.3 我踩过的坑和复盘方法第一个坑是“保存 next 的时机”。我早期写代码经常把 next_node 的赋值放到curr.next prev之后跑起来才发现链断了。后来养成了一个肌肉记忆任何修改 next 指针之前先保证需要保留的后继已经存在变量里。第二个坑是递归解法里忘了清空head.next。Leetcode 的测试用例通常会验证是否有环但如果你在本地上跑一个长链表忘记清空会导致死循环或者内存暴涨。哪怕递归代码看着再优雅head.next None这行都不能省。第三个坑是自认为“简单题不用练习”。实际上 Leetcode 周赛里很多 hard 题写起来到最后被卡住的原因就是基础题不熟练。有一阵子我沉迷刷 hard 题结果某次面试官先让我写反转链表我愣是犹豫了一下这让我非常懊恼。从那以后我把每日一题和经典基础题放在防火梯里过段时间就默写一遍。分享一个我常用的复盘方法每次刷完一道题我会在自己笔记里写三行总结——今天的题考察什么基础能力我卡在哪里下一步需要强化哪类题链表翻转这题我写过好几轮总结每次都会发现新的理解盲区。这也是我建议大家不要过早跳过简单题的原因简单题里的基础思维值得反复咀嚼。最后再多说一句。很多人刷 Leetcode 的时候容易陷入“量”的焦虑觉得今天没刷三道新题就浪费了。但实际上像链表翻转这种基础题反复玩透、能随时在白纸上画出指针流动过程比盲目刷二十道新题更管用。它就像练武功时的马步每天多站一会儿后面学什么招式都稳。