🦄个人主页:修修修也
🎏所属专栏:数据结构
⚙️操作环境:Visual Studio 2022
一.了解项目功能
在本次项目中我们的目标是实现一个带头双向循环链表:
该带头双向循环链表使用动态内存分配空间,可以用来存储任意数量的同类型数据.
带头双向循环链表结点(Node)需要包含三个要素:前指针域prev,数据域data,后指针域next.
结点(Node)逻辑结构图示如下:
带头双向循环链表提供的功能有:
- 带头双向循环链表的初始化.
- 带头双向循环链表的新节点创建.
- 带头双向循环链表元素的尾插.
- 带头双向循环链表元素的头插.
- 带头双向循环链表的元素位置查找.
- 带头双向循环链表的任意指定元素前插入.
- 带头双向循环链表的尾删.
- 带头双向循环链表的头删.
- 带头双向循环链表的任意指定元素删除.
- 带头双向循环链表打印.
- 带头双向循环链表的销毁.
二.项目功能演示
要编写一个带头双向循环链表项目,首先要明确我们想要达到的效果是什么样,下面我将用vs2022编译器来为大家演示一下带头双向循环链表程序运行时的样子:
三.逐步实现项目功能模块及其逻辑详解
通过第二部分对项目功能的介绍,我们已经对带头双向循环链表的功能有了大致的了解,虽然看似需要实现的功能很多,貌似一时间不知该如何下手,但我们可以分步分模块来分析这个项目的流程,最后再将各部分进行整合,所以大家不用担心,跟着我一步一步分析吧!
!!!注意,该部分的代码只是为了详细介绍某一部分的项目实现逻辑,故可能会删减一些与该部分不相关的代码以便大家理解,需要查看或拷贝完整详细代码的朋友可以移步本文第四部分。
1.实现单链表程序菜单
菜单部分的逻辑比较简单,就是利用C语言printf函数打印出这个菜单界面即可。但要注意菜单的标序要和后续switch...case语句的分支相应,以免导致后续执行语句错乱的问题.基础问题就不过多赘述了,代码如下:
该部分功能实现代码如下:
//菜单
void LTMenu()
{
printf("**********************************************\n");
printf("******请选择要进行的操作 ******\n");
printf("******1.双向带头循环链表尾插 ******\n");
printf("******2.双向带头循环链表头插 ******\n");
printf("******3.双向带头循环链表指定元素前插入 ******\n");
printf("******4.双向带头循环链表尾删 ******\n");
printf("******5.双向带头循环链表头删 ******\n");
printf("******6.双向带头循环链表指定元素删除 ******\n");
printf("******7.双向带头循环链表打印 ******\n");
printf("******0.退出双向带头循环链表程序 ******\n");
printf("**********************************************\n");
printf("请选择:>");
}
2.实现单链表程序功能可循环使用
由于我们要实现带头双向循环链表的功能可以反复使用的逻辑,且至少在一开始执行一次,因此我们选择do...while的循环语句来实现这一部分的逻辑.
该部分功能实现代码如下:
int main()
{
LTNode* plist = LTInit();//初始化带头双向循环链表
int swi = 0;//创建变量swi作为do...while循环的终止条件,以及switch语句的运行条件
do //使用do...while实现
{
LTMenu();
scanf("%d", &swi);
switch (swi)
{
case 0:
// 释放链表内存
LTDestroy(plist);
plist = NULL;
printf("您已退出程序:>\n");
break;
case 1:
printf("请输入要尾插的数据:>");
LTDataType pushback_data = 0;
scanf("%d", &pushback_data);
LTPushBack(plist, pushback_data);
printf("已成功插入:>\n");
break;
case 2:
printf("请输入要头插的数据:>");
LTDataType pushfront_data = 0;
scanf("%d", &pushfront_data);
LTPushFront(plist, pushfront_data);
printf("已成功插入:>\n");
break;
case 3:
printf("请输入要插入的数据:>");
LTDataType insert_data = 0;
scanf("%d", &insert_data);
printf("请输入要插入的位置上的数据:>");
LTDataType insert_posdata = 0;
scanf("%d", &insert_posdata);
LTNode* insert_pos = LTFind(plist, insert_posdata);
if (insert_pos != NULL)
{
LTInsert(insert_pos, insert_data);
printf("已成功在'%d'数据前插入'%d':>\n", insert_posdata, insert_data);
}
else
{
printf("该元素不存在,无法插入:<\n");
}
break;
case 4:
LTPopBack(plist);
printf("尾删成功:>\n");
break;
case 5:
LTPopFront(plist);
printf("头删成功:>\n");
break;
case 6:
printf("请输入要删除的数据:>");
LTDataType erase_data = 0;
scanf("%d", &erase_data);
LTNode* erase_pos = LTFind(plist, erase_data);
if (erase_pos == NULL)
{
printf("要删除的元素不存在:<\n");
}
else
{
LTErase(erase_pos);
printf("已成功删除:>\n");
}
break;
case 7:
printf("打印双向带头循环链表:>\n");
LTPrint(plist);
break;
default:
printf("输入错误,请重新输入\n");
break;
}
} while (swi);
return 0;
}
3.创建带头双向循环链表
创建带头双向循环链表成员的结构体应该包括:存储上一结点位置的前指针域prev,存储数据的数据域data,存储下一个结点位置的后指针域next.
图示如下:
因此我们创建ListNode结构体类型时应由一个数据成员类型及两个指向该结构体的结构体指针组成.
这里的第一行使用的typedef类定义的作用是方便我们后续在使用带头双向循环链表时对存储的数据类型做更改,比如后续我们的带头双向循环链表不想存储int类型数据了,就可以很方便的在这里对带头双向循环链表数据域的存储数据类型做更改.比如改成char类型,或者double类型,甚至改成任意自己构造的结构类型.
在之前的实战项目通讯录中,我们就创建过类似的自定义结构体,如下图:
综上,该部分代码如下:
typedef int LTDataType;
typedef struct ListNode
{
struct ListNode* next;
struct ListNode* prev;
LTDataType data;
}LTNode;
4.单链表的新节点创建
因为后续我们带头双向循环链表初始化,尾插,头插等插入操作时都需要先创建一个新结点,为了使代码的利用效率变高,我们不如将创建新节点这一操作封装成一个函数,后续当需要创建新节点时,直接调用该函数即可.
函数的参数需要接收新结点的数据域,至于新结点的指针域,在我们不清楚新结点的用途时,直接将其全部置为NULL即可.
该部分功能实现代码如下:
LTNode* BuyListNode(LTDataType x)
{
LTNode* node = (LTNode*)malloc(sizeof(LTNode));
if (node == NULL)
{
perror("malloc fail");
return NULL;
}
node->next = NULL;
node->prev = NULL;
node->data = x;
return node;
}
5.初始化带头双向循环链表
初始化带头双向循环链表部分和之前单链表中我们的处理方式不同,在无头单链表部分我们需要对链表的初始化的操作仅仅是创建一个指向NULL的头指针即可:
而在本次项目中,我们采用的是带头结点指针链表,因此在初始化的时候我们需要开辟一个头结点,并将它的prev指针和next指针都指向它自己:
该部分功能实现代码如下:
LTNode* LTInit()
{
LTNode* phead = BuyListNode(-1);//头结点数据域设为-1方便辨识
phead->next = phead;
phead->prev = phead;
return phead;
}
6.带头双向循环链表元素的尾插
尾插示意图:
如图,我们在尾插时首先要找到原链表的尾,即head->prev,然后我们需要改变四个指针的指向关系:
- 使旧尾的next连接上newnode
- 使newnode的prev连接上旧尾
- 使head的prev连接上newnode
- 使newnode的next连接上head
然后尾插操作就完成了,该部分功能实现代码如下:
void LTPushBack(LTNode* phead, LTDataType x)
{
assert(phead);//带头结点的头指针必不为空.
//单链表需要二级指针的原因是要改变头指针的指向,要改变指针只能用二级指针
//要改变结构体的成员就不需要二级指针,只需要改变它存储的内容即可
LTNode* newnode = BuyListNode(x);
LTNode* tail = phead->prev;
//链接新尾和旧尾
tail->next = newnode;
newnode->prev = tail;
phead->prev = newnode;
newnode->next = phead;
}
7.带头双向循环链表元素的头插
头插示意图:
如图,我们在头插时首先需要找到旧头,即head->next,然后需要改变四个指针的指向关系:
- 使newnode的next连接上旧头
- 使旧头的prev连接上newnode
- 使head的next连接上newnode
- 使newnode的prev连接上head
注意!在这部分,我们只使用head和nownode两个指针的情况下,一定要先让newnode和旧头链接起来,即图中的1操作,然后再将head和newnode连接起来,即图中的4操作.
因为,如果我们先更改了head的next指针的指向,后续想要找到旧头就只能再循环遍历一遍链表了,这样会非常麻烦.
这四个指针的顺序是可以更改的,但必须要保证第1步操作在第4步操作的前面,只要满足这个条件,剩下的顺序都是可以随意更改的.
然后头插操作就完成了,该部分功能实现代码如下:
void LTPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = BuyListNode(x);
//双指针:先连后再连前
newnode->next = phead->next;
phead->next->prev = newnode;
phead->next = newnode;
newnode->prev = phead;
//三指针顺序可以随便换
}
8.带头双向循环链表的元素位置查找
因为后续我们要使用的带头双向循环链表按位插入和按位删除都需要知道用户传入的链表元素在链表中的位置在哪,因此我们把查找链表元素位置的操作封装成一个单独的函数,后续需要查找某一链表元素的位置直接调用这个函数就行.
函数的参数应该接收待查找的结点的数据域,以便我们在遍历链表的过程中能够找到它.
函数的返回值是链表结点指针型(LTNode*),这样可以方便我们在找到要查找的指针后直接对齐进行相关操作,而不需要再在函数外再遍历一遍链表了.
对于带头双向循环链表的遍历,我们要特别注意,因为是循环链表,所以我们一般的遍历方式都是从头结点的后一个结点开始,直到走到头结点的时候才算结束.
该部分功能实现代码如下:
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; //没找到,返回空
}
9.带头双向循环链表的任意指定元素前插入
因为我们只要知道某一结点的位置,就可以通过访问它的prev和next指针访问它的上一个或下一个结点,所以在指定元素前插入函数中我们只需要两个参数,一个是指定元素的位置,一个是新结点的数据域的数据值.
任意指定位置前插入示意图:
如图,我们这次创建一个指针p来记录下pos的前一个结点的位置,即使用三指针的方式来插入新数据,这时四个指针的修改顺序就可以任意书写了,我们可以先更改p指针的next指针了,我们改变下面四个指针的指向关系即可:
- p的next连接上newnode
- newnode的prev连接上p
- newnode的next连接上pos
- pos的prev连接上newnode
更改完我们的插入操作也就完成了,该部分代码实现如下:
//在pos位置之前插入一个值
void LTInsert(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* newnode = BuyListNode(x);
LTNode* p = pos->prev;
p->next = newnode;
newnode->prev = p;
newnode->next = pos;
pos->prev = newnode;
//尾插pos传head
//头插pos传head->next
}
当我们拥有了任意位置前插入函数后, 其实就不需要再单独写尾插或头插函数了,当我们想要尾插时我们只需要给LTInsert函数传入head的地址就可以了,而当我们想要头插时我们只需要给LTInsert函数传入head->next的地址就可以了.
所以尾插函数可以直接写成:
void LTPushBack(LTNode* phead, LTDataType x) { LTInsert(phead,x); }
头插函数可以直接写成:
void LTPushFront(LTNode* phead, LTDataType x) { LTInsert(phead->next,x); }
10.带头双向循环链表的判空
因为我们在删除链表元素前都需要先判断一下链表当前是不是为空,如果链表为空那就不要再删了,因为尾删,头删,指定元素删除都需要判空操作,所以我们不如封装一个函数,在调用时判断当前链表是否为空,如果为空返回假,不为空返回真.
该部分功能实现代码如下:
bool LTEmpty(LTNode* phead)
{
assert(phead);
return phead->next != phead; //简洁写法
//完整写法,即phead的next不等于phead返回真(即链表不为空)
//phead的next等于phead返回假(即链表为空)
/*
if (phead->next != phead)
{
return true;
}
else
{
return false;
}
*/
}
11.带头双向循环链表的尾删
尾删示意图:
如图,我们尾删前判断一下链表不为空的话就要找到尾结点,然后就可以开始尾删了.我们先创建一个指针tail记录下尾结点的位置,再创建一个指针tailPrev记录下尾结点的前一个结点(即新尾)的位置.
在删除时我们只需要改变两个指针的指向就可以了,即:
- 使phead的prev指针指向新尾(tailPrev)
- 使新尾(tailPrev)的next指针指向phead
更改完我们的尾删操作也就完成了,该部分代码实现如下:
void LTPopBack(LTNode* phead)
{
assert(phead);
assert(LTEmpty(phead));
LTNode* tail = phead->prev;
LTNode* tailPrev = tail->prev;
phead->prev = tailPrev;
tailPrev->next = phead;
free(tail);
tail = NULL;
}
12.带头双向循环链表的头删
头删示意图:
如图,我们头删前判断一下链表不为空的话就要找到首结点,然后就可以开始头删了.我们先创建一个指针tail记录下首结点的位置.然后就可以开始删除了.
在删除时我们只需要改变两个指针的指向就可以了,即:
- 使phead的next指针指向新首(tail->next)
- 使新首(tail->next)的prev指针指向phead
更改完我们的头删操作也就完成了,该部分代码实现如下:
void LTPopFront(LTNode* phead)
{
assert(phead);
assert(LTEmpty(phead));
LTNode* tail = phead->next;
phead->next = tail->next;
tail->next->prev = phead;
free(tail);
tail = NULL;
}
13.带头双向循环链表的任意指定元素删除
删除任意指定元素示意图:
如图,我们任意位置删前判断一下链表不为空的话就要找到待删结点的前驱结点和后继结点,然后就可以开始删除了.我们创建一个指针p记录下待删结点的前驱结点的位置.再创建一个指针n记录下待删结点的后继结点的位置,然后就可以开始删除了.
在删除时我们只需要改变两个指针的指向就可以了,即:
- 使前驱结点p的next指针指向后继结点n
- 使后继结点n的prev指针指向前驱结点p
更改完我们的删除操作也就完成了,该部分代码实现如下:
//删除pos位置的值
void LTErase(LTNode* pos)
{
assert(pos);
LTNode* p = pos->prev;
LTNode* n = pos->next;
n->prev = p;
p->next = n;
free(pos);
pos = NULL;
//头删传phead->next
//尾删传phead->prev
}
当我们拥有了任意位置前删除函数后, 其实就不需要再单独写尾删或头删函数了,当我们想要尾删时我们只需要给LTErase函数传入phead->prev的地址就可以了,而当我们想要头删时我们只需要给LTErase函数传入phead->next的地址就可以了.
所以尾删函数可以直接写成:
void LTPopBack(LTNode* phead, LTDataType x) { LTErase(phead->prev,x); }
头删函数可以直接写成:
void LTPopFront(LTNode* phead, LTDataType x) { LTErase(phead->next,x); }
14.带头双向循环链表打印
在打印部分我们要注意的是:循环链表和单链表的主要差异就在于循环遍历时的判断条件上,原来是判断p->next是否为NULL,现在则是p->next不等于头结点,则循环遍历未结束.
了解了这点后,带头双向循环链表的打印逻辑很简单,顺着头指针的后一个结点向后循环遍历打印整个链表结点的数据域即可,当遍历指针再次走到head结点时,则代表已经遍历打印完链表的所有元素,这时跳出循环即可.
该部分功能实现代码如下:
void LTPrint(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
printf("<->Head<->");
while (cur != phead)
{
printf("%d<->",cur->data);
cur = cur->next;
}
printf("\n");
}
15.带头双向循环链表的销毁
当我们使用完双向带头循环链表想要退出程序时,就应该将之前动态开辟的内存释放掉,还给操作系统.即销毁双向带头循环链表操作.
我们使用free()函数将前面开辟的结点的内存逐一释放,释放完将头指针置为空即可.
要注意,这里循环链表的遍历从头结点的后一个结点开始,然后循环遍历时的判断条件p->next不等于头结点,则循环遍历未结束.
该部分功能实现代码如下:
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"
int main()
{
LTNode* plist = LTInit();
int swi = 0;//创建变量swi作为do...while循环的终止条件,以及switch语句的运行条件
do //使用do...while实现
{
LTMenu();
scanf("%d", &swi);
switch (swi)
{
case 0:
// 释放链表内存
LTDestroy(plist);
plist = NULL;
printf("您已退出程序:>\n");
break;
case 1:
printf("请输入要尾插的数据:>");
LTDataType pushback_data = 0;
scanf("%d", &pushback_data);
LTPushBack(plist, pushback_data);
printf("已成功插入:>\n");
break;
case 2:
printf("请输入要头插的数据:>");
LTDataType pushfront_data = 0;
scanf("%d", &pushfront_data);
LTPushFront(plist, pushfront_data);
printf("已成功插入:>\n");
break;
case 3:
printf("请输入要插入的数据:>");
LTDataType insert_data = 0;
scanf("%d", &insert_data);
printf("请输入要插入的位置上的数据:>");
LTDataType insert_posdata = 0;
scanf("%d", &insert_posdata);
LTNode* insert_pos = LTFind(plist, insert_posdata);
if (insert_pos != NULL)
{
LTInsert(insert_pos, insert_data);
printf("已成功在'%d'数据前插入'%d':>\n", insert_posdata, insert_data);
}
else
{
printf("该元素不存在,无法插入:<\n");
}
break;
case 4:
LTPopBack(plist);
printf("尾删成功:>\n");
break;
case 5:
LTPopFront(plist);
printf("头删成功:>\n");
break;
case 6:
printf("请输入要删除的数据:>");
LTDataType erase_data = 0;
scanf("%d", &erase_data);
LTNode* erase_pos = LTFind(plist, erase_data);
if (erase_pos == NULL)
{
printf("要删除的元素不存在:<\n");
}
else
{
LTErase(erase_pos);
printf("已成功删除:>\n");
}
break;
case 7:
printf("打印双向带头循环链表:>\n");
LTPrint(plist);
break;
default:
printf("输入错误,请重新输入\n");
break;
}
} while (swi);
return 0;
}
List.c文件
#include"List.h"
//菜单
void LTMenu()
{
printf("**********************************************\n");
printf("******请选择要进行的操作 ******\n");
printf("******1.双向带头循环链表尾插 ******\n");
printf("******2.双向带头循环链表头插 ******\n");
printf("******3.双向带头循环链表指定元素前插入 ******\n");
printf("******4.双向带头循环链表尾删 ******\n");
printf("******5.双向带头循环链表头删 ******\n");
printf("******6.双向带头循环链表指定元素删除 ******\n");
printf("******7.双向带头循环链表打印 ******\n");
printf("******0.退出双向带头循环链表程序 ******\n");
printf("**********************************************\n");
printf("请选择:>");
}
LTNode* LTInit()
{
LTNode* phead = BuyListNode(-1);
phead->next = phead;
phead->prev = phead;
return phead;
}
void LTDestroy(LTNode*phead)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
LTNode* next = cur->next;
free(cur);
cur = next;
}
free(phead);
//让调用的人置空
}
LTNode* BuyListNode(LTDataType x)
{
LTNode* node = (LTNode*)malloc(sizeof(LTNode));
if (node == NULL)
{
perror("malloc fail");
return NULL;
}
node->next = NULL;
node->prev = NULL;
node->data = x;
return node;
}
void LTPrint(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
printf("<->Head<->");
while (cur != phead)
{
printf("%d<->",cur->data);
cur = cur->next;
}
printf("\n");
}
bool LTEmpty(LTNode* phead)
{
assert(phead);
return phead->next != phead;
/*
if (phead->next != phead)
{
return true;
}
else
{
return false;
}
*/
}
void LTPushBack(LTNode* phead, LTDataType x)
{
assert(phead);//带头结点的头指针必不为空.
//单链表需要二级指针的原因是要改变头指针的指向,要改变指针只能用二级指针
//要改变结构体的成员就不需要二级指针,只需要改变它存储的内容即可
LTNode* newnode = BuyListNode(x);
LTNode* tail = phead->prev;
//链接新尾和旧尾
tail->next = newnode;
newnode->prev = tail;
phead->prev = newnode;
newnode->next = phead;
}
void LTPopBack(LTNode* phead)
{
assert(phead);
assert(LTEmpty(phead));
LTNode* tail = phead->prev;
LTNode* tailPrev = tail->prev;
phead->prev = tailPrev;
tailPrev->next = phead;
free(tail);
tail = NULL;
}
void LTPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = BuyListNode(x);
//双指针:先连后再连前
newnode->next = phead->next;
phead->next->prev = newnode;
phead->next = newnode;
newnode->prev = phead;
//三指针顺序随便换
}
void LTPopFront(LTNode* phead)
{
assert(phead);
assert(LTEmpty(phead));
LTNode* tail = phead->next;
phead->next = tail->next;
tail->next->prev = phead;
free(tail);
tail = 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;
}
//在pos位置之前插入一个值
void LTInsert(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* newnode = BuyListNode(x);
LTNode* p = pos->prev;
p->next = newnode;
newnode->prev = p;
newnode->next = pos;
pos->prev = newnode;
//尾插pos传head
//头插pos传head->next
}
//删除pos位置的值
void LTErase(LTNode* pos)
{
assert(pos);
LTNode* p = pos->prev;
LTNode* n = pos->next;
n->prev = p;
p->next = n;
free(pos);
pos = NULL;
//头删传phead->next
//尾删传phead->prev
}
List.h文件
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdlib.h>
#include<assert.h>
#include<stdio.h>
#include<stdbool.h>
typedef int LTDataType;
typedef struct ListNode
{
struct ListNode* next;
struct ListNode* prev;
LTDataType data;
}LTNode;
void LTMenu();
LTNode* LTInit();
LTNode* BuyListNode(LTDataType x);
LTNode* LTFind(LTNode* phead, LTDataType x);
void LTDestroy(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);
void LTInsert(LTNode* pos,LTDataType x);
void LTErase(LTNode* pos);
结语
希望这篇双向带头循环表的实现详解能对大家有所帮助,欢迎大佬们留言或私信与我交流.
学海漫浩浩,我亦苦作舟!关注我,大家一起学习,一起进步!
相关文章推荐
【数据结构】什么是线性表?
【数据结构】线性表的链式存储结构
【C语言】free()函数详解(动态内存释放函数)
【C语言】malloc()函数详解(动态内存开辟函数)
【数据结构】链表的八种形态
【数据结构】C语言实现单链表万字详解(附完整运行代码)【实用编程技巧】不想改bug?初学者必须学会使用的报错函数assert!(断言函数详解)
数据结构线性篇思维导图: