【数据结构和算法】实现带头双向循环链表(最复杂的链表)

news2024/11/27 9:46:36

前文,我们实现了认识了链表这一结构,并实现了无头单向非循环链表,接下来我们实现另一种常用的链表结构,带头双向循环链表。如有仍不了解单向链表的,请看这一篇文章(7条消息) 【数据结构和算法】认识线性表中的链表,并实现单向链表_小王学代码的博客-CSDN博客

目录

前言

一、带头双向循环链表是什么?

二、实现带头双向循环链表

1.结构体和要实现函数

2.初始化和打印链表

3.头插和尾插

4.头删和尾删

5.查找和返回结点个数

6.在pos位置之前插入结点

7.删除指定pos结点

8.摧毁链表

三、完整代码

1.DSLinkList.h

2.DSLinkList.c

3.test.c

总结


前言

带头双向循环链表,是链表中最为复杂的一种结构,我们主要实现的功能为,头插尾插,头删尾删,初始化、打印、指定pos位置插入结点或者删除结点、寻找结点、摧毁链表等函数。


一、带头双向循环链表是什么?

如图所示:


二、实现带头双向循环链表

1.结构体和要实现函数

结构体如下:

typedef struct DSLDataType;
//定义双向链表的结构体

//双向链表每一个节点都有一个前驱节点和后继节点,可以根据节点获得其前后的节点
typedef struct DSListNode {
	struct DSListNode* prev;//前驱节点
	struct DSListNode* next;//后继节点
	int value;//数据域
}DSLNode;//重定义

相比单向链表,多一个指针域prev,指向前面的结点地址

实现功能的函数:

//初始化
DSLNode*ListInit();
//打印链表
void ListPrint(DSLNode* ps);
//尾部插入
void ListPushBack(DSLNode* ps, int data);
//头部插入
void ListPushFront(DSLNode* ps, int data);
//尾部删除
void ListPopBack(DSLNode* ps);
//头部删除
void ListPopFront(DSLNode* ps);
//判空
bool ListEmpty(DSLNode* ps);
//返回链表节点个数
int Listsize(DSLNode* ps);
//寻找指定元素,返回对应的节点
DSLNode* ListFind(DSLNode* ps, int data);
//在pos之前插入节点
void ListInsert(DSLNode* pos, int data);
//删除pos位置结点
void ListEarse(DSLNode* pos);
//摧毁链表
void ListDestory(DSLNode* ps);

2.初始化和打印链表

//对于函数的实现
//初始化
DSLNode* ListInit() {
	//初始化创建链表
	//创建新节点
	DSLNode* num = (DSLNode*)malloc(sizeof(DSLNode));
	//判断是否创建成功
	if (num == NULL) {
		perror("malloc fail\n");
		exit(-1);
	}
	num->prev = num;
	num->next = num;//作为头节点,前驱和后驱结点都指向自己
	return num;
}

//打印链表
void ListPrint(DSLNode* ps) {
	assert(ps);//断言
	DSLNode* cur = ps->next;
	printf("phead->");
	while (cur != ps) {//双向链表,回到ps,就表示转了一圈
		printf("%d->", cur->value);
		cur = cur->next;
	}
	printf("NULL\n");
}

3.头插和尾插

尾插如图所示:

代码如下:


DSLNode* CreatNode(int data) {//创建新节点
	DSLNode* cur = (DSLNode*)malloc(sizeof(DSLNode));
	if (cur == NULL) {
		perror("malloc fail\n");
		exit(-1);
	}
	cur->next = cur->prev = NULL;
	cur->value = data;
}
//尾部插入
void ListPushBack(DSLNode* ps, int data) {
	assert(ps);//断言
	DSLNode* newnode = CreatNode(data);
	DSLNode* tail = ps->prev;//把头节点之前的那个地址给tail
	tail->next = newnode;//将ps之前的那个数值,实际上是这个双向链表的最后一个元素的位置,的next指针指向新节点
	newnode->prev = tail;//新节点前后为 tail和ps
	newnode->next = ps;
	ps->prev = newnode;//tail的两个指针域都指向完成,就只剩下ps的前驱指针

}

头插如图所示:
 代码如下:


//头部插入
void ListPushFront(DSLNode* ps, int data) {
	assert(ps);
	//头部插入就是将ps之后的一个地址给临时变量tail
	DSLNode* tail = ps->next;
	DSLNode* newnode = CreatNode(data);//创建新节点
	ps->next->prev = newnode;
	newnode->next = ps->next;
    newnode->prev = ps;
    ps->next = newnode;
}

4.头删和尾删

尾部删除如图所示:

代码如下:

//判空
bool ListEmpty(DSLNode* ps) {
	assert(ps);//断言
	return ps->next == ps;//如果是只有一个ps头节点,则表示为空,返回true,否则返回false

}
//尾部删除
void ListPopBack(DSLNode* ps) {

	assert(ps);//断言
	assert(!ListEmpty(ps));//先判断是否为空
	DSLNode* cur = ps->prev;
	//将cur的next值为NULL
	ps->prev = ps->prev->prev;
	ps->prev->next = ps;
	//这是直接略过尾部最后一个元素
	//跳过ps之前的一个节点,先让其左右节点相连,然后释放这个地址
	free(cur);
	cur = NULL;
}

 头部删除如图所示:

代码如下:

//头部删除
void ListPopFront(DSLNode* ps) {
	assert(ps);
	assert(!ListEmpty(ps));
	DSLNode* cur = ps->next;
	//头删  是将头节点之后的第一个节点删除释放空间
	ps->next = ps->next->next;
	ps->next->prev = ps;
	free(cur);
	cur = NULL;
}

5.查找和返回结点个数

查找:返回一个结点,根据data数值域,来返回第一个遇到的结点cur,若没有返回NULL

//返回链表节点个数
int Listsize(DSLNode* ps) {
	assert(ps);
	int count = 0;
	DSLNode* cur = ps->next;
	while (cur != ps) {
		count++;
		cur = cur->next;
	}
	return count;
}
//查找
DSLNode* ListFind(DSLNode* ps, int data) {
	assert(ps);
	DSLNode* cur = ps->next;
	while (cur != ps) {
		if (cur->value == data) {
			return cur;
		}
	}
	return NULL;//NULL表示不存在
}

6.在pos位置之前插入结点

如图所示:

 代码如下:

//插入节点
void ListInsert(DSLNode* pos, int data) {

	assert(pos);
	//pos三种可能
	//1.空链表
	//2.只有一个节点
	DSLNode* cur = pos->prev;
	//双向链表可以直接找到pos之前的位置,单向链表只能进行循环
	DSLNode* newnode = CreatNode(data);
	pos->prev = newnode;//把新节点newnode的位置给pos的prev
	newnode->prev = cur;//cur表示的是创建new节点之前的时候pos之前的结点
	cur->next = newnode;
	newnode->next = pos;
	//另一种不使用cur的方法
	//newnode->next = pos;
	//pos->prev->next = newnode;
	//newnode->prev = pos->prev;
	//pos->prev = newnode;
}

7.删除指定pos结点

如图所示:

代码如下:


//删除指针
void ListEarse(DSLNode* pos) {
	assert(pos);
	DSLNode* cur = pos->prev;
	DSLNode* tail = pos->next;
	cur->next = tail;//两者相互指向,最后释放pos空间
	tail->prev = cur;
	free(pos);
	pos = NULL;
}

8.摧毁链表

两种方式,可以直接使用二级指针,也可以使用一级指针一个一个free和NULL

//摧毁链表
void ListDestory(DSLNode* ps) {
	//可以设计二级指针直接将ps地址滞空为NULL
	//这里还是使用一级指针
	assert(ps);
	DSLNode* cur = ps->next;
	while (cur != ps) {
		DSLNode* tail = cur->next;//这个地方就是一个精彩的地方
		free(cur);//使用临时变量tail得到cur的下一个地址,然后再free cur
		cur = tail;//tail这个时候就是cur 的下一个结点
	}
	free(ps);
	ps = NULL;

}
void ListDestory2(DSLNode** ps) {
	assert(ps);
	free(ps);//二级指针直接free
	*ps = NULL;
}

三、完整代码

1.DSLinkList.h

#define _CRT_SECURE_NO_WARNINGS

#include<stdio.h>
#include<stdlib.h>
#include<malloc.h>
#include<assert.h>
#include<stdbool.h>

typedef struct DSLDataType;
//定义双向链表的结构体

//双向链表每一个节点都有一个前驱节点和后继节点,可以根据节点获得其前后的节点
typedef struct DSListNode {
	struct DSListNode* prev;//前驱节点
	struct DSListNode* next;//后继节点
	int value;//数据域
}DSLNode;//重定义

//创建头节点,并将tail和head都指向第一个节点
struct DSList {
	struct DSListNode* tail;
	struct DSListNode* head;
	unsigned int len;//表示链表的长度
};
//初始化
DSLNode*ListInit();
//打印链表
void ListPrint(DSLNode* ps);
//尾部插入
void ListPushBack(DSLNode* ps, int data);
//头部插入
void ListPushFront(DSLNode* ps, int data);
//尾部删除
void ListPopBack(DSLNode* ps);
//头部删除
void ListPopFront(DSLNode* ps);
//判空
bool ListEmpty(DSLNode* ps);
//返回链表节点个数
int Listsize(DSLNode* ps);
//寻找指定元素,返回对应的节点
DSLNode* ListFind(DSLNode* ps, int data);
//在pos之前插入节点
void ListInsert(DSLNode* pos, int data);
//删除pos位置结点
void ListEarse(DSLNode* pos);
//摧毁链表
void ListDestory(DSLNode* ps);

2.DSLinkList.c

#define _CRT_SECURE_NO_WARNINGS

#include"DSLinkList.h"

//对于函数的实现
//初始化
DSLNode* ListInit() {
	//初始化创建链表
	//创建新节点
	DSLNode* num = (DSLNode*)malloc(sizeof(DSLNode));
	//判断是否创建成功
	if (num == NULL) {
		perror("malloc fail\n");
		exit(-1);
	}
	num->prev = num;
	num->next = num;
	return num;
}

//打印链表
void ListPrint(DSLNode* ps) {
	assert(ps);//断言
	DSLNode* cur = ps->next;
	printf("phead->");
	while (cur != ps) {//双向链表,回到ps,就表示转了一圈
		printf("%d->", cur->value);
		cur = cur->next;
	}
	printf("NULL\n");
}

DSLNode* CreatNode(int data) {//创建新节点
	DSLNode* cur = (DSLNode*)malloc(sizeof(DSLNode));
	if (cur == NULL) {
		perror("malloc fail\n");
		exit(-1);
	}
	cur->next = cur->prev = NULL;
	cur->value = data;
}
//尾部插入
void ListPushBack(DSLNode* ps, int data) {
	assert(ps);//断言
	DSLNode* newnode = CreatNode(data);
	DSLNode* tail = ps->prev;//把头节点之前的那个地址给tail
	tail->next = newnode;//将ps之前的那个数值,实际上是这个双向链表的最后一个元素的位置,的next指针指向新节点
	newnode->prev = tail;//新节点前后为 tail和ps
	newnode->next = ps;
	ps->prev = newnode;//tail的两个指针域都指向完成,就只剩下ps的前驱指针

}

//头部插入
void ListPushFront(DSLNode* ps, int data) {
	assert(ps);
	//头部插入就是将ps之后的一个地址给临时变量tail
	DSLNode* tail = ps->next;
	DSLNode* newnode = CreatNode(data);//创建新节点
	ps->next->prev = newnode;
	newnode->next = ps->next;
    newnode->prev = ps;
    ps->next = newnode;
}

//判空
bool ListEmpty(DSLNode* ps) {
	assert(ps);//断言
	return ps->next == ps;//如果是只有一个ps头节点,则表示为空,返回true,否则返回false

}

//返回链表节点个数
int Listsize(DSLNode* ps) {
	assert(ps);
	int count = 0;
	DSLNode* cur = ps->next;
	while (cur != ps) {
		count++;
		cur = cur->next;
	}
	printf("\n");
	return count;
}
//尾部删除
void ListPopBack(DSLNode* ps) {

	assert(ps);//断言
	assert(!ListEmpty(ps));//先判断是否为空
	DSLNode* cur = ps->prev;
	//将cur的next值为NULL
	ps->prev = ps->prev->prev;
	ps->prev->next = ps;
	//这是直接略过尾部最后一个元素
	//跳过ps之前的一个节点,先让其左右节点相连,然后释放这个地址
	free(cur);
	cur = NULL;
}
//头部删除
void ListPopFront(DSLNode* ps) {
	assert(ps);
	assert(!ListEmpty(ps));
	DSLNode* cur = ps->next;
	//头删  是将头节点之后的第一个节点删除释放空间
	ps->next = ps->next->next;
	ps->next->prev = ps;
	free(cur);
	cur = NULL;
}
//查找
DSLNode* ListFind(DSLNode* ps, int data) {
	assert(ps);
	DSLNode* cur = ps->next;
	while (cur != ps) {
		if (cur->value == data) {
			return cur;
		}
	}
	return NULL;//NULL表示不存在
}
//插入节点
void ListInsert(DSLNode* pos, int data) {

	assert(pos);
	//pos三种可能
	//1.空链表
	//2.只有一个节点
	DSLNode* cur = pos->prev;
	//双向链表可以直接找到pos之前的位置,单向链表只能进行循环
	DSLNode* newnode = CreatNode(data);
	pos->prev = newnode;//把新节点newnode的位置给pos的prev
	newnode->prev = cur;//cur表示的是创建new节点之前的时候pos之前的结点
	cur->next = newnode;
	newnode->next = pos;
	//另一种不使用cur的方法
	//newnode->next = pos;
	//pos->prev->next = newnode;
	//newnode->prev = pos->prev;
	//pos->prev = newnode;
}


//删除指针
void ListEarse(DSLNode* pos) {
	assert(pos);
	DSLNode* cur = pos->prev;
	DSLNode* tail = pos->next;
	cur->next = tail;//两者相互指向,最后释放pos空间
	tail->prev = cur;
	free(pos);
	pos = NULL;
}
//摧毁链表
void ListDestory(DSLNode* ps) {
	//可以设计二级指针直接将ps地址滞空为NULL
	//这里还是使用一级指针
	assert(ps);
	DSLNode* cur = ps->next;
	while (cur != ps) {
		DSLNode* tail = cur->next;
		free(cur);
		cur = tail;
	}
	free(ps);
	ps = NULL;

}
void ListDestory2(DSLNode** ps) {
	assert(ps);
	free(ps);
	*ps = NULL;
}

3.test.c

#define _CRT_SECURE_NO_WARNINGS
#include"DSLinkList.h"

void test()
{
	DSLNode* phead = ListInit();//初始化
	
	ListPushBack(phead, 1);
	ListPushBack(phead, 2);
	ListPushBack(phead, 3);
	ListPushBack(phead, 4);
	ListPushBack(phead, 5);//检验尾插
	ListPrint(phead);//打印

	ListPushFront(phead, 10);
	ListPushFront(phead, 20);
	ListPushFront(phead, 30);
	ListPushFront(phead, 40);
	ListPushFront(phead, 50);//检验头插
	ListPrint(phead);//打印
	printf("%d\n", Listsize(phead));//检验返回结点个数

	ListPopBack(phead);
	ListPopBack(phead);
	ListPopBack(phead);
	ListPopBack(phead);
	ListPopBack(phead);//尾删
	ListPopFront(phead);
	ListPopFront(phead);
	ListPopFront(phead);
	ListPopFront(phead);
	ListPopFront(phead);//头删
	ListPrint(phead);//打印
	ListInsert(phead->next, 10);//pos指定结点之前插入newnode新节点
	ListPrint(phead);//打印
}
int main()
{
	test();
	return 0;
}

总结

我们本文主要讲解了,链表中最为复杂的带头双向循环链表的使用和代码实现,实现了头插尾插,头删尾删,初始化、打印、指定pos位置插入结点或者删除结点、寻找结点、摧毁链表等函数。并做了板书图示解释相应函数的流程,附带完整代码,以便帮助大家更好的理解。

接下来我们就要进行栈和队列的学习,本文不足之处,欢迎大佬指导一二,让我们开始新章节的学习!!!

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

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

相关文章

Spring Boot之SpringSecurity学习

文章目录一 SpringSecurity简介二 实战演示0. 环境 介绍1. 新建一个初始的springboot项目2. 导入thymeleaf依赖3. 导入静态资源4. 编写controller跳转5. 认证和授权6. 权限控制和注销7. 记住登录8. 定制登录页面三 完整代码3.1 pom配置文件3.2 RouterController.java3.3 Securi…

那些面试官口中常常提到b树(MySQL索引底层数据结构)

各种常见树1.树的基本概念2.二叉树3.b树4.b树5.b树与b树的对比5.MySQL索引底层数据结构1.树的基本概念 树的特点&#xff1a;有一个树根&#xff0c;树根上又有很多枝干&#xff0c;枝干上又有很多树枝&#xff0c;树枝上又有很多叶子 树最为一种数据结构也有相似特点 树是一个…

【计算机网络(考研版)】第二站:物理层(一)

前言 如下图所示&#xff0c;这是我们之前所说的数据流动示意图 我们将按照从下向上的结构进行学习。这一讲学习第一层物理层。物理层关注在一条通信信道上传输原始比特&#xff0c;即无论面对什么样的传输介质(有线或者无线)都可以传输比特流&#xff0c;物理层的作用正是要尽…

Python3 函数

函数是组织好的&#xff0c;可重复使用的&#xff0c;用来实现单一&#xff0c;或相关联功能的代码段。 函数能提高应用的模块性&#xff0c;和代码的重复利用率。你已经知道Python提供了许多内建函数&#xff0c;比如print()。但你也可以自己创建函数&#xff0c;这被叫做用户…

Node require 正解

require 实现原理 流程概述 步骤1&#xff1a;尝试执行代码require("./1"). 开始调用方法require.步骤2&#xff1a;此时会得到filename&#xff0c;根据filename 会判断缓存中是否已经加载模块&#xff0c;如果加载完毕直接返回&#xff0c;反之继续执行步骤3&…

JavaScript 的数据类型

JavaScript 的数据类型 基本数据类型&#xff08;值类型&#xff09; Number&#xff08;包含小数、整数、负数、科学计数法&#xff09; <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><meta http-equiv"…

【Linux】六、Linux 基础IO(四)|动态库和静态库

目录 十一、动态库和静态库 11.1 动态库和静态库定义 11.2 动静态库的基本原理 11.3 静态库的打包与使用 11.3.1 静态库的打包 11.3.2 静态库的使用 11.4 动态库的打包与使用 11.4.1 动态库的打包 11.4.2 动态库的使用 11.5 动态库的加载 十一、动态库和静态库 11.1…

CB2-2CARD的openSUSE安装NAS环境配置

CB2-2CARD的openSUSE安装&NAS环境配置1. 简介2. 规格3. 系统安装3.1 Linux/Unix稳定镜像3.2 基础功能更新&安装3.2.1 更新源3.2.2 升级系统3.2.3 基础功能安装3.3 OpenSUSE系统情况3.3.1 源操作命令3.3.2 源镜像4. 需求 & 配置4.1 MiniDLNAStep 1&#xff1a;安装M…

Day870.全局锁和表锁 -MySQL实战

全局锁和表锁 Hi&#xff0c;我是阿昌&#xff0c;今天学习记录的是关于全局锁和表锁的内容。 数据库锁设计的初衷是处理并发问题。 作为多用户共享的资源&#xff0c;当出现并发访问的时候&#xff0c;数据库需要合理地控制资源的访问规则。锁就是用来实现这些访问规则的重…

数据结构 | C++ | 并查集原理讲解与模拟实现 | 并查集的相关习题

文章目录前言并查集原理并查集的模拟实现leetcode练习省份数量等式方程的可满足性前言 并查集通常会作为高阶数据结构的一个子结构使用&#xff0c;虽然原理不是很难&#xff0c;但其思想值得我们好好学习 并查集原理 并查集是一种树形结构&#xff0c;其保存了多个集合&…

【Maven】多环境配置与应用

目录 1. 多环境配置作用 问题导入 2. 多环境配置步骤 2.1 定义多环境 2.2 使用多环境&#xff08;构建过程&#xff09; 3. 跳过测试&#xff08;了解&#xff09; 问题导入 3.1 应用场景 3.2 跳过测试命令 3.3 细粒度控制跳过测试 1. 多环境配置作用 问题导入 多…

LeetCode 2331. 计算布尔二叉树的值

给你一棵 完整二叉树 的根&#xff0c;这棵树有以下特征&#xff1a; 叶子节点 要么值为 0 要么值为 1 &#xff0c;其中 0 表示 False &#xff0c;1 表示 True 。 非叶子节点 要么值为 2 要么值为 3 &#xff0c;其中 2 表示逻辑或 OR &#xff0c;3 表示逻辑与 AND 。 计算…

【推荐系统】User-Item CF:NGCF

&#x1f4a1; 本次解读的文章是 2019 年发表于 SIGIR 的一篇基于图卷积神经网络的用户物品协同过滤推荐算法论文&#xff0c; 论文将用户-物品交互信息建模为二分图&#xff0c;提出了一个基于二分图的推荐框架 Neural Graph Collaborative Filtering&#xff08;NGCF&#xf…

基于nodejs+vue的社区问答网站与设计

目 录 摘要 I Abstract II 1 绪论 1 1.1 选题背景 1 1.2 选题意义 1 1.3 研究内容 2 2 相关技术介绍 3 3 系统分析 5 3.1可行性分析 5 3.2 需求分析 5 3.2.1非功能性需求 5 3.2.2功能需求 6 3.3 系统用例 6 3.3.1 会员功能需求 6 …

【C++修炼之路】13. priority_queue及仿函数

每一个不曾起舞的日子都是对生命的辜负 stack&&queue一 . priority_queue介绍二. priority_queue的使用三. 仿函数3.1 仿函数的介绍3.2 仿函数的好处四.priority_queue模拟实现五.仿函数之日期比较一 . priority_queue介绍 priority_queue文档介绍 优先队列是一种容器…

机器学习实战(第二版)读书笔记(2)—— LSTMGRU

刚接触深度学习半年的时间&#xff0c;这期间有专门去学习LSTM &#xff0c;这几天读机器学习实战这本书的时候又遇到了&#xff0c;感觉写的挺好的&#xff0c;所以准备结合本书写一下总结方便日后回顾。如有错误&#xff0c;欢迎批评指正。 一、LSTM 优势&#xff1a;可在一…

ApiSix 开启SkyWalking插件,实现链路信息追踪

ApiSix 开启SkyWalking插件&#xff0c;实现链路信息追踪1 ApiSix开启SkyWalking插件1.1 修改config.yml配置文件1.2 在路由中开启SkyWalking插件2 创建两个SpringBoot服务&#xff0c;接入SkyWalking2.1 下载skywalking agent2.2 创建服务2.3 测试SkyWalking1 ApiSix开启SkyWa…

【链表】反转链表

BM1反转链表 描述 给定一个单链表的头结点pHead(该头节点是有值的&#xff0c;比如在下图&#xff0c;它的val是1)&#xff0c;长度为n&#xff0c;反转该链表后&#xff0c;返回新链表的表头。 数据范围&#xff1a; 0\leq n\leq10000≤n≤1000 要求&#xff1a;空间复杂度…

飞行员兄弟( 二进制枚举) --《算法竞赛进阶指南》

题目如下&#xff1a; 输入样例&#xff1a; --- ---- ---- ---输出样例&#xff1a; 6 1 1 1 3 1 4 4 1 4 3 4 4思路 or 题解&#xff1a; 数据量很小可以直接进行 搜索 在这里我使用 二进制枚举 的方法去寻找答案 时间复杂度&#xff1a;O(2n)O(2^n)O(2n) 我们二进制枚举…

计算机相关专业混体制的解决方案(事业编之学校与医院)

文章目录1、教师行业1.1 中小学教师资格1.2 高校教师资格证1.3 应聘中小学教师1.4 待遇2、医疗行业2.1 如何进入医院信息科2.2 医院信息科工作内容2.3 医院信息科待遇主要介绍三个方面&#xff1a; 1、招聘条件&#xff0c;要求是什么。 2、工作内容&#xff0c;需要我做什么工…