【数据结构】第五站:带头双向循环链表

目录

一、链表的八种结构

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

1.链表的定义

2.链表的接口定义

3.接口的具体实现

三、带头双向循环链表的完整代码

四、顺序表和链表的区别


一、链表的八种结构

我们已经知道链表可以有以下三种分法

 

 

而这三种结构又可以排列组合,形成八种结构

其中我们最常见的就是无头单向非循环链表和带头双向循环链表

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

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

1.链表的定义

对于这个链表,我们需要使用两个指针来控制,一个是前驱指针,一个是后继指针

typedef int LTDateType;
typedef struct ListNode
{
	struct ListNode* prev;
	struct ListNode* next;
	LTDateType data;
}ListNode;

2.链表的接口定义

如下代码所示,是我们需要实现的接口

//链表的初始化
ListNode* ListCreat();
//链表的打印
void ListPrint(ListNode* phead);
//链表的尾插
void ListPushBack(ListNode* phead, LTDateType x);
//链表的头插
void ListPushFront(ListNode* phead, LTDateType x);
//链表是否为空
bool ListEmpty(ListNode* phead);
//链表的尾删
void ListPopBack(ListNode* phead);
//链表的头删
void ListPopFront(ListNode* phead);
//链表的查找
ListNode* ListFind(ListNode* phead, LTDateType x);
//链表在pos前面的插入
void ListInsert(ListNode* pos, LTDateType x);
//链表在pos位置处的删除
void ListErase(ListNode* pos);
//链表的销毁
void ListDestroy(ListNode* phead);

3.接口的具体实现

1.链表的初始化

ListNode* BuyListNode(LTDateType x)
{
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
	if (newnode == NULL)
	{
		perror("malloc");
		return NULL;
	}
	newnode->data = x;
	newnode->next = NULL;
	newnode->prev = NULL;
	return newnode;
}
//链表的初始化
ListNode* ListCreat()
{
	ListNode* phead = BuyListNode(-1);

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

如上代码所示,对于这个初始化,他与单链表不同,单链表其实是没有必要去专门做一个初始化函数的。但同时我们需要传二级指针来解决一些问题

对于双向带头循环链表,我们就有必要设置一个初始化函数了。

我们有两种思路去构建这个函数

一种是在主函数中直接定义一个指针,然后通过传这个指针的地址去构建。但是这样我们就需要传二级指针了。

另外一种是我们可以利用返回值,这样我们就直接避免了传参数了。

2.链表的打印

//链表的打印
void ListPrint(ListNode* phead)
{
	assert(phead);
	ListNode* cur = phead->next;
	printf("<=head=>");
	while (cur != phead)
	{
		printf("%d<=>", cur->data);
		cur = cur->next;
	}
	printf("\n");
}

如上代码所示,链表的打印与单链表的打印是一样的,没有什么太大的区别,我们可以利用printf来更加形象的表达双向循环的意思

3.链表的尾插

//链表的尾插
void ListPushBack(ListNode* phead, LTDateType x)
{
	assert(phead);

	ListNode* newnode = BuyListNode(x);
	ListNode* tail = phead->prev;
	tail->next = newnode;
	newnode->prev = tail;
	newnode->next = phead;
	phead->prev = newnode;
}

对于尾插就比单链表简单多了,因为有了头节点,我们就不需要处理是否为空的状态了

4.链表的头插

//链表的头插
void ListPushFront(ListNode* phead, LTDateType x)
{
	assert(phead);
	ListNode* newnode = BuyListNode(x);
	ListNode* first = phead->next;

	phead->next = newnode;
	newnode->prev = phead;
	newnode->next = first;
	first->prev = newnode;
}

头插和尾插基本上是非常类似的,这也得益于这种链表结构基本是无死角的

5.链表的头删尾删以及是否为空

//链表是否为空
bool ListEmpty(ListNode* phead)
{
	assert(phead);
	return phead->next == phead;
}
//链表的尾删
void ListPopBack(ListNode* phead)
{
	assert(phead);
	assert(!ListEmpty(phead));
	ListNode* tail = phead->prev;
	ListNode* tailPrev = tail->prev;
	phead->prev = tailPrev;
	tailPrev->next = phead;
	free(tail);
	tail = NULL;
}
//链表的头删
void ListPopFront(ListNode* phead)
{
	assert(phead);
	assert(!ListEmpty(phead));
	ListNode* first = phead->next;
	ListNode* firstNext = first->next;
	phead->next = firstNext;
	firstNext->prev = phead;
	free(first);
	first = NULL;
}

对于头删和尾删其实也是一致的思路,我们只需要改变结点即可,值得注意的是,我们需要判断链表是否为空, 如果为空肯定不可以删除

6.链表的查找

//链表的查找
ListNode* ListFind(ListNode* phead,LTDateType x)
{
	assert(phead);
	assert(!ListEmpty(phead));
	ListNode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

这个也是比较简单的,他的思路与单链表的是一样的

7.链表在pos位置之前插入

//链表在pos前面的插入
void ListInsert(ListNode* pos, LTDateType x)
{
	assert(pos);
	ListNode* newnode = BuyListNode(x);
	ListNode* prev = pos->prev;
	prev->next = newnode;
	newnode->prev = prev;
	newnode->next = pos;
	pos->prev = newnode;
}

这个的话,我们更能体会到他相对于单链表的优势了。我们只需要pos位置即可。然后思路和头插尾插是一致的

8.链表在pos位置的删除

//链表在pos位置处的删除
void ListErase(ListNode* pos)
{
	assert(pos);
	ListNode* prev = pos->prev;
	ListNode* next = pos->next;
	prev->next = next;
	next->prev = prev;
	free(pos);
	pos = NULL;
}

对于删除,也是与头删尾删思路一致的

9.链表的销毁

//链表的销毁
void ListDestroy(ListNode* phead)
{
	assert(phead);
	ListNode* cur = phead->next;
	while (cur != phead)
	{
		ListNode* next = cur->next;
		free(cur);
		cur = next;
	}
	free(phead);
	phead = NULL;
}

对于链表的销毁思路与单链表也是一致的,只需要循环遍历即可

三、带头双向循环链表的完整代码

List.h

#pragma once
#include<stdio.h>
#include<malloc.h>
#include<assert.h>
#include<stdbool.h>
typedef int LTDateType;
typedef struct ListNode
{
	struct ListNode* prev;
	struct ListNode* next;
	LTDateType data;
}ListNode;

//链表的初始化
ListNode* ListCreat();
//链表的打印
void ListPrint(ListNode* phead);
//链表的尾插
void ListPushBack(ListNode* phead, LTDateType x);
//链表的头插
void ListPushFront(ListNode* phead, LTDateType x);
//链表是否为空
bool ListEmpty(ListNode* phead);
//链表的尾删
void ListPopBack(ListNode* phead);
//链表的头删
void ListPopFront(ListNode* phead);
//链表的查找
ListNode* ListFind(ListNode* phead, LTDateType x);
//链表在pos前面的插入
void ListInsert(ListNode* pos, LTDateType x);
//链表在pos位置处的删除
void ListErase(ListNode* pos);
//链表的销毁
void ListDestroy(ListNode* phead);

List.c

#define _CRT_SECURE_NO_WARNINGS 1
#include"List.h"
ListNode* BuyListNode(LTDateType x)
{
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
	if (newnode == NULL)
	{
		perror("malloc");
		return NULL;
	}
	newnode->data = x;
	newnode->next = NULL;
	newnode->prev = NULL;
	return newnode;
}
//链表的初始化
ListNode* ListCreat()
{
	ListNode* phead = BuyListNode(-1);

	phead->next = phead;
	phead->prev = phead;
	return phead;
}
//链表的打印
void ListPrint(ListNode* phead)
{
	assert(phead);
	ListNode* cur = phead->next;
	printf("<=head=>");
	while (cur != phead)
	{
		printf("%d<=>", cur->data);
		cur = cur->next;
	}
	printf("\n");
}
//链表的尾插
void ListPushBack(ListNode* phead, LTDateType x)
{
	assert(phead);

	ListNode* newnode = BuyListNode(x);
	ListNode* tail = phead->prev;
	tail->next = newnode;
	newnode->prev = tail;
	newnode->next = phead;
	phead->prev = newnode;
}
//链表的头插
void ListPushFront(ListNode* phead, LTDateType x)
{
	assert(phead);
	ListNode* newnode = BuyListNode(x);
	ListNode* first = phead->next;

	phead->next = newnode;
	newnode->prev = phead;
	newnode->next = first;
	first->prev = newnode;
}
//链表是否为空
bool ListEmpty(ListNode* phead)
{
	assert(phead);
	return phead->next == phead;
}
//链表的尾删
void ListPopBack(ListNode* phead)
{
	assert(phead);
	assert(!ListEmpty(phead));
	ListNode* tail = phead->prev;
	ListNode* tailPrev = tail->prev;
	phead->prev = tailPrev;
	tailPrev->next = phead;
	free(tail);
	tail = NULL;
}
//链表的头删
void ListPopFront(ListNode* phead)
{
	assert(phead);
	assert(!ListEmpty(phead));
	ListNode* first = phead->next;
	ListNode* firstNext = first->next;
	phead->next = firstNext;
	firstNext->prev = phead;
	free(first);
	first = NULL;
}
//链表的查找
ListNode* ListFind(ListNode* phead,LTDateType x)
{
	assert(phead);
	assert(!ListEmpty(phead));
	ListNode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}
//链表在pos前面的插入
void ListInsert(ListNode* pos, LTDateType x)
{
	assert(pos);
	ListNode* newnode = BuyListNode(x);
	ListNode* prev = pos->prev;
	prev->next = newnode;
	newnode->prev = prev;
	newnode->next = pos;
	pos->prev = newnode;
}
//链表在pos位置处的删除
void ListErase(ListNode* pos)
{
	assert(pos);
	ListNode* prev = pos->prev;
	ListNode* next = pos->next;
	prev->next = next;
	next->prev = prev;
	free(pos);
	pos = NULL;
}
//链表的销毁
void ListDestroy(ListNode* phead)
{
	assert(phead);
	ListNode* cur = phead->next;
	while (cur != phead)
	{
		ListNode* next = cur->next;
		free(cur);
		cur = next;
	}
	free(phead);
	phead = NULL;
}

Test.c

#define _CRT_SECURE_NO_WARNINGS 1

#include"List.h"

void TestList1()
{
	ListNode* plist = ListCreat();

	ListPushBack(plist, 1);
	ListPushBack(plist, 2);
	ListPushBack(plist, 3);
	ListPushBack(plist, 4);
	ListPushBack(plist, 5);

	ListPrint(plist);

	ListPushFront(plist, 6);
	ListPushFront(plist, 7);
	ListPushFront(plist, 8);
	ListPushFront(plist, 9);
	ListPushFront(plist, 10);

	ListPrint(plist);

	ListPopBack(plist);
	ListPopBack(plist);
	ListPopBack(plist);
	ListPopBack(plist);
	ListPrint(plist);

	ListPopFront(plist);
	ListPopFront(plist);
	ListPopFront(plist);
	ListPopFront(plist);
	ListPrint(plist);

}
void TestList2()
{
	ListNode* plist = ListCreat();

	ListPushBack(plist, 1);
	ListPushBack(plist, 2);
	ListPushBack(plist, 3);
	ListPushBack(plist, 4);
	ListPushBack(plist, 5);

	ListPrint(plist);
	ListNode* pos = ListFind(plist, 3);
	(pos->data) *= 10;
	ListPrint(plist);
	ListInsert(pos, 20);
	ListPrint(plist);

	ListErase(pos);
	ListPrint(plist);

}
int main()
{
	//TestList1();
	TestList2();
	return 0;
}

四、顺序表和链表的区别

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

如上表所示,是顺序表和链表的区别

在这里我们需要注意缓存利用率这个东西。

对于我们的顺序表/链表,想要实现他的数据访问、遍历、修改等

这些数据是由cpu来进行访问的。

也就是这些访问的指令都是cpu执行的。从而访问打印等操作。这时候cpu就需要去访问内存

而cpu其实是不能直接访问内存的,这是因为cpu太快了,内存太慢了

为了解决这个问题,就有了三级缓存,即cpu高速缓存这个东西。

也就是说cpu会先访问缓存,最高级的缓存再次缓存其实就是命中。

在计算机中有一个局部性原理,在访问某个位置的数据的时候,大概率要访问后面的数据。因此他就会将这个数据后面的数据也顺便加载进去。

这时候由于顺序表是连续的,后面的都直接被命中了。所以他的缓存利用率高

而链表他访问某个数据的时候也会加载一长段,但是后面的不一定会用。所以就会浪费,也会导致缓存污染。所以缓存利用率低。也就是他不仅没有起到正向作用,反而起到了反向作用。


本节内容到此位置

如果对你有帮助的话,不要忘记点赞加收藏哦!!!

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Linux】权限详解

前言首先我们先来看一下权限的概念&#xff1a;在多用户计算机系统的管理中&#xff0c;权限&#xff08;privilege&#xff09;是指某个特定的用户具有特定的系统资源使用权力&#xff0c;像是文件夹&#xff0c;特定系统指令的使用或存储量的限制。通常&#xff0c;系统管理员…

蓝桥杯刷题冲刺 | 倒计时16天

作者&#xff1a;指针不指南吗 专栏&#xff1a;蓝桥杯倒计时冲刺 &#x1f43e;马上就要蓝桥杯了&#xff0c;最后的这几天尤为重要&#xff0c;不可懈怠哦&#x1f43e; 文章目录1.青蛙跳杯子1.青蛙跳杯子 题目 链接&#xff1a; 青蛙跳杯子 - 蓝桥云课 (lanqiao.cn) X 星球的…

低代码开发:助力企业高效实现数字转型的一大利器

随着互联网、移动互联网、物联网等技术的迅速普及和应用&#xff0c;数字经济时代的到来&#xff0c;人们的生产、消费和生活方式都发生了巨大的变化&#xff0c;而传统企业也面临着巨大的挑战和机遇。 在数字经济时代&#xff0c;数据成为一种重要的生产要素。数据成为一种重要…

个人简历html网页代码(使用chatgpt完成web开发课的实验)

使用chatgpt完成web开发课的实验 前提&#xff1a; chatgpt的使用&#xff0c;建议看https://juejin.cn/post/7198097078005841980或者自己随便找 要学会用“出国旅游”软件 vscode的基本使用 炼丹开始&#xff1a; 炼丹材料&#xff1a; 帮我写一个html页面&#xff0c;内…

一文学会 Spring MVC 表单标签

✅作者简介&#xff1a;2022年博客新星 第八。热爱国学的Java后端开发者&#xff0c;修心和技术同步精进。 &#x1f34e;个人主页&#xff1a;Java Fans的博客 &#x1f34a;个人信条&#xff1a;不迁怒&#xff0c;不贰过。小知识&#xff0c;大智慧。 &#x1f49e;当前专栏…

【C语言蓝桥杯每日一题】—— 货物摆放

【C语言蓝桥杯每日一题】—— 货物摆放&#x1f60e;前言&#x1f64c;排序&#x1f64c;总结撒花&#x1f49e;&#x1f60e;博客昵称&#xff1a;博客小梦 &#x1f60a;最喜欢的座右铭&#xff1a;全神贯注的上吧&#xff01;&#xff01;&#xff01; &#x1f60a;作者简介…

【Vue3】模板语法

&#x1f3c6;今日学习目标&#xff1a;模板语法 &#x1f603;创作者&#xff1a;颜颜yan_ ✨个人格言&#xff1a;生如芥子&#xff0c;心藏须弥 ⏰本期期数&#xff1a;第三期 &#x1f389;专栏系列&#xff1a;Vue3 文章目录前言声明响应式状态插值文本Attribute&#xff…

瑟瑟发抖吧~OpenAI刚刚推出王炸——引入ChatGPT插件,开启AI新生态

5分钟学会使用ChatGPT 插件&#xff08;ChatGPT plugins&#xff09;——ChatGPT生态建设的开端ChatGPT插件是什么OpenAI最新官方blog资料表示&#xff0c;已经在ChatGPT中实现了对插件的初步支持。插件是专门为以安全为核心原则的语言模型设计的工具&#xff0c;可帮助ChatGPT…

Spring源码面试最难问题——循环依赖

前言 问&#xff1a;Spring 如何解决循环依赖&#xff1f; 答&#xff1a;Spring 通过提前曝光机制&#xff0c;利用三级缓存解决循环依赖&#xff08;这原理还是挺简单的&#xff0c;参考&#xff1a;三级缓存、图解循环依赖原理&#xff09; 再问&#xff1a;Spring 通过提前…

AI真的快让我们失业了,从ChatGPT到Midjourney

参考文章&#xff1a; https://mp.weixin.qq.com/s/3RdHPPhYgDfB6KY6Y9Sk2A跟AI有关的新闻&#xff0c;一个接着一个。前一天你还和往常一样进入梦乡&#xff0c;第二天醒来就能被新的AI新闻“炸弹”震得心惊。 以ChatGPT为代表的AI语言模型&#xff0c;以Midjourney为代表的…

GPT免费网站分享(持续更新)

大家好,我是herosunly。985院校硕士毕业,现担任算法研究员一职,热衷于机器学习算法研究与应用。曾获得阿里云天池比赛第一名,CCF比赛第二名,科大讯飞比赛第三名。拥有多项发明专利。对机器学习和深度学习拥有自己独到的见解。曾经辅导过若干个非计算机专业的学生进入到算法…

【JaveEE】多线程之阻塞队列(BlockingQueue)

目录 1.了解阻塞队列 2.生产者消费者模型又是什么&#xff1f; 2.1生产者消费者模型的优点 2.1.1降低服务器与服务器之间耦合度 2.1.2“削峰填谷”平衡消费者和生产的处理能力 3.标准库中的阻塞队列&#xff08;BlockingQueue&#xff09; 3.1基于标准库&#xff08;Bloc…

13从零开始学Java之数据类型之间的自动、强制与隐含强制类型转换详解

作者&#xff1a;孙玉昌&#xff0c;昵称【一一哥】&#xff0c;另外【壹壹哥】也是我哦CSDN博客专家、万粉博主、阿里云专家博主、掘金优质作者前言在上一篇文章中&#xff0c;壹哥给大家讲解了Java中的数据类型&#xff0c;从此大家就知道了基本类型和引用类型&#xff0c;尤…

100天精通Python丨基础知识篇 —— 03、Python基础知识扫盲(第一个Python程序,13个小知识点)

文章目录&#x1f41c; 1、Python 初体验Pycharm 第一个程序交互式编程第一个程序&#x1f41e; 2、Python 引号&#x1f414; 3、Python 注释&#x1f985; 4、Python 保留字符&#x1f42f; 5、Python 行和缩进&#x1f428; 6、Python 空行&#x1f439; 7、Python 输出&…

iOS 项目嵌入Flutter 运行

一 创建Flutter 模块命令行flutter create --template module my_flutter创建完成后&#xff0c;该模块和普通的Flutter项目一直&#xff0c;可以通过Android Studio或VSCode打开、开发、运行&#xff1b;和之前项目不同的iOS和Android项目是一个隐藏文件&#xff0c;并且我们…

系统分析师每日练习错题知识点1

软件工程---逆向工程 实现级&#xff1a;包括程序的抽象语法树、符号表等信息结构级&#xff1a;包括反映程序分量之间相互依赖关系的信息&#xff0c;例如调用图、结构图等功能级&#xff1a;包括反映程序段功能以及程序段之间关系的信息。领域级&#xff1a;包括反映程序分量…

软件测试方法上篇(等价类、边界值、因果图)

一、基于需求设计测试用例 验证需求的正确性及其合理性细分需求&#xff1a;多细致的需求就涉及多细致的测试用例&#xff0c;从细分的需求里&#xff0c;根据每一个功能点设计测试用例。 二、测试方法 1、等价类 特点&#xff1a;输入过多&#xff0c;无法穷举。 方法&…

windows下iis安装pdo_sqlsrv扩展

1.PHP7.3/7.4/PHP8.0连接Mssql扩展下载: https://jmj.cc/s/6dwbnp 2.首先确认两个问题: php的位数,需核实自己安装的php版本是64位?还是32(x86)位,检测方法:创建一个php探针,访问,如果现实x86则是32位,如果是x64就是64位 2、确认安装的php版本是nts还是ts的,检测方法也是打…

【C语言】深度理解指针(下)

一. 前言&#x1f48e;昨晚整理博客时突然发现指针还少了一篇没写&#xff0c;今天就顺便来补一补。上回书说到&#xff0c;emmm忘记了&#xff0c;没事&#xff0c;我们直接进入本期的内容:本期我们带来了几道指针相关笔试题的解析&#xff0c;还算是相对比较轻松的。话不多说…

【网络】https协议

&#x1f941;作者&#xff1a; 华丞臧. &#x1f4d5;​​​​专栏&#xff1a;【网络】 各位读者老爷如果觉得博主写的不错&#xff0c;请诸位多多支持(点赞收藏关注)。如果有错误的地方&#xff0c;欢迎在评论区指出。 推荐一款刷题网站 &#x1f449; LeetCode刷题网站 文章…
最新文章