数据结构——双链表详解(超详细)

前言:

  小编在之前已经写过单链表的创建了,接下来要开始双链表的讲解了,双链表比单链表要复杂一些,不过确实要比单链表更好进行实现!下面紧跟小编的步伐,开启今天的双链表之旅!

目录

1.概念和结构

1.1.哨兵位

1.2.双链表的结构

2.双向链表的实现 

2.1.初始化双链表

2.2.双链表的尾插

2.3.双链表的打印

 ·2.4.双链表的头插

2.5.双链表的尾删

2.6.双链表的头删

2.7.查找双链表的数据

2.8.在指定位置之后插入数据

2.9. 删除指定结点的数据

 3.0.销毁双链表

3.链表的分类

1.单向不带头不循环链表(也就是小编前面写的单链表):

2.单向带头不循环链表:

​编辑 3.单向不带头循环链表:

4.单向带头循环链表 :

​编辑 5.双向带头循环链表:

​编辑

6.双向不带头不循环链表:

7.双向带头不循环链表:

​编辑 8.双向不带头循环链表:

4.代码展示

4.1.List.h

4.2.List.c

总结


正文:

1.概念和结构

  在前面的博客中小编讲述了单链表,其实单链表的全程叫做,单向不带头不循环链表,这里有很多读者朋友就好奇了,单向和不循环还是比较容易理解的,那么不带头又是什么意思呢,其实这里就牵扯到了一个全新的知识点:哨兵位!

1.1.哨兵位

  哨兵位这个名字看起来是很威武的,其实了解它之后就知道它仅仅就是名字威武点,它才是真正意义上的头结点,哨兵位是不存数据的,它的存在仅仅就是为了说明这个链表是带头的,是用来“放哨的”,小编在讲述单链表的时候曾多次提到了头结点这个名字,那个时候头结点是链表第一个结点,这个叫法是不太规范的,不过为了让读者朋友更好的去理解链表的知识,小编于是就直接头结点来代替链表第一个结点了,不过各位读者朋友一定要清楚真正的头结点其实就是哨兵位。

1.2.双链表的结构

  首先,双链表是带头的链表,说明它的第一个结点是不存数据的,是哨兵位,双链表也是双向的,说明它的结点不仅仅存放着下个结点的数据,也存放着的前一个结点的数据,看着非常复杂,其实有了这个特点以后,在写双链表代码的时候要比单链表要简单许多,等会各位就清楚小编为什么这么说了,它的最后一个特点是循环,这个小编在之前讲解环形链表习题的时候就牵扯到了一点循环链表,下面小编给上双链表的结构图以便大家在等会写代码的时候可以更好的理解:

  小编已经讲述了双链表的概念和结构了,下面就是各位最喜欢的节目:双链表代码的书写,下面跟随小编的脚步,开启我们今天的代码之旅喽~

2.双向链表的实现 

  在写双链表之前,这里小编还是说一下,我们依旧需要创建两个源文件和一个头文件,头文件是负责声明一下写的函数,一个源文件是负责实现函数功能的,另一个是测试用的,废话不多说开始进入正文:

2.1.初始化双链表

  在初始话双链表之前,我们肯定要写双向链表结点的结构体,这个结构体其实就是比单链表多了一个存放上一个结点的地址,其他都是一样的,下面直接上代码:

typedef int LTDataType;
typedef struct List {
	LTDataType date;
	struct List* next; //存放下一个结点的地址
	struct List* prev; //存放上一个函数结点的地址
}LTNode;

  对于双链表的初始化,这里可不是简简单单的就是把它的下一个结点为空,上一个结点为空了,双链表是带头的,所以其实这里的初始化操作就是创建一个哨兵位,按理说哨兵位是不存放任何数据的,不过我们也是可以给它赋值的,只不过不用它这个值罢了,这里我们就把哨兵位的数据暂时弄为-1,因为这个链表是循环的,所以我们需要让它的下一个结点和上一个结点统一指向自己,这里就可以形成死循环,小编在下面通过图文来让大家去理解: 

  此时我们已经完成了尾插的操作,下面开始进行链表常见操作:尾插操作

2.2.双链表的尾插

  对于插入操作,肯定需要先设置一个新的结点,不然尾插就没有什么结点可插了,对于新节点的创建,小编在之前单链表也写过,不过双链表与单链表最大的不同就是它的下一个结点和上一个结点都要去指向自己,下面直接上手代码图了(这里由于难度不大小编就不在多赘述了):

//先创建一个新的结点(对于任何插入操作都得有这个玩意)
LTNode* LTbuynode(LTDataType x)
{
	LTNode* p1 = (LTNode*)malloc(sizeof(LTNode));
	assert(p1);
	p1->date = x;
	p1->next = p1->prev = p1;
}

  接下来就进行插入操作了,小编经过深思熟虑以后,决定搭配着图文进行配套讲解,感觉这样讲起来不费劲,各位理解起来也不算太费劲,下面小编随机弄个图来进行讲解:

  先创建好结点,创建好后开始进行尾插操作:

  之后我们需要让哨兵位的下一个结点指向新创立的结点newlode,让newlode的上一个结点指向哨兵位,:

   之后我们为了达成循环的效果,我们要让哨兵位的上一个结点指向新结点,让新结点的下一个结点的指针指向哨兵位,此时我们就完成了尾插操作,其实这里小编感觉我给的例子不是很好,各位读者朋友最好用用一个比较长的链表来实现,那个更容易理解,这里我先对各位说声抱歉。

   下面小编给上尾插操作的代码,通过这个代码各位读者朋友就知道为什么小编推荐用长一点的链表来实现这个尾插操作了:

void LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);  //首先哨兵位不能是空的
	LTNode* newnode = LTbuynode(x);
	phead->prev->next = newnode;
	newnode->next = phead;
	newnode->prev = phead->prev;
	phead->prev = newnode;
}

  这个代码值得注意的就是,我们再传参的时候并不是像单链表一样传的二级指针,我们传的是一级指针,因为我们并不会对头结点(哨兵位)做出改变,故不用传地址,这个代码还是比较好理解的,各位读者朋友一定要自己先思考,自己先写一遍代码然后再看小编写的,如果直接复制粘贴的话,写完之后知识也不是你的!小编写这篇文章的目的就是让读者朋友去理解的,也让我自己在复习一遍。

2.3.双链表的打印

  双链表的打印其实是蛮容易的,不过写这个函数的目的就是为了测试其他函数写的对不对,对于双链表的打印,它和单链表的打印有些许不同,因为双链表我·是一个带头的,死循环的链表,所以我们需要通过控制循环条件来实现对于双链表的打印:

  首先我们需要先设置一个结点来代表哨兵位下一个结点(因为哨兵位理论上是不存数据的),然后我们开始循环打印结点的数据,结束的条件自然就是我们不能让设置的这个指针循环到哨兵位结点,一旦循环到了哨兵位结点,则说明此时已经完成了一轮循环,所以此时我们无须在进行循环,直接结束就好了,由于本小节内容无须图像,所以我们便直接展示代码了:

void LTPrint(LTNode* phead)
{
	LTNode* pour = phead->next;
	while (pour != phead)
	{
		printf("%d->", pour->date);
		pour = pour->next;
	}
	printf("\n");
}

 ·2.4.双链表的头插

  这里一定要注意,双链表的头插可不是把新节点插到哨兵位之前,而是插在哨兵位之后,哨兵位之后的第一个节点才是有实际意义的头一个结点,下面小编用图文的方式来给大家讲述一下头插的原理:

  这里我们需要设置一个新节点,对于设置新节点的代码小编在上面已经讲述清楚了,为了让文章更加清楚,小

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值