字典通常可以用三种数据类型表示:线性表,跳表,Hash
表。
Hash
表又称为散列表,使用一个散列函数把字典的数对映射到一个散列表的具体位置。如果数对p的关键字是k,散列函数为f,那么在理想情况下,p在散列表中的位置为f(k)。暂时假定散列表的每一个位置最多能够存储一个记录。为了搜索关键字为k的数对,先要计算f(k),然后查看在散列表的*f(k)处是否已有一个数对。如果有,便找到该数对。如果没有,字典就不包含该数对。在前一种情况下,可以删除该数对,为此只需使散列表的f(k)位置为空。在后一种情况下,可以把该数对插入f(k)*的位置。
0x01 bucket 和 home bucket
当关键字的范围太大,不能用理想方法表示时,可以采用并不理想的散列表和散列函数。散列表位置的数量比关键字的个数少,散列函数把若干不同的关键字映射到散列表的同一位置。散列表的每一个位置称为一个bucket;对关键字为k 的数对,f(k) 是home bucket;bucket的数量等同于散列表的长度。因为散列函数可以把若干个关键字映射到同一个bucket,所以bucket要能够容纳多个数对。
0x02 除法散列函数
在多种散列函数中,最常用的就是除法散列函数,它的形式如下:
- f ( k ) = k % D f(k) =k\%D f(k)=k%D
其中k是关键字,D是散列表长度。
0x03 冲突与溢出
我们先考虑一种情况,每个bucket只能存储一个数对。现在有这样一个散列表,它有11个bucket,序号从0到10。
0 1 2 3 4 5 6 7 8 9 10
80 40 65
D为11。我们很容易就可以计算出 80 % 11 = 3 80\%11=3 80%11=3, 40 % 11 = 7 40\%11=7 40%11=7, 65 % 11 = 10 65\%11=10 65%11=10。其余的bucket为空。
现在我们要插入58
,那么我们通过计算 58 % 11 = 3 58\%11=3 58%11=3,但是这个bucket已经有一个数了。当两个不同的关键字所对应的home bucket相同,这个时候就发生了冲突。因为我们这个例子中的bucket只能存放一个数对,所以这个时候同时发生了溢出,但是如果我们的bucket可以存放多个数对的话,就不会发生溢出。
我们可以通过线性探查(找到下一个可用的bucket)的方法解决这个问题。我们将58
加入到4号bucket。通过这种方法,我们将散列表看成是一个环形表。例如,对于 98 % 11 = 10 98\%11=10 98%11=10,我们将98
加入到0号bucket。
0x04 一个好的散列函数
我们很容易就能想明白的一点是:冲突不可怕,可怕的是溢出。除非每个bucket可以存放无限个数对,否则插入时发生溢出就是一个很难解决的问题。当映射到散列表中任何一个bucket里的关键字数量大致相等时,冲突和溢出发生的平均次数最少。基于这一点,我们就有了均匀散列函数。
- 假定散列有b个bucket,且 b > 1 b > 1 b>1,bucket的序号从 0 到 b − 1 b-1 b−1。如果对所有的k,散列函数 f ( k ) = 0 f(k) = 0