HashTable
什么是哈希表
哈希表本质上就是一个用于存放记录的数组,只不过访问这个数组不是简单地通过下标,而是利用要查询对象的某个特征值通过散列函数来映射成数组的下标,接着下标查询数组中的值。充分地利用了数组下标查询的高效性,所以若散列函数的设计好,那么就可能可以达到\(O(1)\)的时间复杂度。
哈希表的一些基本概念
什么是哈希冲突
哈希冲突就是多个不同的特征值通过散列函数映射得到相同的数组下标。但很多时候我们是希望不同的特征值对应不同的目标值。
什么是负载因子
负载因子是权衡当前哈希表的健康状态的,一个能够存放\(n\)个元素的、有\(m\)个槽的哈希表的负载因子
α越小,表示哈希表存储的元素越少,产生冲突的可能性就越低,哈希表的查询更加高效;α越大,表示哈希表的存储元素越多,产生的冲突的可能性就越高,哈希表的性能就越差;α是可以大于1的,只不过很少这么做,因为这样必然存在冲突。一般情况下,最佳的α值是0.75.
什么是散列函数
散列函数就是将要查询对象的特征码映射成数组下标的函数h(key)
,要满足基本的两个条件:
- 散列函数映射到的位置应要在哈希表的容量范围内,即\(h(key)\in[0,m)\)
- 映射到的每一个位置的概率应要相同,尽可能使得哈希表能够均匀,避免堆积。
解决哈希冲突的策略
开散列法
开散列就是把冲突的记录存储在表的外部,由外部解决
拉链法
就是将哈希表中的每一个槽(slot)定义为一个链表的表头,每次存放记录都是查找到存放链表的槽,然后添加。可以添加在末尾,也可以插入到表头,当然插入到表头的效率更高 -- 只需要改变一下表头指针即可。链表的每一个节点包含:关键码、指向下一个节点的指针。
struct Node {
ValueType value;
KeyType key;
Node* next;
}
链表由于需要指针来连接,所以这个方法增加了较多的空间浪费。
闭散列法
闭散列就是把冲突的记录在表内部自己解决
桶式散列法
就是散列表中的连续的槽分为多组,每一组就叫一个桶。这种情况下,散列函数映射到桶的标号,冲突记录存在同一个桶中。
如果桶都容纳不下了,那么就只能将记录存放到溢出桶。
开放地址法
开放地址法就是某个特征码找到一个槽后,发现槽被占了,接着就去寻找下一个空的槽。
这个寻找下一个槽的过程就要有一个探查序列来引导,常见的探查序列有如下:
线性探查
每一次发生冲突,下一次查找的位置到散列函数映射到的原始位置h(key)
的距离d
的线性增长的.说简单点就是,不断向后每次移动相同的距离直到找到一个空位安身。
但是线性探查很容易造成元素的堆积现象,从而导致哈希表的性能下降。
二次探查
基本思想和线性探查是一样的,只不过每一次探查的距离函数是一个二次函数\(d(i) = ai+bi^2\).
双重散列
双散列意如其名,就是综合两个哈希函数的映射结果来确定记录的位置。
\(h(key) = [h1(key) + i \cdot h2(key)]mod(m)\)
哈希表的一些短板
- 哈希表的数据存储是分散的,不能够进行排序