🐶博主主页:@ᰔᩚ. 一怀明月ꦿ
❤️🔥专栏系列:线性代数,C初学者入门训练,题解C,C的使用文章,「初学」C++,数据结构
🔥座右铭:“不要等到什么都没有了,才下定决心去做”
🚀🚀🚀大家觉不错的话,就恳求大家点点关注,点点小爱心,指点指点🚀🚀🚀
目录
🐰带头双向循环链表
🏡概念
🏡链表结构体的定义
🏡链表为空的判断
🏡链表节点的创建
🏡链表的初始化
🏡链表的打印
🏡链表的尾插
🏡链表的头插
🏡链表的尾删
🏡链表的头删
🏡链表的查找
🏡链表中在pos之前插入
🏡删除pos的值
🏡链表的销毁
🏡链表为什么使用的是一级指针
🌸(1)单链表(非头单向不循环连链表)使用二级指针
🌸(2)带头双向循环连链表使用一级指针
🏡狡猾的面试官
🏡链表的源码
🌸main函数
🌸test.h文件
🌸test.c文件
🐰带头双向循环链表
🏡概念
1.无头单向非循环的链表:结构简单,一般不会单独用来存储数据。实际中更多是作为其他数据结构的子结构,例如哈希桶、图的邻接表等等。这种结构在笔试面试中出现的比较多。(之前提到的单链表是一种无头单向非循环的链表。)
2.带头双向循环链表:结构复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构最燃复杂,但是使用代码实现以后会发现结构会带来很多优势,实现起来反而简单。
🏡链表结构体的定义
typedef int LTDatatype; typedef struct ListNode { struct ListNode* next;//保存下个节点的地址 struct ListNode* prev;//保存上个节点的地址 LTDatatype data;//存储的数据 }ListNode;
🏡链表为空的判断
bool ListNodeEmpty(ListNode* pphead) { assert(pphead); return pphead->next==pphead;//哨兵位的下个节点的地址和哨兵位的地址相等则链表为空,这里返回true }
🏡链表节点的创建
动态创建一个节点,将节点的prev指针置空,将节点next指针置空,值赋值为x(x是传进来的参数),然后返回这个节点的地址。
ListNode* BuyListNode(LTDatatype x) { ListNode* newnode=(ListNode*)malloc(sizeof(ListNode)); if(newnode==NULL) { perror("malloc fail!"); return NULL; } newnode->next=NULL; newnode->prev=NULL; newnode->data=x; return newnode; }
🏡链表的初始化
链表的初始化是创建一个哨兵位,哨兵位的数据值为-1。且初始化时,是将哨兵位的prev指针指向自己,哨兵位的next指向自己。返回这个哨兵位的地址
ListNode* ListNodeInit(ListNode* pphead) { pphead=BuyListNode(-1); pphead->next=pphead; pphead->prev=pphead; return pphead; }
🏡链表的打印
创建一个结构体指针cur,让cur指向哨兵位的下一个节点(链表的头节点),然后遍历整个链表并打印每个节点的数据值,当cur的值等于哨兵位的指针的值,说明遍历完成,结束打印。
void ListNodePrint(ListNode* pphead) { assert(pphead); ListNode* cur=pphead->next;//从头节点开始打印 while(cur!=pphead)//当cur的值等与哨兵位的值(这里的值是存放的节点的地址)相等纠结束打印, { printf("%d ",cur->data); cur=cur->next; } printf("\n"); printf("print has done\n"); }
🏡链表的尾插
这里不管是头节点还是中间节点,都能完成尾插,但是单向不循环链表,得分头节点和中间节点,体现出双向循环链表在尾删的便利
void ListNodePushBack(ListNode* pphead,LTDatatype x) { assert(pphead); ListNode* tail=pphead->prev; ListNode* newnode=BuyListNode(x); tail->next=newnode; newnode->prev=tail; newnode->next=pphead; pphead->prev=newnode; }
🏡链表的头插
void ListNodePushFront(ListNode* pphead,LTDatatype x) { //这里不管是头节点还是中间节点,都能完成头插,但是单向不循环链表,得分头节点和中间节点 assert(pphead); ListNode* newnode=BuyListNode(x); newnode->next=pphead->next;//a pphead->next->prev=newnode;//b //这里的a b一定在c d的前面,不然pphead->next被改后,没有找到原来的头节点。 //这里其实用慢指针去赋值,就可以不在意顺序了 pphead->next=newnode;//c newnode->prev=pphead;//d }
🏡链表的尾删
尾删的时候,要注意不要把哨兵位删除了,所以需要判断链表是否为空,如果为空不能删除
void ListNodePopBack(ListNode* pphead) { assert(pphead); assert(!ListNodeEmpty(pphead)); ListNode* tail=pphead->prev; ListNode* tailPrev=tail->prev; free(tail); tail=NULL; pphead->prev=tailPrev; tailPrev->next=pphead; }
🏡链表的头删
void ListNodePopFront(ListNode* pphead) { assert(pphead); assert(!ListNodeEmpty(pphead)); ListNode* tail=pphead->next; ListNode* tailNext=tail->next; free(tail); tail=NULL; pphead->next=tailNext; tailNext->prev=pphead; }
🏡链表的查找
根据给出的值,然后去遍历整个链表,如果链表中有与给出的值相匹配的节点,就返回这个节点的地址,如果没有返回空指针。
ListNode* ListNodeFind(ListNode* pphead,LTDatatype x) { assert(pphead); ListNode* cur=pphead->next; while(cur!=pphead) { if(cur->data==x) { return cur; } cur=cur->next; } return NULL; }
🏡链表中在pos之前插入
void ListNodeInsert(ListNode* pos,LTDatatype x) { assert(pos); ListNode* newnode=BuyListNode(x); //这种方法的可读性高 ListNode* prev=pos->prev; prev->next=newnode; newnode->prev=prev; newnode->next=pos; pos->prev=newnode; //少定义一个指针的方法,但一定要记住先修改pos的右边 // pos->prev->next=newnode; // newnode->prev=pos->prev; // newnode->next=pos; // pos->prev=newnode; }
🏡删除pos的值
void ListNodeErease(ListNode* pos) { assert(pos); ListNode* posPrev=pos->prev; ListNode* posNext=pos->next; free(pos); posPrev->next=posNext; posNext->prev=posPrev; }
🏡链表的销毁
利用快慢指针的方式,做到free掉每个节点
void ListNodeDestroy(ListNode* pphead) { assert(pphead); ListNode* cur=pphead->next; ListNode* prev=cur->next; while(cur!=pphead) { free(cur); cur=prev; prev=prev->next; } free(pphead); }
🏡链表为什么使用的是一级指针
🌸(1)单链表(非头单向不循环连链表)使用二级指针
我们在实现单链表(非头单向不循环连链表)使用的是二级指针。其实我们也可以使用一级指针,但前提是链表不能对头节点进行操作。因为我们在主函数创建的结构体指针plist,如果没有对plist指向的链表的头节点进行操作,在调用删除或插入等函数时,就可以通过形参指针去修改链表。如果对plist指向的链表的头节点进行操作,在调用删除或插入函数时,就不能保证操作正确性了。传给函数的是plist值,函数的形参指针无法对plist进行操作,要想要改变plist的值,只能传plist的地址并且函数形参指针必须是二级指针。
🌸(2)带头双向循环连链表使用一级指针
我们的带头双向循环连链表之所以使用一级指针,因为我们在进行删除或插入等操作时,plist始终指向哨兵位节点,在一系列操作中,哨兵位的位置没有改变。所以链表只需传plist的值且函数形参指针使用一级指针。
🏡狡猾的面试官
如果在面试时,面试官让你在10分钟以内写完一个链表,链表的功能包括:链表的打印,链表的头删,链表的尾删,链表的头插,链表的尾插,链表的任意位置删除,链表任意位置插入,链表销毁。对于我而言,就算我准备的有多么的充足,在面试这种紧张环境下,我根本不可能完成这种要求,也就是我将得不到这家公司的offer。但是我们冷静分析一下,链表面试没有规定是哪一种,链表的种类一共有8之多,我们应该首选带头双向循环链表,别看它结构复杂,但是实现的功能的逻辑简单(可以减少很多空指针的情况),我们选了带头双向循环链表之后,其实链表的任意位置删除和链表的头删,链表的尾删其实功能类似,链表的任意位置删除和链表的头插,链表的尾插实现的功能类似。所以我们只需要复用代码就行。
链表的尾插:
void ListNodePushBack(ListNode* pphead,LTDatatype x) { //复用ListNodeInsert ListNodeInsert(pphead->prev, x); }
链表的头插:
void ListNodePushFront(ListNode* pphead,LTDatatype x) { //复用ListNodeInsert ListNodeInsert(pphead->next, x); }
链表的尾删:
void ListNodePopBack(ListNode* pphead) { //复用ListNodeErease ListNodeErease(pphead->prev); }
链表的头删:
void ListNodePopFront(ListNode* pphead) { // 复用ListNodeErease ListNodeErease(pphead->next); }
这样我们就可以在10分钟之内识破面试官的诡计
🏡链表的源码
🌸main函数
#include "test.h" void test1(void) { ListNode* plist=NULL; plist=ListNodeInit(plist); ListNodePushBack(plist, 1); ListNodePushBack(plist, 2); ListNodePushBack(plist, 3); ListNodePushBack(plist, 4); ListNodePrint(plist); } void test2(void) { ListNode* plist=NULL; plist=ListNodeInit(plist); ListNodePushFront(plist, 1); ListNodePushFront(plist, 2); ListNodePushFront(plist, 3); ListNodePushFront(plist, 4); ListNodePrint(plist); } void test3(void) { ListNode* plist=NULL; plist=ListNodeInit(plist); ListNodePushFront(plist, 1); ListNodePushFront(plist, 2); ListNodePushFront(plist, 3); ListNodePushFront(plist, 4); ListNodePrint(plist); ListNodePopBack(plist);//尾删 ListNodePrint(plist); ListNodePopFront(plist);//头删 ListNodePrint(plist); ListNodePopBack(plist);//尾删 ListNodePrint(plist); ListNodePopFront(plist);//头删 ListNodePrint(plist); //ListNodePopFront(plist);//头删 } void test4(void) { ListNode* plist=NULL; plist=ListNodeInit(plist); ListNodePushFront(plist, 1); ListNodePushFront(plist, 2); ListNodePushFront(plist, 3); ListNodePushFront(plist, 4); ListNodePrint(plist); ListNode* pos=ListNodeFind(plist, 3); ListNodeInsert(pos, 100); ListNodePrint(plist); } void test5(void) { ListNode* plist=NULL; plist=ListNodeInit(plist); ListNodePushFront(plist, 1); ListNodePushFront(plist, 2); ListNodePushFront(plist, 3); ListNodePushFront(plist, 4); ListNodePrint(plist); ListNode* pos=ListNodeFind(plist, 3); ListNodeErease(pos); ListNodePrint(plist); } void test6(void) { ListNode* plist=NULL; plist=ListNodeInit(plist); ListNodePushFront(plist, 1); ListNodePushFront(plist, 2); ListNodePushFront(plist, 3); ListNodePushFront(plist, 4); ListNodePrint(plist); ListNodeDestroy(plist); } int main() { //test1();//头插 //test2();//尾插 //test3();//头删尾删 //test4();//任意位置的插入 test5();//任意位置的删除 //test6();//销毁链表 return 0; }
🌸test.h文件
#ifndef test_h #define test_h #include <stdio.h> #include<stdlib.h> #include<assert.h> #include<stdbool.h> #endif /* test_h */ typedef int LTDatatype; typedef struct ListNode { struct ListNode* next; struct ListNode* prev; LTDatatype data; }ListNode; //链表为空的判断 bool ListNodeEmpty(ListNode* pphead); //链表的初始化 ListNode* ListNodeInit(ListNode* pphead); //链表的打印 void ListNodePrint(ListNode* pphead); //链表的尾插 void ListNodePushBack(ListNode* pphead,LTDatatype x); //链表的头插 void ListNodePushFront(ListNode* pphead,LTDatatype x); //链表的尾删 void ListNodePopBack(ListNode* pphead); //链表的头删 void ListNodePopFront(ListNode* pphead); //链表的查找 ListNode* ListNodeFind(ListNode* pphead,LTDatatype x); //在pos之前插入 void ListNodeInsert(ListNode* pos,LTDatatype x); //删除pos的值 void ListNodeErease(ListNode* pos); //链表的销毁 void ListNodeDestroy(ListNode* pphead);
🌸test.c文件
#include "test.h" bool ListNodeEmpty(ListNode* pphead) { assert(pphead); return pphead->next==pphead; } ListNode* BuyListNode(LTDatatype x) { ListNode* newnode=(ListNode*)malloc(sizeof(ListNode)); if(newnode==NULL) { perror("malloc fail!"); return NULL; } newnode->next=NULL; newnode->prev=NULL; newnode->data=x; return newnode; } ListNode* ListNodeInit(ListNode* pphead) { pphead=BuyListNode(-1); pphead->next=pphead; pphead->prev=pphead; return pphead; } void ListNodePrint(ListNode* pphead) { assert(pphead); ListNode* cur=pphead->next;//从头节点开始打印 while(cur!=pphead)//当cur的值等与哨兵位的值(这里的值是存放的节点的地址)相等纠结束打印, { printf("%d ",cur->data); cur=cur->next; } printf("\n"); printf("print has done\n"); } void ListNodePushBack(ListNode* pphead,LTDatatype x) { // //这里不管是头节点还是中间节点,都能完成尾插,但是单向不循环链表,得分头节点和中间节点 // assert(pphead); // ListNode* tail=pphead->prev; // ListNode* newnode=BuyListNode(x); // tail->next=newnode; // newnode->prev=tail; // newnode->next=pphead; // pphead->prev=newnode; //复用ListNodeInsert ListNodeInsert(pphead->prev, x); } void ListNodePushFront(ListNode* pphead,LTDatatype x) { // //这里不管是头节点还是中间节点,都能完成头插,但是单向不循环链表,得分头节点和中间节点 // assert(pphead); // ListNode* newnode=BuyListNode(x); // newnode->next=pphead->next;//a // pphead->next->prev=newnode;//b // //这里的a b一定在c d的前面,不然pphead->next被改后,没有找到原来的头节点。 // //这里其实用慢指针去赋值,就可以不在意顺序了 // pphead->next=newnode;//c // newnode->prev=pphead;//d //复用ListNodeInsert ListNodeInsert(pphead->next, x); } void ListNodePopBack(ListNode* pphead) { // assert(pphead); // assert(!ListNodeEmpty(pphead)); // ListNode* tail=pphead->prev; // ListNode* tailPrev=tail->prev; // free(tail); // tail=NULL; // pphead->prev=tailPrev; // tailPrev->next=pphead; //复用ListNodeErease ListNodeErease(pphead->prev); } void ListNodePopFront(ListNode* pphead) { // assert(pphead); // assert(!ListNodeEmpty(pphead)); // // ListNode* tail=pphead->next; // ListNode* tailNext=tail->next; // free(tail); // tail=NULL; // // pphead->next=tailNext; // tailNext->prev=pphead; // // 复用ListNodeErease ListNodeErease(pphead->next); } ListNode* ListNodeFind(ListNode* pphead,LTDatatype x) { assert(pphead); ListNode* cur=pphead->next; while(cur!=pphead) { if(cur->data==x) { return cur; } cur=cur->next; } return NULL; } void ListNodeInsert(ListNode* pos,LTDatatype x) { assert(pos); ListNode* newnode=BuyListNode(x); //这种方法的可读性高 ListNode* prev=pos->prev; prev->next=newnode; newnode->prev=prev; newnode->next=pos; pos->prev=newnode; //少定义一个指针的方法,但一定要记住先修改pos的右边 // pos->prev->next=newnode; // newnode->prev=pos->prev; // newnode->next=pos; // pos->prev=newnode; } //ListNodeErease有一个缺陷,就是可能删除了哨兵位 void ListNodeErease(ListNode* pos) { assert(pos); ListNode* posPrev=pos->prev; ListNode* posNext=pos->next; free(pos); posPrev->next=posNext; posNext->prev=posPrev; } void ListNodeDestroy(ListNode* pphead) { assert(pphead); ListNode* cur=pphead->next; ListNode* prev=cur->next; while(cur!=pphead) { free(cur); cur=prev; prev=prev->next; } free(pphead); }
🌸🌸🌸如果大家还有不懂或者建议都可以发在评论区,我们共同探讨,共同学习,共同进步。谢谢大家! 🌸🌸🌸