读者可以先阅读这一篇:数据结构——单链表的增加、删除、查找、修改,详细解析_昵称就是昵称吧的博客-CSDN博客,可以更好的理解带头双向循环链表。
目录
一、带头双向循环链表的处理和介绍
1、带头双向循环链表的概念
链表的概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
带头双向循环链表是链表中最复杂的,因为它具有了链表存在的结构中所有的结构:带头,双向和循环。
1、带头和不带头:带头就是链表带有头节点,这个头节点只具有哨兵位的作用,意思就是它不随着链表的改变而改变,定义之后就固定不变了,并且头节点里面不存放数据;不带头就是链表没有头节点。
2、双向和单项:双向就是一个节点,既存上一个节点的地址,也存下一个节点的地址;单向就是一个节点,只存下一个节点的地址。
3、循环和非循环:循环就是这个链表的最后一个节点里的指针存放的地址,如果链表带头,就存放的头节点的地址;如果链表不带头,就存放第一个节点的地址;非循环就是这个链表的最后一个节点存放的地址是空的。
2、带头双向循环链表的结构
头节点里面是不存放数据的,只具有哨兵位的作用,各个节点之间通过节点里面的指针进行链接。单向链表里面,每个节点里只有一个指针,因为只需要指向下一个节点;但是双向链表中,每个节点里有两个指针,一个指向上一个节点,一个指向下一个节点。
3、节点的处理
因为双向链表里面每个节点,都会存储一个数据和两个指针prev和next,指针prev指向上一个节点,指针next指向下一个节点,所以用结构体来表示节点。
typedef int ListDataType;//类型名重定义
typedef struct ListNode
{
struct ListNode* prev;
struct ListNode* next;
ListDataType data;
}ListNode;
4、头节点的处理
在这里,我们用指针phead/plist表示头节点的地址,因为是带有头双向循环链表,所以定义头节点的时候,其节点里的指针prev和next都必须指向自己。
phead->prev = phead;//头节点不存放数据
phead->next = phead;
5、节点(结构体)内存空间的开辟
因为每个节点都是结构体,为了不会导致内存空间的浪费,需要用一个节点,就开辟一个节点。所以用动态内存开辟函数malloc即可,每次开辟一个结构体大小的内存空间。但是要注意,每个节点创建的时候,其里面的指针prev和next都要先置为空指针,方便后面的修改。
ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
newnode->next = NULL;//因为是新节点,都先初始化置为空指针
newnode->prev = NULL;
newnode->data = x;//将想要的数据放入进去
二、链表的主体框架
1、整体框架
int main()
{
int input = 0;
do
{
meun();
printf("请输入想要进行的操作:");
scanf("%d", &input);
switch (input)
{
case 1:
{
printf("从尾部插入节点:\n");
testList1();//验证尾插函数
printf("\n");
break;
}
case 2:
{
printf("从头部插入节点:\n");
testList2();//验证头插函数
printf("\n");
break;
}
case 3:
{
printf("从尾部删除节点:\n");
testList3();//验证尾删函数
printf("\n");
break;
}
case 4:
{
printf("从头部删除节点:\n");
testList4();//验证头删函数
printf("\n");
break;
}
case 5:
{
printf("给数据找其节点:\n");
testList5();//验证查找节点
printf("\n");
break;
}
case 6:
{
printf("修改数据:\n");
testList6();//验证修改数据
printf("\n");
break;
}
case 7:
{
printf("插入节点:\n");
testList7();//验证插入节点
printf("\n");
break;
}
case 8:
{
printf("删除节点:\n");
testList8();//验证删除节点
printf("\n");
break;
}
case 0:
{
printf("退出操作\n");
break;
}
default:
{
printf("选择错误,请重新选择操作\n");
break;
}
}
} while (input);
return 0;
}
用do......while循环语句和swith分支语句来进入相应的功能。
2、主菜单
//主菜单
void meun()
{
printf("*****************************************************\n");
printf("* 1、尾插 2、头插 *\n");
printf("* 3、尾删 4、头删 *\n");
printf("* 5、查找 6、修改 *\n");
printf("* 7、任意数据前插入 8、删除任意数据 *\n");
printf("* 0、退出 *\n");
printf("*****************************************************\n");
}
三、功能的实现
1、创建一个新节点的函数
//创建一个新节点的函数
ListNode* BuyListNode(ListDataType x)
{
ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
newnode->next = NULL;//因为是新节点,都先初始化置为空指针
newnode->prev = NULL;
newnode->data = x;//将想要的数据放入进去
return newnode;
}
解释请看代码里面的注解,非常详细。
2、初始化创建头节点的函数
//创建一个新节点的函数
ListNode* BuyListNode(ListDataType x)
{
ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
newnode->next = NULL;//因为是新节点,都先初始化置为空指针
newnode->prev = NULL;
newnode->data = x;//将想要的数据放入进去
return newnode;
}
//初始化创建头节点
ListNode* ListInit()
{
ListNode* phead = BuyListNode(0);//这里给的数据0是无用的,只是方便使用创建新节点函数
phead->prev = phead;//头节点里date不把数据0放入(因为无用),并且其内的指针prev和next都指向自己
phead->next = phead;
return phead;
}
解释请看代码里面的注解,非常详细。
3、打印节点数据的函数
//打印节点的数据
void ListPrint(ListNode* phead)
{
assert(phead);//断言函数:防止忘记初始化创建头节点
ListNode* cur = phead->next;
printf("Phead <-> ");
while (cur != phead)//遍历一遍:循环最后一个节点里的指针next,next存储的是头节点phead的地址
{
printf("%d <-> ", cur->data);
cur = cur->next;
}
printf("phead\n");
}
解释请看代码里面的注解,非常详细。
4、尾插和头插增加节点的函数
4.1 从尾部插入节点的函数
//尾插函数
void ListPushBack(ListNode* phead,ListDataType x)
{
assert(phead);//断言函数:防止忘记初始化创建头节点
ListNode* tail = phead->prev;//尾插前,通过头节点找到最后一个节点tail
ListNode* newnode = BuyListNode(x);//创建需要插入的新节点
tail->next = newnode;//使节点phead、tail、newnode里的prev或next指向对应的节点
newnode->prev = tail;
newnode->next = phead;
phead->prev = newnode;
}
解释请看图片和代码里面的注解,非常详细。 当链表里面没有节点,只有头节点的时候,上面代码依旧成立。
4.2 从头部插入节点的函数
//头插函数
void ListPushFront(ListNode* phead,ListDataType x)
{
assert(phead);//断言函数:防止忘记初始化创建头节点
ListNode* first = phead->next;//头插前,通过头节点找到第一个节点first
ListNode* newnode = BuyListNode(x);//创建需要插入的新节点
newnode->next = first;//使节点phead、first、newnode里的prev或next指向对应的节点
first->prev = newnode;
phead->next = newnode;
newnode->prev = phead;
}
解释请看图片和代码里面的注解,非常详细。 当链表里面没有节点,只有头节点的时候,上面代码依旧成立。
5、尾删和头删减少节点的函数
5.1 从尾部删除节点的函数
//尾删函数
void ListPopBack(ListNode* phead)
{
assert(phead);//断言函数:防止忘记初始化创建头节点
if (phead->next != phead)//防止链表里一个节点都没有,就不需要删除
{
ListNode* tail = phead->prev;//通过头节点phead找到最后一个节点tail
ListNode* prev = tail->prev;//再通过最后一个节点tail找到其前面的节点prev
free(tail);//释放最后一个节点的空间
tail == NULL;
prev->next = phead;//使节点phead、prev里的指针prev或next指向对应的节点
phead->prev = prev;
}
}
解释请看图片和代码里面的注解,非常详细。 当链表里面只有一个节点时候,上面代码依旧成立。
5.2 从头部删除节点的函数
//头删函数
void ListPopFront(ListNode* phead)
{
assert(phead);//断言函数:防止忘记初始化创建头节点
if (phead->next != phead)//防止链表里一个节点都没有,就不需要删除
{
ListNode* first = phead->next;//通过头节点phead找到第一个节点first
ListNode* second = first->next;//再通过第一个节点first找到第二个节点second
free(first);//释放最后一个节点的空间
first = NULL;
phead->next = second;//使节点phead、second里的指针prev或next指向对应的节点
second->prev = phead;
}
}
解释请看图片和代码里面的注解,非常详细。 当链表里面只有一个节点时候,上面代码依旧成立。
6、提供一个数据,找到这个数据所在的节点的函数
//提供一个数据,找到这个数据所在的节点的函数
ListNode* ListFindData(ListNode* phead,ListDataType x)
{
assert(phead);
ListNode* cur = phead->next;
while (cur != phead)//遍历:循环最后一个节点里的指针next,next存储的是头节点phead的地址
{
if (cur->data == x)
{
printf("找到了这个节点\n");
return cur;
}
else
{
cur = cur->next;
}
}
printf("找不到这个节点\n");
return NULL;
}
解释请看图片和代码里面的注解,非常详细。
7、给一个数据,在其所在节点前面插入一个节点的函数
此功能要和上面的提供一个数据,找到这个数据所在的节点的函数配合使用才能实现。
//给一个数据,在其所在节点前面插入一个节点的函数
void ListErase(ListNode* phead, ListDataType x, ListDataType y)//x是查找的节点数据,y是新节点的数据
{
assert(phead);
ListNode* pos = ListFindData(phead, x);
if (pos != NULL)//防止没找到节点,就不用继续插入
{
ListNode* prev = pos->prev;//通过节点pos找到它前面的节点prev
ListNode* newnode = BuyListNode(y);//创建新节点
newnode->prev = prev;//使节点prev、newnode、pos里的指针prev或next指向对应的节点
prev->next = newnode;
newnode->next = pos;
pos->prev = newnode;
}
}
解释请看图片和代码里面的注解,非常详细。
8、给一个数据,删除这个数据所在的节点的函数
此功能要和上面的提供一个数据,找到这个数据所在的节点的函数配合使用才能实现。
//给一个数据,删除这个数据所在的节点的函数
void ListInsert(ListNode* phead, ListDataType x)
{
assert(phead);
ListNode* pos = ListFindData(phead, x);
if (pos != NULL)//防止没找到节点,就不用删除
{
ListNode* prev = pos->prev;//通过节点pos找到它前面的节点prev
ListNode* next = pos->next;//通过节点pos找到它后面的节点next
free(pos);//释放节点pos的空间
pos = NULL;
prev->next = next;//使节点prev、next里的指针prev或next指向对应的节点
next->prev = prev;
}
}
解释请看图片和代码里面的注解,非常详细。
9、修改数据的函数
此功能要和上面的提供一个数据,找到这个数据所在的节点的函数配合使用才能实现。
//修改数据的函数
void ListChangeData(ListNode* phead,ListDataType x)
{
int data = 0;//定义想要改成的数据
assert(phead);
ListNode* cur = phead->next;
while (cur != phead)//遍历:循环最后一个节点里的指针next,next存储的是头节点phead的地址
{
if (cur->data == x)
{
printf("找到了这个数据,请输入想要改成的数据:");
scanf("%d", &data);
cur->data = data;
break;
}
else
{
cur = cur->next;
}
}
if (cur == phead)
{
printf("想要修改的数据不存在\n");
}
}
解释请看图片和代码里面的注解,非常详细。
10、部分功能的简化与升级
10.1 尾插和头插函数的简化升级
//尾插函数
void ListPushBack(ListNode* phead, ListDataType x)
{
ListInsert(phead, x);
}
尾插可以理解为在头节点pjead前面插入,因为链表是循环的,从肉眼看感觉放在了头节点的前面,但是鲜果上是尾插。
//头插函数
void ListPushFront(ListNode* phead, ListDataType x)
{
ListInsert(phead->next, x);
}
头插就是在第一个节点前插入,头节点里的指针next存放的就是第一个节点的地址。
四、总代码
这里是总代码的地址所在,有需要的可以自取。