数据结构与算法:堆排序和TOP-K问题

朋友们大家好,本节内容来到堆的应用:堆排序和topk问题

堆排序

  • 1.堆排序的实现
    • 1.1排序
  • 2.TOP-K问题
  • 3.向上调整建堆与向下调整建堆
    • 3.1对比两种方法的时间复杂度

我们在c语言中已经见到过几种排序,冒泡排序,快速排序(qsort)

冒泡排序的时间复杂度为O(N2),空间复杂度为O(1);qsort排序的时间复杂度为
O(nlogn),空间复杂度为O(logn),而今天所讲到的堆排序在时间与空间复杂度上相比于前两种均有优势

堆排序可以在原数组上进行,其空间复杂度为O(1);
堆排序提供了稳定的 (O(nlogn)) 时间复杂度

接下来我们进行讲解

首先我们来看这组代码:

int main()
{int a[] = { 6,3,5,7,11,4,9,13,1,8,15 };Heap hp;HeapInit(&hp);for (int i = 0; i < sizeof(a) / sizeof(int); i++){HeapPush(&hp, a[i]);}while (!HeapEmpty(&hp)){printf("%d ", HeapTop(&hp));HeapPop(&hp);}printf("\n");return 0;}

上节课我们知道,hp这个堆里面,a[i]并不一定是有序的

在这里插入图片描述
这里我们每次打印首元素,即最小元素,再删除掉,下一次获得到的堆顶元素仍为最小的,所以打印出来结果为有序的。但这个并不是堆排序,他只是每次获取堆顶最小元素

堆排序是直接在数组上实现的

1.堆排序的实现

堆排序的实现可以分为两部分:构建最大堆(或最小堆)和执行排序过程

首先我们来看建堆过程

在上述代码中,我们是通过HeapPush(&hp, a[i]);来实现堆的插入,推其本质,是每次插入元素后进行向上调整,我们构建一个堆排序函数,其参数为传入的数组,和数组的元素个数:

void HeapSort(HPDataType* a, int n);

首先建堆,这里我们用向上调整建堆,在文章末尾会给大家引入向下调整建堆

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

从第二个元素开始,每次向上调整,完成堆的构建
在这里插入图片描述

建好之后我们则需要排序

1.1排序

思考一下,如果我们想要进行升序排序,需要建立大堆还是小堆呢?

在上述示例中,如果我们想进行升序,该怎么操作???

这里,如果我们想要升序排序,则需要建立大堆

小堆如果我们想要升序,堆顶元素在对应位置,剩余元素重新建立小堆,则时间复杂度大大增加

上述示例中,我们建了一个小堆,可以将Ajustup父节点与子节点大小关系改变来建立为大堆:

在这里插入图片描述
那思考一下,建立了大堆,我们如何实现升序呢?

这里我们就需要与删除堆顶元素相同的思路

  1. 排序过程
    在大堆构建完成后,数组的根节点(即数组的第一个元素)是当前堆中的最大元素。通过将它与堆的最后一个元素交换,然后减少堆的大小(实际上是忽略数组的末尾元素),可以确保最大元素位于数组的正确位置上。

  2. 调整堆
    交换根节点和最后一个节点之后,新的根节点可能破坏了大堆的性质,因此需要进行调整。调整的方法是将新的根节点“下沉”,直到恢复大堆的性质。

  3. 重复过程
    重复对堆顶元素进行移除并调整堆的过程,直到堆的大小减少到1。在每一次重复过程中,都会将当前的最大元素放置到它在数组中的最终位置上。

所以我们代码实现就两步:

  • 交换首尾元素
  • 向下调整
void HeapSort(HPDataType* a, int n)
{//建堆for (int i = 1; i < n; i++){Ajustup(a, i);}while (n>1){Swap(&a[0], &a[n - 1]);n--;Ajustdown(a, n, 0);}
}

我们进行代码测试
在这里插入图片描述

所以,堆这里可以促进我们快速选数,它的本质是选择排序

2.TOP-K问题

TOP-K问题指的是从一个大规模的数据集中找出“最重要”或“最优”的K个元素的问题,对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决

思路如下:

  1. 用数据集合中前K个元素来建堆
    • 前k个最大的元素,则建小堆
    • 前k个最小的元素,则建大堆
  2. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素

基于已经提供的堆操作函数,我们可以实现一个特定的PrintTopK函数,其目的是从数组a中找到最大的k个元素。

实现这一目标的思路是:

  • 首先,使用数组a中的前k个元素建立一个最小堆。
  • 然后,遍历剩余的n-k个元素。对于每个元素,如果它大于堆顶元素,则用它替换堆顶元素,然后对堆顶元素进行向下调整以维护最小堆的性质。
  • 遍历完成后,堆中的k个元素即为整个数组中最大的k个元素。
void PrintTopK(int* a, int n, int k)
{Heap php;HeapInit(&php);for (int i = 0; i < k; ++i) {HeapPush(&php, a[i]);}for (int i = k; i < n; ++i) {if (a[i] > HeapTop(&php)) { // 如果当前元素比堆顶大HeapPop(&php); // 移除堆顶HeapPush(&php, a[i]); // 将当前元素加入堆中}}// 打印堆中的元素,即TOP K元素for (int i = 0; i < k; ++i) {printf("%d ", php.a[i]);}printf("\n");HeapDestroy(&php);
}
  1. 用a中前k个元素建立堆
  2. 将剩余n-k个元素与堆顶比较,替换并调整

测试代码:
在这里插入图片描述

3.向上调整建堆与向下调整建堆

对于数组a,进行向上调整建堆:

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

要通过向下调整的方式建立堆,我们通常是从最后一个非叶子节点开始,逐层向上进行调整,这能保证每个子树都满足堆的性质

for (int i = n/2 - 1; i >= 0; i--) {AdjustDown(a, n, i);}

3.1对比两种方法的时间复杂度

向下调整建堆

这个方法从最后一个非叶子节点开始,逆序对数组中的元素执行向下调整的操作。每个节点需要执行的向下调整操作取决于其高度,而数组中大约一半的节点是叶子节点,它们不需要被向下调整。对于剩下的节点,只有很少的节点需要移动到树的较低层次。具体地说,树的每一层上的节点数量减半,而向下移动的最大深度从0开始线性增加。

for (int i = n/2 - 1; i >= 0; i--) {AdjustDown(a, n, i);
}

在这里插入图片描述
设向下调整的累计次数为T(h).

  • 倒数第二层调整次数:2h-2*1
  • 倒数第三层调整次数:2h-3*2
  • ……
  • 第一层调整次数:20*(h-1);

对其进行累加和:

为等差×等比求和,通过错位相减则可求出结果:

T(h)=2^h-1-h;
h=log (n+1);
T(n)=n-log(n+1)

导致最大影响的项为n
所以向下调整的时间复杂度为O(N)

向上调整建堆

从第二层开始向上调整:

  • 第二层调整次数:21*1
  • 第三层调整次数:22*2;
  • 倒数第二层:2h-2*(h-2);
  • 倒数第一层:2h-1*(h-1);

向上调整建堆

对于一个节点来说,向上调整可能需要比较和移动直到它的根节点,这在最坏的情况下是树的高度,对于一个完全二叉树来说,树的高度是 O ( log ⁡ n ) O(\log n) O(logn)。对于代码段:

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

这个方法从第二个元素开始,逐一对数组中的元素执行向上调整的操作。对于数组中的第i个元素,最坏情况下向上调整操作需要沿着一条从叶节点到根节点的路径移动,路径的长度大约等于树的高度 h h h,即 O ( log ⁡ i ) O(\log i) O(logi)。因此,对于所有元素的总时间复杂度为:

T ( n ) = ∑ i = 1 n O ( log ⁡ i ) = O ( log ⁡ n ! ) = O ( n log ⁡ n ) T(n) = \sum_{i=1}^{n} O(\log i) = O(\log n!) = O(n \log n) T(n)=i=1nO(logi)=O(logn!)=O(nlogn)

使用斯特灵公式( n ! ≈ 2 π n ( n e ) n n! \approx \sqrt{2\pi n}(\frac{n}{e})^n n!2πn (en)n),可以推导出 O ( log ⁡ n ! ) O(\log n!) O(logn!) 的大致等于 O ( n log ⁡ n ) O(n \log n) O(nlogn),所以向上调整建堆的时间复杂度大约为 O ( n log ⁡ n ) O(n \log n) O(nlogn)

向上调整建堆的时间复杂度是 O ( n log ⁡ n ) O(n \log n) O(nlogn),而向下调整建堆的时间复杂度是 O ( n ) O(n) O(n)。因此,对于从零开始构建堆的场景,通常更倾向于使用向下调整的方法,因为它更加高效。

本节内容到此结束!感谢大家支持!

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

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

相关文章

微信小程序云开发教程——墨刀原型工具入门(安装以及基础使用教程)

引言 作为一个小白&#xff0c;小北要怎么在短时间内快速学会微信小程序原型设计&#xff1f; “时间紧&#xff0c;任务重”&#xff0c;这意味着学习时必须把握微信小程序原型设计中的重点、难点&#xff0c;而非面面俱到。 要在短时间内理解、掌握一个工具的使用&#xf…

稀碎从零算法笔记Day4-LeetCode:交替合并字符串

前言&#xff1a;今天妹有深夜档&#xff0c;因为8点有个飞机 题型&#xff1a;字符串、双指针&#xff08;笔者没用这个思路&#xff09; 链接&#xff1a;1768. 交替合并字符串 - 力扣&#xff08;LeetCode&#xff09; 来源&#xff1a;LeetCode 著作权归作者所有。商业转…

JasperStudio中TextField文本框组件渲染之后,出现行间距不一致的问题

目录 1.1、问题描述 1.2、解决方案 1.1、问题描述 最近在处理线上遇到的一个问题,是有关JasperReports报表相关的问题,问题背景大概是这样的:我们的项目中使用了JasperReports来渲染报表,其中使用到了Text Field文本框组件,但是问题是渲染出来的数据直接会出现一些间距…

洛谷:P3068 [USACO13JAN] Party Invitations S(枚举、前缀和)

这题我们数据范围太大&#xff0c;用二维肯定是不行的&#xff0c;我们可以采用一维线性存储。 如题意&#xff0c;我们可以将每组奶牛编号都存在一维数组里面&#xff0c;只需记录每组的头尾指针就可以了。 如题中样例我们就可以存储成1 3 3 4 1 2 3 4 5 6 7 4 3 2 1 然后第…

[LeetBook]【学习日记】寻找和为指定数字的连续数字

题目 文件组合 待传输文件被切分成多个部分&#xff0c;按照原排列顺序&#xff0c;每部分文件编号均为一个 正整数&#xff08;至少含有两个文件&#xff09;。传输要求为&#xff1a;连续文件编号总和为接收方指定数字 target 的所有文件。请返回所有符合该要求的文件传输组…

【kubernetes】关于k8s集群的存储卷

目录 一、存储卷的分类 二、empty存储卷以及特点 三、hostpath存储卷以及特点 四、nfs存储卷以及特点 五、pvc存储卷 查看pv的定义 查看pvc的定义 实操&#xff1a;静态创建pv的方式 实现pvc存储卷 步骤一&#xff1a;先完成nfs的目录共享&#xff0c;需要准备不同的目…

C# 中 TryParse 将字符串转换为特定类型的方法

在 C# 中&#xff0c;TryParse 是一个用于将字符串转换为特定类型的方法。它用于尝试解析字符串并将其转换为指定类型的值&#xff0c;而不会引发异常。如果解析成功&#xff0c;它将返回 true 并将解析结果存储在输出参数中&#xff1b;如果解析失败&#xff0c;它将返回 fals…

redis10 应用问题(穿透、击穿、雪崩、分布式锁)

思维草图 缓存穿透 查询不存在的数据&#xff0c;穿透redis缓存&#xff0c;请求直接攻击后端db。 问题 当系统中引入redis缓存后&#xff0c;一个请求进来后&#xff0c;会先从redis缓存中查询&#xff0c;缓存有就直接返回&#xff08;相当于一道隔离闸&#xff0c;保护db…

vue设计原理-带你重走vue诞生路程

我们首先看下面这个小demo demo源码: <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><meta name"viewport" conten…

JAVA如何利用接口实现多继承问题

hello&#xff0c;上文带大家学习了java中类的继承&#xff0c;我们可以创建一个父类&#xff0c;将类中的共性抽取出来&#xff0c;通过子类继承的方式来实现代码的复用。今天带大家学习不同类之间的另外几种关系&#xff0c;即多态抽象类和接口。 多态的概念 多态&#xff0c…

Vue 3的Composition API和vue2的不同之处

Vue 3的Composition API是Vue.js框架的一个重要更新&#xff0c;它提供了一种新的组件逻辑组织和复用方式。在Vue 2中&#xff0c;我们通常使用Options API&#xff08;data、methods、computed等&#xff09;来组织组件的逻辑&#xff0c;但这种组织方式在处理复杂组件时可能会…

实体店好做还是电商平台好做?哪个发展前景较高?

我是电商珠珠 大部分人面对新的一年都会比较迷茫&#xff0c;想要创业会自己增加收入&#xff0c;却在开店和线上做电商这两者之间犹豫不决。不知道哪个更加合适自己&#xff0c;换句话来说&#xff0c;就是不知道哪个赔付率低一点。 现在这个行情&#xff0c;按照网友的说法…

C++ string类详解及模拟实现

目录 【本节目标】 1. 为什么学习string类&#xff1f; 1.1 C语言中的字符串 1.2 面试题(暂不做讲解) 2. 标准库中的string类 2.1 string类(了解) 2.2 string类的常用接口说明&#xff08;注意下面我只讲解最常用的接口&#xff09; 3. string类的模拟实现 3.1string类常用…

Express学习(一)

Express Express简介 什么是Express 官方给出的概念&#xff1a;Express是基于Node.js平台&#xff0c;快速、开放、极简的web开发框架。 通俗的理解&#xff1a;Express的作用和Node.js内置的http模块类似&#xff0c;是专门用来创建Web服务器的。进一步理解Express 不使用E…

【软件使用】Markdown编辑器第一次使用介绍

【软件使用】Markdown编辑器第一次使用介绍 markdown格式支持的软件有&#xff1a;VS Code 和 Typora&#xff0c;CSDN写网页博文也是用的.md&#xff0c;CSDN能支持导入的文件也是以.md格式结尾的文件名。 欢迎使用Markdown编辑器 你好&#xff01; 这是你第一次使用 Markd…

C语言——结构体(位段)、联合体、枚举

hello&#xff0c;大家好&#xff01;我是柚子&#xff0c;今天给大家分享的内容是C语言中的自定义类型结构体、联合体以及枚举&#xff0c;有什么疑问或建议可以在评论区留言&#xff0c;会顺评论区回访哦~ 一、结构体 struct a.结构体声明 不同于数组的是&#xff0c;结构…

【CSP试题回顾】202212-2-训练计划

CSP-202212-2-训练计划 解题思路 输入和初始化&#xff1a; 首先&#xff0c;代码从输入中获取项目的截止日期和项目数量。然后&#xff0c;它初始化一个项目列表&#xff0c;每个项目都有其依赖项、被依赖的项目集合、完成时间、总完成时间&#xff08;包括依赖链&#xff09…

【JavaSE】面向对象——多态性

多态性 多态性的概念 所谓多态性&#xff0c;理解为一个事物的多种形态。具体点就是去完成某个动作时&#xff0c;不同的对象会产生不同的状态。 多态性的好处 多态在Java中指的是父类的引用指向子类的对象&#xff0c;或者可以说是子类的对象赋给父类的引用。这样在我们的…

SpringBoot实现分页模糊查询

1. Navicat查询数据 Navicat中查询所有数据 SELECT * FROM sys_user;Navicat中查询前两条数据&#xff08;俩种方式&#xff09; SELECT * FROM sys_user LIMIT 2; //从0开始&#xff0c;第一个参数是起始位置即(pageNum-1)*pageSize&#xff0c;第二个参数是步长 SELECT * …

项目部署后 通过公网IP访问不到的问题解决

目录 1.检查项目是否在运行(第二行命令) 2.检查所用服务器防火墙是否打开 3.检查linux系统防火墙有没有打开 问题如图: 首先确保项目已经成功部署 1.检查项目是否在运行(第二行命令) 第一行命令是监听58080端口,我的项目是使用该端口 2.检查所用云服务器防火墙是否打开 我…