力扣138 - 复制带随机指针的链表【复杂链表的终极试炼】

news2025/7/14 5:35:56

想指☞哪就指哪儿~

  • 一、题目描述
  • 二、思路分析与罗列
    • 思路一:通过原链表的【random】去找控制拷贝链表的【random】
    • 思路二:直接链接到原链表处做相邻结点的【random】修改
      • Step1:把复制的结点插入到原结点后
      • Step2:设置拷贝结点的random值【⭐⭐⭐】
      • Step3:提取复制后的链表恢复原链表
    • 时间复杂度与空间复杂度的分析
  • 三、整体代码展示
  • 四、总结与提炼

一、题目描述

原题传送门

在这里插入图片描述

示例 1:
在这里插入图片描述

输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]

示例 2:

在这里插入图片描述

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

示例 3:
在这里插入图片描述

输入:head = [[3,null],[3,0],[3,null]]
输出:[[3,null],[3,0],[3,null]]

提示:

  • 0 <= n <= 1000
  • -104 <= Node.val <= 104
  • Node.random 为 null 或指向链表中的节点。

此题是力扣上一道中等难度的题目,不过相信很多同学在看了这道题目之后都点云里雾里的感觉,觉得这道题很复杂的样子,一个链表有两个指针域,而且有一个还是随机指针,对于题目的描述完全不读懂它在讲点什么。

是的,这道题虽说是中等难度的题目,但是在我看来本题算是链表章节的题目中比较综合的题,虽然它没有【环形链表】那么绕,需要一些数学的思维,但它的结构是比较复杂的,需要做题者对于链表的掌握程度达到一定的水准,不然得话在处理其指针的关系时就会思绪混乱、无从下手

二、思路分析与罗列

思路一:通过原链表的【random】去找控制拷贝链表的【random】

  • 首先来理一下这个题目的思路,其实把这就是一个单链表,只是它存在一个随机指针罢了,那题目的意思就是复制每一个结点,而且复制出来的结点不可以指向原链表中的结点,也就是要重新修改指向,对于这个【random】指针也需要有一个复制。那其实可以看到,就是完整地将这个链表复制一份出来

在这里插入图片描述

  • 那有同学就说,这不是很简单,把每一个结点做一个拷贝,然后再将它们做一个链接,之后再将【random】指针也做一个相同的指向不就好了。【next】指针确实可以做一个链接,但是你觉得这个【random】指针也可以直接这样做一个链接吗?

在这里插入图片描述

  • 就好比这里的【13】,原链表中是指向7,现在可以指向7,;对于【11】,原链表是指向1,那是在下午我做了一个修改后,现在有两个1了,那指向哪个呢?一般来说都是默认前面那个,因为单链表只能从前往后遍历,那算你能从后往前遍历,【1】是指向【7】的,但是后面又出现一个【7】怎么办呢,那又会出现混乱。所以我们不可以直接指向其原来的位置,要指向哪里?–》相对位置

在这里插入图片描述

  • 这个时候我们就需要去找到这个相对位置,不是去找你【random】值向的值是第几个,而是寻找【random】指向的那个地址,根据地址去进行一个查找。也就是通过原链表的【random】去控制拷贝链表的【random】
  • 首先看到【7】,原链表的【random】指向【NULL】,那拷贝链表也就指向它这里的【NULL】,根据【copy】和【cur】当前所指的值进行一个对比
  • 此时我们需要定义一个变量【i】去寻找当前原链表【cur】的【random】所指向的地址,那此时这个【i】就需要一直去遍历,找到原链表中的【NULL】

在这里插入图片描述

  • 此时就需要这个【i】去遍历这个链表进行查找,【i++】就会加到5,此时拷贝链表只需要进行一个【while(i–)】,就可以找到它那里的这个【NULL】了

在这里插入图片描述

  • 然后【cur】和【copy】后移继续更新【random】,但是【i】不需要动,此时原链表中的【1】指向位置为0的【7】,因此拷贝链表也指向位置为0的【7】

在这里插入图片描述

  • 然后继续进行一个迭代,此时原链表中的【11】指向的是后面的那个【1】,于是下标【i】就开始寻找指向的那个地址

在这里插入图片描述

  • 然后【i】去寻找,找一个【++i】,找一个【++i】,直到找到原链表的这个结点的【random】指向的地址为止

在这里插入图片描述

  • 然后拷贝链表也需要做这么一个修改,后面的也是类似,便不细讲

在这里插入图片描述

  • 我们来分析一下思路,大家认为这个思路的【时间复杂度】是多少,我们知道,时间复杂度是要算最坏的,那我举一个例子,例如说这个【7】,它是指向【NULL】的,然后这个【1】和【11】,我现在做一个修改,让它们也去指向这个【NULL】,那此时这个原链表和拷贝链表都需要遍历【N】次才可以找到这个位置,所以时间复杂度很可能到达O(N2)
  • 那O(N2)这个时间复杂度是不是太高了,于是我们就需要开始思考有没有O(N)的思路可以实现的,你能想到到吗?接下来我们再来讲一种思路

思路二:直接链接到原链表处做相邻结点的【random】修改

先行说明,这种思路的时间复杂度是O(3N),最后进行大O渐进法的优化就是O(N)

  • 好,接下去我们就来讲讲这种思路,它一共分为三步

Step1:把复制的结点插入到原结点后

  • 首先是第一步,从下图可以看到,我们定义一个【cur】指向原链表的结点,然后根据这个结点去【malloc】出一个新的结点,之后让他们的【val】值都相同
  • 接下去我们要修改的就是【next】指针域,这个很容易,保存一下当前【cur】结点的下一个结点,然后将【copy】结点和它们进行一个互连即可,链接好后做一个迭代更新

在这里插入图片描述

  • 展示一下核心代码
Node* cur = head;
while(cur)
{
    Node* copy = (Node *)malloc(sizeof(Node));
    copy->val = cur->val;

    //插入结点
    Node* nextNode = cur->next;
    cur->next = copy;
    copy->next = nextNode;

    //迭代
    cur = nextNode;
}

Step2:设置拷贝结点的random值【⭐⭐⭐】

  • 好,接下去我们开始做第二步,就是去设置每个【copy】结点的【random】指针指向。这一步非常得关键:🔑也是比较难理解的一趴,所以要专注精神哦
  • 首先第一个【7】,它的【random】是指向【NULL】,所以当前【copy】的【random】也要指向【NULL】,这个我们需要单独做判断

在这里插入图片描述

  • 然后对于【13】,我们是不是要让它指向【copy】的【7】,但是要怎么找到它呢?此时我们需要根据当前的【cur】,也就是原链表中当前结点的【random】所指向结点的【next】,去寻找这个拷贝的【random】域
  • 那有同学问,为什么可以这么找呢?这是由什么依据吗?因为我们刚才是将每一个拷贝结点都链接到了源结点之后,所以通过找到源节点的【random】域,找它的【next】,就可以很轻松地寻找到拷贝结点的【random】域

在这里插入图片描述

我们继续看下去。一样,这个【11】可以通过源节点【11】的【random】域所指向的【1】,然后去找到它后面的一个结点,然后【copy->next】等于它即可

在这里插入图片描述

  • 同上

在这里插入图片描述

  • 你可试着自己分析

在这里插入图片描述

  • 给出这部分的代码
cur = head;
while(cur)
{
    Node* copy = cur->next;
    if(cur->random == NULL)
        copy->random = NULL;
    else
        //拷贝结点的随机指针即为原结点的随机指针所指向结点的下一个结点
        copy->random = cur->random->next;
    //迭代(移动两步)
    cur = copy->next;
}
  • 好,到这里我们的第二步就算是完成了,这个时候其实你去力扣上提交的话已经是可以过了,但是你不觉得这样整个链表变得很冗杂吗?那要怎么对其再进行一个优化呢?

Step3:提取复制后的链表恢复原链表

  • 此时我们就需要第三步,也就是将这个拷贝完后链表从原链表中分割出来,然后恢复原来链表的指向。既然我们借用了原来链表的结构,去方便地将拷贝链表的【random】域做了一个链接,那此时我们就要将其恢复
  • 这其实和我们现实中的大哥带小弟一个道理,大哥把房间给到小弟一起住,然后小弟每天努力拼搏,最后有了能力,翅膀也硬了✈,要自己单飞了。但是呢,大哥先前对你这么好,你在临走前是不是要把大哥给你住的房间打扫一遍,收拾一下,这样才算样子

好,插了一个题外话,我们继续分析这第三个步骤

  • 首先,既然是要把拷贝的每一个结点单独拿出来,那就需要将其重新构成一个链表的形式,那其实也就是我们熟悉的尾插,如下图所示

在这里插入图片描述

  • 这里的拷贝结点的【random】域无需去理会它,只是我为了完整性而画的

在这里插入图片描述

  • 可以看到,只需要把【copy】结点一个个拿下来,做一个尾插,然后在原链表中进行一个【三指针的迭代】即可。可能有同学会疑惑将这个结点拿下来后拷贝结点的【random】指针不会变化吗?当然不会,只要你不去修改,那它就一直是指向它之前已经指向的那块地址,
  • 如果有这个疑惑的同学,你可以去仔细看看我的单链表从浅入深详解,你就会明白了

在这里插入图片描述

  • 同上。而且还要说明一点,如果你仔细看可以发现,我对于原链表的指向也是同步地再做修改

在这里插入图片描述

  • 可以看到,此时我们就把依附在原链表中的拷贝结点一一分解下来,然后做成了一个新的链表形式。此时只需要返回【copyHead】就可以获取整个拷贝后的链表

在这里插入图片描述

  • 代码在下一模块统一给出。你可先根据我的推导分析自己画画图写一遍,再与我的代码进行对照

时间复杂度与空间复杂度的分析

  • 第一个思路的时间复杂度说过了,是O(N2),空间复杂度是O(N)
  • 我们主要来分析一下第二个思路的复杂度,开头说过是O(3N):分别分析三个步骤,可以发现他们都需要O(N),第一个步骤是插入、第二个步骤是查找、第三个步骤是尾插,如果你认真看过我的单链表就可以知道,他们都是O(N)的复杂度,那么三个O(N)就是O(3N),可以优化为O(N)。
  • 那对于空间复杂度是多少呢?没错,也是O(N),因为我们需要存放拷贝的结点值,就要从堆区为其开辟一块空间,使用到了【malloc】,如果复杂度这一块不是很懂的同学可以看看我的这篇文章时间与空间复杂度就看这篇了

三、整体代码展示

展示一下整体的代码,不过还是自己去敲一遍比较好哦

  • 第一个思路不够优,因为不做过多分析。我们主要掌握第二个思路
/*
// Definition for a Node.
class Node {
public:
    int val;
    Node* next;
    Node* random;
    
    Node(int _val) {
        val = _val;
        next = NULL;
        random = NULL;
    }
};
*/

class Solution {
public:
    Node* copyRandomList(Node* head) {
        //1、把复制的结点插入到原结点后
        Node* cur = head;
        while(cur)
        {
            Node* copy = (Node *)malloc(sizeof(Node));
            copy->val = cur->val;

            //插入结点
            Node* nextNode = cur->next;
            cur->next = copy;
            copy->next = nextNode;

            //迭代
            cur = nextNode;
        }

        //2、设置拷贝结点的random值
        cur = head;
        while(cur)
        {
            Node* copy = cur->next;
            if(cur->random == NULL)
                copy->random = NULL;
            else
                //拷贝结点的随机指针即为原结点的随机指针所指向结点的下一个结点
                copy->random = cur->random->next;
            //迭代(移动两步)
            cur = copy->next;
        }

        //3、提取复制后的链表恢复原链表
        cur =  head;
        Node* copyHead, *copyTail;
        copyHead = copyTail = NULL;
        while(cur)
        {
            Node* copy = cur->next;
            Node* nextNode = copy->next;

            //恢复原链表的指向
            cur->next = nextNode;

            //尾插逻辑
            if(copyTail == NULL)
            {
                copyHead = copyTail = copy;
            }
            else
            {
                copyTail->next = copy;
                copyTail = copyTail->next;
            }
            cur = nextNode;
        }

        return copyHead;
    }
};
  • 可以看到,这个思路的代码量确实是很大,可能你听懂了我的思路,但若是你自己去画图分析再写代码,是需要耗费一定时间的,如果你可以独立地分析写出来,说明你链表部分掌握的还算是不错了,可以进入下一关🚪

四、总结与提炼

  • 最后我们来总结一下本文所介绍的内容,本文讲解的是一道力扣中有关链表章节比较综合性的题目,一共有两个思路,第一个思路的复杂度较高,不够优,因此我们了解一下即可;第二个思路的解法比较优一些,但是实现起来比较繁琐,需要你对指针这一块有着很熟练的掌握。
  • 本题考察得比较综合,若是还觉得吃力的小伙伴应该再回过头去看看一些没搞懂知识点,正确做到对链表对一块能够熟练掌握,因为这是我们【数据结构】中最重要,也是最基础的一块,为后续的二叉树、红黑树、哈希表等复杂数据结构打下坚实的基础🛡

以上就是本文所要描述的所有内容,感谢您对本文的观看,如有疑问请于评论区留言或者私信我都可以🍀

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/17826.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

计算机毕业设计ssm+vue+elementUI基于html的戒烟网站

项目介绍 大量研究证据表明&#xff0c;戒烟可降低或消除吸烟导致的健康危害。任何人在任何年龄戒烟均可获益&#xff0c;且戒烟越早、持续时间越长&#xff0c;健康获益就越大。随着时代发展人们对健康也越来越重视&#xff0c;更多的人参与到了戒烟的行列中来&#xff0c;本…

React环境搭建

目录 1.React环境搭建 2.React项目结构 3.React优点和缺点 1.声明式设计 2.高效-React通过对DOM的模拟(创建虚拟DOM)&#xff0c;最大限度地减少DOM操作(底层是Diff算法) 3.单向响应的数据流 4.组件化开发-复用和便于维护 5.缺点&#xff1a;React本身能做的事并不多&am…

手机实时预览vscode写的html页面

一、vscode安装Live Server插件 扩展&#xff08;CtrlShiftX&#xff09;搜索Live Server进行安装 二、设置电脑IP &#xff08;1&#xff09;winR 打开终端输入&#xff1a;ipconfig &#xff08;2&#xff09;找到SDN 服务器 &#xff08;3&#xff09;设置IPv4窗口 高级…

为什么WinXP SP2有时候会忘记CD自动播放的设置?

实际上&#xff0c;它并没有忘记&#xff0c;它只是想和你再次确认一下。 这是 Windows XP2 负责开发 CD 自动播放组件的工程师和我说的。 在 Windows XP 中&#xff0c;处理 CD 自动播放时会有两个问题。 第一&#xff0c;当你安装了一个新的可以处理 CD 自动播放的应用程序…

分库分表利器——shardingJdbc

一、分库分表方式 1.1 垂直切分 1.1.1 垂直分表&#xff08;拆分字段——但每张表的数据量是不变的&#xff09; 把一张表的一部分字段存在一张新表里面&#xff0c;将另一部分字段存在另一张新表中 1.1.2 垂直分库&#xff08;专库专表&#xff09; 把单一数据库按照业务…

一篇文章带你学完mysql的DQL查询操作

目录 DQL简介 具体操作 数据准备 简单查询 运算符 条件查询 排序查询 聚合查询 null值的处理 分组查询 分页查询 insert into select语句 总结 DQL简介 概念&#xff1a;DQL&#xff08;data query language&#xff09;数据查询语言 select操作 排序规则&…

关于Excel自动换行,不会在西文单词中间换行的问题

工作上遇到了一个Excel中换行的问题&#xff0c;就是使用了Excel的默认自动换行后&#xff0c;如果一个单词很长&#xff0c;那么一般情况下是不会在单词中间换行的。在网上查了些资料&#xff0c;最终找到了一个不算太完美的方法。 结果 就是使用vba修改单元格里面的内容&am…

SpringBoot SpringBoot 开发实用篇 4 数据层解决方案 4.11 SpringBoot 整合 MongoDB

SpringBoot 【黑马程序员SpringBoot2全套视频教程&#xff0c;springboot零基础到项目实战&#xff08;spring boot2完整版&#xff09;】 SpringBoot 开发实用篇 文章目录SpringBootSpringBoot 开发实用篇4 数据层解决方案4.11 SpringBoot 整合 MongoDB4.11.1 SpringBoot 整…

Redis 到底是单线程还是多线程呢?

前言 Redis是高性能分布式缓存常用中间件&#xff0c;我们经常说Redis是单线程的&#xff0c; 也有人说Redis在6.0版本采用了多线程&#xff0c;那么Redis到底是采用单线程呢&#xff1f;还是多线程&#xff1f; 通常说 Redis 是单线程&#xff0c;其实主要是指 Redis 对外提供…

CNCF基金会成员的分类

CNCF简介 2015年&#xff0c;谷歌与Linux基金会及众多行业合作伙伴一起建立了一个云原生计算基金会&#xff08;CNCF&#xff0c;Cloud Native Computing Foundation&#xff09;。CNCF旨在创建并推动一个新的计算范式&#xff0c;这个范式的目的是增强现代分布式系统&#xf…

dataset.py篇

dataset.py 目录&#xff1a; 前言观察数据书写代码函数解释 前言 在步骤中需要写自己的dataset类&#xff0c;并将label和image一一对应后返回。 观察数据 在书写dataset前最重要的就是要观察数据集&#xff0c;对数据集进行分析&#xff0c;比如了解图片大小&#xff0c…

maven基础入门

maven 1、maven简介 Apache Maven 是一个项目管理和构建工具&#xff0c;它基于项目对象模型(POM)的概念&#xff0c;通过一小段描述信息来管理项目的构建、报告和文档。官网 &#xff1a;http://maven.apache.org/什么是Maven&#xff1f;这里先引用知乎的一个回答 我先不说…

第五届“传智杯”全国大学生计算机大赛(练习赛) [传智杯 #5 练习赛] 复读

[传智杯 #5 练习赛] 复读 题目描述 给定若干个字符串&#xff0c;不定数量&#xff0c;每行一个。有些字符串可能出现了多次。如果读入一个字符串后&#xff0c;发现这个字符串以前被读入过&#xff0c;则这个字符串被称为前面相同的字符串的复读&#xff0c;这个字符串被称为…

Redis分布式锁剖析和几种客户端的实现

1. 背景 在传统的单体项目中&#xff0c;即部署到单个IIS上&#xff0c;针对并发问题&#xff0c;比如进销存中的出库和入库问题&#xff0c;多个人同时操作&#xff0c;属于一个IIS进程中多个线程并发操作的问题&#xff0c;这个时候可以引入线程锁lock/Monitor等&#xff0c;…

信息论随笔(三)交互信息量

之前讨论了一个事件的自信息量&#xff0c;但是实际情况下往往有多个事件发生&#xff0c;而且这些事件之间相互是有联系的。比如知道一个人踢足球&#xff0c;那么这个人很有可能会看世界杯。也就是说&#xff0c;我们可以通过一个事件获得另外一个事件的信息&#xff0c;或者…

解决Android Studio等开发软件出现更新TKK失败的两种方案

解决Android Studio等开发软件出现更新TKK失败的两种方案方案一 配置hosts1. 配置域名与IP2.扫描国内可用的IP方案二 替换翻译引擎百度翻译引擎在Android Studio等开发软件中利用Translation等翻译插件时&#xff0c;出现无法翻译的提示&#xff1a;更新TKK失败&#xff0c;请检…

数据结构之栈的实现及相关OJ题

&#x1f57a;作者启明星使 &#x1f383;专栏&#xff1a;《数据库》《C语言》 &#x1f3c7;分享一句话&#xff1a; 对的人会站在你的前途里 志同道合的人才看得懂同一片风景 大家一起加油&#x1f3c4;‍♂️&#x1f3c4;‍♂️&#x1f3c4;‍♂️ 希望得到大家的支持&am…

【毕业设计】新闻分类系统 - 深度学习 机器学习

文章目录0 前言1 简介2 参与及比较算法3 先说结论4 实现过程4.1 数据爬取4.2 数据预处理5 CNN文本分类6 最后0 前言 &#x1f525; Hi&#xff0c;大家好&#xff0c;这里是丹成学长的毕设系列文章&#xff01; &#x1f525; 对毕设有任何疑问都可以问学长哦! 这两年开始&a…

事件总线EventBus

事件总线是对发布-订阅模式的一种实现&#xff0c;是一种集中式事件处理机制&#xff0c;允许不同的组件之间进行彼此通信而又不需要相互依赖&#xff0c;达到一种解耦的目的。 什么是“总线”&#xff1a;一个集中式的事件处理机制。同时服务多个事件和多个观察者。相当于一个…

C#编程深入研究变量,类型和方法

编写正确的C#代码 简单的调试技术 变量的语法 声明类型和值 仅声明类型 访问修饰符 使用类型 通用内置类型 类型转换 推断式声明 自定义类型 类型综述 命名变量 变量的作用域 运算符 定义方法 指定参数 指定返回值 常见的Unity方法 Start方法 Update方法 …