算法题:反转链表

反转链表详解

什么是链表?

链表是一种线性数据结构,其中的元素不是在内存中连续存储的。每个元素(节点)包含两部分:

  1. 数据域:存储实际的数据
  2. 指针域:存储指向下一个节点的引用
// 链表节点定义
class ListNode {
  constructor(val, next) {
    this.val = val === undefined ? 0 : val;
    this.next = next === undefined ? null : next;
  }
}

为什么需要反转链表?

链表反转是面试中的高频题目,也是实际开发中常见的操作,比如:

  • 实现撤销功能
  • 数据处理中的顺序调整
  • 算法优化中的预处理步骤

方法一:迭代反转(推荐)

核心思想

使用三个指针逐个反转节点的指向关系:

  • prev:指向前一个节点
  • current:指向当前节点
  • next:临时保存下一个节点

详细步骤

  1. 初始化指针
let prev = null;     // 前一个节点,初始为null
let current = head;  // 当前节点,从头节点开始
  1. 遍历链表并反转
while (current) {
  // 步骤1:保存下一个节点
  const next = current.next;
  
  // 步骤2:反转当前节点指针
  current.next = prev;
  
  // 步骤3:移动指针
  prev = current;
  current = next;
}
  1. 返回新头节点
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) - 只使用了常数级别的额外空间

方法二:递归反转

核心思想

将问题分解为更小的子问题:

  1. 递归反转除头节点外的剩余链表
  2. 将头节点连接到反转后的链表末尾

详细步骤

  1. 基础情况处理
// 空链表或只有一个节点,直接返回
if (!head || !head.next) {
  return head;
}
  1. 递归处理剩余部分
// 递归反转head.next开始的链表
const newHead = reverseListRecursive(head.next);
  1. 反转当前连接
// 将当前节点连接到反转后的链表末尾
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();

总结

  1. 迭代方法是实际开发中的首选,空间效率高且易于理解
  2. 递归方法体现了分治思想,代码简洁但空间开销大
  3. 掌握这两种方法有助于理解链表的操作原理
  4. 在面试中能清晰解释两种方法的优缺点会加分不少

链表反转是数据结构与算法学习中的重要基础,熟练掌握后对解决更复杂的链表问题有很大帮助。建议多加练习,理解每种方法的核心思想!