C语言中数据结构——带头双向循环链表

简介: 🏡概念🏡链表结构体的定义 🏡链表为空的判断🏡链表节点的创建🏡链表的初始化🏡链表的打印🏡链表的尾插🏡链表的头插🏡链表的尾删🏡链表的头删🏡链表的查找🏡链表中在pos之前插入🏡删除pos的值🏡链表的销毁🏡链表为什么使用的是一级指针🌸(1)单链表(非头单向不循环连链表)使用二级指针🌸(2)带头双向循环连链表使用一级指针🏡狡猾的面试官🏡链表的源码🌸main函数 🌸test.h文件🌸test.c文件

🚀🚀🚀大家觉不错的话,就恳求大家点点关注,点点小爱心,指点指点🚀🚀🚀

🐰带头双向循环链表

目录

🏡概念

🏡链表结构体的定义

🏡链表为空的判断

🏡链表节点的创建

🏡链表的初始化

🏡链表的打印

🏡链表的尾插

🏡链表的头插

🏡链表的尾删

🏡链表的头删

🏡链表的查找

🏡链表中在pos之前插入

🏡删除pos的值

🏡链表的销毁

🏡链表为什么使用的是一级指针

🌸(1)单链表(非头单向不循环连链表)使用二级指针

🌸(2)带头双向循环连链表使用一级指针

🏡狡猾的面试官

🏡链表的源码

🌸main函数

        🌸test.h文件

🌸test.c文件


🐰带头双向循环链表

🏡概念

1.无头单向非循环的链表:结构简单,一般不会单独用来存储数据。实际中更多是作为其他数据结构的子结构,例如哈希桶、图的邻接表等等。这种结构在笔试面试中出现的比较多。(之前提到的单链表是一种无头单向非循环的链表。)

90B0F6C0-E5F1-487C-B623-BCE18A1EEF9A.png

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

551114C2-7FB5-4D18-BEC9-2C0872EC83F1.png

🏡链表结构体的定义

1. typedef int LTDatatype;
2. typedef struct ListNode
3. {
4. struct ListNode* next;//保存下个节点的地址
5. struct ListNode* prev;//保存上个节点的地址
6.     LTDatatype data;//存储的数据
7. }ListNode;

🏡链表为空的判断

1. bool ListNodeEmpty(ListNode* pphead)
2. {
3. assert(pphead);
4. return pphead->next==pphead;//哨兵位的下个节点的地址和哨兵位的地址相等则链表为空,这里返回true
5. }

🏡链表节点的创建

动态创建一个节点,将节点的prev指针置空,将节点next指针置空,值赋值为x(x是传进来的参数),然后返回这个节点的地址。

1. ListNode* BuyListNode(LTDatatype x)
2. {
3.     ListNode* newnode=(ListNode*)malloc(sizeof(ListNode));
4. if(newnode==NULL)
5.     {
6. perror("malloc fail!");
7. return NULL;
8.     }
9.     newnode->next=NULL;
10.     newnode->prev=NULL;
11.     newnode->data=x;
12. return newnode;
13. }

🏡链表的初始化

链表的初始化是创建一个哨兵位,哨兵位的数据值为-1。且初始化时,是将哨兵位的prev指针指向自己,哨兵位的next指向自己。返回这个哨兵位的地址

1. ListNode* ListNodeInit(ListNode* pphead)
2. {
3.     pphead=BuyListNode(-1);
4.     pphead->next=pphead;
5.     pphead->prev=pphead;
6. return pphead;
7. }

🏡链表的打印

创建一个结构体指针cur,让cur指向哨兵位的下一个节点(链表的头节点),然后遍历整个链表并打印每个节点的数据值,当cur的值等于哨兵位的指针的值,说明遍历完成,结束打印。

1. void ListNodePrint(ListNode* pphead)
2. {
3. assert(pphead);
4.     ListNode* cur=pphead->next;//从头节点开始打印
5. while(cur!=pphead)//当cur的值等与哨兵位的值(这里的值是存放的节点的地址)相等纠结束打印,
6.     {
7. printf("%d ",cur->data);
8.         cur=cur->next;
9.     }
10. printf("\n");
11. printf("print has done\n");
12. }

🏡链表的尾插

这里不管是头节点还是中间节点,都能完成尾插,但是单向不循环链表,得分头节点和中间节点,体现出双向循环链表在尾删的便利

1. void ListNodePushBack(ListNode* pphead,LTDatatype x)
2. {
3. assert(pphead);
4.     ListNode* tail=pphead->prev;
5.     ListNode* newnode=BuyListNode(x);
6.     tail->next=newnode;
7.     newnode->prev=tail;
8.     newnode->next=pphead;
9.     pphead->prev=newnode;
10. }

🏡链表的头插

1. void ListNodePushFront(ListNode* pphead,LTDatatype x)
2. {
3. //这里不管是头节点还是中间节点,都能完成头插,但是单向不循环链表,得分头节点和中间节点
4. assert(pphead);
5.     ListNode* newnode=BuyListNode(x);
6.     newnode->next=pphead->next;//a
7.     pphead->next->prev=newnode;//b
8. //这里的a b一定在c d的前面,不然pphead->next被改后,没有找到原来的头节点。
9. //这里其实用慢指针去赋值,就可以不在意顺序了
10.     pphead->next=newnode;//c
11.     newnode->prev=pphead;//d
12. }

🏡链表的尾删

尾删的时候,要注意不要把哨兵位删除了,所以需要判断链表是否为空,如果为空不能删除

1. void ListNodePopBack(ListNode* pphead)
2. {
3. assert(pphead);
4. assert(!ListNodeEmpty(pphead));
5.     ListNode* tail=pphead->prev;
6.     ListNode* tailPrev=tail->prev;
7. free(tail);
8.     tail=NULL;
9.     pphead->prev=tailPrev;
10.     tailPrev->next=pphead;
11. }

🏡链表的头删

1. void ListNodePopFront(ListNode* pphead)
2. {
3. assert(pphead);
4. assert(!ListNodeEmpty(pphead));
5. 
6.     ListNode* tail=pphead->next;
7.     ListNode* tailNext=tail->next;
8. free(tail);
9.     tail=NULL;
10. 
11.     pphead->next=tailNext;
12.     tailNext->prev=pphead;
13. }

🏡链表的查找

根据给出的值,然后去遍历整个链表,如果链表中有与给出的值相匹配的节点,就返回这个节点的地址,如果没有返回空指针。

1. ListNode* ListNodeFind(ListNode* pphead,LTDatatype x)
2. {
3. assert(pphead);
4.     ListNode* cur=pphead->next;
5. while(cur!=pphead)
6.     {
7. if(cur->data==x)
8.         {
9. return cur;
10.         }
11.         cur=cur->next;
12.     }
13. return NULL;
14. }

🏡链表中在pos之前插入

1. void ListNodeInsert(ListNode* pos,LTDatatype x)
2. {
3. assert(pos);
4.     ListNode* newnode=BuyListNode(x);
5. //这种方法的可读性高
6.     ListNode* prev=pos->prev;
7.     prev->next=newnode;
8.     newnode->prev=prev;
9.     newnode->next=pos;
10.     pos->prev=newnode;
11. //少定义一个指针的方法,但一定要记住先修改pos的右边
12. //    pos->prev->next=newnode;
13. //    newnode->prev=pos->prev;
14. //    newnode->next=pos;
15. //    pos->prev=newnode;
16. }

🏡删除pos的值

1. void ListNodeErease(ListNode* pos)
2. {
3. assert(pos);
4.     ListNode* posPrev=pos->prev;
5.     ListNode* posNext=pos->next;
6. free(pos);
7.     posPrev->next=posNext;
8.     posNext->prev=posPrev;
9. }

🏡链表的销毁

利用快慢指针的方式,做到free掉每个节点

1. void ListNodeDestroy(ListNode* pphead)
2. {
3. assert(pphead);
4.     ListNode* cur=pphead->next;
5.     ListNode* prev=cur->next;
6. while(cur!=pphead)
7.     {
8. free(cur);
9.         cur=prev;
10.         prev=prev->next;
11.     }
12. free(pphead);
13. }

🏡链表为什么使用的是一级指针

🌸(1)单链表(非头单向不循环连链表)使用二级指针

我们在实现单链表(非头单向不循环连链表)使用的是二级指针。其实我们也可以使用一级指针,但前提是链表不能对头节点进行操作。因为我们在主函数创建的结构体指针plist,如果没有对plist指向的链表的头节点进行操作,在调用删除或插入等函数时,就可以通过形参指针去修改链表。如果对plist指向的链表的头节点进行操作,在调用删除或插入函数时,就不能保证操作正确性了。传给函数的是plist值,函数的形参指针无法对plist进行操作,要想要改变plist的值,只能传plist的地址并且函数形参指针必须是二级指针。

🌸(2)带头双向循环连链表使用一级指针

我们的带头双向循环连链表之所以使用一级指针,因为我们在进行删除或插入等操作时,plist始终指向哨兵位节点,在一系列操作中,哨兵位的位置没有改变。所以链表只需传plist的值且函数形参指针使用一级指针。

🏡狡猾的面试官

如果在面试时,面试官让你在10分钟以内写完一个链表,链表的功能包括:链表的打印,链表的头删,链表的尾删,链表的头插,链表的尾插,链表的任意位置删除,链表任意位置插入,链表销毁。对于我而言,就算我准备的有多么的充足,在面试这种紧张环境下,我根本不可能完成这种要求,也就是我将得不到这家公司的offer。但是我们冷静分析一下,链表面试没有规定是哪一种,链表的种类一共有8之多,我们应该首选带头双向循环链表,别看它结构复杂,但是实现的功能的逻辑简单(可以减少很多空指针的情况),我们选了带头双向循环链表之后,其实链表的任意位置删除和链表的头删,链表的尾删其实功能类似,链表的任意位置删除和链表的头插,链表的尾插实现的功能类似。所以我们只需要复用代码就行。

链表的尾插:

1. void ListNodePushBack(ListNode* pphead,LTDatatype x)
2. {
3. //复用ListNodeInsert
4. ListNodeInsert(pphead->prev, x);
5. }

链表的头插:

1. void ListNodePushFront(ListNode* pphead,LTDatatype x)
2. {  
3. //复用ListNodeInsert
4. ListNodeInsert(pphead->next, x);
5. }

链表的尾删:

1. void ListNodePopBack(ListNode* pphead)
2. {    
3. //复用ListNodeErease
4. ListNodeErease(pphead->prev);
5. }

链表的头删:

1. void ListNodePopFront(ListNode* pphead)
2. {
3. //    复用ListNodeErease
4. ListNodeErease(pphead->next);
5. }

这样我们就可以在10分钟之内识破面试官的诡计

🏡链表的源码

🌸main函数

1. #include "test.h"
2. void test1(void)
3. {
4.     ListNode* plist=NULL;
5.     plist=ListNodeInit(plist);
6. ListNodePushBack(plist, 1);
7. ListNodePushBack(plist, 2);
8. ListNodePushBack(plist, 3);
9. ListNodePushBack(plist, 4);
10. ListNodePrint(plist);
11. }
12. void test2(void)
13. {
14.     ListNode* plist=NULL;
15.     plist=ListNodeInit(plist);
16. ListNodePushFront(plist, 1);
17. ListNodePushFront(plist, 2);
18. ListNodePushFront(plist, 3);
19. ListNodePushFront(plist, 4);
20. ListNodePrint(plist);
21. }
22. void test3(void)
23. {
24.     ListNode* plist=NULL;
25.     plist=ListNodeInit(plist);
26. ListNodePushFront(plist, 1);
27. ListNodePushFront(plist, 2);
28. ListNodePushFront(plist, 3);
29. ListNodePushFront(plist, 4);
30. ListNodePrint(plist);
31. ListNodePopBack(plist);//尾删
32. ListNodePrint(plist);
33. ListNodePopFront(plist);//头删
34. ListNodePrint(plist);
35. ListNodePopBack(plist);//尾删
36. ListNodePrint(plist);
37. ListNodePopFront(plist);//头删
38. ListNodePrint(plist);
39. //ListNodePopFront(plist);//头删
40. }
41. void test4(void)
42. {
43.     ListNode* plist=NULL;
44.     plist=ListNodeInit(plist);
45. ListNodePushFront(plist, 1);
46. ListNodePushFront(plist, 2);
47. ListNodePushFront(plist, 3);
48. ListNodePushFront(plist, 4);
49. ListNodePrint(plist);
50.     ListNode* pos=ListNodeFind(plist, 3);
51. ListNodeInsert(pos, 100);
52. ListNodePrint(plist);
53. }
54. void test5(void)
55. {
56.     ListNode* plist=NULL;
57.     plist=ListNodeInit(plist);
58. ListNodePushFront(plist, 1);
59. ListNodePushFront(plist, 2);
60. ListNodePushFront(plist, 3);
61. ListNodePushFront(plist, 4);
62. ListNodePrint(plist);
63.     ListNode* pos=ListNodeFind(plist, 3);
64. ListNodeErease(pos);
65. ListNodePrint(plist);
66. }
67. void test6(void)
68. {
69.     ListNode* plist=NULL;
70.     plist=ListNodeInit(plist);
71. ListNodePushFront(plist, 1);
72. ListNodePushFront(plist, 2);
73. ListNodePushFront(plist, 3);
74. ListNodePushFront(plist, 4);
75. ListNodePrint(plist);
76. ListNodeDestroy(plist);
77. }
78. int main()
79. {
80. //test1();//头插
81. //test2();//尾插
82. //test3();//头删尾删
83. //test4();//任意位置的插入
84. test5();//任意位置的删除
85. //test6();//销毁链表
86. return 0;
87. }

🌸test.h文件

1. #ifndef test_h
2. #define test_h
3. #include <stdio.h>
4. #include<stdlib.h>
5. #include<assert.h>
6. #include<stdbool.h>
7. #endif /* test_h */
8. 
9. typedef int LTDatatype;
10. typedef struct ListNode
11. {
12. struct ListNode* next;
13. struct ListNode* prev;
14.     LTDatatype data;
15. }ListNode;
16. 
17. //链表为空的判断
18. bool ListNodeEmpty(ListNode* pphead);
19. //链表的初始化
20. ListNode* ListNodeInit(ListNode* pphead);
21. //链表的打印
22. void ListNodePrint(ListNode* pphead);
23. //链表的尾插
24. void ListNodePushBack(ListNode* pphead,LTDatatype x);
25. //链表的头插
26. void ListNodePushFront(ListNode* pphead,LTDatatype x);
27. //链表的尾删
28. void ListNodePopBack(ListNode* pphead);
29. //链表的头删
30. void ListNodePopFront(ListNode* pphead);
31. //链表的查找
32. ListNode* ListNodeFind(ListNode* pphead,LTDatatype x);
33. //在pos之前插入
34. void ListNodeInsert(ListNode* pos,LTDatatype x);
35. //删除pos的值
36. void ListNodeErease(ListNode* pos);
37. //链表的销毁
38. void ListNodeDestroy(ListNode* pphead);

🌸test.c文件

1. #include "test.h"
2. bool ListNodeEmpty(ListNode* pphead)
3. {
4. assert(pphead);
5. return pphead->next==pphead;
6. }
7. ListNode* BuyListNode(LTDatatype x)
8. {
9.     ListNode* newnode=(ListNode*)malloc(sizeof(ListNode));
10. if(newnode==NULL)
11.     {
12. perror("malloc fail!");
13. return NULL;
14.     }
15.     newnode->next=NULL;
16.     newnode->prev=NULL;
17.     newnode->data=x;
18. return newnode;
19. }
20. ListNode* ListNodeInit(ListNode* pphead)
21. {
22.     pphead=BuyListNode(-1);
23.     pphead->next=pphead;
24.     pphead->prev=pphead;
25. return pphead;
26. }
27. void ListNodePrint(ListNode* pphead)
28. {
29. assert(pphead);
30.     ListNode* cur=pphead->next;//从头节点开始打印
31. while(cur!=pphead)//当cur的值等与哨兵位的值(这里的值是存放的节点的地址)相等纠结束打印,
32.     {
33. printf("%d ",cur->data);
34.         cur=cur->next;
35.     }
36. printf("\n");
37. printf("print has done\n");
38. }
39. void ListNodePushBack(ListNode* pphead,LTDatatype x)
40. {
41. //    //这里不管是头节点还是中间节点,都能完成尾插,但是单向不循环链表,得分头节点和中间节点
42. //    assert(pphead);
43. //    ListNode* tail=pphead->prev;
44. //    ListNode* newnode=BuyListNode(x);
45. //    tail->next=newnode;
46. //    newnode->prev=tail;
47. //    newnode->next=pphead;
48. //    pphead->prev=newnode;
49. 
50. //复用ListNodeInsert
51. ListNodeInsert(pphead->prev, x);
52. }
53. void ListNodePushFront(ListNode* pphead,LTDatatype x)
54. {
55. //    //这里不管是头节点还是中间节点,都能完成头插,但是单向不循环链表,得分头节点和中间节点
56. //    assert(pphead);
57. //    ListNode* newnode=BuyListNode(x);
58. //    newnode->next=pphead->next;//a
59. //    pphead->next->prev=newnode;//b
60. //    //这里的a b一定在c d的前面,不然pphead->next被改后,没有找到原来的头节点。
61. //    //这里其实用慢指针去赋值,就可以不在意顺序了
62. //    pphead->next=newnode;//c
63. //    newnode->prev=pphead;//d
64. 
65. 
66. //复用ListNodeInsert
67. ListNodeInsert(pphead->next, x);
68. }
69. void ListNodePopBack(ListNode* pphead)
70. {
71. //    assert(pphead);
72. //    assert(!ListNodeEmpty(pphead));
73. //    ListNode* tail=pphead->prev;
74. //    ListNode* tailPrev=tail->prev;
75. //    free(tail);
76. //    tail=NULL;
77. //    pphead->prev=tailPrev;
78. //    tailPrev->next=pphead;
79. 
80. 
81. //复用ListNodeErease
82. ListNodeErease(pphead->prev);
83. }
84. void ListNodePopFront(ListNode* pphead)
85. {
86. //    assert(pphead);
87. //    assert(!ListNodeEmpty(pphead));
88. //
89. //    ListNode* tail=pphead->next;
90. //    ListNode* tailNext=tail->next;
91. //    free(tail);
92. //    tail=NULL;
93. //
94. //    pphead->next=tailNext;
95. //    tailNext->prev=pphead;
96. //
97. //    复用ListNodeErease
98. ListNodeErease(pphead->next);
99. }
100. ListNode* ListNodeFind(ListNode* pphead,LTDatatype x)
101. {
102. assert(pphead);
103.     ListNode* cur=pphead->next;
104. while(cur!=pphead)
105.     {
106. if(cur->data==x)
107.         {
108. return cur;
109.         }
110.         cur=cur->next;
111.     }
112. return NULL;
113. }
114. void ListNodeInsert(ListNode* pos,LTDatatype x)
115. {
116. assert(pos);
117.     ListNode* newnode=BuyListNode(x);
118. //这种方法的可读性高
119.     ListNode* prev=pos->prev;
120.     prev->next=newnode;
121.     newnode->prev=prev;
122.     newnode->next=pos;
123.     pos->prev=newnode;
124. //少定义一个指针的方法,但一定要记住先修改pos的右边
125. //    pos->prev->next=newnode;
126. //    newnode->prev=pos->prev;
127. //    newnode->next=pos;
128. //    pos->prev=newnode;
129. }
130. //ListNodeErease有一个缺陷,就是可能删除了哨兵位
131. void ListNodeErease(ListNode* pos)
132. {
133. assert(pos);
134.     ListNode* posPrev=pos->prev;
135.     ListNode* posNext=pos->next;
136. free(pos);
137.     posPrev->next=posNext;
138.     posNext->prev=posPrev;
139. }
140. void ListNodeDestroy(ListNode* pphead)
141. {
142. assert(pphead);
143.     ListNode* cur=pphead->next;
144.     ListNode* prev=cur->next;
145. while(cur!=pphead)
146.     {
147. free(cur);
148.         cur=prev;
149.         prev=prev->next;
150.     }
151. free(pphead);
152. }

 🌸🌸🌸如果大家还有不懂或者建议都可以发在评论区,我们共同探讨,共同学习,共同进步。谢谢大家! 🌸🌸🌸



相关文章
|
1月前
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
137 9
|
12天前
|
存储 缓存 算法
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式,强调了合理选择数据结构的重要性,并通过案例分析展示了其在实际项目中的应用,旨在帮助读者提升编程能力。
30 5
|
1月前
|
存储 搜索推荐 算法
【数据结构】树型结构详解 + 堆的实现(c语言)(附源码)
本文介绍了树和二叉树的基本概念及结构,重点讲解了堆这一重要的数据结构。堆是一种特殊的完全二叉树,常用于实现优先队列和高效的排序算法(如堆排序)。文章详细描述了堆的性质、存储方式及其实现方法,包括插入、删除和取堆顶数据等操作的具体实现。通过这些内容,读者可以全面了解堆的原理和应用。
67 16
|
1月前
|
C语言
【数据结构】二叉树(c语言)(附源码)
本文介绍了如何使用链式结构实现二叉树的基本功能,包括前序、中序、后序和层序遍历,统计节点个数和树的高度,查找节点,判断是否为完全二叉树,以及销毁二叉树。通过手动创建一棵二叉树,详细讲解了每个功能的实现方法和代码示例,帮助读者深入理解递归和数据结构的应用。
107 8
|
1月前
|
存储 C语言
【数据结构】手把手教你单链表(c语言)(附源码)
本文介绍了单链表的基本概念、结构定义及其实现方法。单链表是一种内存地址不连续但逻辑顺序连续的数据结构,每个节点包含数据域和指针域。文章详细讲解了单链表的常见操作,如头插、尾插、头删、尾删、查找、指定位置插入和删除等,并提供了完整的C语言代码示例。通过学习单链表,可以更好地理解数据结构的底层逻辑,提高编程能力。
62 4
|
1月前
|
C语言
【数据结构】双向带头循环链表(c语言)(附源码)
本文介绍了双向带头循环链表的概念和实现。双向带头循环链表具有三个关键点:双向、带头和循环。与单链表相比,它的头插、尾插、头删、尾删等操作的时间复杂度均为O(1),提高了运行效率。文章详细讲解了链表的结构定义、方法声明和实现,包括创建新节点、初始化、打印、判断是否为空、插入和删除节点等操作。最后提供了完整的代码示例。
43 0
|
C语言
C语言学生信息管理系统链表实现
C语言学生信息管理系统链表实现
198 0
C语言学生信息管理系统链表实现
史上最简单的C语言链表实现,没有之一
#include #include #include #define NR(x) (sizeof(x)/sizeof(x[0])) struct node { int data ; struct node *next ; }; void top_append_li...
1009 0
|
2月前
|
C语言 C++
C语言 之 内存函数
C语言 之 内存函数
37 3
|
9天前
|
存储 缓存 算法
【C语言】内存管理函数详细讲解
在C语言编程中,内存管理是至关重要的。动态内存分配函数允许程序在运行时请求和释放内存,这对于处理不确定大小的数据结构至关重要。以下是C语言内存管理函数的详细讲解,包括每个函数的功能、标准格式、示例代码、代码解释及其输出。
34 6