【线性表】带头双向循环链表详解(C语言版)

目录

引言

一、定义

二、开辟新结点

三、初始化

 四、插入

1.头插

2.尾插

3.中间插入

五、删除

1.头删

2.尾删

3.中间删除

六、查找

七、修改

八、计算链表长度

结尾


ID:HL_5461 

引言

咱先看看带头双向循环链表大概是啥样滴~

 与之前的单链表不同,带头双向循环链表(以下简称链表)有三个特点:1.有一个哨兵位的头结点,也就说无论链表有没有存储数据,它都有一个不变的头结点;2.有两个指针,一个和单链表一样是指向下一个的next指针,一个是指向前一个的prev指针;3.尾结点的下一个是头结点,它逻辑上是一个环形,即尾结点的next指向哨兵位的头结点,头结点的prev指向尾结点。

OK,咱开始进正题!


一、定义

我们将链表结点的结构体分为三部分,一个指向前结点的prev指针,一个指向后结点的next指针,一个存放数据的data。同样的,方便存放数据的类型修改,我们将数据类型typedef成LTDataType。

下面是结点结构体的图示:

图示

 下面是代码:

typedef int LTDataType;//定义LTDataType为int,方便以后修改存储的数据类型

typedef struct ListNode
{
	LTDataType data;//结点数据
	struct ListNode* next;//指向下一个结点的指针
	struct ListNode* prev;//指向上一个结点的指针

}LTNode;

二、开辟新结点

这里和单链表差不多,我们直接写一个开辟新结点的函数,但由于该链表还有哨兵位的头结点,所以我们还需要一个初始化的函数,这个暂且按下不表,先看开辟结点。

首先我们直接使用malloc函数为结点开辟一个结点结构体大小的空间,同时返回该结点的指针,将之赋值给newnode。当然,对于malloc函数开辟空间,很重要一点就是判断开辟是否失败,即指针是否成功,失败结束程序,成功我们就可以将要存储的数据存进新开辟的结点空间中的data里,同时为了避免使用野指针,将该结点的prev置为空,next置为空。

来看代码:

LTNode* BuyLTNode(LTDataType x)
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));//为结点开辟空间
	if (!newnode)//开辟失败
	{
		perror("BuyLTNode");//打印失败原因
		exit(-1);//以错误形式退出程序
	}

	newnode->data = x;//令data为x
	newnode->next = NULL;//next指针为空
	newnode->prev = NULL;//prev指针为空

	return newnode;//返回新结点地址
}

三、初始化

对于初始化函数,我们在函数中开辟一个新结点作为头结点,至于该结点的data,我们将其置成0,算是养成好习惯吧。当然你也可以把头结点的data拿来记录链表长度,每新增一个结点++一下,每删除一个结点--一下。不过我不建议你这样做,因为我们存储的数据不一定是int类型的,也有可能是char之类的,此时如果我们还把头结点的data作为链表长度输出……这到了公司会被辞退的吧……所以考虑到代码的灵活性,这篇文章里的头结点data就做个摆设就好。

啊,跑题了,咱继续!

此时头结点有了,但这还不够,我们还有对头结点的prev指针和next指针初始化,使它们都指向头结点自己。初始化后的链表大概长这个样子:

至此,我们的链表已经有了一个雏形,之后的增删查改都是小case啦~不过在开始增删查改之前我们还是看看初始化这部分的代码:

LTNode* LTInit()
{
	LTNode* phead = BuyLTNode(0);//为头结点开辟结点

	phead->next = phead;//头结点的next指针指向头结点自己
	phead->prev = phead;//头结点的prev指针指向头结点自己

	return phead;//返回头结点地址
}

 四、插入

老规矩,咱还是分成头插、尾插、中间插入。不过由于链表是带头的,尾插就不需要考虑是否为头插的问题了,而且由于是循环的,所以尾插我们也需要修改尾结点的next和头结点的prev,这使得尾插和中间插入没了太大区别。具体,各位可以在后续讲解中细细体会。

1.头插

先来看头插,很简单,直接上图,清晰明了。

先用 BuyLTNode函数为新结点开辟空间。

 用first记录第一个有效结点的地址。当然也可以参考单链表的插入操作,先令新结点的next指向phead->next,即这里的first结点,然后再令phead->next->prev = newnode,之后再修改phead的next和newnode的prev,这样我们就不需要first指针了。但由于双向链表的指针有两个,比较多,为避免弄错,不止这里,包括后面的操作,我们都采用用指针记录要修改结点的位置的方法。这样一来,我们就无需考虑指针的修改顺序了。

修改指针。(请勿在意这跟双螺旋似的线条,我尽力了……)

看代码:

void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);//头指针不为空

	LTNode* newnode = BuyLTNode(x);//开辟新结点

	LTNode* first = phead->next;//用first指针记录下一个结点

	newnode->next = first;//新结点的后一个指向first结点
	newnode->prev = phead;//新结点的前一个指向头结点
	phead->next = newnode;//头结点的后一个指向新结点
	first->prev = newnode;//first结点的前一个指向新结点
}

2.尾插

和头插一样,我们同样需要一个tail指针记录尾结点,方便我们后续修改指针,这样就不用考虑修改的顺序了。

修改指针。(我知道这图好丑,别吐槽了 T^T )

看代码叭~

void LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);//头指针不为空

	LTNode* newnode = BuyLTNode(x);//开辟新结点

	LTNode* tail = phead->prev;//用tail指针记录尾结点

	newnode->prev = tail;//新结点的前一个指向tail结点
	newnode->next = phead;//新结点的后一个指向头结点
	phead->prev = newnode;//头结点的前一个指向新结点
	tail->next = newnode;//tail结点的后一个指向新结点
}

3.中间插入

就像之前强调的,加头循环消除了第一个结点和尾结点的特殊性,所以其实无论方法上还是本质上它和头插尾插没啥区别(其实准确说这三种插入都无甚区别)。

这里的插入,我们指pos前插入。

开辟新结点。prev指针用于记录pos前一个结点。 

修改指针。 

void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);//pos指针不为空

	LTNode* newnode = BuyLTNode(x);//开辟新结点

	LTNode* prev = pos->prev;//用prev指针记录前一个结点

	newnode->next = pos;//新结点的后一个指向next结点
	newnode->prev = prev;//新结点的前一个指向prevl结点
	prev->next = newnode;//prev结点的后一个指向新结点
	pos->prev = newnode;//next结点的前一个指向新结点
}

五、删除

1.头删

用first指针记录要删除的第一个结点,用second记录第一个结点的后一个结点。

释放第一个结点。

修改指针。 

void LTPopFront(LTNode* phead)
{
	assert(phead);//头指针不为空
	assert(phead->next != phead);//链表不为空

	LTNode* first = phead->next;//用first指针记录要删除结点
	LTNode* second = phead->next->next;//用second指针记录要删结点的后一个结点

	free(first);//释放要删除结点

	phead->next = second;//头结点的后一个指向second结点
	second->prev = phead;//second结点的前一个指向头结点
}

2.尾删

tail指针记录尾结点,tailprev记录尾结点的前一个结点。

释放尾结点。

修改指针。

void LTErase(LTNode* pos)
{
	assert(pos);//pos指针不为空

	LTNode* prev = pos->prev;//用prev指针记录前一个结点
	LTNode* next = pos->prev;//用next指针记录下一个结点

	free(pos);//释放pos结点

	prev->next = next;//prev指针的后一个指向next
	next->prev = prev;//next指针的前一个指向prev
}

3.中间删除

我们需要两个指针,分别指向pos前一个和pos后一个。释放掉pos结点,然后将prev和next结点链接即可。

 创建prev指针指向pos前一个结点,next指针指向pos后一个结点。

释放pos结点。

 

修改指针。 

void LTErase(LTNode* pos)
{
	assert(pos);//pos指针不为空

	LTNode* prev = pos->prev;//用prev指针记录前一个结点
	LTNode* next = pos->prev;//用next指针记录下一个结点

	free(pos);//释放pos结点

	prev->next = next;//prev指针的后一个指向next
	next->prev = prev;//next指针的前一个指向prev
}

六、查找

查找还是采用使用指针cur遍历链表,然后比较data的方法,这与单链表没啥区别,唯一的区别在于结束遍历的条件不是cur == NULL,而是cur == phead,由此,cur初始值应为phead->next,而不是phead。

so easy~麻麻再也不用担心我的查找~(bushi)不过多赘述,直接看代码。

LTNode* LTFind(LTNode* phead, LTDataType x)
{
	assert(phead);//头指针不为空

	LTNode* cur = phead->next;//用cur指针遍历链表

	while (cur != phead)//当没有循环回头指针
	{
		if (cur->data == x)//如果找到了
		{
			return cur;//返回此时地址
		}
		cur = cur->next;//否则cur指针后移
	}

	return NULL;//没找到返回空指针
}

七、修改

应该没啥操作会比修改更简单了吧~咱还是直接上代码。

void LTModify(LTNode* pos, LTDataType x)
{
	assert(pos);//pos指针不为空

	pos->data = x;//将pos位置的data改为x
}

八、计算链表长度

咱比平常再加一个操作哈~

思想很简单,跟查找一样遍历链表,每经过一个结点size++就行~

int LTSize(LTNode* phead)
{
	assert(phead);//头指针不为空

	int size = 0;//用size记录链表长度
	LTNode* cur = phead->next;//用cur指针遍历链表

	while (cur != phead)//当没有循环回头指针
	{
		size++;//长度加1
		cur = cur->next;//cur指针后移
	}
	return size;//返回链表长度
}

结尾

带头双向循环链表的操作详解到此就结束了,这是本篇代码码云指路:

class_c: 课上要认真鸭~ - Gitee.com

若有错误,欢迎大家批评斧正!

  • 17
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 14
    评论
评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

是兰兰呀~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值