反转链表详解
什么是链表?
链表是一种线性数据结构,其中的元素不是在内存中连续存储的。每个元素(节点)包含两部分:
- 数据域:存储实际的数据
- 指针域:存储指向下一个节点的引用
// 链表节点定义
class ListNode {
constructor(val, next) {
this.val = val === undefined ? 0 : val;
this.next = next === undefined ? null : next;
}
}
为什么需要反转链表?
链表反转是面试中的高频题目,也是实际开发中常见的操作,比如:
- 实现撤销功能
- 数据处理中的顺序调整
- 算法优化中的预处理步骤
方法一:迭代反转(推荐)
核心思想
使用三个指针逐个反转节点的指向关系:
prev:指向前一个节点current:指向当前节点next:临时保存下一个节点
详细步骤
- 初始化指针
let prev = null; // 前一个节点,初始为null
let current = head; // 当前节点,从头节点开始
- 遍历链表并反转
while (current) {
// 步骤1:保存下一个节点
const next = current.next;
// 步骤2:反转当前节点指针
current.next = prev;
// 步骤3:移动指针
prev = current;
current = next;
}
- 返回新头节点
return prev; // prev现在指向原链表的最后一个节点
图解演示
graph LR
subgraph 原始链表
A[1] --> B[2]
B --> C[3]
C --> D[4]
D --> E[null]
end
subgraph 步骤1
F[prev: null] --> G[curr: 1]
G --> H[next: 2]
H --> I[2]
I --> J[3]
J --> K[4]
K --> L[null]
end
subgraph 步骤2
M[prev: 1] --> N[curr: 2]
O[1] --> P[null]
N --> Q[next: 3]
Q --> R[3]
R --> S[4]
S --> T[null]
end
subgraph 步骤3
U[prev: 2] --> V[curr: 3]
W[2] --> X[1]
Y[1] --> Z[null]
V --> AA[next: 4]
AA --> AB[4]
AB --> AC[null]
end
subgraph 最终结果
AD[prev: 3] --> AE[curr: 4]
AF[3] --> AG[2]
AH[2] --> AI[1]
AJ[1] --> AK[null]
AE --> AL[next: null]
end
原始链表 --> 步骤1
步骤1 --> 步骤2
步骤2 --> 步骤3
步骤3 --> 最终结果
完整代码实现
function reverseListIterative(head) {
let prev = null;
let current = head;
while (current) {
const next = current.next; // 保存下一个节点
current.next = prev; // 反转当前节点指针
prev = current; // 移动prev指针
current = next; // 移动current指针
}
return prev; // 返回新头节点
}
复杂度分析
- 时间复杂度:O(n) - 需要遍历链表一次
- 空间复杂度:O(1) - 只使用了常数级别的额外空间
方法二:递归反转
核心思想
将问题分解为更小的子问题:
- 递归反转除头节点外的剩余链表
- 将头节点连接到反转后的链表末尾
详细步骤
- 基础情况处理
// 空链表或只有一个节点,直接返回
if (!head || !head.next) {
return head;
}
- 递归处理剩余部分
// 递归反转head.next开始的链表
const newHead = reverseListRecursive(head.next);
- 反转当前连接
// 将当前节点连接到反转后的链表末尾
head.next.next = head;
head.next = null; // 断开原连接
图解演示
原链表: 1 -> 2 -> 3 -> 4 -> 5 -> null
递归过程:
reverseList(1) -> reverseList(2) -> reverseList(3) -> reverseList(4) -> reverseList(5)
↑
5是最后一个节点,直接返回
回溯过程:
5 -> null
4.next.next = 4 => 5 -> 4 -> null
4.next = null
5 -> 4 -> 3 -> null
...继续直到:
5 -> 4 -> 3 -> 2 -> 1 -> null
完整代码实现
function reverseListRecursive(head) {
// 基础情况:空链表或只有一个节点
if (!head || !head.next) {
return head;
}
// 递归反转剩余部分
const newHead = reverseListRecursive(head.next);
// 反转当前连接
head.next.next = head;
head.next = null;
return newHead;
}
复杂度分析
- 时间复杂度:O(n) - 每个节点访问一次
- 空间复杂度:O(n) - 递归调用栈的深度
两种方法对比
| 特性 | 迭代方法 | 递归方法 |
|---|---|---|
| 时间复杂度 | O(n) | O(n) |
| 空间复杂度 | O(1) | O(n) |
| 易理解性 | 中等 | 较难 |
| 内存效率 | 高 | 低 |
| 推荐指数 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
测试代码
// 创建链表工具函数
function createLinkedList(arr) {
if (arr.length === 0) return null;
const head = new ListNode(arr[0]);
let current = head;
for (let i = 1; i < arr.length; i++) {
current.next = new ListNode(arr[i]);
current = current.next;
}
return head;
}
// 打印链表工具函数
function printLinkedList(head) {
const result = [];
let current = head;
while (current) {
result.push(current.val);
current = current.next;
}
return result;
}
// 测试
function testReverseList() {
const arr = [1, 2, 3, 4, 5];
const head = createLinkedList(arr);
console.log('原链表:', printLinkedList(head));
const reversedHead = reverseListIterative(head);
console.log('反转后:', printLinkedList(reversedHead));
}
testReverseList();
总结
- 迭代方法是实际开发中的首选,空间效率高且易于理解
- 递归方法体现了分治思想,代码简洁但空间开销大
- 掌握这两种方法有助于理解链表的操作原理
- 在面试中能清晰解释两种方法的优缺点会加分不少
链表反转是数据结构与算法学习中的重要基础,熟练掌握后对解决更复杂的链表问题有很大帮助。建议多加练习,理解每种方法的核心思想!