数据结构——带头双向循环链表(c语言实现)

目录

 

1.单链表和双向链表对比

2.双向链表实现

2.1 创建新节点

2.2 链表初始化 

2.3 尾插 

2.4 头插 

2.5 尾删 

2.6 头删 

2.7 查找 

2.8 指定位置后插入数据 

2.9 删除指定节点 

2.10 销毁链表

2.11 打印链表 


 前言:

     我们在前几期详细地讲解了不带头单向不循环链表(单链表),使用它的底层代码实现了一个简单的通讯录项目,也介绍了链表分为八种,但是其中最常用的只有两种:(1)不带头单向不循环链表,(2)带头双向循环链表,今天我们要讲解的就是第二种带头双向循环链表

1.单链表和双向链表对比

在介绍双向链表之前,我们先来对比一下单链表和双向链表的区别。

这是单链表:

这是双向链表:

 

        双向链表的特点是每相邻两个节点都相互连接,每个节点都有三个部分,包括data,next,prev,其中data负责存放数据,next负责存放后一个节点的地址,prev负责存放前一个节点的地址,最后一个节点和头节点(哨兵位)相互连接,形成了一个循环双向链表,那么什么是哨兵位呢,哨兵位就是双向链表的头节点,它不存放有效数据,只存放第一个有序数据的节点的地址和最后一个有序数据节点的地址。 

       那么它们的区别是什么呢?

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

2.双向链表实现

  介绍完了双向链表的区别我们接下来就要着手开始用代码实现双向链表了。由于代码可能较多,我们将双向链表的代码分成了三个文件,分别是List.h,List.c和tste.c文件:

在list.h文件中,我们要包含我们会用到的头文件,其他文件只要包含List.h文件就可以使用这些头文件了:

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>

双向链表的实现也是在此文件中,由于我们不知道将来使用链表会存放什么样的数据,所以我们使用typedef对这个数据的类型改名,我们实现链表使用的是int类型,所以我们对int改名:

typedef int ListNodeData;

链表中的next和prev链表是用来存放节点地址的,所以它们为指针类型,而为了方便使用,我们将链表使用typedf改名,下面是双向链表实现:

typedef struct ListNode
{
	ListNodeData data;
	struct ListNode* prev;
	struct ListNode* next;

}LTNode;

2.1 创建新节点

创建新节点我们使用malloc从堆申请一块LTNode类型大小的内存,它的data类型用来存放将来要插入的数据,prev和next指针在创建这个节点时先让它指向自己,如果要创建新节点,就调用这个函数,它会返回一个指向这块空间的指针:

LTNode* LTBuyNode(ListNodeData x)
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		exit(1);
	}
	newnode->data = x;
	newnode->next = newnode;
	newnode->prev = newnode;

	return newnode;

}//创建一个新节点

2.2 链表初始化 

 

在最初创建一个链表时,它的内部为空,什么也没有,我们初始化应该此链表让它至少有一个哨兵位:

void LTInit(LTNode** pphead)
{
	assert(pphead);
	*pphead = LTBuyNode(-1);


}//初始化

2.3 尾插 

尾插操作应该先创建一个新节点,插入顺序为:先让新节点的prev指针指向最后一个节点,然后让新节点的next指针指向哨兵位实现循环,以上的两步都不会影响旧节点,接下来就是让最后一个节点的next指针指向这个新节点,然后让哨兵位的prev指针指向新节点完成尾插:

具体实现代码为:

void LTPushBack(LTNode* phead, ListNodeData x)
{
	assert(phead);
	LTNode* newnode = LTBuyNode(x);

	newnode->next = phead;
	newnode->prev = phead->prev;

	phead->prev->next = newnode;
	phead->prev = newnode;
}//尾插

2.4 头插 

     头插操作并不是将新节点放在哨兵位之前,而是将新节点放在第一个有效数据节点之前,所以我们应该将新节点放在哨兵位的后面。先创建一个新节点,让新节点的prev指针指向哨兵位,让它的next指针指向哨兵位的next指向的节点,以上两步不会影响任何节点,做完这两步后,先让哨兵位后面那个节点的prev指针指向新节点,然后让哨兵位的next指针指向新节点,这两步不能调换顺序,否则会找不到哨兵位后面那个节点,下面是代码实现:

void LTPushFront(LTNode* phead, ListNodeData x)
{
	assert(phead);
	LTNode* newnode = LTBuyNode(x);

	newnode->next = phead->next;
	newnode->prev = phead;

	phead->next->prev = newnode;
	phead->next = newnode;

}//头插

2.5 尾删 

   执行删除操作之前我们应该先判断这个链表除哨兵位之外有没有其他节点,如果没有,就无法删除,而尾删操作也比较简单,只需要让尾节点的前一个节点的next指针指向哨兵位,然后让哨兵位的prev指针指向位尾节点,以上过程需要创建一个新变量,否则无法找到我们要删除的节点,接着释放掉尾节点后置空就可以了:

void LTPopBack(LTNode* phead)
{
	assert(phead&&phead->next);
	LTNode* del = phead->prev;

	del->prev->next = del->next;
	del->next->prev = del->prev;
	free(del);
	del = NULL;

}//尾删

画图演示:

 

2.6 头删 

  头删操作与尾删类似,如果没有两个及以上节点的话无法执行删除操作,头删要删除的是哨兵位后面那个节点,所以我们先创建一个指针存放我们要删除节点的地址,将哨兵位的next指针指向我们要删除节点的下一个节点,然后将我们要删除节点的下一个节点的prev指针指向哨兵位,完成这些操作后释放我们要删除的节点然后置空:

void LTPopFront(LTNode* phead)
{
	assert(phead && phead->next);
	LTNode* del = phead->next;

	phead->next = del->next;
	del->next->prev = phead;
	free(del);
	del = NULL;
}//头删

2.7 查找 

如果我们要查找一个节点,应该先判断链表是否为空,然后将我们要查找的节点的数据与链表中节点的数据一一对比,如果数据内容相同,说明找到了,将这个节点返回,如果循环一圈还没有找到,说明链表中不存在这样的节点,返回应一个空指针:

LTNode* LTFind(LTNode* phead, ListNodeData x)
{
	assert(phead && phead->next);
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		if (pcur->data == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	return NULL;
}//查找函数

2.8 指定位置后插入数据 

我们可以先调用查找函数,然后调用它返回的节点,试着在它的后面插入数据,而它的操作与头插非常相像,只是将哨兵位改成了我们指定的节点:

void LTInsertAfter(LTNode* phead, LTNode* pop, ListNodeData x)
{
	assert(phead&&pop);
	LTNode* newnode = LTBuyNode(x);
	newnode->prev = pop;
	newnode->next = pop->next;

	pop->next->prev = newnode;
	pop->next = newnode;
}//指定节点后插入数据

2.9 删除指定节点 

删除节点我们需要先判断链表中是否有两个及以上节点,否则无法删除,删除指定节操作我们先将指定节点的前一个节点的next指向我们指定节点的下一个节点,然后将指定节点的下一个节点的prev指针指向指定节点的前一个节点,这个过程不需要创建中间变量,因为我们有指定节点的地址:

void LTErase(LTNode* phead, LTNode* pop)
{
	assert(phead && phead->next);
	pop->next->prev = pop->prev;
	pop->prev->next = pop->next;

	free(pop);
	pop = NULL;

}//指定删除节点

2.10 销毁链表

我们链表的每一个节点都是使用malloc函数手动在堆上申请的,需要我们手动释放:

void LTDestroy(LTNode* phead)
{
	assert(phead);
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		LTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}

}//销毁链表

2.11 打印链表 

指行这么多插入删除操作我们如果测试的话就使用这个函数打印出来,而打印函数只需要循环打印这个链表一次就可以了:

void LTPrint(LTNode* phead)
{
	assert(phead);
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		printf("%d->", pcur->data);
		pcur = pcur->next;
	}
	printf("NULL\n");

}//打印

接下来我们使用这个函数来测试一下我们的方法:

可以看到我们的方法都没有问题,那么这期的双向链表就到此结束啦,我将代码放在下面,感兴趣的小伙伴可以试试哦。

List.h :

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>

typedef int ListNodeData;
typedef struct ListNode
{
	ListNodeData data;
	struct ListNode* prev;
	struct ListNode* next;

}LTNode;




LTNode* LTFind(LTNode* phead, ListNodeData x);//查找

void LTInit(LTNode** pphead);//初始化

void LTPushBack(LTNode* phead, ListNodeData x);
//尾插

void LTPrint(LTNode* phead);//打印链表

void LTPushFront(LTNode* phead, ListNodeData x);
//头插

void LTPopBack(LTNode* phead);//尾删

void LTPopFront(LTNode* phead);//头删

void LTInsertAfter(LTNode* phead, LTNode* pop, ListNodeData x);
//指定节点后删除

void LTErase(LTNode* phead, LTNode* pop);
//指定位置删除

void LTDestroy(LTNode* phead);
//销毁链表

List.c :

#define _CRT_SECURE_NO_WARNINGS 1
#include"List.h"

LTNode* LTBuyNode(ListNodeData x)
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		exit(1);
	}
	newnode->data = x;
	newnode->next = newnode;
	newnode->prev = newnode;

	return newnode;

}//创建一个新节点

LTNode* LTFind(LTNode* phead, ListNodeData x)
{
	assert(phead && phead->next);
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		if (pcur->data == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	return NULL;
}//查找函数

void LTInit(LTNode** pphead)
{
	assert(pphead);
	*pphead = LTBuyNode(-1);


}//初始化

void LTPushBack(LTNode* phead, ListNodeData x)
{
	assert(phead);
	LTNode* newnode = LTBuyNode(x);

	newnode->next = phead;
	newnode->prev = phead->prev;

	phead->prev->next = newnode;
	phead->prev = newnode;
}//尾插

void LTPushFront(LTNode* phead, ListNodeData x)
{
	assert(phead);
	LTNode* newnode = LTBuyNode(x);

	newnode->next = phead->next;
	newnode->prev = phead;

	phead->next->prev = newnode;
	phead->next = newnode;

}//头插

void LTPopBack(LTNode* phead)
{
	assert(phead&&phead->next);
	LTNode* del = phead->prev;

	del->prev->next = del->next;
	del->next->prev = del->prev;
	free(del);
	del = NULL;

}//尾删

void LTPopFront(LTNode* phead)
{
	assert(phead && phead->next);
	LTNode* del = phead->next;

	phead->next = del->next;
	del->next->prev = phead;
	free(del);
	del = NULL;
}//头删

void LTInsertAfter(LTNode* phead, LTNode* pop, ListNodeData x)
{
	assert(phead&&pop);
	LTNode* newnode = LTBuyNode(x);
	newnode->prev = pop;
	newnode->next = pop->next;

	pop->next->prev = newnode;
	pop->next = newnode;
}//指定节点后插入数据

void LTErase(LTNode* phead, LTNode* pop)
{
	assert(phead && phead->next);
	pop->next->prev = pop->prev;
	pop->prev->next = pop->next;

	free(pop);
	pop = NULL;

}//指定删除节点




void LTPrint(LTNode* phead)
{
	assert(phead);
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		printf("%d->", pcur->data);
		pcur = pcur->next;
	}
	printf("NULL\n");

}//打印

void LTDestroy(LTNode* phead)
{
	assert(phead);
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		LTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}

}//销毁链表

test.c :

#define _CRT_SECURE_NO_WARNINGS 1
#include"List.h"
void test01()
{
	LTNode* plist = NULL;
    LTInit(&plist);
	printf("尾插\n");
	LTPushBack(plist, 1);
	LTPushBack(plist, 2);
	LTPushBack(plist, 3);
	LTPrint(plist);
	printf("头插\n");
	LTPushFront(plist, 0);
	LTPrint(plist);

	/*printf("尾删\n");
	LTPopBack(plist);
	LTPopBack(plist);
	LTPrint(plist);*/
	printf("头删\n");
	LTPopFront(plist);
	LTPopFront(plist);
	LTPrint(plist);

	printf("在3后面插入数据:\n");
	LTNode* Find = LTFind(plist, 3);
	/*if (Find == NULL)
	{
		printf("找不到!\n");
	}
	else
	{
		printf("找到了\n");
	}*/
	LTInsertAfter(plist, Find, 56);
		LTPrint(plist);

		printf("删除指定节点3:\n");
		LTErase(plist, Find);
		LTPrint(plist);
		printf("销毁链表\n");
		LTDestroy(plist);
		plist = NULL;
		
		





}

int main()
{
	test01();
	return 0;
}

 

 

 

 

 

 

 

 

 

 

 

 

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【C#】找不到属性集方法。get只读属性用了反射设置setValue肯定报错

欢迎来到《小5讲堂》 这是《C#》系列文章&#xff0c;每篇文章将以博主理解的角度展开讲解。 温馨提示&#xff1a;博主能力有限&#xff0c;理解水平有限&#xff0c;若有不对之处望指正&#xff01; 背景 找不到属性集方法。get只读属性用了反射设置setValue肯定报错 报错…

OpenCv形态学(一)

目录 形态学转换 结构元素 腐蚀 膨胀 开运算 闭运算 形态学梯度 顶帽 黑帽 图像轮廓 查找轮廓 绘制轮廓 形态学转换 形态变换是一些基于图像形状的简单操作。通常在二值图像上执行。它需要两个输入&#xff0c;一个是我们的原始图像&#xff0c;第二个是决定操作性…

推荐系统(LLM去偏?) | (WSDM24)预训练推荐系统:因果去偏视角

::: 大家好&#xff01;今天我分享的文章是来自威斯康星大学麦迪逊分校和亚马逊AWS AI实验室的最新工作&#xff0c;文章所属领域是推荐系统和因果推理&#xff0c;作者针对跨域推荐中的偏差问题提出了一种基于因果去偏的预训练推荐系统框架PreRec。 ::: 原文&#xff1a;Pre-t…

2024年敏捷开发管理工具10大精选

国内外主流的十大敏捷开发管理系统&#xff1a;PingCode、Tapd、OpenProject、Jira、ClickUp、Monday.com、Wrike、Taiga、Tuleap、Redmine。 敏捷开发已成为软件开发领域的一种标准实践&#xff0c;有效的管理工具是其成功实施的关键。本文将探讨在2024年&#xff0c;哪些敏捷…

迁移学习——CycleGAN

CycleGAN 1.导入需要的包2.数据加载&#xff08;1&#xff09;to_img 函数&#xff08;2&#xff09;数据加载&#xff08;3&#xff09;图像转换 3.随机读取图像进行预处理&#xff08;1&#xff09;函数参数&#xff08;2&#xff09;数据路径&#xff08;3&#xff09;读取文…

基于redisson实现tomcat集群session共享

目录 1、环境 2、修改server.xml 3、修改context.xml 4、新增redisson配置文件 5、下载并复制2个Jar包到Tomcat Lib目录中 6、 安装redis 7、配置nginx负载均衡 8、配置测试页面 9、session共享测试验证 前言&#xff1a; 上篇中&#xff0c;Tomcat session复制及ses…

观测云 VS 开源自建

观测云是一款面向全技术栈的监控观测一体化产品方案&#xff0c;具备强大而丰富的功能&#xff0c;目标是帮助最终用户提升监控观测的能力&#xff0c;化繁为简&#xff0c;轻松的构建起完整的监控观测体系。同时能够帮助整个企业的开发技术团队从统一的观测能力上获得完整的收…

ONLYOFFICE 文档开发者版 8.1:API 更新

随着版本 8.1 新功能的发布&#xff0c;我们更新了编辑器、文档生成器和插件的 API&#xff0c;并添加了 Office API 板块。阅读下文了解详情。 ​ ONLYOFFICE 文档是什么 ONLYOFFICE 文档是一个功能强大的文档编辑器&#xff0c;支持处理文本文档、电子表格、演示文稿、可填写…

探索ChatGPT在程序员日常工作的多种应用

引言 在现代科技迅猛发展的今天&#xff0c;人工智能的应用已经深入到我们生活和工作的各个方面。作为程序员&#xff0c;我们时常面临大量繁杂的任务&#xff0c;从代码编写、错误调试到项目管理和团队协作&#xff0c;每一项都需要花费大量的时间和精力。近年来&#xff0c;…

算法与数据结构——时间复杂度详解与示例(C#,C++)

文章目录 1. 算法与数据结构概述2. 时间复杂度基本概念3. 时间复杂度分析方法4. 不同数据结构的时间复杂度示例5. 如何通过算法优化来提高时间复杂度6. C#中的时间复杂度示例7. 总结 算法与数据结构是计算机科学的核心&#xff0c;它们共同决定了程序的性能和效率。在实际开发中…

大模型产品的“命名经济学”:名字越简单,产品越火爆?

文 | 智能相对论 作者 | 陈泊丞 古人云&#xff1a;赐子千金&#xff0c;不如教子一艺&#xff1b;教子一艺&#xff0c;不如赐子一名。 命名之妙&#xff0c;玄之又玄。 早两年&#xff0c;大模型爆火&#xff0c;本土厂商在大模型产品命名上可谓下足了功夫&#xff0c;引…

C#+uni-app医院HIS预约挂号系统源码 看病挂号快人一步

​​​​​​​ 提到去大型医院机构就诊时&#xff0c;许多人都感到恐惧。有些人一旦走进医院的门诊大厅&#xff0c;就感到迷茫&#xff0c;既无法理解导医台医生的建议&#xff0c;也找不到应该去哪个科室进行检查。实际上&#xff0c;就医也是一门学问&#xff0c;如何优化…

【CS.DS】数据结构 —— 图:深入了解三种表示方法之邻接表(Adjacency List)

文章目录 1 概念2 无向图的邻接表2.1 示例2.2 Mermaid 图示例2.3 C实现2.3.1 简单实现2.3.2 优化封装 2.4 总结 3 有向图的邻接表3.1 示例3.2 C实现3.3 总结 4 邻接图的遍历5 拓展补充References 数据结构 1 概念 优点&#xff1a;空间效率高&#xff0c;适合稀疏图。动态性强…

Win10,Win11电脑重装系统怎么操作,简单一步搞定【保姆级教程】

电脑重装系统怎么操作&#xff1f;电脑使用时间长了&#xff0c;就会出现系统崩溃、病毒感染或者是系统文件损坏等问题。这个时候我们就可以对电脑进行系统重装&#xff0c;也就是恢复电脑出厂设置。现在市面上有很多系统重装工具可以帮助我们解决难题&#xff0c;如果您是电脑…

自定义 Django 管理界面中的多对多内联模型

1. 问题背景 在 Django 管理界面中&#xff0c;用户可以使用内联模型来管理一对多的关系。但是&#xff0c;当一对多关系是多对多时&#xff0c;Django 提供的默认内联模型可能并不适合。例如&#xff0c;如果存在一个产品模型和一个发票模型&#xff0c;并且产品和发票之间是…

Java文件操作小项目-带GUI界面统计文件夹内文件类型及大小

引言 在Java编程中&#xff0c;文件操作是一项基本且常见的任务。我们经常需要处理文件和文件夹&#xff0c;例如读取、写入、删除文件&#xff0c;或者遍历文件夹中的文件等。本文将介绍如何使用Java的File类和相关API来统计一个文件夹中不同类型文件的数量和大小。 准备工作…

数据分析python基础实战分析

数据分析python基础实战分析 安装python&#xff0c;建议安装Anaconda 【Anaconda下载链接】https://repo.anaconda.com/archive/ 记得勾选上这个框框 安装完后&#xff0c;然后把这两个框框给取消掉再点完成 在电脑搜索框输入"Jupyter"&#xff0c;牛马启动&am…

Vitis Accelerated Libraries 学习笔记--OpenCV 安装指南

目录 1. 简介 2. 安装过程 2.1 安装准备 2.2 编译并安装 XRT 2.2.1 下载 XRT 源码 2.2.2 安装依赖项 2.2.3 构建 XRT 2.2.4 打包 DEB 2.2.5 安装 XRT 2.3 编译并安装 OpenCV 2.3.1 下载 OpenCV 源码 2.3.2 创建目录 2.3.3 设置环境变量 2.3.4 构建 opencv 3. 总…

【STM32】看门狗

1.看门狗简介 看门狗起始就是一个定时器&#xff0c;从功能上说它可以让微控制器在程序发生意外&#xff08;程序进入死循环或跑飞&#xff09;的时候&#xff0c;能重新恢复到系统刚上电状态&#xff0c;以保障系统出问题的时候可以重启一次。说的简单一点&#xff0c;看门狗…

加速业务布局,30年老将加盟ATFX,掌舵运营新篇章

全球领先的差价合约经纪商ATFX日前宣布了一项重大人事任命&#xff0c;聘请业界资深人士约翰博格(John Bogue)为机构业务运营总监。约翰博格是一名行业老将&#xff0c;曾在差价合约界深耕三十余载。伴随其加入ATFX&#xff0c;相信他的深厚专业知识和从业经验将为ATFX机构业务…