1、带头双向循环链表介绍

        在上一篇博客我们提到了链表有三个特性可以合成为8种不同类型链表单链表是其中比较重要的一种,那么这次我们选择和带头双向循环链表会会面,这样我们就见识过了所有三种特性呈现

        带头双向循环链表,听起来仿佛是一个复杂结构,但是真正了解后就发现,这种稍微复杂一点的结构实际上为链表提供了完善的功能,使得我们操作链表时变得反而更简单。而这种链表因为自身结构复杂功能结构完善,所以经常成为一个独立数据存储结构用来单独存储数据

2、带头双向循环链表工程

对于一个带头双向循环链表工程,我们一般模式需要分为部分

        List.h头文件,其中包含库函数头文件的包含,顺序结构体的定义声明接口函数声明

        List.c 包含接口函数的定义。

        Test.c 是我们的测试源文件,从这里进入main函数

2.1 链表的定义

        为了实现链表的双向结构,我们需要从定义入手。单链表的链表结点定义是一个指向下一个结点的指针,而双向链表则需要两个指针,分别指向上一个和下一个结点。

typedef int LTDataType;

typedef struct ListNode
{
	LTDataType val;
	struct ListNode* prev;
	struct ListNode* next;
}LTNode;

2.2 链表的函数接口

        用带头双向循环链表管理数据需要一些常用的增删查改接口,但是因为有哨兵位的存在,所以在传参时候我们只需要传递一级指针即可

2.2.1 链表结点申请

        在链表插入初始化时候我们需要申请出一个结点,然后链接在链表之中。所以我们可以和单链表一样,把结点的申请封装成为一个函数

LTNode* CreateLTNode(LTDataType x)
{
	LTNode* tmp = (LTNode*)malloc(sizeof(LTNode));
	if (tmp == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	tmp->val = x;
	tmp->next = NULL;
	tmp->prev = NULL;
	return tmp;
}

2.2.2 链表的初始化

        因为我们现在要创建的链表是一个带头链表,所以需要对一个链表进行初始化,即创造出一个哨兵结点,剩余的链表结点均在哨兵位之后进行操作初始化无需参数最后返回一个链表的头结点即可

        对于哨兵结点而言,其值没有具体的意义,所以我们将其随便写作-1。因为我们的链表是双向循环链表,双向循环链表的尾结点的下一个结点指向头结点,头结点的上一个结点指向尾结点。因此,初始化的时候我们就要对哨兵结点正确赋值,保证即使空链表也满足要求的结构。因此,我们让哨兵结点的nextprev指向自身即可

LTNode* LTInit()
{
	LTNode* tmp = CreateLTNode(-1);
	tmp->next = tmp;
	tmp->prev = tmp;
	return tmp;
}

2.2.3 链表的结点插入

        带头双向循环链表的插入方式分为:头插,即在链表哨兵位之后插入结点;尾插,即在链表尾结点后插入一个结点;随机插入,即在指定结点前插入结点。

        对于带头双向循环链表的节点插入而言,由于双向循环的特性任何一个节点可以轻松的找到自己的前驱结点,所以使得插入不再需要遍历链表。又由于其带头的特性,也使得我们无需考虑链表是否为空的情况。唯一需要注意的就是结点链接顺序,避免出现修改了结点指针而找不到对应位置的结点,当然,如果给出一个临时变量存储对应的结点,顺序可以无需考虑

2.2.3.1 链表的头插

        带头双向循环链表的头插这里采用定义一个临时变量而不考虑链接顺序

void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);

	LTNode* tmp = CreateLTNode(x);
	tmp->next = phead->next;
	tmp->next->prev = tmp;
	phead->next = tmp;
	tmp->prev = phead;
}
2.2.3.2 链表的尾插

        带头双向循环链表的尾插我同样采用定义一个临时变量而不考虑链接顺序。 

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

	LTNode* tail = phead->prev;
	LTNode* tmp = CreateLTNode(x);
	tmp->next = phead;
	tmp->prev = tail;
	tail->next = tmp;
	phead->prev = tmp;
}
2.2.3.3 链表的随机插入

        带头双向循环链表的随机插入指在指定结点pos后插入一个结点。 

//在pos前插入
void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);
	
	LTNode* tmp = CreateLTNode(x);
	tmp->next = pos;
	tmp->prev = pos->prev;
	tmp->prev->next = tmp;
	pos->prev = tmp;
}

2.2.4 链表的结点删除

        带头双向循环链表的删除方式分为:头删,即删除在链表哨兵位之后的结点;尾删,即删除链表尾结点;随机插入,即删除指定结点。

        同样的,由于双向循环的特性使得不再需要遍历链表寻找前驱结点,所以删除时候只需要将待删除结点的前后结点链接起来,然后释放掉待删除结点。

2.2.4.1 链表的头删
void LTPopBack(LTNode* phead)
{
	assert(phead);
	assert(phead->next != phead);

	LTNode* tail = phead->prev;
	phead->prev = tail->prev;
	tail->prev->next = phead;
	free(tail);
	tail = NULL;
}
2.2.4.2 链表的尾删
void LTPopFront(LTNode* phead)
{
	assert(phead);
	assert(phead->next != phead);

	LTNode* first = phead->next;
	phead->next = first->next;
	first->prev = phead;
	free(first);
	first = NULL;
}
2.2.4.3 链表的随机删除
//删除pos位置节点
void LTErase(LTNode* pos)
{
	assert(pos);

	LTNode* prepos = pos->prev;
	prepos->next = pos->next;
	pos->next->prev = prepos;
	free(pos);
	pos = NULL;
}

2.2.5 链表的查找

        用于查找指定值的结点并返回,和单链表一样,只是遍历结束条件遍历到了哨兵位。

LTNode* LTFind(LTNode* phead, LTDataType x)
{
	assert(phead);

	LTNode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->val == x)
		{
			return cur;
		}
		else
		{
			cur = cur->next;
		}
	}
	return NULL;
}

2.2.6 链表的打印

        很简单的接口遍历方法可以参照链表的查找

void LTPrint(LTNode* phead)
{
	assert(phead);

	LTNode* cur = phead->next;
	while (cur != phead)
	{
		printf("%d<=>", cur->val);
		cur = cur->next;
	}
	printf("NULLn");
}

2.2.7 链表的销毁

        销毁链表需要遍历链表,释放每个节点最后释放哨兵位即可。注意,这里的销毁只是释放了所有结点,因为是一级指针传参,所以说明函数中链表的指针在销毁后成为了野指针,需要函数调用者自行置空。

void LTDestroy(LTNode* phead)
{
	assert(phead);

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

3、链表反思

        在完成了单链表和带头双向循环链表后,需要深刻理解不同特性对于我们写代码哪些限制遍历,从而在合适的场景合理地做出选择

        总结一下同为线性表顺序表与链表之间的优劣与区别

        对于顺序表,先说说优势。顺序表最大特征就是物理储存空间连续,这就代表可以支持随机访问,无论是哪个位置的数据,都可以以O(1)的代价获取。储存空间连续还使得操作系统访问数据时是非常高效的,操作系统读取一次连续的空间对其数据覆盖率很高,缓存利用率高。顺序表同样也有劣势,其插入数据可能需要搬动数据,使得插入效率低,同时空间需要扩容,就面临着空间浪费,使得空间存在一定程度的浪费

        对于链表而言,其优势是空间分配非常灵活,不存在浪费的情况,多一个数据就开辟一个结点,删除数据就即刻释放空间。插入和删除不存在挪动数据的情况,只需要链接指针。但是由于其储存空间不连续,所以链表也有一定劣势。由于寻找第k个结点只能遍历,链表访问数据的代价为O(N),对比线性表很大。除此之外,不连续的物理空间储存使得操作系统缓存时对链表数据覆盖率不高,缓存利用率较低。

        由此看来,顺序表和链表二者互为补充,所以我们在选择方面要有所区别。对于需要大量访问的数据而言,顺序表效率明显更高;而对于需要频繁插入删除的数据,链表由于灵动的空间布局而略胜一筹。对二者合理谋划发挥最佳效果便是每个学习者需要不断探索的“内功”了。

原文地址:https://blog.csdn.net/XLZ_44847/article/details/134629613

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任

如若转载,请注明出处:http://www.7code.cn/show_34088.html

如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱suwngjj01@126.com进行投诉反馈,一经查实,立即删除!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注