Hash是把锋利的刀子,处理海量数据时经常用到,大家可能经常用hash,但hash的有些特点你是否想过、理解过。我们可以利用我们掌握的概率和期望的知识,来分析Hash中一些有趣的问题,比如:

  • 平均每个桶上的项的个数
  • 平均查找次数
  • 平均冲突次数
  • 平均空桶个数
  • 使每个桶都至少有一个项的项个数的期望

  本文hash的采用链地址法发处理冲突,即对hash值相同的不同对象添加到hash桶的链表上。

每个桶上的项的期望个数

  将n个不同的项hash到大小为k的hash表中,平均每个桶会有多少个项?首先,对于任意一个项items(i)被hash到第1个桶的概率为1/k,那么将n个项都hash完后,第1个桶上的项的个数的期望为C(项的个数)=n/k,这里我们选取了第一个桶方便叙述,事实上对于任一个特定的桶,这个期望值都是适用的。这就是每个桶平均项的个数。

  用程序模拟的过程如下:

     /***
* 对N个字符串hash到大小为K的哈希表中,每个桶上的项的期望个数
*
* @return
*/
private double expectedItemNum() {
// 桶大小为K
int[] bucket = new int[K];
// 生成测试字符串
List<String> strings = getStrings(N);
// hash映射
for (int i = 0; i < strings.size(); i++) {
int h = hash(strings.get(i), 37);
bucket[h]++;
}
// 计算每个桶的平均次数
long sum = 0;
for (int itemNum : bucket)
sum += itemNum;
return 1.0 * sum / K;
} /***
* 多次测试计算每个桶上的项的期望个数,
*/
private static void expectedItemNumTest() {
MyHash myHash = new MyHash();
// 测试100次
int tryNum = 100;
double sum = 0;
for (int i = 0; i < tryNum; i++) {
double count = myHash.expectedItemNum();
sum += count;
}
// 取100次测试的平均值
double fact = sum / tryNum;
System.out.println("K=" + K + " N=" + N);
System.out.println("程序模拟的期望个数:" + fact);
double expected = N * 1.0 / K;
System.out.println("估计的期望个数 n/k:" + expected);
}

  输出的结果如下,可以看到我们用公式计算的期望与实际是很接近的,这也说明我们的期望公式计算正确了,毕竟实践是检验真理的唯一标准。

K=1000 N=618
程序模拟的期望个数:0.6180000000000007
估计的期望个数 n/k:0.618

空桶的期望个数

  将n个不同的项hash到大小为k的hash表中,平均会有多少个空桶?我们还是以第1个桶为例,任意一个项item(i)没有hash到第一个桶的概率为(1-1/k),hash完n个项后,所有的项都没有hash到第一个桶的概率为(1-1/k)^n,这也是每个桶为空的概率。桶的个数为k,因此期望的空桶个数就是C(空桶的个数)=k(1-1/k)^n,这个公式不好计算,用程序跑还可能被归零了,转化一下就容易计算了:\begin{equation} C(空桶的个数)=k(1-\frac{1}{k})^n=k(1-\frac{1}{k})^{-k(-\frac{n}{k})}=ke^{(-\frac{n}{k})}\end{equation}  同样我们模拟测试一下:

     /***
* 计算期望的空桶个数
*
* @return
*/
private int expectedEmputyBuckts() {
// 桶大小为K
int[] bucket = new int[K];
// 生成测试字符串
List<String> strings = getStrings(N);
// hash映射
for (int i = 0; i < strings.size(); i++) {
int h = hash(strings.get(i), 37);
bucket[h]++;
}
// 记录空桶的个数
int count = 0;
for (int itemNum : bucket)
if (itemNum == 0)
count++;
return count;
} /***
* 多次测试求空桶的期望个数
*/
private static void expectedEmputyBucktsTest() {
MyHash myHash = new MyHash();
// 测试100次
int tryNum = 100;
long sum = 0;
for (int i = 0; i < tryNum; i++) {
int count = myHash.expectedEmputyBuckts();
sum += count;
}
// 取100次测试的平均值
double fact = sum / tryNum;
System.out.println("K=" + K + " N=" + N);
System.out.println("程序模拟的期望空桶个数:" + fact);
double expected = K * Math.exp(-1.0 * N / K);
System.out.println("估计的期望空桶个数ke^(-n/k):" + expected);
}

 输出结果:

K=1000 N=618
程序模拟的期望空桶个数:539.0
估计的期望空桶个数ke^(-n/k):539.021403076357

冲突次数期望

  我们这里的n个项是各不相同的,只要某个项hash到的桶已经被其他项hash过,那就认为是一次冲突,直接计算冲突次数不好计算,但我们知道C(冲突次数)=n-C(被占用的桶的个数),而被占用的桶的个数C(被占用的桶的个数)=k-C(空桶的个数),因此我们的得到:\begin{equation} C(冲突次数)=n-(k-ke^{-n/k}) \end{equation}  程序模拟如下:

     /***
* 期望冲突次数
*
* @return
*/
private int expextedCollisions() {
// 桶大小为K
int[] bucket = new int[K];
int count = 0;
// 生成测试字符串
List<String> strings = getStrings(N);
for (int i = 0; i < strings.size(); i++) {
// hash映射
int h = hash(strings.get(i), 37);
// 桶h没有被占用
if (bucket[h] == 0)
bucket[h] = 1;
// 桶h已经被占用,发生了冲突
else
count++;
}
return count;
} private static void expextedCollisionsTest() {
MyHash myHash = new MyHash();
// 测试100次
int tryNum = 100;
long sum = 0;
for (int i = 0; i < tryNum; i++) {
int count = myHash.expextedCollisions();
sum += count;
}
// 取100次测试的平均值
double fact = sum / tryNum;
System.out.println("K=" + K + " N=" + N);
System.out.println("程序模拟的冲突数:" + fact);
double expected = N - (K - K * Math.exp(-1.0 * N / K));
System.out.println("估计的期望冲突次数n-(k-ke^(-n/k)):" + expected); }

 输出结果:

K=1000 N=618
程序模拟的冲突数:157.89
估计的期望冲突次数n-(k-ke^(-n/k)):157.02140307635705

不发生冲突的概率

  将n个项hash完后,一次冲突也没有发生的概率,首先对第一个被hash的项item(1),item(1)可以hash到任意桶中,但一旦item(1)固定后,第二个项item(2)就只能hash到除item(1)所在位置的其他k-1个位置上了,依次类推,可以知道$$P(不发生冲突的概率)=\frac{k}{k}\times\frac{k-1}{k}\times\frac{k-1}{k}\times\frac{k-2}{k}\times\cdot\cdot\cdot\times\frac{k-(n-1)}{k}$$ 这个概率也是不好计算,但当k比较大、n比较小时,有$$P(不发生冲突的概率)=e^{\frac{-n(n-1)}{2k}}$$  模拟过程:

     /***
* hash N个字符串的过程是否产生冲突
*
* @return
*/
private boolean containsCollison() {
// 桶大小为K
int[] bucket = new int[K];
// 生成测试字符串
List<String> strings = getStrings(N);
for (int i = 0; i < strings.size(); i++) {
// hash映射
int h = hash(strings.get(i), 37);
// 桶h没有被占用
if (bucket[h] == 0)
bucket[h] = 1;
// 桶h已经被占用,发生了冲突,直接返回
else
return true;
}
return false;
} /***
* 重复调用多次containsCollison,计算不发生冲突的概率
*/
private static void probCollisionTest() {
MyHash myHash = new MyHash();
// 测试100次
int tryNum = 100;
// 不冲突的次数
int count = 0;
for (int i = 0; i < tryNum; i++) {
if (!myHash.containsCollison())
count++;
}
// 取100次测试的平均值
double fact = 1.0 * count / tryNum;
System.out.println("K=" + K + " N=" + N);
System.out.println("程序模拟的不冲突概率:" + fact);
double expected = Math.exp(-1.0 * N * (N - 1) / (2 * K));
System.out.println("估计的期望不冲突概率e^(-n(n-1)/(2k)):" + expected);
System.out.println("程序模拟的冲突概率:" + (1 - fact));
System.out.println("估计的期望冲突冲突概率1-e^(-n(n-1)/(2k)):" + (1 - expected)); }

 输出结果如下,这个逼近公式只有在k比较大n比较小时误差较小。

K=1000 N=50
程序模拟的不冲突概率:0.29
估计的期望不冲突概率e^(-n(n-1)/(2k)):0.29375770032353277
程序模拟的冲突概率:0.71
估计的期望冲突冲突概率1-e^(-n(n-1)/(2k)):0.7062422996764672

使每个桶都至少有一个项的项个数的期望

  实际使用Hash时,我们一开始并不知道要hash多少个项,如果把桶设置过大,会浪费空间,一般都是设置一个初始大小,当hash的项超过一定数量时,将桶的大小扩大一倍,并将桶内的元素重新hash一遍。查看Java的HashMap源码可以看到,每次调用put添加数据都会检查大小,当n>k*装置因子时,对hashMap进行重建。

 public V put(K key, V value) {
if(...)
return ...;
...
modCount++;
addEntry(hash, key, value, i);
return null;
}
/**
* Adds a new entry with the specified key, value and hash code to
* the specified bucket. It is the responsibility of this
* method to resize the table if appropriate.
*
* Subclass overrides this to alter the behavior of put method.
*/
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
} createEntry(hash, key, value, bucketIndex);
}

  现在我们不是直接当n大于某一个数时对Hash表进行重建,而是预计Hash表的每一个桶都至少有了一个项时,才对hash表进行重建,现在问当n为多少时,每个桶至少有了一个项。要计算这个n的期望,我们先设$X_i$表示从第一次占用$i-1$个桶到第一次占用$i$个桶所插入的项的个数。首先,很容易理解$X_1=1$,对于$X_2$表示从插入第一个元素后,占用两个桶所需要的插入次数,理论上它可以是任意大于1的值,我们一次接一次的插入项,每次插入有两种独立的结果,一个结果是映射到的桶是第一次映射的桶;另一个是映射到的桶是新的桶,当占用了新桶时插入了项的个数即为$X_2$,又因为此时映射到新桶的概率$p=\frac{k-1}{k}$,因此$X_2$的期望$E(X_2)=\frac{1}/{p}=\frac{k}{k-1}$;同样的道理,占用两个桶后,对任意一次hash映射到新桶的概率为$\frac{k-2}{k}$,因此$E(X_2)=\frac{k}{k-2}$。

  现在定义随机变量$X=X_1+X_2+\cdot\cdot\cdot+X_k$,我们可以看出$X$实际上就是每个桶都填上项所需要插入的项的个数。$$E(X)=\sum_{j=1}^k E(X_j)$$$$=\sum_{j=1}^k \frac{k}{k-j+1}$$$$=k\sum_{j=1}^k \frac{1}{k-j+1}$$$$\overset{\underset{\mathrm{令i=k-j+1}}{}}{=}k\sum_{i=1}^k \frac{1}{i}$$  上面这个数是一个有趣的数,叫做调和数(Harmonic_number),这个数(常记做$H_k$)没有极限,但已经有数学家给我们确定了它关于n的一个等价近似值:$$\frac{1}{4}+\ln k\le H_k \le 1+\ln k$$  因此$E(X)=O(k\ln k)$,当项的个数为$k\ln k$时,平均每个桶至少有一个项。

结论总结

  • 每个桶上的项的期望个数:将n个项hash到大小为k的hash表中,平均每个桶的项的个数为${\frac{n}{k}}$
  • 空桶的期望个数:将n个项hash到大小为k的hash表中,平均空桶个数为$ke^{(-\frac{n}{k})}$
  • 冲突次数期望:当我们hash某个项到一个桶上,而这个桶已经有项了,就认为是发生了冲突,将n个项hash到大小为k的hash表中,平均冲突次数为$n-(k-ke^{-n/k})$
  • 不发生冲突的概率:$$P(不发生冲突的概率)=e^{\frac{-n(n-1)}{2k}}$$
  • 调和数:$H_k=\sum_{i=1}^k \frac{1}{i}$称为调和数,$\sum_{i=1}^k \frac{1}{i}=\Theta{logk}$

  本文主要参考自参考文献[1],写这边博客复习了一下组合数学和概率论的知识,对hash理解得更深入了一点,自己设计hash结构时能对性能有所把握。另外还学会了在博客园插入公式,之前都是在MathType敲好再截图。

  希望本文对您有所帮助,欢迎评论交流!

转载请注明出处:http://www.cnblogs.com/fengfenggirl

参考文献:

[1].http://www.math.dartmouth.edu/archive/m19w03/public_html/Section6-5.pdf

[2].http://preshing.com/20110504/hash-collision-probabilities/

Hash中的一些概率计算的更多相关文章

  1. HMM的概率计算问题和预测问题的java实现

    HMM(hidden markov model)可以用于模式识别,李开复老师就是采用了HMM完成了语音识别. 一下的例子来自于<统计学习方法> 一个HMM由初始概率分布,状态转移概率分布, ...

  2. 隐马尔可夫模型HMM(二)概率计算问题

    摘自 1.李航的<统计学习方法> 2.http://www.cnblogs.com/pinard/p/6955871.html 一.概率计算问题 上一篇介绍了概率计算问题是给定了λ(A,B ...

  3. JAVA实现概率计算(数字不同范围按照不同几率产生随机数)

    程序中经常遇到随机送红包之类的情景,这个随机还得指定概率,比如10%的机率可以得到红包.那么java怎么实现一个简单的概率计算了,见如下例子: int randomInt = RandomUtils. ...

  4. 算法笔记_155:算法提高 概率计算(Java)

    目录 1 问题描述 2 解决方案   1 问题描述 问题描述 生成n个∈[a,b]的随机整数,输出它们的和为x的概率. 输入格式 一行输入四个整数依次为n,a,b,x,用空格分隔. 输出格式 输出一行 ...

  5. jquery抽奖插件+概率计算

    写了一个抽奖的jquery插件和计算概率的方法, 结合起来就是一个简单的概率抽奖, 不过实际项目中基本不会把抽奖概率的计算放在前端处理~. demo lottery.jquery.js $.fn.ex ...

  6. 条件随机场(CRF) - 3 - 概率计算问题

    声明: 1,本篇为个人对<2012.李航.统计学习方法.pdf>的学习总结,不得用作商用,欢迎转载,但请注明出处(即:本帖地址). 2,由于本人在学习初始时有很多数学知识都已忘记,所以为了 ...

  7. nyoj 概率计算

    概率计算 时间限制:1000 ms  |  内存限制:65535 KB 难度:1   描述 A和B两个人参加一场答题比赛.比赛的过程大概是A和B两个人轮流答题,A先答.一旦某人没有正确回答问题,则对手 ...

  8. PTA 社交网络图中结点的“重要性”计算(30 分)

    7-12 社交网络图中结点的“重要性”计算(30 分) 在社交网络中,个人或单位(结点)之间通过某些关系(边)联系起来.他们受到这些关系的影响,这种影响可以理解为网络中相互连接的结点之间蔓延的一种相互 ...

  9. Excel中最精确的计算年龄的公式

    身份证算年龄 假设A1是身份证号所在单元格 =IF(MONTH(NOW())<INT(MID(A1,11,2)),INT(YEAR(NOW())-INT(MID(A1,7,4)))-1,IF(M ...

随机推荐

  1. 使用SoapUI做webservice的模拟系统实例

    实现步骤: 1.打开SoapUI工具,导入工程文件 2.右键生成一个模拟服务 3.由于要考虑到不同的输入返回不同的输出,因此需要写脚本对输入报文进行解释,然后针对输入来判断返回 3.1.建立不同的返回 ...

  2. Effective Java 20 Prefer class hierarchies to tagged classes

    Disadvantage of tagged classes 1. Verbose (each instance has unnecessary irrelevant fields). 2. Erro ...

  3. 【mysql】一个关于order by排序的问题

    I have a table CREATE TABLE `tableMain` ( `id` int(11) NOT NULL AUTO_INCREMENT, `value1` varchar(45) ...

  4. ASP.NET 使用Ajax(转)

    之前在Ajax初步理解中介绍了对Ajax的初步理解,本文将介绍在ASP.NET中如何方便使用Ajax,第一种当然是使用jQuery的ajax,功能强大而且操作简单方便,第二种是使用.NET封装好的Sc ...

  5. Java读写文件方法总结

    Java读写文件方法总结 Java的读写文件方法在工作中相信有很多的用处的,本人在之前包括现在都在使用Java的读写文件方法来处理数据方面的输入输出,确实很方便.奈何我的记性实在是叫人着急,很多时候既 ...

  6. [转]shell脚本打印日志方法

    该文章转自:http://blog.csdn.net/wylfengyujiancheng/article/details/50019299 ----------------------------- ...

  7. Safari5及以下版本不支持Date的横杠字符串格式

    Date构造器有多种方式创建方式 一.new Date() // 当天 var date = new Date() 二.new Date(value) // value 为一个整数 var date ...

  8. iTOP-4412开发板低功耗高性能的开源硬件平台——上手评测

    iTOP-4412开发板现在比较热门的开发板,笔者近期入了一套.也推荐给初学ARM的朋友学习,4412开发板搭载三星Exynos四核处理器,配备1GB内存,4GB固态硬盘EMMC存储,兼具快速读取与超 ...

  9. Hive remote install mode (mysql) step by step

    Prerequisite: Hadoop cluster is avalable; Mysql installed on namenode; Step1: download the latest hi ...

  10. Jenkins插件hyper slaves源码分析

    1.public class HyperSlaves extends Plugin implements Describable<HyperSlaves> (1).init():初始化co ...