个人主页:@Weraphael
✍作者简介:目前学习C++和算法
✈️专栏:数据结构
希望大家多多支持,咱一起进步!
如果文章对你有帮助的话
欢迎 评论 点赞 收藏 加关注
在单链表这篇博客中,我们已经实现了单链表的增删查改。今天这篇博客,我将带领大家实现最后一个常见的链表之双向带头循环链表
如上图所示,双向带头循环链表顾名思义就是有一个哨兵位的头结点,然而这个头结点却不存储有效数据;其次,一个结点存储两个地址,一个地址是存储下一个结点的地址,而另一个地址存储的是上一个结点的地址。
综上,不难可以写出它的结构
typedef int DLDataType;
typedef struct DListNode
{
DLDataType data;
struct DListNode* prev;//指向下一个结点
struct DListNode* next;//指向前一个结点
}DTNode;
为了方便管理,我们可以创建多个文件来实现
test.c - 测试代码逻辑 (源文件)
DList.c - 动态的实现 (源文件)
DList.h - 存放函数的声明 (头文件)
【DList.h】
typedef int DTDataType;
typedef struct DListNode
{
DTDataType data;
struct DListNode* prev;//指向下一个结点
struct DListNode* next;//指向前一个结点
}DTNode;
//开辟新结点
DTNode* BuyListNode(DTDataType x);
//初始化哨兵位头结点
DTNode* DTInit();
//尾插
void DTPushBack(DTNode* phead, DTDataType x);
//打印
void DTPrint(DTNode* phead);
//尾删
void DTPopBack(DTNode* phead);
//判断链表是否为空
bool DTEmpty(DTNode* phead);
//头插
void DTPushFront(DTNode* phead, DTDataType x);
//头删
void DTPopFront(DTNode* phead);
//在pos之前插入x
void DTInsert(DTNode* pos, DTDataType x);
//删除pos结点
void DTErase(DTNode* pos);
//查找
DTNode* DTFind(DTNode* phead, DTDataType x);
//释放
void DTDestroy(DTNode* phead);
//开辟新结点
DTNode* BuyListNode(DTDataType x)
{
DTNode* newnode = (DTNode*)malloc(sizeof(DTNode));
if (newnode == NULL)
{
perror("newnode :: malloc");
return NULL;
}
newnode->next = NULL;
newnode->prev = NULL;
newnode->data = x;
return newnode;
}
作用:有这个接口是因为后面的头结点初始化、尾插、头插等都需要开辟新的结点,有这个接口方便代码复用。
//初始化哨兵位头结点
DTNode* DTInit()
{
DTNode* phead = BuyListNode(-1);
phead->next = phead;
phead->prev = phead;
return phead;
}
【笔记总结】
- 哨兵位的头结点是不存放有意义的数据
- 由于是循环链表,初始化时应该自己指向自己
//尾插
void DTPushBack(DTNode* phead, DTDataType x)
{
//哨兵位绝对不可能为空
assert(phead);
// 1.开辟新结点
DTNode* newnode = BuyListNode(x);
// 2.找尾
DTNode* tail = phead->prev;
// 3.链接 head tail newnode
tail->next = newnode;
newnode->prev = tail;
newnode->next = phead;
phead->prev = newnode;
}
【笔记总结】
- 哨兵位的头结点绝对不可能为空,所以加个断言
- 双向循环链表找尾不需要向单链表那样遍历,因为头结点的
prev
就是尾
【动画展示】
//打印
void DTPrint(DTNode* phead)
{
assert(phead);
DTNode* cur = phead->next;
while (cur != phead)
{
printf("<=%d=>", cur->data);
cur = cur->next;
}
printf("\n");
}
【笔记总结】
- 打印遍历链表时不能从头结点开始。
- 遍历结束条件是
cur != phead
,因为当cur
遍历到尾结点,由于是循环链表,下一个结点就是哨兵位的头结点。
//判断链表是否为空
bool DTEmpty(DTNode* phead)
{
assert(phead);
return phead->next == phead;
}
【笔记总结】
- 当链表只剩下一个哨兵位的头结点,说明链表为空。所以双向链表为空的情况是头结点的
next
指向本身。
//尾删
void DTPopBack(DTNode* phead)
{
assert(phead);
assert(!DTEmpty(phead));
//1.找尾
DTNode* tail = phead->prev;
//2.记录尾结点的前一个结点
DTNode* tailprev = tail->prev;
//3.链接 phead tailprev
tailprev->next = phead;
phead->prev = tailprev;
//4.释放尾结点
free(tail);
}
【学习笔记】
尾删要特判原链表是否为空。空链表不能删!!
【动图展示】
//头插
void DTPushFront(DTNode* phead, DTDataType x)
{
assert(phead);
//1.申请新结点
DTNode* newnode = BuyListNode(x);
//2.链接
newnode->next = phead->next;
phead->next->prev = newnode;
phead->next = newnode;
newnode->prev = phead;
}
【学习笔记】
//头删
void DTPopFront(DTNode* phead)
{
assert(phead);
assert(!DTEmpty(phead));
//1.记录哨兵位的下一个结点(即头结点)
DTNode* del = phead->next;
phead->next = del->next;
del->next->prev = phead;
//2.释放del
free(del);
}
//在pos之前插入x
void DTInsert(DTNode* pos, DTDataType x)
{
assert(pos);
//1.申请新结点
DTNode* newnode = BuyListNode(x);
//2.记录pos的前一个结点
DTNode* posprev = pos->prev;
//3.插入 posprev newnode pos
posprev->next = newnode;
newnode->prev = posprev;
newnode->next = pos;
pos->prev = newnode;
}
//删除pos结点
void DTErase(DTNode* pos)
{
assert(pos);
//1.记录pos前一个结点
DTNode* posprev = pos->prev;
//2.链接
posprev->next = pos->next;
pos->next->prev = posprev;
//3.释放pos
free(pos);
}
//查找
DTNode* DTFind(DTNode* phead, DTDataType x)
{
assert(phead);
//1.不能从哨兵位开始遍历
DTNode* cur = phead->next;
while (cur != phead)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
//若循环结束,还没找到则返回NULL
return NULL;
}
详细细节可参考打印
//释放
void DTDestroy(DTNode* phead)
{
DTNode* cur = phead->next;
while (cur != phead)
{
//在释放前每次记录cur的下一个结点
DTNode* next = cur->next;
free(cur);
cur = next;
}
//最后再单独释放phead
free(phead);
}
- 相比单链表,需要遍历链表找尾,但是带头双向循环链表可以直接找到尾节点,时间复杂度为O(1)。
- 但缺点是:不支持随机访问,缓存命中率相对低。