数据结构——带头双向循环链表(c语言实现)

目录

 

1.单链表和双向链表对比

2.双向链表实现

2.1 创建新节点

2.2 链表初始化 

2.3 尾插 

2.4 头插 

2.5 尾删 

2.6 头删 

2.7 查找 

2.8 指定位置后插入数据 

2.9 删除指定节点 

2.10 销毁链表

2.11 打印链表 


 前言:

     我们在前几期详细地讲解了不带头单向不循环链表(单链表),使用它的底层代码实现了一个简单的通讯录项目,也介绍了链表分为八种,但是其中最常用的只有两种:(1)不带头单向不循环链表,(2)带头双向循环链表,今天我们要讲解的就是第二种带头双向循环链表

1.单链表和双向链表对比

在介绍双向链表之前,我们先来对比一下单链表和双向链表的区别。

这是单链表:

这是双向链表:

 

        双向链表的特点是每相邻两个节点都相互连接,每个节点都有三个部分,包括data,next,prev,其中data负责存放数据,next负责存放后一个节点的地址,prev负责存放前一个节点的地址,最后一个节点和头节点(哨兵位)相互连接,形成了一个循环双向链表,那么什么是哨兵位呢,哨兵位就是双向链表的头节点,它不存放有效数据,只存放第一个有序数据的节点的地址和最后一个有序数据节点的地址。 

       那么它们的区别是什么呢?

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

2.双向链表实现

  介绍完了双向链表的区别我们接下来就要着手开始用代码实现双向链表了。由于代码可能较多,我们将双向链表的代码分成了三个文件,分别是List.h,List.c和tste.c文件:

在list.h文件中,我们要包含我们会用到的头文件,其他文件只要包含List.h文件就可以使用这些头文件了:

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

双向链表的实现也是在此文件中,由于我们不知道将来使用链表会存放什么样的数据,所以我们使用typedef对这个数据的类型改名,我们实现链表使用的是int类型,所以我们对int改名:

typedef int ListNodeData;

链表中的next和prev链表是用来存放节点地址的,所以它们为指针类型,而为了方便使用,我们将链表使用typedf改名,下面是双向链表实现:

typedef struct ListNode
{ListNodeData data;struct ListNode* prev;struct ListNode* next;}LTNode;

2.1 创建新节点

创建新节点我们使用malloc从堆申请一块LTNode类型大小的内存,它的data类型用来存放将来要插入的数据,prev和next指针在创建这个节点时先让它指向自己,如果要创建新节点,就调用这个函数,它会返回一个指向这块空间的指针:

LTNode* LTBuyNode(ListNodeData x)
{LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));if (newnode == NULL){perror("malloc fail");exit(1);}newnode->data = x;newnode->next = newnode;newnode->prev = newnode;return newnode;}//创建一个新节点

2.2 链表初始化 

 

在最初创建一个链表时,它的内部为空,什么也没有,我们初始化应该此链表让它至少有一个哨兵位:

void LTInit(LTNode** pphead)
{assert(pphead);*pphead = LTBuyNode(-1);}//初始化

2.3 尾插 

尾插操作应该先创建一个新节点,插入顺序为:先让新节点的prev指针指向最后一个节点,然后让新节点的next指针指向哨兵位实现循环,以上的两步都不会影响旧节点,接下来就是让最后一个节点的next指针指向这个新节点,然后让哨兵位的prev指针指向新节点完成尾插:

具体实现代码为:

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

2.4 头插 

     头插操作并不是将新节点放在哨兵位之前,而是将新节点放在第一个有效数据节点之前,所以我们应该将新节点放在哨兵位的后面。先创建一个新节点,让新节点的prev指针指向哨兵位,让它的next指针指向哨兵位的next指向的节点,以上两步不会影响任何节点,做完这两步后,先让哨兵位后面那个节点的prev指针指向新节点,然后让哨兵位的next指针指向新节点,这两步不能调换顺序,否则会找不到哨兵位后面那个节点,下面是代码实现:

void LTPushFront(LTNode* phead, ListNodeData x)
{assert(phead);LTNode* newnode = LTBuyNode(x);newnode->next = phead->next;newnode->prev = phead;phead->next->prev = newnode;phead->next = newnode;}//头插

2.5 尾删 

   执行删除操作之前我们应该先判断这个链表除哨兵位之外有没有其他节点,如果没有,就无法删除,而尾删操作也比较简单,只需要让尾节点的前一个节点的next指针指向哨兵位,然后让哨兵位的prev指针指向位尾节点,以上过程需要创建一个新变量,否则无法找到我们要删除的节点,接着释放掉尾节点后置空就可以了:

void LTPopBack(LTNode* phead)
{assert(phead&&phead->next);LTNode* del = phead->prev;del->prev->next = del->next;del->next->prev = del->prev;free(del);del = NULL;}//尾删

画图演示:

 

2.6 头删 

  头删操作与尾删类似,如果没有两个及以上节点的话无法执行删除操作,头删要删除的是哨兵位后面那个节点,所以我们先创建一个指针存放我们要删除节点的地址,将哨兵位的next指针指向我们要删除节点的下一个节点,然后将我们要删除节点的下一个节点的prev指针指向哨兵位,完成这些操作后释放我们要删除的节点然后置空:

void LTPopFront(LTNode* phead)
{assert(phead && phead->next);LTNode* del = phead->next;phead->next = del->next;del->next->prev = phead;free(del);del = NULL;
}//头删

2.7 查找 

如果我们要查找一个节点,应该先判断链表是否为空,然后将我们要查找的节点的数据与链表中节点的数据一一对比,如果数据内容相同,说明找到了,将这个节点返回,如果循环一圈还没有找到,说明链表中不存在这样的节点,返回应一个空指针:

LTNode* LTFind(LTNode* phead, ListNodeData x)
{assert(phead && phead->next);LTNode* pcur = phead->next;while (pcur != phead){if (pcur->data == x){return pcur;}pcur = pcur->next;}return NULL;
}//查找函数

2.8 指定位置后插入数据 

我们可以先调用查找函数,然后调用它返回的节点,试着在它的后面插入数据,而它的操作与头插非常相像,只是将哨兵位改成了我们指定的节点:

void LTInsertAfter(LTNode* phead, LTNode* pop, ListNodeData x)
{assert(phead&&pop);LTNode* newnode = LTBuyNode(x);newnode->prev = pop;newnode->next = pop->next;pop->next->prev = newnode;pop->next = newnode;
}//指定节点后插入数据

2.9 删除指定节点 

删除节点我们需要先判断链表中是否有两个及以上节点,否则无法删除,删除指定节操作我们先将指定节点的前一个节点的next指向我们指定节点的下一个节点,然后将指定节点的下一个节点的prev指针指向指定节点的前一个节点,这个过程不需要创建中间变量,因为我们有指定节点的地址:

void LTErase(LTNode* phead, LTNode* pop)
{assert(phead && phead->next);pop->next->prev = pop->prev;pop->prev->next = pop->next;free(pop);pop = NULL;}//指定删除节点

2.10 销毁链表

我们链表的每一个节点都是使用malloc函数手动在堆上申请的,需要我们手动释放:

void LTDestroy(LTNode* phead)
{assert(phead);LTNode* pcur = phead->next;while (pcur != phead){LTNode* next = pcur->next;free(pcur);pcur = next;}}//销毁链表

2.11 打印链表 

指行这么多插入删除操作我们如果测试的话就使用这个函数打印出来,而打印函数只需要循环打印这个链表一次就可以了:

void LTPrint(LTNode* phead)
{assert(phead);LTNode* pcur = phead->next;while (pcur != phead){printf("%d->", pcur->data);pcur = pcur->next;}printf("NULL\n");}//打印

接下来我们使用这个函数来测试一下我们的方法:

可以看到我们的方法都没有问题,那么这期的双向链表就到此结束啦,我将代码放在下面,感兴趣的小伙伴可以试试哦。

List.h :

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>typedef int ListNodeData;
typedef struct ListNode
{ListNodeData data;struct ListNode* prev;struct ListNode* next;}LTNode;LTNode* LTFind(LTNode* phead, ListNodeData x);//查找void LTInit(LTNode** pphead);//初始化void LTPushBack(LTNode* phead, ListNodeData x);
//尾插void LTPrint(LTNode* phead);//打印链表void LTPushFront(LTNode* phead, ListNodeData x);
//头插void LTPopBack(LTNode* phead);//尾删void LTPopFront(LTNode* phead);//头删void LTInsertAfter(LTNode* phead, LTNode* pop, ListNodeData x);
//指定节点后删除void LTErase(LTNode* phead, LTNode* pop);
//指定位置删除void LTDestroy(LTNode* phead);
//销毁链表

List.c :

#define _CRT_SECURE_NO_WARNINGS 1
#include"List.h"LTNode* LTBuyNode(ListNodeData x)
{LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));if (newnode == NULL){perror("malloc fail");exit(1);}newnode->data = x;newnode->next = newnode;newnode->prev = newnode;return newnode;}//创建一个新节点LTNode* LTFind(LTNode* phead, ListNodeData x)
{assert(phead && phead->next);LTNode* pcur = phead->next;while (pcur != phead){if (pcur->data == x){return pcur;}pcur = pcur->next;}return NULL;
}//查找函数void LTInit(LTNode** pphead)
{assert(pphead);*pphead = LTBuyNode(-1);}//初始化void LTPushBack(LTNode* phead, ListNodeData x)
{assert(phead);LTNode* newnode = LTBuyNode(x);newnode->next = phead;newnode->prev = phead->prev;phead->prev->next = newnode;phead->prev = newnode;
}//尾插void LTPushFront(LTNode* phead, ListNodeData x)
{assert(phead);LTNode* newnode = LTBuyNode(x);newnode->next = phead->next;newnode->prev = phead;phead->next->prev = newnode;phead->next = newnode;}//头插void LTPopBack(LTNode* phead)
{assert(phead&&phead->next);LTNode* del = phead->prev;del->prev->next = del->next;del->next->prev = del->prev;free(del);del = NULL;}//尾删void LTPopFront(LTNode* phead)
{assert(phead && phead->next);LTNode* del = phead->next;phead->next = del->next;del->next->prev = phead;free(del);del = NULL;
}//头删void LTInsertAfter(LTNode* phead, LTNode* pop, ListNodeData x)
{assert(phead&&pop);LTNode* newnode = LTBuyNode(x);newnode->prev = pop;newnode->next = pop->next;pop->next->prev = newnode;pop->next = newnode;
}//指定节点后插入数据void LTErase(LTNode* phead, LTNode* pop)
{assert(phead && phead->next);pop->next->prev = pop->prev;pop->prev->next = pop->next;free(pop);pop = NULL;}//指定删除节点void LTPrint(LTNode* phead)
{assert(phead);LTNode* pcur = phead->next;while (pcur != phead){printf("%d->", pcur->data);pcur = pcur->next;}printf("NULL\n");}//打印void LTDestroy(LTNode* phead)
{assert(phead);LTNode* pcur = phead->next;while (pcur != phead){LTNode* next = pcur->next;free(pcur);pcur = next;}}//销毁链表

test.c :

#define _CRT_SECURE_NO_WARNINGS 1
#include"List.h"
void test01()
{LTNode* plist = NULL;LTInit(&plist);printf("尾插\n");LTPushBack(plist, 1);LTPushBack(plist, 2);LTPushBack(plist, 3);LTPrint(plist);printf("头插\n");LTPushFront(plist, 0);LTPrint(plist);/*printf("尾删\n");LTPopBack(plist);LTPopBack(plist);LTPrint(plist);*/printf("头删\n");LTPopFront(plist);LTPopFront(plist);LTPrint(plist);printf("在3后面插入数据:\n");LTNode* Find = LTFind(plist, 3);/*if (Find == NULL){printf("找不到!\n");}else{printf("找到了\n");}*/LTInsertAfter(plist, Find, 56);LTPrint(plist);printf("删除指定节点3:\n");LTErase(plist, Find);LTPrint(plist);printf("销毁链表\n");LTDestroy(plist);plist = NULL;}int main()
{test01();return 0;
}

 

 

 

 

 

 

 

 

 

 

 

 

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

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

相关文章

一个项目学习Vue3---快速认识JSX

JSX&#xff08;JavaScript XML&#xff09;是一种用于在React框架中编写UI组件的语法扩展。它允许开发者将HTML标记直接嵌入到JavaScript代码中&#xff0c;使得在React组件中编写界面变得更加直观和高效。在编译过程中&#xff0c;JSX会被转换成普通的JavaScript对象&#xf…

工业液晶屏G065VN01 V2规格书简介

G065VN01 V2 背面实物图 2. 概述 G065VN01 V2 专为 VGA &#xff08;640 x RGB x 480&#xff09; 分辨率和 16.2M&#xff08;RGB 6 位 FRC&#xff09;或 262k 色&#xff08;RGB 6 位&#xff09;的工业显示应用而设计。它由TFT-LCD面板、驱动IC、控制和电源电路板以及包括…

css3实现水纹进度条

其实有一个mask-image属性 挺有意思&#xff0c;在元素上面实现遮罩层的效果&#xff0c;不过这玩意有些兼容性问题 需要处理&#xff0c;所以单纯可以通过渐变色的方式来实现 同时加上动画效果 .jianbian {width: 100%;height: 16px;background-color: #eee;display: flex;bor…

华三中小企业组网

一、组网需求 在中小园区中&#xff0c;S5130系列或S5130S系列以太网交换机通常部署在网络的接入层&#xff0c;S5560X系列或 S6520X系列以太网交换机通常部署在网络的核心&#xff0c;出口路由器一般选用MSR系列路由器。 核心交换机配置VRRP保证网络可靠性。园区网中不同的…

MySQL进阶——锁

目录 1全局锁—一致性数据备份 1.1全局锁介绍 1.2语法 1.3 一致性备份案例 1.4 全局锁特点 2表级锁 2.1表锁 2.1.1共享读锁 2.1.2独占写锁 2.2元数据锁 2.3元数据锁 MySQL中的锁&#xff0c;按照锁的粒度分&#xff0c;分为以下三类&#xff1a; &#xff08;1&…

GitLab配置免密登录之后仍然需要Git登录的解决办法

GitLab配置免密登录之后仍然需要Git登录的解决办法 因为实习工作需要&#xff0c;要在本地拉取gitlab上的代码&#xff0c;设置了密钥之后连接的时候还需要登录的token&#xff0c;摸索之后有了下面的解决办法。 方法一&#xff1a; 根据报错的提示&#xff0c;去网站上设置个人…

动手学自然语言处理:解读大模型背后的核心技术

自从 ChatGPT 横空出世以来&#xff0c;自然语言处理&#xff08;Natural Language Processing&#xff0c;NLP&#xff09; 研究领域就出现了一种消极的声音&#xff0c;认为大模型技术导致 NLP “死了”。在某乎上就有一条热门问答&#xff0c;大家热烈地讨论了这个问题。 有…

【STM32】看门狗

1.看门狗简介 看门狗起始就是一个定时器&#xff0c;从功能上说它可以让微控制器在程序发生意外&#xff08;程序进入死循环或跑飞&#xff09;的时候&#xff0c;能重新恢复到系统刚上电状态&#xff0c;以保障系统出问题的时候可以重启一次。说的简单一点&#xff0c;看门狗…

用英文介绍孟买:Mumbai India‘s Transforming MEGACITY

Mumbai: India’s Transforming MEGACITY Link: https://www.youtube.com/watch?vtWD_-Rzrn8o Summary First Paragraph: Mumbai, India’s financial and entertainment capital, is undergoing a major transformation. With its contiguous urban population nearing 25…

web图片怎么导入ps?这个方法给你轻松解决!

随着WebP格式图片因其体积小、加载快的优势在网站中日益普及&#xff0c;对于图片编辑者来说&#xff0c;能够直接在Photoshop中打开和编辑WebP文件变得尤为重要。 WebPShop插件应运而生&#xff0c;它是一个专为Photoshop设计的模块&#xff0c;支持打开和保存WebP图像&#…

ATFX汇市:澳大利亚5月CPI大增0.4百分点,降息预期显著降温

ATFX汇市&#xff1a;据澳大利亚统计局数据&#xff0c;澳大利亚5月加权CPI年率为4%&#xff0c;高于前值3.6%&#xff0c;高于预期3.8%&#xff0c;显示出澳大利亚通胀率颇具韧性。5月份数据公布之前&#xff0c;月度CPI年率平均波幅不足0.1个百分点&#xff0c;呈现出横盘震荡…

《数字图像处理》实验报告六

一、实验任务与要求 比较采用不同的色彩空间对彩色图像处理的效果&#xff0c;处理包括&#xff1a; a&#xff09;直方图均衡化 b&#xff09;图像增强 二、实验报告 &#xff08;一&#xff09;RGB色彩空间的直方图均衡化 / 锐化处理 1、matlab 实现代码&#xff1a; %…

推荐系统(LLM去偏?) | (WSDM24)预训练推荐系统:因果去偏视角

::: 大家好&#xff01;今天我分享的文章是来自威斯康星大学麦迪逊分校和亚马逊AWS AI实验室的最新工作&#xff0c;文章所属领域是推荐系统和因果推理&#xff0c;作者针对跨域推荐中的偏差问题提出了一种基于因果去偏的预训练推荐系统框架PreRec。 ::: 原文&#xff1a;Pre-t…

mobaXterm上传文件进度一直为0%

在这里修改了senssion、重启都没有用 最后发现是文件存放的路径中不能有中文&#xff0c;改了之后就成功上传了

开展FMEA培训时需要做好哪些准备?

FMEA&#xff08;失效模式与影响分析&#xff09;作为一种预防性的质量工具&#xff0c;正逐渐成为当代企业提升产品竞争力的关键。然而&#xff0c;很多企业在开展FMEA时&#xff0c;却常常因为准备工作不足而事倍功半。那么&#xff0c;开展FMEA培训时需要做好哪些准备呢&…

Jenkins流水线发布,一篇就解决你的所有疑惑

这次搭建的项目比较常规,前端是react写的,后端是springboot,并且由于是全栈开发,所以是在同一个项目中。接下来我演示下怎么用jenkins进行自动化发布。 1.jenkins必装插件 这里用到的是jenkinsFile主要是基于Groovy这个沙盒,有些前置插件。这里使用maven进行打包,所以需…

测试基础16:测试用例设计方法-测试大纲法

课程大纲 1、应用场景 验证页面跳转&#xff1a;有多个窗口/页面&#xff0c;每个窗口/页面有多个动作&#xff0c;每个动作之间有相互的联系的场景。看点击后&#xff0c;页面跳转正确与否。 2、设计步骤 step1.列出大纲&#xff1a;列出涉及的页面和页面可执行的动作。 s…

生命在于学习——Python人工智能原理(4.7)

四、Python的程序结构与函数 4.4 函数 函数能将代码划分为若干模块&#xff0c;每一个模块可以相对独立的实现某一个功能&#xff0c;函数有两个主要功能&#xff0c;分别是降低编程难度和实现代码复用&#xff0c;函数是一种功能抽象&#xff0c;复用它可以将一个复杂的大问…

【C#】找不到属性集方法。get只读属性用了反射设置setValue肯定报错

欢迎来到《小5讲堂》 这是《C#》系列文章&#xff0c;每篇文章将以博主理解的角度展开讲解。 温馨提示&#xff1a;博主能力有限&#xff0c;理解水平有限&#xff0c;若有不对之处望指正&#xff01; 背景 找不到属性集方法。get只读属性用了反射设置setValue肯定报错 报错…

IPFoxy Tips:匿名海外代理IP的使用方法及注意事项

在互联网上&#xff0c;隐私和安全问题一直备受关注。为了保护个人隐私和数据安全&#xff0c;使用匿名代理IP是一种常用的方法。匿名代理IP可以隐藏用户的真实IP地址&#xff0c;使用户在访问网站时更加隐秘和安全。 本文将介绍匿名代理IP的基本原理和核心功能。 基本原则 匿…