【数据结构详解】——线性表之双向链表(动图详解)

📖 前言:本期我们将讨论链表的另外一种结构————双向链表,相比单链表那可谓是优势多多。


🎓 作者:HinsCoder
📦 作者的GitHub:代码仓库
📌 往期文章&专栏推荐:

  1. 【C语言详解】专栏
  2. 【C语言详解】——函数栈帧的创建与销毁(动图详解)
  3. 【数据结构详解】——线性表之顺序表(多图详解)
  4. 【数据结构详解】——线性表之单链表(动图详解)

🕒 1. 单链表的缺点

上一期我们学习了单链表(单向不带头非循环链表),发现它虽然相比顺序表是有一定的优势的,但是它的缺陷也是不可忽视的。

  • 假如找一个数据只能从前向后依次遍历,而且链表中一旦一个结点的地址丢失了,这个结点往后所有的结点都找不到了;
  • PopBack(尾删),Insert(任插),Erase(任删)的时间复杂度都是O(N),都是需要从头开始找前一个结点;
  • 几乎每个接口都需要判断链表是否为空,比较麻烦。

针对这些缺陷的解决方案是什么呢?————就是双链表(双向带头循环链表)

🕒 2. 链表的分类

链表分别有六个特点:

  • 单向、双向
  • 带头、不带头
  • 循环、非循环

⚡ 注意: 头结点(哨兵位)是不能存储有效数据的

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

🕘 2.1 带头结点(哨兵位)的优点

不管链表如何,都有一个头结点在那里,但是这个头结点不存储有效数据,尾插的话直接链接在头结点后面就行,这样我们几乎用不到二级指针了,因为我们并不会改变外面的指针,而是在这个结点后面去链接,哪怕链表为空,所以带头结点的链表尾插判断会更简单。

  1. 头删也是如此,如果是不带头单链表,删除一个数据的话,需要定义一个del结点,让del指向被删结点,之后将被删结点后一个与头指针链接起来,然后释放del,由于需要动pHead,因此需要二级指针。
  2. 如果是带头结点链表的话,删除第一个结点,然后将头结点链接到第二个结点上即可,无需二级指针

带头与否单链表头删函数动画
⌛ 小结:

  • 带头结点不需要改变传过来的指针,也就意味着不需要传二级指针。
  • 头结点是不存储有效数据的,更不能存储链表的长度。

虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构:

  1. 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
  2. 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了。

🕒 3. 双向带头循环链表的接口实现

🕘 3.1 定义双链表

typedef int DLTDataType;
typedef struct DListNode
{
	struct DListNode* next;
	struct DListNode* prev;
	DLTDataType data;
}DLTNode;

🕘 3.2 初始化双链表

说起初始化,我们应该会马上想到以下代码的传参方式,

void DListInit(DLTNode** pphead);
int main()
{
	DLTNode* plist = NULL;
	DListInit(&plist);

	return 0;
}

我们思考一下,既然带头结点的链表都不需要传二级指针,那么在初始化这块传二级指针岂不是很突兀?我们想想看,有什么不用传二级指针的方式。

想到了,返回值不就行了。

上代码

DLTNode* DListInit()
{
	DLTNode* guard = (DLTNode*)malloc(sizeof(DLTNode));
	if (guard == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}

	guard->next = guard;	//记得指向自己
	guard->prev = guard;

	return guard;
}
int main()
{
	//DLTNode* plist = NULL;
	//DListInit(&plist);
	DLTNode* plist = DListInit();
	return 0;
}

🕘 3.3 创建新结点

思路:x就是待插入数据,前驱和后驱指针先置为空,方便链接。

DLTNode* BuyDLTNode(DLTDataType x)
{
	DLTNode* node = (DLTNode*)malloc(sizeof(DLTNode));
	if (node == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}

	node->next = NULL;	//刚malloc的结点置空就好
	node->prev = NULL;
	node->data = x;
	return node;

}

🕘 3.4 尾插双链表

以前我们的单链表都需要找尾,时间复杂度为O(n),现在带头双链表找尾可谓易如反掌,直接phead->prev就找到了

思路:如图链接形成循环,不需要判空哦~

在这里插入图片描述

void DListPushBack(DLTNode* phead, DLTDataType x)
{
	assert(phead);

	DLTNode* newnode = BuyDLTNode(x);
	DLTNode* tail = phead->prev;

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

🕘 3.5 打印双链表

⚡ 注意:因为不像单链表的结束标记是NULL,因为是循环结构,所以这里的结束标记为返回到头结点就结束。

思路:打印是不需要打印phead的,让cur指向第一个数据的结点,当cur=phead的时候结束,这样把空链表的情况也考虑到了。

void DListPrint(DLTNode* phead)
{
	assert(phead);
	printf("phead<=>");

	DLTNode* cur = phead->next;
	while (cur != phead)
	{
		printf("%d<=>", cur->data);
		cur = cur->next;
	}
	printf("\n");
}

🕘 3.6 头插双链表

观察下面的代码,这样写对吗❓

void DListPushFront(DLTNode* phead, DLTDataType x)
{
	assert(phead);
	
	DLTNode* newnode = BuyDLTNode(x);
	phead->next = newnode;
	newnode->prev = phead;
	
	newnode->next = phead->next;
	phead->next->prev = newnode;
}

这样先写phead->next = newnode;这句代码就会把后续结点给弄丢了,要注意顺序。

void DListPushFront(DLTNode* phead, DLTDataType x)
{
	assert(phead);
	
	// 先链接newnode和phead->next之间的关系
	DLTNode* newnode = BuyDLTNode(x);
	newnode->next = phead->next;
	phead->next->prev = newnode;
	phead->next = newnode;
	newnode->prev = phead;
}

还有更优的代码,可以不考虑先后顺序

思路:头插也就是在第一个有效数据前,phead头结点后面插入,那么既然是链表就很容易找到phead后面的结点,记做first,然后将newnode和头结点链接上,然后再和first接上。
在这里插入图片描述

void DListPushFront(DLTNode* phead, DLTDataType x)
{
	assert(phead);
	
	// 不关心顺序
	DLTNode* newnode = BuyDLTNode(x);
	DLTNode* first = phead->next;
	phead->next = newnode;
	newnode->prev = phead;
	newnode->next = first;
	first->prev = newnode;
}

🕘 3.7 双链表判空

bool DListEmpty(DLTNode* phead)
{
	assert(phead);
	/*
	if (phead->next == phead)
		return true;
	else
		return false;
	*/		//比较繁琐
	return phead->next == phead;	一步到位
}

🕘 3.8 尾删双链表

思路:尾删和头删也是很相似的,需要两个指针,一个指针指向链表最后一个结点(tail),另外一个指向倒数第二个结点(prev),然后将prev和头结点链接,释放tail。删空的情况也兼顾到了。
在这里插入图片描述

void DListPopBack(DLTNode* phead)
{
	assert(phead);
	assert(!DListEmpty(phead));

	DLTNode* tail = phead->prev;
	DLTNode* prev = tail->prev;

	prev->next = phead;
	phead->prev = prev;
	free(tail);
	tail = NULL;
}

🕘 3.9 头删双链表

思路:定义两个指针,first指向第一个结点,second指向第二个结点,先将头结点phead和第二个数据second链接,然后释放first
在这里插入图片描述

void DListPopFront(DLTNode* phead)
{
	assert(phead);
	assert(!DListEmpty(phead));
	DLTNode* first = phead->next;
	DLTNode* second = first->next;

	phead->next = second;
	second->prev = phead;

	free(first);
	first = NULL;
}

🕘 3.10 统计双链表有效结点个数

思路:遍历统计
⚡ 注意:有些书可能会这么写,在头结点存入结点个数,在类型int情况下没有问题,可一旦用了不是int类型来定义头结点,那就有溢出风险了。

size_t DListSize(DLTNode* phead)
{
	assert(phead);
	size_t n = 0;
	DLTNode* cur = phead->next;
	while (cur != phead)
	{
		n++;
		cur = cur->next;
	}

	return n;
}

🕘 3.11 查找双链表结点

思路:遍历查找

DLTNode* DListFind(DLTNode* phead, DLTDataType x)
{
	assert(phead);

	size_t n = 0;
	DLTNode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->data == x)
		{
			return cur;
		}

		cur = cur->next;
	}

	return NULL;
}

🕘 3.12 任意位置前插入结点

思路:先通过Find查找函数找到pos,并返回的指针,然后找到pos的前一个结点(prev),下一步开始链接。
在这里插入图片描述

void DListInsert(DLTNode* pos, DLTDataType x)
{
	assert(pos);
	DLTNode* prev = pos->prev;
	DLTNode* newnode = BuyDLTNode(x);

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

}

🕤 3.12.1 【复用】尾插双链表

void DListPushBack(DLTNode* phead, DLTDataType x)
{
	assert(phead);
	
	DListInsert(phead, x);
}

🕤 3.12.2 【复用】头插双链表

void DListPushFront(DLTNode* phead, DLTDataType x)
{
	assert(phead);
	
	DListInsert(phead, x);
}

🕘 3.13 任意位置删除结点

思路:用Find返回pos的指针,然后对pos的前后进行标记,分别是prevnext,链接prevnext,释放掉pos即可。
在这里插入图片描述

void DListErase(DLTNode* pos)
{
	assert(pos);

	DLTNode* prev = pos->prev;
	DLTNode* next = pos->next;

	prev->next = next;
	next->prev = prev;
	free(pos);
	pos = NULL;
}

🕤 3.13.1 【复用】尾删双链表

void DListPopBack(DLTNode* phead)
{
	assert(phead);
	
	DListErase(phead->prev);
}

🕤 3.13.2 【复用】头删双链表

void DListPopFront(DLTNode* phead)
{
	assert(phead);
	assert(!DListEmpty(phead));
	
	DListErase(phead->next);
}

🕘 3.13 销毁双链表

思路:

  • 可以传二级,内部置空头结点
  • 也可以考虑用一级指针,让调用DListDestroy的人置空(保持接口的一致性)


void DListDestroy(DLTNode* phead)
{
	assert(phead);

	DLTNode* cur = phead->next;
	while (cur != phead)
	{
		DLTNode* next = cur->next;
		free(cur);
		cur = next;
	}
	free(phead);
	phead = NULL;
}

🕒 4. 完整源码

// DList.h

#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<stdbool.h>

typedef int DLTDataType;
typedef struct DListNode
{
	struct DListNode* next;
	struct DListNode* prev;
	DLTDataType data;
}DLTNode;

//void DListInit(DLTNode** pphead);
DLTNode* DListInit();
void DListDestroy(DLTNode* phead);

void DListPrint(DLTNode* phead);

void DListPushBack(DLTNode* phead,DLTDataType x);
void DListPushFront(DLTNode* phead, DLTDataType x);
void DListPopBack(DLTNode* phead);
void DListPopFront(DLTNode* phead);

bool DListEmpty(DLTNode* phead);
size_t DListSize(DLTNode* phead);

DLTNode* DListFind(DLTNode* phead,DLTDataType x);

// 在pos之前插入
void DListInsert(DLTNode* pos, DLTDataType x);

// 删除pos位置
void DListErase(DLTNode* pos);
// DList.c

#include"DList.h"

DLTNode* DListInit()
{
	DLTNode* guard = (DLTNode*)malloc(sizeof(DLTNode));
	if (guard == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}

	guard->next = guard;	//记得指向自己
	guard->prev = guard;

	return guard;
}

DLTNode* BuyDLTNode(DLTDataType x)
{
	DLTNode* node = (DLTNode*)malloc(sizeof(DLTNode));
	if (node == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}

	node->next = NULL;	//刚malloc的结点置空就好
	node->prev = NULL;
	node->data = x;
	return node;

}

void DListPrint(DLTNode* phead)
{
	assert(phead);
	printf("phead<=>");

	DLTNode* cur = phead->next;
	while (cur != phead)
	{
		printf("%d<=>", cur->data);
		cur = cur->next;
	}
	printf("\n");
}


void DListPushBack(DLTNode* phead, DLTDataType x)
{
	assert(phead);

	DLTNode* newnode = BuyDLTNode(x);
	DLTNode* tail = phead->prev;

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

	//DListInsert(phead, x);
}

void DListPushFront(DLTNode* phead, DLTDataType x)
{
	assert(phead);

	/*
	DLTNode* newnode = BuyDLTNode(x);
	phead->next = newnode;
	newnode->prev = phead;
	//注意顺序
	newnode->next = phead->next;
	phead->next->prev = newnode;
	*/
	/*		先链接newnode和phead->next之间的关系
	DLTNode* newnode = BuyDLTNode(x);
	newnode->next = phead->next;
	phead->next->prev = newnode;
	//正确,但又更优写法
	phead->next = newnode;
	newnod e->prev = phead;
	*/
	// 不关心顺序
	DLTNode* newnode = BuyDLTNode(x);
	DLTNode* first = phead->next;
	phead->next = newnode;
	newnode->prev = phead;
	newnode->next = first;
	first->prev = newnode;

	//DListInsert(phead->next, x);
}

void DListPopBack(DLTNode* phead)
{
	assert(phead);
	assert(!DListEmpty(phead));

	DLTNode* tail = phead->prev;
	DLTNode* prev = tail->prev;

	prev->next = phead;
	phead->prev = prev;
	free(tail);
	tail = NULL;

	//DListErase(phead->prev);
}

void DListPopFront(DLTNode* phead)
{
	assert(phead);
	assert(!DListEmpty(phead));
	DLTNode* first = phead->next;
	DLTNode* second = first->next;

	phead->next = second;
	second->prev = phead;

	free(first);
	first = NULL;

	//DListErase(phead->next);
}

bool DListEmpty(DLTNode* phead)
{
	assert(phead);
	/*
	if (phead->next == phead)
		return true;
	else
		return false;
	*/
	return phead->next == phead;
}

size_t DListSize(DLTNode* phead)
{
	assert(phead);
	size_t n = 0;
	DLTNode* cur = phead->next;
	while (cur != phead)
	{
		n++;
		cur = cur->next;
	}

	return n;
}

DLTNode* DListFind(DLTNode* phead, DLTDataType x)
{
	assert(phead);

	size_t n = 0;
	DLTNode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->data == x)
		{
			return cur;
		}

		cur = cur->next;
	}

	return NULL;
}


void DListInsert(DLTNode* pos, DLTDataType x)
{
	assert(pos);
	DLTNode* prev = pos->prev;
	DLTNode* newnode = BuyDLTNode(x);

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

}

void DListErase(DLTNode* pos)
{
	assert(pos);

	DLTNode* prev = pos->prev;
	DLTNode* next = pos->next;

	prev->next = next;
	next->prev = prev;
	free(pos);
	//pos = NULL;
}

// 可以传二级,内部置空头结点
// 也可以考虑用一级指针,让调用DListDestroy的人置空(保持接口的一致性)

void DListDestroy(DLTNode* phead)
{
	assert(phead);

	DLTNode* cur = phead->next;
	while (cur != phead)
	{
		DLTNode* next = cur->next;
		free(cur);
		cur = next;
	}
	free(phead);
	//phead = NULL;
}
// test.c

#include"DList.h"
void TestList1()
{/*
	DLTNode* plist = NULL;
	DListInit(&plist);*/

	DLTNode* plist = DListInit();
	DListPushBack(plist, 1);
	DListPushBack(plist, 2);
	DListPushBack(plist, 3);
	DListPushBack(plist, 4);

	DListPrint(plist);

	DListPushFront(plist, 10);
	DListPushFront(plist, 20);
	DListPushFront(plist, 30);
	DListPushFront(plist, 40);
	DListPrint(plist);
	DListPrint(plist);

	DListPopBack(plist);
	DListPopBack(plist);
	DListPopBack(plist);
	DListPopBack(plist);
	DListPrint(plist);

	DListPopBack(plist);
	DListPopBack(plist);
	DListPopBack(plist);
	DListPopBack(plist);
	DListPrint(plist);

	//DListPopBack(plist);
	DListDestroy(plist);
	plist = NULL;
}

void TestList2()
{
	DLTNode* plist = DListInit();
	DListPushBack(plist, 1);
	DListPushBack(plist, 2);
	DListPushBack(plist, 3);
	DListPushBack(plist, 4);
	DListPrint(plist);

	DListPopFront(plist);
	DListPopFront(plist);
	DListPrint(plist);

	DListPopFront(plist);
	DListPopFront(plist);
	DListPrint(plist);

	DListDestroy(plist);
	plist = NULL;
}

void TestList3()
{
	DLTNode* plist = DListInit();
	DListPushBack(plist, 1);
	DListPushBack(plist, 2);
	DListPushBack(plist, 3);
	DListPushBack(plist, 4);
	DListPrint(plist);

	// ...
}

int main()
{
	TestList2();

	return 0;
}

OK,以上就是本期知识点“双链表”的知识啦~~ ,感谢友友们的阅读。后续还会继续更新,欢迎持续关注哟📌~
🎉如果觉得收获满满,可以点点赞👍支持一下哟~

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值