基本概念
散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数。
1、若关键字为k,则其值存放在f(k)的存储位置上。由此,不需比较便可直接取得所查记录。称这个对应关系f为散列函数,按这个思想建立的表为散列表。
2、对不同的关键字可能得到同一散列地址,即k1≠k2,而f(k1)=f(k2),这种现象称为碰撞(英语:Collision)。具有相同函数值的关键字对该散列函数来说称做同义词。综上所述,根据散列函数f(k)和处理碰撞的方法将一组关键字映射到一个有限的连续的地址集(区间)上,并以关键字在地址集中的“像”作为记录在表中的存储位置,这种表便称为散列表,这一映射过程称为散列造表或散列,所得的存储位置称散列地址。
3、若对于关键字集合中的任一个关键字,经散列函数映象到地址集合中任何一个地址的概率是相等的,则称此类散列函数为均匀散列函数(Uniform Hash function),这就是使关键字经过散列函数得到一个“随机的地址”,从而减少碰撞。
常用的构造散列函数的方法有:
(1)、直接定址法
取关键字或关键字的某个线性函数值为散列地址,即:h(key) = key 或 h(key) = a * key + b其中a和b为常数。
(2)、数字分析法
(3)、平方取值法
取关键字平方后的中间几位为散列地址。
(4)、折叠法
将关键字分割成位数相同的几部分(最后一部分的位数可以不同),然后取这几部分的叠加和(舍去进位)作为散列地址。
(5)、除留余数法
取关键字被某个不大于散列表表长m的数p除后所得的余数为散列地址,即: h(key) = key MOD p p ≤ m
(6)、随机数法
选择一个随机函数,取关键字的随机函数值为它的散列地址,即:h(key) = random(key)其中random为随机函数。
解决冲突的方法:
⑴链接法(拉链法)。将具有同一散列地址的记录存储在一条线性链表中。例,除留余数法中,设关键字为 (18,14,01,68,27,55,79),除数为13。散列地址为 (5,1,1,3,1,3,1),哈希散列表如图。
⑵开放定址法。如果h(k)已经被占用,按如下序列探查:(h(k)+p⑴)%TSize,(h(k)+p⑵)%TSize,…,(h(k)+p(i))%TSize,…其中,h(k)为哈希函数,TSize为哈希表长,p(i)为探查函数。在 h(k)+p(i-1))%TSize的基础上,若发现冲突,则使用增量 p(i) 进行新的探测,直至无冲突出现为止。其中,根据探查函数p(i)的不同,开放定址法又分为线性探查法(p(i) = i : 1,2,3,…),二次探查法(p(i)=(-1)^(i-1)*((i+1)/2)^2,探查序列依次为:1, -1,4, -4, 9 …),随机探查法(p(i): 随机数),双散列函数法(双散列函数h(key) ,hp (key)若h(key)出现冲突,则再使用hp (key)求取散列地址。探查序列为:h(k),h(k)+ hp(k),…,h(k)+ i*hp(k))。
⑶桶定址法。桶:一片足够大的存储空间。桶定址:为表中的每个地址关联一个桶。如果桶已经满了,可以使用开放定址法来处理。例如,插入A5,A2,A3,B5,A9,B2,B9,C2,采用线性探查法解决冲突。如图。
开放地址法的Java代码实现
public class OpenAddressHashtable<K, V> {
private static final double DEFAULT_LOAD_FACTOR = 0.75;
private static final int DEFAULT_INIT_CAP = 8;
private static final boolean CHECK_INVARIANT = true;
private static final Entry DELETED = new Entry();
private double loadFactor;
private Entry<K, V>[] table;
private int size;
public OpenAddressHashtable() {
this(DEFAULT_LOAD_FACTOR);
}
public OpenAddressHashtable(double loadFactor) {
if (loadFactor <= 0 || loadFactor >= 1) {
throw new IllegalArgumentException("load factor must be between (0, 1)");
}
@SuppressWarnings("unchecked")
Entry<K, V>[] entries = (Entry<K, V>[])new Entry[DEFAULT_INIT_CAP];
table = entries;
this.loadFactor = loadFactor;
}
public int size() {
return size;
}
public boolean isEmpty() {
return size == 0;
}
public boolean put(K key, V value) {
int freeSlot = findSlot(key);
boolean inserted = (table[freeSlot] == null || table[freeSlot] == DELETED);
if (inserted) {
table[freeSlot] = new Entry<K, V>(key, value);
size++;
if (table.length * loadFactor < size) {
rehash();
}
} else {
table[freeSlot].value = value;
}
if (CHECK_INVARIANT) checkInvariant();
return inserted;
}
private void rehash() {
int newLen = table.length << 1;
while (newLen * loadFactor < size) {
newLen <<= 1;
}
Entry<K, V>[] oldTable = this.table;
this.table = new Entry[newLen];
this.size = 0; // clear size
for (int i = 0; i < oldTable.length; i++) {
if (oldTable[i] != null && oldTable[i] != DELETED) {
put(oldTable[i].key, oldTable[i].value);
}
}
}
public boolean delete(K key) {
int freeSlot = findSlot(key);
boolean deleted = table[freeSlot] != null && table[freeSlot] != DELETED;
if (deleted) {
table[freeSlot] = DELETED;
size--;
}
if (CHECK_INVARIANT) checkInvariant();
return deleted;
}
public boolean contains(K key) {
int freeSlot = findSlot(key);
return table[freeSlot] != null && table[freeSlot] != DELETED;
}
public V get(K key) {
int freeSlot = findSlot(key);
if (table[freeSlot] == null || table[freeSlot] == DELETED) {
return null;
} else {
return table[freeSlot].value;
}
}
/**
* 找到一个关键字为key的槽,如果找不到,则返回下一个插入key的槽,这个槽的值要么是null,要么是DELETED。
*/
private int findSlot(K key) {
int freeSlot = -1;
for (int i = 0; i < table.length; i++) {
int h = hash(key, i);
if (table[h] == null || equals(table[h].key, key)) {
return h;
} else if (table[h] == DELETED && freeSlot == -1) {
freeSlot = h;
}
}
// it should never return -1
return freeSlot;
}
/*
* h(k,i)函数,h(k,0),h(k,1)..h(k,m-1)必须是0,1..m-1的一个排列,其中m
* 是table的长度,即table.length。
*/
private int hash(K k, int i) {
// 线性探查,最简单的函数
return (k.hashCode() + 3 * i) % table.length;
// 二次探查,但是该如何选择c1,c2呢?这里选择c1=1,c2=3,但是它似乎并不能
// 使h(k,0), h(k,1)..h(k,m-1)是0,1..m-1的一个排列。
// return (k.hashCode() + i + 3 * i * i) % table.length;
}
private void checkInvariant() {
int len = table.length;
while (len > 1) {
if (len % 2 != 0) {
throw new IllegalStateException(String.format("table.length(%d) is not like 2^n", table.length));
}
len = len / 2;
}
if (loadFactor * table.length < size) {
throw new IllegalStateException(String.format("loadFactor(%0.2f) * table.length(%d) < size(%d)", loadFactor, table.length, size));
}
int acutalSize = 0;
for (int i = 0; i < table.length; i++) {
if (table[i] != null && table[i] != DELETED) {
Entry<K, V> e = table[i];
boolean found = false;
for (int j = 0; j < table.length; j++) {
int h = hash(e.key, j);
if (table[h] == null) {
break;
}
else if (table[h] != DELETED && equals(table[h].key, e.key)) {
found = true;
}
}
if (!found)
throw new IllegalStateException(String.format("Cannot find key \"%s\"", e.key));
acutalSize++;
}
}
if (size != acutalSize) {
throw new IllegalStateException(String.format("actual size(%d) not equal size(%d)", acutalSize, size));
}
}
private static boolean equals(Object o1, Object o2) {
return o1 == o2 || (o1 != null && o1.equals(o2));
}
private static class Entry<K, V> {
K key;
V value;
public Entry() {}
public Entry(K k, V v) { key = k; value = v; }
public boolean equals(Object o) {
if (o == null || !(o instanceof Entry)) return false;
return OpenAddressHashtable.equals(key, ((Entry)o).key);
}
public int hashCode() { return key == null ? 0 : key.hashCode(); }
}
public static void main(String[] args) {
OpenAddressHashtable<Integer, String> hashtable = new OpenAddressHashtable<Integer, String>();
hashtable.put(1, "one");
System.out.println(hashtable.get(1));
hashtable.put(1, "oneone");
System.out.println(hashtable.get(1));
for (int i = 0; i < 20; i++) {
hashtable.put(i, "value " + i);
}
System.out.println(hashtable.size);
System.out.println(hashtable.get(1));
hashtable.delete(1);
System.out.println(hashtable.get(1));
System.out.println(hashtable.size);
}
}