【设计】计数(一)基数计数

所谓基数计数就是统计不重复元素的个数,典型的场景比如页面uv数,这类计数的特点是需要去重。

第一种思路——精确计数之set

即存储所有所有不重复元素的值,用一个set数据结构存。比如存储访问某一个页面的全部去重用户id,需要拿计数值时,只需要计算这个set的元素数目即可。

这个set根据场景,可能是内存中一个set数据结果,可能是mysql数据库中的一个表,也可能是redis里的set等等。

这个方案在基数值不大的情况下是可以的,毕竟方案本身足够简单,但是如果基数值很大时,就会有问题,这个set会很大,存储时需要花费很大空间。当然,如果说访问量不大,直接存db,倒也还行。但是如果访问量页很大,需要存入缓存时,比如redis的set,那么这个成本就很高了。所以说这个方案仅适用于基数不大的情况。

 

第二种思路——精确计数之bitmap

将所有不重复元素的值映射到bit位上,比如0101可以代表[0, 2]这个集合(从右向左,有两个1,分别是第0为和第2位,代表0和2)。这种方案,有多少种取值,就需要多少个bit位。举个例子,有1亿种不同取值,需要空间为:100,000,000/8/1000/1000 = 12.5MB空间,相比于第一种方案,已经很节约了。但是,该方案有一个弊端是:所需要空间大小与当前出现存储的值的数量无关,而与不同值的数目上限有关,也就是说,如果有1亿种取值,即便现在只出现了一个值,那么也需要分配12.5MB的空间,这对于稀疏值的场景并不友好。

 

第三种思路——概率计数之HLL

之前介绍的两种计数方案的优点是结果精确,缺点是需要较大的空间。如果某些需求对于结果的精确性要求不高,可以考虑概率计数,这种方案在计数结果上会有一定误差,但是需要较少的空间。具体怎么选,看业务场景。

这里先简单介绍下其背后概率原理。

将每一种出现的值转换为二进制数,定义k(a)为当前值二进制表示中最右侧的“1”出现的位置(最右或者最左都无所谓,一致就行)。举个例子:假设当前值是9,二进制表示为1001,最右侧1的位置时3,那么“5”这个取值的k(a)=3。

显然,每一种取值都能取到一个k(a),记其中最大的k(a)为kmax,那么基数可以按如下公式估计:

n = 2^kmax

为啥呢?

每一种取值的k(a)计算过程可以看成是一次伯努利过程:

假设现在有n个取值,那么相当于进行了n次伯努利过程。

设n次伯努利kmax值为k,注意k是变量,那么我们计算如下概率:

1.n次伯努利都不大于k的概率为:p1= (1-1/2^k)^n

2.n次伯努利至少有一次等于k的概率为:p2 = 1 - (1-1/2^k)^n

(关于2,看到很多博客写的是1 - (1-1/2^(k-1))^n,我推出来的是k,不知道别人为啥推出来是k-1,有了解的欢迎指正)。

关于推导过程不做深入,高中数学题。

有了这俩基本的公式,我们再来看下,假设现在有n的不同的值,并且k(a)的最大值为kmax',我们可以得出如下结论:

1.这n次伯努利实验的k(a)值一定不大于kmax',因为kmax'是目前统计到的最大值了,这是一个结论;

2.这n次伯努利实验的k(a)值一定至少有一个等于kmax',因为这是kmax'确确实实是我们统计到的值,所以一定存在,这是另一个结论;

将kmax'和n套到之前推到的公式里:

p1= (1-1/2^kmax')^n = 100%;

p2 = 1 - (1-1/2^kmax')^n = 100%

但是,假设n << 2^kmax',p2 ≈ 0,所以n << 2^kmax'不成立;假设n >> 2^max',p1 ≈ 0,所以n >> 2^max'不成立。这么说,n只能约等于 2^kmax'。证明了结论。

这个推理比较绕,我看了网上的介绍,似乎不是特别直观,这里加了一些自己的理解。另外这也只是一个大致的直观感受,严格的证明还得看论文。

再看下这个算法的空间复杂度:Space = O(log2(log2N))

为啥呢?

假设有N种可能的不同值,kmax的取值近似是log2N,而存kmax这个值需要的空间是log2kmax(比如kmax = 8,那么存8这个值,需要4个bit,8 = 1000),所以空间复杂度就是log2(log2N)。

这是HLL的基本原理,但是很显然,直接使用2^kmax来估计n是不准确的,但是如果我们多估计几次呢?估计次数越多,结果更精确。所以HLL里使用的是多次估计的一个调和平均值。

如何多次估计呢?

可以将hash空间分为多个组,将hash值的前m位当做组号,剩余的位用来计算k(a),这样每一个取值,就会进行2^m次k(a),然后将所有k(a)的调和平均值作为最终的k(a)。

redis使用

redis使用了16384个桶,12KB空间,0.81%的误差。

示例:

127.0.0.1:6379> pfadd site1_uv 10056
(integer) 1
127.0.0.1:6379> pfadd site1_uv 10057
(integer) 1
127.0.0.1:6379> pfadd site1_uv 10058
(integer) 1
127.0.0.1:6379> pfadd site1_uv 10059
(integer) 1
127.0.0.1:6379> pfcount site1_uv
(integer) 4

pfadd:往集合里加一个元素;

pfcount:返回基数;

pfmerge:格式为pfmerge targetSet sourceSet1 souceSet2....,可以将多个集合合并为一个。

 

参考:

https://blog.youkuaiyun.com/firenet1/article/details/77247649

http://fhanddx.top/index.php/2019/05/12/redisyuanmapouxihyperloglog/

https://mp.weixin.qq.com/s/h2xEbWIQJ8YuZuT6-tfmBQ

https://www.sohu.com/a/285007716_771850

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值