在单链表中我们详细讲解了单向链表的创建和相关操作。
本篇文章,博主将继续为大家介绍链表的另一种结构---双向链表中的双向带头循环链表。
双向链表的结构特点
在经过对单链表的学习后,大家可能觉得双向链表是一座难以逾越的大山。
但实际上,双向链表的实现更加的简单。
何为双向?
顾名思义,通过当前结点不仅可以找到前一个结点,也能找到后一个结点。
在上图中我们可以看到带头双向循环链表有几个特殊点。
1.需要定义一个哨兵卫作为头,并且哨兵中不存储数据信息。
2.在定义结构体时,不同于单向链表只需定义一个指向下一个节点的指针,还需定义一个指向前一个节点的指针。
3.尾结点的下一个指向要指向哨兵卫头节点,头节点的上一个结点指向尾结点,以达到循环的效果。因此如果想要链表不循环,只需要使尾节点cur->next=null。
4.在对链表进行增删查改操作时,我们不需要传递二级指针,因为哨兵卫节点帮我们保存着第一个位置的地址。
双向链表初始化
首先,我们定义节点的结构。上代码。
typedef int Datatype;
typedef struct ListNode
{
struct ListNode* next;//指向下一个节点的指针
struct ListNode* prev;//指向前一个结点的指针
Datatype data;//节点存储的数据
}LTNode;
不同于单链表,双链表建立前需要先定义一个头节点作为哨兵卫。
LTNode* LTInit()
{
LTNode* phead = BuyNode(-1);//开辟空间,不保存有效数据
phead->prev = phead;//哨兵卫的上一个节点指向自己
phead->next = phead;//哨兵卫的下一个节点指向自己
return phead;
}
由于此时链表中只有一个哨兵卫节点,所以要先使其本身形成循环。
这里BuyNode大家是不是很熟悉呢!在单链表实现中,我们建立一个外部函数来简化开创新空间的代码,双链表也不例外。在这里我们再次复习一下吧。
LTNode* BuyNode(Datatype x)
{
LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));//申请空间
if (newnode == NULL)
{
perror("malloc fail");
return NULL;
}
newnode->data = x;
newnode->next = NULL;
newnode->prev = NULL;
return newnode;
}
双向链表的数据插入
链表初始化后,我们来实现数据的插入。同样的,数据的插入分为头插和尾插。
需要注意的是,双向链表的数据插入要关注节点的前后指向。
头插的实现
void LTPushFront(LTNode* phead, Datatype x)
{
assert(phead);
LTNode* newnode = BuyNode(x);//申请空间
newnode->next = phead->next;//插入的节点指向头节点的下一个节点
phead->next->prev = newnode;//头节点的上一个节点与插入节点相连
phead->next = newnode;//头节点与插入节点相连
newnode->prev = phead;//插入节点的prev指向头节点,完成节点的双向实现
}
博主将画一个丑陋的图便于大家理解。
需要注意的点是,我们得先使newnode指向P1再使P1指向newnode。
尾插的实现
尾插和头插的实现类似,不做赘述,直接上代码。
void LTPushBack(LTNode* phead, Datatype x)
{
assert(phead);
LTNode* tail = phead->prev;//找尾
LTNode* newnode = BuyNode(x);
tail->next = newnode;
newnode->prev = tail;
newnode->next = phead;
phead->prev = newnode;
}
双向链表的数据打印
实现数据插入后,我们打印来验证插入的正确性。
void LTprint(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
printf("guard=>");
while (cur != phead)
{
printf("%d<==>", cur->data);
cur = cur->next;
}
printf("\n");
}
void TextList1()
{
LTNode* plist = LTInit();
LTPushBack(plist, 1);
LTPushBack(plist, 2);
LTPushBack(plist, 3);
LTPushBack(plist, 4);
LTPushBack(plist, 5);
LTPushBack(plist, 6);
LTprint(plist);
}
void TextList2()
{
LTNode* plist = LTInit();
LTPushFront(plist, 1);
LTPushFront(plist, 2);
LTPushFront(plist, 3);
LTPushFront(plist, 4);
LTPushFront(plist, 5);
LTPushFront(plist, 6);
LTprint(plist);
}
打印结果如下。
双向链表数据的删除
同样的,删除分为头删和尾删。
尾删的实现
void LTPopBack(LTNode* phead)
{
assert(phead);
assert(phead->next!=phead);
LTNode* tail = phead->prev;//找尾
LTNode* tailprev = tail->prev;//换尾
free(tail);//释放原来的尾
tailprev->next = phead;//新的尾与哨兵卫相连
phead->prev = tailprev;
}
注意这里的断言,由于我们的哨兵卫头节点是不能删除的,所有当链表中只有一个哨兵卫时不能进行删除操作,头删时亦是如此。
头删的实现
void LTPopFront(LTNode* phead)
{
assert(phead);
assert(phead->next != phead);
LTNode* first = phead->next;
LTNode* second = first->next;
phead->next = second;
second->prev = phead;
free(first);
}
头删时需要注意的时,我们不能过早的将first指针空间释放,否则second指针会找不到phead。正确的顺序是,先建立起second与phead间的联系后再释放first。
双向链表的查找
双向链表的查找与单链表思想相同,直接上代码。
LTNode* LTFind(LTNode* phead, Datatype x)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
双向链表的销毁
在双向链表的销毁中,我们需要在释放空间前保存当前节点的下一个节点,否则会导致释放当前节点后找不到后续节点的地址。
此外,不要忘记释放头节点的空间哦!
void LTDestory(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
LTNode* next = cur->next;//保存下一个节点地址
free(cur);//释放当前节点
cur = next;
}
free(phead);//释放哨兵卫
phead = NULL;
}
指定位置的插入和删除
在指定节点pos前插入
void LTInsert(LTNode* pos, Datatype x)
{
assert(pos);
LTNode* posprev = pos->prev;
LTNode* posnext = pos->next;
LTNode* newnode = BuyNode(x);
newnode->prev = posprev;
newnode->next = posnext;
pos->prev = newnode;
posprev->next = newnode;
}
删除指定节点
void LTErase(LTNode* pos)
{
assert(pos);
LTNode* posprev = pos->prev;
LTNode* posnext = pos->next;
posprev->next = posnext;
posnext->prev = posprev;
free(pos);
}
总结
本篇文章结束后,对于链表的学习就告一段落了。在试题中有很多题目会针对链表进行设计,希望大家能够理解并进行实操。