当前位置:   article > 正文

C语言实现基础数据结构——链表_c语言实现链表

c语言实现链表

目录

链表

链表示意图

链表的特点

链表的分类

单链表

链表实现前置须知

主要实现功能

链表数据的打印(遍历链表)

链表的头部插入

链表的尾部插入

链表的头部删除

链表的尾部删除

查找链表中的数据

在指定位置之前插入数据

在指定位置之后插入数据

删除指定位置的节点

销毁链表

项目文件

双链表

主要实现功能

双向链表初始化

双向链表的打印

判断链表是否为空

双向链表的尾部插入

双向链表的头部插入

双向链表的尾部删除

双向链表的头部删除

双向链表的数据查找

双向链表中指定位置数据插入

删除双向链表指定位置数据

双向链表的销毁

项目文件

小练习


链表

链表是线性表中的其中一种结构,链表在物理存储结构上是非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。在链表中,每一个存储空间都是独立申请的(需要添加数据时申请),也被称为节点(结点)

链表节点有两个组成部分:

  1. 当前节点需要保存的数据
  2. 保存的节点的位置
  1. //单链表结构体定义
  2. //定义需要存储的数据类型
  3. typedef int SListDataType;
  4. //链表节点结构体定义
  5. typedef struct SListNode
  6. {
  7. SListDataType data;//存储数据
  8. struct SListNode* next;//结构体指针指向后一个结构体(节点)
  9. }SLN;
  10. //双向链表结构体定义
  11. //定义存储的数据类型
  12. typedef int DlistDataType;
  13. //链表节点结构体定义
  14. typedef struct DlistNode
  15. {
  16. DlistDataType data;//存储数据
  17. struct DlistNode* pprev;//结构体指针指向前一个节点
  18. struct DListNode* next;//结构体指针指向后一个节点
  19. }DLN;

链表示意图

单链表为例

链表的特点

  1. 链式结构在逻辑上是连续的,在物理结构上不一定连续
  2. 节点⼀般是从堆上申请的
  3. 从堆上申请来的空间,是按照⼀定策略分配出来的,每次申请的空间可能连续,可能不连续

链表的分类

单链表

链表实现前置须知

链表实现时需要熟悉的三个NULL

  1. 函数形参中的二级指针ppheadNULL:说明未正确传入链表首节点地址
  2. 头指针pheadNULL:说明链表中还未创建节点。链表还未创建节点则代表头指针phead中的值为NULL,而phead本身也是指针变量,有着自己的地址,将该地址给二级指针时二级指针则不为空(pphead = &phead
  3. 节点指针域为NULL:说明当前节点为最后一个节点

主要实现功能

  1. //链表数据的打印(遍历链表)
  2. void SLTPrint(SLTNode* phead);
  3. //头部插⼊删除/尾部插⼊删除
  4. void SLTPushBack(SLTNode** pphead, SLTDataType x);
  5. void SLTPushFront(SLTNode** pphead, SLTDataType x);
  6. void SLTPopBack(SLTNode** pphead);
  7. void SLTPopFront(SLTNode** pphead);
  8. //查找链表中的数据
  9. SLTNode* SLTFind(SLTNode* phead, SLTDataType x);
  10. //在指定位置之前插⼊数据
  11. void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
  12. //删除pos节点
  13. void SLTErase(SLTNode** pphead, SLTNode* pos);
  14. //在指定位置之后插⼊数据
  15. void SLTInsertAfter(SLTNode* pos, SLTDataType x);
  16. //删除pos之后的节点
  17. void SLTEraseAfter(SLTNode* pos);
  18. //销毁链表
  19. void SListDesTroy(SLTNode** pphead);

链表数据的打印(遍历链表

  1. //链表数据的打印(遍历链表)
  2. void SLNPrint(SLN* phead)
  3. {
  4. SLN* pstart = phead;//定义一个临时结构体指针变量,直接使用phead遍历会使最后找不到链表的开始处
  5. //使用while循环遍历
  6. //结束条件是pstart不为空指针,因为pstart在遍历的过程中会被更新为当前结构体的下一个结构体地址
  7. //一旦pstart存储为最后一个节点中存储地址的部分的NULL值则退出循环
  8. while (pstart)
  9. {
  10. printf("%d->", pstart->data);
  11. pstart = pstart->next;
  12. }
  13. printf("NULL\n");
  14. }

链表的头部插入

  1. //开辟新空间,用于存储需要插入的新数据
  2. SLN* applySpace(SListDataType x)
  3. {
  4. SLN* phead = (SLN*)malloc(sizeof(SLN));
  5. assert(phead);
  6. phead->data = x;
  7. //开辟的新空间中,存储下一个节点地址的部分要置为空,防止野指针
  8. phead->next = NULL;
  9. return phead;
  10. }
  11. //链表的头部插入
  12. void SLNPushFront(SLN** pphead, SListDataType x)
  13. {
  14. assert(pphead);
  15. SLN* newnode = applySpace(x);
  16. //将形参指向的链表的第一个节点的地址给新开辟的节点中的存储下一个节点地址部分
  17. newnode->next = *pphead;
  18. *pphead = newnode;//将初始的第一个节点中存储下一个节点的部分存储新开辟的节点的地址
  19. }

链表的尾部插入

  1. void SLNPushBack(SLN** pphead, SListDataType x)
  2. {
  3. assert(pphead);
  4. //开辟新空间
  5. SLN* newnode = applySpace(x);
  6. //如果新开辟的空间为链表的第一个节点,则将新节点作为首节点
  7. if (*pphead == NULL)
  8. {
  9. *pphead = newnode;
  10. return;
  11. }
  12. //尾部插入的前提是先找到最后一个节点
  13. //因为最后一个节点中存储下一个节点地址的部分为NULL,故可以遍历链表直到找到该部分为NULL为止
  14. SLN* pmove = *pphead;//防止丢失链表的第一个节点
  15. //使用next作为终止条件,而不是pmove为终止条件,当pmove为终止条件时,即pmove = NULL,
  16. while (pmove->next)
  17. {
  18. pmove = pmove->next;
  19. }
  20. pmove->next = newnode;
  21. }

链表的头部删除

  1. //链表的头部删除
  2. void SLNPopFront(SLN** pphead)
  3. {
  4. assert(pphead);
  5. //判断链表是否为空链表
  6. //如果为空链表则不执行删除操作
  7. if (*pphead == NULL)
  8. {
  9. return;
  10. }
  11. SLN* BlocktoFree = *pphead;
  12. //如果链表不为空时,执行删除操作
  13. //释放完空间后首节点需要改变
  14. *pphead = (*pphead)->next;
  15. //删除节点需要释放开辟的内存空间
  16. free(BlocktoFree);
  17. }

链表的尾部删除

  1. //链表的尾部删除void SLNPopBack(SLN** pphead)
  2. {
  3. assert(pphead);
  4. //删除之前需要判断链表是不是空链表
  5. //如果是空链表则不执行删除操作
  6. if (*pphead == NULL)
  7. {
  8. return;
  9. }
  10. //如果链表只有一个节点时
  11. if ((*pphead)->next == NULL)
  12. {
  13. free(*pphead);
  14. *pphead = NULL;
  15. return;
  16. }
  17. //如果链表不为空且有多个节点时,执行删除操作
  18. //先找到尾结点
  19. SLN* p_end = *pphead;
  20. SLN* pprev = p_end;
  21. while (p_end->next)
  22. {
  23. pprev = p_end;
  24. p_end = p_end->next;
  25. }
  26. //将倒数第二个节点的存储下一个节点地址的部分置为空
  27. pprev->next = NULL;
  28. //释放尾结点的空间
  29. free(p_end);
  30. p_end = NULL;
  31. }

查找链表中的数据

  1. //查找链表中的数据
  2. SLN* SLNFind(SLN* phead, SListDataType x)
  3. {
  4. assert(phead);
  5. SLN* pcur = phead;
  6. //遍历链表数据
  7. while (pcur)
  8. {
  9. //找到数据返回数据所在节点的地址
  10. if (pcur->data == x)
  11. {
  12. return pcur;
  13. }
  14. pcur = pcur->next;
  15. }
  16. //未找到数据返回空指针
  17. return NULL;
  18. }

在指定位置之前插入数据

  1. //在指定位置之前(在指定位置)插入数据
  2. void SLNInsert(SLN** pphead, SLN* pos, SListDataType x)
  3. {
  4. assert(pphead);
  5. //插入的位置不可以为空,否则插入无效
  6. assert(pos);
  7. //插入的链表不能为空,否则不存在pos所指向的位置
  8. assert(*pphead);
  9. //开辟空间
  10. //在指定位置之前插入数据时,需要考虑该位置是不是头结点
  11. //如果需要插入的位置为头结点的位置,则相当于是头部插入操作
  12. if (pos == *pphead)
  13. {
  14. SLNPushFront(pphead, x);
  15. return;
  16. }
  17. //如果需要插入的位置不是头结点位置,则进行后续插入操作
  18. SLN* newnode = applySpace(x);
  19. SLN* pprev = *pphead;
  20. //遍历链表找到pos指向的位置
  21. while (pprev->next != pos)
  22. {
  23. pprev = pprev->next;
  24. }
  25. //更改前一节点指针的存储下一节点位置部分的地址为新节点的地址
  26. pprev->next = newnode;
  27. //更改新节点的存储下一节点部分的地址为pos
  28. newnode->next = pos;
  29. //上述两步可以交换执行顺序
  30. }

在指定位置之后插入数据

  1. //在指定位置之后插⼊数据
  2. void SLNInsertAfter(SLN* pos, SListDataType x)
  3. {
  4. assert(pos);
  5. //指定位置之后插入数据时注意节点连接顺序
  6. SLN* newnode = applySpace(x);
  7. //将新节点中的存储下一节点地址的部分赋值为pos后一节点的地址
  8. newnode->next = pos->next;
  9. //将pos节点中的存储下一节点地址的部分赋值为新节点的地址
  10. pos->next = newnode;
  11. //上述两步不可以交换执行顺序
  12. }

删除指定位置的节点

  1. //删除指定位置的节点
  2. void SLNEraseAfter(SLN* pos)
  3. {
  4. assert(pos);
  5. //如果pos之后的节点为空,则不执行删除操作
  6. if (pos->next == NULL)
  7. {
  8. return;
  9. }
  10. SLN* BlockToFree = pos->next;
  11. //如果pos之后的节点不为空,执行删除操作
  12. //将pos节点的存储下一个节点地址的部分更改为待删除节点的后一个节点地址
  13. pos->next = pos->next->next;
  14. free(BlockToFree);
  15. BlockToFree = NULL;
  16. }

销毁链表

  1. //销毁链表
  2. void SLNDesTroy(SLN** pphead)
  3. {
  4. assert(pphead);
  5. SLN* pcur = *pphead;
  6. //循环销毁链表
  7. while (pcur)
  8. {
  9. SLN* BlockToFree = pcur;
  10. pcur = pcur->next;
  11. free(BlockToFree);
  12. }
  13. *pphead = NULL;
  14. }

项目文件

测试文件
  1. #define _CRT_SECURE_NO_WARNINGS 1
  2. #include "SList.h"
  3. void generate_test()
  4. {
  5. //为链表添加数据需要向内存申请空间,因为不存在扩容问题,故直接使用malloc/calloc即可
  6. SLN* node1 = (SLN*)malloc(sizeof(SLN));//为第一个节点申请空间
  7. assert(node1);
  8. SLN* node2 = (SLN*)malloc(sizeof(SLN));//为第二个节点申请空间
  9. assert(node2);
  10. SLN* node3 = (SLN*)malloc(sizeof(SLN));//为第三个节点申请空间
  11. assert(node3);
  12. SLN* node4 = (SLN*)malloc(sizeof(SLN));//为第四个节点申请空间
  13. assert(node4);
  14. //在各节点的数据存储部分存入数据
  15. node1->data = 1;
  16. node2->data = 2;
  17. node3->data = 3;
  18. node4->data = 4;
  19. SLN* phead = node1;//使用一个指针指向第一个节点的地址
  20. node1->next = node2;//存入第二个节点申请的空间的地址
  21. node2->next = node3;//存入第三个节点申请的空间的地址
  22. node3->next = node4;//存入第四个节点申请的空间的地址
  23. node4->next = NULL;//最后一个节点中存储地址的部分置为空防止野指针
  24. SLNPrint(phead);
  25. SLNPushFront(&phead, 5);
  26. SLNPushFront(&phead, 6);
  27. SLNPushFront(&phead, 7);
  28. SLNPrint(phead);
  29. SLNPushBack(&phead, 8);
  30. SLNPrint(phead);
  31. SLNPopFront(&phead);
  32. SLNPrint(phead);
  33. SLNPopBack(&phead);
  34. SLNPrint(phead);
  35. SLN* ret = SLNFind(phead, 4);
  36. //if (ret)
  37. //{
  38. // printf("找到了");
  39. //}
  40. //else
  41. //{
  42. // printf("未找到");
  43. //}
  44. SLNInsert(&phead, ret, 9);
  45. SLNPrint(phead);
  46. ret = SLNFind(phead, 6);
  47. SLNInsert(&phead, ret, 10);
  48. SLNPrint(phead);
  49. SLNInsertAfter(ret, 11);
  50. SLNPrint(phead);
  51. ret = SLNFind(phead, 4);
  52. SLNInsertAfter(ret, 12);
  53. SLNPrint(phead);
  54. ret = SLNFind(phead, 12);
  55. SLNErase(&phead, ret);
  56. SLNPrint(phead);
  57. ret = SLNFind(phead, 3);
  58. SLNEraseAfter(ret);
  59. SLNPrint(phead);
  60. SLNDesTroy(&phead);
  61. SLNPrint(phead);
  62. }
  63. int main()
  64. {
  65. generate_test();
  66. return 0;
  67. }
头文件
  1. #pragma once
  2. #include <stdio.h>
  3. #include <assert.h>
  4. #include <stdlib.h>
  5. //定义需要存储的数据类型
  6. typedef int SListDataType;
  7. //链表节点结构体定义
  8. typedef struct SListNode
  9. {
  10. SListDataType data;//存储数据
  11. struct SListNode* next;//结构体指针指向下一个结构体
  12. }SLN;
  13. //链表数据的打印(遍历链表)
  14. void SLNPrint(SLN* phead);
  15. //头部插⼊删除/尾部插⼊删除
  16. SLN* applySpace(SListDataType x);
  17. void SLNPushBack(SLN** pphead, SListDataType x);
  18. void SLNPushFront(SLN** pphead, SListDataType x);
  19. void SLNPopBack(SLN** pphead);
  20. void SLNPopFront(SLN** pphead);
  21. //查找链表中的数据
  22. SLN* SLNFind(SLN* phead, SListDataType x);
  23. //在指定位置之前插⼊数据
  24. void SLNInsert(SLN** pphead, SLN* pos, SListDataType x);
  25. //删除pos节点
  26. void SLNErase(SLN** pphead, SLN* pos);
  27. //在指定位置之后插⼊数据
  28. void SLNInsertAfter(SLN* pos, SListDataType x);
  29. //删除pos之后的节点
  30. void SLNEraseAfter(SLN* pos);
  31. //销毁链表
  32. void SLNDesTroy(SLN** pphead);
实现文件
  1. #define _CRT_SECURE_NO_WARNINGS 1
  2. #include "SList.h"
  3. //链表数据的打印(遍历链表)
  4. void SLNPrint(SLN* phead)
  5. {
  6. SLN* pstart = phead;//定义一个临时结构体指针变量,直接使用phead遍历会使最后找不到链表的开始处
  7. //使用while循环遍历
  8. //结束条件是pstart不为空指针,因为pstart在遍历的过程中会被更新为当前结构体的下一个结构体地址
  9. //一旦pstart存储为最后一个节点中存储地址的部分的NULL值则退出循环
  10. while (pstart)
  11. {
  12. printf("%d->", pstart->data);
  13. pstart = pstart->next;
  14. }
  15. printf("NULL\n");
  16. }
  17. //开辟新空间,用于存储需要插入的新数据,包括链表为空的情况
  18. SLN* applySpace(SListDataType x)
  19. {
  20. SLN* phead = (SLN*)malloc(sizeof(SLN));
  21. assert(phead);
  22. phead->data = x;
  23. //开辟的新空间中,存储下一个节点地址的部分要置为空,防止野指针
  24. phead->next = NULL;
  25. return phead;
  26. }
  27. //链表的头部插入
  28. void SLNPushFront(SLN** pphead, SListDataType x)
  29. {
  30. assert(pphead);
  31. SLN* newnode = applySpace(x);
  32. //将形参指向的链表的第一个节点的地址给新开辟的节点中的存储下一个节点地址部分
  33. newnode->next = *pphead;
  34. *pphead = newnode;//在初始的第一个节点中存储下一个节点的部分中存储新开辟的节点的地址
  35. }
  36. //链表的尾部插入
  37. void SLNPushBack(SLN** pphead, SListDataType x)
  38. {
  39. assert(pphead);
  40. //开辟新空间
  41. SLN* newnode = applySpace(x);
  42. //如果新开辟的空间为链表的第一个节点,则将新节点作为首节点
  43. if (*pphead == NULL)
  44. {
  45. *pphead = newnode;
  46. return;
  47. }
  48. //尾部插入的前提是先找到最后一个节点
  49. //因为最后一个节点中存储下一个节点地址的部分为NULL,故可以遍历链表直到找到该部分为NULL为止
  50. SLN* pmove = *pphead;//防止丢失链表的第一个节点
  51. //使用next作为终止条件,而不是pmove为终止条件,当pmove为终止条件时,即pmove = NULL,会使循环多进行一次
  52. while (pmove->next)
  53. {
  54. pmove = pmove->next;
  55. }
  56. pmove->next = newnode;
  57. }
  58. //链表的头部删除
  59. void SLNPopFront(SLN** pphead)
  60. {
  61. assert(pphead);
  62. //判断链表是否为空链表
  63. //如果为空链表则不执行删除操作
  64. if (*pphead == NULL)
  65. {
  66. return;
  67. }
  68. SLN* BlocktoFree = *pphead;
  69. //如果链表不为空时,执行删除操作
  70. //释放完空间后首节点需要改变
  71. *pphead = (*pphead)->next;
  72. //删除节点需要释放开辟的内存空间
  73. //不可free(*pphead),因为*pphead已经指向了下一个节点,此时free会把下一个节点释放
  74. free(BlocktoFree);
  75. BlocktoFree = NULL;
  76. }
  77. //链表的尾部删除
  78. void SLNPopBack(SLN** pphead)
  79. {
  80. assert(pphead);
  81. //删除之前需要判断链表是不是空链表
  82. //如果是空链表则不执行删除操作
  83. if (*pphead == NULL)
  84. {
  85. return;
  86. }
  87. //如果链表只有一个节点时,则没有前一节点,直接释放空间即可
  88. if ((*pphead)->next == NULL)
  89. {
  90. free(*pphead);
  91. *pphead = NULL;
  92. return;
  93. }
  94. //如果链表不为空且有多个节点时,执行删除操作
  95. //先找到尾结点
  96. SLN* p_end = *pphead;
  97. SLN* pprev = p_end;
  98. while (p_end->next)
  99. {
  100. pprev = p_end;//找到倒数第二个节点位置
  101. p_end = p_end->next;//将尾指针指向最后一个节点
  102. }
  103. //将倒数第二个节点的存储下一个节点地址的部分置为空
  104. pprev->next = NULL;
  105. //释放尾结点的空间
  106. free(p_end);
  107. p_end = NULL;
  108. }
  109. //查找链表中的数据
  110. SLN* SLNFind(SLN* phead, SListDataType x)
  111. {
  112. assert(phead);
  113. SLN* pcur = phead;
  114. //遍历链表数据
  115. while (pcur)
  116. {
  117. //找到数据返回数据所在节点的地址
  118. if (pcur->data == x)
  119. {
  120. return pcur;
  121. }
  122. pcur = pcur->next;
  123. }
  124. //未找到数据返回空指针
  125. return NULL;
  126. }
  127. //在指定位置之前(在指定位置)插入数据
  128. void SLNInsert(SLN** pphead, SLN* pos, SListDataType x)
  129. {
  130. assert(pphead);
  131. //插入的位置不可以为空,否则插入无效
  132. assert(pos);
  133. //插入的链表不能为空,否则不存在pos所指向的位置并且确保pos不是野指针
  134. assert(*pphead);
  135. //开辟空间
  136. //在指定位置之前插入数据时,需要考虑该位置是不是头结点
  137. //如果需要插入的位置为头结点的位置,则相当于是头部插入操作
  138. if (pos == *pphead)
  139. {
  140. SLNPushFront(pphead, x);
  141. return;
  142. }
  143. //如果需要插入的位置不是头结点位置,则进行后续插入操作
  144. SLN* newnode = applySpace(x);
  145. SLN* pprev = *pphead;
  146. //遍历链表找到pos指向的位置
  147. while (pprev->next != pos)
  148. {
  149. pprev = pprev->next;
  150. }
  151. //更改前一节点指针的存储下一节点位置部分的地址为新节点的地址
  152. pprev->next = newnode;
  153. //更改新节点的存储下一节点部分的地址为pos
  154. newnode->next = pos;
  155. //上述两步可以交换执行顺序
  156. }
  157. //在指定位置之后插⼊数据
  158. void SLNInsertAfter(SLN* pos, SListDataType x)
  159. {
  160. assert(pos);
  161. //指定位置之后插入数据时注意节点连接顺序
  162. SLN* newnode = applySpace(x);
  163. //将新节点中的存储下一节点地址的部分赋值为pos后一节点的地址
  164. newnode->next = pos->next;
  165. //将pos节点中的存储下一节点地址的部分赋值为新节点的地址
  166. pos->next = newnode;
  167. //上述两步不可以交换执行顺序
  168. }
  169. //删除指定位置的节点
  170. void SLNErase(SLN** pphead, SLN* pos)
  171. {
  172. assert(pphead);
  173. assert(pos);
  174. //确保链表不为空
  175. assert(*pphead);
  176. //如果需要删除的节点是头结点,则执行头部删除操作
  177. if (pos == *pphead)
  178. {
  179. SLNPopFront(pphead);
  180. return;
  181. }
  182. //如果删除的节点不是头结点,则执行后续操作
  183. //遍历链表找到pos位置之前的一个节点
  184. SLN* pprev = *pphead;
  185. while (pprev->next != pos)
  186. {
  187. pprev = pprev->next;
  188. }
  189. pprev->next = pprev->next->next;
  190. //释放pos位置空间
  191. free(pos);
  192. pos = NULL;
  193. }
  194. //删除pos之后的节点
  195. void SLNEraseAfter(SLN* pos)
  196. {
  197. assert(pos);
  198. //如果pos之后的节点为空,则不执行删除操作
  199. if (pos->next == NULL)
  200. {
  201. return;
  202. }
  203. SLN* BlockToFree = pos->next;
  204. //如果pos之后的节点不为空,执行删除操作
  205. //将pos节点的存储下一个节点地址的部分更改为待删除节点的后一个节点地址
  206. pos->next = pos->next->next;
  207. free(BlockToFree);
  208. BlockToFree = NULL;
  209. }
  210. //销毁链表
  211. void SLNDesTroy(SLN** pphead)
  212. {
  213. assert(pphead);
  214. SLN* pcur = *pphead;
  215. //循环销毁链表
  216. while (pcur)
  217. {
  218. SLN* BlockToFree = pcur;
  219. pcur = pcur->next;
  220. free(BlockToFree);
  221. }
  222. *pphead = NULL;
  223. }

双链表

双向链表:即带头双向循环链表

在双向链表中,存在头结点,即默认有一个节点,但是该节点中不存储有效数据,只存储指向上一个节点和下一个节点的地址

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小蓝xlanll/article/detail/470587
推荐阅读
相关标签