【数据结构】--- 深入剖析二叉树(中篇)--- 认识堆堆排序Topk

news2024/9/21 14:51:10

 Welcome to 9ilk's Code World

       

(๑•́ ₃ •̀๑) 个人主页:        9ilk

(๑•́ ₃ •̀๑) 文章专栏:     数据结构之旅 


文章目录

🏠 初识堆

📒 堆的概念

📒 堆的性质

🏠 向上调整算法 && 向下调整算法

📒 向上调整算法

📒 向下调整算法

📒 向上调整 vs 向下调整

🏠 堆的应用场景

📒 堆排序

📒 Top K问题


上篇我们讲解了树以及二叉树,相信小伙伴们对二叉树有了初步的了解,本篇文章我们来了解下由二叉树延伸出来的堆以及堆排序,Top K问题。

🏠 初识堆

     我们知道二叉树的顺序结构适合于完全二叉树和满二叉树,而我们今天的主角也是个完全二叉树,因此它也是使用顺序结构的数组来存储

📒 堆的概念

堆的概念(来自度娘):

⚠️

  • 我们这里的堆是一种数据结构,而操作系统虚拟进程地址空间的堆区是操作系统中管理内存的一块区域分段
  • 堆分为大堆和小堆。大堆指的是双亲结点的值域大于孩子结点,小堆指的是双亲结点的值域小于孩子结点
  • 堆只规定了孩子和双亲的关系,并未规定兄弟间的大小关系
  • 堆在物理层面是数组,逻辑结构上是二叉树。

📒 堆的性质

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

🏠 向上调整算法 && 向下调整算法

对于这样的一个小堆,我们要插入2这个数据,此时不满足小堆的要求,若要调整可能会影响祖先,那有什么方法能解决这个问题呢?这里就要介绍一个新的算法 --- 向上调整算法

📒 向上调整算法

我们先上个动图来感受下 ~ 

我们可以看到这个过程是针对某个结点而言的,若要满足小堆,依次拿这个结点向上与它的祖先比较,如果它比祖先小就交换,直到小于它的某个祖先或交换到根结点的位置。

⚠️  向上调整算法只能帮助我们使根结点的值域最小,而不能保证所有其他结点的大小关系!

  • 代码分析及实现

1.首先需要实现一个交换数组数据的Swap函数

2.如何找某个结点child的祖先parent,这里就要用到我们上节的知识:parent =(child - 1)/ 2;

3.何时不交换:当小于它的某个祖先时或交换到根结点的位置(child>0)

void Swap(Datatype& x,Datatype& y)
{
      Datatype temp = x;
                  x = y;
                  y = temp;
}

void AdjustDown(int* arr,int child)
{
        int parent = (child - 1) / 2;//双亲结点
        while(child > 0)
        {
              if(arr[child] < arr[parent])
              {
                  Swap(arr[child],arr[parent]);
                  child = parent;//更新孩子和双亲
                  parent = (parent-1)/2;     
              }
              else
               {
                    break;
               }
        }
}
  • 利用向上调整算法建堆

假设已知数组a[ ] = {1,5,3,8,7,6},如何把它建成一个大堆呢?这里就可以用到我们的向上调整算法。

整个过程就是,我们每次向新数组插入数据时,采用向上调整算法进行调整。

void HPInit(HP* php)
{
	assert(php);
	php->a = NULL;
	php->size = 0;
	php->capacity = 0;
}
void Swap(HPDataType* px, HPDataType* py)
{
	HPDataType tmp = *px;
	*px = *py;
	*py = tmp;
}

void AdjustUp(HPDataType* a, int child)
{
	int parent = (child - 1) / 2;
	//while (parent >= 0)
	while(child > 0)
	{
		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);
			child = parent;
			parent = (parent - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

void HPPush(HP* php, HPDataType x)
{
	assert(php);

	if (php->size == php->capacity)
	{
		size_t newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;
		HPDataType* tmp = realloc(php->a, sizeof(HPDataType) * newCapacity);
		if (tmp == NULL)
		{
			perror("realloc fail");
			return;
		}
		php->a = tmp;
		php->capacity = newCapacity;
	}

	php->a[php->size] = x;
	php->size++;

	AdjustUp(php->a, php->size-1);
}

int main()
{
    int arr[] = {1,5,3,8,7,6};
    HP hp;
    HPInit(&hp);
    for(int i = 0; i < sizeof(arr)/sizeof(arr[0]);i++)
     {
          HPPush(&hp,arr[i]);
     } 
    return 0;
}

📒 向下调整算法

对于这样的一个小堆我们要删除堆顶数据10,应该怎么删呢?有的小伙伴认为直接循环覆盖不就行了,但这样做会出现两个问题:1.挪动覆盖时间复杂度是O(N) 2.堆结构破坏,父子兄弟间关系乱套。有什么解决方法呢?我们可以采取这样的一个方法

1.首尾(根结点和最后一个叶子结点)交换数据,删除尾部数据

2.对根结点采用向下调整算法恢复堆的结构

我们上动图 ~ 

由动图我们可以知道,向下调整大致是这样的一个流程:若要保证是小堆,先找出左右孩子中较小的那一个,如果调整节点比较小的那个要大,就两者交换。

  • 向下调整代码分析及实现

1.需要一个交换函数

2.选出左右孩子较小的那个(假设小堆),同时要保证有右孩子

3.调整后的更新 parent = child; child = 2*child + 1;

4.调整结束条件:调整结点比较小孩子结点小 或 调整到二叉树最后一层(child < n)

void AdjustDown(int* a, int n, int parent)
{
	int child = parent * 2 + 1;
	while (child < n)
	{
		// 假设法,选出左右孩子中小的那个孩子
		if (child+1 < n && a[child + 1] > a[child])
		{
			++child;
		}

		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}
  • 利用向下调整算法实现删除堆顶数据
void HPPop(HP* php)
{
	assert(php);
	assert(php->size > 0);

	Swap(&php->a[0], &php->a[php->size - 1]);//首尾交换
	php->size--;//删除尾部数据

	AdjustDown(php->a, php->size, 0);//向下调整
}
  • 利用向下调整建堆

利用向下调整建堆我们需要从倒数第一个非叶子结点开始,这与向上调整调整有所不同

从倒数第一个非叶子结点开始的原因:

1. 向下调整的前提结点的左右子树都是堆

如上图若要建小堆,27的右子树是小堆,但左子树不是小堆,若向下调整,会使原本应为根节点的15被忽视。

2.从倒数第一个非叶子结点开始的话,就可以先调整每个子树为小堆或大堆

动图 part ~

  • 代码分析以及实现

我们需要确定倒数第一个非叶子结点。

1.最后一个叶子结点下标为n-1

2.倒数第一个非叶子结点即为最后一个叶子结点的父亲

3.由于parent = (child - 1) / 2 , 因此倒数第一个非叶子结点下标为(n-1-1)/ 2

void HPInitArray(HP* php, HPDataType* a, int n)
{
	assert(php);
	
	php->a = (HPDataType*)malloc(sizeof(HPDataType) * n);
	if (php->a == NULL)
	{
		perror("malloc fail");
		return;
	}
	memcpy(php->a, a, sizeof(HPDataType) * n);
	php->capacity = php->size = n;

	// 向下调整
	for (int i = (php->size-1 - 1)/2; i >= 0; i--)
	{
		AdjustDown(php->a, php->size, i);
	}
}

📒 向上调整 vs 向下调整

  • 时间复杂度 :向上调整 vs 向下调整

由于时间复杂度要考虑最坏的情况,所以二叉树中的结点最坏调整高度次,因此Push操作或向上调整算法的时间复杂度是O(logN);而对于Pop操作,我们一次向下调整最坏是从根结点开始调整,最坏要调整高度h次,因此Pop操作或向下调整算法的时间复杂度是O(logN)

结论: 

1.完全二叉树Pop和Push操作的时间复杂度都是O(logN)

2.向上调整算法和向下调整算法的时间复杂度都是O(logN)

向下调整和向上调整都是O(logN),我们是不是可以随意用呢?别急,我们分析建堆层面 ~ 


  • 时间复杂度: 向上调整建堆 vs 向下调整建堆

向上调整建堆

假设在最坏情况下,设树的高度为h,累计调整次数为f(h),f(h)为每个结点调整次数之和,由于每一层结点个数为2^(i-1),则有:

         f(h) = (2^1)*1 + (2^2)*2 + (2^3)*3 +... + (2^(h-1))*(h-1);

---> 2f(h) = (2^2)*1 + (2^3)*2 + (2^4)*3 +... + (2^(h-1))*(h-2) + (2^(h)*(h-1));

--->错位相减得  f(h) = (2^h)*(h-2)+2     (1)

由于 2^(h) - 1 = N -->  h = log(N + 1)    (2)

联立(1)  (2) 得  f(N) =  (N+1)(log(N+1) - 2) + 2 

由f(N)得   向上调整建堆的时间复杂度为O(N*logN)

向下调整建堆

假设在最坏情况下,设树的高度为h,累计调整次数为f(h),f(h)为每个结点调整次数之和,由于每一层结点个数为2^(i-1),则有:

         f(h) = (2^(h-2))*1 + (2^(h-3))*2 + (2^(h-4))*3 + ... + (2^0)*(h-1)

---> 2f(h) = (2^(h-1))*1 + (2^(h-2))*2 + (2^(h-3))*3 + ... + (2^0)*(h-2) + (2^0)*(h-1)

--->错位相减得  f(h) =  2^(h) + h - 2;   (1)

由于 2^(h) - 1 = N -->  h = log(N + 1)    (2)

联立(1)  (2)  得  f(N) = N + 1 + log(N+1) - 2;

由f(N)得  向上调整建堆的时间复杂度为O(N)

不同算法建堆差异的原因

我们发现向下调整建堆的时间复杂度小于向上调整建堆,效率较高,原因在于完全二叉树层数越大,该层结点数越多,而向下调整是先对倒数第一层的结点(可以说集合了这颗树的大部分结点)开始调整,且调整次数只有1次,也就是说向下调整对结点调整次数是多 x 少,对大部分结点的调整次数少 ;而向上调整建堆是从根结点开始调整,可以说是多 x 多,对大部分结点调整次数多。

因此对同样时间复杂度的算法,采用向下调整建堆的方法效率更高一些!


🏠 堆的应用场景​​​​​​​

对于堆,我们主要有两个应用场景堆排序和Top K问题

📒 堆排序

  • 第一种堆排序

我们前面实现了Pop操作,同时我们知道向上调整或算法可以使根结点为最大或最小,因此我们可以先对数组初始化建小堆,此时若要升序根节点值就是最小的再不断Pop操作就能实现排序

void HPInitArray(HP* php, HPDataType* a, int n)
{
	assert(php);
	
	php->a = (HPDataType*)malloc(sizeof(HPDataType) * n);
	if (php->a == NULL)
	{
		perror("malloc fail");
		return;
	}

	memcpy(php->a, a, sizeof(HPDataType) * n);
	php->capacity = php->size = n;

	for (int i = (php->size-1 - 1)/2; i >= 0; i--)
	{
		AdjustDown(php->a, php->size, i);
	}
}

void HPPop(HP* php)
{
	assert(php);
	assert(php->size > 0);

	Swap(&php->a[0], &php->a[php->size - 1]);
	php->size--;

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


bool HPEmpty(HP* php)
{
	assert(php);

	return php->size == 0;
}

void HeapSort(int* a, int n)
{
	HP hp;
	HPInitArray(&hp, a, n); //初始化堆
	int i = 0;
	while (!HPEmpty(&hp))
	{
		a[i++] = HPTop(&hp);//取堆顶数据
		HPPop(&hp);//删除数据
	}
	HPDestroy(&hp);
}

对于这种堆排序思路比较简单容易理解,但是存在两个问题:1.需要我们自己实现一个堆的数据结构 2.调用HpInitArray(),空间复杂度为O(N)

  • 第二种堆排序

若我们跟第一种堆排序一样升序建小堆而不申请空间原地操作,会有什么问题?

答案是这样我们虽然能得到最小的,但要得到次小的,要重新建堆O(N),重复下来整个过程的时间复杂度是N + N -1 + N - 2 + ... + 1 --> O(N^2) 这个效率是大大不行的,有什么解决之法?那我们就反着来,升序建大堆看看 ~ 

大概流程是:

1.若要升序初始化建大堆

2.首尾交换数据,缩小范围

3.向下调整根结点 循环往复(2)(3) 直到范围缩小为0

void AdjustDown(HPDataType* a, int n, int parent)
{
	int child = parent * 2 + 1;
	while (child < n)
	{
		// 假设法,选出左右孩子中大的那个孩子
		if (child+1 < n && a[child + 1] > a[child])
		{
			++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)
{
	// a数组直接建堆 O(N)
	for (int i = (n-1-1)/2; i >= 0; --i)
	{
		AdjustDown(a, n, i);
	}

	// O(N*logN)
	int end = n - 1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		--end;
	}
}

这种堆排序主要是升序建大堆,再利用堆删除数据的思想,时间复杂度是O(NlogN)

📒 Top K问题

TOP-K问题:即求数据集合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。

对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决。

基本思路如下:

1. 用数据集合中前K个元素来建堆

要前k个最大的元素,则先对前k个元素建小堆;要前k个最小的元素,则先对前k个数据建大堆

2. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素

将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。

说明:若要前k个最大,建小堆就能保证前k大的能通过向下调整沉进这个“三角形”里,同时前k大之外的数据能不断被剔除出去,因为会往上“浮动”

void topk()
{
	printf("请输入k:>");
	int k = 0;
	scanf("%d", &k);

	const char* file = "data.txt";
	FILE* fout = fopen(file, "r");
	if (fout == NULL)
	{
		perror("fopen error");
		return;
	}
     //申请空间准备建堆
	int val = 0;
	int* minheap = (int*)malloc(sizeof(int) * k);
	if (minheap == NULL)
	{
		perror("malloc error");
		return;
	}
    
	for (int i = 0; i < k; i++)
	{
		fscanf(fout, "%d", &minheap[i]);
	}

	// 建k个数据的小堆
	for (int i = (k - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(minheap, k, i);
	}
    //判断调整
	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);

}

本次分享到这里就结束啦,下篇我们将讲解二叉树结构及其遍历和相关oj题,记得三连呀 ~ 

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

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

相关文章

第一天学习(GPT)

1.图片和语义是如何映射的&#xff1f; **Dalle2&#xff1a;**首先会对图片和语义进行预训练&#xff0c;将二者向量存储起来&#xff0c;然后将语义的vector向量转成图片的向量&#xff0c;然后基于这个图片往回反向映射&#xff08;Diffusion&#xff09;——>根据这段描…

Junit 测试中如何对异常进行断言

本文对在 Junit 测试中如何对异常进行断言的几种方法进行说明。 使用 Junit 5 如果你使用 Junit 5 的话,你可以直接使用 assertThrows 方法来对异常进行断言。 代码如下: Exception exception = assertThrows(NumberFormatException.class, () -> {new Integer("on…

基于springboot+vue+Mysql的点餐平台网站

开发语言&#xff1a;Java框架&#xff1a;springbootJDK版本&#xff1a;JDK1.8服务器&#xff1a;tomcat7数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09;数据库工具&#xff1a;Navicat11开发软件&#xff1a;eclipse/myeclipse/ideaMaven包&#xff1a;…

第四百九十二回

文章目录 1. 概念介绍2. 使用方法2.1 SegmentedButton2.2 ButtonSegment 3. 代码与效果3.1 示例代码3.2 运行效果 4. 内容总结 我们在上一章回中介绍了"SearchBar组件"相关的内容&#xff0c;本章回中将介绍SegmentedButton组件.闲话休提&#xff0c;让我们一起Talk …

引领农业新质生产力,鸿道(Intewell®)操作系统助力农业机器人创新发展

4月27日至29日&#xff0c;2024耒耜国际会议在江苏大学召开。科东软件作为特邀嘉宾出席此次盛会&#xff0c;并为江苏大学-科东软件“农业机器人操作系统”联合实验室揭牌。 校企联合实验室揭牌 在开幕式上&#xff0c;江苏大学、科东软件、上交碳中和动力研究院、遨博智能研究…

【c1】数据类型,运算符/循环,数组/指针,结构体,main参数,static/extern,typedef

文章目录 1.数据类型&#xff1a;编译器&#xff08;compiler&#xff09;与解释器&#xff08;interpreter&#xff09;&#xff0c;中文里的汉字和标点符号是两个字节&#xff0c;不能算一个字符&#xff08;单引号&#xff09;2.运算符/循环&#xff1a;sizeof/size_t3.数组…

顶管机种类多样 国内产量不断增长

顶管机种类多样 国内产量不断增长 顶管机是一种用于非开挖管道铺设的机械设备&#xff0c;能够通过非开挖施工技术降低对地面活动的影响&#xff0c;具有工作效率高、安全性好、受地质条件限制小、环保性强等优点&#xff0c;在隧道修建、城市管网建设、地下管线敷设等场景中发…

《QT实用小工具·五十八》模仿VSCode的可任意拖拽的Tab标签组

1、概述 源码放在文章末尾 该项目实现了模仿VSCode的可任意拖拽的Tab标签组&#xff0c;包含如下功能&#xff1a; 拖拽标签页至新窗口 拖拽标签页合并控件 无限嵌套的横纵分割布局&#xff08;类似Qt Creator的编辑框&#xff09; 获取当前使用的标签组、标签页 自动向上合并…

测径仪视窗镜片的维护和保养步骤

关键字:测径仪镜片,测径仪保养,测径仪维护,视窗镜片维护,视窗镜片擦拭保养,视窗镜片的检查, 视窗镜片定期保养 视窗镜片是保护光学镜头免受污染和损伤的光学平镜片&#xff0c;它的污染和破损会直接影响光学系统的测量结果。 视窗镜片一般在受到轻微污染&#xff08;指镜片上…

项目管理-项目采购管理2/2

项目管理&#xff1a;每天进步一点点~ 活到老&#xff0c;学到老 ヾ(◍∇◍)&#xff89;&#xff9e; 何时学习都不晚&#xff0c;加油 本文承接 项目采购管理第二部分&#xff0c;详细讲解项目合同管理。 项目采购管理过程--重点&#xff1a; ①ITTO 输入&#xff0c;输出…

测试环境搭建:JDK+Tomcat+Mysql+Redis

基础的测试环境搭建&#xff1a; LAMPLinux(CentOS、ubuntu、redhat)ApacheMysqlPHP LTMJLinux(CentOS、ubuntu、redhat)TomcatMysql(Oracle)RedisJava 真实的测试环境搭建&#xff1a;&#xff08;企业真实的运维&#xff09; 基于SpringBoot&#xff08;SpringCloud分布式微…

分析:Palo Alto在从SASE向SASO演进中定位不佳

摘要 我们通过上一篇文章&#xff08;Fortinet的愿景——超越SASE&#xff09;中应用于Fortinet的相同框架来回顾Palo Alto Network在网络和网络安全方面的前景。 SASE涉及数据传输的第一英里。不过&#xff0c;随着SASE的发展&#xff0c;投资者还需要考虑中间和最后一英里。…

javaweb学习week7

javaweb学习 十四.Springboot 1.配置优先级 Springboot中支持三种格式的配置文件&#xff1a; 注意&#xff1a;虽然Springboot支持多种格式配置文件&#xff0c;但是在项目开发时&#xff0c;推荐使用一种格式的配置&#xff08;yml是主流&#xff09; Springboot除了支持…

【Osek网络管理测试】[TG3_TC3]tSleepRequestMin_L

&#x1f64b;‍♂️ 【Osek网络管理测试】系列&#x1f481;‍♂️点击跳转 文章目录 1.环境搭建2.测试目的3.测试步骤4.预期结果5.测试结果 1.环境搭建 硬件&#xff1a;VN1630 软件&#xff1a;CANoe 2.测试目的 验证DUT进入NMLimpHome状态后请求睡眠的最短时间是否正确…

周刊是聪明人筛选优质知识的聪明手段!

这是一个信息过载的时代&#xff0c;也是一个信息匮乏的时代。 这种矛盾的现象在 Python 编程语言上的表现非常明显。 它是常年高居编程语言排行榜的最流行语言之一&#xff0c;在国外发展得如火如荼&#xff0c;开发者、项目、文章、播客、会议活动等相关信息如海如潮。 但…

【LeetCode刷题记录】105. 从前序与中序遍历序列构造二叉树 106. 从中序与后序遍历序列构造二叉树

105 从前序与中序遍历序列构造二叉树 给定两个整数数组 preorder 和 inorder &#xff0c;其中 preorder 是二叉树的先序遍历&#xff0c; inorder 是同一棵树的中序遍历&#xff0c;请构造二叉树并返回其根节点。 示例 1: 输入: preorder [3,9,20,15,7], inorder [9,3,1…

近50亿元国资助阵,全球最大量子独角兽登场!

4月30日&#xff0c;澳大利亚与PsiQuantum公司宣布签订一项近10亿澳元&#xff08;约6.2亿美元、47.24亿人民币&#xff09;的协议&#xff0c;旨在建造世界上第一台商业上“有用”的量子计算机。 仅在一天前&#xff0c;澳大利亚还投资了1840万澳元&#xff0c;在悉尼大学成立…

【Osek网络管理测试】[TG3_TC5]等待总线睡眠状态_1

&#x1f64b;‍♂️ 【Osek网络管理测试】系列&#x1f481;‍♂️点击跳转 文章目录 1.环境搭建2.测试目的3.测试步骤4.预期结果5.测试结果 1.环境搭建 硬件&#xff1a;VN1630 软件&#xff1a;CANoe 2.测试目的 验证DUT在满足进入等待睡眠状态的条件时是否进入该状态 …

Linux学习(一)-- 简单的认识

目录 1. Linux的诞生 2.Linux发行版 拓展&#xff1a; &#xff08;1&#xff09;什么是Linux系统的内核&#xff1f; &#xff08;2&#xff09;什么是Linux系统发行版&#xff1f; 1. Linux的诞生 Linux创始人: 林纳斯 托瓦兹 Linux 诞生于1991年&#xff0c;作者上大学…

沃伦·巴菲特将AI比做原子弹:“瓶中精灵”使诈骗成为最快增长产业|TodayAI

在伯克希尔哈撒韦公司的年度股东大会上&#xff0c;投资大师沃伦巴菲特对人工智能的未来提出了严重警告。巴菲特对这项可以模拟现实并产生误导性内容的技术表示担忧&#xff0c;他认为这将成为史上增长最快的行业之一。 巴菲特在会上说&#xff1a;“当你思考诈骗人们的潜力时…