这里首先纠正上篇文章一个错误,链表的一个有效数据点应该称为结点而不是节点。
一,双向链表的概念与结构
1.1概念与结构示意图
我们所说的双向链表全称为带头双向循环链表,也就是说此链表带有哨兵位结点(不存放任何数据的结点,且为头结点)。图示结构如下:
注意:这里的“带头”跟前面我们说的“头结点”是两个概念,实际前面的在单链表阶段称呼不严谨。
1.2双向链表结构代码
typedef int LTNDataType;
typedef struct ListNode
{
LTNDataType val;
struct ListNode* prev;
struct ListNode* next;
}LTNode;
prev为指向前一个结点的指针,next为指向后一个结点的指针,val为该结点中存储的数据。
二,实现双向链表的功能
2.1创建链表节点函数LTBuyNode
由于我们的双向链表在没有存放任何数据时,还有一个哨兵位结点,所以我们需要先实现链表结点创建函数:
LTNode* LTBuyNode(LTNDataType x)
{
LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
if (newnode == NULL)
{
perror("buynode:");
exit(1);
}
newnode->val = x;
newnode->next = newnode->prev = newnode;
return newnode;
}
我们的双向链表为循环链表,所以在只有一个结点时,我们创建的新结点需要自己的两个前后指针均指向自己,以便实现我们的双向链表初始化。
2.2双向链表的初始化函数LTInit
由于我们的哨兵位结点不存放有效数据,所以我们需要给初始结点直接返回一个存放数据为-1(无效数据)的结点,作为双向链表的初始结点:
LTNode* LTInit()
{
LTNode* pcur = LTBuyNode(-1);
return pcur;
}
2.3双向链表的尾插函数LTPushBack
void LTPushBack(LTNode* phead, LTNDataType x)
{
assert(phead);
LTNode* newnode = LTBuyNode(x);
phead->prev->next = newnode;
newnode->prev = phead->prev;
newnode->next = phead;
phead->prev = newnode;
}
先创建一个新结点并用临时变量进行接收,下一步则是使尾节点的next指向我们的新结点,同时将新结点的prev指向先前的尾结点,接下来由于我们创建的新结点为当前的尾结点,所以我们需要让其next指针指向哨兵位结点,最后再使哨兵位结点的prev指向我们的新结点即完成我们的尾插函数。
2.4双向链表的头插函数LTPushStart
这里需要注意,如果我们是直接将数据插到哨兵位前面,我们的方法实际上此时与尾插法无异,所以要实现头插,我们则需要将新结点直接插入到哨兵位结点的后面,从而实现头插:
void LTPushStart(LTNode* phead, LTNDataType x)
{
assert(phead);
LTNode* newnode = LTBuyNode(x);
newnode->prev = phead;
newnode->next = phead->next;
phead->next = newnode;
newnode->next->prev = newnode;
}
为了插入数据时的方便,我们先改变新插入结点的next与prev,这样不会对当前链表的结构产生影响,接下来我们便改变哨兵位结点的next与原哨兵位结点的next结点的prev,使其均指向我们创建的新结点,实现我们的头插法。
2.5判空函数LTEmpty
当我们的链表只剩下哨兵位结点时,我们判定此时链表不存放任何有效数据,又由于我们此时只有一个结点,所以我们直接判定当前哨兵位结点的prev是否指向它自己即可:
bool LTEmpty(LTNode* phead)
{
assert(phead);
return (phead->next == phead);
}
2.6尾删函数LTDeltBack
在进行尾删之前,我们也需要对链表进行判空,然后我们需要先用临时变量去保存尾结点的地址,同时改变尾结点的前结点的next,使其指向哨兵位,然后改变哨兵位结点的prev使其指向我们当前尾结点的前一个结点,最后释放尾结点即可:
void LTDeltBack(LTNode* phead)
{
assert(phead && !LTEmpty(phead));
LTNode* pcur = phead->prev;
phead->prev->prev->next = phead;
phead->prev = phead->prev->prev;
free(pcur);
pcur = NULL;
}
2.7头删函数LTDeltStart
与尾删函数类似,也需要创建临时变量去记住要释放结点的位置,然后接下来步骤的思想与尾删基本相同:
void LTDeltStart(LTNode* phead)
{
assert(phead && !LTEmpty(phead));
LTNode* pcur = phead->next;
phead->next = phead->next->next;
phead->next->next->prev = phead;
free(pcur);
pcur = NULL;
}
2.8寻找目标位置函数LTFind
与单链表的寻找方法基本一致,不过要注意终止条件应该是遍历到哨兵位结点时停止:
LTNode* LTFind(LTNode* phead,LTNDataType x)
{
assert(phead && !LTEmpty(phead));
LTNode* pcur = phead->next;
while (pcur != phead)
{
if (pcur->val == x)
{
return pcur;
}
pcur = pcur->next;
}
printf("没有找到\n");
return NULL;
}
2.9删除指定位置和向指定位置之后插入数据函数LTDeltDesBack与LTInserDesBack
与单链表一样,但是这里向指定位置之前或之后插入数据方法一致,只是你实现之后插入后,如果想实现之前,只需要去用prev寻找上一结点即可:
LTDeltDesBack:
void LTDeltDesBack(LTNode* pos,LTNode* phead)
{
assert(pos && (pos->next != phead));
LTNode* pcur = pos->next;
pos->next->next->prev = pos;
pos->next = pos->next->next;
free(pcur);
pcur = NULL;
}
LTInserDesBack:
void LTInserDesBack(LTNode* pos,LTNDataType x)
{
assert(pos);
LTNode* newnode = LTBuyNode(x);
newnode->prev = pos;
newnode->next = pos->next;
newnode->next->prev = newnode;
pos->next = newnode;
}
2.10销毁链表函数DestoLTN
void DestoLTN(LTNode* phead)
{
assert(phead);
LTNode* pcur = phead->next;
LTNode* pcur1 = NULL;
while (pcur != phead)
{
pcur1 = pcur->next;
free(pcur);
pcur = pcur1;
}
free(phead);
}
三,顺序表与链表的对比
当我们学习完顺序表与链表之后,下篇文章我们将介绍栈与队列的实现,在理解顺序表与链表功能的实现后,栈和队列的实现非常简单,仅仅只会有概念上不同的问题,我们下篇文章见。