当前位置:   article > 正文

C语言实现单链表(超多配图,这下不得不学会单链表了)

C语言实现单链表(超多配图,这下不得不学会单链表了)

目录

一:什么是链表?

二:创建源文件和头文件

(1)头文件

(2)源文件

三:实参和形参

四:一步步实现单向链表

(1)建立一个头指针并置空

(2)打印链表,便于观察测试

(3)创建一个新的结点

(4)尾部插入数据

(5)头部插入

(6)尾部删除

(7)头部删除

(8)查找

(9)指定位置插入

(10)指定删除

(11)清空链表

(12)最终代码

SingleLinkedList.h

SingleLinkedList.c

text.c

五:小结


一:什么是链表?

我们先看下面这个结构体。

6474826145a8496290b457cb48bbbacd.png

这个结构体存储数据的同时保存了一个结构体指针

链表其实就是一个个结构体(后文把这样的一个结构体称为结点)通过保存地址的方式找到下一个结构体,最后一个结构体保存的地址为空。 

链表的两种实现方式

(1)带头结点

(2)不带头结点

区别:带头结点有一个哨兵结点,这个节点作为第一个节点,它的数据域不存储数据。

两者各有利弊:我们进行结点删除时需要用到待删除结点的前一个结点。对于没有哨兵的单链表,当链表中只存在一个节点,需要进行单独处理。从而代码的复杂性增加。但如果设计了哨兵结点,则第一个结点的处理与其他结点一致。但处理链表数据时这个哨兵结点属于无效数据,我们需要规避这个数据,也需要进行处理

本文选择的是无哨兵链表。

二:创建源文件和头文件

(1)头文件

头文件SingleLinkedList.h用来包含一些必要的头文件,声明函数以及定义结构体。

(2)源文件

源文件SingleLinkedList.c用来实现链表的具体功能

源文件text.c用来对各个功能进行测试

三:实参和形参

在实现链表之前,我们需要先深入的认识一下实参和形参的关系。

我们看下面这个代码:

fae8cca3f2144da48dda0fc6452d7cc4.png

我们可以看到a的值并没有发生变化,那我们如果传入a的地址进行解引用呢?我们看下面这个代码。

61db06269de6433ab8ddfb1f2960a66b.png

我们可以看到a成功被修改为了5,但这是为什么呢?

答:其实在传入参数的时候系统临时开辟了一块空间用来接收数据,函数调用结束时这一块空间就会被释放,这意味着如果我们直接传入a的值,我们只是在对这一块临时开辟的空间内的数据进行修改,无论如何都不会影响到a,但如果我们传入的是a的地址,对a进行解引用就能直接找到并修改a

图解:2d79ce8bca0e42b8b043c2b04731a85a.png

2deee6f13ae44417a10c9f8dd0ff5f50.png

 这是否意味着只要我们传入的是地址就一定能改变实参的值呢?我们看下面这个代码。

a6b7355468594d23bbf232a78b0e9279.png

我们发现虽然传入的是地址,但p依旧指向a[0],并没有改变。这是为什么呢?

答:与前面的原理一致,我们传入p的时候也临时开辟了一片空间来保存p的值,无论我们怎么改变p值,在函数调用结束后这片空间会被释放,所以p实际上还是指向a[0]的。

图解: 4fba6749c8044d2c87f35ac16b301ee7.png

那如果我们传入p的地址,是不是就能改变p了呢?我们看代码。

6592e55592574087b2244b4673504500.png

图解:5846b45efb21446282113591856afdb5.png 

四:一步步实现单向链表

(1)建立一个头指针并置空

struct SListNode* head = NULL;

(2)打印链表,便于观察测试

我们用头指针的地址是否为空为循环条件
我们可以分成两种情况讨论,如果链表为空,我们不进行遍历,直接打印NULL。
如果链表中有元素,从头指针(第一个结点)开始,我们打印结点数据,并让头指针指向下一个结点,一直到NULL。

代码:7c4fd64bd4084002a8c160871e547f57.png

图解(以有三个结点为例子):

44ae685eee3e4f0eac9cd4b553b1cdc0.png

(3)创建一个新的结点

只要插入新结点,我们就一定要生成新的结点,我们可以把生成新结点的功能单独封装成函数BuyListNode()

代码:b31ae76d3bec44d1b5efc182f3b52a64.png

(4)尾部插入数据

进行数据插入,我们要改变实参的值(即改变指针的指向),必须传入头指针的地址(二级指针)。

基础思路:【1】在进行数据插入之前,我们要先生成一个新的结点

【2】要进行尾部插入,我们需要找到链表的最后一个结点,并将它存储的指针指向新生成的结构体

【3】我们设计一个指针tail来找尾部结点,如果tail->next为NULL,我们就找到了尾部结点,结束循环。

图解:4b29d1c243e5473cb673be88da30eb2e.png

 

现阶段代码:

e2ecb7828a5d49fd8cd1c3a5c535d6e4.png

我们对代码进行测试:

6baf6efc94e14f9fb81f4d4781554370.png

可以发现程序崩溃了,这是为什么呢?

答:这是因为我们没有考虑链表为空的情况,如果链表为空,我们会直接对空指针进行解引用,导致程序崩溃。

bf1dee301bff4c10a5fdb295de1b54b7.png

 为了解决这个问题,我们可以对这种情况进行单独处理。

 代码:79c1e38713e64d61a77946d6a8f56edf.png

再次测试,观察结果。

7243601c914648718d1666a3415045a1.png 我们发现数据成功插入了。

(5)头部插入

进行数据插入,我们要改变实参的值(即改变指针的指向),必须传入头指针的地址(二级指针)。

思路:头部插入我们只需要让头指针指向新结点,让新结点的指针域指向原来的头结点

代码:2705aca8e83a4c909cff010bb6bcd708.png

前面进行尾部插入的时候需要考虑链表为空的情况,那头部插入需不需要单独进行这个临界条件的处理呢? 

图解:261769f9d5e44d02aef1c66feab4dcba.png

 我们可以发现最后结点的指针域会指向空,所以不需要考虑这个临界情况。

(6)尾部删除

进行数据插入,我们要改变实参的值(即改变指针的指向),必须传入头指针的地址(二级指针)。

思路:和尾部插入一样我们需要使用一个tail指针找到尾部结点(方法与前面一致),然后释放这个结点

我们看代码和运行结果:

19cd341a061848e7bc25cdf2cb0dfa08.png

ecc2788f069e4c56b3da63998ab1b009.png

 我们发现程序打印的是随机数,这是为什么呢?

答:因为我们释放最后一个结点的时候上一个结点的指针域没有指空,但空间已经被系统回收了,此时我们进行指针的引用是非法的,也就是我们常说的野指针

解决方案:我们可以设计一个指针prev来记录倒数第二个结点,在释放尾结点后让倒数第二个结点的指针域指向空

但此时程序依然存在不足。
如果链表为空,我们就会对空指针进行解引用,所以我们需要单独处理这种情况,这里提供两种解决方案
第一,我们可以直接返回空
第二,我们可以使用断言让程序直接报错

这里使用第一种方法。
代码和测试结果:
ec6c276d5639432885b9fa6f83d5c970.png

8f5af09c05f9409aa39e1ab492f42fb0.png

(7)头部删除

进行数据插入,我们要改变实参的值(即改变指针的指向),必须传入头指针的地址(二级指针)。

思路:进行头部删除,可以将第一个结点释放,然后让头指针指向第二个结点

代码和运行结果:

65e4af5ed59d4374a50ee445d8e33520.png

699e430580424850b41aa4f7a4a7fbe3.png

(8)查找

查找有两种实现方式,一种返回结点地址,一种返回数据在第几个结点

思路:遍历整个链表,一直到找到要查找的数据或最后一个结点为止。
如果没有查找到数据,返回NULL或者0。

第一种(我用的):

0971c2934fdf466ebd30175deeb709b2.png

 第二种:

3d8810c7f62f42dabf5055a2c471f4e1.png

上述函数我们只能找到第一个数,后面相同的找不到,如果我们需要查找链表中所有该数的位置 ,我们可以设计一个pos指针并进行循环,循环结束条件为pos为空,这样就可以实现多次查找,我们看代码。(这个方法只适用于第一种)

8660c2b7711f4983967cbca7ce2cfb51.png

(9)指定位置插入

这里给出两种插入方式,一种是指定在那个结点前插入,一种是指定在那个结点后插入

第一种前插入:c0cedcff8da74f45bb26531cca01ec92.png

 第一种后插入:

d4026db1939a4e6d8d2188a607d95dbc.png

 在进行插入前我们需要找到要插入的那个结点的前面(后面),可以先使用查找函数找到位置,在进行插入。

测试结果:1ada667b6e604c55a536650d3e870f92.png

(10)指定删除

思路:删除的思路同尾删类似,我们需要找到待删除的结点并保存待删除的结点的指针域。

代码:6099e8f0d3754045aeacc4a6d83026d7.png

在进行删除前我们需要找到要删除的那个结点的前一个结点(目的是让前面的结点指针域指向下下个结点),可以先使用查找函数找到位置,再进行删除。

测试:b3cb40c6f02f4a7784af4ae437ea83f7.png

 e195101c84744289a70bcb39f8165f78.png

4c13c10e46784c7cbbe2cbb8369047ee.png

c337b97cbca84d6bb4f8580bd497db5c.png

(11)清空链表

思路:从第一个结点开始,设计一个pos指针,每次循环把头指针指向的结点地址赋给pos,让头指针指向下一个结点地址,调用free()释放pos指向的结点。

图解(以三个结点为例子):

 

代码和测试结果:

321c36b131d8475b89ab775d1a6a9b05.png

1e62f5ee1e124f3ea665a50ab4b2a72c.png

046004a7aa3d4b51a0a496c54b8014e4.png

(12)最终代码

SingleLinkedList.h

  1. #pragma once
  2. #include <stdio.h>
  3. #include <stdlib.h>
  4. //#include <assert.h> 要使用断言的话注意包含头文件
  5. //结构体数据类型重定义,方便我们更改要存储的元素类型
  6. typedef int SLTDataType;
  7. struct SListNode
  8. {
  9. SLTDataType data; //要存储的数据(数据域)
  10. struct SListNode* next; //用来存储下一个结构体的地址(指针域)
  11. };
  12. //打印
  13. void SListPrint(struct SListNode* phead);
  14. //创建一个新节点
  15. struct SListNode* BuyListNode(SLTDataType x);
  16. //尾部插入
  17. void SListPushBack(struct SListNode** pphead, SLTDataType x);
  18. //头部插入
  19. void SListPushFront(struct SListNode** pphead, SLTDataType x);
  20. //尾部删除
  21. void SListPopBack(struct SListNode** pphead);
  22. //头部删除
  23. void SListPopFront(struct SListNode** pphead);
  24. //查找,返回对应结点地址
  25. //int SListFind(struct SListNode* phead, SLTDataType x);
  26. struct SListNode* SListFind(struct SListNode* phead, SLTDataType x);
  27. //指定插入(还有一种按输入位置来插入)(在前面插入)
  28. void SListInsertF(struct SListNode** pphead, struct SListNode* pos, SLTDataType x);
  29. void SListInsertB(struct SListNode** pphead, struct SListNode* pos, SLTDataType x);
  30. //指定删除
  31. void SListEarse(struct SListNode** pphead, struct SListNode* pos);
  32. //销毁链表
  33. void SListDestory(struct SListNode** pphead);

SingleLinkedList.c

  1. #define _CRT_SECURE_NO_WARNINGS 1
  2. #include "SingleLinkedList.h"
  3. //打印链表
  4. void SListPrint(struct SListNode* phead)
  5. {
  6. //一直循环,直到找到最后一个结点
  7. while (phead)
  8. {
  9. printf("%d-> ", phead->data); //依次打印结点存储的数据
  10. phead = phead->next; //让phead指向下一个结点
  11. }
  12. printf("NULL\n");
  13. }
  14. //生成新节点
  15. struct SListNode* BuyListNode(SLTDataType x)
  16. {
  17. //调用maoolc()函数生成一个结点
  18. struct SListNode* newNode = (struct SListNode*)malloc(sizeof(struct SListNode));
  19. //如果申请失败,打印错误并结束程序
  20. if (newNode == NULL)
  21. {
  22. printf("malloc error\n");
  23. exit(-1);
  24. }
  25. //将要插入的数据赋给新结点
  26. newNode->data = x;
  27. //新节点的next置空
  28. newNode->next = NULL;
  29. //返回生成的结点的地址
  30. return newNode;
  31. }
  32. //尾部插入
  33. void SListPushBack(struct SListNode** pphead, SLTDataType x)
  34. {
  35. //生成一个新的结点
  36. struct SListNode* newnode = BuyListNode(x);
  37. //如果链表为空,直接把新结点地址赋给*pphead
  38. if (*pphead == NULL)
  39. {
  40. *pphead = newnode;
  41. }
  42. else
  43. {
  44. //设置一个指针tail用来找到尾部结点
  45. struct SListNode* tail = *pphead;
  46. //不断循环,直到找到尾部结点
  47. while (tail->next)
  48. {
  49. tail = tail->next;//指向下一个结点
  50. }
  51. //让原本置空的指针指向新生成的结点
  52. tail->next = newnode;
  53. }
  54. }
  55. //头部插入
  56. void SListPushFront(struct SListNode** pphead, SLTDataType x)
  57. {
  58. //生成新结点
  59. struct SListNode* newnode = BuyListNode(x);
  60. //保存原来第一个结点的地址
  61. struct SListNode* prev = *pphead;
  62. //让头指向新结点
  63. *pphead = newnode;
  64. newnode->next = prev;
  65. }
  66. //尾部删除
  67. void SListPopBack(struct SListNode** pphead)
  68. {
  69. //如果链表为空,就直接返回空,也可以使用assert(*pphead!=NULL)
  70. if (*pphead == NULL)
  71. {
  72. return;
  73. }
  74. //如果只有一个结点
  75. if ((*pphead)->next == NULL)
  76. {
  77. free(*pphead);
  78. *pphead = NULL;
  79. }
  80. else
  81. {
  82. //找尾部
  83. struct SListNode* tail = *pphead;
  84. //记录尾部的前一个结点的地址
  85. struct SListNode* prev = NULL;
  86. //找尾部结点,并保存尾部结点的前一个结点的地址
  87. while (tail->next)
  88. {
  89. prev = tail;
  90. tail = tail->next;
  91. }
  92. //找到尾部结点,释放
  93. free(tail);
  94. //置空
  95. tail = NULL;
  96. //把尾部的前一个结点保存的地址置空
  97. prev->next = NULL;
  98. }
  99. }
  100. //头部删除
  101. void SListPopFront(struct SListNode** pphead)
  102. {
  103. //如果链表为空,返回空,也可以使用assert(*pphead!=NULL)
  104. if (*pphead == NULL)
  105. {
  106. return;
  107. }
  108. else
  109. {
  110. //找到下一个结点的地址
  111. struct SListNode* prev = (*pphead)->next;
  112. //释放第一个结点
  113. free(*pphead);
  114. //头指针指向第二个结点
  115. *pphead = prev;
  116. }
  117. }
  118. //查找
  119. struct SListNode* SListFind(struct SListNode* phead, SLTDataType x)
  120. {
  121. struct SListNode* cur = phead;
  122. //循环查找
  123. while (cur)
  124. {
  125. //找到返回该结点地址
  126. if (cur->data == x)
  127. {
  128. return cur;
  129. }
  130. //没找到指向下一个结点
  131. else
  132. {
  133. cur = cur->next;
  134. }
  135. }
  136. //如果没找到,返回NULL
  137. return NULL;
  138. }
  139. //第二种
  140. //int SListFind(struct SListNode* phead, SLTDataType x)
  141. //{
  142. // //记录第几个结点
  143. // int i = 1;
  144. // struct SListNode* cur = phead;
  145. // //循环查找
  146. // while (cur)
  147. // {
  148. // //找到返回该结点地址
  149. // if (cur->data == x)
  150. // {
  151. // return i;
  152. // }
  153. // //没找到指向下一个结点,i加1
  154. // else
  155. // {
  156. // i = i + 1;
  157. // cur = cur->next;
  158. // }
  159. // }
  160. // //如果没找到,返回0
  161. // return 0;
  162. //}
  163. //指定结点前插入
  164. void SListInsertF(struct SListNode**pphead,struct SListNode* pos,SLTDataType x)
  165. {
  166. //生成一个新的结点
  167. struct SListNode* newnode = BuyListNode(x);
  168. //只有一个结点或者链表为空,进行头插
  169. if (*pphead == pos)
  170. {
  171. newnode->next = *pphead;
  172. *pphead= newnode;
  173. }
  174. else
  175. {
  176. //设计一个结构体指针来找pos的前一个结点
  177. struct SListNode* posprev = *pphead;
  178. while (posprev->next != pos)
  179. {
  180. posprev = posprev->next;
  181. }
  182. posprev->next = newnode;
  183. newnode->next = pos;
  184. }
  185. }
  186. //指定结点后插入
  187. void SListInsertB(struct SListNode**pphead,struct SListNode* pos,SLTDataType x)
  188. {
  189. //生成一个新的结点
  190. struct SListNode* newnode = BuyListNode(x);
  191. //新结点指针域指向该结点的后一个
  192. newnode->next = pos->next;
  193. //结点的指针域指向新结点
  194. pos->next = newnode;
  195. }
  196. //指定位置删除
  197. void SListEarse(struct SListNode** pphead, struct SListNode* pos)
  198. {
  199. //如果链表为空,返回空,也可以使用assert(*pphead!=NULL)
  200. if (*pphead == NULL)
  201. {
  202. return;
  203. }
  204. //要删除的结点是第一个结点
  205. if (pos == *pphead)
  206. {
  207. //找到下一个结点的地址
  208. struct SListNode* prev = (*pphead)->next;
  209. //释放第一个结点
  210. free(*pphead);
  211. //头指针指向第二个结点
  212. *pphead= prev;
  213. }
  214. else
  215. {
  216. //要找到pos结点的前一个结点位置
  217. struct SListNode* posprev = *pphead;
  218. while (posprev->next != pos)
  219. {
  220. posprev = posprev->next;
  221. }
  222. //让posprev的指针域指向下下个结点
  223. posprev->next = pos->next;
  224. //释放结点pos的空间
  225. free(pos);
  226. pos= NULL;
  227. }
  228. }
  229. //清空链表
  230. void SListDestory(struct SListNode** pphead)
  231. {
  232. struct SListNode* prev = *pphead;
  233. while ((*pphead)!= NULL)
  234. {
  235. //找到头指针指向的结点
  236. prev = *pphead;
  237. //让头指针指向下一个结点
  238. *pphead = (*pphead)->next;
  239. //释放前面的结点
  240. free(prev);
  241. }
  242. }

text.c

  1. #define _CRT_SECURE_NO_WARNINGS 1
  2. #include "SingleLinkedList.h"
  3. void text1()
  4. {
  5. struct SListNode* head = NULL;
  6. SListPushFront(&head, 5);
  7. SListPushFront(&head, 50);
  8. SListPushFront(&head, 50);
  9. SListPushFront(&head, 5);
  10. struct SListNode* pos = SListFind(head, 50);
  11. int i = 1;
  12. //查找多个相同的值
  13. while (pos)
  14. {
  15. printf("第%d个pos节点:%p->%d\n", i++, pos, pos->data);
  16. //pos指向目标结点的下一个结点
  17. pos = SListFind(pos->next, 50);
  18. }
  19. SListPrint(head);
  20. //修改
  21. pos = SListFind(head, 50);
  22. if (pos)
  23. {
  24. pos->data = 30;
  25. }
  26. SListPrint(head);
  27. }
  28. void text2()
  29. {
  30. struct SListNode* head = NULL;
  31. //插入
  32. SListPushBack(&head, 2);
  33. SListPushBack(&head, 5);
  34. SListPushBack(&head, 15);
  35. //查找14位置
  36. struct SListNode* pos = SListFind(head, 14);
  37. //判断是否有14
  38. if (pos == NULL)
  39. {
  40. printf("没有该数据\n");
  41. }
  42. else
  43. {
  44. //删除14
  45. SListEarse(&head, pos);
  46. }
  47. SListPrint(head);
  48. //清空
  49. SListDestory(&head);
  50. //插入
  51. SListPushBack(&head, 2);
  52. SListPushBack(&head, 5);
  53. SListPrint(head);
  54. }
  55. int main()
  56. {
  57. /*text1();*/
  58. text2();
  59. return 0;
  60. }

五:小结

相较于顺序表,链表能够更好的利用零散的空间,并且插入数据不需要大量移动数据,但是单链表在物理层上不是连续存储的,我们只能前找后却无法后找前,而且一旦指针域的数据丢失我们就没法找到后续结点,后续的双向链表可以很好的解决这个问题。

顺序表讲解链接:http://t.csdn.cn/V96aI

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/一键难忘520/article/detail/802955
推荐阅读
相关标签
  

闽ICP备14008679号