浅尝带头双向循环链表(C语言数据结构)
*文章目录:
一.双向循环链表的介绍及优势
二.代码实现双向循环链表
2.1 文件层次结构
2.2 基本结构体样式
2.3 函数功能介绍
2.4 函数功能实现
2.5 具体实测运行
三.总结
1.双向链表的介绍
——什么是带头双向循环链表
今天提及的带头循环双向链表本质上属于链表的变型,结构上仍然继承了链表非顺序的存储结构、通过指针链接次序等特点。
然而 实际上 ,链表有多种实现结构,如:单链表、双向链表、是否带头或者是否循环的链表等等
而今天的主角——带头循环双向链表则是集合了众多特性和“宠爱”于一身的超级plus版的链表结构。此种结构最为复杂,一般用在单独存储数据,实际中使用的链式存储结构都会是带头循环双向链表。
另外,虽然此种结构复杂,但是当用代码实现以后会发现此种结构反而会带来很多优势使得目标变得简单。
2.代码实现
2.1文件层次结构
——在具体实现中 ,我们准备把结构体、相关库以及函数声明包含在LTList.h头文件中,把各个函数的具体实现包含在LTList.c文件中,把实现函数后具体的测试test.c中
2.2基本结构体样式
——既然实现自定义类型的就必须使用结构体创建一个双向链表节点的结构体和数据类型,其中按照设计需求需要包含该节点的值(data)、数据指向下一个的指针(next)以及指向前一个的指针(prev) 并且该结构体使用LTNode来简称,代指ListNode结构体本身。
typedef int LTDataType;
typedef struct ListNode
{
struct ListNode* next;
struct ListNode* prev;
LTDataType data;
}LTNode;
2.3函数功能介绍
——众所周知,大部分数据结构的实现必然逃脱不了“增删查改”四部曲,那么除此之外,基于C语言的限制,还需手动开辟内存空间(BuyListNode)、销毁内存空间(LTDestroy)、初始化链表(LTInit)以及打印链表**(LTPrint)等等。
LTNode* BuyListNode(LTDataType x); //开辟内存
LTNode* ListInit(); //初始化
void LTPrint(LTNode* phead);//打印
void LTDestroy(LTNode* phead); //销毁
void LTInsert(LTNode*pos,LTDataType x);// 任意位置插
void LTErase(LTNode* pos);// 任意位置删
LTNode* ListFind(LTNode* phead,LTDataType x);//查找
2.4函数功能实现
——开辟内存函数(BuyListNode)
首先需要用malloc函数给链表的一个个节点开辟内存空间。
开辟完空间后节点的值(data)、下一个节点(next)、上一个节点(prev)默认给空。
**温馨提示:**malloc开辟的节点如果不判是否为空的话,VS2019 和 VS2022 两个版本可能会报错。
LTNode* BuyListNode(LTDataType x)
{
LTNode* node = (LTNode*)malloc(sizeof(LTNode)); //开辟节点内存空间
if (node == NULL) //判断开辟失败
{
perror("malloc fail");
exit(-1); // 若失败 则异常退出
}
node->data = x; //否则 给节点初始化
node->next = NULL;
node->prev = NULL;
return node;
}
——初始化函数(LTInit)
链表默认有一个头结点,因为当头结点存在时,表示该链表的存在(因为该链表已经可以通过头结点指针被找到)其next结点和prev结点也都默认为空。
LTNode* ListInit()
{
LTNode* phead = BuyListNode(-1); //创建头结点
phead->next = phead;
phead->prev = phead;
return phead;
}
——插入(任意位置)函数(LTInsert)
首先需要断言(需要满足pos位置不为空),其次找出pos位置节点以及前一个节点 Prev,然后把新节点的Next给prev节点
新节点的prev给pos->prev 即可实现链接
**温馨提示:**应该是默认在pos位置之前插入,若是在之后插入则可能为空
void LTInsert(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;
}
具体图解:
——删除(任意位置)函数(LTErase)
首先仍然需要断言,标记定义一个pos位置的前一个(Prev)和后一个(Next)节点 ,当把pos位置节点删除后free释放空间,再将Prev和Next结点链接即可。
void LTErase(LTNode* pos)
{
assert(pos);
LTNode* prev = pos->prev;
LTNode* next = pos->next;
free(pos);
prev->next = next;
next->prev = prev;
——打印输出函数(LTPrint)
断言不为空后,定义一个cur指针从头往后走依次遍历打印输出即可;
void LTPrint(LTNode* phead)
{
assert(phead);
LTNode* cur = phead;
while (cur != phead)
{
printf("%d ", cur->data);
}
printf("\n");
}
——查找函数(ListFind)
定义cur指针遍历链表 发现data值相等返回cur即可,否则返回空。
LTNode* ListFind(LTNode* phead, LTDataType x)
{
LTNode* cur = phead;
while (cur != phead)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
——销毁函数(LTDestroy)
断言完后,需要cur指针遍历链表每个位置free掉当前节点,最后再释放掉phead头指针空间即可。
**温馨提示:**用malloc函数申请的内存空间一定要free归还,俗称“有借有还"。
void LTDestroy(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
LTNode* next = cur->next;
free(cur);
cur = next;
}
free(phead);
}
2.5具体实测运行
写完功能函数后我们在test.c文件里面调用函数查看测试结果
void TestList1()
{
LTNode* phead = ListInit(); //初始化
LTInesrt(phead,1);//插入数据
LTInesrt(phead,2);
LTInesrt(phead,3);
LTInesrt(phead,4);
LTInesrt(phead,5);
LTPrint(phead); //打印
}
int main()
{
TestList1();
return 0;
}
测试结果成功!
值得注意的是有些地方由于特殊情境可能需要用到尾插尾删,头插头删等函数,这些函数都可以用任意插,任意删来改编完成,笔者就不在此一一赘述。
3.总结
带头双向循环链表虽然实现起来有一定难度,可是当实现之后却又有很多优势之处,比如相较于之前迂腐的结构节省了大量的时间或者空间。 亦或者以后面试官要求写出链表结构,突然写出一个带头双向循环链表可能会让面试官眼前一亮。最后,笔者若有出错之地,还望批评指正!