字符串哈希(BKDR_Hash)
BKDR_Hash函数把一个任意长度的字符串映射成一个非负整数,并且其冲突 [ 1 ] ^{[1]} [1]概率几乎为零。
取一固定值 P P P,把字符串看作 P P P进制数,并分配一个大于 0 0 0的数值,代表每种字符一般来说,我们分配的数值都远小于 P P P。例如对于小写字母构成的字符串,可以令 a = 1 , b = 2 , … … , z = 26 a = 1, b = 2,……,z = 26 a=1,b=2,……,z=26。取一固定值 M M M,求出该P进制数对 M M M的余数,作为该字符串的 H a s h Hash Hash值。
一般来说,我们取 P = 131 P = 131 P=131或 P = 13331 P = 13331 P=13331,此时 H a s h Hash Hash值产生冲突 [ 1 ] ^{[1]} [1]的概率极低,只要 H a s h Hash Hash值相同,我们就可以认为原字符串是相等的。通常我们取 M = 2 64 M = 2^{64} M=264,即直接使用 u n s i g n e d l o n g l o n g unsigned\,\,\,long\,\,\, long unsignedlonglong类型存储这个 H a s h Hash Hash值,在计算时不处理算术溢出问题,产生溢出时相当于自动对 2 64 2^{64} 264取模,这样可以避免低效的取模( m o d mod mod)运算。
除了在极特殊构造的数据上,上述 H a s h Hash Hash算法很难产生冲突 [ 1 ] ^{[1]} [1],一般情况下上述 H a s h Hash Hash算法完全可以出现在题目的标准解答中。我们还可以多取一下恰当的 P P P和 M M M的值(例如大质数),多进行几组 H a s h Hash Hash运算,当结果都相同时才认为原字符串相等,就更加难以构造出使这个 H a s h Hash Hash产生错误的数据。
对字符串的各种操作,都可以直接对 P P P进制数进行算术运算反映到 H a s h Hash Hash值上。
[1] : 冲突指的是两个不同字符串通过上述哈希函数计算出来的哈希值几乎不会相等(冲突)。并不是说映射到哈希表上时,键(位置)不会冲突。因为哈希值在映射时还要进行取模运算,故有概率发生哈希冲突。
哈希冲突不可避免
BKDR_HASH的计算过程
- 理解 ( P P P取一个质数)
s t r str str | NULL | a | b | c | d | e | f |
---|---|---|---|---|---|---|---|
H a s h Hash Hash | 0 | ||||||
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
Hash[0] = 0;
Hash[1] = Hash[0] * P + str[1];
Hash[2] = Hash[1] * P + str[2];
Hash[3] = Hash[2] * P + str[3];
…………………………
Hash[6] = Hash[5] * P + str[6];
最终hash[6]存放的就是整个字符串的哈希值。
- 辅助理解 ( P P P取10,看成十进制数)
将字符串“654321”===>十进制
s t r str str | NULL | 6 | 5 | 4 | 3 | 2 | 1 |
---|---|---|---|---|---|---|---|
H a s h Hash Hash | 0 | ||||||
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
Hash[0] = 0;
Hash[1] = Hash[0] * 10 + str[1] = 6
Hash[2] = Hash[1] * 10 + str[2] = 65
Hash[3] = Hash[2] * 10 + str[3] = 654
…………………………
Hash[6] = Hash[5] * 10 + str[6] = 654321
最终hash[6]存放的就是整个字符串的哈希值(前缀和方式计算)。
子串的hash值
子串区间
[
l
,
r
]
子串区间[l, r]
子串区间[l,r]
H
a
s
h
[
r
]
−
H
a
s
h
[
l
−
1
]
∗
p
(
r
−
l
+
1
)
Hash[r] - Hash[l - 1] * p ^{(r - l + 1)}
Hash[r]−Hash[l−1]∗p(r−l+1)
如果我们已知字符串
S
S
S的
H
a
s
h
Hash
Hash值为
H
(
S
)
H(S)
H(S), 那么在
S
S
S后添加一个字符
c
c
c构成的新字符串
S
+
c
S+c
S+c的
H
a
s
h
Hash
Hash值就是
H
(
S
+
c
)
=
(
H
(
S
)
∗
P
+
v
a
l
u
e
[
c
]
)
m
o
d
M
H(S +c)= (H(S)*P + value[c]) \,\,mod\,\,M
H(S+c)=(H(S)∗P+value[c])modM。其中乘
P
P
P就相当于
P
P
P进制下的左移运算,
v
a
l
u
e
[
c
]
value[c]
value[c] 是我们为
c
c
c选定的代表数值。
如果我们已知字符串 S S S的 H a s h Hash Hash值为 H ( S ) H(S) H(S), 字符串 S + T S+T S+T的 H a s h Hash Hash值为 H ( S + T ) H(S+T) H(S+T),那么字符串 T T T的 H a s h Hash Hash值 H ( T ) = ( H ( S + T ) − H ( S ) ∗ p l e n g t h ( T ) ) m o d M H(T)= (H(S + T)- H(S)* p^{length(T)})\,\, mod\,\, M H(T)=(H(S+T)−H(S)∗plength(T))modM。这就相当于通过 P P P进制下在 S S S后边补 0 0 0的方式,把 S S S左移到与 S + T S+T S+T的左端对齐,然后二者相减就得到了 H ( T ) H(T) H(T)。
例如,S = “abc”, c = “d”, T = “xyz”,则:
S S S表示为 P P P进制数: 1 2 3 1\,2\,3 123
H ( S ) = 1 ∗ p 2 + 2 ∗ P + 3 H(S)= 1*p^2+ 2*P+3 H(S)=1∗p2+2∗P+3
H ( S + c ) = 1 ∗ p 3 + 2 ∗ p 2 + 3 ∗ P + 4 = H ( S ) ∗ P + 4 H(S+c)= 1*p^3+2*p^2+3*P+4= H(S)*P+4 H(S+c)=1∗p3+2∗p2+3∗P+4=H(S)∗P+4
S + T S+T S+T表示为 P P P进制数: 1 2 3 24 25 26 1\,2\,3\,24\,25\,26 123242526
H ( S + T ) = 1 ∗ p 5 + 2 ∗ p 4 + 3 ∗ P 3 + 24 ∗ p 2 + 25 ∗ P + 26 H(S +T)= 1*p^5+2*p^4+3*P^3+ 24*p^2+ 25*P+ 26 H(S+T)=1∗p5+2∗p4+3∗P3+24∗p2+25∗P+26
S S S在 P P P进制下左移 l e n g t h ( T ) length(T) length(T) 位: 1 2 3 0 0 0 1\,2\,3\,0\,0\,0 123000
二者相减就是 T T T表示为 P P P进制数: 24 25 26 24 \,25 \,26 242526
H ( T ) = H ( S + T ) − ( 1 ∗ P 2 + 2 ∗ P + 3 ) ∗ P 3 = 24 ∗ P 2 + 25 ∗ P + 26 H(T)=H(S+T)-(1*P^2 +2*P+3)*P^3= 24*P^2 +25*P+26 H(T)=H(S+T)−(1∗P2+2∗P+3)∗P3=24∗P2+25∗P+26
代码实现
散列函数构造方法:BKDR_Hash
冲突解决方法:链式地址法
/*************************************************************************
> File Name: hash_table.c
> Author: Luzelin
************************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct Node { //表结点定义
char *str;
struct Node *next;
} Node;
typedef struct HashTable { //哈希表定义
Node **data;
int size;
} HashTable;
Node *init_node(char *str, Node *head) { //实例化新结点
Node *p = (Node *)malloc(sizeof(Node));
p->str = strdup(str); //strdup()在内部调用了malloc()为变量分配内存,不需要使用返回的字符串时,需要用free()释放相应的内存空间,否则会造成内存泄漏。返回一个指针,指向为复制字符串分配的空间;如果分配空间失败,则返回NULL值。
p->next = head;
return p; //配合接收返回值的对象完成链表头插
}
HashTable *init_hashtable(int n) { //哈希表初始化
HashTable *h = (HashTable *)malloc(sizeof(HashTable));
h->size = n << 1; //将哈希表长度设置为给定上限的两倍防溢出
h->data = (Node **)calloc(h->size, sizeof(Node *)); //实例化Node*类型数组(指针数组)
return h;
}
int BKDRHash(char *str) { //计算字符串的hash值
// 也可以直接把hash定义成unsigned类型
int seed = 31, hash = 0; //31优质乘子(前提必是质数)
for(int i = 0; str[i]; ++i) hash = hash * seed + str[i]; //前缀和方法计算
return hash & 0x7fffffff; //处理负数情况 0x7fffffff-->0 + 31个1
}
int insert(HashTable *h, char *str) { //在哈希表h中插入一个字符串str
int hash = BKDRHash(str); //计算出该字符串的哈希值
int ind = hash % h->size; //用除留余数法确定在哈希表中的位置
h->data[ind] = init_node(str, h->data[ind]); //在目标位置进行头插
return 1;
}
int search(HashTable *h, char *str) { //在哈希表h中进行查找str
int hash = BKDRHash(str); //计算出该字符串的哈希值
int ind = hash % h->size; //用除留余数法确定在哈希表中的位置
Node *p = h->data[ind]; //取出目标位置的头结点
while(p && strcmp(p->str, str)) p = p->next; //寻找
return p != NULL;
}
void clear_node(Node *node) { //释放结点
if(node == NULL) return ;
Node *p = node, *q;
while(p) {
q = p->next;
free(p->str);
free(p);
p = q;
}
}
void clear_hashtable(HashTable *h) { //释放哈希表内存
if(h == NULL) return ;
for(int i = 0; i < h->size; ++i) {
clear_node(h->data[i]);
}
free(h->data);
free(h);
return ;
}
int main() {
int op;
#define max_n 100
char str[max_n + 5] = {0};
HashTable *h = init_hashtable(max_n + 5); //初始化哈希表
while(~scanf("%d%s", &op, str)) { //0 插入 1 查找
switch(op) {
case 0:
printf("insert %s to HashTable\n", str);
insert(h, str);
break;
case 1:
printf("search %s from HashTable result = %d\n", str, search(h, str));
break;
}
}
#undef max_n
clear_hashtable(h);
return 0;
}