摘要:基于堆排序算法和分治法求解千万级数据最值排序的Top K问题。

问题描述

  这是在网上找到的一道百度的面试题:搜索引擎会通过日志文件把用户每次检索使用的所有检索串都记录下来,每个检索串的长度为1-255字节。假设目前有一千万个记录,这些检索串的重复度比较高,虽然总数是1千万,但如果除去重复检索串后,总量不超过三百万个。一个检索串的重复度越高,说明查询它的用户越多,也就是越热门。请你统计最热门的10个检索串,要求使用的内存不能超过1G。

问题解析

  要统计最热门查询,首先就是要统计每个检索串出现的次数,然后根据统计结果,找出Top 10。所以我们可以基于这个思路分两步来设计该算法,下面分别给出这两步的算法。

第一步:查询次数统计

  从题目可以得知,虽然有一千万个Query,但是由于重复度比较高,因此事实上只有300万的Query,每个Query255Byte,因此我们可以考虑把它们都放进内存中去。而现在只是需要一个合适的数据结构,在这里,哈希表绝对是我们优先的选择,因为哈希表的查询速度非常的快,几乎是O(1)的时间复杂度。那么,我们的算法就有了:维护一个Key为Query字串,Value为该Query出现次数的哈希表,每次读取一个Query,如果该字串不在哈希表中,那么加入该字串,并且将Value值设为1;如果该字串在哈希表中,那么将该字串的计数加一即可。最终我们在O(N)的时间复杂度内完成了对该海量数据的处理。

  该方法只需要IO数据文件一次,因此该算法在工程上有更好的可操作性。另外,key也可以换成Query的哈希值(如MD5的值),以节约内存。

第二步:找出Top 10

算法一:排序

  我想对于排序算法大家都已经不陌生了,这里不再赘述,我们要注意的是排序算法的时间复杂度是NlgN,在本题目中,三百万条记录,用1G内存是可以存下的。

算法二:部分排序

  题目要求是求出Top 10,因此没有必要对所有的Query都进行排序,我们只需要维护一个10个大小的数组,初始化放入10Query,按照每个Query的统计次数由大到小排序,然后遍历这300万条记录,每读一条记录就和数组最后一个Query对比,如果小于这个Query,那么继续遍历,否则,将数组中最后一条数据淘汰,加入当前的Query。最后当所有的数据都遍历完毕之后,那么这个数组中的10个Query便是我们要找的Top10了。不难分析出,这样的算法的时间复杂度是N*K, 其中K是指top多少。

算法三:堆排序

  在算法二中,我们已经将时间复杂度由NlogN优化到NK,不得不说这是一个比较大的改进了,可是有没有更好的办法呢?分析一下,在算法二中,每次比较完成之后,需要的操作复杂度都是K,因为要把元素插入到一个线性表之中,而且采用的是顺序比较。这里我们注意一下,该数组是有序的,我们每次查找的时候可以采用二分的方法查找,这样操作的复杂度就降到了logK,可是,随之而来的问题就是数据移动,因为移动数据次数增多了。不过,这个算法还是比算法二有了改进。

  基于以上的分析,我们想想,有没有一种既能快速查找,又能快速移动元素的数据结构呢?回答是肯定的,那就是堆。借助堆结构,我们可以在log量级的时间内查找和调整/移动。因此到这里,我们的算法可以改进为这样,维护一个元素个数为k(该题目中是10小的小根堆,然后遍历300万的Query,分别和根元素进行对比。最小堆构建过程如下:

1、构造初始堆

  遍历待排序的N个数,把最先遍历到的k个数存放到最小堆中,并假设它们就是我们要找的最大的K个数,然后构造第一个小根堆。

2、首尾交换,断尾重构

  对断尾后剩余部分重新构造小根堆

3、迭代执行第二步

  将第二步进行重复,直到首尾重叠,排序完成

  注意:大根堆排序后是从小到大,小根堆排序后是从大到小。那么这样,这个算法发时间复杂度就降到了NlogK,和算法二相比,又有了比较大的改进。

算法四:分治法

   借助快速排序方法获取Top K。思路如下:

  (1)比如有10亿的数据,找出Top 1000,我们先将10亿的数据分成1000份,每份100万条数据。

  (2)在每一份中找出对应的Top 1000,整合到一个数组中,得到100万条数据,这样过滤掉了90%的数据。

  (3)使用快速排序对这100万条数据进行一轮排序,一轮排序之后指针的位置指向的数字假设为S,会将数组分为两部分,一部分大于S记作Si,一部分小于S记作Sj。

  (4)如果Si元素个数大于1000,我们对Si数组再进行一轮排序,再次将Si分成了Si和Sj。如果Si的元素小于1000,则我们需要在Sj中获取1000-count(Si)个元素的,也就是对Sj进行排序

  (5)如此递归下去即可获得Top K。

  时间复杂度与空间复杂度:(1)时间复杂度:一份获取前TopK的时间复杂度:O((N/n)logK)。则所有份数为:O(NlogK),但是分治法我们会使用多核多机的资源,比如我们有S个线程同时处理。则时间复杂度为:O((N/S)logK)。之后进行快排序,一次的时间复杂度为:O(N),假设排序了M次之后得到结果,则时间复杂度为:O(MN)。所以 ,总时间复杂度大约为O(MN+(N/S)logK) 。(2)空间复杂度:需要每一份一个数组,则空间复杂度为O(N)。

小结

  至此,我们的算法就完全结束了,经过步骤一和步骤二的最优结合,最终的时间复杂度是O(N) + O(N’)logK。如果各位有什么好的算法,欢迎跟帖讨论。

利用堆排序和分治法求解千万级数据排序的Top K问题—百度面试的更多相关文章

  1. 分治法求解最近对问题(c++)

    #include"stdafx.h" #include<iostream> #include<cmath> #define TRUE 1 #define F ...

  2. Java算法——分治法

         一.基本概念 在计算机科学中,分治法是一种很重要的算法.字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简 ...

  3. [C++] 分治法之棋盘覆盖、循环赛日程表

    一.分治的基本思想 将一个难以直接解决的大问题,分割成一些规模较小的相同问题,以便各个击破,分而治之. 对于一个规模为 n 的问题,若问题可以容易地解决,则直接解决,否则将其分解为 k 个规模较小的子 ...

  4. 分治法及其python实现例子

    在前面的排序算法学习中,归并排序和快速排序就是用的分治法,分治法作为三大算法之一的,有非常多的应用例子. 分治法概念 将一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题-- ...

  5. 分治法 - Divide and Conquer

    在计算机科学中,分治法是一种很重要的算法.分治法即『分而治之』,把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的 ...

  6. 分治法避免定义多个递归函数,应该使用ResultType

    总结:对二叉树应用分治法时,应避免定义多个递归函数,当出现需要递归求解多种的结果时,尽量使用ResultType来让一次递归返回多种结果. 题目:Binary Tree Maximum Path Su ...

  7. 分治法(一)(zt)

    这篇文章将讨论: 1) 分治策略的思想和理论 2) 几个分治策略的例子:合并排序,快速排序,折半查找,二叉遍历树及其相关特性. 说明:这几个例子在前面都写过了,这里又拿出来,从算法设计的策略的角度把它 ...

  8. C语言实现快速排序法(分治法)

    title: 快速排序法(quick sort) tags: 分治法(divide and conquer method) grammar_cjkRuby: true --- 算法原理 分治法的基本思 ...

  9. p1257 平面上最接近点对---(分治法)

    首先就是一维最接近点的情况... #include<iostream> #include<cstdio> #include<cstring> #include< ...

  10. MySQL千万级数据分区存储及查询优化

    作为传统的关系型数据库,MySQL因其体积小.速度快.总体拥有成本低受到中小企业的热捧,但是对于大数据量(百万级以上)的操作显得有些力不从心,这里我结合之前开发的一个web系统来介绍一下MySQL数据 ...

随机推荐

  1. ZLMediaKit: 快速入门

    目录 ZLMediaKit是什么 编译 安装依赖库 构建项目 问题处理 问题1: srtp 未找到, WebRTC 相关功能打开失败 问题2:依赖库问题 安装 配置和运行 问题处理 问题1: 无权限监 ...

  2. C#如何使用HttpClient对大文件进行断点上传和下载

    什么是Http的断点上传和下载 断点上传:在向服务商上传大文件的时候,将一个大的文件拆分成多个小的文件,每个文件通过单独的Http请求上传给服务器. 断点下载:在向服务器请求下载一个大的资源文件的时候 ...

  3. xxe学习笔记

    什么是xxe XXE(XML External Entity Injection)全称为XML外部实体注入,由于程序在解析输入的XML数据时,解析了攻击者伪造的外部实体而产生的.例如PHP中的simp ...

  4. vue2中如何使用组合式API和vueuse工具包

    vue2中如何使用组合式API和vueuse工具包 1. 安装 @vue/composition-api 依赖包 yarn add @vue/composition-api # 或 npm insta ...

  5. 如何编写正确高效的Dockerfile

    Dockerfile是什么 Dockerfile 非常普通,它就是一个纯文本,里面记录了一系列的构建指令,比如选择基础镜像.拷贝文件.运行脚本等等,RUN, COPY, ADD指令都会生成一个 Lay ...

  6. go module基本使用

    前提 go版本为1.13及以上 官方文档 如果你想更深层次的了解GO MODULE的意义及开发者们的顾虑,可以直接访问官方文档(EN) https://github.com/golang/go/wik ...

  7. Go语言遍历字符串——获取每一个字符串元素

    遍历字符串有下面两种写法. 遍历每一个ASCII字符 遍历 ASCII 字符使用 for 的数值循环进行遍历,直接取每个字符串的下标获取 ASCII 字符,如下面的例子所示. theme := &qu ...

  8. 在Ubuntu上安装php7.2、php7.3、php7.4

    目录 开始之前 在Ubuntu 18.04或16.04上安装PHP 7.4 更新Ubuntu 添加PHP存储库 安装PHP 7.4 在Ubuntu 16.04上安装PHP 7.2 更新Ubuntu 添 ...

  9. Netty源码—5.Pipeline和Handler

    大纲 1.Pipeline和Handler的作用和构成 2.ChannelHandler的分类 3.几个特殊的ChannelHandler 4.ChannelHandler的生命周期 5.Channe ...

  10. StringBuilder的介绍、构造方法及成员方法

    1.StringBuilder的介绍 1.StringBuilder是字符串缓冲区,可以认为是一种容器,能装任何类型的数据,但被装入的数据都会变为字符串 如图 无论是什么类型的数据,被装入字符串缓冲区 ...