数据结构:双向链表

一.双向链表的结构

最常用的链表就是单链表和双向链表。我们首先要知道,链表有八种分类。单链表是不带头单向不循环链表。而此篇博客要讲的是带头双向循环链表。

结构如下:

注意:带头链表里的头节点,实际为“哨兵位”,哨兵位节点不存储任何有效元素,只是站在这里“放哨的” 。它存在的意义就是遍历循环链表避免死循环。

我们所说的双向链表为空,跟单链表不同,双向链表就是只存在哨兵位。双向链表的实现就比单链表简单的多了。

二.双向链表的实现

1.申请一个新节点和初始化

//创建一个新节点
LTNode* LTBuyNode(LTDataType x)
{
	LTNode* node = (LTNode*)malloc(sizeof(LTNode));//动态申请空间
	if (node == NULL)
	{
		perror("node");
		exit(1);
	}
	node->next = node;
	node->prev = node;//把新节点的next指针和prev指针都指向自己
	node->data = x;
	return node;//返回我们的新节点
}
//初始化
void LTInit(LTNode** pphead)
{
	//给双向链表创建一个哨兵位
	*pphead = LTBuyNode(-1);//初始化时我们需要改变首节点,用二级指针传参
}

注意在创建新节点的时候我们一定要把next指针和prev都指向自己。初始化其实就是给我们创建一个哨兵位。

其实初始化不用二级指针也可以做到初始话:

//不用二级指针的初始化
LTNode* LTInit(LTNode* phead)
{
	phead = LTBuyNode(-1);
	return phead;
}

返回指针就行了。

2.尾插

//尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = LTBuyNode(x);//创建一个新节点
	newnode->prev = phead->prev;
	newnode->next = phead;//先把newnode的指向指针都给改了

	phead->prev->next = newnode;
	phead->prev = newnode;//这一行不能与上一行交换位置
}

为了方便查看我们写的代码是否有错误,我们可以打印出来:

//打印
void LTPrint(LTNode* phead)
{
	assert(phead);
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		printf("%d->", pcur->data);
		pcur = pcur->next;
	}
	printf("\n");
}

这个是在后面的测试过程来用的。

3.头插

//头插
void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = LTBuyNode(x);
	newnode->prev = phead;
	newnode->next = phead->next;//先改变newnode节点指针的指向

	phead->next->prev = newnode;
	phead->next = newnode;//同样这两个的位置也不可以调换
}

4.尾删

尾删就更简单了:

//尾删
void LTPopBack(LTNode* phead)
{
	assert(phead&&phead->next!=NULL);//链表必须不能为NULL,且必须有效
	LTNode* del = phead->prev;//把尾节点存起来
	del->prev->next = phead;
	phead->prev = del->prev;
	free(del);
	del = NULL;//释放后置为NULL,这是习惯
}

5.头删

头删的时候一定要注意,我们删的可不是哨兵位,是有效位的头。

//头删
void LTPopFront(LTNode* phead)
{
	assert(phead && phead->next != NULL);
	LTNode* del = phead->next;
	del->next->prev = phead;
	phead->next = del->next;//这两行代码可以互换,因为我们已经提前把phead->next的值存起来了
	free(del);
	del = NULL;
}

6.指定位置之后插入

在指定位置插入,我们可以先写一个函数来找到指定位置:

LTNode* LTFind(LTNode* phead, LTDataType x)
{
	LTNode* pcur = phead->next;
	while (pcur != phead)//遍历双向链表
	{
		if (pcur->data == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	//没有找到
	return NULL;
}

然后执行插入:

//在pos位置之后插入数据
void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);

	LTNode* newnode = LTBuyNode(x);

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

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

7.删除指定节点

//删除pos节点
void LTErase(LTNode* pos)
{
	//pos理论上来说不能为phead,但是没有参数phead,无法增加校验
	assert(pos);
	
	pos->next->prev = pos->prev;
	pos->prev->next = pos->next;

	free(pos);
	pos = NULL;
}

8.销毁链表

void LTDesTroy(LTNode* phead)
{
	assert(phead);

	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		LTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	//此时pcur指向phead,而phead还没有被销毁
	free(phead);
	phead = NULL;
}

三.双向链表功能实现的测试

void text1()
{
	//初始化
	LTNode* plist = LTInit();
	//尾插
	LTPushBack(plist, 1);
	LTPushBack(plist, 2);
	LTPushBack(plist, 3);
	//打印
	LTPrint(plist);
	//头插
	LTPushFront(plist, 100);
	LTPushFront(plist, 200);
	LTPushFront(plist, 300);
	LTPrint(plist);
	//尾删
	LTPopBack(plist);
	LTPrint(plist);
	//头删
	LTPopFront(plist);
	LTPrint(plist);
	//查找
	LTNode* find = LTFind(plist, 1);
	//在指定位置之后插入数据
	LTInsert(find, 66);
	LTPrint(plist);
	//删除指定位置的数据
	LTErase(find);
	find = NULL;
	LTPrint(plist);
	//销毁链表
	LTDesTroy(find);
    plist=NULL;
}
int main()
{
	text1();
	return 0;
}

为了保证接口的一致性,即使是LTErase和LTDesTory这些理论上需要传递二级指针的函数,我们也给它传递了一级指针。但是我们一定要注意 LTDesTory和LTErase当我们在函数内部把节点给释放掉之后,在函数的内部我们把释放掉的指针给置为了NULL,但是在出了函数的时候,我们是实参并没有置为NULL,所以我们需要多加一步对实参置为NULL,这样代码才完整。

关于双向链表是比单向链表要简单的多的。感谢大家的观看,如有错误,请多多指出。

上一篇:实验1产品原型设计-YHealth健康APP


下一篇:普乐蛙VR神州飞船设备VR太空舱体验馆VR博物馆