为什么评价算法性能是根据时间和空间复杂度,而不是别的参数?是因为计算机结构是冯诺依曼体系,除了输入输出设备和控制器,就剩下运算器和存储器了

问题引入

搜索引擎的热门搜索排行榜功能是如何实现的?搜索引擎每天会接收大量的用户搜索请求,把这些用户输入的搜索关键词记录下来,然后再离线地统计分析,得到最热门的 Top 10 搜索关键词。假设现在我们有一个包含 10 亿个搜索关键词的日志文件,如何能快速获取到热门榜 Top 10 的搜索关键词呢

堆这种数据结构几个非常重要的应用:优先级队列、求 Top K 和求中位数

堆的应用一:优先级队列

优先级队列

堆的核心功能就是取出堆顶数据!(大顶堆--最大值;小顶堆--最小值)。 使用方法:

1.插入数据,与堆顶比较决定插入与否(比如小顶堆堆顶拿到最小值,若新插入数据比它大,就删掉堆顶最小值,插入新数据,从而堆中保留了TopK);

2.取数据,取最大值/最小值(如优先队列的优先级数值就是大顶堆堆顶即最大值,入队后自下而上堆化得到最新的堆顶,出队时直接出堆顶即可。)

1、优先级队列中,数据的出队顺序不是先进先出,而是按照优先级来,优先级最高的,最先出队

2、用堆来实现是最直接、最高效的

3、往优先级队列中插入一个元素,就相当于往堆中插入一个元素;从优先级队列中取出优先级最高的元素,就相当于取出堆顶元素。

合并有序小文件

假设我们有 100 个小文件,每个文件的大小是 100MB,每个文件中存储的都是有序的字符串。我们希望将这些 100 个小文件合并成一个有序的大文件。这里就会用到优先级队列。

方案一

1、使用一个数组,从这100个文件中各自取出第一个字符串放入数组比较大小,然后将最小的放入合并后的文件中,并从数组中删除。

2、假设最小的字符串来自A文件,那么再从A文件中取出第二个字符串放入比较数组中重新比较大小

3、重复执行步骤1,直到所有文件中数据都放入合并后的大文件

缺点:每次从数组中取最小字符串,都需要循环遍历整个数组

方案二

使用优先级队列(堆)

1、从100个文件中取出第一个元素放到小顶堆中,堆顶元素也即优先级队列队首就是最小字符串。将该字符串放入到合并后最终大文件中,并从堆顶部删除

2、再从小文件中取出下一个元素放入堆顶,循环该过程

3、删除堆顶元素和堆中插入元素的复杂度都是O(logn)

高性能的定时器

假设有一个定时器,定时器中维护了很多定时任务,每个任务都设定了一个要触发执行的时间点。定时器每过一个很小的单位时间(比如 1 秒),就扫描一遍任务,看是否有任务到达设定的执行时间。如果到达了,就拿出来执行

这种方式的缺点:每过 1 秒就扫描一遍任务列表的做法比较低效,第一,任务的约定执行时间离当前时间可能还有很久,这样前面很多次扫描其实都是徒劳的;第二,每次都要扫描整个任务列表,如果任务列表很大的话,势必会比较耗时。

优化方案

  • 按照任务设定的执行时间,将这些任务存储在优先级队列中,队列首部(也就是小顶堆的堆顶)存储的是最先执行的任务。
  • 拿队首任务的执行时间点,与当前时间点相减,得到一个时间间隔 T
  • 定时器就可以设定在 T 秒之后,再来执行任务。从当前时间点到(T-1)秒这段时间里,定时器都不需要做任何事情
  • T 秒时间过去之后,定时器取优先级队列中队首的任务执行。然后再计算新的队首任务的执行时间点与当前时间点的差值,把这个值作为定时器执行下一个任务需要等待的时间

利用堆求Top K

一类:静态数据集合

  • 维护一个大小为 K 的小顶堆,顺序遍历数组,从数组中取出数据与堆顶元素比较
  • 如果比堆顶元素大,把堆顶元素删除,并且将这个元素插入到堆中;如果比堆顶元素小,则不做处理,继续遍历数组
  • 最后堆中的数据就是前K大数据

遍历数组需要 O(n) 的时间复杂度,一次堆化操作需要 O(logK) 的时间复杂度,所以最坏情况下,n 个元素都入堆一次,时间复杂度就是 O(nlogK)

二类:动态数据集合

一个数据集合中有两个操作,一个是添加数据,另一个询问当前的前 K 大数据。如果每次询问前 K 大数据,我们都基于当前的数据重新计算的话,那时间复杂度就是 O(nlogK),n 表示当前的数据的大小;

方案:

  • 实际上,可以一直维护一个 K 大小的小顶堆,当有数据被添加到集合中时,与堆顶的元素对比。
  • 如果比堆顶元素大,把堆顶元素删除,并且将这个元素插入到堆中;如果比堆顶元素小,则不做处理。
  • 无论任何时候需要查询当前的前 K 大数据,都可以立刻返回给他

利用堆求中位数

如何求动态数据集合中的中位数:中位数就是处在中间位置的那个数。如果数据的个数是奇数,把数据从小到大排列,那第 2n​+1 个数据就是中位数(注意:假设数据是从 0 开始编号的);如果数据的个数是偶数的话,那处于中间位置的数据有两个,第 2n​ 个和第 2n​+1 个数据,这个时候可以随意取一个作为中位数,比如取两个数中靠前的那个,就是第 2n​ 个数据

静态数据

对于静态数据,中位数是固定的。可以先排序,第 2n​ 个数据就是中位数。每次询问中位数的时候,直接返回这个固定的值。所以,尽管排序的代价比较大,但是边际成本会很小。

动态数据

动态数据集合,中位数在不停地变动。

需要维护两个堆,一个大顶堆,一个小顶堆。大顶堆中存储前半部分数据,小顶堆中存储后半部分数据,且小顶堆中的数据都大于大顶堆中的数据。如果有 n 个数据,n 是偶数,我们从小到大排序,那前 2n​ 个数据存储在大顶堆中,后 2n​ 个数据存储在小顶堆中。这样,大顶堆中的堆顶元素就是我们要找的中位数。如果 n 是奇数,情况是类似的,大顶堆就存储 2n​+1 个数据,小顶堆中就存储 2n​ 个数据

当添加一个数据时候:如何调整两个堆,使得大顶堆中的堆顶数据继续是中位数呢?

  • 如果新加入的数据小于等于大顶堆的堆顶元素,将这个新数据插入到大顶堆;否则,将这个新数据插入到小顶堆
  • 可能情况:如果 n 是偶数,两个堆中的数据个数都是 2n​;如果 n 是奇数,大顶堆有 2n​+1 个数据,小顶堆有 2n​ 个数据
  • 这时候需要从一个堆中不停地将堆顶元素移动到另一个堆,通过这样的调整,来让两个堆中的数据满足上面的约定
  • 最终求中位数只需要返回大顶堆的堆顶元素就可以了,所以时间复杂度就是 O(1)。

如何快速求接口的 99% 响应时间?

99% 响应时间。如果有 100 个接口访问请求,每个接口请求的响应时间都不同,比如 55 毫秒、100 毫秒、23 毫秒等,我们把这 100 个接口的响应时间按照从小到大排列,排在第 99 的那个数据就是 99% 响应时间,也叫 99 百分位响应时间。

利用两个堆实现。一个大顶堆,一个小顶堆。假设当前总数据的个数是 n,大顶堆中保存 n*99% 个数据,小顶堆中保存 n*1% 个数据。大顶堆堆顶的数据就是我们要找的 99% 响应时间。方法类似前面求中位数

解答开头

一个包含 10 亿个搜索关键词的日志文件,如何快速获取到 Top 10 最热门的搜索关键词呢?

多台机器可以采用map-reduce的解决方法

单机环境:

方案一

1、首先统计每个关键词出现频率

2、采用散列表,顺序扫描10亿个关键词,存在次数+1,不存在记录次数为1;遍历完成后散列表中就存储了不重复关键词以及出现次数

3、利用堆求Top K的方法,建立一个小顶堆大小为10,遍历散列表,然后从散列表汇中依次取出每个搜索关键词以及对应的出现次数,与堆顶中的搜索关键词进行对比。大就删除堆顶关键词替换为新的关键词,小于则不作处理

缺点:如果每个搜索关键词的平均长度是 50 个字节,那存储 1 亿个关键词起码需要 5GB 的内存空间,散列表因为要避免频繁冲突,不会选择太大的装载因子,所以消耗的内存空间就更多了。如果机器只有1G内存空间,那么无法一次性将所有关键词加入内存,

方案二

1、创建 10 个空文件 00,01,02,……,09

2、遍历这 10 亿个关键词,通过哈希算法对其求哈希值,然后哈希值同 10 取模,得到的就是搜索关键词应该被分到的文件编号

3、10 亿个关键词分片之后,每个文件都只有 1 亿的关键词,去除掉重复的,可能就只有 1000 万个,每个关键词平均 50 个字节,所以总的大小就是 500MB。1GB 的内存完全可以放得下

4、每个包含 1 亿条搜索关键词的文件,利用散列表和堆,分别求出 Top 10,然后把这个 10 个 Top 10 放在一块,然后取这 100 个关键词中,出现次数最多的 10 个关键词,这就是这 10 亿数据中的 Top 10 最频繁的搜索关键词

总结

优先级队列是一种特殊的队列,优先级高的数据先出队,而不再像普通的队列那样,先进先出。堆就可以看作优先级队列,只是称谓不一样罢了。

求 Top K 问题又可以分为针对静态数据和针对动态数据,只需要利用一个堆,就可以做到非常高效率地查询 Top K 的数据。

求中位数实际上还有很多变形,比如求 99 百分位数据、90 百分位数据等,处理的思路都是一样的,即利用两个堆,一个大顶堆,一个小顶堆,随着数据的动态添加,动态调整两个堆中的数据,最后大顶堆的堆顶元素就是要求的数据。

思考

有一个访问量非常大的新闻网站,我们希望将点击量排名 Top 10 的新闻摘要,滚动显示在网站首页 banner 上,并且每隔 1 小时更新一次。如果你是负责开发这个功能的工程师,你会如何来实现呢?

方案一 1、实时建立散列表,key是新闻的摘要,value是点击量;
2、建立一个10的小顶堆,每隔一个小时扫描一次散列表,根据点击量大小放入到小顶堆中,扫描完散列表后即出现Top10 的新闻点击量。
方案二 1,对每篇新闻摘要计算一个hashcode,并建立摘要与hashcode的关联关系,使用map存储,以hashCode为key,新闻摘要为值
2,按每小时一个文件的方式记录下被点击的摘要的hashCode
3,当一个小时结果后,上一个小时的文件被关闭,开始计算上一个小时的点击top10
4,将hashcode分片到多个文件中,通过对hashCode取模运算,即可将相同的hashCode分片到相同的文件中
5,针对每个文件取top10的hashCode,使用Map<hashCode,int>的方式,统计出所有的摘要点击次数,然后再使用小顶堆(大小为10)计算top10,
6,再针对所有分片计算一个总的top10,最后合并的逻辑也是使用小顶堆,计算top10
7,如果仅展示前一个小时的top10,计算结束
8,如果需要展示全天,需要与上一次的计算按hashCode进行合并,然后在这合并的数据中取top10
9,在展示时,将计算得到的top10的hashcode,转化为新闻摘要显示即可
方案三

1,维护两个散列表,一个是一小时新增的点击量的散列表,以新闻id为键,点击次数为值。一个是全部点击量的散列表。每隔一小时把新增的散列表的数据同步到全部点击量的散列表。然后把这小时内有变化的全部点击量的散列表的数据(即此小时有新增点击量的新闻数据)和我们维护的10个元素小顶堆堆顶进行比较,比堆顶的点击量大的,则使用该元素替换堆顶,再进行堆化。比堆顶点击量小的则不做处理。然后比较完,根据堆顶的10个元素的id,从数据库读取相应的新闻摘要显示在banner上。除此之外,还要把变化后的全部点击量散列表同步到数据库。因为保存的是新闻id,所以散列表长度不会很大,所占用的内存也不会很大。而每个小时新增的访问量的新闻id数也不会很多,毕竟很多人只会阅读热门消息。所以新增的点击量的新闻数据假设为k,则每小时同步小顶堆的时间负责度为o(klg 10);

2018-12-02

29 | 堆的应用:如何快速获取到Top 10最热门的搜索关键词?相关推荐

  1. 如何快速获取到Top 10最热门的搜索关键词?

    因为用户搜索的关键词,有很多可能都是重复的,所以我们首先要统计每个搜索关键词出现的频率.我们可以通过散列表.平衡二叉查找树或者其他一些支持快速查找.插入的数据结构,来记录关键词及其出现的次数. 假设我 ...

  2. python使用列表,快速获取1到10的平方

    python使用列表,快速获取1到10的平方 squares = [value**2 for value in range(1, 11)] print(squares) 这个蛮活的 1.要使用三次方或 ...

  3. MS SQLSERVER中如何快速获取表的记录总数

    (转自:http://www.cnblogs.com/pingkeke/archive/2006/05/29/411995.html) 在数据库应用的设计中,我们往往会需要获取某些表的记录总数,用于判 ...

  4. java快速获取大图片的分辨率(大图片格式JPG,tiff ,eg)

    问题描述:怎样快速获取一个20MB图片的分辨率? 程序代码: 1 package test; 2 3 import java.awt.Dimension; 4 import java.awt.imag ...

  5. 快速获取Windows系统上的国家和地区信息

    Windows系统上包含了200多个国家和地区的数据,有时候编程需要这些资料.以下代码可以帮助你快速获取这些信息. 将Console语句注释掉,可以更快的完成分析. 1 static void Mai ...

  6. iOS中使用c函数快速获取一个目录所占的空间大小

    在编写iOS应用时,我们都会大量把服务器的接口调用数据或者图片数据进行缓存,为了更好的使用体验,有时候我们也会把各种缓存的数据所占的空间大小告诉用户,提供给用户参考,并让用户可以手动清除缓存数据,就像 ...

  7. 怎么快速获取复杂json的结构

    1.背景 一段json非常长,且结构复杂,怎么快速获取嵌套结构 2.方法 使用vscode的折叠功能,配合前端界面的图片内容提示,删除冗余的内容(list中的内容只保留第一个),这样便可以缩短json ...

  8. 红蓝对抗-红队渗透下的入口权限快速获取

    红队渗透下的入口权限快速获取 文章目录 红队渗透下的入口权限快速获取 前言 红队攻击流程概述 获取入口权限 利用常见组件的漏洞 高危漏洞的检测与攻击 POC的集成与自动化验证 POC bomber 前 ...

  9. 网络营销外包专员浅析网站网络营销外包如何快速获取关键词排名

    企业网站正式上线后在激烈的行业竞争中理应思索如何做好网站推广工作,站长可根据企业品牌现状为网站指定相应的网站排名优化策略,为网站更好的呈现和展示在用户面前,争取能够获得搜索引擎和用户的喜爱.那么应该如 ...

最新文章

  1. 对不起,作为导师,我让您失望了!
  2. Nmap 7.70新增功能——扫描主机所有IP
  3. .net 互操作之p/invoke- 数据封送之字符串(2)
  4. 数据库设计_SQL数据库设计(数据建模)
  5. Notepad ++添加到每一行
  6. java hssfworkbook 乱码_java各种乱码汇总
  7. 【Unity3D 灵巧小知识点】☀️ | Unity 移动物体到指定位置的几种方法【精选快捷使用】
  8. 无法解析 taglibs:standard:1.1.2
  9. 服务网格在好未来学而思网校的实践和思考
  10. idea配置有道翻译引擎
  11. oracle sql 动态参数,sql – 具有动态参数的Oracle Lag函数
  12. vi/vim 中可以使用 :s 命令来替换字符串。
  13. 从excel中读取信号,首先计算信号的vmd分解,得到imf分量
  14. 拉勾前端高薪就业课程笔记第十一弹(模块4-1)
  15. [转载]modbus通讯协议详解和几张modbus图解 力求通俗易懂
  16. 互联网云厂商,打响能源TO B争夺战
  17. Windows网络的管理与维护(转)
  18. 城市运行管理系统建设案例(南京、深圳)
  19. 用一小时实现的娃娃机,你敢信?
  20. 使用APICloud低代码开发平台开发物流仓储app

热门文章

  1. 前端学习(1990)vue之电商管理系统电商系统之自定义时间过滤器
  2. 前端学习(1705):前端系列javascript之原型中的this
  3. 前端学习(477):前端简介1
  4. mybatis学习(52):扩展结果的展现
  5. spring mvc学习(46):自定义配置类
  6. 44response对象
  7. 实例61:python
  8. linux下有四个作业优先级,第一次作业:对Linux系统分析
  9. 阿里云设置域名解析到主机ip
  10. python管理工具ports_Python options.port方法代码示例