二叉树,堆排序及TopK问题

news/2025/2/22 21:02:58

要讲二叉树的概念,就要先讲树的概念。
树是什么呢?
树其实是一种储存数据的结构,因为他的结构倒过来和生活中的树很相似所以才被称之为树。
在这里插入图片描述

这是一颗多叉树,从最顶端的节点可以找到下边的几个节点,下边的节点又可以找到他的下一级节点,注意观察,如下边的g h i节点,他们返回上一级只有一条路径,从最上边找到他也只有唯一的路径。
树的结构之间不可以交叉,一个节点只能有一个爸爸节点,但是一个爸爸可以有多个孩子。
联想一下这个结构,是不是和我们windows下的文件夹很相似,一个文件夹打开后可以找到很多文件夹,从某个文件夹内找到另一个文件夹只有一条路径。
讲几个比较重要的概念。
节点的度:一个节点含有的子树的个数,即一个爸爸有几个孩子。
在这里插入图片描述
A的度为4,B的度为2。

树的度:一棵树里面所有节点中,最大的节点的度为树的度
在这里插入图片描述
A的节点为2,B的节点为3,该树的度为3。
树的高度:就是树的深度
父节点:有孩子结点的节点,上图ABC都为父节点
子节点:上图除了A别的都可以称作子节点,他们都有父节点
节点的祖先:所有节点的祖宗,即A
兄弟节点:有相同的父节点的节点。F和G就不是相同的父节点,就不是兄弟节点。但是他们的爸爸在同一层,所以称为堂兄弟节点。、


多叉树的实现比较难搞,每个节点要保存父节点,还要保存他的兄弟节点,
所以我们先来实现一些简单的树形结构:二叉树。
二叉树
概念:这棵树的每个节点的子节点最多只能有两个,左孩子或者右孩子。
任意二叉树都由以下几种情况复合而成
在这里插入图片描述
特殊的二叉树

  1. 满二叉树:一个二叉树,如果他的每一层的节点数都达到最大值,这棵树就是满二叉树,如果该二叉树的层数为K,总结点个数为2k -1。
    例如:
    在这里插入图片描述
    每个父亲节点都有两个孩子。

现实中的二叉树如图:

在这里插入图片描述在这里插入图片描述
是不是超级标准。
2,完全二叉树
完全二叉树由满二叉树发展而来(满二叉树是完全二叉树的一种),如果一棵树有K个节点,这些节点从左往右依次数都是连着的,假设这里有一棵完全二叉树,节点个数为9。

在这里插入图片描述
只能4有左孩子后才能有右孩子,同一层节点,如果左边的节点没有两个儿子,后边的节点都不能有孩子。
下边两棵树都不是完全二叉树。
二叉树的存储结构
1,顺序结构
顺序结构用数组来存储,但一般只适用于完全二叉树,完全二叉树使用数组存储的话不会因为有些地方是空的会造成空间的浪费,所以现实中只有用堆才会使用数组存储。
二叉树顺序存储在物理上是一个数组,在逻辑上是一棵完全二叉树。

在这里插入图片描述
第二种是链式储存,用链表表示一棵二叉树,这种结构在数据结构高阶中才会用到,我们不做仔细讲解。

普通二叉树是不适合用数组建堆的,会造成大量的空间浪费,但是完全二叉树不一样,他的节点是挨个建立的,我们接下来建堆就是用这种结构。
堆的概念
有一堆元素,以顺序表按照完全二叉树的顺序将其储存起来,堆有两种,大堆和小堆。
大堆
all父节点大于子节点,根节点的值最大,这样的堆叫做大堆
如果根节点最小,所有父节点的值小于子节点,那这个堆被称为小堆。
堆的创建
给一个数组,

a[5,6,2,8,9,4,7]

这个数组在逻辑上是一棵完全二叉树,但还不是一个堆。
想要将它变成一个大堆,就要调整他的顺序。
首先我们会想到让根节点小于其子节点,然而其子节点也必须小于自己的子节点,所以想要让一棵树变为一个大堆,就要让自己的子树也变成一个大堆。

建堆过程如图
在这里插入图片描述
用数组的方式存储,但其逻辑上可以看作是一棵二叉树,完全二叉树
在这里插入图片描述

这里的1,2,3,4,5,6,7是数组的下标,
可以发现,若知道一个子节点的下标为child,其父节点的下标为(child-1)/2。
知道一个父节点parent,其左孩子节点的下标为parent2+1。右孩子的下标为parent2+2。

建堆的时间复杂度
向下调整法
向下调整法通过父节点的下标找左右孩子,不断判断值的大小,交换建堆。
代码如下

//向下调整
void AdjustDown(HeapStyle* a, int n, int parent)
{
	int child = parent * 2 + 1;
	while (child<n)
	{
		//找出小的孩子
		if (child + 1 < n && a[child]> a[child + 1])
		{
			++child;
		}
		if (a[child] < a[parent])
		{
			swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

为了容易计算,且满二叉树是完全二叉树的一种,为了化简过程就利用满二叉树来证明。
假设树的高度为n
第一层一个节点最多需要向下调整n-1层
第二层21个节点,最多每个节点要调整h-2层
第三层有22个节点,最多每个节点要调整h-3层

第h-1层2h-2个节点,最多向下调整一层
在这里插入图片描述
建堆的时间复杂度为O(N)。
向上调整法建堆
第一层不需要调整
第二层21个节点,每个节点最多需要向上调整1次。
第三层22个节点,每个节点最多向上调整2次

第h-1层2h-2个节点,最多每个节点向上调整h-2次。
第h层2h-1个节点,每个节点最多向上调整h-1次。
不用算就可以得出向上调整的时间复杂度为O(N2)。
向上调整法通过孩子找父亲判断交换建堆
代码如下

//向上调整
void AdjustUp(HeapStyle* a, 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;
		}
	}
}

建堆过程
通过上边的分析,我们已经知道向下调整法相对于向上调整优势巨大,所以在建堆的过程中,使用向下调整法,遍历数组的每个节点,使用向下调整法建大堆,上边的流程图可知如果从根节点开始建堆,不能确保左子树和右子树是否为堆,最后一层不需要调整,从倒数第二层开始调整,使其变为一个堆。

//找出小的孩子
		if (child + 1 < n && a[child]> a[child + 1])
		{
			++child;
		}

找出该节点左右孩子中大的那个。

if (a[child] < a[parent])
		{
			swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}

如果小于,就互换,越小越往上,所以建的是小堆,将判断条件更改为大于号,即建大堆。这里的大于号和小于号是建大堆还是建小堆的关键。
利用循环,交换后将parent改为child,child也更新一次,是因为更换过来的父节点有可能比该子树的子节点更小。
如图所示
在这里插入图片描述
给定一个数组a[k]

	for (int i = (k - 2) / 2; i >= 0; --i)//这里要思考一下
	{
		AdjustDown(minheap, k, i);
	}

从倒数第二层开始建堆即可,k-1是数组最后一个函数,要想知道其父节点下标,要减一再除以2,所以就变成了(k-2)/2。
堆排序
假设我们要排升序
建立一个大堆,堆顶元素一定是最大的那个,如果直接取出根节点,那么我们建的堆将被破坏,左子树和右子树可能就不再是大堆。
就像上图数组,建堆后下标为0,1,2,3,4的数字为4 6 5 7 7,如果取出4,剩余的6,5,7,7明显不再是一个堆,我们又要重新建堆,这样堆排序有什么优势可言,时间复杂度就变为O(N2)。如何取出根节点又不破坏左子树和右子树的大堆状态呢?
将堆顶元素与最后一个交换,最大的数就到了数组最后边,然后只需要对换过去的根节点向下调整即可。
代码如下

//向下调整
void AdjustDown(HeapStyle* a, int n, int parent)
{
	int child = parent * 2 + 1;
	while (child<n)
	{
		//找出小的孩子
		if (child + 1 < n && a[child]< a[child + 1])//找出左右子树大的那个
		{
			++child;
		}
		if (a[child] > a[parent])//建大堆,大于就交换
		{
			swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

void Heapsort(int* a, int n)//建大堆
{
	for (int i = (n-2)/2; i >=0; i--)
	{
		AdjustDown(a,n, i);
	 }//
	int end = n - 1;
	while (end > 0)
	{
		swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		--end;
	}
}

int end=n-1。a[end]为最后一个节点,交换后传参为n-1,向下调整就不再带最后一个节点玩了,直到调整至第一个节点,这样这个数组就有序了。
TOPK问题
TOPK问题在生活中常常出现,就比如年级前几名,饭店味道排名等等等等,如何在一大地数据中找到前几名呢?
如果我们建一个小堆,那么堆顶节点即为整棵树最小的那个,假设有10000个数据,我们要找出其中的前10个,我们就可以使用前十个元素建一个小堆,然后再让剩下的元素与堆顶元素进行比较,如果大于对顶元素,就与对顶元素进行交换,然后向下调整重新建出一个小堆,这时堆顶元素会发生变化,会变成一个新的堆中最小的数,遍历完成之后队中剩下的元素即为这10000个数里面最小的那个。
创建一个数组向里面写入10000个随机数进行测试。
在这里插入图片描述
随机数的范围是32767,如果我们创建100000个数据,只会出现很多重复的数据,所以我们就使用一万个数据测试一下数据。
代码如下

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
//int main()
//{
//	printf("%d ", RAND_MAX);
//
//	return 0;
//}

void  swap(int* a, int* b)
{
	int swp = 0;
	swp = *a;
	*a = *b;
	*b = swp;
}
void AdjustDown(int* a, int n, int parent)
{
	int child = parent * 2 + 1;
	while (child < n)
	{
		//找出小的孩子
		if (child + 1 < n && a[child] > a[child + 1])
		{
			++child;
		}
		if (a[child] < a[parent])
		{
			swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}
void TestTopk()
{
	int n = 10000;
	int* a = (int*)malloc(sizeof(int) * (n));
	srand(time(0));
	for (int i = 0; i < n; i++)
	{
		a[i] = rand() % 10000;
	}
	a[5] = 100000 + 1;
	a[6100] = 100002;
	a[1007] = 100003;
	a[8678] = 888888;
	a[3459] = 777777;
	int k = 10;
	for (int i = (k - 2) / 2; i >= 0; i--)//倒数第二层最后一个节点
	{
		AdjustDown(a, k, i);//传入下标
	}
	//以前k个数建堆完毕
	for (int i = k + 1; i < n; i++)
	{
		if (a[i] > a[0])
		{
			swap(&a[i], &a[0]);
		}
		AdjustDown(a, k, 0);
	}
	for (int i = 0; i < k; i++)
	{
		printf("%d ", a[i]);
	}
}

int main()
{
	TestTopk();

	return 0;
}

为了测试方便,我们修改了数组中的几个数值。
运行后如下
在这里插入图片描述
是不是小堆呢?
在这里插入图片描述
答案是,他就是个小堆,这样一万个数据里的前10个最大的数据就找出来了。
然而10000个数据是不是有点太少了。
我们普通创建的数组存储在栈上,malloc出的数组存放在堆里,如果有很多很多数据,就会占据很多时间,我们可以考虑将这些数字存放在一个文件里,然后利用相关的文件操作找出这些数字里最大得前几个。
向文件里写入数据

//创造数据
void CreatNData()
{
	int n = 1000000;
	srand(time(0));
	const char* file = "data.txt";
	FILE* fin = fopen(file, "w");
	if (fin == NULL)
	{
		perror("fopen,error");
		return;
	}

	for (int i = 0; i < n; ++i)
	{
		int x = rand() % 100000;//然而最大不过32767,有很多重复数据得了
		fprintf(fin, "%d\n", x);
	}
	int k = 10;
	//PrintTopk(file, k);

	fclose(fin);
}

创建完成之后可以打开修改修改数据,然后在main函数里将创建数据的函数注释,防止数据覆盖。
在这里插入图片描述
打开后直接就是一顿修改
在这里插入图片描述
那几个后边数字相同且贼大的就是修改的数据。

void PrintTopk(const char*file, int k)
{
	//1,建堆--用a中的前k个元素建堆
	FILE* fout = fopen(file, "r");
	if (fout == NULL)
	{
		perror("fopen fail");
		return;
	}
	int* minheap = (int*)malloc(sizeof(int) * k);
	if (minheap == NULL)
	{
		perror("malloc fail");
		return;
	}
	for (int i = 0; i < k; i++)
	{
		fscanf(fout, "%d", &minheap[i]);//依次先读K个数
	}
	
	for (int i = (k - 2) / 2; i >= 0; --i)//这里要思考一下
	{
		AdjustDown(minheap, k, i);
	}
	//2.将剩余的n-k个元素依次与堆顶元素
	int x = 0;
	while (fscanf(fout, "%d", &x) != EOF)
	{
		if (x > minheap[0])
		{
			//替换进堆
			minheap[0] = x;
			AdjustDown(minheap, k, 0);
		}
	}
	for (int i = 0; i < k; i++)
	{
		printf("%d ", minheap[i]);
	}
	fclose(fout);
}

逻辑和上边malloc出的数组找最大得前十个一样。建小堆,找到小于根节点的数据就进行交换,交换后再次进行向下调整。
要注意,rand出的数据最大32767,因为我们创建了一百万个数据,所以如果不修改数据的话,查找出来的都会是32767。
运行结果如图
在这里插入图片描述
是一个小堆
在这里插入图片描述
对顶元素在预料之中。这样Topk问题就解决啦。
这篇文章就讲解到这里,如果有什么问题欢迎大家提出指正。


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

相关文章

WAL 模式(PostgreSQL 14 Internals翻译版)

性能 当服务器正常运行时&#xff0c;WAL文件不断被写入磁盘。但是&#xff0c;这些写操作是顺序的:几乎没有随机访问&#xff0c;因此即使是HDD也可以处理这个任务。由于这种类型的加载与典型的数据文件访问非常不同&#xff0c;因此有必要为WAL文件设置一个单独的物理存储&a…

leetcode-1438: 绝对差不超过限制的最长连续子数组

给你一个整数数组 nums &#xff0c;和一个表示限制的整数 limit&#xff0c;请你返回最长连续子数组的长度&#xff0c;该子数组中的任意两个元素之间的绝对差必须小于或者等于 limit 。 如果不存在满足条件的子数组&#xff0c;则返回 0 。 示例 1&#xff1a; 输入&#x…

探讨Unity新的收费模式:对开发者与游戏行业的影响、负面因素的解析及面对挑战的建议

本人详解 作者&#xff1a;王文峰&#xff0c;参加过 CSDN 2020年度博客之星&#xff0c;《Java王大师王天师》采购供应链共享平台人员,财务规则对账人员&#xff0c;物流门禁计量系统对接人员&#xff0c;ERP事业部人员 Unity是一款备受开发者欢迎的跨平台游戏引擎&#xff0c…

【C语言必知必会 | 第二篇】编译器的安装与使用

引言 C语言是一门面向过程的、抽象化的通用程序设计语言&#xff0c;广泛应用于底层开发。它在编程语言中具有举足轻重的地位。 此文为【C语言必知必会】系列第二篇&#xff0c;介绍C语言编译器的安装与使用&#xff0c;为之后的学习安装环境基础 文章目录 1️⃣ Dev-C的安装与…

STM32的hex文件格式的分析

前言 最近研究Bootloader&#xff0c;通过串口实现STM32程序的更新。需要学习了解STM32的Hex文件格式。在这进行一下总结。 HEX文件格式 我们通过文本形式打开hex文件&#xff0c;可以看到&#xff1a; 这一行就是一条指令数据&#xff0c;这里对数据帧格式进行说明&#xff…

【数据结构】线性表的顺序存储结构

&#x1f984;个人主页:修修修也 &#x1f38f;所属专栏:数据结构 ⚙️操作环境:Visual Studio 2022 一.顺序存储定义 上篇文章中介绍了线性表一共分为两种数据结构——顺序存储结构和链式存储结构. 今天我们就来一起学习一下第一种——顺序存储结构. 线性表的顺序存储结构,指…

三江学院“火焰杯”软件测试高校就业选拔赛颁奖仪式

11月25日下午&#xff0c;“火焰杯”软件测试开发选拔赛及三江-慧科卓越工程师班暑期编程能力训练营颁奖仪式在s楼会议室隆重举行。计算机科学与工程学院院长刘亚军、副院长叶传标、曹阳、吴德、院党总支副书记王兰英、系主任杨少雄、慧科企业代表尹沁伊人、项目负责人王旭出席…

UE4逆向篇-2_各类数据的查找方式

写在前面 1.通过前面的文章&#xff0c;相信各位已经能够自己找到GNames并使用DUMP工具导出GNames了。 2.本篇文章将介绍各种所需数据的查找方法。 一、准备工作 1.CheatEngine&#xff0c;本篇以及后续篇幅的重要工具。 2.一个记事本&#xff0c;保证你能记录下关键信息。…