【数据结构】双向带头循环链表(笔记总结)

👦个人主页:@Weraphael
✍🏻作者简介:目前学习C++和算法
✈️专栏:数据结构
🐋 希望大家多多支持,咱一起进步!😁
如果文章对你有帮助的话
欢迎 评论💬 点赞👍🏻 收藏 📂 加关注


前景回顾

单链表这篇博客中,我们已经实现了单链表的增删查改。今天这篇博客,我将带领大家实现最后一个常见的链表之双向带头循环链表

一、结构介绍

在这里插入图片描述

如上图所示,双向带头循环链表顾名思义就是有一个哨兵位的头结点,然而这个头结点却不存储有效数据;其次,一个结点存储两个地址,一个地址是存储下一个结点的地址,而另一个地址存储的是上一个结点的地址。

综上,不难可以写出它的结构

typedef int DLDataType;

typedef struct DListNode
{
	DLDataType data;
	struct DListNode* prev;//指向下一个结点
	struct DListNode* next;//指向前一个结点
}DTNode;

二、准备工作

为了方便管理,我们可以创建多个文件来实现

test.c - 测试代码逻辑 (源文件)
DList.c - 动态的实现 (源文件)
DList.h - 存放函数的声明 (头文件)
在这里插入图片描述

三、接口

【DList.h】


typedef int DTDataType;

typedef struct DListNode
{
	DTDataType data;
	struct DListNode* prev;//指向下一个结点
	struct DListNode* next;//指向前一个结点
}DTNode;

//开辟新结点
DTNode* BuyListNode(DTDataType x);
//初始化哨兵位头结点
DTNode* DTInit();
//尾插
void DTPushBack(DTNode* phead, DTDataType x);
//打印
void DTPrint(DTNode* phead);
//尾删
void DTPopBack(DTNode* phead);
//判断链表是否为空
bool DTEmpty(DTNode* phead);
//头插
void DTPushFront(DTNode* phead, DTDataType x);
//头删
void DTPopFront(DTNode* phead);
//在pos之前插入x
void DTInsert(DTNode* pos, DTDataType x);
//删除pos结点
void DTErase(DTNode* pos);
//查找
DTNode* DTFind(DTNode* phead, DTDataType x);
//释放
void DTDestroy(DTNode* phead);

四、代码实现

4.1 开辟新结点

//开辟新结点
DTNode* BuyListNode(DTDataType x)
{
	DTNode* newnode = (DTNode*)malloc(sizeof(DTNode));
	if (newnode == NULL)
	{
		perror("newnode :: malloc");
		return NULL;
	}
	newnode->next = NULL;
	newnode->prev = NULL;
	newnode->data = x;

	return newnode;
}

作用:有这个接口是因为后面的头结点初始化、尾插、头插等都需要开辟新的结点,有这个接口方便代码复用。

4.2 初始化哨兵位头结点

//初始化哨兵位头结点
DTNode* DTInit()
{
	DTNode* phead = BuyListNode(-1);
	phead->next = phead;
	phead->prev = phead;
	
	return phead;
}

【笔记总结】

  1. 哨兵位的头结点是不存放有意义的数据
  2. 由于是循环链表,初始化时应该自己指向自己

4.3 尾插

//尾插
void DTPushBack(DTNode* phead, DTDataType x)
{
	//哨兵位绝对不可能为空
	assert(phead);
	// 1.开辟新结点
	DTNode* newnode = BuyListNode(x);
	// 2.找尾
	DTNode* tail = phead->prev;
	// 3.链接  head tail newnode
	tail->next = newnode;
	newnode->prev = tail;
	newnode->next = phead;
	phead->prev = newnode;
}

【笔记总结】

  1. 哨兵位的头结点绝对不可能为空,所以加个断言
  2. 双向循环链表找尾不需要向单链表那样遍历,因为头结点的prev就是尾

【动画展示】

在这里插入图片描述

4.4 打印

//打印
void DTPrint(DTNode* phead)
{
	assert(phead);

	DTNode* cur = phead->next;
	while (cur != phead)
	{
		printf("<=%d=>", cur->data);
		cur = cur->next;
	}
	printf("\n");
}

【笔记总结】

  1. 打印遍历链表时不能从头结点开始。
  2. 遍历结束条件是cur != phead,因为当cur遍历到尾结点,由于是循环链表,下一个结点就是哨兵位的头结点。

4.5 判断链表是否为空

//判断链表是否为空
bool DTEmpty(DTNode* phead)
{
	assert(phead);
	return phead->next == phead;
}

【笔记总结】

  1. 当链表只剩下一个哨兵位的头结点,说明链表为空。所以双向链表为空的情况是头结点的next指向本身。

4.6 尾删

//尾删
void DTPopBack(DTNode* phead)
{
	assert(phead);
	assert(!DTEmpty(phead));

	//1.找尾
	DTNode* tail = phead->prev;
	//2.记录尾结点的前一个结点
	DTNode* tailprev = tail->prev;
	//3.链接 phead  tailprev 
	tailprev->next = phead;
	phead->prev = tailprev;
	//4.释放尾结点
	free(tail);
}

【学习笔记】
尾删要特判原链表是否为空。空链表不能删!!

【动图展示】

在这里插入图片描述

4.7 头插

//头插
void DTPushFront(DTNode* phead, DTDataType x)
{
	assert(phead);
	//1.申请新结点
	DTNode* newnode = BuyListNode(x);
	//2.链接
	newnode->next = phead->next;
	phead->next->prev = newnode;
	phead->next = newnode;
	newnode->prev = phead;
}

【学习笔记】

  1. 此处注意链接顺序。不要先让head的next指向newnode,否则后面newnode的next想指向head的next就找不到了
    在这里插入图片描述
  2. 那有没有什办法可以不用注意链接顺序,当然有!提前记录head下一个结点就可以不用注意链接顺序啦
    在这里插入图片描述

4.8 头删

//头删
void DTPopFront(DTNode* phead)
{
	assert(phead);
	assert(!DTEmpty(phead));

	//1.记录哨兵位的下一个结点(即头结点)
	DTNode* del = phead->next;
	phead->next = del->next;
	del->next->prev = phead;
	//2.释放del
	free(del);
}

【动图展示】
在这里插入图片描述

4.9 在pos前插入x

//在pos之前插入x
void DTInsert(DTNode* pos, DTDataType x)
{
	assert(pos);
	//1.申请新结点
	DTNode* newnode = BuyListNode(x);
	//2.记录pos的前一个结点
	DTNode* posprev = pos->prev;
	//3.插入 posprev newnode pos
	posprev->next = newnode;
	newnode->prev = posprev;
	newnode->next = pos;
	pos->prev = newnode;
}

【动图展示】
在这里插入图片描述

4.10 删除pos结点

//删除pos结点
void DTErase(DTNode* pos)
{
	assert(pos);
	//1.记录pos前一个结点
	DTNode* posprev = pos->prev;
	//2.链接
	posprev->next = pos->next;
	pos->next->prev = posprev;
	//3.释放pos
	free(pos);
}

【动图展示】
在这里插入图片描述

4.11 查找某个节点

//查找
DTNode* DTFind(DTNode* phead, DTDataType x)
{
	assert(phead);
	//1.不能从哨兵位开始遍历
	DTNode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	//若循环结束,还没找到则返回NULL
	return NULL;
}

详细细节可参考打印

4.12 释放

//释放
void DTDestroy(DTNode* phead)
{
	DTNode* cur = phead->next;
	while (cur != phead)
	{
		//在释放前每次记录cur的下一个结点
		DTNode* next = cur->next;
		free(cur);
		cur = next;
	}
	//最后再单独释放phead
	free(phead);
}

五、总结

  1. 相比单链表,需要遍历链表找尾,但是带头双向循环链表可以直接找到尾节点,时间复杂度为O(1)。
  2. 但缺点是:不支持随机访问,缓存命中率相对低。
  • 13
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值