• [数据结构]单链表(从0->1)


    前言

    小蜗牛向前冲

    名言我可以接收失败,但我不能接收放弃

     如果觉的博主的文章还不错的话,还点赞,收藏,关注👀支持博主。如果发现有问题的地方欢迎❀大家在评论区指正。

    目录

    一 顺序表的不足之处

    二 链表 

    三 单链表的实现

    1 创建三个文件

    2 SList.h

    3 SList.c


     在前面我们学习了,顺序表(一个连续存储的物理单元,是用数组实现)实现了对数据的增删查改,我们本期将来学习单链表(也实现对数据的增删查改),既然都是对数据增删查改,为什么我们不用顺序表而要弄出个个单链表呢?这就不得不提顺序表的缺陷。

    一 顺序表的不足之处

    对于顺序表的不足,我们主要从时间复杂度内存的浪费上考虑。

    1. 中间/头部的插入删除,时间复杂度为O(N)

    2. 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。

    3. 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到 200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。

    二 链表 

    概念:

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

     1 链表的分类

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

    单向或者双向

     

    带头或者不带头

     循环或者非循环

     

     1. 无头单向非循环链表结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。

    2. 带头双向循环链表结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了。

    下面我们主要研究无头单向非循环链表。

    2 单链表

     单链表结构简单,但是又是非常重要的,从图中我们可以看出。

    1 从逻辑是单链表是连续的,但是在物理层面是非连续的

    2 链表的节点,一般都是从堆区申请的

    为了更好的理解单链表,我们将他的功能一一实现。

    三 单链表的实现

    1 创建三个文件

    1 SList.h

    在该文件中我们完成,单表的类型定义接口函数的声明引用的头文件

    2 SList.c

    完成单链表表接口函数的实现

    3 Test.c

    主函数测试顺序表各接口的功能 

    2 SList.h

    1. #pragma once//防止重复头文件被包含
    2. #include
    3. #include
    4. #include
    5. typedef int SLTDataType;//定义节点中数据类型
    6. typedef struct SListNode
    7. {
    8. SLTDataType data;//指向节点中的数据
    9. struct SListNode* next;//指向下个节点的指针
    10. }SListNode;//注意这里重命名不能在结构体中中生效
    11. //打印单链表
    12. void SListPrint(SListNode* phead);
    13. //申请新节点
    14. SListNode* BuySLTNode(SLTDataType x);
    15. //单链表销毁
    16. void SListDestory(SListNode** pphead);
    17. //单链表头插
    18. void SListPushFort(SListNode** pphead, SLTDataType x);
    19. //单链表头删
    20. void SListPopFort(SListNode** pphead);
    21. //单链表尾插
    22. void SListPushBack(SListNode** pphead, SLTDataType x);
    23. //单链表尾删除
    24. void SListPopBack(SListNode** pphead);
    25. //单链表查找
    26. SListNode* SListFind(SListNode* phead, SLTDataType x);
    27. //在pos之前插入
    28. void SListInsert(SListNode** pphead, SListNode* pos, SLTDataType x);
    29. //在pos后插入
    30. void SListInsertAfter(SListNode* pos, SLTDataType x);

     这里和顺序表一样就是要定义函数实现单链表的功能。

    这里我们重点关注,结构体的定义

    1. typedef int SLTDataType;//定义节点中数据类型
    2. typedef struct SListNode
    3. {
    4. SLTDataType data;//指向节点中的数据
    5. struct SListNode* next;//指向下个节点的指针
    6. }SListNode;//注意这里重命名不能在结构体中中生效

    一个节点中主要包括存储的数据指向下个节点的指针 

    3 SList.c

    1. #define _CRT_SECURE_NO_WARNINGS
    2. #include"SList.h"
    3. //打印单链表
    4. void SListPrint(SListNode* phead)
    5. {
    6. SListNode* cur = phead;//保存单链表头的地址
    7. while (cur)
    8. {
    9. printf("%d->", cur->data);//打印链表数据
    10. cur = cur->next;//保存指向下个节点的地址
    11. }
    12. printf("NULL\n");//链表最后指向空
    13. }
    14. //申请新节点
    15. SListNode* BuySLTNode(SLTDataType x)
    16. {
    17. SListNode* NewNode = (SListNode*)malloc(sizeof(SListNode));//为新节点的数据申请空间
    18. if (NewNode==NULL)
    19. {
    20. perror("malloc fail");//申请失败报错
    21. exit(-1);
    22. }
    23. NewNode->data = x;//将为新节点插入数据
    24. NewNode->next = NULL;//将节点指向下个位置的地址置空
    25. return NewNode;
    26. }
    27. //链表销毁
    28. void SListDestory(SListNode** pphead)
    29. {
    30. assert(pphead);//断言(plist地址肯定不为空)
    31. SListNode* cur = *pphead;//保存好链表头的地址
    32. while (cur)
    33. {
    34. SListNode* next = cur->next;//将指向一个链的地址记录下来
    35. free(cur);//释放头的空间
    36. cur = next;//指向一个链
    37. }
    38. *pphead = NULL;//销毁完毕,单链表内容为空
    39. }
    40. //单链表头插
    41. void SListPushFort(SListNode** pphead,SLTDataType x)
    42. {
    43. assert(pphead);//断言(plist地址肯定不为空)
    44. SListNode* NewNode = BuySLTNode(x);//申请一个节点
    45. NewNode->next = *pphead;
    46. *pphead = NewNode;
    47. }
    48. //单链表头删
    49. void SListPopFort(SListNode** pphead)
    50. {
    51. assert(pphead);//断言(plist地址肯定不为空)
    52. //温柔的检测
    53. if (*pphead == NULL)//链表为空就不删除
    54. {
    55. return;
    56. }
    57. //暴力检测
    58. //assert(*pphead!=NULL);
    59. SListNode* del = *pphead;//将要删除地址给del
    60. *pphead = (*pphead)->next;//plist指向下个链表的头
    61. free(del);//释放要删除的空间
    62. del = NULL;
    63. }
    64. //单链表尾插
    65. void SListPushBack(SListNode** pphead, SLTDataType x)
    66. {
    67. assert(pphead);//断言
    68. SListNode* NewNode = BuySLTNode(x);//申请一个节点
    69. //1链表为空
    70. //2联邦不为空
    71. if (*pphead == NULL)
    72. {
    73. *pphead = NewNode;
    74. }
    75. else
    76. {
    77. SListNode* tail = *pphead;//记录链表头的地址
    78. //找尾
    79. while (tail->next != NULL)
    80. {
    81. tail = tail->next;//不断的指向下个链
    82. }
    83. tail->next = NewNode;//尾插入数据
    84. }
    85. }
    86. //单链表尾删除
    87. void SListPopBack(SListNode** pphead)
    88. {
    89. assert(pphead);//断言(plist地址肯定不为空)
    90. //温柔的检测
    91. if (*pphead == NULL)//链表为空就不删除
    92. {
    93. return;
    94. }
    95. if ((*pphead)->next == NULL)//一个节点
    96. {
    97. free(*pphead);
    98. *pphead = NULL;
    99. }
    100. else//多个节点
    101. {
    102. SListNode* tail = *pphead;//记录链表头的地址
    103. SListNode* prev = NULL;//置空
    104. //找尾
    105. while (tail->next != NULL)
    106. {
    107. prev = tail;//保存上个节点的地址
    108. tail = tail->next;//不断的指向下个节点
    109. }
    110. prev->next = NULL;//将上个节点的位置置空
    111. free(tail);//释放
    112. tail = NULL;
    113. }
    114. }
    115. //单链表查找
    116. SListNode* SListFind(SListNode* phead, SLTDataType x)
    117. {
    118. SListNode* cur = phead;//标记头节点
    119. //遍历链表
    120. while (cur)
    121. {
    122. if (cur->data == x)
    123. {
    124. return cur;//找到返回标记点
    125. }
    126. cur = cur->next;
    127. }
    128. return NULL;//没找到返回NULL
    129. }
    130. //在pos之前插入
    131. void SListInsert(SListNode** pphead, SListNode* pos, SLTDataType x)
    132. {
    133. assert(pphead);//断言
    134. assert(pos);
    135. if (pos == *pphead)//第一个节点就是pos
    136. {
    137. SListPushFort(pphead, x);//直接头插
    138. }
    139. else//第一个节点不为pos
    140. {
    141. SListNode* prev = *pphead;
    142. while (prev->next != pos)
    143. {
    144. prev = prev->next;//不断指向下个节点
    145. // 暴力检查,pos不在链表中.prev为空,还没有找到pos,说明pos传错了
    146. assert(prev);
    147. }
    148. SListNode* NewNode = BuySLTNode(x);//申请新的节点
    149. prev->next = NewNode;//上个节点的next指向新节点
    150. NewNode->next = pos;//新节点指向pos;
    151. }
    152. }
    153. //在pos后插入
    154. void SListInsertAfter(SListNode* pos, SLTDataType x)
    155. {
    156. assert(pos);//断言
    157. //直接插入就行了
    158. SListNode* NewNode = BuySLTNode(x);//申请新的节点
    159. NewNode->next = pos->next;//指向pos后的节点
    160. pos->next = NewNode;//节点pos指向新节点
    161. }

    在这里我们主要完成对单链表增删查改函数接口的实现。

    我们考虑到要观察储存在单链表中的数据,所以我们首先要实现一个打印单链表的函数。

    打印单链表

    1. //打印单链表
    2. void SListPrint(SListNode* phead)
    3. {
    4. SListNode* cur = phead;//保存单链表头的地址
    5. while (cur)
    6. {
    7. printf("%d->", cur->data);//打印链表数据
    8. cur = cur->next;//保存指向下个节点的地址
    9. }
    10. printf("NULL\n");//链表最后指向空
    11. }

    这里我们只要不断的通过cur找到的节点下个数据即可,还要注意中单链表打印完毕后,不要忘记在最后一行打印NULL表示链表到此为止。

    在实现单链表的其他功能的前提是我们需要有一个节点。

    申请节点

    1. //申请新节点
    2. SListNode* BuySLTNode(SLTDataType x)
    3. {
    4. SListNode* NewNode = (SListNode*)malloc(sizeof(SListNode));//为新节点的数据申请空间
    5. if (NewNode==NULL)
    6. {
    7. perror("malloc fail");//申请失败报错
    8. exit(-1);
    9. }
    10. NewNode->data = x;//将为新节点插入数据
    11. NewNode->next = NULL;//将节点指向下个位置的地址置空
    12. return NewNode;
    13. }

    在这个节点中就一个数据和一个空指针。

    有时候当我们不要这个单链表时,我们可以写函数接口来销毁他。

    链表销毁

    1. //链表销毁
    2. void SListDestory(SListNode** pphead)
    3. {
    4. assert(pphead);//断言(plist地址肯定不为空)
    5. SListNode* cur = *pphead;//保存好链表头的地址
    6. while (cur)
    7. {
    8. SListNode* next = cur->next;//将指向一个链的地址记录下来
    9. free(cur);//释放头的空间
    10. cur = next;//指向一个链
    11. }
    12. *pphead = NULL;//销毁完毕,单链表内容为空
    13. }

    在这里我们只要注意通过free不断释放节点申请的空间即可,顺便提一句,当我们要修改一级指针的值,通常我们要用到二级指针。在测试中我们给SListDestory传的是&plist(plist是一个一级指针),所以要用二级指针来接收。

    下面我们来实现一个简单的头插

    单链表的头插

    1. //单链表头插
    2. void SListPushFort(SListNode** pphead,SLTDataType x)
    3. {
    4. assert(pphead);//断言(plist地址肯定不为空)
    5. SListNode* NewNode = BuySLTNode(x);//申请一个节点
    6. NewNode->next = *pphead;
    7. *pphead = NewNode;
    8. }

    这里我们注意节点之间的链接。

    有了头插我们中来个头删除吧。

    单链表的头删除

    1. //单链表头删
    2. void SListPopFort(SListNode** pphead)
    3. {
    4. assert(pphead);//断言(plist地址肯定不为空)
    5. //温柔的检测
    6. if (*pphead == NULL)//链表为空就不删除
    7. {
    8. return;
    9. }
    10. //暴力检测
    11. //assert(*pphead!=NULL);
    12. SListNode* del = *pphead;//将要删除地址给del
    13. *pphead = (*pphead)->next;//plist指向下个链表的头
    14. free(del);//释放要删除的空间
    15. del = NULL;
    16. }

    这里我们要判断链表是否为空,为空就不删除,不为空直接释放头节点就可以,但这里我们注意重新指向下个节点。

    写道这里我们还没有测试我们之前写的功能,下面我们测试一下。

     测试很重要(不要全部写完在来测试)

    单链表的尾插

    1. //单链表尾插
    2. void SListPushBack(SListNode** pphead, SLTDataType x)
    3. {
    4. assert(pphead);//断言
    5. SListNode* NewNode = BuySLTNode(x);//申请一个节点
    6. //1链表为空
    7. //2联邦不为空
    8. if (*pphead == NULL)
    9. {
    10. *pphead = NewNode;
    11. }
    12. else
    13. {
    14. SListNode* tail = *pphead;//记录链表头的地址
    15. //找尾
    16. while (tail->next != NULL)
    17. {
    18. tail = tail->next;//不断的指向下个链
    19. }
    20. tail->next = NewNode;//尾插入数据
    21. }
    22. }

    对于单链表的尾插,我们要分为二种情况:

    1 链表为空

    为空的直接把新节点赋值给*pphead(plist)即可

    2 链表不为空

    首先我们要找链表的尾,其次才是插入数据

    单链表的尾删

    1. /单链表尾删除
    2. void SListPopBack(SListNode** pphead)
    3. {
    4. assert(pphead);//断言(plist地址肯定不为空)
    5. //温柔的检测
    6. if (*pphead == NULL)//链表为空就不删除
    7. {
    8. return;
    9. }
    10. if ((*pphead)->next == NULL)//一个节点
    11. {
    12. free(*pphead);
    13. *pphead = NULL;
    14. }
    15. else//多个节点
    16. {
    17. SListNode* tail = *pphead;//记录链表头的地址
    18. SListNode* prev = NULL;//置空
    19. //找尾
    20. while (tail->next != NULL)
    21. {
    22. prev = tail;//保存上个节点的地址
    23. tail = tail->next;//不断的指向下个节点
    24. }
    25. prev->next = NULL;//将上个节点的位置置空
    26. free(tail);//释放
    27. tail = NULL;
    28. }
    29. }

    单链表的尾删的话条件更加复杂一点,我们要考虑链表为空就不删除链表中为一个节点(不要尾)和链表中多个节点(要找尾)。

    下面我们继续测试函数的功能

     有了增删,那么我们还要写查和改。

    单链表查找

    1. //单链表查找
    2. SListNode* SListFind(SListNode* phead, SLTDataType x)
    3. {
    4. SListNode* cur = phead;//标记头节点
    5. //遍历链表
    6. while (cur)
    7. {
    8. if (cur->data == x)
    9. {
    10. return cur;//找到返回标记点
    11. }
    12. cur = cur->next;
    13. }
    14. return NULL;//没找到返回NULL
    15. }

    单链表的查的话主要是,遍历链表,找我们查找数据,返回指向该数据的指针就可。

    其实查找的功能主要是方便我们能找道pos的地址从而实现对他的前插和后插。

    在pos之前插入

    1. //在pos之前插入
    2. void SListInsert(SListNode** pphead, SListNode* pos, SLTDataType x)
    3. {
    4. assert(pphead);//断言
    5. assert(pos);
    6. if (pos == *pphead)//第一个节点就是pos
    7. {
    8. SListPushFort(pphead, x);//直接头插
    9. }
    10. else//第一个节点不为pos
    11. {
    12. SListNode* prev = *pphead;
    13. while (prev->next != pos)
    14. {
    15. prev = prev->next;//不断指向下个节点
    16. // 暴力检查,pos不在链表中.prev为空,还没有找到pos,说明pos传错了
    17. assert(prev);
    18. }
    19. SListNode* NewNode = BuySLTNode(x);//申请新的节点
    20. prev->next = NewNode;//上个节点的next指向新节点
    21. NewNode->next = pos;//新节点指向pos;
    22. }
    23. }

    在对pos之前插入我们要考虑许多,首先pos是不能为空的,其实当第一个节点就是pos时,我们直接头插入就可以了,最后当第一个节点不为pos时,就要在判断链表中是pos是否存在。

    在pos后插入

    1. //在pos后插入
    2. void SListInsertAfter(SListNode* pos, SLTDataType x)
    3. {
    4. assert(pos);//断言
    5. //直接插入就行了
    6. SListNode* NewNode = BuySLTNode(x);//申请新的节点
    7. NewNode->next = pos->next;//指向pos后的节点
    8. pos->next = NewNode;//节点pos指向新节点
    9. }

    对于pos后插就简单了,我们直接插入就好。

    继续测试

     对于修改pos值就不多说了,我们有pos的指针直接改就是了。

    单链表就基本功能就都实现了,大家一定要动手多多尝试。

     

  • 相关阅读:
    datax和datax-web编译安装和使用
    C4D坐标与渲染
    普通交换机可以改成POE供电吗?
    MySQL基本操作之数据库设计理论
    Vue3初体验
    C++ 入门14:STL 容器之向量(vector)
    基于php的超市仓库管理系统
    Hadoop系列——Hadoop简介day1-2
    C语言---单身狗问题
    已解决java.nio.BufferOverflowException: 缓冲区溢出异常的正确解决方法,亲测有效!!!
  • 原文地址:https://blog.csdn.net/qq_61552595/article/details/126417370