题目描述

✅ LCR 024. 反转链表

image-20260928235026690

image-20260928235026691

题意分析

将单链表的节点连接方向全部反转,并返回反转后的头节点。原来的尾节点成为新头,原头成为尾且后继应为空,节点值保持不变。

单链表只能通过 next 访问后继,修改连接时必须保住尚未处理部分的入口。空链表返回空,单节点仍返回自身。原题进阶要求同时给出迭代与递归实现,下面分别说明。

解法:迭代反转链表指针

核心思路

[!blue]

将链表看成已处理与未处理两部分。pre 指向已反转部分的头,p 指向尚未处理部分的头。初始时已处理部分为空,令 pre = null,未处理部分从 head 开始。

每轮把 p 指向的节点移到已反转部分的最前面。先保存 q = p.next,因为接下来修改 p.next 后,原后继就不能再通过当前节点找到。

再令 p.next = pre,完成这一条边的反转;将 pre 更新为当前节点,p 更新为保存的 q。处理完一轮后,前面所有节点已倒序相连,剩余节点仍沿原方向连接,双方入口均被保存。

每轮恰好处理一个节点,直到 p 为空,pre 就是整条反转链表的头。第一轮把原头接到空的已处理部分,也自然让它成为新尾,省去单独断尾操作。

空链表不进入循环,单节点只执行一轮,两种边界都由同一流程覆盖。

解题步骤

  1. 初始化 pre = null、p = head。
  2. 当前节点存在时,先保存它的旧后继 q。
  3. 令当前节点指向 pre,再将 pre 移到当前节点。
  4. 将 p 移到旧后继,重复直到待处理部分为空。
  5. 返回 pre。

代码实现

class Solution {
    public ListNode reverseList(ListNode head) {
        ListNode pre = null;
        ListNode p = head;

        while (p != null) {
            // 改 p.next 之前先保住后继,否则待处理段整体丢失。
            ListNode q = p.next;

            p.next = pre;
            pre = p;
            p = q;
        }

        return pre;
    }
}
func reverseList(head *ListNode) *ListNode {
    var pre *ListNode
    for p := head; p != nil; {
        // 改 p.Next 之前先保住后继,否则待处理段整体丢失。
        q := p.Next
        p.Next = pre
        pre = p
        p = q
    }
    return pre
}

复杂度分析

  • 时间复杂度:$O(n)$,每个节点访问一次、改写一次后继。
  • 空间复杂度:$O(1)$,只保存当前节点、旧后继与已反转头。

关键点总结

[!green]

  • 修改后继前先保存旧后继,避免丢失未处理部分。
  • 每轮将一个节点头插到已反转部分,状态含义始终不变。
  • 初始空前驱让原头自然变为尾节点。
  • 循环结束返回已反转头,而不是已经走空的当前指针。

解法:递归反转后缀

核心思路

[!blue]

定义 reverseList(head) 返回以当前 head 开始的整段链表反转后的新头。空链表或单节点已经反转完成,直接返回。

对更长链表,先递归反转 head.next 后面的全部节点。递归结束后,原第二个节点变成了这段反转后缀的尾节点,而 head.next 仍指向这个原第二节点,因此可以用 head.next.next = head 把当前头接到它后面。

接好以后,当前节点成为整段的新尾,需要令 head.next = null 清除旧方向的边,否则它仍指回刚才的第二节点而构成环。整段新头在递归中已经得到,回溯时保持它不变并返回。

子问题规模每次减少一个节点,到尾节点后逐层接回,完整实现所有边反向。节点仍原地复用,但递归调用占用线性栈空间。

解题步骤

  1. 当前节点为空或没有后继时直接返回。
  2. 递归反转后缀,保存返回的新头。
  3. 将后缀的新尾接向当前头,再将当前头的旧后继清空。
  4. 返回递归得到的新头。

代码实现

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.next 保留了原第二节点引用,它在递归后恰好是后缀尾。
  • 接回当前头后必须断开旧边,才能保持无环。
  • 迭代与递归都复用节点,额外空间差异来自调用栈。

易错点总结

[!yellow]

  • 没保存后继就覆盖 next,会失去继续访问剩余链表的入口。
  • 迭代条件只检查 p.next,会漏掉最后一个节点,并且不能自然处理空输入。
  • pre 不能初始化为 head,否则第一次连接会形成自环。
  • 反转非空链后,原 head 位于新尾;统一返回 pre 才能得到完整结果。
  • 递归回溯后不清空原头的旧后继,会让最后两个节点相互指向而形成环。
  • 递归返回的是整段新头,不能在回溯每一层时改为返回当前原头。

相似题目

题目 难度 关联与区别
92. 反转链表 II 中等 把整条链表反转限制到给定区间,额外保存区间前驱并接回两端。
25. K 个一组翻转链表 困难 把反转作为子过程,每k个节点处理一次,并保留不足k的尾段。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/96367658
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!