数据结构——带头循环双向链表(List)

news2024/11/25 19:25:28

1、带头双向循环链表介绍

        在上一篇博客中我们提到了链表有三个特性,可以组合成为8种不同类型的链表。单链表是其中比较重要的一种,那么这次我们选择和带头双向循环链表会会面,这样我们就见识过了所有三种特性的呈现。

        带头双向循环链表,听起来仿佛是一个很复杂的结构,但是真正了解后就发现,这种稍微复杂一点的结构实际上为链表提供了完善的功能,使得我们在操作链表时变得反而更简单。而这种链表因为自身结构复杂,功能结构完善,所以经常成为一个独立的数据存储结构,用来单独存储数据。

2、带头双向循环链表工程

对于一个带头双向循环链表工程,我们一般模式需要分为三部分。

        List.h 为头文件,其中包含库函数头文件的包含,顺序表结构体的定义与声明,接口函数的声明。

        List.c 包含接口函数的定义。

        Test.c 是我们的测试源文件,从这里进入main函数。

2.1 链表的定义

        为了实现链表的双向结构,我们需要从定义入手。单链表的链表结点定义是一个指向下一个结点的指针,而双向链表则需要两个指针,分别指向上一个和下一个结点。

typedef int LTDataType;

typedef struct ListNode
{
	LTDataType val;
	struct ListNode* prev;
	struct ListNode* next;
}LTNode;

2.2 链表的函数接口

        用带头双向循环链表管理数据也需要一些常用的增删查改接口,但是因为有哨兵位的存在,所以在传参的时候我们只需要传递一级指针即可。

2.2.1 链表结点申请

        在链表插入与初始化的时候我们需要申请出一个结点,然后链接在链表之中。所以我们可以和单链表一样,把结点的申请封装成为一个函数。

LTNode* CreateLTNode(LTDataType x)
{
	LTNode* tmp = (LTNode*)malloc(sizeof(LTNode));
	if (tmp == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	tmp->val = x;
	tmp->next = NULL;
	tmp->prev = NULL;
	return tmp;
}

2.2.2 链表的初始化

        因为我们现在要创建的链表是一个带头链表,所以需要对一个链表进行初始化,即创造出一个哨兵结点,剩余的链表结点均在哨兵位之后进行操作。初始化无需参数,最后返回一个链表的头结点即可。

        对于哨兵结点而言,其值没有具体的意义,所以我们将其随便写作-1。因为我们的链表是双向循环链表,双向循环链表的尾结点的下一个结点指向头结点,头结点的上一个结点指向尾结点。因此,初始化的时候我们就要对哨兵结点正确赋值,保证即使空链表也满足要求的结构。因此,我们让哨兵结点的next和prev都指向自身即可。

LTNode* LTInit()
{
	LTNode* tmp = CreateLTNode(-1);
	tmp->next = tmp;
	tmp->prev = tmp;
	return tmp;
}

2.2.3 链表的结点插入

        带头双向循环链表的插入方式分为:头插,即在链表哨兵位之后插入结点;尾插,即在链表尾结点后插入一个结点;随机插入,即在指定结点前插入结点。

        对于带头双向循环链表的节点插入而言,由于双向循环的特性任何一个节点都可以轻松的找到自己的前驱结点,所以使得插入不再需要遍历链表。又由于其带头的特性,也使得我们无需考虑链表是否为空的情况。唯一需要注意的就是结点链接的顺序,避免出现修改了结点指针而找不到对应位置的结点,当然,如果给出一个临时变量存储对应的结点,顺序也可以无需考虑。

2.2.3.1 链表的头插

        带头双向循环链表的头插这里我采用定义一个临时变量而不考虑链接顺序。

void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);

	LTNode* tmp = CreateLTNode(x);
	tmp->next = phead->next;
	tmp->next->prev = tmp;
	phead->next = tmp;
	tmp->prev = phead;
}
2.2.3.2 链表的尾插

        带头双向循环链表的尾插我同样采用定义一个临时变量而不考虑链接顺序。 

void LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);

	LTNode* tail = phead->prev;
	LTNode* tmp = CreateLTNode(x);
	tmp->next = phead;
	tmp->prev = tail;
	tail->next = tmp;
	phead->prev = tmp;
}
2.2.3.3 链表的随机插入

        带头双向循环链表的随机插入指在指定结点pos后插入一个结点。 

//在pos前插入
void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);
	
	LTNode* tmp = CreateLTNode(x);
	tmp->next = pos;
	tmp->prev = pos->prev;
	tmp->prev->next = tmp;
	pos->prev = tmp;
}

2.2.4 链表的结点删除

        带头双向循环链表的删除方式分为:头删,即删除在链表哨兵位之后的结点;尾删,即删除链表尾结点;随机插入,即删除指定结点。

        同样的,由于双向循环的特性使得不再需要遍历链表寻找前驱结点,所以删除的时候只需要将待删除结点的前后结点链接起来,然后释放掉待删除结点。

2.2.4.1 链表的头删
void LTPopBack(LTNode* phead)
{
	assert(phead);
	assert(phead->next != phead);

	LTNode* tail = phead->prev;
	phead->prev = tail->prev;
	tail->prev->next = phead;
	free(tail);
	tail = NULL;
}
2.2.4.2 链表的尾删
void LTPopFront(LTNode* phead)
{
	assert(phead);
	assert(phead->next != phead);

	LTNode* first = phead->next;
	phead->next = first->next;
	first->prev = phead;
	free(first);
	first = NULL;
}
2.2.4.3 链表的随机删除
//删除pos位置节点
void LTErase(LTNode* pos)
{
	assert(pos);

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

2.2.5 链表的查找

        用于查找指定值的结点并返回,和单链表一样,只是遍历结束条件是遍历到了哨兵位。

LTNode* LTFind(LTNode* phead, LTDataType x)
{
	assert(phead);

	LTNode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->val == x)
		{
			return cur;
		}
		else
		{
			cur = cur->next;
		}
	}
	return NULL;
}

2.2.6 链表的打印

        很简单的接口,遍历方法可以参照链表的查找。

void LTPrint(LTNode* phead)
{
	assert(phead);

	LTNode* cur = phead->next;
	while (cur != phead)
	{
		printf("%d<=>", cur->val);
		cur = cur->next;
	}
	printf("NULL\n");
}

2.2.7 链表的销毁

        销毁链表需要遍历链表,释放每个节点,最后释放哨兵位即可。注意,这里的销毁只是释放了所有结点,因为是一级指针传参,所以说明主函数中链表的指针在销毁后成为了野指针,需要函数调用者自行置空。

void LTDestroy(LTNode* phead)
{
	assert(phead);

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

3、链表反思

        在完成了单链表和带头双向循环链表后,需要深刻理解到不同的特性对于我们写代码有哪些限制与遍历,从而在合适的场景下合理地做出选择。

        总结一下同为线性表的顺序表与链表之间的优劣与区别。

        对于顺序表,先说说优势。顺序表最大的特征就是物理储存空间连续,这就代表其可以支持随机访问,无论是哪个位置的数据,都可以以O(1)的代价获取。储存空间连续还使得操作系统在访问其数据时是非常高效的,操作系统读取一次连续的空间对其数据的覆盖率很高,缓存利用率高。顺序表同样也有劣势,其插入数据时可能需要搬动数据,使得插入效率低,同时空间需要扩容,就面临着空间浪费,使得空间存在一定程度的浪费。

        对于链表而言,其优势是空间分配非常灵活,不存在浪费的情况,多一个数据就开辟一个结点,删除数据就即刻释放空间。插入和删除不存在挪动数据的情况,只需要链接指针。但是由于其储存空间不连续,所以链表也有一定劣势。由于寻找第k个结点只能遍历,链表访问数据的代价为O(N),对比线性表很大。除此之外,不连续的物理空间储存使得操作系统缓存时对链表数据覆盖率不高,缓存利用率较低。

        由此看来,顺序表和链表二者互为补充,所以我们在选择方面要有所区别。对于需要大量访问的数据而言,顺序表效率明显更高;而对于需要频繁插入删除的数据,链表由于灵动的空间布局而略胜一筹。对二者的合理谋划发挥最佳效果便是每个学习者需要不断探索的“内功”了。

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

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

相关文章

qt pdf 模块简介

文章目录 1. 技术平台2. Qt pdf 模块3. cmake 使用模块4. 许可证5. 简单示例5.1 CMakeLists.txt5.2 main.cpp 6. 总结 1. 技术平台 项目说明OSwin10 x64Qt6.6compilermsvc2022构建工具cmake 2. Qt pdf 模块 Qt PDF模块包含用于呈现PDF文档的类和函数。 QPdfDocument 类加载P…

Python基础:字符串详解(需补充完善)

1. 字符串定义 在Python中&#xff0c;字符串是一种数据类型&#xff0c;用于表示文本数据。字符串是由字符组成的序列&#xff0c;可以包含字母、数字、符号和空格等字符。在Python中&#xff0c;你可以使用单引号&#xff08;&#xff09;或双引号&#xff08;"&#x…

SpringBoot 拦截器高级篇

Springboot 拦截器 定义使用场景拦截器与过滤器的区别实现步骤全局拦截器的局限性全局拦截器VS局部拦截器局部拦截器自定义局部拦截器使用多个局部拦截器 定义 拦截器是Spring MVC框架中的一个重要组件&#xff0c;它是一种AOP&#xff08;面向切面编程&#xff09;的实现方式&…

汽车租聘管理与推荐系统Python+Django网页界面+协同过滤推荐算法

一、介绍 汽车租聘管理与推荐系统。本系统使用Python作为主要编程语言&#xff0c;前端采用HTML、CSS、BootStrap等技术搭建前端界面&#xff0c;后端采用Django框架处理用户的请求。创新点&#xff1a;使用协同过滤推荐算法实现对当前用户个性化推荐。 其主要功能如下&#x…

lua的gc原理

lua垃圾回收(Garbage Collect)是lua中一个比较重要的部分。由于lua源码版本变迁&#xff0c;目前大多数有关这个方面的文章都还是基于lua5.1版本&#xff0c;有一定的滞后性。因此本文通过参考当前的5.3.4版本的Lua源码&#xff0c;希望对Lua的GC算法有一个较为详尽的探讨。 L…

4.前端--HTML标签-表格列表表单【2023.11.25】

1.表格 1.1表格的作用 表格的作用&#xff1a;表格主要用于显示、展示数据 1.2表格的基本格式 <table><tr><td>单元格内的文字</td><td>单元格内的文字</td>...</tr>... </table><table> </table> 是用于定义表…

【C++】类和对象——拷贝构造和赋值运算符重载

上一篇我们讲了构造函数&#xff0c;就是对象实例化时会自动调用&#xff0c;那么&#xff0c;我们这里的拷贝构造在形式上是构造函数的一个重载&#xff0c;拷贝构造其实也是一种构造函数&#xff0c;那么我们就可以引出这里的规则 1.拷贝构造函数的函数名必须与类名相同。 2.…

vue3+tsx的使用

<template><div><xiaoman on-click"getItem" name"似懂非懂"></xiaoman></div> </template><script setup langts>import xiaoman from "./App"const getItem(item:any)>{console.log(item,it…

Java多线程二-线程安全

1、线程安全问题 多个线程&#xff0c;同时操作同一个共享资源的时候&#xff0c;可能会出现业务安全问题。 2、实例&#xff1a;取钱的线程安全问题 2.1、场景 小明和小红是夫妻&#xff0c;他们有个共同账户&#xff0c;余额是十万元&#xff0c;如果两人同时取钱并且各自取…

Bytebase 2.11.1 - 数据脱敏支持语义类型和脱敏算法

&#x1f680; 新功能 数据脱敏支持自定义脱敏算法和语义类型。 &#x1f514; 重大变更 用户页面的 URL 由 /u/{uid} 变更为 /users/{email}。工作空间的所有者和开发者分别更名为&#xff1a;管理员和成员。 &#x1f384; 改进 SQL 编辑器支持显示表的 DDL 语句&#…

【Unity基础】8.简单场景的搭建

【Unity基础】8.简单场景的搭建 大家好&#xff0c;我是Lampard~~ 欢迎来到Unity基础系列博客&#xff0c;所学知识来自B站阿发老师~感谢 &#xff08;一&#xff09;场景资源 &#xff08;1&#xff09;Import资源包 今天我们将手动去搭一个简单的场景&#xff0c;当…

HONOR荣耀MagicBook 15 2021款 锐龙版R5(BMH-WFQ9HN)原厂Windows11预装OEM系统含F10智能还原

链接&#xff1a;https://pan.baidu.com/s/1faYtC5BIDC2lsV_JSMI96A?pwdj302 提取码&#xff1a;j302 原厂系统Windows11.22H2工厂模式安装包,含F10一键智能还原&#xff0c;自带所有驱动、出厂主题壁纸、系统属性专属LOGO标志、Office办公软件、荣耀 电脑管家等预装程序 …

SQL JOIN 子句:合并多个表中相关行的完整指南

SQL JOIN JOIN子句用于基于它们之间的相关列合并来自两个或更多表的行。 让我们看一下“Orders”表的一部分选择&#xff1a; OrderIDCustomerIDOrderDate1030821996-09-1810309371996-09-1910310771996-09-20 然后&#xff0c;看一下“Customers”表的一部分选择&#xff…

thinkphp最新开发的物业管理系统 缴费管理、停车管理、收费管理、值班管理

物业费&#xff0c;水电燃气费&#xff0c;电梯费&#xff0c;租金&#xff0c;临时收费等多种收费规则完全自定义&#xff0c;账单自动生成&#xff0c;无需人工计算 实时数据互通&#xff1a;一键报事报修&#xff0c;购买车辆月卡&#xff0c;管理家人信息&#xff0c;参加物…

java根据时间区间计算区间中都包含那几个月

在一些需要统计类的需求中可能会计算同比/环比数据,往往我们拿到的并不是每个月的准确时间,需要自行计算,一点一点计算还是挺麻烦的,因此搞一个工具类出来 工具类 public static void main(String[] args) {//为了准确,这里使用的时间毫秒值long beginTime 1577836800000L;…

从0到1建立前端规范

本文适合打算建立前端规范的小伙伴阅读 一、为什么需要规范 规范能给我们带来什么好处&#xff0c;如果没有规范会造成什么后果&#xff1f;这里主要拿代码规范来说。 统一代码规范的好处&#xff1a; 提高代码整体的可读性、可维护性、可复用性、可移植性和可靠性&#xf…

Java制作“简易王者荣耀”小游戏

第一步是创建项目 项目名自拟 第二部创建个包名 来规范class 然后是创建类 GameFrame 运行类 package com.sxt;import java.awt.Graphics; import java.awt.Image; import java.awt.Toolkit; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; im…

圆通速递单号查询入口,以表格的形式导出详细物流信息

批量查询圆通速递单号的物流信息&#xff0c;并以表格的形式导出单号的详细物流信息。 所需工具&#xff1a; 一个【快递批量查询高手】软件 圆通速递单号若干 操作步骤&#xff1a; 步骤1&#xff1a;运行【快递批量查询高手】软件&#xff0c;第一次使用的伙伴记得先注册&…

灯塔的安装

Docker 安装 docker 安装参考&#xff1a;https://docs.docker.com/engine/install/ shell脚本: curl -fsSL https://get.docker.com -o get-docker.sh sudo sh get-docker.sh灯塔安装 mkdir docker-ARL;cd docker-ARL curl https://bootstrap.pypa.io/get-pip.py -o get-pip…

如何在Ubuntu系统上安装Node.js

简单介绍 简单的说Node.js就是运行在服务端的JavaScript。Node.js是一个基于Chrome JavaScript运行时建立的一个平台。Node.js是一个事件驱动I/O服务端JavaScript 环境&#xff0c;基于Google的V8引擎&#xff0c;V8引擎执行Javascript的速度非常快&#xff0c;性能非常好。 no…