1.双向链表的结构
2.双向链表的使用
(1)双向链表的初始化
(2)双向链表的打印
(3)双向链表的尾/头插和尾/头删
(4)查找
(5)指定位置插入
(6)销毁双链表
(7)补充
3.双向链表与单链表的对比
我以过客之名,祝你前程似锦
一.双向链表的结构
1.链表的8种类型:
2.带头双向循环链表:
双向链表与单链表第一点不同 的就是单链表为空,那就是一个空链表 ,但双向链表为空时,链表里还剩下一个头节点 (即哨兵位)
二.双向链表的使用
1.初始化:
cs
#include"List.h"
//申请结点
LTNode* LTBuyNode(LTDataType x)
{
LTNode* node = (LTNode*)malloc(sizeof(LTNode));
if (node == NULL)
{
perror("malloc failed!");
exit(1);
}
node->data = x;
node->next = node->prev = node;
//初始化时,不可以将哨兵位的next指针和prev指针指向NULL(因为这样就不符合"循环")
return node;
}
void LTInit(LTNode** pphead)
{
//给双向链表创建一个哨兵位
*pphead = LTBuyNode(1);
}
哨兵位的data值是啥其实都无关紧要,就像这里的"1"也是我随便传的一个数字,因为在后续双向链表的使用过程中,哨兵位的data数据从始至终是不参与的
2.双向链表的打印:
cs
//打印链表
void Print(LTNode* phead)
{
LTNode* pcur = phead->next;
while (pcur != phead)
{
printf("%d->", pcur->data);
pcur = pcur->next;
}
printf("\n");
}
3.双向链表的尾/头插和尾/头删
(1)尾插:
首先,无论是头插尾插头删尾删,哨兵位的结点都不能被删除,节点的地址也不能发生改变
因此,在进行"插"的一系列操作时,函数传递的参数都不会是哨兵位的地址的地址(也就是我们所说的二级指针因为实参与形参的关系,所以在修改双向链表时传递的与哨兵位有关的指针只能说哨兵位的地址而不能是其地址的地址, 因为前者的传递是不会影响哨兵位本身的地址属性,而后者二级指针的传递这会在函数的运行时修改不应该被修改的哨兵位的地址)
(但二级指针也不是说完全不行,这里为了方便理解和使用才这么说的)
如图,在尾插时需要改变这三个结点:phead , phead->prev 和 newnode, 其中需要解释的就是这个phead->prev结点,首先phead代表哨兵位head的地址,而head是一个结构体,而作为head的指向上一个结点(在双向链表里即指的是尾节点)的指针prev,phead->prev这个整体的含义同时也代表了上一个结点的地址(如果这里还是有些不理解的话,你想想我们上一步既然可以通过phead(head的指针访问head的结构体内容,为何就不能通过phead->prev这个整体的指针去访问上一个结构体的内容?因此本质上,这里无论是phead->prev这个整体还是phead本身,都代表着一个结点的地址))这样是不是容易理解些?
那接下来就是代码的具体实现了:
cs
//尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = LTBuyNode(x);
//此时有三个结点的指向方向需要改变:
// phead phead->prev 和 newnode
newnode->prev = phead->prev;//使newnode的前驱指针指向原链表最后一个结点
newnode->next = phead;//使newnode的next指针指向哨兵位
//先修改newnode的指向
phead->prev->next = newnode;//使原链表的最后一个结点的next指针指向newnode
phead->prev = newnode;//最后修改哨兵位的前驱指针使其指向newnode
//再修改head的指向
//注意:一定要先修改newnode,如果先修改head的话,head的前驱指针prev就会发生改变,就无法使newnode的前驱指针指向原链表最后一个结点
}
当然,这里经过检测验证如果双向链表为空(只有一个哨兵位)也是没有问题的
(2)头插:
cs
//头插
void LTPushFront(LTNode* phead, LTDataType x)
{
assert(phead);//哨兵位不能为空
LTNode* newnode = LTBuyNode(x);
//插入结点是往哨兵位后面一位插入结点,若插入到头结点前就是尾插了
//phead phead->next newnode
newnode->next = phead->next;
newnode->prev = phead;
phead->next->prev = newnode;
phead->next = newnode;
}
同样的,在修改几个指针指向是要注意指向修改的顺序,如果是先修改原来的结点指针使其指向我们的新结点,那么我们在后续对新节点使其指向旧节点操作时就会发现旧节点的指向早已被修改从而发生错误了
(3)尾删:
cs
//尾删
void LTPopBack(LTNode* phead)
{
//双向链表需有效且不为空
assert(phead && phead->next != phead);
LTNode* del = phead->prev;//del这里就表示尾结点
//接下来涉及的三个结点:phead ,del->prev ,del
del->prev->next = phead;//使原尾结点的上一个结点的next指针程序指回head(这里de->prev是一个整体)
phead->prev = del->prev;//再使得哨兵位的prev指针指向原尾结点的上一个结点
//删除del结点
free(del);
del = NULL;
}
(4)头删:
cs
//头删
void LTPopFront(LTNode* phead)
{
assert(phead && phead->next != phead);
LTNode* del = phead->next;
//phead del del->next
del->next->prev = phead;
phead->next = del->next;
free(del);
del = NULL;
}
4.查找结点:
查找结点实际上也就是一一遍历链表,一样就输出,找不到就NULL
cs
//查找
void LTFind(LTNode* phead, LTDataType x)
//遍历,核对
{
LTNode* pcur = phead->next;
while (pcur != phead)
{
if (pcur->data == x)
{
return pcur;
}
pcur = pcur->next;
}
//没有找到
return NULL;
}
5.指定位置插入和删除:
和前面几种套路也是一模一样的,画一下图就会清晰很多:
cs
//在pos位置之后插⼊数据
void LTInsert(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* newnode = LTBuyNode(x);
//pos newnode pos->next
newnode->next = pos->next;
newnode->prev = pos;
pos->next->prev = newnode;
pos->next = newnode;
}
但这里也需要关注一点就是指定位置插入也可以包含着头插,但头插不能是指定位置
cs
//删除pos结点数据
void LTErase(LTNode* pos)
{
assert(pos);
//pos->prev pos pos->next
pos->prev->next = pos->next;
pos->next->prev = pos->prev;
free(pos);
pos = NULL;
}
6.销毁双链表:
cs
void LTDesTroy(LTNode* phead)
{
LTNode* pcur = phead->next;
while (pcur != phead)
{
LTNode* next = pcur->next;
free(pcur);
pcur = next;
}
free(phead);
phead = NULL;
}
7.补充:
这里一直传一级指针是为了保证接口的一致性,但为什么我最开始的初始化还传的二级指针?
这就不得不说到两种传递方式的差异:一种以参数形式返回而另外一种以返回值形式返回:
如图,上面一种表示以参数形式返回,涉及到对哨兵位本身数据的修改,因此使用到了二级指针(即&plist),而下一种以返回值形式返回则是间接地返回了新创建的哨兵位的地址,从而达到与第一种一样的效果
三.双向链表与单链表的对比
双向链表与单向链表的主要区别在于节点结构及其访问方式:
节点结构:
单向链表:每个节点只包含一个数据域和一个指向下一个节点的指针(即"后继指针")。
双向链表:每个节点不仅包含一个数据域和一个指向下一个节点的指针(后继指针),还包含一个指向前一个节点的指针(即"前驱指针")
访问方式:
单向链表:由于节点只有后继指针,因此只能从头节点开始顺序遍历链表,无法直接访问前一个节点。
双向链表:由于节点包含前驱和后继两个指针,因此可以在链表中前后双向遍历,即可以从头到尾,也可以从尾到头遍历
插入和删除操作的效率:
单向链表:在单向链表中,插入和删除节点时需要遍历链表以找到前驱节点(特别是在删除或在中间插入节点时),这可能增加时间复杂度。
双向链表:由于可以直接访问前驱节点,插入和删除操作通常更加高效,特别是在已知节点位置的情况下
存储空间:
单向链表:每个节点只存储一个指针,因此相对节省空间。
双向链表:每个节点存储两个指针,因此需要更多的存储空间。
综上所述,双向链表与单向链表的主要区别在于节点是否包含前驱指针,这使得双向链表在访问灵活性和操作效率上有所增强,但相应地也增加了其空间复杂度。