双向链表详解

一.双向链表结构

 我们一般所说的双向链表是带头循环双向链表,这里的带头更我们之前的头节点不是一回事。带头链表里的头节点,实际上为哨兵位,哨兵位的头节点种是不存放任何有效数据的,只是站在这里起到放哨的作用。

哨兵位的意义:避免遍历数组时发生死循环。

虽然双向链表的结构比单链表更加复杂,但是实现其实更加简单。

二.双向链表的实现

typedef int LTDataType;
typedef struct ListNode
{
	struct ListNode* prev; //指针保存前1个节点的地址
	LTDataType data;
	struct ListNode* next; //指针保存下1个节点的地址
}LTNode;

在双向链表中,我们需要两个指针一个指向前一个节点,另一个指向下一个节点。这里我们还是以存储整形数据为例。

在链表中,我们存储新的数据是要创造节点的,所以为了方便,我们单独封装一个函数来实现这个功能。

然后就是链表的初始化。 

void ListNodeInit(struct ListNode** pphead);

 为什么这里要使用二级指针呢?

phead是指向哨兵位的指针,因为链表开始为空,所以phead指向空,pphead是指向pphead的指针,我们为了让phead指向哨兵位,需要改变phead的值,为了改变一级指针的值,我们就需要使用二级指针。

void ListNodeInit(struct ListNode** pphead){
	*pphead = LTNBuyNode(-1);
};

我们直接使用我们前面写的创造节点函数就行,这也是为什么我们在创造节点函数中,要让节点自己指向自己的原因。

 在我们创建完了双链表后,如何打印数据呢?

void LTNPrint(LTNode* phead);
void LTNPrint(LTNode* phead){
    assert(phead);
	LTNode*pcur = phead->next;
	while(pcur != phead){
		printf("%d->",pcur->data);
		pcur = pcur->next;
	}
	printf("\n");
};

在双向链表中,我们知道第一个节点哨兵位是不存放有效数据的,所以我们pcur起始位置就应该是第二个节点,为了避免死循环,当pcur遇到phead的时候就应该停止打印了.

这里与单链表很像,单链表是遇到空停止。

尾插函数:

void ListNodepushback(LTNode* phead, LTDataType x);

在双链表中,尾插数据很简单,我们知道哨兵位的前一个节点就是尾结点,所以不需要遍历数组找到尾结点了。

尾结点有一个特点就是,它的next指针指向哨兵位,它的prev指针是指向倒数第二个节点。

如何插入数据呢?

首先我们先创建一个新的节点。

我们先不改变原本的链表,我们先来改变新的节点,这个节点要称为新的尾结点,那么它的next指针要指向哨兵位,它的prev指针要指向原本的尾结点。 

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

然后我们在看原链表,我们需要将原本的尾结点的next指向新的尾结点,然后就是将哨兵位的prev指针指向新节点,此时我们就完成了尾插数据。 

void ListNodepushback(LTNode* phead, LTDataType x) {
	assert(phead);
	LTNode*newnode = LTNBuyNode(x);
	newnode->next = phead;
	newnode->prev = phead->prev;
	
	phead->prev->next = newnode;//1
	phead->prev = newnode;//2
};

思考一个问题1和2两行代码可不可以交换,如果不可以说明原因。

答案是不可以的。读者可以自行画图理解。这里简单说明一下

如果先执行2,我们就改变了phead的prev指针,这样我们就找不到原本的尾结点了,这样1就不对了。

头插函数:

void ListNodepushFront(LTNode* phead, LTDataType x);

 如何头插数据呢?

同理我们先创建新的节点。

	assert(phead);
	LTNode*newnode = LTNBuyNode(x);

我们还是现在新的链表上进行更改,新节点作为哨兵位的下一个节点,它的next要指向原本的第二个节点,它的prev节点要指向哨兵位。

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

如何我们在改变原链表,原本链表的第二个节点的prev要指向新节点,然后让phead的next指向新节点这样就完成了插入。 

	phead->next->prev = newnode;//1
	phead->next = newnode; //2

 同理思考一下这里的1和2可不可以互换位置?

答案是不可以,如果互换我们就找不到原本的第二个节点了。

尾删函数:

void ListNodepopback(LTNode* phead){
	assert(phead);
	assert(phead->next != phead);
	LTNode* delnode = phead->prev;
	
	delnode->prev->next = phead;
	phead->prev = delnode->prev;
	
	free(phead->prev);
	delnode = NULL;
}

为了方便我们使用,我们不能直接上来就释放要删除的节点,不然我们就无法找到倒数第二个节点了,所以我们先存储起来。

我们要让倒数第二个节点的next指向phead使其称为新的尾结点,然后再将phead的prev指针指向它,这样就完成了删除,最后释放掉delnode。

注:开头的断言多了一个,双链表中的节点个数至少要是一个,我们的哨兵位是不能删除的,所以我们要进行,断言以防止将哨兵位删掉了。

头删函数

void ListNodepopFront(LTNode* phead);

 头删就是删除哨兵位的下一个节点,所以为了防止删除哨兵位,所以我们也要断言一下。

void ListNodepopFront(LTNode* phead) {
	assert(phead);
	assert(phead->next != phead);
	LTNode* delnode = phead->next;
	delnode->next->prev = phead;
	phead->next = delnode->next;
	free(phead->prev);
	delnode = NULL;
}

 接下来就是指定位置插入数据的函数了。

但是在我们删除数据之前我们需要先找到这个位置才行,所以我们要完成查找函数。

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

查找到时就返回这个节点的地址,否则就返回空。

//在pos之后插入数据
void LTInsert(LTNode* pos, LTDataType x) { 
	if(pos)
		ListNodepushFront(pos,x);
};
//在pos之前插入数据
void LTInsertbefore(LTNode* pos, LTDataType x) {
	if (pos)
		ListNodepushback(pos,x);
};

通过前面的头插尾插函数,我们不难发现头插起始就是在哨兵位的后面插入一个数据

尾插就是在哨兵位值前插入一个数据,

所以我们可以直接使用这两个函数,将pos当作哨兵位。

void LTErase(LTNode* pos) {
	if (pos)
		ListNodepopFront(pos->prev);
};

头删是删除哨兵位之后的节点,那么我们要删除pos位置上的数据,起始就是将pos->prev当作头节点删除其后位置的数据就行。

 销毁双链表

void LTDestory(LTNode* phead) {
	assert(phead);
	LTNode* pcur = phead->next;
	LTNode* next = pcur->next;
	while (pcur != phead) {
		free(pcur);
		pcur = next;
		next = next->next;
	}
	free(phead);
	phead = NULL;
};

销毁双链表,需要释放掉所以节点,为了防止释放后无法找到下一个节点,还需要一个指针来保存下一个节点。 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/554280.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【C++从练气到飞升】08---模板

🎈个人主页:库库的里昂 ✨收录专栏:C从练气到飞升 🎉鸟欲高飞先振翅,人求上进先读书。 目录 一、泛型编程 什么是泛型编程: 二、函数模板 1. 函数模板概念 2. 函数模板格式 3. 函数模板的原理 4. 函数模板的实例…

Nginx part2.2

目录 如何用Nginx搭建多网址服务器? 基于ip地址的虚拟主机 1. 先建立存储网页的目录 2.进行子配置 3.编写.conf文件 基于端口号的虚拟主机 基于域名的虚拟主机 如何用Nginx搭建多网址服务器? 有些网站,ip不同,域名不同&…

格林兰岛和南极洲的流域边界文件下载

(1)南极流域系统边界和掩蔽区 下图显示了由戈达德冰面高程小组使用ICESat数据开发的南极分水岭。我们对西南极冰盖(系统18-23和1)、东南极冰盖(系统2-17)和南极半岛(系统24-27)的定…

案例与脚本实践:DolphinDB 轻量级实时数仓的构建与应用

DolphinDB 高性能分布式时序数据库,具有分布式计算、事务支持、多模存储、以及流批一体等能力,非常适合作为一款理想的轻量级大数据平台,轻松搭建一站式的高性能实时数据仓库。 本教程将以案例与脚本的方式,介绍如何通过 Dolphin…

LevelDB源码阅读笔记(1、整体架构)

LevelDB源码阅读笔记(1、整体架构) LeveDB源码笔记系列: LevelDB源码阅读笔记(0、下载编译leveldb) LevelDB源码阅读笔记(1、整体架构) 前言 对LevelDB源码的博客,我准备采用总…

ragflow知识库使用案例

参考: https://github.com/infiniflow/ragflow/blob/main/README_zh.md 支持丰富的文件类型,包括 Word 文档、PPT、excel 表格、txt 文件、图片、PDF、影印件、复印件、结构化数据, 网页等。 运行步骤: 1、确保 vm.max_map_count 不小于 262144 【更多】: 如需确认 vm.…

【大数据】分布式文件系统HDFS

目录 1.什么是分布式文件系统 2.HDFS的特点 3.HDFS的核心概念 4.HDFS的体系结构 5.HDFS的配置建议 6.HDFS的局限性 7.HDFS的存储机制 7.1.数据冗余机制 7.2.错误与恢复 8.HDFS数据读写过程 1.什么是分布式文件系统 分布式文件系统是整个大数据技术的基础&#xff0c…

单位个人信息宣传这样投稿审核轻松出稿快

在我担任单位信息宣传员的初期阶段,每月的对外信息宣传任务就像一座大山横亘在前,尤其是与媒体对接、投稿发表的工作,更是充满了挑战与艰辛。那段时光,我如同一个摸索前行的独行者,在浩瀚的媒体海洋中“摸着石头过河”。 我曾经花费大量的时间逐一查找各类媒体联系方式,通过电话…

短视频去水印解析接口 可测试

短视频解析聚合接口80多个热们短视频平台。可测试 接口开发文档: 返回格式: JSON 请求方式: GET/POST 示例请求地址:https://www.dspqsy.vip/spapi?keykey&url短视频url 请求参数说明: 字段必填类型说明url是…

良友:献上今天(打开心窗说亮话)- 情绪的秘密

目录 一 二 三 四 五 六 七 八 九 十 十一 十二 十三

C/C++中程序内存区域划分

总结C/C中程序内存区域划分 C/C程序内存分配的几个区域: 1. 栈区(stack):在执⾏函数时,函数内局部变量的存储单元都可以在栈上创建,函数执⾏结束时 这些存储单元⾃动被释放。栈内存分配运算内置于处理器的…

【Vue3】StoresTorefs:简化状态管理的实用工具

💗💗💗欢迎来到我的博客,你将找到有关如何使用技术解决问题的文章,也会找到某个技术的学习路线。无论你是何种职业,我都希望我的博客对你有所帮助。最后不要忘记订阅我的博客以获取最新文章,也欢…

淘宝京东商品详情API接口:打造高效电商数据交互新体验

淘宝京东商品详情API接口:打造高效电商数据交互新体验 随着电商行业的迅猛发展,商家们对于商品详情数据的获取和更新需求日益增长。为满足这一需求,淘宝和京东两大电商巨头纷纷推出了商品详情API接口,为商家提供了高效、便捷的数…

uni-app 小兔鲜儿 Day 6(有作业)

​ 黑马程序员uni-app 小兔鲜儿 项目及bug记录&#xff08;下&#xff09; Day 6&#xff08;有作业&#xff09; 包含视频中提到的作业及最终琐屑代码 Day 6 填写订单页面 相关琐屑代码 <script setup lang"ts"> import { computed, ref } from vue impo…

玩转OurBMC第六期:OpenBMC之传感器配置及使用

栏目介绍&#xff1a;“玩转OurBMC”是OurBMC社区开创的知识分享类栏目&#xff0c;主要聚焦于社区和BMC全栈技术相关基础知识的分享&#xff0c;全方位涵盖了从理论原理到实践操作的知识传递。OurBMC社区将通过 “玩转OurBMC” 栏目&#xff0c;帮助开发者们深入了解到社区文化…

光纤和铜缆:了解不同通信媒介的优势

在现代通信技术中&#xff0c;光纤和铜缆是两种主要的数据传输媒介。它们各有优势和局限性&#xff0c;但都在我们的日常生活中扮演着不可或缺的角色。 左侧&#xff08;网络跳线&#xff09;右侧&#xff08;光纤跳线&#xff09; 一、光纤的原理与优势 ADOP光纤跳线 光纤通信…

LeetCode 1.两数之和(HashMap.containsKey()、.get、.put操作)

给定一个整数数组 nums 和一个整数目标值 target&#xff0c;请你在该数组中找出 和为目标值 target 的那 两个 整数&#xff0c;并返回它们的数组下标。 你可以假设每种输入只会对应一个答案。但是&#xff0c;数组中同一个元素在答案里不能重复出现。 你可以按任意顺序返回…

U盘惊现USBC乱码文件?别担心,这里有救星!

在数字化时代&#xff0c;U盘作为便捷的数据存储工具&#xff0c;在我们的日常生活和工作中扮演着至关重要的角色。然而&#xff0c;有时我们可能会遭遇一个令人头疼的问题——U盘突然出现了USBC乱码文件。这些乱码文件不仅使得U盘中的数据无法正常读取&#xff0c;还可能意味着…

【氮化镓】GaN HEMTs结温和热阻测试方法

文章《Temperature rise detection in GaN high-electron-mobility transistors via gate-drain Schottky junction forward-conduction voltages》&#xff0c;由Xiujuan Huang, Chunsheng Guo, Qian Wen, Shiwei Feng, 和 Yamin Zhang撰写&#xff0c;发表在《Microelectroni…

鸿蒙Next和鸿蒙4.0开发者如何选择

目录 一、 开头一句话重点落在鸿蒙原生开发&#xff0c;也就是ArkUI、Ability、ArkTS、ArkWeb、ArkData等。不管将来是鸿蒙Next2.0或者鸿蒙6.0都游刃有余。 二、 鸿蒙4.0与鸿蒙Next的共性共性概述详细分析总结 三、HarmonyOS Next与HarmonyOS 4的主要区别内核与兼容性设备与应用…
最新文章