动态哈希表
哈希表就是通过对键值使用哈希函数计算得到入口点,从而可以插入或获取需要的值。对于计算得到相同入口点的情况,可以使用冲突消解策略来解决,一般使用附加链表的方式;
哈希表在初始化时固定了入口点的个数,如果随着哈希表的使用,不断有数据进入,哈希表膨胀到一定的程度,需要扩展哈希表,该如何设计一个动态的、可扩展的哈希表呢?
一般来讲需要解决两个问题:
1 、扩展后得到的新入口项如何通过哈希函数访问到?
2 、哈希函数新的入口项资源分配问题?
关于哈希函数的问题:
berkerlyDB 的解决方案是通过编码;
假设哈希函数返回的值时一个 32 位数 H 。如果哈希表只有 2 个入口项,则只需要使用 H 的第 0 位就可以区分,即只需 1 位就可以达到记录入口项编号。如果有 3 个入口项,则使用 H 的第 0 和第 1 为就可以区分,即使用 2 位,但是 2 位数据有四种组合 —00 、 01 、 10 、 11 。这就需要使用屏蔽技术, 3 个入口项,则最大入口项的编号为 2 ( 0x10 ,从 0 计数),当 H 的低 2 位的值大于最大入口项编号,再去除低 2 位的最高位,即只使用第 0 位来区分。这样达到区分的目的。
从上面可以看出,可以通过调节 H 的有效位数来到达哈希函数扩展的问题。
假设当前有 N 个入口项, N 大于等于 2 的 B 次方,小于 2 的 B+1 次方。则使用 H 的低 B+1 位和低 B 位可以有效的达到区分的目的。首先,使用低 B+1 位来判断入口项,如果不能区分(对于 B 小于 2 的 B+1 次方的情形,如果 H 的低 B+1 位大于 B 小于 2 的 B+1 次方时),则使用低 B 位区分。当入口项的个数扩展至 N+1 时,如果 N+1 仍然处于 2 的 B 次方与 B+1 次方之间,则上述区分方法仍然有效,当 N+1 超过 2 的 B+1 次方式,将 B 递增 1 位就解决问题了。通过这样编码的手段就可以在不用更改哈希函数的情况下有效的达到调节哈希表扩展的问题。
其实从上述方法可以看出,如果输入数据在哈希函数上均匀分布,则当入口项的个数不是 2 的幂的话,有一部分数据通过高位掩码(即获取 H 低 B+1 位的方式)不能找到入口项,在通过低位掩码找到,这之间就是将这类数据从哈希表入口项的搞半段转移到了第半段,这样入口项低半段的这部分将比其他部分具有更高的概率存储数据,这些入口项将比其他入口项具有更多的记录,当哈希表扩展时,就是在高半段增加一个入口项,将那些本来应该存储到这一入口项的记录从对应的低半段转移过来,从而平衡负载。
例如:当哈希表只有 3 个入口项时, H 低 2 位为 01 , 11 的情况都存储在第 1 入口项, 00 存储在第 0 入口项, 10 存储在第 2 入口项,则第 1 入口项有较重的负载,哈希表扩展 1 个入口项后,有了第 3 ( 0x11 )入口项,这样讲以前 H 低 2 位为 11 的记录从第 1 入口项转移到第 3 入口项,这样四个入口项在概率上具有平等的机会存储入口项。即具有相同低位的入口项在哈希表压缩时可以归并到一起,扩展时,可以安全的分裂开,从而有效的解决动态哈希表的哈希函数问题。
关于哈希函数新的入口项资源分配的问题,涉及到资源的充分利用和解决资源分配冲突。
哈希表中一般有四种类型的数据页:元数据页,入口项页,入口项链表页,大数据存储页。
元数据页:存放哈希表自身信息的页,位置和大小固定,存放在文件的开始部分,即第 0 页,占据页面的数量依赖于页面的大小。元数据页之后的页面均用于存放其它三种类型的页面;
入口项页:即通过哈希函数计算键值,得到入口项,用于存放实际的数据,使用链表来解决冲突消解,则入口项页其实就是入口项链表的首页,这类页面只能通过哈希函数计算键值的访问;
入口项链表页:即使用链表作为冲突消解,存放数据的非首页页面;这类页面的访问基本上是通过链表遍历;
大数据存储页:对于具有较大的键值和数据值的记录,需要额外的页面单独存放,入口项中保存存放页面的编号;对于尺寸大的记录,可能需要使用页面链表的存放。这类页面的访问就是通过上两类页面中保存的页面编号来访问;
当哈希表扩展时,新入口项页的分配就需要解决与入口项页和大数据存储页的冲突问题。
如果直接将哈希函数的结算的结果作为页面的编号,入口项页和大数据存储页的分配将十分麻烦。由于入口项页与大数据存储页都是额外的页面,下文统称为溢出页。
假设初始入口项的个数是 N ,则扩展后的页面编号都是 N+x ,那么在未扩展之前,溢出页面如何分配呢?由于文件是按照页面连续存储的,如果直接在 N 之后分配,则必然在扩展时与扩展的新入口项冲突。如果规定在 N 之后 M 页面处开始分配,则不过是延缓这一冲突的过程,而且 N 到 N+M 的页面自初始化之后就一直占用资源作为预留空间,如其这样不如一开始就直接将 N+M 初始化。
由上判断直接将哈希函数计算得到的值作为入口项页面编号是欠妥当的。可以使用另一种策略,溢出页面的分配直接在 N