哈希表封装与实现原理
**哈希(hash)**又称散列,是一种组织数据的方式。从译名来看,有散乱排列的意思。本质就是通过哈希函数把关键字 Key 跟存储位置建立一个映射关系,查找时通过这个哈希函数计算出 Key 存储的位置,进行快速查找。
哈希函数
一个好的哈希函数应该让 N 个关键字被等概率的均匀地散列分布到哈希表的 M 个空间中,但是实际中却很难做到。因此我们要尽量往这个方向去考量设计。
除法散列法
当数据比较分散的情况下,用直接定址法是无法很好地处理问题的。除法散列法也叫做除留余数法,假设哈希表的大小为 M,那么通过 key 除以 M 的余数作为映射位置的下标,也就是哈希函数为:h(key) = key % M。
哈希冲突和负载因子
当使用除法散列法时,要避免 M 为某些值,如 2 的幂、10 的幂等。如果是 2 的幂,那么 key % 2^X 本质相当于保留 key 的后 X 位,后 x 位相同的值计算出的哈希值都是一样的,就冲突了。因此当使用除法散列法时,建议 M 取不太接近 2 的整数次幂的一个质数。
负载因子:假设哈希表中已经映射存储了 N 个值,哈希表的大小为 M,M 一定要大于 N,那么负载因子 = N/M,保证负载因子小于 1。负载因子越大,说明 M 是接近于 N 的,则空间利用率越高,相对地哈希冲突的概率越高;负载因子越小,说明 M 的空间很大,则空间利用率低,相对地哈希冲突的概率越低。
处理哈希冲突
实践中哈希表一般还是选择除法散列法作为哈希函数,当然哈希表无论选择什么哈希函数也避免不了冲突,那么插入数据时,如何解决冲突呢?主要有两种方法,开放定址法和链地址法。
开放定址法
在开放定址法中所有的元素都放到哈希表里,当一个关键字 key 用哈希函数计算出的位置冲突了,则按照某种规则找到一个没有存储数据的位置进行存储,开放定址法中负载因子一定是小于 1 的。这里的规则有三种:线性探测、二次探测、双重探测。
状态表示
哈希表有三种状态表示:存在、空、删除。
enum Status { EMPTY, EXIST, DELETE };
template<class K, class V>
struct HashData {
pair<K, V> _kv;
Status _status = EMPTY;
};
线性探测
在映射数据的时候可能会存在哈希冲突,此时从发生冲突的位置开始,依次线性向后探测,直到寻找到下一个没有存储数据的位置为止,如果走到哈希表尾,则回绕到哈希表头的位置。
- h(key) = hash0 = key % M,hash0 位置冲突了,则线性探测公式为:hc(key, i) = hashi = (hash0 + i) % M,i = {1, 2, 3, ..., M − 1},保证线性探测时能从队尾走到队头,且因为负载因子小于 1,则最多探测 M-1 次,一定能找到一个存储 key 的位置。
可以发现线性探测的问题会占用其他值可能映射到的空间,会导致原本不冲突的值产生哈希冲突。严重的话可能会使多个 hash0,hash1,hash2,hash3 的值都争夺 hash3 位置,这种现象叫做群集/堆积。
扩容
方案一:新建一个哈希表,遍历旧表让里面的数据重新映射到新表当中; 方案二:采用复用的手段将旧表数据映射到新表中。
if ((double)_n / (double)_tables.size() > 0.7) {
HashTable<K, V, Hash> newHT(__stl_next_prime(_tables.size() + ));
( i = ; i < _tables.(); i++) {
(_tables[i]._status == EXIST) {
newHT.(_tables[i]._kv);
}
}
_tables.(newHT._tables);
}


