哈希表中执行插入和搜索操作可以达到O(1)的时间级,如果没有发生冲突,只需要使用一次哈希函数和数组的引用,就可以插入一个新数据项或找到一个已经存在的数据项。
如果发生冲突,存取时间就依赖后来的探测长度。一个单独的查找或插入时间与探测的长度成正比,这里还要加上哈希函数的常量时间。
平均探测长度以及平均存取时间,取决于填装因子,随着填装因子变大,探测长度也越来越长。
随着填装因子变大,效率下降的情况,在不同开放地址法方案中比链地址法更严重, 所以我们来对比一下他们的效率, 再决定我们选取的方案.
装填因子
- 在分析效率之前, 我们先了解一个概念: 装填因子.
- 装填因子表示当前哈希表中已经包含的数据项和整个哈希表长度的比值.
- 装填因子 = 总数据项 / 哈希表长度.
- 开放地址法的装填因子最大是多少呢? 1, 因为它必须寻找到空白的单元才能将元素放入.
- 链地址法的装填因子呢? 可以大于1, 因为拉链法可以无限的延伸下去, 只要你愿意. (当然后面效率就变低了)
开放地址法
- 我们来一个个认识一下开放地址法中每种方案的效率.
线性探测
-
下面的等式显示了线性探测时,探测序列(P)和填装因子(L)的关系
- 对成功的查找: P = (1+1/(1-L))/2
- 对不成功的查找: P=(1+1/(1-L)^2)/2
-
公式来自于Knuth(算法分析领域的专家, 现代计算机的先驱人物), 这些公式的推导自己去看了一下, 确实有些繁琐, 这里不再给出推导过程, 仅仅说明它的效率.
-
图解算法的效率:
-
图片解析:
- 当填装因子是1/2时,成功的搜索需要1.5次比较,不成功的搜索需要2.5次
- 当填装因子为2/3时,分别需要2.0次和5.0次比较
- 如果填装因子更大,比较次数会非常大。
- 应该使填装因子保持在2/3以下,最好在1/2以下,另一方面,填装因子越低,对于给定数量的数据项,就需要越多的空间。
- 实际情况中,最好的填装因子取决于存储效率和速度之间的平衡,随着填装因子变小,存储效率下降,而速度上升。
二次探测和再哈希
-
二次探测和再哈希法的性能相当。它们的性能比线性探测略好。
- 对成功的搜索,公式是: -log2(1 - loadFactor) / loadFactor
- 对于不成功的搜搜, 公式是: 1 / (1-loadFactor)
-
对应的图:
-
图片解析:
- 当填装因子是0.5时,成功和不成的查找平均需要2次比较
- 当填装因子为2/3时,分别需要2.37和3.0次比较
- 当填装因子为0.8时,分别需要2.9和5.0次
- 因此对于较高的填装因子,对比线性探测,二次探测和再哈希法还是可以忍受的。
链地址法
-
链地址法的效率分析有些不同, 一般来说比开放地址法简单. 我们来分析一下这个公式应该是怎么样的.
- 假如哈希表包含arraySize个数据项, 每个数据项有一个链表, 在表中一共包含N个数据项.
- 那么, 平均起来每个链表有多少个数据项呢? 非常简单, N / arraySize.
- 有没有发现这个公式有点眼熟? 其实就是装填因子.
-
OK, 那么我们现在就可以求出查找成功和不成功的次数了
- 成功可能只需要查找链表的一半即可: 1 + loadFactor/2
- 不成功呢? 可能需要将整个链表查询完才知道不成功: 1 + loadFactor.
-
对应的图
效率的结论
- 经过上面的比较我们可以发现, 链地址法相对来说效率是好于开放地址法的.
- 所以在真实开发中, 使用链地址法的情况较多, 因为它不会因为添加了某元素后性能急剧下降.
- 比如在Java的HashMap中使用的就是链地址法.
- 代码实现:
- 到目前为止, 我们讲了很久的哈希表原理, 依然没有写任何代码.
- 因为我觉得理解了原理, 再去写代码相对思路会清晰一些.
- 后面, 我会使用链地址法来实现我们的哈希表(你也可以试着使用开放地址法来实现, 原理已经非常清晰了)
- 但是, 我们还有N多的细节, 可以深入探讨.
- 欲知后事如何, 且听下回分解!!!