数据结构与算法——双向链表

news2025/5/18 10:55:08

双向链表

  • 定义
    • 链表分类
    • 双向链表:带头双向循环链表
  • 初始化
  • 打印
  • 尾插
  • 头插
  • 尾删
  • 头删
  • 查找
  • 在pos(指定位置)之后插入结点
  • 在pos(指定位置)之前插入结点
  • 删除pos(指定位置)的结点
  • 销毁
  • 顺序表与链表的分析

定义

链表分类

在这里插入图片描述

单向和双向
在这里插入图片描述
带头和不带头
在这里插入图片描述
带头是指存在一个头结点(哨兵位),但是不存储任何有效数据,用来占位子
注意:单链表是不带头的,方便理解,有时候称为第一个结点为头结点,实际上是没有头结点的

循环和不循环

在这里插入图片描述

双向链表:带头双向循环链表

在这里插入图片描述
组成:保存的数据 + 指向下一个结点的指针 + 指向上一个节点的指针

单链表为空时,指向第一个节点的指针为空,双链表为空时,链表中只有一个头结点
在这里插入图片描述

初始化

法一:定义一个双向链表的结构体,然后将指向双向链表的指针plist作为形参开始初始化然后返回

法二:通过函数的返回值来返回

函数的声明
//结构的定义
typedef int LTDataType;
typedef struct ListNode {
	LTDataType data;
	struct ListNode* next;
	struct ListNode* prev;
}LTNode;

//初始化(法一)
void LTInit(LTNode** pphead);
//法二:
//LTNode* LTInit();
函数的实现
#include"List.h"

//申请空间
LTNode* LTBuyNode(LTDataType x)
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	if (newnode == NULL)
	{
		perror("malloc fail!");
		exit(1);
	}
	newnode->data = x;
	newnode->next = newnode->prev = newnode;

	return newnode;
}

//初始化(法一)
void LTInit(LTNode** pphead)
{
	assert(pphead);
	*pphead = LTBuyNode(-1);
}

/*法二
LTNode* LTInit()
{
	LTNode* phead = LTBuyNode(-1);
	return phead;//返回双向链表的头结点
}*/
函数的测试
#include"List.h"

void test01()
{  //法一:
	LTNode* plist = NULL;//单链表为空时,指向第一个节点的指针为空
	                     //双链表为空时,链表中只有一个头结点!
	LTInit(&plist);
	/*法二:
	LTNode* plist = LTInit();
	*/
}

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

法一:相当于你提着一个空的油桶(一个形参)去买油,然后给你装满油;
法二:相当于你空手去买油,然后连油带桶给你(推荐这种)。

打印

单链表的打印是从第一个结点开始往后遍历,且第一个结点不能为空while(pcur),但是双链表的第一个结点(哨兵位)就不储存数据,所以当还是用相同的办法会陷入死循环,如何解决?
将哨兵位的下一个结点定为pcur,向后遍历,停止的标志为pcur = phead

//打印
void LTPrint(LTNode* phead)
{
	LTNode* pcur = phead->next;
	while (pcur != phead) //将哨兵位的下一个结点定为pcur,向后遍历,停止的标志为pcur = phead
	{
		printf("%d -> ", pcur->data);
		pcur = pcur->next;
	}
	printf("\n");
}

尾插

双向链表中尾插,头结点不会发生改变(哨兵位),所以传一级指针。

phead指向链表的第一个结点,通过phead->prev找到尾结点,然后申请一个新结点(newnode),当尾插进来,受到影响的指针有“尾结点的next指针不再指向头结点,而是指向newnode;头结点的prev指针不再指向尾结点,而是指向newnode”,最后newnode的next指针指向头结点。

但是修改的顺序是?先修改对原链表没有影响的结点,因为先修改头结点的prev指针指向newnode后你就找不到原来的尾结点了;通常一般先改变newnodeprev和next指针,最后改头结点受到影响的。

//尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = LTBuyNode(x);
	newnode->prev = phead->prev;
	newnode->next = phead;

	phead->prev->next= newnode;

	phead->prev = newnode;
}

头插

newnode放在哨兵位的后面,如果放在哨兵位的前面则相当于尾插。

phead指向链表的第一个结点,通过phead->next找到哨兵位下一个结点,然后申请一个新结点(newnode),当头插进来,受到影响的指针有“头结点的next指针不再指向哨兵位下一个结点,而是指向newnode;哨兵位下一个结点的prev指针不再指向头结点,而是指向newnode”,最后newnode的next指针指向头结点。

但是修改的顺序是?先修改对原链表没有影响的结点,因为先修改头结点的next指针指向newnode后你就找不到原来哨兵位下一个结点了;通常一般先改变newnodeprev和next指针,最后改头结点受到影响的。

//头插
void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = LTBuyNode(x);
	newnode->next = phead->next;
	newnode->prev = phead;

	phead->next->prev = newnode;

	phead->next = newnode;
}

尾删

删除phead->prev指针,思考能不能直接释放掉?
如果直接释放掉,phead->prev和phead->prev->prev->next均会变成野指针,所以要先将phead->prev->prev->next指向头结点,然后将头结点的prev指针指向phead->prev->prev->next。删除之前要将此结点存起来del = phead->prev,执行完上述操作后释放掉,并且置位空。

//判断链表是否为空:只有一个头结点,记得在头文件声明
bool LTEmpty(LTNode* phead)
{
	assert(phead);
	return phead->next == phead;
} // 返回true为空,反回flase不为空

//尾删
void LTPopBack(LTNode* phead)
{
	assert(!LTEmpty(phead));
	LTNode* del = phead->prev;

	del->prev->next = phead;
	phead->prev = del->prev;

	free(del);
	del = NULL;
}

头删

删除phead->next指针,先将del->next->prev指向头结点,然后将头结点的next指针指向del->next。删除之前要将此结点存起来del = phead->next,执行完上述操作后释放掉,并且置位空。

//判断链表是否为空:只有一个头结点
bool LTEmpty(LTNode* phead)
{
	assert(phead);
	return phead->next == phead;
} // 返回true为空,反回flase不为空

//头删
void LTPopFront(LTNode* phead)
{
	assert(!LTEmpty(phead));
	LTNode* del = phead->next;

	del->next->prev = phead;
	phead->next = del->next;

	free(del);
	del = NULL;

}
以上时间复杂度全是O(1),不需要遍历

查找

定义一个pcur为phead的next结点,然后进行遍历查找,找到了就返回查找的值,没有找到就返回空,循环的条件是pcur不走到头结点。

//查找,记得添加到头文件
LTNode* LTFind(LTNode* phead, LTDataType x)
{
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		if (pcur->data == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	return NULL;
}

在pos(指定位置)之后插入结点

先修改对原链表没有影响的结点
受到影响的结点:pos,pos->next这两个结点
还是先改newnode指针的指向,然后改pos->next指向的结点,最后改pos结点

//在pos位置后插入结点
void LTInsertBack(LTNode* pos, LTDataType x)
{
	assert(pos);
	LTNode* newnode = LTBuyNode(x);

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

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

在pos(指定位置)之前插入结点

先修改对原链表没有影响的结点
受到影响的结点:pos,pos->prev这两个结点
还是先改newnode指针的指向,然后改pos->prev指向的结点,最后改pos结点

//在pos位置前插入结点
void LTInsertFront(LTNode* pos, LTDataType x)
{
	assert(pos);
	LTNode* newnode = LTBuyNode(x);

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

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

删除pos(指定位置)的结点

//删除pos位置的结点
void LTErase(LTNode* pos)
{
	assert(pos);
	pos->next->prev = pos->prev;
	pos->prev->next = pos->next;

	free(pos);
	pos = NULL;
}

销毁

定义一个pcur指向头结点的下一个结点的地址,当pcur不等于头结点(哨兵位)时,进入循环,将pcur的下一个结点先保存下来,然后销毁pcur,然后让pcur走到刚刚保存的下一个结点,就这样进行遍历,最后销毁完全部后在手动销毁头结点并且置位空。

//链表的销毁,因为形参会影响实参,所以传二级指针
void LTDestroy(LTNode** pphead)
{
	LTNode* pcur = (*pphead)->next;
	while (pcur != *pphead)
	{
		LTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	free(*pphead);
	*pphead = NULL;
}
void test01()
{
	LTDestroy(&plist);
}

但是这个定义的函数违背了接口一致性?因为其他的所有函数都是传的一级指针,此处传的二级,如何解决?

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

因为这样不能改变形参的值,需要手动置空

void test01()
{
	LTDestroy(plist);
	plist = NULL;
}

顺序表与链表的分析

对比维度顺序表单链表双向链表
存储结构物理连续(数组)节点 + 单向指针节点 + 前驱/后继双指针
随机访问效率O(1)O(n)O(n)
头插/头删效率O(n)(需移动元素)O(1)O(1)
尾插/尾删效率O(1)(若预留空间)O(n)(需遍历)O(1)(直接访问尾节点)
任意位置插入删除O(n)O(n)(需找前驱节点)O(1)(已知位置时)
空间开销仅数据数据 + 1个指针/节点数据 + 2个指针/节点
内存灵活性需预分配,可能浪费动态分配,无浪费动态分配,无浪费
逆向遍历支持(但效率同正向)不支持支持(O(n))
适用场景高频访问、数据量稳定频繁头操作、未知数据量需要双向操作(如LRU缓存)

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MODBUS RTU调试助手使用方法详解

一、软件简介 485调试助手是一款常用的串口通信调试工具,专门用于RS-485总线设备的测试、调试和通信监控。它支持多种串口参数设置,提供数据收发功能,是工业现场调试的必备工具之一。 二、软件安装与启动 1. 系统要求 Windows 7/10/11操作…

自由学习记录(60)

Lecture 16 Ray Tracing 4_哔哩哔哩_bilibili 老师说的“高频采样”问题是什么? 现在考虑一个特殊情况: ❗ 一个像素内,图像信号变化很剧烈(高频): 比如: 细网格纹理 马赛克背景 很高频的…

现代计算机图形学Games101入门笔记(三)

三维变换 具体形式缩放,平移 特殊点旋转。这里涉及到坐标系,先统一定义右手坐标系,根据叉乘和右手螺旋判定方向。这里还能法线Ry Sina 正负与其他两个旋转不一样。这里可以用右手螺旋,x叉乘z,发现大拇指朝下&#xff0…

WeakAuras Lua Script <BiaoGe>

WeakAuras Lua Script <BiaoGe> 表格拍卖插件WA字符串 表格字符串代码&#xff1a; !WA:2!S3xA3XXXrcoE2VH9l7ZFy)C969PvDpSrRgaeuhljFlUiiSWbxaqXDx(4RDd0vtulB0fMUQMhwMZJsAO5HenLnf1LPSUT4iBrjRzSepL(pS)e2bDdWp5)cBEvzLhrMvvnAkj7zWJeO7mJ8kYiJmYiImYF0b(XR)JR9JRD…

chrome 浏览器插件 myTools, 日常小工具。

1. 起因&#xff0c; 目的: 比如&#xff0c;chatgpt, google&#xff0c; 打开网页&#xff0c;就能直接输入文字&#xff0c;然后 grok 就不行&#xff0c;必须用鼠标点一下&#xff0c;才能输入文字。 对我而言&#xff0c;是个痛点&#xff01;写个插件&#xff0c;自动点…

智慧校园(含实验室)智能化专项汇报方案

该方案聚焦智慧校园(含实验室)智能化建设,针对传统实验室在运营监管、环境监测、安全管控、排课考勤等方面的问题,依据《智慧校园总体框架》等标准,设计数字孪生平台、实验室综合管理平台、消安电一体化平台三大核心平台,涵盖通信、安防、建筑设备管理等设施,涉及 395 个…

第三十四节:特征检测与描述-SIFT/SURF 特征 (专利算法)

一、特征检测:计算机视觉的基石 在计算机视觉领域中,特征检测与描述是实现图像理解的核心技术。就像人类通过识别物体边缘、角点等特征来认知世界,算法通过检测图像中的关键特征点来实现: 图像匹配与拼接 物体识别与跟踪 三维重建 运动分析 其中,SIFT(Scale-Invariant F…

【前端优化】vue2 webpack4项目升级webpack5,大大提升运行速度

记录一下过程 手里有个老项目&#xff0c;vue2webpack4 项目很大&#xff0c;每次运行、运行都要将近10分钟 现在又要往里面写很多东西&#xff0c;再不优化&#xff0c;开发着会更难受&#xff0c;所以决定先将它升级至webpack5 最初失败的尝试 直接在项目里安装了webpack5 但…

Nginx应用场景详解与配置指南

1. 什么是Nginx&#xff1f; Nginx&#xff08;发音为"engine-x"&#xff09;是一个高性能的HTTP和反向代理服务器&#xff0c;也是一个IMAP/POP3/SMTP代理服务器。它以高性能、稳定性、丰富的功能集、简单的配置和低资源消耗而闻名。 2. Nginx的主要应用场景 2.1 …

vue2 切换主题色以及单页面好使方法

今天要新增一个页面要根据不同公司切换不同页面主题色&#xff0c;一点一点来&#xff0c;怎么快速更改 el-pagination 分页组件主题色。 <el-pagination :page-size"pageSize" :pager-count"pageCount"layout"sizes, prev, pager, next, jumper,…

JavaScript【6】事件

1.概述&#xff1a; 在 JavaScript 中&#xff0c;事件&#xff08;Event&#xff09;是浏览器或 DOM&#xff08;文档对象模型&#xff09;与 JavaScript 代码之间交互的一种机制。它代表了在浏览器环境中发生的特定行为或者动作&#xff0c;比如用户点击鼠标、敲击键盘、页面…

STM32F10xx 参考手册

6. 什么是寄存器 本章参考资料&#xff1a;《STM32F10xx 参考手册》、《STM32F10xx数据手册》、 学习本章时&#xff0c;配合《STM32F10xx 参考手册》“存储器和总线架构”及“通用I/O(GPIO)”章节一起阅读&#xff0c;效果会更佳&#xff0c;特别是涉及到寄存器说明的部分。…

TIFS2024 | CRFA | 基于关键区域特征攻击提升对抗样本迁移性

Improving Transferability of Adversarial Samples via Critical Region-Oriented Feature-Level Attack 摘要-Abstract引言-Introduction相关工作-Related Work提出的方法-Proposed Method问题分析-Problem Analysis扰动注意力感知加权-Perturbation Attention-Aware Weighti…

Redis 发布订阅模式深度解析:原理、应用与实践

在现代分布式系统架构中&#xff0c;实时消息传递机制扮演着至关重要的角色。Redis 作为一款高性能的内存数据库&#xff0c;其内置的发布订阅(Pub/Sub)功能提供了一种轻量级、高效的消息通信方案。本文将全面剖析 Redis 发布订阅模式&#xff0c;从其基本概念、工作原理到实际…

飞帆控件 post or get it when it has get

我在这里分享两个链接&#xff1a; post_get_it 设计 - 飞帆 有人看出来这个控件是干什么用吗&#xff1f; 控件的配置&#xff1a;

SQL里where条件的顺序影响索引使用吗?

大家好&#xff0c;我是锋哥。今天分享关于【SQL里where条件的顺序影响索引使用吗&#xff1f;】面试题。希望对大家有帮助&#xff1b; SQL里where条件的顺序影响索引使用吗&#xff1f; 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 在 SQL 查询中&#xff0c;W…

SAP学习笔记 - 开发豆知识02 - com.sap.cds.services.cds.CdsService 废止,那么用什么代替呢?

我看很多人代码里面用的都是这个CdsService类&#xff0c;可以自从2.0版本往上这个类就没了啊。 它的代替是什么呢&#xff1f;用的CqnService 那么怎么查的呢&#xff1f; 我也是下载好几个包&#xff0c;解压看&#xff0c;然后发现这里还可以直接看&#xff0c;挺方便的。…

OpenResty 深度解析:构建高性能 Web 服务的终极方案

引言 openresty是什么&#xff1f;在我个人对它的理解来看相当于嵌入了lua的nginx; 我们在nginx中嵌入lua是为了不需要再重新编译,我们只需要重新修改lua脚本,随后重启即可; 一.lua指令序列 我们分别从初始化阶段&#xff0c;重写/访问阶段&#xff0c;内容阶段&#xff0c;日志…

什么是路由器环回接口?

路由器环回接口&#xff08;LoopbackInterface&#xff09;是网络设备中的一种逻辑虚拟接口&#xff0c;不依赖物理硬件&#xff0c;但在网络配置和管理中具有重要作用。以下是其核心要点&#xff1a; 一、基本特性 1.虚拟性与稳定性 环回接口是纯软件实现的逻辑接口&#x…

【MySQL进阶】如何在ubuntu下安装MySQL数据库

前言 &#x1f31f;&#x1f31f;本期讲解关于如何在ubuntu环境下安装mysql的详细介绍~~~ &#x1f308;感兴趣的小伙伴看一看小编主页&#xff1a;GGBondlctrl-CSDN博客 &#x1f525; 你的点赞就是小编不断更新的最大动力 &#x1f3…