网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
目录
一、前言
在笔者的前两篇文章具体讲解的单链表的实现,即无头单向非循环。但是除此之外,链表还有另外七种类型,本篇文章将主要介绍双向链表的实现,即带头双向链表。相信有的小伙伴一定会问那另外六种需不需要讲解呢,要不要掌握呢?其实,条条大路通罗马,在掌握的单链表和双链表这两大基础链表,其他的链表的实现和运用也是轻而易举,毫不费力。
因此,就让我们一起实现双向链表叭!
二、正文——双向链表的实现
2.1模块化
相信看过博主前几篇文章的小伙伴对**“模块化**”已经熟悉的不能再熟悉了,为了后期代码的修改和维护能够更加简便,我们这次双向链表的实现依旧采取模块化的方式。本次的程序共分为3个文件,分别是:List.c[具体功能的实现]、List.h[[函数、变量的声明]]、Text.c[测试部分]。
2.2 数据类型与结构体定义
在进行链表功能的实现之前,首先是要对数据类型进行定义和结构体定义。
数据类型采取的是整形的形式,与前文的单链表相同。
结构体定义这一块则与单链表的定义有所不同。由于单链表是单向链表,故每个结点只需存放下一个结点的地址,而无需存放上一个结点的地址。而双向链表,顾名思义是双向的,通过一个结点我们能够找到它的上一个结点和下一个结点。因此在对结构体进行定义的时候我们就需要两个结构体指针,分别用于存放该结点的上下结点。
具体代码实现如下:
//数据类型与结构体定义
typedef int LTDataType;
typedef struct ListNode
{
struct ListNode* next; //存放下一个结点
struct ListNode* prev; //存放上一个结点
LTDataType data; //数据类型
}LTNode;
2.3链表的初始化
在进行完数据类型和结构体定义,还需要做一些准备工作,来帮助我们更好的实现和测试后续的链表功能。而准备工作大致分为以下几点:链表的初始化,链表的打印,链表的查找,判断链表是否只有哨兵卫,申请新的结点。
我们先从链表的初始化开始吧。首先毫无疑问的是先建立的链表的哨兵卫,为了表现链表双向的特性,接下来我们还要将哨兵卫的上下结点指向它本身。
具体代码如下:
//链表初始化
LTNode* LTInit();
//链表初始化
LTNode* LTInit()
{
LTNode* phead =BuyListNode(-1);
phead->next = phead; //下一个结点指向本身
phead->prev = phead; //上一个结点指向本身
return phead;
}
2.4链表的打印
链表的打印是为了更好的测试我们后续的插入、删除是否完成。那么该如何实现链表的打印呢?
双向链表的打印与单链表也是有所不同的。对于单链表的打印,我们只需从从头指针开始遍历链表,直至为空。而对于双向链表来说,遍历链表是不可行的,这是由它的结构决定的。双向链表的链接是循环的,单凭遍历的话,是永远也不可能将这个链表打印结束的,因此我们就需要做一点小小的改变,让遍历链表的指针【cur】从哨兵卫的下一个结点开始遍历,直至遍历到哨兵卫停止,这样就可以将链表的数据全部打印出来了。
具体代码如下:
//链表的打印[声明]
void LTPrint(LTNode* phead);
//链表的打印[具体实现]
void LTPrint(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
if (cur->next == phead)
{
printf("<->%d<->", cur->data);
}
else
{
printf("<->%d", cur->data);
}
cur = cur->next;
}
printf("\n");
}
2.5 链表的查找
为了后续链表指定位置的插入和删除,我们需要找到指定数据所指向的结点,为了提高代码的复用,我们将链表的查找单独封装成一个函数。
那么该如何去编写呢?这里的代码其实与单向链表差不多,即遍历链表,直至找到数据所对应的结点,并将其地址返回即可。相对而言还是很简单的。
具体代码实现如下:
//链表的查找[声明]
LTNode* LTFind(LTNode* phead, LTDataType x);
//链表的查找[实现]
LTNode* LTFind(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
2.6判断链表是否只有哨兵卫
这一部分是为了链表的删除的准备的,对于单链表而言,当链表为空的时候,就不能进行数据的删除了,而对于双向链表来说,有了哨兵卫的存在,自然就不存在链表为空的情况,相应的,就是当双向链表只存在哨兵卫的时候,就不能进行数据的删除了。
那么如何判断链表是否只有哨兵卫呢?方法其实很简单,只需要判断所给的头指针的上下结点是否指向本身即可,当然两条件选其一即可。
具体代码实现如下:
//判断链表是否只有哨兵卫[声明]
bool LTEmpty(LTNode* phead);
//判断链表是否只有哨兵卫[实现]
bool LTEmpty(LTNode* phead)
{
assert(phead);
return phead->next == phead;
}
2.7申请新的结点
这一部分主要是为了链表的插入而准备的,无论是头插、尾插还是指定位置的插入都需要申请新的结点,而重复的书写未免使代码显得冗杂,因此将“申请新的结点”这一功能单独分离出来并封装成一个函数,大大提高代码的简洁性。
而这一功能的实现与单链表大同小异,都是使用内存函数来向内存申请一片空间并返回指向这片空间的指针,不同的只是结构体的内容发生了改变而已。
具体代码如下:
//申请新的结点[声明]
LTNode* BuyListNode(LTDataType x);
/申请新的结点[实现]
LTNode* BuyListNode(LTDataType x)
{
LTNode* newnode = malloc(sizeof(LTNode));
if (newnode == NULL)
{
perror("malloc error");
}
else
{
newnode->data = x;
//避免野指针
newnode->next = NULL;
newnode->prev = NULL;
return newnode;
}
}
2.8 链表的尾插
准备工作准备就绪后,就开始链表的主要功能的实现啦。
首先就是链表的尾插。我们先来看看单链表是如何进行尾插的,对于单链表,我们需要对单链表进行遍历找到最后一个结点,再对这个结点和新结点进行链接的操作。而对于双向链表,就没有这么复杂了,因为哨兵卫的上一个结点就已经指向了链表的最后一个结点,在找到这个尾结点之后,剩下的就是链接的问题了。相比于单链表要复杂一点,要将哨兵卫、尾结点、新结点这三个结点进行链接。
具体链接和代码如下:
//链表的尾插[声明]
void LTPushBack(LTNode* phead, LTDataType x);
//链表的尾插[实现]
void LTPushBack(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* tail = phead->prev;
//创建新的结点
LTNode* newnode = BuyListNode(x);
//尾结点的链接
tail->next = newnode;
newnode->prev = tail;
//哨兵卫的链接
phead->prev = newnode;
newnode->next = phead;
}
2.9链表的尾删
在讲解完链表的尾插,接着就是链表的尾删了。同样地,我们不需要对链表进行遍历来找到尾结点,只需要通过哨兵卫来找到最后一个结点,并将其置空,再将倒数第二个结点和哨兵卫进行链接。不
具体图片和代码如下:
//链表的尾删
void LTPopBack(LTNode* phead);
//链表的尾删
void LTPopBack(LTNode* phead)
{
assert(phead);
//判断是否只有哨兵卫
assert(!LTEmpty(phead));
//改变哨兵卫的链接
phead->prev = phead->prev->prev;
//释放尾结点的内存
free(phead->prev->next);
//改变尾部链接
phead->prev->next = phead;
}
2.10链表的头插
在尾插、尾删之后,就是头插、头删了,那么该如何实现链表的头插呢。
其步骤也很简单,先是申请一个新的结点,进而将该结点与哨兵卫与链表的头结点进行链接。需要注意的一点就是判断链表是否只有哨兵卫,若是只有哨兵卫就无须删除了。
具体图片和代码如下:
//链表的头插[声明]
void LTPushFront(LTNode* phead, LTDataType x);
//链表的头插[实现]
void LTPushFront(LTNode* phead, LTDataType x)
{
//创建新的结点
LTNode* newnode = BuyListNode(x);
LTNode* first = phead->next;
//改变链接关系
newnode->next = first;
first->prev = newnode;
phead->next = newnode;
newnode->prev = phead;
//代码复用
//LTInsert(phead->next, x);
}
2.11链表的头删
头插之后便是头删了,这一部分的代码也是相对简单的,主要思路是将哨兵卫与头结点的下一个结点链接,并将头结点的内存释放。唯一需要注意的一点就是判断链表是否只有哨兵卫,若是只有哨兵卫就无须删除了。
//链表的头删[声明]
void LTPopFront(LTNode* phead);
//链表的头删[实现]
void LTPopFront(LTNode* phead)
{
assert(phead);
//改变链接关系+释放内存
phead->next = phead->next->next;
free(phead->next->prev);
phead->next->prev = phead;
}
2.12链表的插入【pos之前】
链表的头插、头删、尾插、尾删已经全部讲解完毕了,但其实对于双向链表来说,以上四个功能并不需要,只需要通过链表的插入和删除便可以实现,也就是代码的复用。但是为了帮助小伙伴们更好的理解双向链表,便将上述功能一一实现。
接下来就让我们进入“”链表的插入“”这一功能的实现,我们采取的是在**"pos"之前进行数据的插入**。对“pos”之后插入数据的感兴趣的小伙伴也可以尝试自己编写,两者的逻辑是类似的。
具体的实现逻辑呢,就是申请一个新的结点,并将pos位置的结点、pos位置之前的结点和新结点进行链接。不过需要注意的一点是,如果你用的的指针过少,便要注意链接的顺序,若是链接顺序便可能无法找到原来结点的数据,比较保险且快捷的方法就是再另外设置一个指针用来记录链接的结点,便可以毫无顾虑的进行结点间的链接了。
注:在尾插的代码中对该段代码的复用有些独特,是通过传哨兵卫的指针进行插入,这是由于双向链表的循环特性决定的,链表的最后一个结点就是哨兵卫的前一个结点。
//链表的插入(前)[声明]
void LTInsert(LTNode* pos, LTDataType x);
//链表的插入(前)[实现]
void LTInsert(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* newnode = BuyListNode(x);
LTNode* prev = pos->prev;
//改变链接关系
prev->next = newnode;
newnode->prev = prev;
newnode->next = pos;
pos->prev = newnode;
}
2.13链表的删除
链表的删除的实现逻辑大致就是将要删除的结点置空,并将其前后两个结点进行链接即可。不过要注意的依旧是链表是否只含有哨兵卫,若只含有哨兵卫,就无需进行链表的删除。
//链表的删除[声明]
void LTErase(LTNode* pos);
//链表的删除[实现]
void LTErase(LTNode* pos)
{
assert(pos);
LTNode* next = pos->next;
LTNode* prev = pos->prev;
//改变链接关系并释放内存
prev->next = next;
next->prev = prev;
free(pos);
}
2.14链表的销毁
不知不觉之中,就来到了双向链表的最后一个功能:链表的销毁
其实现逻辑:从哨兵卫的下一个结点开始遍历链表,边遍历边销毁,直至遍历到哨兵卫为止,最后将哨兵卫的内存销毁并将指针置空。
//链表的销毁[声明]
void LTDestroy(LTNode* phead);
//链表的销毁[实现]
void LTDestroy(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
LTNode* next = cur->next;
free(cur);
cur = next;
}
free(phead);
phead = cur=NULL;
![img](https://img-blog.csdnimg.cn/img_convert/01d07fc5fcdd92377ffec5232c5624f2.png)
![img](https://img-blog.csdnimg.cn/img_convert/129dc108ce3a818901bc2f5b14f42aa7.png)
![img](https://img-blog.csdnimg.cn/img_convert/9bea90b83b01c45f4e6f2abeed0747ff.png)
**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!**
**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**
**[需要这份系统化资料的朋友,可以戳这里获取](https://bbs.csdn.net/topics/618545628)**
//链表的销毁[声明]
void LTDestroy(LTNode* phead);
//链表的销毁[实现]
void LTDestroy(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
LTNode* next = cur->next;
free(cur);
cur = next;
}
free(phead);
phead = cur=NULL;
[外链图片转存中...(img-oSfFSCdZ-1715791546351)]
[外链图片转存中...(img-jJ0BHSYX-1715791546351)]
[外链图片转存中...(img-VbhWBPZT-1715791546351)]
**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!**
**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**
**[需要这份系统化资料的朋友,可以戳这里获取](https://bbs.csdn.net/topics/618545628)**