数据结构线性表——带头双向循环链表

news/2025/2/22 19:47:29

前言:小伙伴们好久不见啦,上篇文章我们一起学习了数据结构线性表其一的单链表,了解了单链表的不少好处,但是不可能有完美的数据结构,就算是单链表,也会有很多缺点。

那么今天这篇文章,我们就来学习单链表的promax版本——带头双向循环链表


一.什么是带头双向循环链表

关于带头双向循环链表,我们将它拆分为带头、双向、循环、链表四个部分,其中链表我们已经知道是怎么回事了,那我们就来一起结合下图分析前三个概念。

1.带头 

        所谓带头,也就是在链表的开头处,有一个不存放任何数据的头节点,我们通常称其为“哨兵位”。

        那么哨兵位存在的意义是什么呢???

        它可以帮助我们更方便的进行对链表的各种操作。具体好在哪里,我们结合后边实现链表的各种操作来进行展示。

2.双向

        我们前边学习过的单链表,它的每个节点之间只有一条链子相连,并且只能由前一个节点去找到后一个节点

        而双向链表,也就是两个节点之间有两条链子相连,不仅能从前一个找到后一个,也能从后一个去找到前一个

3.循环

        循环,顾名思义,就是将链表头尾也进行连接,形成一个逻辑意义上的环形链表

那么理解完带头双向循环链表的含义之后,我就就一起来看看到底来如何实现它吧。

此后我们将该链表的名字简化为链表


二.双链表的实现

1.双链表的定义

typedef int DLLDataType;
//定义双链表
typedef struct DLinkList
{
	DLLDataType data;
	struct DLinkList* prev;//指向前一个节点
	struct DLinkList* next;//指向后一个节点
}DLLNode;

链表是在单链表的基础上,比它多出一个prev指针去指向前一个节点,还是比较容易理解的。


2.双链表的初始化

//初始化双链表
DLLNode* DLinkListInit()
{
	DLLNode* phead = (DLLNode*)malloc(sizeof(DLLNode));
	if (phead == NULL)
	{
		perror("DLinkListInit->malloc");
	}
	phead->next = phead;
	phead->prev = phead;
	return phead;
}

链表的初始化需要先造出哨兵位考虑到链表为空,并且链表还要循环,所以我们将哨兵位的prev和next都指向自己

    DLLNode* dll = DLinkListInit();

创建一个双链表,我们习惯于运用上述方式。

因为如果用单链表的初始化方式,我们需要用到二级指针,但是我们后续双链表各种功能的操作,完全不和二级指针沾边

所以为了让我们的双链表全部由一级指针完成,选择采用接收函数返回值的方式来创建双链表


3.双链表节点的创建

DLLNode* CreateNewNode(DLLDataType x)
{
	DLLNode* newnode = (DLLNode*)malloc(sizeof(DLLNode));
	if (newnode == NULL)
	{
		perror("CreateNewNode->malloc");
	}
	newnode->data = x;
	newnode->next = NULL;
	newnode->prev = NULL;
	return newnode;
}

链表创建新节点就和单链表差不多啦,要注意的就是不要忘记两个指针置空,防止出现野指针

这样,我们就实现了一个基本的双链表框架,下面来实现双链表的各种基础操作。


 三.双链表的操作

1.双链表的打印

那么为了方便其他功能的测试,我们还是先来实现双链表打印功能:

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

我们还是严格的进行一下assert断言如果phead为空,就说明双链表不存在

这里要注意两点:

1.cur为什么是phead->next???

        不难理解,我们在双链表初始化的时候,给到dll的返回值是哨兵位,但是哨兵位不存储数据,所以要从哨兵位的下一个节点开始。

2.while循环的判断条件

        因为我们是一个可循环链表,所以并不存在cur为空的情况,但是cur最后会重新指向哨兵位,所以当cur == phead时,说明我们已经将双链表遍历一遍了

至于printf函数的内容,只是为了好看哈哈,展示一下:

这样能够让大家更形象的认识双链表


2.双链表的尾插

链表的尾插相较于单链表有什么优势呢???

链表想尾插,首先要进行循环找尾时间复杂度就高了,但是双链表就好办,因为哨兵位的前一个节点就是尾,也就是phead->prev,尾找到之后,就好办了:

//尾插
void DLinkListPushBack(DLLNode* phead, DLLDataType x)
{
	assert(phead);
	DLLNode* newnode = CreateNewNode(x);
	DLLNode* tail = phead->prev;
	tail->next = newnode;
	newnode->next = phead;
	newnode->prev = tail;
	phead->prev = newnode;
}

用tail代替尾,接下来的一顿操作,就是:

旧尾的next指向新尾

新尾的next指向哨兵位

新尾的prev指向旧尾

哨兵位的prev指向新尾

看起来很简单,但是我们知道,链表必须得考虑一下链表是否为空的特例,但是链表不需要

因为双链表如果为空,那就只有哨兵位,哨兵位自己的头尾相连,带入上述代码操作之后,不会有任何错误。


 3.双链表的尾删

尾删就更简单了,只需要找到尾,再通过尾找到尾的前一个节点,再让此节点和哨兵位互连,再将尾free即可:

//尾删
void DLinkListPopBack(DLLNode* phead)
{
	assert(phead);
	DLLNode* tail = phead->prev;
	DLLNode* tailprev = tail->prev;
	phead->prev = tailprev;
	tailprev->next = phead;
	free(tail);
	tail = NULL;
}

尾删要考虑只有一个节点的特例吗,依然不用,因为就算是空链表,进行一顿操作之后,还是让哨兵位自己头尾相连

到这里,小伙伴们是否已经感受到了哨兵位,以及双链表的强势之处啦


4.双链表的头插

头插就和尾插差不多了,这里我直接给出代码,希望小伙伴们可以自己理解掌握哦。

//头插
void DLinkListPushFront(DLLNode* phead, DLLDataType x)
{
	assert(phead);
	DLLNode* head = phead->next;
	DLLNode* newnode = CreateNewNode(x);
	phead->next = newnode;
	newnode->next = head;
	head->prev = newnode;
	newnode->prev = phead;
}

5.双链表的头删

头删也和尾删类似:

//头删
void DLinkListPopFront(DLLNode* phead)
{
	assert(phead);
	DLLNode* head = phead->next;
	DLLNode* headnext = head->next;
	phead->next = headnext;
	headnext->prev = phead;
	free(head);
	head = NULL;
}

6.双链表的查找

如果是查找的话,那我们还得老老实实的从头遍历:

//查找
DLLNode* DLinkListFind(DLLNode* phead,DLLDataType x)
{
	assert(phead);
	DLLNode* cur = phead->next;
	while(cur)
	{
		if (cur->data == x)
			return cur;
		else
			cur = cur->next;
	}
	return NULL;
}

还是要注意这里while循环的条件,和双链表的打印一样


7.双链表的任意插

链表任意位置的插入依然要和查找连用,因为只有查找才能得到pos位置的地址

但是我们这里规定一下,任意插就是pos位置前插

比如说我想在表的第四个位置插入新数据,那我就要把第四个位置空出来,让原来的第四位以及他后边的都老老实实往后退

这样一来,我们就需要找到pos节点的前一个节点,这样方便我们进行操作:

//pos位置插
void DLinkListInsert(DLLNode* pos, DLLDataType x)
{
	assert(pos);
	DLLNode* newnode = CreateNewNode(x);
	DLLNode* posprev = pos->prev;
	posprev->next = newnode;
	newnode->prev = posprev;
	pos->prev = newnode;
	newnode->next = pos;
}

8.双链表的任意删

任意删的形式就和任意插差不多,只是还需要另外记录pos的下一个节点

//pos位置删
void DLinkListEease(DLLNode* pos)
{
	assert(pos);
	DLLNode* posprev = pos->prev;
	DLLNode* posnext = pos->next;
	posprev->next = posnext;
	posnext->prev = posprev;
	free(pos);
    pos = NULL;
}

9.双链表的修改

想要修改数据,还是要用查找操作来找到要修改pos位置的地址,而后就简单了:

//pos位置改
void DLinkListAmend(DLLNode* pos, DLLDataType x)
{
	assert(pos);
	pos->data = x;
}

直接修改data即可。


10.双链表的销毁

链表的销毁,同样是需要遍历对个个空间进行free,值得注意的是,哨兵位也需要销毁

//销毁
void DLinkListDestroy(DLLNode* phead)
{
	assert(phead);
	DLLNode* cur = phead->next;
	while (cur != phead)
	{
		DLLNode* next = cur->next;
		free(cur);
		cur = next;
	}
	free(phead);
	phead = NULL;
}

四.完整代码展示

1.DLinkList.h

#include <stdio.h>
#include <assert.h>
#include <stdlib.h>

typedef int DLLDataType;
//定义双链表
typedef struct DLinkList
{
	DLLDataType data;
	struct DLinkList* prev;
	struct DLinkList* next;
}DLLNode;

//初始化双链表
DLLNode* DLinkListInit();
//打印双链表
void DLinkListPrint(DLLNode* phead);
//创造新节点
DLLNode* CreateNewNode(DLLDataType x);
//尾插
void DLinkListPushBack(DLLNode* phead, DLLDataType x);
//尾删
void DLinkListPopBack(DLLNode* phead);
//头插
void DLinkListPushFront(DLLNode* phead, DLLDataType x);
//头删
void DLinkListPopFront(DLLNode* phead);
//查找
DLLNode* DLinkListFind(DLLNode* phead,DLLDataType x);
//pos位置插
void DLinkListInsert(DLLNode* pos, DLLDataType x);
//pos位置删
void DLinkListEease(DLLNode* pos);
//pos位置改
void DLinkListAmend(DLLNode* pos,DLLDataType x);
//销毁
void DLinkListDestroy(DLLNode* phead);

2.DLinkList.c

#include "DLinkList.h"
//初始化双链表
DLLNode* DLinkListInit()
{
	DLLNode* phead = (DLLNode*)malloc(sizeof(DLLNode));
	if (phead == NULL)
	{
		perror("DLinkListInit->malloc");
	}
	phead->next = phead;
	phead->prev = phead;
	return phead;
}
//打印双链表
void DLinkListPrint(DLLNode* phead)
{
	assert(phead);
	DLLNode* cur = phead->next;
	printf("phead<=>");
	while (cur != phead)
	{
		printf("%d<=>", cur->data);
		cur = cur->next;
	}
	printf("phead\n");
}
//创造新节点
DLLNode* CreateNewNode(DLLDataType x)
{
	DLLNode* newnode = (DLLNode*)malloc(sizeof(DLLNode));
	if (newnode == NULL)
	{
		perror("CreateNewNode->malloc");
	}
	newnode->data = x;
	newnode->next = NULL;
	newnode->prev = NULL;
	return newnode;
}
//尾插
void DLinkListPushBack(DLLNode* phead, DLLDataType x)
{
	assert(phead);
	DLLNode* newnode = CreateNewNode(x);
	DLLNode* tail = phead->prev;
	tail->next = newnode;
	newnode->next = phead;
	newnode->prev = tail;
	phead->prev = newnode;
}
//尾删
void DLinkListPopBack(DLLNode* phead)
{
	assert(phead);
	DLLNode* tail = phead->prev;
	DLLNode* tailprev = tail->prev;
	phead->prev = tailprev;
	tailprev->next = phead;
	free(tail);
	tail = NULL;
}
//头插
void DLinkListPushFront(DLLNode* phead, DLLDataType x)
{
	assert(phead);
	DLLNode* head = phead->next;
	DLLNode* newnode = CreateNewNode(x);
	phead->next = newnode;
	newnode->next = head;
	head->prev = newnode;
	newnode->prev = phead;
}
//头删
void DLinkListPopFront(DLLNode* phead)
{
	assert(phead);
	DLLNode* head = phead->next;
	DLLNode* headnext = head->next;
	phead->next = headnext;
	headnext->prev = phead;
	free(head);
	head = NULL;
}
//查找
DLLNode* DLinkListFind(DLLNode* phead,DLLDataType x)
{
	assert(phead);
	DLLNode* cur = phead->next;
	while(cur)
	{
		if (cur->data == x)
			return cur;
		else
			cur = cur->next;
	}
	return NULL;
}
//pos位置插
void DLinkListInsert(DLLNode* pos, DLLDataType x)
{
	assert(pos);
	DLLNode* newnode = CreateNewNode(x);
	DLLNode* posprev = pos->prev;
	posprev->next = newnode;
	newnode->prev = posprev;
	pos->prev = newnode;
	newnode->next = pos;
}
//pos位置删
void DLinkListEease(DLLNode* pos)
{
	assert(pos);
	DLLNode* posprev = pos->prev;
	DLLNode* posnext = pos->next;
	posprev->next = posnext;
	posnext->prev = posprev;
	free(pos);
	pos = NULL;
}
//pos位置改
void DLinkListAmend(DLLNode* pos, DLLDataType x)
{
	assert(pos);
	pos->data = x;
}
//销毁
void DLinkListDestroy(DLLNode* phead)
{
	assert(phead);
	DLLNode* cur = phead->next;
	while (cur != phead)
	{
		DLLNode* next = cur->next;
		free(cur);
		cur = next;
	}
	free(phead);
	phead = NULL;
}

测试代码大家自行进行测试,这里就不在进行展示啦。


五.总结

链表相比于单链表还是有很大优势的,建议大家在学习过单链表的基础上完全靠自己的写一写双链表,这将会让你对链表知识的掌握更上一层楼!

最后还是提醒大家不要忘记一键三连哦!!!

我们下期再见啦!


http://www.niftyadmin.cn/n/5171251.html

相关文章

0062【Edabit ★☆☆☆☆☆】Arrow Functions

0062【Edabit ★☆☆☆☆☆】Arrow Functions closures higher_order_functions language_fundamentals logic Instructions In the Code tab you will find code that is missing a single character in order to pass the tests. However, your goal is to submit a functi…

python3GUI--QQ音乐By:PyQt5(附下载地址)

文章目录 一&#xff0e;前言二&#xff0e;展示0.播放页1.主界面1.精选2.有声电台3.排行4.歌手5.歌单 2.推荐3.视频1.视频2.分类3.视频分类 4.雷达5.我喜欢1.歌曲2.歌手 6.本地&下载7.最近播放8.歌单1.一般歌单2.自建歌单3.排行榜 9.其他1.搜索词推荐2.搜索结果 三&#x…

Qt界面设计时使各控件依据窗口缩放进行自适应填充的方法——使用布局、Spacer等控件

Qt界面设计时使各控件依据窗口缩放进行自适应填充的方法—使用布局、Spacer等控件 Chapter1 Qt界面设计时使各控件依据窗口缩放进行自适应填充的方法—使用布局、Spacer等控件Chapter2 Qt Creator中布局器详解01. 概述02. 开发环境03. 布局器概述04. 布局属性设置05. 弹簧条属性…

归并排序 merge Sort + 图解 + 递归 / 非递归

归并排序(merge sort)的主要思想是&#xff1a;将若干个有序序列逐步归并&#xff0c;最终归并为一个有序序列二路归并排序(2-way merge sort)是归并排序中最简单的排序方法 &#xff08;1&#xff09;二路归并排序的递归实现 // 二路归并排序的递归实现 void merge(vector&l…

js 根据当前时间往前推15天或往后推15天等(例如当前时间往后15天后的日期。并实现今天、明天、后天、周)

本次分享&#xff0c;在项目中开发车票购买功能需要用到日期筛选 思路&#xff1a; 1、首先获取当前时间戳 2、根据当前时间戳拿到15天后的日期 3、根据天匹配星期几 4、将时间戳转换年、月、日并重组 实现代码 // 获取当前日期 const today new Date();// 往前推15天的…

vue3中的父子间传值

一、父传子 defineProps 父组件传值给子组件主要是由父组件为子组件通过v-bind绑定数值&#xff0c;而后传给子组件&#xff1b;子组件则通过defineProps接收使用。 <template><div class"fa"><div style"margin: 10px;">我是父组件&l…

C语言--输入10个数字,要求输出其中值最大的元素和该数字是第几个数

今天小编带大家了解一下什么是“打擂台”算法。 一.思路分析 可以定义一个数组arr&#xff0c;长度为10&#xff0c;用来存放10个数字&#xff0c;设计一个函数Max&#xff0c;用来求两个数中的较大值&#xff0c; 定义一个临时变量tmparr[0],保存临时最大的值&#xff0c;下标…