手撕数据结构 —— 带头双向循环链表(C语言讲解)

news2024/10/12 19:31:26

目录

0.前言

1.什么是带头双向循环链表

理解带头

​编辑

理解双向

理解循环

2.带头双向循环链表的实现

List.h文件中接口总览

具体实现 

结点的定义

申请结点

初始化

打印链表

尾插

尾删

头插

头删

​编辑​编辑

获取大小

查找 

在指定位置前插入

​编辑

删除指定位置的值


0.前言

本篇文章旨在讲解带头双向循环链表的实现,如果读者并不了解链表的基础知识,推荐阅读 —— 手撕数据结构 —— 单链表(C语言讲解)

1.什么是带头双向循环链表

理解带头

什么是带头:带头的意思是链表多申请一个结点放在链表的起始位置,该结点并不存储有效元素。上图中的head结点就是头结点,该结点也往往称之为哨兵位。

带头的作用:该结点的主要作用是为了方便实现链表和操作链表。主要体现在两个方面:1、提供统一的操作方式。2、避免二级指针的使用。

  • 提供统一的操作方式。因为操作没有头结点的链表的时候,往往需要记录当前结点的上一个结点,但是第一个结点是没有上一个结点的,就需要特殊处理,设置哨兵位的头结点可以对所有存储有效元素的结点提供统一的操作方式。
  • 避免二级指针的使用。在实现单链表的时候,我们有时候需要改变的是结构体指针,这个时候就需要将参数设置为二级指针。有的时候需要改变的是结构体当中的成员,这个时候需要将参数设置为一级指针。这样不方便理解和实现,引入哨兵位的头结点之后,我们不需要改变结构体指针,避免了二级指针的使用。

关于带头的好处读者先了解,后续实现会深有体会。

理解双向

什么是双向:双向就是指结点中会包含两个指针域,一个指针域记录上一个结点的地址,一个指针域记录下一个结点的地址。 不像单链表,只是记录了下一个结点的地址。

双向的作用:在链表的实现中,往往需要使用当前结点的上一个结点(比如在某个位置之前插入节点)。对于单链表来说,只能在寻找指定节点的时候记录上一个结点,操作比较复杂,而双向链表中记录了上一个结点的地址,直接就能找到上一个结点,操作简单。

理解循环

什么是循环:循环的意思就是链表形成回路,最后一个结点的指针域指向第一个节点。

循环的作用:在操作链表的时候,我们往往知道头结点,需要寻找尾结点。单链表只能遍历链表去找,时间复杂度尾O(N);双向循环链表的头结点中记录了尾结点的地址,直接就能找到,时间复杂度为O(1),在需要找尾结点的操作中,大大提高了效率。

可以看出,带头双向循环链表是对单链表的升级,是一种提高链表效率的结构,是一种十分优秀的结构。

2.带头双向循环链表的实现

实现带头双向循环链表,我们主要实现List.h文件和List.c文件,List.h文件中存放声明,List.c文件中存放定义。

List.h文件中接口总览

具体实现 

结点的定义

带头双向循环链表的结点中需要记录数据、上一个结点以及下一个结点。

  • data用来记录有效数据。
  • prev记录上一个结点的地址。
  • next记录下一个结点的地址。

申请结点

我们使用malloc函数申请结点。

  • 申请的数据的类型是自定义的结点类型。
  • data设置为指定的值。
  • next指针和prev指针设置为空。

初始化

初始化就是申请一个哨兵位的头结点,该节点的prev和next都指向自己。

  • 初始化的时候,我们申请的结点是在堆空间上申请的,堆上申请的变量除非手动释放,否则一直存在。
  • 最后返回指向这块空间的指针变量。

打印链表

打印链表只需要遍历输出即可。

  • 注意phead的值不能为空,使用断言暴力判断。
  • cur指向要打印的元素,从哨兵位的下一个结点开始打印,当 cur == phead 的时候,说明所有的结点都打印了,退出循环,打印结束。

注意,所有涉及 LTNode* 类型的指针都不能为空! 

尾插

在链表的末尾插入结点,寻找尾结点的时候,直接一步到位,不需要遍历寻找。找到尾结点之后,依次和前一个结点连接,和后一个结点连接即可。

尾删

删除尾结点的时候,首先要找到尾结点和尾结点的前一个结点;释放尾结点后,将新的尾结点和哨兵位连接。

头插

头插是在哨兵位的后面,第一个有效结点的前面插入数据。需要注意的是:

  • 该代码中并没有记录phead的下一个结点,连接的时候需要从后往前连接。如果记录了phead的下一个结点,那么先连接和后连接哪个结点都可以。

头删

在头部删除数据时,我们删除的是哨兵位后面的第一个结点。

  • 依次记录哨兵位后面的第一个结点和第二个结点,删除的时候,只需要改变对于指针的指向即可。

获取大小

和打印链表的方法是一样的,只不过遍历的时候记录结点的个数并返回即可。

查找 

查找和打印差不多,通过遍历进行查找,当结点的数据等于指定元素时,返回该节点的地址即可,没找到返回NULL。

在指定位置前插入

我们先记录pos位置的前一个位置,然后连接即可。

删除指定位置的值

删除指定位置的结点,我们可以先记录指定位置的前一个结点和后一个结点,释放指定位置的节点,然后连接posPrev和posNext即可。

3.完整代码附录

List.h

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

typedef int LTDataType;
typedef struct ListNode
{
	struct ListNode* next;                     //指向后一个结点 
	struct ListNode* prev;                     //指向前一个结点 
	LTDataType data;                           //存储数据 
}LTNode;


LTNode* BuyLTNode(LTDataType x);               //申请结点 

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

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

void LTPushBack(LTNode* phead, LTDataType x);  //尾插 

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

void LTPushFront(LTNode* phead, LTDataType x); //头插 

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

int LTSize(LTNode* phead);                     //获取链表大小 

LTNode* LTFind(LTNode* phead, LTDataType x);   //查找指定结点 

void LTInsert(LTNode* pos, LTDataType x);      //在指定结点位置插入 

void LTErase(LTNode* pos);                     //删除指定结点 

List.c

#include"List.h"

// 申请结点 
LTNode* BuyLTNode(LTDataType x)
{
	LTNode* node = (LTNode*)malloc(sizeof(LTNode));
	if (node == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}

	node->data = x;
	node->next = NULL;
	node->prev = NULL;

	return node;
}

// 初始化 
LTNode* LTInit()
{
	LTNode* phead = BuyLTNode(0);
	phead->next = phead;
	phead->prev = phead;

	return phead;
}

// 打印 
void LTPrint(LTNode* phead)
{
	assert(phead);

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

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

	LTNode* tail = phead->prev;      // 直接找到尾结点 
	LTNode* newnode = BuyLTNode(x);  // 申请一个新结点 

	// 连接newnode的tail 
	newnode->prev = tail;
	tail->next = newnode;
	// 连接newnode和phead 
	newnode->next = phead;
	phead->prev = newnode;
}

// 尾删 
void LTPopBack(LTNode* phead)
{
	assert(phead);
	assert(phead->next != phead);  // 保证链表中有数据才删 

	LTNode* tail = phead->prev;    // 找到尾结点 
	LTNode* tailPrev = tail->prev; // 找到尾结点的前一个结点 
	free(tail);                    // 释放尾结点 
	
	// 将新的尾结点和哨兵位连接 
	tailPrev->next = phead;
	phead->prev = tailPrev;
}

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

	LTNode* newnode = BuyLTNode(x); // 申请新结点 
	
	// 连接新结点和哨兵位的下一个结点 
	newnode->next = phead->next;
	phead->next->prev = newnode;
	
	// 连接哨兵位和新结点 
	phead->next = newnode;
	newnode->prev = phead;
}

// 头删 
void LTPopFront(LTNode* phead)
{
	assert(phead);
	assert(phead->next != phead); // 确保有存储数据的结点再删除 

	LTNode* first = phead->next;  // 记录第一个存储有效数据的结点 
	LTNode* second = first->next; // 记录第二个存储有效数据的结点 

	free(first);                  // 释放第一个存储有效数据的结点 

	// 将哨兵位和第二个存储有效数据的结点连接 
	phead->next = second;
	second->prev = phead;
}


// 查找 
LTNode* LTFind(LTNode* phead, LTDataType x)
{
	assert(phead);
	
	LTNode* cur = phead->next; // 记录第一个存储有效元素的结点 
	while(cur != phead)
	{
		if(cur->data == x)
			return cur;
	}
	
	return NULL;
}

// 在指定位置之前插入数据 
void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);                    // 指定的位置不能为空 

	LTNode* posPrev = pos->prev;    // 记录指定位置的前一个位置 
	
	LTNode* newnode = BuyLTNode(x); // 申请新结点 

	// 将新结点链接进链表中 
	posPrev->next = newnode;
	newnode->prev = posPrev;
	newnode->next = pos;
	pos->prev = newnode;
}

// 删除指定位置的值 
void LTErase(LTNode* pos)
{
	assert(pos);                  // 指定位置不能为空 
	
	LTNode* posPrev = pos->prev;  // 记录pos的前一个位置 
	LTNode* posNext = pos->next;  // 记录pos的后一个位置 

	free(pos);                    // 释放pos指向的节点 

	// 连接posPrev和posNext 
	posPrev->next = posNext;
	posNext->prev = posPrev;
}

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

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

相关文章

初知C++:AVL树

文章目录 初知C&#xff1a;AVL树1.AVL树的概念2.AVL树的是实现2.1.AVL树的结构2.2.AVL树的插入2.3.旋转2.4.AVL树的查找2.5.AVL树平衡检测 初知C&#xff1a;AVL树 1.AVL树的概念 • AVL树是最先发明的自平衡⼆叉查找树&#xff0c;AVL是⼀颗空树&#xff0c;或者具备下列性…

中国剩余定理 C++

题目 解题思路 原链接&#xff1a;https://www.acwing.com/solution/content/3539/ 大致步骤&#xff1a; 将第2,3,4…n个方程不断与第一个方程合并&#xff0c;得到方程a1k1a2k2m2-m1;用扩展欧几里得算法解出a1k1a2k2gcd(a1, a2)的结果&#xff0c;再将结果扩大(m2-m1)/d倍即…

2-laravel-路由配置

文章目录 定义控制器设计控制器设置路由启动服务 基本路由视图路由建立视图路由建立视图文件 控制器视图路由创建视图二级目录控制器 定义控制器 打开laravel 工程 建立一个 Demo 名字的控制器去集成 模板控制器 安装两个插件 设计控制器 <?phpnamespace App\Http\…

关于电动自行车新增的通信功能要求如下

1、 电动自行车应具有采用TLS加密的4G或5G公网通信模块&#xff0c;或类似功能的地面通信模块。 注:根据我国无线电管理有关规定&#xff0c;可能需要对无线电发射模块或整车进行无线电发射设备型号核准。 2、通信模块具备向电动自行车管理平台发送以下动态安全监测信息的功能&…

SpringBoot项目升级JDK版本(1.8 => 17)

项目&#xff1a;这里使用gitee上一个开源项目做测试《SpringBoot 流媒体项目》 工具&#xff1a;Intellij IDEA 一、下载项目&#xff08;git clone …&#xff09; 1、打开 pom.xml 看一下 2、启动项目&#xff0c;记住它最原本的样子先。成功启动后控制台后面是会输出地址…

怎么进行智能配音?一文告诉你

如何给文字智能配音呢&#xff1f;将文字转化为生动的语音&#xff0c;是提升内容吸引力的有效方式。 无论是制作视频、音频课程还是电子读物&#xff0c;合适的配音都能让信息传递更加高效。 如果你正寻找简单易用的方法来给文字添加配音&#xff0c;这里有几款智能配音软件…

0基础能不能转行做网络安全?

0基础能不能转行做网络安全&#xff1f;网络安全人才发展路线 最近有同学在后台留言&#xff0c;0基础怎么学网络安全&#xff1f;0基础可以转行做网络安全吗&#xff1f;以前也碰到过类似的问题&#xff0c;想了想&#xff0c;今天简单写一下。 我的回答是先了解&#xff0c…

Java对接ModbusRTU协议案例测试(相关代码及调试工具使用)

目的 在无设备的情况下&#xff0c;电脑搭建虚拟环境&#xff0c;进行代码编写以及测试 准备工作 一.测试工具 1.模拟虚拟串口工具&#xff0c;modbus RTU使用一问一答模式进行数据交互。 本文内容&#xff1a;模拟主站从站发送读取数据 2.个人设置&#xff1a;COM10为主站&am…

simulink分段函数

两段函数 20<v<60为真时&#xff0c;输出0.8 20<v<60为假时&#xff0c;即v<20或v>60时&#xff0c;输出0v>20时&#xff0c;输出0.8 v<20时&#xff0c;输出0 n段函数 n-1个switch模块

d3dcompiler_43.dll 文件的缺失常见方法分析,一键修复d3dcompiler_43.dll

在使用电脑的过程中&#xff0c;d3dcompiler_43.dll 文件的缺失是一个常见的技术挑战&#xff0c;这一问题通常会影响到基于Direct3D技术的应用程序的正常运行&#xff0c;可能导致程序启动失败或运行错误。d3dcompiler_43.dll 是DirectX组件的一部分&#xff0c;专责于编译用于…

CLIP图文多模态模型onnxruntime和tensorrt推理

首先下载github项目&#xff1a;https://github.com/Lednik7/CLIP-ONNX 修改clip_onnx/utils.py第61行opset_version12为opset_version15 , 运行测试脚本&#xff1a; import clip from PIL import Image import numpy as np# ONNX不支持CUDA model, preprocess clip.load(&q…

FFmpeg的简单使用【Windows】--- 视频倒叙播放

实现功能 点击【选择文件】按钮可以选择视频&#xff0c;当点击【开始处理】按钮之后&#xff0c;会先将视频上传到服务器&#xff0c;然后开始进行视频倒叙播放的处理&#xff0c;当视频处理完毕之后会将输出的文件路径返回&#xff0c;同时在页面中将处理好的视频展示出来。…

SpringBoot Data JPA基本使用

一、项目起步 1.1 pom配置 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId> </dependency><dependency><groupId>org.springframework.boot</groupId><…

基于ssm的美妆分享网站的设计与实现

文未可获取一份本项目的java源码和数据库参考。 经济的快速发展&#xff0c;人均可支配收入提高&#xff0c;多数年轻人化妆理念都已经改变。在化妆品消费渗透率不断提升、核心化妆人口持续扩散因素的多重影响下&#xff0c;化妆品行业持续高速增长&#xff0c;但是我们对美妆的…

C++题 十进制转二进制

文章目录 1. 使用C20 std::format2. 使用 std::bitset 类3. 手动实现十进制到二进制的转换反过来&#xff0c;手动二进制到十进制 VisualStudio2022使用C&#xff0c;进行十进制到二进制的转换&#xff0c;常见的实现方式 1. 使用C20 std::format 需要将VisualStudio默认的标准…

信息学奥赛复赛复习16-CSP-J2022-01乘方-循环特判、pow函数、快速幂

PDF文档回复:20241012 此前解析题&#xff0c;P8813 [CSP-J 2022] 乘方&#xff0c;给出了循环的解题思路&#xff0c;当时在洛谷提交是通过的&#xff0c;后台收到留言&#xff0c;a1,b1e9会炸吧&#xff1f;&#xff0c;确实啊整除要求1s内循环次数最大可以到10^7,现在测试数…

微信自动化工具,让多微管理更轻松更高效!

对于多个微信账号的管理&#xff0c;往往会让人感到繁琐和耗时。 这时&#xff0c;通过个微管理系统实现微信自动化设置&#xff0c;将大大提升我们的管理效率。 1、批量自动加好友 只需将客户号码一次性导入系统&#xff0c;设置好加好友规则&#xff0c;系统便会自动发送加…

PS文件保存后突然消失?别急,这里有7种解决方案!

咨询&#xff1a;“我刚做完的PS文件&#xff0c;保存得好好的&#xff0c;怎么就突然消失了呢&#xff1f;连回收站里都没有&#xff0c;这可怎么办才好。” 面对PS&#xff08;Photoshop&#xff09;文件在保存后突然消失的情况&#xff0c;许多设计师和图像处理爱好者可能会…

List的实现类

1.ArrayList&#xff08;数组&#xff09; &#xff08;1&#xff09;代码 新建学生类&#xff1a; package com.collection;public class Student {private String name;private int age;//添加构造方法 都是使用altenter快捷键public Student() {this.name name;this.age…

OCR经典神经网络(二)文本检测算法DBNet算法原理及其在icdar15数据集上的应用

OCR经典神经网络(二)文本检测算法DBNet算法原理及其在icdar15数据集上的应用 场景文本检测任务&#xff0c;一直以来是OCR整个任务中最为重要的一环。虽然有一些相关工作是端对端的&#xff0c;但是从工业界来看&#xff0c;相关落地应用较为困难。因此&#xff0c;两阶段的OC…