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

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


前景回顾

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

目录

  • 前景回顾
  • 一、结构介绍
  • 二、准备工作
  • 三、接口
  • 四、代码实现
      • 4.1 开辟新结点
      • 4.2 初始化哨兵位头结点
      • 4.3 尾插
      • 4.4 打印
      • 4.5 判断链表是否为空
      • 4.6 尾删
      • 4.7 头插
      • 4.8 头删
      • 4.9 在pos前插入x
      • 4.10 删除pos结点
      • 4.11 查找某个节点
      • 4.12 释放
  • 五、总结

一、结构介绍

【数据结构】双向带头循环链表(笔记总结)_第1张图片

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

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

typedef int DLDataType;

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

二、准备工作

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

test.c - 测试代码逻辑 (源文件)
DList.c - 动态的实现 (源文件)
DList.h - 存放函数的声明 (头文件)
【数据结构】双向带头循环链表(笔记总结)_第2张图片

三、接口

【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就是尾

【动画展示】

【数据结构】双向带头循环链表(笔记总结)_第3张图片

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张图片

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就找不到了
    【数据结构】双向带头循环链表(笔记总结)_第5张图片
  2. 那有没有什办法可以不用注意链接顺序,当然有!提前记录head下一个结点就可以不用注意链接顺序啦
    【数据结构】双向带头循环链表(笔记总结)_第6张图片

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);
}

【动图展示】
【数据结构】双向带头循环链表(笔记总结)_第7张图片

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;
}

【动图展示】
【数据结构】双向带头循环链表(笔记总结)_第8张图片

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);
}

【动图展示】
【数据结构】双向带头循环链表(笔记总结)_第9张图片

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. 但缺点是:不支持随机访问,缓存命中率相对低。

你可能感兴趣的:(数据结构,链表,数据结构,c++,学习,算法)