• 数据结构·双向链表


    目录

    链表的分类

    带头双向循环链表

    List.h

    List.c

    Test.c

    顺序表和链表的区别

    结束语


    链表的分类

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

              前面实现了单向链表 -- 但是在使用的过程中会发现,在尾部的操作在时间复杂度是O(N)级别,而且在扩容方面存在着较多的缺陷,导致了效率还是较低,为此我们引入了双向链表,这时才可以完美解决顺序表的所以缺陷

    1. 无头单向非循环链表: 结构简单 ,一般不会单独用来存数据。实际中更多是作为 其他数据结 构的子结构 ,如哈希桶、图的邻接表等等。另外这种结构在 笔试面试 中出现很多。
    2. 带头双向循环链表: 结构最复杂 ,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面代码实现了就知道了。

    带头双向循环链表

    List.h

    1. #pragma once
    2. #include
    3. #include
    4. #include
    5. #include
    6. typedef int LTDataType; //重定义 方便改变数据类型
    7. typedef struct ListNode
    8. {
    9. struct ListNode* next;
    10. struct ListNode* prve;
    11. LTDataType data;
    12. }LTNode;
    13. //初始话可以使用二级指针,也可以利用返回值来解决
    14. //1.二级指针初始化
    15. //void ListInit(LTNode** pphead);
    16. //2.返回值初始化
    17. LTNode* ListInit();
    18. //销毁链表 -- 遍历节点一个一个释放
    19. void ListDestory(LTNode* phead);
    20. //打印
    21. void ListPrint(LTNode* phead);
    22. //尾插
    23. void ListPushBack(LTNode* phead, LTDataType x);
    24. //头插
    25. void ListPushFront(LTNode* phead, LTDataType x);
    26. //判断空链表(不包括哨兵位的头节点)
    27. bool ListEmpty(LTNode* phead);
    28. //尾删
    29. void ListPopBack(LTNode* phead);
    30. //头删
    31. void ListPopFront(LTNode* phead);
    32. //显示节点数量 -- 一般是单独把数量放在一个接口里面,尽量不要放在结构体里面
    33. size_t ListSize(LTNode* phead);
    34. //查找
    35. LTNode* ListFind(LTNode* phead, LTDataType x);
    36. //在pos之前插入
    37. void ListInsert(LTNode* pos, LTDataType x);
    38. //删除pos节点
    39. void ListErase(LTNode* pos);

    List.c

    1. #include "List.h"
    2. //初始化
    3. LTNode* ListInit()
    4. {
    5. LTNode* guard = (LTNode*)malloc(sizeof(LTNode));
    6. if (guard == NULL)
    7. {
    8. perror("malloc fail");
    9. exit(-1);
    10. }
    11. guard->next = guard;
    12. guard->prve = guard;
    13. return guard;
    14. }
    15. //创立一个新的节点
    16. LTNode* BuyListNode(LTDataType x)
    17. {
    18. LTNode* node = (LTNode*)malloc(sizeof(LTNode));
    19. if (node == NULL)
    20. {
    21. perror("malloc fail");
    22. exit(-1);
    23. }
    24. node->next = NULL; //新的节点置空
    25. node->prve = NULL;
    26. node->data = x;
    27. return node;
    28. }
    29. //打印
    30. void ListPrint(LTNode* phead)
    31. {
    32. assert(phead); //一定不会为空因为有哨兵位的头节点
    33. printf("phead<=>");
    34. LTNode* cur = phead->next; //当cur为哨兵位的头节点时就停下来结束打印
    35. while (cur != phead)
    36. {
    37. printf("%d<=>", cur->data);
    38. cur = cur->next;
    39. }
    40. printf("\n");
    41. }
    42. //尾插
    43. void ListPushBack(LTNode* phead, LTDataType x)
    44. {
    45. assert(phead); //因为引入了哨兵位的头所以一定不会为空
    46. //LTNode* newnode = BuyListNode(x);
    47. 利用头直接找到尾
    48. //LTNode* tail = phead->prve;
    49. //tail->next = newnode;
    50. //newnode->prve = tail;
    51. //newnode->next = phead;
    52. //phead->prve = newnode;
    53. //当写好任意位置的插入时,就可以直接调用,不需要再写上面的了 -- 创建链表效率高
    54. ListInsert(phead, x); //在phead的前面插入一个就相当于尾插了
    55. }
    56. //头插
    57. void ListPushFront(LTNode* phead, LTDataType x)
    58. {
    59. assert(phead);
    60. //1.这样子写需要注意先后顺序 -- 防止覆盖找不到下一个节点
    61. //LTNode* newnode = BuyListNode(x);
    62. //phead->next->prve = newnode;
    63. //newnode->next = phead->next;
    64. //phead->next = newnode;
    65. //newnode->prve = phead;
    66. //2.也可以创立一个新的指针来保存下一个节点,这样就可以随便改了
    67. //LTNode* newnode = BuyListNode(x);
    68. //LTNode* first = phead->next;
    69. //phead->next = newnode;
    70. //newnode->prve = phead;
    71. //newnode->next = first;
    72. //first->prve = newnode;
    73. //同尾插一样 -- 当写好任意位置的插入时,就可以直接调用,不需要再写上面的了
    74. ListInsert(phead->next, x);
    75. }
    76. //判断空链表(不包括哨兵位的头节点)
    77. bool ListEmpty(LTNode* phead)
    78. {
    79. assert(phead);
    80. //1.可以这样写 -- 较易理解
    81. //if (phead->next == NULL)
    82. // return true;
    83. //else
    84. // return false;
    85. //2.也可以直接返回
    86. return phead->next == phead; //相等就是真(为空)否则就是假(不为空)
    87. }
    88. //尾删
    89. void ListPopBack(LTNode* phead)
    90. {
    91. assert(phead);
    92. assert(!ListEmpty(phead)); //是空返回真,!致反就为假,就报错
    93. //LTNode* tail = phead->prve;
    94. //LTNode* prve = tail->prve;
    95. //phead->prve = prve;
    96. //prve->next = phead;
    97. //free(tail);
    98. //可以置空 -- 习惯,实际不起作用
    99. //tail = NULL;
    100. //一样一样
    101. ListErase(phead->prve);
    102. }
    103. //头删
    104. void ListPopFront(LTNode* phead)
    105. {
    106. assert(phead);
    107. assert(!ListEmpty(phead));
    108. //LTNode* frist = phead->next;
    109. //LTNode* second = frist->next;
    110. //phead->next = second;
    111. //second->prve = phead;
    112. //free(frist);
    113. //frist = NULL;
    114. //一样一样
    115. ListErase(phead->next);
    116. }
    117. //显示节点数量
    118. size_t ListSize(LTNode* phead)
    119. {
    120. assert(phead);
    121. size_t n = 0;
    122. LTNode* cur = phead->next;
    123. while (cur != phead)
    124. {
    125. ++n;
    126. cur = cur->next;
    127. }
    128. return n;
    129. }
    130. //查找
    131. LTNode* ListFind(LTNode* phead,LTDataType x)
    132. {
    133. assert(phead);
    134. LTNode* cur = phead->next;
    135. while (cur != phead)
    136. {
    137. if (cur->data == x)
    138. {
    139. return cur;
    140. }
    141. cur = cur->next;
    142. }
    143. return NULL;
    144. }
    145. //在pos之前插入
    146. void ListInsert(LTNode* pos, LTDataType x)
    147. {
    148. assert(pos); //因为这里不需要传原链表,所以没有判断链表是否为空
    149. LTNode* prve = pos->prve;
    150. LTNode* newnode = BuyListNode(x);
    151. // prve newnode pos 三个指针的链接
    152. prve->next = newnode;
    153. newnode->prve = prve;
    154. newnode->next = pos;
    155. pos->prve = newnode;
    156. }
    157. //删除pos节点
    158. void ListErase(LTNode* pos) //也没有传哨兵位的头节点 -- 所以不检查了
    159. {
    160. assert(pos);
    161. LTNode* prve = pos->prve;
    162. LTNode* next = pos->next;
    163. prve->next = next;
    164. next->prve = prve;
    165. free(pos);
    166. //pos->next == NULL; //这里是习惯 -- 因为不是二级指针 这里改是没用的
    167. }
    168. //销毁链表 -- 遍历节点一个一个释放
    169. //可以传二级指针,这样就内部置空 -- 但是也可以不置空,让调用的人自己置空(保持接口的一致性,都使用一级指针)
    170. void ListDestory(LTNode* phead)
    171. {
    172. assert(phead);
    173. LTNode* cur = phead->next;
    174. while (cur != phead)
    175. {
    176. LTNode* next = cur->next;
    177. free(cur);
    178. cur = next;
    179. }
    180. free(phead);
    181. //phead = NULL; -- 这里置空没有用
    182. }

    Test.c

    1. #include "List.h"
    2. //测试
    3. void TestList1()
    4. {
    5. //二级指针初始化
    6. //LTNode* plist = NULL;
    7. //ListInit(&plist);
    8. //返回值初始化
    9. LTNode* plist = ListInit();
    10. //尾插
    11. ListPushBack(plist, 1);
    12. ListPushBack(plist, 2);
    13. ListPushBack(plist, 3);
    14. ListPushBack(plist, 4);
    15. //打印
    16. ListPrint(plist);
    17. //头插
    18. ListPushFront(plist, 10);
    19. ListPushFront(plist, 20);
    20. ListPushFront(plist, 30);
    21. ListPushFront(plist, 40);
    22. //打印
    23. ListPrint(plist);
    24. //尾删
    25. ListPopBack(plist);
    26. ListPopBack(plist);
    27. ListPopBack(plist);
    28. ListPopBack(plist);
    29. //打印
    30. ListPrint(plist);
    31. //头删
    32. ListPopFront(plist);
    33. //打印
    34. ListPrint(plist);
    35. //显示节点数 -- %zu专门打印size_t类型的打印格式
    36. printf("%zu\n", ListSize(plist));
    37. //查找并在其前面插入一个节点
    38. ListInsert(ListFind(plist,10),200);
    39. //打印
    40. ListPrint(plist);
    41. //查找并在其删除这个节点
    42. ListErase(ListFind(plist, 200));
    43. //打印
    44. ListPrint(plist);
    45. ListPopFront(plist);
    46. //打印
    47. ListPrint(plist);
    48. //销毁链表 -- 因为内部不置空,所以外部要置空
    49. ListDestory(plist);
    50. plist = NULL;
    51. }
    52. int main()
    53. {
    54. TestList1();
    55. return 0;
    56. }

            到了这里关于链表的问题也差不多入门了,这也是在实际中运用广泛的一种链表了,可以很好的解决单链表的缺点,它完美的解决了增删查改的单链表的各个方面的不便。但是关于链表与顺序表之间它们又有很多方面的不同,在实际的运用也要加以区分。

    顺序表和链表的区别

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

    备注:缓存利用率参考存储体系结构 以及 局部原理性

    顺序表的优缺点

    优点 

    1、尾插尾删效率很高

    2、随机访问(用下标访问) 

    3、相比链表结构:cpu高速缓存命中率更高 

    缺点

    1、头部和中部插入删除效率低 -- O(N)

    2、扩容 -- 性能消耗 + 空间浪费

    链表的优缺点

    优点 

    1、任意位置插入删除效率很高 -- O(1)

    2、按需申请释放

    缺点

    1、不支持随机访问

    关于cpu告诉缓存命中率 

     

    在电脑的任务管理器中->性能->cpu中右下角可以看到

     

     

    cpu执行指令,不会直接访问内存

    1、先看数据在不在三级缓存,在(命中)。直接访问

    2、不在(不命中),先加载到缓存,再访问

            加载的时候,由于顺序表开辟的空间是连续的空间,找到一个就相当于找到了后面的数据,而链表开辟的空间并不是连续的,所以访问到的命中率的概率就会较低

    与程序员相关的CPU缓存知识 | 酷 壳 - CoolShell

    结束语

    上穷碧落下黄泉,两处茫茫皆不见。
                                                            唐·白居易 《长恨歌》 

  • 相关阅读:
    14 【TS类型声明 keepAlive】
    emq证书过期问题
    项目部署服务器【java】
    python如何判断字符串中的所有字符都是字母——isalpha函数的用法及实例
    设计模式:代理模式详解
    【附源码】计算机毕业设计SSM设计与实现大学常规信息管理系统
    从缺货到暴跌,芯片江湖的故事正愈演愈烈
    汽车空调工作总结
    SpringBoot中幕——配置文件properties与yml
    【Word】页眉编辑小技巧
  • 原文地址:https://blog.csdn.net/weixin_67595436/article/details/126192708