解决top-k问题--堆排序

目录

TOP-K问题

堆排序

考虑以下情况:

1.在n个数里面找最大的一个数

2.在n个数里面找最大的两个数

 3.在n个数中求前k大的数

 为什么不用大根堆呢?

代码

 时间复杂度



TOP-K问题

即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。

假设我们要在1e6个数中找出前10大个数。

方法一:整体排序(快排或者并排),取前面10个数,时间复杂度nlogn

方法二:堆排序,用一个容量为k(10)的小根堆存放这k(10)个最大的数,时间复杂度为nlogk(这里k为要求的前k个数)

如果对二叉树和堆不了解的朋友可以去看看我的上一篇博客

二叉树详讲(一)---完全二叉树、满二叉树、堆-CSDN博客

考虑以下情况:

1.在n个数里面找最大的一个数

通常的做法是用一个变量max1依次去比较数组中的每一个数,并更新。

int a[10] = {1,2,3,4,5,6,7,8,9,10 };
int max1 = 0;
for (int i = 0; i < 10; i++) {max1 = max(a[i], max1);
}
printf("%d \n", max1);

2.在n个数里面找最大的两个数

我们可以模仿上面的做法,用两个变量去比较数组中a[]的每一个元素,需要确定两个元素谁是代表最大,谁代表次大。假设max1表示最大,max2表示次大。对于每一个元素:先跟max2比较

1.如果a[i]大于max2。max2原来的值我们就一定不需要了。因为max1>max2,a[i]>max2,所以此时的max2的值已经不是这个数组次大的了。之后我们再调整a[i]和max1max2=min(a[i],max1),max1=max(a[i],max1)

2.如果a[i]小于或者等于max2.说明a[i]没有资格成为数组中前二的存在,也就不用考虑。

int a[10] = {1,2,3,4,5,6,7,8,9,10 };
int max1 = a[0];
int max2 = -1;
for (int i = 1; i < 10; i++) {if (a[i] > max2) {max2 = min(a[i], max1);max1 = max(a[i], max1);}else {continue;}
}
printf("%d %d\n", max1,max2);

 

 3.在n个数中求前k大的数

根据情况2我们可以扩展的想到,用k个变量去依次表示数组中的前k大的数。按照上面的比较方法,先跟这k个变量中最小的maxk去比较,如果大于此时的maxk,说明就目前来说,a[i]有资格进入这前k大的数,先把此时的maxk的值去掉,算上a[i]之后调整这k个数。这样一来,这k个变量一直维护着数组前k大的数。小的数不断地被淘汰,大的数不断地加入,被淘汰说明有前k个数大于自己,加入top-k说明此时的a[i]至少大于目前的maxk

这k个变量我们可以用一个数组或者树存起来,重要的是怎么实现“调整”这个动作。

由于数组中的每个元素都可能加入前top-k,那么每次加入的调整动作的时间复杂度就影响着整个算法的时间复杂度

这里我们就自然而然地想到了使用小根堆这种数据结构来维护这k个变量。

并且为了方便,用一个一维数组来模拟一棵二叉树,如何模拟可以去看上面我的博客链接。

利用小根堆的特性,每次遍历到a[i]的时候,让a[i]跟堆顶元素去比较,如果大于此时的堆顶元素,那么这个堆就先弹出堆顶元素,再将a[i]入堆.

 为什么不用大根堆呢?

注意我这里举的例子是求前k大的数。如果是大根堆则意味着堆顶元素是最大的数,诺此时的a[i]小于这个数,那么我们能判断a[i]是否有能进入堆的资格吗?显然是不能的。所以,用大根堆还是小根堆的关键在于,是否能利用堆顶的元素,不断地更新堆内元素,使得堆内元素不断接近top-k.

相反,如果求的是前k小的数,我们就可以用大根堆去维护了。 

代码

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<time.h>
typedef int HPDataType;
void Swap(HPDataType* a, HPDataType* b) {HPDataType t = *a;*a = *b;*b = t;
}
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 = (child - 1) / 2;//child = (child - 1) / 2;//parent = (parent - 1) / 2;}else{break;}}
}
void AdjustDown(HPDataType* a, int size, int pos)
{int parent = pos;int child = parent * 2 + 1;while (child < size){if (child + 1 < size && 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 GetTop(int* a, int k, int n) {assert(k <= n);//如果k大于n,不合法//建立一个k个数的小根堆int* topk = (int*)malloc(sizeof(int)*k);//初始化这个堆内元素,维护一个小根堆for (int i = 0; i < k; i++) {topk[i] = a[i];//入堆AdjustUp(topk, i);//向上调整,维护小根堆}for (int i = k; i < n; i++) {if (a[i] > topk[0]) {//topk[0]表示堆顶元素topk[0] = a[i];//将堆顶元素换为a[i],变现的去掉了堆顶元素,//但是此时的a[i]不一定就是堆里最小的//再向下调整,维护小根堆AdjustDown(topk, k, 0);}}//打印结果printf("前%d大的数为:",k);for (int i = 0; i < k; i++) {printf("%d ", topk[i]);}printf("\n");}int main() {int a[10] = { 1,2,3,4,5,6,7,8,9,10 };GetTop(a,4,10);return 0;
}

 向下调整函数AdjustDown()

大致思路就是,自上到下,为了维护小根堆,比较当前节点和孩子节点的值,并与最小的那个孩子交换,直到遍历到parent>=size为止

 

void AdjustDown(HPDataType* a, int size, int pos)//size为堆的大小,pos为需要调整的位置起点
{int parent = pos;//parent表示的是父亲节点int child = parent * 2 + 1;//child表示的是,左右孩子中值最小的节点,默认左孩子while (child < size){if (child + 1 < size && a[child + 1] <a[child])//如果右孩子才是最小的{++child;}if (a[child] < a[parent])//发现父亲节点的值大于孩子节点的值,不符合小根堆的特性		                    {  //交换当前节点和最小的孩子节点Swap(&a[child], &a[parent]);parent = child;//更新孩子节点和父节点child = parent * 2 + 1;}else{break;}}
}

向上调整函数 AdjustUp()

跟向下调整意思一致,方向相反。在入堆的时候,先把这个元素反在堆中最后面,由于此时这个元素可能比父亲节点的值要小,所以要在先上调整

 

void AdjustUp(HPDataType* a, int child)//child为向上调整的起点
{int parent = (child - 1) / 2;//向上找child的父亲节点//while (parent >= 0)while (child > 0){if (a[child] <a[parent])//如果child的值比父亲的还要小,交换{Swap(&a[child], &a[parent]);child = parent;parent = (child - 1) / 2;//child = (child - 1) / 2;//parent = (parent - 1) / 2;}else//否则就不需要再先上调整了,此时已经调整成功了{break;}}
}

 

堆排序

堆排序是一种基于堆数据结构的排序算法。它将待排序数组构造成一个二叉堆,然后进行堆排序。堆是一个完全二叉树,它的每个节点的值都大于等于(或小于等于)它的子节点的值。堆排序将堆的根节点与最后一个节点交换,然后将堆的大小减1,并进行堆的重构。这个过程将重复执行,直到堆的大小变为1。堆排序的时间复杂度为O(n log n),它是一种不稳定的排序算法。

前面利用堆求解决top-k问题,现在我们利用堆来对数组升序排序,与top-k问题不同的是,这里我们的堆排序在原本的数组上进行。

思考

升序排序用大根堆还是小根堆来维护呢?

如果用小根堆,取出来的堆顶元素就是最小的元素,应该放在数组的首元素位置。

可一旦首元素位置被确定为是最小的,整个堆的父节点与子节点的下标关系就被打乱了。而如果用大根堆,类似于冒泡排序。每次取出来的最大的数,我们都放在后面,然后堆的元素个数减1,前面还未被确定的数下标从0开始依旧是一个堆

升序排序用大根堆

将整个数组入堆并维护一个大根堆每次取出堆顶元素从数组最后面一个位置开始放,数组最后面的位置一定是最先取出的堆顶元素,也就是数组中最大的元素,依次向前是第二次取出的最大值、第三次、第三次、直到第n次。此时整个数组递增,每个下标的元素必定小于下一个下标的元素。


void HeapSort(int* a, int n) {//升序,用大根堆//建堆,从最后一个非叶子节点开始向下调整,时间复杂度o(n)for (int i = (n - 1 - 1) / 2; i >= 0; i--) {AdjustDown(a, n, i);}int end = n - 1;//end是此时需要确定的最大的数放的位置,从下标n-1开始while (end > 0) {Swap(&a[0], &a[end]);//将堆顶元素放在a[end]处AdjustDown(a, end, 0);//从[0,end)处调整end--;//更新end}for (int i = 0; i < n; i++) {printf("%d ", a[i]);}}
int main() {int a[10] = { 10,9,8,7,6,5,4,3,2,1 };HeapSort(a, 10);return 0;
}

时间复杂度

求前top-k问题

总的时间复杂度是:建堆的时间(klog(k))+遍历整个数组并调整的时间(n*log(k))

化简用大O表示法:Nlog(K)

N为数组元素个数,k为要求的前k大的数的个数

堆排序

时间复杂度为:建堆(o(n))+交换调整(o(nlog(n)))

化简用大O表示法:Nlog(N)

N为数组元素个数

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Java基本数据类型详解

✨个人主页&#xff1a;全栈程序猿的CSDN博客 &#x1f4a8;系列专栏&#xff1a;Java从入门到精通 ✌座右铭&#xff1a;编码如诗&#xff0c;Bug似流星&#xff0c;持续追求优雅的代码&#xff0c;解决问题如同星辰般自如 Java是一种强类型语言&#xff0c;数据类型在程序中起…

iOS NSDate的常用API

目录 一、创建日期 1.获取当前时间 2.当前时间指定秒数之后/前的时间 3.指定日期之后/后的时间 4.2001年之后/前指定秒数的时间 5.1970年之后/后指定秒数的时间 二、初始化日期 1.init 2.时间间指定秒数的时间 3.指定时间指定秒数之前/后的时间 4.2001年指定秒数之后…

QML中常见布局方法

目录 引言常见方法锚定&#xff08;anchors&#xff09;定位器Row、ColumnGridFlow 布局管理器RowLayout、ColumnLayoutGridLayoutStackLayout 总结 引言 UI界面由诸多元素构成&#xff0c;如Label、Button、Input等等&#xff0c;各种元素需要按照一定规律进行排布才能提高界…

Spring MVC学习随笔-控制器(Controller)开发详解:控制器跳转与作用域(二)视图模板、静态资源访问

学习视频&#xff1a;孙哥说SpringMVC&#xff1a;结合Thymeleaf&#xff0c;重塑你的MVC世界&#xff01;&#xff5c;前所未有的Web开发探索之旅 衔接上文Spring MVC学习随笔-控制器(Controller)开发详解&#xff1a;控制器跳转与作用域&#xff08;一&#xff09; SpingMVC中…

创建JDK8版本的SpringBoot项目的方法

目录 一.通过阿里云下载 二.通过IDEA创建 1.下载安装JDK17 2.创建SpringBoot 3.X的项目 3.把JDK17改成JDK8 截止到2023.11.24&#xff0c;SpringBoot不再支持3.0X之前的版本&#xff0c;3.0X之后的版本所对应的JDK版本为JDK17&#xff0c;下面介绍如何在idea上继续使用JDK…

python动态圣诞下雪图

运行图片 代码 import pygame import random# 初始化Pygame pygame.init()# 创建窗口 width, height 800, 600 screen pygame.display.set_mode((width, height)) pygame.display.set_caption(Christmas Tree)# 定义颜色 GREEN (34, 139, 34) RED (255, 0, 0) WHITE (255…

彻底删除VsCode配置和安装过的插件与缓存

前言 当你准备对 Visual Studio Code&#xff08;VSCode&#xff09;进行重新安装时&#xff0c;可能遇到一个常见问题&#xff1a;重新安装后&#xff0c;新的安装似乎仍然保留了旧的配置信息&#xff0c;这可能会导致一些麻烦。这种情况通常是由于卸载不彻底所致&#xff0c…

视图层与模板层

视图层 1 视图函数 一个视图函数&#xff0c;简称视图&#xff0c;是一个简单的Python 函数&#xff0c;它接受Web请求并且返回Web响应。响应可以是一张网页的HTML内容&#xff0c;一个重定向&#xff0c;一个404错误&#xff0c;一个XML文档&#xff0c;或者一张图片. . . 是…

android studio安装SDK时无法勾选

这两天帮助学妹安装android studio安装SDK时无法勾选&#xff0c;记录一下最终解决办法。头大。 核心 360 问题 网上所有方法都尝试了包括挂梯子&#xff0c;改hosts&#xff0c;盘符权限等等。 最终解决下载360 使用这两个&#xff0c;DNS注意要用8.8.8.8的 成功解决

C语言——指针(三)

&#x1f4dd;前言&#xff1a; 上篇文章C语言——指针&#xff08;二&#xff09;中对&#xff1a;指针的运算和指针变量类型对指针使用的影响开展了进一步的探讨&#xff0c;这篇文章我们继续学习一下指针与一维数组之间的关系&#xff1a; 1&#xff0c;对数组名的理解 2&am…

Go语言 值传递

官方说法&#xff0c;Go中只有值传递&#xff0c;没有引用传递 而Go语言中的一些让你觉得它是引用传递的原因&#xff0c;是因为Go语言有值类型和引用类型&#xff0c;但是它们都是值传递。 值类型 有int、float、bool、string、array、sturct等 引用类型有slice&#xff0c…

Python笔记1.2(with open() as file和open()、logging、os、shutil、glob、decode和encode)

Python笔记1.1&#xff08;字符串日期转换、argparse、sys、overwrite、eval、json.dumpsloads、os.system(cmd)、zfill、endswith、深浅拷贝&#xff09; Python笔记2&#xff08;函数参数、面向对象、装饰器、高级函数、捕获异常、dir&#xff09; Python笔记1.2 13、with …

PyLMKit(3):基于角色扮演的应用案例

角色扮演应用案例RolePlay 0.项目信息 日期&#xff1a; 2023-12-2作者&#xff1a;小知课题: 通过设置角色模板并结合在线搜索、记忆和知识库功能&#xff0c;实现典型的对话应用功能。这个功能是大模型应用的基础功能&#xff0c;在后续其它RAG等功能中都会用到这个功能。功…

Linux的基本指令(3)

目录 制作小文件&查看 nano指令 cat指令 tac指令 制作大文件&查看 一切皆文件 echo指令 > 输出重定向 以写"w"的形式打开文件 以追加"a"的形式打开文件 cat指令 < 输入重定向 创建big.txt more指令 less指令&#xff08;推…

Docker—更新应用程序

在本部分中&#xff0c;你将更新应用程序和映像。您还将了解如何停止和移除容器。 一、更新源代码 在以下步骤中&#xff0c;当您没有任何待办事项列表项时&#xff0c;您将把“空文本”更改为“您还没有待办事项&#xff01;在上面添加一个&#xff01;” 1、在src/static/…

HTML5 的全局属性 hidden 和 display:none 的关系

目录 1&#xff0c;hidden 和 display:none 的关系2&#xff0c;其他隐藏元素的方式2.1&#xff0c;语意上的隐藏2.2&#xff0c;视觉上的隐藏 1&#xff0c;hidden 和 display:none 的关系 hidden - MDN 参考 一句话总结&#xff1a;hidden 是HTML5 新增的全局布尔属性&…

数据挖掘之时间序列分析

一、 概念 时间序列&#xff08;Time Series&#xff09; 时间序列是指同一统计指标的数值按其发生的时间先后顺序排列而成的数列&#xff08;是均匀时间间隔上的观测值序列&#xff09;。 时间序列分析的主要目的是根据已有的历史数据对未来进行预测。 时间序列分析主要包…

Java数据结构之《构造哈夫曼树》题目

一、前言&#xff1a; 这是怀化学院的&#xff1a;Java数据结构中的一道难度中等(偏难理解)的一道编程题(此方法为博主自己研究&#xff0c;问题基本解决&#xff0c;若有bug欢迎下方评论提出意见&#xff0c;我会第一时间改进代码&#xff0c;谢谢&#xff01;) 后面其他编程题…

unity学习笔记17

一、动画组件 Animation Animation组件是一种更传统的动画系统&#xff0c;它使用关键帧动画。你可以通过手动录制物体在时间轴上的变换来创建动画。 一些重要的属性&#xff1a; 1. 动画&#xff08;Animation&#xff09;&#xff1a; 类型&#xff1a; Animation组件允许…

七、Lua字符串

文章目录 一、字符串&#xff08;一&#xff09;单引号间的一串字符&#xff08;二&#xff09;local str "Hello, "&#xff08;三&#xff09;[[ 与 ]] 间的一串字符&#xff08;四&#xff09;例子 二、字符串长度计算&#xff08;一&#xff09;string.len&…