【数据结构】带头双向循环链表

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

上一篇已经介绍过链表的相关概念,并且对单链表进行了详细讲解,这一篇将继续对双链表进行详细讲解。
既然已经学习了单链表,那为什么还要学习带头双向循环链表呢?因为单链表是存在缺陷的,它是单向非循环链表,我们只能通过指针plist(单链表为非空链表时,plist指向第一个结点的位置,单链表为空链表时,plist的值为NULL)去查找它的结点。当单链表为非空链表时,我们只能通过指针plist直接找到它的第一个结点,而其它的结点都需要通过指针plist从前往后遍历查找。这将会在很大程度上限制我们的操作,非常不方便,而双向循环链表可以很好的解决这些问题。

双向链表结点结构体:
一个数据域,两个指针域,两个指针分别指向前一个结点和后一个结点。
代码"typedef int LTDataType"重命名是因为当结点所存数据类型发生改变时方便统一更换,只要在这里将int改成相应的数据类型就行(结点结构体里的变量data的数据类型就是LTDataType)

typedef int LTDataType; 
typedef struct ListNode
{
	LTDataType data;
	struct ListNode* prev;//指向前一个结点
	struct ListNode* next;//指向后一个结点
}LTNode;

带头双向循环链表的结构:

当它为空链表时,只有一个头结点,该头结点的两个两个指针都指向自己。
在这里插入图片描述
当它为非空链表时。
在这里插入图片描述
从带头双向循环链表的结构就可以看出来,它可以由给出的,一个结点找到其它任意的结点,而且可以由头节点直接找到尾结点,任何时候它的指针都不可能为NULL值,所以用起来特别方便。

二、函数的实现

1、动态申请一个结点


//申请一个节点
LTNode* BuyLTNode(LTDataType x)
{
	LTNode* node = (LTNode*)malloc(sizeof(LTNode));
	if (node == NULL) //检查动态申请是否失败
	{
		perror("malloc fail");
		exit(-1);
	}

	node->data = x;
	node->next = NULL;
	node->prev = NULL;
	return node;
}

2、初始化

为什么需要初始化,因为带头双向循环链表不管是不是空链表都会有一个头结点(要注意此处头节点是指哨兵位头结点,而不是第一个存数据的结点,不要搞混了),

//初始化
LTNode* LTInit()
{
	LTNode* phead=BuyLTNode(-1);
	phead->next = phead;
	phead->prev = phead;
	return phead;
}

3、尾插

对于带头双向循环链表可以通过phead快速找到尾结点,从而进行尾插操作,而且不管被尾插的链表是不是空链表,都不会改变phead的值。这样不需要对链表为空链表时进行特殊的考虑(这是带头结点对于尾插的优势)

在这里插入图片描述在这里插入图片描述

//尾插
void LTPushBack(LTNode* phead,LTDataType x)
{
	assert(phead); //判断指针是否为空

	LTNode* newnode = BuyLTNode(x);
	LTNode* tail = phead->prev;

	tail->next = newnode;
	newnode->prev = tail;
	phead->prev = newnode;
	newnode->next = phead;
	
}

4、 尾删

尾删需要注意不要将头结点删掉。对于带头双向循环链表可以快速通过尾结点找到前一个结点,这样有利于尾删,而且尾删时也不会改变phead的值。

//尾删
void LTPopBack(LTNode* phead)
{
	assert(phead);
	assert(phead->next !=phead);//防止头节点被删除

	LTNode* tail = phead->prev;
	LTNode* tailPrev = tail->prev;

	phead->prev = tailPrev;
	tailPrev->next = phead;
	free(tail);//将被删的结点释放掉
}

5、头插

不管链表为不为空都可以直接进行头插,也不会改变phead的值。

//头插
void LTPushFront(LTNode* phead,LTDataType x)
{
	assert(phead);
	LTNode* newnode=BuyLTNode(x);
	newnode->next= phead->next;
	phead->next->prev = newnode;
	phead->next= newnode;
	newnode->prev=phead;
}

6、头删

头删比较简单,但要注意不要将头结点删掉。

//头删
void LTPopFront(LTNode* phead)
{
	assert(phead);
	assert(phead->next != phead);//防止将头结点删掉

	LTNode* cur = phead->next;
	LTNode* next = cur->next;
	free(cur);
	phead->next = next;
	next->prev = phead;
}

7、打印

从第一个存储数据的结点开始依次往后,当前结点不是头结点时,对其进行打印操作。

//打印
void LTPrint(LTNode* phead)
{
	assert(phead);
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		printf("%d ", cur->data);
		cur = cur->next;
	}
	printf("\n");
}

8、查找

从第一个存储数据的结点开始依次往后,当前结点不是头结点时,对其进行查找操作,如果找到则返回该结点地址,如果最终没有找到则返回NULL。

//查找
LTNode* LTFind(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->data == x)
			return cur;
		cur = cur->next;
	}
	return NULL;
}

9、在pos位置之前插入

因为“带头双向循环链表”本身已经带头结点了,所以不管pos位置是第一个结点还是其它存储数据的结点,对其进行”在pos位置之前插入“操作都不会改变phead的值。

这里解释下为什么“带头双向循环链表”只有“在pos位置之前插入”而不像单链表还有“在pos位置之后插入”,因为单链表从pos位置找后一个结点可以通过pos->next直接找到,但找前一个结点却只能从前往后遍历查找,所以单链表“在pos位置之后插入”的操作可以很便利的插入数据,但“带头双向循环链表”不需要这样,因为它是循环链表,它通过pos位置查找前一个结点和查找后一个结点是同样方便的。

//在pos之前插入
void LTInsert(LTNode* pos,LTDataType x)
{
	assert(pos);//判断指针不为空
	
	LTNode* prev = pos->prev;
	LTNode* newnode = BuyLTNode(x);

	prev->next = newnode;
	newnode->prev = prev;
	newnode->next = pos;
	pos->prev = newnode;
}

10、删除pos位置

因为“带头双向循环链表”本身已经带头结点了,所以不管pos位置是第一个结点还是其它存储数据的结点,对其进行“删除pos位置”操作都不会改变phead的值。

//删除pos位置
void LTErase(LTNode* pos)
{
	assert(pos);//判断指针不为空

	LTNode* first = pos->prev;
	LTNode* second= pos->next;

	free(pos);
	first->next = second;
	second->prev = first;
}

11、判空

如果phead->next == phead,则该链表为空。

//判空
bool LTEmpty(LTNode* phead)
{
	assert(phead);
	return phead->next == phead;
}

12、求链表长度

//求链表长度
size_t LTSize(LTNode* phead)
{
	assert(phead);

	LTNode* cur = phead->next;
	size_t size = 0;
	while (cur != phead)
	{
		size++;
		cur = cur->next;
	}
	return size;
}

13、销毁

先将存储数据的第一个结点到最后一个结点释放掉,再将头结点释放掉。

这里还要注意,在调用完该函数之后,要将该链表的头指针置为空,因为该函数该从实参到形参是传值(头指针变量的值),而不是传址(头指针变量的地址),无法通过将形参phead置为NULL从而达到将实参也变为NULL的效果。

//销毁
void LTDestroy(LTNode* phead)
{
	assert(phead);
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		LTNode* next = cur->next;
		free(cur);
		cur = next;
	}
	free(phead);

}

三、 源代码

List.h

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>

typedef int LTDataType;
typedef struct ListNode
{
	LTDataType data;
	struct ListNode* prev;
	struct ListNode* next;
}LTNode;


LTNode* BuyLTNode(LTDataType x);

//初始化
LTNode* LTInit();

//尾插
void LTPushBack(LTNode* phead, LTDataType x);

//尾删
void LTPopBack(LTNode* phead);

//头插
void LTPushFront(LTNode* phead, LTDataType x);

//头删
void LTPopFront(LTNode* phead);

//打印
void LTPrint(LTNode* phead);

//查找
LTNode* LTFind(LTNode* phead, LTDataType x);

//在pos前插
void LTInsert(LTNode* pos, LTDataType x);

//判空
bool LTEmpty(LTNode* phead);

//求链长
size_t LTSize(LTNode* phead);

//销毁
void LTDestroy(LTNode* phead);

List.c

#include"list.h"

//申请一个节点
LTNode* BuyLTNode(LTDataType x)
{
	LTNode* node = (LTNode*)malloc(sizeof(LTNode));
	if (node == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}

	node->data = x;
	node->next = NULL;
	node->prev = NULL;
	return node;
}

//初始化
LTNode* LTInit()
{
	LTNode* phead=BuyLTNode(-1);
	phead->next = phead;
	phead->prev = phead;
	return phead;
}

//尾插
void LTPushBack(LTNode* phead,LTDataType x)
{
	assert(phead);

	LTNode* newnode = BuyLTNode(x);
	LTNode* tail = phead->prev;

	tail->next = newnode;
	newnode->prev = tail;
	phead->prev = newnode;
	newnode->next = phead;
	
}

//尾删
void LTPopBack(LTNode* phead)
{
	assert(phead);
	assert(phead->next !=phead);

	LTNode* tail = phead->prev;
	LTNode* tailPrev = tail->prev;

	phead->prev = tailPrev;
	tailPrev->next = phead;
	free(tail);
}

//头插
void LTPushFront(LTNode* phead,LTDataType x)
{
	assert(phead);
	LTNode* newnode=BuyLTNode(x);
	newnode->next= phead->next;
	phead->next->prev = newnode;
	phead->next= newnode;
	newnode->prev=phead;
}

//头删
void LTPopFront(LTNode* phead)
{
	assert(phead);
	assert(phead->next != phead);

	LTNode* cur = phead->next;
	LTNode* next = cur->next;
	free(cur);
	phead->next = next;
	next->prev = phead;
}

//打印
void LTPrint(LTNode* phead)
{
	assert(phead);
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		printf("%d ", cur->data);
		cur = cur->next;
	}
	printf("\n");
}

//查找
LTNode* LTFind(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->data == x)
			return cur;
		cur = cur->next;
	}
	return NULL;
}

//在pos之前插入
void LTInsert(LTNode* pos,LTDataType x)
{
	assert(pos);
	
	LTNode* prev = pos->prev;
	LTNode* newnode = BuyLTNode(x);

	prev->next = newnode;
	newnode->prev = prev;
	newnode->next = pos;
	pos->prev = newnode;
}

//删除pos位置
void LTErase(LTNode* pos)
{
	assert(pos);

	LTNode* first = pos->prev;
	LTNode* second= pos->next;

	free(pos);
	first->next = second;
	second->prev = first;
}

//判空
bool LTEmpty(LTNode* phead)
{
	assert(phead);
	return phead->next == phead;
}

//求链表长度
size_t LTSize(LTNode* phead)
{
	assert(phead);

	LTNode* cur = phead->next;
	size_t size = 0;
	while (cur != phead)
	{
		size++;
		cur = cur->next;
	}
	return size;
}

//销毁
void LTDestroy(LTNode* phead)
{
	assert(phead);
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		LTNode* next = cur->next;
		free(cur);
		cur = next;
	}
	free(phead);

}

Test.c

#include"list.h"

int main()
{
	LTNode* phead = LTInit();

	LTPushBack(phead, 100);
	LTPrint(phead);
	LTPopBack(phead);
	LTPrint(phead);

	LTPushFront(phead, 200);
	LTPushFront(phead, 300);
	LTPushFront(phead, 400);
	LTPrint(phead);

	LTNode* pos = LTFind(phead, 400);
	LTInsert(pos, 500);
	LTPrint(phead);

	pos = LTFind(phead, 500);
	LTErase(pos);
	LTPrint(phead);

	LTDestroy(phead);
	phead = NULL;

	return 0;
}

四、 总结

通过对单链表(不带头单向非循环链表)和带头双向循环链表的学习,我们可以发现带头双向循环链表优势更明显,虽然它看起来结构更复杂,但用起来却更方便。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值