当前位置:   article > 正文

DS:带头双向循环链表的实现

DS:带头双向循环链表的实现

创作不易,友友们给个三连吧!!!

      博主的上篇文章介绍了链表,以及单链表的实现。

单链表的实现(超详细!!)
    其实单链表的全称叫做不带头单向不循环链表,本文会重点介绍链表的分类以及双链表的实现!

一、链表的分类

   链表的结构⾮常多样,组合起来就有8种(2 x 2 x 2)链表结构:

1.1 单向或者双向

    双向链表,即上一个结点保存着下一个结点的地址,且下一个结点保存着上一个结点的地址,即我们可以从头结点开始遍历,也可以从尾结点开始遍历

1.2 带头或者不带头 

     单链表中我们提到的“头结点”的“头”和“带头”链表的头是两个概念!单链表中提到的“头结点”指的是第一个有效的结点,“带头”链表里的“头”指的是无效的结点(即不保存任何有效的数据!)

    

1.3 循环或者不循环

     不循环的链表最后一个结点的next指针指向NULL,而循环的链表,最后一个结点的next指针指向第一个结点!!

      虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构: 单链表(不带头单向不循环链表)和 双向链表(带头双向循环链表)

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

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

二、带头双向循环链表的结构

      带头链表⾥的头节点,实际为“哨兵位”,哨兵位节点不存储任何有效元素,只是站在这⾥“放哨的”

“哨兵位”存在的意义:遍历循环链表避免死循环。

三、双向链表结点结构体的创建

     与单链表结点结构体不同的是,双向链表的结点结构体多了一个前驱结点!!

  1. typedef int LTDataType;//对类型进行重命名,后面可以通过修改存储其他数据类型
  2. typedef struct ListNode
  3. {
  4. LTDataType data;//保存的数据
  5. struct ListNode* prev;//指针保存前一个结点的地址
  6. struct ListNode* next;//指针保存后一个结点的地址
  7. }LTNode;

四、带头双向循环链表的实现

4.1 新结点的申请

      涉及到需要插入数据,都需要申请新节点,所以优先封装一个申请新结点的函数!利用返回值返回该结点

  1. LTNode* LTBuyNode(LTDataType x)
  2. {
  3. LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
  4. if (newnode == NULL)
  5. {
  6. perror("malloc fail");
  7. exit(1);//申请失败需要强制退出程序
  8. }
  9. //申请成功,则新节点的前驱结点和后驱结点都指向自己
  10. newnode->data = x;
  11. newnode->prev = newnode->next = newnode;
  12. return newnode;
  13. }

4.2 初始化(哨兵位结点)

       对于双向链表来说,需要优先创建一个哨兵结点,和其他结点不同的是,该哨兵结点可以不存储数据,这里我们默认给他一个-1。并利用返回值返回该结点。

  1. LTNode* LTInit()
  2. {
  3. LTNode* phead = LTBuyNode(-1);//哨兵结点可以不存储数据,我们默认给个-1
  4. return phead;//返回哨兵结点
  5. }

4.3 尾插 

       如图,因为这个一个循环链表,相当于我们要把新节点插在最后一个结点和哨兵结点之间,并且最后一一个结点可以用哨兵结点的前驱结点(phead->prev)就可以找到,然后建立phead phead->prev newnode的联系!

  1. void LTPushBack(LTNode* phead, LTDataType x)
  2. {
  3. assert(phead);
  4. LTNode* newnode = LTBuyNode(x);//申请新节点
  5. //建立phead phead->prev newnode的联系
  6. newnode->prev = phead->prev;
  7. newnode->next = phead;
  8. phead->prev->next = newnode;//尾结点的后继指针指向新节点
  9. phead->prev = newnode;//哨兵结点的前驱指针指向新结点
  10. }

 单链表中我们的参数选择二级指针,为什么这里选择一级指针???

      对于单链表来收,单链表的头节点是会改变的,所以我们需要用二级指针,但是双链表的头节点相当于哨兵位,哨兵位是不需要被改变的,他是固定死的,所以我们选择了一级指针。(单链表改了完全头节点,但是双链表只会改变头结点的成员——prev和next)

注:phead->prev->next = newnode和phead->prev = newnode不能替换顺序,因为尾结点是通过头节点找到的,所以要优先让他与newnode建立联系,双链表虽然不需要像单链表一样找最后一个结点需要遍历链表,但是要十分注意修改指针指向的先后顺序!!

4.4 头插

       如图可知,相当于将新节点插入在头节点和头节点下一个结点之间,头节点下一个结点可以通过phead->next找到,然后建立phead、phead->next、newnode的联系!!

  1. void LTPushFront(LTNode* phead, LTDataType x)
  2. {
  3. assert(phead);
  4. LTNode* newnode = LTBuyNode(x);//申请新节点
  5. //建立phead phead->next newnode的联系
  6. newnode->prev = phead;
  7. newnode->next = phead->next;
  8. phead->next->prev = newnode;//头节点的下一个结点的前驱指针指向新结点
  9. phead->next = newnode;//头节点的后继指针指向新节点
  10. }

4.5 打印

      因为是循环链表,所以为了避免死循环打印,我们要设置一个指针接收头节点的下一个结点,然后往后遍历,直到遍历到头节点结束。

  1. void LTPrint(LTNode* phead)
  2. {
  3. assert(phead);
  4. LTNode* pcur = phead->next;
  5. while (pcur != phead)
  6. {
  7. printf("%d->", pcur->data);
  8. pcur = pcur->next;
  9. }
  10. printf("\n");
  11. }

4.6 尾删

      由图可知,要建立phead和phead->prev->prev的联系,同时由于还要释放最后一个结点(phead->prev),所以在建立联系之前要现保存这个被释放的空间,等建立联系完再释放!!同时要注意一条规则,就是当链表中只有哨兵结点的时候,我们称该链表为空链表!因此如果链表只存在哨兵结点,那么删除是没有意义的,所以必须断言!

  1. void LTPopBack(LTNode* phead)
  2. {
  3. assert(phead);
  4. assert(phead->next != phead);//链表只有哨兵结点时删除没意义
  5. LTNode* del = phead->prev;//del记录最后一个结点
  6. del->prev->next = phead;//倒数第二个结点的后驱指针指向头结点
  7. phead->prev = del->prev;//头节点的前驱结点指向倒数第二个结点
  8. free(del);//释放最后一个结点
  9. del = NULL;
  10. }

4.7 头删

       由图可知,要建立phead和phead->next->next的联系,同时由于还要释放第二个结点(phead->next),所以在建立联系之前要现保存这个被释放的空间,等建立联系完再释放!!

  1. void LTPopFront(LTNode* phead)
  2. {
  3. assert(phead);
  4. assert(phead->next != phead);//链表只有哨兵结点时删除没意义
  5. LTNode* del = phead->next;//del记录第二个结点
  6. del->next->prev = phead;//第二个结点的前驱指针指向头结点
  7. phead->next = del->next;//头节点的后驱指针指向第三个结点
  8. free(del);//释放第二个结点
  9. del = NULL;
  10. }

4.8 查找

     涉及到对指定位置进行操作的时候,需要设置一个查找函数,根据我们需要的数据返回他的结点地址

  1. LTNode* LTFind(LTNode* phead, LTDataType x)
  2. {
  3. assert(phead);
  4. LTNode* pcur = phead->next;
  5. while (pcur != phead)//遍历链表
  6. {
  7. if (pcur->data == x)
  8. return pcur;//找到的话返回该结点
  9. pcur = pcur->next;
  10. }
  11. //循环结束还是没找到
  12. return NULL;
  13. }

4.9 指定位置之后插入

        由图可知,指定位置插入相当于将新结点插入到指定位置(pos)和指定位置下一个结点的位置(pos->next),然后建立pos pos->next newnode的联系,而且这里用不到头节点!

  1. void LTInsert(LTNode* pos, LTDataType x)
  2. {
  3. assert(pos);//保证pos为有效结点
  4. LTNode* newnode = LTBuyNode(x);//申请新节点
  5. //建立pos pos->next newnode的联系
  6. newnode->prev = pos;
  7. newnode->next = pos->next;
  8. pos->next->prev = newnode;//pos的后一个结点的前驱结点指向新节点
  9. pos->next = newnode;//pos结点的后继结点指向新结点
  10. }

4.10 指定位置删除

       右图可知建立指定位置的前一个结点(pos->prev)和指定位置的后一个结点(pos->next)的联系,并释放pos。

  1. void LTErase(LTNode* pos)
  2. {
  3. assert(pos);//保证pos为有效结点
  4. pos->prev->next = pos->next;//pos的前一个结点的后继指针指向pos后一个结点
  5. pos->next->prev = pos->prev;//pos的后一个结点的前驱指针指向pos的前一个结点
  6. free(pos);//释放pos
  7. pos = NULL;
  8. }

4.11 销毁链表

  1. void LTDestroy(LTNode* phead)
  2. {
  3. assert(phead);
  4. LTNode* pcur = phead->next;
  5. LTNode*next = NULL;
  6. while (pcur != phead)
  7. {
  8. next = pcur->next;
  9. free(pcur);
  10. pcur = next;
  11. }
  12. //除了头结点都释放完毕
  13. free(phead);
  14. //phead = NULL;//没有用!
  15. }

为什么phead=NULL没有用??

       因为我们使用的是一级指针,这里相当于是值传递,值传递形参改变不了实参,所以将phead置空是没有意义的,其实如果这里使用二级指针,然后传地址就可以了,但是为了保持接口一致性,我们还是依照这种方法,但是phead=NULL必须在主函数中去使用,所以我们在调用销毁链表的函数的时候,别忘记了phead=NULL!!

五、带头双向循环链表实现的全部代码

List.h

  1. #pragma once
  2. #include<stdio.h>
  3. #include<stdlib.h>
  4. #include<assert.h>
  5. typedef int LTDataType;//对类型进行重命名,后面可以通过修改存储其他数据类型
  6. typedef struct ListNode
  7. {
  8. LTDataType data;//保存的数据
  9. struct ListNode* prev;//指针保存前一个结点的地址
  10. struct ListNode* next;//指针保存后一个结点的地址
  11. }LTNode;
  12. LTNode* LTBuyNode(LTDataType x);//申请新的链表结点
  13. LTNode* LTInit();//初始化(申请一个哨兵结点)
  14. void LTPushBack(LTNode* phead, LTDataType x);//尾插 (最后一个结点后插入或哨兵结点前插入)
  15. void LTPushFront(LTNode* phead, LTDataType x);//头插 (哨兵结点后的插入)
  16. void LTPrint(LTNode* phead);//打印
  17. void LTPopBack(LTNode* phead);//尾删
  18. void LTPopFront(LTNode* phead);//头删
  19. LTNode* LTFind(LTNode* phead, LTDataType x);//查找
  20. void LTInsert(LTNode* pos, LTDataType x);//指定位置之后插入
  21. void LTErase(LTNode* pos);//指定位置删除
  22. void LTDestroy(LTNode* phead);//销毁链表

List.c

  1. #include"List.h"
  2. LTNode* LTBuyNode(LTDataType x)
  3. {
  4. LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
  5. if (newnode == NULL)
  6. {
  7. perror("malloc fail");
  8. exit(1);//申请失败需要强制退出程序
  9. }
  10. //申请成功,则新节点的前驱结点和后驱结点都指向自己
  11. newnode->data = x;
  12. newnode->prev = newnode->next = newnode;
  13. return newnode;
  14. }
  15. LTNode* LTInit()
  16. {
  17. LTNode* phead = LTBuyNode(-1);//哨兵结点可以不存储数据,我们默认给个-1
  18. return phead;//返回哨兵结点
  19. }
  20. void LTPushBack(LTNode* phead, LTDataType x)
  21. {
  22. assert(phead);
  23. LTNode* newnode = LTBuyNode(x);//申请新节点
  24. //建立phead phead->prev newnode的联系
  25. newnode->prev = phead->prev;
  26. newnode->next = phead;
  27. phead->prev->next = newnode;//尾结点的后继结点指向新节点
  28. phead->prev = newnode;//哨兵结点的前驱指针指向新结点
  29. }
  30. void LTPushFront(LTNode* phead, LTDataType x)
  31. {
  32. assert(phead);
  33. LTNode* newnode = LTBuyNode(x);//申请新节点
  34. //建立phead phead->next newnode的联系
  35. newnode->prev = phead;
  36. newnode->next = phead->next;
  37. phead->next->prev = newnode;//头节点的下一个结点的前驱指针指向新结点
  38. phead->next = newnode;//头节点的后继指针指向新节点
  39. }
  40. void LTPrint(LTNode* phead)
  41. {
  42. assert(phead);
  43. LTNode* pcur = phead->next;
  44. while (pcur != phead)
  45. {
  46. printf("%d->", pcur->data);
  47. pcur = pcur->next;
  48. }
  49. printf("\n");
  50. }
  51. void LTPopBack(LTNode* phead)
  52. {
  53. assert(phead);
  54. assert(phead->next != phead);//链表只有哨兵结点时删除没意义
  55. LTNode* del = phead->prev;//del记录最后一个结点
  56. del->prev->next = phead;//倒数第二个结点的后驱指针指向头结点
  57. phead->prev = del->prev;//头节点的前驱结点指向倒数第二个结点
  58. free(del);//释放最后一个结点
  59. del = NULL;
  60. }
  61. void LTPopFront(LTNode* phead)
  62. {
  63. assert(phead);
  64. assert(phead->next != phead);//链表只有哨兵结点时删除没意义
  65. LTNode* del = phead->next;//del记录第二个结点
  66. del->next->prev = phead;//第二个结点的前驱指针指向头结点
  67. phead->next = del->next;//头节点的后驱指针指向第三个结点
  68. free(del);//释放第二个结点
  69. del = NULL;
  70. }
  71. LTNode* LTFind(LTNode* phead, LTDataType x)
  72. {
  73. assert(phead);
  74. LTNode* pcur = phead->next;
  75. while (pcur != phead)//遍历链表
  76. {
  77. if (pcur->data == x)
  78. return pcur;//找到的话返回该结点
  79. pcur = pcur->next;
  80. }
  81. //循环结束还是没找到
  82. return NULL;
  83. }
  84. void LTInsert(LTNode* pos, LTDataType x)
  85. {
  86. assert(pos);//保证pos为有效结点
  87. LTNode* newnode = LTBuyNode(x);//申请新节点
  88. //建立pos pos->next newnode的联系
  89. newnode->prev = pos;
  90. newnode->next = pos->next;
  91. pos->next->prev = newnode;//pos的后一个结点的前驱结点指向新节点
  92. pos->next = newnode;//pos结点的后继结点指向新结点
  93. }
  94. void LTErase(LTNode* pos)
  95. {
  96. assert(pos);//保证pos为有效结点
  97. pos->prev->next = pos->next;//pos的前一个结点的后继指针指向pos后一个结点
  98. pos->next->prev = pos->prev;//pos的后一个结点的前驱指针指向pos的前一个结点
  99. free(pos);//释放pos
  100. pos = NULL;
  101. }
  102. void LTDestroy(LTNode* phead)
  103. {
  104. assert(phead);
  105. LTNode* pcur = phead->next;
  106. LTNode*next = NULL;
  107. while (pcur != phead)
  108. {
  109. next = pcur->next;
  110. free(pcur);
  111. pcur = next;
  112. }
  113. //除了头结点都释放完毕
  114. free(phead);
  115. //phead = NULL;//没有用!
  116. }

六、顺序表和链表的优缺点分析

1、存储空间

顺序表物理上连续

链表逻辑上连续,但是物理上不连续

2、随机访问

顺序表可以通过下标去访问

链表不可以直接通过下标去访问

3、任意位置插入或者删除元素

顺序表需要挪移元素,效率低

链表只需修改指针指向

4、插入

动态顺序表空间不够时需要扩容

链表没有容量的概念

5、应用场景

顺序表应用于元素高效存储+频繁访问的场景

链表应用于任意位置插入和删除频繁的场景

总之:没有绝对的优劣,都要各自适合的应用场景!!

 

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

闽ICP备14008679号