双向带头循环链表+OJ题讲解

news2025/1/11 7:10:41

💓博主个人主页:不是笨小孩👀
⏩专栏分类:数据结构与算法👀 刷题专栏👀 C语言👀
🚚代码仓库:笨小孩的代码库👀
⏩社区:不是笨小孩👀
🌹欢迎大家三连关注,一起学习,一起进步!!💓

在这里插入图片描述

复杂链表+OJ

  • 链表的分类
  • 带头双向循环链表的实现
    • 结构
    • 接口有哪些呢?
      • 初始化
      • 打印链表
      • 尾插
      • 头插
      • 尾删
      • 头删
      • 查找
      • 在pos前面插入
      • 删除pos位置的数据
      • 销毁链表
  • 链表和顺序表的区别
  • 用随机指针复制列表

链表的分类

实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:

1.单向or双向
在这里插入图片描述

2.带头or不带头(哨兵位头结点)
在这里插入图片描述

3.循环or不循环

在这里插入图片描述

它们组合起来一共有8种结构,虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构:

1.无头单向非循环链表
在这里插入图片描述
这个链表我们前面已经讲过了,想详细了解的可以去看单链表详解。

2.带头双向循环链表
在这里插入图片描述
这个是我们本章节的重点,下面细讲。

总结:

1.无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结
构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
2.带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都
是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带
来很多优势,实现反而简单了,后面我们代码实现了就知道了。

带头双向循环链表的实现

结构

既然是双向,那么这个结构体种就需要两个指针,一个用来存储下一个节点的指针,一个用来存储上一个节点的指针。

typedef int LTDateType;

typedef struct ListNode
{
	//存储数据
	LTDateType date;
	//保存下一个节点的地址
	struct ListNode* next;
	//保存上一个节点的地址
	struct ListNode* prev;
}ListNode;

接口有哪些呢?

// 创建返回链表的头结点.(初始化)
ListNode* ListCreate();

// 双向链表销毁
void ListDestory(ListNode* pHead);

// 双向链表打印
void ListPrint(ListNode* pHead);

// 双向链表尾插
void ListPushBack(ListNode* pHead, LTDateType x);

// 双向链表尾删
void ListPopBack(ListNode* pHead);

// 双向链表头插
void ListPushFront(ListNode* pHead, LTDateType x);

// 双向链表头删
void ListPopFront(ListNode* pHead);

// 双向链表查找
ListNode* ListFind(ListNode* pHead, LTDateType x);

// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDateType x);

// 双向链表删除pos位置的节点
void ListErase(ListNode* pos);

接下来我们来一一实现:

初始化

我们这里讲的是带头双向循环链表,所以我们在初始化的时候由于只有一个哨兵位头结点,所以我们直接让他的next和prev先指向自己,里面的数据可以是任何数。

// 创建返回链表的头结点.
ListNode* ListCreate()
{
	ListNode* node = (ListNode*)malloc(sizeof(ListNode));
	//判断是否开辟成功
	if (node == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	//初始化
	node->next = node;
	node->prev = node;
	node->date = 0;
	return node;
}

打印链表

打印链表很简单只需要遍历链表就可以了,但是这里的头结点是哨兵位不需要遍历,所以我们从phead的next开始,由于他是循环的,所以到phead结束。

// 双向链表打印
void ListPrint(ListNode* pHead)
{
	assert(pHead);
	ListNode* cur = pHead->next;
	while (cur != pHead)
	{
		printf("%d->", cur->date);
		cur = cur->next;
	}
	printf("\n");
}

尾插

由于插入数据我们都需要开辟新的节点,所以我们下一个函数专门来创建节点:

ListNode* BuyListNode(int x)
{
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	//节点里面的指针可以初始化为NULL,因为他们暂时无任何指向。
	newnode->date = x;
	newnode->next = NULL;
	newnode->prev = NULL;
	return newnode;
}

有了节点以后,我们可以利用pHead的prev找到尾,并保存在tail中,然后开始改变链接关系,让tail->next指向newnode,newnode->prev指向tail,然后将pHead->prev指向newnode,newnode->next指向pHead。在没有节点的情况下同样使用,这就体现了结构的优势。

// 双向链表尾插
void ListPushBack(ListNode* pHead, LTDateType x)
{
	assert(pHead);
	ListNode* newnode = BuyListNode(x);
	ListNode* tail = pHead->prev;
	tail->next = newnode;
	newnode->next = pHead;
	pHead->prev = newnode;
	newnode->prev = tail;
}

头插

头插同样需要新的节点,有了新的节点以后,我们保存pHead->next到first中去,然后改变链接关系,将first->prev指向newnode,将newnode->next指向first,将newnode的prev指向pHead,将pHead的next指向newnode。

// 双向链表头插
void ListPushFront(ListNode* pHead, LTDateType x)
{
	assert(pHead);
	ListNode* newnode = BuyListNode(x);
	newnode->prev = pHead;
	newnode->next = pHead->next;
	pHead->next->prev = newnode;
	pHead->next = newnode;
}

尾删

尾删我们可以先保存一下尾节点,在保存尾节点的前一个节点,然后释放尾节点,在改变链接关系,将新的尾节点的next指向pHead,将pHead的prev指向新的尾。

// 双向链表尾删
void ListPopBack(ListNode* pHead)
{
	assert(pHead);
	//链表为空就别删了
	assert(pHead->next != pHead);
	//保存尾节点
	ListNode* tail = pHead->prev;
	//保存尾节点的前一个节点
	ListNode* tailPrev = tail->prev;
	//释放尾节点
	free(tail);
	//改变链接关系
	tailPrev->next = pHead;
	pHead->prev = tailPrev;
}

头删

我们保存第一个节点,也就是pHead->next到first中去,然后改变链接关系,将pHead->next改为first->next,然后将first后面的一个节点的prev,也就是first->next->prev改为pHead,然后释放first即可。

// 双向链表头删
void ListPopFront(ListNode* pHead)
{
	assert(pHead);
	assert(pHead->next != pHead);
	ListNode* first = pHead->next;
	pHead->next = first->next;
	first->next->prev = pHead;
	free(first);
}

查找

查找和打印的方法一样遍历链表,找到就返回该节点,找不到返回NULL。

// 双向链表查找
ListNode* ListFind(ListNode* pHead, LTDateType x)
{
	ListNode* cur = pHead->next;
	while (cur != pHead)
	{
		if (cur->date == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

在pos前面插入

我们可以先保存pos前面的的节点到posPrev中去,在开辟一个新的节点,然后改变链接关系,将posPrev->next改为newnode,然后将newnode->prev指向posPrev,然后将newnode->next指向pos,最后将pos->prev指向newnode即可。

// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDateType x)
{
	assert(pos);
	ListNode* posPrev = pos->prev;
	ListNode* newnode = BuyListNode(x);
	posPrev->next = newnode;
	newnode->prev = posPrev;
	newnode->next = pos;
	pos->prev = newnode;
}

删除pos位置的数据

删除就更简单了,只需要将pos前一个节点的next指向pos的next,将pos的下一个节点的prev指向pos的prev,然后释放pos即可。

// 双向链表删除pos位置的节点
void ListErase(ListNode* pos)
{
	assert(pos);
	pos->prev->next = pos->next;
	pos->next->prev = pos->prev;
	free(pos);
}

销毁链表

遍历链表,依次销毁,最后销毁头即可。

// 双向链表销毁
void ListDestory(ListNode* pHead)
{
	ListNode* cur = pHead->next;
	while (cur != pHead)
	{
		ListNode* del = cur;
		cur = cur->next;
		free(del);
	}
	free(pHead);
}

双向带头循环链表到这里基本上就讲完了,它的结构复杂,但是代码写起来比起单链表还是非常简单的,接下来我们来比较一下链表和顺序表的区别。

链表和顺序表的区别

在这里插入图片描述

下图是缓存的一些知识:

在这里插入图片描述

用随机指针复制列表

在这里插入图片描述

如果我们单纯的拷贝一个链表,那非常简单,只需要遍历原链表,在遍历的同时开辟新的节点,存储原链表的值,然后将新的节点尾插起来就可以了。
但是这个题目出除了需要拷贝这个以外,每个结构中还有一个random指针,他们在原链表中指向原链表的任意节点或者空,我们还要拷贝这个,但是问题是我们在新的链表中无法找到原链表中random所对应的新的链表中的地址,所以我们原链表中找random的同时,还有在新的链表中找对应的random,这是比较麻烦的,而且时间复杂度是O(N^2),效率也低。

这里我们还有另外一个方法,就是我们先将每个节点都拷贝一份,放个拷贝的节点放在对应节点的后面,将他们重新链接起来,我们的问题就是找不到random对应的节点的地址,这样我们的节点的next即使random对应节点的地址,然后我们将拷贝的链表摘下来,尾插起来形成新的链表,同时恢复原链表,这样就可以了,这样做时间复杂度是O(N),而且没有额外的空间损耗,是个非常厉害的思路。

第一步:
在这里插入图片描述

第二步;
改变拷贝链表的random的值,他就是原链表的random的next。
在这里插入图片描述

第三步
就是恢复链表,并且将拷贝的拿来尾插成新的链表。

代码如下:

struct Node* copyRandomList(struct Node* head) 
{
	struct Node* cur = head;
    //第一步插入copy
    while(cur)
    {
        struct Node* next = cur->next;
        struct Node* copy = (struct Node*)malloc(sizeof(struct Node));

        //拷贝
        copy->val = cur->val;

        //改变链接关系
        cur->next = copy;
        copy->next = next;

        //cur迭代
        cur = copy->next;
    }
    cur = head;
    //第二步,改变copy的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,*copytail = NULL;
    while(cur)
    {
        struct Node* copy = cur->next;
        struct Node* next = copy->next;

        //尾插copy
        if(copyhead==NULL)
        {
            copyhead=copytail=copy;
        }
        else
        {
            copytail->next = copy;
            copytail = copytail->next;
        }

        //恢复原链表
        cur->next = next;
        cur = next;       
    }
    return copyhead;
}

今天的分享就到这里,感谢大家的关注和支持。

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

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

相关文章

前端渲染数据

在前端对接受后端数据处理后返回的接收值的时候&#xff0c;为了解决数据过于庞大&#xff0c;而对数据进行简化处理例如性别&#xff0c;经常会使用1&#xff0c; 0这俩个来代替文字的男&#xff0c;女。以下就是前端渲染的具体实现。 以下是部分代码 <el-table-columnpr…

维深(Wellsenn):2023中国消费端VR内容开发商调研报告(附下载

关于报告的所有内容&#xff0c;公众【营销人星球】获取下载查看 核心观点 国内互联网大厂商入局VR&#xff0c;字节跳动、网易表态明确。字节跳动2021年收购国内头部VR硬件厂商PICO后&#xff0c;加速构建VR内容生态&#xff0c;2021年 成立海南创见未来当前已推出VR视频应用…

fabric.js里toDataURL后,画布内容展示不全?

复现场景&#xff1a; 用fabric生成画布后&#xff0c;转成图片&#xff0c;然后直接在浏览器里打开&#xff0c;画布展示内容缺失 画布原图&#xff1a; toDataURL后链接在浏览器打开&#xff1a; 原因解析&#xff1a; base64链接太长&#xff0c;输入浏览器链接被截断&…

HOT79-跳跃游戏 II

leetcode原题链接&#xff1a;跳跃游戏 II 题目描述 给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 nums[0]。 每个元素 nums[i] 表示从索引 i 向前跳转的最大长度。换句话说&#xff0c;如果你在 nums[i] 处&#xff0c;你可以跳转到任意 nums[i j] 处: 0 < j &…

VUE+view table.exportCsv()导出.csv文档时如何防止数据格式为科学计数

当使用table.exportCsv()方法导出数据时&#xff0c;出现科学计数法问题&#xff0c;像电话号码&#xff0c;身份证号码等&#xff0c;当数据大于15位后面的会用0替代。 针对这一问题&#xff0c;解决方法如下&#xff1a;就是再数字前加上制表符“\t”注意双引号&#xff0c;…

设计模式-简单工厂模式(静态工厂模式)java实现

介绍 简单工厂模式根据所提供的参数数据返回几个可能类中的一个类的实例。通常返回的类都有一个公共的父类和公共的方法。 意图 提供一个类&#xff0c;负责根据一定的条件创建某一具体类的实例。同时使用工厂模式也是为了隐藏创建对象的过程 角色及其职责 (1)工厂(Creator…

机械工业信息研究院:2023年中国生物制药行业报告(附下载)

关于报告的所有内容&#xff0c;公众【营销人星球】获取下载查看 核心观点 医药工业宏观情况分析 2021 年生物制药带动医药工业经 济指标大幅增长。根据统计&#xff0c;2021年规 模以上医药工业增加值同比增长 23.1%&#xff0c;增速较上年同期提升 17.2个百分点&#xff0…

MVSnet点云定量评估指标总结

根据MVSnet论文[1]原文说明&#xff0c;点云评估主要从准确性和完整性两个方面来评估。 针对准确性的评估&#xff0c;采用平均距离指标来度量&#xff0c;具体指标分别为Acc、Comp、overall&#xff0c;准确性指标越低越好&#xff0c;表示R与G之间的距离越小&#xff0c;恢复…

《CodeGeeX2 一个让你编码效率翻倍的扩展,分享几个使用小技巧》学习笔记

《CodeGeeX2 一个让你编码效率翻倍的扩展&#xff0c;分享几个使用小技巧》学习笔记 【Only Key Control】使用按键触发提示 使用注释来提升CodeGeeX生成代码的准确性 在函数的顶部添加对函数的说明然后输入function的关键字再使用【Alt /】来触发自动补全 使用CodeGeeX解释…

构建Docker容器监控系统(Cadvisor +Prometheus+Grafana)

Cadvisor PrometheusGrafana 1.1、Cadvisor产品简介 Cadvisor是Google开源的一款用于展示和分析容器运行状态的可视化工具。通过在主机上运行Cadvisor用户可以轻松的获取到当前主机上容器的运行统计信息&#xff0c;并以图表的形式向用户展示。 1.2、安装docker-ce [rootloc…

LeetCode 热题 100 JavaScript--98. 验证二叉搜索树

给你一个二叉树的根节点 root &#xff0c;判断其是否是一个有效的二叉搜索树。 有效 二叉搜索树定义如下&#xff1a; 节点的左子树只包含 小于 当前节点的数。 节点的右子树只包含 大于 当前节点的数。 所有左子树和右子树自身必须也是二叉搜索树。 *** Definition for …

根据渲染数据长度动态渲染后缀图标

在动态获取数据时&#xff0c;想要渲染后面的图标是根据数据的长度渲染图标位置&#xff0c;效果如下&#xff1a; 代码如下&#xff1a; <el-row :gutter"60"><el-col :span"24"><el-form-item><el-input v-model.trim"form…

zuul实现黑名单,request多次读取问题,stream close

一&#xff0c;背景及设计 1.需要在网关实现黑名单功能&#xff0c;实现拦截指定接口。黑名单用户&#xff0c;会加入指定黑名单列表&#xff0c;关联对应功能&#xff0c;如用户登录&#xff0c;用户下单&#xff0c;用户接单。 2.表设计 平台表&#xff1a;不同系统 黑名单…

TimedCache 类的作用和使用

TimedCache 类的作用是实现一个带有过期时间的缓存。它允许存储键值对&#xff0c;并在一定时间后自动删除过期的键值对。使用 TimedCache 可以提高程序的性能&#xff0c;减少对数据库或其他资源的访问次数。 使用 TimedCache 类时&#xff0c;可以按照以下步骤进行操作&…

干货!esp8266+ds3231低功耗解决方案,在特定时间唤醒

最近首次接触esp8266&#xff0c;也是第一次接触硬件&#xff0c;在一个墨水屏日历项目上遇到了低功耗问题&#xff0c;特此记录。 此墨水屏日历不需要一直处于启动状态&#xff0c;我希望它每几小时或者每天启动一次即可。 解决方案 1&#xff1a;仅使用内部 RTC 通过将GPIO…

Android监听设备亮灭屏广播(动态广播代码)

MainActivity中 public class MainActivity extends Activity {private WakeAndLockReceiver wakeAndLockReceiver;Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);//注册亮屏和息…

强化学习研究 PG

由于一些原因&#xff0c; 需要学习一下强化学习。用这篇博客来学习吧&#xff0c; 用的资料是李宏毅老师的强化学习课程。 深度强化学习(DRL)-李宏毅1-8课&#xff08;全&#xff09;_哔哩哔哩_bilibili 这篇文章的目的是看懂公式&#xff0c; 毕竟这是我的弱中弱。 强化…

想要实现高效数据复制?Paxos并不总是最佳选择!

数据复制典型的算法就是Paxo和Raft。 1 分片元数据的存储 分布式存储系统中&#xff0c;收到客户端请求后&#xff0c;承担路由功能的节点&#xff1a; 先访问分片元数据&#xff08;简称元数据&#xff09;&#xff0c;确定分片对应节点然后才访问真正数据 元数据&#xf…

PoseiSwap 开启“Poseidon”池,治理体系或将全面开启

PoseiSwap曾在前不久分别以IDO、IEO的方式推出了POSE通证&#xff0c;但PoseiSwap DEX中并未向除Zepoch节点外的角色开放POSE资产的交易。而在前不久&#xff0c;PoseiSwap推出了全新的“Poseidon”池&#xff0c;该池将向所有用户开放&#xff0c;并允许用户自由的进行质押、交…

Python机器学习实战-建立随机森林模型预测肾脏疾病(附源码和实现效果)

实现功能 建立随机森林模型预测肾脏疾病 实现代码 import pandas as pd import warnings warnings.filterwarnings("ignore") pd.set_option(display.max_columns, 26)#读取数据 df pd.read_csv("E:\数据杂坛\datasets\kidney_disease.csv") dfpd.Data…