双向链表的实现(增删查改)——最好理解的链表

2023-09-17 20:37:35

一,双向链表的特点

这里的双向链表就是单链表的升级版,链表有的共性他也有感兴趣的可以先看看单链表的内容链接: link

二,双向链表的结构

typedef int LTDataType;
typedef struct ListNode
{
	struct ListNode* prev;
	LTDataType date;
	struct ListNode* next;
}ListNode;

这里还是老生常谈,在写比较大一些的代码的时候我们还是要重新定义一下数据类型这样方便我们以后可以更方便的使用各种数据类型。
至于双向链表,就是它有两个指针既可以指向前面的数据也可以指向后面的数据,所以这里定义的时候就分成了前指针prev和后指针next,这里贴一张图方便理解双向链表。
在这里插入图片描述

三,双向链表的内容实现

// 创建返回链表的头结点.
ListNode* BuyLTNode(LTDataType x);
//初始化
ListNode* LTInit();
// 双向链表销毁
void ListDestory(ListNode* pHead);
// 双向链表打印
void ListPrint(ListNode* pHead);
// 双向链表尾插
void ListPushBack(ListNode* pHead, LTDataType x);
// 双向链表尾删
void ListPopBack(ListNode* pHead);
// 双向链表头插
void ListPushFront(ListNode* pHead, LTDataType x);
// 双向链表头删
void ListPopFront(ListNode* pHead);
// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x);
// 双向链表删除pos位置的节点
void ListErase(ListNode* pos);

这里还要提醒一下,我们在写代码对我们的函数命名的时候尽量是按照它对应的英文单词取进行命名,这样有助于提升我们的代码质量和可读性。

3.1创建node节点

//创建节点
ListNode* BuyLTNode(LTDataType x)
{
	ListNode* node = (ListNode*)malloc(sizeof(ListNode));
	if (node == NULL)
	{
		perror("malloc failed");
		exit(-1);
	}
	node->prev = NULL;
	node->date = x;
	node->next = NULL;

	return node;
}

3.2初始化

//初始化
ListNode* LTInit()
{
	ListNode* phead = BuyLTNode(0);
	phead->next = phead;
	phead->prev = phead;
	return phead;
}

初始化是所有的开始,我们前面的顺序表,单链表都有进行初始化,后边我们要学习的栈和队列也是要有初始化的。

3.3打印

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

打印这里是为了方便我们进行调试代码的。

3.4插入

这里我们的双向链表的插入一共分为了三种插入分为是尾插,头插,以及在特定位置下插入

3.4.1尾插

//尾插
void ListPushBack(ListNode* pHead, LTDataType x)
{
	ListNode* newnode = BuyLTNode(x);
	ListNode* tail = pHead->prev;
	newnode->prev = tail;
	tail->next = newnode;
	newnode->next = pHead;
	pHead->prev = newnode;
}

在这里插入图片描述
这里我们需要理解原本d3作为尾它的next是指向d1的,而d1的prev是指向d3的所以我们先记录下d1的prev的数据然后再把newnode的prev指向d3也就是tail,然后tail的next就指向的newnode,接着newnode的next就指向的头,头的prev就指向了newnode的尾。

3.4.2头插

//头插
void ListPushFront(ListNode* pHead, LTDataType x)
{
	assert(pHead);
	ListNode* newnode = BuyLTNode(x);
	newnode->next = pHead->next;
	pHead->next->prev = newnode;
	pHead->next = newnode;
	newnode->prev = pHead;
}

在这里插入图片描述

我们还是画图分析,因为是头插,我们newnode的next就指向了phead的下面一个位置也就是d2,接着d2的prev就要指向newnode,头的next的位置就指向了newnode,newnode的prev就指向了头。

3.4.3在pos位置上插入

//在pos前面插入
void ListInsert(ListNode* pos, LTDataType x)
{
	assert(pos);
	ListNode* prevpos = pos->prev;
	ListNode* newnode = BuyLTNode(x);
	newnode->next = pos;
	pos->prev = newnode;
	newnode->prev = prevpos;
	prevpos->next = newnode;
}

这里我们就不具体分析了,我们重点说一下这里的技巧,在双向链表的插入中我们可以看到其实头插和尾插都是在指定位置上插入的特例,所以我们在写双向链表的插入的时候完全可以先写这个函数然后头插和尾插也就完成了,这里我们展示一下改造后的头尾插。

//尾插
void ListPushBack(ListNode* pHead, LTDataType x)
{
	ListInsert(pHead->prev, x);
}
//头插
void ListPushFront(ListNode* pHead, LTDataType x)
{
	ListInsert(pHead->next, x);
}

怎么样是不是非常的方便,那么举一反三我们在后面的删除中叶有这样的技巧。

3.5删除

3.5.1尾删

//尾删
void ListPopBack(ListNode* pHead)
{
	assert(pHead);
	assert(pHead->next);
	ListNode* tail = pHead->prev;
	ListNode* prevtail = tail->prev;
	prevtail->next = pHead;
	pHead->prev = prevtail;
	free(tail);
}

在这里插入图片描述
看图分析,我们删除了tail,记录前一个的位置也就是prevtail,让prevtail和phead再次变成循环就可以了。

3.5.2头删

//头删
void ListPopFront(ListNode* pHead)
{
	assert(pHead);
	assert(pHead->next);
	ListNode* first = pHead->next;
	ListNode* second = first->next;
	free(first);
	second->prev = pHead;
	pHead->next = second;
}

头删也时同样的记录下一个的数据在跟前面链接free掉第一个数据。

3.5.3删除pos位置上的数据

//删除pos的数据
void ListErase(ListNode* pos)
{
	assert(pos);
	ListNode* nextpos = pos->next;
	ListNode* prevpos = pos->prev;
	free(pos);
	prevpos->next = nextpos;
	nextpos->prev = prevpos;
}

同样的,尾删和头删也是包括在这一种删除中的,所以我们也可以进一步简化我们的代码。

//尾删
void ListPopBack(ListNode* pHead)
{
	ListErase(pHead->prev);
}
//头删
void ListPopFront(ListNode* pHead)
{
	ListErase(pHead->next);
}

四,调试技巧(具体示例)

我们在写大型的代码的时候有一种调试方法可以极大的简便的让我们去检查代码就是写一查一
例如我们写了尾插的代码。

void Text1()
{
	ListNode* plist = LTInit();
	ListPushBack(plist, 1);
	ListPushBack(plist, 2);
	ListPushBack(plist, 3);
	ListPushBack(plist, 4);
	ListPushBack(plist, 5);
	ListPrint(plist);
}
int main()
{
	Text1();
	return 0;
}

在这里插入图片描述

我们就可以用Text1去检查我们的尾插有没有错误。
同样的我们写完头插之后再写一个Text2去检查我们的函数有没有错误。把一个复杂的大型代码去分解为一个个小的单元会让我们的效率提升很多。

五,总结

双向链表虽然是单链表之后的内容,但是我们会发现因为有两个指针的原因他的所有的操作比单链表比起来更加的方便,更易于理解。总之,还是要自己上手操作才能加深自己的印象。数据结构重点就是:画图!!画图!!画图!!

更多推荐

JavaScript事件流:深入理解事件处理和传播机制

🎬岸边的风:个人主页🔥个人专栏:《VUE》《javaScript》⛺️生活的理想,就是为了理想的生活!目录引言1.事件流的发展流程1.1传统的DOM0级事件1.2DOM2级事件和addEventListener方法1.3W3CDOM3级事件1.4React与VirtualDOM2.事件流的属性2.1事件捕获阶段2.

《从菜鸟到大师之路 Redis 篇》

《从菜鸟到大师之路Redis篇》(一):Redis基础理论与安装配置Nosql数据库介绍是一种非关系型数据库服务,它能解决常规数据库的并发能力,比如传统的数据库的IO与性能的瓶颈,同样它是关系型数据库的一个补充,有着比较好的高效率与高性能。专注于key-value查询的redis、memcached、ttserver。

transformer大语言模型(LLM)部署方案整理

说明大模型的基本特征就是大,单机单卡部署会很慢,甚至显存不够用。毕竟不是谁都有H100/A100,能有个3090就不错了。目前已经有不少框架支持了大模型的分布式部署,可以并行的提高推理速度。不光可以单机多卡,还可以多机多卡。我自己没啥使用经验,简单罗列下给自己备查。不足之处,欢迎在评论区指出。框架名称出品方开源地址Fa

JWT 令牌撤销:中心化控制与分布式Kafka处理

【squids.cn】全网zui低价RDS,免费的迁移工具DBMotion、数据库备份工具DBTwin、SQL开发工具等令牌对于安全数字访问至关重要,但如果您需要撤销它们怎么办?尽管我们尽了最大努力,但有时代币可能会被泄露。这可能是由于编码错误、意外记录、零日漏洞和其他因素造成的。令牌撤销是现代安全性的一个重要方面,确

并发编程——synchronized

文章目录原子性、有序性、可见性原子性有序性可见性synchronized使用synchronized锁升级synchronized-ObjectMonitor原子性、有序性、可见性原子性数据库事务的原子性:是一个最小的执行的单位,一次事务的多次操作要么都成功,要么都失败。并发编程的原子性:一个或多个指令在CPU执行过程

【无公网IP内网穿透】 搭建Emby媒体库服务器并远程访问「家庭私人影院」

目录1.前言2.Emby网站搭建2.1.Emby下载和安装2.2Emby网页测试3.本地网页发布3.1注册并安装cpolar内网穿透3.2Cpolar云端设置3.3Cpolar内网穿透本地设置4.公网访问测试5.结语1.前言在现代五花八门的网络应用场景中,观看视频绝对是主力应用场景之一,加上移动网络技术的发展,随时随地

vue3.2+ts封装axios

1.创建utils文件夹/server下面创建index.ts,代码如下:importaxios,{AxiosRequestConfig}from"axios";import{BASE_URL,TIMEOUT}from"@/config/axios";/***@说明接口请求返回信息(按照自己的实际情况分配基础请求格式)

解决WSL2占用内存过多问题(Docker on WSL2: VmmemWSL)

解决WSL2占用内存过多问题(DockeronWSL2:VmmemWSL)一、问题描述二、问题解决2.1创建`.wslconfig`文件2.2重启wsl2一、问题描述安装完WSL2后,又安装了Docker,使用了一段时间,发现电脑变卡,进一步查看,发现CPU和内存占用过大,如下图:docker仅仅运行了mysql和zk

【LLM】Prompt tuning大模型微调实战

noteprompttuning可看做是prefixtuning的简化版本,在输入层加入prompttokens,并不需要加入MLP进行调整来解决难训练的问题,作者实验表明随着预训练模型参数量的增加,prompttuning效果逼近finetuning效果文章目录note一、Propmttuning1.peft库中的t

热点探测技术架构设计与实践

1.概述说到热点问题,首先我们先理解一下什么是热点?热点通常意义来说,是指在一段时间内,被广泛关注的物品或事件,例如微博热搜,热卖商品,热点新闻,明星直播等等,所以热点产生主要包含2个条件:1.有限时间,2流量高聚。而在互联网领域,热点又主要分为2大类:1.有预期的热点:比如在电商活动当中推出的爆款联名限量款的商品,又

软件机器人财务报表信息的采集和录入、抵押贷款信息查询助力银行贷款业务管理

随着科技的飞速发展,自动化的应用场景也越来越广泛。博为小帮软件机器人的出现,无疑为众多行业带来了巨大的转变,其中就包括银行贷款业务。软件机器人是一种可以模拟人类行为,自动化执行高重复性任务。银行业务中,许多重复性高、规则明确的工作,如企业客户财务报表信息的采集和录入、抵质押贷款的抵质押物信息查询等,正适合软件机器人的应

热文推荐