• C语言描述数据结构 —— 单链表


    前言

    对于链表这部分,在数据结构里面是非常重要的。链表的学习周期长一些,博客的内容会多一些,所以对于链表的描述我将分为多篇。基本概念、基本实现、基本习题、进阶习题我都会通过博客向大家分享。那么本篇是对链表有一个初步的认识,并且能够掌握最基本的单链表的增删查改

    1. 顺序表的问题及思考

    在学习链表之前,我们回顾以下顺序表,那么我们来思考下面几个问题:

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

    那么我们要解决上述问题,就必须学习链表。所以带着问题去学习,那么将事半功倍。

    2. 链表的基本概念和基本结构

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

    如何理解物理存储结构是不连续的呢?

     链表与顺序表不一样的是,顺序表它是一个数组,但是链表不是,所有的连续数据都是通过指针来链接的。那么现在又引出了一个问题,链表的每个数据都要链接下一个数据,而链接的方式是通过指针,那么如何做到呢?我们可以运用结构体来解决这个问题,并且提出结点的概念。

    可以看到,每个结点都有数据域和指针域,指针是指向下一个结点的地址,那么需要补充的是,第一个结点的地址需要用一个指针来保存,最后一个结点的指针是指向空指针。 保存第一个结点的地址的意义在于:可以找到链表的头结点,从而进一步访问结点之后的结点。最后一个结点的指针指向空指针的意义在于:避免野指针的问题,客观上增加一种链表的结束条件,例如字符串的结束标志 '\0' 。

    那么我们就得出了链表的三个特征:

    1. 链表的结构在逻辑上是连续的,但是在物理上不一定连续
    2. 现实中的结点一般都是从堆上申请出来的
    3. 从堆上申请的空间,是按照一定的策略来分配的,两次申请的空间可能连续,也可能不连续 

    3. 链表的分类 

    链表的结构是有多种的,单向、双向、循环等等,但我们主要的侧重点在单向的不带哨兵卫的链表。

    4. 链表的实现

     结点的声明:

    1. //结点的声明
    2. typedef int LLDataType;
    3. typedef struct LinkList
    4. {
    5. LLDataType val;
    6. struct LinkList* next;
    7. }LL;

    存储头结点的指针在主函数中定义:

    1. #include "LinkList.h"
    2. int main()
    3. {
    4. LL* head = NULL;
    5. return 0;
    6. }

    头插的实现:

    1. //创建新结点
    2. static LL* CreateNode(LLDataType x)
    3. {
    4. LL* RetNode = (LL*)calloc(1, sizeof(LL));
    5. assert(RetNode);
    6. RetNode->val = x;
    7. RetNode->next = NULL;
    8. return RetNode;
    9. }
    10. //头插
    11. void ListPushHead(LL** phead, LLDataType x)
    12. {
    13. assert(phead);
    14. //此时我们还没有结点,需要创建一个结点
    15. LL* NewNode = CreateNode(x);
    16. //新结点的指针要指向以前的头结点
    17. NewNode->next = *phead;
    18. //头指针的内容更新为新结点的地址
    19. *phead = NewNode;
    20. }

    需要注意,即使存储头结点的指针是一个指针,但我们在函数内操作时,需要改变指针的指向位置,所以需要使用二级指针。我们可能对最后两条代码存在疑问,我在这里解释一下:

    链表打印实现:

    1. //打印
    2. void ListPrint(LL* phead)
    3. {
    4. LL* cur = phead;//使用临时变量来操作链表,但因为是传址调用,不使用也是可以的
    5. while (cur)
    6. {
    7. printf("%d->", cur->val);
    8. cur = cur->next;
    9. }
    10. printf("NULL\n");
    11. }

     测试:头插一个数据以及打印:

    1. #include "LinkList.h"
    2. int main()
    3. {
    4. LL* head = NULL;
    5. ListPushHead(&head, 5);
    6. ListPrint(head);
    7. return 0;
    8. }

    尾插的实现:

    1. //尾插
    2. void LIstPushBack(LL** phead, LLDataType x)
    3. {
    4. assert(phead);
    5. LL* newnode = CreateNode(x);
    6. //如果链表没有结点,新结点直接作为头结点
    7. if (*phead == NULL)
    8. {
    9. *phead = newnode;
    10. }
    11. else
    12. {
    13. LL* tail = *phead;
    14. //向后寻找链表的最后一个结点
    15. while (tail->next != NULL)
    16. {
    17. tail = tail->next;
    18. }
    19. tail->next = newnode;
    20. }
    21. }

    可以看到,我们尾插的情况是有两个的,一个链表里面没有结点,那么就让这个要尾插的结点直接为头。而另一种则是链表中存在结点,我们需要遍历找到最后一个结点,并让这个结点的指针指向新的结点。同时需要注意,我们传的参数是二级指针,我们要解引用才能找到存放头结点地址的指针。

    我们思考一个问题:我们能否像头插一样定义一个指针,专门用来存储最后一个结点的地址呢?当然可以,不过我们需要对函数的参数做出一些改变。

    头插更改:

    1. //头插
    2. void ListPushHead(LL** phead, LL** ptail,LLDataType x)
    3. {
    4. //assert(phead);
    5. 此时我们还没有结点,需要创建一个结点
    6. //LL* NewNode = CreateNode(x);
    7. 新结点的指针要指向以前的头结点
    8. //NewNode->next = *phead;
    9. 头指针的内容更新为新结点的地址
    10. //*phead = NewNode;
    11. assert(phead);
    12. //此时我们还没有结点,需要创建一个结点
    13. LL* NewNode = CreateNode(x);
    14. //链表为空时,第一个头插的结点就是尾结点
    15. if (*phead == NULL)
    16. {
    17. *ptail = NewNode;
    18. }
    19. //新结点的指针要指向以前的头结点
    20. NewNode->next = *phead;
    21. //头指针的内容更新为新结点的地址
    22. *phead = NewNode;
    23. }

    尾插更改:

    1. //尾插
    2. void LIstPushBack(LL** phead,LL** ptail, LLDataType x)
    3. {
    4. //assert(phead);
    5. //LL* NewNode = CreateNode(x);
    6. 如果链表没有结点,新结点直接作为头结点
    7. //if (*phead == NULL)
    8. //{
    9. // *phead = NewNode;
    10. //}
    11. //else
    12. //{
    13. // LL* tail = *phead;
    14. // //向后寻找链表的最后一个结点
    15. // while (tail->next != NULL)
    16. // {
    17. // tail = tail->next;
    18. // }
    19. // tail->next = NewNode;
    20. //}
    21. assert(phead && ptail);
    22. LL* NewNode = CreateNode(x);
    23. //如果链表为空,那么头尾指针都指向这个新结点
    24. if (*phead == NULL)
    25. {
    26. *ptail = *phead = NewNode;
    27. }
    28. else
    29. {
    30. (*ptail)->next = NewNode;//尾结点的指针指向新结点
    31. (*ptail) = (*ptail)->next;//尾指针指向新结点
    32. }
    33. }

    我们对修改之后的代码进行数据测试:

    1. #include "LinkList.h"
    2. int main()
    3. {
    4. LL* head = NULL;
    5. LL* tail = NULL;//专门用来存储最后一个结点的地址
    6. ListPushHead(&head,&tail, 5);
    7. ListPushHead(&head, &tail, 6);
    8. ListPushHead(&head, &tail, 7);
    9. ListPrint(head);
    10. LIstPushBack(&head,&tail, 3);
    11. LIstPushBack(&head, &tail, 4);
    12. LIstPushBack(&head, &tail, 5);
    13. ListPrint(head);
    14. return 0;
    15. }

     可以看到,输出结果正是我们想要的。那么修改之后的代码有什么优势呢?其一就是减少了时间复杂度,用循环的方式去找尾,如果数据量足够大,是非常消耗时间的。其二就是我们掌握了这个方法以后,在一些题目上面,我们能取得一些优势

    头删的实现:

    1. //头删
    2. void ListPopHead(LL** phead, LL** ptail)
    3. {
    4. assert(phead && ptail);
    5. assert(*phead);//链表为空不需要删除,强制退出
    6. LL* cur = *phead;
    7. //如果链表仅有一个结点
    8. if (cur->next == NULL)
    9. {
    10. free(cur);//释放结点
    11. *phead = *ptail = NULL;//存储头尾结点地址的指针置空
    12. }
    13. //链表有两个或两个以上结点时
    14. else
    15. {
    16. *phead = cur->next;
    17. free(cur);
    18. //尾指针不需要变
    19. }
    20. }

    尾删的实现: 

    1. //尾删
    2. void ListPopBack(LL** phead, LL** ptail)
    3. {
    4. assert(phead && ptail);
    5. assert(*phead);//链表里面没有结点不需要删除
    6. LL* cur = *ptail;
    7. LL* NewPtail = *phead;
    8. //找到倒数第二个结点
    9. while (NewPtail->next != *ptail)
    10. {
    11. NewPtail = NewPtail->next;
    12. }
    13. //对删除之前的尾结点释放
    14. free(cur);
    15. //存储尾结点地址的指针更新
    16. *ptail = NewPtail;
    17. (*ptail)->next = NULL;
    18. }

    测试:头删和尾删:

    1. #include "LinkList.h"
    2. int main()
    3. {
    4. LL* head = NULL;
    5. LL* tail = NULL;//专门用来存储最后一个结点的地址
    6. ListPushHead(&head,&tail, 5);
    7. ListPushHead(&head, &tail, 6);
    8. ListPushHead(&head, &tail, 7);
    9. ListPrint(head);
    10. LIstPushBack(&head,&tail, 3);
    11. LIstPushBack(&head, &tail, 4);
    12. LIstPushBack(&head, &tail, 5);
    13. ListPrint(head);
    14. ListPopHead(&head, &tail);
    15. ListPrint(head);
    16. ListPopBack(&head, &tail);
    17. ListPrint(head);
    18. return 0;
    19. }

     

    数据的查找:

    1. //查找
    2. LL* ListFind(LL* phead,LLDataType x)
    3. {
    4. LL* cur = phead;
    5. while (cur)
    6. {
    7. if (cur->val == x)
    8. return cur;
    9. cur = cur->next;
    10. }
    11. return NULL;
    12. }

    我们的目的是找到数据的结点地址,如果确实有结点存储这个数据,那么就返回结点地址,否则返回空。如果有多个结点存储同一个数据,那么返回第一个结点的地址。这是符合常理的设计方法。

    在 pos 位置插入数据:

    1. //在 pos 位置插入
    2. void ListInsert(LL** phead, LL** ptail, LL* pos, LLDataType x)
    3. {
    4. assert(phead && ptail);
    5. assert(*phead);//确保链表有结点
    6. //如果 pos 位置与头结点位置重合就头插
    7. if (pos == *phead)
    8. {
    9. ListPushHead(phead, ptail, x);
    10. }
    11. else
    12. {
    13. //在 pos 位置之前插入
    14. LL* cur = *phead;
    15. while (cur->next != pos)
    16. {
    17. cur = cur->next;
    18. }
    19. LL* NewNode = CreateNode(x);
    20. cur->next = NewNode;
    21. NewNode->next = pos;
    22. }
    23. }

    测试:在 pos 位置插入数据(与查找配合):

    1. #include "LinkList.h"
    2. int main()
    3. {
    4. LL* head = NULL;
    5. LL* tail = NULL;//专门用来存储最后一个结点的地址
    6. ListPushHead(&head,&tail, 5);
    7. ListPushHead(&head, &tail, 6);
    8. ListPushHead(&head, &tail, 7);
    9. ListPrint(head);
    10. LIstPushBack(&head,&tail, 3);
    11. LIstPushBack(&head, &tail, 4);
    12. LIstPushBack(&head, &tail, 5);
    13. ListPrint(head);
    14. ListPopHead(&head, &tail);
    15. ListPrint(head);
    16. ListPopBack(&head, &tail);
    17. ListPrint(head);
    18. ListInsert(&head, &tail, ListFind(head,5), 50);
    19. ListPrint(head);
    20. return 0;
    21. }

     

    在 pos 位置删除结点:

    1. //在 pos 位置删除
    2. void ListPopInsert(LL** phead, LL** ptail, LL* pos)
    3. {
    4. assert(phead && ptail);
    5. assert(*phead);//确保链表有结点
    6. //如果 pos 位置是头结点就头删
    7. if (pos == *phead)
    8. {
    9. ListPopHead(phead, ptail);
    10. }
    11. //如果 pos 位置是尾结点就尾删
    12. else if (pos == *ptail)
    13. {
    14. ListPopBack(phead, ptail);
    15. }
    16. else
    17. {
    18. LL* cur = *phead;
    19. while (cur->next != pos)
    20. {
    21. cur = cur->next;
    22. }
    23. cur->next = pos->next;
    24. free(pos);
    25. }
    26. }

    测试:在 pos 位置删除结点:

    1. #include "LinkList.h"
    2. int main()
    3. {
    4. LL* head = NULL;
    5. LL* tail = NULL;//专门用来存储最后一个结点的地址
    6. ListPushHead(&head,&tail, 5);
    7. ListPushHead(&head, &tail, 6);
    8. ListPushHead(&head, &tail, 7);
    9. ListPrint(head);
    10. LIstPushBack(&head,&tail, 3);
    11. LIstPushBack(&head, &tail, 4);
    12. LIstPushBack(&head, &tail, 5);
    13. ListPrint(head);
    14. ListPopHead(&head, &tail);
    15. ListPrint(head);
    16. ListPopBack(&head, &tail);
    17. ListPrint(head);
    18. ListInsert(&head, &tail, ListFind(head,5), 50);
    19. ListPrint(head);
    20. ListPopInsert(&head, &tail, ListFind(head, 5));
    21. ListPrint(head);
    22. return 0;
    23. }

     

    5. 完整各文件代码

    test.c 源文件

    1. #define _CRT_SECURE_NO_WARNINGS 1
    2. #include "LinkList.h"
    3. int main()
    4. {
    5. LL* head = NULL;
    6. LL* tail = NULL;//专门用来存储最后一个结点的地址
    7. ListPushHead(&head,&tail, 5);
    8. ListPushHead(&head, &tail, 6);
    9. ListPushHead(&head, &tail, 7);
    10. ListPrint(head);
    11. LIstPushBack(&head,&tail, 3);
    12. LIstPushBack(&head, &tail, 4);
    13. LIstPushBack(&head, &tail, 5);
    14. ListPrint(head);
    15. ListPopHead(&head, &tail);
    16. ListPrint(head);
    17. ListPopBack(&head, &tail);
    18. ListPrint(head);
    19. ListInsert(&head, &tail, ListFind(head,5), 50);
    20. ListPrint(head);
    21. ListPopInsert(&head, &tail, ListFind(head, 5));
    22. ListPrint(head);
    23. return 0;
    24. }

     ListLink.c 源文件

    1. #define _CRT_SECURE_NO_WARNINGS 1
    2. #include "LinkList.h"
    3. //创建新结点
    4. static LL* CreateNode(LLDataType x)
    5. {
    6. LL* RetNode = (LL*)calloc(1, sizeof(LL));
    7. assert(RetNode);
    8. RetNode->val = x;
    9. RetNode->next = NULL;
    10. return RetNode;
    11. }
    12. //头插
    13. void ListPushHead(LL** phead, LL** ptail,LLDataType x)
    14. {
    15. //assert(phead);
    16. 此时我们还没有结点,需要创建一个结点
    17. //LL* NewNode = CreateNode(x);
    18. 新结点的指针要指向以前的头结点
    19. //NewNode->next = *phead;
    20. 头指针的内容更新为新结点的地址
    21. //*phead = NewNode;
    22. assert(phead);
    23. //此时我们还没有结点,需要创建一个结点
    24. LL* NewNode = CreateNode(x);
    25. //链表为空时,第一个头插的结点就是尾结点
    26. if (*phead == NULL)
    27. {
    28. *ptail = NewNode;
    29. }
    30. //新结点的指针要指向以前的头结点
    31. NewNode->next = *phead;
    32. //头指针的内容更新为新结点的地址
    33. *phead = NewNode;
    34. }
    35. //打印
    36. void ListPrint(LL* phead)
    37. {
    38. LL* cur = phead;//使用临时变量来操作链表,但因为是传址调用,不使用也是可以的
    39. while (cur)
    40. {
    41. printf("%d->", cur->val);
    42. cur = cur->next;
    43. }
    44. printf("NULL\n");
    45. }
    46. //尾插
    47. void LIstPushBack(LL** phead,LL** ptail, LLDataType x)
    48. {
    49. //assert(phead);
    50. //LL* NewNode = CreateNode(x);
    51. 如果链表没有结点,新结点直接作为头结点
    52. //if (*phead == NULL)
    53. //{
    54. // *phead = NewNode;
    55. //}
    56. //else
    57. //{
    58. // LL* tail = *phead;
    59. // //向后寻找链表的最后一个结点
    60. // while (tail->next != NULL)
    61. // {
    62. // tail = tail->next;
    63. // }
    64. // tail->next = NewNode;
    65. //}
    66. assert(phead && ptail);
    67. LL* NewNode = CreateNode(x);
    68. //如果链表为空,那么头尾指针都指向这个新结点
    69. if (*phead == NULL)
    70. {
    71. *ptail = *phead = NewNode;
    72. }
    73. else
    74. {
    75. (*ptail)->next = NewNode;//尾结点的指针指向新结点
    76. (*ptail) = (*ptail)->next;//尾指针指向新结点
    77. }
    78. }
    79. //头删
    80. void ListPopHead(LL** phead, LL** ptail)
    81. {
    82. assert(phead && ptail);
    83. assert(*phead);//链表为空不需要删除,强制退出
    84. LL* cur = *phead;
    85. //如果链表仅有一个结点
    86. if (cur->next == NULL)
    87. {
    88. free(cur);//释放结点
    89. *phead = *ptail = NULL;//存储头尾结点地址的指针置空
    90. }
    91. //链表有两个或两个以上结点时
    92. else
    93. {
    94. *phead = cur->next;
    95. free(cur);
    96. //尾指针不需要变
    97. }
    98. }
    99. //尾删
    100. void ListPopBack(LL** phead, LL** ptail)
    101. {
    102. assert(phead && ptail);
    103. assert(*phead);//链表里面没有结点不需要删除
    104. LL* cur = *ptail;
    105. LL* NewPtail = *phead;
    106. //找到倒数第二个结点
    107. while (NewPtail->next != *ptail)
    108. {
    109. NewPtail = NewPtail->next;
    110. }
    111. //对删除之前的尾结点释放
    112. free(cur);
    113. //存储尾结点地址的指针更新
    114. *ptail = NewPtail;
    115. (*ptail)->next = NULL;
    116. }
    117. //查找
    118. LL* ListFind(LL* phead,LLDataType x)
    119. {
    120. LL* cur = phead;
    121. while (cur)
    122. {
    123. if (cur->val == x)
    124. return cur;
    125. cur = cur->next;
    126. }
    127. return NULL;
    128. }
    129. //在 pos 位置插入
    130. void ListInsert(LL** phead, LL** ptail, LL* pos, LLDataType x)
    131. {
    132. assert(phead && ptail);
    133. assert(*phead);//确保链表有结点
    134. //如果 pos 位置与头结点位置重合就头插
    135. if (pos == *phead)
    136. {
    137. ListPushHead(phead, ptail, x);
    138. }
    139. else
    140. {
    141. //在 pos 位置之前插入
    142. LL* cur = *phead;
    143. while (cur->next != pos)
    144. {
    145. cur = cur->next;
    146. }
    147. LL* NewNode = CreateNode(x);
    148. cur->next = NewNode;
    149. NewNode->next = pos;
    150. }
    151. }
    152. //在 pos 位置删除
    153. void ListPopInsert(LL** phead, LL** ptail, LL* pos)
    154. {
    155. assert(phead && ptail);
    156. assert(*phead);//确保链表有结点
    157. //如果 pos 位置是头结点就头删
    158. if (pos == *phead)
    159. {
    160. ListPopHead(phead, ptail);
    161. }
    162. //如果 pos 位置是尾结点就尾删
    163. else if (pos == *ptail)
    164. {
    165. ListPopBack(phead, ptail);
    166. }
    167. else
    168. {
    169. LL* cur = *phead;
    170. while (cur->next != pos)
    171. {
    172. cur = cur->next;
    173. }
    174. cur->next = pos->next;
    175. free(pos);
    176. }
    177. }

    ListLink.h 头文件

    1. #define _CRT_SECURE_NO_WARNINGS 1
    2. #include
    3. #include
    4. #include
    5. //结点的声明
    6. typedef int LLDataType;
    7. typedef struct LinkList
    8. {
    9. LLDataType val;
    10. struct LinkList* next;
    11. }LL;
    12. //存放头结点地址的指针
    13. void ListPushHead(LL** phead, LL** ptail,LLDataType x);//头插
    14. void ListPrint(LL* phead);//打印
    15. void LIstPushBack(LL** phead,LL** ptail ,LLDataType x);//尾插
    16. void ListPopHead(LL** phead, LL** ptail);//头删
    17. void ListPopBack(LL** phead, LL** ptail);//尾删
    18. LL* ListFind(LL* phead,LLDataType x);//查找
    19. void ListInsert(LL** phead, LL** ptail, LL* pos, LLDataType x);//在 pos 位置插入
    20. void ListPopInsert(LL** phead, LL** ptail, LL* pos);//删除 pos 位置的数据

  • 相关阅读:
    ruoyi(若依)接口拦截路径配置,接口访问要授权,放开授权直接访问
    每日一文(第一天)
    vivo大数据日志采集Agent设计实践
    matlab实现MCMC的马尔可夫转换MS- ARMA - GARCH模型估计
    Docker Compose安装部署Jenkins
    Study Git - Data Model
    机器学习(十七):网格搜索(Grid Search)和SVM
    第一季:1自增变量【Java面试题】
    Spring Cloud Alibaba —— 分布式事务组件
    海思3559万能平台搭建:OSD功能的优化
  • 原文地址:https://blog.csdn.net/weixin_59913110/article/details/126103367