代码位置: test-c-2024: 对C语言习题代码的练习 (gitee.com)
双向循环链表的实现所需要的基本操作主要有初始化,销毁,判空,打印,插入,删除,查找以及修改。而插入和删除又可以分为 头插 、 头删, 尾插 、 尾删。
因此,我们可以通过三个文件来实现一个双向循环链表。其中:
头文件-List.h:主要用来实现所需函数的声明以及include的调用。
源文件-List.c:主要用来实现所需函数的定义。
源文件-Test.c:主要用于检测和调用各个函数。
头文件-List.h
对于 头文件-List.h 我们主要目的是在该文件中实现 初始化,销毁,判空,打印,插入,删除,查找及修改 等操作的函数声明以及链表结构体的定义。插入和删除又可以分为 头插 、 头删, 尾插 、 尾删。除此之外,我们还需要调用各个函数定义所需要的头文件。
代码如下:
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>
typedef int LTDataType; //自己重命名一下数据类型,便于后继类型的改变
//如你所需要的链表内容为字符串形式则只需将int改成char即可
typedef struct ListNode
{
struct ListNode* prev;
struct ListNode* next;
LTDataType data;
}LTNode;
LTNode* LTInit(); //初始化
void LTDestory(LTNode* phead); //销毁
void LTPrint(LTNode* phead); //打印
bool LTEmpty(LTNode* phead);
void LTPushBack(LTNode* phead, LTDataType x); //尾插
void LTPopBack(LTNode* phead); //尾删
void LTPushFront(LTNode* phead, LTDataType x); //头插
void LTPopFront(LTNode* phead); //头删
LTNode* LTFind(LTNode* phead, LTDataType x); //查找
void LTInsert(LTNode* pos, LTDataType x); //插入
void LTErase(LTNode* pos); //删除
void LTRevamp(LTNode* pos, LTDataType x); //修改
源文件-List.c
对于 源文件-List.c 我们需要对 头文件-List.h 自己声明的函数进行定义如:实现初始化,销毁,判空,打印,插入,删除,查找及修改等函数的定义。
开辟空间-BuyListNode:
开辟空间函数主要用于为后继的初始化及插入做铺垫。开辟空间函数这里需要返回所开辟的结点用于后继的插入操作。这里注意:我们需要判断一下开辟空间是否为成功,若失败则为空,这里我们可以通过perror()函数来提示空间开辟失败的原因。
代码如下:
LTNode* BuyListNode(LTDataType x)
{
LTNode* node = (LTNode*)malloc(sizeof(LTNode));
if (node == NULL)
{
perror("malloc BuyListNode");
exit(-1);
}
node->next = NULL;
node->prev = NULL;
node->data = x;
return node;
}
初始化-LTInit():
双向循环链表需要初始化一个哨兵位来构成链表的循环。其中,要注意双向循环链表若只有哨兵位本身需要自成循环。这里初始化需要返回哨兵位节点。
代码如下:
LTNode* LTInit() //初始化
{
//双向循环链表需要一个哨兵位来实现循环操作
LTNode* phead = BuyListNode(-1);
phead->next = phead;
phead->prev = phead;
return phead;
}
销毁及释放-LTDestory:
这里需要注意空间的开辟一定要销毁和释放(为了避免空间泄露)。函数里传入头节点我们判断一下这个节点是否为空,若为空则会出现野指针情况,为了避免这种情况我们可以在判断为空指针的时候返回。当然也可以采用断言的形式,这里我采用了断言的形式,若为传入的为空指针则会直接报错并提示在哪一处断言出现了空指针,这样的话便于修改。
代码如下:
void LTDestory(LTNode* phead) //销毁
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
LTNode* next = cur->next;
free(cur);
cur = NULL;
cur = next;
}
free(phead);
phead = NULL;
}
判空-LTEmpty:
这里判空函数是用于在后继的头删,尾删函数中起断言作用。若在链表中只含哨兵位节点则说明要在进行删除操作则会出现错误。
代码如下:
bool LTEmpty(LTNode* phead) //判空
{
assert(phead);
return phead->next == phead;
}
打印-LTPrint:
在打印函数中,我们也需要断言一下传入的哨兵位节点是否出错。同时我们还可以修饰一下打印的形式,可以用<=>来体现一下链表的双向循环。
代码如下:
void LTPrint(LTNode* phead) //打印
{
assert(phead);
printf("<=head=>");
LTNode* cur = phead->next;
while (cur != phead)
{
printf("%d<=>", cur->data);
cur = cur->next;
}
printf("\n");
}
头插-LTPushFront:
头插法这里我们也需要断言,接着开辟插入值的空间,然后进行插入。
代码如下:
void LTPushFront(LTNode* phead, LTDataType x) //头插
{
assert(phead);
LTNode* newnode = BuyListNode(x);
newnode->next = phead->next;
phead->next->prev = newnode;
newnode->prev = phead;
phead->next = newnode;
}
头删-LTPopFront:
头删这里需要断言传入的哨兵位是否为空, 还需断言一下链表是否为空即链表只有哨兵位节点本身此时若继续删除则会出现错误故需要断言一下,注意为删后需要释放一下删除的节点。
代码如下:
void LTPopFront(LTNode* phead) //头删
{
assert(phead);
assert(!LTEmpty(phead));
LTNode* head = phead->next;
phead->next = head->next;
head->next->prev = phead;
free(head);
}
尾插-LTPushBack:
这里同头插一样,需要断言等操作。注意尾插的话因为是循环链表所以哨兵位节点phead的前继节点即为尾节点,故只需对哨兵位的前继节点进行插入操作。
代码如下:
void LTPushBack(LTNode* phead, LTDataType x) //尾插
{
assert(phead);
LTNode * newnode = BuyListNode(x);
newnode->prev = phead->prev;
phead->prev->next = newnode;
newnode->next = phead;
phead->prev = newnode;
}
尾删-LTPopBack:
尾删也同头删一样需要断言哨兵位节点以及断言链表是否为空。这里也是对哨兵位的前继节点即尾节点进行删除。
代码如下:
void LTPopBack(LTNode* phead) //尾删
{
assert(phead);
assert(!LTEmpty(phead));
LTNode* tail = phead->prev;
phead->prev = tail->prev;
tail->prev->next = phead;
free(tail);
}
插入-LTInsert:
这里插入操作需要给定一个插入位置,然后进行插入。同时也需注意插入位置不能为空,这里我们也需断言一下插入位置。
代码如下:
void LTInsert(LTNode* pos, LTDataType x) //插入
{
assert(pos);
LTNode* newnode = BuyListNode(x);
LTNode* prev = pos->prev;
prev->next = newnode;
newnode->next = pos;
newnode->prev = prev;
pos->prev = newnode;
}
删除-LTErase:
这里我们只需对删除位置进行删除操作的同时并释放删除的节点避免空间的泄露,同时也需要断言一下删除的节点。
代码如下:
void LTErase(LTNode* pos) //删除
{
assert(pos);
LTNode* p = pos->prev;
LTNode* q = pos->next;
p->next = q;
q->prev = p;
free(pos);
pos = NULL;
}
查找-LTFind:
这里通过循环一遍链表来查找所查找的值是否存在,若存在则返回该节点,若不存在则返回NULL,同理这里也需断言哨兵位。(通常与插入,删除,修改连用)
代码如下:
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;
}
修改-LTRevamp:
修改这里的话通常与查找连用,修改所查找出需要修改位置的值。
代码如下:
void LTRevamp(LTNode* pos, LTDataType x) //修改
{
assert(pos);
pos->data = x;
}
源文件-Test.c
对于 源文件-Test.c 我们需对各个操作进行检查。
头插,尾插检测:
头删,尾删检测:
插入,删除检测:
查找及插入检测:
查找及修改检测:
结尾:
上述内容,即是我个人对简单双向循环链表基本操作的实现和见解。若有大佬发现哪里有问题可以私信或评论指教一下我这个萌新。感谢各位友友们的点赞,收藏与支持,让我们一起进步吧!