数据结构——Top-k问题

Top-k问题

  • 方法一:堆排序(升序)(时间复杂度O(N*logN))
    • 向上调整建堆(时间复杂度:O(N * logN) )
    • 向下调整建堆(时间复杂度:O(N) )
    • 堆排序代码
  • 方法二:直接建堆法(时间复杂度:O(N+k×logN) ≈ O(N))
  • 方法三:建一个k个数的小堆(时间复杂度O(k+(N-k)×logk)≈O(N))

方法一:堆排序(升序)(时间复杂度O(N*logN))

升序堆排序的一般思路就是将给定的一组数据放在堆得数据结构当中去,然后进行不断被的取堆顶元素放在数组当中,不断地pop(删除)。但是这种方法太麻烦了,自己还要手写一个堆的数据结构以及一些接口函数,还要创建数组等,显然不是最优解。
接上文的写的两种调整方式,向上调整和向下调整。 详细见
思路:
①可以用向上调整或者向下对原数组进行调整,也就是建一个大堆(排升序大堆后面讲为啥)
②接下来利用堆删除的原理,将堆顶的数据和数组最后一个交换(也就是将堆顶和堆尾的数据进行交换),然后就相当于最大的数放在了最后一个位置,所以最后一个位置的数据确定了,接下来对剩下的数据进行向下调整,再重复以上操作。
在这里插入图片描述

ps:排升序建大堆而不是小堆的原因,反证思路来看,若建小堆的话,最小的数据在第一个,第一个数据确定了,但是剩下的数据很暖再重新调整成为一个新的小堆的数据结构,所以排升序建小堆很难是实现

向上调整和向下调整都可以完成建堆的操作,但是他们的时间复杂度有所不同,接下来讲一下他们的取舍。

向上调整建堆(时间复杂度:O(N * logN) )

for (int i = 1; i < n; i++){AdjustUp(a, i);}

因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明(时间复杂度本来看的就是近似值,多几个节点不影响最终结果):
在这里插入图片描述

向下调整建堆(时间复杂度:O(N) )

for (int i = (n - 1) / 2; i >= 0; i--){AdjustDown(a, n, i);//a是堆的数组,n是防止数组越界的数组数据个数,i是开始向下调整的位置}

向下调整建堆得思路:从第一个非叶子结点开始从数组得后面向前面一个一个进行调整
在这里插入图片描述

复杂度证明:
在这里插入图片描述

看到这里很容易发现向下调整方法建堆得时间复杂度更加合适

堆排序代码

// 对数组进行堆排序
void HeapSort(int* a, int n)
{//思路:向上调整方法建堆建堆//>这里排升序要建大堆,因为建小堆的话,接下来排升序第一个数据好处理,//但是剩下的数据重新排成堆的话关系全都乱了//所以这时候建大堆,和删除的思路一样,首先交换堆顶和堆尾,这时候最大的数据放到了最后一个位置//然后将前面n-1个数据看成新的堆进行向下调整,然后再找次大的数放在倒数第二的位置//建堆有两种方式 向上调整建堆和向下调整建堆,时间复杂度分别为O(N*logN)和O(N)//向上调整建堆 建堆时间复杂度 O(N*logN)/*for (int i = 1; i < n; i++){AdjustUp(a, i);}*///向下调整建堆  时间复杂度为O(N)//向下调整的条件是调整节点的左右子树都为大堆或者小堆//所以从下边开始进行向下调整(大堆),这时候大的数会慢慢上浮,最先调整的位置不能是叶子节点,那样会重复很多操作//应该从最后一个父亲节点开始进行向下调整 最后一个父亲节点的下标为 (n-1)/2,然后按数组下标的顺序递减进行调整for (int i = (n - 1) / 2; i >= 0; i--){AdjustDown(a, n, i);//a是堆的数组,n是防止数组越界的数组数据个数,i是开始向下调整的位置}while (--n)  //排序的时间复杂度为O(N*logN){//此处为--n的原因:一共有n个数据,循环一次确定一个数据的位置,循环n-1次之后就可以确定n-1个数据的位置,// 前面已经确定了n-1个位置,最后一个数据的位置也已经确定,所以当n=0的时候的那一次循环可以不需要进行就可以// //此时n已经自减1,所以此时n就为堆尾数据,且n为下一个新堆的数据个数,所以后面向下调整直接传参nSwap(&a[0], &a[n]);//交换堆顶和堆尾的数据AdjustDown(a, n, 0);//n为新堆的数据个数}//总结:堆排序的时间复杂度为O(N*logN),因为堆排序有两个步骤①建堆②排序//建堆向上调整建堆的时间复杂度为O(N*logN),向下调整建堆的时间复杂度为O(N),相对较快,//但是排序的时间复杂度都为O(N*logN),所以决定了堆排序的时间复杂度为O(N*logN)。
}

方法二:直接建堆法(时间复杂度:O(N+k×logN) ≈ O(N))

思路:有了堆排序中的向下调整建堆法,可以将这N个数建一个小堆,然后取堆顶元素打印,然后Pop(删除)k次这样稍微简单些,但是当数据个数N太大得时候,对内存得要求很大,会占用很多内存,因为这种方法中,需要在堆区中创建一个N个数据的动态1数组,然后将数据放在数组当中去,因为如果在N很大的情况下,有可能你的设备内存并没有那么大。所以这是这种方法的缺点。
一般数据都是放在磁盘或者文件中,所以我接口函数可以传文件流

//创建随机n个数据
void CreatData(int n)
{int k = 10;srand((unsigned int)time(NULL));//调用rand()的返回值形成随机数必须调用srand函数//srand函数的形参为unsigned int类型的,且形参为不断变化的数才可以生成为随机数//所以形参传参为时间戳函数time,time函数的形参得传NULLconst char* file = "data.txt";FILE* pf = fopen(file, "w");if (pf == NULL){perror("fopen fail\n");return;}else{printf("打开成功\n");}for (int i = 0; i < n; i++){fprintf(pf, "%d\n", rand() % 10000);}fclose(pf);//关闭文件pf = NULL;
}//打印n个数据中最大的k个数据
void PrintTop1(const char* file, int k, int n)//n是数据个数
{FILE* pf = fopen(file, "r");if (pf == NULL){perror("PrintTop fopen fail");return;}//1.把n个数建大堆int* top = (int*)malloc(sizeof(int) * n);//创建一个动态数组存放堆的数据if (top == NULL){perror("top malloc fail\n");return;}//读取n个数放在堆当中去for (int i = 0; i < n; i++){fscanf(pf, "%d", &top[i]);}//向下调整建大堆for (int i = (n - 1) / 2; i >= 0; i--){AdjustDown(top, n, i);}//打印 k次堆顶元素for (int i = 0; i < k; i++){printf("%d ", top[0]);//打印堆顶元素Swap(&top[0], &top[n - i]);AdjustDown(top, n - i - 1, 0);//这里交换后的那个数据不能算入内}
}
//测试CreatData(10000000);//创建数据
PrintTop1("data.txt", 10,10000000);//这里测试时候可以直接只改PrintTop1中n的大小,因为前面CreatData创建数据会花费很多时间,导致第二个函数并不能直接运行//CreatData(10000000);//创建数据
//PrintTop1("data.txt", 10, 100000000);//CreatData(10000000);//创建数据
//PrintTop1("data.txt", 10, 1000000000);

代码先放在这里,接下来我来验证:
在这里插入图片描述
所以这种方法当数据N很大的时候并不可取

方法三:建一个k个数的小堆(时间复杂度O(k+(N-k)×logk)≈O(N))

思路:取前k个数建立一个k个数的小堆,然后遍历剩下的所有数据,并和堆顶进行比较,只要比堆顶大就和堆顶交换,然后进行调整,然后进行循环,遍历结束之后也就证明这k个数为所要求的k个数。例如:
在这里插入图片描述
代码:

//小堆的向下调整   和大堆的向下调整一样
void AdjustDownSmall(HPDateType* a, int n, int parent)//向下调整(小堆)  时间复杂度O(logN)
//向下调整的条件是调整节点的左右子树都为大堆或者小堆
//思路为从下标为parent位置开始向下的孩子节点不断比较进行调整,直到最后一个数据,
//所以需要传参堆的有效数据个数n
{int leftchild = parent * 2 + 1;while (leftchild < n)//{int rightchild = parent * 2 + 2;if (rightchild < n && a[leftchild] > a[rightchild]){//判断一下该父亲节点是否有右孩子,防止数组越界//默认左孩子小于右孩子//若右孩子小于左孩子则交换下标Swap(&leftchild, &rightchild);}if (a[parent] > a[leftchild]){Swap(&a[parent], &a[leftchild]);//若不符合堆的要求则交换parent = leftchild;//将原父亲数据对应下标也赋值过来leftchild = parent * 2 + 1;//新的孩子的下标}else//若符合堆的要求就退出循环{break;}}
}//Top-k 问题  取前k个较大的数
void PrintTop(const char* file,  int k)//把文件传进来和需要找的前k个数
{FILE* pf = fopen(file, "r");if (pf == NULL){perror("PrintTop fopen fail");return ;}//1.把前k个数建小堆int* top = (int*)malloc(sizeof(int) * k);//创建一个动态数组存放堆的数据if (top == NULL){perror("top malloc fail\n");return;}//从文件中读取k个数据放在数组中for (int i = 0; i < k; i++){fscanf(pf, "%d", &top[i]);}for (int i = (k - 1) / 2; i >= 0; i--){AdjustDownSmall(top, k, i);}//2.遍历剩下的n-k个数并与堆顶作比较,若比堆顶大则交换然后再进行向下调整int val = 0;int ret = fscanf(pf, "%d", &val);while (ret != EOF){if (val > top[0]){Swap(&val, &top[0]);AdjustDownSmall(top, k, 0);}ret = fscanf(pf, "%d", &val);}//打印数组for (int i = 0; i < k; i++){printf("%d\n", *(top+i));//printf("%d\n", top[i]);//会报错C6385//像数组一样在连续内存空间存储的多个数据才使用下标法//这种应该是编译器问题 具体不清楚}free(top);top = NULL;fclose(pf);pf = NULL;
}

分析:对比时间复杂度方法二和方法三的时间复杂度都差不多,方法三在N为很大的情况下,所用内存空间是取决于k的,因为k一般是一个很小的数一般不会很大,导致内存崩溃。
在这里插入图片描述

方法二 方法三自己实测其实时间上,对于1000 0000个数据的时候运行的时候都会大约等待个5 6秒,所以他们的时间复杂度大差不差,优势在于空间的使用。

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

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

相关文章

LeetCode---386周赛

题目列表 3046. 分割数组 3047. 求交集区域内的最大正方形面积 3048. 标记所有下标的最早秒数 I 3049. 标记所有下标的最早秒数 II 一、分割数组 这题简单的思维题&#xff0c;要想将数组分为两个数组&#xff0c;且分出的两个数组中数字不会重复&#xff0c;很显然一个数…

Redis 的哨兵模式配置

1.配置 vim sentinel.conf# mymaster 给主机起的名字 # 192.168.205.128 主机的ip地址 # 6379 端口号 # 2 当几个哨兵发现主观宕机&#xff0c;则判定为客观宕机。 原则上是大于一半。比如三个哨兵&#xff0c;则设置为 2 sentinel monitor mymaster 192.168.205.128 63…

【动态规划入门】01背包问题

每日一道算法题之01背包问题 一、题目描述二、思路三、C++代码四、结语一、题目描述 题目来源:Acwing 有N件物品和一个容量是 V的背包。每件物品只能使用一次。第 i件物品的体积是 vi,价值是 wi。 求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大…

LeetCode题练习与总结:合并K个升序链表

一、题目 给你一个链表数组&#xff0c;每个链表都已经按升序排列。 请你将所有链表合并到一个升序链表中&#xff0c;返回合并后的链表。 二、解题思路 创建一个最小堆&#xff08;优先队列&#xff09;来存储所有链表的头节点。这样我们可以始终取出当前所有链表中值最小…

人工智能指数报告2023

人工智能指数报告2023 主要要点第 1 章 研究与开发第 2 章 技术性能第 3 章 人工智能技术伦理第 4 章 经济第 5 章 教育第 6 章 政策与治理第 7 章 多样性第 8 章 舆论 人工智能指数是斯坦福大学以人为本的人工智能研究所&#xff08;HAI&#xff09;的一项独立倡议&#xff0c…

Java 石头剪刀布小游戏

一、任务 编写一个剪刀石头布游戏的程序。程序启动后会随机生成1~3的随机数&#xff0c;分别代表剪刀、石头和布&#xff0c;玩家通过键盘输入剪刀、石头和布与电脑进行5轮的游戏&#xff0c;赢的次数多的一方为赢家。若五局皆为平局&#xff0c;则最终结果判为平局。 二、实…

redis 为什么会阻塞

目录 前言 客户端交换时的阻塞 redis 磁盘交换的阻塞 主从节点交互的阻塞 切片集群交互时的阻塞 异步执行的演变 redis 异步执行如何实现的 前言 大家对redis 比较熟悉吧&#xff0c;只要做项目都会用到redis&#xff0c;提高系统的吞吐。小米商城抢购高峰18k的qps&…

KubeSphere平台安装系列之三【Linux多节点部署KubeSphere】(3/3)

**《KubeSphere平台安装系列》** 【Kubernetes上安装KubeSphere&#xff08;亲测–实操完整版&#xff09;】&#xff08;1/3&#xff09; 【Linux单节点部署KubeSphere】&#xff08;2/3&#xff09; 【Linux多节点部署KubeSphere】&#xff08;3/3&#xff09; **《KubeS…

一句话讲清楚数据库中事务的隔离级别(通俗易懂版)

为什么我只说通俗易懂版不说严谨版? 因为严谨版遍地都是, 但是他们却有一个缺点就是让人看得云里雾里, 所以这就是我写通俗易懂版的初衷! 但是既然是通俗易懂版就必然有缺陷, 只为了各位在开发过程中头脑更加清晰, 如有错误还望兄弟们不吝赐教! 在MySQL数据库中,事务一共有4…

C语言之strcmp函数,strlen函数

strcmp函数是比较两个字符串ASCII大小的函数。 比较方式是自左向右比较&#xff0c;直到出现不同字符或者\0为止 语法格式 strcmp(字符串1,字符串2&#xff09; 如果两个字符串相同&#xff0c;会返回数值0 如果字符串1>字符串2,会返回一个正数 如果字符串1<字符串2…

新一代电话机器人开源PHP源代码

使用easyswoole 框架开发的 新一代电话机器人开源PHP源码 项目地址&#xff1a;https://gitee.com/ddrjcode/robotphp 代理商页面演示地址 http://119.23.229.15:8080 用户名&#xff1a;c0508 密码&#xff1a;123456 包含 AI外呼管理&#xff0c;话术管理&#xff0c;CR…

每日一题 — 复写零

1089. 复写零 - 力扣&#xff08;LeetCode&#xff09; 思路&#xff1a; 首先找到最后一个复写的数&#xff1a; 双指针算法&#xff1a; 1、先判断 cur 位置上的值 2、然后决定 dest 移动一步还是两步 3、然后判断 dest 是否到终点了 4、最后 cur 处理越界的情况 arr[n-1] …

使用sourceCompatibility = 11不匹配解决方法

运行springbootgradle项目报错。 原因&#xff1a;在生产该项目时&#xff0c;选择的JDK是11版本的&#xff0c;但是本地电脑只安装了1.8版本。不兼容所以报错。 解决办法&#xff1a; 找到build.gradle配置文件—>找到sourceCompatibility ‘11’—>把11改成自己本地…

思维题(蓝桥杯 填空题 C++)

目录 题目一&#xff1a; ​编辑 代码&#xff1a; 题目二&#xff1a; 代码&#xff1a; 题目三&#xff1a; 代码&#xff1a; 题目四&#xff1a; 代码&#xff1a; 题目五&#xff1a; 代码&#xff1a; 题目六&#xff1a; 代码七&#xff1a; 题目八&#x…

用python和pygame库实现刮刮乐游戏

用python和pygame库实现刮刮乐游戏 首先&#xff0c;确保你已经安装了pygame库。如果没有安装&#xff0c;可以通过以下命令安装&#xff1a; pip install pygame 示例有两个。 一、简单刮刮乐游戏 准备两张图片&#xff0c;一张作为背景bottom_image.png&#xff0c;一张作…

Leetcoder Day35| 动态规划part02

62.不同路径 一个机器人位于一个 m x n 网格的左上角 &#xff08;起始点在下图中标记为 “Start” &#xff09;。 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角&#xff08;在下图中标记为 “Finish” &#xff09;。 问总共有多少条不同的路径&#xff…

如何在Linux配置C、C++、Go语言的编译环境?

在 Linux 系统上配置 C、C、Go 语言的编译环境可以通过安装相应的编译器和相关工具来实现。以下是在 Linux 系统上配置这些语言的编译环境的一般步骤&#xff1a; 1. C 和 C 编译环境配置&#xff1a; 安装 GCC 编译器&#xff08;一般 Linux 发行版都会包含&#xff09;&…

Android 显示系统框架

一.FrameBuffer FrameBuffer 介绍&#xff1a; FrameBuffer中文译名为帧缓冲驱动&#xff0c;它是出现在2.2.xx内核中的一种驱动程序接口。主设备号为29&#xff0c;次设备号递增。 Linux抽象出FrameBuffer这个设备来供用户态进程实现直接写屏。FrameBuffer机制模仿显卡的功能…

Day11:信息打点-Web应用企业产权指纹识别域名资产网络空间威胁情报

目录 Web信息收集工具 业务资产-应用类型分类 Web单域名获取-接口查询 Web子域名获取-解析枚举 Web架构资产-平台指纹识别 思维导图 章节知识点&#xff1a; Web&#xff1a;语言/CMS/中间件/数据库/系统/WAF等 系统&#xff1a;操作系统/端口服务/网络环境/防火墙等 应用…

dart中的事件队列与微任务

dart在每个事件循环中&#xff0c;会先执行同步任务代码&#xff0c;然后分别检查两个任务队列&#xff1a;微任务队列和事件队列。dart总是先执行微任务队列中的代码&#xff0c;然后才是事件队列中的代码。当两个队列中的任务都执行完成后&#xff0c;线程进入休眠状态&#…