当前位置:   article > 正文

链表入门:“单链表“的基本操作详解(C语言)_单链表的基本操作

单链表的基本操作

目录

一,了解链表

二,基本操作的实现

1.  在代码开头的预处理和声明

2.  对链表进行初始化

一个错误案例的分析:

3.  对链表进行“增”操作

(1) “头插法”在链表头结点之后插入结点

(2) “尾插法”在链表的最后一个结点后插入结点

(3) 在指定位置插入结点

3,对链表进行“删”操作

 (1) 从链表中删除第 i 个元素

 (2) 销毁单链表

4.  对链表进行“查”操作

(1) 打印链表中的元素

(2) 获取链表中元素的个数

(3) 在单链表中查找元素e的位置

 (4) 在单链表中获取 i 位置的元素

5.  对链表进行“改”操作

三,整体的实现和效果


一,了解链表

链式存储结构——借助指示元素存储地址的指针表示数据

                             元素间的逻辑关系:(逻辑相邻,物理不一定相邻)

链表是由一系列结点(链表中每一个元素称为结点)组成,每个结点包括两个部分:数据域(存储本结点的数据元素)和指针域(存储下一个结点的地址)。

由于链表在运行时可以动态生成结点,所以链表相比于数组,具有动态分配内存、方便插入和删除、节省空间等优点。

链表按照节点的连接方式可以分为单链表、双向链表和循环链表三种类型。这里我们只讨论单链表。

一些概念的了解

  • 头结点(在链表的首元结点之前附设的一个结点)是用来辅助链表操作的,它本身并不算作链表的节点,因此在统计链表长度时需要将头结点去掉。头结点的数据域通常不赋值
  • 头指针:指向第一个结点(链表有头结点的时候就是头结点)
  • 空链表:链表中无元素(有头结点)
  •  下图是一个带有头结点的单链表 

二,基本操作的实现

对数据的进行的操作基本就是“增删查改”。

1.  在代码开头的预处理和声明

  1. #include<stdio.h>
  2. #include<stdlib.h>
  3. typedef struct node
  4. {
  5. int data; //数据域
  6. struct node* next; //指针域
  7. }LinkNode;

如果你使用的是Visual Studio进行编写的代码,请在第一行添加:

#define _CRT_SECURE_NO_WARNINGS

2.  对链表进行初始化

  1. LinkNode* InitList()
  2. {
  3. //创建一个结构体指针并进行分配空间
  4. LinkNode* temp = (LinkNode*)malloc(sizeof(LinkNode));
  5. //将结构体指针的指针域赋值为NULL
  6. temp->next = NULL;
  7. //返回初始化完成的结构体指针
  8. return temp;
  9. }
  •  malloc向内存申请一块连续可用的空间,开辟成功会返回指向这块空间的指针(类型为void*), 开辟失败返回NULL, 所以得判断是否开辟成功并对指向空间的指针的类型的强制转换。

 在main函数中创建指向结构体L的指针来接收已经初始化完成的链表(只有头结点)。

  1. //创建一个指向结构体的指针来记录头结点
  2. LinkNode* L = InitList();

一个错误案例的分析:

对下面错误的代码进行分析:

  1. void InitList(LinkNode* pL){
  2. pL = (LinkNode*)malloc(sizeof(LinkNode));
  3. pL->next = NULL;
  4. }
  5. int main()
  6. {
  7. LinkNode L;//创建一个结构体变量
  8. InitList(&L);//初始化链表
  9. //...
  10. return 0;
  11. }

在main函数中创建了一个L结构体变量(L已经有了自己的地址),将L的地址传递给InitList()函数,InitList()函数内接收一个指向结构体变量L的指针pL。

  • 使用malloc开辟空间会返回一块空间的地址,而函数中 pL 是一个记录着 结构体变量L 地址的指针。如果把 开辟的空间地址 给到 pL ,则 pL 就指向了为malloc开辟的大小为sizeof(LinkNode)的空间的地址,此时pL不再记录的是结构体变量L的地址,后面的 pL->next = NULL;也只修改pL这个指针指向的空间的变量。当执行走出函数体后,pL这个指针就会被销毁,在main函数中,L还是未初始化。
  • 上面例子的正确修改: 把InitList函数中的 malloc开辟空间赋值给pL 的代码去掉

LinkNode* L    指向链表的某一个结点。(只能修改所指向结点中的内容)

LinkNode** L  指向记录链表地址的指针,可以修改链表。(可以改变一级指针的地址)

一级指针记录变量的地址,二级指针记录一级指针的地址。

一级指针只能修改所记录变量的内容,二级指针可以修改一级指针的内容(即一级指针的地址)。

3.  对链表进行“增”操作

(1) “头插法”在链表头结点之后插入结点

1. 从一个空表开始,重复读入数据:
2.生成新结点,将读入数据存放到新结点的数据域中3. 从最后一个结点开始,依次将各结点插入到链表的前端

  1. void CreateLink_H()
  2. {
  3. //创建一个带头结点的单链表
  4. LinkNode* L = (LinkNode*)malloc(sizeof(LinkNode));
  5. L->next = NULL;
  6. int n = 3, e = 0;//n为创建的新结点个数,e为结点的数据
  7. for (int i; i < n; i++)
  8. {
  9. //生成新结点P
  10. LinkNode* P = (LinkNode*)malloc(sizeof(LinkNode));
  11. //给新结点P的数据域赋值
  12. printf("请输入新结点元素e的值:\n");
  13. scanf("%d", &e);
  14. P->data = e;
  15. //将新结点插入表头
  16. P->next = L->next;
  17. L->next = P;
  18. }
  19. }

(2) “尾插法”在链表的最后一个结点后插入结点

1.从一个空表L开始,将新结点逐个插入到链表的尾部,尾指针r指向链表的尾结点。
2.初始时,r同L均指向头结点。每读入一个数据元素则申请一个新结点,将新结点插入到尾结点后,r指向新结点。

  1. void CreateLink_R(LinkNode* L)
  2. {
  3. //创建一个带头结点的单链表
  4. LinkNode* L = (LinkNode*)malloc(sizeof(LinkNode));
  5. L->next = NULL;
  6. //创建一个尾指针指向头结点(空链表中的头就是尾)
  7. LinkNode* r = L;
  8. int n = 3, e = 0;//n为创建的新结点个数,e为结点的数据
  9. for (int i; i < n; i++)
  10. {
  11. //创建一个新结点
  12. LinkNode* P = (LinkNode*)malloc(sizeof(LinkNode));
  13. //给新结点P的数据域赋值
  14. printf("请输入新结点元素e的值:\n");
  15. scanf("%d", &e);
  16. P->data = e;
  17. //将新结点插入表尾
  18. P->next = NULL;
  19. r->next = P;
  20. //r指向新的尾结点
  21. r = P;
  22. }
  23. }

(3) 在指定位置插入结点

  • 在 i 位置插入新元素, 需要找到第 i-1 个结点, 将第 i-1 个结点的指针域赋值给新结点P的指针域,然后将第 i-1 个结点的next赋值为新结点P的地址。
    1. int LinkInsert(LinkNode* L, int i, int e)
    2. {
    3. //L为记录头结点的指针, i为插入位置, e为插入元素
    4. LinkNode* temp = L;
    5. int count = 0; //默认长度为0
    6. while (temp->next && count < i - 1)
    7. {
    8. //寻找第i个结点,并使temp指向第i-1个结点
    9. temp = temp->next;
    10. count++;
    11. }
    12. if (count < i - 1)
    13. {
    14. ///第i个位置不存在,则进行提示并返回0
    15. printf(">>>访问越界,请重试!\n");
    16. return 0;
    17. }
    18. //在链表的第i个位置插入新结点P
    19. LinkNode* P = (LinkNode*)malloc(sizeof(LinkNode));
    20. P->data = e;
    21. P->next = temp->next; //将temp下一个结点给P的地址域
    22. temp->next = P; //将P的地址赋值给temp的地址域
    23. return 1; //成功插入则返回1
    24. }

 分析:

  1. int count = 0;
  2. while (temp->next && count < i - 1)
  3. {
  4. temp = temp->next;
  5. count++;
  6. }
  • temp->next 判断当前结点的下一个结点是否存在,如果不存在说明已经到达链表末尾。
  • count < i - 1 判断遍历过的结点个数是否小于 i - 1,即判断是否找到了第 i - 1 个结点。
  • 2个条件一起使用可以确保 temp 指针移动到第 i-1个结点的位置(i-1不大于表长时),并避免访问空指针导致的错误。

  1. if (count < i - 1)
  2. {
  3. ///第i个位置不存在,则进行提示并返回0
  4. printf(">>>访问越界,请重试!\n");
  5. return 0;
  6. }

    在上面循环的while中已经根据 i 对count的值进行计数,count最大时等于链表长度,
    当 i-1 大于链表长度时,则无法访问到 i 结点(没有 i 这个结点). 则进行提示并返回。

·要注意的是:①结点的指针域记录着②结点的地址,所以不可以先把新结点的地址赋值给①结点(temp->next = P;)

而是先把②结点的地址赋给P的指针域,再把P的地址赋值给①结点的指针域,才能正确链接.

(P->next = temp->next; temp->next = P; )

3,对链表进行“删”操作

 (1) 从链表中删除第 i 个元素

  • 对第 i 个结点进行删除, 需要找到第 i -1 个结点, 将其指针域更改为第 i+1 个结点的地址, 然后再对第 i 个结点进行删除。
    1. int LinkDelect(LinkNode* L, int i) {
    2. LinkNode* temp = L;
    3. int count = 0;
    4. while (temp->next && count < i - 1)
    5. {
    6. //寻找第i个结点,并使temp指向第i-1个结点
    7. temp = temp->next;
    8. count++;
    9. }
    10. if (!(temp->next) || count < i - 1)
    11. {
    12. ///要删除的位置不存在,则进行提示并返回0
    13. printf(">>>删除位置不合理,请重试!\n");
    14. return 0;
    15. }
    16. LinkNode* P = temp->next; //临时存储要删除结点的地址,用于后续释放
    17. temp->next = P->next;
    18. free(P); //释放被删除的地址
    19. }

 分析:

  •  其中 while 遍历部分, 和 if 判断部分和上面插入的功能是一样的。

  1. LinkNode* P = temp->next;
  2. temp->next = P->next;
  • 这一部分代码就是将要删除结点( i )的后继结点的地址赋值给 i 的前驱结点的地址域, 当 i 结点删除后,也可以通过 i 的前驱结点内的地址域去访问到 i 的后继结点。
  • 在删除某个结点以后,需要将其原先的地址进行释放(防止内存泄漏), 上述代码使用指针P记录了要删除结点( i )的地址, 防止删除结点后地址丢失。
    free(P);	//释放被删除的地址

 (2) 销毁单链表

  1. void LinkDestroy(LinkNode* L)
  2. {
  3. LinkNode* P;
  4. while (L)//当L为NULL时停止
  5. {
  6. P = L;
  7. L = L->next;
  8. free(P);
  9. }
  10. }
  •  创建一个指针P来指向L当前的结点
  • 然后L->next指向下一个结点
  • 删除P所指向的地址
  • 然后再循环执行上述过程,直到L指向NULL(空结点)

4.  对链表进行“查”操作

(1) 打印链表中的元素

开头先创建一个结构体指针指向头结点,通过循环遍历来获取每一个结点的数据并打印。

  1. void LinkPrint(LinkNode* L)
  2. {
  3. LinkNode* P = L->next;
  4. printf(">链表中的元素为:");
  5. while (P)
  6. {
  7. printf(" %d", P->data);
  8. P = P->next;
  9. }
  10. printf("\n");
  11. }

(2) 获取链表中元素的个数

使用计数器思想即可解决!最后记得返回计数器len(返回类型为int)。

  1. int LinkLenght(LinkNode* L)
  2. {
  3. LinkNode* temp = L;
  4. int len = 0; //默认长度为0
  5. while (temp->next)
  6. {
  7. temp = temp->next;
  8. len++; //遍历得到链表长度
  9. }
  10. return len;
  11. }

(3) 在单链表中查找元素e的位置

整体思路和上面一样。

当指针P遍历找到数据e时,则会退出循环;反之,指针P找不到元素e则会循环到P=NULL。退出循环后进行判断,P为非空指针则表示找到数据e,返回其位置,P为空指针则返回0。

  1. int LocationElem(LinkNode* L, int e)
  2. {
  3. LinkNode* P = L;
  4. int index = 0;
  5. while (P && P->data != e)
  6. {
  7. P = P->next;
  8. index++;
  9. }
  10. if (P)
  11. return index; //返回数据e的位置
  12. else
  13. return 0; //找不到则返回0
  14. }

 (4) 在单链表中获取 i 位置的元素

整体思路和上面一样。

需要注意的是:输入的 i 小于1(不合理的值)和P为NULL时返回-1

  1. int GetElem(LinkNode* L, int i)
  2. {
  3. LinkNode* P = L->next;//从第一个结点开始
  4. int count = 1;
  5. while (P && count < i)
  6. {
  7. P = P->next;
  8. count++;
  9. }
  10. if (!P || count > i)
  11. {
  12. //没找到,或者i的值不合理(小于1)则返回-1
  13. return -1;
  14. }
  15. //找到则返回对应的元素值
  16. return P->data;
  17. }

5.  对链表进行“改”操作

(1),修改第i个结点的数据

理解上面的代码后,这个基本差不多,就不写了  :3

三,整体的实现和效果

下面代码中没有:1,头插法创建链表;2,尾插法创建链表;3,“查找元素e的位置”

  1. #define _CRT_SECURE_NO_WARNINGS
  2. #include<stdio.h>
  3. #include<stdlib.h>
  4. typedef struct node
  5. {
  6. int data; //数据域
  7. struct node* next; //指针域
  8. }LinkNode;
  9. LinkNode* InitList()
  10. {
  11. //初始化一个带头结点的链表
  12. LinkNode* temp = (LinkNode*)malloc(sizeof(LinkNode));
  13. temp->next = NULL;
  14. return temp;
  15. }
  16. int LinkEmpty(LinkNode* L)
  17. {
  18. if (L)
  19. {
  20. return 1;
  21. }
  22. return 0;
  23. }
  24. int LinkInsert(LinkNode* L, int i, int e)
  25. {
  26. //L为记录头结点的指针, i为插入位置, e为插入元素
  27. LinkNode* temp = L;
  28. int count = 0; //默认长度为0
  29. while (temp->next && count < i - 1)
  30. //确保 temp 指针移动到第 i-1 个结点的位置,并避免访问空指针导致的错误。
  31. {
  32. //寻找第i个结点,并使temp指向第i-1个结点
  33. temp = temp->next;
  34. count++;
  35. }
  36. //上面循环中已经根据i对count的值进行计数,count最大时等于链表长度,
  37. //当i-1大于链表长度时,则无法访问i结点(没有i这个结点)
  38. if (!(temp) || count < i - 1)
  39. {
  40. ///第i个位置不存在,则进行提示并返回0
  41. printf(">>>访问越界,请重试!\n");
  42. return 0;
  43. }
  44. //在链表的第i个位置插入新结点P
  45. LinkNode* P = (LinkNode*)malloc(sizeof(LinkNode));
  46. P->data = e;
  47. P->next = temp->next; //将temp下一个结点给P的地址域
  48. temp->next = P; //将P的地址赋值给temp的地址域
  49. return 1; //成功插入则返回1
  50. }
  51. void LinkPrint(LinkNode* L)
  52. {
  53. LinkNode* P = L->next;
  54. printf(">链表中的元素为:");
  55. while (P)
  56. {
  57. printf(" %d", P->data);
  58. P = P->next;
  59. }
  60. printf("\n");
  61. }
  62. int LinkLenght(LinkNode* L)
  63. {
  64. LinkNode* temp = L;
  65. int len = 0; //默认长度为0
  66. while (temp->next)
  67. {
  68. temp = temp->next;
  69. len++; //遍历得到链表长度
  70. }
  71. return len;
  72. }
  73. int LinkDelect(LinkNode* L, int i) {
  74. LinkNode* temp = L;
  75. int count = 0;
  76. while (temp->next && count < i - 1)
  77. {
  78. //寻找第i个结点,并使temp指向第i-1个结点
  79. temp = temp->next;
  80. count++;
  81. }
  82. if (!(temp->next) || count < i - 1)
  83. {
  84. ///第i个位置不存在,则进行提示并返回0
  85. printf(">删除位置不合理,请重试!\n");
  86. return 0;
  87. }
  88. LinkNode* P = temp->next;//临时存储要删除结点的地址,用于后续释放
  89. temp->next = P->next;
  90. free(P); //释放被删除的地址
  91. }
  92. void LinkDestroy(LinkNode* L)
  93. {
  94. LinkNode* P;
  95. while (L)//当L为NULL时停止
  96. {
  97. P = L;
  98. L = L->next;
  99. free(P);
  100. }
  101. }
  102. int main()
  103. {
  104. //创建一个结构体变量指针
  105. LinkNode* L = InitList();
  106. printf("^链表初始化成功.\n");
  107. printf("\n");
  108. //判断链表是否为空
  109. int flag = LinkEmpty(L);
  110. flag ? printf(">>>链表不为空\n") : printf(">>>链表为空\n");
  111. printf("\n");
  112. //插入新元素 + 打印链表中的元素
  113. LinkInsert(L, 1, 11);//L,i,e
  114. LinkPrint(L);
  115. LinkInsert(L, 2, 12);
  116. LinkPrint(L);
  117. LinkInsert(L, 3, 13);
  118. LinkPrint(L);
  119. LinkInsert(L, 4, 14);
  120. LinkPrint(L);
  121. LinkInsert(L, 3, 3);
  122. LinkPrint(L);
  123. LinkInsert(L, 6, 5);
  124. LinkPrint(L);
  125. LinkInsert(L, 9, 5);//越界访问
  126. LinkPrint(L);
  127. printf("\n");
  128. //求链表中元素个数
  129. int len = LinkLenght(L);
  130. printf(">>>链表的元素个数为: %d\n", len);
  131. printf("\n");
  132. //从链表中删除第i个元素 并打印
  133. LinkDelect(L, 2);
  134. LinkPrint(L);
  135. LinkDelect(L, 5);
  136. LinkPrint(L);
  137. printf("\n");
  138. //销毁单链表
  139. LinkDestroy(L);
  140. return 0;
  141. }
上方代码实现效果

如果文章有错误的地方,请帮忙指出进行改正,谢谢!  

栈:链栈和顺序栈的实现icon-default.png?t=N7T8https://blog.csdn.net/Mzyh_c/article/details/135180651?spm=1001.2014.3001.5501

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

闽ICP备14008679号