啃算法:02自定义单向链表的翻转

这是一个经典的算法题。我们通过自定义一个链表模型,然后通过持有的头结点去翻转一个链表。比如说: 1234,翻转过来就是4321,这道题的要求是:

空间复杂度O(1),时间复杂度是O(n)

这就说明,最佳的性能上来说,变量的申请是常量个数,无论是1个还是多个,不随着n的变化而增加,时间复杂度是O(n),所以最多只能有一个循环,无论这个循环递归还是while 或者说for 循环,只能有一层。

算法的实现方式不多的,如有雷同,那一定是我抄别人的。

正文

我先来看,这个自定义的非线程安全的单向链表怎么定义的:

aidl 复制代码
    static public class ListNode {
        int val;
        ListNode next = null;

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

这很符合链单向链表的定义,任意的节点都持有来下一个节点的数据。我们这里就不分析内存模型啥的了。实现的思路分为两种:

  • 一种是引入一个外部的数组栈,先将数据添加到引入的对象里面去,然后通过一个循环把数据读取出来。
  • 另外一种就是计算类的了,通过循环去改变当前节点的next 节点的值而达到链表的翻转的目的。

链表的赋值与遍历

这个变量有两个值,一个是 val,一个是next,next 表示的是下一个节点。单向链表上一个节点持有下一个节点,这很符合逻辑。 我们先定义一个头节点,然后通过循环添加链表。

aidl 复制代码
        ListNode head= new ListNode(0);
        ListNode prent=head;
        for (int i=1;i<10;i++){
            ListNode body= new ListNode(i);
            prent.next=body;
            prent=body;
        }

OK,链表数据是添加成功了。那么我们如何遍历这个链表呢?可以看到,上面的代码里面我们最后一个节点的next 一定是空的。所以我们这里通过while循环, 判断当前对象的next是否为空,为空了就表示是链表的最后一个节点了。

aidl 复制代码
        ListNode showParent=head;
        while (showParent!=null){
            System.out.println(showParent.val);
            showParent=showParent.next;
        }

翻转链表

我们结合上面的思路,开整。

最容易想到的方式:新增一个List

整体思路是将链表循环出来添加到一个list 里面,然后通过for 循环倒序获取到最后一个节点,然后循环把节点赋值给上一个节点,同时处理最后的节点的next为空。

aidl 复制代码
    static public ListNode ReverseList(ListNode head) {
        List<ListNode> nodes=new ArrayList<>();
        while (head!=null){
            nodes.add(head);
            head=head.next;
        }
        head= nodes.get(nodes.size()-1);
        ListNode prent=head;
        for (int i=nodes.size()-2;i>-1;i--){
            prent.next=nodes.get(i);
            prent=nodes.get(i);
            if (i==0){
                prent.next=null;
            }
        }
        return head;
    }

可以看到,这个点时间复杂度是O(n),因为我们有两个循环,空间复杂度是O(n),这明显不符合我们对要求。

通过倒序不优雅(看起来有点菜),那么通过栈去实现

栈的特性就是先进后出,所以我们每次只需要获取栈顶即可,只要栈顶没有了数据,循环就结束了。

这种实现思路和上面的代码通过list处理的思路是一致的,只是说利用了栈的特性。

因为栈是先进后出,所以实现的原理就是先把数据添加到一个新栈里面,然后出栈的时候形成一个新的链表。比如说,我们这里用Stack:

  • push 将item 压入栈中。
  • pop 移除并返回栈顶元素。
  • peek 返回栈顶的元素,但不移除。
  • empty 检查栈是否为空。
  • dsearch 返回对象在栈中的位置。
aidl 复制代码
    private static ListNode ReverseList(ListNode head) {
        Stack<ListNode> stack=new Stack<>();
        while (head!=null){
            stack.push(head);
            head=head.next;
        }
        if (stack.isEmpty()){
            return null;
        }
        ListNode dummy=stack.pop();
        ListNode node=dummy;
        while (!stack.isEmpty()){
           ListNode tempNode= stack.pop();
           dummy.next=tempNode;
           dummy=tempNode;
        }
        dummy.next=null;
        return node;
    }

当然了除了使用Stack,还可以使用ArrayDeque,LinkedList 或 ConcurrentLinkedDeque实现。可以看到,这个空间复杂度和时间复杂度和第一种是一模一样的。

通过计算实现双链表

可以看到,上面我们的解题思路是两个循环,一个是获取到所有,一个是倒序重新排列。既然我们在循环的时候可以拿到所有的节点,那么我们思考一下 1234 和 4321的关系。

我们循环中获取到了的首先就是头节点和他的下一个节点。默认数据的插入方式是从左到右,那么我们能不能从右到左插入数据。

  • 比如说,拿到1,我们设置他的next 为null,并且把1保存起来。
  • 循环到2,我们把2的next 设置1,那么这个时候链表就是:2 1,同时又把2保存起来。
  • 循环到3,我们把3的next 设置成2,那么链表就是 3 2 1,再次把3保存起来。
  • 循环到4 ,我们把4的next 设置成3,那么链表的就是 4321 了。

基于这套逻辑去处理,这套逻辑可以理解成老的链表往新的链表里面放数据,是否可以达到这个效果呢?

ini 复制代码
private static ListNode ReverseList(ListNode head) {
    ListNode newHead=null;
    while (head!=null){
       ListNode temp= head.next;
       head.next=newHead;
       newHead=head;
       head=temp;
    }
    return newHead;
}

因为我们新的变量个数是常量1,所以空间复杂度是O(1),只有一个循环,那么时间复杂度是O(n),所以这种思路是契合要求的。

通过递归计算

实现思路还是上面的双链表的概念思路。

ini 复制代码
private static ListNode ReverseList(ListNode head) {
    if (head==null||head.next==null){
        return head;
    }
    ListNode reverse= ReverseList(head.next);
    head.next.next=head;
    head.next=null;
    return reverse;
}

或者:

ini 复制代码
private static ListNode ReverseList(ListNode head) {
    return ReverseListInt(head,null);
}

private static ListNode ReverseListInt(ListNode head, ListNode newHead) {
    if (head==null){
        return newHead;
    }
    ListNode next= head.next;
    head.next=newHead;
    ListNode node= ReverseListInt(next,head);
    return node;
}

抄的时候,不是太明白两种递归的差异性在哪,这明显是对于两种递归的执行有一定的知识点盲区在里面的,慢慢补吧。

相关推荐
OPEN-F17 分钟前
C++11/14新特性精讲:移动语义与智能指针实战
开发语言·c++·算法
协议的旁观者31 分钟前
Android 高级逆向实战(一):对抗 360 企业加固,Native 抽取还原与 Activity 生命周期重建
android
lisin-lee-cooper35 分钟前
【leetcode658】有序数组找出k个最接近x的数
java·数据结构·算法
sunburn-43 分钟前
Java堆(Heap)详解与实战教学
java·开发语言·数据结构·ide·算法
智碳能碳管理平台1 小时前
工业能耗台账标准化:能碳管理系统的数据口径怎么设计
算法·能碳管理系统·智碳能碳管理平台·企业能碳管理系统·碳排放核算软件·绿色工厂申报saas·能碳管理平台
带多刺的玫瑰2 小时前
Leecode#15刷题之三数之和
算法·leetcode·职场和发展
哭泣方源炼蛊2 小时前
并查集进阶 P1(带权并查集,并查集分类)
数据结构·c++·算法·二进制·带权并查集
牧羊人.3332 小时前
动手学深度学习 02 | 手写数字识别
图像处理·人工智能·深度学习·算法
武帝为此3 小时前
【InnoDB存储引擎介绍】
数据库·算法
mmsx3 小时前
osmdroid 屏幕坐标与测量坐标互转:中心点+比例尺仿射换算
android·源码·地图·osmdroid