数据结构之双链表

大家好,经过单链表的学习,接下来我们将学习双链表的内容!

首先,给大家展示一下双链表的结构,也就是带头双向循环链表:
在这里插入图片描述
注意:这⾥的“带头”跟前⾯我们说的“头节点”是两个概念,实际前⾯的在单链表阶段称呼不严谨,但是为了更好的理解就直接称为单链表的头节点。
带头链表⾥的头节点,实际为“哨兵位”,哨兵位节点不存储任何有效元素,只是站在这⾥“放哨的”
“哨兵位”存在的意义:
遍历循环链表避免死循环。

双向链表的实现

双链表的结构

struct ListNode{
	LTDataType data;//存储数据
	struct ListNode* next;//连接下一个节点
	struct ListNode* prev;//连接上一个节点
}
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>

typedef int LTDataType;//数据类型
typedef struct ListNode {
	LTDataType data;
	struct ListNode* next;
	struct ListNode* prev;
}LTNode;

void LTPrint(LTNode* phead);//打印双链表

LTNode* LTInit();//初始化双链表
void LTDestroy(LTNode* phead);//销毁双链表
LTNode* ListBuyNode(LTDataType x);//创建新节点

void LTPushBack(LTNode* phead, LTDataType x);//尾插
void LTPushFront(LTNode* phead, LTDataType x);//头插
void LTPopBack(LTNode* phead);//尾删
void LTPopFront(LTNode* phead);//头删

void LTInsert(LTNode* pos, LTDataType x);//pos位置之后插入
void LTErase(LTNode* pos);//pos位置元素删除
LTNode* LTFind(LTNode* phead,LTDataType x);//找到指定节点

以上就是需要实现的函数

我们发现,双链表中传入的只是一级指针,而不像单链表一样传的是二级指针,这是为什么呢?
双链表(双向链表)与单链表最大的不同之处在于,每个节点不仅有一个指向后继节点的 next 指针,还有一个指向前驱节点的 prev 指针。这使得在双链表中可以通过 nextprev 指针轻松地访问、插入或删除前后节点。

相比之下,单链表只有 next 指针,无法直接获取前驱节点,在插入或删除节点时通常需要更改节点的地址。

因此,在使用双链表时,只需要传递一个指向头节点的一级指针就足够了,通过 nextprev 指针,可以准确地访问、插入和删除双链表中的元素,而无需改变地址。这样的设计使得代码更加简洁和高效。

需要注意的是,在某些情况下,可能仍然需要使用二级指针(指向整个双链表的指针),特别是在操作头节点或删除整个链表等情况下。

打印双链表

void LTPrint(LTNode* phead) {
	assert(phead);//头节点不为空
	LTNode* pcur = phead->next;//从哨兵位的下一位开始
	while (pcur != phead)//pcur走到头节点的时候结束循环
	{
		printf("%d->", pcur->data);
		pcur = pcur->next;//pcur往后遍历
	}
	printf("\n");
}

示意图:
在这里插入图片描述
其实就是pcur从头节点的后一个元素开始向后遍历,依次打印链表中的数据

双链表的初始化

LTNode* LTInit() {
	LTNode* phead = (LTNode*)malloc(sizeof(LTNode));//创建头节点
	phead->data = -1;//这个没关系,哨兵位的值无用
	phead->next = phead;
	phead->prev = phead;//头节点的next和prev节点都指向自己
	return phead;
}

示意图:
在这里插入图片描述

销毁双链表

void LTDestroy(LTNode* phead) {
	assert(phead);
	LTNode* pcur = phead->next;
	while (pcur != phead)//pcur遍历双链表
	{
		LTNode* next = pcur->next;//先把pcur的下一个节点存储下来
		free(pcur);
		pcur = NULL;
		pcur = next;//pcur走向下一个节点
	}

	free(phead);//最后要把头节点(哨兵位)也释放掉
	phead = NULL;
}

这里有一点需要注意,那就是当我们使用函数销毁双链表的时候,会发现一个问题:
在这里插入图片描述
在这里插入图片描述
我们会发现phead显然已经为空,而plist的后面的数据也已经清空,但是plist这个头节点却并没有置为空,但我们在销毁函数中明明已经写过了,这实在令人疑惑。

其实,在该销毁函数中,不能直接通过将 phead 设置为 NULL 来修改传入的头节点指针。这是因为 phead 是函数参数 LTNode* phead 的副本,对其进行修改不会影响原本的指针。

如果要修改原本的头节点指针,需要传入一个指向头节点指针的指针(即二级指针),并在函数内部通过间接访问来修改原本的指针。

但是传二级指针,又会影响接口的一致性,而我们的用户并不知道一级指针和二级指针的区别,这样子,使用起来就会很麻烦,所以,我们选择的方法是,在销毁之后,手动给plist置为空,而不使用二级指针,保证接口的一致性。

创建新节点

LTNode* ListBuyNode(LTDataType x) {
	LTNode* node = (LTNode*)malloc(sizeof(LTNode));//动态开辟空间
	if (node == NULL)//若开辟失败
	{
		perror("malloc");
		return;
	}
	node->data = x;//给节点赋值
	node->next = node->prev = NULL;//后继节点与前驱节点都指向空
	return node;//返回节点
}

头插

void LTPushFront(LTNode* phead, LTDataType x) {
	assert(phead);
	LTNode* node = ListBuyNode(x);//先拿到新节点
	
	//先处理node
	node->prev = phead;//node的前一个节点是头节点
	node->next = phead->next;//node的下一个节点是本来头节点的节点,也就是1
	
	//在处理其他节点
	phead->next->prev = node;//头节点的下一个节点的前一个节点不再是头节点,而是node
	phead->next = node;//头节点的下一个节点是node
}

示意图:
在这里插入图片描述

头删

void LTPopFront(LTNode* phead) {
	assert(phead);
	assert(phead->next != phead);//删除都需要考虑是否还有节点可删

	LTNode* del = phead->next;
	del->next->prev = phead;//要头删的节点的下一个节点的前一个节点不再是要删除的节点,而是头节点
	phead->next = del->next;//头节点的下一个节点变成要头删的节点的下一个节点

	free(del);//删除节点
	del = NULL;
}

示意图:
在这里插入图片描述

尾插

void LTPushBack(LTNode* phead, LTDataType x) {
	assert(phead);
	LTNode* node = ListBuyNode(x);//新节点

	node->next = phead;//node的下一个节点就是头节点
	node->prev = phead->prev;//node的前一个节点就是头节点原本的前一个节点

	phead->prev->next = node;//头节点原本的前一个节点的下一个节点变为node
	phead->prev = node;//头节点现在的前一个节点变为node
}

示意图:
在这里插入图片描述

尾删

void LTPopBack(LTNode* phead) {
	assert(phead);
	assert(phead->next != phead);

	LTNode* del = phead->prev;
	del->prev->next = phead;//del的前一个节点的下一个节点变成头节点
	phead->prev = del->prev;//头节点的前一个节点变成del的前一个节点

	free(del);//删除节点
	del = NULL;
}

示意图:
在这里插入图片描述

在pos节点之后插入数据

void LTInsert(LTNode* pos, LTDataType x) {
	assert(pos);
	LTNode* node = ListBuyNode(x);//创建新节点

	node->next = pos->next;//node的下一个节点是pos的下一个节点
	node->prev = pos;//node的前一个节点连接pos

	pos->next = node;//pos节点现在的下一个节点连接node
	node->next->prev = node;//node节点的下一个节点的前一个节点现在不再是pos的下一个节点,而是与node相连
}

示意图:
在这里插入图片描述

删除pos节点的数据

void LTErase(LTNode* pos) {
	assert(pos);
	
	pos->prev->next = pos->next;//pos的前一个节点的的下一个节点改为pos的下一个节点
	pos->next->prev = pos->prev;//pos的下一个节点的前一个节点变为pos的前一个节点
	free(pos);//删除pos
	pos = NULL;
}

示意图:
在这里插入图片描述

找到指定数据在链表中的位置

LTNode* LTFind(LTNode* phead,LTDataType x) {
	assert(phead);
	LTNode* pcur = phead->next;
	while (pcur != phead)//遍历链表
	{
		if (pcur->data == x)
			return pcur;//找到了

		pcur = pcur->next;
	}
	return NULL;//没找到返回空
}

相信通过上述的讲解,大家对于双链表一定多了些了解,而下面这张表,则是比较单链表和顺序表的差异的,大家也可以仔细看看!希望看完这篇文章大家都能学到东西!

特点顺序表链表(单链表)
存储空间上物理上一定连续物理上不一定连续
随机访问支持O(1)不支持:O(N)
任意位置插入和删除可能需要搬移元素,效率低 O(N)只需修改指针指向
插入动态顺序表,空间不够时需要扩容没有容量的概念
应用场景元素高效存储+频繁访问任意位置插入和删除频繁
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值