1 基本概念
1.1 基数
基数指一个集合中不同元素的个数。例如集合{A,B,C,B,C}共5个元素,但只有3个不重复的元素。所以基数为3。
1.2 基数估计算法
基于概率统计理论估计指定集合基数的算法。这种类型的算法降低了存储空间的使用,会带来统计误差。但可以通过一定方法将误差控制在一定范围内
1.3 伯努利试验
一次实验的结果只有发生和不发生两种,重复做这个实验,直到结果发生为止,记录下实验的次数。
2 使用场景
使用较少的存储空间估计不同元素数量。例如,每日某个网页的链接被不同用户的点击的总量。
3 LogLog估计算法
LogLog代表了算法的空间复杂度,算法的空间复杂度为O(log log Nmax),通过kb级的存储空间估计亿级基数。
算法通过哈希函数计算出所有元素的哈希值,然后通过这些哈希值进行基数估计。哈希函数需要满足如下条件:
- 均匀性,哈希值要尽可能服从均匀分布。
- 哈希值定长,所有结果计算出的哈希值的长度应该是一样长。
- 哈希冲突可忽略,所有结果计算出的哈希值碰撞概率非常低可忽略不计。
估计的步骤简述如下:
- 计算集合中一个元素的哈希值,可转换得到一个二进制比特串。
- 由选取哈希函数的特性知,对集合中所有元素计算出的哈希值服从均匀分布,因此随机抽取的一个元素,得到的比特串每个位置上0或1的概率为1/2,且相互独立。
- 令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出现的最大位置。
- 随后对所有桶中的最大位置求平均,最终根据这个平均值来进行基数估计。
4 实现分析
根据上述的算法执行过程得出如下实现步骤:
- 生成随机元素, 进行估计。通过generateWords函数实现。
- 为了验证准确率,通过一个k/v结构对元素进行统计,最终与loglog算法的结果比较,并给出loglog的准确率。这步通过cardinality函数实现。
- 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