当前位置:   article > 正文

【数据结构】--双向链表

双向链表

本篇文章经过检测基本没问题,放心

目录

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

一、ListInit--初始化函数

二、CreateListNode--创造节点函数

三、ListPushBack--尾插函数

四、ListPrint--打印函数

五、ListPopback--尾删函数

六、ListPushFront--头插函数

七、ListPopFront--头删函数

八、ListFind---查找函数

九、ListInsert--指定位置pos前面插入函数

十、 ListErase--指定pos位置删除节点的函数

十一、ListDestroy--销毁链表函数

二、回顾链表与顺序表的优缺点


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

①双链表的头结点:

头结点只是为了操作更方便,有头结点功能实现更简洁,但是你可以在功能实现中忽略他,比如打印数据,插入数据等都不要动这个头结点,不打印头结点的数据等,并且删除更不要动头结点,而是要删除头结点后面的节点

②实际中链表的结构非常多样,以下情况组合有8中链表结构:

1、单向、双向

2、带头、不带头

3、循环、非循环

 

 注意:哨兵位的头结点不存储任何有效数据,带头双向循环链表的实现比之前学的无头单向非循环更简单,有头的链表无需传二级指针改变指向了,他很方便,他跟双向单向,循环不循环无关,只要带头它对某些操作就很方便

在之前单向链表的实现过程我们就知道,如果想改变你传进来的链表,你需要传链表头的地址,也就是用二级指针,但也有另外一种方法,即使用返回值,返回这个链表类型的头结点

需要头是因为有了头结点对于我们插入等操作,都很方便

那么我们必须知道双向带头循环链表为空是什么样的,为空就是只有一个头结点,如下图:

 用vs分三个文件:List.c List.h test.c                                                                                              

首先我们是用以下操作,定义了链表,和链表每个节点的数据类型                                                 

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

一、ListInit--初始化函数                                                                                                              

所以初始化双向带头循环链表就是开辟一个头,然后两个指针域都指向自己就可以

  1. LTNode* ListInit(void)
  2. {
  3. //哨兵位的头结点
  4. LTNode* phead = (LTNode*)malloc(sizeof(LTNode));
  5. phead->prev = phead;
  6. phead->next = phead;
  7. return phead;
  8. }

二、CreateListNode--创造节点函数

  1. LTNode* CreateListNode(LTDateType x)
  2. {
  3. LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
  4. if (newnode == NULL)
  5. {
  6. exit(-1);
  7. }
  8. newnode->data = x;
  9. newnode->next = NULL;
  10. newnode->prev = NULL;
  11. return newnode;;
  12. }

三、ListPushBack--尾插函数

尾插是否要传二级指针,因为尾插你并不改变结构体指向的头,这个链表你已经创建一个头了,它是改变这个链表(结构体)的内容。注:你传入的plist一定是初始化完带头的,所以要assert断言一下,防止你传入的是无头的,防止是NULL。

那这么写对空链表(只有一个头的时候)尾插时是否需要特殊考虑呢?不需要,因为它带头,空链表也是符合的,但是我们之前实现的单链表无头是需要特殊考虑的,这就是带头链表的优势,当然,他也有对应的缺点,比如要释放它,并且函数返回时返回的不是头结点,而是下一个。

  1. void ListPushBack(LTNode* phead)
  2. {
  3. assert(phead);
  4. //断言是因为如果你没有创建哨兵位的头结点,assert
  5. //就会帮你直接报错,防止你出现错误
  6. LTNode* tail = phead->prev;
  7. LTNode* newnode = CreateListNode(x);
  8. //phead tail newnode
  9. tail->next = newnode;
  10. newnode->prev = tail;
  11. newnode->next = phead;
  12. phead->prev = newnode;
  13. }

四、ListPrint--打印函数

因为带头,故只需从头结点的后一个节点开始遍历,而头结点无需遍历,因为头不存储有效数据,你无需打印头结点的数据,头结点只是为了你操作方便而涉及,他的数据域是随机值。遍历到什么时候?循环链表无NULL,但我们遍历到头(phead)就可以,也就是从头结点的后一个节点开始,往后打印数据,直到遇到了头结点,说明我已经遍历一次了。

  1. void ListPrint(LTNode* phead)
  2. {
  3. assert(phead);
  4. //断言的意思是你传的链表一定至少有个头
  5. //因为这里讲的就是有头结点的链表,不能传入NULL
  6. //也就是不能传入没有头结点的链表
  7. LTNode* cur = phead->next;
  8. while (cur != phead)
  9. {
  10. printf("%d", cur->data);
  11. cur = cur->next;
  12. }
  13. printf("\n");
  14. }

五、ListPopback--尾删函数

尾删就是需要注意先保存尾删之前一个的节点,改变指向,最后再释放尾节点,因为如果你先释放,释放导致的结果就是你的尾节点的地址和值都会变成随机值。而你此时对已经释放的地址去访问(因为你tail保存原来的尾节点地址了),那么再改变指向,就是会对NULL解引用,那么就会非法访问内存。这是经典的野指针问题。

还有一个问题,这个链表的头结点不能删除,删到头结点之前应该也加一个断言。

  1. void ListPopBack(LTNode* phead)
  2. {
  3. assert(phead);//传进来的链表的头不能为NULL,要有头结点
  4. assert(phead->next != phead);//这种情况如果相等是链表为空的情况,就不能往下删了
  5. //当然,用if也可以,但是更推荐assert这种暴力法
  6. //这里也可以写成assert(phead->prev != phead);也可以,因为都表示链表为空的状态
  7. LTNode* tail = phead->prev;
  8. phead->prev = tail->prev;
  9. tail->prev->next = phead;
  10. free(tail);
  11. //也可以写成如下,如下的方法更推荐!
  12. /*assert(phead);
  13. assert(phead->next != phead);
  14. LTNode* tail = phead->prev;
  15. LTNode* tailprev = tail->prev;
  16. free(tail);
  17. tailprev->next = phead;
  18. phead->prev = tailprev;*/
  19. }

六、ListPushFront--头插函数

注意头插,因为有头结点,头插直接插在头的后面就可以,一定不要插在头结点的前面。那么还需考虑临界问题,若链表为空链表呢?还是可以的,这就是因为带头结点的优势。

  1. void ListPushFront(LTNode* phead, LTDateType x)
  2. {
  3. assert(phead);//你传入的链表必须有头
  4. LTNode* newnode = CreateListNode(x);
  5. if (newnode == NULL)
  6. exit(-1);
  7. //exit(-1)表示异常退出程序
  8. //eixt(0)表示正常退出程序
  9. //动态开辟内存都失败了,肯定属于异常退出了
  10. LTNode* next = phead->next;
  11. phead->next = newnode;
  12. newnode->prev = phead;
  13. newnode->next = next;
  14. next->prev = newnode;
  15. }

七、ListPopFront--头删函数

带头的链表关于删除的问题,一定不能把头结点删除了,所以加一个断言,并且,头删一定是从头结点后面的那个节点开始删除,不要动头结点。

  1. void ListPopFront(LTNode* phead)
  2. {
  3. assert(phead);
  4. //链表为空的情况(只有头结点了),就不能继续删除了
  5. assert(phead->next != phead);//不能动头结点
  6. LTNode* next = phead->next;
  7. LTNode* nextNext = next->next;
  8. phead->next = nextNext;
  9. nextNext->prev = phead;
  10. free(next);
  11. }

八、ListFind---查找函数

查找意味着遍历这个链表一次,但是循环链表没有NULL,那这个函数实现和ListPrint函数实现的思路很类似了,找到头了,那就意味这个链表遍历了一遍,因为尾节点是指向头的。我们规定如果没找到返回这个节点,则返回NULL

  1. LTNode* ListFind(LTNode* phead, LTDateType x)
  2. {
  3. assert(phead);
  4. LTNode* cur = phead->next;//从头结点的后一个节点开始遍历
  5. while (cur != phead)
  6. {
  7. if (cur->data == x)
  8. return cur;
  9. cur = cur->next;
  10. }
  11. return NULL;
  12. }

九、ListInsert--指定位置pos前面插入函数

注意我们这里不用下标,因为之前我们已经写了一个ListFind查找函数,我们用pos可以更方便的使用查找函数。pos是一个节点,即LTNode类型的。pos肯定是通过ListFind函数查找到的,所以不可能存在插入节点在头结点之前的情况。

  1. //在pos位置之前插入
  2. void ListInsert(LTNode* pos, LTDateType x)
  3. {
  4. assert(pos);//因为查找节点存在节点为NULL的情况,即不存在
  5. LTNode* posprev = pos->prev;
  6. LTNode* newnode = CreateListNode(x);
  7. posprev->next = newnode;
  8. newnode->prev = posprev;
  9. newnode->next = pos;
  10. pos->prev = newnode;
  11. }

那么有了ListInsert函数就可以改一下头插和尾插函数,均可用ListInsert函数实现,下面为用ListInsert改头插尾插函数的实现逻辑

也就是可以改成 

  1. void ListPushBack(LTNode* phead,LTDateType x)
  2. {
  3. assert(phead);
  4. ListInsert(phead,x);
  5. //在头结点的前面插入一个节点,就实现了尾插
  6. //因为头结点的前面插入一个节点就会被看做尾节点,因为指针域等原因
  7. }
  8. void ListPushFront(LTNode* phead,LTDateType x)
  9. {
  10. assert(phead);
  11. ListInsert(phead->next,x);
  12. //在头结点的下一个节点前面插入一个节点,就实现了头插
  13. }

十、 ListErase--指定pos位置删除节点的函数

思路和指定插入大差不差,但是为什么没有断言或者说判断会不会删到头结点的情况,因为pos是通过ListFind函数找到的,他肯定是LTNode类型的,也就是一个结点,而ListFind函数只能找到这个链表中的除了头结点以外的其他节点,所以不存在会删掉头结点的情况。

  1. //删除pos位置的节点
  2. void ListErase(LTNode* pos)
  3. {
  4. assert(pos);
  5. LTNode* posprev = pos->prev;
  6. LTNode* posnext = pos->next;
  7. posprev->next = posnext;
  8. posnext->prev = posprev;
  9. free(pos);
  10. pos = NULL;
  11. //pos不置为NULL也可以,因为他就一个函数内的局部变量
  12. //出了函数就找不到了
  13. }

同理,头删和尾删都可以用ListErase来修改,代码如下

  1. void ListPopBack(LTNode* phead,LTDateType x)
  2. {
  3. assert(phead);
  4. ListErase(phead->prev,x);
  5. }
  6. void ListPopFront(LTNode* phead,LTDateType x)
  7. {
  8. assert(phead);
  9. ListErase(phead->next,x);
  10. }

十一、ListDestroy--销毁链表函数

销毁链表就是释放动态开辟的内存,归还给操作系统。

并且头结点也应该释放,头指针置为空,这个操作涉及头结点,因为在ListDestroy函数中最后会free掉头结点,那你头结点也要置NULL吧,但是因为你是一级指针你改变函数内部的phead,并不会影响函数外部的plist,但你为了保持接口函数一致性而不用二级指针,那么可以在主函数ListDestroy用完之后,主动写一个plist = NULL。

  1. void ListDestroy(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. //phead = NULL;这个操作并不会改变外面的plist,因为你没用二级指针,所以可以在主函数主动置plist为NULL
  13. }

最后代码如下:

test.c:

  1. #define _CRT_SECURE_NO_WARNINGS 1
  2. #include"List.h"
  3. void TestList1()
  4. {
  5. LTNode* plist = ListInit();
  6. ListPushBack(plist, 1);
  7. ListPushBack(plist, 3);
  8. ListPushBack(plist, 1);
  9. ListPushBack(plist, 4);
  10. ListPushFront(plist, 1);
  11. ListPushFront(plist, 2);
  12. ListPushFront(plist, 5);
  13. ListPrint(plist);
  14. LTNode* pos = ListFind(plist, 1);
  15. if (pos)
  16. {
  17. ListErase(pos);
  18. }
  19. pos = ListFind(plist, 3);
  20. if (pos)
  21. {
  22. ListErase(pos);
  23. }
  24. pos = ListFind(plist, 1);
  25. if (pos)
  26. {
  27. ListErase(pos);
  28. }
  29. pos = ListFind(plist, 4);
  30. if (pos)
  31. {
  32. ListErase(pos);
  33. }
  34. ListPrint(plist);
  35. //ListDestroy(plist);
  36. //plist = NULL;
  37. }
  38. int main()
  39. {
  40. TestList1();
  41. return 0;
  42. }
  43. //void ListPopBack(LTNode* phead)
  44. //{
  45. // assert(phead);
  46. // assert(phead->next != phead);
  47. // ListErase(phead->prev);
  48. //}
  49. //void ListPopFront(LTNode* phead)
  50. //{
  51. // assert(phead);
  52. // assert(phead->next != phead);
  53. // ListErase(phead->next);
  54. //}

 List.c:

 

  1. #define _CRT_SECURE_NO_WARNINGS 1
  2. #include"List.h"
  3. LTNode* ListInit()
  4. {
  5. //哨兵位的头结点
  6. LTNode* phead = (LTNode*)malloc(sizeof(LTNode));
  7. phead->prev = phead;
  8. phead->next = phead;
  9. return phead;
  10. }
  11. LTNode* CreateListNode(LTDateType x)
  12. {
  13. LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
  14. if (newnode == NULL)
  15. {
  16. exit(-1);
  17. }
  18. newnode->data = x;
  19. newnode->next = NULL;
  20. newnode->prev = NULL;
  21. return newnode;;
  22. }
  23. void ListPushBack(LTNode* phead,LTDateType x)
  24. {
  25. assert(phead);
  26. //断言是因为如果你没有创建哨兵位的头结点,assert
  27. //就会帮你直接报错,防止你出现错误
  28. LTNode* tail = phead->prev;
  29. LTNode* newnode = CreateListNode(x);
  30. //phead tail newnode
  31. tail->next = newnode;
  32. newnode->prev = tail;
  33. newnode->next = phead;
  34. phead->prev = newnode;
  35. }
  36. void ListPrint(LTNode* phead)
  37. {
  38. assert(phead);
  39. //断言的意思是你传的链表一定至少有个头
  40. //因为这里讲的就是有头结点的链表
  41. LTNode* cur = phead->next;
  42. while (cur != phead)
  43. {
  44. printf("%d", cur->data);
  45. cur = cur->next;
  46. }
  47. printf("\n");
  48. }
  49. void ListPopBack(LTNode* phead)
  50. {
  51. assert(phead);//传进来的链表的头不能为NULL,要有头结点
  52. assert(phead->next != phead);//这种情况如果相等是链表为空的情况,就不能往下删了
  53. //当然,用if也可以,但是更推荐assert
  54. //这里也可以写成assert(phead->prev != phead);也可以,因为都表示链表为空的状态
  55. LTNode* tail = phead->prev;
  56. phead->prev = tail->prev;
  57. tail->prev->next = phead;
  58. free(tail);
  59. //也可以写成
  60. /*assert(phead);
  61. assert(phead->next != phead);
  62. LTNode* tail = phead->prev;
  63. LTNode* tailprev = tail->prev;
  64. free(tail);
  65. tailprev->next = phead;
  66. phead->prev = tailprev;*/
  67. }
  68. void ListPushFront(LTNode* phead, LTDateType x)
  69. {
  70. assert(phead);//你传入的链表必须有头
  71. LTNode* newnode = CreateListNode(x);
  72. if (newnode == NULL)
  73. exit(-1);
  74. LTNode* next = phead->next;
  75. phead->next = newnode;
  76. newnode->prev = phead;
  77. newnode->next = next;
  78. next->prev = newnode;
  79. }
  80. void ListPopFront(LTNode* phead)
  81. {
  82. assert(phead);
  83. //链表为空的情况(只有头结点了),就不能继续删除了
  84. assert(phead->next != phead);
  85. LTNode* next = phead->next;
  86. LTNode* nextNext = next->next;
  87. phead->next = nextNext;
  88. nextNext->prev = phead;
  89. free(next);
  90. }
  91. LTNode* ListFind(LTNode* phead, LTDateType x)
  92. {
  93. assert(phead);
  94. LTNode* cur = phead->next;
  95. while (cur != phead)
  96. {
  97. if (cur->data == x)
  98. return cur;
  99. cur = cur->next;
  100. }
  101. return NULL;
  102. }
  103. //在pos位置之前插入
  104. void ListInsert(LTNode* pos, LTDateType x)
  105. {
  106. assert(pos);
  107. LTNode* posprev = pos->prev;
  108. LTNode* newnode = CreateListNode(x);
  109. posprev->next = newnode;
  110. newnode->prev = posprev;
  111. newnode->next = pos;
  112. pos->prev = newnode;
  113. }
  114. //删除pos位置的节点
  115. void ListErase(LTNode* pos)
  116. {
  117. assert(pos);
  118. LTNode* posprev = pos->prev;
  119. LTNode* posnext = pos->next;
  120. posprev->next = posnext;
  121. posnext->prev = posprev;
  122. free(pos);
  123. pos = NULL;
  124. //pos不置为NULL也可以,因为他就一个函数内的局部变量
  125. //出了函数就找不到了
  126. }
  127. void ListDestroy(LTNode* phead)
  128. {
  129. assert(phead);
  130. LTNode* cur = phead->next;
  131. while (cur != phead)
  132. {
  133. LTNode* next = cur->next;
  134. free(cur);
  135. cur = next;
  136. }
  137. free(phead);
  138. //phead = NULL;这个操作并不会改变外面的plist,因为你没用二级指针,所以可以在主函数主动置plist为NULL
  139. }

List.h :

  1. #pragma once
  2. #include<stdio.h>
  3. #include<stdlib.h>
  4. #include<assert.h>
  5. typedef int LTDateType;
  6. typedef struct ListNode
  7. {
  8. LTDateType data;
  9. struct ListNode* next;
  10. struct ListNode* prev;
  11. }LTNode;
  12. LTNode* ListInit(void);
  13. void ListPrint(LTNode* phead);
  14. void ListPushBack(LTNode* phead, LTDateType x);
  15. void ListPopBack(LTNode* phead);
  16. void ListPushFront(LTNode* phead, LTDateType x);
  17. void ListPopFront(LTNode* phead);
  18. LTNode* ListFind(LTNode* phead, LTDateType x);
  19. void ListInsert(LTNode* pos, LTDateType x);
  20. void ListErase(LTNode* pos);
  21. void ListDestroy(LTNode* phead);

二、回顾链表与顺序表的优缺点

那么最后总结一下链表和顺序表的区别:

这两个结构各有优势,很难说谁更优秀

严格来说,他们俩是相辅相成的两个结构,各有优缺点

顺序表

优点:

1、支持随机访问。需要随机访问结构支持算法可以很好的适用。

2、cpu高速缓存命中率更高。

缺点:

1、头部中部插入删除时间效率低。时间复杂度:o(n)

2、连续的物理空间,空间不够了以后需要增容。

     a、增容有一定程度的消耗。 

     b、为了避免频繁增容,我们一般会按倍数去增,用不完可能存在一定的空间浪费

链表(双向带头循环链表)

优点:

1、任意位置插入删除效率高。时间复杂度:o(1)

2、按需申请释放空间。

缺点:

1、不支持随机访问。(用下标访问)意味着;一些排序,二分查找等在这种结构上不适用。

2、链表存储一个值,同时要存储链接指针,也有一定的消耗,但这个缺点问题不大。

3、cpu高速缓存命中率更低。

关于cpu命中率的理解:

注意:了解即可,cache是缓冲的意思,cpu旁边两个东西是一级二级三级缓冲区和寄存器。

而你取顺序表中的内存时,因为计算机本身有很大概率取这个位置临近的内存,顺序表因为本身的连续性,可以把后面的也取到,而链表不连续,你拿到一个地方的内存,无法访问到后面的,会造成命中率变低 

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

闽ICP备14008679号