题目描述

✅ 206. 反转链表

image-20260928181518689

image-20260928181518690

题意分析

反转的是节点之间的连接方向:每个节点改为指向它原来的前一个节点,原尾节点成为新头节点,原头节点成为新尾节点。节点的值不变,也不需要重新创建节点。

单链表只有指向后继的 next,没有指向前驱的指针。因此,既要记住当前节点应该接到哪里,又要在修改 next 前保存原来的后继,否则会失去剩余链表的入口。反转完成后,新尾节点的 next 必须为空。

空链表返回空,只有一个节点时返回该节点。题目进阶要求分别用迭代和递归实现:迭代逐个修改指针,递归先处理后面的链表,再把当前节点接到末尾。

解法:迭代反转指针

核心思路

[!blue]

从头到尾遍历,把链表分成“已经反转”和“还未处理”两部分。prev 指向已反转部分的头节点,curr 指向未处理部分的第一个节点。开始时没有节点被处理,所以 prev = null,curr = head。

每轮把 curr 从未处理部分移到已反转部分的最前面。先用 next 保存 curr.next,再令 curr.next = prev,当前节点就接到了已反转链表前面;随后令 prev = curr、curr = next,两个指针重新指向各自部分的头节点。

这样每轮都会反转一个节点的连接,并且通过保存的 next 保留后续入口。第一轮把原头节点指向空,使它成为新尾节点;当 curr 为空时,所有节点都进入了已反转部分,prev 就是整条链表的新头节点。

解题步骤

  1. 初始化 prev = null、curr = head。
  2. 只要 curr 不为空,先保存 next = curr.next。
  3. 执行 curr.next = prev,把当前节点接到已反转部分前面。
  4. 依次更新 prev = curr、curr = next,继续处理原来的后继。
  5. 循环结束后返回 prev。Go 代码直接用 head 作为当前指针,与 curr 含义相同。

代码实现

class Solution {
    public ListNode reverseList(ListNode head) {
        ListNode prev = null;
        ListNode curr = head;

        while (curr != null) {
            ListNode next = curr.next;

            curr.next = prev;
            prev = curr;
            curr = next;
        }

        return prev;
    }
}
func reverseList(head *ListNode) *ListNode {
    var prev *ListNode

    for head != nil {
        next := head.Next
        head.Next = prev
        prev = head
        head = next
    }
    return prev
}

复杂度分析

  • 时间复杂度:$O(n)$,n 为链表节点数,每个节点只处理一次。
  • 空间复杂度:$O(1)$,只使用固定数量的节点指针。

关键点总结

[!green]

  • prev 不只是前一个节点:它还是整段已反转链表的入口,curr.next = prev 会把当前节点接到这一整段前面。
  • 先保存再修改:next 保留未处理部分,prev 保留已反转部分,修改连接后两部分都不会丢失。
  • 空链表自然结束:初始 curr 为空时不进入循环,返回的 prev 也是空。

解法:递归反转指针

核心思路

[!blue]

先明确递归函数的含义:reverseList(head) 负责反转以 head 开头的整条链表,并返回反转后的头节点。如果链表为空或只剩一个节点,已经不需要反转,直接返回 head。

对于更长的链表,先调用 reverseList(head.next),把当前节点后面的链表反转好,得到它的新头节点 newHead。递归返回后,原来的第二个节点已经成为这段链表的尾节点,而当前的 head.next 仍然指向它。

因此执行 head.next.next = head,就能把当前节点接到这段已反转链表的末尾。随后必须令 head.next = null,断开当前节点指向原第二个节点的旧连接,否则这两个节点会形成环。

每层递归只把自己的 head 接到已反转后缀的末尾,不会改变 newHead。所以一直向上返回 newHead,最终得到的就是原链表的尾节点,也就是反转后的头节点。

解题步骤

  1. 如果 head 为空或 head.next 为空,直接返回 head。
  2. 递归反转 head.next 开头的链表,并保存返回值 newHead。
  3. 执行 head.next.next = head,把当前节点接到已反转后缀的末尾。
  4. 执行 head.next = null,让当前节点成为合法的新尾节点。
  5. 返回 newHead,供上一层继续连接。

代码实现

class Solution {
    public ListNode reverseList(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }

        ListNode newHead = reverseList(head.next);

        head.next.next = head;
        head.next = null;
        return newHead;
    }
}
func reverseList(head *ListNode) *ListNode {
    if head == nil || head.Next == nil {
        return head
    }

    newHead := reverseList(head.Next)
    head.Next.Next = head
    head.Next = nil
    return newHead
}

复杂度分析

  • 时间复杂度:$O(n)$,每个节点对应一层递归,每层只修改固定数量的指针。
  • 空间复杂度:$O(n)$,递归调用栈的深度与链表长度相同。

关键点总结

[!green]

  • 先反转后缀,再连接当前节点:递归返回时后缀已经处理完毕,当前层只需把 head 接到它的尾部。
  • 返回值始终是新头节点:head 在当前层成为尾节点,不能用它替代 newHead 返回。
  • 递归并非常数空间:虽然没有新建链表节点,仍然需要保存各层调用;链表较长时,迭代写法更稳妥。

易错点总结

[!yellow]

  • 迭代时丢失后续链表:必须先保存 curr.next 再反转,反转后沿 curr.next 前进会回到已处理部分。
  • 迭代时返回错误指针:循环结束后当前指针为空,新头节点应从 prev 取得。
  • 递归时形成环:接上 head.next.next = head 后,必须断开 head.next;两步也不能颠倒,否则会失去后缀尾节点的入口。
  • 递归边界判断顺序错误:先判断 head 是否为空,再访问它的 next,空链表才能正常返回。

相似题目

题目 难度 关联与区别
92. 反转链表 II 中等 把整条链表反转限制到给定区间,额外保存区间前驱并接回两端。
25. K 个一组翻转链表 困难 把反转作为子过程,每k个节点处理一次,并保留不足k的尾段。
143. 重排链表 中等 拆分链表后反转并重新连接;本题完整反转链表,该题从中点拆分并交替合并首尾。
234. 回文链表 简单 拆分链表后反转并重新连接;本题完整反转链表,该题反转后半段后比较对称值。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/71018228
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!