在上篇链表的介绍里已经说过了,带头双向循环链表也是一种十分常见的结构,这种结构比简单的单链表复杂一些,但是功能性更强,且在实际运用中更加常见。接下来我们就来实现一下它的基本功能,并且看一下这种结构相较于单链表有什么优势。
目录
先来看看此次带头双向循环链表我们实现的具体功能以及结构体变量的定义。
一、头文件
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>
typedef int LTDataType;
typedef struct ListNode
{
struct ListNode* next;
struct LIstNode* prev;
LTDataType data;
}LTNode;
//节点的创建
LTNode* BuyListNode(LTDataType x);
//节点的初始化
void ListInit(LTNode** pphead);
//打印
void ListPrint(LTNode* phead);
//头插
void ListPushFront(LTNode* phead, LTDataType x);
//尾插
void ListPushBack(LTNode* phead, LTDataType x);
//任意位置插入
void ListInsert(LTNode* pos, LTDataType x);
//判断链表是否为空
bool ListEmpty(LTNode* phead);
//查找
LTNode* ListFind(LTNode* phead, LTDataType x);
// 尾删
void ListPopBack(LTNode* phead);
//头删
void ListPopFront(LTNode* phead);
//删除
void ListErase(LTNode* pos);
// 双向链表销毁
void ListDestory(LTNode* pHead);
二、功能实现
1. 结点的定义
因为实现的是双向循环链表,所以我们创建结点的时候要有两个指针,分别指向该结点的前一个和该节点的后一个。
typedef int LTDataType;
typedef struct ListNode
{
struct ListNode* next; //下一个结点
struct LIstNode* prev; //上一个结点
LTDataType data; //存放数据
}LTNode;
2. 结点的创建
做好准备工作之后,第一步肯定是创建结点了。
//创建结点
LTNode* BuyListNode(LTDataType x)
{
LTNode* Node = (LTNode*)malloc(sizeof(LTNode));
assert(Node);
Node->data = x;
Node->next = NULL;
Node->prev = NULL;
return Node;
}
3. 结点的初始化
因为我们这次的链表是带头结点的,即哨兵结点,哨兵结点就是指向第一个结点,用来记录第一个结点的地址,不存放有效数据,这个哨兵结点对于我们后面实现一些功能提供了便利。
我们实现的这个功能则是将第一个结点变成哨兵结点,不存放有效数据,如果想存放数据,则要再创建一个结点,新创建的这个结点就是第一个存放数据的结点。
void ListInit(LTNode** pphead)
{
//创建一个新节点
*pphead=BuyListNode(-1);
//让链表头和尾都指向自己
(*pphead)->next = *pphead;
(*pphead)->prev = *pphead;
}
这里我们让头节点的next和prev都指向自己,因为当前链表中就存放了这一个元素,所以前和后都指向本身,这样的指向让后续操作变得十分便利。
4.链表的销毁
销毁这个功能是肯定要实现的,所以我们先就来实现这个功能,防止后面我们忘记了。
void ListDestory(LTNode* phead)
{
LTNode* cur = phead->next;
LTNode* prev = NULL;
//当cur指针指向头的时候
while (cur != phead)
{
prev = cur->next;
ListErase(cur);
cur = prev;
}
free(phead);
}
5. 链表的尾插
//尾插
void ListPushBack(LTNode* phead, LTDataType x)
{
//创建一个新节点
LTNode* newnode = BuyListNode(x);
//哨兵位的前一个就是尾
LTNode* tail = phead->prev;
//尾的下一个是newnode 即 将newnode置为新的尾
tail->next = newnode;
//新节点的前一个指向以前的尾
newnode->prev = tail;
//newnode的下一个结点指向头
newnode->next = phead;
//哨兵结点的前一个指向尾
phead->prev = newnode;
}
循环链表的尾不是指向NULL的,而是指向头,而头节点的前一个结点指向尾,这就是双向循环链表的特点。
6. 链表的头插
//头插
void ListPushFront(LTNode* phead, LTDataType x)
{
可以实现空节点的尾插
//创建一个节点
LTNode* Node = BuyListNode(x);
assert(Node);
Node->next = phead->next;
Node->prev = phead;
phead->next = Node;
Node->next->prev = Node;
}
7. 链表的打印
//打印
void ListPrint(LTNode* phead)
{
assert(phead);
//让cur指向phead的下一个节点 ,从下一个节点开始打印,直到打印到cur == phead
LTNode* cur = phead->next;
while (cur != phead)
{
printf("%d ", cur->data);
cur = cur->next;
}
printf("\n");
}
检查一下上面的功能并展示一下效果:
8.任意pos位置之前插入
//在任意位置之前插入
void ListInsert(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* prev = pos->prev;
LTNode* newnode = BuyListNode(x);
prev->next = newnode;
newnode->prev = prev;
newnode->next = pos;
pos->prev = newnode;
}
在单链表的实现中,我们如果想在pos之前插入要先遍历一遍链表,找到pos位置之前的结点,但是在双向循环链表中,我们有保存前一个结点的指针,所以这个功能实现起来十分简单。
有了这个函数,那我们可不可以想着,服用这个函数去代替头插、尾插呢?
//尾插
void ListPushBack(LTNode* phead, LTDataType x)
{
//哨兵位的前一个就是尾
listinsert(phead,x);
}
//头插
void ListPushFront(LTNode* phead, LTDataType x)
{
//在phead下一个结点之前插入就是头插
ListInsert(phead->next, x);
}
因为哨兵位的上一个结点就是尾,所以我们传入phead就可以了,尾插就这样实现了。
又因为在phead下一个结点就是第一个结点,所以我们在原本的第一个结点之前插入一个结点,就完成了头插。
9. 链表的判空
接下来我们是要实现链表的尾删的,但是通常我们在实现删除结点之前要判断当前链表是否为空链表,但是因为我们这是一个带头的链表类型,除了空链表的情况还有只有哨兵结点的情况,所以我们要来做一个链表的判空处理。
既然这样,我们不如将此功能封装成一个函数,这样各种删除功能我们都可以调用此函数,方便快捷。
//链表的判空
bool ListEmpty(LTNode* phead)
{
//如果链表一个结点都没有
assert(phead);
//如果phead的下一个结点还指向自己,则表示当前链表只有一个哨兵结点,返回true,
//表示当前链表就是一个空链表;如果pehad的下一个结点不是自己,则返回false,
//表示当前链表不是一个空链表。
return phead->next == phead;
}
9. 链表的尾删
尾删还是很简单的,我们要做的就是要先判空一下,然后再将尾结点取消关联,再free释放。
//尾删
void ListPopBack(LTNode* phead)
{
assert(phead);
//判断这个链表是不是空
//如果函数为空,则Empty返回真 如果为真,!assert则报错
assert(!ListEmpty(phead));
//链表的尾就是phead的prev
LTNode* tail = phead->prev;
LTNode* tailprev = tail->prev;
free(tail);
//如果有只有两个节点 则会让头节点的指向回到初始状态
tailprev->next = phead;
phead->prev = tailprev;
}
10. 链表的头删
头删还是很简单的,尾删新建了两个指针来控制,这次我们新建一个指针来控制头删。
//头删
void ListPopFront(LTNode* phead)
{
assert(phead);
assert(!ListEmpty(phead));
//新的头节点
LTNode* NewHead = phead->next->next;
//释放旧的头结点
free(phead->next);
//将新的结点链接起来
phead->next = NewHead;
NewHead->prev = phead;
}
11. 删除pos位置之前的结点
那接下来我们来实现删除任意pos位置之前的值,实现了这个共能我们便可以在头删、尾删中复用。
//删除pos位置之前的结点
void ListErase(LTNode* pos)
{
assert(pos);
assert(!ListEmpty(pos));
LTNode* prev=pos->prev;
LTNode* next = pos->next;
prev->next = next;
next->prev = prev;
free(pos);
}
12. 查找链表的结点
这个就简单了,遍历就行了,不过多介绍,链表的修改这里也不写上了,修改直接复用查找的功能然后改data里的值就行了,这里也不介绍了。
//查找
LTNode* ListFind(LTNode* phead, LTDataType x)
{
LTNode* cur = phead->next;
while (cur !=phead)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
总结
带头双向循环链表的结构虽然复杂一些,但是它的功能实现却十分简单,插入、删除结点都只需要简单的几句代码,思路也是非常的清晰。带头双向循环链表弥补了单链表中的许多不足,是实际中会用到的数据结构。
本篇博客到此就结束了,如果感觉还不错的话,麻烦各位点个赞互关一下,我们共同进步。
后续会不定更新习题或者更新如何实现队栈和队列。