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

目录

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.mzph.cn/diannao/55990.shtml

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

相关文章

基于顺序表实现通讯录项目

目录 1.通讯录的基本构成 2.通讯录的底层原理 3.通讯录的底层——顺序表 ——————————————————————————————————————————— 正文开始 1. 通讯录的基本构成 1.1 联系人信息的主要内容 ●姓名 ●性别 ●年龄 ●电话 ●住址 1.2 数…

第33次CCF计算机软件能力认证-第4题十滴水

题干&#xff1a; 十滴水是一个非常经典的小游戏。 小 C C C 正在玩一个一维版本的十滴水游戏。 我们通过一个例子描述游戏的基本规则。 游戏在一个 1 c 1c 1c 的网格上进行&#xff0c;格子用整数 x ( 1 ≤ x ≤ c ) x(1≤x≤c) x(1≤x≤c) 编号&#xff0c;编号从左往…

【Flutter、H5、Web?前端个人总结】分享从业经历经验、自我规范准则,纯干货

前言 hi&#xff0c;正式接触web前端已经经过了两年的时间&#xff0c;从大学的java后端转型到web前端&#xff0c;再到后续转战Flutter&#xff0c;逐渐对前端有了一些心得体会&#xff0c;其实在当下前端的呈现形式一直在变化&#xff0c;无论你是用原生、还是web还是混编的…

初始项目托管到gitee教程,开箱即用

0.本地仓库与远程仓库关联&#xff08;需先在gitee创建仓库&#xff09; ①打开powershell生成ssh key ssh-keygen -t ed25519 -C "Gitee SSH Key"-t key 类型-C 注释 生成成功如下&#xff0c;并按下三次回车 ②查看公私钥文件 ls ~/.ssh/输出&#xff1a; id_…

怎么提取伴奏?5个人声分离方法一步搞定!

在进行音乐创作或混音等操作时&#xff0c;提取出音乐中的伴奏是一个常见的需求。伴奏是指音乐中除了主旋律和歌唱部分之外的音轨&#xff0c;通常包括鼓、贝斯、吉他等乐器的演奏&#xff0c;提取出伴奏可以让我们更方便地对音乐进行处理。 本文分享几个可以提取伴奏的方法&a…

el-table表头加红色星标

代码&#xff1a; <el-table-column prop"name" label"姓名" width"auto"><template #header><span style"color: red; margin-right: 4px">*</span><span>姓名</span></template></el…

Mysql—高可用集群MHA

1:什么是MHA&#xff1f; MHA&#xff08;Master High Availability&#xff09;是一套优秀的MySQL高可用环境下故障切换和主从复制的软件。 MHA 的出现就是解决MySQL 单点的问题。 MySQL故障切换过程中&#xff0c;MHA能做到0-30秒内自动完成故障切换操作。 MHA能在故障切…

22个无版权的4K高清视频素材网站,各种风格视频都有!

平时我也会做一些短视频和宣传片&#xff01; 所以&#xff0c;对找素材这回事不在话下&#xff0c;国内外也有不少高清无水印的视频素材网站&#xff0c;今天就分享一些高清剪辑必备的视频素材渠道&#xff0c;不少也是免费哒&#xff0c;平时需要找素材的同学千万不要错过啦…

Python中的数据可视化艺术:用Matplotlib和Seaborn讲故事

Python中的数据可视化艺术&#xff1a;用Matplotlib和Seaborn讲故事 数据可视化不仅仅是图表的绘制&#xff0c;更是通过视觉形式传达复杂信息的一种艺术。使用Python中的两个强大的库——Matplotlib和Seaborn&#xff0c;可以将数据转化为清晰、优美的图表&#xff0c;帮助我…

【Kubernetes】常见面试题汇总(五十六)

目录 123. pod 创建失败&#xff1f; 124. kube-flannel-ds-amd64-ndsf7 插件 pod 的 status 为 Init:0/1 &#xff1f; 特别说明&#xff1a; 题目 1-68 属于【Kubernetes】的常规概念题&#xff0c;即 “ 汇总&#xff08;一&#xff09;~&#xff08;二十二&#x…

计算机前沿技术-人工智能算法-大语言模型-最新研究进展-2024-10-05

计算机前沿技术-人工智能算法-大语言模型-最新研究进展-2024-10-04 目录 文章目录 计算机前沿技术-人工智能算法-大语言模型-最新研究进展-2024-10-04目录1. LLM-Augmented Symbolic Reinforcement Learning with Landmark-Based Task Decomposition摘要研究背景问题与挑战如何…

『网络游戏』下线数据清理【29】

修改服务端脚本&#xff1a;ServerSession.cs 修改服务端脚本&#xff1a;ServerRoot.cs 修改服务端脚本&#xff1a;CacheSvc.cs 修改服务端脚本&#xff1a;LoginSys.cs 运行服务器 运行客户端 - 连续登录账号 本章结束

VSCode的常用插件(持续更新)

点击左边工具栏的“扩展”&#xff0c;在搜索栏中查找对应插件&#xff0c;点击“安装”&#xff0c;安装完成后右边界面的插件会显示“卸载”按钮。 1、中文&#xff08;简体&#xff09;语言包 2、Auto Rename Tag 修改开始标签&#xff0c;结束标签也会随之自动变化。 3、O…

docker+mysql创建用户名密码_docker里面的mysql 更换密码

进入mysql容器 操作vi etc/mysql/my.cnf 默认是不安装vi编辑器的&#xff0c;下面安装vi 更新安装包 apt-get update 安装vim 执行这条语句 apt-get install vim 到修改docker容器里面的mysql数据库密码了 启动mysql容器 docker exec -it mysql /bin/bash 编辑配置文件 我这里是…

【C++】第五节:内存管理

1、C/C内存分布 看下面一段代码 int globalVar 1; static int staticGlobalVar 1; void Test() {static int staticVar 1;int localVar 1;int num1[10] { 1, 2, 3, 4 };char char2[] "abcd";const char* pChar3 "abcd";int* ptr1 (int*)malloc(s…

与C++内存管理和STL简介的爱恨情仇

本文 1.C/C内存分布2.C语言中动态内存管理方式&#xff1a;malloc/calloc/realloc/free总结 3.C内存管理方式new/delete操作内置类型new和delete操作自定义类型 4.operator new与operator delete函数&#xff08;重要点进行讲解&#xff09;5.new和delete的实现原理内置类型自定…

制造业DT数字化之生产制造业务建模

一、工厂建模为何物&#xff1f; 对制造业人员&#xff08;人&#xff09;、设备&#xff08;机&#xff09;、材料&#xff08;料&#xff09;、工艺流程&#xff08;法&#xff09;、工厂环境&#xff08;环&#xff09;数据化管理的过程就叫工厂建模。 二、制造建模有哪几大…

【操作系统】引导(Boot)电脑的奇妙开机过程

&#x1f339;&#x1f60a;&#x1f339;博客主页&#xff1a;【Hello_shuoCSDN博客】 ✨操作系统详见 【操作系统专项】 ✨C语言知识详见&#xff1a;【C语言专项】 目录 什么是操作系统的引导&#xff1f; 操作系统的引导&#xff08;开机过程&#xff09; Windows操作系…

QD1-P2 HTML 编辑器:HBuilderX

本节学习&#xff1a; HTML课程内容介绍HBuilderX编辑器的使用 本节视频 www.bilibili.com/video/BV1n64y1U7oj?p2 HTML 内容 基础语法 标签整体架构DOCTYPE 常用标签 标题和水平线段落和换行列表div 和 span格式化标签图片超链接标签表格表单字符实体 编辑器 HBuilder…

设计测试用例的方法

目录 1、等价类 2、边界值 3、场景法 4、正交表法 5、设计正交表 6、判定表法 7、错误猜想法 1、等价类 在测试中选取一些数据作为等价类进行测试&#xff0c;如果测试通过&#xff0c;就代表测试通过&#xff0c;可以用少量代表性的测试数据取得较好的测试结果。 等价类…