🔄 深入理解链表反转:从基础到进阶的完整指南
📝 前言
链表反转是程序员面试中的高频考题,也是理解链表操作的基础。虽然题目标注为"简单",但要真正理解其原理、写出 bug-free 的代码,并能灵活运用到实际场景中,还是需要下一番功夫的。
本文将从多个维度深入剖析链表反转问题,包括迭代法和递归法的实现、复杂度分析、实际应用场景,以及迭代法在其他数据结构中的应用。
🎯 问题描述
LeetCode 206. 反转链表
给你单链表的头节点
head
,请你反转链表,并返回反转后的链表。
示例:
ini
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]
🏗️ 数据结构定义
首先,我们需要定义链表节点的数据结构:
typescript
class ListNode {
val: number
next: ListNode | null
constructor(val?: number, next?: ListNode | null) {
this.val = (val === undefined ? 0 : val)
this.next = (next === undefined ? null : next)
}
}
💡 解法一:迭代法(重点)
核心思想
迭代法的核心是逐个改变节点的指向,使用三个指针完成反转:
prev
:指向当前节点的前一个节点curr
:当前正在处理的节点next
:临时保存下一个节点
图解过程
rust
初始状态:
prev = null
curr = head
↓
1 -> 2 -> 3 -> 4 -> 5 -> null
第一轮循环后:
null <- 1 2 -> 3 -> 4 -> 5 -> null
↑ ↑
prev curr
最终状态:
null <- 1 <- 2 <- 3 <- 4 <- 5
↑
prev (新头节点)
代码实现
typescript
function reverseListIterative(head: ListNode | null): ListNode | null {
// 边界情况处理
if (!head || !head.next) {
return head;
}
let prev: ListNode | null = null;
let curr: ListNode | null = head;
while (curr !== null) {
// 保存下一个节点
const next = curr.next;
// 反转当前节点的指向
curr.next = prev;
// 移动指针
prev = curr;
curr = next;
}
return prev;
}
🔍 详细步骤解析
让我们用 [1,2,3]
作为例子,详细追踪每一步:
typescript
// 初始: prev=null, curr=1->2->3->null
// 第1轮:
// 1. next = 2->3->null
// 2. 1.next = null
// 3. prev = 1->null
// 4. curr = 2->3->null
// 第2轮:
// 1. next = 3->null
// 2. 2.next = 1->null
// 3. prev = 2->1->null
// 4. curr = 3->null
// 第3轮:
// 1. next = null
// 2. 3.next = 2->1->null
// 3. prev = 3->2->1->null
// 4. curr = null
// 返回: prev = 3->2->1->null
⚠️ 常见错误
typescript
// ❌ 错误1:忘记保存 next
while (curr !== null) {
curr.next = prev; // 这里改变了 curr.next
prev = curr;
curr = curr.next; // curr.next 已经是 prev 了!
}
// ❌ 错误2:返回错误的节点
return head; // 应该返回 prev
💡 解法二:递归法
核心思想
递归法利用函数调用栈,从链表尾部开始反转,逐层返回新的头节点。
代码实现
typescript
function reverseListRecursive(head: ListNode | null): ListNode | null {
// 递归终止条件
if (head === null || head.next === null) {
return head;
}
// 递归反转子链表
const newHead = reverseListRecursive(head.next);
// 反转当前节点
head.next.next = head;
head.next = null;
return newHead;
}
递归过程分析
vbscript
reverseList(1->2->3->null)
├─ reverseList(2->3->null)
│ ├─ reverseList(3->null)
│ │ └─ 返回 3 (base case)
│ ├─ 2.next.next = 2 (即 3.next = 2)
│ ├─ 2.next = null
│ └─ 返回 3
├─ 1.next.next = 1 (即 2.next = 1)
├─ 1.next = null
└─ 返回 3
📊 复杂度分析
方法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
---|---|---|---|---|
迭代法 | O(n) | O(1) | 空间效率高,不会栈溢出 | 代码相对复杂 |
递归法 | O(n) | O(n) | 代码简洁优雅 | 可能栈溢出,空间开销大 |
🧪 测试用例
typescript
// 辅助函数
function createLinkedList(arr: number[]): ListNode | null {
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 linkedListToArray(head: ListNode | null): number[] {
const result: number[] = [];
let current = head;
while (current) {
result.push(current.val);
current = current.next;
}
return result;
}
// 测试
describe('reverseList', () => {
test('常规链表', () => {
const head = createLinkedList([1, 2, 3, 4, 5]);
const reversed = reverseListIterative(head);
expect(linkedListToArray(reversed)).toEqual([5, 4, 3, 2, 1]);
});
test('两个节点', () => {
const head = createLinkedList([1, 2]);
const reversed = reverseListIterative(head);
expect(linkedListToArray(reversed)).toEqual([2, 1]);
});
test('空链表', () => {
expect(reverseListIterative(null)).toBeNull();
});
test('单节点', () => {
const head = createLinkedList([1]);
const reversed = reverseListIterative(head);
expect(linkedListToArray(reversed)).toEqual([1]);
});
});
🚀 进阶应用
1. 反转链表的一部分
typescript
function reverseBetween(head: ListNode | null, left: number, right: number): ListNode | null {
if (!head || left === right) return head;
const dummy = new ListNode(0);
dummy.next = head;
let prev = dummy;
// 找到 left 的前一个节点
for (let i = 0; i < left - 1; i++) {
prev = prev.next!;
}
// 反转 [left, right] 区间
let curr = prev.next;
for (let i = 0; i < right - left; i++) {
const next = curr!.next;
curr!.next = next!.next;
next!.next = prev.next;
prev.next = next;
}
return dummy.next;
}
2. K 个一组反转链表
typescript
function reverseKGroup(head: ListNode | null, k: number): ListNode | null {
if (!head || k === 1) return head;
// 计算链表长度
let length = 0;
let curr = head;
while (curr) {
length++;
curr = curr.next;
}
const dummy = new ListNode(0);
dummy.next = head;
let prev = dummy;
while (length >= k) {
curr = prev.next;
let next = curr!.next;
// 反转 k 个节点
for (let i = 1; i < k; i++) {
curr!.next = next!.next;
next!.next = prev.next;
prev.next = next;
next = curr!.next;
}
prev = curr!;
length -= k;
}
return dummy.next;
}
💼 实际应用场景
1. 撤销/重做功能
typescript
class UndoRedoManager<T> {
private history: ListNode | null = null;
private current: ListNode | null = null;
push(action: T): void {
const newNode = new ListNode(action as any);
if (!this.history) {
this.history = newNode;
} else {
newNode.next = this.history;
this.history = newNode;
}
this.current = this.history;
}
undo(): T | null {
if (!this.current) return null;
const action = this.current.val;
this.current = this.current.next;
return action;
}
// 反转历史记录
reverseHistory(): void {
this.history = reverseListIterative(this.history);
this.current = this.history;
}
}
2. 数据流处理
typescript
class DataPipeline<T> {
private processors: ListNode | null = null;
addProcessor(fn: (data: T) => T): void {
const node = new ListNode(fn as any);
node.next = this.processors;
this.processors = node;
}
// 反转处理顺序
reverseProcessingOrder(): void {
this.processors = reverseListIterative(this.processors);
}
process(data: T): T {
let current = this.processors;
let result = data;
while (current) {
result = current.val(result);
current = current.next;
}
return result;
}
}
🔄 迭代法的广泛应用
迭代法不仅适用于链表,在其他数据结构中也有广泛应用:
1. 树的迭代遍历
typescript
// 二叉树的中序遍历(迭代法)
function inorderTraversal(root: TreeNode | null): number[] {
const result: number[] = [];
const stack: TreeNode[] = [];
let current = root;
while (current || stack.length > 0) {
while (current) {
stack.push(current);
current = current.left;
}
current = stack.pop()!;
result.push(current.val);
current = current.right;
}
return result;
}
2. 图的 BFS
typescript
function bfs(graph: Map<number, number[]>, start: number): number[] {
const visited = new Set<number>();
const queue: number[] = [start];
const result: number[] = [];
visited.add(start);
while (queue.length > 0) {
const node = queue.shift()!;
result.push(node);
for (const neighbor of graph.get(node) || []) {
if (!visited.has(neighbor)) {
visited.add(neighbor);
queue.push(neighbor);
}
}
}
return result;
}
3. 动态规划
typescript
// 背包问题(迭代法)
function knapsack(weights: number[], values: number[], capacity: number): number {
const n = weights.length;
const dp: number[] = new Array(capacity + 1).fill(0);
for (let i = 0; i < n; i++) {
for (let w = capacity; w >= weights[i]; w--) {
dp[w] = Math.max(dp[w], dp[w - weights[i]] + values[i]);
}
}
return dp[capacity];
}
📚 学习建议
- 理解本质:迭代法的本质是通过循环和状态变量的更新来解决问题
- 画图辅助:遇到链表问题时,画图能帮助理解指针的移动
- 注意边界:空链表、单节点等边界情况要特别处理
- 多写多练:链表操作需要大量练习才能熟练掌握
🎯 总结
链表反转虽然是一道"简单"题,但它包含了链表操作的核心思想:
- 指针操作:理解指针的移动和修改顺序
- 边界处理:正确处理各种边界情况
- 空间优化:迭代法实现 O(1) 空间复杂度
- 递归思想:理解递归的执行过程和回溯
掌握链表反转不仅能帮助我们解决面试题,更重要的是理解其背后的思想,并能灵活运用到实际开发中。
🔗 相关题目推荐
📖 关于作者:前端算法工程师,专注于数据结构与算法的研究与分享。
💬 交流讨论:欢迎在评论区分享你的想法和疑问!
👍 如果本文对你有帮助,请点赞收藏,你的支持是我创作的动力!
#算法 #链表 #数据结构 #前端 #LeetCode