[数据结构初阶]双链表

news2024/10/7 8:29:49

目录

双链表定义

初始化

创建节点 

尾插

​编辑

尾删

头插

头删

打印

查找

pos插入

头插复用

尾插复用

 pos删除

头删复用

尾删复用

判空

size

销毁

 完整代码


前面我们学习了单链表,但按照带头不带头(哨兵)和循环不循环我们可以有四种单链表,双链表也是如此,我们最常用到的是无头单向非循环链表带头双向循环链表,今天我们给大家讲解这种双链表的相关知识,让大家对链表有一个更深层次的理解。

双链表定义

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

初始化

        思考一下,双链表的初始化该怎么定义?

        我们不妨对比一下单链表和顺序表的初始化,顺序表是一块连续储存结构,他需要通过结构体使size=0完成初始化(capacity可以分配初始空间),而单链表是一个一个节点通过指针连结起来的,在创建新节点后,我们就直接将它的指针置空,可以说基本不需要写一个初始化函数。

        而带头循环双链表不同,它的初始化要先创建出哨兵节点为了构成循环,它的两个指针必须指向自己

创建节点 

 首先我们写一个创建节点函数:

LTNode* BuyListNode(LTDataType x)
{
	LTNode* node = (LTNode*)malloc(sizeof(LTNode));//开空间
	if (node == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	LTNode* next = nullptr;
	LTNode* prev = nullptr;
	node->data = x;
	return node;
}

因为改变了指针,我们用返回值接收。 

接着让两个结构体两个成员指针指向自身(哨兵节点),完成初始化。

LTNode* LTInit()
{
	LTNode* phead = BuyListNode(-1);//数据无意义
	phead->next = phead;
	phead->prev = phead;
	return phead;
}

因为初始化只需一次,我们这里也用一级指针返回接收,当然,传递指针地址改变指针也行。

尾插


 

尾插的实现很简单,不需要像单链表一样遍历找尾,直接通过头部的prev节点就能进行尾的插入。而且后面的增删改操作都不需要传二级指针(头节点不变)。

void LTPushBack(LTNode* phead, LTDataType x)//尾插
{
	assert(phead);//避免人为传错

	LTNode* newnode = BuyListNode(x);
	LTNode* tail = phead->prev;
	tail->next = newnode;

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

刚才我们将头节点自己指向自己的目的现在大家应该能体会到了。 

        不要去看代码,要自己去想这个图,这样你会觉得连结的过程十分丝滑。这里为什么没有将新节点指针置空呢,因为双向节点的每一个指针都指向前/后的对象,所以不存在指向空指针问题。

尾删

尾删类似于尾插,同样妙的一点是,删除操作也仅仅是改变指针指向并释放空间,并不复杂。

 只有一个节点:


 

当删除到只剩一个节点时,tail的前一个节点正好就是头节点,此时删除后就又恢复到了初始状态。

void LTPopBack(LTNode* phead)//尾删
{
	assert(phead);
	assert(phead->next != phead);//防止删掉自己
	LTNode* tail = phead->prev;
	LTNode* tailPrev = tail->prev;

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

头插

头插需要注意要保存下一个节点的地址或者先将newnode连结下一个节点再用phead连结newnode,避免找不到后面的节点。

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

	phead->next = newnode;
	newnode->prev = phead;
//顺序无关
	/*LTNode* first = phead->next;
	phead->next = newnode;
	newnode->next = first;
	newnode->prev = phead;
	first->prev = newnode;*/
}

头删

注意判断是否可能删除自身的情况。 

void LTPopFront(LTNode* phead)//头删
{
	assert(phead);
	assert(phead->next != phead);//空
	LTNode* first = phead->next;
	LTNode* second = first->next;
	free(first);

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

打印

我们先来实现一下打印函数并测试一下我们之前的代码。打印可以倒着打印或者正向打印,这里我们实现正向打印。注意部是不能打印的。

void LTPrint(LTNode* phead)//打印
{
	assert(phead);
	LTNode* cur = phead->next;//正向
	while (cur != phead)//结束条件
	{
		printf("%d ", cur->data);
		cur = cur->next;
	}
		printf("\n");
}

尾插尾删:

 头插头删:

查找

查找是从头节点的下一个开始。找到数据后可以通过返回的指针对数据进行修改。

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 nullptr;//找不到返回空
}

 测试:

pos插入

与单链表不同的是,这里不需要传二级头指针就能实现插入,且不用循环找它的前一个节点(Prev指针)。这也体现了这种链表的优势,

void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);

	LTNode* prev = pos->prev;
	LTNode* newnode = BuyListNode(x);
	// prev newnode pos
	prev->next = newnode;
	newnode->prev = prev;
	newnode->next = pos;
	pos->prev = newnode;
}

头插复用


void LTPushFront(LTNode* phead, LTDataType x)//头插
{
	assert(phead);
    LTInsert(phead->next, x);
}

尾插复用

尾插复用一般人认为是在最后一个节点复用,实际上我们传递的是头节点。因为我们的插入位置是pos的前一个节点,如果传递最后一个节点就插入到它前面去了,而传递phead,通过它的prev节点就能找到最后一个节点并插入,请注意这一点。

void LTPushBack(LTNode* phead, LTDataType x)//尾插
{
	assert(phead);
    LTInsert(phead,x);
}

 pos删除

删除只需找到pos位置的前后节点然后就可以删除啦。在复用尾删头删的时候也是直接传要删除的节点位置就行了。

void LTErase(LTNode* pos)//pos删除
{
	assert(pos);
	LTNode* prev = pos->prev;
	LTNode* next = pos->next;
	free(pos);

	prev->next = next;
	next->prev = prev;
}

头删复用

void LTPopFront(LTNode* phead)//头删
{	
    assert(phead);
	assert(phead->next != phead);//空
    LTErase(phead->next);
}

尾删复用

void LTPopBack(LTNode* phead)//尾删
{
	assert(phead);
	assert(phead->next != phead);
    LTErase(phead->prev);
}

测试:

判空

bool LTEmpty(LTNode* phead)//判空
{
	return phead->next == phead;
}

size

这个接口一般不常用,遍历一遍就能得到长度。大家想想可不可以用哨兵位记录它的长度呢?

size_t LTSize(LTNode* phead)
{
	assert(phead);
	size_t size = 0;
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		++size;
		cur = cur->next;
	}
	return size;
}

销毁

和单链表销毁的流程一样,注意最后头节点也要释放掉

和单链表一样,带头双向链表也要传递二级指针才能使指针正确置空(顺序表是直接将成员指针置空)。为了保持一级指针一致性,我们也可以在上一层栈帧最后进行置空操作。

void LTDestroy(LTNode* phead)
{
	assert(phead);

	LTNode* cur = phead->next;
	while (cur != phead)
	{
		LTNode* next = cur->next;
		free(cur);

		cur = next;
	}

	free(phead);
}

测试: 

 完整代码

//List.h
#pragma once

#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <stdbool.h>//判空
typedef int LTDataType;
typedef struct ListNode
{
	struct ListNode* prev;
	struct ListNode* next;
	LTDataType data;
}LTNode;
LTNode* BuyListNode(LTDataType x);//创建节点

LTNode* LTInit();//初始化

void LTPushBack(LTNode* phead, LTDataType x);//尾插
void LTPopBack(LTNode* phead);//尾删
void LTPushFront(LTNode* phead, LTDataType x);//头插
void LTPrint(LTNode* phead);//打印
void LTPopFront(LTNode* phead);//头删

LTNode* LTFind(LTNode* phead, LTDataType x);//查找
void LTInsert(LTNode* pos, LTDataType x);//pos插入
void LTErase(LTNode* pos);//pos删除
bool LTEmpty(LTNode* phead);//判空
size_t LTSize(LTNode* phead);
void LTDestroy(LTNode* phead);//销毁
//List.cpp
#include"List.h"

LTNode* BuyListNode(LTDataType x)
{
	LTNode* node = (LTNode*)malloc(sizeof(LTNode));//开空间
	if (node == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	LTNode* next = nullptr;
	LTNode* prev = nullptr;
	node->data = x;
	return node;
}
LTNode* LTInit()
{
	LTNode* phead = BuyListNode(-1);//数据无意义
	phead->next = phead;
	phead->prev = phead;
	return phead;
}
void LTPushBack(LTNode* phead, LTDataType x)//尾插
{
	assert(phead);//可以不断言

	LTNode* newnode = BuyListNode(x);
	LTNode* tail = phead->prev;
	tail->next = newnode;

	newnode->prev = tail;
	newnode->next = phead;
	phead->prev = newnode;
	//LTInsert(phead, x);
}
void LTPopBack(LTNode* phead)//尾删
{
	assert(phead);
	assert(phead->next != phead);//防止删掉自己
	LTNode* tail = phead->prev;
	LTNode* tailPrev = tail->prev;

	phead->prev = tailPrev;
	tailPrev->next = phead;
	free(tail);
	LTErase(phead->prev);
}
void LTPushFront(LTNode* phead, LTDataType x)//头插
{
	assert(phead);
	ListNode* newnode = BuyListNode(x);
	newnode->next = phead->next;
	phead->next->prev = newnode;

	phead->next = newnode;
	newnode->prev = phead;
	//顺序无关
	/*LTNode* first = phead->next;
	phead->next = newnode;
	newnode->next = first;
	newnode->prev = phead;
	first->prev = newnode;*/
	//LTInsert(phead->next, x);
}

void LTPopFront(LTNode* phead)//头删
{
	assert(phead);
	assert(phead->next != phead);//空
	LTNode* first = phead->next;
	LTNode* second = first->next;
	free(first);

	second->prev = phead;
	phead->next = second;
	//LTErase(phead->next);
}
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 nullptr;//找不到返回空
}
void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);

	LTNode* prev = pos->prev;
	LTNode* newnode = BuyListNode(x);
	// prev newnode pos
	prev->next = newnode;
	newnode->prev = prev;
	newnode->next = pos;
	pos->prev = newnode;
}
void LTErase(LTNode* pos)//pos删除
{
	assert(pos);
	LTNode* prev = pos->prev;
	LTNode* next = pos->next;
	free(pos);

	prev->next = next;
	next->prev = prev;
}
void LTPrint(LTNode* phead)//打印
{
	assert(phead);
	LTNode* cur = phead->next;//正向
	while (cur != phead)//结束条件
	{
		printf("%d ", cur->data);
		cur = cur->next;
	}
	printf("\n");
}
bool LTEmpty(LTNode* phead)//判空
{
	return phead->next == phead;
}
size_t LTSize(LTNode* phead)
{
	assert(phead);
	size_t size = 0;
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		++size;
		cur = cur->next;
	}
	return size;
}
void LTDestroy(LTNode* phead)
{
	assert(phead);

	LTNode* cur = phead->next;
	while (cur != phead)
	{
		LTNode* next = cur->next;
		free(cur);

		cur = next;
	}

	free(phead);
}

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

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

相关文章

知识蒸馏学习记录

最近在学习降噪处理不良天气的算法过程中&#xff0c;接触到了知识蒸馏&#xff0c;该算法作为一个深度学习通用算法&#xff0c;不仅广泛应用在自然语言处理方面&#xff0c;在计算机视觉等领域也广受追捧。 概要 简单来说&#xff0c;知识蒸馏就是将一个大的教师网络萃取到…

Nginx虚拟机主机

Nginx虚拟机主机 简述 虚拟主机是一种特殊软硬件技术&#xff0c;将网络上每一台计算机分成多个虚拟主机&#xff0c;每个虚拟主机可独立对外提供www服务&#xff0c;实现一台主机对外提供多个web服务&#xff0c;每个虚拟主机之间独立&#xff0c;互不影响。 配置位置 既可以在…

基于深度学习的高精度塑料瓶检测识别系统(PyTorch+Pyside6+YOLOv5模型)

摘要&#xff1a;基于深度学习的高精度塑料瓶检测识别系统可用于日常生活中或野外来检测与定位塑料瓶目标&#xff0c;利用深度学习算法可实现图片、视频、摄像头等方式的塑料瓶目标检测识别&#xff0c;另外支持结果可视化与图片或视频检测结果的导出。本系统采用YOLOv5目标检…

自动化面试题5

一、Modbus通信485/232/422的优缺点。 &#xff08;1&#xff09;RS232和RS422是全双工的&#xff0c;RS485是半双工的。 &#xff08;2&#xff09;RS485与RS232仅仅是通讯的物理协议&#xff08;即接口标准&#xff09;有区别&#xff0c;RS485是差分传输方式&#xff0c;R…

Spring6 AOT 提前编译

文章目录 1、AOT概述1.1、JIT与AOT的区别1.2、Graalvm1.3、Native Image 2、Native Image构建过程2.1、GraalVM安装&#xff08;1&#xff09;下载GraalVM&#xff08;2&#xff09;配置环境变量&#xff08;3&#xff09;安装native-image插件 2.2、安装C的编译环境&#xff0…

法规标准-ISO 23374标准解读

ISO 23374是做什么的&#xff1f; ISO 23374全名为智能交通系统-自动代客泊车系统(AVPS) 第一部分&#xff1a;系统框架、自动驾驶要求和通信接口&#xff0c;针对AVPS的系统框架及功能要求、通信接口进行介绍&#xff0c;由于通信接口涉及功能实现&#xff0c;但此处介绍较为…

ubuntu20.04 磁盘故障,然后重装22.04

ubuntu20.04 磁盘故障&#xff0c;然后重装22.04 重装原因开机自启动不需要使用sudo 软件截图 flameshot输入法 fcitx5 重装原因 编译程序报错 /usr/include/x86_64-linux-gnu/bits/signum.h:26:10: fatal error: /usr/include/x86_64-linux-gnu/bits/signum-generic.h: 结构需…

VUE L ∠脚手架 配置代理 ⑩⑧

目录 文章有误请指正&#xff0c;如果觉得对你有用&#xff0c;请点三连一波&#xff0c;蟹蟹支持✨ V u e j s Vuejs Vuejs初识 V u e C L I VueCLI VueCLI C L I CLI CLI V u e Vue Vue配置代理 C L I CLI CLI配置方法一 C L I CLI CLI配置方法二 C L I CLI CLI V u …

经典文献阅读之--VIP-SLAM(紧耦合RGB-D视觉惯性平面SLAM)

0. 简介 现有的视觉SLAM很多的算法让仍然是基于特征提取的方法来完成地图的建立&#xff0c;而RGB-D传感器的算法仍然是主要基于稀疏点的SLAM系统&#xff0c;这就导致在构建稠密点云地图的时候需要保持大量的地图点来建模环境。大量的地图点给我们带来了很高的计算复杂性&…

Python 进阶(二):Python使用ORM框架peewee操作MySQL数据库

Python使用ORM框架peewee操作数据库 前言1. 安装Peewee库并初始化数据库2. 创建数据库连接3. 定义数据表模型类4. 连接数据库并创建表5. 操作数据库5.1 插入数据5.2 查询数据5.3 更新数据5.4 删除数据 6. 聚合查询 前言 本文基于MySQL8.x版本的学习&#xff0c;python版本基于…

美客多、Newegg卖家如何提高店铺销量?测评自养号的重要性

作为美客多和Newegg平台上的卖家&#xff0c;提高店铺销量是卖家取得商业成功的关键。珑哥今天来说一些有效的策略&#xff0c;帮助卖家增加销售额并提升店铺的知名度和竞争力。 优化产品页面&#xff1a; 优秀的产品页面可以吸引更多买家并促进销售。确保产品标题准确、吸引人…

经典文献阅读之--NICE-SLAM(SLAM的神经隐含可扩展编码)

0. 简介 对于深度学习而言&#xff0c;NeRF一定是最近两年最火的工作之一了&#xff0c;**NeRF&#xff08;Neural Radiance Fields&#xff09;**是最早在2020年ECCV会议上的Best Paper&#xff0c;其将隐式表达推上了一个新的高度&#xff0c;仅用 2D 的 posed images 作为监…

stm32配置基本定时功能

Clock Source参数说明 内部时钟源是由STM32F103芯片内部的RC振荡器提供的。它的频率为8MHz&#xff0c;可以通过PLL倍频器进行倍频&#xff0c;最高可达到72MHz。 Internal clock Division No Division&#xff1a;不分频&#xff0c;即系统时钟直接作为时钟信号。Divided by 2…

ModaHub AI模型社区:向量数据库CPU 版 Milvus和GPU 版 Milvus 版本比较

目录 CPU 版 Milvus 版本比较 概述 CPU 版 Milvus 支持的索引类型 浮点型向量 二值型向量 GPU 版 Milvus 版本比较 概述 GPU 版 Milvus 支持的索引类型 浮点型向量 二值型向量 CPU 版 Milvus 版本比较 概述 Milvus 提供两个发行版本&#xff1a;CPU 版本和 GPU 版本…

学生成绩管理系统(Python+数据库)

文章目录 前言MySQL部分1. 导入信息2. 演示说明 Python程序设计部分1. 连接数据库2. 登录界面3. 注册界面4. 主界面5. 查询信息6. 修改密码7. 成绩分析7.1 通过学号查询成绩7.2 通过课程号查询成绩 7. 主函数 尾声 前言 用Python和数据库一起实现了一个简单的学生成绩管理系统…

GPDB-内核特性-UDP流量控制

GPDB-内核特性-UDP流量控制 GPDB是在开源PostgreSQL基础上&#xff0c;采用MPP架构的关系型分布式数据库&#xff0c;具有强大的大规模数据分析任务处理能力。采用Shared-Nothing架构&#xff0c;整个集群由多个数据节点&#xff08;segment&#xff09;和控制节点&#xff08;…

Python爬虫 从小白到高手 各种最新案例! Urllib Xpath

Urllib 1.什么是互联网爬虫&#xff1f; 如果我们把互联网比作一张大的蜘蛛网&#xff0c;那一台计算机上的数据便是蜘蛛网上的一个猎物&#xff0c;而爬虫程序就是一只小蜘蛛&#xff0c;沿着蜘蛛网抓取自己想要的数据 解释1&#xff1a;通过一个程序&#xff0c;根据Url(http…

实训五:数据库安全控制 - 创建用户

创建用户 第1关&#xff1a;创建用户任务描述相关知识增加用户修改用户名修改用户密码删除用户 编程要求测试说明参考代码 第2关&#xff1a;创建用户-练习任务描述相关知识编程要求测试说明参考代码 第1关&#xff1a;创建用户 任务描述 本关任务&#xff1a;创建用户 user1…

你不知道的HTML属性,让你的网页更加响应式

前言 当我们谈论 HTML 时&#xff0c;通常会想到一些常见的属性&#xff0c;如 class、id、href 等等。但是&#xff0c;HTML 还有许多你可能从未听说过的属性&#xff0c;这些属性可以让你的网页更加丰富多彩。在本文中&#xff0c;我将介绍一些你可能不知道的 HTML 属性&…

SPI通信协议SPI通信外设

目录 SPI 介绍 硬件电路 移位示意图 软件SPI SPI时序基本单元 SPI时序​编辑 W25Q64 硬件电路 W25Q64框图 Flash操作注意事项 指令集 硬件SPI SPI框图 主模式全双工连续传输 非连续传输 SPI 介绍 硬件电路 推挽输出&#xff0c;高低电平都有很强的驱动能力&…