目录
(一) Hash表
1.哈希(散列)函数
在一般的线性表和树中, 存储的元素在结构中的相对位置是随机的, 即和存储的元素之间不存在确定的关系. 所以,在以上数据结构中查找存储的元素时需进行一系列和关键字(key)的比较.
如: 在数组中, 从0索引处到末尾索引中取出数据与关键字(key)一一比较; 在树中, 从根节点到叶子节点中取出数据与关键字(key)一一比较.
设想, 是否存在一种函数 f(), 直接通过关键字(key) 找到 值(value)所在位置(索引或节点…), 那么函数 f()就称之为 hash(哈希)函数
哈希冲突: 哈希函数并不能保证每一个关键字(key) 找到 对应不同的位置, 即:不同的关键字对应到同一个存储位置的现象
2.哈希表
哈希表是基于哈希函数建立的一种特殊的数据结构, 这种数据结构最复杂的操作在于设计哈希函数 和 解决哈希冲突
在哈希表中, “key” 通过哈希函数得到的 “索引(位置)” 分布的越均匀越好, 则哈希冲突越低,效率越高。为了降低哈希冲突,需要采用大于实际存储数据数量的哈希表. 哈希表的设计充分的提现算法设计领域的经典思想: 空间换时间
3.哈希函数的设计
设计遵循三大原则:
- 一致性: 如果 a == b, 则 hash(a) == hash(b);
- 均匀性: 哈希值分布均匀
- 高效性: hash函数计算高效
-
整型:
- 小范围的正整数直接使用
- 小范围的负整数进行偏移: -100 ~ 100 ====> 0 ~ 200
- 大整数: 取模 于 一个素数: 25 ~ 26 ====> % 53
-
浮点型: 浮点数在计算机中都是32位或64位的二进制表示, 只不过是计算机解析成了浮点数, 将二进制转成整型处理
-
字符串: 转成整型处理 (B: 代表进制; M: 代表素数)
- “166” ====> 1 * 102 + 6 * 101 + 6 * 100
- “code” ====> c * 263 + o * 262 + d * 261 + e * 260
- “code” ====> c * B3 + o * B2 + d * B1 + e * B0
- hash(“code”) ====> ( c * B3 + o * B2 + d * B1 + e * B0 ) % M
- hash(“code”) ====> ((((c * B) + o) * B + d) * B + e ) % M
- hash(“code”) ====> ((((c % M) * B + o) % M * B + d) % M * B + e ) % M
-
复合类型: 转换成整型处理
- Date(year, month, day): hash(“Date”) ====> (((data.year % M) * B + data.month) % M * B + data.day) % M
3.哈希冲突的解决方案
-
链地址法(Seperate Chaining): 添加元素时根据哈希函数计算得出在数组中的位置索引, 然后进行存储. 当位置索引重复时存储在查找表中, 查找表可以为链表, 平衡树等数据结构.
-
开放地址法: 与链地址法刚好相反, 在链地址法中, 索引地址是封闭的, 只能存储相同哈希值的元素. 而开放地址法中的索引地址可以存储不相同哈希值的元素, 即相同哈希值的元素可以存储在不同的索引地址.
平方探测遇到哈希冲突: +1 +4 +9 +16…
(二) 自定义HashTable
基于Java中的TreeMap(底层采用红黑树)实现, 哈希表实际存储了 TreeMap[] 数组
1.HashTable基础结构
public class HashTable<K, V> {
/**
* 存储元素的数据结构: TreeMap[] 数组
*/
private TreeMap<K, V>[] hashTable;
/**
* 取模的素数
*/
private int M;
/**
* 存储元素个数
*/
private int size;
@SuppressWarnings("unchecked")
public HashTable(int M) {
this.M = M;
this.size = 0;
hashTable = new TreeMap[M];
for (int i = 0; i < M; i++) {
hashTable[i] = new TreeMap<>();
}
}
public HashTable() {
this(97);
}
/**
* 哈希函数: 先将K转换为大整型, 然后取模于一个素数M
*
* @param key
* @return
*/
private int hash(K key) {
return (key.hashCode() & 0x7fffffff) % M;
}
}
2.HashTable的CRUD操作
/**
* 向hashTable中添加键值对key-value数据
*
* @param key
* @param value
*/
public void add(K key, V value) {
// hashTable通过哈希函数获取指定位置索引的TreeMap
TreeMap<K, V> map = hashTable[hash(key)];
if (map.containsKey(key)) {
map.put(key, value);
} else {
map.put(key, value);
size++;
}
}
/**
* 从HashTable中删除键为key的数据
*
* @param key
* @param value
*/
public V remove(K key) {
TreeMap<K, V> map = hashTable[hash(key)];
V res = null;
if (map.containsKey(key)) {
res = map.remove(key);
size--;
}
return res;
}
/**
* 从HashTable中修改Key的值为Value
*
* @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 + " donen't exist!");
}
map.put(key, value);
}
/**
* 判断HashTable是否存在Key
*
* @param key
* @return
*/
public boolean contains(K key) {
return hashTable[hash(key)].containsKey(key);
}
/**
* 从HashTable中取出Key对应的值
*
* @param key
* @return
*/
public V get(K key) {
return hashTable[hash(key)].get(key);
}
(三) 自定义HashTable时间复杂度分析
1.HashTable静态空间的时间复杂度分析
HashTable时间复杂度分析分为两步:
- 根据哈希函数计算得出的索引找到对应的查找表: 由于HashTable是个数组, 支持随机索引访问, 时间复杂度为O(1)
- 在查找表中根据key寻找value: 假设哈希表中的数组有M个地址,且放入哈希表的元素为N. 则哈希表的每个地址平均存储 N/M 个元素
- 查找表底层实现为链表, 时间复杂度为: O(N/M)
- 查找表底层实现为平衡树, 时间复杂度为: O(log(N/M))
M 为数组长度且不变, 与静态数组一样, 固定的地址空间是不合理的
2.HashTable动态空间处理
- 扩容: 平均每个地址承载的元素多过一定程度, 如 N / M >= upperTol
- 缩容: 平均每个地址承载的元素少过一定程度, 如 N / M < lowerTol
重置哈希表的数组容量, 必须保证数组容量(M)一定为素数, 优化add(K, V) 和 remove(K) 方法
/**
* 每个地址承载的上限元素个数
*/
private static final int UPPER_TOTAL = 10;
/**
* 每个地址承载的下限元素个数
*/
private static final int LOWER_TOTAL = 2;
/**
* 素数(容量)数组常量
*/
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 int capacityIndex = 0;
@SuppressWarnings("unchecked")
public HashTable() {
this.M = CAPACITY[capacityIndex];
this.size = 0;
hashTable = new TreeMap[M];
for (int i = 0; i < M; i++) {
hashTable[i] = new TreeMap<>();
}
}
@SuppressWarnings("unchecked")
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 = this.M;
// 哈希函数取模于 newM
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;
}
/**
* 向hashTable中添加键值对key-value数据
*
* @param key
* @param value
*/
public void add(K key, V value) {
// hashTable通过哈希函数获取指定位置索引的TreeMap
TreeMap<K, V> map = hashTable[hash(key)];
if (map.containsKey(key)) {
map.put(key, value);
} else {
map.put(key, value);
size++;
// 平均每个地址承载的元素个数 >= 上限元素个数
if(size >= UPPER_TOTAL * M && capacityIndex + 1 < CAPACITY.length) {
capacityIndex++;
resize(CAPACITY[capacityIndex]);
}
}
}
/**
* 从HashTable中删除键为key的数据
*
* @param key
* @param value
*/
public V remove(K key) {
TreeMap<K, V> map = hashTable[hash(key)];
V res = null;
if (map.containsKey(key)) {
res = map.remove(key);
size--;
// 平均每个地址承载的元素个数 < 下限元素个数
if(size < LOWER_TOTAL * M && capacityIndex - 1 >= 0) {
capacityIndex--;
resize(CAPACITY[capacityIndex]);
}
}
return res;
}
3.HashTable动态空间的时间复杂度分析
对于动态空间哈希表来说, 添加元素的时间复杂度为O(1), 只有元素数从N 增加到 upperTol * N, 地址空间翻倍O(n), 但均摊到每次添加操作中 平均复杂度为 O(1).
或者可以理解为添加操作在 O(lowerTol) ~ O(upperTol) 中, lowerTol 和 upperTol 为自定义常数, 则平均时间复杂为 O(1), 缩容同理. (哈希表牺牲了元素的顺序性).