数据结构:带头双向循环链表——增加、删除、查找、修改,详细解析

读者可以先阅读这一篇:数据结构——单链表的增加、删除、查找、修改,详细解析_昵称就是昵称吧的博客-CSDN博客,可以更好的理解带头双向循环链表


目录

 一、带头双向循环链表的处理和介绍

1、带头双向循环链表的概念

2、带头双向循环链表的结构

 3、节点的处理

4、头节点的处理

5、节点(结构体)内存空间的开辟

二、链表的主体框架

1、整体框架

 2、主菜单

 三、功能的实现

1、创建一个新节点的函数

2、初始化创建头节点的函数

3、打印节点数据的函数

4、尾插和头插增加节点的函数

4.1 从尾部插入节点的函数

4.2 从头部插入节点的函数

5、尾删和头删减少节点的函数

5.1 从尾部删除节点的函数

 5.2 从头部删除节点的函数

 6、提供一个数据,找到这个数据所在的节点的函数

7、给一个数据,在其所在节点前面插入一个节点的函数

8、给一个数据,删除这个数据所在的节点的函数

 9、修改数据的函数

10、部分功能的简化与升级

 10.1 尾插和头插函数的简化升级

 四、总代码

 五、代码运行实例


 一、带头双向循环链表的处理和介绍

1、带头双向循环链表的概念

 链表的概念:链表是一种物理存储结构上非连续非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。

带头双向循环链表是链表中最复杂的,因为它具有了链表存在的结构中所有的结构:带头,双向和循环。

1、带头和不带头带头就是链表带有头节点,这个头节点只具有哨兵位的作用意思就是它不随着链表的改变而改变,定义之后就固定不变了,并且头节点里面不存放数据;不带头就是链表没有头节点。

2、双向和单项:双向就是一个节点,既存上一个节点的地址,也存下一个节点的地址单向就是一个节点,只存下一个节点的地址。


3、循环和非循环:循环就是这个链表的最后一个节点里的指针存放的地址,如果链表带头,就存放的头节点的地址;如果链表不带头,就存放第一个节点的地址非循环就是这个链表的最后一个节点存放的地址是空的。


2、带头双向循环链表的结构

数据结构:带头双向循环链表——增加、删除、查找、修改,详细解析_第1张图片

 头节点里面是不存放数据的,只具有哨兵位的作用,各个节点之间通过节点里面的指针进行链接。单向链表里面,每个节点里只有一个指针,因为只需要指向下一个节点;但是双向链表中,每个节点里有两个指针,一个指向上一个节点,一个指向下一个节点。


 3、节点的处理

因为双向链表里面每个节点,都会存储一个数据和两个指针prevnext指针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 从尾部插入节点的函数

数据结构:带头双向循环链表——增加、删除、查找、修改,详细解析_第2张图片

//尾插函数
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;
}

  解释请看图片和代码里面的注解,非常详细。 当链表里面没有节点,只有头节点的时候,上面代码依旧成立。

数据结构:带头双向循环链表——增加、删除、查找、修改,详细解析_第3张图片


4.2 从头部插入节点的函数

数据结构:带头双向循环链表——增加、删除、查找、修改,详细解析_第4张图片

//头插函数
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、尾删和头删减少节点的函数

5.1 从尾部删除节点的函数

数据结构:带头双向循环链表——增加、删除、查找、修改,详细解析_第6张图片

//尾删函数
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;
	}
}

 解释请看图片和代码里面的注解,非常详细。 当链表里面只有一个节点时候,上面代码依旧成立。

数据结构:带头双向循环链表——增加、删除、查找、修改,详细解析_第7张图片


 5.2 从头部删除节点的函数

数据结构:带头双向循环链表——增加、删除、查找、修改,详细解析_第8张图片

//头删函数
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;
	}
}

 解释请看图片和代码里面的注解,非常详细。 当链表里面只有一个节点时候,上面代码依旧成立。

数据结构:带头双向循环链表——增加、删除、查找、修改,详细解析_第9张图片​ 


 6、提供一个数据,找到这个数据所在的节点的函数

数据结构:带头双向循环链表——增加、删除、查找、修改,详细解析_第10张图片

//提供一个数据,找到这个数据所在的节点的函数
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、给一个数据,在其所在节点前面插入一个节点的函数

此功能要和上面的提供一个数据,找到这个数据所在的节点的函数配合使用才能实现。

 数据结构:带头双向循环链表——增加、删除、查找、修改,详细解析_第11张图片

//给一个数据,在其所在节点前面插入一个节点的函数
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、给一个数据,删除这个数据所在的节点的函数

此功能要和上面的提供一个数据,找到这个数据所在的节点的函数配合使用才能实现。

数据结构:带头双向循环链表——增加、删除、查找、修改,详细解析_第12张图片

//给一个数据,删除这个数据所在的节点的函数
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、修改数据的函数

 此功能要和上面的提供一个数据,找到这个数据所在的节点的函数配合使用才能实现。

 数据结构:带头双向循环链表——增加、删除、查找、修改,详细解析_第13张图片

//修改数据的函数
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存放的就是第一个节点的地址。


 四、总代码

这里是总代码的地址所在,有需要的可以自取。

放代码: 代码 - Gitee.com


 五、代码运行实例

数据结构:带头双向循环链表——增加、删除、查找、修改,详细解析_第14张图片

你可能感兴趣的:(数据结构(初阶),c语言,大数据,学习,安全)