数据结构与算法之美:双向循环链表

        Hello大家好!很高兴我们又见面啦!给生活添点passion,开始今天的编程之路!

91bfeb2bb1414a2ebf09cbc4f9706779.gif

我的博客:<但凡.

我的专栏:《编程之路》《数据结构与算法之美》《题海拾贝》

欢迎点赞,关注!

 今天我们用C语言实现一个带头双向循环链表。

 

目录

1、什么是带头双向循环链表

2、带头双向循环链表的实现

2.1初始化与开辟新节点

2.2头插与尾插

2.3头删和尾删

2.4任意节点位置插入

2.5对头插和尾插的改造

2.6打印链表

2.7销毁链表

2.8测试

3、双向循环链表的优势

4、三种传参方式的分析


 

1、什么是带头双向循环链表

        带头,就是有哨兵。这个哨兵记录这这个链表的第一个节点,但是不存放数据。

        双向就是我们每个节点既存放着下一个结点的地址,有存放着上一个节点的地址。

        而循环呢,就是我们的尾节点链接了头节点。

        现在我们用C语言实现一下这个链表,还是同样的工程体系,一个头文件两个源文件,然后实现增删查改。相信有了前两篇链表的铺垫,双向链表易如反掌。

2、带头双向循环链表的实现

2.1初始化与开辟新节点

初始化:

void Init(Dlist** head)
{
	*head = BuyNode(0);
	(*head)->next = *head;
	(*head)->pre = *head;//让head自己指向自己
}

        初始化的时候我们要让这个头节点的前后指针都指向自己:dffd844ccfb34e629e2bc30832bdd598.png

节点的开辟:

Dlist* BuyNode(ListDate a)
{
	Dlist* newnode = (Dlist*)malloc(sizeof(Dlist));
	if (newnode == NULL)
	{
		printf("开辟失败!");
		return NULL;
	}
	newnode->a = a;//初始化这个新建节点
	newnode->next = NULL;
	newnode->next = NULL;
	return newnode;
}

2.2头插与尾插

头插:

void Toucha(Dlist** head, ListDate a)
{
	assert(*head);//链表必须有头(存在)
	Dlist* newnode = BuyNode(a);
	Dlist* ph = *head;//定义个变量更清楚些
	Dlist* tail = ph->next;//记录尾部变量
	ph->next = newnode;
	newnode->next = tail;
	newnode->pre = ph;
	tail->pre = newnode;
}

尾插:

void WeiCha(Dlist** head, ListDate a)
{
	assert(*head);//链表必须有头(存在)
	Dlist* tail = (*head)->pre;//双向循环链表的优势
	Dlist* newnode = BuyNode(a);
	newnode->pre = tail;
	tail->next = newnode;
	newnode->next = *head;
	(*head)->pre = newnode;
}

        需要注意的是,我们要先判断head存在然后再插入。当然了我们也可以写一个if条件,当head不存在时初始化头节点。

        这里有个问题,就是只有assert里放*head才能达到断言的效果,不然头文件为空传进来就程序崩溃了,不会报错。为什么呢?

95c13b6e04714f0f876e52f8b9748ae6.png

        我们看图片来理解一下。倘若说我们传入的实参head是个NULL,那我们仍然会创建一个二级指针head指向这个NULL的实参,那也就是说,无论如何我们的形参head都不为空。而我们真正要检验是不是空的是我们传入的实参head。所以说assert断言的对象应该是*head。

2.3头删和尾删

头删:

void TouShan(Dlist** head)
{
	assert(*head && (*head)->next);//我们的链表要有头并且有存放元素的节点
	Dlist* ph = *head;//注意我们删的是第一个节点,不是哨兵
	Dlist* del = (*head)->next;
	ph->next = del->next;
	(del->next)->pre = ph;
	free(del);
	del = NULL;
}

尾删:

void WeiShan(Dlist** head)
{
	assert(*head && (*head)->next);
	Dlist* ph = *head;
	Dlist* tail = ph->pre;
	Dlist* newtail = tail->pre;
	newtail->next = ph;
	ph->pre = newtail;
	free(tail);
	tail = NULL;
}

        大家也看出来了,我很喜欢定义中间变量来进行删除操作。因为这样可读性更高,而且也不容易出现问题。

2.4任意节点位置插入

void Insert(Dlist** head, Dlist* pos, ListDate a)
{
	assert(*head && pos);
	Dlist* in = BuyNode(a);
	Dlist* poss = pos->next;
	pos->next = in;
	in->next = poss;
	poss->pre = in;
	in->pre = pos;
}

        其实在这里我们把*head传进来就可以了,然后断言的内容改成head&&pos。我们传入head的意义就是判断一下这个链表是否存在,并没有对head进行更改。所以说在保证head存在的情况下,如果去掉Dlist**head这个参数也是Ok的。

2.5对头插和尾插的改造

        在这儿有个技巧,就是在头插和尾插时调用我们的任意位置插入,Insert传入参数的时候把head传到pos的位置就是头插,把head->pre传入就是尾插。

改造后的头插:

void Toucha(Dlist** head, ListDate a)
{
	assert(head);//链表必须有头(存在)
	//Dlist* newnode = BuyNode(a);
	//Dlist* ph = head;//定义个变量更清楚些
	//Dlist* tail = ph->next;//记录尾部变量
	//ph->next = newnode;
	//newnode->next = tail;
	//newnode->pre = ph;
	//tail->pre = newnode;
	Insert(head, *head,a);
}

改造后的尾插:

void WeiCha(Dlist** head, ListDate a)
{
	assert(*head);//链表必须有头(存在)
	//Dlist* tail = (*head)->pre;//双向循环链表的优势
	//Dlist* newnode = BuyNode(a);
	//newnode->pre = tail;
	//tail->next = newnode;
	//newnode->next = *head;
	//(*head)->pre = newnode;
	Insert(head, (*head)->pre, a);
}

2.6打印链表

void print(Dlist* head)
{
	assert(head);
	Dlist* pcur = head->next;
	while (pcur!=head)
	{
		printf("%d-->", pcur->a);
		pcur = pcur->next;
	}
}

2.7销毁链表

void des(Dlist** head)
{
	assert(*head);
	Dlist* pcur = (*head)->next;
	Dlist* p = *head;
	while (pcur!= *head)
	{
		p = pcur;
		pcur = pcur->next;
		free(p);
		p = NULL;
	}
	free(*head);
	*head = NULL;
	/*free(head);
	head=NULL;*/
}

其实写这里的时候我犯了一个小错,给大家说下:

        比方说现在有两个节点 ,第一个节点链接者第二个节点。这时候我们把第二个节点释放了,我们再调用第一个节点->next,这时候访问的不是空指针,而是野指针!所以说我们在写链表时要规避这种问题。

        另外,在这里还我们要注意,free的时候我们应该释放一级指针。如果free二级指针会报错,因为我们的二级指针时调用函数时创建的参数,他是存放在栈区的,也就是不是动态开辟出来的空间。而我们的一级指针是存放在堆区的,free的对象只能是动态开辟出来的,存放在堆区的指针(地址)。如果有不太了解的可以移步我的专栏《编程之路》,里面的动态内存详解篇提到过这个问题。

2.8测试

#include"main.h"
int main()
{
	Dlist* head = NULL;
	Init(&head);
	Toucha(&head, 1);//1 
	WeiCha(&head, 2);//1  2
	WeiCha(&head, 2);//1  2 2
	WeiCha(&head, 2);//1  2 2 2
	WeiCha(&head, 2);//1  2 2 2 2
	WeiShan(&head);//1  2 2 2
	TouShan(&head);// 2 2 2
	Dlist*pos = (head->next)->next;
	Insert(head, pos,10);//2 2 10 2
	print(head);
	des(&head);
}

输出结果:

16af8ce9565842f3b1bca097cf1084f6.png

3、双向循环链表的优势

        第一,时间复杂度更优。我们可以发现,无论插入和删除,他的时间复杂度都是O(1),而前面的单链表和顺序表他都存在两项(插入或删除)时间复杂度为O(N)。

        第二、实现尾插和头插更简单。我们直接调个Insert就好了呀!

4、三种传参方式的分析

        前面我们介绍了两种传参方式,第一个是传地址,接受的形参为二级指针,第二个是c++里面的取别名,我们直接传值就行。

        在这里我想介绍第三种方法,我们就传值,并且我们不会c++我不会取别名,那我们就把函数返回值从void改成NODE*,最后返回我们更改完的头节点的地址。其中NODE是我们的节点类型名。当然了这样做的坏处是我们在使用是还得给一个地址来接受返回值。我在这里只是提一下思路就不实现了,感兴趣的同学自己尝试一下哈~

        在这里有个小技巧,如果你要通过函数改变一个东西的话,就得传地址,而传地址的化我们调用函数时一定得是&+变量,不然都是传值。

        好了,今天的内容就分享到这,我们下期再见!

 

 

评论 21
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值