深入理解链表反转:从基础到进阶的完整指南

🔄 深入理解链表反转:从基础到进阶的完整指南

📝 前言

链表反转是程序员面试中的高频考题,也是理解链表操作的基础。虽然题目标注为"简单",但要真正理解其原理、写出 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];
}

📚 学习建议

  1. 理解本质:迭代法的本质是通过循环和状态变量的更新来解决问题
  2. 画图辅助:遇到链表问题时,画图能帮助理解指针的移动
  3. 注意边界:空链表、单节点等边界情况要特别处理
  4. 多写多练:链表操作需要大量练习才能熟练掌握

🎯 总结

链表反转虽然是一道"简单"题,但它包含了链表操作的核心思想:

  1. 指针操作:理解指针的移动和修改顺序
  2. 边界处理:正确处理各种边界情况
  3. 空间优化:迭代法实现 O(1) 空间复杂度
  4. 递归思想:理解递归的执行过程和回溯

掌握链表反转不仅能帮助我们解决面试题,更重要的是理解其背后的思想,并能灵活运用到实际开发中。

🔗 相关题目推荐


📖 关于作者:前端算法工程师,专注于数据结构与算法的研究与分享。

💬 交流讨论:欢迎在评论区分享你的想法和疑问!

👍 如果本文对你有帮助,请点赞收藏,你的支持是我创作的动力!

#算法 #链表 #数据结构 #前端 #LeetCode

相关推荐
ohMyGod_12339 分钟前
React16,17,18,19新特性更新对比
前端·javascript·react.js
@大迁世界41 分钟前
第1章 React组件开发基础
前端·javascript·react.js·前端框架·ecmascript
Hilaku1 小时前
从一个实战项目,看懂 `new DataTransfer()` 的三大妙用
前端·javascript·jquery
爱分享的程序员1 小时前
前端面试专栏-算法篇:20. 贪心算法与动态规划入门
前端·javascript·node.js
我想说一句1 小时前
事件委托与合成事件:前端性能优化的"偷懒"艺术
前端·javascript
爱泡脚的鸡腿1 小时前
Web第二次笔记
前端·javascript
Dream耀1 小时前
React合成事件揭秘:高效事件处理的幕后机制
前端·javascript
洋流1 小时前
0基础进大厂,第12天:ES6语法基础篇
javascript
I'm写代码1 小时前
el-tree树形结构笔记
javascript·vue.js·笔记
然我1 小时前
面试必问:JS 事件机制从绑定到委托,一篇吃透所有考点
前端·javascript·面试