深圳幻海软件技术有限公司 欢迎您!

【数据结构】双向链表

2023-07-03

前面我们已经学完了单向链表,知道了单向链表如何进行增删查改等基本功能,而今天,我们将要学习双向链表。目录1.链表的分类2.双向链表定义3.双向链表接口的实现所有接口函数一览创建返回链表头节点初始化链表双向链表打印双向链表尾插双向链表尾删双向链表头插双向链表头删双向链表在pos的前面进行插入双向链表删

前面我们已经学完了单向链表,知道了单向链表如何进行增删查改等基本功能,而今天,我们将要学习双向链表。

目录

1.链表的分类

2.双向链表定义

3.双向链表接口的实现

所有接口函数一览

创建返回链表头节点

初始化链表

双向链表打印

双向链表尾插

双向链表尾删

双向链表头插

双向链表头删

双向链表在pos的前面进行插入

双向链表删除pos位置的节点

求双向链表长度

双向链表销毁

4.完整代码


回忆上次学习的单向链表结构图:

 下面还要详细的介绍一下各种链表示意图,上期遗漏了这部分内容

1.链表的分类

1. 单向或者双向

 2. 带头或者不带头

 3. 循环或者非循环

 虽然有这么多的链表的结构,但是我们实际中最常用还是以下两种结构:

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

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

由于上期我们已经学完了无头单向非循环链表,因此这期我们来学习带头双向循环链表

2.双向链表定义

和单向链表一样,双向链表每个节点都是由结构体组成的数据类型,但是双向链表结构体中增加了一个指向前一个节点的指针。

  1. typedef int LTDataType; //方便修改数据类型
  2. typedef struct ListNode
  3. {
  4. struct ListNode* next;//指向下一个节点的指针
  5. struct ListNode* prev;//指向前一个节点的指针
  6. LTDataType data; //存储的数据
  7. }LTNode;

我们今天要将的是带哨兵卫头节点的双向循环链表,它的头节点不存储有效数据,头节点中的prev指针指向最后一个节点

 当链表为空时,指针的指向关系图:

3.双向链表接口的实现

所有接口函数一览

下面是双向链表中经常要用到的一些接口函数:

  1. //初始化
  2. LTNode* ListInit();
  3. //打印
  4. void printList(LTNode* phead);
  5. //尾插
  6. void ListPushBack(LTNode* phead, LTDataType x);
  7. //头插
  8. void ListPushFront(LTNode* phead, LTDataType x);
  9. //尾删
  10. void ListPopBack(LTNode* phead);
  11. //头删
  12. void ListPopFront(LTNode* phead);
  13. //判断链表是否为空
  14. bool ListEmpty(LTNode* phead);
  15. //在pos位置前插入x
  16. void ListInsert(LTNode* pos, LTDataType x);
  17. //删除pos位置节点
  18. void ListErase(LTNode* pos);
  19. //求链表长度
  20. int ListSize(LTNode* phead);
  21. //销毁
  22. void ListDestory(LTNode* phead);

创建返回链表头节点

函数使用malloc动态分配了一个 LTNode类型的内存空间,用node进行接收。同时要检查内存分配是否成功,将新节点中的data赋值为传入的数据x,将新节点的nextprev指针都设置为NULL,最后返回新节点的地址。

  1. //创建新节点
  2. LTNode* BuyListNode(LTDataType x)
  3. {
  4. LTNode* node = (LTNode*)malloc(sizeof(LTNode));
  5. if (node == NULL)
  6. {
  7. perror("malloc fail");
  8. exit(-1);
  9. }
  10. node->data = x;
  11. node->next = NULL;
  12. node->prev = NULL;
  13. return node;
  14. }

初始化链表

初始化链表就是给链表创建一个头节点,这里需要使用到上面介绍完的创建新节点BuyListNode函数,因为头节点不存储有效数据,所以我们将data赋值为-1,同时头节点中的next和prev都指向自己,最后返回头节点的地址。

  1. LTNode* ListInit()
  2. {
  3. LTNode* phead = BuyListNode(-1);
  4. phead->next = phead;
  5. phead->prev = phead;
  6. return phead;
  7. }

双向链表打印

双向链表的打印和单向链表类似,注意循环的截止条件不是cur为空,而当当cur重新指向头节点的时候停止循环。因为最后一个节点的next指针指向的是头节点。

  1. //打印
  2. void printList(LTNode* phead)
  3. {
  4. LTNode* cur = phead->next;
  5. while (cur != phead)
  6. {
  7. printf("%d->", cur->data);
  8. cur = cur->next;
  9. }
  10. printf("NULL\n");
  11. }

 

双向链表尾插

普通的方法是创建一个新的节点插入到链表的尾部,当然还有更为简单的方法,就是调用一下我们后面要讲到的 ListInsert 函数,这个函数可以在指定位置前插入一个节点并存储有效数据。如果这个指定位置是头节点的位置,那么头节点的前一个位置就是链表最后一个节点所在的位置。

下面介绍一下普通的方法吧:

 将最后一个节点命名为tail,然后根据上图修改一下phead tail newNode之间的指向关系即可

  1. //尾插
  2. void ListPushBack(LTNode* phead, LTDataType x)
  3. {
  4. assert(phead);
  5. /*
  6. //可以直接调用void ListInsert来实现
  7. ListInsert(phead , x);
  8. //代码复用
  9. */
  10. LTNode* newNode = BuyListNode(x);
  11. LTNode* tail = phead->prev;
  12. newNode->next = phead;
  13. newNode->prev = tail;
  14. tail->next = newNode;
  15. phead->prev = newNode;
  16. }

 

双向链表尾删

这里也可以使用两种方法来实现,普通方法是找到最后一个节点,修改指向关系,再释放掉最后一个节点的空间。简单的方法是调用一个可以删除指定位置节点的函数ListErase,将最后一个节点的位置传入到这个函数就可以进行尾删,(最后一个节点的位置是phead->prev)这个函数下面也将会给大家介绍到。

普通方法:

 将最后一个节点命名为tail,倒数第二个节点命名为tailPrev,按上图修改它们的指向关系即可。

为了防止出现尾删空链表的情况出现,我们需要使用一个ListEmpty函数来判断链表是否为空,同时使用assert断言,一旦链表为空,用户仍然使用尾删程序就会报错。头删同理如此。

  1. //尾删
  2. void ListPopBack(LTNode* phead)
  3. {
  4. assert(phead);
  5. assert(ListEmpty(phead));
  6. /*
  7. //可以复用ListErase函数来实现后面的代码
  8. ListErase(phead->prev);
  9. */
  10. LTNode* tail = phead->prev;
  11. LTNode* tailPrev = tail->prev;
  12. phead->prev = tailPrev;
  13. tailPrev->next = phead;
  14. free(tail);
  15. }

双向链表头插

这里也可使用两种方法,我们介绍一下普通的方法:

 将头节点的后一个节点命名为pheadNext,然后将newNode插入到pheadpheadNext之间即可。

  1. //头插
  2. void ListPushFront(LTNode* phead, LTDataType x)
  3. {
  4. assert(phead);
  5. /*
  6. //可以直接调用void ListInsert来实现
  7. ListInsert(phead->next , x);
  8. //代码复用
  9. */
  10. LTNode* newNode = BuyListNode(x);
  11. LTNode* pheadNext = phead->next;
  12. phead->next = newNode;
  13. newNode->prev = phead;
  14. newNode->next = pheadNext;
  15. pheadNext->prev = newNode;
  16. }

双向链表头删

这里也可使用两种方法,我们介绍一下普通的方法:

  1. //头删
  2. void ListPopFront(LTNode* phead)
  3. {
  4. assert(phead);
  5. assert(ListEmpty(phead));
  6. /*
  7. //可以复用ListErase函数来实现后面的代码
  8. ListErase(phead->next);
  9. */
  10. LTNode* next = phead->next;
  11. phead->next = next->next;
  12. next->next->prev = phead;
  13. free(next);
  14. }

双向链表在pos的前面进行插入

直接将pos的前一个节点命名为prev,再将newNode插入。

  1. //在pos位置前插入x
  2. void ListInsert(LTNode* pos, LTDataType x)
  3. {
  4. assert(pos);
  5. LTNode* prev = pos->prev;
  6. LTNode* newNode = BuyListNode(x);
  7. prev->next = newNode;
  8. newNode->prev = prev;
  9. newNode->next = pos;
  10. pos->prev = newNode;
  11. }

双向链表删除pos位置的节点

将pos前一个节点命名为prev,后一个节点命名为next,这样命名方便我们修改指针的指向。然后按照上图修改指针即可。

  1. //删除pos位置节点
  2. void ListErase(LTNode* pos)
  3. {
  4. assert(pos);
  5. LTNode* prev = pos->prev;
  6. LTNode* next = pos->next;
  7. prev->next = next;
  8. next->prev = prev;
  9. free(pos);
  10. }

求双向链表长度

这里使用的方法是遍历整个链表。

  1. //求链表长度
  2. int ListSize(LTNode* phead)
  3. {
  4. assert(phead);
  5. LTNode* cur = phead->next;
  6. int size = 0;
  7. while (cur != phead)
  8. {
  9. ++size;
  10. cur = cur->next;
  11. }
  12. return size;
  13. }

双向链表销毁

这一步可以复用删除pos位置节点的函数ListErase,我们只要将链表的每一个节点地址都传入到这个函数,就可销毁链表。当然,最后也要将头节点给释放销毁。

  1. //销毁
  2. void ListDestory(LTNode* phead)
  3. {
  4. assert(phead);
  5. LTNode* cur = phead->next;
  6. while (cur != phead)
  7. {
  8. LTNode* next = cur->next;
  9. ListErase(cur);
  10. cur = next;
  11. }
  12. free(phead);
  13. }

4.顺序表和链表的区别

不同点顺序表链表
存储空间上物理上一定连续逻辑上连续,但物理上不一定连 续
随机访问支持O(1)不支持:O(N)
任意位置插入或者删除元 素可能需要搬移元素,效率低O(N)只需修改指针指向
插入动态顺序表,空间不够时需要扩 容没有容量的概念
应用场景元素高效存储+频繁访问任意位置插入和删除频繁
缓存利用率

顺序表优点:下标随机访问,cpu高速缓存命中率高

顺序表缺点:头部或者中间插入删除效率低,扩容有一定程度性能消耗,可能存在一定程度空间浪费。

链表优点:任意位置插入删除O(1)复杂度,按需申请释放。

链表缺点:不支持下标随机访问。

5.完整代码

头文件: 存放函数声明部分

  1. #pragma once
  2. #include<stdio.h>
  3. #include<assert.h>
  4. #include<stdbool.h>
  5. #include<stdlib.h>
  6. typedef int LTDataType; //方便修改数据类型
  7. typedef struct ListNode
  8. {
  9. struct ListNode* next;//指向下一个节点的指针
  10. struct ListNode* prev;//指向前一个节点的指针
  11. LTDataType data; //存储的数据
  12. }LTNode;
  13. //初始化
  14. LTNode* ListInit();
  15. //打印
  16. void printList(LTNode* phead);
  17. //尾插
  18. void ListPushBack(LTNode* phead, LTDataType x);
  19. //头插
  20. void ListPushFront(LTNode* phead, LTDataType x);
  21. //尾删
  22. void ListPopBack(LTNode* phead);
  23. //头删
  24. void ListPopFront(LTNode* phead);
  25. //判断链表是否为空
  26. bool ListEmpty(LTNode* phead);
  27. //在pos位置前插入x
  28. void ListInsert(LTNode* pos, LTDataType x);
  29. //删除pos位置节点
  30. void ListErase(LTNode* pos);
  31. //求链表长度
  32. int ListSize(LTNode* phead);
  33. //销毁
  34. void ListDestory(LTNode* phead);

List.c文件  存放函数定义

  1. #define _CRT_SECURE_NO_WARNINGS 1
  2. #include"List.h"
  3. //创建新节点
  4. LTNode* BuyListNode(LTDataType x)
  5. {
  6. LTNode* node = (LTNode*)malloc(sizeof(LTNode));
  7. if (node == NULL)
  8. {
  9. perror("malloc fail");
  10. exit(-1);
  11. }
  12. node->data = x;
  13. node->next = NULL;
  14. node->prev = NULL;
  15. return node;
  16. }
  17. //初始化
  18. LTNode* ListInit()
  19. {
  20. LTNode* phead = BuyListNode(-1);
  21. phead->next = phead;
  22. phead->prev = phead;
  23. return phead;
  24. }
  25. //判断链表是否为空
  26. bool ListEmpty(LTNode* phead)
  27. {
  28. assert(phead);
  29. return phead->next != phead;
  30. }
  31. //打印
  32. void printList(LTNode* phead)
  33. {
  34. LTNode* cur = phead->next;
  35. while (cur != phead)
  36. {
  37. printf("%d->", cur->data);
  38. cur = cur->next;
  39. }
  40. printf("NULL\n");
  41. }
  42. //尾插
  43. void ListPushBack(LTNode* phead, LTDataType x)
  44. {
  45. assert(phead);
  46. /*
  47. //可以直接调用void ListInsert来实现
  48. ListInsert(phead , x);
  49. //代码复用
  50. */
  51. LTNode* newNode = BuyListNode(x);
  52. LTNode* tail = phead->prev;
  53. newNode->next = phead;
  54. newNode->prev = tail;
  55. tail->next = newNode;
  56. phead->prev = newNode;
  57. }
  58. //头插
  59. void ListPushFront(LTNode* phead, LTDataType x)
  60. {
  61. assert(phead);
  62. /*
  63. //可以直接调用void ListInsert来实现
  64. ListInsert(phead->next , x);
  65. //代码复用
  66. */
  67. LTNode* newNode = BuyListNode(x);
  68. LTNode* pheadNext = phead->next;
  69. phead->next = newNode;
  70. newNode->prev = phead;
  71. newNode->next = pheadNext;
  72. pheadNext->prev = newNode;
  73. }
  74. //尾删
  75. void ListPopBack(LTNode* phead)
  76. {
  77. assert(phead);
  78. assert(ListEmpty(phead));
  79. /*
  80. //可以复用ListErase函数来实现后面的代码
  81. ListErase(phead->prev);
  82. */
  83. LTNode* tail = phead->prev;
  84. LTNode* tailPrev = tail->prev;
  85. phead->prev = tailPrev;
  86. tailPrev->next = phead;
  87. free(tail);
  88. }
  89. //头删
  90. void ListPopFront(LTNode* phead)
  91. {
  92. assert(phead);
  93. assert(ListEmpty(phead));
  94. /*
  95. //可以复用ListErase函数来实现后面的代码
  96. ListErase(phead->next);
  97. */
  98. LTNode* next = phead->next;
  99. phead->next = next->next;
  100. next->next->prev = phead;
  101. free(next);
  102. }
  103. //在pos位置前插入x
  104. void ListInsert(LTNode* pos, LTDataType x)
  105. {
  106. assert(pos);
  107. LTNode* prev = pos->prev;
  108. LTNode* newNode = BuyListNode(x);
  109. prev->next = newNode;
  110. newNode->prev = prev;
  111. newNode->next = pos;
  112. pos->prev = newNode;
  113. }
  114. //删除pos位置节点
  115. void ListErase(LTNode* pos)
  116. {
  117. assert(pos);
  118. LTNode* prev = pos->prev;
  119. LTNode* next = pos->next;
  120. prev->next = next;
  121. next->prev = prev;
  122. free(pos);
  123. }
  124. //求链表长度
  125. int ListSize(LTNode* phead)
  126. {
  127. assert(phead);
  128. LTNode* cur = phead->next;
  129. int size = 0;
  130. while (cur != phead)
  131. {
  132. ++size;
  133. cur = cur->next;
  134. }
  135. return size;
  136. }
  137. //销毁
  138. void ListDestory(LTNode* phead)
  139. {
  140. assert(phead);
  141. LTNode* cur = phead->next;
  142. while (cur != phead)
  143. {
  144. LTNode* next = cur->next;
  145. ListErase(cur);
  146. cur = next;
  147. }
  148. free(phead);
  149. }

test.c文件   用于测试接口函数正确性

  1. #define _CRT_SECURE_NO_WARNINGS 1
  2. #include"List.h"
  3. //尾插测试
  4. void ListTest1()
  5. {
  6. LTNode* plist = ListInit();
  7. ListPushBack(plist, 1);
  8. ListPushBack(plist, 2);
  9. ListPushBack(plist, 3);
  10. ListPushBack(plist, 4);
  11. ListPushBack(plist, 5);
  12. printList(plist);
  13. }
  14. //头插测试
  15. void ListTest2()
  16. {
  17. LTNode* plist = ListInit();
  18. ListPushFront(plist, 1);
  19. ListPushFront(plist, 2);
  20. ListPushFront(plist, 3);
  21. ListPushFront(plist, 4);
  22. ListPushFront(plist, 5);
  23. printList(plist);
  24. }
  25. //尾删测试
  26. void ListTest3()
  27. {
  28. LTNode* plist = ListInit();
  29. ListPushFront(plist, 1);
  30. ListPushFront(plist, 2);
  31. ListPushFront(plist, 3);
  32. ListPushFront(plist, 4);
  33. ListPushFront(plist, 5);
  34. printList(plist);
  35. ListPopBack(plist);
  36. ListPopBack(plist);
  37. ListPopBack(plist);
  38. ListPopBack(plist);
  39. printList(plist);
  40. }
  41. //头删测试
  42. void ListTest4()
  43. {
  44. LTNode* plist = ListInit();
  45. ListPushBack(plist, 1);
  46. ListPushBack(plist, 2);
  47. ListPushBack(plist, 3);
  48. ListPushBack(plist, 4);
  49. ListPushBack(plist, 5);
  50. printList(plist);
  51. ListPopFront(plist);
  52. ListPopFront(plist);
  53. ListPopFront(plist);
  54. ListPopFront(plist);
  55. printList(plist);
  56. }
  57. void ListTest5()
  58. {
  59. LTNode* plist = ListInit();
  60. ListPushBack(plist, 1);
  61. ListPushBack(plist, 2);
  62. ListPushBack(plist, 3);
  63. ListPushBack(plist, 4);
  64. ListPushBack(plist, 5);
  65. printList(plist);
  66. ListDestory(plist);
  67. //printList(plist);
  68. }
  69. int main()
  70. {
  71. //ListTest1();
  72. ListTest1();
  73. return 0;
  74. }

文章知识点与官方知识档案匹配,可进一步学习相关知识
算法技能树首页概览48678 人正在系统学习中