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

news2024/11/24 3:11:06

目录

 

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;
}

 

 

 

 

 

 

 

 

 

 

 

 

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

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

相关文章

EthernetIP IO从站设备数据 转opc ua项目案例

1 案例说明 设置网关采集EthernetIP IO设备数据把采集的数据转成opc ua协议转发给其他系统。 2 VFBOX网关工作原理 VFBOX网关是协议转换网关&#xff0c;是把一种协议转换成另外一种协议。网关可以采集西门子&#xff0c;欧姆龙&#xff0c;三菱&#xff0c;AB PLC&#xff0…

day41--Redis(三)高级篇之最佳实践

Redis高级篇之最佳实践 今日内容 Redis键值设计批处理优化服务端优化集群最佳实践 1、Redis键值设计 1.1、优雅的key结构 Redis的Key虽然可以自定义&#xff0c;但最好遵循下面的几个最佳实践约定&#xff1a; 遵循基本格式&#xff1a;[业务名称]:[数据名]:[id]长度不超过…

STM32音频应用开发:DMA与定时器的高效协作

摘要: 本文章将深入浅出地介绍如何使用STM32单片机实现音频播放功能。文章将从音频基础知识入手&#xff0c;逐步讲解音频解码、DAC转换、音频放大等关键环节&#xff0c;并结合STM32 HAL库给出具体的代码实现和电路设计方案。最后&#xff0c;我们将通过一个实例演示如何播放W…

FPGA SATA高速存储设计

今天来讲一篇如何在fpga上实现sata ip&#xff0c;然后利用sata ip实现读写sata 盘的目的&#xff0c;如果需要再速度和容量上增加&#xff0c;那么仅仅需要增加sata ip个数就能够实现增加sata盘&#xff0c;如果仅仅实现data的读写整体来说sata ip设计比较简单&#xff0c;下面…

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

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

小公司选择高水平LabVIEW团队外包

小公司选择高水平LabVIEW团队外包的优点可以从多个方面进行详细说明&#xff0c;包括专业技能、成本效益、时间效率、技术支持、风险管理和灵活性等。以下是具体的优点分析&#xff1a; 1. 专业技能和经验 优点&#xff1a; 高水平专业技能&#xff1a;高水平的LabVIEW团队通…

力扣316.去除重复字母

力扣316.去除重复字母 从左到右遍历每个字母 若当前字母比栈顶字母小 并且右边仍然后栈顶字母出现弹出栈顶字母 最后加入当前字母 class Solution {public:string removeDuplicateLetters(string s) {//记录每个字母出现次数;当前字符串中字母是否出现vector<int> lef…

单目标应用:基于吸血水蛭优化器(Blood-Sucking Leech Optimizer,BSLO)的微电网优化(MATLAB代码)

一、微电网模型介绍 微电网多目标优化调度模型简介_vmgpqv-CSDN博客 参考文献&#xff1a; [1]李兴莘,张靖,何宇,等.基于改进粒子群算法的微电网多目标优化调度[J].电力科学与工程, 2021, 37(3):7 二、吸血水蛭优化器求解微电网 2.1算法简介 吸血水蛭优化器&#xff08;B…

AI技术与艺术的融合:开创性的用户界面与产品体验

引言 近年来&#xff0c;人工智能&#xff08;AI&#xff09;的飞速发展改变了我们的生活和工作方式。AI技术不仅在算力和模型上取得了重大进步&#xff0c;更在用户界面和产品体验方面迎来了突破。近日&#xff0c;科技博客 Stratechery 的文章以及硅谷投资基金 AI Grant 的两…

platform 设备驱动实验

platform 设备驱动实验 Linux 驱动的分离与分层 代码的重用性非常重要&#xff0c;否则的话就会在 Linux 内核中存在大量无意义的重复代码。尤其是驱动程序&#xff0c;因为驱动程序占用了 Linux内核代码量的大头&#xff0c;如果不对驱动程序加以管理&#xff0c;任由重复的…

注意!短视频的致命误区,云微客教你避开!

为什么你做短视频就是干不过同行&#xff1f;因为你总想着拍剧情、段子这些娱乐视频&#xff0c;还想着当网红做IP人设&#xff0c;但是这些内容跟你的盈利没有半毛钱关系&#xff0c;况且难度大、见效慢&#xff0c;还不是精准客户。 以上这些就代表你走进了短视频的误区&…

Linux常用环境变量PATH

Linux常用环境变量 一、常用的默认的shell环境变量二、环境变量 PATH三、持久化修改环境变量四、常用的环境变量 一、常用的默认的shell环境变量 1、当我们在shell命令行属于一个命令&#xff0c;shell解释器去解释这个命令的时候&#xff0c;需要先找到这个命令. 找到命令有两…

边缘混合计算智慧矿山视频智能综合管理方案:矿山安全生产智能转型升级之路

一、智慧矿山方案介绍 智慧矿山是以矿山数字化、信息化为前提和基础&#xff0c;通过物联网、人工智能等技术进行主动感知、自动分析、快速处理&#xff0c;实现安全矿山、高效矿山的矿山智能化建设。旭帆科技TSINGSEE青犀基于图像的前端计算、边缘计算技术&#xff0c;结合煤…

k8s学习--Kruise Rollouts 基本使用

文章目录 Kruise Rollouts简介什么是 Kruise Rollouts&#xff1f;核心功能 应用环境一、OpenKruise部署1.安装helm客户端工具2. 通过 helm 安装 二、Kruise Rollouts 安装2. kubectl plugin安装 三、Kruise Rollouts 基本使用(多批次发布)1. 使用Deployment部署应用2.准备Roll…

当前的网安行业绝对不是高薪行业

昨天&#xff0c;面试了一个刚毕业两年的同学小A。第一学历为某大专&#xff0c;第二学历为某省地区的本科院校。面试过程表现一般偏下&#xff0c;但动不动就要薪资15K 这个人&#xff0c;我当场就PASS了。主要原因是&#xff0c;并非是否定小A同学的能力&#xff0c;而是他…

Axure 教程 | 雅虎新闻焦点

主要内容 在雅虎首页&#xff0c;新闻焦点大图和焦点小图同步切换轮播&#xff0c;本课程我们来学习如何实现这个效果。 交互说明 1.页面载入后&#xff0c;切换当前屏幕显示的5张焦点图&#xff0c;小图标处以横线提示当前焦点图。 2.鼠标移入焦点大图&#xff0c;新闻标题显示…

打破数据分析壁垒:SPSS复习必备(九)

有序定性资料统计推断 1.分类 单向有序行列表 双向有序属性相同行列表 双向有序属性不同行列表 2.单向有序行列表 秩和检验 ① 两组单向有序分类资料 ②多组单向有序定性资料 步骤&#xff1a; 1.建立检验假设和确定检验水准 2.编秩 3.求秩和 4.确定检验统计量 5…

MAC 查看公钥私钥

电脑配置过公钥私钥&#xff0c;现在需要查看&#xff1a; 1、 查看本地是否存在SSH密钥 命令&#xff1a;ls -al ~/.ssh 如果在输出的文件列表中发现id_rsa和id_rsa.pub的存在&#xff0c;证明本地已经存在SSH密钥&#xff0c;请执行第3步 2、 生成SSH密钥 命令&#xff1…

【Java】已解决java.nio.channels.OverlappingFileLockException异常

文章目录 一、分析问题背景二、可能出错的原因三、错误代码示例四、正确代码示例五、注意事项 已解决java.nio.channels.OverlappingFileLockException异常 在Java的NIO&#xff08;New I/O&#xff09;编程中&#xff0c;java.nio.channels.OverlappingFileLockException是一…

C#学习系列之DataGrid无故添加空行

C#学习系列之DataGrid无故添加空行 前言解决前解决后总结 前言 采用别人的轮子&#xff0c;想在基础上改界面&#xff0c;但是copy后&#xff0c;无论怎么样都会有空行&#xff0c;实在是绑定数据的输入没有任何赋值。 解决前 绑定的数据中输入三组数据&#xff0c;但是没有第…