每日一题:LeetCode 206:反转链表——三指针与原地反转详解

LeetCode 206「反转链表」是一道经典的链表算法题,主要考察链表结构、指针操作、迭代与递归,以及原地修改引用。

这道题的核心并不是交换节点的值,而是改变每个节点的 next****指向,让原本从前往后的链表变成从后往前。

题目描述

给定一个单链表的头节点 head,将链表反转,并返回反转后的新头节点。

例如:

复制代码
原链表:

1 → 2 → 3 → 4 → 5 → null

反转后:

5 → 4 → 3 → 2 → 1 → null

需要注意,反转的是链表节点之间的连接方向,而不是简单地把数组倒过来。

什么是链表?

理解反转链表之前,需要先知道链表的基本结构。

数组通过下标访问元素,而链表由多个节点连接而成。每个节点通常包含两部分:

  • val:当前节点保存的数据。

  • next:指向下一个节点的引用。

例如:

javascript 复制代码
const node1 = {
  val: 1,
  next: null
}

const node2 = {
  val: 2,
  next: null
}

node1.next = node2

此时形成:

复制代码
node1                  node2
┌─────┬──────┐          ┌─────┬──────┐
│  1  │ next │ ───────→ │  2  │ null │
└─────┴──────┘          └─────┴──────┘

所以:

javascript 复制代码
node1.val
// 1

node1.next
// node2 对象

node1.next.val
// 2

也就是说,next 保存的不是下一个节点的数值,而是下一个节点对象的引用。

因此:

复制代码
node1.next = null

就表示让 node1 不再指向 node2。

反转链表,正是通过不断修改这样的 next 引用完成的。

为什么使用三个指针?

假设原链表是:

复制代码
1 → 2 → 3 → null

我们希望变成:

复制代码
3 → 2 → 1 → null

最直接的思路就是遍历链表,将当前节点的 next 指向前一个节点。

但是这里存在一个问题。

原本:

复制代码
1 → 2 → 3

如果直接执行:

复制代码
curr.next = prev

当前节点的指向会发生改变,可能导致原来后面的节点无法继续访问。

因此,在修改指向之前,必须先保存下一个节点。

这就是为什么需要三个变量:

变量 作用
prev 保存已经反转部分的头节点
curr 当前正在处理的节点
next 临时保存原链表的下一个节点

三个变量互相配合,就可以在不丢失后续节点的情况下完成反转。

JavaScript 迭代实现

javascript 复制代码
var reverseList = function(head) {
  let prev = null
  let curr = head

  while (curr) {
    const next = curr.next

    curr.next = prev

    prev = curr

    curr = next
  }

  return prev
}

这段代码虽然很短,但最重要的是理解其中四条语句的执行顺序。

代码执行过程

初始状态:

复制代码
let prev = null
let curr = head

假设:

复制代码
null       1 → 2 → 3 → null
 ↑         ↑
prev      curr

prev 初始为 null,因为第一个节点反转后,会成为链表尾节点,它的 next 应该指向 null。

curr 指向头节点,表示从第一个节点开始处理。

循环条件:

复制代码
while (curr)

意思是:

只要当前节点不为 null,就继续反转。

接下来进入最核心的四条语句。

保存下一个节点

复制代码
const next = curr.next

当前:

复制代码
curr → 1

next → 2

先保存节点 2,防止后续修改 curr.next 时失去对剩余链表的访问。

反转当前节点的指向

复制代码
curr.next = prev

因为最开始:

复制代码
prev = null

所以相当于:

复制代码
node1.next = null

此时:

复制代码
1 → null

2 → 3 → null

节点 1 已经完成反转,而后续链表仍然可以通过 next 找到。

更新 prev

复制代码
prev = curr

让 prev 指向刚刚完成反转的节点 1。

复制代码
prev
 ↓
 1 → null

注意:这里不是复制一个新节点,而是让变量 prev 保存 curr 当前引用的节点。

更新 curr

复制代码
curr = next

让 curr 移动到原来的下一个节点:

复制代码
prev          curr
 ↓             ↓
 1 → null      2 → 3 → null

第一轮完成。

接下来继续处理节点 2。

复制代码
const next = curr.next // 保存节点 3
curr.next = prev       // 2 → 1
prev = curr            // prev 指向 2
curr = next            // curr 指向 3

此时:

复制代码
已反转部分:

2 → 1 → null
↑
prev

待处理部分:

3 → null
↑
curr

再执行一轮:

复制代码
3 → 2 → 1 → null
↑
prev

curr = null

因为:

复制代码
curr === null

循环结束。

最终执行:

复制代码
return prev

此时 prev 指向节点 3,它已经成为反转后链表的新头节点。

为什么最后返回 prev,而不是 head?

原链表:

复制代码
head
 ↓
 1 → 2 → 3 → null

反转以后:

复制代码
3 → 2 → 1 → null
        ↑
       head

原来的 head 仍然指向节点 1,只是节点 1 已经变成尾节点。

而 prev 最终指向节点 3,也就是新链表的头节点。

因此必须:

复制代码
return prev

而不是:

复制代码
return head

递归方式实现

除了迭代,也可以通过递归反转链表。

javascript 复制代码
var reverseList = function(head) {
  if (head === null || head.next === null) {
    return head
  }

  const newHead = reverseList(head.next)

  head.next.next = head
  head.next = null

  return newHead
}

递归的思路是:先反转后面的链表,再把当前节点接到末尾。

例如:

复制代码
1 → 2 → 3 → null

递归一直向后,直到节点 3。

随后在返回过程中逐层反转:

复制代码
3 → 2 → null

最后变成:

复制代码
3 → 2 → 1 → null

其中:

复制代码
head.next.next = head

用于让下一个节点反过来指向当前节点。

而:

复制代码
head.next = null

用于断开当前节点原来的向后连接,避免形成环。

递归写法更加简洁,但需要额外的函数调用栈空间。

时间复杂度与空间复杂度

实现方式 时间复杂度 额外空间复杂度
迭代(三指针) O(n) O(1)
递归 O(n) O(n)

迭代方式 只遍历链表一次,并且只使用 prev、curr、next 几个变量,因此时间复杂度为 O(n),额外空间复杂度为 O(1)。

递归方式 同样需要处理每个节点一次,因此时间复杂度为 O(n),但最坏情况下递归调用栈需要 O(n) 的额外空间。

面试中通常更推荐掌握迭代写法,因为它不需要额外递归栈,空间效率更高。

总结

反转链表的本质就是改变节点之间的 next****引用方向。

最核心的代码:

javascript 复制代码
var reverseList = function(head) {
  let prev = null
  let curr = head

  while (curr) {
    const next = curr.next
    curr.next = prev
    prev = curr
    curr = next
  }

  return prev
}

整个过程可以总结为:

复制代码
保存下一个节点
      ↓
反转当前节点指向
      ↓
更新 prev
      ↓
curr 移动到下一个节点
      ↓
继续循环
      ↓
返回 prev

记住这四句:

复制代码
const next = curr.next // 保存后续节点

curr.next = prev       // 改变指向

prev = curr            // 更新已反转部分

curr = next            // 继续处理下一个节点

一句话总结:先保存后续节点,再反转当前指向,最后移动两个指针,直到整个链表完成反转。

掌握这道题以后,再学习「反转链表 II」「K 个一组翻转链表」等进阶题目,就会更容易理解。

相关推荐
岁月蹉跎的一杯酒1 小时前
CCM-可调颜色校正与噪声权衡
图像处理·算法·isp
在所不辞兄2 小时前
选择隐式方法求解刚性ODE
算法
码爸2 小时前
贪心算法介绍与示例
算法·贪心算法
-dzk-2 小时前
【多维动态规划】LC 64.最小路径和
算法·动态规划
垆边人似月.3 小时前
华为机试题 :字符串压缩
算法·华为
ggb喔3 小时前
52pojie 的图片工具:为什么无损转换后体积涨了 6 倍、照片方向还是歪的
图像处理·python·算法·计算机视觉·图形渲染·媒体
菜鸟~noob2333 小时前
【电子战】第25篇:自由空间、双射线与菲涅尔区【含matlab代码】
开发语言·算法·matlab
地平线开发者4 小时前
地平线征程6M YOLOv8s 部署
算法·自动驾驶
Run_Teenage4 小时前
算法:Manacher(马拉车)算法、中心扩展算法
算法