目录
前言
之前我们成功实现的单链表,而现在我们将实现一种解决了单链表缺点的带头双向循环链表,只要明白其中的逻辑它比单链表更好实现。
一、带头双向循环链表是什么?
就如同字面意思一样,我们会有一个不存放数据的哨兵位用来初始化,并且链表内的指针是互相指向对方,也就是说链表最后不会指向NULL,而会再次循环指向起点也就是哨兵位,哨兵位还有好处,因为我们通过哨兵位增加数据时不用直接修改自己的地址,所以不需要用到二级指针。
二、使用步骤
1.创建结构体变量
双链表和单链表不同的我们新增一个指向之前数据地址的指针prev。
代码如下(示例):
typedef int typedata;
typedef struct SSList {
struct SSList* next;
struct SSList* prev;
typedata data;
}SSList;
2.初始化链表
我们对链表进行初始化时,因为这个双向链表带着头节点,所以我们需要创建一块空间,这块空间并不会传入任何数组,创建完成后,头节点的下一个和上一个都指向自己以实现循环并返回地址,由于之后也要开辟空间,所以我们也创建一个开开辟空间的函数。
代码如下(示例):
SSList* capacity(typedata x)
{
SSList* newnode = (SSList*)malloc(sizeof(SSList));
if (newnode == NULL)
{
perror("malloc fail");
exit(-1);
}
newnode->data = x;
newnode->next = NULL;
newnode->prev = NULL;
return newnode;
}
SSList* Initlist()
{
SSList* newnode = capacity(0);
newnode->next = newnode;
newnode->prev = newnode;
return newnode;
}
3.链表的头插
双向带头结点的链表在实现头插时,我们先找到头节点的next也就是第一个数据,找到和我们就可以将新开辟的空间的next指向它,它的prev也指向开辟的空间,再用哨兵位指向插入的头并且使其也指向头节点。
代码如下(示例):
void SSLpushfront(SSList* phead, typedata data)
{
assert(phead);
SSList* newnode = capacity(data);
SSList* next = phead->next;
newnode->next = next;
next->prev = newnode;
phead->next = newnode;
newnode->prev = phead;
}
4.链表的头删
实现头删时,我们要先判断是否还有数据,因此我们写一个函数检查链表是否为空,如果头节点的next指向其本身,那么便说明链表为空,反之亦然。
链表的头删,我们首先需要找到要删除头节点下一位,之后通过头节点的next指向它,它的prev指向哨兵位头节点,最后将删除的结构体指针将其释放置空。
代码如下(示例):
bool SSLempty(SSList* phead)
{
assert(phead);
if (phead->next == phead)
return true;
else
return false;
}
void SSLpopfront(SSList* phead)
{
assert(phead);
assert(!SSLempty(phead));
SSList* next = phead->next->next;
SSList* cur = phead->next;
free(cur);
cur = NULL;
phead->next = next;
next->prev = phead;
}
5.链表的尾插
正如我们开始所说,它解决了单链表的确定,如上图所示,我们要找到链表的尾,它就在头节点哨兵位的前一个,我们就不需要用到循环遍历的,找到尾之后,将尾的next赋值我们新开辟的空间,并也将其的prev指向原来的尾,再用新的尾指向头节点,再用头节点的prev指向新的尾。
代码如下(示例):
void SSLpushtail(SSList* phead, typedata data)
{
assert(phead);
SSList* newnode = capacity(data);
SSList* tail = phead->prev;
tail->next = newnode;
newnode->prev = tail;
newnode->next = phead;
phead->prev = newnode;
}
6.链表的尾删
尾删也是同样,我们也要先检查是否有数据可删,我们找到原来尾的前一个,直接可以通过找到尾节点,再通过尾节点的prev找到尾节点的前一个,为了方便释放,我们可以把原来的尾节点存下来防止丢失,用原来尾节点的上一个指向头节点,再用头节点指向其完成循环,最后将保存下来原来尾节点的地址释放赋值为NULL
代码如下(示例):
void SSLpoptail(SSList* phead)
{
assert(phead);
assert(!SSLempty(phead));
SSList* tail = phead->prev;
SSList* cur = tail->prev;
free(tail);
tail = NULL;
cur->next = phead;
phead->prev = cur;
}
7.链表的插入
链表的插入实现是找到要插入的空间pos的前一个空间为prve,用prev指向新开辟的空间,再新开辟的空间指向prev,再用开辟的空间指向pos,同时pos也指向开辟的空间。
代码如下(示例):
void SSLinsert(SSList* pos, typedata data)
{
assert(pos);
SSList* newnode = capacity(data);
SSList* prev = pos->prev;
prev->next = newnode;
newnode->prev = prev;
newnode->next = pos;
pos->prev = newnode;
}
8.链表的删除
链表的删除同样要先判断是否有数据可删,然后找到pos的prev的地址和next的地址,找到了之后用找到的prev和next互相指向对方,最后将posfree掉置空。
代码如下(示例):
void SSLerase(SSList* pos)
{
assert(pos);
SSList* prev = pos->prev;
SSList* next = pos->next;
prev->next = next;
next->prev = prev;
free(pos);
pos = NULL;
}
9.链表的打印
实现链表的打印我们需要用到循环遍历,首先找到头节点哨兵位的next将其赋值位cur,因为这是第一个数据存放的地址,结束条件位如果cur为头节点就结束。
代码如下(示例):
void Printtail(SSList* phead)
{
assert(phead);
SSList* node = phead->next;
while (node != phead)
{
printf("%d ", node->data);
node = node->next;
}
}
10.链表的实现
总结
双向循环链表弥补了单链表的一些不足,使它可以完美适合头插头删和尾插尾删,并且带头节点之后可以只传一级指针便可以改变内容,只要理解其的逻辑结构,双向链表的实现是可以轻松实现的,希望我的这篇文章对您有所帮助。