【数据结构】堆——堆排序与海量TopK问题

news2024/11/15 19:39:26

目录

  • 前言
  • 一、堆排序
    • 1.1 整体思路
    • 1.2 代码部分
    • 1.3 建堆的时间复杂度
    • 1.4 堆排序的总结
  • 二、向下调整算法的时间复杂度
  • 三、向上调整算法的复杂度
  • 四、海量TopK问题
    • 4.1 TopK题目
  • 总结


前言

上一篇我们学习了堆的数据结构,现在我们来看看堆的日常应用和排序


一、堆排序

首先,我们不能因为排个序,而写一个堆的数据结构,过于麻烦
但是正常我们排序的数组都是乱序而且并不是堆结构
而使用堆排序之前,必须用到向下调整算法
向下调整算法:该结点的左右子树都必须是小堆或大堆才能使用

在这里插入图片描述

经过上面的结论,我们得出第一步需要建堆

通过向下调整,从倒数第一个非叶子节点开始调整,因为你从顶开始向下调整是错误的,必须左右子树都是小堆或大堆,默认乱序是不能调整的。

在这里插入图片描述

  • 为什么不从最后一个节点开始调?

    • 最后的都是叶子结点不用调整,因为没有左右子树,默认是堆结构

现在又多了一个问题,排升序到底是建大堆还是建小堆呢?

建大堆,为什么不建小堆呢,小堆堆顶就是最小值,最小值不正好在最前面吗,为什么要建大堆?

  • 如果建小堆的话,会破坏堆的结构,又需要重新选择根,之前堆的关系都会乱掉,导致我们辛辛苦苦的建的堆结构浪费了
    在这里插入图片描述
    建好了大堆,我们改如何进行排序呢?
    堆顶是最大值,我们需要将堆顶的数据和最后一个数据交换,但是同样也破坏了大根堆的结构,但是堆顶数据依旧能使用向下调整算法
    在这里插入图片描述

1.1 整体思路

  1. 建堆,排升序:建大堆,排降序:建小堆
  • 通过向下调整,从倒数第一个非叶子节点开始调整
  1. 堆结构建立完,就排序
  • 堆顶的数据和最后一个数据交换,排好的数据排除掉(缩小区间,end–),在重新向下调整把次大的数据放到堆顶,直到堆顶结束(end>0)

1.2 代码部分

// 交换
void Swap(int* pa, int* pb)
{
	int tmp = *pa;
	*pa = *pb;
	*pb = tmp;
}

// 向下调整
// 大堆
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 HeapSort(int* a, int n)
{
	// 建大堆,从最后非叶子结点开始到第一个结点
	for (int i = (n - 1 - 1) / 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--;					// 已经排好的数据,给排除
	}
}

1.3 建堆的时间复杂度

时间复杂度是求最坏的情况,我们假设需要建一颗满二叉树的堆,满二叉树也是特殊的完全二叉树,从最后一个非叶子结点开始调整一共需要调整多少次呢?
在这里插入图片描述

通过上图我们等到了一个求全部节点的最大调整等式
在这里插入图片描述
此时该等式是由数列组成,等比数列和等差数列,这里我们运用错位相减法乘以q公比2,得出新等式②减去等式①

在这里插入图片描述

如图得出建堆的时间复杂度是O(N)

1.4 堆排序的总结

建堆的时间复杂度是O(N),排序的时间复杂度是O(n * logn),整体是O(n * logn),具体怎么算出来的大家可以看下面内容。
建堆需要从倒数第一个非叶子结点开始调整,从后往前,直到堆顶的数据

  • 最后一个非叶子结点的计算方式:n - 1是最后一个节点的下标,父亲 = (孩子 - 1) / 2,所以(n - 1 - 1) / 2

排序中向下调整注意事项:

  • end是最后一个数据的下标,我们先交换,在向下调整,就传过去了end(n-1)个要调整的数据,正好排除掉了最后已经排序好的数据
  • 调整的次数:end > 0,即可,不用>=0。end == 0 就是自己和自己交换和调整。

测试
在这里插入图片描述

二、向下调整算法的时间复杂度

堆顶开始调整是最坏的情况,有3层需要调2次,得出h层,最多需要调h-1次, l o g 2 N + 1 log_2N+1 log2N+1,得出时间复杂度为 O ( l o g 2 N ) O(log_2N) O(log2N)
在这里插入图片描述

堆排序时间复杂度解析
一个数调整一次是 O ( l o g 2 N ) O(log_2N) O(log2N),排序的时候除了叶子节点不能调整,其他结点都需要调整,叶子可以忽略不计,所以排序的时间复杂度是 O ( N ∗ l o g 2 N ) O(N*log_2N) O(Nlog2N),建好堆和排完序一共时间复杂度是 O ( N + N ∗ l o g 2 N ) O(N +N*log_2N) O(N+Nlog2N),但是取最大阶,所以整体是 O ( N ∗ l o g 2 N ) O(N*log_2N) O(Nlog2N)

三、向上调整算法的复杂度

最后一个节点开始调整是最坏的情况,有3层需要调2次,得出h层,最多需要调h-1次, l o g 2 N + 1 log_2N+1 log2N+1,得出时间复杂度为 O ( l o g 2 N ) O(log_2N) O(log2N)
在这里插入图片描述

向上调整的主要用途是在插入数据后进行调整,让新数据插入进来一直保持堆结构

四、海量TopK问题

4.1 TopK题目

海量Topk题目
求出最小的k个数,输入n个整数,如:1,3,5,7,2,4,6,8,k = 4,返回1,2,3,4
面试里经常遇到这样的问题:有一亿个浮点数,如何找出其中最大的10000个?

这类问题,我们就称为海量TopK问题:指从大量数据(源数据)中获取最大(或最小)的K个数据。

在这里插入图片描述

思路:

  1. 我们先开辟一个k个数据的数组,应为在函数内不能创建临时变量,要用malloc
  2. 让n(源数据)的前k个拷到topK数组里,为了作建堆的数据来源
  3. 把当前topK数组调整成大堆结构
  4. 正常情况:应该是建小堆,然后用剩下的数据比哪个更小,剩下的数据更小就放到堆顶,然后就调整就能保持堆的结构,同时最小的数就排到堆顶
  • 这里用的我用的逆向,建的大堆,用大堆的堆顶(最大值)和剩下的数比较,堆顶大就交换,最终目的是让剩下的数据都比大堆堆顶(堆的最大值)大。
  • 这样就能满足topK数组是最小的k个数,因为最后大堆的堆顶(堆内的最大值)都是比剩下的数据小了,肯定就是最小的前k个数了

时间复杂度是 O ( n ∗ l o g 2 K ) O(n*log_2K) O(nlog2K),空间复杂度为 O ( K ) O(K) O(K)

// 交换
void Swap(int* pa, int* pb)
{
	int tmp = *pa;
	*pa = *pb;
	*pb = tmp;
}

// 向下调整
// 大堆
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;
		}
	}
}

int* smallestK(int* arr, int arrSize, int k, int* returnSize){
		
    // 确定返回个数
    *returnSize = k;
    
    // k为0,直接返回空,不判定会越界
    if (k <= 0)
    {
        return NULL;
    }
		
    // 开辟k个数据空间,因为需要返回指针,不能创建临时变量
    int* topK = (int*)malloc(k * sizeof(int));

    // 将数组的前k个放入topk数组
    for (int i = 0; i < k; i++)
    {
        topK[i] = arr[i];
    }
	
    // 当前topK数组调整成大堆结构
    for (int i = (k - 1 - 1) / 2; i >= 0; i--)
    {
        AdjustDown(topK, k, i);
    }

    // 拿剩下arrSize和topK数组比较
    // 正常情况:应该是建小堆,然后用剩下的数据比哪个更小
    // 剩下的数据更小就放到堆顶,然后就调整就能保持堆的结构,同时最小的数就排到堆顶

    // 我用的逆向,建的大堆,用大堆的堆顶(最大值)和剩下的数比较,堆顶大就交换,最终目的是让剩下的数据都比大堆堆顶(堆的最大值)大,这样就能满足topK数组是最小的k个数,因为最后大堆的堆顶(堆内的最大值)都是比剩下的数据小了
    for (int i = k; i < arrSize; i++)
    {
        // 堆顶大,就剩下的小数据进来堆在调整
        if (topK[0] > arr[i])
        {
            topK[0] = arr[i];
            AdjustDown(topK, k, 0);
        }
    }
    return topK;
}

在这里插入图片描述
类似的TopK问题还有:

  • 有10000000个记录,这些查询串的重复度比较高,如果除去重复后,不超过3000000个。一个查询串的重复度越高,说明查询它的用户越多,也就是越热门。请统计最热门的10个查询串,要求使用的内存不能超过1GB。
  • 有10个文件,每个文件1GB,每个文件的每一行存放的都是用户的query,每个文件的query都可能重复。按照query的频度排序。
  • 有一个1GB大小的文件,里面的每一行是一个词,词的大小不超过16个字节,内存限制大小是1MB。返回频数最高的100个词。
  • 提取某日访问网站次数最多的那个IP。
  • 10亿个整数找出重复次数最多的100个整数。
  • 搜索的输入信息是一个字符串,统计300万条输入信息中最热门的前10条,每次输入的一个字符串为不超过255B,内存使用只有1GB。
  • 有1000万个身份证号以及他们对应的数据,身份证号可能重复,找出出现次数最多的身份证号。

总结

通过近期堆方面的两篇文章,想必大家也对堆有了一定了解,堆排序和TopK问题是重中之重,而堆在处理大量数据时候能快速解决问题,大家一定要熟练掌握

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

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

相关文章

内网安全-横向移动【3】

1.域横向移动-内网服务-Exchange探针 Exchange是一个电子右键服务组件&#xff0c;由微软公司开发。它不仅是一个邮件系统&#xff0c;还是一个消息与协作系统。Exchange可以用来构建企业、学校的邮件系统&#xff0c;同时也是一个协作平台&#xff0c;可以基于此开发工作流、…

用广播星历计算卫星运动的平均角速度

用广播星历计算卫星位置 1.计算卫星运动的平均角速度 首先根据广播星历中给出的参数计算参考时刻的平均角速度: 式中&#xff0c;GM为万有引力常数G与地球总质量M之乘积&#xff0c;其值为GM3.98600510^14b m3/s2。 然后根据广播星历中给定的摄动参数计算观测时刻卫星的平均…

模版方法模式template method

学习笔记&#xff0c;原文链接 https://refactoringguru.cn/design-patterns/template-method 超类中定义了一个算法的框架&#xff0c; 允许子类在不修改结构的情况下重写算法的特定步骤。 上层接口有默认实现的方法和子类需要自己实现的方法

论文阅读笔记: Segment Anything

文章目录 Segment Anything摘要引言任务模型数据引擎数据集负责任的人工智能 Segment Anything Model图像编码器提示编码器mask解码器解决歧义损失和训练 Segment Anything 论文地址: https://arxiv.org/abs/2304.02643 代码地址:https://github.com/facebookresearch/segment-…

C++-----STL简介(了解)

1. 什么是STL STL(standard template libaray-标准模板库)&#xff1a;是C标准库的重要组成部分&#xff0c;不仅是一个可复用的组件库&#xff0c;而且是一个包罗数据结构与算法的软件框架。 2. STL的版本 原始版本 Alexander Stepanov、Meng Lee 在惠普实验室完成的原始版…

手撕Python之生成器、装饰器、异常

1.生成器 生成器的定义方式&#xff1a;在函数中使用yield yield值&#xff1a;将值返回到调用处 我们需要使用next()进行获取yield的返回值 yield的使用以及生成器函数的返回的接收next() def test():yield 1,2,3ttest() print(t) #<generator object test at 0x01B77…

MATLAB-基于高斯过程回归GPR的数据回归预测

目录 目录 1 介绍 1. 1 高斯过程的基本概念 1.2 核函数&#xff08;协方差函数&#xff09; 1.3 GPR 的优点 1.4. GPR 的局限 2 运行结果 3 核心代码 1 介绍 高斯过程回归&#xff08;Gaussian Process Regression, GPR&#xff09;是一种强大的非参数贝叶斯方法&…

JAVA- 多线程

一&#xff0c;多线程的概念 1.并行与并发 并行&#xff1a;多个任务在同一时刻在cpu 上同时执行并发&#xff1a;多个任务在同一时刻在cpu 上交替执行 2.进程与线程 进程&#xff1a;就是操作系统中正在运行的一个应用程序。所以进程也就是“正在进行的程序”。&#xff0…

Java 数据类型详解:基本数据类型与引用数据类型

在 Java 编程语言中&#xff0c;数据类型主要分为两大类&#xff1a;基本数据类型和引用数据类型。理解这两种类型的区别、使用场景及其转换方式是学习 Java 的基础。本文将深入探讨这两类数据类型的特点&#xff0c;并展示自动类型转换、强制类型转换以及自动拆箱和封箱的使用…

Level3 — PART 3 — 自然语言处理与文本分析

目录 自然语言处理概要 分词与词性标注 N-Gram 分词 分词及词性标注的难点 法则式分词法 全切分 FMM和BMM Bi-direction MM 优缺点 统计式分词法 N-Gram概率模型 HMM概率模型 词性标注(Part-of-Speech Tagging) HMM 文本挖掘概要 信息检索(Information Retr…

AI预测福彩3D采取888=3策略+和值012路或胆码测试9月8日新模型预测第81弹

经过80期的测试&#xff0c;当然有很多彩友也一直在观察我每天发的预测结果&#xff0c;得到了一个非常有价值的信息&#xff0c;那就是9码定位的命中率非常高&#xff0c;70多期一共只错了8次&#xff0c;这给喜欢打私房菜的朋友提供了极高价值的预测结果~当然了&#xff0c;大…

线性代数|机器学习-P36在图中找聚类

文章目录 1. 常见图结构2. 谱聚类 感觉后面几节课的内容跨越太大&#xff0c;需要补充太多的知识点&#xff0c;教授讲得内容跨越较大&#xff0c;一般一节课的内容是书本上的一章节内容&#xff0c;所以看视频比较吃力&#xff0c;需要先预习课本内容后才能够很好的理解教授讲…

代码日常问题 --day01

1.刚开始我遇到的问题 1.1项目场景&#xff1a; 首先&#xff0c;请确认已经成功创建了一个Maven项目。 接下来&#xff0c;为了验证JDK和Maven配置是否正确&#xff0c;我需要访问项目的设置页面。 操作路径是点击“File”菜单栏选项&#xff0c;然后选择“Settings”。 …

可公开的公开学习分享课

2024.9.8AI分享 1、推荐软件 --智谱清言 2、通义灵码 通义灵码官网 3、沉浸式翻译 &#xff08;看英文文献&#xff09; 沉浸式翻译官网 4、aicheck.cc &#xff08;AI 写论文的工具–形成提纲&#xff09;–不免费 AI 写论文的工具 无法挑战可以复制粘贴 5、aminer.cn&…

Nginx 是如何解决惊群效应的?

什么是惊群效应&#xff1f; 第一次听到的这个名词的时候觉得很是有趣&#xff0c;不知道是个什么意思&#xff0c;总觉得又是奇怪的中文翻译导致的。 复杂的说&#xff08;来源于网络&#xff09;TLDR; 惊群效应&#xff08;thundering herd&#xff09;是指多进程&#xff…

养老院管理系统小程序的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;管理员管理&#xff0c;护工管理&#xff0c;老人管理&#xff0c;房间管理&#xff0c;公告信息管理&#xff0c;床位管理&#xff0c;健康信息管理 微信端账号功能包括&#xff1a;系统首页&#xf…

【PyTorch】常用网络层layers总结

文章目录 前言一、Convolution Layers二、Pooling Layers三、Padding Layers总结 前言 PyTorch中网络搭建主要是通过调用layers实现的&#xff0c;这篇文章总结了putorch中最常用的几个网络层接口及其参数。 一、Convolution Layers pytorch官方文档介绍了众多卷积层算法&…

017.PL-SQL编程—函数

我 的 个 人 主 页&#xff1a;&#x1f449;&#x1f449; 失心疯的个人主页 &#x1f448;&#x1f448; 入 门 教 程 推 荐 &#xff1a;&#x1f449;&#x1f449; Python零基础入门教程合集 &#x1f448;&#x1f448; 虚 拟 环 境 搭 建 &#xff1a;&#x1f449;&…

网上花店管理系统小程序的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;管理员管理&#xff0c;客服聊天管理&#xff0c;基础数据管理&#xff0c;论坛交流管理&#xff0c;公告信息管理&#xff0c;用户管理&#xff0c;轮播图信息 微信端账号功能包括&#xff1a;系统首…

Shader 渲染路径

实际的游戏开发中&#xff0c;场景中的光源肯定是更多、更复杂的&#xff0c;如果只有一个平行光的处理&#xff0c;完全不能满足需求。处理更多的光源&#xff0c;我们就需要了解Unity底层是如何处理这些光源的。 1、渲染路径是什么 渲染路径&#xff08;Rendering Path&…