【数据结构初阶】链表(下)——带头双向循环链表的实现

目录

带头双向循环链表的实现

        1.带头双向循环链表的节点类型

        2.创建带头双向循环链表的节点

        3.向带头双向循环链表中插入数据

                <3.1>从链表尾部插入数据

                <3.2>从链表头部插入数据

                <3.3>从链表某个节点前插入数据

        4.从带头双向循环链表中删除数据

                <4.1>从链表的尾部删除数据

                <4.2>从链表的头部删除数据

                <4.3>删除链表某个节点

        5.在带头双向循环链表中查找数据

        6.遍历带头双向循环链表

        7.销毁带头双向循环链表

带头双向循环链表实现总代码

测试

        1.测试代码

        2.测试效果

 链表总结

        无头单向非循环链表和带头双向循环链表的对比

        链表和顺序表的对比


本期博客我们来对最复杂的带头双向循环链表结构进行实现:

我们可以看到该种链表比单链表多了一个头节点(head(该节点一般不存放数据))和一个指向前一个节点的指针,该种类型的链表有着比单链表更多的优势,更适合来存储数据。

带头双向循环链表的实现

        1.带头双向循环链表的节点类型

在实现该链表之前我们先要确定节点类型:

typedef int LTDataType;//数据类型
typedef struct ListNode//节点类型
{
	LTDataType _data;
	struct ListNode* _next;//指向后一个节点的指针
	struct ListNode* _prev;//指向前一个节点的指针
}ListNode;

:这里的对int类型进行重定义是为了我们更好的看懂Data只是一种数据而不仅仅是int类型。所以我们在使用链表时存储的数据并不限制于int类型,这里仅仅是举例。

        2.创建带头双向循环链表的节点

ListNode* ListCreate()
{
	ListNode* NewNode = (ListNode*)malloc(sizeof(ListNode));//向堆区申请一个节点大小的空间
	if (NewNode == NULL)//判断申请是否成功
	{
		perror("malloc");
		exit(-1);
	}
	NewNode->_next = NewNode;
	NewNode->_prev = NewNode;//将_next和_prev指针设为NewNode可以方便直接创建一个头节点
	return NewNode;//返回的指针的_next和_prev可以后期根据需要修改
}

        3.向带头双向循环链表中插入数据

                <3.1>从链表尾部插入数据

void ListPushBack(ListNode* pHead, LTDataType x)
{
	assert(pHead);//传入的头节点不能为空
	ListNode* temp = pHead;
	ListNode* newnode = ListCreate();//申请一个新节点
	newnode->_data = x;//存入数据
	//下面是将新节点插入到链表的末尾的过程:
	newnode->_prev = temp->_prev;//将新节点_prev指针指向链表中最后一个节点
	newnode->_next = pHead;//将新节点_next指针指向链表中的头节点
	temp->_prev->_next = newnode;//将链表中最后一个节点的_next指向新节点
	temp->_prev = newnode;//将头节点的_prev指针指向新节点
}

                <3.2>从链表头部插入数据

void ListPushFront(ListNode* pHead, LTDataType x)
{
	assert(pHead);//传入的头节点不能为空
	ListNode* temp = pHead;
	ListNode* newnode = ListCreate();//申请一个新节点
	newnode->_data = x;//存入数据
	//下面是将新节点插入到链表的头部的过程:
	newnode->_prev = temp;//将新节点_prev指针指向链表中头节点
	newnode->_next = temp->_next;//将新节点_next指针指向链表中的第一个数据节点
	temp->_next->_prev = newnode;//将链表中第一个数据节点的_perv指向新节点
	temp->_next = newnode;//将头节点的_next指针指向新节点
}

                <3.3>从链表某个节点前插入数据

void ListInsert(ListNode* pos, LTDataType x)//pos传入将要插入其前的节点
{
	assert(pos);//传入的节点不能为空
	ListNode* temp = pos;
	ListNode* newnode = ListCreate();//申请一个新节点
	newnode->_data = x;//存入数据
	//下面是将新节点插入到链表pos节点前的过程:
	newnode->_prev = temp->_prev;//将新节点_prev指针指向pos节点的前一个节点
	newnode->_next = temp;//将新节点_next指针指向pos节点
	temp->_prev->_next = newnode;//将pos前一个节点的_next指针指向新节点
	temp->_prev = newnode;//将pos节点的_prev指针指向新节点
}

        4.从带头双向循环链表中删除数据

                <4.1>从链表的尾部删除数据

void ListPopBack(ListNode* pHead)
{
	assert(pHead);//传入的头节点不能为空
	assert(pHead->_prev);//链表不能为空
	ListNode* Del = pHead->_prev;
	//下面是将最后一个数据节点删除过程:
	Del->_prev->_next = Del->_next;//将最后一个数据节点的上一个节点的_next指针指向头节点
	Del->_next->_prev = Del->_prev;//将头节点的_next指针指向最后一个数据节点的上一个节点
	free(Del);//释放最后一个数据节点的空间
	Del = NULL;//防止野指针的产生
}

                <4.2>从链表的头部删除数据

void ListPopFront(ListNode* pHead)
{
	assert(pHead);//传入的头节点不能为空
	assert(pHead->_next);//链表不能为空
	ListNode* Del = pHead->_next;
	//下面是将第一个数据节点删除过程:
	Del->_next->_prev = Del->_prev;//将第一个数据节点的下一个节点的_prev指针指向头节点
	Del->_prev->_next = Del->_next;//将头节点的_next指针指向第一个数据节点的下一个节点
	free(Del);//释放第一个数据节点的空间
	Del = NULL;//防止野指针的产生
}

                <4.3>删除链表某个节点

void ListErase(ListNode* pos)//pos传入要删除的节点
{
	assert(pos);//传入的节点不能为空
	ListNode* Del = pos;
	//下面是将pos数据节点删除过程:
	Del->_prev->_next = Del->_next;//将pos数据节点的前一个节点的_next指针指向pos数据节点的下一个节点
	Del->_next->_prev = Del->_prev;//将pos数据节点的下一个节点的_prev指针指向pos数据节点的上一个节点
	free(Del);//释放pos数据节点的空间
	Del = NULL;//防止野指针的产生
}

        5.在带头双向循环链表中查找数据

ListNode* ListFind(ListNode* pHead, LTDataType x)
{
	assert(pHead);//传入的头节点不能为空
	ListNode* find = pHead->_next;//跳过头节点开始查找
	while (find != pHead)
	{
		if (find->_data == x)
			return find;//找到返回该节点
		find = find->_next;
	}
	return NULL;//没找到返回空指针
}

        6.遍历带头双向循环链表

void ListPrint(ListNode* pHead)
{
	assert(pHead);
	printf("Head->");
	ListNode* print = pHead->_next;//跳过头节点开始打印
	while (print != pHead)
	{
		printf("%d->", print->_data);
		print = print->_next;
	}
	printf("Head\n");
}

        7.销毁带头双向循环链表

void ListDestory(ListNode* pHead)
{
	assert(pHead);
	ListNode* Del = pHead->_next;//跳过头节点开始删除节点
	ListNode* temp = Del;
	while (Del != pHead)
	{
		temp = temp->_next;
		free(Del);
		Del = temp;
	}
	pHead->_next = pHead;
	pHead->_prev = pHead;//将链表置空
}

带头双向循环链表实现总代码

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
// 带头+双向+循环链表增删查改实现
typedef int LTDataType;
typedef struct ListNode
{
	LTDataType _data;
	struct ListNode* _next;
	struct ListNode* _prev;
}ListNode;

ListNode* ListCreate()
{
	ListNode* NewNode = (ListNode*)malloc(sizeof(ListNode));//向堆区申请一个节点大小的空间
	if (NewNode == NULL)//判断申请是否成功
	{
		perror("malloc");
		exit(-1);
	}
	NewNode->_next = NewNode;//防止野指针的产生
	NewNode->_prev = NewNode;//防止野指针的产生
	return NewNode;
}
// 双向链表尾插
void ListPushBack(ListNode* pHead, LTDataType x)
{
	assert(pHead);//传入的头节点不能为空
	ListNode* temp = pHead;
	ListNode* newnode = ListCreate();//申请一个新节点
	newnode->_data = x;//存入数据
	//下面是将新节点插入到链表的末尾的过程:
	newnode->_prev = temp->_prev;//将新节点_prev指针指向链表中最后一个节点
	newnode->_next = temp;//将新节点_next指针指向链表中的头节点
	temp->_prev->_next = newnode;//将链表中最后一个节点的_next指向新节点
	temp->_prev = newnode;//将头节点的_prev指针指向新节点
}
// 双向链表头插
void ListPushFront(ListNode* pHead, LTDataType x)
{
	assert(pHead);//传入的头节点不能为空
	ListNode* temp = pHead;
	ListNode* newnode = ListCreate();//申请一个新节点
	newnode->_data = x;//存入数据
	//下面是将新节点插入到链表的头部的过程:
	newnode->_prev = temp;//将新节点_prev指针指向链表中头节点
	newnode->_next = temp->_next;//将新节点_next指针指向链表中的第一个数据节点
	temp->_next->_prev = newnode;//将链表中第一个数据节点的_perv指向新节点
	temp->_next = newnode;//将头节点的_next指针指向新节点
}
// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x)
{
	assert(pos);//传入的节点不能为空
	ListNode* temp = pos;
	ListNode* newnode = ListCreate();//申请一个新节点
	newnode->_data = x;//存入数据
	//下面是将新节点插入到链表pos节点前的过程:
	newnode->_prev = temp->_prev;//将新节点_prev指针指向pos节点的前一个节点
	newnode->_next = temp;//将新节点_next指针指向pos节点
	temp->_prev->_next = newnode;//将pos前一个节点的_next指针指向新节点
	temp->_prev = newnode;//将pos节点的_prev指针指向新节点
}
// 双向链表尾删
void ListPopBack(ListNode* pHead)
{
	assert(pHead);//传入的头节点不能为空
	assert(pHead->_prev);//链表不能为空
	ListNode* Del = pHead->_prev;
	//下面是将最后一个数据节点删除过程:
	Del->_prev->_next = Del->_next;//将最后一个数据节点的上一个节点的_next指针指向头节点
	Del->_next->_prev = Del->_prev;//将头节点的_next指针指向最后一个数据节点的上一个节点
	free(Del);//释放最后一个数据节点的空间
	Del = NULL;//防止野指针的产生
}
// 双向链表头删
void ListPopFront(ListNode* pHead)
{
	assert(pHead);//传入的头节点不能为空
	assert(pHead->_next);//链表不能为空
	ListNode* Del = pHead->_next;
	//下面是将第一个数据节点删除过程:
	Del->_next->_prev = Del->_prev;//将第一个数据节点的下一个节点的_prev指针指向头节点
	Del->_prev->_next = Del->_next;//将头节点的_next指针指向第一个数据节点的下一个节点
	free(Del);//释放第一个数据节点的空间
	Del = NULL;//防止野指针的产生
}
// 双向链表删除pos位置的节点
void ListErase(ListNode* pos)
{
	assert(pos);//传入的节点不能为空
	ListNode* Del = pos;
	//下面是将pos数据节点删除过程:
	Del->_prev->_next = Del->_next;//将pos数据节点的前一个节点的_next指针指向pos数据节点的下一个节点
	Del->_next->_prev = Del->_prev;//将pos数据节点的下一个节点的_prev指针指向pos数据节点的上一个节点
	free(Del);//释放pos数据节点的空间
	Del = NULL;//防止野指针的产生
}
// 双向链表查找
ListNode* ListFind(ListNode* pHead, LTDataType x)
{
	assert(pHead);//传入的头节点不能为空
	ListNode* find = pHead->_next;//跳过头节点开始查找
	while (find != pHead)
	{
		if (find->_data == x)
			return find;//找到返回该节点
		find = find->_next;
	}
	return NULL;//没找到返回空指针
}
// 双向链表打印
void ListPrint(ListNode* pHead)
{
	assert(pHead);
	printf("Head->");
	ListNode* print = pHead->_next;//跳过头节点开始打印
	while (print != pHead)
	{
		printf("%d->", print->_data);
		print = print->_next;
	}
	printf("Head\n");
}
// 双向链表销毁
void ListDestory(ListNode* pHead)
{
	assert(pHead);
	ListNode* Del = pHead->_next;//跳过头节点开始删除节点
	ListNode* temp = Del;
	while (Del != pHead)
	{
		temp = temp->_next;
		free(Del);
		Del = temp;
	}
	pHead->_next = pHead;
	pHead->_prev = pHead;
}

测试

        1.测试代码

int main()
{
	ListNode* Head= ListCreate();
	ListPrint(Head);
	ListPushBack(Head, 1);
	ListPrint(Head);
	ListPushBack(Head, 2);
	ListPrint(Head);
	ListPushBack(Head, 3);
	ListPrint(Head);
	ListPushBack(Head, 4);
	ListPrint(Head);
	ListPushFront(Head, 5);
	ListPrint(Head);
	ListInsert(ListFind(Head, 5), 6);
	ListPrint(Head);
	ListPopFront(Head);
	ListPrint(Head);
	ListPopFront(Head);
	ListPrint(Head);
	ListPopBack(Head);
	ListPrint(Head);
	ListErase(ListFind(Head, 2));
	ListPrint(Head);
	ListDestory(Head);
	ListPrint(Head);
	return 0;
}

        2.测试效果


 
链表总结

        无头单向非循环链表和带头双向循环链表的对比

        在我们实现完无头单向非循环链表和带头双向循环链表之后发现带头双向循环链表虽然结构复杂但是在尾插尾删时不用遍历整个链表,并且在实际使用时也比单链表更方便操作,具有绝对的优势,所以在存储数据时尝尝使用的是带头双向循环链表。

        链表和顺序表的对比

存储空间上:              顺序表物理空间上一定连续。

                                    链表在逻辑上连续,但在物理空间上不一定连续(具体要看malloc函数的分配了)。

在访问数据时:           顺序表可以随机访问,时间复杂度为O(1)。

                                    链表不可以随机访问,时间复杂度为O(N)。

在插入(删除)数据时: 顺序表可能需要移动元素,效率低,时间复杂度为O(N)。

                                    链表只需要改变指针指向,效率高,时间复杂度为O(N)。

添加数据时:              动态顺序表可能需要扩容。

                                   链表添加一个数据开辟一块空间没有扩容概念。

应用场景:                 顺序表一般运用在元素高效存储和频繁访问的场景。

                                   链表一般运用在任意位置插入和删除频繁的场景。

缓存利用率:             顺序表开辟的空间全部都用来存储数据,利用率高。

                                   链表开辟的一部分空间用来存储指针,利用率低。


数据结构的初阶链表到这里就结束了,代码量较多难免会有不足之处,还请各位大佬在评论区赐教!

各位看客咱们下一期见~

  • 8
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

1e-12

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

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

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

打赏作者

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

抵扣说明:

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

余额充值