HyperLogLog 使用及其算法原理详细讲解

3.2 估值优化

关于上述估值偏差较大的问题,可以采用如下方式结合来缩小误差:

  1. 增加测试的轮数,取平均值。假设三次伯努利试验为1轮测试,我们取出这一轮试验中最大的的kmax作为本轮测试的数据,同时我们将测试的轮数定位100轮,这样我们在100轮实验中,将会得到100个kmax,此时平均数就是(k_max_1 + … + k_max_m)/m,这里m为试验的轮数,此处为100.

  2. 增加修正因子,修正因子是一个不固定的值,会根据实际情况来进行值的调整。

上述这种增加试验轮数,去kmax的平均值的方法,是LogLog算法的实现。因此LogLog它的估算公式如下:

690927-20190617140341209-107676860.png

HyperLogLog与LogLog的区别在于HyperLogLog使用的是调和平均数,并非平均数。调和平均数指的是倒数的平均数(调和平均数)。调和平均数相比平均数能降低最大值对平均值的影响,这个就好比我和马爸爸两个人一起算平均工资,如果用平均值这么一下来我也是年薪数十亿,这样肯定是不合理的。

使用平均数和调和平均数计算方式如下:

假设我的工资20000,马云1000000000

使用平均数的计算方式:(20000 + 1000000000) / 2 = 500010000

调和平均数的计算方式:2/(1/20000 + 1/1000000000) ≈ 40000

很明显,平均工资月薪40000更加符合实际平均值,5个亿不现实。

调和平均数的基本计算公式如下:

3.3 HyperLogLog的实现

根据3.1和3.2大致可以知道HyperLogLog的实现原理了,它的主要精髓在于通过记录下低位连续零位的最大长度K(也就是上面我们说的kmax),来估算随机数的数量n。

HyperLogLog.png

任何值在计算机中我们都可以将其转换为比特串,也就是0和1组成的bit数组,我们从这个bit串的低位开始计算,直到出现第一个1为止,这就好比上面的伯努利试验抛硬币,一直抛硬币直到出现第一个正面为止(只是这里是数字0和1,伯努利试验中使用的硬币的正与反,并没有区别)。而HyperLogLog估算的随机数的数量,比如我们统计的UV,就好比伯努利试验中试验的次数。

综上所述,HyperLogLog的实现主要分为三步:

第一步:转为比特串

通过hash函数,将输入的数据装换为比特串,比特串中的0和1可以类比为硬币的正与反,这是实现估值统计的第一步

第二步:分桶

分桶就是上面3.2估值优化中的分多轮,这样做的的好处可以使估值更加准确。在计算机中,分桶通过一个单位是bit,长度为L的大数组S,将数组S平均分为m组,m的值就是多少轮,每组所占有的比特个数是相同的,设为 P。得出如下关系:

  • L = S.length

  • L = m * p

  • 数组S的内存 = L / 8 / 1024 (KB)

在HyperLogLog中,我们都知道它需要12KB的内存来做基数统计,原因就是HyperLogLog中m=16834,p=6,L=16834 * 6,因此内存为=16834 * 6 / 8 / 1024 = 12 (KB),这里为何是6位来存储kmax,因为6位可以存储的最大值为64,现在计算机都是64位或32位操作系统,因此6位最节省内存,又能满足需求。

HyperLogLog分桶.png

第三步:桶分配

最后就是不同的数据该如何分配桶,我们通过计算hash的方式得到比特串,只要hash函数足够好,就很难产生hash碰撞,我们假设不同的数值计算得到不同的hash值,相同的数值得到相同的hash值(这也是HyperLogLog能用来统计UV的一个关键点),此时我们需要计算值应该放到那个桶中,可以计算的方式很多,比如取值的低16位作为桶索引值,或者采用值取模的方式等等。

3.4 代码实现-BernoulliExperiment(伯努利试验)

首先来写一个3.1中伯努利试验n=2^kmax的估算值验证,这个估算值相对偏差会比较大,在试验轮次增加时估算值的偏差会有一定幅度的减小,其代码示例如下:

package com.lizba.pf;

import java.util.concurrent.ThreadLocalRandom;

/**

*      伯努利试验 中基数n与kmax之间的关系  n = 2^kmax

* @Author: Liziba

* @Date: 2021/8/17 23:16

*/

public class BernoulliExperimentTest {

static class BitKeeper {

/** 记录最大的低位0的长度 */

private int kmax;

public void random() {

// 生成随机数

long value = ThreadLocalRandom.current().nextLong(2L << 32);

int len = this.lowZerosMaxLength(value);

if (len > kmax) {

kmax = len;

}

}

/**

* 计算低位0的长度

* 这里如果不理解看下我的注释

* value >> i 表示将value右移i,  1<= i <32 , 低位会被移出

* value << i 表示将value左移i,  1<= i <32 , 低位补0

* 看似一左一右相互抵消,但是如果value低位是0右移被移出后,左移又补回来,这样是不会变的,但是如果移除的是1,补回的是0,那么value的值就会发生改变

* 综合上面的方法,就能比较巧妙的计算低位0的最大长度

* @param value

* @return

*/

private int lowZerosMaxLength(long value) {

int i = 1;

for (; i < 32; i++) {

if (value >> i << i != value) {

break;

}

}

return i - 1;

}

}

static class Experiment {

/** 测试次数n */

private int n;

private BitKeeper bitKeeper;

public Experiment(int n) {

this.n = n;

this.bitKeeper = new BitKeeper();

}

public void work() {

for(int i = 0; i < n; i++) {

this.bitKeeper.random();

}

}

/**

* 输出每一轮测试次数n

* 输出 logn / log2 = k 得 2^k = n,这里的k即我们估计的kmax

* 输出 kmax,低位最大0位长度值

*/

public void debug() {

System.out.printf(“%d %.2f %d\n”, this.n, Math.log(this.n) / Math.log(2), this.bitKeeper.kmax);

}

}

public static void main(String[] args) {

for (int i = 0; i < 100000; i++) {

Experiment experiment = new Experiment(i);

experiment.work();

experiment.debug();

}

}

}

我们可以通过修改main函数中,测试的轮次,再根据输出的结果来观察,n=2^kmax这样的结果还是比较吻合的。

image.png

3.5 代码实现-HyperLogLog

接下来根据HyperLogLog中采用调和平均数+分桶的方式来做代码优化,模拟简单版本的HyperLogLog算法的实现,其代码如下:

package com.lizba.pf;

import java.util.concurrent.ThreadLocalRandom;

/**

*      HyperLogLog 简单实现

* @Author: Liziba

* @Date: 2021/8/18 10:40

*/

public class HyperLogLogTest {

static class BitKeeper {

/** 记录最大的低位0的长度 */

private int kmax;

/**

* 计算低位0的长度,并且保存最大值kmax

* @param value

*/

public void random(long value) {

int len = this.lowZerosMaxLength(value);

if (len > kmax) {

kmax = len;

}

}

/**

* 计算低位0的长度

* 这里如果不理解看下我的注释

* value >> i 表示将value右移i,  1<= i <32 , 低位会被移出

* value << i 表示将value左移i,  1<= i <32 , 低位补0

* 看似一左一右相互抵消,但是如果value低位是0右移被移出后,左移又补回来,这样是不会变的,但是如果移除的是1,补回的是0,那么value的值就会发生改变

* 综合上面的方法,就能比较巧妙的计算低位0的最大长度

* @param value

* @return

*/

private int lowZerosMaxLength(long value) {

int i = 1;

for (; i < 32; i++) {

if (value >> i << i != value) {

break;

}

}

return i - 1;

}

}

static class Experiment {

private int n;

private int k;

/** 分桶,默认1024,HyperLogLog中是16384个桶,并不适合我这里粗糙的算法 */

private BitKeeper[] keepers;

public Experiment(int n) {

this(n, 1024);

}

public Experiment(int n, int k) {

this.n = n;

this.k = k;

this.keepers = new BitKeeper[k];

for (int i = 0; i < k; i++) {

this.keepers[i] = new BitKeeper();

}

}

/**

* (int) (((m & 0xfff0000) >> 16) % keepers.length) -> 计算当前m在keepers数组中的索引下标

* 0xfff0000 是一个二进制低16位全为0的16进制数,它的二进制数为 -> 1111111111110000000000000000

* m & 0xfff0000 可以保理m高16位, (m & 0xfff0000) >> 16 然后右移16位,这样可以去除低16位,使用高16位代替高16位

* ((m & 0xfff0000) >> 16) % keepers.length 最后取模keepers.length,就可以得到m在keepers数组中的索引

*/

public void work() {

for (int i = 0; i < this.n; i++) {

long m = ThreadLocalRandom.current().nextLong(1L << 32);

BitKeeper keeper = keepers[(int) (((m & 0xfff0000) >> 16) % keepers.length)];

keeper.random(m);

}

}

/**

* 估算 ,求倒数的平均数,调和平均数

* @return

*/

public double estimate() {

double sumBitsInverse = 0.0;

// 求调和平均数

for (BitKeeper keeper : keepers) {

sumBitsInverse += 1.0 / (float) keeper.kmax;

}

double avgBits = (float) keepers.length / sumBitsInverse;

return Math.pow(2, avgBits) * this.k;

}

}

/**

* 测试

* @param args

*/

public static void main(String[] args) {

for (int i = 100000; i < 1000000; i+=100000) {

Experiment experiment = new Experiment(i);

experiment.work();

double estimate = experiment.estimate();

// i 测试数据

// estimate 估算数据

// Math.abs(estimate - i) / i 偏差百分比

System.out.printf(“%d %.2f %.2f\n”, i, estimate, Math.abs(estimate - i) / i);

}

}

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Java)

最后

金三银四到了,送上一个小福利!

image.png

image.png

专题+大厂.jpg
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
中…(img-wgqrdsP9-1713856627174)]

[外链图片转存中…(img-g5coZS8N-1713856627174)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Java)

[外链图片转存中…(img-8oSnYlWG-1713856627174)]

最后

金三银四到了,送上一个小福利!

[外链图片转存中…(img-tbjGErGU-1713856627175)]

[外链图片转存中…(img-0iuBD8de-1713856627175)]

[外链图片转存中…(img-TRieGZGv-1713856627176)]
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 25
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值