【数据结构基础/接口函数编写】双向链表之带头循环双向链表的基础接口函数功能的实现(编写思路加逻辑分析加代码实操,一应俱全的汇总)

目录

 

双向链表的基础功能的实现

基本介绍

单向链表优缺点

双向链表基本介绍​​​​​​​

链表的分类

带头双向循环链表的编写

基础头文件的包含

初始化 ListInit

尾插 ListPushBack

尾删 ListPopBack

头插 ListPushFront

头删 ListPopFront

任意位置插入 ListInsert

任意列表删除 ListErase

销毁 ListDestroy

顺序表和链表的比较

顺序表

优点

缺点

链表

优点

缺点


双向链表的基础功能的实现

基本介绍

单向链表优缺点

单向链表,查找的方向只能是一个方向,而双向链表可以向前或者向后查找。
单向链表不能自我删除,需要靠辅助节点 ,而双向链表则可以自我删除。

双向链表基本介绍

双向链表也叫双向表,是链表的一种,它由多个结点组成,每个结点都由一个数据域和两个指针域组成,数据域(data)用来存储数据,其中一个指针域(next)用来指向其后继结点,另一个指针域用来指向前驱结点(prev指针)。链表的头结点的数据域不存储数据,指向前驱结点的指针域值为null,指向后继结点的指针域指向第一个真正存储数据的结点。

链表的分类

  • 单向或者双向

  • 带头或者不带头(哨兵位的头节点,不储存有效数据)

  • 循环或者非循环(循环是特殊的环链表,最后一个指向第一个节点)

总共2^3 = 8种排列组合,但是并不是说我们需要全部学习,我们世纪中最常用的是两种结构:

  • 无头单向非循环链表

  • 带头双向循环链表

看上去带头双向循环比较难实现,但实际上比无头单向非循环更容易实现。

带头双向循环链表的编写

基础头文件的包含

 #pragma once
 #include <stdio.h>
 ​
 typedef int LTDataType;
 typedef struct ListNode
 {
     LTDataType data;
     struct ListNode *next;
     struct ListNode *prev;
 }ListNode;
 ​
 void ListInit(ListNode *phead);
 void ListPushBack(ListNode *phead, LTDataType x);

初始化 ListInit

我们发现传递一级指针并不可行,因为我们是带头的链表,需要改变plist的值,类似下面

 void ListInit(ListNode **pphead)
 {
     *pphead = (ListNode *)malloc(sizeof(ListNode));
 *pphead->prev = *pphead;
 *pphead->next = *pphead;
 }

这是一种做法,当然我们也可以用返回值接收这个形参也可以。我们本次采用第二种方法去编写。

 ListNode *ListInit()
 {
     ListNode *phead = (ListNode *)malloc(sizeof(ListNode));
     phead->prev = phead;
     phead->next = phead;
 ​
     return phead;
 }

然后我们只需要在测试代码中编写这个即可完成初始化。

 void TestList1()
 {
     ListNode *plist = ListInit();
 }

尾插 ListPushBack

双向链表是非常简单的,这是因为我们有两个指向,即可以知道节点前面的节点,也可以知道节点后面的节点。

由于是尾插,需要找到尾,但是因为是循环链表,phead的前面的节点就是尾节点了,不需要再遍历寻找尾巴了。于是这样容易得出:

 void ListPushBack(ListNode *phead, LTDataType x)
 {
     assert(phead);
     ListNode *tail = phead->prev;
     ListNode *newnode = (ListNode *)malloc(sizeof(ListNode));
     //赋值
     newnode->data = x;
     //修改指向
     tail->next = newnode;
     newnode->prev = tail;
     newnode->next = phead;
     phead->prev = newnode;
 }

尾删 ListPopBack

尾删需要注意的一个问题是,要free还要在合适的位置释放。如果释放得早,会很容易出现野指针的情况,比如

 ListNode* tail = phead->prev;
 free(phead->prev);
 tail->prev->next = phead;

这上面free掉phead->prev后,是释放掉这部分的空间,而tail正是使用这部分的空间,所以会出现野指针的问题。解决的办法就是

  • 可以先储存一下tail->prev

  • 或者最后释放

 void ListPopBack(ListNode *phead, LTDataType x)
 {
     assert(phead);
     assert(phead->prev != phead);
     ListNode *tail = phead->prev;
     ListNode *tailPrev = tail->prev;
     //修改指向
     tailPrev->next = phead;
     phead->prev = tailPrev;
     free(tail);
 }

头插 ListPushFront

 void ListPushFront(ListNode *phead, LTDataType x)
 {
     assert(phead);
     ListNode *newnode = (ListNode *)malloc(sizeof(ListNode));
     //赋值
     newnode->data = x;
     ListNode *next = phead->next;
     //修改指向
     phead->next = newnode;
     newnode->prev = phead;
     newnode->next = next;
     next->prev = newnode;
 }

头删 ListPopFront

 void ListPopFront(ListNode *phead, LTDataType x)
 {
     assert(phead->next != phead);
     assert(phead);
     ListNode *headNext = phead->next;
     //修改指向
     phead->next = headNext->next;
     headNext->next->prev = phead;
     free(headNext);
 }

任意位置插入 ListInsert

我们当前写的是在pos位置之前插入,代码如下:

 void ListInsert(ListNode *pos, LTDataType x)
 {
     assert(pos);
     ListNode *posPrev = pos->prev;
     ListNode *newnode = (ListNode *)malloc(sizeof(ListNode));
     newnode->data = x;
     //指向
     posPrev->next = newnode;
     newnode->prev = posPrev;
     newnode->next = pos;
     pos->prev = newnode;
 }

写完之后,发现前面的头插和尾插都可以复用插入函数:

 void ListPushBack(ListNode *phead, LTDataType x)
 {
     assert(phead);
 ListInsert(phead->next,x);
 }
 ​
 void ListPushFront(ListNode *phead, LTDataType x)
 {
     assert(phead);
     //就是在phead之前插入而非phead的->prev插入!!
 ListInsert(phead,x);
 }

任意列表删除 ListErase

 void ListErase(ListNode *pos)
 {
     assert(pos);
     //检查pos是不是哨兵位
     if(pos->prev == pos)
    {
         return;
    }
     ListNode *posPrev = pos->prev;
     ListNode *posNext = pos->next;
     //指向
     posPrev->next = posNext;
     posNext->prev = posPrev;
     free(pos);
 }

同样的,头删和尾删也是可以复用的。

 void ListPopBack(ListNode *phead, LTDataType x)
 {
     assert(phead);
     assert(phead->prev != phead);
     ListErase(phead->prev);
 }
 void ListPopFront(ListNode *phead, LTDataType x)
 {
     assert(phead->next != phead);
     assert(phead);
 ListErease(phead->next);
 }

销毁 ListDestroy

写销毁,一般是传递二级指针进去,然后才可以把其头节点置空,否则但留一个头节点free掉而不置空,会有野指针的风险,但是我们其他接口函数都是写的是一级指针,为了保持接口的一致性,我们可以写一级指针。

区别于之前的单向链表,双向链表的销毁是可以在外面置空而且可以销毁得干干净净的。

 void ListDestroy(ListNode *phead)
 {
     assert(phead);
     ListNode *cur = phead->next;
     while (cur!=phead)
    {
         ListNode *next = cur->next;
         free(cur);
         cur = next;
         next = next->next;
    }
     free(phead);
 }

顺序表和链表的比较

这两个结构各有优势,很难说谁更优。严格说,顺序表和链表是相辅相成的两个结构。

顺序表

优点

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

  2. cpu高速缓存利用率更高。

缺点

  1. 头部、中部插入删除时间效率低。O(N)

  1. 连续的物理空间,空间不够了以后需要增容,增容有一定程度的消耗;为了避免频繁增容,一般我们都按照倍数去增,用不完可能存在一定的空间浪费。

链表

优点

  1. 任意位置插入删除效率高。O(1)

  2. 按需申请释放空间。

缺点

  1. 不支持随机访问(用下标访问)。意味着有些算法,比如快排,二分查找等在这种结构并不适用。

  2. 链表存储一个值,同时要存储链接指针,也有一定的消耗。

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

cpu处理并非直接处理主存中的数据,而是先看主存的数据的地址在不在缓存,在就直接访问,不在就先加载到缓存,再访问。

但是顺序表的加载并非直接加载一个位置,而是加载一片,比如20个字节(具体加载多少取决于硬件体系),所以cpu高速缓存命中率高。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值