当前位置:   article > 正文

数据结构之双链表的相关知识点及应用

数据结构之双链表的相关知识点及应用

 找往期文章包括但不限于本期文章中不懂的知识点:

个人主页我要学编程(ಥ_ಥ)-CSDN博客

所属专栏数据结构

目录

双链表的实现 

初始化双链表 

在双链表中尾插数据 

在双链表中尾删数据

在双链表中头插数据 

在双链表中头删数据 

在双链表中的指定位置之后插入数据 

在双链表中删除指定位置的数据

在双链表中查找指定位置

销毁双链表 

双链表源码 


学习完单链表后,就要开始学习链表中最重要的双链表了。

双链表是双向带头循环链表。与单链表是恰恰相反。

接下来就用双链表来实现一系列增删查改的功能。 

双链表的实现 

 在创建双链表之前,还得要做一些提起准备。创建三个文件:List.h  List.c  test.c  前面两个是实现双链表的,后面的 test.c 文件是测试双链表的各种功能。同样链表是由一个一个的节点组成的。我们就得由节点的结构。

创建节点:

  1. typedef int LTDataType;
  2. typedef struct ListNode
  3. {
  4. struct ListNode* next; //指针保存下⼀个节点的地址
  5. struct ListNode* prev; //指针保存前⼀个节点的地址
  6. LTDataType data;
  7. }LTNode;

初始化双链表 

因为双链表带头,因此,我们就得创建一个哨兵位。这个函数也可以叫做初始化函数。

  1. //初始化
  2. void LTInit(LTNode** pphead)//注意这里需要改变哨兵位,因此用二级指针接收
  3. {
  4. //创建一个新的节点
  5. LTNode* phead = (LTNode*)malloc(sizeof(LTNode));
  6. if (phead == NULL)
  7. {
  8. perror("malloc:");
  9. exit(1);
  10. }
  11. //把哨兵位的数据初始化为-1(无效数据),后驱指针指向自己,前驱指针指向自己
  12. *pphead = phead;
  13. (*pphead)->data = -1;
  14. (*pphead)->next = (*pphead)->prev = *pphead;
  15. }

只要是增加节点,就会有重复的代码因此我们分装成一个函数,并且我们在初始化函数也可以传我们想要设置的无效数据。

  1. //增加节点
  2. LTNode* LTBuyNode(LTDataType x)
  3. {
  4. LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
  5. if (newnode == NULL)
  6. {
  7. perror("malloc:");
  8. exit(1);
  9. }
  10. newnode->data = x;
  11. newnode->next = newnode->prev = newnode;
  12. return newnode;
  13. }
  1. //初始化
  2. void LTInit(LTNode** pphead)
  3. {
  4. *pphead = LTBuyNode(-1);
  5. }

在双链表中尾插数据 

情况一:链表中只有哨兵位:

情况二:链表中不止有哨兵位:

我们要尾插数据,就是要d3的next指针的指向,还要改变head的prev指针的指向。此外还得把新增加的节点prev指针指向d3,next指向head。

不能改变顺序的原因:如果改变了,就先把哨兵位的指向改变了,后面我们就找不到原链表的尾节点了,除非能把原链表的尾节点的地址提前存起来。

  1. //尾插数据
  2. void LTPushBack(LTNode* phead, LTDataType x)//哨兵位已经确定,不再改变,因此用一级指针
  3. {
  4. assert(phead);//链表不能为空
  5. LTNode* newnode = LTBuyNode(x);
  6. //开始尾插节点
  7. //链表中只有哨兵位
  8. if (phead->next == phead)
  9. {
  10. //先把新节点安排好
  11. newnode->next = phead;
  12. newnode->prev = phead;
  13. //哨兵位
  14. phead->next = newnode;
  15. phead->prev = newnode;
  16. }
  17. else
  18. {
  19. //先把新节点安排好
  20. newnode->next = phead;
  21. newnode->prev = phead->prev;
  22. //头节点:phead 尾节点:phead->prev 新节点:newnode
  23. phead->prev->next = newnode;
  24. phead->prev = newnode;
  25. }
  26. }

写完之后,还得测试一下我们所写的代码是否正确:可以用打印函数来判断看看结果是否和我们的预期一样。

  1. //打印数据
  2. void LTPrint(LTNode* phead)
  3. {
  4. //遍历寻找
  5. LTNode* pcur = phead->next;//头节点的数据是无效的,因此就不需要打印
  6. //因为是循环的,所以不能用空指针来判断,要看看是否指向的哨兵位
  7. while (pcur != phead)
  8. {
  9. printf("%d->", pcur->data);
  10. pcur = pcur->next;
  11. }
  12. printf("\n");
  13. }

在双链表中尾删数据

情况一:链表中由多个有效数据:

情况二:链表中只有一个有效数据:

  1. //尾删数据
  2. void LTPopBack(LTNode* phead)
  3. {
  4. assert(phead && phead->next != phead);//链表不能为空并且链表中不能没有有效元素
  5. if (phead->next->next == phead)//只有一个有效数据
  6. {
  7. LTNode* freenode = phead->next;
  8. free(freenode);
  9. phead->next = phead;
  10. phead->prev = phead;
  11. }
  12. else
  13. {
  14. phead->prev->prev->next = phead;
  15. LTNode* freenode = phead->prev;
  16. phead->prev = phead->prev->prev;
  17. free(freenode);
  18. freenode = NULL;
  19. }
  20. }

其实上面的写法可以简化为:

  1. //尾删数据
  2. void LTPopBack(LTNode* phead)
  3. {
  4. assert(phead && phead->next != phead);//链表不能为空并且链表中不能没有有效元素
  5. phead->prev->prev->next = phead;
  6. LTNode* freenode = phead->prev;
  7. phead->prev = phead->prev->prev;
  8. free(freenode);
  9. freenode = NULL;
  10. }

也就是说不管链表的有效元素的个数有多少,都不影响。(把特殊情况带入这个简化版里判断就可以了)。

在双链表中头插数据 

情况一:链表中不止有哨兵位: 

之所以在哨兵位的前面插入数据叫作尾插,是因为双链表是循环的,当遍历到d3后就会找到前面的节点,这就是在尾部,因此也叫作尾插。 

同样顺序不能变的原因也是因为顺序一旦改变,先把phead的next指向给改变了,就找不到了要更改的数据了。

情况二:链表中只有哨兵位:

  1. //头插数据
  2. void LTPushFront(LTNode* phead, LTDataType x)
  3. {
  4. assert(phead);
  5. LTNode* newnode = LTBuyNode(x);
  6. if (phead->next == phead)//只有哨兵位
  7. {
  8. newnode->next = phead;
  9. newnode->prev = phead;
  10. phead->next = newnode;
  11. phead->prev = newnode;
  12. }
  13. else
  14. {
  15. //先安排新节点
  16. newnode->next = phead->next;
  17. newnode->prev = phead;
  18. //头节点:phead 尾节点(相较于新节点):phead->prev 新节点:newnode
  19. phead->next->prev = newnode;
  20. phead->next = newnode;
  21. }
  22. }

这个头插也是可以简化的:

  1. //头插数据
  2. void LTPushFront(LTNode* phead, LTDataType x)
  3. {
  4. assert(phead);
  5. LTNode* newnode = LTBuyNode(x);
  6. //先安排新节点
  7. newnode->next = phead->next;
  8. newnode->prev = phead;
  9. //头节点:phead 尾节点(相较于新节点):phead->prev 新节点:newnode
  10. phead->next->prev = newnode;
  11. phead->next = newnode;
  12. }

简化判断的方法就是把特殊情况带入进去,看看能否成功。

在双链表中头删数据 

  1. //头删数据
  2. void LTPopFront(LTNode* phead)
  3. {
  4. assert(phead && phead->next != phead);//链表不为空并且链表的有效数据不能为空
  5. phead->next->next->prev = phead;
  6. LTNode* freenode = phead->next;
  7. phead->next = phead->next->next;
  8. free(freenode);
  9. freenode = NULL;
  10. }

在双链表中的指定位置之后插入数据 

情况一:pos是哨兵位:

情况二:pos是中间位置:

情况三:pos是尾节点: 

上面的代码不管是在哪个位置都满足。

  1. //在pos位置之后插⼊数据
  2. void LTInsert(LTNode* pos, LTDataType x)
  3. {
  4. assert(pos);
  5. LTNode* newnode = LTBuyNode(x);
  6. //先安排好新节点
  7. newnode->prev = pos;
  8. newnode->next = pos->next;
  9. pos->next = newnode;
  10. pos->next->prev = newnode;
  11. }

在双链表中删除指定位置的数据

 情况一:pos在中间位置

情况二:pos在结尾位置:

注意:这个指定位置不能是哨兵位。

  1. //在pos位置删除数据
  2. void LTErase(LTNode* pos)
  3. {
  4. assert(pos);
  5. pos->next->prev = pos->prev;
  6. pos->prev->next = pos->next;
  7. free(pos);
  8. }

在双链表中查找指定位置

这个也比较简单,就是直接遍历整个双链表,如果没找到就返回NULL,找到就返回这个地址。

  1. //查找指定数据
  2. LTNode* LTFind(LTNode* phead, LTDataType x)
  3. {
  4. assert(phead && phead->next != phead);//链表不能为空并且链表不能只有哨兵位
  5. LTNode* pcur = phead->next;
  6. while (pcur != phead)
  7. {
  8. if (pcur->data == x)
  9. {
  10. return pcur;
  11. }
  12. pcur = pcur->next;
  13. }
  14. return NULL;
  15. }

销毁双链表 

就是把链表中的节点一个一个的释放空间就行了。

  1. //销毁链表
  2. void LTDestroy(LTNode* phead)
  3. {
  4. assert(phead);
  5. //就是节点一个一个的销毁
  6. LTNode* pcur = phead->next;
  7. while (pcur != phead)
  8. {
  9. LTNode* next = pcur->next;
  10. free(pcur);
  11. pcur = next;
  12. }
  13. free(pcur);
  14. pcur = NULL;
  15. }

综合上面的代码来看,还有一个地方有点小瑕疵,就是初始化链表时,我们用的是二级指针,为了保持接口一致性,我们要用一级指针或者不传参数。

  1. //初始化
  2. LTNode* LTInit()
  3. {
  4. LTNode* pplist = LTBuyNode(-1);
  5. return pplist;
  6. }

双链表源码 

下面就是双链表完整的源码:

List.c

  1. #include "List.h"
  2. //增加节点
  3. LTNode* LTBuyNode(LTDataType x)
  4. {
  5. LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
  6. if (newnode == NULL)
  7. {
  8. perror("malloc:");
  9. exit(1);
  10. }
  11. newnode->data = x;
  12. newnode->next = newnode->prev = newnode;
  13. return newnode;
  14. }
  15. //初始化
  16. LTNode* LTInit()
  17. {
  18. LTNode* pplist = LTBuyNode(-1);
  19. return pplist;
  20. }
  21. //尾插数据
  22. void LTPushBack(LTNode* phead, LTDataType x)
  23. {
  24. assert(phead);
  25. LTNode* newnode = LTBuyNode(x);
  26. //开始尾插节点
  27. //链表中只有哨兵位
  28. if (phead->next == phead)
  29. {
  30. //先把新节点安排好
  31. newnode->next = phead;
  32. newnode->prev = phead;
  33. //哨兵位
  34. phead->next = newnode;
  35. phead->prev = newnode;
  36. }
  37. else
  38. {
  39. //先把新节点安排好
  40. newnode->next = phead;
  41. newnode->prev = phead->prev;
  42. //头节点:phead 尾节点:phead->prev 新节点:newnode
  43. phead->prev->next = newnode;
  44. phead->prev = newnode;
  45. }
  46. }
  47. //打印数据
  48. void LTPrint(LTNode* phead)
  49. {
  50. //遍历寻找
  51. LTNode* pcur = phead->next;//头节点的数据是无效的,因此就不需要打印
  52. //因为是循环的,所以不能用空指针来判断,要看看是否指向的哨兵位
  53. while (pcur != phead)
  54. {
  55. printf("%d->", pcur->data);
  56. pcur = pcur->next;
  57. }
  58. printf("\n");
  59. }
  60. //尾删数据
  61. void LTPopBack(LTNode* phead)
  62. {
  63. assert(phead && phead->next != phead);//链表不能为空并且链表中不能没有有效元素
  64. phead->prev->prev->next = phead;
  65. LTNode* freenode = phead->prev;
  66. phead->prev = phead->prev->prev;
  67. free(freenode);
  68. freenode = NULL;
  69. }
  70. //头插数据
  71. void LTPushFront(LTNode* phead, LTDataType x)
  72. {
  73. assert(phead);
  74. LTNode* newnode = LTBuyNode(x);
  75. //先安排新节点
  76. newnode->next = phead->next;
  77. newnode->prev = phead;
  78. //头节点:phead 尾节点(相较于新节点):phead->prev 新节点:newnode
  79. phead->next->prev = newnode;
  80. phead->next = newnode;
  81. }
  82. //头删数据
  83. void LTPopFront(LTNode* phead)
  84. {
  85. assert(phead && phead->next != phead);//链表不为空并且链表的有效数据不能为空
  86. phead->next->next->prev = phead;
  87. LTNode* freenode = phead->next;
  88. phead->next = phead->next->next;
  89. free(freenode);
  90. freenode = NULL;
  91. }
  92. //在pos位置之后插⼊数据
  93. void LTInsert(LTNode* pos, LTDataType x)
  94. {
  95. assert(pos);
  96. LTNode* newnode = LTBuyNode(x);
  97. //先安排好新节点
  98. newnode->prev = pos;
  99. newnode->next = pos->next;
  100. pos->next = newnode;
  101. pos->next->prev = newnode;
  102. }
  103. //在pos位置删除数据
  104. void LTErase(LTNode* pos)
  105. {
  106. assert(pos);
  107. pos->next->prev = pos->prev;
  108. pos->prev->next = pos->next;
  109. free(pos);
  110. pos = NULL;
  111. }
  112. //查找指定数据
  113. LTNode* LTFind(LTNode* phead, LTDataType x)
  114. {
  115. assert(phead && phead->next != phead);//链表不能为空并且链表不能只有哨兵位
  116. LTNode* pcur = phead->next;
  117. while (pcur != phead)
  118. {
  119. if (pcur->data == x)
  120. {
  121. return pcur;
  122. }
  123. pcur = pcur->next;
  124. }
  125. return NULL;
  126. }
  127. //销毁链表
  128. void LTDestroy(LTNode* phead)
  129. {
  130. assert(phead);
  131. //就是节点一个一个的销毁
  132. LTNode* pcur = phead->next;
  133. while (pcur != phead)
  134. {
  135. LTNode* next = pcur->next;
  136. free(pcur);
  137. pcur = next;
  138. }
  139. free(pcur);
  140. pcur = NULL;
  141. }

List.h

  1. #include <stdio.h>
  2. #include <assert.h>
  3. #include <stdlib.h>
  4. typedef int LTDataType;
  5. typedef struct ListNode
  6. {
  7. struct ListNode* next; //指针保存下⼀个节点的地址
  8. struct ListNode* prev; //指针保存前⼀个节点的地址
  9. LTDataType data;
  10. }LTNode;
  11. //初始化
  12. LTNode * LTInit();
  13. //销毁链表
  14. void LTDestroy(LTNode* phead);
  15. //打印数据
  16. void LTPrint(LTNode* phead);
  17. //尾插数据
  18. void LTPushBack(LTNode* phead, LTDataType x);
  19. //尾删数据
  20. void LTPopBack(LTNode* phead);
  21. //头插数据
  22. void LTPushFront(LTNode* phead, LTDataType x);
  23. //头删数据
  24. void LTPopFront(LTNode* phead);
  25. //在pos位置之后插⼊数据
  26. void LTInsert(LTNode* pos, LTDataType x);
  27. //在pos位置删除数据
  28. void LTErase(LTNode* pos);
  29. //查找指定数据
  30. LTNode* LTFind(LTNode* phead, LTDataType x);

好啦!本期数据结构双链表的学习就到此为止啦!我们下一期再一起学习吧!

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

闽ICP备14008679号