当前位置:   article > 正文

数据结构:双向链表

数据结构:双向链表

目录

一、链表的分类

二、双向链表

2.1 双向链表的结构

2.2 双向链表的实现

2.2.1 头文件

2.2.2 各个功能的实现

初始化

头部/尾部数据插入与删除

    头插

    尾插

    头删

    尾删

 指定位置的插入与删除

 查找数据

打印链表

 销毁链表

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


一、链表的分类

    链表的结构有很多种,以下情况组合就有8中链表结构:

     虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构:单链表和双向带头循环链表,上一篇我们已经讲了单链表,今天就来了解一下双向链表并将其实现。

1.无头单向非循环链表:结构简单,⼀般不会单独用来存数据,实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。

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

二、双向链表

2.1 双向链表的结构

     这里的“带头”跟“头节点”是两个概念,带头链表里的头节点,实际为“哨兵位”,哨兵位节点不存储任何有效元素,只是站在这里“放哨 的” ,存在的意义: 遍历循环链表避免死循环。

2.2 双向链表的实现

2.2.1 头文件

   还是老样子,在头文件里写出我们需要功能的函数名以及结构体的成员。

  1. typedef int LTDataType;
  2. typedef struct ListNode
  3. {
  4. struct ListNode* next; //指针保存下⼀个节点的地址
  5. struct ListNode* prev; //指针保存前⼀个节点的地址
  6. LTDataType data;
  7. }LTNode;
  8. //链表初始化
  9. void LTInit(LTNode** pphead);
  10. //销毁链表
  11. void LTDestroy(LTNode* phead);
  12. //打印链表
  13. void LTPrint(LTNode* phead);
  14. //尾部插入删除数据/头部插入删除数据
  15. void LTPushBack(LTNode* phead, LTDataType x);
  16. void LTPopBack(LTNode* phead);
  17. void LTPushFront(LTNode* phead, LTDataType x);
  18. void LTPopFront(LTNode* phead);
  19. //在指定位置之后插⼊数据
  20. void LTInsert(LTNode* pos, LTDataType x);
  21. //删除指定位置数据
  22. void LTErase(LTNode* pos);
  23. //查找数据
  24. LTNode *LTFind(LTNode* phead,LTDataType x);

2.2.2 各个功能的实现

初始化

    初始化时,我们需要创造一个“哨兵位”,而且后面每一次插入数据都需要先申请一段空间,需要写一个“creat”这样的函数。

  1. LTNode* creat(LTDataType x)
  2. {
  3. LTNode* a = (LTNode*)malloc(sizeof(LTNode));
  4. a->data = x;
  5. a->next = a;
  6. a->bef = a;
  7. return a;
  8. }
  9. void LTInit(LTNode** pphead)
  10. {
  11. *pphead = creat(-1);
  12. }
头部/尾部数据插入与删除
    头插

    注意这里的头插,是指在“哨兵位”后的第一个节点插入,不是在“哨兵位”之前插入。

  1. void LTPushFront(LTNode* phead, LTDataType x)
  2. {
  3. assert(phead);
  4. LTNode* a = creat(x);
  5. a->next = phead->next;
  6. a->bef = phead;
  7. phead->next = a;
  8. phead->next->bef = a;
  9. }
    尾插
  1. void LTPushBack(LTNode* phead, LTDataType x)
  2. {
  3. assert(phead);
  4. LTNode* a=creat(x);
  5. a->bef = phead->bef;
  6. phead->bef->next = a;
  7. phead->bef = a;
  8. a->next = phead;
  9. }
    头删

    与头插一个道理,别把“哨兵位”删除了

  1. void LTPopFront(LTNode* phead)
  2. {
  3. assert(phead && phead->next != phead);
  4. LTNode* a = phead->next;
  5. phead->next = a->next;
  6. a->next->bef = phead;
  7. free(a);
  8. a = NULL;
  9. }
    尾删
  1. void LTPopBack(LTNode* phead)
  2. {
  3. assert(phead&&phead->next!=phead);
  4. LTNode* a = phead->bef;
  5. a->bef->next = phead;
  6. phead->bef = a->bef;
  7. free(a);
  8. a = NULL;
  9. }
 指定位置的插入与删除
  1. void LTInsert(LTNode* pos, LTDataType x)//插入
  2. {
  3. LTNode* a = creat(x);
  4. pos->next->bef = a;
  5. a->next = pos->next;
  6. pos->next = a;
  7. a->bef = pos;
  8. }
  9. void LTErase(LTNode* pos)//删除
  10. {
  11. pos->bef->next = pos->next;
  12. pos->next->bef = pos->bef;
  13. free(pos);
  14. pos = NULL;
  15. }
 查找数据

    查找前先断言一下是否为空指针或者是否只有一个“哨兵位”节点,这样能够在出错时快速发现。

  1. LTNode* LTFind(LTNode* phead, LTDataType x)
  2. {
  3. assert(phead && phead->next != phead);
  4. LTNode* a = phead->next;
  5. while (a != phead)
  6. {
  7. if (a->data = x)
  8. return a;
  9. a = a->next;
  10. }
  11. return NULL;
  12. }
打印链表
  1. void LTPrint(LTNode* phead)
  2. {
  3. LTNode* a = phead->next;
  4. while (a != phead)
  5. {
  6. printf("%d->", a->data);
  7. a = a->next;
  8. }
  9. printf("\n");
  10. }
 销毁链表
  1. void LTDestroy(LTNode* phead)
  2. {
  3. LTNode* a = phead->next;
  4. while (a != phead)
  5. {
  6. LTNode* b = a->next;
  7. free(a);
  8. a =b;
  9. }
  10. free(phead);
  11. phead = NULL;
  12. }

释放完后的指针不要忘了指向空指针,避免造成野指针。 

     最后还是那句话,写完不要忘记测试,只有测试了才会知道自己的代码是否有误。

三、顺序表和链表的优缺点

    顺序表和链表各有各的优缺点,没有高低之分,根据不同的需求运用在不同的场景


     本篇的内容就到这里了,希望对各位有帮助,如果有错误欢迎指出。

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

闽ICP备14008679号