【算法】TopK问题超详解

TopK算法

TopK问题基本框架就是:

从n个数中,找出最大(或最小)的前k个数。

在我们生活中,经常会遇到TopK问题

比如大众点评的必吃榜;成绩单的前十名;各种数据的最值筛选;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NOc0UsTc-1721352065061)(https://i-blog.csdnimg.cn/direct/d54a704560c64ea0991b938683e70d1e.png)]

我们应该知道的是

TopK问题的最基本流程包括以下几个阶段:

  1. 首先对数据进行排序,从小到大或者从大到小
  2. 直接返回排序后的数组的前k个即可

这是最简单的排序思想,我们可以使用快速排序或者冒泡排序等来实现排序过程

一、冒泡排序

冒泡排序作为常见的排序方法,它在这的核心思想和算法步骤是这样的:

不断比较前后a[i]和a[i+1]两个元素,将较大的那一个往后放;直到冒泡完,最后的k个即为要求TopK元素

代码详解

//冒泡排序的解法
void TopK_BubSort(int* a, int n, int k)
{for (int i = 0; i < n - 1; i++){for (int j = 0; j < n - 1 - i; j++)//冒泡排序的核心代码{int tmp = 0;if (a[j] > a[j + 1])//进行冒泡排序{tmp = a[j];a[j] = a[j + 1];a[j + 1] = tmp;}}}for (int i = n - 1; i >= n - k; i--)//返回排序完之后的k个元素{printf("%d ", a[i]);}
}

时间复杂度

O(n^2)

二、快速排序

第二种排序方法我们可以使用快速排序

//快速排序的解法
void TopK_QuickSort(int* a, int n, int k)
{QuickSort(a, 0, n - 1, k);for (int i = n - 1; i >= n - k; i--){printf("%d ", a[i]);}
}void QuickSort(int* a, int left, int right, int k)
{if (left >= right)//当左边大于右边的时候,直接返回即可{return;}int div = PartSort(a, left, right);//进行快排if (div == k)//当div等于k的时候,直接返回{return;}else if (div < k)//当div小于k的时候,继续递归{QuickSort(a, div + 1, right, k);}else//当div大于k的时候,继续递归{QuickSort(a, left, div - 1, k);}
}int PartSort(int *a,int left,int right)
{//设置左右两个指针,分别指向数组的第一个元素和最后一个元素int begin = left;int end = right;int tmp = a[right];//我们选择数组最后一个元素作为基准值while (begin < end)//快排的核心代码(具体的算法思想自行搜索){while (begin < end && a[begin] >= tmp){begin++;}while (begin < end && a[end] <= tmp){end--;}if (begin < end){Swap(&a[begin], &a[end]);}}Swap(&a[begin], &a[right]);return begin;
}

时间复杂度

在最坏的情况下,即每次分区都选择了当前序列的最大或最小元素作为基准值,快速排序的时间复杂度为O(n^2)。但是在平均情况下,快速排序的时间复杂度为O(nlogn)

O(nlogn)

三、快速选择

快速选择算法是快速排序的变种,利用分治思想,通过不断划分数组,将枢轴(pivot)放在其正确位置,最终找到第k大的元素;同时要注意的是:快速选择需要较好的平均性能且对最坏情况性能要求不高。

int partition(int arr[], int low, int high) {int pivot = arr[high];int i = low - 1;for (int j = low; j < high; j++) {if (arr[j] >= pivot) {i++;int temp = arr[i];arr[i] = arr[j];arr[j] = temp;}}int temp = arr[i + 1];arr[i + 1] = arr[high];arr[high] = temp;return i + 1;
}int quickSelect(int arr[], int low, int high, int k) {if (low <= high) {int pi = partition(arr, low, high);if (pi == k - 1)return arr[pi];else if (pi > k - 1)return quickSelect(arr, low, pi - 1, k);elsereturn quickSelect(arr, pi + 1, high, k);}return -1;
}void topKQuickSelect(int arr[], int n, int k) {for (int i = 0; i < k; i++) {int result = quickSelect(arr, 0, n - 1, i + 1);printf("%d ", result);}printf("\n");
}

时间复杂度

期望时间复杂度是O(n),但最坏情况为O(n^2)。

O(n)

除了排序以外,实际上还有其他的方法来实现。

四、堆排序

无序地返回TopK

我们了解到TopK的核心思想是找出这k个数,但并非要我们对这k个数也进行排序;所以我们直接使用堆来找出k个数即可。在时间复杂度上会大大降低从而提高时间效率。

具体的算法步骤是这样的:

  1. 初始化

    首先建一个小堆,将数组的前k个元素放入堆中(注意:这里是前k个元素,而不是最终要求的k个元素,我们最终要返回这个堆,所以初始化也使用k个元素的空间)

  2. 比较

    将堆顶元素与数组中第k+1个元素以及以后的元素依次进行比较,假设这里我们要得到的是最大的k个元素,那么当数组中元素比堆顶元素大时,就进行交换;

  3. 返回

    最终当所有元素都比较完后,此时堆中的k个元素就是我们最终要求的那k个元素,返回即可。

代码详解

void TopK_Heap(int* a, int n, int k)
{HP hp;HeapInit(&hp);for (int i = 0; i < k; i++)//先将k个元素放入一个小顶堆中,方便后续用于比较元素的大小{HeapPush(&hp, a[i]);}for (int i = k; i < n; i++)//从第k+1个元素开始,与堆顶元素进行比较,完成TopK问题的主要流程{if (a[i] > HeapTop(&hp))//当前元素比堆顶元素大的时候,将堆顶元素替换为这个元素{HeapTop(&hp);//首先弹出堆顶元素HeapPush(&hp, a[i]);//再将当前元素放入堆中}}while (!HeapEmpty(&hp))//当循环到结束时,这时堆中的元素就是所求的k个元素,将它们先打印再弹出即可、{printf("%d ", HeapTop(&hp));//依次打印元素HeapPop(&hp);//弹出元素}//完成一切工作之后,不要忘了销毁这个堆HeapDestory(&hp);}

时间复杂度

这个算法的时间复杂度可以分为两个部分来分析。
首先,对于前面的循环,它将前k个元素依次插入堆中,插入一个元素的时间复杂度是O(logk),而循环执行k次,所以这部分的时间复杂度是O(klogk)
接下来,对于后面的循环,它从第k+1个元素开始,依次与堆顶元素进行比较。如果当前元素比堆顶元素大,就将堆顶元素替换为当前元素,并进行堆的调整。堆的调整操作的时间复杂度是O(logk)。这个循环执行了n-k次,所以这部分的时间复杂度是O((n-k)logk)
综合起来,这个算法的时间复杂度是O(klogk + (n-k)logk)。从数量级上来看,平均的时间复杂度就是:。

O(nlogk)

需要注意的是,这个算法的时间复杂度是基于堆的操作的时间复杂度,而堆的操作的时间复杂度是基于堆的大小的,即k。因此,这个算法的时间复杂度与k的大小有关。当k较小的时候,算法的时间复杂度较低;当k接近n时,算法的时间复杂度较高。

有序地返回TopK

事实上,如果要求我们有序地返回这k个数的话,我们只需多写一个Sort函数即可。

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[end], &a[0]);AdjustDown(a, end, 0);end--;}
}//TopK排序版
void TopK_Sort(int* a, int n, int k)
{HeapSort(a, n);//排序完直接返回前k个元素即可for (int i = n - 1; i >= n - k; i--){printf("%d ", a[i]);}printf("\n");
}

总结

读完这篇文章,相信你对TopK问题已经有了大致的了解并且基本知道其算法思想了。

TopK问题是我们生活中也会常常遇见的问题,所以说掌握它的常见算法绝对不是一件坏事。针对上方的几种算法:

  • 排序法适用于数据集较小且有排序需求的情况。
  • 快速选择法适用于期望时间复杂度较低,能容忍最坏情况的场景。
  • 堆排序法适用于数据集较大且k远小于n的情况。

这三种方法各有优缺点,我们可以根据具体需求选择合适的算法,从而在生活和工作中提高时间效率。

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

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

相关文章

【人工智能】Transformers之Pipeline(四):零样本音频分类(zero-shot-audio-classification)

​​​​​​​ 目录 一、引言 二、零样本音频分类&#xff08;zero-shot-audio-classification&#xff09; 2.1 概述 2.2 意义 2.3 应用场景 2.4 pipeline参数 2.4.1 pipeline对象实例化参数​​​​​​​ 2.4.2 pipeline对象使用参数 2.4 pipeline实战 2.5 模…

【MySQL】:对库和表的基本操作方法

数据库使用的介绍 什么是SQL 学习数据库的使用——>基于 SQL编程语言 来对数据库进行操作 重点表述的是“需求”&#xff0c;期望得到什么结果。&#xff08;至于结果是如何得到的&#xff0c;并不关键&#xff0c;都是数据库服务器在背后做好了&#xff09; 重点表述的是…

线程之间的通信

第一题 #include <stdio.h> #include <string.h> #include <stdlib.h> #include <stdarg.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <dirent.h> #include <…

蔡司小乐圆:护航青少年视力健康,专业应对近视挑战

在科技日新月异的今天&#xff0c;电子产品已深度融入青少年的日常&#xff0c;为生活带来便利的同时&#xff0c;也悄然间对他们的视力构成了威胁。近视&#xff0c;这一日益严峻的健康问题&#xff0c;正牵动着无数家庭的心弦。蔡司眼镜&#xff0c;作为眼镜行业的领军者&…

7月21日,贪心练习

大家好呀&#xff0c;今天带来一些贪心算法的应用解题、 一&#xff0c;柠檬水找零 . - 力扣&#xff08;LeetCode&#xff09; 解析&#xff1a; 本题的贪心体现在对于20美元的处理上&#xff0c;我们总是优先把功能较少的10元作为找零&#xff0c;这样可以让5元用处更大 …

代码随想录算法训练营第35天|LeetCode 01背包问题 二维、01背包问题 一维、416. 分割等和子集

1. LeetCode 01背包问题 二维 题目链接&#xff1a;https://kamacoder.com/problempage.php?pid1046 文章链接&#xff1a;https://programmercarl.com/背包理论基础01背包-1.html#算法公开课 视频链接&#xff1a;https://www.bilibili.com/video/BV1cg411g7Y6/ 思路&#xf…

面向对象三大特征及其优劣接口的特点、抽象类的特点

简单介绍下面向对象三大特征&#xff1f; 封装&#xff1a;封装指的就是把对象的属性隐藏在内部&#xff0c;不允许外部对象直接访问内部信息&#xff0c;但是可以提供一些被外界访问的方法来访问属性。优点在于数据隐藏&#xff08;通过定义私有属性&#xff0c;避免直接访问…

JavaScript银行卡实名核验接口集成 代码详解

银行卡实名核验接口是一种用于验证银行卡持有人身份的服务&#xff0c;通常应用于金融机构或第三方支付平台。这种接口允许应用程序或服务发送请求&#xff0c;以验证银行卡所有者的身份信息是否与银行记录相匹配。 应对市场发展需求&#xff0c;翔云提供了银行卡实名认证接口…

压缩pdf大小的方法 指定大小软件且清晰

在数字化时代&#xff0c;pdf文件因其良好的兼容性和稳定性&#xff0c;已成为文档分享的主流格式。然而&#xff0c;高版本的pdf文件往往体积较大&#xff0c;传输和存储都相对困难。本文将为您详细介绍几种简单有效的方法&#xff0c;帮助您减小pdf文件的大小&#xff0c;让您…

Gradle构建配置包:一键生成构建脚本的秘籍

标题&#xff1a;Gradle构建配置包&#xff1a;一键生成构建脚本的秘籍 在软件开发过程中&#xff0c;构建系统是项目自动化构建的核心。Gradle&#xff0c;作为一种流行的构建自动化工具&#xff0c;以其灵活性和强大的扩展性而广受开发者欢迎。Gradle构建配置包&#xff08;…

任务3 git基础知识(主要是pr的笔记)

任务要求 https://github.com/InternLM/Tutorial/blob/camp3/docs/L0/Git/task.md 文档 https://github.com/InternLM/Tutorial/blob/camp3/docs/L0/Git/readme.md 任务 任务1&#xff1a;提交PR https://github.com/InternLM/Tutorial/pull/1242 任务2&#xff1a;实践…

UML的六大关系---泛化、实现、关联、聚合、组合、依赖

文章目录 前言1. 泛化关系(Generalization)2. 实现关系(Realization)3. ‌关联关系(Association)4. 聚合关系(Aggregation)5. 组合关系(Composition)6. 依赖关系(Dependency)总结 前言 讲到设计模式&#xff0c;就会有 U M L UML UML类图这个东西。 一开始就很难理解各种线啥意…

Android RSA 加解密

文章目录 一、RSA简介二、RSA 原理介绍三、RSA 秘钥对生成1. 密钥对生成2. 获取公钥3. 获取私钥 四、PublicKey 和PrivateKey 的保存1. 获取公钥十六进制字符串1. 获取私钥十六进制字符串 五、PublicKey 和 PrivateKey 加载1. 加载公钥2. 加载私钥 六、 RSA加解密1. RSA 支持三…

selenium.common.exceptions.NoAlertPresentException: Message:

这个错误 selenium.common.exceptions.NoAlertPresentException 表示在尝试访问警告框时&#xff0c;当前页面上并没有活动的警告框。这通常发生在两种情况下&#xff1a; 警告框实际上并没有出现&#xff0c;或者在你尝试访问它之前已经被自动处理或关闭了。你的代码在警告框…

CTF-Web习题:2019强网杯 UPLOAD

题目链接&#xff1a;2019强网杯 UPLOAD 解题思路 打开靶场如下图所示&#xff0c;是一个注册和登录界面 那就注册登录一下&#xff0c;发现是一个提交头像的页面&#xff1a; 试了一下只有能正确显示的png图片才能提交成功&#xff0c;同时F12拿到cookie&#xff0c;base6…

树形背包问题

一些题目给定了树形结构&#xff0c;在这个树形结构中选取一定数量的点或边&#xff08;也可能是其他属性&#xff09;&#xff0c;使得某种与点权或者边权相关的花费最大或者最小。解决这类问题&#xff0c;一般要考虑使用树上背包。 树上背包&#xff0c;顾名思义&#xff0c…

Linux 基础开发工具 : Vim编辑器

Vim 是 Linux 和其他类 Unix 系统上广泛使用的文本编辑器之一。它基于更早的 vi 编辑器&#xff0c;但添加了许多增强功能和扩展。Vim 是“Vi IMproved”的缩写&#xff0c;意为“改进的 Vi”&#xff0c;我们常使用Vim编辑器编写c/c代码。 ps&#xff1a;该篇介绍均为最基础介…

驱动开发系列07 - 驱动程序如何分配内存

一:概述 Linux 内核提供了丰富的内存分配函数、在本文中,我们将介绍在设备驱动程序中分配和使用内存的方法,以及如何优化系统的内存资源。由于内核为驱动程序提供了统一的内存管理接口。所以我们不会去讨论不同架构是如何管理内存的,文本不涉及分段、分页等问题,此外在本文…

Blender中保存透明图片

在Blender中保存透明图片&#xff0c;主要是通过在渲染设置中调整背景透明度&#xff0c;并选择合适的文件格式来保存图像。以下是一个详细的步骤指南&#xff1a; 一、设置渲染属性 打开Blender并加载你想要渲染的模型。在右侧的属性编辑器中&#xff0c;找到并点击“渲染属…

解决Visual studio内报错信息:MSB8036:找不到 Windows SDK 版本问题

问题描述&#xff1a; 找不到WindowsSDK版本&#xff0c;请安装所需版本的Windows SDK&#xff0c;或者在项目属性页中通过右键单击解决方案并选择“重定解决方案目标”来更改SDK版本。 首先&#xff0c;如果你尝试了以下两种方法&#xff1a; &#xff08;1&#xff09;重新…