数据结构——双向循环链表

news2024/11/26 20:31:08

目录

前言

一、链表的分类

二、双向循环链表

2.1 开辟新的节点

2.2 链表初始化

2.3 打印链表

2.4 链表的尾插

2.5 链表的头插

2.6 链表的尾删

2.7 链表的头删

2.8 查找链表

2.9 在pos位置之后插入数据

2.10 删除pos位置的数据

三、完整代码实现

四、顺序表和双向链表的优缺点分析

总结


前言

我们之前讲了顺序表和单链表,它们但是线性表的一种,今天我们来讲链表中的双向循环链表。


一、链表的分类

链表的结构⾮常多样,以下情况组合起来就有8种(2 x 2 x 2)链表结构:
其中分为:

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

二、双向循环链表

我们之前讲了单链表,今天我们来实现双向带头循环链表。

接口实现:

//list.h

//链表初始化
//void LTInit(LTNode** pphead);
LTNode* LTInit();

//打印链表
void LTPrint(LTNode* phead);

//尾插 在最后有效节点或者哨兵位前插入都是尾插
void LTPushBack(LTNode* phead, LTDataType x);

//头插 在第一个有效节点之前插入
void LTPushFront(LTNode* phead, LTDataType x);

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

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

//查找节点
LTNode* LTFind(LTNode* phead, LTDataType x);

//在pos后面插入数据
void LTInsert(LTNode* pos, LTDataType x);

//删除pos位置的数据
void LTErase(LTNode* pos);

//销毁链表 保持接口一致性
//void LTDesTroy(LTNode** pphead);

void LTDesTroy(LTNode* phead);

在实现代码前,我们要先用结构体来定义链表的类型。由于是循环链表,所以我们需要两个指针,分别指向节点的前驱节点和后继节点。

typedef int LTDataType;
//双向循环链表结构体类型
typedef struct ListNode {
	LTDataType data;
	struct ListNode* prev;//前驱节点
	struct ListNode* next;//后继节点
}LTNode;

2.1 开辟新的节点

在初始化之前,我们来实现开辟新的节点

//新的节点
LTNode* LTBuyNode(LTDataType x) {
	//为新的节点开辟空间
	LTNode* newNode = (LTNode*)malloc(sizeof(LTNode));
	if (newNode == NULL) {
		perror("malloc fail!");
		exit(1);
	}
	newNode->data = x;
	//让新节点头尾相连
	newNode->next = newNode->prev = newNode;
	return newNode;
}

2.2 链表初始化

链表的初始化我们可以有两种写法:

//写法一 传入头节点的地址
void LTInit(LTNode** pphead) {
	assert(pphead);
    //哨兵位
	*pphead = LTBuyNode(-1);
}
//写法二 返回哨兵位,不传入值
LTNode* LTInit() {
	LTNode* pphead = LTBuyNode(-1);
	return pphead;
}

我们给哨兵位的值赋为-1(任意都可以,哨兵位不作为有效数据)。

我们更推荐使用第二种方法,因为保持接口的一致性。

2.3 打印链表

如果我们往链表中插入数据,可以通过打印知道是否插入成功

void LTPrint(LTNode* phead) {
	assert(phead);
	//从哨兵位下一个节点开始打印
	LTNode* pcur = phead->next;
	while (pcur != phead) {
		printf("%d->", pcur->data);
		pcur = pcur->next;
	}
	printf("\n");
}

其中要注意的是循环开始是从哨兵位下一个节点开始的,结束条件是pcur走到哨兵位,即遍历了整个链表。

2.4 链表的尾插

void LTPushBack(LTNode* phead, LTDataType x) {
	assert(phead);
    //要插入的新的节点
	LTNode* newNode = LTBuyNode(x);
	
    //phead phead->prev newNode
	
	newNode->next = phead;
	newNode->prev = phead->prev;

	phead->prev->next = newNode;
	phead->prev = newNode;
}

尾插入一个节点,我们要改变的是哨兵位,哨兵位的前驱节点(即尾节点),新节点三个节点的指向

2.5 链表的头插

void LTPushFront(LTNode* phead, LTDataType x) {
	assert(phead);
    //插入的新节点
	LTNode* newNode = LTBuyNode(x);

	//phead phead->next newNode

	newNode->next = phead->next;
	newNode->prev = phead;

	phead->next->prev = newNode;
	phead->next = newNode;
}

头插入一个节点,我们要改变的是哨兵位,哨兵位的后继节点(即第一个有效数据节点),新节点三个节点的指向

2.6 链表的尾删

void LTPopBack(LTNode* phead) {
	assert(phead);
    //链表不为空
	assert(phead->next != phead);

	//phead phead->prev->prev(prev) phead->prev(del)
	LTNode* prev = phead->prev->prev;
	LTNode* del = phead->prev;

	phead->prev = prev;
	prev->next = phead;
	free(del);
	del = NULL;
}

尾部删除一个节点,我们要改变的是删除元素的前驱节点,哨兵位的指向,最后释放删除节点

2.7 链表的头删

void LTPopFront(LTNode* phead) {
	assert(phead);
    //链表不为空
	assert(phead->next != phead);

	//phead phead->next(del) phead->next->next(next)
	LTNode* del = phead->next;
	LTNode* next = phead->next->next;

	phead->next = next;
	next->prev = phead;
	free(del);
	del = NULL;
}

头部删除一个节点,我们要改变的是哨兵位,删除节点的后继节点,最后释放删除节点

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

如果存在返回当前节点,不存在返回空。

2.9 在pos位置之后插入数据

void LTInsert(LTNode* pos, LTDataType x) {
	assert(pos);
	LTNode* newNode = LTBuyNode(x);

	//newNode pos pos->next

	newNode->next = pos->next;
	newNode->prev = pos;

	pos->next->prev = newNode;
	pos->next = newNode;
}

我们要改变新节点,pos节点,pos节点的后继节点的指向。

注意:我们要先把pos的后继节点的前驱节点指向新节点,才能把pos的后继节点指向新节点,不然反过来会找不到pos节点后继节点的位置。

2.10 删除pos位置的数据

//删除pos位置的数据
void LTErase(LTNode* pos) {
	assert(pos);

	pos->prev->next = pos->next;
	pos->next->prev = pos->prev;
	free(pos);
	pos = NULL;
}

我们要改变新节点,pos节点的前驱,pos节点的后继节点的指向。

2.11 销毁链表

因为每个节点都是单独开辟的空间,所以我们要依次销毁。

//方法一
void LTDesTroy(LTNode** pphead) {
	assert(pphead);
	//哨兵位不能为空
	assert(*pphead);

	LTNode* pcur = (*pphead)->next;
	while (pcur != *pphead) {
		LTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	free(*pphead);
	*pphead = NULL;
}
//方法二
void LTDesTroy(LTNode* phead) {
	assert(phead);

	LTNode* pcur = phead->next;
	while (pcur != phead) {
		LTNode* next = pcur->next;
		free(pcur);
		pcur =next;
	}
	free(phead);
	phead = NULL;
}

与链表的初始化一样,我们有两种方法,但是我们一般选择第二种方法,为了保持接口的一致性,但是第二种方法我们要在函数外面手动给链表置为空。

三、完整代码实现

list.h

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

typedef int LTDataType;
//双向循环链表结构体类型
typedef struct ListNode {
	LTDataType data;
	struct ListNode* prev;//前驱节点
	struct ListNode* next;//后继节点
}LTNode;

//链表初始化
//void LTInit(LTNode** pphead);
LTNode* LTInit();

//打印链表
void LTPrint(LTNode* phead);

//尾插 在最后有效节点或者哨兵位前插入都是尾插
void LTPushBack(LTNode* phead, LTDataType x);

//头插 在第一个有效节点之前插入
void LTPushFront(LTNode* phead, LTDataType x);

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

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

//查找节点
LTNode* LTFind(LTNode* phead, LTDataType x);

//在pos后面插入数据
void LTInsert(LTNode* pos, LTDataType x);

//删除pos位置的数据
void LTErase(LTNode* pos);

//销毁链表 保持接口一致性
//void LTDesTroy(LTNode** pphead);

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->next = newNode->prev = newNode;
	return newNode;
}

//链表初始化
//写法一 传入头节点的地址
//void LTInit(LTNode** pphead) {
//	assert(pphead);
//   哨兵位
//	*pphead = LTBuyNode(-1);
//}
//写法二 返回哨兵位,不传入值
LTNode* LTInit() {
	LTNode* pphead = LTBuyNode(-1);
	return pphead;
}

//打印链表
void LTPrint(LTNode* phead) {
	assert(phead);
	//从哨兵位下一个节点开始打印
	LTNode* pcur = phead->next;
	while (pcur != phead) {
		printf("%d->", pcur->data);
		pcur = pcur->next;
	}
	printf("\n");
}

//尾插	
void LTPushBack(LTNode* phead, LTDataType x) {
	assert(phead);
	LTNode* newNode = LTBuyNode(x);
	//phead phead->prev newNode
	
	newNode->next = phead;
	newNode->prev = phead->prev;

	phead->prev->next = newNode;
	phead->prev = newNode;
}

//头插	
void LTPushFront(LTNode* phead, LTDataType x) {
	assert(phead);
	LTNode* newNode = LTBuyNode(x);

	//phead phead->next newNode

	newNode->next = phead->next;
	newNode->prev = phead;

	phead->next->prev = newNode;
	phead->next = newNode;
}

//尾删
void LTPopBack(LTNode* phead) {
	assert(phead);
	assert(phead->next != phead);

	//phead phead->prev->prev(prev) phead->prev(del)
	LTNode* prev = phead->prev->prev;
	LTNode* del = phead->prev;

	phead->prev = prev;
	prev->next = phead;
	free(del);
	del = NULL;
}

//头删
void LTPopFront(LTNode* phead) {
	assert(phead);
	assert(phead->next != phead);

	//phead phead->next(del) phead->next->next(next)
	LTNode* del = phead->next;
	LTNode* next = phead->next->next;

	phead->next = next;
	next->prev = phead;
	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;
}

//在pos位置之后插入数据
void LTInsert(LTNode* pos, LTDataType x) {
	assert(pos);
	LTNode* newNode = LTBuyNode(x);

	//newNode pos pos->next

	newNode->next = pos->next;
	newNode->prev = pos;

	pos->next->prev = newNode;
	pos->next = newNode;
}

//删除pos位置的数据
void LTErase(LTNode* pos) {
	assert(pos);

	pos->prev->next = pos->next;
	pos->next->prev = pos->prev;
	free(pos);
	pos = NULL;
}

//销毁链表
/*void LTDesTroy(LTNode** pphead) {
	assert(pphead);
	//哨兵位不能为空
	assert(*pphead);

	LTNode* pcur = (*pphead)->next;
	while (pcur != *pphead) {
		LTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	free(*pphead);
	*pphead = NULL;
}*/
void LTDesTroy(LTNode* phead) {
	assert(phead);

	LTNode* pcur = phead->next;
	while (pcur != phead) {
		LTNode* next = pcur->next;
		free(pcur);
		pcur =next;
	}
	free(phead);
	phead = NULL;
}

listest.c

#include"list.h"

void Listest() {
	//LTNode* plist = NULL;
	//LTInit(&plist);
	LTNode* plist=LTInit();
	//尾插
	LTPushBack(plist, 1);
	LTPushBack(plist, 2);
	LTPushBack(plist, 3);
	LTPushBack(plist, 4);//1 2 3 4;
	LTPrint(plist);

	//头插
	/*LTPushFront(plist, 8);
	LTPushFront(plist, 7);
	LTPushFront(plist, 6);
	LTPushFront(plist, 5);
	LTPrint(plist);*/

	//尾删
	/*	LTPopBack(plist);
	LTPrint(plist);
	LTPopBack(plist);
	LTPrint(plist);
	LTPopBack(plist);
	LTPrint(plist);
	LTPopBack(plist);
	LTPrint(plist);
	//删除失败,链表为空
	//LTPopBack(plist);*/

	//头删
	/*LTPopFront(plist);
	LTPrint(plist);
	LTPopFront(plist);
	LTPrint(plist);
	LTPopFront(plist);
	LTPrint(plist);
	LTPopFront(plist);
	LTPrint(plist);
	//删除错误链表为空
	//LTPopFront(plist);*/

	//查找
	LTNode* retFInd = LTFind(plist,1);
	/*if (retFInd) {
		printf("找到了\n");
	}
	else {
		printf("没找到\n");
	}*/

	//在pos后面插入数据
	/*LTInsert(retFInd, 50);
	LTPrint(plist);*/

	//删除pos位置上的数据
	/*LTErase(retFInd);
	LTPrint(plist);*/

	//销毁链表
	//LTDesTroy(&plist);
	//保持接口一致性
	LTDesTroy(plist);
	plist = NULL;
}


int main() {
	Listest();
	return 0;
}

四、顺序表和双向链表的优缺点分析

不同点
顺序表
链表(单链表)
存储空间上
物理上⼀定连续
逻辑上连续,但物理上不⼀定连续
随机访问
⽀持O(1)
不⽀持:O(N)
任意位置插⼊或删除元素
可能需要搬移元素,效率低O(N)
只需修改指针指向
插⼊
动态顺序表,空间不够时需要扩
没有容量的概念
应⽤场景
元素⾼效存储+频繁访问
任意位置插⼊和删除频繁


总结

上述文章我们讲了链表的双向带头循环链表的实现,希望对你有所帮助

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

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

相关文章

Redis中的订阅发布和事务(一)

订阅发布 PUBSUB NUMSUB PUBSUB NUMSUB [channel-1 channel-2… channel-n]子命令接受任意多个频道作为输入参数&#xff0c;并返回这些频道的订阅者数量。 这个子命令是通过pubsub_channels字典中找到频道对应的订阅者链表&#xff0c;然后返回订阅者链表的长度来实现的(订阅…

bdf文件导入事件错误情况

先打开脑电再导入事件 数据情况

500元左右的运动耳机怎么选?五大质量超群品牌分享

在运动中&#xff0c;一款合适的耳机不仅可以提升运动的乐趣&#xff0c;更能激励我们坚持锻炼&#xff0c;在市场上的运动耳机种类繁多&#xff0c;价格不一&#xff0c;如何选择一款既适合自己又物有所值的运动耳机呢&#xff1f;特别是在500元左右的预算范围内&#xff0c;我…

PostgreSQL入门到实战-第二十七弹

PostgreSQL入门到实战 PostgreSQL中数据分组操作(二)官网地址PostgreSQL概述PostgreSQL中HAVING命令理论PostgreSQL中HAVING命令实战更新计划 PostgreSQL中数据分组操作(二) 使用PostgreSQL HAVING子句来指定组或聚合的搜索条件 官网地址 声明: 由于操作系统, 版本更新等原因…

卷积神经网络的结构组成与解释(详细介绍)

文章目录 前言 1、卷积层 2、激活层 3、BN层 4、池化层 5、FC层&#xff08;全连接层&#xff09; 6、损失层 7、Dropout层 8、优化器 9、学习率 10、卷积神经网络的常见结构 前言 卷积神经网络是以卷积层为主的深层网络结构&#xff0c;网络结构包括有卷积层、激活层、BN层、…

Visual Studio 2019 社区版下载

一、网址 https://learn.microsoft.com/zh-cn/visualstudio/releases/2019/release-notes#start-window 二、选择这个即可

【每日刷题】Day16

【每日刷题】Day16 &#x1f955;个人主页&#xff1a;开敲&#x1f349; &#x1f525;所属专栏&#xff1a;每日刷题&#x1f34d; &#x1f33c;文章目录&#x1f33c; 1. 24. 两两交换链表中的节点 - 力扣&#xff08;LeetCode&#xff09; 2. 160. 相交链表 - 力扣&…

【编程Tool】DevC++的安装配置及使用保姆级教程

目录 前言&#xff1a;软件介绍 1.软件下载及安装 1.1. 双击可执行文件进行安装 2.软件配置 2.1.选择语言 2.2 同意相关协议 2.3.组件保持默认并点击Next 2.4. 修改安装路径 2.5. 等待安装 2.6. 点击Finish&#xff0c;完成安装 2.7 选择语言 2.8.个性化设置 2.9. 点击OK&…

MySQL——创建和插入

一、插入数据 INSERT 使用建议; 在任何情况下建议列出列名&#xff0c;在 VALUES 中插入值时&#xff0c;注意值和列的意义对应关系 values 指定的值顺序非常重要&#xff0c;决定了值是否被保存到正确的列中 在指定了列名的情况下&#xff0c;你可以仅对需要插入的列给到…

MongoDB的go SDK使用集锦

在上一章解读MongoDB官方文档获取mongo7.0版本的安装步骤与基本使用介绍了如何使用mongo shell操作mongo数据库&#xff0c;接下来介绍如何使用sdk来操作数据库&#xff0c;这里以go语言为例&#xff0c;其他语言请查看源文档mongo docs Quick Start 内置数据结构 MongoDB是存…

24 静动态库

首先创建两个函数的头文件和源文件 最后的main函数 #include <stdio.h> #include "print.h" #include "sum.h"int main() {Print("时间");printf("%d\n", sum(3, 5));return 0; }将函数编译成.o文件 make生成文件 make he…

ENVI实战—一文学会使用传感器自带信息配准工具进行几何校正

实验1&#xff1a;学会使用传感器自带信息配准工具 目的&#xff1a;利用ENVI的传感器自带信息配准工具&#xff0c;掌握几何校正的一般方法。 过程&#xff1a; 1.对MODIS影像进行校正&#xff1a; ①读取影像&#xff1a;打开文件&#xff0c;点击“打开为”&#xff0c;…

windows下已经创建好了虚拟环境,但是切换不了的解决方法

用得多Ubuntu&#xff0c;今天用Windows重新更新anaconda出问题&#xff0c;重新安装之后&#xff0c;打开pycharm发现打开终端之后&#xff0c;刚开始是ps的状态&#xff0c;后面试了网上改cmd的方法&#xff0c;终端变成c盘开头了 切换到虚拟环境如下&#xff1a;目前的shell…

IDA动态调试

动态调试 这里我使用测试程序是buuctf逆向题目中的第一题easyre.exe 将其拖进IDA中打开 在调试前要选择调试器&#xff0c;在菜单栏找到调试器&#xff0c;如下选择之后点击确定。 在启动调试前下断点 下好断电到调试器里点击启动进程 会有警告信息&#xff0c;选择是即可…

ARP代理

10.1.0.1/8 和10.2.0.1/8是在同一个网段 10.1.0.2/16 和10.2.0.2/16 不在同一个网段 10.1.0.1/8 和10.1.0.2/16 是可以ping通的 包发出来了&#xff0c;报文有发出来&#xff0c;目的地址是广播包 广播请求&#xff0c;发到路由器的接口G 0/0/0 target不是本接口&#xff0…

Linux 序列化、反序列化、实现网络版计算器

目录 一、序列化与反序列化 1、序列化&#xff08;Serialization&#xff09; 2、反序列化&#xff08;Deserialization&#xff09; 3、Linux环境中的应用实例 二、实现网络版计算器 Sock.hpp TcpServer.hpp Jsoncpp库 Protocol.hpp MyDaemon.hpp CalServer.cc Ca…

3.1 iHRM人力资源 - 组织架构、树形结构、添加子部门

iHRM人力资源 - 组织架构 文章目录 iHRM人力资源 - 组织架构一、展示数据-树形组件1.1 组件说明1.2 树组件自定义结构获取作用域数据1.2.1 说明1.2.2 页面代码1.2.3 获取组织架构数据-api 1.3 效果图1.4 修改树形结构bug 二、添加子部门2.1 表单弹层2.1.1 下拉菜单点击事件2.1.…

中国科学院大学学位论文LaTeX模版

Word排版太麻烦了&#xff0c;公式也不好敲&#xff0c;推荐用LaTeX模版&#xff0c;全自动 官方模版下载位置&#xff1a;国科大sep系统 → \rightarrow → 培养指导 → \rightarrow → 论文 → \rightarrow → 论文格式检测 → \rightarrow → 撰写模板下载百度云&#…

Vitis HLS 学习笔记--readVec2Stream 函数-探究

目录 1. 高效内存存取的背景 2. readVec2Stream() 参数 3. 函数实现 4. 总结 1. 高效内存存取的背景 在深入研究《Vitis HLS 学习笔记--scal 函数探究》一篇文章之后&#xff0c;我们对于scal()函数如何将Y alpha * X这种简单的乘法运算复杂化有了深刻的理解。本文将转向…

前端console用法分享

console对于前端人员来讲肯定都不陌生&#xff0c;相信大部分开发者都会使用console来进行调试&#xff0c;但它能做的绝不仅限于调试。 最常见的控制台方法 作为开发者&#xff0c;最常用的 console 方法如下&#xff1a; 控制台打印结果&#xff1a; 今天我分享的是一些 co…