• 数据结构-双向链表


    目录

    1.带头双向循环链表:

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

    双向链表初始化:

    双向链表打印:

    开辟节点函数:

    双向链表头插:

    双向链表尾插:

    双向链表头删:

    双向链表尾删:

     双向链表查找:

    双向链表在pos之前插入:

    双向链表在pos位置删除:

     双向链表的销毁:

    3.完整代码:

    test.c

    List.h

    List.c

    4.测试:​编辑

    5.顺序表和链表的区别


    1.带头双向循环链表

    前面我们已经知道了链表的结构有8种,我们主要学习下面两种:

    前面我们已经学习了无头单向非循环链表,今天我们来学习带头双向循环链表:

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

     带头双向循环链表不需要二级指针,因为我们刚开始就为其开辟了一个节点,叫做哨兵位头节点,它是结构体中的指针,用结构体指针就能改变它,而要改变结构体外面的指针才会用二级指针。

    双向循环链表顾名思义,除了哨兵位头节点以外,每个节点里面应该有两个指针,下面我们定义一个结构体:

    1. typedef int ListDatatype;
    2. typedef struct ListNode
    3. {
    4. struct ListNode* prev;
    5. struct ListNode* next;
    6. ListDatatype data;
    7. }LTNode;

    prev指向前一个节点,next指向后一个节点。

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

    双向链表初始化:

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

     双向循环链表开始时头和尾都指向自己:

    BuyList函数的功能是创建节点,我们在初始化时用它创建哨兵位头节点,因为后面还有多次使用,所以把它封装为函数。

    双向链表打印:

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

    开辟节点函数:

    1. LTNode* BuyList(ListDatatype x)
    2. {
    3. LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
    4. if (newnode == NULL)
    5. {
    6. perror("malloc fail");
    7. return NULL;
    8. }
    9. newnode->prev = NULL;
    10. newnode->next = NULL;
    11. newnode->data = x;
    12. return newnode;
    13. }

    双向链表头插:

    头插实际是在哨兵位头节点phead的后面插入,保存住head->next的位置,然后把newnode前后分别和head、head->next链接起来就行。

    代码如下: 

    1. void ListPushFront(LTNode* phead, ListDatatype x)
    2. {
    3. assert(phead);
    4. LTNode* newnode = BuyList(x);
    5. LTNode* next = phead->next;
    6. phead->next = newnode;
    7. next->prev = newnode;
    8. newnode->next = next;
    9. newnode->prev = phead;
    10. }

    这段代码神奇的地方在于,即使链表为空,它也能头插,并且不需要判断链表是否为空

    因为就算链表为空,我们有哨兵位头节点存在,就不用担心空指针的问题。 

    双向链表尾插:

    双向链表相对于单链表的优势就是不用找尾,因为它的phead->prev就是尾,尾插同头插差不多, 把newnode的前后分别和链表的尾和头链接起来即可。

    代码如下:

    1. void ListPushBack(LTNode* phead, ListDatatype x)
    2. {
    3. assert(phead);
    4. LTNode* newnode = BuyList(x);
    5. LTNode* tail = phead->prev;
    6. tail->next = newnode;
    7. phead->prev = newnode;
    8. newnode->prev = tail;
    9. newnode->next = phead;
    10. }

    和头插一样,尾插也不用判断链表为空的情况。

    双向链表头删:

    头删指的是删除哨兵位头节点后面一个节点,只要将头节点与要删除的节点后面的节点相连接,然后free掉要删除的节点即可。

     代码如下:

    1. void ListPopFront(LTNode* phead)
    2. {
    3. assert(phead);
    4. assert(!ListEmpty(phead));
    5. LTNode* cur = phead;
    6. LTNode* first = cur->next;
    7. LTNode* second = first->next;
    8. second->prev = phead;
    9. phead->next = second;
    10. free(first);
    11. }

     删除时要注意不能删除哨兵位头节点,所以要断言一下,链表为空就不能再删了,我们也可以封装一个判断链表是否为空的函数ListEmpty():

    1. bool ListEmpty(LTNode* phead)
    2. {
    3. assert(phead);
    4. return phead->next == phead;
    5. }

    bool类型的返回值是true或者false。 

    双向链表尾删:

    尾删要保存尾节点的前一个节点,然后把前一个节点和头节点链接起来,free尾节点即可。

    代码如下:

    1. void ListPopBack(LTNode* phead)
    2. {
    3. assert(phead);
    4. assert(!ListEmpty(phead));
    5. LTNode* tail = phead->prev;
    6. LTNode* tailPrev = tail->prev;
    7. tailPrev->next = phead;
    8. phead->prev = tailPrev;
    9. free(tail);
    10. }

     注意:同头删一样,尾删也要判断是否为空链表。

     双向链表查找:

    双向循环链表的查找和单链表的查找不同,遍历时从head的下一个节点开始,到head的上一个节点(即尾节点)结束,所以判断条件有所不同,注意区分。找到时,返回该节点位置。

    代码如下:

    1. LTNode* ListSearch(LTNode*phead,ListDatatype x)
    2. {
    3. assert(phead);
    4. LTNode* cur = phead->next;
    5. while (cur != phead)
    6. {
    7. if (cur->data == x)
    8. {
    9. return cur;
    10. }
    11. cur = cur->next;
    12. }
    13. return NULL;
    14. }

    双向链表在pos之前插入:

    保存pos的前一个节点,把newnode的前后分别与pos的前一个节点和pos链接起来即可。

    要测试该功能,可以配合查找函数,先找到pos。

    代码如下:

    1. void ListInsert(LTNode* pos, ListDatatype x)
    2. {
    3. assert(pos);
    4. LTNode* newnode = BuyList(x);
    5. LTNode* posPrev = pos->prev;
    6. newnode->next = pos;
    7. newnode->prev = posPrev;
    8. posPrev->next = newnode;
    9. pos->prev = newnode;
    10. }

    这段代码可以直接在头插和尾插中复用,也就是说,我们要实现头插、尾插和任意位置插入,只用这一个函数就可以解决。

    头插:

    ListInsert(phead->next, x);

     尾插:

    ListInsert(phead, x);

    双向链表在pos位置删除:

    配合查找函数先找到pos位置,然后删除就行。

    代码如下:

    1. void ListErase(LTNode* pos)
    2. {
    3. assert(pos);
    4. LTNode* posPrev = pos->prev;
    5. LTNode* posNext = pos->next;
    6. posPrev->next = posNext;
    7. posNext->prev = posPrev;
    8. free(pos);
    9. }

    这段代码也可以同时实现头删、尾删和任意位置删除:

    头删:
     

    ListErase(phead->next);

     尾删:

    ListErase(phead->prev);

     双向链表的销毁:

    销毁也是从哨兵位的下一个节点开始,注意每次都要保存要销毁节点的后面一个节点的位置,防止找不到后面的节点,最终要把哨兵位也销毁掉。

    代码如下:

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

     以上就是双向链表的全部功能实现,下面给出完整代码:

    3.完整代码:

    test.c

    1. #define _CRT_SECURE_NO_WARNINGS 1
    2. #include"List.h"
    3. //测试
    4. ListTest1()
    5. {
    6. LTNode* plist = InitList();
    7. Print(plist);
    8. //头插
    9. ListPushFront(plist, 1);
    10. ListPushFront(plist, 2);
    11. ListPushFront(plist, 3);
    12. ListPushFront(plist, 4);
    13. Print(plist);
    14. //尾插
    15. ListPushBack(plist, 5);
    16. ListPushBack(plist, 6);
    17. ListPushBack(plist, 7);
    18. ListPushBack(plist, 8);
    19. Print(plist);
    20. //头删
    21. ListPopFront(plist);
    22. ListPopFront(plist);
    23. Print(plist);
    24. //尾删
    25. ListPopBack(plist);
    26. ListPopBack(plist);
    27. Print(plist);
    28. //在pos位置之前插入
    29. LTNode* pos = ListSearch(plist, 1);
    30. if (pos != NULL)
    31. ListInsert(pos, 666);
    32. Print(plist);
    33. //在pos位置删除
    34. pos = ListSearch(plist, 6);
    35. if(pos!=NULL)
    36. ListErase(pos);
    37. Print(plist);
    38. }
    39. int main()
    40. {
    41. ListTest1();
    42. return 0;
    43. }

    List.h

    1. #pragma once
    2. #include
    3. #include
    4. #include
    5. #include
    6. typedef int ListDatatype;
    7. typedef struct ListNode
    8. {
    9. struct ListNode* prev;
    10. struct ListNode* next;
    11. ListDatatype data;
    12. }LTNode;
    13. //双向链表初始化
    14. LTNode* InitList();
    15. //双向链表打印
    16. void Print(LTNode* phead);
    17. //双向链表头插
    18. void ListPushFront(LTNode* phead, ListDatatype x);
    19. //双向链表尾插
    20. void ListPushBack(LTNode* phead, ListDatatype x);
    21. //双向链表头删
    22. void ListPopFront(LTNode* phead);
    23. //双向链表尾删
    24. void ListPopBack(LTNode* phead);
    25. //双向链表查找
    26. LTNode* ListSearch(LTNode*phead,ListDatatype x);
    27. //在pos位置之前插入
    28. void ListInsert(LTNode*pos, ListDatatype x);
    29. //在pos位置删除
    30. void ListErase(LTNode* pos);
    31. //销毁链表
    32. void ListDestory(LTNode* phead);

    List.c

    1. #define _CRT_SECURE_NO_WARNINGS 1
    2. #include"List.h"
    3. //开辟节点函数
    4. LTNode* BuyList(ListDatatype x)
    5. {
    6. LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
    7. if (newnode == NULL)
    8. {
    9. perror("malloc fail");
    10. return NULL;
    11. }
    12. newnode->prev = NULL;
    13. newnode->next = NULL;
    14. newnode->data = x;
    15. return newnode;
    16. }
    17. //双向链表初始化
    18. LTNode* InitList()
    19. {
    20. LTNode* phead = BuyList(-1);
    21. phead->next = phead;
    22. phead->prev = phead;
    23. return phead;
    24. }
    25. //打印函数
    26. void Print(LTNode* phead)
    27. {
    28. LTNode*cur = phead->next;
    29. printf("guard<->");
    30. while (cur != phead)
    31. {
    32. printf("%d<->", cur->data);
    33. cur = cur->next;
    34. }
    35. printf("\n");
    36. }
    37. //双向链表头插
    38. void ListPushFront(LTNode* phead, ListDatatype x)
    39. {
    40. assert(phead);
    41. LTNode* newnode = BuyList(x);
    42. LTNode* next = phead->next;
    43. phead->next = newnode;
    44. next->prev = newnode;
    45. newnode->next = next;
    46. newnode->prev = phead;
    47. /*ListInsert(phead->next, x);*/
    48. }
    49. //双向链表尾插
    50. void ListPushBack(LTNode* phead, ListDatatype x)
    51. {
    52. assert(phead);
    53. LTNode* newnode = BuyList(x);
    54. LTNode* tail = phead->prev;
    55. tail->next = newnode;
    56. phead->prev = newnode;
    57. newnode->prev = tail;
    58. newnode->next = phead;
    59. /*ListInsert(phead, x);*/
    60. }
    61. //判断空链表函数
    62. bool ListEmpty(LTNode* phead)
    63. {
    64. assert(phead);
    65. return phead->next == phead;
    66. }
    67. //双向链表头删
    68. void ListPopFront(LTNode* phead)
    69. {
    70. assert(phead);
    71. assert(!ListEmpty(phead));
    72. LTNode* cur = phead;
    73. LTNode* first = cur->next;
    74. LTNode* second = first->next;
    75. second->prev = phead;
    76. phead->next = second;
    77. free(first);
    78. /*ListErase(phead->next);*/
    79. }
    80. //双向链表尾删
    81. void ListPopBack(LTNode* phead)
    82. {
    83. assert(phead);
    84. assert(!ListEmpty(phead));
    85. LTNode* tail = phead->prev;
    86. LTNode* tailPrev = tail->prev;
    87. tailPrev->next = phead;
    88. phead->prev = tailPrev;
    89. free(tail);
    90. /*ListErase(phead->prev);*/
    91. }
    92. //双向链表查找
    93. LTNode* ListSearch(LTNode*phead,ListDatatype x)
    94. {
    95. assert(phead);
    96. LTNode* cur = phead->next;
    97. while (cur != phead)
    98. {
    99. if (cur->data == x)
    100. {
    101. return cur;
    102. }
    103. cur = cur->next;
    104. }
    105. return NULL;
    106. }
    107. //双向链表在pos之前插入
    108. void ListInsert(LTNode* pos, ListDatatype x)
    109. {
    110. assert(pos);
    111. LTNode* newnode = BuyList(x);
    112. LTNode* posPrev = pos->prev;
    113. newnode->next = pos;
    114. newnode->prev = posPrev;
    115. posPrev->next = newnode;
    116. pos->prev = newnode;
    117. }
    118. //双向链表在pos位置删除
    119. void ListErase(LTNode* pos)
    120. {
    121. assert(pos);
    122. LTNode* posPrev = pos->prev;
    123. LTNode* posNext = pos->next;
    124. posPrev->next = posNext;
    125. posNext->prev = posPrev;
    126. free(pos);
    127. }
    128. //双向链表销毁
    129. void ListDestory(LTNode* phead)
    130. {
    131. assert(phead);
    132. LTNode* cur = phead->next;
    133. while (cur != phead)
    134. {
    135. LTNode* next = cur->next;
    136. free(cur);
    137. cur = next;
    138. }
    139. free(phead);
    140. }

    4.测试:

    5.顺序表和链表的区别

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

    总结一下:

    下面我们再来补充一些内容:

    这里有个问题,在计算机中使用顺序表效率高还是使用链表效率高呢?

    答案是:顺序表。

    因为在计算机中,由于运行速度不匹配的问题,CPU不会直接和主存交换数据,而是先把数据从主存中取出来放到高速缓存中,然后再进行访问数据,而访问数据会出现两种情况:

    1.如果数据在缓存中,就叫做缓存命中,可以直接访问。

    2.如果数据不在缓存中,就叫做缓存不命中,这时候需要先把数据加载到缓存中,然后再访问数据

    当缓存不命中时,计算机会把数据加载到缓存中,而加载时会将这个数据后面的数据也一起加载进去(局部性原理),如果是顺序表,因为它的内存空间是连续的,后面的数据会直接命中,这样它的缓存命中率就高;如果是链表,它一旦命中不了,也会加载一段数据,但是这些数据不一定会用,这就造成了浪费,还会导致数据污染,这样它的缓存命中率就低了。

    这就是今天关于双向链表的全部内容了,未完待续。。。

  • 相关阅读:
    互联网Java工程师面试题·Spring篇·第七弹
    如何创建线程
    盘点Vue2和Vue3的10种组件通信方式(值得收藏)
    Python数据分析--Numpy常用函数介绍(4)--Numpy中的线性关系和数据修剪压缩
    营销-活动-优惠券这么做后续会省不少事
    js逆向之反调试之无限debugger解决
    电脑计算机xinput1_3.dll丢失的解决方法分享,四种修复手段解决问题
    绕过防盗链的几种方式
    蓝桥杯备战刷题-滑动窗口
    学习笔记 --- RabbitMQ
  • 原文地址:https://blog.csdn.net/syh163/article/details/134268122