深圳幻海软件技术有限公司 欢迎您!

今天小胡杨手绘28张图只为教会你单链表

2023-04-25

          欢迎光临          个人主页:欢迎大家光临——>沙漠下的胡杨&nb

                   欢迎光临              

  个人主页:欢迎大家光临——>沙漠下的胡杨

  各位大帅哥,大漂亮

 如果觉得文章对自己有帮助

 可以一键三连支持博主

 你的每一分关心都是我坚持的动力

 

 ☄: 本期重点 :数据结构篇单链表实现

  希望大家每天都心情愉悦的学习工作。 

 

  学数据结构,先学画图,本期和小胡杨一起开始绘画之旅吧!    


           目录                

什么是单链表呢?

链表的实际地址的存放   

创建链表   

申请结点

链表的尾插

链表的头插

链表的头删:

链表的尾删

查找链表元素:

修改链表元素

在链表任意位置后插

在链表任意位置前插

在链表任意位置后删

删除链表的任意位置

链表的打印

链表空间释放

关于整体的代码头文件篇:

关于整体代码源文件:          


什么是单链表呢?

链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表 中的指针链接次序实现的 。

类似于向一条链串联起来的一样。 

链表的实际地址的存放

链表我们一般的图示为下面的样子:

 实际上我们要看链表的实际地址存放:

代码如下:

  1. SLTNode* d1 = (SLTNode*)malloc(sizeof(SLTNode));
  2. assert(d1);
  3. SLTNode* d2 = (SLTNode*)malloc(sizeof(SLTNode));
  4. assert(d2);
  5. SLTNode* d3 = (SLTNode*)malloc(sizeof(SLTNode));
  6. assert(d3);
  7. d1->data = 1;
  8. d2->data = 2;
  9. d3->data = 3;
  10. d1->next = d2;
  11. d2->next = d3;
  12. d3->next = NULL;
  13. SListPrint(d1);

                                     这是单链表的实际存放的地址                                              

创建链表

首先我们要先创建链表,用结构体创建这就不在多说啦,关于这个头文件定义结构体细节处理都在代码中了。    结构体有不明白的请移步☞结构体详解

  1. #pragma once//防止头文件重复包含
  2. #define _CRT_SECURE_NO_WARNINGS//防止scanf函数报错
  3. #include <stdio.h>
  4. #include <stdlib.h>
  5. #include <assert.h>//断言头文件
  6. typedef int SLTDataType;//把int重命名
  7. typedef struct SListNode//结构体重命名
  8. {
  9. SLTDataType data;
  10. struct SListNode *next;//这个不可以使用重命名后的名字
  11. }SLTNode;

申请结点

我们有链表了,但是我们还没有链表的组成成员呀!所以我们要申请结点,什么时候需要申请结点呢?那肯定是选择插入就会需要结点啊,所以我们不妨写函数是就直接申请结点,并附上数据,指针我们可以默认指向为NULL

看下代码:

  1. SLTNode* BuySListNode(SLTDataType x)
  2. {
  3. SLTNode* newnode = (SLTNode *)malloc(sizeof(SLTNode));
  4. assert(newnode);
  5. newnode->next = NULL;
  6. newnode->data = x;
  7. return newnode;
  8. }

我们可以直接要把要赋给的值传给函数,然后开辟一个结点的空间,进行断言,判断是否申请到了空间,接着把要赋给的值给data,把结点指针的指向NULL。

链表的尾插

首先我们先想一下传什么参数呢?

首先是要插入的值,那么是传链表的地址,还是直接传链表呢?如果链表为NULL呢?

我们先看一个示意图:

我们已经有啦一个结点,并且已经放好值了,那么怎么操作呢?我们只需要把d3中的next的NULL指向新开辟的结点,是不是就可以啦!接着看图:

 变为这个样子就可以啦。(最重要的是plist没有改变)

可是如果我们的链表没有元素呢?接着看图:

 此时的plist是为NULL,那我们直接吧plist指向 d 的地址就好呀,并且只有一个 d 刚好d的下一个结点为NULL。(注意:plist已经由空指针变为了指向新节点 d 了)

看下代码(很多人都会错误的示范)

  1. void SListPushBack(SLTNode *pphead, SLTDataType x)
  2. {
  3. SLTNode* newnode = BuySListNode(x);
  4. if (pphead == NULL)
  5. {
  6. pphead = newnode;
  7. }
  8. else
  9. {
  10. SLTNode* cur = pphead;
  11. while (cur->next)
  12. {
  13. cur = cur->next;
  14. }
  15. cur->next = newnode;
  16. }
  17. }

这样写的话有没有一点点的不妥呢?

我们调试下,发现plist没有变,链表没有插入,接着看图:

 我们得出了一个结论:

不是传指针,就可以改变这个指针的指向的,我们应该传二级指针的形式,然后解引用,达到改变plist地址的效果,所以我们链表所有的插入 和 删除函数都传二级指针,传二级指针时,我们使用头结点只要解引用就好了。

所以正确的代码为:

  1. void SListPushBack(SLTNode **pphead, SLTDataType x)
  2. {
  3. SLTNode* newnode = BuySListNode(x);
  4. if (*pphead == NULL)
  5. {
  6. *pphead = newnode;
  7. }
  8. else
  9. {
  10. SLTNode* cur = *pphead;
  11. while (cur->next)
  12. {
  13. cur = cur->next;
  14. }
  15. cur->next = newnode;
  16. }
  17. }

其他的都没有改变,只是把用链表头的地方,改为解引用链表头。


链表的头插

头插我们依然要考虑这个问题就是 plist 是否改变呢?

这是肯定的,头插就是换个头呀!就是链表的头被改变了,所以我们要传地址。

我们接着看个图来理解下:

 这是刚开始的情况,我们要把 plist 的地址放入新结点,并且把plist改为新结点的地址。

下面我们接着看图示:

 我们是没有plist,但是我们有 pphead,直接解引用就是头结点。

需要注意的点:

必须要先进行步骤一,再进行步骤二,否则,我们的头结点就被覆盖了,然后就找不到以后的节点啦,所以我们一定要先执行步骤一,在执行步骤二。

下图是先执行步骤二,在执行步骤一的后果(死循环)

下面是正确的代码:

  1. void SListPushFrond(SLTNode **pphead, SLTDataType x)
  2. {
  3. SLTNode* newnode = BuySListNode(x);
  4. newnode->next = *pphead;
  5. *pphead = newnode;
  6. }

链表的头删:

链表的头删很简单,我们还是传递二级指针,因为如果就一个结点,删除后变为了NULL,此时链表的指向还是发生了变换。

看下面图示:

 我们只需要把原来头结点的指向第二个结点,然后把原来头结点free掉就可以啦。

需要注意的是:

我们要先创建一个结点指针,保存第二个结点的地址

然后我们要释放原来的头指针(*pphead)

接着把保存第二个结点的地址赋值给  *pphead,让它成为新的链表的头指针。

如果刚好是一个结点,也是可以的,因为*pphead的下个结点为NULL

我们先把NULL存起来,然后把 *pphead 这个结点 free掉

再把NULL放到*pphead中,就变为NULL了,符合条件。

下面是代码:

  1. void SListPopFrond(SLTNode **pphead)
  2. {
  3. assert(pphead);
  4. SLTNode* next = (*pphead)->next;
  5. free(*pphead);
  6. *pphead = next;
  7. }

链表的尾删

链表的尾删我们要考虑的就要多点,如果有一个结点,和有多个结点分开分析

还是先看图:

如果是这种情况,就直接free掉,然后置为NULL就好了。

 这种情况就要把d3这个结点 free掉,然后把 d2 结点的指向NULL就好了。

那我们怎么找到 d2 呢?接着看图:

   

 这就是我们写代码的实际逻辑啦,下面是代码,代码中的注释部分是第二种办法,通过访问两次下一个指针的指向:

  1. void SListPopBack(SLTNode **pphead)
  2. {
  3. assert(*pphead);
  4. if ((*pphead)->next == NULL)
  5. {
  6. free(*pphead);
  7. *pphead = NULL;
  8. }
  9. else
  10. {
  11. //SLTNode *cur = *pphead;
  12. //while (cur->next->next != NULL)
  13. //{
  14. //cur = cur->next;
  15. //}
  16. //free(cur->next);
  17. //cur->next = NULL;
  18. SLTNode *tail = *pphead;
  19. SLTNode *tailprev = NULL;
  20. while (tail->next != NULL)
  21. {
  22. tailprev = tail;//失误三
  23. tail = tail->next;
  24. }
  25. free(tail);
  26. tailprev->next = NULL;
  27. }
  28. }

查找链表元素:

查找就很简单了,我们直接暴力查找就好啦:

看下代码:

  1. SLTNode* SListFind(SLTNode *pphead, SLTDataType x)
  2. {
  3. SLTNode* cur = pphead;
  4. while (cur)
  5. {
  6. if (cur->data == x)
  7. {
  8. return cur;
  9. }
  10. cur = cur->next;
  11. }
  12. return NULL;
  13. }

需要注意的地方:

首先,我们要先保存头指针,不要改变头指针。

其次,我们要考虑代码的健壮性,并且配合其他的函数接口使用,所以我们返回值设定为结点指针。如果找不到,我们就返回NULL。

修改链表元素

修改就可以配合查找一起使用也很简单,直接看代码吧:

我们可以通过查找到的下标,进行修改,没啥说的。

  1. void SListModify(SLTNode *pos, SLTDataType x)
  2. {
  3. assert(pos);
  4. pos->data = x;
  5. }

在链表任意位置后插

在链表任意位置后插入,首先我们应该找该位置,然后进行插入。

先看图:

 这个插入位置我们已知,然后我们只需要改变指向就可以啦,接着看图:

 我们要注意的是先进行步骤一,在进行步骤二,顺序一定不能反,否则就和头插一样死循环了。

下面是具体的代码:

  1. void SListInsertAfter(SLTNode *pos, SLTDataType x)
  2. {
  3. assert(pos);
  4. SLTNode* newnode = BuySListNode(x);//注意下面代码的位置
  5. newnode->next = pos->next;
  6. pos->next = newnode;
  7. }

在链表任意位置前插

在链表任意位置后插,这个比较复杂啦。下面看图一点点的分析:

首先如果只有一个结点,那就相当于头插了

和头插一样,不在赘述啦~

如果有多个结点,就复杂啦~  我们先看图吧:

我们怎么知道这个位置前一个的结点值呢?方法其实和尾删一样,使用一个变量遍历,循环结束的条件变为 p1->next != 这个位置 。

看下图:

 最后看下代码:

  1. void SListInsertBefore(SLTNode **pphead, SLTNode *pos, SLTDataType x)
  2. {
  3. assert(pos);
  4. if (pos == *pphead)
  5. {
  6. SListPushFrond(pphead, x);
  7. }
  8. else
  9. {
  10. SLTNode *prve = *pphead;
  11. while (prve->next != pos)
  12. {
  13. prve = prve->next;
  14. }
  15. SLTNode* newnode = BuySListNode(x);
  16. newnode->next = pos;
  17. prve->next = newnode;
  18. }
  19. }

在链表任意位置后删

删除任意位置比较简单,我们只需要注意一点,

就是链表元素为1时,就没有后续啦,就不能删除啦。

还是看下图吧:

只有一个结点,不能删除后面的元素,其他的,直接覆盖,然后free掉pos位置的结点

我们直接看代码吧:

  1. void SListEraseAfter(SLTNode *pos)
  2. {
  3. assert(pos);
  4. SLTNode *next = pos->next;
  5. if (next)
  6. {
  7. pos->next = next->next;
  8. free(next);
  9. next = NULL;
  10. }
  11. }

删除链表的任意位置

这个又点麻烦,我们一点点分析:

首先如果这个位置在链表的头上,那么我们就是头删,

还是看图:

 如果在其他位置删除,和任意位置前插一样,我们要创建一个指针结点,进行遍历,循环结束的条件是 p1->next != pos,然后把断裂的链表链接就好啦。

接着看图吧:

下面是代码:

  1. void SListErase(SLTNode **pphead, SLTNode *pos)
  2. {
  3. assert(pphead);
  4. assert(pos);
  5. if (*pphead == pos)
  6. {
  7. SListPopFrond(pphead);
  8. }
  9. else
  10. {
  11. SLTNode *prve = *pphead;
  12. while (prve->next != pos)
  13. {
  14. prve = prve->next;
  15. }
  16. prve->next = pos->next;
  17. free(pos);
  18. pos = NULL;
  19. }
  20. }

链表的打印

打印可以不用传链表地址,我们不会修改链表内容,还有一点就是创建临时变量来遍历。

看代码吧:

  1. void SListPrint(SLTNode* pphead)
  2. {
  3. SLTNode* cur = pphead;
  4. while (cur)
  5. {
  6. printf("%d->", cur->data);
  7. cur = cur->next;
  8. }
  9. printf("NULL\n");
  10. }

链表空间释放

这个没说的,还是要传链表的地址,因为我们要把链表置为NULL

直接看代码吧:

  1. void SListDestroy(SLTNode **pphead)
  2. {
  3. assert(pphead);
  4. SLTNode *cur = *pphead;
  5. while (cur)
  6. {
  7. SLTNode *next = cur->next;
  8. free(cur);
  9. cur = next;
  10. }
  11. *pphead = NULL;
  12. }

关于整体的代码头文件篇:

  1. #pragma once
  2. #define _CRT_SECURE_NO_WARNINGS
  3. #include <stdio.h>
  4. #include <stdlib.h>
  5. #include <assert.h>
  6. typedef int SLTDataType;
  7. typedef struct SListNode
  8. {
  9. SLTDataType data;
  10. struct SListNode *next;
  11. }SLTNode;
  12. void SListPrint(SLTNode* pphead);//打印
  13. SLTNode* BuySListNode(SLTDataType x);//申请新节点
  14. void SListPushBack(SLTNode **pphead, SLTDataType x);//尾插
  15. void SListPushFrond(SLTNode **pphead, SLTDataType x);//头插
  16. void SListPopFrond(SLTNode **pphead);//头删
  17. void SListPopBack(SLTNode **pphead);//尾删
  18. SLTNode* SListFind(SLTNode *pphead, SLTDataType x);//查找
  19. void SListInsertBefore(SLTNode **pphead, SLTNode *pos, SLTDataType x);//任意位置前插
  20. void SListInsertAfter(SLTNode *pos, SLTDataType x);//任意位置后插
  21. void SListErase(SLTNode **pphead, SLTNode *pos);//删除任意位置
  22. void SListEraseAfter(SLTNode *pos);//任意位置后删
  23. void SListDestroy(SLTNode **pphead);//释放链表
  24. void SListModify(SLTNode *pos, SLTDataType x);//修改链表

关于整体代码源文件:

  1. #include "SList.h"
  2. void SListPrint(SLTNode* pphead)
  3. {
  4. SLTNode* cur = pphead;
  5. while (cur)
  6. {
  7. printf("%d->", cur->data);
  8. cur = cur->next;
  9. }
  10. printf("NULL\n");
  11. }
  12. SLTNode* BuySListNode(SLTDataType x)
  13. {
  14. SLTNode* newnode = (SLTNode *)malloc(sizeof(SLTNode));
  15. assert(newnode);
  16. newnode->next = NULL;
  17. newnode->data = x;
  18. return newnode;
  19. }
  20. void SListPushBack(SLTNode **pphead, SLTDataType x)
  21. {
  22. SLTNode* newnode = BuySListNode(x);
  23. if (*pphead == NULL)
  24. {
  25. *pphead = newnode;
  26. }
  27. else
  28. {
  29. SLTNode* cur = *pphead;
  30. while (cur->next)
  31. {
  32. cur = cur->next;
  33. }
  34. cur->next = newnode;
  35. }
  36. }
  37. void SListPushFrond(SLTNode **pphead, SLTDataType x)
  38. {
  39. SLTNode* newnode = BuySListNode(x);
  40. newnode->next = *pphead;
  41. *pphead = newnode;
  42. }
  43. void SListPopBack(SLTNode **pphead)
  44. {
  45. assert(*pphead);
  46. if ((*pphead)->next == NULL)
  47. {
  48. free(*pphead);
  49. *pphead = NULL;
  50. }
  51. else
  52. {
  53. //方法二
  54. //SLTNode *cur = *pphead;
  55. //while (cur->next->next != NULL)
  56. //{
  57. //cur = cur->next;
  58. //}
  59. //free(cur->next);
  60. //cur->next = NULL;
  61. SLTNode *tail = *pphead;
  62. SLTNode *tailprev = NULL;
  63. while (tail->next != NULL)
  64. {
  65. tailprev = tail;
  66. tail = tail->next;
  67. }
  68. free(tail);
  69. tailprev->next = NULL;
  70. }
  71. }
  72. void SListPopFrond(SLTNode **pphead)
  73. {
  74. assert(pphead);
  75. SLTNode* next = (*pphead)->next;
  76. free(*pphead);
  77. *pphead = next;
  78. }
  79. SLTNode* SListFind(SLTNode *pphead, SLTDataType x)
  80. {
  81. SLTNode* cur = pphead;
  82. while (cur)
  83. {
  84. if (cur->data == x)
  85. {
  86. return cur;
  87. }
  88. cur = cur->next;
  89. }
  90. return NULL;
  91. }
  92. void SListInsertBefore(SLTNode **pphead, SLTNode *pos, SLTDataType x)
  93. {
  94. assert(pos);
  95. if (pos == *pphead)
  96. {
  97. SListPushFrond(pphead, x);
  98. }
  99. else
  100. {
  101. SLTNode *prve = *pphead;
  102. while (prve->next != pos)
  103. {
  104. prve = prve->next;
  105. }
  106. SLTNode* newnode = BuySListNode(x);
  107. newnode->next = pos;
  108. prve->next = newnode;
  109. }
  110. }
  111. void SListInsertAfter(SLTNode *pos, SLTDataType x)
  112. {
  113. assert(pos);
  114. SLTNode* newnode = BuySListNode(x);//注意下面代码的位置
  115. newnode->next = pos->next;
  116. pos->next = newnode;
  117. }
  118. void SListErase(SLTNode **pphead, SLTNode *pos)
  119. {
  120. assert(pphead);
  121. assert(pos);
  122. if (*pphead == pos)
  123. {
  124. SListPopFrond(pphead);
  125. }
  126. else
  127. {
  128. SLTNode *prve = *pphead;
  129. while (prve->next != pos)
  130. {
  131. prve = prve->next;
  132. }
  133. prve->next = pos->next;
  134. free(pos);
  135. pos = NULL;
  136. }
  137. }
  138. void SListEraseAfter(SLTNode *pos)
  139. {
  140. assert(pos);
  141. SLTNode *next = pos->next;
  142. if (next)
  143. {
  144. pos->next = next->next;
  145. free(next);
  146. next = NULL;
  147. }
  148. }
  149. void SListDestroy(SLTNode **pphead)
  150. {
  151. assert(pphead);
  152. SLTNode *cur = *pphead;
  153. while (cur)
  154. {
  155. SLTNode *next = cur->next;
  156. free(cur);
  157. cur = next;
  158. }
  159. *pphead = NULL;
  160. }
  161. void SListModify(SLTNode *pos, SLTDataType x)
  162. {
  163. assert(pos);
  164. pos->data = x;
  165. }

———————————————————————————————————————————

                    关于链表的简单实现就讲解到这里啦,                  

            码字不容易,希望各位不要吝啬你们的三联哦~             

文章知识点与官方知识档案匹配,可进一步学习相关知识
算法技能树首页概览45038 人正在系统学习中