链表(2)——带头双向循环链表

news2024/9/29 3:19:57

🍁一、链表的分类

🌕1.单向或者双向

🌕2.带头或者不带头(有无哨兵)

🌕3.循环或者不循环

🌕4.无头单向非循环链表(常用)

🌕5.带头双向循环链表(常用)

🌕注意:

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

🍁二、双向链表的定义:

我们知道单链表的结点有一个数据域用于存放数据,一个指针域用于指向下一个结点。而 双向链表即是在此基础上每个结点多了一个指针域用于指向前一个结点;

🍁三、带头双向循环链表的定义

带头双向循环链表:即在双向链表的基础上,尾结点的next域指向头结点,使之体现出一个循环的结构。


🍁四、带头双向循环链表操作实现(多文件)

🌕1.定义:

只需在单链表定义的基础上多一个指针域prve,用于指向前驱;

typedef int SLDataType;

typedef struct ListNode
{
	struct ListNode* prev;//指向前驱
	struct ListNode* next;//指向后继
	SLDataType data;//数据域
}ListNode;

🌕2.获得新结点

因为后续经常用到此函数,所以首先介绍。

操作很简单,用malloc函数生成即可

//获得新结点
ListNode* BuyLTNode(SLDataType x)
{
	//用malloc函数动态生成即可
	ListNode* node = (ListNode*)malloc(sizeof(ListNode));
	if (node == NULL)
	{
		//检查malloc错误原因
		perror("malloc");
		exit(-1);
	}
	//处理新结点的成员
	node->data = x;
	node->next = NULL;
	node->prev = NULL;
	return node;
}

🌕3.初始化

①:原本初始化需要改变头结点phead,所以需要结构体二级指针,但其他操作都不需要二级指针,所以为了排面,我们可以用返回值来代替使用二级指针;

②:初始化只需要获得一个新结点作为一个头结点,然后头结点的两个指针域互相指向代表此时为空表;

//初始化
ListNode* Init()
{
	//获得头结点,头结点数据域可以存点有意义的数据,也可以随便存,因为用不着
	ListNode* phead = BuyLTNode(0);
	//初始化头结点的两个指针域指向头结点本身表示为空表
	phead->next = phead;
	phead->prev = phead;
	return phead;
}

🌕4.尾插法

该种类链表虽然结构复杂,但操作却非常简单,比如尾插法就有几点优势于单链表;

2.1:优势

①:单链表尾插需要考虑元素是否为空,当链表中没有元素时会改变头指针(头结点),所以需要使用结构体二级指针;但带头双向循环链表因为带有头,所以不管有无元素,在尾插时只需改变结构体指针域,即改变结构体,所以都只需要使用结构体指针;

②:单链表尾插时需要找到尾结点,但带头双向循环链表不需要,因为多了一个prev指针域,头结点的prev域就是尾结点;可以参考上述图片;

2.2:尾插法大致分为“四步骤”:

首先创建一个临时指针tail指向头结点的prev域,即指向尾结点便于操作

①:将tail的next域指向新结点;

②:将新结点的prev域指向tail结点(尾结点);

③:将新结点的next域指向头结点;

④:将头结点的prev域指向新结点。

2.3:源代码
//尾插
//因为带有头,所以操作只需要改变结构体,所以只需要结构体指针
//具体操作看注释
void LTPushBack(ListNode* phead, SLDataType x)
{
	//因为是带头的,所以phead至少是个头指针,所以phead不可能为空,所以需要用assert检查一下
	assert(phead);
	//找到尾结点tail
	ListNode* tail = phead->prev;
	//获取新结点newnode
	ListNode* newnode = BuyLTNode(x);
	//四步骤
	tail->next = newnode;
	newnode->prev = tail;
	phead->prev = newnode;
	newnode->next = phead;
}

🌕5.打印数据

此链表打印数据与单链表有一个区别,就是结束条件不同;因为带头双向循环链表的尾结点的next域不指向NULL,而是指向头结点,所以结束条件为“tail==head”;

//打印
void LTprint(ListNode* phead)
{
	//创建一个临时指针便于遍历操作
	ListNode* node = phead->next;
	//为了体现此链表结构而打印
	printf("phead<->");
	//打印数据,当临时指针node等于头结点时结束
	while (node != phead)
	{
		printf("%d<->", node->data);
		node = node->next;
	}
	//为了体现此链表结构而打印
	printf("phead\n");
}

🌕6.尾删法

6.1:相对于单链表,该链表也有几个优点:

①:尾删不用找尾结点以及倒数第二个结点,用prev域就可以找到;

②:当表中只有一个元素时,单链表需要改变结构体指针,所以需要单独分类;而此链表因为有带头结点和prev域,所以用正常尾删方法即可;

6.2:尾删步骤:

①:判断单链表是否为空(条件:phead->next=phead时即为空);

②:创建一个临时指针tail1用于保存尾结点,方便后续释放尾结点;

③:创建一个临时指针tail2用于保存尾结点的prev域(尾结点的前一个结点),方便进行尾删操作;

④:tail2的next域指向头结点:tail2->next=phead;

⑤:头结点的prev域指向tail2结点:phead->prev=tail2;

⑥:释放尾结点tail1。

6.3:源代码:

//尾删
void LTPopBack(ListNode* phead)
{
	assert(phead);
	//检查是否为空
	if (phead->next == phead)
	{
		printf("此链表为空,尾删失败!\n");
		return;
	}
	//临时指针保存结点
	ListNode* tail1 = phead->prev;
	ListNode* tail2 = phead->prev->prev;
	//断开与尾结点的链接
	tail2->next = phead;
	phead->prev = tail2;
	//释放尾结点
	free(tail1);
}

🌕7.头插法

同上,因为prev的存在,所以不用考虑初始表是否为空表的情况;

7.1:四步骤:

①:新结点的next域指向head的next域(即指向插入前的首结点);

②:head的next域的prev域指向新结点(即插入前的首结点的prev域指向新结点);

③:新结点的prev域指向头结点head;

④:头结点head的next域指向新结点。

7.2:源代码
//头插
void LTPushFront(ListNode* phead, SLDataType x)
{
	assert(phead);
	//新结点
	ListNode* newnode = BuyLTNode(x);
	//四步骤
	newnode->next = phead->next;
	phead->next->prev = newnode;
	newnode->prev = phead;
	phead->next = newnode;
}

🌕8.头删法

头删法也很简单,只需考虑个个指针的链接即可;

8.1:步骤

①:创建临时指针first指向首结点,便于后续释放首结点;

②:创建临时指针second指向第二个结点,便于进行删除操作;

③:改变指针链接:

second->prev = phead;

phead->next = second;

④:释放首结点;

8.2:源代码
//头删
void LTPopFront(ListNode* phead)
{
	assert(phead);
	if (phead->next == phead)
	{
		printf("链表为空,头删失败!\n");
		return;
	}
	//临时指针first指向首结点,便于后续释放首结点
	//临时指针second指向第二个结点,便于进行删除操作
	ListNode* first = phead->next;
	ListNode* second = first->next;
	//删除
	second->prev = phead;
	phead->next = second;
	//释放首结点
	free(first);
}

🌕9.在pos位置之前插入结点

其实很简单,只需要搞得指针域的链接顺序,防止指针丢失即可

9.1:源代码如下:
 
//在pos位置之前插入结点
ListNode* LTInsrt(ListNode* pos, SLDataType x)
{
	assert(pos);
	//新结点
	ListNode* newnode = BuyLTNode(x);
	//插入
	pos->prev->next = newnode;
	newnode->prev = pos->prev;
	newnode->next = pos;
	pos->prev = newnode;
}
9.2:有了这个算法后我们可以改进头插与尾插:

①:当pos==phead->next时,即为头插算法:
 

//头插
void LTPushFront(ListNode* phead, SLDataType x)
{
	assert(phead);

	//改进
	LTInsrt(phead->next, x);
}

②:当pos等于phead时,即为尾插算法:

//尾插
void LTPushBack(ListNode* phead, SLDataType x)
{
	//因为是带头的,所以phead至少是个头指针,所以phead不可能为空,所以需要用assert检查一下
	assert(phead);

	//改进
	LTInsrt(phead, x);
}

🌕10.删除pos位置的结点

10.1:步骤:

①:创建临时指针first保存pos前一个结点;

②:创建临时指针second保存pos后一个结点;

③:改变指针链接,删除pos结点:

first->next = second; 

second->prev = first;

④:释放pos结点。

10.2:源代码
//删除pos位置的结点
void LTErase(ListNode* pos)
{
	assert(pos);
	//临时指针
	ListNode* first = pos->prev;
	ListNode* second = pos->next;
	//删除
	first->next = second;
	second->prev = first;
	//释放pos
	free(pos);
}
10.3:有了这个算法,我们可以改进头删与尾删

①:当pos==phead->next时,即为头删算法:

//头删
void LTPopFront(ListNode* phead)
{
	assert(phead);

	//改进
	LTErase(phead->next);
}

②:当pos==phead->prev时,即为尾删算法:

//尾删
void LTPopBack(ListNode* phead)
{
	assert(phead);

	//改进
	LTErase(phead->prev);
}

🍁五、测试源代码

main.c

#include"List.h"

void STTest1()
{
	ListNode* plist = NULL;
	plist = Init();//初始化
	//尾插
	LTPushBack(plist, 1);
	LTPushBack(plist, 2);
	LTPushBack(plist, 3);
	LTPushBack(plist, 4);
	LTPushBack(plist, 5);
	//打印
	LTprint(plist);
}

void STTest2()
{

	ListNode* plist = NULL;
	plist = Init();//初始化
	//尾插
	LTPushBack(plist, 1);
	//打印
	LTprint(plist);
	//尾删
	LTPopBack(plist);
	//打印
	LTprint(plist);
}

void STTest3()
{
	ListNode* plist = NULL;
	plist = Init();//初始化
	//头插
	LTPushFront(plist, 1);
	LTPushFront(plist, 2);
	//打印
	LTprint(plist);
	//头删
	LTPopFront(plist);
	//打印
	LTprint(plist);
	//头删
	LTPopFront(plist);
	//打印
	LTprint(plist);
}

int main()
{
	//STTest1();
	//STTest2();
	STTest3();
	return 0;
}

List.c

#include"List.h"

//获得新结点
ListNode* BuyLTNode(SLDataType x)
{
	//用malloc函数动态生成即可
	ListNode* node = (ListNode*)malloc(sizeof(ListNode));
	if (node == NULL)
	{
		//检查malloc错误原因
		perror("malloc");
		exit(-1);
	}
	//处理新结点的成员
	node->data = x;
	node->next = NULL;
	node->prev = NULL;
	return node;
}

//初始化
ListNode* Init()
{
	//获得头结点,头结点数据域可以存点有意义的数据,也可以随便存,因为用不着
	ListNode* phead = BuyLTNode(0);
	//初始化头结点的两个指针域指向头结点本身表示为空表
	phead->next = phead;
	phead->prev = phead;
	return phead;
}

//打印
void LTprint(ListNode* phead)
{
	assert(phead);
	//创建一个临时指针便于遍历操作
	ListNode* node = phead->next;
	//为了体现此链表结构而打印
	printf("phead<->");
	//打印数据,当临时指针node等于头结点时结束
	while (node != phead)
	{
		printf("%d<->", node->data);
		node = node->next;
	}
	//为了体现此链表结构而打印
	printf("phead\n");
}

//尾插
//因为带有头,所以操作只需要改变结构体,所以只需要结构体指针
//具体操作看注释
void LTPushBack(ListNode* phead, SLDataType x)
{
	//因为是带头的,所以phead至少是个头指针,所以phead不可能为空,所以需要用assert检查一下
	assert(phead);
	找到尾结点tail
	//ListNode* tail = phead->prev;
	获取新结点newnode
	//ListNode* newnode = BuyLTNode(x);
	四步骤
	//tail->next = newnode;
	//newnode->prev = tail;
	//phead->prev = newnode;
	//newnode->next = phead;


	//改进
	LTInsrt(phead, x);
}

//尾删
void LTPopBack(ListNode* phead)
{
	assert(phead);
	检查是否为空
	//if (phead->next == phead)
	//{
	//	printf("此链表为空,尾删失败!\n");
	//	return;
	//}
	临时指针保存结点
	//ListNode* tail1 = phead->prev;
	//ListNode* tail2 = phead->prev->prev;
	断开与尾结点的链接
	//tail2->next = phead;
	//phead->prev = tail2;
	释放尾结点
	//free(tail1);

	//改进
	LTErase(phead->prev);
}

//头插
void LTPushFront(ListNode* phead, SLDataType x)
{
	assert(phead);
	//新结点
	//ListNode* newnode = BuyLTNode(x);
	四步骤
	//newnode->next = phead->next;
	//phead->next->prev = newnode;
	//newnode->prev = phead;
	//phead->next = newnode;

	//改进
	LTInsrt(phead->next, x);
}

//头删
void LTPopFront(ListNode* phead)
{
	assert(phead);
	/*if (phead->next == phead)
	{
		printf("链表为空,头删失败!\n");
		return;
	}*/
	临时指针first指向首结点,便于后续释放首结点
	临时指针second指向第二个结点,便于进行删除操作
	//ListNode* first = phead->next;
	//ListNode* second = first->next;
	删除
	//second->prev = phead;
	//phead->next = second;
	释放首结点
	//free(first);

	//改进
	LTErase(phead->next);
}

//在pos位置之前插入结点
void LTInsrt(ListNode* pos, SLDataType x)
{
	assert(pos);
	//新结点
	ListNode* newnode = BuyLTNode(x);
	//插入
	pos->prev->next = newnode;
	newnode->prev = pos->prev;
	newnode->next = pos;
	pos->prev = newnode;
}

//删除pos位置的结点
void LTErase(ListNode* pos)
{
	assert(pos);
	//临时指针
	ListNode* first = pos->prev;
	ListNode* second = pos->next;
	//删除
	first->next = second;
	second->prev = first;
	//释放pos
	free(pos);
}

List.h

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

typedef int SLDataType;

typedef struct ListNode
{
	struct ListNode* prev;//指向前驱
	struct ListNode* next;//指向后继
	SLDataType data;//数据域
}ListNode;

//获得一个新结点
ListNode* BuyLTNode(SLDataType x);

//初始化
ListNode* Init();

//打印
void LTprint(ListNode* phead);

//尾插
void LTPushBack(ListNode* phead,SLDataType x);

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

//头插
void LTPushFront(ListNode* phead, SLDataType x);

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

//在pos位置之前插入结点
void LTInsrt(ListNode* pos, SLDataType x);

//删除pos位置的结点
void LTErase(ListNode* pos);

本次知识到此结束,希望对你有所帮助!

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

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

相关文章

SNMP报文与MIB Browser软件讲解

目录 SNMP报文结构 MIB Browser软件讲解 具体的操作步骤 MIB操作方式 SNMP报文结构 UDP端口读/写为161&#xff0c;Trap为162 版本号 版本号 名称 0 V1 1 V2c 2 V3 团体字 团体字相当于管理方和被管理方进行校验的密钥 读写团体字 两端需要配置为一致 PDU类型——标…

5项先进采购技术,帮助你的企业脱颖而出

持续的改进对保持每个企业的正常运转有着重要作用&#xff0c;采购部门也不例外。 以前&#xff0c;采购团队主要关注两个方面&#xff1a;降低成本和减少风险。随着自动化和云服务的兴起&#xff0c;如今他们还需要关注采购决策的效率、可访问性和可持续性。 技术与采购的融合…

python中pytorch的广播机制——Broadcasting

广播机制 numpy 在算术运算期间采用“广播”来处理具有不同形状的 array &#xff0c;即将较小的阵列在较大的阵列上“广播”&#xff0c;以便它们具有兼容的形状。Broadcasting是一种没有copy数据的expand 不过两个维度不相同&#xff0c;在前面插入维度1扩张维度1到相同的维…

轻盈百搭头戴式耳机——umelody轻律 U1头戴式复古耳机分享

最近买了款热门的轻律U1头戴式耳机&#xff0c;今天和大家来分享一下&#xff0c;看看究竟效果怎样呢&#xff1f; UMELODY轻律品牌将复古潮流文化结合与音频设备之中&#xff0c;一直以来致力于音频领域的研究和创新。产品外观定位时下流行之‘Retro Futurism’ “ 复古未来主…

软件测试工程师岗位核心任务

最近转正&#xff0c;需要完成一个OA任务&#xff0c;其中有一项“你认为软件测试工程师岗位核心任务是什么&#xff1f;”要求写出三到五条&#xff0c;并简单地阐明。 这个问题似乎很好回答&#xff0c;软件测试工程师不就是做测试&#xff1f;仅仅这样吗&#xff1f;小酋抠…

进程相关介绍(一)

目录 进程标识符 查看进程的标识符 ps axj | head -1&& ps axj | grep 程序名 ls /proc/进程标识符 获得进程标识符 getpid()函数 getppid()函数 创建一个子进程 fork函数解析 fork函数返回子进程的pid给父进程的原因 fork函数有两个返回值的原因 一个进程实质上就是一…

【每日一记】OSPF中Hello报文详讲

个人名片&#xff1a; &#x1f43c;作者简介&#xff1a;一名大二在校生&#xff0c;喜欢编程&#x1f38b; &#x1f43b;‍❄️个人主页&#x1f947;&#xff1a;小新爱学习. &#x1f43c;个人WeChat&#xff1a;hmmwx53 &#x1f54a;️系列专栏&#xff1a;&#x1f5bc…

SpringBoot 实现EMQ设备的上下线告警

前言 上下线通知 我遇到了一个难题&#xff0c;即在使用EMQ X 4.4.10的开源版本时&#xff0c;我需要实现设备的上下线状态监控&#xff0c;但该4.4.10开源版本并未内置设备上下线提醒模块&#xff0c;只有企业版才内置了该模块。这为我带来了一些技术上的难题&#xff0c;迫…

远程办公软件的未来趋势:预测2023年及以后的发展方向

随着科技的迅速发展&#xff0c;远程办公已经成为现代工作方式的重要组成部分。远程办公软件在过去几年中取得了巨大的进步&#xff0c;并且在全球范围内被广泛使用。本文将探讨远程办公软件在2023年及以后可能的发展方向&#xff0c;包括增强的协作功能、智能化的辅助工具、改…

坦克 400 Hi4-T:用产品诠释越野新能源

9 月 25 日&#xff0c;坦克 400 Hi4-T 正式上市&#xff0c;新车共推出两款车型配置&#xff0c;售价区间 27.98-28.98 万元。同时&#xff0c;坦克 400 Hi4-T 将上市及即交付。 权益方面&#xff0c;坦克 400 Hi4-T 共有七重好礼&#xff1a; 质保无忧&#xff1a;整车 5 年…

02 认识Verilog HDL

02 认识Verilog HDL ‍ 对于Verilog的语言的学习&#xff0c;我认为没必要一开始就从头到尾认真的学习这个语言&#xff0c;把这个语言所有细节都搞清楚也不现实&#xff0c;我们能够看懂当前FPGA的代码的程度就可以了&#xff0c;随着学习FPGA深度的增加&#xff0c;再不断的…

Autosar诊断实战系列24-0x2E服务代码级分析及ECU-Pending期间的处理

本文框架 前言1. UDS-0x2E服务逻辑整理2. Pending期间ECU的处理3. 相关工程问题思考前言 开始本篇讲述前,先抛出几个问题,UDS 2E服务在执行过程中进行了哪些操作?在2E写期间由于要操作NvM,会执行时间较长导致ECU先回复NRC 0x78,这期间ECU在进行哪些处理?ECU是如何判断2E…

单目标应用:蚁群算法(Ant Colony Optimization,ACO)求解微电网优化MATLAB

一、微网系统运行优化模型 微电网优化模型介绍&#xff1a; 微电网多目标优化调度模型简介_IT猿手的博客-CSDN博客 二、蚁群算法ACO 蚁群算法&#xff08;Ant Clony Optimization&#xff0c; ACO&#xff09;由意大利学者Colorni A., Dorigo M. 等于1991年提出&#xff0c…

开啥玩笑?一个SSD硬盘可以使用100多年?MTBF正解

在之前文章中&#xff0c;有一个参数“平均无故障时间”&#xff0c;对应的参数是MTBF&#xff0c;比如这个盘MTBF150万小时。 小编发现有一些朋友对这个参数还有误解。大家看到这个参数误认为盘可以使用150万小时都没有发生故障。如果真的是这样&#xff0c;那么这盘的质量简直…

基于springboot实现家具销售电商平台管理系统项目【项目源码+论文说明】

基于springboot实现家具销售电商平台管理系统演示 摘要 社会的发展和科学技术的进步&#xff0c;互联网技术越来越受欢迎。网络计算机的交易方式逐渐受到广大人民群众的喜爱&#xff0c;也逐渐进入了每个用户的使用。互联网具有便利性&#xff0c;速度快&#xff0c;效率高&am…

【OSPF宣告——network命令与多区域配置实验案例】

个人名片&#xff1a; &#x1f43c;作者简介&#xff1a;一名大二在校生&#xff0c;喜欢编程&#x1f38b; &#x1f43b;‍❄️个人主页&#x1f947;&#xff1a;小新爱学习. &#x1f43c;个人WeChat&#xff1a;hmmwx53 &#x1f54a;️系列专栏&#xff1a;&#x1f5bc…

ChatGLM2-6B微调实践-P-Tuning方案

ChatGLM2-6B微调实践 环境准备安装部署1、安装 Anaconda2、安装CUDA3、安装PyTorch4、安装 ChatGLM2-6B 微调实践1、准备数据集2、安装python依赖3、微调并训练新模型4、微调后模型的推理与评估5、验证与评估微调后的模型6、微调模型优化7、P-Tuning微调灾难性遗忘问题 微调过程…

vulnhub_Inferno靶机渗透测试

Inferno靶机 靶机地址&#xff1a;https://www.vulnhub.com/entry/inferno-11,603/ 文章目录 Inferno靶机信息收集web渗透获取权限横向移动权限提升靶机总结 信息收集 1.通过nmap扫描得到靶机开放22和80端口&#xff0c;看来是主web端渗透了 使用dirsearch目录扫描没得到结果…

【Mybatis】动态 SQL

动态 SQL \<if>标签\<trim>标签\<where>标签\<set>标签\<foreach>标签 动态 sql 是 Mybatis 的强⼤特性之⼀&#xff0c;能够完成不同条件下不同的 sql 拼接。 <if>标签 前端用户输入时有些选项是非必填的, 那么此时传到后端的参数是不确…

面试总结(mysql定精度/oom排查/spring三级缓存/stream流)

Mysql数据类型上的一个把握 1、MySQL Decimal为什么不会丢失精度 DECIMAL的存储方式和其他数据类型都不同&#xff0c;它是以字符串形式存储的。假设一个字段为DECIMAL(3,0)&#xff0c;当我们存入100时&#xff0c;实际上存入的1、0、0这三个字符拼接而成的字符串的二进制值&…