DS:带头双向循环链表的实现(超详细!!)

news2024/10/6 2:31:53

创作不易,友友们给个三连吧!!!

      博主的上篇文章介绍了链表,以及单链表的实现。

单链表的实现(超详细!!)
    其实单链表的全称叫做不带头单向不循环链表,本文会重点介绍链表的分类以及双链表的实现!

一、链表的分类

   链表的结构⾮常多样,组合起来就有8种(2 x 2 x 2)链表结构:

1.1 单向或者双向

    双向链表,即上一个结点保存着下一个结点的地址,且下一个结点保存着上一个结点的地址,即我们可以从头结点开始遍历,也可以从尾结点开始遍历

1.2 带头或者不带头 

     单链表中我们提到的“头结点”的“头”和“带头”链表的头是两个概念!单链表中提到的“头结点”指的是第一个有效的结点,“带头”链表里的“头”指的是无效的结点(即不保存任何有效的数据!)

    

1.3 循环或者不循环

     不循环的链表最后一个结点的next指针指向NULL,而循环的链表,最后一个结点的next指针指向第一个结点!!

      虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构: 单链表(不带头单向不循环链表)和 双向链表(带头双向循环链表)

1. 无头单向非循环链表:结构简单,⼀般不会单独⽤来存数据。实际中更多是作为其他数据结 构的⼦结构,如哈希桶、图的邻接表等等。另外这种结构在笔试⾯试中出现很多。

2. 带头双向循环链表:结构最复杂,⼀般⽤在单独存储数据。实际中使⽤的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使⽤代码实现以后会发现结构会带 来很多优势,实现反⽽简单了,后⾯我们代码实现了就知道了。

二、带头双向循环链表的结构

      带头链表⾥的头节点,实际为“哨兵位”,哨兵位节点不存储任何有效元素,只是站在这⾥“放哨的”

“哨兵位”存在的意义:遍历循环链表避免死循环。

三、双向链表结点结构体的创建

     与单链表结点结构体不同的是,双向链表的结点结构体多了一个前驱结点!!

typedef int LTDataType;//对类型进行重命名,后面可以通过修改存储其他数据类型
typedef struct ListNode
{
	LTDataType data;//保存的数据
	struct ListNode* prev;//指针保存前一个结点的地址
	struct ListNode* next;//指针保存后一个结点的地址
}LTNode;

四、带头双向循环链表的实现

4.1 新结点的申请

      涉及到需要插入数据,都需要申请新节点,所以优先封装一个申请新结点的函数!利用返回值返回该结点

LTNode* LTBuyNode(LTDataType x)
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		exit(1);//申请失败需要强制退出程序
	}
	//申请成功,则新节点的前驱结点和后驱结点都指向自己
	newnode->data = x;
	newnode->prev = newnode->next = newnode;
    return newnode;
}

4.2 初始化(哨兵位结点)

       对于双向链表来说,需要优先创建一个哨兵结点,和其他结点不同的是,该哨兵结点可以不存储数据,这里我们默认给他一个-1。并利用返回值返回该结点。

LTNode* LTInit()
{
	LTNode* phead = LTBuyNode(-1);//哨兵结点可以不存储数据,我们默认给个-1
	return phead;//返回哨兵结点
}

4.3 尾插 

       如图,因为这个一个循环链表,相当于我们要把新节点插在最后一个结点和哨兵结点之间,并且最后一一个结点可以用哨兵结点的前驱结点(phead->prev)就可以找到,然后建立phead phead->prev newnode的联系!

void LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = LTBuyNode(x);//申请新节点
	//建立phead phead->prev newnode的联系
	newnode->prev = phead->prev;
	newnode->next = phead;
	phead->prev->next = newnode;//尾结点的后继指针指向新节点
	phead->prev = newnode;//哨兵结点的前驱指针指向新结点
}

 单链表中我们的参数选择二级指针,为什么这里选择一级指针???

      对于单链表来收,单链表的头节点是会改变的,所以我们需要用二级指针,但是双链表的头节点相当于哨兵位,哨兵位是不需要被改变的,他是固定死的,所以我们选择了一级指针。(单链表改了完全头节点,但是双链表只会改变头结点的成员——prev和next)

注:phead->prev->next = newnode和phead->prev = newnode不能替换顺序,因为尾结点是通过头节点找到的,所以要优先让他与newnode建立联系,双链表虽然不需要像单链表一样找最后一个结点需要遍历链表,但是要十分注意修改指针指向的先后顺序!!

4.4 头插

       如图可知,相当于将新节点插入在头节点和头节点下一个结点之间,头节点下一个结点可以通过phead->next找到,然后建立phead、phead->next、newnode的联系!!

void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = LTBuyNode(x);//申请新节点
	//建立phead phead->next newnode的联系
	newnode->prev = phead;
	newnode->next = phead->next;
	phead->next->prev = newnode;//头节点的下一个结点的前驱指针指向新结点
	phead->next = newnode;//头节点的后继指针指向新节点
}

4.5 打印

      因为是循环链表,所以为了避免死循环打印,我们要设置一个指针接收头节点的下一个结点,然后往后遍历,直到遍历到头节点结束。

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

4.6 尾删

      由图可知,要建立phead和phead->prev->prev的联系,同时由于还要释放最后一个结点(phead->prev),所以在建立联系之前要现保存这个被释放的空间,等建立联系完再释放!!同时要注意一条规则,就是当链表中只有哨兵结点的时候,我们称该链表为空链表!因此如果链表只存在哨兵结点,那么删除是没有意义的,所以必须断言!

void LTPopBack(LTNode* phead)
{
	assert(phead);
	assert(phead->next != phead);//链表只有哨兵结点时删除没意义
	LTNode* del = phead->prev;//del记录最后一个结点
	del->prev->next = phead;//倒数第二个结点的后驱指针指向头结点
	phead->prev = del->prev;//头节点的前驱结点指向倒数第二个结点
	free(del);//释放最后一个结点
	del = NULL;
}

4.7 头删

       由图可知,要建立phead和phead->next->next的联系,同时由于还要释放第二个结点(phead->next),所以在建立联系之前要现保存这个被释放的空间,等建立联系完再释放!!

void LTPopFront(LTNode* phead)
{
	assert(phead);
	assert(phead->next != phead);//链表只有哨兵结点时删除没意义
	LTNode* del = phead->next;//del记录第二个结点
	del->next->prev = phead;//第二个结点的前驱指针指向头结点
	phead->next = del->next;//头节点的后驱指针指向第三个结点
	free(del);//释放第二个结点
	del = NULL;
}

4.8 查找

     涉及到对指定位置进行操作的时候,需要设置一个查找函数,根据我们需要的数据返回他的结点地址

LTNode* LTFind(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* pcur = phead->next;
	while (pcur != phead)//遍历链表
	{
		if (pcur->data == x)
			return pcur;//找到的话返回该结点
		pcur = pcur->next;
	}
	//循环结束还是没找到
	return NULL;
}

4.9 指定位置之后插入

        由图可知,指定位置插入相当于将新结点插入到指定位置(pos)和指定位置下一个结点的位置(pos->next),然后建立pos pos->next newnode的联系,而且这里用不到头节点!

void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);//保证pos为有效结点
	LTNode* newnode = LTBuyNode(x);//申请新节点
	//建立pos pos->next newnode的联系
	newnode->prev = pos;
	newnode->next = pos->next;
	pos->next->prev = newnode;//pos的后一个结点的前驱结点指向新节点
	pos->next = newnode;//pos结点的后继结点指向新结点
}

4.10 指定位置删除

       右图可知建立指定位置的前一个结点(pos->prev)和指定位置的后一个结点(pos->next)的联系,并释放pos。

void LTErase(LTNode* pos)
{
	assert(pos);//保证pos为有效结点
	pos->prev->next = pos->next;//pos的前一个结点的后继指针指向pos后一个结点
	pos->next->prev = pos->prev;//pos的后一个结点的前驱指针指向pos的前一个结点
	free(pos);//释放pos
	pos = NULL;
}

4.11 销毁链表

void LTDestroy(LTNode* phead)
{
	assert(phead);
	LTNode* pcur = phead->next;
	LTNode*next = NULL;
	while (pcur != phead)
	{
		next = pcur->next;
		free(pcur);
		pcur = next;
	}
	//除了头结点都释放完毕
	free(phead);
	//phead = NULL;//没有用!
}

为什么phead=NULL没有用??

       因为我们使用的是一级指针,这里相当于是值传递,值传递形参改变不了实参,所以将phead置空是没有意义的,其实如果这里使用二级指针,然后传地址就可以了,但是为了保持接口一致性,我们还是依照这种方法,但是phead=NULL必须在主函数中去使用,所以我们在调用销毁链表的函数的时候,别忘记了phead=NULL!!

五、带头双向循环链表实现的全部代码

List.h

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>

typedef int LTDataType;//对类型进行重命名,后面可以通过修改存储其他数据类型
typedef struct ListNode
{
	LTDataType data;//保存的数据
	struct ListNode* prev;//指针保存前一个结点的地址
	struct ListNode* next;//指针保存后一个结点的地址
}LTNode;


LTNode* LTBuyNode(LTDataType x);//申请新的链表结点
LTNode* LTInit();//初始化(申请一个哨兵结点)
void LTPushBack(LTNode* phead, LTDataType x);//尾插 (最后一个结点后插入或哨兵结点前插入)
void LTPushFront(LTNode* phead, LTDataType x);//头插 (哨兵结点后的插入)
void LTPrint(LTNode* phead);//打印
void LTPopBack(LTNode* phead);//尾删
void LTPopFront(LTNode* phead);//头删
LTNode* LTFind(LTNode* phead, LTDataType x);//查找
void LTInsert(LTNode* pos, LTDataType x);//指定位置之后插入
void LTErase(LTNode* pos);//指定位置删除
void LTDestroy(LTNode* phead);//销毁链表

List.c

#include"List.h"

LTNode* LTBuyNode(LTDataType x)
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		exit(1);//申请失败需要强制退出程序
	}
	//申请成功,则新节点的前驱结点和后驱结点都指向自己
	newnode->data = x;
	newnode->prev = newnode->next = newnode;
	return newnode;
}

LTNode* LTInit()
{
	LTNode* phead = LTBuyNode(-1);//哨兵结点可以不存储数据,我们默认给个-1
	return phead;//返回哨兵结点
}

void LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = LTBuyNode(x);//申请新节点
	//建立phead phead->prev newnode的联系
	newnode->prev = phead->prev;
	newnode->next = phead;
	phead->prev->next = newnode;//尾结点的后继结点指向新节点
	phead->prev = newnode;//哨兵结点的前驱指针指向新结点
}

void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = LTBuyNode(x);//申请新节点
	//建立phead phead->next newnode的联系
	newnode->prev = phead;
	newnode->next = phead->next;
	phead->next->prev = newnode;//头节点的下一个结点的前驱指针指向新结点
	phead->next = newnode;//头节点的后继指针指向新节点
}

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

void LTPopBack(LTNode* phead)
{
	assert(phead);
	assert(phead->next != phead);//链表只有哨兵结点时删除没意义
	LTNode* del = phead->prev;//del记录最后一个结点
	del->prev->next = phead;//倒数第二个结点的后驱指针指向头结点
	phead->prev = del->prev;//头节点的前驱结点指向倒数第二个结点
	free(del);//释放最后一个结点
	del = NULL;
}

void LTPopFront(LTNode* phead)
{
	assert(phead);
	assert(phead->next != phead);//链表只有哨兵结点时删除没意义
	LTNode* del = phead->next;//del记录第二个结点
	del->next->prev = phead;//第二个结点的前驱指针指向头结点
	phead->next = del->next;//头节点的后驱指针指向第三个结点
	free(del);//释放第二个结点
	del = NULL;
}

LTNode* LTFind(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* pcur = phead->next;
	while (pcur != phead)//遍历链表
	{
		if (pcur->data == x)
			return pcur;//找到的话返回该结点
		pcur = pcur->next;
	}
	//循环结束还是没找到
	return NULL;
}

void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);//保证pos为有效结点
	LTNode* newnode = LTBuyNode(x);//申请新节点
	//建立pos pos->next newnode的联系
	newnode->prev = pos;
	newnode->next = pos->next;
	pos->next->prev = newnode;//pos的后一个结点的前驱结点指向新节点
	pos->next = newnode;//pos结点的后继结点指向新结点
}

void LTErase(LTNode* pos)
{
	assert(pos);//保证pos为有效结点
	pos->prev->next = pos->next;//pos的前一个结点的后继指针指向pos后一个结点
	pos->next->prev = pos->prev;//pos的后一个结点的前驱指针指向pos的前一个结点
	free(pos);//释放pos
	pos = NULL;
}

void LTDestroy(LTNode* phead)
{
	assert(phead);
	LTNode* pcur = phead->next;
	LTNode*next = NULL;
	while (pcur != phead)
	{
		next = pcur->next;
		free(pcur);
		pcur = next;
	}
	//除了头结点都释放完毕
	free(phead);
	//phead = NULL;//没有用!
}

六、顺序表和链表的优缺点分析

1、存储空间

顺序表物理上连续

链表逻辑上连续,但是物理上不连续

2、随机访问

顺序表可以通过下标去访问

链表不可以直接通过下标去访问

3、任意位置插入或者删除元素

顺序表需要挪移元素,效率低

链表只需修改指针指向

4、插入

动态顺序表空间不够时需要扩容

链表没有容量的概念

5、应用场景

顺序表应用于元素高效存储+频繁访问的场景

链表应用于任意位置插入和删除频繁的场景

总之:没有绝对的优劣,都要各自适合的应用场景!!

 

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

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

相关文章

uni-app 接口封装,token过期,自动获取最新的token

一、文件路径截图 2、新建一个文件app.js let hosthttp://172.16.192.40:8083/jeecg-boot/ //本地接口 let myApi {login: ${host}wx/wxUser/login, //登录 } module.exports myApi 3、新建一个文件request.js import myApi from /utils/app.js; export const r…

Linux ---- Shell编程之函数与数组

目录 一、函数 1、函数的基本格式 2、查看函数列表 3、删除函数 4、函数的传参数 5、函数返回值 实验&#xff1a; 1.判断输入的ip地址正确与否 2. 判断是否为管理员用户登录 6、函数变量的作用范围 7、函数递归&#xff08;重要、难点&#xff09; 实验&#xff1…

P1024 [NOIP2001 提高组] 一元三次方程求解————C++

目录 [NOIP2001 提高组] 一元三次方程求解题目描述输入格式输出格式样例 #1样例输入 #1样例输出 #1 提示 解题思路Code运行结果 [NOIP2001 提高组] 一元三次方程求解 题目描述 有形如&#xff1a; a x 3 b x 2 c x d 0 a x^3 b x^2 c x d 0 ax3bx2cxd0 这样的一个一元…

【2024-01-27可用】NVM安装太慢,镜像地址失效

安装nvm时&#xff0c; Could not retrieve https://registry.npm.taobao.org/latest/SHASUMS256.txt. 解决如下 ### 具体配置 安装路径 root: D:\Program Files\nvm path: D:\Program Files\nodejs镜像地址 node_mirror: https://npmmirror.com/mirrors/node/ npm_mirror:…

STL容器大总结区分(上)

如图所示 ,按大小说明其重要性 那就先说两个最重要的: vector---数组 list-----链表 vector 基本概念 功能&#xff1a; vector 数据结构和 数组非常 相似 &#xff0c;也称为 单端数组 vector 与普通数组区别&#xff1a; 不同之处在于数组是静态空间&…

vue3添加pinia

概述&#xff1a;Pinia 是一个专为 Vue.js 开发的状态管理库。Vue.js 是一个流行的 JavaScript 框架&#xff0c;用于构建用户界面。Pinia 旨在提供一个简单、灵活且性能高效的状态管理方案&#xff0c;使开发者能够更容易地管理应用的状态。 以下是 Pinia 的一些特点和概念&a…

在 React 组件中使用 JSON 数据文件,怎么去读取请求数据呢?

要在 React 组件中使用 JSON 数据&#xff0c;有多种方法。 常用的有以下几种方法&#xff1a; 1、直接将 JSON 数据作为一个变量或常量引入组件中。 import jsonData from ./data.json;function MyComponent() {return (<div><h1>{jsonData.title}</h1>&…

Vue3中ElementPlus组件二次封装,实现原组件属性、插槽、事件监听、方法的透传

本文以el-input组件为例&#xff0c;其它组件类似用法。 一、解决数据绑定问题 封装组件的第一步&#xff0c;要解决的就是数据绑定的问题&#xff0c;由于prop数据流是单向传递的&#xff0c;数据只能从父流向子&#xff0c;子想改父只能通过提交emit事件通知父修改。 父&a…

第十八讲_HarmonyOS应用开发实战(实现电商首页)

HarmonyOS应用开发实战&#xff08;实现电商首页&#xff09; 1. 项目涉及知识点罗列2. 项目目录结构介绍3. 最终的效果图4. 部分源码展示 1. 项目涉及知识点罗列 掌握HUAWEI DevEco Studio开发工具掌握创建HarmonyOS应用工程掌握ArkUI自定义组件掌握Entry、Component、Builde…

Leetcode—2942. 查找包含给定字符的单词【简单】

2023每日刷题&#xff08;一零一&#xff09; Leetcode—2942. 查找包含给定字符的单词 实现代码 class Solution { public:vector<int> findWordsContaining(vector<string>& words, char x) {vector<int> ans;for(int i 0; i < words.size(); i)…

JDK8新特性:Stream

Stream 认识Stream 也叫Stream流&#xff0c;是jdk8开始新增的一套API&#xff08;java.util.stream.*&#xff09;&#xff0c;可以用于操作集合或者数组的数据。优势&#xff1a;Stream流大量的结合了Lambda的语法风格来编程&#xff0c;提供了一种更强大&#xff0c;更加简…

TCS34725使用记录

TCS34725使用记录 1、IIC通信 1、tcs34725硬件通信采用标准的IIC协议&#xff1b; 2、在寄存器读写上需要注意一下&#xff0c;在读写寄存时&#xff0c;需要将地址最高位置1&#xff1b; I2C_SendByte(reg|0x80);//一般的iic操作寄存器都是直接传入reg 2、配置与数据读取 …

简单介绍----微服务和Spring Cloud

微服务和SpringCloud 1.什么是微服务&#xff1f; 微服务是将一个大型的、单一的应用程序拆分成多个小型服务&#xff0c;每个服务负责实现特定的业务功能&#xff0c;并且可以通过网络通信与其他服务通信。微服务的优点是开发更灵活&#xff08;不同的微服务可以使用不同的开…

HTML 曲线图表特效

下面是代码 <!doctype html> <html> <head> <meta charset"utf-8"> <title>基于 ApexCharts 的 HTML5 曲线图表DEMO演示</title><style> body {background: #000524; }#wrapper {padding-top: 20px;background: #000524;b…

基于InceptionV2/InceptionV3/Xception不同参数量级模型开发构建中草药图像识别分析系统,实验量化对比不同模型性能

最近正好项目中在做一些识别相关的内容&#xff0c;我也陆陆续续写了一些实验性质的博文用于对自己使用过的模型进行真实数据的评测对比分析&#xff0c;感兴趣的话可以自行移步阅读即可&#xff1a; 《移动端轻量级模型开发谁更胜一筹&#xff0c;efficientnet、mobilenetv2、…

什么情况会发生Full GC?如何避免频繁Full GC?Minor GC、Major GC 和 Full GC区别?

Minor GC、Major GC 和 Full GC区别&#xff1f; Minor GC、Major GC和Full GC是垃圾回收中的三个重要概念&#xff0c;它们描述了垃圾回收的不同阶段和范围&#xff1a; Minor GC&#xff08;新生代GC&#xff09;&#xff1a; Minor GC主要关注清理年轻代&#xff08;Young …

ansible处理多台机器部署基础环境

本次以多台机器需部署zabbix客户端为例&#xff1a; 机器先做免密互信&#xff0c;ansible主机上执行ssh-keygen,一路回车&#xff0c;然后将公钥发送给需管理的主机&#xff1a; ssh-copy-id rootIP 1、编辑hosts文件&#xff0c;添加需配置的主机IP&#xff0c;并测试连通…

LC每日一题记录 2861. 最大合金数

题干 思路 所有合金都需要由同一台机器制造&#xff0c;因此我们可以枚举使用哪一台机器来制造合金。 对于每一台机器&#xff0c;我们可以使用二分查找的方法找出最大的整数 xxx&#xff0c;使得我们可以使用这台机器制造 xxx 份合金。找出所有 xxx 中的最大值即为答案。 代…

鸿蒙ArkUI 宫格+列表+HttpAPI实现

鸿蒙ArkUI学习实现一个轮播图、一个九宫格、一个图文列表。然后请求第三方HTTPAPI加载数据&#xff0c;使用了axios鸿蒙扩展库来实现第三方API数据加载并动态显示数据。 import {navigateTo } from ../common/Pageimport axios, {AxiosResponse } from ohos/axiosinterface IDa…

C语言入门(二)、每日Linux(三)——gcc命令,通过gcc命令熟悉C语言程序实现的过程

使用gcc编译C语言程序 C语言程序实现的过程gcc命令基础用法常用选项编译和汇编选项&#xff1a;优化选项&#xff1a;调试选项&#xff1a;链接选项&#xff1a;警告选项&#xff1a; 实验对于-o选项 通过gcc命令熟悉C语言程序的执行过程1.预处理2.编译阶段3.汇编阶段4.链接阶段…