当前位置:   article > 正文

单链表详解(无哨兵位),实现增删改查

单链表详解(无哨兵位),实现增删改查

1.顺序表对比单链表的缺点

  1. 中间或头部插入时,需要移动数据再插入,如果数据庞大会导致效率降低
  2. 每次增容就需要申请空间,而且需要拷贝数据,释放旧空间
  3. 增容造成浪费,因为一般都是以2倍增容

2.链表的基础知识

  1. 链表也是线性表的一种。物理结构:不一定线性,逻辑结构:一定是线性的
  2. 链表物理结构也不是线性的。链表由一个一个的节点组成
  3. 节点由数据和指向下一个地方的指针。每一个节点都会存储下一个节点的地址
  4. List 表示链表, S表示single ,Node表示节点

2.1链表基本结构

  1. typedef int SLDataType;//节点类型,S 表示节点,L表示链表
  2. typedef struct SListNode
  3. {
  4. SLDataType data;
  5. struct SListNode* next;
  6. }SLNode;

3.代码实现

  1. 设置三个文件,SList.h 头文件 SList.c 功能实现文件,test.c测试文件
  2. 声明写在头文件里面 SList.h
  1. #pragma once
  2. #include <stdio.h>
  3. #include <stdlib.h>//用一个就申请一个空间
  4. #include <assert.h>
  5. typedef int SLDataType;
  6. typedef struct SListNode
  7. {
  8. int data;
  9. struct SListNode* next;
  10. }SLNode;
  11. //链表打印
  12. void SLPrint(SLNode* phead);

3.1.实现思路

  1. 为两个节点开辟空间,并通过赋值给他们链接起来,通过节点内的地址
  2. 通过传过来的头节点去找尾节点,从而达到遍历链表
  3. 通过改变pcur的指向,相当于可以访问下一块空间了

  1. void SLPrint(SLNode* phead)//phead 表示头节点
  2. {
  3. SLNode* pcur = phead;//pcur 临时的节点
  4. while (pcur != 0)
  5. {
  6. printf("%d->", pcur->data);
  7. pcur = pcur->next;
  8. }
  9. printf("NULL\n");
  10. }

3.1.1测试方法

  1. 开辟了空间并且把他们都串起来。
  1. void SListTest1()
  2. {
  3. SLNode* Node1 = (SLNode*)malloc(sizeof(SLNode));//初始化
  4. Node1->data = 1;
  5. SLNode* Node2 = (SLNode*)malloc(sizeof(SLNode));
  6. Node2->data = 2;
  7. SLNode* Node3 = (SLNode*)malloc(sizeof(SLNode));
  8. Node3->data = 3;
  9. Node1->next = Node2;
  10. Node2->next = Node3;
  11. Node3=->next = NULL;//最后一个置为NULL
  12. //打印链表
  13. SLPrint(Node1);
  14. }

3.1.2当while循环条件不一样时

  1. 这里用图说明问题,当pcur 和pcur->next 。pcur不为NULL,还会多走一步。
  2. pucr->next 为空不往下走,就是说,pcur 比pcur->next,多走一步

4.0尾插

  1. 在尾插开始之前,需要判断两种情况
    1. 第一种就是空链表和非空链表,如果为空链表,肯定要开辟空间的
    2. 在这个新开辟的空间里面,顺便一起把需要插入的数据放这里一起插入,也就说涉及到插入会对空间进行增加的就要新空间
    3. 开辟好了空间,判断有效性,然后放入数据,再给个NULL
  1. //新空间
  2. SLNode* SLBuyNode(SLDataType x)
  3. {
  4. SLNode* Newnode = (SLNode*)malloc(sizeof(SLNode));
  5. if (Newnode == NULL)
  6. {
  7. perror("malloc New");
  8. exit(1);//非0表示异常返回,会导致直接跳出整个程序
  9. }
  10. //开辟完新空间记得放入数据
  11. Newnode->data = x;
  12. Newnode->next = NULL;
  13. return Newnode;//开辟完空间要记得返回
  14. }
  1. 如果不是空链表,就要通过头结点找到尾节点,一定是要找到存放下一个节点的next
  2. 找了尾节点,就可以直接在尾节点放上新开辟好,因为对应数据已经在新空间里弄好了
  1. //链表尾插
  2. void SLPushBack(SLNode** pphead, SLDataType x)
  3. {
  4. assert(pphead);//这里不能判断*pphead,因为有可能本来就是空链表
  5. //尾插的时候需要看是否为NULL,空链表和非空链表
  6. SLNode* Newnode = SLBuyNode(x);//把需要插入的值传过去
  7. if (*pphead == NULL)
  8. {
  9. *pphead = Newnode;//如果是NULL就把新开辟的给到头节点
  10. }
  11. else
  12. {
  13. SLNode* ptail = *pphead;
  14. while (ptail->next != NULL)//查找尾节点
  15. {
  16. ptail = ptail->next;
  17. }
  18. ptail->next = Newnode;//新节点里面已经置为空指针
  19. }
  20. }

4.1测试方法

  1. 往后的测试方法都不写了,学会了后根据对应参数测试就OK了。
  2. 这里注意一定要传二级指针的地址,对一级指针改变指向就是要传二级指针
  3. 举个例子,看下面一个传址调用,另外一个是传值调用

  1. SListTest2()
  2. {
  3. SLNode* node = NULL;//新创建的节点要初始化
  4. //测试尾插
  5. //SLPushBack(&node, 2);
  6. //SLPushBack(&node, 3);
  7. //SLPushBack(&node, 4);
  8. //SLPrint(node);
  9. //测试头插
  10. SLPushFront(&node, 4);
  11. SLPushFront(&node, 3);
  12. SLPushFront(&node, 2);
  13. SLPushFront(&node, 1);
  14. SLPrint(node);
  15. }

1.可以看上面你要&node的地址才能对node改变,而不是他创建node,而你就传node,这个就和传值调用不就一样了吗?

5.0头插

  1. assert(pphead);//这里不能判断*pphead,因为有可能本来就是空链表
  2. 头插当然就很简单了,直接让新开辟的空间指向头节点
  3. 然后把头结点权限给新空间,让他newnode来当第一个
  1. //链表头插
  2. void SLPushFront(SLNode** pphead, SLDataType x)
  3. {
  4. assert(pphead);//这里不能判断*pphead,因为有可能本来就是空链表
  5. SLNode* newnode = SLBuyNode(x);
  6. newnode->next = *pphead;//把之前的头节点给到新节点
  7. *pphead = newnode;//头插成为新节点
  8. }

6.0尾删

  1. 第一种:如果是一个节点就可以直接删除
  2. 第二种:在进行尾删的时候也要找尾,而且还有保留前一个节点的地址
  3. 然后保留的前一个节点的地址,他指向的下一个地址就可以置为NULL
  1. //链表尾删
  2. void SLDelBack(SLNode** pphead)
  3. {
  4. //传过来的地址必须有效
  5. assert(pphead && *pphead);
  6. //首先要考虑2种情况,有链表的情况,一个节点的情况
  7. if ((*pphead)->next == NULL)
  8. {
  9. free(*pphead);
  10. *pphead = NULL;
  11. }
  12. else
  13. {
  14. SLNode* ptail = *pphead;//再创建一个临时的指针更好,可以避免优先级问题,还有能保留源地址
  15. SLNode* pcur = *pphead;
  16. while (ptail->next)
  17. {
  18. pcur = ptail;//ptail指向前的一个节点
  19. ptail = ptail->next;
  20. }
  21. free(ptail);//直接free ptail
  22. ptail = NULL;
  23. pcur->next = NULL;
  24. }
  25. }

6.1尾删最后一步,倒推图

7.头删

  1. 链表不能为空,为NULL删什么啊,当然对应的二级指针也不能为空
  2. -> 符号优先级 比 * 星号高
  3. 两个问题点
    1. 一个节点:这个问题怎么提出的因为链表为空不能删会报错,那么本身方法无意义了
    2. 多个节点:先把指向的节点给到*pphead,也就是我们的头节点,然后再删除之前的头节点
  4. 头删的时候要把这个节点删除,但是我们要能找到下一个节点并成为头节点
  5. 由此可以得出先把next = Node1->next。不能直接给赋值给*pphead,因为后面还要删除头节点,所以我们先保存下一个节点的地址
  1. void SLDelFront(SLNode** pphead)
  2. {
  3. assert(pphead && *pphead);//链表为空就没有删除的意义了
  4. SLNode* next = (*pphead)->next;//保存下一个节点
  5. free(*pphead);
  6. *pphead = next;
  7. }

8.链表查找节点

  1. 在查找链表的时候,你可能要想想,指针走到后面的NULL,万一后面还有需要查找的值呢?所以还要备份一个源数据
  2. 遍历向后查找,找到返回这个节点。
  3. 这个查找代码就是向后遍历,如果相等就返回,否则返回NULL
  1. SLNode* SLFind(SLNode* phead, SLDataType x)
  2. {
  3. assert(phead);//等价与 phead != NULL
  4. SLNode* pcur = phead;//可能需要多次查找
  5. while (pcur)//结束调试是最后一个节点的后一个NULL,往下查找无意义
  6. {
  7. if (pcur->data == x)
  8. {
  9. return pcur;//如果到了当前节点,就找到当前节点下面的值
  10. }
  11. pcur = pcur->next;//访问到当前节点指向的下一个,没有向下查找条件会死循环
  12. }
  13. return NULL;
  14. }

9.指定位置之前插入

  1. insert 插入 after 在什么之后 prev 在什么之前,对数据增加和删除,就需要二级指针
  2. 链表的二级指针不能为空,而且链表也不能为空 因为要pos要通过链表找打指定位置,当然pos也不能为NULL,在NULL的位置插入吗
  3. prev需要找到pos之前的位置while(prev->next ! = 0),没有找到就继续往下走
  4. prev 和 pos都找到了,那么要在两者之前插入
  5. 注意:关于pos参数,是Find查找到前的,因为find是查找函数,找到指定位置然后返回
  1. void SLInsert(SLNode** pphead, SLNode* pos, SLDataType x)
  2. {
  3. assert(pphead && *pphead);
  4. assert(pos);//不能为空,为你还找啥呢?
  5. if (pos == *pphead)
  6. {
  7. SLPushFront(pphead, x);
  8. }
  9. else
  10. {
  11. SLNode* prev = *pphead;//prev 表示 pos的前一个节点
  12. SLNode* newnode = SLBuyNode(x);
  13. while (prev->next != pos)//如过指向的下一个节点不是pos,则继续往下找
  14. {
  15. prev = prev->next;
  16. }
  17. newnode->next = pos;
  18. prev->next = newnode;
  19. }
  20. }

10.在指定位置之后插入数据

  1. 从图中可以看出正确的方式是的二种,第一种导致找不到pos->next的节点
  2. 在指定位置之后不需要第一个有效节点,用不到第一个节点,通过pos就可以找到后面一个节点
  1. //在指定位置之后插入
  2. void SLInsetAfter(SLNode* pos, SLDataType x)
  3. {
  4. assert(pos);//指定为空,还怎么删除
  5. SLNode* newnode = SLBuyNode(x);
  6. newnode->next = pos->next;
  7. pos->next = newnode;//pos->next 原先指向的数据已经给到newnode->next
  8. }

11.pos位置删除

  1. 首先就是要断言,传过来的二级指针里面装着的链表,当然都不能为空,pos节点为空了就说明指定位置没有该数据
  2. 情况1:pos不是第一个有效节点的位置
  3. 情况2:pos是第一个有效节点的位置
  1. //删除pos节点
  2. void SLErase(SLNode** pphead, SLNode* pos)
  3. {
  4. assert(pphead && *pphead);
  5. assert(pos);
  6. //两种情况:1.pos节点没有指向第一个有效节点,反之就是指向了
  7. if (pos == *pphead)
  8. {
  9. //相当于头删了
  10. SLDelFront(pphead);
  11. }
  12. else
  13. {
  14. SLNode* prev = *pphead;
  15. while (prev->next != pos)//他指向的节点,等于了pos就相当于找到了前一个节点
  16. {
  17. prev = prev->next;//通过当前的找到下一个,直到找到pos
  18. }
  19. //prev -> pos -> pos->next
  20. prev->next = pos->next;
  21. free(pos);
  22. pos = NULL;
  23. }
  24. }

12.pos之后的节点删除

  1. pos和pos->next,的节点都不能为NULL,下一个节点为NULL你删除什么?
  2. 在删除pos之后的节点之前,我们要先保存pos->next这个节点,防止内存泄漏,创建一个临时的del 变量来保存
  1. //删除pos节点之后的
  2. void SLEraseAfter(SLNode* pos)
  3. {
  4. assert(pos && pos->next);//都不能为空
  5. SLNode* del = pos->next;
  6. pos->next = del->next;
  7. free(del);
  8. del = NULL;
  9. }

13.销毁节点

  1. 销毁节点需要一个一个的销毁
  2. 大致的思路是
    1. 先保存下一个节点的地址,然后再释放这个节点
    2. 释放完后,把下一个节点的地址给pcur,从而构成循环
    3. 直到为NULL为止
  1. //销毁链表
  2. void SLDesTroy(SLNode** pphead)
  3. {
  4. SLNode* pcur = *pphead;
  5. while (pcur)
  6. {
  7. SLNode* del = pcur->next;//在销毁之前保存下一个节点的地址
  8. free(pcur);
  9. pcur = del;
  10. }
  11. pcur = NULL;
  12. }

4.最后的完整代码

4.1头文件部分

  1. #define _CRT_SECURE_NO_WARNINGS
  2. #include <stdio.h>
  3. #include <assert.h>
  4. #include <stdlib.h>
  5. typedef int SLDataType;
  6. typedef struct SListNode
  7. {
  8. SLDataType val;
  9. struct SListNode* next;
  10. }SLNode;
  11. //链表打印
  12. void SLPrint(SLNode* phead);
  13. //链表尾插和头插
  14. void SLPushBack(SLNode** pphead, SLDataType x);
  15. void SLPushFront(SLNode** pphead, SLDataType x);
  16. //链表头删和尾删
  17. void SLDelBack(SLNode** pphead);
  18. void SLDelFront(SLNode** pphead);
  19. //链表查找
  20. SLNode* SLFind(SLNode* phead, SLDataType x);
  21. //链表pos之前插入
  22. void SLInset(SLNode** pphead, SLNode* pos, SLDataType x);
  23. //链表pos之后插入
  24. void SLInsetAfter(SLNode* pos, SLDataType x);
  25. //链表pos节点删除
  26. void SLErase(SLNode** pphead,SLNode* pos);
  27. //链表pos之后删除
  28. void SLEraseAfter(SLNode* pos);
  29. //链表的销毁
  30. void SLDesTroy(SLNode* pphead);

4.2方法部分

  1. #include "SList.h"
  2. void SLPrint(SLNode* phead)
  3. {
  4. SLNode* pcur = phead;
  5. while (pcur)//如果是pcur->next为结束条件会导致,提前结束不进入循环
  6. {
  7. printf("%d->", pcur->val);
  8. pcur = pcur->next;
  9. }
  10. printf("NULL\n");
  11. }
  12. //新节点空间
  13. SLNode* SLBuyNode(SLDataType x)
  14. {
  15. SLNode* Newnode = (SLNode*)malloc(sizeof(SLNode));
  16. if (Newnode == NULL)
  17. {
  18. perror("malloc new");
  19. exit(1);//异常返回退出整个工程
  20. }
  21. Newnode->val = x;
  22. Newnode->next = NULL;
  23. return Newnode;
  24. }
  25. //尾插
  26. void SLPushBack(SLNode** pphead, SLDataType x)
  27. {
  28. assert(pphead);
  29. SLNode* newnode = SLBuyNode(x);
  30. //空链表和非空链表
  31. if (*pphead == NULL)
  32. {
  33. *pphead = newnode;
  34. }
  35. else
  36. {
  37. SLNode* ptail = *pphead;
  38. while (ptail->next != NULL)
  39. {
  40. ptail = ptail->next;
  41. }
  42. ptail->next = newnode;
  43. }
  44. }
  45. //头插
  46. void SLPushFront(SLNode** pphead, SLDataType x)
  47. {
  48. assert(pphead);
  49. SLNode* newnode = SLBuyNode(x);
  50. newnode->next = *pphead;
  51. *pphead = newnode;//直接把第一个节点的位置给newnode,成为新的头结点
  52. }
  53. //尾删
  54. void SLDelBack(SLNode** pphead)
  55. {
  56. assert(pphead && *pphead);
  57. //一个节点的情况
  58. if ((*pphead)->next == NULL)
  59. {
  60. free(*pphead);
  61. *pphead = NULL;
  62. }
  63. else
  64. {
  65. SLNode* prev = *pphead;//保留的前一个节点
  66. SLNode* ptail = *pphead;
  67. while (ptail->next != NULL)//跳出循环就表示找到前一个节点了
  68. {
  69. prev = ptail;//保留了结束前的前一个地址
  70. ptail = ptail->next;
  71. }
  72. free(ptail);
  73. ptail = NULL;
  74. prev->next = NULL;//指向的下一个节点为NU
  75. }
  76. }
  77. //头删
  78. void SLDelFront(SLNode** pphead)
  79. {
  80. assert(pphead && *pphead);
  81. SLNode* pcur = (*pphead)->next;
  82. free(*pphead);
  83. *pphead = pcur;
  84. }
  85. //查找
  86. SLNode* SLFind(SLNode* phead, SLDataType x)
  87. {
  88. assert(phead);
  89. SLNode* pcur = phead;
  90. while (pcur)
  91. {
  92. if (pcur->val == x)
  93. return pcur;
  94. pcur = pcur->next;
  95. }
  96. return NULL;
  97. }
  98. //pos位置之前插入
  99. void SLInset(SLNode** pphead, SLNode* pos, SLDataType x)
  100. {
  101. assert(pphead && *pphead);
  102. assert(pos);
  103. SLNode* newnode = SLBuyNode(x);
  104. if (*pphead == pos)//因为下面一种会找不到
  105. {
  106. SLPushFront(pphead, x);//这里要传二级指针因为,要对链表进行改变
  107. }
  108. else
  109. {
  110. SLNode* prev = *pphead;
  111. while (prev->next != pos)
  112. {
  113. prev = prev->next;
  114. }
  115. //perv pos
  116. newnode->next = pos;
  117. prev->next = newnode;
  118. }
  119. }
  120. //pos之后插入
  121. void SLInsetAfter(SLNode* pos, SLDataType x)
  122. {
  123. assert(pos);
  124. SLNode* newnode = SLBuyNode(x);
  125. newnode->next = pos->next;
  126. pos->next = newnode;
  127. }
  128. //pos位置处删除
  129. void SLErase(SLNode** pphead, SLNode* pos)
  130. {
  131. assert(pos);
  132. if (*pphead == pos)//调用头删
  133. {
  134. SLDelFront(pphead);
  135. }
  136. else
  137. {
  138. SLNode* prev = *pphead;
  139. while (prev->next != pos)
  140. {
  141. prev = prev->next;
  142. }
  143. prev->next = pos->next;
  144. free(pos);
  145. pos = NULL;
  146. }
  147. }
  148. //pos之后删除
  149. void SLEraseAfter(SLNode* pos)
  150. {
  151. assert(pos && pos->next);//pos后面的一个节点也不能为NULL
  152. SLNode* del = pos->next;
  153. pos->next = del->next;
  154. free(del);
  155. del = NULL;
  156. }
  157. //销毁链表
  158. void SLDesTroy(SLNode** pphead)
  159. {
  160. SLNode* pcur = *pphead;
  161. while (pcur)
  162. {
  163. SLNode* del = pcur->next;//在销毁之前保存下一个节点的地址
  164. free(pcur);
  165. pcur = del;
  166. }
  167. pcur = NULL;
  168. }

4.3测试部分

  1. #include "SList.h"
  2. SListTest1()
  3. {
  4. SLNode* node = NULL;//新创建的节点要初始化
  5. //测试尾插
  6. //SLPushBack(&node, 2);
  7. //SLPushBack(&node, 3);
  8. //SLPushBack(&node, 4);
  9. //SLPrint(node);
  10. //测试头插
  11. SLPushFront(&node, 4);
  12. SLPushFront(&node, 3);
  13. SLPushFront(&node, 2);
  14. SLPushFront(&node, 1);
  15. SLPrint(node);
  16. 测试尾删
  17. //SLDelBack(&node);
  18. //SLDelBack(&node);
  19. //SLDelBack(&node);
  20. //SLDelBack(&node);
  21. //SLPrint(node);
  22. 测试头删
  23. //SLDelFront(&node);
  24. //SLDelFront(&node);
  25. //SLDelFront(&node);
  26. //SLDelFront(&node);
  27. //SLDelFront(&node);
  28. //SLPrint(node);
  29. //测试查找
  30. SLNode* find = SLFind(node,3);
  31. if (find)//非0值,NULL其实原本意思也是0
  32. printf("找到了\n");
  33. else
  34. printf("没找到\n");
  35. //pos前插入
  36. //SLInset(&node, find, 11);
  37. //SLPrint(node);
  38. //pos后插入
  39. //SLInsetAfter(find, 22);
  40. //SLPrint(node);
  41. //删除pos位置
  42. //SLErase(&node, find);
  43. //SLPrint(node);
  44. //删除pos位置之后的
  45. SLEraseAfter(find);
  46. SLPrint(node);
  47. SLDesTroy(&node);
  48. }
  49. int main()
  50. {
  51. SListTest1();
  52. return 0;
  53. }

总结:

  1. 做代码题的时候,把可能发生情况按照步骤写清楚,分为几点等等
  2. 最好把每个情况里的推导步骤也要写清楚
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/2023面试高手/article/detail/464122
推荐阅读
相关标签
  

闽ICP备14008679号