当前位置:   article > 正文

【数据结构初阶】三、 线性表里的链表(无头+单向+非循环链表 -- C语言实现)_无头结点链表

无头结点链表

=========================================================================

相关代码gitee自取

C语言学习日记: 加油努力 (gitee.com)

 =========================================================================

接上期

【数据结构初阶】二、 线性表里的顺序表(C语言实现顺序表)-CSDN博客

 =========================================================================

                   

引言

通过上期对顺序表介绍使用

我们可以知道顺序表有以下优点缺点:

            

顺序表优点

                  

  • 尾插尾删 操作足够快
                   
  • 能够使用下标随机访问修改,这是很方便

            

-----------------------------------------------------------------------------------------------

            

顺序表缺点

                  

  • 头部/中间插入删除操作较慢时间复杂度为O(N)
                              
  • 增容需要申请新空间拷贝数据释放旧空间。会有不小的消耗
                         
  • 增容一般呈2倍的增长势必会有一定的空间浪费
    例如:
    当前容量100,满了以后增容到 200,我们再继续插入了5个数据
    后面没有数据插入了,那么就浪费了95个数据空间

            

            

下面将要介绍和实现的链表不会出现这些问题

         

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

             

1 . 链表

(1). 链表的概念及结构:

           

链表的概念

链表是一种物理存储结构非连续非顺序存储结构

数据元素的逻辑顺序通过链表中的指针链接次序实现的,
单链表一般不会单独使用
只有头插头删实现简单效率高

              

链表的结构

  • 链表属于线性表线性表物理存储时,
    通常数组(顺序表)链式结构(链表)形式存储
    链式结构逻辑上连续的,但在物理上不一定连续
                     
  • 现实中的结点一般是从堆上申请出来的
                  
  • 从堆上申请的空间,是按照一定的策略来分配的,
    两次申请的空间可能连续,也可能不连续
图示:

                     


                    

(2). 链表的分类:

            

实际链表的结构非常多样
以下情况组合起来就有8种链表结构

           

单向 或 双向 链表

单向链表图示

                

双向链表图示

            

            

---------------------------------------------------------------------------------------------

            

带头(哨兵位) 或 不带头(哨兵位) 链表

带头链表图示

               

不带头链表图示

            

            

---------------------------------------------------------------------------------------------

            

循环 或 非循环 链表

循环链表图示

                  

非循环链表图示

            

            

虽然有这么多的链表的结构,
但是我们实际中最常用还是两种结构

无头单向非循环链表 和 带头双向循环链表

                     


                    

(3). 两种常用链表结构:

            

无头单向非循环链表

简介:

结构简单一般不会单独用来存数据

实际中更多是作为其他数据结构的子结构

哈希桶图的邻接表等等

另外这种结构在笔试面试中出现很多

          

图示:

            

            

---------------------------------------------------------------------------------------------

            

带头双向循环链表

简介:

结构最复杂一般用在单独存储数据

实际中使用的链表数据结构,都是带头双向循环链表

另外这个结构虽然结构复杂

但是使用代码实现以后会发现结构会带来很多优势实现反而简单

          

图示:

         

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

             

2 . 链表的实现
(无头+单向+非循环链表)

(详细解释在图片的注释中,代码分文件放最后)

                  

实现具体功能前的准备工作

  • 创建单链表数据类型 -- 链表结点中数据域里存储的数据的类型
                    
  • 创建单链表结点结构体(类型) -- 结点中应有 数据域 指针域
图示

            

            

---------------------------------------------------------------------------------------------

            

PrintSList函数 -- 遍历打印链表

  • 需要使用链表头指针phead为了后续使用不能改变头指针,所以要代替头指针
                     
  • 因为链表到最后结点里指针域的NULL(0x00000000)才结束
    所以可以使用while循环进行遍历并打印
                  
  • 最后提示已指向NULL指针链表遍历结束
图示

            

            

---------------------------------------------------------------------------------------------

            

SLTNode函数 -- 生成链表的结点

  • 开辟结点所需的动态空间,要注意一个结点大小看数据域和指针域的大小
               
  • 检查空间是否开辟成功
               
  • 将要放入结点数据域的数据放入
               
  • 设置新创建结点的指针域
               
  • 返回创建的新结点的指针(地址)
图示

测试 -- PrintSList、SLTNode函数

            

            

---------------------------------------------------------------------------------------------

            

SLTPushBack函数(难) -- 向链表尾部插入一个结点(尾插)

  • 因为要插入一个结点,所以先使用BuySListNode函数创建一个结点
               
  • 尾插时要判断链表是否已经有结点
    如果没有结点
    将新创建结点的地址赋给头指针
    因为是对指针本身进行操作,所以二级指针对头指针进行操作
               
  • 如果已经有了结点
    先创建一个指针替代头指针
    (这里是对头指针直线的结点结构体进行操作,所以用指针就够了不需要二级指针
    使用while循环获得最后一个结点的地址
    最后将尾插的结点连接起来
图示

(可以结合单链表物理图来理解

↓↓↓↓

测试 -- SLTPushBack函数

            

            

---------------------------------------------------------------------------------------------

            

SLTPushFront函数 -- 向链表头部插入一个结点(头插)

  • 因为要插入一个结点,所以先使用BuySListNode函数创建一个结点
               
  • 使用plist把原本头结点地址赋给新插入头结点指针域
               
  • 再把头指针改为指向新插入头结点
图示

测试 -- SLTPushFront函数

            

            

---------------------------------------------------------------------------------------------

            

SLTPopBack函数 -- 删除链表尾部结点(尾删)

  • 链表尾删考虑三种情况
    第一种情况链表为空,没有任何结点
    这种情况直接assert断言即可
               
  • 第二种情况链表只有一个结点
    这种情况要释放掉唯一的结点
               
  • 第三种情况链表有一个以上结点
    这种情况需要两个指针来进行操作
图示

测试 -- SLTPopBack函数

            

            

---------------------------------------------------------------------------------------------

            

SLTPopFront函数 -- 删除链表头部结点(头删)

  • 头删分两种情况就够了:空链表 非空链表
               
  • 空链表头删
    直接assert断言pass掉
    (莫名戳中笑点哈哈哈哈哈哈哈哈)
               
  • 非空链表头删
    通过plist头指针获得第二个结点地址,方便头删后让原本的第二个结点做新的头结点
    free释放掉头指针
    最后让plist头指针指向原本的第二个结点做新的头结点
图示

测试 -- SLTPopFront函数

            

            

---------------------------------------------------------------------------------------------

            

SLTFind函数 -- 查找指定值(x)所在结点的地址

  • 创建一个变量代替头指针
               
  • 使用while循环在链表中进行遍历查找指定值(x)
图示

测试 -- SLTFind函数

            

            

---------------------------------------------------------------------------------------------

            

SLTInsert函数 -- 在链表指定位置(pos)之前插入一个值(x)

  • 分三种情况讨论
    链表为空(空链表)在第一个结点前插入(头插)非头插
               
  • 链表为空
    直接assert断言pass掉
               
  • 在第一个结点前插入(头插)
    复用头插SLTPushFront函数进行插入
               
  • 非头插
    使用链表头指针找到pos结点的前一个结点地址prev
  • 创建一个新结点newnode
    将新结点newnode插入pos结点之前prev结点之后
图示

测试 -- SLTInsert函数

            

            

---------------------------------------------------------------------------------------------

            

SLTInsertAfter函数 -- 在链表指定位置(pos)之后插入一个值(x)

  • 如果接收的pos结点地址为空无法继续操作
    用assert断言继续处理
               
  • 因为要在链表中插入一个值
    所以要用BuySListNode函数新增一个结点
               
  • 让newnode结点的指针域next指向后一个结点
               
  • 让pos结点指向newnode结点
    将链表连接起来
图示

测试 -- SLTInsertAfter函数

            

            

---------------------------------------------------------------------------------------------

            

SLTErase函数 -- 在链表删除指定位置(pos)结点

  • 防止要删的pos结点指针为空指针
    使用assert断言
               
  • 分两种情况
    头删非头删
               
  • 头删
    直接复用头删SLTPopFront函数
               
  • 非头删
    找到pos结点的前一个结点prev
    prev结点 pos结点后一个结点 连接起来
               
  • 最后释放(删除)pos结点完成删除操作
图示

测试 -- SLTErase函数

            

            

---------------------------------------------------------------------------------------------

            

SLTEraseAfter函数 -- 在链表删除指定位置(pos)之后一个结点

  • 使用assert断言分别排除pos结点指针为空pos结点为尾结点的情况,
    我们是删除pos结点的后一个结点
    如果pos为尾结点,就会无法操作
               
  • 将要删除的pos结点下个结点posNext地址保存起来
               
  • pos结点下下个结点连接起来
               
  • 最后释放(删除)posNext结点
图示

测试 -- SLTEraseAfter函数

            

            

---------------------------------------------------------------------------------------------

            

SLTDestroy函数 -- 销毁链表释放开辟的动态空间

  • 因为链表释放后,还要把头指针plist给置为空
    要操作到plist,所以要用到二级指针
               
  • 进行assert断言
    pphead二级指针不为空
    以上函数中参数带二级指针pphead的都需要进行此操作
               
  • 创建一个指针进行遍历释放空间
               
  • 最后将链表头指针置为空
图示

         

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

             

3 . 对应代码

SList.h -- 单链表头文件

  1. //链表头文件:
  2. #pragma once
  3. //先包含之后要用到的头文件:
  4. #include <stdio.h>
  5. #include <stdlib.h>
  6. #include <assert.h>
  7. //重命名一个单链表数据类型
  8. typedef int SLTDataType;
  9. //SLT -- single link table
  10. //创建一个单链表结点结构体
  11. //这里数据域是int类型4字节,指针域指针也是4个字节,
  12. //所以一个结点就是8个字节
  13. typedef struct SListNode
  14. {
  15. //结点数据域:
  16. SLTDataType data;
  17. //结点指针域:
  18. //因为是指向单链表结点结构体的指针,
  19. //所以是单链表结点结构体类型的指针
  20. struct SListNode* next;
  21. //第一个结点要找到第二个结点,物理空间不连续
  22. //通过上个结点指向下个结点,实现逻辑连续
  23. }SLTNode; //将struct SListNode重命名为SLTNode
  24. //创建遍历打印单链表的函数 -- 接收单链表头指针
  25. void PrintSList(SLTNode* phead);
  26. //创建生成结点的函数 -- 接收要存入结点数据域的数据;返回该结点的指针
  27. SLTNode* BuySListNode(SLTDataType x);
  28. //创建尾插函数 --
  29. //使用二级指针接收单链表头指针的地址 和 接收要插入的值
  30. void SLTPushBack(SLTNode** pphead, SLTDataType x);
  31. //创建头插函数 --
  32. //使用二级指针接收单链表头指针的地址 和 接收要插入的值
  33. void SLTPushFront(SLTNode** pphead, SLTDataType x);
  34. //创建尾删函数 --
  35. //使用二级指针接收单链表头指针的地址
  36. void SLTPopBack(SLTNode** pphead);
  37. //创建头删函数 --
  38. //使用二级指针接收单链表头指针的地址
  39. void SLTPopFront(SLTNode** pphead);
  40. //创建查找函数 --
  41. //查找指定值(x)所在结点的地址
  42. //接收 链表头指针phead、查找值x
  43. SLTNode* SLTFind(SLTNode* phead, SLTDataType x);
  44. //创建指定位置插入函数1 --
  45. //在链表指定位置(pos)之前插入一个值(x)
  46. //接收 链表头指针地址pphead、指定结点指针pos、插入值x
  47. void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
  48. //创建指定位置插入函数2 --
  49. //在链表指定位置(pos)之后插入一个值(x)
  50. //接收 指定结点指针pos、插入值x
  51. void SLTInsertAfter(SLTNode* pos, SLTDataType x);
  52. //创建删除指定结点函数1 --
  53. //在链表删除指定位置(pos)结点
  54. ///接收 链表头指针地址pphead、指定结点指针pos
  55. void SLTErase(SLTNode** pphead, SLTNode* pos);
  56. //创建删除指定结点函数2 --
  57. //在链表删除指定位置(pos)之后一个结点
  58. //接收 指定结点指针pos
  59. void SLTEraseAfter(SLTNode* pos);
  60. //创建销毁链表函数:
  61. void SLTDestroy(SLTNode** pphead);

            

            

---------------------------------------------------------------------------------------------

SList.c -- 单链表函数实现文件

  1. //链表实现文件:
  2. #define _CRT_SECURE_NO_WARNINGS 1
  3. //包含链表头文件:
  4. #include "SList.h"
  5. //创建遍历单链表的函数:接收单链表结点的头指针
  6. void PrintSList(SLTNode* phead)
  7. {
  8. //phead头指针指向第一个结点,
  9. //之后还要多次使用phead头指针,
  10. //所以不要改变phead头指针,
  11. //所以可以创建一个和phead头指针一样类型的指针cur代替phead头指针
  12. SLTNode* cur = phead;
  13. //因为链表到最后结点的指针域为NULL才结束
  14. //所以只要cur不为NULL就继续遍历结点进行打印:
  15. while (cur != NULL)
  16. {
  17. //通过cur当前指向的结点打印cur里数据域的内容:
  18. printf("%d->", cur->data);
  19. //通过结点里指针域指向下一个结点,方便打印下个结点的数据域内容
  20. cur = cur->next;
  21. }
  22. //最后提示已指向NULL指针链表,遍历结束:
  23. printf("NULL\n");
  24. }
  25. //创建生成结点的函数 -- 接收要存入结点数据域的数据;返回该结点的指针
  26. SLTNode* BuySListNode(SLTDataType x)
  27. {
  28. //给结点开辟动态空间(注意开辟空间的大小是SLTNode结点的大小--8个字节)
  29. SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
  30. //返回该结点地址
  31. //检查是否开辟成功:
  32. if (newnode == NULL) //返回空指针,说明开辟失败
  33. {
  34. //打印错误信息:
  35. perror("malloc fail");
  36. //开辟失败直接退出程序:
  37. exit(-1);
  38. }
  39. //将接收的值x赋给该结点的数据域:
  40. newnode->data = x;
  41. //设置新创建结点的指针域:
  42. //因为是最新的结点,即最尾部的结点,
  43. //所以指针域的指针应是NULL(链表末尾结束)
  44. //之后通过指针域连接各个结点的操作要看情况操作,先都初始化为NULL
  45. newnode->next = NULL;
  46. //返回该新结点的指针(地址)
  47. return newnode;
  48. }
  49. //创建尾插函数
  50. //使用二级指针接收单链表头指针的地址 和 接收要插入的值
  51. void SLTPushBack(SLTNode** pphead, SLTDataType x)
  52. {
  53. //改变结构体,要用结构体指针
  54. //改变结构体指针,要用结构体指针的指针(二级指针)
  55. //因为pphead二级指针存储的是plist指针,
  56. //plist指针指向的值可能为空,但指针的地址不能为空
  57. //pphead是plist的地址,正常情况下一定不为空
  58. assert(pphead);
  59. //先使用newnode函数为要尾插的值创建一个结点
  60. //并返回该结点地址
  61. SLTNode* newnode = BuySListNode(x);
  62. //判断phead是否是NULL,如果是说明链表还没开辟过一个结点
  63. if (*pphead == NULL)
  64. {
  65. //如果为空,则将上面开辟的newnode结点地址赋给phead
  66. *pphead = newnode;
  67. //要改变结构体的指针,就要用二级指针
  68. //这里pphead是二级指针,存放链表头指针plist的地址
  69. //因为如果用一级指针SLTNode* phead的话,
  70. //phead形参只是plist实参的临时拷贝,两者空间相互独立
  71. //改变phead的话不会改变plist,plist还会是空指针
  72. //所以要用二级指针pphead存放plist指针的地址,
  73. //*pphead解引用就能得到一级指针plist,
  74. //即 *pphead = plist
  75. //所以实际上上面的代码就相当于:plist = newnode
  76. //这样就让本来指向空指针的头指针指向了新创建的结点指针
  77. //链表就能够连接起来
  78. }
  79. else
  80. //不为空则往尾部插入新结点:
  81. {
  82. //创建另一个指针存放单链表头指针
  83. SLTNode* tail = *pphead; //*pphead == plist
  84. //使用while循环获得最后一个结点的地址
  85. while (tail->next != NULL)
  86. //如果下个结点的next指针域不为NULL,
  87. //则继续往后找,直到tail等于最后结点的地址
  88. {
  89. //把当前结点指针域里下个结点的地址给到tail,
  90. //进行while循环下次判断:
  91. tail = tail->next;
  92. }
  93. //tail找到最后结点地址后,
  94. //把尾插的新结点地址给到tail的next指针域,
  95. //连接起链表:
  96. tail->next = newnode;
  97. //要改变结构体,用结构体的指针即可
  98. //因为newnode、tail、pphead都是临时(局部)变量,
  99. //所以函数运行结束后都会销毁,
  100. //但malloc函数开辟的空间(结点)都在堆上不会销毁
  101. //通过解引用二级指针pphead改变plist也没有问题
  102. }
  103. }
  104. //创建头插函数 --
  105. //使用二级指针接收单链表头指针的地址 和 接收要插入的值
  106. void SLTPushFront(SLTNode** pphead, SLTDataType x)
  107. {
  108. //因为pphead二级指针存储的是plist指针,
  109. //plist指针指向的值可能为空,但指针的地址不能为空
  110. //pphead是plist的地址,正常情况下一定不为空
  111. assert(pphead);
  112. //先使用newnode函数为要头插的值创建一个结点
  113. //并返回该结点地址
  114. SLTNode* newnode = BuySListNode(x);
  115. //因为也要用到链表头指针本身,所以也要使用二级指针
  116. //因为plist指向原本头结点地址,
  117. //所以可以使用plist把原本头结点地址赋给新插入头结点指针域:
  118. newnode->next = *pphead;
  119. //再把头指针改为指向新插入头结点:
  120. *pphead = newnode;
  121. }
  122. //创建尾删函数 --
  123. //使用二级指针接收单链表头指针的地址
  124. void SLTPopBack(SLTNode** pphead)
  125. {
  126. //尾删需要考虑三种情况:
  127. //注:*pphead 就是 plist
  128. //因为pphead二级指针存储的是plist指针,
  129. //plist指针指向的值可能为空,但指针的地址不能为空
  130. //pphead是plist的地址,正常情况下一定不为空
  131. assert(pphead);
  132. // 第一种情况:链表为空 -- 没有任何结点
  133. //没有结点那就删不了了,使用assert接收到空指针就报警告
  134. assert(*pphead);
  135. // 第二种情况:链表只有一个结点
  136. if ((*pphead)->next == NULL)
  137. // -> 也是解引用(结构体的),优先级比 * 高
  138. //所以 *pphead 要用() 括起来
  139. {
  140. //只有一个结点,又要尾删,那就直接把这唯一的结点删掉:
  141. free(*pphead); //直接free释放掉plist指向的结点
  142. //再把释放掉的plist置为空指针,防止成为野指针:
  143. *pphead = NULL;
  144. }
  145. else
  146. // 第三种情况:链表有一个以上结点
  147. {
  148. //这种情况额外两个指针,
  149. //一个tail指针 -- 用来找到最后一个结点地址并将其释放,
  150. //还有一个tailPrev指针 -- 指向tail指针的前一个结点地址,
  151. //用来改变该结点的指针域,
  152. //防止原本指向tail结点的指针域变成野指针
  153. //先替代头指针plist:
  154. SLTNode* tail = *pphead;
  155. //创建tailPrev指针,
  156. //之后操作会指向tail结点的前一个结点,
  157. //即倒数第二个结点
  158. SLTNode* tailPrev = NULL;
  159. //再使用while循环找到尾结点:
  160. //和尾插的操作类似
  161. while (tail->next != NULL)
  162. {
  163. //tail查找之前先把当前指向结点地址给tailPrev
  164. //这样最后tail会指向尾结点,
  165. //tailPrev会指向倒数第二个结点
  166. tailPrev = tail;
  167. tail = tail->next;
  168. }
  169. //结束while循环后tail指向尾结点地址,
  170. //因为是尾删,所以free释放掉tail就可以“删掉”尾结点
  171. free(tail);
  172. //因为tail是局部(临时)变量,出了函数就销毁,
  173. //所以不置为指针也可以,不用担心成为野指针
  174. //删除原尾结点后,倒数第二个结点成为尾结点,
  175. //要把其指针域next置为空指针,成为链表新结尾
  176. tailPrev->next = NULL;
  177. }
  178. }
  179. //创建头删函数 --
  180. //使用二级指针接收单链表头指针的地址
  181. void SLTPopFront(SLTNode** pphead)
  182. {
  183. //因为pphead二级指针存储的是plist指针,
  184. //plist指针指向的值可能为空,但指针的地址不能为空
  185. //pphead是plist的地址,正常情况下一定不为空
  186. assert(pphead);
  187. //分两种情况:
  188. //第一种情况:链表里没有结点(空链表)
  189. //没有结点可以删,直接assert断言pass掉:
  190. assert(*pphead);
  191. //第二种情况:链表有一个和多个结点(非空链表)
  192. //因为是删掉头结点,所以不用考虑找尾结点
  193. //所以不用细分一个或多个结点的情况:
  194. //这种情况要先通过plist拿到第二个结点的地址:
  195. SLTNode* newhead = (*pphead)->next;
  196. //使用newhead存储第二个结点地址
  197. //拿到第二个结点地址后,再释放掉头结点,
  198. //实现头删效果:
  199. free(*pphead);
  200. //这时要让第二个结点成为新的头结点:
  201. *pphead = newhead;
  202. //头指针指向原本的第二个结点
  203. }
  204. //创建查找函数 --
  205. //查找指定值(x)所在结点的地址
  206. //接收 链表头指针phead、查找值x
  207. SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
  208. {
  209. //像SLTFind和PrintSList函数只用头指针遍历
  210. //不改变指针本身就不需要用到二级指针
  211. //创建个变量代替头指针:
  212. SLTNode* cur = phead;
  213. //使用while循环对链表进行遍历查找:
  214. while (cur != NULL)
  215. //只要cur指针指向地址不为空就继续循环
  216. //while遍历时,(cur->next != NULL) 和 (cur != NULL) 的区别:
  217. //(cur->next != NULL):这个条件最后cur会是最后结点的地址
  218. //(cur != NULL):这个条件最后cur会是NULL
  219. //(cur->next != NULL) 会比 (cur != NULL) 少一次循环
  220. {
  221. //遍历过程中依次查找各结点数据域数据是否与x相同:
  222. if (cur->data == x)
  223. {
  224. //找到了一个结点数据域数据是x,返回该结点地址
  225. return cur;
  226. }
  227. //这里如果while循环的条件是(cur->next != NULL)
  228. //最后一个结点进不来,不能判断最后结点数据域数据是不是x
  229. cur = cur->next; //改变循环条件,指向下个结点
  230. }
  231. //如果能指向到这说明没找到,返回NULL:
  232. return NULL;
  233. }
  234. //创建指定位置插入函数1 --
  235. //在链表指定位置(pos)之前插入一个值(x)
  236. //接收 链表头指针地址pphead、指定结点指针pos、插入值x
  237. void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
  238. {
  239. //因为pphead二级指针存储的是plist指针,
  240. //plist指针指向的值可能为空,但指针的地址不能为空
  241. //pphead是plist的地址,正常情况下一定不为空
  242. assert(pphead);
  243. //第一种情况:空指针
  244. //先排除空指针的情况:
  245. assert(pos);
  246. //第二种情况:头插
  247. if (pos == *pphead)
  248. // *pphead == plist
  249. //如果pos是pphead即第一个指针,
  250. //则在第一个指针前插入一个值,相当于头插
  251. {
  252. //直接复用头插函数SLTPustFront:
  253. SLTPushFront(pphead, x);
  254. //直接传pphead二级指针过去
  255. }
  256. else
  257. //第三种情况:非头插
  258. {
  259. //从头开始找pos结点的前一个结点:
  260. //先获得头指针
  261. SLTNode* prev = *pphead;
  262. //当前结点的指针域不是指向pos结点则继续循环
  263. while (prev->next != pos)
  264. {
  265. prev = prev->next;
  266. }
  267. //此时prev已指向pos结点的前一个结点
  268. //为要插入的结点创建一个结点newnode:
  269. //插入位置是pos结点之前,prev结点之后
  270. SLTNode* newnode = BuySListNode(x);
  271. //让prev结点指针域指向新插入结点地址:
  272. prev->next = newnode;
  273. //newnode结点指针域指向pos结点:
  274. newnode->next = pos;
  275. //此时newnode新结点就插入完成了
  276. }
  277. }
  278. //创建指定位置插入函数2 --
  279. //在链表指定位置(pos)之后插入一个值(x)
  280. //接收 指定结点指针pos、插入值x
  281. void SLTInsertAfter(SLTNode* pos, SLTDataType x)
  282. {
  283. //因为是在pos结点后插入一个值(结点)
  284. //所以不可能会有头插的情况,那就不需要头指针plist
  285. //pos指针存储结点地址,可能会接收到空指针
  286. //使用assert断言pass掉
  287. assert(pos);
  288. //先为插入值创建一个新结点newnode:
  289. SLTNode* newnode = BuySListNode(x);
  290. //先让newnode的指针域next指向后一个结点:
  291. //这里后一个结点就是pos结点指针域里的地址
  292. //因为未插入前pos就是指向后一个地址
  293. newnode->next = pos->next;
  294. //再让pos的指针域next指向newnode:
  295. pos->next = newnode;
  296. }
  297. //创建删除指定结点函数1 --
  298. //在链表删除指定位置(pos)结点
  299. ///接收 链表头指针地址pphead、指定结点指针pos
  300. void SLTErase(SLTNode** pphead, SLTNode* pos)
  301. {
  302. //因为pphead二级指针存储的是plist指针,
  303. //plist指针指向的值可能为空,但指针的地址不能为空
  304. //pphead是plist的地址,正常情况下一定不为空
  305. assert(pphead);
  306. //防止要删的pos结点指针为空指针
  307. //使用assert断言:
  308. assert(pos);
  309. //分两种情况:
  310. // 1.头删:
  311. if (pos == *pphead)
  312. //pos结点 == 头结点 --> 头删
  313. {
  314. //直接复用SLTPopFront头删函数:
  315. SLTPopFront(pphead);
  316. }
  317. else
  318. // 2.非头删:
  319. //尾删不用单独分出一种情况,因为还得判断是不是尾结点
  320. //直接用通用逻辑删除也可以处理尾删的情况
  321. {
  322. //从头开始找pos结点的前一个结点:
  323. //先获得头指针
  324. SLTNode* prev = *pphead;
  325. //当前结点的指针域不是指向pos结点则继续循环
  326. while (prev->next != pos)
  327. {
  328. prev = prev->next;
  329. }
  330. //此时prev已指向pos结点的前一个结点
  331. //因为要删除pos结点,
  332. //所以要让pos前一个和后一个结点连接起来:
  333. prev->next = pos->next;
  334. //连接成功后再把pos结点释放,实现删除效果:
  335. free(pos);
  336. }
  337. }
  338. //创建删除指定结点函数2 --
  339. //在链表删除指定位置(pos)之后一个结点
  340. //接收 指定结点指针pos
  341. void SLTEraseAfter(SLTNode* pos)
  342. {
  343. //删除pos结点后的一个结点,
  344. //那么就不可能出现头删的情况,
  345. //pos结点是尾结点也没用,
  346. //因为尾结点后就没有结点可以删了
  347. //使用assert断言处理:
  348. assert(pos); //防止接收“空结点”
  349. assert(pos->next); //防止接收尾结点
  350. //将要删的pos结点的下个结点posNext先保存起来:
  351. SLTNode* posNext = pos->next;
  352. //再把pos结点和下下个结点连接起来:
  353. pos->next = posNext->next;
  354. //posNext的指针域next就是pos结点的下下个结点地址
  355. //最后释放(删除)posNext结点:
  356. free(posNext);
  357. }
  358. //创建销毁链表函数:
  359. void SLTDestroy(SLTNode** pphead)
  360. {
  361. //因为链表释放后,还要把头指针plist给置为空,
  362. //要操作到plist,所以要用到二级指针:
  363. //plist指向空链表,pphead也不能为空:
  364. assert(pphead);
  365. //创建一个指针进行遍历:
  366. SLTNode* cur = *pphead;//*pphead == plist
  367. //进行遍历释放:
  368. while (cur != NULL)
  369. {
  370. //创建一个指针保存下个结点地址:
  371. SLTNode* next = cur->next;
  372. //释放当前结点:
  373. free(cur);
  374. //cur指针移向下一个结点:
  375. cur = next;
  376. }
  377. //将链表头指针置为空:
  378. *pphead = NULL;
  379. }

            

            

---------------------------------------------------------------------------------------------

test.c -- 单链表测试文件

  1. //链表测试文件:
  2. #define _CRT_SECURE_NO_WARNINGS 1
  3. /* 链表学习引入小测试:
  4. #include <stdio.h>
  5. //重命名一个单链表数据类型
  6. typedef int SLTDataType;
  7. //SLT -- single link table
  8. //创建一个单链表结点结构体
  9. typedef struct SListNode
  10. {
  11. //结点数据域:
  12. SLTDataType data;
  13. //结点指针域:
  14. //因为是指向单链表结点结构体的指针,
  15. //所以是单链表结点结构体类型的指针
  16. struct SListNode* next;
  17. }SLTNode; //将struct SListNode重命名为SLTNode
  18. //创建遍历单链表的函数:接收单链表结点的头指针
  19. void PrintSList(SLTNode* phead)
  20. {
  21. //phead头指针指向第一个结点,
  22. //之后还要多次使用phead头指针,
  23. //所以不要改变phead头指针,
  24. //所以可以创建一个和phead头指针一样类型的指针cur代替phead头指针
  25. SLTNode* cur = phead;
  26. //因为链表到最后结点的指针域为NULL才结束
  27. //所以只要cur不为NULL就继续遍历结点进行打印:
  28. while (cur != NULL)
  29. {
  30. //通过cur当前指向的结点打印cur里数据域的内容:
  31. printf("%d->", cur->data);
  32. //通过结点里指针域指向下一个结点,方便打印下个结点的数据域内容
  33. cur = cur->next;
  34. }
  35. //最后提示已指向NULL指针链表,遍历结束:
  36. printf("NULL\n");
  37. }
  38. int main()
  39. {
  40. //创建单链表结点:
  41. SLTNode* n1 = (SLTNode*)malloc(sizeof(SLTNode));
  42. n1->data = 10; //在该结点数据域存放数据
  43. SLTNode* n2 = (SLTNode*)malloc(sizeof(SLTNode));
  44. n2->data = 20; //在该结点数据域存放数据
  45. SLTNode* n3 = (SLTNode*)malloc(sizeof(SLTNode));
  46. n3->data = 30; //在该结点数据域存放数据
  47. n1->next = n2; //n1的指针域指向结点n2
  48. n2->next = n3; //n2的指针域指向结点n3
  49. n3->next = NULL; //n3的指针域指向NULL(结束)
  50. PrintSList(n1); //调用函数打印单链表
  51. return 0;
  52. }
  53. */
  54. //包含链表头文件:
  55. #include "SList.h"
  56. //测试函数1:测试--PrintSList、BuySListNode函数
  57. void TestList1()
  58. {
  59. int n; //存放链表长度
  60. //打印提示信息:
  61. printf("请输入链表的长度:>");
  62. //接收链表长度
  63. scanf("%d", &n);
  64. //打印提示信息:
  65. printf("\n请依次输入每个结点的值:>");
  66. SLTNode* plist = NULL; //链表头指针,一开始链表没数据所以为NULL
  67. //使用for循环,循环创建结点并赋值,形成链表:
  68. for (int i = 0; i < n; i++)
  69. {
  70. int val; //存放结点数据域数据
  71. scanf("%d", &val); //接收结点数据域数据
  72. //使用BuySListNode函数创建结点并给数据域赋值:
  73. SLTNode* newnode = BuySListNode(val);
  74. //头插: 使用头插把链表连接起来
  75. //把链表头指针plist指向的头结点地址赋给新创建结点的指针域next
  76. //这样新结点的指针域next就能指向原来的头结点地址
  77. newnode->next = plist;
  78. //再把新创建结点的地址赋给头指针,
  79. //这样头指针就指向了新创建结点地址,让其成为新的头结点
  80. plist = newnode;
  81. }
  82. //使用PrintSList函数打印链表
  83. PrintSList(plist); //接收头指针后打印
  84. //使用SLTPushBack函数手动向链表尾部插入数据(尾插):
  85. SLTPushBack(plist, 10000);
  86. //再使用PrintSList函数打印插入后的链表
  87. PrintSList(plist);
  88. //plist和phead都是单链表头指针,
  89. //但 plist是实参 phead是形参
  90. //phead 是 plist 的一份临时拷贝
  91. }
  92. //测试函数2:测试--SLTPushBack、SLTPushFront函数
  93. void TestList2()
  94. {
  95. //创建单链表头指针:
  96. SLTNode* plist = NULL;
  97. //使用尾插随机插入几个值:
  98. //(此时头指针指向NULL还没有开辟空间创建结点)
  99. SLTPushBack(&plist, 1);
  100. SLTPushBack(&plist, 2);
  101. SLTPushBack(&plist, 3);
  102. SLTPushBack(&plist, 4);
  103. SLTPushBack(&plist, 5);
  104. //使用头插随机插入几个值:
  105. SLTPushFront(&plist, 10);
  106. SLTPushFront(&plist, 20);
  107. SLTPushFront(&plist, 30);
  108. SLTPushFront(&plist, 40);
  109. //使用SLTPrintf函数打印该链表:
  110. PrintSList(plist);
  111. }
  112. //测试函数3:测试--SLTPopBack(尾删)函数
  113. void TestList3()
  114. {
  115. //创建单链表头指针:
  116. SLTNode* plist = NULL;
  117. //使用尾插随机插入几个值:
  118. //(此时头指针指向NULL还没有开辟空间创建结点)
  119. SLTPushBack(&plist, 1);
  120. SLTPushBack(&plist, 2);
  121. SLTPushBack(&plist, 3);
  122. //使用SLTPrintf函数打印该链表:
  123. printf("尾删前链表:\n");
  124. PrintSList(plist);
  125. //使用尾删函数:
  126. SLTPopBack(&plist);
  127. //使用SLTPrintf函数打印该链表:
  128. printf("尾删后链表:\n");
  129. PrintSList(plist);
  130. SLTPopBack(&plist);
  131. //使用SLTPrintf函数打印该链表:
  132. printf("尾删后链表:\n");
  133. PrintSList(plist);
  134. SLTPopBack(&plist);
  135. //使用SLTPrintf函数打印该链表:
  136. printf("尾删后链表:\n");
  137. PrintSList(plist);
  138. SLTPopBack(&plist);
  139. //使用SLTPrintf函数打印该链表:
  140. printf("尾删后链表:\n");
  141. PrintSList(plist);
  142. //使用SLTPrintf函数打印该链表:
  143. printf("尾删后链表:\n");
  144. PrintSList(plist);
  145. }
  146. //测试函数4:测试--SLTPopFront(头删)函数
  147. void TestList4()
  148. {
  149. //创建单链表头指针:
  150. SLTNode* plist = NULL;
  151. //使用尾插随机插入几个值:
  152. //(此时头指针指向NULL还没有开辟空间创建结点)
  153. SLTPushBack(&plist, 1);
  154. SLTPushBack(&plist, 2);
  155. SLTPushBack(&plist, 3);
  156. //使用SLTPrintf函数打印该链表:
  157. printf("头删前链表:\n");
  158. PrintSList(plist);
  159. SLTPopFront(&plist);
  160. //使用SLTPrintf函数打印该链表:
  161. printf("头删后链表:\n");
  162. PrintSList(plist);
  163. SLTPopFront(&plist);
  164. //使用SLTPrintf函数打印该链表:
  165. printf("头删后链表:\n");
  166. PrintSList(plist);
  167. SLTPopFront(&plist);
  168. //使用SLTPrintf函数打印该链表:
  169. printf("头删后链表:\n");
  170. PrintSList(plist);
  171. SLTPopFront(&plist);
  172. //使用SLTPrintf函数打印该链表:
  173. printf("头删后链表:\n");
  174. PrintSList(plist);
  175. }
  176. //测试函数5:测试 -- SLTFind函数
  177. void TestList5()
  178. {
  179. //创建单链表头指针:
  180. SLTNode* plist = NULL;
  181. //使用尾插随机插入几个值:
  182. //(此时头指针指向NULL还没有开辟空间创建结点)
  183. SLTPushBack(&plist, 1);
  184. SLTPushBack(&plist, 2);
  185. SLTPushBack(&plist, 3);
  186. //使用SLTPrintf函数打印该链表:
  187. printf("操作前链表:\n");
  188. PrintSList(plist);
  189. //用SLTFind函数查找链表中数据域为3的结点的地址
  190. SLTNode* pos = SLTFind(plist, 1);
  191. if (pos != NULL)
  192. {
  193. //找到了可以对该结点进行修改
  194. pos->data *= 10;
  195. //所以SLTFind查找函数可以负责查找和修改的操作
  196. }
  197. printf("操作后链表:\n");
  198. PrintSList(plist);
  199. }
  200. //测试函数6:测试 -- SLTInsert函数
  201. void TestList6()
  202. {
  203. //创建单链表头指针:
  204. SLTNode* plist = NULL;
  205. //使用尾插随机插入几个值:
  206. //(此时头指针指向NULL还没有开辟空间创建结点)
  207. SLTPushBack(&plist, 1);
  208. SLTPushBack(&plist, 2);
  209. SLTPushBack(&plist, 3);
  210. //使用SLTPrintf函数打印该链表:
  211. printf("操作前链表:\n");
  212. PrintSList(plist);
  213. //用SLTFind函数查找链表中数据域为3的结点的地址
  214. SLTNode* pos = SLTFind(plist, 2);
  215. if (pos != NULL)
  216. {
  217. int x; //接收要插入的值
  218. scanf("%d", &x); //输入要插入的值
  219. SLTInsert(&plist, pos, x); //在2前面插入x
  220. }
  221. printf("操作后链表:\n");
  222. PrintSList(plist);
  223. }
  224. //测试函数7:测试 -- SLTInsertAfter函数
  225. void TestList7()
  226. {
  227. //创建单链表头指针:
  228. SLTNode* plist = NULL;
  229. //使用尾插随机插入几个值:
  230. //(此时头指针指向NULL还没有开辟空间创建结点)
  231. SLTPushBack(&plist, 1);
  232. SLTPushBack(&plist, 2);
  233. SLTPushBack(&plist, 3);
  234. //使用SLTPrintf函数打印该链表:
  235. printf("操作前链表:\n");
  236. PrintSList(plist);
  237. //用SLTFind函数查找链表中数据域为3的结点d的地址
  238. SLTNode* pos = SLTFind(plist, 2);
  239. if (pos != NULL)
  240. {
  241. int x; //接收要插入的值
  242. scanf("%d", &x); //输入要插入的值
  243. SLTInsertAfter(pos, x); //在2后面插入x
  244. }
  245. printf("操作后链表:\n");
  246. PrintSList(plist);
  247. }
  248. //测试函数8:测试 -- SLTErase函数
  249. void TestList8()
  250. {
  251. //创建单链表头指针:
  252. SLTNode* plist = NULL;
  253. //使用尾插随机插入几个值:
  254. //(此时头指针指向NULL还没有开辟空间创建结点)
  255. SLTPushBack(&plist, 1);
  256. SLTPushBack(&plist, 2);
  257. SLTPushBack(&plist, 3);
  258. //使用SLTPrintf函数打印该链表:
  259. printf("操作前链表:\n");
  260. PrintSList(plist);
  261. //用SLTFind函数查找链表中数据域为3的结点d的地址
  262. SLTNode* pos = SLTFind(plist, 2);
  263. if (pos != NULL)
  264. {
  265. int x; //接收要插入的值
  266. scanf("%d", &x); //输入要插入的值
  267. SLTErase(&plist, pos); //删除pos所在结点
  268. //pos结点指针删除(释放)后,要将其置为空:
  269. pos = NULL;
  270. }
  271. printf("操作后链表:\n");
  272. PrintSList(plist);
  273. }
  274. //测试函数9:测试 -- SLTEraseAfter函数
  275. void TestList9()
  276. {
  277. //创建单链表头指针:
  278. SLTNode* plist = NULL;
  279. //使用尾插随机插入几个值:
  280. //(此时头指针指向NULL还没有开辟空间创建结点)
  281. SLTPushBack(&plist, 1);
  282. SLTPushBack(&plist, 2);
  283. SLTPushBack(&plist, 3);
  284. //使用SLTPrintf函数打印该链表:
  285. printf("操作前链表:\n");
  286. PrintSList(plist);
  287. //用SLTFind函数查找链表中数据域为3的结点d的地址
  288. SLTNode* pos = SLTFind(plist, 2);
  289. if (pos != NULL)
  290. {
  291. int x; //接收要插入的值
  292. scanf("%d", &x); //输入要插入的值
  293. SLTEraseAfter(pos); //删除pos结点的下个结点
  294. //pos结点指针删除(释放)后,要将其置为空:
  295. pos = NULL;
  296. }
  297. printf("操作后链表:\n");
  298. PrintSList(plist);
  299. }
  300. //测试函数10:测试 -- SLTDestroy函数
  301. void TestList10()
  302. {
  303. //创建单链表头指针:
  304. SLTNode* plist = NULL;
  305. //使用尾插随机插入几个值:
  306. //(此时头指针指向NULL还没有开辟空间创建结点)
  307. SLTPushBack(&plist, 1);
  308. SLTPushBack(&plist, 2);
  309. SLTPushBack(&plist, 3);
  310. //使用SLTPrintf函数打印该链表:
  311. printf("操作前链表:\n");
  312. PrintSList(plist);
  313. SLTDestroy(&plist);
  314. }
  315. //主函数:
  316. int main()
  317. {
  318. //TestList1();
  319. //TestList2();
  320. //TestList3();
  321. //TestList4();
  322. //TestList5();
  323. //TestList6();
  324. //TestList7();
  325. //TestList8();
  326. //TestList9();
  327. TestList10();
  328. return 0;
  329. }
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Monodyee/article/detail/670117
推荐阅读
相关标签
  

闽ICP备14008679号