目录
链表的分类
在讲解双向链表之前,首先需要了解一下链表的分类,链表的分类有很多,共8种。
带头与不带头
带头指的是链表中有哨兵位节点,该哨兵位节点即为头节点。也就是head节点。
在单链表中,我们所说的头节点实际上指的是第一个有效的节点,为了方便理解才这么称呼。
单向与双向
单向链表只能从一个方向遍历,通过节点的next指针只能找到下一个节点。
双向链表可以从两个方向遍历,通过节点的前驱指针可以找到上一个节点,后继指针可以找到下一个节点。
循环与不循环
不循环链表的尾节点的next指针为NULL。
循环链表的尾节点的next指针指向的是头节点。
双向链表
根据链表的分类我们可以知道单链表是不带头单向不循环链表。
双向链表就是,带头双向循环链表
双向链表的结构
双向链表由节点组成,每个节点由数据+指向下一个节点的指针+指向前一个节点的指针组成。
typedef int LTDataType;
typedef struct ListNode
{
LTDataType data;
struct ListNode* next;
struct ListNode* prev;
}LTNode;
双向链表的申请节点
正如上面所说,双向链表是一个循环的链表,所以在申请一个新节点时,我们需要让这个节点的前驱指针与后继指针都指向它自己。
LTNode* LTBuyNode(int 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;
}
双向链表的初始化
方法一
以参数的形式将哨兵位传给函数,然后gei哨兵位赋值
双向链表是一个带头链表,所以在插入数据之前必须要初始化到只有一个头节点的情况。哨兵位节点的值可以为任意值。
void LTInit(LTNode** pphead)
{
*pphead = LTBuyNode(-1);
}
方法二
在函数中定义一个指针,让他作为双向链表的哨兵位,再返回该指针。
//初始化 方法二
LTNode* LTInit2()
{
LTNode* phead = LTBuyNode(-1);
return phead;
}
双向链表的打印
想要打印双向链表,我们就需要应用while循环,在新建一个指针pcur,由于哨兵位节点的数据是无效的不需要打印,所以我们可以让pcur指针指向phead->next,循环的结束条件就是当pcur走到phead的时候,就可以跳出循环了。
//打印链表
void LTPrint(LTNode* phead)
{
LTNode* pcur = phead->next;
while (pcur != phead)
{
printf("%d->", pcur->data);
pcur = pcur->next;
}
printf("\n");
}
双向链表的尾插
在双向链表中尾插就是改变链表中哨兵位节点和尾节点的指针指向。首先申请一个新节点newnode为了方便,先改变newnode的prev指针和next指针的指向。先将prev指针指向phead->prev,再将next指针指向phead。再改变原链表,为了防止找不到尾节点,我们先将尾节点phead->prev->next改为newhead,再将哨兵位节点phead->prev改为newhead。
另外需要注意的是,在调用尾插这个函数的时候链表传参是应该传一级指针还是二级指针。答案是传一级指针就足够了,因为哨兵位节点是不能被删除的,节点的地址也是不能被更改的,所以在传参时,只需要传一级指针就足够了。
//尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = LTBuyNode(x);
//先改变newnode的两个指针
newnode->next = phead;
newnode->prev = phead->prev;
//再改变原链表
phead->prev->next = newnode;
phead->prev = newnode;
}
双向链表的头插
头插的思想与尾插相似,但是需要注意的是,头插指的是在哨兵位节点后面插入,也就是在第一个有效节点之前插入,即phead->next。
//头插
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;
}
双向链表的尾删
当我们需要在双向链表中尾删时,首先我们需要确保链表是有效链表且不为空链表,需要含有哨兵位节点,才可以进行尾删。
尾删时,我们需要改变尾节点的前一个节点,即phead->prev->prev的指针方向。同样,为了防止找不到倒数第二个节点,我们需要先更改倒数第二个节点的指针指向,再更改哨兵位的指针指向。
//尾删
void LTPopBack(LTNode* phead)
{
//链表需要有效,且链表不能为空,且需要只含有一个哨兵位节点
assert(phead && phead->next != phead);
LTNode* del = phead->prev;
//更改链表尾节点
phead->prev = del->prev;
del->prev->next = phead;
//释放尾节点
free(del);
del = NULL;
}
双向链表的查找数据
想要查找数据,我们需要先遍历整个链表,如果有查找的数据,则返回该节点,若没有,则返回空指针。循环的基本思想与链表的打印相同。
//查找数据
LTNode* LTFind(LTNode* phead, LTDataType x)
{
LTNode* pcur = phead->next;
while (pcur != phead)
{
if (pcur->data == x)
{
return pcur;
}
pcur = pcur->next;
}
return NULL;
}
双向链表的在指定位置pos之后插入数据
在pos之后插入数据的基本思想与头插尾插相似。需要先申请新节点newnode,先更改newnode的两个指针的指向,再去更改pos和pos->next指针的指向。
//在指定位置pos之后插入数据
void LTInsert(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* newnode = LTBuyNode(x);
//先改newnode的两个指针
newnode->next = pos->next;
newnode->prev = pos;
pos->next->prev = newnode;
pos->next = newnode;
}
双向链表的删除pos节点
删除pos节点,就是把pos节点的前一个节点与后一个节点相连。再将pos节点释放掉。
//删除pos节点
void LTErase(LTNode* pos)
{
assert(pos);
pos->next->prev = pos->prev;
pos->prev->next = pos->next;
free(pos);
pos = NULL;
}
双向链表的销毁
销毁双向链表应该先从第一个有效节点开始释放,为了防止销毁节点后找不到下一个节点,我们需要先新建一个节点用来存储销毁节点的下一个节点。在将所有有效节点释放完毕后,我们还需要将哨兵位节点释放,并将其置为NULL。
//销毁链表
void LTDestory(LTNode* phead)
{
assert(phead);
LTNode* pcur = phead->next;
while (pcur != phead)
{
LTNode* next = pcur->next;
free(pcur);
pcur = next;
}
free(phead);
phead = NULL;
}
补充
可能有人会有这样的疑问,为什么在删除pos节点和销毁链表的时候传递的是一级指针而不是二级指针呢。
其实LTErase和LTDestory参数在理论上是需要传递二级指针,因为我们需要让形参的改变影响到实参。但是由于之前实现的所有操作都是传递的一级指针,所以为了保证接口的一致性,即为了防止在读代码时不易理解,所以才将传递的数据统一为二级指针。那么这个操作也有一定的弊端,就是每当在调用这个函数以后,当形参phead被置为NULL后,实参plist不会被更改为NULL,这个时候就需要我们自己手动的去将实参置为NULL。
如果我们不将plist置为NULL后果会是怎样的呢?若我们后续不使用plist那么将不会有影响。但是如果后续还需要使用plist,由于保存plist的地址的空间被释放掉了,在调用plist的时候就会出现野指针等问题。