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

一.前言

再次之前我已经讲过了单链表的使用了。但是我们会发现,单链表还存在一些问题,比如尾插尾删的时候比较麻烦,或者单链表很多函数还要传二级指针…这些情况在双向循环带头链表这里都不是问题。

二.双向循环带头链表结构

在这里插入图片描述这个链表的结构虽然是所有链表中最复杂的那一种,但是我肯定它用起来绝对是最简单的。过会我们实现起来的时候,你们就能发现它能给我们带来很多便利。

因为功能强大,实际使用的过程中双向循环带头链表使用的是很多的。但这不代表单链表就没人用了,实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。

三.双向循环带头链表的实现

3.1双向循环带头链表节点的结构体

typedef int LTDataType;

//定义结构体
typedef struct LTNode
{
	LTDataType data;
	struct LTNode* next;
	struct LTNode* prev;
}LTNode;

3.2创建一个节点

//创建一个节点
LTNode* BuyListNode(LTDataType x)
{
	//用malloc函数创建一个节点
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	if (newnode == NULL)
	{
		perror("BuyListNode");
		exit(-1);
	}

	//将节点内容赋值
	newnode->data = x;
	newnode->next = NULL;
	newnode->prev = NULL;

	return newnode;
}

3.3初始化链表

//初始化链表
LTNode* LTInit()
{
	//头节点里的数据不能存长度
	LTNode* phead = BuyListNode(-1);
	phead->next = phead;
	phead->prev = phead;

	return phead;
}

我们这个链表是带头并且是双向的,带头的意思是带有一个哨兵位。刚开始时链表长度为0,这里初始化的内容是把链表的头创建好:
在这里插入图片描述因为是循环链表,所以这个链表的最后一个的下一个应该指向第一个,第一个的前面应该指向最后一个。但此时只有一个节点所以这个节点的下一个和上一个都指向自己。

在这里我把哨兵位里面的内容放成-1,这里我是随便放的,你们可以放其他的,但是不要在里面放成链表的个数,有的书里可能会将链表哨兵位的数据放成链表的个数,虽然这里肯上去是挺好的,而且也可以实现出来,无非就是在定义一个变量再添加节点时加一个,删除节点时减一个。但是这样会存在一些缺陷:
首先链表里的数据不可能任何时候都是int类型,虽然现在我定义好的类型是int类型,但是我改成char类型呢?
在这里插入图片描述
这样就会导致结构体里data的类型也是char类型的,现在问题就来了char类型的取值范围是-128~127.如果你的链表长度是128呢?这样不就越界了吗?这样的出来的结果还是你想要的吗?

如果真的想要计算链表长度,还是写成一个函数比较好,这样写容易出错。

3.4打印链表

//打印链表
void LTPrint(LTNode* phead)
{
	//	判断链表是否为空
	if (phead->next == phead)
	{
		printf("NULL");
		exit(-1);
	}

	LTNode* cur = phead->next;

	//判断cur指针是否走到底
	while (cur != phead)
	{
		printf("%d ", cur->data);
		cur = cur->next;
	}
	printf("\n");
}

首先判断链表是否为空,如果为空就打印NULL。通过图可以看到phead,phead->next,phead->prev其实指向的是一块地方:在这里插入图片描述所以我们随便判断其中两个是否相等就行,相等就代表是空链表,此时只有一个光杆司令在这里杵着,后面没其他的节点。

可能在单链表的时候我们判断循环结束的条件是cur是否为空,但是现在是双向循环链表,链表只要存在节点cur就不可能为空,所以此时我们换一种写法,先看图:在这里插入图片描述本来cur走到这里的时候,只要把此时指向的节点打印出来然后再往后走一步就该结束了,但是注意它的下一步其实指向的是phead,也就是说在cur == phead的时候就该停下来跳出循环了,所以循环的判断条件应该是:

while (cur != phead)

3.5链表的头插

//链表的头插
void LTPushFront(LTNode* phead, LTDataType x)
{
	//需要插入的节点
	LTNode* newnode = BuyListNode(x);
	LTNode* cur = phead->next;
	
	newnode->next = cur;
	newnode->prev = phead;
	phead->next = newnode;
	cur->prev = newnode;
}

直接看图:在这里插入图片描述如果刚看我刚给的代码,可能不是很好理解,但通过图来看,应该就能看懂了吧。

newnode->next = cur;

在这里插入图片描述

newnode->prev = phead;

在这里插入图片描述

phead->next = newnode;

在这里插入图片描述

cur->prev = newnode;

在这里插入图片描述
这样就把新节点完美的插进去了。

3.6链表的头删

//链表头删
void LTPopFront(LTNode* phead)
{
	assert(phead);
	//如果phead == phead->next说明这个链表是空链表
	//空链表就不要再删了
	assert(phead != phead->next);

	LTNode* next = phead->next;
	
	phead->next = next->next;
	next->next->prev = phead;
}

在这里插入图片描述头删非常简单,两步就能搞定:
在这里插入图片描述
在这里插入图片描述
将这两个节点链接起来之后,把next指向的节点free掉就行

3.7链表的尾插

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

	LTNode* newnode = BuyListNode(x);
	phead->prev->next = newnode;
	newnode->prev = phead->prev;
	phead->prev = newnode;
	newnode->next = phead;
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
可以先将新的节点与之前最后一个节点连起来,接下来就是将此时最后一个节点与头节点相连:
在这里插入图片描述
在这里插入图片描述
这样就完成了尾插的操作。因为循环的关系,最后一个节点就不用麻烦的去遍历了,直接找头节点的上一个节点就行。将本来复杂的尾删变得十分的简洁。

3.8链表的尾删

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

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

	tailprev->next = phead;
	phead->prev = tailprev;

	free(tail);
}

在这里插入图片描述把最后一个节点操作,只需将头节点与倒数第二个节点连起来就行,把最后一个节点切开就能完成尾删的操作。
在这里插入图片描述在这里插入图片描述连好之后就可以把tail指向的节点释放掉了,当然这一步在链接之前完成也可以但前提是先将tail->prev的地址保存下来。

3.9链表的查找

//链表的查找
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;
}

因为phead是哨兵位的节点,我们是不在这里保存数据的,所以定义cur指针遍历的时候应从头节点的下一个节点开始遍历。
如果cur == phead说明已经走到末节点又回来了,如果这期间一直没有找到需要找的x,说明找不到就返回NULL.

3.10在pos位之前插入x

//在pos位之前插入x
void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);

	LTNode* newnode = BuyListNode(x);
	LTNode* prev = pos->prev;

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

在这里插入图片描述在这里插入图片描述在这里插入图片描述因为pos,prev,newnode三个指针我们都已经知道了,所以这四段代码的顺序也不用这么在意,只要能连起来就可以。

3.11删除pos位位置

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

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

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

在这里插入图片描述删除也很简单把prev,next指向的两个节点相连,然后把pos指向的节点释放掉。
在这里插入图片描述

3.11判断链表是否为空

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

bool类型有两种true和false,如果整型表达式为真,这个函数就返回true,假就返回false
在这里插入图片描述

我们这个函数是判断是否为空链表如果phead == phead->next说明此时链表只有一个头节点,也就是链表内容为空返回true.反之就返回false.

3.12计算链表长度

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

	size_t count = 0;
	LTNode* cur = phead->next;

	while (cur != phead)
	{
		count++;
		cur = cur->next;
	}
	return count;
}

和查找的步骤差不多,从头节点下一个节点开始一直到末尾,每走一步,新定义的变量就++。最后结果返回。

3.13删除链表

//删除链表
void LTDostroy(LTNode* phead)
{
	assert(phead);

	LTNode* cur = phead->next;

	while (cur != phead)
	{
		LTNode* next = cur->next;
		free(cur);
		cur = next;
	}
	free(phead);
}

在这里插入图片描述每次在删除cur指向的节点时,都应该把此时节点的下一个节点的位置保存下来,否则删除之后就找不到下一个节点的位置了。
在循环里,cur指针一直往后走,直到删除掉最后一个节点时结束,跳出循环后,要记得把头节点的空间也释放掉。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值