一篇文章讲透带头双向循环链表!

目录

一.前言

二.双向链表的功能

三.双向链表的定义

四.双向链表的功能

4.0双向链表结点申请

4.1初始化双向链表

4.2双向链表尾插

4.3双向链表尾删

4.4双向链表头插

4.5双向链表头删

4.6双向链表查找

4.7修改指定数据

4.8指定插入数据

4.9指定删除数据

4.10打印双向链表


  点击这里进入裤裤的数据结构专栏

一.前言

前面我们已经学习了单链表,它解决了顺序表中插入或者删除数据需要挪动大量数据的缺点。

但仍有需改进的地方。就比如我们需要寻找某个结点的前一个结点,就只能通过遍历来寻找。

这样就可能会造成大量的时间浪费。为了解决这个问题,今天就要学习今天的主角-->带头双

向循环链表

在学习单链表中,我们大家可能被二级指针搞昏了头脑,现在我们实现一个带头的链表,也就是带哨兵位的链表,哨兵位只充当哨兵作用,它在链表的第一个位置,但不保存数据。因此我们在使用时,并不会将哨兵位当作头结点,而是将它的下一个结点当作头结点。这样做的好处是在插入数据时不需要修改物理结构上的头结点,也就不需要传二级指针。

而实现为双向链表,则可以让我们在使用时更加方便,不必像学习单链表时单独定义一个指针寻找前一个结点。

二.双向链表的功能

双向链表需要实现的功能如下

  1. 双向链表结点申请(其实这个不是一个功能,但是我们要在实现下面的功能时使用它)
  2. 初始化双向链表中的数据。
  3. 对双向链表进行尾插(末尾插入数据)。
  4. 对双向链表进行头插(开头插入数据)。
  5. 对双向链表进行头删(开头删除数据)。
  6. 对双向链表进行尾删(末尾删除数据)。
  7. 对双向链表就像查找数据。
  8. 对双向链表数据进行修改。
  9. 任意位置的删除和插入数据。
  10. 打印双向链表中的数据。
  11. 销毁双向链表。

三.双向链表的定义

双向链表,就要能够从两个方向遍历,也就是说,每个结点都要有两个指针,一个指向前一个结点,一个指向下一个结点。

因此,我们应这样定义一个双向链表:

typedef struct ListNode
{
	LTDataType data;//保存数据
	struct ListNode* next;//指向下一个结点
	struct ListNode* prev;//指向上一个结点
}LTNode;

四.双向链表的功能

4.0双向链表结点申请

为了不让开辟的空间在函数栈帧销毁时被销毁掉,我们应在堆上开辟一块空间,因此应用malloc开辟空间。 

LTNode* LTBuyNode(LTDataType x)
{
	LTNode* node = (LTNode*)malloc(sizeof(LTNode));
	if (!node)
	{
		perror("malloc fail");
		exit(1);
	}
	node->data = x;
	//由于要实现循环链表,这里让他们指向自己。
	//如果不是循环链表,让他们指向空即可。
	node->next = node->prev = node;
}

4.1初始化双向链表

初始化双向链表,也就是创建一个哨兵位。

由于每个结点都应有数据,我们用-1表示哨兵位的数据。 

LTNode* LTinit()
{
	LTNode* phead = LTBuyNode(-1);
	return phead;
}

4.2双向链表尾插

插入数据之前,链表必须已经初始化,也就是必须有哨兵位,否则它就不带头了。

在这里由于哨兵位实际上充当了头节点的作用,因此我们不可能会修改头节点的地址,所以传一级指针即可。 

尾插,我们需要更改新结点的prev结点和next结点,哨兵位的prev指针,尾结点的next指针。

如下图所示:下图中的序号是代码中最后四行的顺序,这里在上课时要带着大家重点分析一下。

//插入数据之前,链表必须已经初始化。
//不改变哨兵位的地址,因此传一级即可
//尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = LTBuyNode(x);
	newnode->next = phead;
	newnode->prev = phead->prev;
	phead->prev->next = newnode;//-> 操作符来间接访问该指针指向的内存
	phead->prev = newnode;
}

4.3双向链表尾删

双向链表的尾删,就是删除最后一个结点。 

删除最后一个结点,需要更改倒数第二个结点的next指针,以及哨兵位的prev结点。

//尾删
void LTPopBack(LTNode* phead)
{
	//链表有效而且链表不能只有一个哨兵位
	assert(phead && phead->next != phead);
	//不定义del结点也可以,只不过写出来的代码很难被理解。
	//课堂上有时间的话可以先写不定义del的,然后优化为这段代码。
	LTNode* del = phead->prev;
	del->prev->next = phead;
	phead->prev = del->prev;
	free(del);
	del = NULL;
}

4.4双向链表头插

双向链表的头插,实际上是在哨兵位后面插入一个结点。

我们需要更改新结点的prev和next指针、哨兵位的next指针、原第二个结点的prev结点。

下图中的序号是更改顺序

为什么是这样更改呢?

由于原链表之间相互具有关系,我们如果先修改原链表的话,也就是先完成图中3、4的代码,就会导致哨兵位和头结点之间的关系消失了,后续我们就没有办法使用->进行操作。

而又因为我们创建出的新结点和原链表并没有关系,我们先修改新结点并不会影响到原链表,因此先修改新结点。

在修改完新结点之后,我们就可以着手于修改原链表了,在修改原链表时,我们发现要访问链表中的第二个结点,是需要连续使用->操作符进行间接访问的。所以我们如果先修改->操作符的左操作数,就会导致我们连续使用->操作符时出现错误。因此我们应先修改需要连续使用->操作符的指针。

结论:

表述1:1.先修改与原链表没有关系而且可以直接访问的结点。

             2.然后修改在原链表之中相互联系但是不可以直接访问的结点

            3.最后修改在原链表之中相互联系而且可以直接访问的直接。

表述2:1.先更改新结点,之后更改相关联的结点的指针。

             2.在更改相关联的结点的指针时,需先改变需要通过连续使用—>间接访问的结点的指针。 

//头插
void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = LTBuyNode(x);
	//先修改newnode,再修改相关联的-->写详细解释
	newnode->next = phead->next;
	newnode->prev = phead;

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

4.5双向链表头删

由于刚刚讲解了更改指针的原理和结论,下面只会分析更改的指针,而不会再画图帮助大家理解顺序。  (上课时有时间还是画图带着大家学习,时间不足就直接用结论)

删除一个结点,我们需要更改头结点的next指针以及删除结点的下一个结点的prev结点,由于并没有新结点,因此我们要先更改需要连续->操作的结点,然后再更改不需要连续使用->操作的结点。

void LTPopFront(LTNode* phead)
{
	assert(phead && phead->next != phead);
	LTNode* del = phead->next;
	phead->next = del->next;
	del->next->prev = phead;
	free(del);
	del = NULL;
}

4.6双向链表查找

查找一个数值最为结点,我们直接遍历原链表查找即可。

这里需要注意的是,由于我们的链表是循环的,因此我们while循环内的终止条件为不等于哨兵位。 

LTNode* LTFind(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

4.7修改指定数据

修改指定数据需要我们传入三个参数

1.哨兵位

2.要修改的位置

3.要修改的数据

这个函数的作用为把指定位置的数据修改为指定数据。

void LTModify(LTNode* phead, LTNode* pos, LTDataType x)
{
	assert(phead);
	assert(phead != pos);//防止对头节点操作
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		if (cur == pos)
		{
			cur->data = x;
		}
		cur = cur->next;
	}
}

这个函数可以配合查找数据的函数使用

下面给出一个使用用例:

//将数值为3的位置的数值改为x
void LTModify(phead, LTfind(phead,3),x)

4.8指定插入数据

这里我们实现一个在pos结点之后插入数据的函数。

首先,这个需要我们创建结点。

我们需要更改的指针是我们创建的结点的prev和next指针;以及pos结点的next指针以及pos结点的下一个结点的prev指针。

我们应该先修改新结点的next结点和prev结点,之后修改需要连续使用->操作的指针,最后修改不需要连续使用->操作的指针。

//在pos结点之后插入数据
void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);
	LTNode* newnode = LTBuyNode(x);
	newnode->next = pos->next;
	newnode->prev = pos->prev;
	pos->next->prev = newnode;
	pos->next = newnode;
}

4.9指定删除数据

删除pos位置的结点,我们需要改变的指针是pos的前一个结点的next指针以及pos的下一个结点的prev结点。这两个都需要连续使用->连续访问,因此先修改谁就无所谓了,下面的两行互换位置也无所谓了。

void LTErase(LTNode* pos)
{
	//pos理论上不能是phead,但是这里我们没传phead,没法检验phead
	assert(pos);
	pos->next->prev = pos->prev;
	pos->prev->next = pos->next;
	free(pos);
	pos = NULL;
}

4.10打印双向链表

打印双向链表,我们直接通过一个while循环打印即可。

但是要注意循环的终止条件为pcur!=phead。

LTNode* LTPrint(LTNode* phead, LTDataType x)
{
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		printf("%d->", pcur->data);
		pcur = pcur->next;
	}
	printf("\n");
}

List.h

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int LTDataType;
//定义双向链表节点的结构
typedef struct ListNode
{
	LTDataType data;
	struct ListNode* next;
	struct ListNode* prev;
}LTNode;
//声明双向链表中提供的方法
LTNode* LTBuyNode(LTDataType x);
//初始化
//void LTInit(LTNode** pphead);
LTNode* LTInit();
//销毁
void LTDesTroy(LTNode* phead);
//打印
void LTPrint(LTNode* phead);

//插入数据之前,链表必须已经有了一个哨兵位。
//不改变哨兵位的地址,因此传一级即可
//尾插
void LTPushBack(LTNode* phead, LTDataType x);
//头插
void LTPushFront(LTNode* phead, LTDataType x);

//尾删
void LTPopBack(LTNode* phead);
//头删
void LTPopFront(LTNode* phead);


//在pos位置之后插入数据
void LTInsert(LTNode* pos, LTDataType x);
//删除pos节点
void LTErase(LTNode* pos);
LTNode* LTFind(LTNode* phead, LTDataType x);

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值