读过《编程珠玑》(<Programming Pearls>)的人应该还对开篇的Case记忆犹新,大概的场景是:

作者的一位在电话公司工作的朋友想要统计一段时间内不同的电话号码的个数,电话号码的数量很大,当时的内存很小,所以不能把所有的电话号码全部放到内存来去重统计,他的朋友很苦恼。

作者聪明的想到了用bit数组来解决问题,每个电话号码可以映射为bit数组的index,bit数组初始状态所有位为0,所有电话号码逐一处理:将bit数组对应位置为1,处理完之后统计bit数组中有多少个1即可。

示例:[0,1,0,0,0,1,0,...] 这个bit数组表示2和5存在

不得不说这种想法非常精妙,即减少了内存占用(8位电话号码如果全部放到内存需要381M(每个电话号码存成Integer占4Byte计算),而使用bit数组只需要11M),而且只需要两次循环就可以得到结果;

这是一个基数计数的问题,Cardinality:estimating the number of distinct elements.

1 Bitmap

上边提到的方式是使用bitmap,思路是将dataset中的每一个element映射到一个bit位,不允许冲突,所需要的内存空间大概为基数*1bit(上例中是100,000,000bit),并且计数精准;

但是当基数非常大时,即使bitmap内存也放不下!

好消息是如果不要求计数精准(允许一定范围内的误差),可以采用概率估算算法:

2 LC:Linear Counting

公式

其中:n是估算值,m是bitmap大小,Vn是bitmap中0出现的比率,比如0.1;

原理

使用固定大小的hashtable来存放dataset,可以想见:

  • dataset的基数越小,hashtable越空,当基数为0时hashtable所有的key都是空的
  • dataset的基数越大,hashtable越满,冲突也越多,当基数无穷大时hashtable所有的key都被占用并且每个key都有大量冲突

所以可以根据hashtable中key被占用的情况来估算dataset的基数,这里主要用到了对数曲线的特性(0<x<1这一段):

过程

初始化一个bitmap,所有位为0,dataset的每一个element都hash到bitmap的一个bit位,hash之后将对应的bit位置为1,hash允许冲突,最后根据bitmap中0的数量和bitmap大小由公式来估算基数;

注意:虽然LC用的也是bitmap,但是相比原始的bitmap算法,LC的bitmap大小可以比基数小很多,因为LC的映射允许冲突,另外可以设置bitmap大小来决定误差的大小;

推导过程详见参考

参考:A linear-time probabilistic counting algorithm for database applications

3 LLC:LogLog Counting

公式


其中:m是桶的数量,M为桶的集合,k是用于分桶的位数,ρ为bit数组中第一个为1的下标即index,E是估算值;

原理

来看抛硬币的过程,抛硬币的过程是伯努利Bernoulli过程,每次的结果要么是0,要么是1,并且概率均为1/2,假设一次抛硬币game定义为抛到1为止,可能第一次就抛到1(P=0.5),也可能前边抛了i次0最后才抛到1(P=1-0.5^i);抛硬币game玩的次数越多,越容易出现前边很多次都是0的情况,这是因为开头连续出现的0的个数越多,出现概率越小,需要尝试伯努利过程的次数就越多,所以可以利用概率根据结果(开头出现0的个数,即i)来反推出条件(game玩了多少次,即n=2^i,这个也很容易理解,比如彩票的中奖概率是1/10000,即平均买10000张彩票才能中一次奖,反过来说,如果有人中了一次奖,他很大概率上应该买了10000张彩票);

但是由于随机性的存在导致误差较大,所以通过将dataset分为m份,每份单独统计,最后取算数平均值的方式来降低随机性从而减小误差;

过程

dataset的每一个element先映射到一个bit数组,比如32位bit数组,将这个bit数组的1到k位的值作为桶的bucket_index(即第几个桶),将k+1到32位中第一个为1的index作为value放到桶中,如果桶里已经有value,桶会保存一个最大的value,数据集元素全部映射完之后,将所有桶的value取算数平均值,根据n=2^i,这样可以得到每个桶内的基数,再乘以m可以得到整个dataset的基数,公式最前边的α是修正参数;

比如k=2,则m=2^k=4,即4个桶,dataset中一个element映射的bit数组为[1,0,0,0,0,1,0,...],取前两位[1,0]对应的值是2,即第2个桶,取第3位之后的数组[0,0,0,1,0,...]可见第一个位1的index是3,将3放到桶2中,以此类推;

推导过程详见参考

参考:Loglog Counting of Large Cardinalities

4 HLLC:Hyper LogLog Counting

公式

原理

同LLC,只有一点不同:取均值的时候不使用算数平均数而改用调和平均数

推导过程详见参考

参考:HyperLogLog: the analysis of a near-optimal cardinality estimation algorithm

5 最优实践

公式

过程

在大量实践中根据各个参数和结果的情况进行调优,综合使用HLLC和LC等算法

参考:HyperLogLog in Practice: Algorithmic Engineering of a State of The Art Cardinality Estimation Algorithm

总结

目前HLLC在很多开源组件中都有应用,比如redis、druid等

其他:

https://research.neustar.biz/2012/10/25/sketch-of-the-day-hyperloglog-cornerstone-of-a-big-data-infrastructure/

【原创】大叔算法分享(4)Cardinality Estimate 基数计数概率算法的更多相关文章

  1. 【原创】大叔算法分享(5)聚类算法DBSCAN

    一 简介 DBSCAN:Density-based spatial clustering of applications with noise is a data clustering algorit ...

  2. 【原创】大叔经验分享(83)impala执行多个select distinct

    impala在一个select中执行多个count distinct时会报错,比如执行 select key, count(distinct column_a), count(distinct col ...

  3. 解读Cardinality Estimation<基数估计>算法(第一部分:基本概念)

    基数计数(cardinality counting)是实际应用中一种常见的计算场景,在数据分析.网络监控及数据库优化等领域都有相关需求.精确的基数计数算法由于种种原因,在面对大数据场景时往往力不从心, ...

  4. 【原创】经验分享:一个小小emoji尽然牵扯出来这么多东西?

    前言 之前也分享过很多工作中踩坑的经验: 一个线上问题的思考:Eureka注册中心集群如何实现客户端请求负载及故障转移? [原创]经验分享:一个Content-Length引发的血案(almost.. ...

  5. 基数计数——HyperLogLog

    所谓的基数计数就是统计一组元素中不重复的元素的个数.如统计某个网站的UV,或者用户搜索网站的关键词数量:再如对一个网站分别统计了三天的UV,现在需要知道这三天的UV总量是多少,怎么融合多个统计值. 1 ...

  6. 【原创】大叔案例分享(4)定位分析--见证scala的强大

    一 场景分析 定位分析广泛应用,比如室外基站定位,室内蓝牙beacon定位,室内wifi探针定位等,实现方式是三点定位 Trilateration 理想情况 这种理想情况要求3个基站‘同时’采集‘准确 ...

  7. 【原创】大叔经验分享(51)docker报错Exited (137)

    docker container启动失败,报错:Exited (137) *** ago,比如 Exited (137) 16 seconds ago 这时通过docker logs查不到任何日志,从 ...

  8. 【原创】大叔经验分享(50)hue访问mysql(librdbms)

    cloudera manager安装hue后想开启访问mysql(librdbms)需要在这里配置(hue_safety_valve.ini) 添加配置如下 [librdbms] # The RDBM ...

  9. 【原创】大叔经验分享(49)hue访问hdfs报错/hue访问oozie editor页面卡住

    hue中使用hue用户(hue admin)访问hdfs报错: Cannot access: /. Note: you are a Hue admin but not a HDFS superuser ...

随机推荐

  1. mysql 解压版安装

    1.官网下载压缩包 2.解压 3.配置环境变量 添加系统环境变量  MYSQL_HOME 值为解压的主目录,例如     D:\mysql-5.7.25-winx64 修改Path 环境变量,点击编辑 ...

  2. swiper 轮播图,拖动之后继续轮播

    在此贴出swiper官网地址:https://www.swiper.com.cn/api/index.html 示例如下(官网示例): <script> var mySwiper = ne ...

  3. Python——字符转换(int , bool ,str)

    while True: pass while 1: pass #效果相同,后者 效果更快 s = 1 y = bool (s) #结果为True,int只要不是0,就为真 s = 'abc' y = ...

  4. Python——使用Pycharm连接数据库

  5. redis哨兵(Sentinel)、虚拟槽分区(cluster)和docker入门

    一.Redis-Sentinel(哨兵) 1.介绍 Redis-Sentinel是redis官方推荐的高可用性解决方案,当用redis作master-slave的高可用时,如果master本身宕机,r ...

  6. Flask 蓝图,数据库链接

    蓝图 使用场景 如果代码非常多,要进行归类.不同的功能放在不同的文件,把相关的视图函数也放进去. 蓝图也就是对flask的目录结构进行分配(应用于小,中型的程序) 当然对于大型项目也可以通过   ur ...

  7. 并发容器学习—ConcurrentSkipListMap与ConcurrentSkipListSet 原

    一.ConcurrentSkipListMap并发容器 1.ConcurrentSkipListMap的底层数据结构     要学习ConcurrentSkipListMap,首先要知道什么是跳表或跳 ...

  8. 整理CSS选择符

    1.星号选择器 ;; } 星号选择器将匹配页面里的每一个元素.很多开发者使用这个技巧将外边距和内边距重置为零.虽然在快速测试时这确实很好用,但我建议你永远不要再生产代码中使用它.它给浏览器带来大量不必 ...

  9. Linux常用硬盘分区工具简介

    1.fdisk 查看当前硬盘分区: [root@yqtrack-zabbix /]# fdisk -l 2.cfdisk 查看当前硬盘分区: 3.sfdisk 查看当前分区: 4.parted 查看当 ...

  10. Java中String连接性能的分析【转】

    [转]http://www.blogjava.net/javagrass/archive/2010/01/24/310650.html 总结:如果String的数量小于4(不含4),使用String. ...