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

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

我的博客: <但凡.

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

欢迎点赞,关注!

今天我们用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初始化与开辟新节点

初始化:

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

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

节点的开辟:

cpp 复制代码
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头插与尾插

头插:

cpp 复制代码
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;
}

尾插:

cpp 复制代码
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才能达到断言的效果,不然头文件为空传进来就程序崩溃了,不会报错。为什么呢?

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

2.3头删和尾删

头删:

cpp 复制代码
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;
}

尾删:

cpp 复制代码
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任意节点位置插入

cpp 复制代码
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传入就是尾插。

改造后的头插:

cpp 复制代码
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);
}

改造后的尾插:

cpp 复制代码
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打印链表

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

2.7销毁链表

cpp 复制代码
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测试

cpp 复制代码
#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);
}

输出结果:

3、双向循环链表的优势

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

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

4、三种传参方式的分析

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

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

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

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

相关推荐
序属秋秋秋23 分钟前
算法基础_基础算法【位运算 + 离散化 + 区间合并】
c语言·c++·学习·算法·蓝桥杯
念_ovo42 分钟前
【算法/c++】利用中序遍历和后序遍历建二叉树
数据结构·c++·算法
luckyme_1 小时前
leetcode-代码随想录-链表-移除链表元素
算法·leetcode·链表
_安晓1 小时前
数据结构 -- 图的存储
数据结构·算法
.YY001.2 小时前
数据结构第一轮复习--第六章图包含代码
数据结构·算法
星星火柴9363 小时前
数据结构:链表 (C++实现)
数据结构·c++·笔记·链表
在努力的韩小豪3 小时前
B树和B+树的区别(B Tree & B+ Tree)
数据结构·数据库·b树·b+树·索引·数据库索引
ん贤3 小时前
2024第十五届蓝桥杯大赛软件赛省赛C/C++ 大学 B 组
c语言·数据结构·c++·经验分享·笔记·算法·蓝桥杯
口嗨农民工4 小时前
mksquashfs文件系统的使用
c语言
Phoebe鑫4 小时前
数据结构每日一题day11(链表)★★★★★
数据结构·算法