LogLog基数估计算法学习与实现分析

1 基本概念

1.1 基数

基数指一个集合中不同元素的个数。例如集合{A,B,C,B,C}共5个元素,但只有3个不重复的元素。所以基数为3。

1.2 基数估计算法

基于概率统计理论估计指定集合基数的算法。这种类型的算法降低了存储空间的使用,会带来统计误差。但可以通过一定方法将误差控制在一定范围内

1.3 伯努利试验

一次实验的结果只有发生和不发生两种,重复做这个实验,直到结果发生为止,记录下实验的次数。

2 使用场景

使用较少的存储空间估计不同元素数量。例如,每日某个网页的链接被不同用户的点击的总量。

3 LogLog估计算法

LogLog代表了算法的空间复杂度,算法的空间复杂度为O(log log Nmax),通过kb级的存储空间估计亿级基数。

算法通过哈希函数计算出所有元素的哈希值,然后通过这些哈希值进行基数估计。哈希函数需要满足如下条件:

  • 均匀性,哈希值要尽可能服从均匀分布。
  • 哈希值定长,所有结果计算出的哈希值的长度应该是一样长。
  • 哈希冲突可忽略,所有结果计算出的哈希值碰撞概率非常低可忽略不计。

估计的步骤简述如下

  1. 计算集合中一个元素的哈希值,可转换得到一个二进制比特串。
  2. 由选取哈希函数的特性知,对集合中所有元素计算出的哈希值服从均匀分布,因此随机抽取的一个元素,得到的比特串每个位置上0或1的概率为1/2,且相互独立。
  3. 令p(a)为元素a对应的比特传中第一个出现1的位置。遍历所有元素的比特串,并得到最大max(p(a)),那么2^max(p(a))则是所有不同元素的一个粗糙估计。

2^max(p(a)): 因为所选取哈希函数的特性,所有元素的比特串服从均匀分布。而每个比特串中0/1的出现均是独立的。找第一个1出现的过程可以看作是一次伯努利实验。而每个0/1出现的概率为1/2。当最高位出现1的位置为pn,则这个比特串出现的概率为1/2^pn。 因此总数估计为 2^pn。即候选元素又pn个,那么会有一个元素的比特串其1第一次出现的位置为pn。

简化考虑,假设有9个元素二进制串,分布均匀,人为让这些串每个元素均会出现一次或多次。这组串中最高位出现1的位置为3,那么可以得到一个基数的粗略估计为8。

若没有人为干预,集合元素数量较少,那么偏差会较大。当集合中元素非常大时,而哈希函数生成的结果又是几乎均匀的。那么根据上面概率统计的理论分析也可以想到粗略估计结果是2^max(p(a))

011
010
001
010
011
100
101
110
111

从上面的简单例子看,如果元素数量不够,那么在二进制序列可能在地址空间上分布存在偶然性。因此为了减小误差,通过分桶平均的方式来进行改进。

分桶平均: 为了避免偶然性,将这些元素进行分桶。

  1. 每个元素分配到一个桶中。随后计算出每个桶中的元素二进制位上1出现的最大位置。
  2. 随后对所有桶中的最大位置求平均,最终根据这个平均值来进行基数估计。

4 实现分析

根据上述的算法执行过程得出如下实现步骤:

  1. 生成随机元素, 进行估计。通过generateWords函数实现。
  2. 为了验证准确率,通过一个k/v结构对元素进行统计,最终与loglog算法的结果比较,并给出loglog的准确率。这步通过cardinality函数实现。
  3. loglog算法实现,通过LogLog函数实现。该函数通过哈希函数hash算出每个元素的哈希值。随后通过scan1得到二进制串中第一个1出现的位置。接着取元素的前5位作为桶的编号,算出每个元素所在的桶,并通过后27位计算每个元素第一个1所在的位置的最大值。并将每个桶中最大1的位置存放于M数组中。最后对M求平均值得到集合基数的估计。
function generateWords(count) {
    var result = [];

    while (count > 0) {
        var word = '';
        for (var j = 0; j < (parseInt(Math.random() * (8 - 1)) + 1); j++) { // from 1char to 8chars
            word += String.fromCharCode(parseInt(Math.random() * (122 - 97)) + 97); // a-z
        }

        for (var i = 0; i < Math.random() * 100; i++) {
            result.push(word);
            count--;
        }
    }

    return result;
}

function cardinality(arr) {
    var t = {}, r = 0;
    for (var i = 0, l = arr.length; i < l; i++) {
        if (!t.hasOwnProperty(arr[i])) {
            t[arr[i]] = 1;
            r++;
        }
    }
    return r;
}

function LogLog(arr) {
    var HASH_LENGTH = 32, // bites
        HASH_K = 5; // HASH_LENGTH = 2 ^ HASH_K

    /**
     * Jenkins hash function
     *
     * @url http://en.wikipedia.org/wiki/Jenkins_hash_function
     *
     * @param {String} str
     * @return {Number} Hash
     */
    function hash(str) {
        var hash = 0;

        for (var i = 0, l = str.length; i < l; i++) {
            hash += str.charCodeAt(i);
            hash += hash << 10;
            hash ^= hash >> 6;
        }

        hash += hash << 3;
        hash ^= hash >> 6;
        hash += hash << 16;

        return hash;
    }

    /**
     * Offset of first 1-bit
     *
     * @example 00010 => 4
     *
     * @param {Number} bites
     * @return {Number}
     */
    function scan1(bites) {
        if (bites == 0) {
            return HASH_LENGTH - HASH_K;
        }
        var offset = parseInt(Math.log(bites) / Math.log(2));
        offset = HASH_LENGTH - HASH_K - offset;
        return offset;
    }

    /**
     * @param {String} $bites
     * @param {Number} $start >=1
     * @param {Number} $end   <= HASH_LENGTH
     *
     * @return {Number} slice of $bites
     */
    function getBites(bites, start, end) {
        var r = bites >> (HASH_LENGTH - end);
        r = r & (Math.pow(2, end - start + 1) - 1);

        return r;
    }

    var M = [];
    for (i = 0, l = arr.length; i < l; i++) {
        var h = hash(arr[i]),
            j = getBites(h, 1, HASH_K) + 1,
            k = getBites(h, HASH_K + 1, HASH_LENGTH);
        k = scan1(k);

        if (typeof M[j] == 'undefined' || M[j] < k) {
            M[j] = k;
        }
    }

    var alpha = 0.77308249784697296; // (Gamma(-1/32) * (2^(-1/32) - 1) / ln2)^(-32)

    var E = 0;
    for (var i = 1; i <= HASH_LENGTH; i++) {
        if (typeof M[i] != 'undefined') {
            E += M[i];
        }
    }
    E /= HASH_LENGTH;
    E = alpha * HASH_LENGTH * Math.pow(2, E);

    return parseInt(E);
}

var words = generateWords(1000000);
console.log("Number of words");
console.log(words.length);

console.log("------\nPrecision")

var s = (new Date()).getTime();
console.log(cardinality(words));
console.log('time:', (new Date()).getTime() - s + 'ms');

console.log("------\nLogLog");

var s = (new Date()).getTime();
console.log(LogLog(words));
console.log('time:', (new Date()).getTime() - s + 'ms');


5 参考

[1].loglog算法概述,https://www.jianshu.com/p/e7d9a4b630b4
[2].loglog算法分析论文,http://algo.inria.fr/flajolet/Publications/DuFl03-LNCS.pdf
[3].基数估计,https://blog.longyb.com/2018/10/12/cardinality_estimation/
[4].loglog js实现,https://github.com/buryat/loglog/blob/master/loglog.js

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值