• 【数据结构初阶】三、 线性表里的链表(无头+单向+非循环链表)


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

    相关代码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
    5. #include
    6. #include
    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
    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. }
  • 相关阅读:
    7-3 LVS+Keepalived集群叙述与部署
    「重启程序」的正面和反面
    49从零开始用Rust编写nginx,我竟然在同一个端口上绑定了多少IP
    Golang中的包和模块设计
    Spring面试题15:Spring支持几种bean的作用域?singleton、prototype、request的区别是什么?
    第10章 MySQL(二)
    Python编程 元组的创建
    拯救SQL Server数据库事务日志文件损坏的终极大招
    使用Element进行前端开发
    Jetson NX安装opencv3.2.0报错及问题汇总
  • 原文地址:https://blog.csdn.net/weixin_63176266/article/details/132781490