数据结构初阶--带头双向循环链表

目录

一.带头双向循环链表的定义

二.带头双向循环链表的功能实现

2.1.带头双向循环链表的定义

2.2.带头双向循环链表的结点创建

2.3.带头双向循环链表的初始化

2.4.带头双向循环链表的打印

2.5.带头双向循环链表的判空

2.6.带头双向循环链表的尾插

2.7.带头双向循环链表的头插

2.8.带头双向循环链表的尾删

2.9.带头双向循环链表的头删

2.10.带头双向循环链表的在pos位置之前插入

2.11.带头双向循环链表的删除pos位置的结点

2.12.带头双向循环链表的求链表长度

2.13.带头双向循环链表的销毁

2.14.完整程序

List.h

List.c

test.c

三. 顺序表和链表的比较

逻辑结构

存储结构

基本操作

创建

销毁

增加与删除

查找


一.带头双向循环链表的定义

循环单链表虽然能够实现从任一结点出发沿着链能找到其前驱结点,但时间耗费是O(n)。如果希望从表中快速确定某一个结点的前驱,另一个解决方法就是在单链表的每个结点里再增加一个指向其前驱的指针域prior。这样形成的链表中就有两条方向不同的链,称之为双(向)链表。

与单链表类似,双链表也可增加头结点使双链表的某些运算变得方便。同时双向链表也可以有循环表,称为双向循环链表。

由于在双向链表中既有前向链又有后向链,寻找任一结点的直接前驱结点与直接后继结点都变得非常方便了。

二.带头双向循环链表的功能实现

2.1.带头双向循环链表的定义

//定义
typedef int LTDataType;typedef struct ListNode
{struct ListNode* next;struct ListNode* pre;LTDataType data;
}LTNode;

 与单链表的定义不同,带头双向循环链表要定义两个指针:前驱指针pre和后继指针next。前驱指针pre用于指向当前结点的上一个结点,后继指针next用于指向当前结点的下一个结点。

2.2.带头双向循环链表的结点创建

LTNode* BuyListNode(LTDataType x)
{//动态开辟一个结点nodeLTNode* node = (LTNode*)malloc(sizeof(LTNode));//判空if (node == NULL){perror("malloc fail!");exit(-1);}//前驱与后继结点均置为空node->data = x;node->next = NULL;node->pre = NULL;return node;
}

结点的创建主要是通过调用malloc函数来实现,初始化时要将前驱指针和后继指针都置为NULL。

2.3.带头双向循环链表的初始化

版本一:

void ListInit(LTNode** phead)
{//这里需要传入二级指针,即传地址,才能实现对链表的修改//判空assert(phead);//创建头结点*phead = BuyListNode(-1);//将头结点的前驱指针和后继指针均指向自身(*phead)->next = *phead;(*phead)->pre = *phead;
}

版本二:

LTNode* ListInit()
{//创建头结点LTNode* phead = BuyListNode(-1);//将头结点的前驱指针和后继指针均指向自身phead->next = phead;phead->pre = phead;//返回头结点return phead;
}

链表的初始化采用了两种方式:传二级指针和设置返回值。

总结:

如果要改变头指针,就要传二级指针。不需要改变头指针的话,则传入一级指针。

在使用带头结点的单链表时:

  1. 初始化链表头指针需要传二级指针;
  2. 销毁链表需要传二级指针;
  3. 插入、删除、遍历、清空结点用一级指针即可。

不带头结点的单链表,除了初始化和销毁,插入、删除和清空结点也需要二级指针。

调试分析:

2.4.带头双向循环链表的打印

void ListPrint(LTNode* phead)
{//判空assert(phead);//cur指向链表的第一个结点LTNode* cur = phead->next;//cur依次向后遍历,直到cur重新回到头结点while (cur != phead){printf("%d ", cur->data);cur = cur->next;}printf("\n");
}

设置一个临时变量cur,指向当前链表的第一个结点(非头结点),然后依次向后遍历该链表,直到cur重新回到头结点phead的位置。

2.5.带头双向循环链表的判空

bool ListEmpty(LTNode* phead)
{//判空assert(phead);//如果phead->next等于phead,则链表为空,返回true//如果phead->next不等于phead,则链表不为空,返回falsereturn phead->next == phead;
}

如果phead->next等于phead,则链表为空,返回true;如果phead->next不等于phead,则链表不为空,返回false。

2.6.带头双向循环链表的尾插

void ListPushBack(LTNode* phead, LTDataType x)
{//判空assert(phead);//创建新结点LTNode* newnode = BuyListNode(x);//查找尾结点LTNode* tail = phead->pre;//尾插//原尾和新尾相互链接tail->next = newnode;newnode->pre = tail;//头结点和新尾相互链接newnode->next = phead;phead->pre = newnode;
}

相较于单链表的尾插,带头双向循环链表的尾插不需要从头结点开始依次向后遍历,因为头结点的前驱结点便指向尾结点tail。在找到尾结点tail之后,便可将新结点newnode插入到尾结点tail的后面。此时newnode变为新的尾结点。

调试分析:

运行结果:

2.7.带头双向循环链表的头插

void ListPushFront(LTNode* phead, LTDataType x)
{//判空assert(phead);//创建新结点LTNode* newnode = BuyListNode(x);//头插//phead newnode next:三者不分先后顺序//法一:LTNode* next = phead->next;phead->next = newnode;newnode->pre = phead;newnode->next = next;next->pre = newnode;//phead newnode phead->next:先处理后两个,再处理前两个//法二://phead->next->pre = newnode;//newnode->next = phead->next;//phead->next = newnode;//newnode->pre = phead;
}

在进行头插时,要注意结点之间插入的先后顺序,这里主要介绍两种方式。方式一:创建一个临时变量next,然后将头结点的下一个结点保存在next当中。首先调用BuyListNode(x)创建一个新结点newnode,然后将phead,newnode和next三个结点进行链接。三个结点不分先后顺序,直接进行链接即可。该方式最为简单,也最不容易出错;方式二:不创建临时变量next。首先调用BuyListNode(x)创建一个新结点newnode,然后将phead,newnode和phead->next三个结点进行链接。链接是关键:要先将后两个结点进行链接,然后再将前两个结点进行链接。三个结点一定要注意先后顺序,不可随意链接。

调试分析:

运行结果:

2.8.带头双向循环链表的尾删

void ListPopBack(LTNode* phead)
{//判空assert(phead);//判断链表是否为空assert(phead->next != phead);//assert(!ListEmpty(phead));//找尾结点LTNode* tail = phead->pre;//找尾结点的前一结点LTNode* tailPre = tail->pre;//释放尾结点free(tail);tailPre->next = phead;phead->pre = tailPre;
}

在进行尾删之前,首先要判断链表是否为空,可以通过phead->next != phead进行判断,也可以调用ListEmpty(phead)函数进行判断;然后找到链表的尾结点tail,以及链表尾结点的前一个结点tailPre;接着调用free函数释放尾结点tail,并将tailPre作为新的尾结点;最后再将新的尾结点与头结点phead进行相连即可。

调试分析:

运行结果:

2.9.带头双向循环链表的头删

void ListPopFront(LTNode* phead)
{//判空assert(phead);//判断链表是否为空assert(phead->next != phead);//assert(!ListEmpty(phead));//tail记录第一个结点之后的下一个结点LTNode* tail = phead->next->next;//释放第一个结点free(phead->next);//将头结点和tail相链接phead->next = tail;tail->pre = phead;
}

在进行头删之前,首先要判断链表是否为空,可以通过phead->next != phead进行判断,也可以调用ListEmpty(phead)函数进行判断;然后找到链表的第二个有效结点tail;接着调用free函数释放掉第一个有效结点,并将tail作为新的第一个有效结点;最后再将新的第一个结点tail与头结点phead进行相连即可。

调试分析:

运行结果:

2.10.带头双向循环链表的在pos位置之前插入

void ListInsert(LTNode* pos, LTDataType x)
{//判空assert(pos);//查找pos的前一个结点LTNode* pre = pos->pre;//创建新结点LTNode* newnode = BuyListNode(x);//pre newnode pospre->next = newnode;newnode->pre = pre;newnode->next = pos;pos->pre = newnode;
}

给定一个结点pos,如果是带头双向循环链表,那么pos之前的结点和pos之后的结点都是可知的。要在pos位置之前插入,首先要找到pos的前一结点pre,然后调用BuyListNode(x)创建一个新结点newnode,接着将pre,newnode和pos三个结点进行链接即可。此时pos位置的结点将由pos变为newnode。

调试分析:

运行结果:

2.11.带头双向循环链表的删除pos位置的结点

void ListErase(LTNode* pos)
{//判空assert(pos);//查找pos的前一个结点LTNode* pre = pos->pre;//查找pos的后一个结点LTNode* next = pos->next;//将前一个结点pre与后一个结点next相链接pre->next = next;next->pre = pre;//释放pos结点free(pos);
}

在删除pos位置的结点之前,首先要找到pos位置的前一个结点pre,然后找到pos位置的后一个结点next,接着将结点pre与next相链接,最后再调用free函数释放掉pos结点即可。

调试分析:

运行结果:

2.12.带头双向循环链表的求链表长度

int ListSize(LTNode* phead)
{//判空assert(phead);//cur指向当前链表的第一个结点LTNode* cur = phead->next;//用于记录遍历过的结点数int size = 0;//从第一个结点开始依次向后遍历,直到遍历到头结点while (cur != phead){++size;cur = cur->next;}return size;
}

调试分析:

运行结果:

2.13.带头双向循环链表的销毁

void ListDestory(LTNode* phead)
{//判空assert(phead);//cur指向当前第一个结点LTNode* cur = phead->next;while (cur != phead){//保存cur的下一个结点LTNode* next = cur->next;//删除curListErase(cur);//更新curcur = next;}//释放头结点free(phead);
}

调试分析:

运行结果:

2.14.完整程序

List.h

#pragma once#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>//带头双向循环链表//定义
typedef int LTDataType;typedef struct ListNode
{struct ListNode* next;struct ListNode* pre;LTDataType data;
}LTNode;//创建结点
LTNode* BuyListNode(LTDataType x);//初始化:版本一
//void ListInit(LTNode** phead);//初始化:版本二
LTNode* ListInit();//打印
void ListPrint(LTNode* phead);//判空
bool ListEmpty(LTNode* phead);//尾插
//不用二级指针的原因:尾插时不会改变phead,因为它带哨兵位,尾插时不会对哨兵位进行修改
void ListPushBack(LTNode* phead, LTDataType x);//头插
void ListPushFront(LTNode* phead, LTDataType x);//尾删
void ListPopBack(LTNode* phead);//头删
void ListPopFront(LTNode* phead);//在pos位置之前插入
void ListInsert(LTNode* pos, LTDataType x);//删除pos位置的结点
void ListErase(LTNode* pos);//链表长度
int ListSize(LTNode* phead);//销毁
void ListDestory(LTNode* phead);

List.c

#define _CRT_SECURE_NO_WARNINGS 1#include"List.h"//创建结点
LTNode* BuyListNode(LTDataType x)
{//动态开辟一个结点nodeLTNode* node = (LTNode*)malloc(sizeof(LTNode));//判空if (node == NULL){perror("malloc fail!");exit(-1);}//前驱与后继结点均置为空node->data = x;node->next = NULL;node->pre = NULL;return node;
}//初始化
/*
void ListInit(LTNode** phead)
{//这里需要传入二级指针,即传地址,才能实现对链表的修改//判空assert(phead);//创建头结点*phead = BuyListNode(-1);//将头结点的前驱指针和后继指针均指向自身(*phead)->next = *phead;(*phead)->pre = *phead;
}
*///初始化
LTNode* ListInit()
{//创建头结点LTNode* phead = BuyListNode(-1);//将头结点的前驱指针和后继指针均指向自身phead->next = phead;phead->pre = phead;//返回头结点return phead;
}//打印
void ListPrint(LTNode* phead)
{//判空assert(phead);//cur指向链表的第一个结点LTNode* cur = phead->next;//cur依次向后遍历,直到cur重新回到头结点while (cur != phead){printf("%d ", cur->data);cur = cur->next;}printf("\n");
}//判空
bool ListEmpty(LTNode* phead)
{//判空assert(phead);//如果phead->next等于phead,则链表为空,返回true//如果phead->next不等于phead,则链表不为空,返回falsereturn phead->next == phead;
}//尾插
void ListPushBack(LTNode* phead, LTDataType x)
{//判空assert(phead);/*//创建新结点LTNode* newnode = BuyListNode(x);//查找尾结点LTNode* tail = phead->pre;//尾插//原尾和新尾相互链接tail->next = newnode;newnode->pre = tail;//头结点和新尾相互链接newnode->next = phead;phead->pre = newnode;*///尾插ListInsert(phead, x);//是phead而不是phead->pre
}//头插
void ListPushFront(LTNode* phead, LTDataType x)
{//判空assert(phead);/*//创建新结点LTNode* newnode = BuyListNode(x);//头插//phead newnode next:三者不分先后顺序//法一:LTNode* next = phead->next;phead->next = newnode;newnode->pre = phead;newnode->next = next;next->pre = newnode;//phead newnode phead->next:先处理后两个,再处理前两个//法二://phead->next->pre = newnode;//newnode->next = phead->next;//phead->next = newnode;//newnode->pre = phead;*///头插ListInsert(phead->next, x);
}//尾删
void ListPopBack(LTNode* phead)
{//判空assert(phead);//判断链表是否为空assert(phead->next != phead);//assert(!ListEmpty(phead));/*//找尾结点LTNode* tail = phead->pre;//找尾结点的前一结点LTNode* tailPre = tail->pre;//释放尾结点free(tail);tailPre->next = phead;phead->pre = tailPre;*///尾删ListErase(phead->pre);
}//头删
void ListPopFront(LTNode* phead)
{//判空assert(phead);//判断链表是否为空assert(phead->next != phead);//assert(!ListEmpty(phead));/*//tail记录第一个结点之后的下一个结点LTNode* tail = phead->next->next;//释放第一个结点free(phead->next);//将头结点和tail相链接phead->next = tail;tail->pre = phead;*///头删ListErase(phead->next);
}//在pos位置之前插入x
void ListInsert(LTNode* pos, LTDataType x)
{//判空assert(pos);//查找pos的前一个结点LTNode* pre = pos->pre;//创建新结点LTNode* newnode = BuyListNode(x);//pre newnode pospre->next = newnode;newnode->pre = pre;newnode->next = pos;pos->pre = newnode;
}//删除pos位置的结点
void ListErase(LTNode* pos)
{//判空assert(pos);//查找pos的前一个结点LTNode* pre = pos->pre;//查找pos的后一个结点LTNode* next = pos->next;//将前一个结点pre与后一个结点next相链接pre->next = next;next->pre = pre;//释放pos结点free(pos);
}//链表长度
int ListSize(LTNode* phead)
{//判空assert(phead);//cur指向当前链表的第一个结点LTNode* cur = phead->next;//用于记录遍历过的结点数int size = 0;//从第一个结点开始依次向后遍历,直到遍历到头结点while (cur != phead){++size;cur = cur->next;}return size;
}//销毁
void ListDestory(LTNode* phead)
{//判空assert(phead);//cur指向当前第一个结点LTNode* cur = phead->next;while (cur != phead){//保存cur的下一个结点LTNode* next = cur->next;//法一:删除cur//ListErase(cur);//法二:删除curfree(cur);//更新curcur = next;}//释放头结点free(phead);
}

test.c

#define _CRT_SECURE_NO_WARNINGS 1#include"List.h"void test()
{LTNode* plist = NULL;//初始化plist = ListInit();//头插ListPushFront(plist, 1);ListPushFront(plist, 2);ListPushFront(plist, 3);ListPushFront(plist, 4);ListPushFront(plist, 5);ListPrint(plist);ListDestory(plist);ListPrint(plist);
}int main()
{test();return 0;
}

三. 顺序表和链表的比较

下面分别从逻辑结构,存储结构,基本操作的角度对顺序表和链表进行比较。

逻辑结构

都属于线性表,都是线性结构。

存储结构

基本操作

对于任何一个数据结构,基本操作基本都能归纳为创销,增删改查。其中改建立在查的基础上。

创建

销毁

增加与删除

查找

用链表还是顺序表

          顺序表          链表
弹性(可扩容)             ×           √
     增,删             ×           √
        改             √           ×

表长难以预估,经常需要增加/删除元素--链表

表长可预估,查询(搜索)操作较多--顺序表

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

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

相关文章

【node.js】02-path模块

目录 1. path.join() 2. path.basename() 3. path.extname() 1. path.join() 使用 path.join() 方法&#xff0c;可以把多个路径片段拼接为完整的路径字符串&#xff0c;语法格式如下&#xff1a; path.join([...paths]) 例子&#xff1a; const path require(path)co…

白皮书|数字孪生工厂:成就智能制造的未来

前言 目前我国已经进入“技术应用”的数字孪生双驱动时代&#xff0c;在市场需求和国家政策的推动下&#xff0c;各地因地制宜发展数字孪生技术&#xff0c;深入开展数字孪生应用建设。今天我们就重点来聊一聊数字孪生技术在工厂中的应用&#xff0c;希望通过本次案例的深度剖析…

深“扒”云原生高性能分布式文件系统JuiceFS

JuiceFS 是一款面向云原生设计的高性能分布式文件系统&#xff0c;在 Apache 2.0 开源协议下发布。提供完备的 POSIX 兼容性&#xff0c;可将几乎所有对象存储接入本地作为海量本地磁盘使用&#xff0c;亦可同时在跨平台、跨地区的不同主机上挂载读写。 JuiceFS 简介 JuiceFS…

测试PHP与MySQL数据库连接查询,新手

创建库 &#xff1a;create database if not exists web; 创建表 &#xff1a; create table if not exists web.students(id int,name varchar(3),age int,birth datetime); 插入数据&#xff1a;insert web.students values (1,"张三",18,now()),(1,"李…

智慧养殖APP及小程序多端跨平台技术选型分析

智慧养殖APP及小程序多端跨平台技术选型分析 1. 框架选择 ------Uniapp------ 优势&#xff1a;使用Vue.js语法&#xff0c;学习成本较低&#xff1b;一套代码&#xff0c;能同时支持APP/H5/小程序&#xff0c;是较为全面的跨平台框架。 ​ 开发小程序的不二选择&#xff0c;…

线性代数——特征值和特征向量

文章目录 版权声明补充知识求和公式的性质常用希腊字符读音 特征值和特征向量相似矩阵相似对角化实对称矩阵 版权声明 本文大部分内容皆来自李永乐老师考研教材和视频课。 补充知识 求和公式的性质 ∑ i 1 n k a i k ∑ i 1 n a i \sum_{i1}^nka_ik\sum_{i1}^na_i i1∑n​…

visual studio 2022换背景遇到的问题

如果要自定义背景图&#xff0c;则可以下载ClaudialIDE 1.在拓展->点击拓展管理->右上角搜索background->点击下载ClaudialIDE->加载完之后需要关闭vs界面进行下载&#xff0c;下载失败&#xff0c;弹出“由于出现以下错误 无法安装一个或多个扩展”。 解决&#x…

推荐几款不错的AI绘画工具

随着近年来数据、算法等核心技术的不断进步&#xff0c;人工智能在内容创作各垂直领域的比例不断增加&#xff0c;包括人工智能写作、人工智能编辑和最近流行的人工智能绘画。 许多朋友也想跟上潮流&#xff0c;使用人工智能绘画生成软件创建人工智能图像&#xff0c;但我不知…

ssh、scp和sshpass使用

1、ssh ssh 用户名ip地址2、scp 2.1 拉取文件 scp -r 用户名1ip地址:/远程文件目录/远程服务器文件 /本地文件目录/2.2 上传文件 scp -r /本地文件 用户名1ip地址:/远程文件目录/3、sshpass 3.1 定义 sshpass 的定义&#xff1a; ssh 登陆不能在命令行中指定密码&#xf…

手机变局2023:一场瞄准产品和技术的“思维革命”

以折叠屏冲高端&#xff0c;已成为中国手机厂商们的共识。 在这个苹果未涉足的领域&#xff0c;国产手机厂商们加快脚步迭代推新&#xff0c;积极抢占机遇。但平心而论&#xff0c;虽然国产折叠屏机型众多&#xff0c;但市场上始终缺乏一款突破性的产品作为标杆&#xff0c;为…

数据结构【树】

第五章 树 一、树 1.定义&#xff1a;n个结点的集合&#xff0c;n0为空树&#xff1b;是递归的&#xff0c;是一种逻辑结构&#xff0c;同时也是一种分层结构&#xff1b; 特点 树的根结点没有前驱结点&#xff0c;除了根结点外的所有结点有且只有一个前驱结点&#xff1b;树…

MySQL学习-第一部分

文章目录 MySQL数据库1 概述1.1概述1.2安装MYSQL&#xff08;Windows下&#xff09;1.2.1需要注意的事项&#xff1a; 1.3 Mysql的卸载1.4查看Mysql服务1.5启动/关闭SQL的服务1.6客户端登录/使用MySql登录退出问题&#xff1a;ERROR 1045 (28000)解决方案第一步&#xff1a;关闭…

Clion开发STM32之W5500系列(综合实验)

说明 此为w5500模块的综合实验测试模块,包含dhcp、dns、ntp以上三个模块的驱动参考之前的文章&#xff0c;本篇不做说明.使用的开发芯片 stm32f103vet6系列,外设接口使用的spi2 实验内容: 通过dhcp动态获取ip,通过dns解析NTP服务域名的ip通过NTP服务ip获取时间 w5500配置驱…

学生护眼台灯几瓦最舒适?2023全新五款护眼台灯推荐

每次经过学校&#xff0c;发现戴眼镜的小学生真的不少&#xff0c;通过近几年的儿童以及青少年的近视人数可看出&#xff0c;我国的近视人数中&#xff0c;儿童以及青少年占比53.5%&#xff0c;所以许多家长逐渐重视孩子用眼健康问题&#xff0c;会选择护眼台灯用于孩子写作业和…

第1章 获取数据库中的数据

CoreShop源程序是以数据库优先进行定义的&#xff0c;所以其本身不包含代码优先的定义&#xff0c;但本从更习惯于代码优先&#xff0c;所以为其定义了代码优先的定义。 1 CoreCms.Net.Model.Entities.SysRole using SqlSugar; using System.ComponentModel.DataAnnotations…

【JavaEE初阶】Tomcat安装与使用及初识Servlet

文章目录 1. Tomcat的安装与使用1.1 Tomcat安装1.2 Tomcat的启动1.3 Tomcat部署前端页面 2. Servlet2.1 Servlet是什么2.2 第一个Servlet程序2.3 常见错误 1. Tomcat的安装与使用 1.1 Tomcat安装 在浏览器中搜索Tomcat,打开官方网页.Tomcat官网 点击下载Tomcat8. 点击下载压…

PSP - 使用 MMseqs2 工具快速搜索蛋白质序列数据库 (GMGC)

欢迎关注我的CSDN&#xff1a;https://spike.blog.csdn.net/ 本文地址&#xff1a;https://spike.blog.csdn.net/article/details/131934642 MMseq2 是非常强大和高效的生物信息学软件&#xff0c;可以在极短的时间内对大规模的核苷酸和蛋白质序列进行搜索和聚类。主要特点有&a…

fpga_pwm呼吸灯(EP4CE6F17C8)

文章目录 一、呼吸灯二、代码实现三、引脚分配 一、呼吸灯 呼吸灯是指灯光在微电脑的控制之下完成由亮到暗的逐渐变化&#xff0c;使用开发板上的四个led灯实现1s间隔的呼吸灯。 二、代码实现 c module pwm_led( input clk ,input rst_n ,output reg [3:0] led ); …

spring学习笔记六

一、构造器注入 1.1、BookDao接口和实现类 public interface BookDao {void save(); } public class BookDaoImpl implements BookDao {private int connectionNum;private String databaseName;public BookDaoImpl(int connectionNum, String databaseName) {this.connectio…

【Spring MVC】小文件下载的多种方法

文章目录 HTTP Header 之 Content-Disposition文件下载代码Servlet 实现方式 1Servlet 实现方式 2Spring 实现方式 1 Content-Disposition 指定 inline Win、JDK 17、 Spring Boot 3.1.2 HTTP Header 之 Content-Disposition 以下内容来自 mdn web docs 在常规的 HTTP 应答中&…