目录
一.前言
前面我们已经学习了单链表,它解决了顺序表中插入或者删除数据需要挪动大量数据的缺点。
但仍有需改进的地方。就比如我们需要寻找某个结点的前一个结点,就只能通过遍历来寻找。
这样就可能会造成大量的时间浪费。为了解决这个问题,今天就要学习今天的主角-->带头双
向循环链表
在学习单链表中,我们大家可能被二级指针搞昏了头脑,现在我们实现一个带头的链表,也就是带哨兵位的链表,哨兵位只充当哨兵作用,它在链表的第一个位置,但不保存数据。因此我们在使用时,并不会将哨兵位当作头结点,而是将它的下一个结点当作头结点。这样做的好处是在插入数据时不需要修改物理结构上的头结点,也就不需要传二级指针。
而实现为双向链表,则可以让我们在使用时更加方便,不必像学习单链表时单独定义一个指针寻找前一个结点。
二.双向链表的功能
双向链表需要实现的功能如下
- 双向链表结点申请(其实这个不是一个功能,但是我们要在实现下面的功能时使用它)
- 初始化双向链表中的数据。
- 对双向链表进行尾插(末尾插入数据)。
- 对双向链表进行头插(开头插入数据)。
- 对双向链表进行头删(开头删除数据)。
- 对双向链表进行尾删(末尾删除数据)。
- 对双向链表就像查找数据。
- 对双向链表数据进行修改。
- 任意位置的删除和插入数据。
- 打印双向链表中的数据。
- 销毁双向链表。
三.双向链表的定义
双向链表,就要能够从两个方向遍历,也就是说,每个结点都要有两个指针,一个指向前一个结点,一个指向下一个结点。
因此,我们应这样定义一个双向链表:
typedef struct ListNode
{
LTDataType data;//保存数据
struct ListNode* next;//指向下一个结点
struct ListNode* prev;//指向上一个结点
}LTNode;
四.双向链表的功能
4.0双向链表结点申请
为了不让开辟的空间在函数栈帧销毁时被销毁掉,我们应在堆上开辟一块空间,因此应用malloc开辟空间。
LTNode* LTBuyNode(LTDataType x)
{
LTNode* node = (LTNode*)malloc(sizeof(LTNode));
if (!node)
{
perror("malloc fail");
exit(1);
}
node->data = x;
//由于要实现循环链表,这里让他们指向自己。
//如果不是循环链表,让他们指向空即可。
node->next = node->prev = node;
}
4.1初始化双向链表
初始化双向链表,也就是创建一个哨兵位。
由于每个结点都应有数据,我们用-1表示哨兵位的数据。
LTNode* LTinit()
{
LTNode* phead = LTBuyNode(-1);
return phead;
}
4.2双向链表尾插
插入数据之前,链表必须已经初始化,也就是必须有哨兵位,否则它就不带头了。
在这里由于哨兵位实际上充当了头节点的作用,因此我们不可能会修改头节点的地址,所以传一级指针即可。
尾插,我们需要更改新结点的prev结点和next结点,哨兵位的prev指针,尾结点的next指针。
如下图所示:下图中的序号是代码中最后四行的顺序,这里在上课时要带着大家重点分析一下。
//插入数据之前,链表必须已经初始化。
//不改变哨兵位的地址,因此传一级即可
//尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = LTBuyNode(x);
newnode->next = phead;
newnode->prev = phead->prev;
phead->prev->next = newnode;//-> 操作符来间接访问该指针指向的内存
phead->prev = newnode;
}
4.3双向链表尾删
双向链表的尾删,就是删除最后一个结点。
删除最后一个结点,需要更改倒数第二个结点的next指针,以及哨兵位的prev结点。
//尾删
void LTPopBack(LTNode* phead)
{
//链表有效而且链表不能只有一个哨兵位
assert(phead && phead->next != phead);
//不定义del结点也可以,只不过写出来的代码很难被理解。
//课堂上有时间的话可以先写不定义del的,然后优化为这段代码。
LTNode* del = phead->prev;
del->prev->next = phead;
phead->prev = del->prev;
free(del);
del = NULL;
}
4.4双向链表头插
双向链表的头插,实际上是在哨兵位后面插入一个结点。
我们需要更改新结点的prev和next指针、哨兵位的next指针、原第二个结点的prev结点。
下图中的序号是更改顺序
为什么是这样更改呢?
由于原链表之间相互具有关系,我们如果先修改原链表的话,也就是先完成图中3、4的代码,就会导致哨兵位和头结点之间的关系消失了,后续我们就没有办法使用->进行操作。
而又因为我们创建出的新结点和原链表并没有关系,我们先修改新结点并不会影响到原链表,因此先修改新结点。
在修改完新结点之后,我们就可以着手于修改原链表了,在修改原链表时,我们发现要访问链表中的第二个结点,是需要连续使用->操作符进行间接访问的。所以我们如果先修改->操作符的左操作数,就会导致我们连续使用->操作符时出现错误。因此我们应先修改需要连续使用->操作符的指针。
结论:
表述1:1.先修改与原链表没有关系而且可以直接访问的结点。
2.然后修改在原链表之中相互联系但是不可以直接访问的结点
3.最后修改在原链表之中相互联系而且可以直接访问的直接。
表述2:1.先更改新结点,之后更改相关联的结点的指针。
2.在更改相关联的结点的指针时,需先改变需要通过连续使用—>间接访问的结点的指针。
//头插
void LTPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = LTBuyNode(x);
//先修改newnode,再修改相关联的-->写详细解释
newnode->next = phead->next;
newnode->prev = phead;
phead->next->prev = newnode;
phead->next = newnode;
}
4.5双向链表头删
由于刚刚讲解了更改指针的原理和结论,下面只会分析更改的指针,而不会再画图帮助大家理解顺序。 (上课时有时间还是画图带着大家学习,时间不足就直接用结论)
删除一个结点,我们需要更改头结点的next指针以及删除结点的下一个结点的prev结点,由于并没有新结点,因此我们要先更改需要连续->操作的结点,然后再更改不需要连续使用->操作的结点。
void LTPopFront(LTNode* phead)
{
assert(phead && phead->next != phead);
LTNode* del = phead->next;
phead->next = del->next;
del->next->prev = phead;
free(del);
del = NULL;
}
4.6双向链表查找
查找一个数值最为结点,我们直接遍历原链表查找即可。
这里需要注意的是,由于我们的链表是循环的,因此我们while循环内的终止条件为不等于哨兵位。
LTNode* LTFind(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
4.7修改指定数据
修改指定数据需要我们传入三个参数
1.哨兵位
2.要修改的位置
3.要修改的数据
这个函数的作用为把指定位置的数据修改为指定数据。
void LTModify(LTNode* phead, LTNode* pos, LTDataType x)
{
assert(phead);
assert(phead != pos);//防止对头节点操作
LTNode* cur = phead->next;
while (cur != phead)
{
if (cur == pos)
{
cur->data = x;
}
cur = cur->next;
}
}
这个函数可以配合查找数据的函数使用
下面给出一个使用用例:
//将数值为3的位置的数值改为x
void LTModify(phead, LTfind(phead,3),x)
4.8指定插入数据
这里我们实现一个在pos结点之后插入数据的函数。
首先,这个需要我们创建结点。
我们需要更改的指针是我们创建的结点的prev和next指针;以及pos结点的next指针以及pos结点的下一个结点的prev指针。
我们应该先修改新结点的next结点和prev结点,之后修改需要连续使用->操作的指针,最后修改不需要连续使用->操作的指针。
//在pos结点之后插入数据
void LTInsert(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* newnode = LTBuyNode(x);
newnode->next = pos->next;
newnode->prev = pos->prev;
pos->next->prev = newnode;
pos->next = newnode;
}
4.9指定删除数据
删除pos位置的结点,我们需要改变的指针是pos的前一个结点的next指针以及pos的下一个结点的prev结点。这两个都需要连续使用->连续访问,因此先修改谁就无所谓了,下面的两行互换位置也无所谓了。
void LTErase(LTNode* pos)
{
//pos理论上不能是phead,但是这里我们没传phead,没法检验phead
assert(pos);
pos->next->prev = pos->prev;
pos->prev->next = pos->next;
free(pos);
pos = NULL;
}
4.10打印双向链表
打印双向链表,我们直接通过一个while循环打印即可。
但是要注意循环的终止条件为pcur!=phead。
LTNode* LTPrint(LTNode* phead, LTDataType x)
{
LTNode* pcur = phead->next;
while (pcur != phead)
{
printf("%d->", pcur->data);
pcur = pcur->next;
}
printf("\n");
}
List.h
#pragma once #include<stdio.h> #include<stdlib.h> #include<assert.h> typedef int LTDataType; //定义双向链表节点的结构 typedef struct ListNode { LTDataType data; struct ListNode* next; struct ListNode* prev; }LTNode; //声明双向链表中提供的方法 LTNode* LTBuyNode(LTDataType x); //初始化 //void LTInit(LTNode** pphead); LTNode* LTInit(); //销毁 void LTDesTroy(LTNode* phead); //打印 void LTPrint(LTNode* phead); //插入数据之前,链表必须已经有了一个哨兵位。 //不改变哨兵位的地址,因此传一级即可 //尾插 void LTPushBack(LTNode* phead, LTDataType x); //头插 void LTPushFront(LTNode* phead, LTDataType x); //尾删 void LTPopBack(LTNode* phead); //头删 void LTPopFront(LTNode* phead); //在pos位置之后插入数据 void LTInsert(LTNode* pos, LTDataType x); //删除pos节点 void LTErase(LTNode* pos); LTNode* LTFind(LTNode* phead, LTDataType x);