无论是折半查找、二叉排序树查找还是B树,性能都依赖于查找中的比较次数。
一种理想情况是不经过任何比较,一次直接定位索要查找的记录,即:若数据结构中存在关键字和K相等,则其必定在f(K)的存储位置上,我们称这个对应关系f为哈希函数。
冲突(Collision):对不同的关键字,可能得到同一哈希地址,即存在key1!=key2,但f(key1)=f(key2)。此时称为冲突或碰撞。
由于在实际应用中,哈希函数都是压缩函数,所以冲突只能尽可能的减少,很难完全避免。
哈希表:根据设定的哈希函数H(key)和处理冲突的方法将一组关键字映像到一个有限的连续地址集合(区间)上,并以关键字在地址集合中的“像”作为记录在表中的位置,这种表称为哈希表,这一映像过程称为哈希造表或散列。所得的存储位置称为哈希地址或散列地址。
常见的哈希函数构造方法:
(1) 直接定址,如H(key) = a*key + b,如统计每个年龄的用户数。
(2) 数字分析,根据数据特点,取某一位或某几位叠加。
(3) 平方取中,取关键字平方后的中间几位(或平方)为哈希地址。
(4) 折叠法,将关键字纷呈几部分,然后每一部分的叠加和作为哈希地址。
(5) 除数余留法,取关键字被某个不大于哈希表长度的m的数p除后的余数,即算模 H(key) = key mod p。p的选择很重要,一般选质数或不包含小于20的质数因子的合数。
(6) 随机数法,利用伪随机特性,使用key作为种子,得到的随机数作为哈希地址。这个真有,比如Hadoop中某个Hash函数。
以上都是书上很陈旧的内容了,更为现代化的Hash函数请移步Wikipedia,List of Hash Functions。
我个人认为几个比较给力的是:BKDR(用于英文单词)、MurmerHash(速度最快的,针对X86优化CPU运算,基本纯位操作),据说Goog开发了CityHash也是借鉴MurmerHash,不知道效果怎么样。
下面说说处理冲突的方法:
(1) 开放定址法,H = (H(key)+d) mode m,其中d是发现冲突后,每次的增量。若d=1,2....则称作线性探测再散列;若d=1, -1, 4, -4....则叫做二次探测再散列;d也可以是随机数,称为随机探测再散列。注意,二次探测再散列只能在m=4j+3时才能使用!
(2) 再哈希法:H = RH(key)
(3) 链地址法:将所有关键字为同一词的记录存储再同一线性链表中,拉链,如下图所示。
(4) 建立公共溢出区,不管哈希地址是什么,一旦发生冲突,都填入溢出表。个人觉得这个也很靠谱。
下面用Java实现了一个简单的哈希表,Hash函数是平方取模,冲突处理是线性探测再散列,使用cap=0.75判断是否需要重哈希,以减少冲突。
代码如下:
import java.util.ArrayList;
import java.util.Arrays;
public class HashTable {
public HashTable() {
// default capbility
cap = 10;
// default cnt
cnt = 0;
// default size
ht = new int[cap];
Arrays.fill(ht, Integer.MAX_VALUE);
// default load factor
lf = 0.75;
}
private int FindPos(int key) {
int pos = Hash(key);
while (ht[pos] != Integer.MAX_VALUE && ht[pos] != key) {
pos = (pos + 1) % cap; // 线性探测再散列
}
if (ht[pos] == key) {
return -1;
} else {
return pos;
}
}
private int Hash(int key) {
// A very simple & bad hash function
return (key * key) % cap;
}
private void ReHash() {
// System.out.println("Rehash");
// Simple * 2
int[] ht_old = ht;
cap *= 2;
ht = new int[cap];
Arrays.fill(ht, Integer.MAX_VALUE);
// Rehash all old key
for (int k : ht_old) {
if (k != Integer.MAX_VALUE) {
Insert(k);
}
}
}
public boolean Insert(int key) {
int pos = FindPos(key);
if (pos == -1) {
// Already in hash table
return false;
} else {
// Add to hash table
ht[pos] = key;
cnt++;
// Check if need to rehash
if ((double) cnt / cap > lf) {
ReHash();
}
return true;
}
}
public boolean Find(int key) {
int pos = FindPos(key);
if (pos == -1) {
return true;
} else {
return false;
}
}
/* Hash Table */
private int[] ht = null;
/* Load factor */
private double lf;
/* Capbility */
private int cap;
/* Cnt for actual elems */
private int cnt;
public static void main(String[] args) {
HashTable ht = new HashTable();
for (int i = 0; i < 100; i++) {
ht.Insert(i);
}
System.out.println(ht.Find(99));
}
}
