数据结构——带头双向循环链表(C语言实现详解)

1.各种链表的介绍

在这里插入图片描述
链表的主要特征有是否带哨兵位头结点,是否双向,即是否有两个指针比变量存放该结点前一个结点和后一个结点,是否循环。实际上链表的组合不仅仅是图上的四种,而是一共有8种组合。前面介绍的单链表,即无头单向不循环链表,是一种最经典的链表结构。通常用于哈希桶和图的邻接表,由于结构简单通常不会用来单独存储数据。今天介绍的带头双向循环链表是一种复杂的链表结构,一般用于单独存储数据。实际使用链表单独存储数据都是用带头双向循环链表这种链表结构。下面,我就边实现边介绍这种结构的优势。

2.带头双向循环链表的实现

2.1.带头双向循环链表的定义

带头双向循环链表定义,首先需要定义一个存储的数据类型data,然后是两个指针,分别指向前一个结点(prev)和后一个结点(next)。

代码

typedef int ListDataType;

//带头双向循环链表的定义
typedef struct ListNode
{
    
    
	struct ListNode* next;
	struct ListNode* prev;
	ListDataType data;
}LNode;

2.2.创建哨兵位头结点接口

该接口实现思路如下,该接口用于创建创建哨兵位头结点,所以我们这个结点是不用来存储数据的。一开始哨兵位头结点
的next指针和prev指针都应该只想自己,这样才能够达成一个循环的效果。
在这里插入图片描述

代码

// 创建返回链表的头结点.
LNode* ListCreate()
{
    
    
	LNode* guard = (LNode*)malloc(sizeof(LNode));
	if (NULL == guard)
	{
    
    
		perror("malloc fail\n");
		return NULL;
	}
	//循环
	guard->prev = guard->next = guard;
	guard->data = -1;

	return guard;
}

补充

可能有些书籍上,哨兵位头结点可能会存储链表的长度。但是,设想一下这种情况可能只有当数据类型是整型是才有可能方便实现。如果链表存储的是浮点型,不就需要另外定义一个哨兵位头结点的类型。我这里实现的哨兵位头结点不存储数据,只有“站岗”的作用。

2.3.销毁接口

这里的销毁需要遍历一遍链表,然后依次释放结点,需要注意的是带头双向循环链表的结束条件和单链表的有所不同。单链表遍历的结束条件是当链表指向空,而带头双向循环链表遍历需要从第一个结点开始,当链表走回哨兵位头结点时,表示遍历结束。
在这里插入图片描述

代码

// 双向链表销毁
void ListDestory(LNode* pHead)
{
    
    
	assert(pHead);
	//从第一个结点开始释放
	LNode* cur = pHead->next;

	while (cur != pHead)
	{
    
    
		LNode* next = cur->next;
		free(cur);
		cur = next;
	}
	//释放头结点
	free(pHead);
}

2.4.打印接口

打印无非就是遍历一遍链表,然后输出每个结点的数据。

代码

// 双向链表打印
void ListPrint(LNode* pHead)
{
    
    
	LNode* cur = pHead->next;
	printf("<=head=>");
	//cur==头结点结束打印
	while (cur != pHead)
	{
    
    
		printf("<=%d=>",cur->data);
		cur = cur->next;
	}

	printf("\n");
}

2.5.申请结点接口

每一次插入数据都需要申请新的结点存放数据,所以我单独将申请结点这部分代码封装成接口,便于使用。

代码

//开辟新结点
LNode* BuyNode(ListDataType x)
{
    
    
	//动态开辟
	LNode* newnode = (LNode*)malloc(sizeof(LNode));
	if (NULL == newnode)
	{
    
    
		perror("malloc fail\n");
		return NULL;
	}
	//初始化
	newnode->prev = NULL;
	newnode->next = NULL;
	newnode->data = x;

	return newnode;
}

2.6.尾插接口

带头双向循环链表尾插数据并不需要像单链表一样,每次都遍历链表找尾。因为,哨兵位头结点的prev指针存放着最后一个结点的地址。只需要通过prev指针就可以找到尾结点。将尾结点的next指针指向新节点,将新节点的prev指针指向原尾结点,将新节点的next指向哨兵位头结点,最后,将哨兵位头结点的prev指针指向新节点。
在这里插入图片描述

代码

void ListPushBack(LNode* pHead, ListDataType x)
{
    
    
	assert(pHead);

	//申请结点
	LNode* newnode = BuyNode(x);
	//判空
	if (NULL == newnode)
	{
    
    
		perror("malloc fail\n");
		return;
	}
	//保存原尾结点
	LNode* Headprev = pHead->prev;
	//改变链表指向
	newnode->prev = Headprev;
	Headprev->next = newnode;
	newnode->next = pHead;
	pHead->prev = newnode;

}

2.7.判断空链表接口

当哨兵位头结点的next指向这自己时,表示链表为空。
在这里插入图片描述

bool ListEmpty(LNode* pHead)
{
    
    
	assert(pHead);

	return pHead == pHead->next;
}

2.8.尾删接口

尾删实现思路如下,想将尾结点的地址保存在一个指针变量中。使原尾结点的prev指针的next指针指向哨兵位头结点,使哨兵位头结点的prev指向原尾结点的prev(即新结点),释放原尾结点。
在这里插入图片描述

代码

// 双向链表尾删
void ListPopBack(LNode* pHead)
{
    
    
	assert(pHead);
	//空表不可删除
	assert(!ListEmpty(pHead));
	
	//保存原尾结点
	LNode* prevnode = pHead->prev;
	//尾删
	pHead->prev = prevnode->prev;
	prevnode->prev->next = pHead;
	//释放动态内存
	free(prevnode);
	prevnode = NULL;

}

2.9.头插接口

带头双向循环链表的头插的实现思路如下,这里我定义一个临时结点来保存哨兵位头结点的下一个结点,然后开辟一个新节点,将新节点的next指向临时结点,让临时结点的prev指向新节点,让新节点的prev指向哨兵位的头结点,最后,让哨兵位头结点的next指向新结点即可。
在这里插入图片描述

代码

// 双向链表头插
void ListPushFront(LNode* pHead, ListDataType x)
{
    
    
	assert(pHead);
	//开辟空间
	LNode* newnode = BuyNode(x);
	//临时结点存放哨兵位的下一个结点
	LNode* first = pHead->next;
	
	//头插
	first->prev = newnode;
	newnode->next = first;
	newnode->prev = pHead;
	pHead->next = newnode;

}

2.10.头删接口

先定义一个临时结点保存第一个结点,然后让pHead的next指向临时结点的next,再让临时结点的next的prev指向pHead,最后释放掉第一个结点达成头删。
在这里插入图片描述

代码

// 双向链表头删
void ListPopFront(LNode* pHead)
{
    
    
	assert(pHead);
	//空表不可删除
	assert(!ListEmpty(pHead));

	LNode* first = pHead->next;
	//头删
	pHead->next = first->next;
	first->next->prev = pHead;
	free(first);
	first = NULL;
}

2.11.pos位置前插入数据

实现pos位置前插入可以先定义一个临时变量存放pos的前一个结点,然后改变pos和pos前一个结点的指向关系,达到pos前插入数据的效果。完成pos位置前插入这个接口便可以根据该接口复用达到头插尾插的效果
在这里插入图片描述

代码

// 双向链表在pos的前面进行插入
void ListInsert(LNode* pos, ListDataType x)
{
    
    
	assert(pos);
	
	LNode* newnode = BuyNode(x);
	LNode* posprev = pos->prev;

	posprev->next = newnode;
	newnode->prev = posprev;
	newnode->next = pos;
	pos->prev = newnode;

}

完成insert接口后,便可以对头插和尾插接口的实现用该接口进行复用,需要注意的是尾插其实就是在哨兵位头结点的prev指针前插入数据,头插就是在哨兵位头结点的next前插入数据。

2.12.删除pos位置数据

定义两个临时结点保存pos位置前和pos位置后的结点,然后让pos前的结点的next指向pos后的结点,让pos后的结点的prev指向pos结点前的结点。
在这里插入图片描述

代码

// 双向链表删除pos位置的节点
void ListErase(LNode* pos)
{
    
    
	assert(pos);
	assert(!ListEmpty(pos));

	LNode* prev = pos->prev;
	LNode* next = pos->next;
	//删除
	prev->next = next;
	next->prev = prev;

	//请手动置空
	free(pos);

}

当然,完成本接口后,可以用本接口对实现头删尾删进行接口的复用。尾删其实就是删除在哨兵位头结点的prev指针指向的数据,头删就是删除哨兵位头结点的next处数据。

猜你喜欢

转载自blog.csdn.net/m0_71927622/article/details/129757206