数据结构之线性表中的双向循环链表【详解】

news2024/11/29 22:38:13

前言:

嗯!昨天我们的无头单向非循环链表咱已经是可以顺利完成出来了的,今天我们就来看一下什么是有头双向循环链表,不要看着这个链表又双向又循环的就比单向不循环链表难,其实这个更加的简单哦!前提是你有自己去完成单链表,此时你就会觉得双链表是比单链表更加简单的,所以不要害怕,不就是一个链表吗?

一、有头双向循环链表

(一、)我们先讲一些学习的小知识点和注意点

我们不管它昨天几点睡和今天有没有学习,把握好当下,我们今天要进行双向链表的学习

1.什么是双向链表(最主要讲的就是每个节点不仅是上一个结点存了下一个结点的地址(此时后一个结点也存了上一个结点的地址),这个就是双向链表)

2.所以现在我们还是一样不管那么多,我们先进行一个结构体的创建(但是此时的这个结构体不同于单链表的结构体,此时的结构体是多了一个指针的结构体)

3.单链表的题目考察较多,双链表没什么题目

4.二叉树也是两个指针

5.带头和不带头(就看与没有哨兵位),就是看头指针是否存放数据(头指针单独是一个指针就是哨兵位)

6.循环和非循环(如果是双向链表它的头结点和尾结点都是指向空指针的,但是如果是循环链表则不是)

7.循环链表的最后一个结点指向的位置是头结点

8.所以链表为什么会有8中结构(单、双)(带头、不带头)(循环、非循环)这些的排列组合刚好8种

9.但是在这8种中比较有价值(最常用的)的就是(最简单的和最复杂的):无头单向非循环链表、带头双向循环链表

10.所以我们就开始带头双向循环链表的学习(比无头单向非循环链表更简单哦)
(二、)有关的图解(便于理解)
在这里插入图片描述

(三、)有头双向循环链表的实现
可以分为一下这些接口:
1.初始化
2.打印
3.尾插
4.尾删
5.头插
6.头删
7.查找
8.销毁
9.任意位置插入
10.任意位置删除

//1.初始化
DLNode* ListInit();
//2.打印
void ListPrint(DLNode* phead);
//3.尾插
void ListPushBack(DLNode* phead, DSListType x);
//4.尾删
void ListPoptBack(DLNode* phead);
//5.头插
void ListPushFront(DLNode* phead, DSListType x);
//6.头删
void ListPoptFront(DLNode* phead);
//7.查找
DLNode* ListFindname(DLNode* phead, DSListType x);
//8.销毁
void ListDestory(DLNode* phead);
//9.任意位置插入
void ListInsert(DLNode* pos, DSListType x);
//10.任意位置删除
void ListDete(DLNode* pos);
//11.上面的这两个函数是整个双向循环链表中最重要的

1.初始化接口

//1.初始化
//因为此时我有对这个传过来的指针进行改变(进行了初始化)所以此时的头结点是发生的改变的,所以这边一定要有返回值
DLNode* ListInit()
{
	//因为我们是玩带哨兵位的,所以此时我们应该要先malloc出来一个结点(让这个结点成为我的哨兵位)
	//哨兵位头结点:(但是我函数外面的plist并拿不到这个哨兵位(也就是plist不会发生改变),所以需要有一个返回值来处理这个问题或者用二级指针也行)
	DLNode* phead = (DLNode*)malloc(sizeof(DLNode));
	//因为此时的结构是一个双向的循环,所以(此时结构体中的两个指针应该要有不同的指向)
	//一个指针指向自己
	phead->next = phead;
	//另一个指针也指向它自己
	phead->prev = phead;

	return phead;

}//以上就是对一个带头循环双向链表的初始化(这个就是C语言的魅力,什么都是靠自己来弄,你只是给了我一个概念的模型,剩下的东西都是要靠我自己来搞定这个结构应该是怎样的)

2.打印接口

//3.打印
//
void ListPrint(DLNode* phead)
{
	//因为此时phead中存的并不是一个有效的数据,所以此时不需要从头结点开始遍历(下一个结点开始)
	//判断结束条件就是通过:此时的这些数据在循环的过程之中最后会等于我的头结点(哨兵位),因为是循环的,所以不要以为它是一个循环就不能打印(只是停止的条件发生了一些的改变而已)
	assert(phead);
	DLNode* cur = phead->next;
	while (cur != phead)
	{
		printf("<-%d->", cur->data);
		cur = cur->next;

	}
}

3.尾插接口

//2.尾插
//因为此时我的头结点plist传过来之后我并没有对其进行改变(因为我在初始化那步就已经改变过了,此时的这个plist就是已经拥有了两个指针(已经把哨兵位给创建好了),所以此时我在尾插时,就不会对plist进行任何形式的改变,所以此时我就不需要有返回值,当然也不需要有二进指针(这个也就是哨兵位的好处))
void ListPushBack(DLNode* phead, DSListType x)
{
	assert(phead);
	//DLNode* tail = phead->prev;//这步就是循环链表的大好处了(直接就可以找到最后一个结点的位置)
	//DLNode* newnode = (DLNode*)malloc(sizeof(DLNode));//这个就是开辟新结点(尾插就一定要开辟一个新结点不然插什么)
	//newnode->data = x;

	以上就是一个准备工作,下面就是真正的双向的循环的原理实现(最好是附上一幅图)
	//tail->next = newnode;//这步的意思是因为此时的tail就是尾结点(就是让我的尾结点的最后一个指针去指向我新开辟出来的结点,这样就实现了尾插)
	//newnode->prev = tail;//这个就是为了实现双向循环链表(因为双向循环链表有两个指针,此时的一个指针就要指向刚刚那个尾结点(tail),只有这样我的newnode才可以取而代之)
	//newnode->next = phead;//然后此时的另一个指针就去指向我的头指针(为了实现循环,尾—>头)
	//phead->prev = newnode;//然后这步就是把刚刚的头的其中一个指针指向我的newnoode(还是为了循环)(头->尾),这样就实现了循环双指针链表
	
	//附用任意插
	ListInsert(phead, x);

}

4.尾删接口

//4.尾删(我的正确写法)
void ListPoptBack(DLNode* phead)
{
	assert(phead);
	assert(phead->next != phead);//这边就是说明我不可以把哨兵位给删掉(就是当phead->next=phead;时,此时就是代表我这个循环双链表就只剩下哨兵位自己了(因为这是一个循环的链表),此时就不能再进行删除了)
	首先肯定是不需要开辟新结点的
	//DLNode* tail = phead->prev;
	while (cur->next != tail)//找尾的上一个
	{
		cur = cur->next;
	}
	 不敢把这个单链表的理解带过来我们的双链表(因为此时的整体的结构就是不一样的,因为此时的双链表是有两个指针的,一个是next指针,一个是prev指针,所以此时的尾的前一个只需要用prev这个指针去找就可以了)
	//DLNode* prev = tail->prev;
	//free(tail);
	//prev->next = NULL;
	此时以上只是大致的把尾给删除了,但是我还没有重新将这个循环链表给链接起来
	//prev->next = phead;
	//phead->prev = prev;

	//附用任意删
	ListDete(phead->prev);

}

5.头插接口

//5.头插
void ListPushFront(DLNode* phead, DSListType x)
{
	assert(phead);

	//DLNode* newnode = (DLNode*)malloc(sizeof(DLNode));
	//newnode->data = x;
	//DLNode* tail = phead->next;
	//newnode->next = tail;
	//tail->prev = newnode;

	//newnode->prev = phead;
	//phead->next = newnode;
	
	//附用任意插
	ListInsert(phead->next, x);//这个位置一定要记住是phead->next,不然就会出问题,因为哨兵位的后一个才是头结点,所以在这个头结点前面插入才是我的头插
    
}

6.头删接口

//6.头删
void ListPoptFront(DLNode* phead)
{
	//这边只要是与删除有关的代码,就要多断言一下,防止把哨兵位给删掉
	//assert(phead);
	//assert(phead->next != phead);//表示链表为空就不再需要断言了
	//
	//DLNode* tail = phead->next;
	//DLNode* next = tail->next;
	//free(tail);//这个free什么时候free就看你自己的方式了,可以最后free也可以后面free

	//next->prev = phead;
	//phead->next = next;

	//附用任意删
	ListDete(phead->next);

}

7.查找接口

//7.查找
DLNode* ListFindname(DLNode* phead, DSListType x)//这个是为了找x,不敢当傻子了
{
	assert(phead);
	//这个的逻辑就有点像是print的逻辑,就是靠那个循环条件来完成
	DLNode* cur = phead->next;
	while (cur != phead)//这个循环是在保证我这个链表中不止只有哨兵位,而是有数据结点才开始找
	{
		if (cur->data == x)
		{
			return cur;
		}
		//这个位置就表示找到了
		cur = cur->next;

	}
	return NULL;
}

8.销毁接口

//8.销毁
void ListDestory(DLNode* phead)
{
	DLNode* cur = phead->next;
	DLNode* next = NULL;
	//while (cur->next != phead)//这步一开始你是这样写的,但是如果你写成这样的话,就会导致最后一个结点free不了,所以应该写成下面这样
	while (cur != phead)
	{
		//可以的怎么野指针怎么来(这就是我吗?)
		next = cur->next;
		free(cur);
		cur = NULL;
		cur = next;

	}
	//并且在你把所有的结点释放完之后(不能把哨兵位这个动态开辟的内存空间给忘记掉释放了),所以这边还要加一步
	free(phead);//但是这边要注意一个二级指针的问题(因为这步现在再改变我的函数外部的plist哨兵位)
	phead = NULL;
	
}

9.任意位置插入接口

//9.任意位置插入(pos位置)
void ListInsert(DLNode* pos, DSListType x)//这个位置不一定要用pos,用一个int pos的下标也是一样的,但一定注意这个pos是一个DLNode结构体,里面是有两个指针和一个数据的
{
	assert(pos);

	DLNode* newnode = (DLNode*)malloc(sizeof(DLNode));
	newnode->data = x;
	DLNode* prev = pos->prev;
	newnode->prev = prev;
	prev->next = newnode;

	newnode->next = pos;
	pos->prev = newnode;

}

10.任意位置删除接口

//10.任意位置删除(pos位置)
void ListDete( DLNode* pos)
{
	assert(pos);
	/*assert(phead->next != phead);*/

	DLNode* next = pos->next;
	DLNode* prev = pos->prev;
	free(pos);
	next->prev = prev;
	prev->next = next;

}

附上图解:
在这里插入图片描述

(四、)测试代码

void Test1()

中的测试内容:
在这里插入图片描述

void Test2()

中的测试内容:
在这里插入图片描述

void Test3()

在这里插入图片描述
完整的测试代码:



#include"标头.h"


void Test1()
{
	//DLNode* plist = NULL;//(此时我的头结点是一个带哨兵位的,)
	//先初始化
	/*ListInit(&plist);*///我们在使用哨兵位的就不用这种传地址的方式
	DLNode* plist = ListInit();//而是使用这种直接获得返回值的方式
    //再尾插
	ListPushBack(plist, 1);
	ListPushBack(plist, 2);
	ListPushBack(plist, 3);
	ListPushBack(plist, 4);
	ListPushBack(plist, 5);

	ListPrint(plist);
	printf("\n");
	//再尾删
	ListPoptBack(plist);
	ListPoptBack(plist);
	ListPoptBack(plist);
	ListPoptBack(plist);

	ListPrint(plist);
	printf("\n");
	//再头插
	ListPushFront(plist, 2);
	ListPushFront(plist, 3);
	ListPushFront(plist, 4);
	ListPushFront(plist, 5);

	ListPrint(plist);
	printf("\n");
	//再头删
	ListPoptFront(plist);
	ListPoptFront(plist);
	ListPoptFront(plist);

    //打印
 	printf("NULL");
	ListPrint(plist);
	printf("NULL\n");

	ListDestory(plist);

}

void Test2()
{
	DLNode* plist = ListInit();//而是使用这种直接获得返回值的方式

	//先尾插
	ListPushBack(plist, 1);
	ListPushBack(plist, 2);
	ListPushBack(plist, 3);
	ListPushBack(plist, 4);
	ListPushBack(plist, 5);
	ListPrint(plist);
	printf("\n");
	//再头插
	ListPushFront(plist, 1);
	ListPushFront(plist, 2);
	ListPushFront(plist, 3);
	ListPushFront(plist, 4);
	ListPushFront(plist, 5);
	ListPrint(plist);
	printf("\n");
	//查找
	DLNode* pos = ListFindname(plist, 1);
	printf("\n");
	//打印
	printf("NULL");
	ListPrint(plist);
	printf("NULL\n");

	ListDestory(plist);
}

void Test3()
{
	DLNode* plist = ListInit();

	//先尾插
	ListPushBack(plist, 1);
	ListPushBack(plist, 2);
	ListPushBack(plist, 3);
	ListPushBack(plist, 4);
	ListPushBack(plist, 5);
	//再头插
	ListPushFront(plist, 1);
	ListPushFront(plist, 2);
	ListPushFront(plist, 3);
	ListPushFront(plist, 4);
	ListPushFront(plist, 5);

	ListPrint(plist);
	printf("\n");
	//再尾删
	ListPoptBack(plist);
	ListPoptBack(plist);
	ListPoptBack(plist);
	ListPoptBack(plist);

	ListPrint(plist);
	printf("\n");
	//再头删
	ListPoptFront(plist);
	ListPoptFront(plist);
	ListPoptFront(plist);

	ListPrint(plist);
	printf("\n");

	ListDestory(plist);
	//可以不使用二级指针,直接自己在外面置空指针(为了保持接口的一致性,所以不使用二级指针)
	plist = NULL;
}

int main()
{
	//Test1();
	//Test2();
	Test3();

    return 0;
}

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

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

相关文章

SpringBoot SpringBoot 原理篇 1 自动配置 1.17 自动配置原理【3】

SpringBoot 【黑马程序员SpringBoot2全套视频教程&#xff0c;springboot零基础到项目实战&#xff08;spring boot2完整版&#xff09;】 SpringBoot 原理篇 文章目录SpringBootSpringBoot 原理篇1 自动配置1.17 自动配置原理【3】1.17.1 看源码了1.17.2 小结1 自动配置 1.…

【STA】(1)引言

目录 1. 纳米级设计 2. 什么是STA 3. 为什么要进行STA 4. 设计流程 5. 不同阶段的STA 6. STA的局限性 1. 纳米级设计 在半导体器件中&#xff0c;金属互连线通常被用来连接电路中的各个部分&#xff0c;进而实现整个芯片。随着制造工艺的进一步缩小&#xff0c;这些互连线…

【电源专题】案例:不导这颗MOS管的原因是在电路上不通用?

本案例发生在MOS管替代料导入时。正常情况下在替代料导入、部品导入的时候,我们需要查看规格书。怎么查找规格书可以看文章【电子通识】芯片资料查询方法 对于一些关键的信息我们要做对比,一般来说要通过列表进行对比。但因为不同的供应商的测试标准不同,有很多是很难对比的…

信号与系统2——LTI

信号与系统2——LTI一、Introduction1. Representation of LTI systems2. Significance of unit impulse二、DT-LTI&#xff1a;Convolution Sum1. Output2. Impulse response of LTI system H3. Convolution sum4. Convolution Sum Evaluation Procedure5. Sequence Convoluti…

Python 数据容器(1) - list(列表)

文章目录什么是数据容器&#xff1f;Python中的数据容器数据容器&#xff1a;list&#xff08;列表&#xff09;基本语法案例演示列表的下标&#xff08;索引&#xff09;列表常用操作list容器操作总结什么是数据容器&#xff1f; 一种可以容纳多份数据的数据类型&#xff0c;容…

算法学习 | 回溯算法之深度优先搜索常见题型练习

目录 岛屿的最大面积 电话号码的字母组合 二进制手表 组合总数 活字印刷 岛屿的最大面积 题目链接&#xff1a;leetcode-695.岛屿的最大面积 示例 输入&#xff1a;grid [[0,0,1,0,0,0,0,1,0,0,0,0,0],[0,0,0,0,0,0,0,1,1,1,0,0,0],[0,1,1,0,1,0,0,0,0,0,0,0,0],[0,…

线程“八锁“ synchronized到底是对哪个对象加锁?

线程"八锁" synchronized到底是对哪个对象加锁? 习题一 class Number{public synchronized void a(){System.out.println("1");}public synchronized void b(){System.out.println("2");} } public class TestBlock {public static void main(…

从Zemax OpticStudio导入光学系统

摘要 ZemaxOpticStudio是一款广泛使用的光线追迹软件。VirtualLab Fusion可以从Zemax OpticStudio导入光学系统&#xff0c;包括完整3D位置信息和镜片玻璃。导入后&#xff0c;光学系统的结构数据将显示为单独的表面或可以组合成VirtualLab Fusion中的组件。VirtualLab Fusion可…

docker入门(一):在centOS虚拟机上安装docker

索引CentOS虚拟机安装1.下载CentOS镜像问题1-报错“您已输入用户名&#xff0c;客户机操作系统将保留此用户名”2.根据docker官方指导进行安装1.卸载旧版本&#xff08;初次安装可以忽略&#xff09;2.确保能联网后下载前置软件包3.设置镜像库&#xff08;阿里版&#xff09;4.…

CLIP后续--LSeg,GroupViT,ViLD

这个博客开了有两个月&#xff0c;一直没写成&#xff0c;最近封寝给它完成~躺平第三天 CLIP应用领域概览&#xff1a; 1. LSeg 原论文地址&#xff1a;https://arxiv.org/abs/2201.03546 代码&#xff1a;https://github.com/isl-org/lang-seg 这个图就很清楚的说明了zero…

mysql数据库管理

目录 一、MySQL数据库管理 1、库和表 2、常用的数据类型 3、char和varchar区别 二、查看数据库结构 三、SQL语句 1、SQL语句分类&#xff1a; 四、创建及删除数据库和表 五、管理表中的数据记录 六、修改表名和表结构 七、自增 八、填充 九、克隆表 十、清空表&am…

信号与系统1——Signals and Systems

信号与系统1——Signals and Systems一、Introduction1. Signals and Systems信号与系统(1) Signal信号(2) System系统2. Classification of Signals信号的分类(1) Continuous-time & discrete-time1) Continuous-Time signal连续时间信号2) Discrete-Time signal离散时间信…

【Hack The Box】linux练习-- Passage

HTB 学习笔记 【Hack The Box】linux练习-- Passage &#x1f525;系列专栏&#xff1a;Hack The Box &#x1f389;欢迎关注&#x1f50e;点赞&#x1f44d;收藏⭐️留言&#x1f4dd; &#x1f4c6;首发时间&#xff1a;&#x1f334;2022年9月7日&#x1f334; &#x1f36…

浅析数据仓库和建模理论

第一章 认识数据仓库 1.1 数据仓库概念 数据仓库&#xff0c;英文名称为 Data Warehouse&#xff0c;可简写为 DW 或 DWH。数据仓库&#xff0c;是为企业所有级别的决策制定过程&#xff0c;提供所有类型数据支持的战略集合。它是单个数据存储&#xff0c;出于分析性报告和决…

BDD - SpecFlow SpecRun Web UI 多浏览器测试

BDD - SpecFlow & SpecRun 一个 Cases 匹配多个浏览器引言方案SpecFlow Runner profiles实现被测 Web Application创建一个 Class Libary 项目添加 NuGet PackagesSpecFlow & SpecRun 包添加 Selenium包其它包创建 Feature 文件配置 Default.srprofileDefault.srprofil…

MySQL的概念

MySQL的概念一.数据库的基本概念1、数据&#xff08;Data&#xff09;2、表3、数据库4、数据库管理系统&#xff08;DBMS&#xff09;4.1 关系数据库4.2 非关系型数据库 NoSQL5、数据库系统6、访问数据库的流程二.数据库系统发展史1.第一代数据库2.第二代数据库3.第三代数据库三…

JAVA多线程(MultiThread)的各种用法

多线程简单应用 单线程的问题在于&#xff0c;一个线程每次只能处理一个任务&#xff0c;如果这个任务比较耗时&#xff0c;那在这个任务未完成之前&#xff0c;其它操作就会无法响应。 如下示例中&#xff0c;点击了“进度1”后&#xff0c;程序界面就没反应了&#xff0c;强行…

类文件结构和初识一些字节码指令

文章目录类文件的结构为什么要了解字节码指令Class文件结构Java虚拟机规定的类结构魔数版本常量池访问标志类索引、父类索引、接口索引Ⅰ. interfaces_count&#xff08;接口计数器&#xff09;Ⅱ. interfaces[]&#xff08;接口索引集合&#xff09;字段表集合**1. 字段表访问…

【React】使用 react-pdf 将数据渲染为pdf并提供下载

文章目录前言环境步骤1. 安装react脚手架2. 使用 create-react-app 创建项目 &#xff08;首字母不要大写、不要使用特殊字符&#xff09;3. 用 vscode 打开目录 react-staging4. yarn 启动项目5. 参考 react-pdf readme加入依赖6. 结合 github readme 和官方文档产出 demo 代码…

OpenGL 色彩替换

目录 一.OpenGL 色彩替换 1.IOS Object-C 版本1.Windows OpenGL ES 版本2.Windows OpenGL 版本 二.OpenGL 色彩替换 GLSL Shader三.猜你喜欢 零基础 OpenGL ES 学习路线推荐 : OpenGL ES 学习目录 >> OpenGL ES 基础 零基础 OpenGL ES 学习路线推荐 : OpenGL ES 学习目录…