一、引言
哈希表是一种组合的数据结构,它通常的实现方式是数组加链表,或者数组加红黑树。哈希表是一种牺牲空间去换取时间的数据结构,需要在空间与时间上有取舍,哈希表是时间和空间之间的平衡。哈希表的核心是哈希函数,哈希表最关键的问题哈希冲突也是取决于哈希函数的设计。
二、哈希函数
1、什么是哈希函数
哈希函数是一种将“键”转换为“索引”的逻辑规则。它的设计好坏对哈希表的性能影响巨大。优良的哈希函数能够最大程度的减少哈希冲突,使得哈希表中的元素分布得尽可能的均匀,离散程度更大,这样哈希表就会性能优越;较差的哈希函数设计,带来的可能是一场灾难,哈希冲突严重,空间利用率低,时间复杂度呈线性恶化,造成频繁的扩容操作。
2、哈希函数的设计
对于哈希函数的设计,以下举了一些简单的例子,都基本类型转换成整型处理,并不是唯一的方法,仅供参考。以下简单列了几条设计原则。
- 一致性:如果a == b,则hash(a) == hash(b)
- 高效性:哈希函数计算高效简便
- 均匀行:哈希值均匀分布
1、整型
- 小范围正整数可直接使用
- 小范围负整数进行偏移
- 大整数可以与一个合适的素数取模
说明:上图(左)与一个不合适的合数取模,获得的索引冲突严重,不可取;而上图(右)与一个质数取模,明显获得的索引分布更均匀,离散程度更好。点击获取合适的哈希表素数。
2、浮点型
- 浮点型转换成整型处理
3、字符串型
- 字符串型转换成整型处理
说明:其中B为一个常数,M为一个合适的质数。上图将一个字符串型加入一些规则映射成了一个整型。
4、日期类型
- 日期类型转换成整型处理
三、哈希冲突的处理
1、链地址法(Separate Chaining)
说明:哈希冲突的元素可以用链表这种线性的数据结构保存,当然也可以用平衡树结构保存,Java8中的哈希表实现,当哈希冲突达到一定的程度,会将链表替换成红黑树,前提是哈希表中原本的元素具备可比较性。
2、开放地址法
1、线性探测
说明:当遭遇哈希冲突时,会按照规则顺延往下找空挡位置插入元素,这种方式如果哈希冲突比较严重,会造成寻找空挡位置效率变低。哈希表性能变差。这种方式就是要设置合适的哈希表容量。
2、平方探测
说明:这种方式相对于线性探测,加大了寻找空挡位置的步长。
3、二次哈希
说明:当遭遇哈希冲突后,再运行一个哈希函数,对上一次哈希运算结果再计算一次结果,二次运算后得到的索引位置,是一个空挡位置的几率增大。
四、哈希表动态空间处理
说明:当哈希冲突达到一个所能容忍的上界位置时(upperTol),对哈希表进行扩容操作,以减少哈希冲突;当哈希冲突降低到一个下界位置(lowerTol)时,对哈希表进行缩容操作,以节约空间。
五、哈希表实现
1、基于数组和TreeMap实现
import java.util.TreeMap;
/**
* 哈希表
* @param <K>
* @param <V>
*/
public class HashTable<K extends Comparable<K>, V> {
/**
* 容量素数表,用于确定扩容、缩容的容量值
*/
private final int[] capacity
= {53, 97, 193, 389, 769, 1543, 3079, 6151, 12289, 24593,
49157, 98317, 196613, 393241, 786433, 1572869, 3145739, 6291469,
12582917, 25165843, 50331653, 100663319, 201326611, 402653189, 805306457, 1610612741};
/**
* 容忍度上界(扩容临界位)
*/
private static final int upperTol = 10;
/**
* 容忍度下界(缩容临界位)
*/
private static final int lowerTol = 2;
/**
* 初始化容量
*/
private int capacityIndex = 0;
/**
* TreeMap数组容器
*/
private TreeMap<K,V>[] hashTable;
/**
* 取一个合适的素数,次数决定数组的长度
*/
private int m;
/**
* 哈希表中元素个数
*/
private int size;
public HashTable(int m) {
this.m = capacity[capacityIndex];
this.size = 0;
this.hashTable = new TreeMap[m];
for (int i = 0; i < m; i++) {
hashTable[i] = new TreeMap<>();
}
}
/**
* 根据key获取哈希码,非负数
* @param key
* @return
*/
private int hash(K key) {
return (key.hashCode() & 0x7fffffff) % m;
}
/**
* 获取哈希表中元素个数
* @return
*/
public int getSize() {
return size;
}
/**
* 向哈希表中添加元素
* @param key
* @param value
*/
public void add(K key, V value) {
TreeMap<K, V> map = hashTable[hash(key)];
if (map.containsKey(key)) {
map.put(key, value);
} else {
map.put(key, value);
size ++;
//扩容操作
if (size >= upperTol * m && capacityIndex + 1 < capacity.length) {
capacityIndex++;
resize(capacity[capacityIndex]);
}
}
}
/**
* 删除哈希表中指定的元素
* @param key
* @return
*/
public V remove(K key) {
TreeMap<K,V> map = hashTable[hash(key)];
V ret = null;
if (map.containsKey(key)) {
ret = map.remove(key);
size--;
//缩容操作
if (size < lowerTol * m && capacityIndex - 1 >= 0) {
capacityIndex--;
resize(capacity[capacityIndex]);
}
}
return ret;
}
/**
* 修改哈希表中指定的元素
* @param key
* @param value
*/
public void set(K key, V value) {
TreeMap<K, V> map = hashTable[hash(key)];
if (!map.containsKey(key)) {
throw new IllegalArgumentException(key + " doesn't exist");
}
map.put(key, value);
}
/**
* 判断哈希表中是否包含元素
* @param key
* @return
*/
public boolean contains(K key) {
return hashTable[hash(key)].containsKey(key);
}
/**
* 从哈希表中查询指定元素
* @param key
* @return
*/
public V get(K key) {
return hashTable[hash(key)].get(key);
}
/**
* 扩容、缩容操作
* @param newM 新的容量
*/
private void resize(int newM) {
TreeMap<K, V>[] newHashTable = new TreeMap[newM];
for (int i = 0; i < newM; i++) {
newHashTable[i] = new TreeMap<>();
}
int oldM = m;
this.m = newM;
for (int i = 0; i < oldM; i++) {
TreeMap<K,V> map = hashTable[i];
for (K key : map.keySet()) {
newHashTable[hash(key)].put(key, map.get(key));
}
}
this.hashTable = newHashTable;
}
}
六、时间复杂度分析
哈希表的平均时间复杂度是O(1)级别的,这是哈希表用空间换来的性能,这是哈希表最大的优势。哈希表扩缩容时这种费时操作也是不常发生,平摊下去依然趋近于O(1)级别。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。