代码随想录—力扣算法题:206反转链表.Java版(示例代码与导图详解)

206.反转链表

力扣题目链接
更多内容可点击此处跳转到代码随想录,看原版文件

题意:反转一个单链表。

示例: 输入: 1->2->3->4->5->NULL 输出: 5->4->3->2->1->NULL

思路

如果再定义一个新的链表,实现链表元素的反转,其实这是对内存空间的浪费。

其实只需要改变链表的next指针的指向,直接将链表反转 ,而不用重新定义一个新的链表,如图所示:

代码随想录—力扣算法题:206反转链表.Java版(示例代码与导图详解)_第1张图片

之前链表的头节点是元素1, 反转之后头结点就是元素5 ,这里并没有添加或者删除节点,仅仅是改变next指针的方向。

那么接下来看一看是如何反转的呢?

我们拿有示例中的链表来举例,如下面所示:(纠正:动画应该是先移动pre,在移动cur)

首先定义一个cur指针,指向头结点,再定义一个pre指针,初始化为null。

cur = head
pre = null

代码随想录—力扣算法题:206反转链表.Java版(示例代码与导图详解)_第2张图片

然后就要开始反转了,首先要把 cur->next 节点用tmp指针保存一下,也就是保存一下这个节点。

temp = cur - > next

代码随想录—力扣算法题:206反转链表.Java版(示例代码与导图详解)_第3张图片

为什么要保存一下这个节点呢,因为接下来要改变 cur->next 的指向了,将cur->next 指向pre ,此时已经反转了第一个节点了。

现在,cur->next 指向pre ,此时我们反转了第一个节点。

cur - > next = pre

代码随想录—力扣算法题:206反转链表.Java版(示例代码与导图详解)_第4张图片

接下来,就是循环走如下代码逻辑了,继续移动pre和cur指针。先移动pre到cur的位置,再把cur往temp的位置去移动。(如果先把cur往temp的位置去移动,那此时cur = temp,cur的值已变化,pre就无法指向正确位置了)

pre = cur
cur = temp

代码随想录—力扣算法题:206反转链表.Java版(示例代码与导图详解)_第5张图片

代码随想录—力扣算法题:206反转链表.Java版(示例代码与导图详解)_第6张图片

一次循环就改变一个节点,循环下去改变下一个节点,知道cur指向null了,那么这个循环就结束了,链表也反转完毕了。而此时pre就等于我们新链表中的新头结点,最后直接返回就可以了。

while(cur)
{
	temp = cur - > next
	cur - > next = pre
	pre = cur
	cur = temp
}
return pre;

代码随想录—力扣算法题:206反转链表.Java版(示例代码与导图详解)_第7张图片

双指针法

思路已在上方呈现,下面是示例代码:

public class Solution {
    public ListNode reverseList(ListNode head) {
        ListNode pre = null; // 初始化前一个节点为null
        ListNode cur = head; // 初始化当前节点为头节点
        ListNode temp = null; // 用于保存下一个节点的临时变量
        while (cur != null) {
            temp = cur.next; // 保存下一个节点
            cur.next = pre; // 将当前节点指向前一个节点,实现链表反转
            pre = cur; // 更新前一个节点为当前节点
            cur = temp; // 更新当前节点为下一个节点
        }
        return pre; // 返回反转后的头节点
    }
}
  • 时间复杂度: O(n)
  • 空间复杂度: O(1)

递归法

递归法相对抽象一些,但是其实和双指针法是一样的逻辑,同样是当cur为空的时候循环结束,不断将cur指向pre的过程。

关键是初始化的地方,可能有的同学会不理解, 可以看到双指针法中初始化 cur = head,pre = NULL,在递归法中可以从如下代码看出初始化的逻辑也是一样的,只不过写法变了。

具体可以看代码(已经详细注释),双指针法写出来之后,理解如下递归写法就不难了,代码逻辑都是一样的。

class Solution {
    public ListNode reverseList(ListNode head) {
        return reverse(null, head); // 调用私有递归方法,反转链表
    }

    /**
     * 递归反转链表
     *
     * @param pre 反转后的链表头节点
     * @param cur  当前节点
     * @return 反转后的链表头节点
     */
    private ListNode reverse(ListNode pre, ListNode cur) {
        if (cur == null) {
            return pre; // 如果当前节点为空,说明已遍历到链表末尾,返回反转后的链表头节点
        }
        ListNode temp = null; // 用于保存下一个节点的临时变量
        temp = cur.next; // 保存下一个节点
        cur.next = pre; // 反转当前节点指向前一个节点
        // 更新pre、cur位置
        // pre = cur; // 将当前节点指向的前一个节点更新为当前节点
        // cur = temp; // 将下一个节点更新为当前节点
        return reverse(cur, temp); // 递归反转后续节点,往下继续走的意思
    }
}
  • 时间复杂度: O(n), 要递归处理链表的每个节点
  • 空间复杂度: O(n), 递归调用了 n 层栈空间
  1. reverseList方法是公共的入口方法,用于调用递归反转链表方法。
  2. reverse方法是私有的递归方法,用于实现链表的反转
  3. 在每次递归调用前,将当前节点指向下一个节点,实现链表反转。

我们可以发现,上面的递归写法和双指针法实质上都是从前往后翻转指针指向,其实还有另外一种与双指针法不同思路的递归写法:从后往前翻转指针指向。

具体代码如下(带详细注释):

class Solution {
    ListNode reverseList(ListNode head) {
        // 边缘条件判断
        if(head == null) return null;
        if (head.next == null) return head;
        
        // 递归调用,翻转第二个节点开始往后的链表
        ListNode last = reverseList(head.next);
        // 翻转头节点与第二个节点的指向
        head.next.next = head;
        // 此时的 head 节点为尾节点,next 需要指向 NULL
        head.next = null;
        return last;
    } 
}

两种方法的区别

双指针法和递归法是两种常见的用于反转链表的方法,它们的主要区别在于实现方式和思路。

  1. 双指针法:
    • 双指针法使用两个指针,分别指向当前节点前一个节点,通过改变指针的指向来实现链表的反转。
    • 遍历链表,每次迭代将当前节点的next指针指向前一个节点,然后更新pre指针和cur指针的位置。
    • 遍历结束后,pre指针即为反转后的头节点
    • 双指针法的空间复杂度为O(1),需要额外的两个指针变量来保存当前节点和前一个节点。
  2. 递归法:
    • 递归法是通过递归函数来实现链表的反转。递归函数将链表分为两部分:头节点和剩余部分。
    • 递归函数的终止条件是当前节点为null或者下一个节点为null,即遍历到链表末尾。
    • 在递归调用中,先递归翻转剩余部分的链表
    • 然后,将当前节点的next指针指向前一个节点,完成当前节点与剩余部分的反转。
    • 最后,返回反转后的头节点。
    • 递归法的空间复杂度取决于递归调用的深度,最坏情况下是O(n),其中n是链表的长度。

总结

  • 双指针法是通过迭代遍历链表来实现反转,它使用两个指针来改变指针的指向。
  • 递归法是通过递归函数实现反转,它将链表分为头节点和剩余部分,并递归翻转剩余部分。
  • 双指针法的空间复杂度为O(1),递归法的空间复杂度为O(n)。
  • 在实际应用中,根据具体情况选择双指针法或递归法来实现链表的反转。

对双指针中的代码进行编写测试类,用于试运行:

第一版:

public class Solution {
    public static ListNode reverseList(ListNode head) {
        ListNode pre = null; // 初始化前一个节点为null
        ListNode cur = head; // 初始化当前节点为头节点
        ListNode temp = null; // 用于保存下一个节点的临时变量
        while (cur != null) {
            temp = cur.next; // 保存下一个节点
            cur.next = pre; // 将当前节点指向前一个节点,实现链表反转
            pre = cur; // 更新前一个节点为当前节点
            cur = temp; // 更新当前节点为下一个节点
        }
        return pre; // 返回反转后的头节点
    }

    public static void main(String[] args) {
        // 创建测试链表
        ListNode head = new ListNode(1);
        ListNode node2 = new ListNode(2);
        ListNode node3 = new ListNode(3);
        ListNode node4 = new ListNode(4);
        ListNode node5 = new ListNode(5);

        head.next = node2;
        node2.next = node3;
        node3.next = node4;
        node4.next = node5;

        // 打印原链表
        System.out.print("原链表:");
        printLinkedList(head);

        // 调用反转链表方法
        ListNode reversed =reverseList(head);

        // 打印反转后的链表
        System.out.print("反转后的链表:");
        printLinkedList(reversed);
    }

    // 辅助方法,用于遍历打印链表
    public static void printLinkedList(ListNode head) {
        ListNode temp = head;
        StringBuilder sb = new StringBuilder(); // 使用StringBuilder来逐次构建字符串

        while (temp != null) {
            sb.append(temp.val).append(" -> "); // 逐次追加链表节点的值
            temp = temp.next;
        }
        sb.append("NULL"); // 最后追加 "NULL" 字符串

        System.out.println(sb.toString()); // 输出构建好的字符串
    }
}

class ListNode {
    int val;
    ListNode next;

    ListNode(int val) {
        this.val = val;
    }
}

第二版:

public class Solution {
    public ListNode reverseList(ListNode head) {
        ListNode pre = null; // 初始化前一个节点为null
        ListNode cur = head; // 初始化当前节点为头节点
        ListNode temp = null; // 用于保存下一个节点的临时变量
        while (cur != null) {
            temp = cur.next; // 保存下一个节点
            cur.next = pre; // 将当前节点指向前一个节点,实现链表反转
            pre = cur; // 更新前一个节点为当前节点
            cur = temp; // 更新当前节点为下一个节点
        }
        return pre; // 返回反转后的头节点
    }

    public static void main(String[] args) {
        // 创建测试链表
        ListNode head = new ListNode(1);
        ListNode node2 = new ListNode(2);
        ListNode node3 = new ListNode(3);
        ListNode node4 = new ListNode(4);
        ListNode node5 = new ListNode(5);
        
        head.next = node2;
        node2.next = node3;
        node3.next = node4;
        node4.next = node5;
        
        // 创建Solution对象
        Solution solution = new Solution();
        
        // 调用反转链表方法
        ListNode reversed = solution.reverseList(head);
        
        // 打印原链表
        System.out.print("原链表:");
        printLinkedList(head);
        
        // 打印反转后的链表
        System.out.print("反转后的链表:");
        printLinkedList(reversed);
    }
    
    // 辅助方法,用于遍历打印链表
    public static void printLinkedList(ListNode head) {
        ListNode temp = head;
        while (temp != null) {
            System.out.print(temp.val + " -> ");
            temp = temp.next;
        }
        System.out.println("NULL");
    }
}

class ListNode {
    int val;
    ListNode next;
    
    ListNode(int val) {
        this.val = val;
    }
}

这两段代码的区别在于打印原链表的时机不同

第一段代码中,先打印原链表,然后再调用反转链表方法进行链表反转,最后打印反转后的链表。

// 打印原链表
System.out.print("原链表:");
printLinkedList(head);

// 调用反转链表方法
ListNode reversed = reverseList(head);

// 打印反转后的链表
System.out.print("反转后的链表:");
printLinkedList(reversed);

而第二段代码中,先调用反转链表方法进行链表反转,然后再打印原链表和反转后的链表。

// 调用反转链表方法
ListNode reversed = reverseList(head);

// 打印原链表
System.out.print("原链表:");
printLinkedList(head);

// 打印反转后的链表
System.out.print("反转后的链表:");
printLinkedList(reversed);

因此,第一段代码的输出结果为:

原链表:1 -> 2 -> 3 -> 4 -> 5 -> NULL
反转后的链表:5 -> 4 -> 3 -> 2 -> 1 -> NULL

而第二段代码的输出结果为:

原链表:1 -> NULL
反转后的链表:5 -> 4 -> 3 -> 2 -> 1 -> NULL

所以,区别在于第一段代码输出了完整的原链表,而第二段代码输出了只有一个节点的原链表。推荐使用第一段代码的方式来打印完整的原链表和反转后的链表。

你可能感兴趣的:(力扣算法题学习笔记(自用),算法,leetcode,链表,java,蓝桥杯)