【数据结构与算法】之单链表反转

单链表反转是一个经典的算法问题,考察对指针操作的理解。本文将详细解释单链表反转的原理。注意:均为Java语言实现

相关教程:

数据结构之链表详解

递归详解

1. 问题描述

给定一个单链表的头节点,要求将其反转,并返回新的头节点。

例如:

java 复制代码
原始链表: 1 -> 2 -> 3 -> 4 -> NULL

反转后: 4 -> 3 -> 2 -> 1 -> NULL


输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]

输入:[1,2]
输出:[2,1]

输入:[]
输出:[]

2. 思路讲解

反转单链表的核心思想是逐个改变节点的指向。我们可以使用三个指针:prev,curr 和 next 来完成这个操作。

  • prev: 指向前一个节点,初始值为 null。

  • curr: 指向当前节点,初始值为头节点。

  • next: 指向下一个节点,用于保存当前节点的下一个节点,防止在改变 curr 指向时丢失后续节点的信息。

反转的步骤如下:

  • 初始化 prev = null,curr = head。

  • 循环遍历链表,直到 curr 为 null。

  • 在循环内部,执行以下操作:

    • next = curr.next:保存当前节点的下一个节点。

    • curr.next = prev:将当前节点的 next 指针指向前一个节点,实现反转。

    • prev = curr:将 prev 指针移动到当前节点,为下一次循环做准备。

    • curr = next:将 curr 指针移动到下一个节点。

  • 循环结束后,prev 指针指向新的头节点。

3. Java代码实现

java 复制代码
public class ReverseLinkedList {

    // 定义链表节点
    static class ListNode {
        int val;
        ListNode next;

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

    public static ListNode reverseList(ListNode head) {
        // prev 指向前一个节点,curr 指向当前节点,next 指向下一个节点
        ListNode prev = null;
        ListNode curr = head;
        ListNode next = null;

        // 循环遍历链表
        while (curr != null) {
            // 保存下一个节点
            next = curr.next;

            // 反转当前节点的指向
            curr.next = prev;

            // 更新 prev 和 curr 指针
            prev = curr;
            curr = next;
        }

        // 循环结束后,prev 指向新的头节点
        return prev;
    }

    // 测试代码
    public static void main(String[] args) {
        ListNode head = new ListNode(1);
        head.next = new ListNode(2);
        head.next.next = new ListNode(3);
        head.next.next.next = new ListNode(4);

        ListNode reversedHead = reverseList(head);

        // 打印反转后的链表
        while (reversedHead != null) {
            System.out.print(reversedHead.val + " ");
            reversedHead = reversedHead.next;
        } // 输出:4 3 2 1 
    }
}

4. 其它方式

除了迭代方法,递归也是反转单链表的常用方法。下面提供迭代法、递归法以及头插法三种方式实现单链表的反转,并按照思路难度递增排序:

4.1 迭代法 (Iterative)

这是最直观且容易理解的方法,与之前的解释相同。

java 复制代码
public static ListNode reverseListIterative(ListNode head) {
    ListNode prev = null;
    ListNode curr = head;
    ListNode next = null;

    while (curr != null) {
        next = curr.next;
        curr.next = prev;
        prev = curr;
        curr = next;
    }
    return prev;
}

其原理是:

  • 初始化三个指针:prev指向null,curr指向链表的头节点head,next用来暂存curr的下一个节点。
  • 在while循环中,只要curr不为null(即还有节点需要处理),就执行以下步骤:
    • 将next指向curr的下一个节点,以保存对下一个节点的引用。
    • 将curr.next指向prev,这样就把当前节点从原来的链表中摘下来,并反转它的指向。
    • 将prev和curr向前移动一位,即prev变成curr,curr变成next。
  • 当curr为null时,循环结束,此时prev指向了新的头节点,也就是原链表的尾节点。
  • 返回prev,即反转后的链表头节点。

通过这种方式,迭代地改变每个节点的指向,最终实现整个链表的反转。

4.2 递归法 (Recursive)

递归方法的思路是:先递归反转子链表,然后将当前节点连接到反转后的子链表的尾部。

java 复制代码
public static ListNode reverseListRecursive(ListNode head) {
    if (head == null || head.next == null) {
        return head; // 递归终止条件:空链表或只有一个节点的链表
    }

    ListNode newHead = reverseListRecursive(head.next); // 递归反转子链表
    head.next.next = head; // 将当前节点连接到反转后的子链表的尾部
    head.next = null; // 将原链表尾节点的 next 指针置为 null

    return newHead;
}

递归方法的理解难点在于递归调用的过程。可以想象一下,递归会一直深入到链表的最后一个节点,然后从最后一个节点开始逐层返回,并依次修改节点的指向。

4.3 头插法 (Head Insertion)

头插法是另一种迭代的思路,它模拟了构建新链表的过程。我们将原链表的节点逐个取下,然后插入到新链表的头部。

java 复制代码
public static ListNode reverseListHeadInsertion(ListNode head) {
    ListNode newHead = null; // 新链表的头节点,初始为空
    ListNode curr = head;

    while (curr != null) {
        ListNode next = curr.next;  // 保存下一个节点
        curr.next = newHead;      // 将当前节点插入到新链表的头部
        newHead = curr;          // 更新新链表的头节点
        curr = next;             // 移动到下一个节点
    }

    return newHead;
}

头插法的思路相对简单,但需要理解构建新链表的过程。

总结:

  • 迭代法: 最直观易懂,代码简洁,推荐使用。

  • 递归法: 代码简洁优雅,但理解起来稍微复杂一些,需要注意递归的调用过程和终止条件。栈深度可能会成为一个问题,尤其对于非常长的链表。

  • 头插法: 思路清晰,模拟构建新链表的过程,代码也比较容易理解。

希望以上三种方法的解释和代码能够帮助你更好地理解单链表反转的各种实现方式。 选择哪种方法取决于你的个人偏好和具体场景。 在实际应用中,迭代法由于其简洁性和避免了递归的栈深度问题,通常是首选。

为了方便测试,你可以使用以下代码创建一个链表并测试不同的反转方法:

java 复制代码
public static void main(String[] args) {
    ListNode head = new ListNode(1);
    head.next = new ListNode(2);
    head.next.next = new ListNode(3);
    head.next.next.next = new ListNode(4);

    // 测试不同的反转方法
    ListNode reversedHead1 = reverseListIterative(head);
    // ... (打印 reversedHead1) ...  记得重新创建一个链表,或者深拷贝一份进行测试

    head = new ListNode(1); // 重新创建链表
    head.next = new ListNode(2);
    head.next.next = new ListNode(3);
    head.next.next.next = new ListNode(4); // 重新构建链表,避免影响后续测试
    ListNode reversedHead2 = reverseListRecursive(head);
    // ... (打印 reversedHead2) ...

    head = new ListNode(1);
    head.next = new ListNode(2);
    head.next.next = new ListNode(3);
    head.next.next.next = new ListNode(4);
    ListNode reversedHead3 = reverseListHeadInsertion(head);
    // ... (打印 reversedHead3) ...
}

请注意,在测试不同方法时,需要重新创建链表或者深拷贝一份,因为反转操作会修改原链表的结构。

5. 复杂度分析

  • 时间复杂度: O(n),其中 n 是链表的长度。我们需要遍历链表一次。

  • 空间复杂度: O(1),我们只使用了常数个额外变量。

6. 总结

单链表反转是一个重要的算法,理解其原理并能够熟练地编写代码至关重要。本文提供的代码清晰易懂,希望能够帮助读者更好地理解和掌握这个算法。 通过使用 prev、curr 和 next 三个指针,我们可以优雅地实现链表的反转。记住关键步骤:保存下一个节点、反转当前节点指向、更新指针。 熟练掌握这个技巧,可以帮助你解决更多更复杂的链表问题。

最后感谢各位看官的观看,下期见,谢谢~

相关推荐
Codeking__4 分钟前
”一维前缀和“算法原理及模板
数据结构·算法
休息一下接着来5 分钟前
C++ 条件变量与线程通知机制:std::condition_variable
开发语言·c++·算法
爱尚你199315 分钟前
Java 泛型与类型擦除:为什么解析对象时能保留泛型信息?
java
Code哈哈笑18 分钟前
【机器学习】支持向量回归(SVR)从入门到实战:原理、实现与优化指南
人工智能·算法·机器学习·回归·svm
小哈里24 分钟前
【pypi镜像源】使用devpi实现python镜像源代理(缓存加速,私有仓库,版本控制)
开发语言·python·缓存·镜像源·pypi
努力学习的小廉29 分钟前
【C++】 —— 笔试刷题day_29
开发语言·c++·算法
小羊在奋斗30 分钟前
【LeetCode 热题 100】搜索插入位置 / 搜索旋转排序数组 / 寻找旋转排序数组中的最小值
算法·leetcode·职场和发展
meisongqing36 分钟前
【软件工程】符号执行与约束求解缺陷检测方法
人工智能·算法·软件工程·软件缺陷
电商数据girl37 分钟前
酒店旅游类数据采集API接口之携程数据获取地方美食品列表 获取地方美餐馆列表 景点评论
java·大数据·开发语言·python·json·旅游
天天打码37 分钟前
python版本管理工具-pyenv轻松切换多个Python版本
开发语言·python