zoukankan      html  css  js  c++  java
  • 利用堆排序和分治法求解千万级数据排序的Top K问题—百度面试

    摘要:基于堆排序算法和分治法求解千万级数据最值排序的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。如果各位有什么好的算法,欢迎跟帖讨论。


      读后有收获,小礼物走一走,请作者喝咖啡。

    赞赏支持

  • 相关阅读:
    Electron(3)调用第三方DLL
    Electron(1)概述
    Java SpringMVC(6)Mybatis-Plus
    Socket粘包问题的3种解决方案
    HTTP
    2020再见 2021你好
    再谈领域驱动设计
    使用Domain-Driven创建Hypermedia API
    使用函数式语言来建立领域模型--类型组合
    PHP安装扩展
  • 原文地址:https://www.cnblogs.com/east7/p/14738990.html
Copyright © 2011-2022 走看看