[数据结构——lesson10.2堆排序以及TopK问题]

目录

前言

学习目标

堆排序

TopK问题:

解法一:建立N个数的堆

解法二:建立K个数的堆(最优解)

完整代码

结束语


前言

上节内容我们详细讲解了堆[数据结构——lesson10.堆及堆的调整算法],接下来我们来讲解堆的一个经典应用——TopK问题

学习目标

  • 堆排序
  • 掌握堆的应用理解TopK问题

两种调整算法的复杂度精准剖析🌟🌟🌟

开头讲了两种堆的调整算法,分别是【向上调整】和【向下调整】,在接口算法实现Push和Pop的时候又用到了它们,以及在建堆这一块我也对它们分别做了一个分析,所以我们本文的核心就是围绕这两个调整算法来的,但是它们两个到底谁更加优一些呢❓
这里就不做过多解释,直接看图即可


1、向下调整算法【重点掌握】
 

2、向上调整算法

好,我们来总结一下,【向上调整算法】,它的时间复杂度为O(NlogN);【向下调整算法】,它的时间复杂度为O(N)
很明显,【向下调整算法】来得更优一些,因为向下调整随着堆的层数增加结点数也会变多,可是结点越多调整得就越少,因为在一些大型数据处理场合我们会使用向下调整
当然在下面要讲的堆排序中我们建堆也是利用的向下调整算法,所以大家重点掌握一个就行
 

堆排序

讲了那么久的堆,学习了两种调整算法以及它们的时间复杂度分析,接下去我们来说说一种基于堆的排序算法——【堆排序】

堆排序即利用堆的思想来进行排序,总共分为两个步骤:
1. 建堆
  • 升序:建大堆
  • 降序:建小堆
这里我们以升序为例(升序建大堆 or 小堆❓)
  • 在上面解说的时候,我建立的默认都是大堆,但是在这里我们要考虑排序问题了,现在面临的是【升序】,对于升序就是数组前面的元素小,后面的元素大,这个堆也是基于数组建立的,那就是要堆顶小,堆顶大,很明显就是建【小堆】
  • 一波分析猛如虎🐅,我们通过画图来分析是否可以建【小堆】

  • 可以看到,对于建小堆来说,原本的左孩子结点就会变成新的根结点,而右孩子结点就会变成新的左孩子结点,整个堆会乱,而且效率并不是很高,因此我们应该反一下,去建大堆
//建立大根堆(倒数第一个非叶子结点)
for (int i = ((n - 1) - 1) / 2 ; i >= 0; --i)
{Adjust_Down(a, n, i);
}

所以应建大堆

 

堆排序的基本思路是每次将堆顶元素取出放到有序区间。大堆(大顶堆)中每个节点的值都大于或等于其子节点的值,堆顶元素为最大值。升序排序时建大堆,可将堆顶的最大值与无序区间最后一个数交换,使有序区间增加一个最大值。然后对剩余元素重新调整堆结构,重复此过程,就能逐渐将序列变为升序。

 

若升序建小堆,虽然堆顶是最小值,但确定次小值时会很麻烦,因为次小值可能是堆顶的左孩子或右孩子,甚至需要重新建堆,导致排序效率降低。

如何进一步实现排序❓
  • 有了一个大堆之后,如何去进一步实现升序呢,这里就要使用到在Pop堆顶数据的思路了,也就是现将堆顶数据与堆底末梢数据做一个交换,然后对这个堆顶数据进行一个向下调整,将大的数往上调。具体过程如下
2. 利用堆删除思想来进行排序
建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序。

  • 对照代码,好好分析一下堆排的全过程吧
/*堆排序*/
void HeapSort(int* a, int n)
{//建立大根堆(倒数第一个非叶子结点)for (int i = ((n - 1) - 1) / 2 ; i >= 0; --i){Adjust_Down(a, n, i);}int end = n - 1;while (end > 0){swap(&a[0], &a[end]);		//首先交换堆顶结点和堆底末梢结点Adjust_Down(a, end, 0);		//一一向前调整end--;}
}
  • 看一下时间复杂度,建堆这一块是O(N),调整这一块的话就是每次够把当前堆中最的数放到堆底来,然后每一个次大的数都需要向下调整O(log2N),数组中有N个数需要调整做排序,因而就是O(Nlog2N)
  • 当然你可以这么去看:第一次放最大的数,第二次是次大的数,这其实和我们上面讲过的向上调整差不多了,【结点越少,调整越少;结点越多,调整越多】,因此它也可以使用之前我们分析过的使用的【错位相减法】去进行求解,算出来也是一个O(Nlog2N)
  • 最后将两段代码整合一下,就是O(N + Nlog2N),取影响结果大的那一个就是O(Nlog2N),这也就是堆排序最终的时间复杂度


TopK问题:

Top-K 问题是一类常见的算法和数据处理问题,指从包含 N 个元素的大量数据集合中找到前 K 个最大或最小的元素,通常 N 远大于 K。

Top-k问题在生活中是非常的常见,比如游戏中某个大区某个英雄熟练度最高的前10个玩家的排名,我们就要根据每个玩家对该英雄的熟练度进行排序,可能有200万个玩家,但我只想选出前10个,要对所有人去排个序吗?显然没这个必要。

再比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
 

  • 问题特点:数据量 N 极大时,若直接对所有数据排序(如快排,时间复杂度为 O (n log n)),不仅耗时久,还可能需将所有数据加载到内存,空间成本极高。而 K 通常很小,只需关注 “最大或最小的前 K 个”,无需对所有数据排序,因此需要更高效的算法来解决。

解法一:建立N个数的堆

建一个 N 个数的堆(C++中可用优先级队列priority_queue),不断的选数,选出前 k 个。

时间复杂度:建N个数的堆为O(N),获取堆顶元素 (也即是最值) 并删除掉堆顶元素为O(log2N),上述操作重复 k 次,所以时间复杂度为O(N+k*log2N)

【思考】

但是这样也会存在上述所讲的可能需将所有数据加载到内存,空间成本极高的问题,能否再优化一下呢?


解法二:建立K个数的堆(最优解)

解决思路堆排序

  • 若要找前 K 个最大的元素,则建立小顶堆
  • 若要找前 K 个最小的元素,则建立大顶堆
  • 首先用数据集合中前 K 个元素来建堆,然后将剩余的 N-K 个元素依次与堆顶元素比较;
  • 大于(针对小顶堆)小于(针对大顶堆)堆顶元素,则替换堆顶元素并重新调整堆;
  • 遍历完剩余元素后,堆中的 K 个元素就是所求的前 K 个最大或最小的元素。

时间复杂度:

▶ 建 k 个元素的堆为O(K);
▶ 遍历剩余的 N-K 个元素的时间代价为O(N-K),假设运气很差,每次遍历都入堆调整;
▶ 入堆调整:删除堆顶元素和插入元素都为O(log2K);
▶ 所以时间复杂度为O(k + (N-K)log2K)。当 N 远大于 K 时,为O(N*log2K),这种解法更优。

 

假如要找出最大的前 10 个数:

建立 10 个元素的小堆,数据集合中前 10 个元素依次放入小堆,此时的堆顶元素是堆中最小的元素,也是堆里面第 10 个最小的元素,
▶  然后把数据集合中剩下的元素与堆顶比较,若大于堆顶则去掉堆顶,再将其插入,
▶  这样一来,堆里面存放的就是数据集合中的前 10 个最大元素,
此时小堆的堆顶元素也就是堆中的第 10 个最大的元素

 

思考:为什么找出最大的前10个数,不能建大堆呢?

  • 找出最大的前 10 个数不能建大堆,原因在于大堆的特性会导致只能找到最大的数,而无法找到其余较大的数。
  • 大堆的性质是堆顶元素为堆中最大的元素。当使用 10 个元素建大堆时,堆顶就是这 10 个元素中最大的,若数据集合中还有其他更大的数,由于它们都小于当前堆顶元素,根据大堆的插入规则,这些数无法进入堆中。所以最终只能得到最大的那个数,无法找出前 10 个最大的数。
  • 相反,若建立小堆,堆顶是堆中最小的元素,当有比堆顶大的元素出现时,就可以替换堆顶元素,并通过调整堆结构使小堆性质得以维持,这样就能保证较大的数逐渐进入堆中,最终堆中的 10 个元素就是数据集合中前 10 个最大的数。

完整代码

以从1w个数里找出最大的前10个数为例:

#define _CRT_SECURE_NO_WARNINGS
#include<stdlib.h>
#include<stdio.h>
#include<time.h>// 大堆调整
void max_heapify(int* arr, int i, int size)
{int largest = i;int left = 2 * i + 1;int right = 2 * i + 2;if (left < size && arr[left] > arr[largest])largest = left;if (right < size && arr[right] > arr[largest])largest = right;if (largest != i){int temp = arr[i];arr[i] = arr[largest];arr[largest] = temp;max_heapify(arr, largest, size);}
}// 构建大根堆
void build_max_heap(int* arr, int size)
{for (int i = size / 2 - 1; i >= 0; i--)max_heapify(arr, i, size);
}// 堆排序
void heap_sort(int* arr, int size)
{build_max_heap(arr, size);for (int i = size - 1; i > 0; i--) {int temp = arr[0];arr[0] = arr[i];arr[i] = temp;max_heapify(arr, 0, i);}
}// 获取前k个最小元素
void get_topk_smallest(int* arr, int n, int k, int* result)
{if (k > n) k = n;int* heap = (int*)malloc(k * sizeof(int));if (heap == NULL) {printf("内存分配失败\n");return;}// 取前k个元素构建大堆for (int i = 0; i < k; i++)heap[i] = arr[i];build_max_heap(heap, k);// 遍历剩余元素for (int i = k; i < n; i++){if (arr[i] < heap[0]){heap[0] = arr[i];max_heapify(heap, 0, k);}}// 排序结果并输出heap_sort(heap, k);for (int i = 0; i < k; i++)result[i] = heap[i];free(heap);
}// 生成随机数组
void generate_random_array(int* arr, int size, int min, int max)
{srand(time(NULL));for (int i = 0; i < size; i++){arr[i] = min + rand() % (max - min + 1);}
}// 打印数组
void print_array(int* arr, int size)
{for (int i = 0; i < size; i++){printf("%d ", arr[i]);if ((i + 1) % 10 == 0)printf("\n");}printf("\n");
}// 验证结果正确性(通过全排序对比)
void verify_result(int* arr, int n, int k, int* topk)
{// 创建数组副本并排序int* copy = (int*)malloc(n * sizeof(int));for (int i = 0; i < n; i++)copy[i] = arr[i];heap_sort(copy, n);  // 注意:这里堆排序是升序printf("\n验证结果(前10个最小元素):\n");printf("算法结果:");for (int i = 0; i < k; i++)printf("%d ", topk[i]);printf("\n正确结果:");for (int i = 0; i < k; i++)printf("%d ", copy[i]);printf("\n");// 检查是否一致int correct = 1;for (int i = 0; i < k; i++){if (topk[i] != copy[i]){correct = 0;break;}}printf("验证结果:%s\n", correct ? "正确" : "错误");free(copy);
}int main()
{const int N = 10000;  // 数据总量const int K = 10;     // 要找的最小元素个数int* arr = (int*)malloc(N * sizeof(int));int* topk = (int*)malloc(K * sizeof(int));// 生成10000个1到100000之间的随机数generate_random_array(arr, N, 1, 100000);printf("已生成10000个随机数\n");// 计算前10个最小元素clock_t start = clock();get_topk_smallest(arr, N, K, topk);clock_t end = clock();// 输出结果printf("\n最小的前10个数(升序排列):\n");print_array(topk, K);// 输出耗时double time_spent = (double)(end - start) / CLOCKS_PER_SEC;printf("计算耗时:%.6f秒\n", time_spent);// 验证结果verify_result(arr, N, K, topk);// 释放内存free(arr);free(topk);return 0;
}

运行结果:

结束语

经过上节堆的学习,这一节我们对于堆的Top K问题的学习与理解相对会轻松很多。

感谢您的三连支持!!!

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

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

相关文章

使用HTTPS 服务在浏览器端使用摄像头的方式解析

1.方式1 // vite.config.js import { defineConfig } from vite import vue from vitejs/plugin-vue import basicSsl from vitejs/plugin-basic-sslexport default defineConfig({plugins: [vue(),basicSsl({name: test,domains: [192.168.15.166, localhost], // 添加您的IPc…

上下文管理器和异步I/O

目录 一、上下文管理器 1.1 定义 1.2 特点 1.3 适用场景 1.4 具体实现 1.5 具体实例 1.5.1 文件管理器 1.5.2 线程锁释放资源 二、异步I/O 2.1 定义 2.2 特点 2.3 实现方式 2.4 适用场景 高并发网络服务&#xff1a;Web服务器、API服务等需要处理大量并发连接 2…

LabVIEW信号监测与分析

借助 LabVIEW 平台&#xff0c;生成含正弦波与噪声的信号&#xff0c;经频谱分析等处理&#xff0c;结合动态限值判断信号是否超限&#xff0c;广泛用于音频、振动等领域的信号监测&#xff0c;助力高效开展信号分析与质量把控。概念说明系统围绕信号的生成、处理、分析及监测展…

MySQL数据库与表的创建、修改及数据操作指南

精选专栏链接 &#x1f517; MySQL技术笔记专栏Redis技术笔记专栏大模型搭建专栏Python学习笔记专栏深度学习算法专栏 欢迎订阅&#xff0c;点赞&#xff0b;关注&#xff0c;每日精进1%&#xff0c;与百万开发者共攀技术珠峰 更多内容持续更新中&#xff01;希望能给大家带来…

​new species of flying reptile1 discovered in Scotland​

Pterosaur: new species of flying reptile1 discovered in Scotland 苏格兰斯凯岛发现新翼龙物种 考古学家们在苏格兰斯凯岛发现了一个新的翼龙物种。这种独特的飞行爬行动物生活在1.68 – 1.66亿年前。 This flying reptile soared over the heads of dinosaurs2 when Scotla…

03 节点行为

审批流程图如下图&#xff0c;在此流程图中&#xff0c;存在两个UserTask节点&#xff0c;第一个节点是主管审批&#xff0c;第二个节点是产品经理审批&#xff0c;两个节点中间有一个排他网关&#xff0c;此网关用来对主管审批的结果进行判断&#xff0c;如果主管审批通过&…

深度卷积生成对抗网络详解与实现

深度卷积生成对抗网络详解与实现 0. 前言 1. 网络架构 1.1 批归一化 1.2 激活 1.3 上采样 2. 构建 DCGAN 2.1 生成器 2.2 判别器 2.3 训练 DCGAN 0. 前言 深度卷积生成对抗网络 (Deep Convolutional Generative Adversarial Network, DCGAN) 是基于生成对抗网络 (Generative A…

CF607B Zuma -提高+/省选-

CF607B Zuma codeforces 原链接 题目描述 Genos\texttt{Genos}Genos 最近在他的手机上下载了祖玛游戏。在祖玛游戏里&#xff0c;存在 nnn 个一行的宝石&#xff0c;第 iii 个宝石的颜色是 CiC_iCi​。这个游戏的目标是尽快的消灭一行中所有的宝石。 在一秒钟&#xff0c;Ge…

拆分了解HashMap的数据结构

文章目录 前言 一、底层数据结构总览 二、核心组成部分详解 1. 数组&#xff08;哈希表&#xff09; 2. 节点&#xff08;Node&#xff09; 3. 红黑树&#xff08;TreeNode&#xff09; 三、哈希函数与索引计算 四、哈希冲突的解决 五、扩容机制 六、关键特性与注意事…

关于电脑连接不到5g的WiFi时的一些解决办法

方法一、设备管理器重卸载驱动后&#xff0c;重装驱动。方法二、打开控制面板 “控制面板\网络和 Internet\网络连接” &#xff08;亲测有效&#xff09;点击更改适配器配置右击当前的WLAN属性点击配置选择“高级” 802.11a/b/g 无线模式选项栏 值&#xff1a;6.的双…

Mathtype公式批量编号一键设置公式居中编号右对齐

插件[ygtools] 批量编号一键设置公式居中编号右对齐 单栏/多栏均可https://wwon.lanzout.com/i0NRf35vyw8j 下载密码8543

基于ssm的小橘子出行客户体验评价系统[SSM]-计算机毕业设计源码+LW文档

摘要&#xff1a;随着出行行业的快速发展&#xff0c;客户体验评价对于出行服务质量的提升至关重要。本文设计并实现了基于SSM&#xff08;Spring Spring MVC MyBatis&#xff09;框架的小橘子出行客户体验评价系统。该系统涵盖系统用户管理、司机信息管理、客户评价管理等功…

算法日记---二分查找

目录 前言 一、二分查找 1.思想 2.简单二分 3.优点 4.局限性 二、模板 1.基本模板 2.简单例题&#xff08;LeetCode&#xff09; 4.有重复元素的二分 5.0-1问题 总结 前言 本文通过讲解简单的二分查找配合leetcode例题对二分查找本质、模板进行了基础的总结 提示&a…

Level Set(水平集)算法——形象化讲解

目录 维度一&#xff1a;核心思想与比喻&#xff08;它像什么&#xff1f;&#xff09; 维度二&#xff1a;要解决什么问题&#xff1f;&#xff08;它能干嘛&#xff1f;有什么用&#xff1f;&#xff09; 维度三&#xff1a;工作原理&#xff08;它是怎么做到的&#xff1…

DDoS 攻防“军备竞赛”的幕后

谈到 DDoS&#xff08;分布式拒绝服务攻击&#xff09;&#xff0c;很多人会想到“黑客租用肉鸡发流量&#xff0c;网站直接崩”。但事实上&#xff0c;如今的 DDoS 攻防早已变成一场 军备竞赛。攻击者的武器越来越“工业化”&#xff1a;僵尸网络商品化&#xff1a;黑市上&…

如何用 Rust 重写 SQLite 数据库(二):是否有市场空间?

用 Rust 实现一个类似 SQLite 的嵌入式数据库非常有意义&#xff0c;但需要结合具体目标和场景来评估其价值。以下从技术、生态、市场需求和个人成长等多个维度展开分析&#xff0c;并给出结论。一、技术价值&#xff1a;Rust 与数据库的天然契合 SQLite 作为全球装机量最大的数…

【Web】ImaginaryCTF 2025 wp

目录 imaginary-notes certificate codenames-1 passwordless pearl imaginary-notes I made a new note taking app using Supabase! Its so secure, I put my flag as the password to the "admin" account. I even put my anonymous key somewhere in the si…

oracel如何找到外键子表

要找到导致外键约束冲突的子表&#xff08;即包含"child record"的表&#xff09;&#xff0c;可以通过以下SQL查询在Oracle数据库中定位&#xff1a;1. 查询约束基本信息&#xff08;确定父表和子表&#xff09;SELECT owner, constraint_name, table_name AS child…

智源研究院新研究:突破物理世界智能边界的RoboBrain 2.0,将重构具身AI能力天花板

当你对着家用机器人说"把杯子放在笔筒和键盘之间&#xff0c;对齐杯身logo"时&#xff0c;它能精准理解空间关系并执行动作&#xff1b;当多台机器人在超市协作补货时&#xff0c;它们能自主规划轨迹、避免冲突并完成长周期任务——这些曾经出现在科幻电影中的场景&a…

【2025】Office核心组件Microsoft word,Excel,PowerPoint详细使用指南

Office 核心组件使用指南 Microsoft Word 文字处理 Word主要用于创建和编辑文档&#xff0c;如信件、报告、论文等。 2025Office&#x1f517; 1. 界面认识 快速访问工具栏&#xff1a;位于左上角&#xff0c;可自定义保存、撤销、恢复等常用命令。功能区&#xff1a;顶部…