【数据结构】双链表

本文介绍了如何创建和使用带头结点的双向链表,包括链表结构的声明、初始化、增删查改等基本操作,强调了哨兵位的作用以及头插、尾插、头删、尾删的实现逻辑,并提供了相应的C语言实现代码。此外,文章还讨论了如何在特定位置插入和删除节点的方法,以及链表的销毁过程。
摘要由CSDN通过智能技术生成

简介

双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。这里,我们介绍一下带头结点的双向链表的创建和使用。

双链表实现

声明

所有链表都一样,我们需要进行结构体的声明,才可以进行创建,所以,我们得到下面的头文件:

#pragma once
#define _CRT_SCURE_NO_WARNINGS 1
#include <stdio.h>
#include <string.h>
#include <stdbool.h>
#include<assert.h>

typedef int LTDataType;
typedef struct ListNode
{
	struct ListNode* next;
	struct ListNode* prev;
	LTDataType data;
}LTNode;

LTNode* LTInit();
bool LTEmpty(LTNode* phead);
void LTPrint(LTNode* phead);
void LTPushBack(LTNode* phead, LTDataType x);
void LTPopBack(LTNode* phead);
void LTPushFront(LTNode* phead, LTDataType x);
void LTPopFront(LTNode* phead);
LTNode* LTFind(LTNode* phead, LTDataType x);
//在pos之前插入
void LTInsert(LTNode* pos, LTDataType x);
//删除pos位置的值
void LTErase(LTNode* pos);
void LTDestroy(LTNode* phead);

创建链表

接下来,我们就可以进行双链表的创建

LTNode* BuyLTNode(LTDataType x)
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		return;
	}
	newnode->data = x;
	newnode->next = NULL;
	newnode->prev = NULL;
	return newnode;
}

LTNode* LTInit()
{
	LTNode* phead = BuyLTNode(-1);
	phead->next = phead;
	phead->prev = phead;
}

我们在这里介绍的带头结点的双向链表的头结点,实际上就是哨兵位,因为有了哨兵位的设置,使我们的结构整体上的增删查改简单了很多,同时,哨兵位不需要进行给值,也就是说phead的data并不重要,可以随便给值,当然,如果你的链表类型是int,也可以用phead的data进行计数,不过并不推荐,我们可以写一个计数函数。

链表的功能

创建链表后,我们就可以开始写这个双链表的增删查改了,我们可以先编译一下判断是否为空和打印的函数,将他们写在前面,方便后面的函数使用。

这两个函数的实现很简单,判空函数只需要使用布尔类型,返回头结点的下一位即可,如果不为空则为TRUE,为空为FALSE,打印函数我们可以设置一个变量cur进行链表的遍历,通过循环打印每个节点的数据。

bool LTEmpty(LTNode* phead)
{
	assert(phead);//记得断言
	return phead->next == phead;
}

void LTPrint(LTNode* phead)
{
	assert(phead);
	printf("烧饼<==>");
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		printf("%d<==>", cur->data);
		cur = cur->next;
	}
	printf("\n");
}

关于断言的问题,我在上一篇文章 ☞ 单链表 ☜ 中做了一些解释,感兴趣可以去看看 :D

 好,接下来就可以开始写增删查改了,首先是插入功能,链表都会有头插和尾插的功能,头插,是将新节点插入到哨兵位后面,也就是phead->next,要实现这个功能,我们要理清逻辑关系,如图所示

 想要插入这个newnode,就要将它对应的next和prev通通连接上,形成一个链式关系,我们最开始能想到的思路是:直接将phead->next指向newnode,然后让newnode->next指向phead->next,这种思路乍一看好像没什么问题,但我们细品,当我们用phead->next指向newnode时,头结点的指向就改变了,也就是说,本来的phead->next已经找不到了,整个链表就剩下了phead和newnode两个节点,这与我们想要实现的功能不符,因此,为了避免这种情况,我们有两种解决办法:

1.标记法

顾名思义,我们可以创建一个变量point用来存储phead->next节点,这样我们就不用担心将phead->next指向newnode后找不到后面的节点:

void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = BuyLTNode(x);
	LTNode* point = phead->next;
	phead->next = newnode;
	newnode->prev = phead;
	newnode->next = point;
	point->prev = newnode;
}

2.后入法

这个名字是我自己取的,我也不知道该叫他什么XD,总之,这个方法,就是让newnode放弃对phead的想法,先去追求phead->next,链接上phead->next后,再去追求phead,因为哨兵位始终存在,所以我们可以把它放到最后去链接,这样也避免了找不到节点的问题。

void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = BuyLTNode(x);
	newnode->next = phead->next;
	phead->next->prev = newnode;
	phead->next = newnode;
	newnode->prev = phead;
}

接下来是尾插,尾插就要简单许多,同时,设计尾插时,我们可以看到本次介绍的链表的优越性:我们知道,单链表的尾插是需要进行遍历找到尾结点进行插入的,而这个循环双向链表,巧妙地解决了这个问题,phead->prev就指向了这个链表的最后一个节点,这使得我们省去了很多代码和时间,直接用变量存值,然后对他进行逻辑变换即可

 

void LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* tail = phead->prev;
	LTNode* newnode = BuyLTNode(x);
	tail->next = newnode;
	newnode->prev = tail;
	newnode->next = phead;
	phead->prev = newnode;
}

有头插尾插就有头删尾删,删除函数的思路其实跟插入的思路差别不大,不过我们要注意的是,删除函数需要多一个断言:判断链表是否为空。这很好理解,如果链表为空,进行删除会导致出错,毕竟,我们的删除本质上是释放这个节点。关于头删,基本思路是将phead->next节点进行free,那我们可以将他先踢出这个链表,再进行free,即让phead->next->next节点与phead链接,这样说可能有些抽象,为了方便理解,我们可以创建一个变量tail和tailPrev分别存放phead->next和phead->next->next节点,链接好后,再free掉tail,可以结合代码图片理解:

 

void LTPopFront(LTNode* phead)
{
	assert(phead);
	assert(!LTEmpty(phead));
	LTNode* tail = phead->next;
	LTNode* tailPrev = tail->next;
	phead->next = tailPrev;
	tailPrev->prev = phead;
	free(tail);
}

理解了头删后,尾删就变得so easy了,仍然可以通过标记的方法,达到我们的目的,废话不多说,上图上代码!

 

void LTPopBack(LTNode* phead)
{
	assert(phead);
	assert(!LTEmpty(phead));
	LTNode* tail = phead->prev;
	LTNode* tailPrev = tail->prev;
	tailPrev->next = phead;
	phead->prev = tailPrev;
	free(tail);
}

到这里,插入和删除功能基本上介绍完了,接下来我们看一下这个双链表的查找和修改功能。查找函数主要靠的是遍历这个链表,将遍历到的值与要查找的值比较,相等则返回,因此很容易实现

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;
}

实现了查找函数后,我们准备实现修改功能。。先等等,其实我们已经实现了这个功能不是吗,如果想要修改某个节点,只需要调用插入函数和删除函数就行了,但是我们只实现了头尾的插入删除,对于中间的节点我们该怎么办?于是,我们可以写一个在pos之前插入和删除pos位置的函数

基本思路仍然相同,理清逻辑关系就好,在pos之前插入,我们可以用变量表示节点的位置,方便理解,如图

 由于左右两边的节点都有变量存储表示,所以不管先链接哪个都不会出现错误。

void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);
	LTNode* prev = pos->prev;
	LTNode* newnode = BuyLTNode(x);
	prev->next = newnode;
	newnode->prev = prev;
	newnode->next = pos;
	pos->prev = newnode;
}

同理,删除pos节点,只需要将pos位置的节点赶出链表,让他的prev和next的节点相互链接,最后释放pos节点

void LTErase(LTNode* pos)
{
	assert(pos);
	LTNode* posPrev = pos->prev;
	LTNode* posNext = pos->next;
	posPrev->next = posNext;
	posNext->prev = posPrev;
	free(pos);
}

 完成这两个函数后,可以发现,似乎这两个函数可以应用到我们的头尾插入删除功能中,头插和尾插就是在pos位置之前插入嘛,而头删和尾删不就是删除pos位置的值吗?因此,我们可以简化代码,直接调用我们的LTInsert和LTErase函数。

最后,我们来实现一下销毁函数。销毁函数就是将使用完毕的链表进行销毁,其本质也是通过循环遍历节点进行释放,这里就不上图了,直接看代码

void LTDestroy(LTNode* phead)
{
	assert(phead);
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		LTNode* next = cur->next;
		free(cur);
		cur = cur->next;
	}
	free(phead);
}

可能有同学会问,在释放掉这个节点时不用置空吗?答案是不用,至少在这个函数中不用,因为形参的改变不影响实参,在这个函数里你无法改变这个节点的值,如果真的想要严谨一点,我们可以在调用它后对其进行置空。

到此为止,一个较为完整的,带头结点的双向链表就完成了。我们可以在另一个源文件中进行测试,这里是一点测试样例

void test1()
{
	LTNode* plist = LTInit();
	LTPushFront(plist, 1);
	LTPushBack(plist, 2);
	LTPushBack(plist, 3);
	LTPushBack(plist, 4);
	LTPrint(plist);
	LTDestroy(plist);
	plist = NULL;
}

int main()
{
	test1();
	return 0;
}

总结

u1s1,链表真的很重要,对于刚开始进行学习的同学可能会感觉很抽象,但是只要我们不懈的坚持,多看多打代码,到最后我们会发现,原来链表如此简单!

最后,创作不易,还请多多支持!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值