上篇博客重点讲解了单链表的相关知识,单链表(无头单向不循环)是链表里面最简单的一种结构,而今天我们要讲的带头双向循环链表则是链表中最复杂的一种结构,叫做带头双向循环链表
事实上,链表一种有八种结构:单向/双向,带头/不带头,循环/不循环,2^3便是八种,所以我们就讲了链表中的两种极端结构,带头双向循环链表虽然结构复杂,但代码实现起来反而容易
带头双向循环链表结构
1.带头:带哨兵位的头节点,之所以叫哨兵位,是因为这个节点不存储有效数据,只存储链表头节点的地址,方便我们找头节点以及更改头节点
2.双向:和单链表每一节点只存下一个节点的地址不同的是,该链表每个节点有两个指针域,第一个指针域存储上一个节点的地址,而第二个指针域存储下一个节点的地址,因此双向链表很方便找到上一个节点(而单链表需要定义一个新的指针,遍历找到指定节点的前一个节点,时间复杂度高)
3.循环:我们知道,单链尾节点的next存储的是NULL,而循环链表尾节点的next存储的是哨兵位头节点的地址,哨兵位头节点的prev存储的是尾节点的地址,因此构成了循环结构
带头双向循环链表实现
全部代码如下
List.h
#include<stdio.h>
#include<stdbool.h>
#include<assert.h>
#include<stdlib.h>
typedef int LTDataType;
typedef struct ListNode
{
struct ListNode* prev;
LTDataType data;
struct ListNode* next;
}LTNode;
//初始化
LTNode* LTInit();
//打印
void LTPrint(LTNode* phead);
//尾插
void LTPushBack(LTNode* phead, LTDataType x);
//头插
void LTPushFront(LTNode* phead, LTDataType x);
//判空
bool LTEmpty(LTNode* phead);
//尾删
void LTPopBack(LTNode* phead);
//头删
void LTPopFront(LTNode* phead);
//查找
LTNode* LTFind(LTNode* phead, LTDataType x);
//插入
void LTInsertFront(LTNode* pos, LTDataType x);
//删除
void LTErase(LTNode* pos);
//销毁
void LTDestroy(LTNode* phead);
List.c
#include"List.h"
LTNode* BuyLTNode(LTDataType x)
{
LTNode* newNode = (LTNode*)malloc(sizeof(LTNode));
if (newNode == NULL)
{
perror("malloc");
return;
}
newNode->data = x;
newNode->prev = NULL;
newNode->next = NULL;
return newNode;
}
LTNode* LTInit()
{
LTNode* phead = BuyLTNode(-1);
phead->prev = phead;
phead->next = phead;
return phead;
}
void LTPrint(LTNode* phead)
{
assert(phead);
printf("guard<=>");
LTNode* cur = phead->next;
while (cur != phead)
{
printf("%d<==>", cur->data);
cur = cur->next;
}
printf("\n");
}
void LTPushBack(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newNode = BuyLTNode(x);
LTNode* tail = phead->prev;
tail->next = newNode;
newNode->prev = tail;
newNode->next = phead;
phead->prev = newNode;
}
void LTPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* head = phead->next;
LTNode* newNode = BuyLTNode(x);
phead->next = newNode;
newNode->prev = phead;
newNode->next = head;
head->prev = newNode;
}
//void LTPushFront(LTNode* phead, LTDataType x)
//{
// assert(phead);
// LTNode* newNode = BuyLTNode(x);
//
// newNode->next = phead->next;
// phead->next->prev = newNode;
// phead->next = newNode;
// newNode->prev = phead;
//}
bool LTEmpty(LTNode* phead)
{
assert(phead);
return phead->next == phead;
}
void LTPopBack(LTNode* phead)
{
assert(phead);
assert(!LTEmpty(phead));
LTNode* tail = phead->prev;
LTNode* tailPrev = tail->prev;
free(tail);
phead->prev = tailPrev;
tailPrev->next = phead;
}
void LTPopFront(LTNode* phead)
{
assert(phead);
assert(!LTEmpty(phead));
LTNode* first = phead->next;
LTNode* second = first->next;
phead->next = second;
second->prev = phead;
free(first);
}
//void LTPopFront(LTNode* phead)
//{
// assert(phead);
// assert(!LTEmpty(phead));
//
// LTNode* first = phead->next;
//
// phead->next = first->next;
// first->next->prev = phead;
// free(first);
//}
LTNode* LTFind(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* cur = phead->next;
while(cur != phead)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
void LTInsertFront(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* posprev = pos->prev;
LTNode* newNode = BuyLTNode(x);
posprev->next = newNode;
newNode->prev = posprev;
newNode->next = pos;
pos->prev = newNode;
}
void LTErase(LTNode* pos)
{
assert(pos);
LTNode* posprev = pos->prev;
LTNode* posnext = pos->next;
posprev->next = posnext;
posnext->prev = posprev;
free(pos);
}
void LTDestroy(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
LTNode* next = cur->next;
free(cur);
cur = next;
}
free(phead);
}
test.c
#include"List.h"
void TestList1()
{
LTNode* plist = LTInit();
LTPushBack(plist, 1);
LTPushBack(plist, 2);
LTPushBack(plist, 3);
LTPushBack(plist, 4);
LTPrint(plist);
}
void TestList2()
{
LTNode* plist = LTInit();
LTPushFront(plist, 1);
LTPushFront(plist, 2);
LTPushFront(plist, 3);
LTPushFront(plist, 4);
LTPrint(plist);
}
void TestList3()
{
LTNode* plist = LTInit();
LTPushFront(plist, 1);
LTPushFront(plist, 2);
LTPushFront(plist, 3);
LTPushFront(plist, 4);
LTPopBack(plist);
LTPrint(plist);
}
void TestList4()
{
LTNode* plist = LTInit();
LTPushBack(plist, 1);
LTPushBack(plist, 2);
LTPushBack(plist, 3);
LTPushBack(plist, 4);
LTPopFront(plist);
LTPopFront(plist);
LTPrint(plist);
}
void TestList5()
{
LTNode* plist = LTInit();
LTPushBack(plist, 1);
LTPushBack(plist, 2);
LTPushBack(plist, 3);
LTPushBack(plist, 4);
LTNode* pos = LTFind(plist,3);
pos->data = 30;
LTPrint(plist);
}
void TestList6()
{
LTNode* plist = LTInit();
LTPushBack(plist, 1);
LTPushBack(plist, 2);
LTPushBack(plist, 3);
LTPushBack(plist, 4);
LTNode* pos = LTFind(plist, 3);
LTInsertFront(pos,10);
LTPrint(plist);
}
void TestList7()
{
LTNode* plist = LTInit();
LTPushBack(plist, 1);
LTPushBack(plist, 2);
LTPushBack(plist, 3);
LTPushBack(plist, 4);
LTNode* pos = LTFind(plist, 3);
LTErase(pos);
LTPrint(plist);
}
int main()
{
//TestList1();
//TestList2();
//TestList3();
//TestList4();
//TestList5();
//TestList6();
TestList7();
}
1.定义结构
有了单链表的基础,这块理解起来就容易多啦,无非多加了一个prev指针指向前一个节点
2.功能实现
需要先给大家说明的是,在讲解单链表函数实现的时候,头插头删等等这些需要改变头节点的指针的函数,我们的形参都使用的是二级结构体指针,但是我们带头双向循环链表的所有函数形参使用一级指针即可,因为带哨兵位的头节点中存储了头节点地址,因此想要改变头节点,只需要改变哨兵位头节点这个结构体的成员变量(next)即可,不需要改变外部的plist,因此一级指针就行
创建节点
①链表初始化
注意,带头双向循环链表为空不是没有一个节点,而是只有带哨兵位的头节点 ,因此我们对其进行初始化一下。
虽然只有一个哨兵位头节点,但还是应该满足带头双向循环的特点,因此结构如下:
说简单一点,就是其中的prev和next都指向了自己这个节点本身
由于头节点不存储有效数据,因此我们随便给个数字就行
②打印链表
思路:和单链表一样,需要遍历整个链表,但关键点就是循环结束的条件,单链表是尾节点的next指向空,而带头双向循环链表特点是尾节点的next是phead
为了避免改掉头节点,我们还是选择用新的变量tail/cur 进行遍历
注意两点:
1. cur 的初始值不是phead, 而是phead->next,因为哨兵位头节点不存储有效数据,所以无需打印
2.循环继续条件不是 cur->next != phead, 是cur!=phead,否则cur为尾节点的时候,循环不会进去,就会少打印尾节点的数据
③尾插
思路很简单,就是链接新节点和尾节点,以及链接新节点和头节点,注意是双向链表,因此prev和next都要考虑到位
还记得我们在单链表中讲的插入删除这些操作要注意什么吗?就是要注意链表中只有一个节点(不算头节点)和链表为空的情况,我们此处也需要考虑一下。当然由于不论链表为空还是只有一个节点,都是有哨兵位节点的,进行尾插时的逻辑和有多个节点都没有任何区别
因此上述代码适用链表的所有情况,我们应该能初步感知到带头双向循环的优势了
尾插逻辑测试
④头插
思路:
1.先定义一个head头指针变量保存头节点地址,然后插入新节点进行链接
2.若不想定义另外的变量,则应该先链接新节点和头节点,再链接新节点和带哨兵位的头节点
法一:
若链表为空,即phead->next 就是phead,因此head就是phead,该代码也没有任何问题
法二:
若链表为空,即phead->next 就是phead,该代码也没有任何问题
头插测试结果
⑤判空
已经提到过,链表为空的特点是phead的next还是phead,注意返回值是bool,而且判空函数的逻辑是为空返回真,不为空返回假,为空则返回真
⑥尾删
尾删逻辑:重点是定义一个指针变量 tailPrev,存储尾节点前一个节点的地址
当然链表为空就不能删了,所以我们加了一个断言,用来处理链表为空的时候。这里重点理解assert(!LTEmpty(phead))这句代码,因为assert(条件)表示的是条件为真继续执行,条件为假会报错,我们此处想要的是链表不为空正常执行,链表为空(LTEmpty返回值为真)报错,因此加了!逻辑反操作这一操作符,表示返回值为假(链表不为空)正常执行
尾删逻辑测试
⑦头删
思路:先保存头节点地址,链接新的头节点和哨兵位头节点
法一
当然,其中phead->next->prev这句代码就不是很直观了,所以不是很推荐这种写法,我们宁可多定义一个变量,使代码的可读性提高
法二
头删逻辑测试
⑧查找
①cur初始值!
②while循环继续条件
查找逻辑测试
⑨任意位置插入(pos之前rnei)
既然有了任意位置的插入,那么开始写的头插和尾插函数可以直接来调用该函数即可
头插:LTInsertFront(phead->next,x);
尾插:LTInsertFront(phead,x);
注意尾插的pos是phead,可能有些小伙伴会有疑问,不是尾插嘛,怎么插到了哨兵位头节点的前面
要说明的是链表是靠指针链接的,而不是像数组是块连续的空间,画图把节点画在哪并不要紧,链接的时候是正常顺序就行
例如打印链表的时候依旧是从phead->next开始遍历,到cur == phead结束!!!
任意位置插入逻辑测试
⑩任意位置删除
保存pos前一个节点和后一个节点的地址,释放pos,链接posprev和posnext
11.链表的销毁
记得在最后把哨兵位头节点释放了
好啦,带头双向循环链表的相关知识,大家就重点去感知该链表结构的设计巧妙之处,虽然结构复杂了,但是代码实现起来却更加地容易,也更加容易理解,本篇博客仍有许多不足,欢迎小伙伴们交流指正