可扩展Hash
可扩展Hash
可扩展Hash是结合了Hash算法的一种数据结构组织方法, 这种数据组织方式涉及到Hash函数, 目录映射存储地址, 使用Hash函数的结果作为地址下标的索引, 以及使用桶的方式存储键值对数据.
可扩展hash (Extendible Hashing ) 是一种动态 hash 方法. 可扩展 Hash 是从链式 hash 方法延申的. 链式 Hash 也是动态 hash 方法, 示例如下:
链式 hash 中, Directories
大小是固定的, 当不同的元素的 key 映射到相同的 Directories
的 slot 之后, 相同的Key会放入相同的 Bucket
, Bucket
的数据结构有很多种形式, 通常是链表, Bucket
没有大小限制, 随着元素的写入无线增大. Bucket
的无限扩张会导致随着元素的写入, 查找效率越来越低. 因此一种优化的方式为可扩展 Hash 的数据结构.
可扩展Hash 的特点
- Hash 函数也会随着数据结构的调整而动态调整.
- 可扩展Hash的
Directories
与链式 Hash类似,Directories
中的每一个slot存储的是Bucket
的起始地址, 可扩展Hash的一大特点就是它的Directories
会随着元素的写入而改变大小, 但是Directories
的大小一定是 \(2^{global\_depth}\). Buckets
: 可扩展Hash的Buckets
是存储数据value
的地方, 但是通常还需要存储这个Bucket
的大小等信息. 通常实现的方式是链表或者数组.
可扩展Hash的数据结构
可扩展Hash的主要结构示意图如下:
可扩展Hash数据结构的解释
Directories
: 可扩展Hash的Directories
与链式Hash类似, 目录的每一项存储着一个Bucket
的地址. 不同点在于可扩展Hash的Directories
更像是一张表, 每一个表项是可以用下标检索的. 并且Directories
的大小为 \(2^{global\_depth}\) . 每次当可扩展Hash表新插入元素的时候,Directories
都有可能扩充, 此时Directories
的结构会发生改变, 每一个slot的下标index会改变.Buckets
: 可扩展Hash表的Buckets
也是用来存储数据value的. 目录中的 slot 指向Bucket
, 每一个Bucket
有一个对应的局部深度(Local Depth), 当该Bucket的局部深度小于全局深度(Global Depth)的时候, 目录中会有多个 slots 指向该 Bucket. 可扩展Hash的每一个Bucket有一个大小限制, 当一个Bucket中存储的value的个数大于这个限制后, 这个Bucket会分裂.Global Depth
: 全局深度Global Depth
. 通常在可扩展Hash数据结构中进行索引时, Hash 函数首先作用于Key以获得一个无符号的整数Hash_Index
, 用于在目录中作为地址下标的索引. 而目录id 由Global Depth位表示. 例如Global Depth为3, 那么目录的slots的个数是 8个, 索引index的位数是 3位. 通常就取Hash_Index
的最低3位作为页目录的索引.Local Depth
: 每一个Bucket都有自己的Local Depth, Local Depth 表示使用目录的 index 的最后几位用于指向该Bucket, 例如一个Bucket的Local Depth 是2, 而 Global Depth是3, 那么index为 1, 5 都有可能指向该Bucket.Bucket Splitting
: 初始化可扩展Hash表的时候, 会设置一个Bucket大小限制, 当写入的value超过Bucket的大小限制的时候, Bucket会分裂, 此时, 分裂的Bucket的 Local Depth 会增加 1, 如果增大后的 Local Depth 大于 Global Depth, 将会触发Directory Expansion
.Directory Expansion
: Local Depth 表示的是使用index的Local Depth
位索引到某一个Bucket. 当某次Bucket 分裂之后, Local Depth 大于 Global Depth, Global Depth 必须增加, 此时目录大小增大, 因为目录的大小为 \(2^{global\_depth}\), 目录变为原来的两倍. Hash 函数会改变, 目录指向的 Bucket的指针也会发生改变.
可扩展hash(Extendible Hashing)工作流程
可扩展Hash的实际工作流程很简单, 需要关注的是上述所属的 Bucket Splitting
(桶拆分) 以及目录扩张(Directionary Expansion
) 的过程.
- 分析数据类型, 可扩展Hash支持多种数据类型, 使用 Hash 函数将数据转换为无符号整数类型(通常是无符号整数类型) 作为 Hash值(
Hash_Index
). 这个Hash值通常不可以直接作为 Dictionaries 的Index的索引, 需要通过下面的计算. - 将 Hash值 转化为二进制的表达形式. 例如我们计算得到的 Hash值 是49, 二进制表示是
110001
. - 获取Global Depth: 假设我们的 Global Depth 是3.
- 确认目录的Index: 目录的slot的ID(index), 使用Hash值的二进制数的Global Depth LSB. 如果Global Depth为3, 那么就是 3LSB. 这里
110001
的3LSB为001
. - 获取目录中slot指向的 Bucket, 这里
001
指向第一个 Bucket. - 向 Bucket 中插入元素, 并检查是否溢出(Bucket的大小是初始化的时候固定的). 如果溢出, 参考步骤7, 不溢出表示插入数据成功.
- 向
Bucket
中插入数据后溢出: 获取Local Depth. 将 Bucket 分裂, Local Depth增加1. 此时判断 Local Depth 与 Global Depth 之间的关系.
a) 如果Loacl Depth 加一之后小于等于Global Depth: 将旧的Bucket一分为二后, Local Depth+1, 因此指向这两个Bucket的索引的个数改变(Local Depth 是Index指向Bucket索引的最后的位数). 更新目录指向Bucket的索引.
b) 如果Loacl Depth 加一之后大于Global Depth: 这种情况Global Depth需要增加 1, 目录扩充为原来的两倍. 此时原来指向某个Bucket的slot会有可能变成两个, 例如index为 01 变成001和101, 这两个 slot 均指向这个Bucket. Splitting Bucket会被分成两个, 两个不同的slot分别指向两个Bucket. - 重新分配原来的Hash的Directionaries, 更新Hash 函数.
使用实例
我们用下面的例子来说明可扩展Hash的工作流程.
假设我们的插入的元素形成的 Hash值 的序列为 [16,4,6,22,24,10,31,7,9,20,26].
我们的 Bucket Size 大小为 3.
从 Hash值 的序列到目录的Index使用的是 X LSBs函数. 使用二进制的最后的X位表示目录的Index.
首先, 我们得到上述序列的二进制表示, 如下:
16- 10000
4- 00100
6- 00110
22- 10110
24- 11000
10- 01010
31- 11111
7- 00111
9- 01001
20- 10100
26- 11010
初始状态如下图的图1所示:
- 初始状态下, Global Depth 与 Local Depth 均为 1. 还没有元素存入到Bucket中.
- 当我们顺序插入三个元素后, 16, 4, 6, 它们二进制的最后一位都是0, 所以插入第0个Bucket(Local Depth为1), 继续插入元素
- 当插入元素 22 之后, Bucket的大小为4, 超过Bucket的大小限制, 该Bucket分裂, 一分为二. 分裂之后, 分裂的两个Bucket的Local Depth为2, 大于Global Depth, Global Depth也需要加一, 变为2, 然后目录 Directories 扩充, 变为原来的两倍.
- 由于分裂的Bucket的Local Depth为2, 所以使用Index的两位作为索引, 因此00和10分别指向分裂的两个Bucket, 分裂之前的Bucket的Index为0, 新增一位后就是 00 和 10. 原来Index为 1的Bucket没有分裂, 它的Local Depth为1, 所以 01 和 11 的Index均指向该Bucket.
- 分裂之后我们继续写入, 该示例中, 在下图的图2到图3的过程中, 当插入到元素20的时候, Index
00
指向的Bucket又超过了Bucket的限制大小, Bucket分裂, 一分为二. 此时 Local Depth增加到3, 所以Directories扩充, 变成原来的两倍.000
指向分裂的第一个Bucket,100
指向分裂的第二个Bucket. 原来Local Depth为2的Bucket会有两个slot指向它, 分别是010
和110
. 在这个目录的所有slot中, 最后一位为 1 的slot均指向最后一个Bucket, 它的Local Depth为 1. - 接下来从图4到图5就很容易解释了, Local Depth 增加, 但是小于等于Global Depth, 所以可以直接修改Directories 的slot的Index的指向, 而实际上, Hash函数也需要做对应的修改.
总结
上述是我对可扩展Hash的理解, 我参考了一些文章, 总结来说, 可扩展Hash的Hash值计算部分和普通的 Hash表是一样的, 不同点在于可扩展 Hash 计算得到Hash值是作为一个索引, 用于存储键值对, 也就是说数据存储的构造方式不同, 这也就是为什么可扩展 Hash 可以用于数据库存储.