哈希表
哈希表的关键是哈希函数,以及如何处理冲突。常见的处理冲突的方法有拉链法和开放寻址法。
1)拉链法

使用数组实现的哈希表如图所示,数组中每个槽对应一条单链表。插入操作的基本原理是:根据哈希函数给出的哈希值,判断数组中该位置是否被占用,如果没有,则插入到此位置,如果被占用,则插入到单链表的头部(头插法)。查询操作的基本原理是:根据哈希函数给出的哈希值,如果数组中该位置没被占用,则代表查询的值不存在,如果该位置被占用,则在位置对应的单链表中顺序查找。
算法题中,哈希表一般只有插入和查找操作。如果要实现删除操作的话,不会直接对数组进行删除,一般是再创建一个布尔数组,对需要删除的元素打个标记(如上图所示)。
2)开放寻址法
只需使用一个一维数组来存储,且数组长度需为一个质数,经验上取为输入数据量的2~3倍。插入操作的基本原理是:根据哈希函数的哈希值,判断数组中该位置是否被占用,如果没有,则插入到此位置,如果被占用,就再看下一个位置是否被占用,直到找到一个没被占用的位置插入。查询操作的基本原理是:根据哈希函数的哈希值,判断数组中该位置是否被占用,如果不被占用,则查询的值不存在,如果被占用,则比较该位置的值与查询到值是否相等,相等则表示查询的值存在,不相等的话继续看下一个位置,并重复前述的判断。
如果碰到要实现删除操作,也是不会直接对数组元素进行删除,而是再创建一个布尔数组,对需要删除的元素在新数组的相应位置打上标记。
如果碰到要手写简易哈希表的情形,推荐使用开放寻址法,代码比较简单,不易出错。代码模板如下:
// 数组初始化,应初始化成一个输入中不可能出现的值,下面用null表示
// N为输入数据量的2~3倍
int[] h = new int[N];
for(int i = 0; i < N; i ++) h[i] = null;
// 如果x在哈希表中,返回x所在数组中的下标;如果x不在哈希表中,返回x应该插入的位置
public int find(int x){
int t = (x % N + N) % N; // 防止x为负数时取余运算的结果出错
while(h[t] != null && h[t] != x){
t ++;
if(t == N) t = 0;
}
return t;
}
字符串哈希
计算出字符串任一子串的哈希值,包括如下内容:
1)求出字符串的哈希值
可将字符串看成一个 P P P进制的数( m o d Q mod \space Q mod Q),经验上 P P P取131或13231, Q Q Q取 2 64 2^{64} 264时出现冲突的概率很低。
例如,对于字符串"abcd",可看作 P P P进制数,转换成十进制数为 ( a ∗ P 3 + b ∗ P 2 + c ∗ P 1 + d ∗ P 0 ) m o d Q (a*P^3+b*P^2+c*P^1+d*P^0)\space mod \space Q (a∗P3+b∗P2+c∗P1+d∗P0) mod Q。
2)求出字符串的前缀哈希
例如对于字符串"abcd",需预处理出下面这些前缀字符串的哈希值:
子串 | 哈希值 |
---|---|
“” | h [ 0 ] = 1 m o d Q h[0] = 1\space mod\space Q h[0]=1 mod Q |
“a” | h [ 1 ] = a ∗ P 0 m o d Q h[1] = a*P^0\space mod\space Q h[1]=a∗P0 mod Q |
“ab” | h [ 2 ] = ( a ∗ P 1 + b ∗ P 0 ) m o d Q h[2]=(a*P^1+b*P^0)\space mod \space Q h[2]=(a∗P1+b∗P0) mod Q |
“abc” | h [ 3 ] = ( a ∗ P 2 + b ∗ P 1 + c ∗ P 0 ) m o d Q h[3]=(a*P^2+b*P^1+c*P^0)\space mod \space Q h[3]=(a∗P2+b∗P1+c∗P0) mod Q |
“abcd” | h [ 4 ] = ( a ∗ P 3 + b ∗ P 2 + c ∗ P 1 + d ∗ P 0 ) m o d Q h[4]=(a*P^3+b*P^2+c*P^1+d*P^0)\space mod \space Q h[4]=(a∗P3+b∗P2+c∗P1+d∗P0) mod Q |
3)对于字符串的任意子串,假设起始索引为 l l l,终止索引为 r r r(包括在内),那么,该字符串子串的哈希值为 h [ r ] − h [ l ] ∗ P r − l + 1 h[r]-h[l]*P^{r-l+1} h[r]−h[l]∗Pr−l+1。
有个小技巧是,取模的数用
2
6
4
2^64
264,这样直接用unsigned long long
存储,溢出的结果就是取模的结果。由于数值很大通常会溢出,而java中的long类型又为有符号整型,这里给出C++代码。代码模板如下:
// 假设字符串用char类型数组str[1...n]来存储与,起始位置为1,长度为n
typedef unsigned long long ULL;
ULL h[N], p[N]; // h[k]存储字符串前k个字母的哈希值,p[k]存储P^k mod 2^64
// 预处理
p[0] = 1;
for(int i = 1; i <= n; i ++){
p[i] = p[i - 1] * P;
h[i] = h[i - 1] * P + str[i];
}
// 计算子串str[l~r]的哈希值
ULL get(int l, int r){
return h[r] - h[l] * p[r - l + 1];
}
上述字符串哈希看起来可能没啥用,但在有些场景下还是可以应用起来以达到优化的目的:
- 在经过预处理后,可以在 O ( 1 ) O(1) O(1)的时间内判断字符串的任意子串是否相等。
- 在一些以
String
作为key
的哈希表中,查询的时候并不是 O ( 1 ) O(1) O(1),而与字符串的长度有关,这一时间复杂度常会被遗漏掉。可以考虑将这些作为key
的字符串转换成对应的哈希值,这样的话可将查询的时间复杂度优化至 O ( 1 ) O(1) O(1)。