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. Java静态同步方法和非静态同步方法

             所有的非静态同步方法用的都是同一把锁——该实例对象本身.也就是说如果一个实例对象的非静态同步方法获取锁后,该实例对象的其他非静态同步方法必须等待获取锁的方法释放锁后才能获取锁进而执行 ...

  2. Android 实用代码片段

    一些不常见确又很实用的代码块. 1.精确获取屏幕尺寸(例如:3.5.4.0.5.0寸屏幕) public static double getScreenPhysicalSize(Activity ct ...

  3. Effective Java 76 Write readObject methods defensively

    Principle readObject method is effectively another public constructor, and it demands all of the sam ...

  4. Windows 2003 Server C盘空间被IIS日志文件消耗殆尽案例

    今天突然收到手头一台数据库服务器的磁盘空间告警邮件,C盘空间只剩下5.41GB大小(当系统磁盘剩余空间小于总大小的10%时,发出告警邮件),如下图所示: 由于还有一些微弱印象:前阵子这台服务器的C盘剩 ...

  5. JVM相关问答

    大部分内容来源网络,整理一下,留个底. 问:堆和栈有什么区别? 答:堆是存放对象的,但是对象内的临时变量是存在栈内存中,如例子中的methodVar是在运行期存放到栈中的. 栈是跟随线程的,有线程就有 ...

  6. TestNG之参数化

    TestNG提供了两种参数化的方式,一种是通过XML,一种是通过代码实现,下面对这两种方式做介绍. 一.通过xml /** * <suite name="Suite" par ...

  7. selenium循环点击文本框

    1.可以用xpath循环点击checkbox List<WebElement> list = dr.findElements(By.className("datagrid-row ...

  8. 多次访问节点的DFS POJ 3411 Paid Roads

    POJ 3411 Paid Roads Time Limit: 1000MS   Memory Limit: 65536K Total Submissions: 6553   Accepted: 24 ...

  9. ZBrush中如何才能快速完成脸部雕刻(下)

      骨骼,是一门基础艺术,几百年来一直为伟大的艺术大师所研究,它曾经,也将一直是创作现实且可信角色的关键,提高骨骼知识更将大大提高雕刻技能. 查看更多内容请直接前往:http://www.zbrush ...

  10. hdu 5862 Counting Intersections

    传送门:hdu 5862 Counting Intersections 题意:对于平行于坐标轴的n条线段,求两两相交的线段对有多少个,包括十,T型 官方题解:由于数据限制,只有竖向与横向的线段才会产生 ...