双向带头循环链表

作者主页

lovewold少个r博客主页

人生的每一个选择,都是得到一些,失去一些。


目录

前言

双向循环链表是什么

双向带头循环链表的实现

开辟节点空间

创建返回链表的头结点

双向链表打印

 双向链表在pos的前面进行插入

双向链表尾插

 双向链表头插

双向链表删除pos位置的节点

双向链表尾删

双向链表头删

 双向链表查找

双向链表销毁

源文件

test.c

List.h

List.c

总结


前言

在单链表的实现过程中,单链表展示了对碎片化内存空间的有效利用,并且在实现的过程中我们也发现在一些操作过程中,单链表也表现出了他的劣势。主要对插入的情况需要判断的太繁琐,并且查找上一个元素还比较麻烦,那有没有什么能解决单链表存在的这种劣势,实现对查找删除的更加优化的结构呢。答案就是双向带头循环列表。

双向循环链表是什么

双向循环链表和单链表类似,我们把一个节点定义为两个指针域和一个数据域,数据域用来存放值,两个指针域一个指向下一个节点,一个指向上一个节点。所以相对于单链表,他查找上一个节点就非常的方便快捷。

双向带头循环链表_第1张图片

双向带头循环链表的实现

在头文件中包含函数声明。


#pragma once
#include
#include
#include
#include

// 带头+双向+循环链表增删查改实现
typedef int LTDataType;
typedef struct ListNode
{
	LTDataType data;
	struct ListNode* next;
	struct ListNode* prev;
}ListNode;

//开辟节点空间
ListNode* BuyLTnewNode(LTDataType x);
// 创建返回链表的头结点.
ListNode* ListCreate();
// 双向链表销毁
void ListDestory(ListNode* pHead);
// 双向链表打印
void ListPrint(ListNode* pHead);
// 双向链表尾插
void ListPushBack(ListNode* pHead, LTDataType x);
// 双向链表尾删
void ListPopBack(ListNode* pHead);
// 双向链表头插
void ListPushFront(ListNode* pHead, LTDataType x);
// 双向链表头删
void ListPopFront(ListNode* pHead);
// 双向链表查找
ListNode* ListFind(ListNode* pHead, LTDataType x);
// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x);
// 双向链表删除pos位置的节点
void ListErase(ListNode* pos);

开辟节点空间

ListNode* BuyLTnewNode(LTDataType x);
 
节点包含两个域,一个指向节点的直接前驱,一个指向节点的直接后继,在数据域我们存储我们的值。

双向带头循环链表_第2张图片

ListNode* BuyLTnewNode(LTDataType x)
{
	ListNode* newNode = (ListNode*)malloc(sizeof(ListNode));//申请空间
	if (newNode == NULL)//开辟成功判断
	{
		perror("malloc fail\n");
		exit(-1);
	}
    //初始化
	newNode->data = x;
	newNode->prev = NULL;
	newNode->next = NULL;

	return newNode;
}

创建返回链表的头结点

ListNode* ListCreate();
 这里我们要确定头节点的状态情况,当只有一个头节点的时候,他的直接前驱和后继指向应该是指向直接本身,达到循环的状态。

双向带头循环链表_第3张图片

ListNode* ListCreate()
{
	ListNode* phead = BuyLTnewNode(-1);
	phead->prev = phead;
	phead->next = phead;

	return phead;
}

双向链表打印

void ListPrint(ListNode* pHead);

双向链表的打印操作依旧是基于对链表的遍历过程,主要是理解遍历的过程和遍历的起止条件。在有头节点的遍历过程中,起始遍历位置应该是pHead->next;终止条件会最后回到头节点,此时应该终止。即cur==pHead时。
双向带头循环链表_第4张图片

void ListPrint(ListNode* pHead)
{
	assert(pHead);
	printf("pHead<=>");
	ListNode* cur = pHead->next;
	while (cur != pHead)
	{
		printf("%d<=>",cur->data);
		cur = cur->next;
	}
	printf("\n");
}

双向链表在pos的前面进行插入

void ListInsert(ListNode* pos, LTDataType x);

在一个已有链表中插入一个节点,要注意先链接后面节点在断开,避免先链接新节点把后面的节点地址忘记。同时对于一个节点,我们是不是也能在pos(指头节点)前插入呢,答案是可以的,因为头节点本身就是循环的一个结构,其前驱和后继都是其本身,插入新节点,无外乎就是在pHead前面插入一个,其头节点的下一个指向依旧是新节点也就等同于在pHead后面放置一个新元素。即只有一个节点时,pos为头节点时,插入新节点依旧不需要额外考虑,等同于在pHead后尾插。

双向带头循环链表_第5张图片

void ListInsert(ListNode* pos, LTDataType x)
{
	assert(pos);
	ListNode* newNode = BuyLTnewNode(x);
	ListNode* tail = pos->prev;
	newNode->next = pos;
	pos->prev=newNode;
	
	tail->next = newNode;
	newNode->prev = tail;
}

双向链表尾插

void ListPushBack(ListNode* pHead, LTDataType x);
 
尾插的在单链表的操作过程中首先需要找到尾部,而对于带头双向循环链表,他的优势就展现的淋漓尽致了。对于找尾操作过程,因为头节点和尾节点本身就有前后关系,即只需要pHead->prev即可访问到尾节点,完全避免了单链表的尾插时间复杂度。更关键点是我们在双向链表尾插等同于在pHead节点(pos位)前插入,也就可以复用void ListInsert(ListNode* pos, LTDataType x)函数。

void ListPushBack(ListNode* pHead, LTDataType x)
{
	assert(pHead);
	ListInsert(pHead, x);
}

 双向链表头插

void ListPushFront(ListNode* pHead, LTDataType x);

同理,对于头插,等同于在头节点的下一个元素进行插入,而如果只有一个头节点,其下一个节点为本身,依旧不需要额外考虑。pHead->next即可访问到下一节点(pos位),并在前面进行插入。

void ListPushFront(ListNode* pHead, LTDataType x)
{
	assert(pHead->next);
	ListInsert(pHead->next, x);
}

双向链表删除pos位置的节点

void ListErase(ListNode* pos);

删除pos位置的时候,只需要改变pos位置的上一个节点和下一个节点的连接关系,并free pos位节点,对于链表的任何一个位置,因为整体为循环结构,边界情况也一样,完全不需要额外考虑。也就是说当pos位分别位于pHead->next和pHead->prev就可以实现头删和尾删。

双向带头循环链表_第6张图片

void ListErase(ListNode* pos)
{
	assert(pos);
	ListNode* first = pos->prev;
	ListNode* second = pos->next;
	first->next = second;
	second->prev = first;
	free(pos);

}

双向链表尾删

void ListPopBack(ListNode* pHead);

void ListPopBack(ListNode* pHead)
{
	assert(pHead);
	ListErase(pHead->prev);
}

双向链表头删

void ListPopFront(ListNode* pHead);

void ListPopFront(ListNode* pHead)
{
	assert(pHead);
	ListErase(pHead->next);
}

 双向链表查找

ListNode* ListFind(ListNode* pHead, LTDataType x);

实现过程就是对双向链表的遍历和比对查找,找到返回值的pos位。后续可以对地址,值的更改,前后插入进行操作。

ListNode* ListFind(ListNode* pHead, LTDataType x)
{
	ListNode* cur = pHead->next;
	while (cur != pHead)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

双向链表销毁

void ListDestory(ListNode* pHead);

销毁操作可以理解遍历头删或者尾删,当最后的遍历到头节点本身时候即销毁完毕,释放整个链表即可。

void ListDestory(ListNode* pHead)
{
	assert(pHead);
	while (pHead->next != pHead)
	{
		ListPopBack(pHead);
	}
	free(pHead);
}

测试环境我们通过建立菜单的方式进行操作,以下为全部源码。

源文件

test.c

#define _CRT_SECURE_NO_WARNINGS 1
#include"List.h"

void menu()
{
	printf("*********************************************\n");
	printf("***********    1.头插       *****************\n");
	printf("***********    2.尾插       *****************\n");
	printf("***********    3.头删       *****************\n");
	printf("***********    4.尾删       *****************\n");
	printf("***********    5.查找       *****************\n");
	printf("***********    6.删除pos位  *****************\n");
	printf("***********    7.销毁链表   *****************\n");
	printf("***********    0.exit       *****************\n");
	printf("*********************************************\n");
	printf("*********************************************\n");
}
int main()
{
	ListNode* List = ListCreate();
	int x = 0;
	int input = 1;
	menu();
	while (input)
	{
		printf("请选择你要执行的操作->\n");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			printf("请输入你要头插的值\n");
			scanf("%d", &x);
			ListPushFront(List,x);
			ListPrint(List);
			break;
		case 2:
			printf("请输入你要尾插的值\n");
			scanf("%d", &x);
			ListPushBack(List, x);
			ListPrint(List);
			break;
		case 3:
			ListPopFront(List);
			ListPrint(List);
			break;
		case 4:
			ListPopBack(List);
			ListPrint(List);
			break;
		case 5:
			printf("请输入你要查找的值\n");
			scanf("%d", &x);
			ListNode* Node = ListFind(List, x);
			if (Node != NULL)
			{
				printf("这个值在内存中的地址是->%p", &Node);
			}
			break;
		case 6:
			printf("请输入你要删除的值\n");
			scanf("%d", &x);
			ListNode* Node1 = ListFind(List, x);
			if (Node1 != NULL)
			{
				ListErase(Node1);
				ListPrint(List);
			}
			else
			{
				printf("链表没有'%d'这个值\n",x);
			}
			break;
		case 7:
			ListDestory(List);
			ListPrint(List);
			break;
		default:
			printf("输入错误,重新输入\n");
			break;
		}
	}
	return 0;
}

List.h


#pragma once
#include
#include
#include
#include

// 带头+双向+循环链表增删查改实现
typedef int LTDataType;
typedef struct ListNode
{
	LTDataType data;
	struct ListNode* next;
	struct ListNode* prev;
}ListNode;

//开辟节点空间
ListNode* BuyLTnewNode(LTDataType x);
// 创建返回链表的头结点.
ListNode* ListCreate();
// 双向链表销毁
void ListDestory(ListNode* pHead);
// 双向链表打印
void ListPrint(ListNode* pHead);
// 双向链表尾插
void ListPushBack(ListNode* pHead, LTDataType x);
// 双向链表尾删
void ListPopBack(ListNode* pHead);
// 双向链表头插
void ListPushFront(ListNode* pHead, LTDataType x);
// 双向链表头删
void ListPopFront(ListNode* pHead);
// 双向链表查找
ListNode* ListFind(ListNode* pHead, LTDataType x);
// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x);
// 双向链表删除pos位置的节点
void ListErase(ListNode* pos);

List.c

#define _CRT_SECURE_NO_WARNINGS 1
#include"List.h"

ListNode* BuyLTnewNode(LTDataType x)
{
	ListNode* newNode = (ListNode*)malloc(sizeof(ListNode));
	if (newNode == NULL)
	{
		perror("malloc fail\n");
		exit(-1);
	}
	newNode->data = x;
	newNode->prev = NULL;
	newNode->next = NULL;

	return newNode;
}
ListNode* ListCreate()
{
	ListNode* phead = BuyLTnewNode(-1);
	phead->prev = phead;
	phead->next = phead;

	return phead;
}
void ListPrint(ListNode* pHead)
{
	assert(pHead);
	printf("pHead<=>");
	ListNode* cur = pHead->next;
	while (cur != pHead)
	{
		printf("%d<=>",cur->data);
		cur = cur->next;
	}
	printf("\n");
}
void ListInsert(ListNode* pos, LTDataType x)
{
	assert(pos);
	ListNode* newNode = BuyLTnewNode(x);
	ListNode* tail = pos->prev;
	newNode->next = pos;
	pos->prev=newNode;
	
	tail->next = newNode;
	newNode->prev = tail;
}
void ListPushFront(ListNode* pHead, LTDataType x)
{
	assert(pHead->next);
	ListInsert(pHead->next, x);
}
void ListPushBack(ListNode* pHead, LTDataType x)
{
	assert(pHead);
	ListInsert(pHead, x);
}
void ListPopFront(ListNode* pHead)
{
	assert(pHead);
	ListErase(pHead->next);
}
void ListPopBack(ListNode* pHead)
{
	assert(pHead);
	ListErase(pHead->prev);
}
void ListErase(ListNode* pos)
{
	assert(pos);
	ListNode* first = pos->prev;
	ListNode* seconed = pos->next;
	first->next = seconed;
	seconed->prev = first;
	free(pos);

}
ListNode* ListFind(ListNode* pHead, LTDataType x)
{
	ListNode* cur = pHead->next;
	while (cur != pHead)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}
void ListDestory(ListNode* pHead)
{
	assert(pHead);
	while (pHead->next != pHead)
	{
		ListPopBack(pHead);
	}
	free(pHead);
}

总结

        整个实现过程中,我们发现我们并不需要像单链表一样,对不同的情况进行分别考虑,在删除和插入的实现可以通用删除和插入函数。只是传递的pos节点不同即可实现一样的效果。双向链表看起来实现需要两个指针,需要额外小心删除和插入的操作,但是当这个问题解决后,他的实现效率大大提高。通过建立两个指针在空间上是比单链表和顺序表消耗了更多的空间,这种空间换时间的方式也大大提高了销量,毕竟目前空间的消耗成本往往低于时间。

  1. 结构特点:

    • 双向性:每个节点都有指向前一个节点和后一个节点的指针,这使得在链表中可以双向遍历元素。
    • 带头节点:通常情况下,双向带头循环链表会包含一个头节点(dummy node),该节点不包含数据,但用于简化链表的操作,例如在插入和删除元素时无需特殊处理边界情况。
    • 循环性:链表的尾节点指向头节点,形成一个循环。
  2. 基本操作:

    • 插入元素:在链表中插入元素通常涉及修改前后节点的指针,以保持链表的连续性。
    • 删除元素:从链表中删除元素需要同样修改前后节点的指针,并释放被删除节点的内存。
    • 遍历:可以从头节点开始,沿着指针依次遍历链表的元素。由于是双向链表,可以实现正向和逆向遍历。
  3. 优点:

    • 插入和删除操作效率较高:与数组相比,在链表中插入和删除元素的开销较小,因为不需要移动其他元素。
    • 循环性质:适用于需要循环访问数据的场景,例如循环队列、循环缓冲区等。
  4. 缺点:

    • 随机访问低效:与数组不同,链表需要从头节点开始遍历,才能访问到特定位置的元素,因此随机访问的性能较差。
    • 额外空间开销:每个节点都需要额外的指针空间,占用额外的内存。

        双向带头循环链表是一种灵活的数据结构,适用于特定的应用场景,其中插入和删除操作是频繁进行的情况下表现出色。然而,需要权衡其随机访问性能较差和额外的空间开销。在选择数据结构时,应根据具体需求和操作的频率来考虑是否使用双向带头循环链表。


文章由于个人水平有限,如果有错误欢迎读者批评指正!感激不尽!


你可能感兴趣的:(链表,数据结构,c语言,c++)