堆的实际应用(topk问题以及堆排序)

news2024/7/6 20:44:01

目录

前言:

一:解决topk问题

二:堆排序

【1】第一种方法(很少用)

【2】第二种方法(很实用)


前言:

上一次我们进行了二叉树的初步介绍并实现了堆的基本功能,但堆的作用并不是存储数据,它可以用来解决topk问题(求一组数据较大或者较小的前k个)以及对数据进行排序

附上一期链接:http://t.csdn.cn/pMOia

一:解决topk问题

在讨论topk问题之前我们先来回顾一下堆的性质:

(1)大堆的父亲节点总是大于孩子节点

(2)小堆的父亲节点总是小于孩子节点

由此我们可以得到一个结论:

根部节点一定是这个堆的最大或者最小值(大堆为最大,小堆为最小)。

【1】我们可以把所有数据存储在堆中,得到根部的数据后删除根部数据,然后通过调整保持堆的结构,不断重复这个操作直到找到前k个数。

代码(我建立的是大堆,找较大的数):

//初始化
void HeapInit(HP* hp)
{
	//断言,不能传空的结构体指针
	assert(hp);
	hp->a = NULL;
	//初始化size和容量都为0
	hp->size = hp->capacity = 0;
}
//交换函数
void HeapSwap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}
void AdjustUp(HPDataType* a, int child)
{
	//断言,不能传空指针
	assert(a);
	//找到父结点的下标
	int parent = (child - 1) / 2;
	//循环,以child到树根为结束条件
	while (child > 0)
	{
		//如果父结点比child小,交换并更新
		if (a[child] > a[parent])
		{
			HeapSwap(&a[child], &a[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		//如果父结点比child大,跳出循环
		else
		{
			break;
		}
	}
}
//向下调整
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加1变成右孩子
			child++;
		}
		//如果父亲比大孩子小,进行调整,否则跳出
		if (a[child] > a[parent])
		{
			HeapSwap(&a[child], &a[parent]);
			//迭代
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

//插入数据
void HeapPush(HP* hp, HPDataType x)
{
	if (hp->size == hp->capacity)
	{
		//判断扩容多少
		int newcapacity = hp->capacity == 0 ? 4 : hp->capacity * 2;
		//扩容
		HPDataType* tmp =
			(HPDataType*)realloc(hp->a, sizeof(HPDataType) * newcapacity);
		//更新
		hp->capacity = newcapacity;
		hp->a = tmp;
	}
	//存储数据
	hp->a[hp->size] = x;
	hp->size++;
	//进行调整
	AdjustUp(hp->a, hp->size-1);
}

//打印数据
void HeapPrint(HP* hp)
{
	//断言,不能传空的结构体指针
	assert(hp);
	int i = 0;
	for (i = 0; i < hp->size; i++)
	{
		printf("%d ", hp->a[i]);
	}
	printf("\n");
}

//删除数据
void HeapPop(HP* hp)
{
	//断言,不能传空的结构体指针
	assert(hp);
	//如果为空,不能删除,避免数组越界
	assert(!HeapEmpty(hp));
	//不为空,先交换根和最后一片叶子,然后size减1
	HeapSwap(&hp->a[0], &hp->a[hp->size - 1]);
	hp->size--;
	AdjustDown(hp->a, hp->size, 0);
}
//取根部数据
HPDataType HeapTop(HP* hp)
{
	return hp->a[0];
}
int main()
{
    HP hp;
    HeapInit(&hp);
    int arr[20] = { 4,5,6,1,2,44,33,25,69,78,3,0,11,22,77,55,88,75,14,8 };
    //找前k个最大的数
    int k = 5;
    for (int i = 0; i < 20; i++)
    {
        HeapPush(&hp, arr[i]);
    }
    for (int i = 0; i < k; i++)
    {
        printf("%d ", HeapTop(&hp));
        HeapPop(&hp);
    }
}

 

 

缺点:

(1)需要建立一个堆,消耗了额外的空间

(2)如果要排序的数字很多,内存存储不下

【2】还有另一种更常用的方法,这个方法只需要建立一个能够存储k个数据的堆

这里先给结论:

(1)这个方式找较大要建立小堆

(2)这个方式找较小要建立大堆

看到这两个结论大家可能会有点懵逼,因为前面找大我就建立大堆,找小我就建立小堆,为什么这里就反过来了呢?先不用着急,我们先讲解一下为什么找较小数要建立大堆

我们看下面这一组数据:

10  20  58  97  55  66  44  

假设我们要找出这些数据中的前4个较小的数据,我们先将前4个数据存储在大堆中,如下:

 然后我们从55(下标为k)开始遍历,如果数据小于根部,就将堆顶删除,然后将这个数据入堆,调整来保持大堆的结构

大堆根部数据是堆中最大的数据,我们遍历插入调整的行为其实是不断淘汰数据中较大的元素,一直到遍历结束还在堆中的元素就是前k个较小的值。

我们从55开始遍历替换:

后面的操作一致

 

最后堆中的元素分别是55,44,10,20,满足了我们的需求。 

相反的,如果要求前k个较大的数,我们就建立小堆,遇到比根部大的数据就将堆顶删除,然后将这个数据入堆,调整来保持小堆的结构

通过遍历插入和调整逐渐淘汰较小的数据,最后堆中的数据就是前k个较大的数据。

为了更好的测试,我们随机生成大量数据求其中的前k个较小数。

代码:

void topk()
{
	HP hp;
	//堆初始化
	HeapInit(&hp);
	//随机生成一万个数
	int n = 10000;
	//找前五个数
	int k = 5;
	int* a = (int*)malloc(sizeof(int) * n);
	if (a == NULL)
	{
		printf("malloc error\n");
		exit(-1);
	}
	srand(time(0));
	for (int i = 0; i < n; i++)
	{
		//随机生成500到1000的数据
		a[i] = rand() % (500) + 500;
	}
	for (int i = 0; i < k; i++)
	{
		//把数组前面几个数拿过来作堆
		HeapPush(&hp, a[i]);
	}
	//为了方便我们观察,我们设置5个小于500的数据
	a[100] = 423;
	a[888] = 55;
	a[999] = 450;
	a[887] = 478;
	a[56] = 256;
	for (int i = k; i < n; i++)
	{
		//如果a[i]比堆顶小,删除堆顶,然后入堆
		if (a[i] < HeapTop(&hp))
		{
			HeapPop(&hp);
			HeapPush(&hp, a[i]);
		}
	}
	//遍历调整结束,最后堆中元素为最小的前5个
	HeapPrint(&hp);
}

这个方式的优点:

(1)只需要建立存储k个数据的堆,空间消耗小

(2)因为是一个个数据进行遍历,可以把数据存储在磁盘中,从磁盘中读取数据

二:堆排序

【1】第一种方法(很少用)

(1)我们可以建立一个堆,把数据存储在堆中

(2)堆的物理结构是数组,我们可以把根部节点(最大的数据)和最后一个叶子节点交换,将size(堆中的有效数据)减1。

(3)再进行调整来保持堆的结构,一直到size变成1

图解(以大堆为例,怎么调整的大家可以看前一期):

 代码:

void HeapSort1(int* arr, int n)
{
	//建堆
	HP hp;
	HeapInit(&hp);
	for (int i = 0; i < n; i++)
	{
		HeapPush(&hp, arr[i]);
	}
	//排序
	while (hp.size > 1)
	{
		HeapSwap(&hp.a[0], &hp.a[hp.size - 1]);
		hp.size--;
		AdjustDown(hp.a, hp.size, 0);
	}
	for (int i = 0; i < n; i++)
	{
		printf("%d ", hp.a[i]);
	}
}

int main()
{
	int arr[20] = { 78,5,8,9,7,44,55,66,99,458,41,20,0,777,458,994,2,57,7789,956 };
	HeapSort1(arr, sizeof(arr) / sizeof(arr[0]));
}

缺点:

(1)消耗了额外的空间来建堆。

(2)这个方式几乎要用到堆的所有功能,再使用前要写大量的接口

【2】第二种方法(很实用)

(1)把数组原地调整成堆

这里有两种调整思路:

①自下而上进行调整(以大堆为例)

图解:

 时间复杂度分析:

②自上而下调整(以大堆为例)

图解:

 时间复杂度分析:

(2)进行排序(排序的时间复杂度和自上而下调整一致,计算思路也一致)

排序的思路和第一种方法一致

①可以把根部节点(最大的数据)和最后一个叶子节点交换,将n(堆中的有效数据)减1。

②再进行调整来保持堆的结构,一直到n变成1

代码:

//堆排序
void HeapSort2(int*a,int n)
{
	//调整成堆
	int parent = (n - 1) / 2;
	while (parent>=0)
	{
		AdjustDown(a, n, parent);
		parent--;
	}
	//进行堆排序
	while (n>1)
	{
		HeapSwap(&a[0], &a[n - 1]);
		n--;
		AdjustDown(a, n, 0);
	}
}

int main()
{
	int arr[] = {300,578,65,78,5,8,9,7,44,55,66,99,458,41,20,0,777,458,994,2,57,7789,956 };
	HeapSort2(arr, sizeof(arr) / sizeof(arr[0]));
	for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
	{
		printf("%d ", arr[i]);
	}
}

这个方法的优点:

(1)原地调整,不需要消耗额外的空间

(2)只需要用到调整的接口,代码量大大减少。

堆排序的时间复杂度分析

(1)如果调整选择自上而下,整个排序时间复杂度为O(2*N*log2(N))=O(N*log2(N))。

(2)如果调整选择自下而上,整个排序时间复杂度为O(N*log2(N)+N)O(N*log2(N))。

综上所述,堆排序是一个时间复杂度为O(N*log2(N))的算法

这个时间复杂度相较于冒泡是一个很大的提升。

 

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

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

相关文章

【C语言】9000字长文操作符详解

简单不先于复杂&#xff0c;而是在复杂之后。 目录 1. 操作符分类 2. 算数操作符 3. 移位操作符 3.1 左移操作符 3.2 右移操作符 4. 位操作符 4.1 按位与 & 4.2 按位或 | 4.3 按位异或 ^ 4.4 一道变态的面试题 4.5 练习 5. 赋值操作符 5.1 复合赋值…

主流接口测试框架对比,究竟哪个更好用

公司计划系统的开展接口自动化测试&#xff0c;需要我这边调研一下主流的接口测试框架给后端测试&#xff08;主要测试接口&#xff09;的同事介绍一下每个框架的特定和使用方式。后端同事根据他们接口的特点提出一下需求&#xff0c;看哪个框架更适合我们。 需求 1、接口编写…

项目工作分解工具WBS

WBS工作分解结构&#xff08;Work Breakdown Structure&#xff09;&#xff0c;是一个描述思路的规划和设计工具&#xff0c;它可以清晰地表示各项目之间相互联系的结构&#xff0c;详细说明为完成项目所必须完成的各项工作&#xff0c;也可以向高层管理者和客户报告项目完成的…

【Redis】高可用架构之Cluster集群和分⽚

高可用架构之Cluster集群和分⽚1. 前言2. Cluster 模式介绍2.1 什么是Cluster模式&#xff1f;2.2 为什么需要Cluster模式&#xff1f;2.2.1 垂直拓展&#xff08;scale up&#xff09;和水平拓展&#xff08;scale out&#xff09;2.2.2 Redis Cluster 提供的好处2.2.3 Redis …

学术期刊《广西物理》简介及投稿要求

学术期刊《广西物理》简介及投稿要求 《广西物理》&#xff08;季刊&#xff09;创刊于1980年&#xff0c;是由广西师范大学&#xff1b;广西物理学会主办的物理杂志。本刊报道与物理有关的基础研究、应用研究以及与此有关的交叉学科研究领域的最新重要研究成果和发展趋势&…

编码与加密基础笔记

文章目录&#x1f449;1、ASCII 编码&#x1f449;2、了解Base64&#x1f449;3、MD5消息摘要算法&#x1f449;4、对称加密与 AES&#x1f449;5、非对称加密与 RSA参考书籍《Python 3 反爬虫原理与绕过实战》&#x1f449;1、ASCII 编码 ASCII编码实际上约定了字符串和二进制…

Python中使用matplotlib时显示中文乱码_(或更改字体)

一、问题描述 在使用matplotlib绘制可视化图表时&#xff0c;图表的中文显示乱码&#xff0c;只能正常显示英文内容&#xff0c;如下图所示&#xff1a; 二、问题分析 一般显示乱码是由于编码问题导致的&#xff0c;而matplotlib 默认使用ASCII 编码&#xff0c;但是当使用pypl…

全语言通用的ASCIL表讲解,这一篇就够了

目录 ASCII码诞生背景 ASCII码特性 可显示字符 大佬们可以看这个 小白们看这个更详细 对控制字符的解释 ASCII码诞生背景 在计算机中&#xff0c;所有的数据在存储和运算时都要使用二进制表示。例如&#xff0c;像a、b、c、d这样的52个字母&#xff08;包括大写&#xf…

Winnolin 绘制药时曲线图C-T

文章目录前言一、各受试者C-T图1.导入数据2.设置-运行2.查看结果&#xff0c;修改参数二、各制剂C-T图1.导入数据2.设置-运行2.查看结果&#xff0c;修改参数三、平均C-T图1.计算统计量2.设置统计量计算参数&#xff08;Set Up&#xff09;3.绘图XY Plot4.查看结果&#xff0c;…

Java多线程基础面试总结(三)

线程的生命周期和状态 Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态&#xff1a; NEW&#xff1a;初始状态&#xff0c;线程被创建出来&#xff0c;但是还没有调用start()方法。RUNABLE&#xff1a;运行中状态&#xff0c;调用了start()…

Java设计模式 11-代理模式

代理模式 一、 代理模式(Proxy) 1、代理模式的基本介绍 代理模式&#xff1a;为一个对象提供一个替身&#xff0c;以控制对这个对象的访问。即通过代理对象访问目标对象.这样做的好处是: 可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能。被代理的对象…

时间序列的平稳性

如何检查时间序列是否平稳&#xff0c;如果它是非平稳的&#xff0c;我们可以怎么处理 当未来的数据与现在相似时&#xff0c;它更容易建模。平稳性描述了时间序列的统计特征不随时间变化的概念。因此一些时间序列预测模型&#xff0c;如自回归模型&#xff0c;依赖于时间序列…

IoC 之 Spring 统一资源加载策略【Spring源码】

《JavaPub源码》 文末是系列文章 IoC 之 Spring 统一资源加载策略 Spring 框架的核心是 IoC&#xff08;Inversion of Control&#xff09;&#xff0c;它将应用程序的对象依赖关系管理和控制反转到容器中。在 Spring IoC 容器中&#xff0c;组件的创建和配置是通过外部配置…

IDEA 运行启动 pulsar-manager项目

IDEA 运行 pulsar-manager项目&#xff08;gradle&#xff09; 1、下载pulsar-manager源码 giithub地址 smn-manager 2、将项目导入IDEA并初始化项目 问题&#xff1a;SSL peer shut down incorrectly 将https改成http之后又会出现 Server returned HTTP response code: …

服务(第四篇)Apache的网页优化

一、网页压缩 ①首先检查是否安装 mod_deflate 模块 ②如果没有安装mod_deflate 模块&#xff0c;重新编译安装 Apache 添加 mod_deflate 模块 yum -y install gcc gcc-c pcre pcre-devel zlib-devel cd /opt/httpd-2.4.29/ ./configure \ --prefix/usr/local/httpd \ --enabl…

HDFS学习笔记 【Namenode/DN管理】

说明 DN管理管理了什么&#xff1f; NN上如何表示DN DN存储和块的关系 梳理DatanodeManager存储类 DatanodeDescriptor DN的抽象&#xff0c;依次继承。每一层增加一点额外的信息。 DatanodeId 基本的DN信息&#xff0c;hostname&#xff0c;数据传输接口&#xff0c;info服…

QTableView 设置selection-background-color和border不同时生效问题记录

问题&#xff1a; qtableview在使用过程种设置qss样式&#xff0c;设置选中时的背景色&#xff0c;以及边框颜色&#xff0c;不能同时生效。 解决&#xff1a; 在qss中设置QTableView的样式时&#xff0c;对于item项&#xff0c;selection-background-color的参数设置应该分…

在将公司“一拆六”后,阿里巴巴未来将释放出哪些投资价值?

来源&#xff1a;猛兽财经 作者&#xff1a;猛兽财经 阿里巴巴为何要将公司拆分为六大业务集团 3月28日&#xff0c;阿里巴巴集团董事会主席兼首席执行官张勇发布全员信&#xff0c;宣布启动“16N”组织变革&#xff0c;将公司拆分为六大业务集团和多家业务分公司。 在阿里巴巴…

关于FPGA(Vivado)后仿真相关问题的探讨

FPGA后仿真时&#xff0c;相比于功能仿真增加了门延时和布线延时&#xff0c;相对于门级仿真增加了布线延时&#xff0c;因此后仿真相比于功能仿真具有不同的特点。 下面所示的代码在功能仿真时是正确的的&#xff0c;但在后仿真时&#xff0c;似乎是有问题的。功能很简…

大数据项目组-月度考核汇报0102

目录 01-2023年02月-月度考核汇报 2月份完成项目情况 2月份学习情况 3月份学习计划 老师点评 02-2023年03月-月度考核汇报 项目完成情况 本月学习内容 下月学习计划 老师点评 01-2023年02月-月度考核汇报 2月份完成项目情况 MySQL数据同步到ElasticSearch任务进展&a…