数据结构学习之路--实现带头双向循环链表的详解(附C源码)

本文主要是介绍数据结构学习之路--实现带头双向循环链表的详解(附C源码),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

   嗨嗨大家~本期带来的内容是:带头双向循环链表的实现。在上期文章中我们提到过带头双向循环链表,那么它的实现又是怎样的呢?今天我们来一探究竟!


目录

前言 

一、认识带头双向循环链表

1 认识双向链表 

2 带头双向循环链表的定义

二、带头双向循环链表的实现 

2.1 定义

2.2 创建结点 

2.3 初始化 

方法一: 

方法二:

2.4 链表的判空

2.5 链表的尾插 

2.6 链表的头插 

方法一: 

方法二:

2.7 链表的尾删 

2.8 链表的头删 

2.9 在pos位置之前插入 

2.10  删除pos位置的结点

2.11 链表的长度

2.12 链表的打印

2.13 链表的销毁

三、总代码 


前言 

   我们在上期内容中讲过,链表结构是多样化的。但在实际中最常用的只有两种:无头单向非循环链表带头双向循环链表。前者已经在上篇博客(http://t.csdnimg.cn/s8ieT)进行了全面的讲解,现在我们来认识并实现后者。

一、认识带头双向循环链表

1 认识双向链表 

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

   与单链表类似,双向链表也可增加头结点使双向链表的某些运算变得方便。同时双向链表也可以有循环表,称为双向循环链表。 由于在双向链表中既有前向链又有后向链,所以寻找任一结点的直接前驱结点与直接后继结点都变得非常便捷。

2 带头双向循环链表的定义

   带头双向循环链表:结构最复杂,一般用于单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了。

二、带头双向循环链表的实现 

2.1 定义

代码实现:

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

分析:这里与单链表的定义不同,带头双向循环链表要定义两个指针:前驱指针prev和后继指针next。前驱指针prev用于指向当前结点的上一个结点,后继指针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->prev = NULL;return node;
}

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

2.3 初始化 

  • 带头双向循环链表的初始化可以使用两种方法:传二级指针设置返回值

方法一: 

代码实现:

//初始化
void ListInit(LTNode** phead)
{//这里需要传入二级指针,即传地址,才能实现对链表的修改//判空assert(phead);//创建头结点*phead = BuyListNode(-1);//因为是带头双向链表,故将头结点的前驱指针和后继指针均指向它们自己(*phead)->next = *phead;(*phead)->prev = *phead;
}

方法二:

代码实现:

初始化
LTNode* ListInit()
{//创建头结点LTNode* phead = BuyListNode(-1);//因为是带头循环双向链表,故将头结点的前驱指针和后继指针均指向它们自己phead->next = phead;phead->prev = phead;//返回头结点return phead;
}

注意:

  • 若想要改变头指针,就要传二级指针;不需要改变头指针的话,便传入一级指针。
  • 在使用带头结点的单链表时:
  1. 初始化链表头指针需要传二级指针;
  2. 销毁链表需要传二级指针;
  3. 插入、删除、遍历、清空结点用一级指针即可。
  • 不带头结点的单链表,除了初始化和销毁,插入、删除和清空结点也需要二级指针。 

2.4 链表的判空

代码实现:

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.5 链表的尾插 

代码实现:

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

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

2.6 链表的头插 

  • 带头双向循环链表的头插有两种方式实现。

方法一: 

代码实现:

//头插
void ListPushFront(LTNode* phead, LTDataType x)
{//判空assert(phead);//创建新结点LTNode* newnode = BuyListNode(x);//phead newnode next:三者不分先后顺序LTNode* next = phead->next;phead->next = newnode;newnode->prev = phead;newnode->next = next;next->prev = newnode;
}

方法二:

代码实现:

//头插
void ListPushFront(LTNode* phead, LTDataType x)
{//判空assert(phead);//创建新结点LTNode* newnode = BuyListNode(x);//phead newnode phead->next:先处理后两个,再处理前两个phead->next->prev = newnode;newnode->next = phead->next;phead->next = newnode;newnode->prev = phead;
}

分析:当进行头插时,要注意结点之间插入的先后顺序,这里主要介绍两种方式:

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

2.7 链表的尾删 

代码实现:

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

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

2.8 链表的头删 

代码实现:

//头删
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->prev = phead;
}

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

2.9 在pos位置之前插入 

代码实现:

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

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

2.10  删除pos位置的结点

代码实现:

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

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

2.11 链表的长度

 代码实现:

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

2.12 链表的打印

代码实现:

//打印
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.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);
}

总结:可以在该链表的任意位置插入和删除(但不能删除head),也无需考虑特殊情况进行单独判断。 

三、总代码 

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* prev;LTDataType data;
}LTNode;//创建结点
LTNode* BuyListNode(LTDataType x);//初始化:方法一
//void ListInit(LTNode** phead);//初始化:方法二
LTNode* ListInit();//判空
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 ListPrint(LTNode* phead);//销毁
void ListDestory(LTNode* phead);
List.c#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->prev = NULL;return node;
}
​​//初始化
void ListInit(LTNode** phead)
{//这里需要传入二级指针,即传地址,才能实现对链表的修改//判空assert(phead);//创建头结点*phead = BuyListNode(-1);//因为是带头双向链表,故将头结点的前驱指针和后继指针均指向它们自己(*phead)->next = *phead;(*phead)->prev = *phead;
}​​/*
//初始化
LTNode* ListInit()
{//创建头结点LTNode* phead = BuyListNode(-1);//因为是带头循环双向链表,故将头结点的前驱指针和后继指针均指向它们自己phead->next = phead;phead->prev = phead;//返回头结点return phead;
}
*///判空
​​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->prev;//原尾和新尾相互链接tail->next = newnode;newnode->prev = tail;//头结点和新尾相互链接newnode->next = phead;phead->prev = newnode;
}
​//头插
void ListPushFront(LTNode* phead, LTDataType x)
{//判空assert(phead);//创建新结点LTNode* newnode = BuyListNode(x);//phead newnode next:三者不分先后顺序LTNode* next = phead->next;phead->next = newnode;newnode->prev = phead;newnode->next = next;next->prev = newnode;
}
​​//头插
void ListPushFront(LTNode* phead, LTDataType x)
{//判空assert(phead);//创建新结点LTNode* newnode = BuyListNode(x);//phead newnode phead->next:先处理后两个,再处理前两个phead->next->prev = newnode;newnode->next = phead->next;phead->next = newnode;newnode->prev = phead;
}
​​//尾删
void ListPopBack(LTNode* phead)
{//判空assert(phead);//判断链表是否为空assert(phead->next != phead);//assert(!ListEmpty(phead));//找尾结点LTNode* tail = phead->prev;//找尾结点的前一结点LTNode* tailPrev = tail->prev;//释放尾结点free(tail);tailPrev->next = phead;phead->prev = tailPrev;
}
​​//头删
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->prev = phead;
}
​​//在pos前插入
void ListInsert(LTNode* pos, LTDataType x)
{//判空assert(pos);//查找pos的前一个结点LTNode* prev = pos->prev;//创建新结点LTNode* newnode = BuyListNode(x);//prev newnode posprev->next = newnode;newnode->prev = prev;newnode->next = pos;pos->prev = newnode;
}
​​//删除pos位置的结点
void ListErase(LTNode* pos)
{//判空assert(pos);//查找pos的前一个结点LTNode* prev = pos->prev;//查找pos的后一个结点LTNode* next = pos->next;//将前一个结点pre与后一个结点next相链接prev->next = next;next->prev = prev;//释放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;//删除curListErase(cur);//更新curcur = next;}//释放头结点free(phead);
}​
test.c#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;
}

   大家学到这里,链表的知识分享已经接近尾声,综合本期文章以及前两篇博客来看,其实不难发现,对于任何一个数据结构,基本操作大致上都能归纳为创建销毁,增删改查。其中改建立在查的基础上。


   那么本期的内容就告一段落,有关于链表的总结也已经结束,此时的你是否对链表有了更深层次的了解和掌握呢?如果大家觉得这篇文章对你们有所帮助,记得给博主留下三连支持哈~你们的支持是我创作的最大动力!博主也会继续竭尽所能地为大家带来更加优质的内容,当然啦,或许我存在许多不足之处,欢迎各位佬们的指点!请相信相信的力量,一切也终有回甘!诸君加油~我们下期再会啦。

这篇关于数据结构学习之路--实现带头双向循环链表的详解(附C源码)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/910188

相关文章

MySQL数据库双机热备的配置方法详解

《MySQL数据库双机热备的配置方法详解》在企业级应用中,数据库的高可用性和数据的安全性是至关重要的,MySQL作为最流行的开源关系型数据库管理系统之一,提供了多种方式来实现高可用性,其中双机热备(M... 目录1. 环境准备1.1 安装mysql1.2 配置MySQL1.2.1 主服务器配置1.2.2 从

C++中unordered_set哈希集合的实现

《C++中unordered_set哈希集合的实现》std::unordered_set是C++标准库中的无序关联容器,基于哈希表实现,具有元素唯一性和无序性特点,本文就来详细的介绍一下unorder... 目录一、概述二、头文件与命名空间三、常用方法与示例1. 构造与析构2. 迭代器与遍历3. 容量相关4

C++中悬垂引用(Dangling Reference) 的实现

《C++中悬垂引用(DanglingReference)的实现》C++中的悬垂引用指引用绑定的对象被销毁后引用仍存在的情况,会导致访问无效内存,下面就来详细的介绍一下产生的原因以及如何避免,感兴趣... 目录悬垂引用的产生原因1. 引用绑定到局部变量,变量超出作用域后销毁2. 引用绑定到动态分配的对象,对象

Linux kill正在执行的后台任务 kill进程组使用详解

《Linuxkill正在执行的后台任务kill进程组使用详解》文章介绍了两个脚本的功能和区别,以及执行这些脚本时遇到的进程管理问题,通过查看进程树、使用`kill`命令和`lsof`命令,分析了子... 目录零. 用到的命令一. 待执行的脚本二. 执行含子进程的脚本,并kill2.1 进程查看2.2 遇到的

MyBatis常用XML语法详解

《MyBatis常用XML语法详解》文章介绍了MyBatis常用XML语法,包括结果映射、查询语句、插入语句、更新语句、删除语句、动态SQL标签以及ehcache.xml文件的使用,感兴趣的朋友跟随小... 目录1、定义结果映射2、查询语句3、插入语句4、更新语句5、删除语句6、动态 SQL 标签7、ehc

SpringBoot基于注解实现数据库字段回填的完整方案

《SpringBoot基于注解实现数据库字段回填的完整方案》这篇文章主要为大家详细介绍了SpringBoot如何基于注解实现数据库字段回填的相关方法,文中的示例代码讲解详细,感兴趣的小伙伴可以了解... 目录数据库表pom.XMLRelationFieldRelationFieldMapping基础的一些代

Java HashMap的底层实现原理深度解析

《JavaHashMap的底层实现原理深度解析》HashMap基于数组+链表+红黑树结构,通过哈希算法和扩容机制优化性能,负载因子与树化阈值平衡效率,是Java开发必备的高效数据结构,本文给大家介绍... 目录一、概述:HashMap的宏观结构二、核心数据结构解析1. 数组(桶数组)2. 链表节点(Node

Java AOP面向切面编程的概念和实现方式

《JavaAOP面向切面编程的概念和实现方式》AOP是面向切面编程,通过动态代理将横切关注点(如日志、事务)与核心业务逻辑分离,提升代码复用性和可维护性,本文给大家介绍JavaAOP面向切面编程的概... 目录一、AOP 是什么?二、AOP 的核心概念与实现方式核心概念实现方式三、Spring AOP 的关

详解SpringBoot+Ehcache使用示例

《详解SpringBoot+Ehcache使用示例》本文介绍了SpringBoot中配置Ehcache、自定义get/set方式,并实际使用缓存的过程,文中通过示例代码介绍的非常详细,对大家的学习或者... 目录摘要概念内存与磁盘持久化存储:配置灵活性:编码示例引入依赖:配置ehcache.XML文件:配置

从基础到高级详解Go语言中错误处理的实践指南

《从基础到高级详解Go语言中错误处理的实践指南》Go语言采用了一种独特而明确的错误处理哲学,与其他主流编程语言形成鲜明对比,本文将为大家详细介绍Go语言中错误处理详细方法,希望对大家有所帮助... 目录1 Go 错误处理哲学与核心机制1.1 错误接口设计1.2 错误与异常的区别2 错误创建与检查2.1 基础