【初阶数据结构】——剑指 Offer : 复杂链表(带随机指针)的复制

简介: 【初阶数据结构】——剑指 Offer : 复杂链表(带随机指针)的复制

前言

这篇文章,我们一起来解决一道与链表相关的经典面试题:复杂链表(带随机指针)的复制。134b04e091174f61bf41984bf55090b5.png

1.题目介绍

我们先来一起了解一下这道题:

这道题是《剑指offer》上的一道经典题目:

6dcafd652f314e4eb2881ee2f286fa6b.png

在力扣上也有原题:

链接: link

这篇文章,就给大家详细讲解一下这道题。

我们一起来看一下题目:

2e5591be29934abc849e58ed64360929.png

题目呢,看起来还挺长的。

但是我们不能上去被题目就吓到了,其实这个题目就是让我们复制链表嘛,给我们一个链表,我们要自己再创建一个和它一样的链表就行了嘛。

🆗,那接下来,我们就来一起分析一下这道题。

2. 题目分析

那既然这道题是让我们复制链表的,那我们就先来思考一下应该如何复制?

通过前面的学习,我们已经学会了如果创建一个链表,那复制的话,就是创建一个一模一样的链表嘛。

我们就拿一个题目给出的输入样例来分析一下:

003a33b8f6fd4761948c5ca95d9a0dc9.png



那要复制这样一个链表,是不是好像也不难啊。

它有5个结点,我们就创建的链表也带5个结点就行了嘛,每个结点1个数据域,2个指针域,next指针依次存下一个结点的地址。

对吧,这些操作好像都不难。

那求解这道题的关键之处或者说难点在哪呢?

是不是就麻烦在这个随机指针的问题啊,这是不是比较难搞啊。

每个节点包含一个额外增加的随机指针 random ,该指针可以指向链表中的任何节点或空节点。

我们除了要把链表的连接关系复制出来,每个结点的随机指针指向哪里,我们也要复制出来的。

但是每个结点的随机指针的指向随机的,可能指向空,或者是任意一个结点,那我们要复制随机指针,就必须知道每个结点的随机指针的指向,这就不好搞了。


我们看是很容易看出来的,但是如何让我们的程序面对不同的输入,都能正确找到每一个结点随机指针的指向,并进行复制呢?

3. 思路讲解

思路1

首先思路1就是暴力求解:

复制随机指针的时候,每个复制结点的random指针,我们都要一一去寻找它对应的源结点指向的是第几个结点(如果指向空是比较好搞的),然后让复制结点也指向对应的结点,但是注意不能看它指向的数值是几,因为不同结点数据域的数值可能是一样的。

58f7af3848344c8487f4d4b587809cf0.png

但是这种解法时间复杂度是O(N^2) ,不是太好,我们就不实现了。

接下来我们再讲另外一种比较优的算法。

思路2

这个思路是什么呢?需要我们做三件事情:

  1. 创建拷贝结点链接到源链表每一个结点的后面
  2. ea1c30ed898645e081dc356382d842c2.png
  3. 那这么做有什么目的呢?

这里先不解释,等到下一步操作时我们就知道为什么要这么做了。

那这一步代码要如何实现呢?

那也很简单,循环对链表进行遍历,每次循环都创建一个copy结点,copy结点的数据域的值和源节点相同,然后连接到源结点后面,一次向后直到遍历结束。

  struct Node* cur=head;
    //拷贝结点到源节点后面
    while(cur)
    {
        struct Node* copy=(struct Node*)malloc(sizeof(struct Node));
        copy->val=cur->val;
        //链接
        copy->next=cur->next;
        cur->next=copy;
        //cur往后走
        cur=copy->next;
    }

接下来是第二步:

  1. 设置拷贝结点的random指针域
  2. 9d4d0119898740f29f2853aacb681411.png
  3. 这么设置呢?

如果源结点的random域为空,则拷贝结点的random域也直接置为空;如果不为空,拷贝结点的random域指向 【其对应源节点的random域指向的源节点】的next对应的拷贝结点。

b456ea4b265a4bb288b1108587b72a4e.png

大家可以结合图理解一下这一步的操作。

这样拷贝结点的random指针设置起来是不是就很方便了,这也是我们第一步把拷贝结点链接在源节点后面的原因。

    cur=head;
    //设置拷贝结点的random域
    while(cur)
    {
        struct Node* copy=cur->next;
        if(cur->random==NULL)
        {
            copy->random=NULL;
        }
        else
        {
            copy->random=cur->random->next;
        }
        cur=copy->next;
    }

那接下来就是第3步,也是最后一步了。


第三步:将拷贝结点解绑下来,链接组成最终要返回的拷贝链表

经过前面两步的努力,拷贝链表的所有结点都已经存在了,而且它们的随机指针random也设置好了,那现在我们把所有的拷贝结点从源链表上解下来,再组成一个完整的链表不就完成了吗?当然因为前两步的操作我们改变了原链表,所以最后最好将它还原一下,避免题目测试的时候会检查原链表是否被改变了。

047583d9c56944b8a5799c43b47b30f3.png

那要将所有结点组成链表,我们就可以依次尾插。

这里给不给头结点都可以,我们这里选择不给头结点,不过这样第一个结点尾插进行一次判断就行了。

    //将拷贝结点解下尾插成新链表
    cur=head;
    struct Node* copyhead=NULL;
    struct Node* copytail=NULL;
    while(cur)
    {
        struct Node* copy=cur->next;
        //将原链表还原
        cur->next=copy->next;
        //尾插
        if(copytail==NULL)
        {
            copyhead=copytail=copy;
        }
        else
        {
            copytail->next=copy;
            copytail=copy;
        }
        //cur向后走
        cur=copy->next;
    }

拷贝链表创建好,最好作为函数返回值返回就行了。

4. 分析图及源码展示

ae99e98c01e5463596c4f3ec6255934a.png

源码:

struct Node* copyRandomList(struct Node* head) {
  struct Node* cur=head;
    //拷贝结点到源节点后面
    while(cur)
    {
        struct Node* copy=(struct Node*)malloc(sizeof(struct Node));
        copy->val=cur->val;
        //链接
        copy->next=cur->next;
        cur->next=copy;
        //cur往后走
        cur=copy->next;
    }
    cur=head;
    //设置拷贝结点的random域
    while(cur)
    {
        struct Node* copy=cur->next;
        if(cur->random==NULL)
        {
            copy->random=NULL;
        }
        else
        {
            copy->random=cur->random->next;
        }
        cur=copy->next;
    }
    //将拷贝结点解下尾插成新链表
    cur=head;
    struct Node* copyhead=NULL;
    struct Node* copytail=NULL;
    while(cur)
    {
        struct Node* copy=cur->next;
        //将原链表还原
        cur->next=copy->next;
        //尾插
        if(copytail==NULL)
        {
            copyhead=copytail=copy;
        }
        else
        {
            copytail->next=copy;
            copytail=copy;
        }
        //cur向后走
        cur=copy->next;
    }
    return copyhead;
}

网络异常,图片无法展示
|
🆗,那这道题的讲解就完了,希望能帮助到大家。

如果有写的不好的地方,欢迎大家指正!!!

网络异常,图片无法展示
|
17d7454dce91413e93965f9ed34df1c0.png


目录
相关文章
|
29天前
|
存储 算法 Perl
数据结构实验之链表
本实验旨在掌握线性表中元素的前驱、后续概念及链表的建立、插入、删除等算法,并分析时间复杂度,理解链表特点。实验内容包括循环链表应用(约瑟夫回环问题)、删除单链表中重复节点及双向循环链表的设计与实现。通过编程实践,加深对链表数据结构的理解和应用能力。
53 4
|
22天前
|
存储 缓存 算法
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式,强调了合理选择数据结构的重要性,并通过案例分析展示了其在实际项目中的应用,旨在帮助读者提升编程能力。
43 5
|
29天前
|
存储 人工智能 算法
数据结构实验之C 语言的函数数组指针结构体知识
本实验旨在复习C语言中的函数、数组、指针、结构体与共用体等核心概念,并通过具体编程任务加深理解。任务包括输出100以内所有素数、逆序排列一维数组、查找二维数组中的鞍点、利用指针输出二维数组元素,以及使用结构体和共用体处理教师与学生信息。每个任务不仅强化了基本语法的应用,还涉及到了算法逻辑的设计与优化。实验结果显示,学生能够有效掌握并运用这些知识完成指定任务。
51 4
|
1月前
|
存储 C语言
【数据结构】手把手教你单链表(c语言)(附源码)
本文介绍了单链表的基本概念、结构定义及其实现方法。单链表是一种内存地址不连续但逻辑顺序连续的数据结构,每个节点包含数据域和指针域。文章详细讲解了单链表的常见操作,如头插、尾插、头删、尾删、查找、指定位置插入和删除等,并提供了完整的C语言代码示例。通过学习单链表,可以更好地理解数据结构的底层逻辑,提高编程能力。
76 4
|
1月前
|
算法 安全 搜索推荐
2024重生之回溯数据结构与算法系列学习之单双链表精题详解(9)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构王道第2.3章之IKUN和I原达人之数据结构与算法系列学习x单双链表精题详解、数据结构、C++、排序算法、java、动态规划你个小黑子;这都学不会;能不能不要给我家鸽鸽丢脸啊~除了会黑我家鸽鸽还会干嘛?!!!
|
1月前
|
存储 Web App开发 算法
2024重生之回溯数据结构与算法系列学习之单双链表【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构之单双链表按位、值查找;[前后]插入;删除指定节点;求表长、静态链表等代码及具体思路详解步骤;举例说明、注意点及常见报错问题所对应的解决方法
|
29天前
|
算法
数据结构之购物车系统(链表和栈)
本文介绍了基于链表和栈的购物车系统的设计与实现。该系统通过命令行界面提供商品管理、购物车查看、结算等功能,支持用户便捷地管理购物清单。核心代码定义了商品、购物车商品节点和购物车的数据结构,并实现了添加、删除商品、查看购物车内容及结算等操作。算法分析显示,系统在处理小规模购物车时表现良好,但在大规模购物车操作下可能存在性能瓶颈。
47 0
|
1月前
|
C语言
【数据结构】双向带头循环链表(c语言)(附源码)
本文介绍了双向带头循环链表的概念和实现。双向带头循环链表具有三个关键点:双向、带头和循环。与单链表相比,它的头插、尾插、头删、尾删等操作的时间复杂度均为O(1),提高了运行效率。文章详细讲解了链表的结构定义、方法声明和实现,包括创建新节点、初始化、打印、判断是否为空、插入和删除节点等操作。最后提供了完整的代码示例。
56 0
|
2月前
|
存储
[数据结构] -- 双向循环链表
[数据结构] -- 双向循环链表
25 0
|
2月前
(剑指offer)18、删除链表的节点—22、链表中倒数第K个节点—25、合并两个排序的链表—52、两个链表的第一个公共节点(2021.12.07)
(剑指offer)18、删除链表的节点—22、链表中倒数第K个节点—25、合并两个排序的链表—52、两个链表的第一个公共节点(2021.12.07)
54 0