LeetCode 206:反转链表

文章内容

https://leetcode.cn/problems/reverse-linked-list/description/

一、题目解析

1.1 题意

给定单链表的头节点 head,反转链表,返回反转后的头节点。原链表尾节点变成新头,每个节点的 next 指针反向指向前驱。

plain text
原链表:1 → 2 → 3 → null反转后:3 → 2 → 1 → null

链表节点定义是标准的 ListNode,有 val 和 next 两个字段。

单链表反转

二、递归解法一:类变量存新头

思路:递归走到链表末尾,用一个类变量 ans 抓住尾节点作为反转后的新头。回溯时逐层翻转指针。

java
class Solution {    // 类变量,存反转后的新头节点(原链表尾节点)    private ListNode ans = null;     private void reverseListHelp(ListNode head) {        // 走到最后一个节点,它就是新头        if (head.next == null) {            ans = head;            return;        }        // 先递归到底        reverseListHelp(head.next);        // 回溯:让后继节点回头指向自己        head.next.next = head;        // 断开自己原来的 next,防止成环        head.next = null;    }     public ListNode reverseList(ListNode head) {        if (head == null) {            return null;        }        reverseListHelp(head);        return ans;    }}

*reverseListHelp 一路沿着 next 往下走,直到 head.next == null,也就是原链表的尾节点。此时把 ans = head,这个引用会跟着对象一路活到方法返回。

回溯时,递归到底后开始逐层返回。假设当前在节点 2,下一层已经处理完节点 3。head.next.next = head 让 3 的 next 指向 2,head.next = null 断开 2 原本指向 3 的指针。每一层只翻转自己和后继的关系,等回溯到第一层时整个链表就反过来了。

递归反转:深入与回溯

三、递归解法二:通过返回值传递新头

思路:不不需要引入类变量,而是让新头通过返回值从底层一路透传上来。

java
class Solution {    public ListNode reverseList(ListNode head) {        // 空链表或走到尾节点,直接返回(它就是新头)        if (head == null || head.next == null) {            return head;        }        // 递归反转后续链表,next 即为反转后的新头        ListNode next = reverseList(head.next);        // 让后继节点回头指向自己        head.next.next = head;        // 断开原指针        head.next = null;        // 把新头原样传回上层        return next;    }}

返回值是什么。 reverseList 的返回值始终是新链表的头节点(原尾节点)。递归到底时 head.next == null,返回的就是尾节点本身。回溯时每一层拿到下层返回的 next,干完指针翻转的活之后,把 next 原封不动扔回给上层。新头从头到尾不变,只是在递归链上被传递。

和解法一的区别。 解法一把新头存在类变量里,helper 方法返回 void;解法二用返回值透传,省掉一个类变量,代码更短。两版的指针翻转逻辑完全一致:head.next.next = head + head.next = null。

四、栈模拟递归

思路:既然递归的本质是隐式调用栈,那就自己搞一个显式的 Stack,先把所有节点按顺序推进去,再按出栈顺序重新串起来。

java
class Solution {    public ListNode reverseList(ListNode head) {        if (head == null) {            return null;        }        Stack<ListNode> stack = new Stack<>();        // 把所有节点依次压栈        while (head != null) {            stack.push(head);            head = head.next;        }        // 栈顶是原尾节点,即新头        ListNode ans = stack.pop();        ListNode temp = ans;        // 逐个弹出,反向链接        while (!stack.isEmpty()) {            ListNode node = stack.pop();            // 清掉弹出节点的 next,避免残留指针            node.next = null;            temp.next = node;            temp = node;        }        return ans;    }}

压栈阶段。 从头到尾遍历链表,每个节点 push 进栈。栈顶是尾节点(最后一个 push),栈底是头节点(第一个 push)。

出栈重建。 先 pop 出栈顶作为新头 ans。之后每次 pop 一个节点,把它的 next 置空(因为原链表中它指向的是前驱节点,不置空会成环),然后将当前尾节点 temp 的 next 指向它,temp 后移。最后栈空时链表重建完毕。

为什么要 node.next = null。 原链表里 node 的 next 指向它的后继。压栈时我们没有改动任何指针,所以弹出重建时必须手动切断原 next,否则链表末尾会指回前面的节点。

五、复杂度分析

解法 时间 空间
递归一 O(n) O(n)(递归栈)
递归二 O(n) O(n)(递归栈)
栈模拟 O(n) O(n)(显式栈)

三种解法都是 O(n) 时间、O(n) 空间。递归的空间开销是调用栈深度,栈模拟的空间开销是 Stack 的大小,都是 n。


本文由 AgentPlanFlow 生成

觉得不错?给作者点个赞吧

分类:题解

评论 (0)

无需注册,留言经站长审核后公开。

加载评论中…

LeetCode 206:反转链表 · Yinchen 的博客