LeetCode 206题“反转链表”,我怀疑每个认真刷题的人都至少写过三遍。我自己第一次做的时候,靠的是背下来的三行迭代代码,能AC,但心虚。真正让我想通的一刻,是某次把链表画成一张一张的箭头图,然后发现整道题做的无非是两件事:先把下一个节点记住,再把当前节点的箭头回指。最近重刷到这一题,我特意没有直接写代码,而是把迭代、递归、头插几种思路都重新推了一遍,发现经典的确实值得反复看。
这篇文章不打算只给一份能通过的代码,而是想把反转链表背后那些“为什么要这样”,以及重刷时容易被忽略的细节、边界和扩展题,全部摊开讲清楚。无论你是第一次接触链表,还是准备面试前想彻底吃透它,这篇都可以直接当复习资料用。
1. 这题值得重刷的核心原因:反转是所有链表操作的“原子能力”
1.1 一个看似简单的问题,为什么常被拿来当考点
反转链表的要求一句话就能说清:给定单链表的头节点head,反转链表,返回反转后的头节点。比如输入 1→2→3→4→5,输出 5→4→3→2→1。看起来是不是很简单?但越是这种“所有讲解都能看懂”的题,越容易在实际写代码时翻车。原因在于链表这种数据结构的特殊性:它不像数组那样支持随机访问,也不像双向链表那样天然有前驱指针。单链表里每个节点只存了指向下一个节点的引用,你一旦把箭头掉转,后面的节点就可能“失联”。所以整道题的核心就变成一个工程问题:如何在切断联系之前,先把路记下来。
也正是这个特性,让反转链表成为无数链表题的地基。后面遇到的“反转局部链表”“K个一组翻转链表”“回文链表判断”,本质上都是在反复调用这段“局部反转”的能力。刷题圈常说的“链表题无非是穿针引线”,穿针引线的起手式,基本就是206这一题教会你的。
1.2 现在的大厂面试里,它还够用吗
很多准备面试的朋友会问:都2025年了,还考这种题是不是太基础了?我的看法是,它往往不是作为一道独立难题出现,而是作为“前置步骤”被嵌在更大体的解法里。面试官可能不会直接让你写“反转整条链表”,但可能会让你在O(1)空间内反转链表的某一段,或者让你判断一个链表是不是回文结构。这时候如果你对反转过程的理解还停留在“背模板”层面,大概率会在指针移动的某一步突然卡住,然后越改越乱。
这题“重刷”的价值就在这里:第一次刷,你记住了怎么解;第二次刷,你该记住为什么这么解。比如为什么迭代里要先保存next节点,为什么递归里返回前要把head.next置空,为什么循环结束条件是cur为空而不是cur.next为空。这些问题想通了,你才能在考场上把它当作“搭积木”的零件,而不是一个孤立题目。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 三种解法逐一拆解:迭代、递归与头插
2.1 迭代法:三指针让每个节点原地掉头
迭代法的思路可以理解成“排队改方向”。想象一队人面朝同一个方向站着,每个人都只能看见自己前面那个人。现在要让整队人转过身来,变成每个人都看向自己原来身后的人。操作方式很简单:从队伍最前面的人开始,依次让每个人把手指向自己身后的人。这就要求你在动手前,先看一眼自己身后原本是谁,否则一转身就把人跟丢了。
对应到代码里就是三指针:
- prev:当前节点的前一个节点,初始为 null
- cur:当前要处理的节点,初始为 head
- nextTemp:cur 原本的下一个节点,用来防止断链
每次循环做三步:
javascript复制const reverseList = function(head) {
let prev = null;
let cur = head;
while (cur !== null) {
const nextTemp = cur.next; // 1. 先保存下一个节点
cur.next = prev; // 2. 当前节点指向前驱
prev = cur; // 3. prev 前进一位
cur = nextTemp; // 4. cur 前进一位
}
return prev;
};
这里最关键的是第一步:一定要先用nextTemp把cur.next存起来。因为第二步执行完后,cur原来的后继就断了,如果没有提前保存,循环就没法继续。很多人第一次写会漏掉这个临时变量,结果链表反转了一部分就变成环,或者直接丢了一串节点。
整个循环结束的条件是cur === null。为什么要等到cur为空?因为当cur走到原链表最后一个节点的下一个位置时,说明每一个节点都已经处理完了。此时prev正好停在原链表的尾节点上,也就是反转后新链表的头节点。如果你把条件写成cur.next !== null,最后那个节点的箭头还没来得及回指,循环就已经退出,结果会差一个节点。
从空间复杂度看,迭代法只用到了有限的几个指针,空间是O(1),这也是面试中最被推崇的写法。对于“反转
