数据结构——二叉树2.0

news2024/11/16 1:57:59

 ✅<1>主页:我的代码爱吃辣
📃<2>知识讲解:数据结构——二叉树
🔥<3>创作者:我的代码爱吃辣
☂️<4>开发环境:Visual Studio 2022
💬<5>前言:上期讲了二叉树的相关概念,今天来讲一下二叉树的顺序存储——堆。

目录

📔一.二叉树的存储结构

📕(1)顺序结构

📖(2)链式存储

💰二.二叉树的顺序存储结构的实现

 🪙(1)堆的概念及结构

 🍗(2)堆的实现

🥞1.堆结构的定义初始化,及销毁

🧇2.堆的插入

🧀3.堆的创建

🥣4. 堆的删除:

🍿 5.拿到堆顶数据

🥗 6.堆的数据个数

🍘7.堆的判空

🍙8.TopK问题

🍛三.时间复杂度分析:

🍡(1)向上调整算法,插入建堆的时间复杂苏分析:

 🥟(2)向下调整算法,直接建堆的时间复杂苏分析:

🥡 四.堆排序

🍠(1)排序思想

🍱(2)代码代码:

🍥最后:

我们什么都没有,唯一的本钱就是青春。梦想让我与众不同,奋斗让我改变命运!


📔一.二叉树的存储结构

二叉树一般可以使用两种结构存储,一种顺序结构,一种链式结构。

📕(1)顺序结构

顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。而现实中使用中只有才会使用数组来存储,关于堆我们后面的章节会专门讲解。二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树。

📖(2)链式存储

二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。链式结构又分为二叉链和三叉链,当前我们学习中一般都是二叉链,以后学到高阶数据结构如红黑树等会用到三叉链。

二叉树的二叉链表三叉链表存储结构

// 二叉链
struct BinaryTreeNode
{
	struct BinTreeNode* _pLeft; // 指向当前节点左孩子
	struct BinTreeNode* _pRight; // 指向当前节点右孩子
	BTDataType _data; // 当前节点值域
};
// 三叉链
struct BinaryTreeNode
{
	struct BinTreeNode* _pParent; // 指向当前节点的双亲
	struct BinTreeNode* _pLeft; // 指向当前节点左孩子
	struct BinTreeNode* _pRight; // 指向当前节点右孩子
	BTDataType _data; // 当前节点值域
};

💰二.二叉树的顺序存储结构的实现

普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费而完全二叉树更适合使用顺序结构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。

堆的孩子和父亲下标关系:

 🪙(1)堆的概念及结构

堆是一个可以看成完全二叉树的数组对象,在将它看成完全二叉树时候,完全二叉树的每一个父亲结点的值总是大于(小于)孩子结点的值,我们称之为大堆(小堆)或者大根堆(小根堆)。

堆得性质:

  • 堆中某个节点的值总是不大于或不小于其父节点的值;
  • 堆总是一棵完全二叉树。
小堆结构图

大堆结构图

 🍗(2)堆的实现

🥞1.堆结构的定义初始化,及销毁

由于堆是一个可以看成完全二叉树的数组对象,所以在实现堆及其相关操作的时候,实际上都是对顺序表的操作。

typedef struct Heap
{
    
	HPDateType* a;    //数据域
	int size;         //数据个数
	int capacity;     //容量
}Heap;
//初始化
void HeapInit(Heap* php)
{
	assert(php);
	php->a = NULL;
	php->capacity = php->size = 0;
}
//堆的销毁
void HeapDestroy(Heap* php)
{
	assert(php);
	free(php->a);
	php->capacity = php->size = 0;
}

🧇2.堆的插入

堆的插入需要保证插入以后还是一个堆,所以这里用到了向上调整算法

在数组中就是,插入一个数在数组的尾上,再通过向上调整算法,调整到合适的位置。

在以堆的角度来看(小堆)为例,将新插入的值看作孩子与其父亲位置的值比较,如果比父亲位置的值还要小,那就将该值与父亲位置的值进行交换,交换后将父亲位置当作新的孩子,继续与其父亲位置的值比较,这样一直向上比较并交换,直到父亲位置的值比自己小或该位置已经没有父亲了,调整结束。

 代码:

//交换
void Swap(HPDateType* n1, HPDateType* n2)
{
	HPDateType tmp = *n1;
	*n1 = *n2;
	*n2 = tmp;
}

//判断是否需要扩容
void JudgeCapacity(Heap*php)
{
	assert(php);
	if (php->capacity == php->size)
	{
		int newcaprcity = php->capacity == 0 ? 4 : php->capacity * 2;
		HPDateType* tmp = realloc(php->a, sizeof(HPDateType) * newcaprcity);
		if (tmp == NULL)
		{
			perror("realloc");
			exit(-1);
		}
		php->a = tmp;
		php->capacity = newcaprcity;
	}
}

//向上调整算法
void AdjustUp(HPDateType* a, int size, int child)
{    
    //根据孩子求出父亲
	int parent = (child - 1) / 2;
    //循环调整
	while (child>0) //当孩子没有父亲的时候,调整结束
	{    
        //如果孩子比父亲小,就调整
		if (a[child] < a[parent])
		{    
            //交换孩子和父亲
			Swap(&a[child], &a[parent]);
            //让父亲位置重新左做孩子,继续调整
			child = parent;
			parent = (child - 1) / 2;
		}
		else //父亲比孩子小,调整结束
		{
			break;
		}
	}
}
//堆的插入
void HeapPush(Heap* php, HPDateType x)
{
	assert(php);
    //判断时候需要扩容
	JudgeCapacity(php);
    //在数组的尾上插入数据
	php->a[php->size++] = x;
    //将新插入的数据进行向上调整
	AdjustUp(php->a, php->size,php->size - 1);
}

测试:

void Print(Heap* php)
{
	for (int i = 0; i < php->size; i++)
	{
		printf("%d ", php->a[i]);
	}
}

int main()
{
	//定义
	Heap hp;
	//初始化
	HeapInit(&hp);

	int array[] = { 27,15,19,18,28,34,65,49,25,37 };
	int n = sizeof(array) / sizeof(array[0]);
	//数组数据一次插入堆中
	for (int i = 0; i < n; i++)
	{
		HeapPush(&hp, array[i]);
	}
	//打印堆的内容
	Print(&hp);
	//销毁堆
	HeapDestroy(&hp);
	return 0;
}

 每一次插入都会经过一次向上调整,最后生成的就是妥妥的小堆。

🧀3.堆的创建

下面我们给出一个数组,这个数组逻辑上可以看做一颗完全二叉树,但是还不是一个堆,现在我们通过算法,把它构建成一个堆。根节点左右子树不是堆,我们怎么调整呢?这里我们从倒数的第一个非叶子节点的子树开始调整,一直调整到根节点的树,就可以调整成堆。这里用到的就是向下调整算法。

向下调整算法(大堆为例):从第一个非叶子结点开始,找到其孩子结点中较大的一个与父亲位置进行交换,然后将孩子作为新的父亲,再次比较和交换,直到父亲结点比两个结点的值都大或者已经没有孩子了为止。

从第一个不为叶子的结点开始,一次往前都进行向下调整,最后得到的就是堆。

 代码:

void Swap(HPDateType* n1, HPDateType* n2)
{
	HPDateType tmp = *n1;
	*n1 = *n2;
	*n2 = tmp;
}
//向下调整算法(大堆)
void AdjustDown(HPDateType* arr,int n, int parent)
{
    //找到第一个孩子
	int child = parent * 2 + 1;
	
	while (child < n)//求出的孩子,已经超过数组下标的大小
	{
        //与第二个孩子比较大小
		if (child + 1 < n && arr[child] < arr[child + 1])
		{
			child++;
		}
        //与其父亲的值比较,如果孩子的值比父亲大,就交换
		if (arr[child] > arr[parent])
		{
			Swap(&arr[child], &arr[parent]);
            //将孩子作为新的父亲,并找到新的孩子,继续进行比较
			parent = child;
			child = parent * 2 + 1;
		}
		else//如果父亲的值比孩子的大,此时以该父亲为根的完全二叉树已经是堆
		{
			break;
		}
	}
}
//对已经给出的数组进行建堆
void HeapCreate(Heap* php, HPDateType* arr, int n)
{
	assert(php);
    //初始化堆
	php->a = (HPDateType*)malloc(sizeof(HPDateType) * n);
	php->capacity = n;
	php->size = n;
    //数据拷贝到堆中
	memmove(php->a,arr,sizeof(arr[0]) * n);
	//找到第一个非叶子结点,并依次向下调整
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(php->a, n,i);
	}
}

测试:

void Print(Heap* php)
{
	for (int i = 0; i < php->size; i++)
	{
		printf("%d ", php->a[i]);
	}
}

int main()
{
	//定义
	Heap hp;
	//初始化
	HeapInit(&hp);

	int array[] = { 27,15,19,18,28,34,65,49,25,37 };
	int n = sizeof(array) / sizeof(array[0]);

	HeapCreate(&hp, array, n);
	
	//打印堆的内容
	Print(&hp);
	//销毁堆
	HeapDestroy(&hp);
	return 0;
}

🥣4. 堆的删除:

堆的删除删除堆顶数据,需要保证删除以后还是一个堆,我们通常的做法就是,将堆顶数据与堆的最后数据进行交换,再将堆的数据个数减一,为了保证删除以后还是一个堆,再将堆顶数据进行向下调整。

堆的删除

 代码:

//堆的删除,删除堆顶数据,并保证删除以后仍是堆
void HeapPop(Heap* php)
{
	assert(php);
	assert(php->size > 0);
	Swap(&php->a[0], &php->a[php->size]-1);
	php->size--;

	AdjustDown(php->a, php->size, 0);
}

测试:

void Print(Heap* php)
{
	for (int i = 0; i < php->size; i++)
	{
		printf("%d ", php->a[i]);
	}
	printf("\n");
}

int main()
{
	//定义
	Heap hp;
	//初始化
	HeapInit(&hp);

	int array[] = { 27,15,19,18,28,34,65,49,25,37 };
	int n = sizeof(array) / sizeof(array[0]);

	HeapCreate(&hp, array, n);
	Print(&hp);
	HeapPop(&hp);
	Print(&hp);
	HeapPop(&hp);
	Print(&hp);
	
	//销毁堆
	HeapDestroy(&hp);
	return 0;
}

🍿 5.拿到堆顶数据

//拿到堆顶数据
HPDateType HeapTop(Heap* php)
{
	assert(php);
	assert(php->size > 0);

	return php->a[0];
}

🥗 6.堆的数据个数

//堆的数据个数
int HeapSize(Heap* php)
{
	assert(php);
	return php->size;
}

🍘7.堆的判空

bool HeapEmpty(Heap* php)
{
	assert(php);
	return php->size == 0;
}

🍙8.TopK问题

第一类TopK问题:

假如我们有一组数据,现在的需求就是,找这一组数据中的前k大的k个数。首先将数据建堆,我们可以通过不断地从大堆里面选堆顶数据的数,再将堆顶数据删除,循环选出k个数,这样选出来的就是前k大的k个数。

代码:

//topk问题
void HeapTopK(Heap* php, int k)
{
	assert(php);
	assert(k > 0);

	for (int i = 0; i < k; i++)
	{
		printf("%d ", HeapTop(php));
		HeapPop(php);
	}

	printf("\n");
}

测试:

int main()
{
	//定义
	Heap hp;
	//初始化
	HeapInit(&hp);

	int array[] = { 27,15,19,18,28,34,65,49,25,37 };
	int n = sizeof(array) / sizeof(array[0]);
	HeapCreate(&hp, array, n);

	//选出前3大的3个数据
	HeapTopK(&hp, 3);

	//销毁堆
	HeapDestroy(&hp);
	return 0;
}

 第二类TopK问题:

上述的TopK问题,有的小伙伴想到我们用一个排序,也能解决问题呀,为啥非要建堆呢?但是如果我们的数据量非常的大,数据有100G大小存储在我们的磁盘里面,此时我们再想寻选出TopK,如果使用排序就需要把数据加载到内存中,这个时候电脑的内存是不够加载的,也就无法排序;日过我们使用堆,我们只需要建可以容纳K个数据小堆,每次从磁盘中读取一个数据,读取的数据与堆顶数据比较,去过比堆顶数据大,就将该数据赋值到堆顶,并将堆顶数据进行一次向下调整,保证堆顶数据一直是堆中最小的。这里大家可以想象是打擂台。

注意:这里再选前K大的数的时候,只能建小堆,如果建大堆,我们读取的第一个数就是最大,并且放在了堆顶,后面的数据,就会被堵在外面,堆中的数据就会无法更新。

代码:

//topk 文件大数据量

void TopK_File(int k)
{
	//造数据,造1000个10000以下的随机数
	FILE* pfile = fopen("test.txt", "w");
	srand(time(0));
	for (int i = 0; i < 1000; i++)
	{
		fprintf(pfile, "%d\n", rand() % 10000);
	}
    fclose(pfile);

	//建小堆
	Heap hp;
	HeapInit(&hp);
	int* arr = (int*)malloc(sizeof(arr[0]) * k);
	for (int i = 0; i < k; i++)
	{
		arr[i] = INT_MIN;
	}
	HeapCreate(&hp, arr, k);
	
	//topK 选数
	FILE* pfile_ = fopen("test.txt", "r");	
	for (int i = 0; i < 1000; i++)
	{
		int tmp = 0;
		fscanf(pfile,"%d", &tmp);
		int hptop = HeapTop(&hp);
        //文件中读取的数据与堆顶数据比较
		if (tmp > hptop)
		{
			hp.a[0] = tmp;
			AdjustDown(hp.a, k, 0);
		}
	}
	fclose(pfile_);
	Print(&hp);
    HeapDestroy(&hp);
}

测试:

int main()
{
	//前5大的5个数据
	TopK_File(5);
}

🍛三.时间复杂度分析:

🍡(1)向上调整算法,插入建堆的时间复杂苏分析:

 🥟(2)向下调整算法,直接建堆的时间复杂苏分析:

🥡 四.堆排序

🍠(1)排序思想

利用堆来排序主要有两个步骤:

1.建堆:排升序建大堆,排降序建小堆

2.利用堆删除的思想经行排序

首先我们来针对,排升序建大堆,来解释一下,我们知道大堆堆顶数据,是最大数据,如果我们将顶数据与最够一个数据进行交换,这样我们就可以将最大的数放在了最后,除去最后一个数据,将其他数据按栈顶数据数据进行向下调整。依次这样就可以达到排序的效果。

🍱(2)代码代码:

void HeapSort(int *arr,int n)
{
	//建大堆,选择时间复杂度比较小,向下调整建堆
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(arr, n, i);
	}

	//选数,排序
	int end = n - 1;
	while (end)
	{
        //每次选出堆顶数据往后放
		Swap(&arr[0], &arr[end]);
		AdjustDown(arr, end, 0);
		end--;
	}
}

🍥最后:

我们什么都没有,唯一的本钱就是青春。梦想让我与众不同,奋斗让我改变命运!

 

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

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

相关文章

尚医通-医院接口设置(七)

&#xff08;1&#xff09;后台系统-医院设置接口-需求和准备 &#xff08;2&#xff09;后台系统-医院设置接口-创建基础类 &#xff08;3&#xff09;医院设置接口-查询和逻辑删除 &#xff08;4&#xff09;医院的设置接口-统一返回结果定义 &#xff08;5&#xff09;医…

[附源码]Python计算机毕业设计Django校园代取快递系统

项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等等。 环境需要 1.运行环境&#xff1a;最好是python3.7.7&#xff0c;…

知到/智慧树——大学生心理健康(华东政法大学)参考答案

目录 注&#xff1a;有些图片上传异常&#xff0c;请以参考答案为准&#xff0c;不要以蓝色选项为全部答案。 第一章测试 第二章测试 第三章测试 第四章测试 第五章测试 第六章测试 第七章测试 第八章测试 第九章测试 第十章测试 第十一章测试 第十二章测试 第一章…

传统技术如何阻碍零信任以及如何应对

随着组织采用零信任安全模型&#xff0c;传统技术制造了一些障碍。事实上&#xff0c;根据最近的一项研究&#xff0c;更换或重建现有的遗留基础设施是实施零信任的最大挑战。 通用动力公司的 2022 年零信任研究报告对美国联邦、民事和国防机构的 300 名 IT 和项目经理进行了调…

计算机毕设Python+Vue兴澜幼儿园管理系统(程序+LW+部署)

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

重定向和缓冲区

文章目录一个奇怪的现象缓冲区详解如何理解缓冲区缓冲区是谁给我提供的&#xff1f;缓冲区的源码体现案例&#xff1a;模拟实现FILE结构体第二个奇怪的现象重定向命令行上使用重定向操作dup2系统调用接口stdout和stderror一个奇怪的现象 首先&#xff0c;我们来看这样一段代码…

毕业设计 - 基于Java的聊天室系统设计与实现【源码+论文】

文章目录前言一、项目设计1. 模块设计服务器模块设计客户端模块设计2. 实现效果二、部分源码项目源码前言 今天学长向大家分享一个 java 设计项目: 基于Java聊天室系统的设计与实现 一、项目设计 1. 模块设计 服务器模块设计 服务端的功能主要如下&#xff1a; 一&#xf…

R语言绘制森林图

在绘制森林图之前当然需要先下载RStudio软件啦&#xff0c;在下载后需要安装对应的rtool,最后将两者关联起来才能使用其中对应的包&#xff0c;否则只安装了软件很多功能不能使用而且还会报错&#xff0c;这篇文章主要是总结怎么使用forestploter包绘制森林图&#xff0c;本来是…

【Linux修炼手册:基本指令(完结)】

Life is about waiting for the right moment to act. 目录 1 zip/unzip指令 2 tar指令&#xff08;重要&#xff09;&#xff1a;打包/解包&#xff0c;不打开它&#xff0c;直接看内容 3 bc指令 4 uname –r指令 5 重要的几个热键[Tab],[ctrl]-c, [ctrl]-d 6 shutdown 7…

智能语音之远场关键词识别实践(二)

上篇&#xff08;智能语音之远场关键词识别实践&#xff08;一&#xff09;&#xff09;讲了“远场关键词识别”项目中后端上的实践。本篇将讲在前端上的一些实践以及将前端和后端连起来形成一个完整的方案。下图是其框图&#xff1a;&#xff08;麦克风阵列为圆阵且有四个麦克…

SpringMVC【学习笔记】

SpringMVC是什么? Spring Web MVC是一种基于Java的实现了Web MVC设计模式的请求驱动类型的轻量级Web 框架&#xff0c;即使用了MVC架构模式的思想&#xff0c;将web 层进行职责解耦&#xff0c;基于请求驱动指的就是使用请求-响应模型&#xff0c;框架的目的就是帮助我们简化…

看2022年卡塔尔世界杯有感

一、我印象中第一次看世界杯是2010年南非世界杯 自己第一次踢球是什么时候已经记不清了&#xff0c;大概是小学时候。因为我印象中第一次看世界杯是2010年南非世界杯&#xff0c;因为世界杯主题曲也比较好听&#xff0c;当然&#xff0c;我认为1998年法国世界杯的主题曲最为经…

Python中的魔法方法

python中的魔法方法是一些可以让你对类添加“魔法”的特殊方法,它们经常是两个下划线包围来命名的 Python的魔法方法&#xff0c;也称为dunder(双下划线)方法。大多数的时候&#xff0c;我们将它们用于简单的事情&#xff0c;例如构造函数(init)、字符串表示(str&#xff0c; r…

计算机毕设Python+Vue兴发农家乐服务管理系统(程序+LW+部署)

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

Linux | 套接字(socket)编程 | UDP协议讲解

文章目录TCP与UDP的区别网络字节序套接字接口介绍sockaddr结构服务端UDP套接字设置客户端UDP套接字设置TCP与UDP的区别 TCPUDP传输层协议传输层协议有连接无连接可靠连接不可靠连接面向字节流面向数据报 首先&#xff0c;网络通信模型是分层的&#xff0c;模型的每一层都有属于…

深入jvm字节码

深入jvm字节码1.深入剖析class文件结构1.1初探class文件1.2 class文件结构解析1.2.1 魔数1.2.2 版本号1.2.3 常量池1.2.4 Access flags1.2.5 this_class,super_name,interfaces1.2.6 字段表1.2.7 方法表1.2.8 属性表1.3使用javap查看类文件2.字节码基础2.1字节码概述2.2java虚拟…

一文看懂---B树及其简单实现

目录 1.B树的引入 2.B树的概念 3.B树是如何插入的&#xff1f; 4.具体的代码实现 1.B树的引入 在以往我们在内存中搜索数据时&#xff0c;可以使用红黑树&#xff0c;平衡树&#xff0c;哈希表等数据结构&#xff0c;但是当数据量比较大&#xff0c;不能一次放进内存&…

[附源码]计算机毕业设计Python仓储综合管理系统(程序+源码+LW文档)

该项目含有源码、文档、程序、数据库、配套开发软件、软件安装教程 项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等…

C++PrimerPlus 第七章 函数-C++的编程模块-7.9 递归

目录 7.9 递归 7.9.1 包含一个递归调用的递归 7.9.2 包含多个递归调用的递归 7.9 递归 下面介绍一些完全不同的内容。C函数有一种有趣的特点——可以调用自己&#xff08;然而&#xff0c;与C语言不同的是&#xff0c;C不允许main()调用自己&#xff09;&#xff0c;这种功能…

SpringCloud Gateway简单使用

前言 SpringCloud Gateway是一个网关框架&#xff0c;也是现在流行的的一个网关框架&#xff0c;它包括了过滤器、限流、权限、基本路由、整合Eureka 断言predicates 等功能&#xff0c;也会介绍和zuul这个框架的一个对比&#xff0c; Spring Cloud 生态系统中的网关&#xff…