当前位置:   article > 正文

数据结构之链表详解(2)——双向链表

双向链表

目录

   

    前言

一.双向链表

A.双向链表的含义

B.双向链表的实现

1.双向链表的结构

2.链表的初始化

        初始化图解:

        函数代码:

 3.动态申请节点函数

        函数代码:

 4.打印双向链表函数

        函数代码:

5.尾部插入节点

        图解:

        函数代码: 

         测试:

 6.头插函数

        图解:

         函数代码:

        测试:

 7.尾删函数

        图解:

        函数代码:

        测试:

 8.头删函数

        图解

         函数代码:

        测试:

9.链表查找函数

        函数代码: 

        测试: 

10.在链表指定的位置前插入函数 

        图解:

        函数代码:

        测试:

11.在链表的pos位置处删除节点

        图解:

        函数代码:

         测试:

12.求链表的长度函数

        函数代码:

        测试:

13.释放链表空间

        函数代码:


    前言

        前几日,我写了一篇关于单链表的博客,想要了解的可以看一看: 数据结构之链表详解(1

        单链表的特征就是:单向、不带头、非循环,你们可以叫它三无链表hhh,单向就是说链表的所有节点都是只有一个指针next,它只能链接一个节点的地址或者NULL;不带头就是没有哨兵位头节点,这个之后介绍;非循环就是链表中节点之间没有环相连。其次在上篇博客里还实现了单链表的初始化、申请新节点、头插头删、尾插尾删、在某个指定位置的插入删除等功能实现。

        而今天实现的双向链表与单链表完全相反,接下来就带着大家来看一看。

一.双向链表

A.双向链表的含义

双向链表:Double List Node,它的特征为:带头+双向+循环。

        带头:就是哨兵位头节点,哨兵位头节点是用动态申请(malloc、calloc、realloc)下的一个节点,它的里面绝不存储有效数据,即它不可以作为节点进行存值,但可以存储节点的地址 ,它最大的优势就是让链表的起始指针永远指向哨兵位头节点,由哨兵位头节点去插入或删除节点,这样做就不会修改链表起始指针,也就用不到二级指针;而单链表中没有哨兵头节点,所以常常要改变起始指针的指向,使用二级指针接收实参,才能去改变实参!!!

        双向:单链表只有一个指针,所以一个节点只能链接一个地址;而双链表的节点中有两个指针地址,既可以链接它前面的节点地址,也可以链接它后面的节点地址,十分方便。

        循环:便是双链表中头尾使用环去链接,链表的最后一个节点不会指向NULL,而是与头节点相互链接,便组成了循环。

        如上图 :双向链表的head结点就是哨兵位头节点,它用于指向第一个结点地址,而每个结点的后指针都相互指向下一个结点的前指针。

注:这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带 来很多优势,实现反而简单了。

B.双向链表的实现

1.双向链表的结构

共有三部分:一个数据域,两个指针域

  1. typedef int DLTDataType;//重命名int结构用于双向链表,使用双向链表就用这种新结构,使用普通变量还是
  2. //使用int类型,重定义只是做一个区分而已。
  3. typedef struct DListNode {
  4. DLTDataType data; //数据域
  5. struct DListNode* prev; //前指针
  6. struct DListNode* next; //后指针
  7. }DLNode;//重定义结构体类型名称

2.链表的初始化

初始化图解:

 

函数代码:

  1. //链表初始化
  2. void DListInit(DLNode* phead) {
  3. DLNode* guard = (DLNode*)malloc(sizeof(DLNode));//哨兵位头节点
  4. if (guard == NULL) {
  5. prerror("malloc fail\n");
  6. return -1;
  7. }
  8. guard->prev = guard;
  9. guard->next = guard;
  10. return guard;
  11. }

        初始化便是要创建哨兵位头节点,因为哨兵位头节点不存储有效数据,且该开始两指针并不知道指向谁,所以根据双向链表循环的特性,就让该结点的两个指针自己指向自己。


 3.动态申请节点函数

函数代码:

  1. //动态申请节点函数
  2. DLNode* BuyDLTNode( DLTDataType x) {
  3. DLNode* node = (DLNode*)malloc(sizeof(DLNode));
  4. //刚申请下的堆区空间有可能开辟失败,所以要进行检查
  5. if (node == NULL) {
  6. perror("malloc fail\n");
  7. return -1;
  8. }
  9. //开辟好后就赋值
  10. node->data = x;
  11. node->prev = NULL;
  12. node->next = NULL;
  13. return node;
  14. }

因为刚开辟下节点,还没有进行链接,所以先暂时让两指针存空。

 


 

 4.打印双向链表函数

        打印链表就是遍历每一个节点,因为双向链表为循环,尾节点不指向NULL,头尾相连,所以需要新的限制条件——哨兵位头节点,哨兵位头节点不存储有效数据,所以遍历指针只要遇到哨兵节点就会停止,即打印完毕!

函数代码:

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


 

5.尾部插入节点

图解:

情况一:


 

        情况一讲解:之前对于单链表的尾插实现时需要进行找尾才能插入节点(需要遍历cur!=NULL才行) ,但是双向链表的尾插不同,它不需要遍历寻找,如图1,phead作为头指针,永远指向哨兵头,而哨兵头的prev指针却是与尾部节点d3相连(头尾组成循环相连),那么尾节点就一定是哨兵头的prev指向地址,所以不需要遍历,可以轻易的找到!


 情况二:

情况一与情况二原理相同。 

 

函数代码: 

  1. //链表尾插
  2. void DListPushBack(DLNode* phead, DLTDataType x) {
  3. assert(phead);
  4. DLNode* tail = phead->prev;//找到尾 尾节点指针处在哨兵位头节点的prev
  5. DLNode* newnode = BuyDLTNode(x);//新节点
  6. //改变顺序
  7. tail->next = newnode;
  8. newnode->prev = tail;
  9. newnode->next = phead;
  10. phead->prev = newnode;
  11. }

 测试:

  1. void TestDlist1() {
  2. DLNode* plist1=DListInit();
  3. //传参时,不需要传plist1的地址,因为函数不会改变plist1的指向,因为plist1永远指向哨兵位头节点,
  4. //使用哨兵位头节点进行改变,所以用plist传递,用一级指针接收就行
  5. DListPushBack(plist1, 1);
  6. DListPushBack(plist1, 2);
  7. DListPushBack(plist1, 3);
  8. DListPushBack(plist1, 4);
  9. DListPrint(plist1);
  10. }
  11. int main() {
  12. TestDlist1();
  13. return 0;
  14. }

结果:

 注:phead就等价于哨兵位头节点!


 6.头插函数

图解:

 函数代码:

  1. //头插
  2. void DListPushFront(DLNode* phead,DLTDataType x) {
  3. assert(phead);
  4. DLNode* first = phead->next;
  5. DLNode* newnode = BuyDLTNode(x);
  6. newnode->next = first;
  7. newnode->prev = phead;
  8. phead->next = newnode;
  9. first->prev = newnode;
  10. }

测试:

 


 7.尾删函数

图解:

情况一:当链表有多个节点时

 


 

 情况二:当链表只有一个节点时 ,尾删后,哨兵位头节点的两个指针就会自己指向自己

函数代码:

关于头尾部的删除,需要进行检查链表是否为空,所以,我封装了一个检查函数:

  1. //暴力检查
  2. bool DListEmpty(DLNode* phead) {
  3. assert(phead);
  4. return phead->next == phead;
  5. }

注:bool类型的返回值为true、false两种,若是phead->next==phead说明链表为空,那么assert(!DListEmpty(phead)==assert(NULL);函数就报错,且停止下面语句的执行;若是       phead->next !=phead说明链表不为空,那么assert(!DListEmpty(phead)这条语句就不起作用,检查通过,继续下面语句的执行。

  1. //尾删
  2. void DListPopBack(DLNode* phead) {
  3. assert(phead);
  4. //暴力检查
  5. assert(!DListEmpty(phead));
  6. DLNode* tail = phead->prev;
  7. DLNode* last = tail->prev;
  8. //改变顺序
  9. last->next = phead;
  10. phead->prev = last;
  11. //释放尾节点
  12. free(tail);
  13. tail = NULL;
  14. }

测试:


 8.头删函数

图解:

 函数代码:

  1. //头删
  2. void DListPopFront(DLNode* phead) {
  3. assert(phead);
  4. //暴力检查
  5. assert(!DListEmpty(phead));
  6. DLNode* first = phead->next;
  7. DLNode* second = first->next;
  8. phead->next = second;
  9. second->prev = phead;
  10. free(first);
  11. first = NULL;
  12. }

 

测试:


9.链表查找函数

        查找函数可以用来查找某个节点,还可以通过对查找到的节点进行修改、对指定位置的增删功能实现。

函数代码: 

  1. //链表查找
  2. DLNode* DListFind(DLNode* phead, DLTDataType x) {
  3. assert(phead);
  4. DLNode* cur = phead->next;
  5. //遍历查找
  6. while (cur != phead){
  7. if (cur->data == x) {
  8. return cur;//找到了,返回
  9. }
  10. cur = cur->next;//继续往后找
  11. }
  12. return NULL;//找不到,返回空
  13. }

测试: 


10.在链表指定的位置前插入函数 

图解:

 

函数代码:

  1. //链表在指定位置pos前插入函数
  2. void DListInsert(DLNode* pos, DLTDataType x) {
  3. //指定的位置不可以为空
  4. assert(pos);
  5. DLNode* last = pos->prev;
  6. DLNode* newnode = BuyDLTNode(x);
  7. last->next = newnode;
  8. newnode->prev = last;
  9. newnode->next = pos;
  10. pos->prev = newnode;
  11. }

测试:

 

注:在某个位置前插入节点时,总要先找到pos的位置,然后才可以使用Insert函数! 

 


11.在链表的pos位置处删除节点

图解:

 

函数代码:

  1. //链表在指定位置pos处删除节点
  2. void DListErase(DLNode* pos) {
  3. assert(pos);
  4. DLNode* later = pos->next;
  5. DLNode* last = pos->prev;
  6. last->next = later;
  7. later->prev = last;
  8. free(pos);
  9. pos = NULL;
  10. }

 测试:

 


12.求链表的长度函数

这个函数与打印函数原理相同,都是通过指针遍历每一个节点去统计节点个数。

函数代码:

  1. //求链表的长度函数
  2. size_t DListSize(DLNode* phead) {
  3. assert(phead);
  4. size_t n = 0;
  5. DLNode* cur = phead->next;
  6. while (cur != phead) {
  7. n++;
  8. cur = cur->next;
  9. }
  10. return n;
  11. }

测试:

 


13.释放链表空间

函数代码:

  1. //释放空间
  2. void DListDestory(DLNode* phead) {
  3. assert(phead);
  4. DLNode* cur = phead->next;
  5. //以遍历节点的方式,一个一个的释放
  6. while (cur != phead) {
  7. DLNode* later = cur->next;
  8. free(cur);
  9. cur = NULL;
  10. }
  11. //释放完后,再把哨兵位头节点也释放掉
  12. free(phead);
  13. phead = NULL;
  14. }

 

完整代码:

Test.c:

  1. #define _CRT_SECURE_NO_WARNINGS 1
  2. #include"DList.h"
  3. //尾插
  4. void TestDlist1() {
  5. DLNode* plist1=DListInit();
  6. //传参时,不需要传plist1的地址,因为函数不会改变plist1的指向,因为plist1永远指向哨兵位头节点,
  7. //使用哨兵位头节点进行改变,所以用plist传递,用一级指针接收就行
  8. printf("尾插\n");
  9. DListPushBack(plist1, 1);
  10. DListPushBack(plist1, 2);
  11. DListPushBack(plist1, 3);
  12. DListPushBack(plist1, 4);
  13. DListPrint(plist1);
  14. //尾删
  15. printf("\n尾删\n");
  16. printf("尾删第一次:");
  17. DListPopBack(plist1);
  18. DListPrint(plist1);
  19. printf("尾删第二次:");
  20. DListPopBack(plist1);
  21. DListPrint(plist1);
  22. printf("尾删第三次:");
  23. DListPopBack(plist1);
  24. DListPrint(plist1);
  25. printf("尾删第四次:");
  26. DListPopBack(plist1);
  27. DListPrint(plist1);
  28. }
  29. //头插
  30. void TestDlist2() {
  31. DLNode* plist2 = DListInit();
  32. printf("头插\n");
  33. DListPushFront(plist2, 10);
  34. DListPushFront(plist2, 20);
  35. DListPushFront(plist2, 30);
  36. DListPushFront(plist2, 40);
  37. DListPrint(plist2);
  38. //头删
  39. printf("\n头删\n");
  40. printf("头删第一次:");
  41. DListPopFront(plist2);
  42. DListPrint(plist2);
  43. printf("头删第二次:");
  44. DListPopFront(plist2);
  45. DListPrint(plist2);
  46. printf("头删第三次:");
  47. DListPopFront(plist2);
  48. DListPrint(plist2);
  49. printf("头删第四次:");
  50. DListPopFront(plist2);
  51. DListPrint(plist2);
  52. }
  53. //查找,修改
  54. void TestDlist3() {
  55. DLNode* plist3 = DListInit();
  56. printf("头插\n");
  57. DListPushFront(plist3, 10);
  58. DListPushFront(plist3, 20);
  59. DListPushFront(plist3, 30);
  60. DListPushFront(plist3, 40);
  61. DListPrint(plist3);
  62. DLNode* find = DListFind(plist3, 20);
  63. if (find) {
  64. printf("找到了\n");
  65. find->data = 999;
  66. printf("修改节点数值成功\n");
  67. DListPrint(plist3);
  68. }
  69. else {
  70. printf("没找到\n");
  71. return -1;
  72. }
  73. }
  74. //在某个位置pos前插入
  75. void TestDlist4() {
  76. DLNode* plist4 = DListInit();
  77. printf("尾插\n");
  78. DListPushBack(plist4, 100);
  79. DListPushBack(plist4, 200);
  80. DListPushBack(plist4, 300);
  81. DListPushBack(plist4, 400);
  82. DListPrint(plist4);
  83. //在某个位置前插入
  84. //DLNode* find = DListFind(plist4, 100);//找个pos位置
  85. //if (find) {
  86. // printf("找到了\n");
  87. // DListInsert(find, 2999);
  88. // DListPrint(plist4);
  89. //}
  90. //else {
  91. // return -1;
  92. //}
  93. DLNode* find2 = DListFind(plist4, 400);//找个pos位置
  94. if (find2) {
  95. printf("找到了\n");
  96. DListInsert(find2, 6999);
  97. DListPrint(plist4);
  98. }
  99. else {
  100. return -1;
  101. }
  102. }
  103. //在某个位置pos删除节点
  104. void TestDlist5() {
  105. DLNode* plist5 = DListInit();
  106. printf("尾插\n");
  107. DListPushBack(plist5, 100);
  108. DListPushBack(plist5, 200);
  109. DListPushBack(plist5, 300);
  110. DListPushBack(plist5, 400);
  111. DListPrint(plist5);
  112. //在某个位置删除
  113. DLNode* find = DListFind(plist5, 100);//找个pos位置
  114. if (find) {
  115. printf("找到了\n");
  116. DListErase(find);//删除值为100的节点
  117. DListPrint(plist5);
  118. }
  119. else {
  120. return -1;
  121. }
  122. //DLNode* find2 = DListFind(plist5, 400);//找个pos位置
  123. //if (find2) {
  124. // printf("找到了\n");
  125. // DListInsert(find2, 6999);
  126. // DListPrint(plist5);
  127. //}
  128. //else {
  129. // return -1;
  130. //}
  131. }
  132. void TestDlist6() {
  133. DLNode* plist6 = DListInit();
  134. size_t count = DListSize(plist6);//刚开始链表的长度为0
  135. printf("当前链表的长度为:%zu\n",count);
  136. DListPushBack(plist6, 100);
  137. DListPushBack(plist6, 200);
  138. DListPushBack(plist6, 300);
  139. DListPushBack(plist6, 400);
  140. DListPrint(plist6);
  141. count= DListSize(plist6);
  142. printf("当前链表的长度为:%zu", count);
  143. }
  144. int main() {
  145. //TestDlist1();
  146. //TestDlist2();
  147. //TestDlist3();
  148. //TestDlist4();
  149. //TestDlist5();
  150. TestDlist6();
  151. return 0;
  152. }

DList.c代码:

  1. #define _CRT_SECURE_NO_WARNINGS 1
  2. #include"DList.h"
  3. //链表初始化
  4. DLNode* DListInit() {
  5. DLNode* guard = (DLNode*)malloc(sizeof(DLNode));//哨兵位头节点
  6. if (guard == NULL) {
  7. perror("malloc fail\n");
  8. return -1;
  9. }
  10. guard->prev = guard;
  11. guard->next = guard;
  12. return guard;
  13. }
  14. //动态申请节点函数
  15. DLNode* BuyDLTNode( DLTDataType x) {
  16. DLNode* node = (DLNode*)malloc(sizeof(DLNode));
  17. if (node == NULL) {
  18. perror("malloc fail\n");
  19. return -1;
  20. }
  21. node->data = x;
  22. node->prev = NULL;
  23. node->next = NULL;
  24. return node;
  25. }
  26. //链表尾插
  27. void DListPushBack(DLNode* phead, DLTDataType x) {
  28. assert(phead);
  29. DLNode* newnode = BuyDLTNode(x);//新节点
  30. DLNode* tail=phead->prev;//找到尾节点指针处在哨兵位头节点的prev
  31. //改变顺序
  32. tail->next = newnode;
  33. newnode->prev = tail;
  34. newnode->next = phead;
  35. phead->prev = newnode;
  36. }
  37. //打印
  38. void DListPrint(DLNode* phead) {
  39. assert(phead);
  40. DLNode* cur = phead->next;
  41. printf("guard<=>");
  42. while (cur != phead) { //遍历
  43. printf("%d<=>", cur->data);
  44. cur = cur->next;
  45. }
  46. printf("\n");
  47. }
  48. //头插
  49. void DListPushFront(DLNode* phead,DLTDataType x) {
  50. assert(phead);
  51. //找到头节点
  52. DLNode* first = phead->next;
  53. DLNode* newnode = BuyDLTNode(x);
  54. //改变顺序
  55. newnode->next = first;
  56. newnode->prev = phead;
  57. phead->next = newnode;
  58. first->prev = newnode;
  59. }
  60. //暴力检查
  61. bool DListEmpty(DLNode* phead) {
  62. assert(phead);
  63. return phead->next == phead;
  64. }
  65. //尾删
  66. void DListPopBack(DLNode* phead) {
  67. assert(phead);
  68. //暴力检查
  69. assert(!DListEmpty(phead));
  70. DLNode* tail = phead->prev;
  71. DLNode* last = tail->prev;
  72. //改变顺序
  73. last->next = phead;
  74. phead->prev = last;
  75. //释放尾节点
  76. free(tail);
  77. tail = NULL;
  78. }
  79. //头删
  80. void DListPopFront(DLNode* phead) {
  81. assert(phead);
  82. //暴力检查
  83. assert(!DListEmpty(phead));
  84. DLNode* first = phead->next;
  85. DLNode* second = first->next;
  86. phead->next = second;
  87. second->prev = phead;
  88. free(first);
  89. first = NULL;
  90. }
  91. //链表查找
  92. DLNode* DListFind(DLNode* phead, DLTDataType x) {
  93. assert(phead);
  94. DLNode* cur = phead->next;
  95. //遍历查找
  96. while (cur != phead){
  97. if (cur->data == x) {
  98. return cur;//找到了,返回
  99. }
  100. cur = cur->next;//继续往后找
  101. }
  102. return NULL;//找不到,返回空
  103. }
  104. //链表在指定位置pos前插入函数
  105. void DListInsert(DLNode* pos, DLTDataType x) {
  106. //指定的位置不可以为空
  107. assert(pos);
  108. DLNode* last = pos->prev;
  109. DLNode* newnode = BuyDLTNode(x);
  110. last->next = newnode;
  111. newnode->prev = last;
  112. newnode->next = pos;
  113. pos->prev = newnode;
  114. }
  115. //链表在指定位置pos处删除节点
  116. void DListErase(DLNode* pos) {
  117. assert(pos);
  118. DLNode* later = pos->next;
  119. DLNode* last = pos->prev;
  120. last->next = later;
  121. later->prev = last;
  122. free(pos);
  123. pos = NULL;
  124. }
  125. //求链表的长度函数
  126. size_t DListSize(DLNode* phead) {
  127. assert(phead);
  128. size_t n = 0;
  129. DLNode* cur = phead->next;
  130. while (cur != phead) {
  131. n++;
  132. cur = cur->next;
  133. }
  134. return n;
  135. }
  136. //释放空间
  137. void DListDestory(DLNode* phead) {
  138. assert(phead);
  139. DLNode* cur = phead->next;
  140. //以遍历节点的方式,一个一个的释放
  141. while (cur != phead) {
  142. DLNode* later = cur->next;
  143. free(cur);
  144. cur = NULL;
  145. }
  146. //释放完后,再把哨兵位头节点也释放掉
  147. free(phead);
  148. phead = NULL;
  149. }

DList.h头文件代码:

  1. #pragma once
  2. #include<stdio.h>
  3. #include<stdlib.h>
  4. #include<assert.h>
  5. #include<stdbool.h>
  6. typedef int DLTDataType;//重命名int结构用于双向链表,使用双向链表就用这种新结构,使用普通变量还是使用
  7. //int类型,重定义只是做一个区分而已。
  8. typedef struct DListNode {
  9. DLTDataType data; //数据域
  10. struct DListNode* prev; //前指针
  11. struct DListNode* next; //后指针
  12. }DLNode;//重定义结构体类型名称
  13. //链表初始化
  14. DLNode* DListInit();
  15. //链表尾插
  16. void DListPushBack(DLNode* phead, DLTDataType x);
  17. //打印
  18. void DListPrint(DLNode* phead);
  19. //头插
  20. void DListPushFront(DLNode* phead, DLTDataType x);
  21. //尾删
  22. void DListPopBack(DLNode* phead);
  23. //头删
  24. void DListPopFront(DLNode* phead);
  25. //链表查找
  26. DLNode* DListFind(DLNode* phead, DLTDataType x);
  27. //链表在指定位置pos前插入函数
  28. void DListInsert(DLNode* pos, DLTDataType x);
  29. //链表在指定位置pos处删除节点
  30. void DListErase(DLNode* pos);
  31. //求链表的长度函数
  32. size_t DListSize(DLNode* phead);
  33. //释放空间
  34. void DListDestory(DLNode* phead);

以上就是对双向链表的讲解和功能实现了。

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

闽ICP备14008679号