从排序算法到TopK问题
一、前言
排序算法大家都很熟悉了,解法多种多样。
有一个问题和排序算法很相近,TopK问题:从N个数中选出最大的K个数,N通常远大于K。
总结了一些解法,供大家参考。
二、冒泡
private static float[] pickTopKByBubbleSort(float[] a, int k) { int n = a.length; for (int i = 0; i < k; i++) { for (int j = 0; j < n - i - 1; j++) { if (a[j] > a[j + 1]) { float t = a[j]; a[j] = a[j + 1]; a[j + 1] = t; } } } float[] r = new float[k]; for (int i = 0; i < k; i++) { r[i] = a[n - i - 1]; } return r; }
冒泡排序的要义在于:外循环为排序趟数,内循环为每趟比较的次数,每趟得到一个最大的数,放到这一趟的末端。
如果是全数组排序的话,复杂度为O(n^2), 如果只选TopK的话,复杂度为O(n*k)。
三、堆
private static float[] pickTopKByHeapSort(float a[], int k) { int n = a.length; PriorityQueue<Float> queue = new PriorityQueue<>(n); for (int i = 0; i < k; i++) { queue.offer(a[i]); } for (int i = k; i < n; i++) { if (a[i] > queue.peek()) { queue.poll(); queue.offer(a[i]); } } float[] r = new float[k]; for (int i = k - 1; i >= 0; i--) { r[i] = queue.poll(); } return r; }
堆的实现,在JDK中对应的是PriorityQueue,默认情况下是最小堆。
元素插入和删除的时间复杂度都是O(logn)。
堆可以用于排序:将所有元素插入堆中,在依次取出,即可得到有序排列的元素,时间复杂度为O(nlogn)。
如果按照上面冒泡排序的做法,将所有元素插入,再取出K个:
插入时间复杂度为O(nlogn), 取出的时间复杂度为O(klogn), 总体时间复杂度还是O(nlogn)。
当然,虽然复杂度都是O(nlogn),但是比全体元素排序快一点(不用取尽n个元素)。
堆在TopK问题上更优的解法是:
1、只放k个元素到堆中,然后遍历剩下的元素,和堆顶的元素(堆中最小的值)比较,
2、若大于堆顶,则取出堆顶的元素,插入当前元素;若小于等于堆顶,直接跳过;
3、最后依次取出堆中元素。
平均时间复杂度为O(nlogk);
最快的情况下是O(n),如果数组本身已经有序且是逆序的话。
四、快排
先来看下快排的实现:
private static int partition(float[] a, int left, int right) { int i = left; int j = right; float key = a[left]; while (i < j) { while (i < j && a[j] <= key) { j--; } a[i] = a[j]; while (i < j && a[i] >= key) { i++; } a[j] = a[i]; } a[i] = key; return i; } private static void sort(float[] a, int left, int right) { if (left >= right) { return; } int index = partition(a, left, right); sort(a, left, index - 1); sort(a, index + 1, right); }
快排的基本思想是:
1、通过一趟排序将要数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的数据都要小;
2、然后再按此方法对这两部分数据分别进行第1步的操作(递归),以此达到整个数据变成有序序列。
在TopK问题上,可以借鉴以上第1点,以及第2点的一部分:递归。
不同之处在于,求解TopK不需要对整个数据排序,只需要最大的K个数移动到数组的第K个位置的前面即可。
代码如下:
private static void sort(float[] a, int left, int right, int k) { if (left >= right) { return; } int index = partition(a, left, right); int len = index - left + 1; if (k < len) { sort(a, left, index - 1, k); } else if (k > len) { sort(a, index + 1, right, k - len); } } private static float[] pickTopKByQuickSort(float a[], int k) { sort(a, 0, a.length - 1, k); float[] r = new float[k]; for (int i = 0; i < k; i++) { r[i] = a[i]; } // 执行以上操作之后,r[]数组确实包含了top k的元素,但是不一定有序 // 如果仅仅是要求TopK而不需要有序,则不需要执行下面的排序函数 sort(r, 0, k - 1); return r; }
最坏的情况下,时间复杂度和快排一样: O(n^2)。
平均情况下,快排的时间复杂度为O(nlogn),求解TopK时间复杂度为O(n)。
五、桶排序
冒泡排序,堆排序,快速排序等都是基于比较的排序,时间复杂度下限为O(nlogn)。
而有一些排序的时间复杂度可以是O(n), 比如桶排序和基数排序。
但是这类排序有较为苛刻的限制条件:元素须是数值类型。
同时,如果是桶排序的话,范围不能太大,否则需要的桶太多,空间复杂度无法接受。
桶排序的元素不一定要是整数,有时候浮点数也可以。
比如,如果数据取值范围在[0, 100.0], 并且只有两位小数, 可以这么解:
private static float[] pickTopKByBucketSort(float[] a, int k) { int[] bucket = new int[10000]; for (int i = 0; i < a.length; i++) { int index = Math.round(a[i] * 100f); bucket[index]++; } float[] r = new float[k]; int p = 0; for (int i = bucket.length - 1; i >= 0 && p < k; i--) { int c = bucket[i]; while (c > 0 && p < k) { r[p++] = i / 100f; c--; } } return r; }
取一万个桶,将每个元素乘以100,四舍五入为整数(有一些小数在计算机二进制中无法精确表示,同时直接强转成int的话是向下取整),
放到对应的桶中;
然后从最大的桶开始检索,收集100个元素。
时间复杂度跟数据量和桶数量(取决于数据的大小范围)有关。
在数据量远大于桶数量时,时间复杂度为O(n)。
六、测试
集成测试的代码如下:
public static void main(String[] args) { int n = 100000; int k = 100; boolean success = true; Random r = new Random(); for (int j = 0; j < 100; j++) { float[] a = new float[n]; for (int i = 0; i < n; i++) { float t = r.nextFloat() * 100f; t = (int) (t * 100) / 100f; a[i] = t; } float[] tmp = Arrays.copyOf(a, n); float[] top1 = pickTopKByBubbleSort(tmp, k); tmp = Arrays.copyOf(a, n); float[] top2 = pickTopKByHeapSort(tmp, k); tmp = Arrays.copyOf(a, n); float[] top3 = pickTopKByQuickSort(tmp, k); tmp = Arrays.copyOf(a, n); float[] top4 = pickTopKByBucketSort(tmp, k); if (!Arrays.equals(top1, top2) || !Arrays.equals(top1, top3) || !Arrays.equals(top1, top4)) { success = false; break; } } if (success) { System.out.println("success"); } else { System.out.println("failed"); } }
七、总结
这么多种方案,那种方案比较好呢?这个要看情况。
- 冒泡的方法看起来时间复杂度是最高的,但是假如K很小,比如极端情况 , K=1,这时候冒泡的方法反而是最快的;
- 平均情况下,如果可以用的上桶排序(数据取值范围较小),那桶排序是最快的;
- 如果数值取值范围较小,而N很大(比方说100000),K又比较小(比方说100)时,运行发现堆的方法比快排的方法快。
原因猜测:范围小,数据多,应该有很多数值是重复的; 用堆的方法,堆会很快攒到Top的数值(即使还不是TopK),检索到那些小的数值的时候就不需要插入,从而检索后面的元素时,操作就只剩简单的遍历了。
跳脱排序算法和TopK问题本身,最主要的还是思想。
例如,日常项目中,堆(优先队列)就不时会遇到,这前提是脑海中知道有优先队列这个东西,知道它的特性。
有的知识就是这样的,专门去学的时候不知道有什么用,但或许某一天碰到了,就会想起它了。
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
手把手用Python网络爬虫带你爬取全国著名高校附近酒店评论
/1 前言/ 简介:本文介绍如何用python爬取全国著名高校附近的酒店点评,并进行分析,带大家看看著名高校附近的酒店怎么样。 /2 具体实现/ 具体的实现主要是分为三步,具体的操作过程如下。 一、抓取高校附近的酒店信息 由于电脑客户端的美团酒店没有评论信息,于是我从手机端的网页入手,网页地址为:https://i.meituan.com/awp/h5/hotel/search/search.html 通过搜索北京大学附近的酒店,抓包找到了返回酒店json信息的url。 其中,limit代表返回酒店的最大数量(经测试,limit最大为50),offset为每次返回酒店数量的起点,cityId为城市的标志,在网页信息中可以找到,时间参数可以修改,sort为返回酒店信息的排序,sort=distance代表按距离搜索,q和keyword都是大学名称。 返回的数据如下图所示: 包含酒店的名字、地理位置、评分、realPoiId(相当于酒店的身份证号,后面爬评论用的到)、酒店和大学的距离等信息。 下面我们开始爬排名前10高校附近的酒店信息(不要在乎大学排名,我乱找的,以学习为主): (图片来源...
- 下一篇
前端科普系列(1):前端简史
本文首发于 vivo互联网技术 微信公众号 链接: https://mp.weixin.qq.com/s/VRSl5_yn5BZcqtRxWkXU-Q 作者:孔垂亮 一、什么是前端 回答这个问题之前,我想起了一道非常经典的前端面试题:“从输入URL到页面呈现在你面前到底发生了什么?”这个题目可以回答的很简单,但仔细思考,也可以回答的很深,这个过程涉及的东西很多。先看一张图: 简单说就是 DNS (Domain Name System) 解析 TCP (Transmission Control Protocol) 链接 HTTP (HyperText Transfer Protocol) 请求 HTTP 响应 HTML解析 & CSS渲染 JS 解析执行 为什么提这个呢,因为这是一整个web服务生命周期的全过程,而在最早的时候是根本没有前端或者后端的概念的。当时就是用 Dreamweaver 写 html 静态页面,然后部署到一台电脑的 IIS (Internet Information Services) 上。当请求这个页面时,返回这个 html 文件。 再后面一点,服务端变得...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- CentOS8安装MyCat,轻松搞定数据库的读写分离、垂直分库、水平分库
- SpringBoot2整合Thymeleaf,官方推荐html解决方案
- Mario游戏-低调大师作品
- 2048小游戏-低调大师作品
- SpringBoot2初体验,简单认识spring boot2并且搭建基础工程
- 设置Eclipse缩进为4个空格,增强代码规范
- Windows10,CentOS7,CentOS8安装Nodejs环境
- MySQL8.0.19开启GTID主从同步CentOS8
- Docker快速安装Oracle11G,搭建oracle11g学习环境