数据结构体进阶链表【带头双向循环链表,单向链表的优化,从根部解决了顺序表的缺点】一文带你深入理解链表
创始人
2024-04-09 22:59:32
0

 前言:

   对于链表,上一篇的单链表解决了顺序表的一部分缺陷,但并没有彻底的解决顺序表的问题,比如在进行单链表尾插尾删的时候还是需要进行遍历找尾,并没有达到全部的O(1),并且在头插的时候还要分情况来考虑,比如传入为空指针和不是空指针时候还要分情况考虑,对于指针的改变还要传二级指针,这对于一部分人来说并不熟悉,所以!!!今天看完这篇文章,掌握带双向循环数据表,让我们不再害怕链表的增删插改😎😎

     💞 💞    欢迎来到小马学习代码博客!!!!

                

 思维导图:

目录

一、链表实现前的准备 

💜1.1结构

图:

💜1.2初步的理解:

二、带头双向链表功能实现前的准备

🤎 2.1链表实现所需要的头文件:

🤎2.2链表实现的初始化:

🤎2.2链表实现的打印:

🤎2.3定义一个节点为了实现插入:

🤎2.4判断节点是否为空节点:

🤎2.5链表实现后的销毁:

3、链表功能的实现

❤️3.1链表的头插:

❤️3.2链表的尾插:

❤️3.3链表的头删:

❤️3.4链表的尾删:

❤️3.5链表的pos位插入:

❤️3.6链表的pos位删除:

❤️3.7链表的查找:

4、带头双向循环链表的源码

💚4.1 List.h

💚4.2List.c

💚4.3Test.c

总结:


一、链表实现前的准备 

💜1.1结构图:

~带头双向循环链表我们不要被他的结构图所吓到,只要我么深入理解他的原理,你会发现它的代码实现起来比单链表还要简单。

💜1.2初步的理解:

1.2.1 对于带头双向循环链表,要知道它的结构是比较复杂的,但是他的代码实现是比较简单的,所以,对于代码实现前,我想先讲述一下带头双向循环链表:对于带头双向循环链表我们先一步步理解,👉带头的意思就是在插入之前我们先初始化一个结构体,他并不是用来存储数据,而是保证链表有一个头节点,就可以减少讨论(当传入指针为空时,当传入指针不为空时),👉双向是指我们的结构体中存量结构体指针,一个是指向下一个,另一个是指向他的前一个,这样就保证在寻找的时候找不到前一个的位置。 👉循环是指在尾节点我们并不让他指向空指针,而是让他指向头节点,而头节点的前一个也不让他指向空指针,而是让他指向尾节点,这样就会达成一个循环的作用,他的好处就是在于我们们寻找尾节点的时候就不用遍历了,而是通过找头节点的前一个就能找到尾节点,从而达到时间复杂度为O(1)来完成尾插和尾删!

二、带头双向链表功能实现前的准备

🤎 2.1链表实现所需要的头文件:

#include 
#include
#include
#include

💖  这里是害怕有的小伙伴有的头文件并不知道^ _ ^ 。

🤎2.2链表实现的初始化:

LTNode* ListInit() //返回结构体指针
{LTNode*guard=(LTNode*)malloc(sizeof(LTNode)); //这里就像定义一个哨兵,也就是头节点,就不用再担心头节点是不是NULL了if(guard==NULL) //确保能够正常扩容{perror("ListNodeInit");}guard->next=guard; //头节点的下一个先指向自己guard->prev=guard; //头节点的上一个也先指向自己return guard;
}

🤎2.2链表实现的打印:

void ListNodePrint(LTNode* phead)
{assert(phead);   //保证传入的不是空指针LTNode* cur =phead->next; //定一个结构体指针进行遍历打印printf("guard<=>");  //这里是为了打印效果好看一点while(cur!=phead){printf("%d<=>",cur->data);cur=cur->next;}printf("\n");
}

🤎2.3定义一个节点为了实现插入:

LTNode*  BuyListNode(LTDataType x)
{LTNode* newnode=(LTNode*)malloc(sizeof(ListNode)); //节点申请空间if(newnode==NULL){perror("BuyListNode");}newnode->data=x;  //节点所带的值newnode->prev=NULL;newnode->next=NULL;return newnode;
}

🤎2.4判断节点是否为空节点:

bool ListEmpty(LTNode*phead)
{assert(phead);return phead->next==phead; //如果头和头的下一个指向同一个则返回真,证明只有一个哨兵
}

🤎2.5链表实现后的销毁:

void ListDestroy(LTNode*phead)
{LTNode*cur=phead->next; //定义一个结构体指针进行遍历销毁while(cur!=phead){LTNode*next=cur->next;free(cur);cur=next;}free(phead); //最后释放头节点
}

3、链表功能的实现

❤️3.1链表的头插:

void  ListPushFront(LTNode*phead,LTDataType x)
{assert(phead);LTNode* newnode=BuyListNode(x); //先定义一个节点进行头插newnode->next=phead->next;  //这里要考虑顺序问题,不然很容易出错,先让定义的next指向哨兵的下一个,然后哨兵下一个的prev指向我们新的节点//然后让我们我们哨兵的next指向新节点,新节点的prev指向哨兵, 这里如果不理解的可以画一下图//或者我么先定义一个节点把哨兵的下一个进行保存这里就不用考虑先后顺序了,大家可以试一试phead->next->prev=newnode;phead->next=newnode;newnode->prev=phead;
}

❤️3.2链表的尾插:

void ListPushBack(LTNode*phead,LTDataType x)
{assert(phead);LTNode* newnode=BuyListNode(x);  //定义一个新节点LTNode* tail=phead->prev;  //找到尾节点tail->next=newnode;  //然后就是交换结构体里面的next prev指向的位置来实现插入。头插我已经讲述过程了,主要是大家尝试一下画画图newnode->prev=tail;newnode->next=phead;phead->prev=newnode;
}

❤️3.3链表的头删:

void ListPopFront(LTNode*phead)
{assert(phead);assert(!ListEmpty(phead)); //这里防止只剩下一个哨兵,如果只剩下一个哨兵还进行删除就强制类型报错LTNode*first=phead->next;  //定义一个first指向哨兵的nextLTNode* second=first->next; //在定义一个second指向first的nextphead->next=second;second->prev=phead;free(first);  //把first删除后进行freefirst=NULL;  //first指针指向空,防止出现野指针
}

❤️3.4链表的尾删:

void ListPopBack(LTNode*phead)
{assert(phead);assert(!ListEmpty(phead));//这里防止只剩下一个哨兵,如果只剩下一个哨兵还进行删除就强制类型报错LTNode*tail=phead->prev;tail->prev->next=phead;phead->prev=tail->prev;free(tail);tail=NULL;
}

❤️3.5链表的pos位插入:

void ListInsert(LTNode*pos,LTDataType x)
{assert(pos); //保证传入的pos位不是空指针LTNode*prev=pos->prev; //找到pos位的前一位LTNode*newnode=BuyListNode(x);  //定义一个新节点进行插入pos->prev=newnode;newnode->next=pos;prev->next=newnode;newnode->prev=prev;
}

❤️3.6链表的pos位删除:

void ListErase(LTNode*pos)
{LTNode*prev=pos->prev;//找到pos位的前一个LTNode*next=pos->next; //找到pos的后一个prev->next=next;next->prev=prev;free(pos);  //释放pos
}

❤️3.7链表的查找:

LTNode* ListNodeFind(LTNode*phead,LTDataType x)  //查找后返回指针,如果找到返回找的到的结构体指针,没有找到返回空指针
{assert(phead);LTNode* cur=phead->next;  //定义一个结构体指针进行遍历寻找while(cur!=phead){if(cur->data==x){return cur;  //找到后返回的指针}cur=cur->next;  }return NULL;
}

4、带头双向循环链表的源码

💚4.1 List.h

#ifndef List_hpp
#define List_hpp#include 
#include
#include
#include
typedef int LTDataType;
typedef struct ListNode
{struct ListNode*next;struct ListNode*prev;LTDataType data;
}LTNode;
LTNode* ListInit();
LTNode*  BuyListNode(LTDataType x);
void  ListPushBack(LTNode*phead,LTDataType x);
void ListNodePrint(LTNode* phead);
void  ListPushFront(LTNode*phead,LTDataType x);
void ListPopBack(LTNode*phead);
bool ListEmpty(LTNode*phead);
void ListPopFront(LTNode*phead);
size_t ListSize(LTNode*phead);
LTNode* ListNodeFind(LTNode*phead,LTDataType x);
void ListInsert(LTNode*pos,LTDataType x);
void ListErase(LTNode*pos);
void ListDestroy(LTNode*phead);#endif /* List_hpp */

💚4.2List.c

#include "List.hpp"
LTNode* ListInit()
{LTNode*guard=(LTNode*)malloc(sizeof(LTNode)); //这里就像定义一个哨兵,也就是头节点,就不用再担心头节点是不是NULL了if(guard==NULL) //确保能够正常扩容{perror("ListNodeInit");}guard->next=guard; //头节点的下一个先指向自己guard->prev=guard; //头节点的上一个也先指向自己return guard;
}
LTNode*  BuyListNode(LTDataType x)
{LTNode* newnode=(LTNode*)malloc(sizeof(ListNode)); //节点申请空间if(newnode==NULL){perror("BuyListNode");}newnode->data=x;  //节点所带的值newnode->prev=NULL;newnode->next=NULL;return newnode;
}
void ListNodePrint(LTNode* phead)
{assert(phead);   //保证传入的不是空指针LTNode* cur =phead->next; //定一个结构体指针进行遍历打印printf("guard<=>");  //这里是为了打印效果好看一点while(cur!=phead){printf("%d<=>",cur->data);cur=cur->next;}printf("\n");
}
void ListPushBack(LTNode*phead,LTDataType x)
{assert(phead);LTNode* newnode=BuyListNode(x);  //定义一个新节点LTNode* tail=phead->prev;  //找到尾节点tail->next=newnode;  //然后就是交换结构体里面的next prev指向的位置来实现插入。头插我已经讲述过程了,主要是大家尝试一下画画图newnode->prev=tail;newnode->next=phead;phead->prev=newnode;
}
void  ListPushFront(LTNode*phead,LTDataType x)
{assert(phead);LTNode* newnode=BuyListNode(x); //先定义一个节点进行头插newnode->next=phead->next;  //这里要考虑顺序问题,不然很容易出错,先让定义的next指向哨兵的下一个,然后哨兵下一个的prev指向我们新的节点//然后让我们我们哨兵的next指向新节点,新节点的prev指向哨兵, 这里如果不理解的可以画一下图//或者我么先定义一个节点把哨兵的下一个进行保存这里就不用考虑先后顺序了,大家可以试一试phead->next->prev=newnode;phead->next=newnode;newnode->prev=phead;
}
bool ListEmpty(LTNode*phead)
{assert(phead);return phead->next==phead; //如果头和头的下一个指向同一个则返回真,证明只有一个哨兵
}
void ListPopBack(LTNode*phead)
{assert(phead);assert(!ListEmpty(phead));//这里防止只剩下一个哨兵,如果只剩下一个哨兵还进行删除就强制类型报错LTNode*tail=phead->prev;tail->prev->next=phead;phead->prev=tail->prev;free(tail);tail=NULL;
}
void ListPopFront(LTNode*phead)
{assert(phead);assert(!ListEmpty(phead)); //这里防止只剩下一个哨兵,如果只剩下一个哨兵还进行删除就强制类型报错LTNode*first=phead->next;  //定义一个first指向哨兵的nextLTNode* second=first->next; //在定义一个second指向first的nextphead->next=second;second->prev=phead;free(first);  //把first删除后进行freefirst=NULL;  //first指针指向空,防止出现野指针
}
size_t ListSize(LTNode*phead)
{assert(phead);LTNode*cur=phead->next;size_t n=0;while(cur!=phead){++n;cur=cur->next;}return n;
}
LTNode* ListNodeFind(LTNode*phead,LTDataType x)  //查找后返回指针,如果找到返回找的到的结构体指针,没有找到返回空指针
{assert(phead);LTNode* cur=phead->next;  //定义一个结构体指针进行遍历寻找while(cur!=phead){if(cur->data==x){return cur;  //找到后返回的指针}cur=cur->next;  }return NULL;
}
void ListInsert(LTNode*pos,LTDataType x)
{assert(pos); //保证传入的pos位不是空指针LTNode*prev=pos->prev; //找到pos位的前一位LTNode*newnode=BuyListNode(x);  //定义一个新节点进行插入pos->prev=newnode;newnode->next=pos;prev->next=newnode;newnode->prev=prev;
}
void ListErase(LTNode*pos)
{LTNode*prev=pos->prev;//找到pos位的前一个LTNode*next=pos->next; //找到pos的后一个prev->next=next;next->prev=prev;free(pos);  //释放pos
}
void ListDestroy(LTNode*phead)
{LTNode*cur=phead->next; //定义一个结构体指针进行遍历销毁while(cur!=phead){LTNode*next=cur->next;free(cur);cur=next;}free(phead); //最后释放头节点
}

💚4.3Test.c

#include "List.hpp"
void test1()
{LTNode* plist=ListInit();ListPushBack(plist,1);ListPushBack(plist,2);ListPushBack(plist,3);ListPushBack(plist,4);ListPushBack(plist,5);ListPopBack(plist);ListPopBack(plist);ListNodePrint(plist);ListDestroy(plist);plist=NULL;
}
void test2()
{LTNode* plist=ListInit();ListPushFront(plist,1);ListPushFront(plist,2);ListPushFront(plist,3);ListPushFront(plist,4);ListPushFront(plist,5);ListPopFront(plist);ListPopFront(plist);size_t ret=ListSize(plist);printf("%zu\n",ret);ListNodePrint(plist);}
int main()
{test1();test2();return 0;
}

总结:

     对于带头双向循环链表的实现我还是那句话,不要害怕!!!他只是结构体比较复杂,但是代码实现它确比较容易的,它不像顺序表那样要考虑是否要扩容,也不需要像单链表那样要考虑传入是否为空指针,所以他的实现还是比较容易的,而本文对于他的所有实现,我想小马应该写的很详细啦,如果有不懂的可以私信问我或者直接在评论区中问我啦!!

       再者,其实思考一下,双向循环链表pos插入删除时,当我们pos等于phead时,你会发现他就像尾插和尾删,发现就不需要我们写的那么复杂啦,只需要调用一下pos函数,使pos指向phead就行啦,这里我给大家讲一个思路,对于他的实现,大家下去可以尝试一下

  最后的最后,小马码文不易,如果觉得文章有帮助的话,就多多支持小马啦!!!!

相关内容

热门资讯

喜欢穿一身黑的男生性格(喜欢穿... 今天百科达人给各位分享喜欢穿一身黑的男生性格的知识,其中也会对喜欢穿一身黑衣服的男人人好相处吗进行解...
发春是什么意思(思春和发春是什... 本篇文章极速百科给大家谈谈发春是什么意思,以及思春和发春是什么意思对应的知识点,希望对各位有所帮助,...
网络用语zl是什么意思(zl是... 今天给各位分享网络用语zl是什么意思的知识,其中也会对zl是啥意思是什么网络用语进行解释,如果能碰巧...
为什么酷狗音乐自己唱的歌不能下... 本篇文章极速百科小编给大家谈谈为什么酷狗音乐自己唱的歌不能下载到本地?,以及为什么酷狗下载的歌曲不是...
家里可以做假山养金鱼吗(假山能... 今天百科达人给各位分享家里可以做假山养金鱼吗的知识,其中也会对假山能放鱼缸里吗进行解释,如果能碰巧解...
华为下载未安装的文件去哪找(华... 今天百科达人给各位分享华为下载未安装的文件去哪找的知识,其中也会对华为下载未安装的文件去哪找到进行解...
四分五裂是什么生肖什么动物(四... 本篇文章极速百科小编给大家谈谈四分五裂是什么生肖什么动物,以及四分五裂打一生肖是什么对应的知识点,希...
怎么往应用助手里添加应用(应用... 今天百科达人给各位分享怎么往应用助手里添加应用的知识,其中也会对应用助手怎么添加微信进行解释,如果能...
客厅放八骏马摆件可以吗(家里摆... 今天给各位分享客厅放八骏马摆件可以吗的知识,其中也会对家里摆八骏马摆件好吗进行解释,如果能碰巧解决你...
苏州离哪个飞机场近(苏州离哪个... 本篇文章极速百科小编给大家谈谈苏州离哪个飞机场近,以及苏州离哪个飞机场近点对应的知识点,希望对各位有...