哈希算法教程
一、哈希函数
哈希函数的实质,就是把一个键值空间为
哈希函数的设计有两个主要的要去:
(1)哈希函数计算要快,效率要高,即哈希的计算公式要比较简单,不要太复杂。
(2)哈希函数的冲突率要低。
哈希冲突指的是两个不同键值
这里我们定义一个负载因子(装填因子):
为了避免冲突,有三种解决方案:
1.把哈希表的大小开的足够大,比键值空间
2.采用拉链法.
3.选择一个完美哈希函数,是一一对应的。
补充了解:哈希冲突有
(1)开放定址法
(2)再哈希法(多个哈希函数,嵌套使用)
(3)链地址法(即上面说的拉链法,竞赛主要用这种)
(4)建立公共溢出区
回到之前说的
方法1:设哈希表中实际被填满的槽为
之间比较好。
方法2:拉链法(链式前向星),如有冲突,直接连接即可
方法3:所谓的完美一一对应的哈希函数,其实主要是由目标数据和可用内存空间两个一起决定的
举个简单例子,
二、哈希表的大小为何是素数(通俗版)
1.选取数列
数列的选取很重要,之前看到有些文章将验证的数列间隔选为1,发现素数与合数并没有什么区别,这是因为素数与合数最大的区别不是间隔为
素数只有两个因子,
合数至少有
素数与合数有公因子
看到这里,我们可以做这样的一个假设,取模运算的冲突与因子相关,但是具体是如何相关呢,我们后面来实际验证一下,首先验证一下与因子相关;
数列1(因子2):
1,3,5,7,9,11,13,15,17
数列2(因子2):
2,4,6,8,10,12,14,16,18
数列3(因子3):
1,4,7,10,13,16,19,22,25,28
数列4(因子6):
1,7,13,19,25,31,37,43,49
数列5(因子7):
1,8,15,22,29,36,43,50,57
上面我们根据6,7的因子,取了5个数列,数列1与数列2取因子2,分别用奇偶数表示,用来验证因子与取模运算是否是相关的;
然后再取后面的3个数列,验证另一个假设,数列的分布以因子为间隔。
2.检验
数列1(1,3,5,7,9,11,13,15,17)=> 取模6
余数 | 0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|---|
哈希表 | 1 | 3 | 5 | |||
哈希表 | 7 | 9 | 11 | |||
哈希表 | 13 | 15 | 17 |
2是6的因子,数列1产生了冲突
上面我们通过取模运算,发现如果待存数据如果是以2为间隔的话,那么取模6就会有很多冲突,分布不均匀,0,2,4都没有存储,1,3,5冲突很多;
数列1(1,3,5,7,9,11,13,15,17)=> 取模7
余数 | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|
哈希表 | 7 | 1 | 9 | 3 | 11 | 5 | 13 |
哈希表 | 15 | 17 | 19 |
2不是7的因子,数列1分布均匀
然后通过将模取为7,发现同样的数列1,这时候冲突减少,并且分布均匀,因为我们取的是奇数列,再使用偶数列验证是否是这样;
数列2(2,4,6,8,10,12,14,16,18)=> 取模6
余数 | 0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|---|
哈希表 | 6 | 2 | 4 | |||
哈希表 | 12 | 8 | 10 | |||
哈希表 | 18 | 14 | 16 |
2为6的因子,分布不均匀
数列2(2,4,6,8,10,12,14,16,18)=> 取模7
余数 | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|
哈希表 | 14 | 8 | 2 | 10 | 4 | 12 | 6 |
哈希表 | 16 | 18 |
2不是7的因子,分布均匀
通过上面的取模运算,我们发现,因子确实会影响数列的冲突,并且冲突的间隔就是因子大小,下面再通过其他数列,看一下是否是这样;
数列3(1,4,7,10,13,16,19,22,25,28)=> 取模6
余数 | 0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|---|
哈希表 | 1 | 4 | ||||
哈希表 | 7 | 10 | ||||
哈希表 | 13 | 16 | ||||
哈希表 | 19 | 22 | ||||
哈希表 | 25 | 28 |
3是6的因子,分布不均匀
数列3(1,4,7,10,13,16,19,22,25,28)=> 取模7
余数 | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|
哈希表 | 7 | 1 | 16 | 10 | 4 | 19 | 13 |
哈希表 | 28 | 22 | 25 |
3不是7的因子,分布均匀
数列4(1,7,13,19,25,31,37,43,49)=> 取模6
余数 | 0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|---|
哈希表 | 1 | |||||
哈希表 | 7 | |||||
哈希表 | 13 | |||||
哈希表 | 19 | |||||
哈希表 | 25 | |||||
哈希表 | 31 | |||||
哈希表 | 37 | |||||
哈希表 | 43 | |||||
哈希表 | 49 |
6是6的因子,分布不均匀,分部间隔为6
数列4(1,7,13,19,25,31,37,43,49)=> 取模7
余数 | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|
哈希表 | 7 | 1 | 37 | 31 | 25 | 19 | 13 |
哈希表 | 49 | 43 |
6不是7的因子,分布均匀
数列5( 1,8,15,22,29,36,43,50,57)=> 取模6
余数 | 0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|---|
哈希表 | 36 | 1 | 8 | 15 | 22 | 29 |
哈希表 | 43 | 50 | 57 |
7不是6的因子,分布均匀
数列5( 1,8,15,22,29,36,43,50,57)=> 取模7
余数 | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|
哈希表 | 1 | ||||||
哈希表 | 8 | ||||||
哈希表 | 15 | ||||||
哈希表 | 22 | ||||||
哈希表 | 29 | ||||||
哈希表 | 36 | ||||||
哈希表 | 43 | ||||||
哈希表 | 50 | ||||||
哈希表 | 57 |
7是7的因子,分布不均匀,分布间隔为7
3.结论
根据上面的结果,我们来分析一般性结论:
哈希表中的分布按照数列的间隔进行分隔,如果数列的间隔恰好整除模,也就是模的因子,那么就会哈希表的分布就会产生间隔,恰好是数列的间隔。
由此得到下面的结论:
如果有一个数列
如果一个数列
数列的冲突分布间隔为因子大小,同样的随机数列,因子越多,冲突的可能性就越大
通过上面的分析,现在就很明确了,如果给我们随机的数列放到哈希表中,如何保障它能尽量减少冲突呢,就需要模的因子最少,而因子最少的就是素数了,这就是为什么哈希表取模为素数的原因了。
三、哈希表的大小为何是素数(数学证明)
假设需要放入长为
也就是我们算出来的
四、链式前向星实现拉链法的一个模板
struct hash_map { // 哈希表模板
struct data {
long long u;
int v, nex;
}; // 前向星结构
data e[SZ << 1]; // SZ 是 const int 表示大小, SZ为质数
int h[SZ], cnt;
int hash(long long u) { return u % SZ; } //哈希函数
int& operator[](long long u) {
int hu = hash(u); // 获取头指针
for (int i = h[hu]; i; i = e[i].nex)
if (e[i].u == u) return e[i].v;
return e[++cnt] = (data){u, -1, h[hu]}, h[hu] = cnt, e[cnt].v;
}
hash_map() {
cnt = 0;
memset(h, 0, sizeof(h));
}
};