数据结构:双向链表
这里我们在List.h文件中定义了一个结构体,并将其改名为了LTNode,如上图结构体中的两个指针分别用来保存前一个和后一个结构体(节点)的地址,另还有一个date数据用来存储数据。然后我们再在test.c文件中定义plist指针。随后我们的下一步就是初始化链表了!
在这之前,小编已经讲了数据结构中的顺序表以及单向链表,接下来我们将更进一步,继续来讲双向链表!
结合之前,我们先来复习一下单向链表,单向链表是这样的:
1.概念:相较于顺序表在物理上和逻辑上都是连续的,单链表是⼀种物理存储结构上⾮连续、⾮顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
2.结构:链表的结构跟火车车厢相似,淡季时车次的车厢会相应减少,旺季时车次的车厢会额外增加几节。只需要将火车里的某节车厢去掉或加上,不会影响其他车厢,每节车厢都是独⽴存在的。
在单链表中,单链表的每一个节点都是一个结构体,结构体中包含了一个数据以及指向下一个节点的指针,而接下来要讲的双链表与其有略微不同,如下图:
这是一个双向带头链表
注意:这⾥的“带头”跟前⾯我们说的“头节点”是两个概念,带头链表⾥的头节点,实际为“哨兵位”,哨兵位节点不存储任何有效元素,只是站在这⾥“放哨的” “哨兵位”存在的意义就是为了方便我们更好的遍历循环链表避免死循环。
接下来就要开始讲解双向链表的实现了!
双向链表的实现
如之前一样,我们也是先创建了三个文件;
1.定义结构体
这里我们在List.h文件中定义了一个结构体,并将其改名为了LTNode,如上图结构体中的两个指针分别用来保存前一个和后一个结构体(节点)的地址,另还有一个date数据用来存储数据。
然后我们再在test.c文件中定义plist指针。随后我们的下一步就是初始化链表了!
2.初始化
在这里,我们的初始化就是创建一个哨兵位,所以我们首先要写一个用来申请节点的函数,如下:
//申请节点
LTNode* LTBuyNode(Type x)
{
LTNode* node = (LTNode*)malloc(sizeof(LTNode));
if (node == NULL)
{
perror("malloc fail");
exit(1);
}
node->data = x;
node->next = node->prev = node;
return node;
}
注意,在我们申请完结点之后,我们需要让这个节点的前后指针都指向自己!
申请完节点之后,由于哨兵位只是起”放哨“作用,所以这里我们就将哨兵位结构体中的data数据设为-1;如下
到这里,我们的初始化工作就算完成了!接下来我们就可以开始链表的增删改查工作了!
3.尾插
我们先来看以下代码:
//尾插
void PushBack(LTNode* phead, Type x)
{
assert(phead);
LTNode* node = LTBuyNode(x);
node->prev = phead->prev;
node->next = phead->next;
phead->prev->next = node;
phead->prev = node;
}
看到这个代码,小伙伴们是不是有点疑惑了,不着急,我们我们需要结合图来看
还是这个图,这里我们是尾插,想象我们新创建了一个节点d4,看这个图,再解红我们的代码,很容易就可以看出上面的代码到底指向的是什么。所以小伙伴们,在以后写代码的学习过程中,画图是非常重要的!!
接下来我们就来讲头插!
4.头插
同样先看以下代码:
//头插
void PushFrond(LTNode* phead, Type x)
{
assert(phead);
LTNode* node = LTBuyNode(x);
node->next = phead->next;
node->prev = phead;
phead->next->prev = node;
phead->next = node;
}
头插与尾插差不多,结合代码看图就可以很容易看懂,但这里与前面尾插同样有一个点需要注意,就是在上面代码中phead->next->prev = node;和phead->next = node;这两行的顺序可不能更改,若更改的话,我们就无法找到phead->next->prev这个节点了!
5.尾删
看以下代码:
//尾删
void PopBack(LTNode* phead)
{
//链表必须有效并且不能为空(只有一个哨兵位)
assert(phead && phead->next != phead);
LTNode* p1 = phead->prev;
p1->prev->next = phead;
phead->prev = p1->prev;
//删除p1节点
free(p1);
p1 = NULL;
}
尾删比较简单,我们只需要先创建一个节点并将最后节点存在这个节点中,这是为了方便我们后续通过这个节点找到其他节点,并释放这个节点。
这里还有需要注意的是assert(phead && phead->next != phead),这里assert需要同时判断两个节点因为链表必须有效并且不能为空(只有一个哨兵位)!!
最后就是头删了
6.头删
//头删
void PopFrond(LTNode* phead)
{
assert(phead && phead->next != phead);
LTNode* p1 = phead->next;
phead->next = p1->next;
p1->next->prev = phead;
//删除节点
free(p1);
p1 = NULL;
}
头删与尾删基本差不多,比较简单,它的注意事项也与尾删相同,这里小编就不重复讲述了。
到这里,我们的双拉链表就基本讲完了,当然,还有在指定位置插入数据和删除数据,这个与小编上面讲的插入和删除异曲同工,仅仅只需要我们先找到我们要删除或插入的节点的位置,然后再将他们的地址作为参数上传就可以直接进行更改了!小伙伴们下去后要多多尝试哦!在尝试的过程中大家不要忘了画图,画图可是非常重要的!!
双链表的基本操作已经讲完,大家点个关注,防止迷路,欢迎大家共同学习交流!!!

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐
所有评论(0)