1、动态集合结构,它至少要支持 INSERT、SEARCH 和 DELETE字典操作
散列表 是实现字典操作的 一种有效的数据结构。尽管 最坏情况下,散列表中 查找一个元素的时间 与链表中 查找的时间相同,达到了 Θ(n)。在实际应用中,散列表的性能 是极好的。在一些 合理的假设下,在 散列表中 查找一个元素的平均时间是 O(1)
2、散列表是 普通数组概念的推广。如果 存储空间允许,可以 提供一个数组,为每个可能的 关键字 保留一个位置,以利用 直接寻址的技术优势
当 实际存储的 关键字数目 比全部的可能关键字 总数要小时,采用 散列表就成为 直接数组寻址的 一种有效替代,因为 散列表使用一个 长度与实际存储的 关键字数目 成比例的数组来 存储
在散列表中,不是 直接把关键字 作为 数组的下标,而是 根据关键字 计算出 相应的下标
1、直接寻址表
1、当关键字的全域U 比较小时,直接寻址 是一种简单而有效的技术
2、用一个数组,或 称为 直接寻址表,记为T[0…m-1]。其中 每个位置,或称为槽,对应全域U中的一个关键字。槽k指向集合中 一个关键字为k的元素。如果 该集合中没有关键字为k 的元素,则 T[k] = NIL
几个字典操作
DIRECT-ADDRESS-SEARCH(T, k)
return T[k]
DIRECT-ADDRESS-INSERT(T, x)
T[x.key] = x
DIRECT-ADDRESS-DELETE(T, x)
T[x.key] = NIL
直接寻址表本身就可以 存放动态集合中的元素。直接 把该对象存放在表的槽中,从而节省了空间。我们使用 对象内的一个特殊关键字 来表明该槽为空槽(比如-1)
3、假设一动态集合S 用一个长度为m的直接寻址表T 来表示。给出 一个查找S中最大元素的过程
DIRECT-ADDRESS-MAXIMUM(T)
max = -∞
for i = 1 to m
if T[i] ≠ NIL and T[i].key > max
max = T[i].key
j = i
return T[j]
过程 在最坏情况下的运行时间是 O(m)
4、位向量 是一个仅包含0和1的数组
2、散列表
1、直接寻址技术的缺点:如果 全域U很大,要存储大小为 |U| 的一张表T 也许不太实际
如果 实际存储的关键字集合K 相对U来说可能很小,使得 分配给T的大部分空间 都将被浪费掉
2、当存储在字典中的 关键字集合K 比所有可能的关键字的全域U 要小许多时,散列表 需要的存储空间 要比直接寻址表 少得多。将散列表的存储需求 降至 Θ(|K|),同时 散列表中查找一个元素的优势 仍得到保持,只需要 O(1)的时间。问题是 这个界 是针对 平均情况时间的,而对于 直接寻址 来说,它是 适用于 最坏情况时间的
3、在 直接寻址 方式下,具有 关键字k的元素 被存放在 槽k中。在 散列方式下,该元素 被放在h(k)中;即 利用 散列函数h,由关键字k 计算出 槽的位置。函数h 将关键字的全域U 映射到 散列表 T[0…m - 1]的槽位上
可以说 一个具有关键字k的元素 被散列到 槽 h(k) 上,也可以说 h(t) 是关键字的散列值。即 减少了 数组的大小,使其 由|U|减少为m
4、两个关键字 可能映射到 同一个槽中,我们称 这种情形为冲突
可以试图 选择一个适合的散列函数h 来做到 避免冲突
一个想法就是 使h尽可能的“随机”。但是 |U|>m,故至少 有两个关键字 其散列值 相同,所以 要想完全 避免冲突 是不可能的
一方面 可以通过 精心设计的散列函数 来尽量减少 冲突的次数,另一方面 仍需要 有解决 可能出现冲突的方法
5、最简单的冲突解决方法:链接法
在 链接法中,把 散列到 同一槽中的所有元素 都放在 一个链表中,槽j中有 一个指针,它指向 存储所有散列到j的元素的 链表的表头;如果 不存在 这样的元素,则 槽j中为NIL
CHAINED-HASH-INSERT(T, x)
insert x at the head of list T[h(x.key)]
CHAINED-HASH-SEARCH(T, k)
search for an element with key k in list T[h(k)]
CHAINED-HASH-DELETE(T, x)
delete x from the list T[h(x.key)]
插入操作的最坏情况 运行时间为 O(1)。查找操作的最坏情况运行时间 与表的长度成正比
6、如果散列表中的链表是 双向链接的,则删除一个元素x的操作 可以在 O(1) 时间内完成
CHAINED-HASH-DELETE 以元素x 而不是它的关键字k 作为输入,所以无需 先搜索x。如果散列表支持 删除操作,则为了能够更快地删除 某一元素,应该将其链表设计为 双向链接的(可以直接 找到前驱)
如果表是 单链接的,则为了删除元素x,我们首先 必须在表 T[h(x.key)] 中找到元素x,然后通过 更改x前驱元素的next属性,把x从链表中删除
7、链接法 散列分析:给定 一个能存放n个元素的、具有 m个槽位的散列表T,定义T的装载因子α为 n/m,即 一个链的平均存储元素数
用链接法散列的最坏情况 性能很差:所有的两个关键字 都散列到同一个槽中,从而产生出 一个长度为n的链表。这时,最坏情况下 查找的时间为 Θ(n),再加上 计算散列函数的时间
8、散列方法的平均性能 依赖于 所选取的散列函数h,将所有的关键字集合 分布在m个槽位上的均匀程度
先假定 任何一个给定元素 等可能地散列到m个槽位中的 任何一个(等可能),且与 其他元素被散列到什么位置上 无关(独立),我们称这个假设为 简单均匀散列
列表 T[j] 的长度 用nj表示,有
并且 nj 的期望值为 E[nj] = a = n/m
假定可以在 O(1) 时间内 计算出散列值 h(k),从而 查找关键字为k的元素的时间线性地依赖于 表T[A(k)] 的长度 nh(k)
分两种情况来考虑。在第一种情况中,查找不成功:表中没有一个元素的关键字为k。在第二种情况中,成功地查找到 关键字为k的元素
9、在简单均匀散列的假设下,对于 用链接法解决冲突的散列表,一次不成功查找的平均时间为 Θ(1+α)
证明:当查找一个关键字时,在不成功的情况下,查找的期望时间 就是查找至 链表 T[h(k)] 末尾的期望时间,这一时间的期望长度为E[nh(k)] = α,于是,一次不成功的查找 平均要检查α个元素,并且所需要的总时间(包括计算 h(k) 的时间)为 Θ(1+α)
对于成功的查找来说,情况略有不同,这是因为每个链表 并不是等可能地被查找到的。某个链表被查找到的概率与它所包含的元素数成正比
期望的查找时间仍然是 Θ(1+α)
10、在简单均匀散列的假设下,对于 用链接法解决冲变的散列表,一次成功查找 所需的平均时间为 Θ(1+α)
证明:在对元素x的 一次成功查找中,所检查的元素数 就是x所在的链表中x前面的元素 多1。新的元素 都是在表头插入的,所以 出现在x之前的元素 都是在x之后插入的。在 简单均匀散列的假设下,有 Pr{h(ki) = h(kj)} = 1/m,有 E[Xij] = 1/m
于是,在一次成功的查找中,所检查元素的期望数目 为
一次成功的查找 所需要的全部时间(包括计算散列函数的时间)为 Θ(2 + α / 2 - α / 2n) = Θ(1 + α)
如果散列表中 槽数 至少与表中的元素数 成正比,则有 n=O(m),从而 a = n / m = O(m) / m = O(1) 。所以,查找操作平均需要常数时间。当链表采用双向链接时,插入操作 在最坏情况下 需要 O(1)时间,删除操作 最坏情况下 也需要 O(1) 时间,因而,全部的字典操作 平均情况下 都可以在 O(1) 时间内完成
11、假设 采用的是 简单均匀散列,对关键字k和l,定义指示器随机变量 Xkl= Ⅰ{h(k)=h(l)}。在简单均匀散列的假设下,有Pr{h(k)=h(l)}=1/m,从而 有E[Xkl] = 1/m。于是,集合{{k,l}:k≠l,且h(k)=h(l)}基的期望值是
12、假设将n个关键字存储到一个大小为m 且通过链接法解决冲突的散列表中,关键字 均源于 全域U,且 |U| > nm
因为 |U| > nm,所以当将全域U中的所有关键字存储到一个大小为 m 的散列表中时,每个槽位中至少有 n 个关键字。因此,U中有一个大小为n的子集,其由散列到 同一槽位中的所有关键字构成,使得链接法散列的查找时间 最坏情况下为 Θ(n)
3、散列函数
1、其中的两种方法 (用除法进行散列和用乘法进行散列)本质上 属于启发式方法,而第三种方法(全域散列)则利用了 随机技术 来提供 可证明的良好性能
2、好的散列函数的特点:一个好的 散列函数应(近似地)满足 简单均匀散列假设, 遗憾的是,一般 无法检查这一条件 是否成立,因为 很少能知道 关键字散列 所满足的概率分布,而且 各关键字 可能并不是完全独立的
常常 可以运用启发式方法 来构造 性能好的散列函数。设计过程中,可以利用 关键字分布的有用信息
一些很相近的符号 经常会出现在 同一个程序中,如 pt 和 pts。好的散列函数 应能将这些相近符号 散列到 相同槽中的可能性 最小化
一种好的方法导出的 散列值,在 某种程度上 应独立于 数据可能存在的任何模式
注意到 散列函数的某些应用 可能会 要求比简单均匀散列更强的性质。例如,可能希望 某些很近似的关键字 具有截然不同的散列值
3、将关键字 转换为 自然数:多数散列函数 都假定关键字的全域为 自然数集 N = {0,1,2…}。因此,如果所给关键字不是自然数,就需要 找到一种方法来 将它们转换为自然数。例如,一个字符串 可以被 转换为按适当的基数符号表示的整数
这样,就可以将标识符pt 转换为 十进制整数对(112,116),这是因为 在 ASCII字符集中:p = 112,t = 116。然后,以128为基数(二进制转十进制 就是以2为基数的) 来表示,pt 即为(112×128)+116 = 14452
假定所给的关键字都是自然数
3.1 除法散列法
1、通过 k除以m的余数,将关键字k 映射到m个槽中的某一个上,即散列函数为:
当应用除法散列法时,要避免选择m的某些值。例如,m 不应为2的幂,因为如果m=2p,则h(k)就是 k的p个最低位数字,除非 已知各种最低p位的排列形式 为等可能的
当 k是一个按基数2p表示的字符串时,选 m = 2p - 1 可能是一个槽糕的选择
如果 串x可 由串y通过其自身的字符置换排列 导出,则 x和y具有 相同的散列值
证明:
用 除法散列表 来计算 一个字符串的散列值,如何 才能在除了 该串本身 占用的空间外,只利用 常数个机器字
在 模运算下,加法和乘法 都满足分配律,这样 可以在乘法过程中 保持结果的大小 在合适范围内
设字符串x表示成以2p为基数的数为 k = a1 a2 … ar,根据上一题的结果,
因为 ai 在 0~2p-2 之间,所以 mod(2p-1) 可以直接去
2、一个 不太接近2的整数幂的素数,常常 是m的一个较好的选择。例如,假定 我们要分配—张散列表 并用链接法解决冲突,表中 大约要存放2000个字符串,其中的每个字符 有8位。如果我们 不介意一次不成功的查找 需要平均检查3个元素,这样分配散列表的大小为 m=701(它是一个 接近 2000/3 但又不接近2的任何次幂的素数)
散列函数为:h(k) = k mod 701
3.2 乘法散列法
1、乘法散列法 包含两个步骤,第一步,用关键字k乘上常数A(0<A<1),并提取 kA 的小数部分。第二步,用m乘以这个值,再向下取整
乘法散列法 的一个优点是 对m的选择不是特别关键,一般选择它 为2的某个幂次(m=2p,p为某个整数),这是因为我们可以在大多数计算机上 按下面所示方法 较容易地实现 散列函数
假设 某计算机的字长为w位,而k正好可用 一个单字表示。限制A为形如 s/2w 的一个分数,其中s是一个 取自 0<s<2w 的整数
先用w位整数 s = A * 2w (左移一个字长)乘上k,其结果 是一个 2w 位的值 r1*2w + r0,这里 r1 为乘积的高位字,r0 为乘积的低位字。所求的 p位散列值中,包含了 r0的p个最高有效位(m = 2p)
虽然这个方法 对任何的A值 都适用,但对 某些值的效果更好。最佳选择 与 待散列的数据的特征有关
假设 k = 123456,p = 14,m = 214 = 16384,且 w = 32。取A为形如 s/232 的分数,它与 (√5-1) / 2 最为接近,于是 A = 2654435769 / 2 (s = 2654435769),那么,k x s = 327706022297664 = (76300 X 232) + 17612864,从而有 r1 = 76300 和 r0 = 17612864。r0 的 14个最高位 产生了 散列值 h(k) = 67(将17612864转成 二进制,并在前面加上7个零 凑够32位,取前14位 就是67)
3.3 全域散列法
1、将 n个关键字 全部 散列到 同一个槽中,使得平均的检索时间为 Θ(x)。任何一个特定的散列函数 都可能出现 这种令人恐怖的最坏情况。唯一有效的改进方法是 随机地选择散列函数,使之独立于 要存储的关键字。这种方法称为全域散列,不管 选择了怎么样的关键字,其平均性能都很好
2、全域散列法 在执行开始时 就从一组精心设计的函数中,随机地选择一个 作为散列函数。就像 在快速排序中 一样,随机化 保证了 没有哪一种输入 会始终导致 最坏情况性能
算法 在每一次执行时 都会有所不同,甚至对于相同的输入 都会如此。这样就可以确保 对于任何输入,算法都具有 较好的平均情况性能
设 H为一组 有限散列函数,它将给定的关键字的全域U 映射到 {0, 1, …, m-1} 中,这样的一个函数组 为全域的。如果 从H中 随机地选择 一个散列函数,当 关键字 k!=l 时,两者发生冲突的概率 不大于 1/m,这也是 正好从集合 {0,1,…,m - 1} 中 独立地随机选择 h(k) 和 h(l) 时 发生冲突的概率
3、ni 表示链表 T[i] 的长度。h选自 一组全域散列函数。如果 关键字k不在表中,则 k被散列至 其中的链表的期望长度 E[nh(k)] 至多为 α = n/m。如果 关键字k在表中,则 包含关键字k的链表的 期望长度 E[nh(k)] 至多为 1+α
证明:期望值 与 散列函数的选择有关,且 不依赖于 任何有关 关键字分布的假设。因为 由 全域散列函数的定义,一对 关键字发生冲突的概率 至多为 1/m,有 Pr{h(k) = h(l)} <= 1/m,所以有 E[Xkl] <= 1/m
对于 每个关键字k,定义 随机变量Yk,它表示 与k散列到 同一槽位中的 非k的 其他关键字的数目
余下部分 按关键字k是否在表T中,分情况讨论
- 如果 k!∈T,则 nh(k) = Yk,并且 |{l:l∈T 且 l!=k}| = n。于是,E[nh(k)] = E[Yk] <= n / m = α
- 如果 k∈T,由于 关键字k出现在 链表 T[h(k)] 中,且 计数Yk中 并没有包括关键字k,所以 nh(k) = Yk + 1,并且 |{l:l∈T 且 l != k}| = n - 1。于是 E[nh(k)] = E[Yk] + 1 <= (n - 1) / m + 1 = 1 + α - 1/m < 1 + α
已经无法通过选择一个操作序列 来迫使达到 最坏情况运行时间了
4、对于 一个具有m个槽位,且初始时为空的表,利用 全域散列法 和 链接法解决冲突,需要 Θ(n) 的期望时间 来处理任何包含了n个 INSERT、SEARCH和DELETE的操作序列,其中 该序列包含了 O(m) 个INSERT操作
证明:
在全域散列法和链接法中,解决冲突的时间复杂度取决于散列函数的质量和表的装载因子。在这种情况下,我们假设:
散列函数是良好设计的,并且在平均情况下能够均匀地将元素分布到表的不同槽位中。
表的装载因子是 Θ(1) 的,即元素数量与表的大小之比是常数。
在这样的假设下,全域散列法和链接法解决冲突的平均时间复杂度是 Θ(1)。
对于链接法(也称为开放地址法):
在平均情况下,对于一个给定的槽位,搜索或删除一个元素的时间复杂度是 Θ(1),因为每个槽位是一个链表,查找或删除一个元素只需要遍历链表
在 INSERT 操作中,我们需要计算元素的哈希值,然后将其插入到对应槽位的链表中。由于散列函数是均匀的,每个槽位的链表平均长度为 Θ(n/m),因此插入的平均时间复杂度是 Θ(1)。
因此,链接法的平均时间复杂度是 Θ(1)
对于全域散列法:
在平均情况下,搜索或删除一个元素的时间复杂度也是 Θ(1),因为我们可以直接计算出元素所在的槽位
在 INSERT 操作中,我们需要计算元素的哈希值,并找到对应的槽位。由于散列函数是均匀的,每个槽位平均只包含 Θ(n/m) 个元素,因此插入的平均时间复杂度是 Θ(1)。
因此,全域散列法的平均时间复杂度也是 Θ(1)
综上所述,无论是链接法还是全域散列法,对于一个具有 m 个槽位的哈希表,在平均情况下,处理包含了 n 个 INSERT、SEARCH 和 DELETE 操作的序列的时间复杂度都是 Θ(1),所以 整个n个操作序列的期望时间 为 Θ(n)
5、设计一个全域散列函数类:设 Zp 表示集合 {0,1,…,p - 1},Zp* 表示集合 {1,2,…,p - 1},由于 p是一个素数
对于 任何 a∈Zp* 和 任何 b∈Zp,定义 散列函数 hab
散列函数 构成的 函数簇为
这个函数簇 是全域的
一个散列函数被称为全域散列函数,如果它满足以下两个性质:
1)均匀性:对于任意不同的输入键,散列函数产生的哈希值在哈希表中的每个槽位中出现的概率相等。换句话说,对于任意两个不同的键 𝑘1 和 𝑘2,如果哈希函数 ℎ是全域散列函数,则满足:Pr[h(k1) = h(k2)] = 1/m
其中 m 是哈希表的大小,Pr[⋅] 表示概率
2)独立性:全域散列函数的输出 在给定一个键的情况下是不可预测的,并且 与其他键的哈希值无关。换句话说,对于一个给定的键 k 和任意给定的哈希值 𝑦,如果哈希函数 ℎ是全域散列函数,则满足:Pr[h(k) = y] = 1 / m
其中 𝑚 是哈希表的大小
全域散列函数的均匀性和独立性保证了在散列过程中,每个键被哈希到哈希表的每个槽位的概率是相等的,并且每个键的哈希值都是不可预测的。这样可以最大程度地减少冲突,提高哈希表的性能
证明这个函数簇 是全域的:
可以导出 r != s,因为 p是素数,且 a和(k - l)模p的结果不为0,所以 它们乘积模p后 也不为0。所以 计算任何 hab∈Hpm,不同的输入k和l会被映射到 不同的值r和s(模p)(r != s);在模p层次上,不会产生冲突(线性函数,一一对应)此外,数对(a, b)(a != 0) 有 p(p - 1)中可能的选择。其中的每一种 都会产生一个 不同的结果数对 (r, s) (r != s)
解出 a和b
因为 (r, s) 有p(p - 1)种可能,所以 数对(a, b) 和 数对(r, s)之间 存在一一对应的关系。对 任意给定的输入对 k和l,如果 从 Zp* × Zp 中均匀地 随机选择(a, b),则 结果数对 (r, s) 就等可能地 为任何不同的数值对(模p)
当 r和s为 随机选择的不同的值(模p)时,不同的关键字k和l发生冲突的概率 等于 r ≡ s(mod m) 的概率。对于 某个给定的r值,s的可能取值 就为余下的 p - 1 种,其中 满足 s != r 且 s ≡ r(mod m) 的s值的数目至多为(s与r之差 正好是m的倍数)
当模m进行 归约时,s与r发生冲突的概率 至多为 ((p - 1) / m) / (p - 1) = 1 / m
6、查找的时候 怎么确定关键字使用的是 哈希函数族中的哪个哈希函数
在构建好哈希表后,可以使用 相同的哈希函数来 执行查找操作。由于哈希函数在构建哈希表时 已经确定了,在构建的时候 参数的值 会随着一起保存,因此在查找时 不需要再确定关键字 使用的是哪个哈希函数。相反,只需根据哈希函数的定义 将关键字哈希到哈希表中的相应槽位上,然后执行相应的查找操作即可
在全域散列法中,参数 a 和 b 通常是选定一个固定的范围,并且对于每个不同的关键字都随机选择一组 a 和 𝑏的值。换句话说,对于哈希函数族中的每个哈希函数 h(a, b),都会为 每个不同的关键字选择不同的 a 和 b 的值
4、开放寻址法
1、在 开放寻址法中,所有的元素 都存放在 散列表里。每个表项 或包含动态集合的一个元素,或包含 NIL;其 装载因子α绝对不会超过1
也可以 将用作 链接的链表 存放在 散列表未用的槽中,但 开放寻址法的好处 就是 它不用使用指针,而是 计算出 要存取的槽序列。不用 存储指针 而节省空间,使得 可以用 同样的空间 来提供更多的槽,潜在地减小了冲突,提高了 检索速度
2、为了 使用开放寻址法 插入一个元素,需要 连续地检查 散列表,或 称为探查,直到 找到一个空槽 来放置 待插入的关键字为止
对于 每一个关键字k,使用开放寻址法的探查序列
h(k, 1) 为第一个备用…
使得 当散列表 逐渐填满时,每一个表位 最终都可以 被考虑为用来 插入新关键字的槽
查找过程中碰到一个空槽时,查找算法 就(非成功地)停止,因为 如果在表中,它就应该在此处,而不会 在探查序列随后的位置上
从开放寻址法的散列表中 删除操作元素 比较困难。当我们从槽i中 删除关键字时,不能仅将 NIL置于 其中来标识它为空,如果这样做,就会有问题:在插人关键字k时,发现槽i被占用了,则就被插人到后面的位置上;此时将i中的关键字删除后,就无法检索到关键字了(到空就停)
在槽i中置一个特定的值DELETED替代NIL来标记该槽,这样 就要对过程HASH-INSERT做相应的修改,将这样的一个槽当做空槽,使得在此 仍然可以插人新的关键字。对HASH-SEARCH无需做什么改动,因为它在搜索时会绕过DELETED标识。但是,当我们使用特殊的值DELETED时,查找时间就 不再依赖于装载因子了,为此,在必须删除关键字的应用中,更常见的做法是采用链接法来解决冲突
由于删除操作 不会改变表的大小,因此装载因子𝑎 不再影响 查找操作的性能。在使用开放寻址法时,查找操作的性能 取决于 表中空槽位的数量,而不仅仅是 已插入元素的数量。它取决于表中空槽位的数量,即 1−a,因为空槽位的数量越多,冲突的可能性就越小,查找操作的性能就越好
3、做一个均匀散列 的假设:每个关键字的探查序列 等可能地为(0,1,…,m-1)的m!种排列中 的任一种。均匀散列 将前面定义过的简单均匀散列的概念 加以了一般化,推广到散列函数的结果 不只是一个数,而是 一个完整的探查序列
有三种技术 常用来 计算开放定址法中的探查序列:线性探查、二次探查 和 双重探查
这些技术 都不能满足 均匀散列的假设,因为 他们能产生的 不同探查序列数 都不超过m2个(均匀散列 要求有 m! 个探查序列)。双重散列 产生的探查序列数最多,似乎能 给出最好的结果
4.1 线性探查
1、给定 一个普通的散列函数 h’:U->{0, 1, …, m - 1},称之为 辅助散列函数,线性探查 采用的散列函数为
对于 关键字k,首先 探查槽 T[h’(k)],即由 辅助散列函数 所给出的槽位,再 探查槽 T[h’(k) + 1],依次类推,直到槽 T[m - 1]。然后,又绕到槽 T[0],T[1],…,直到 最后探查到槽 T[h’(k) - 1]。在 线性探查方法中,初始探查位置 决定了 整个序列,故 只有m种 不同的探查序列
2、线性探查 存在一个问题,称为 一次群集。随着 连续被占用的槽 不断增加,平均查找时间 也随之不断增加。因为 当一个空槽前 有i个满的槽,该空槽 下一个将被占用的概率是 (i + 1) / m。连续被占用的槽 就会变得越来越长,因而 平均查找时间 也会越来越大
4.2 二次探查
1、散列函数:
h’ 是 一个辅助散列函数,c1和c2 为正的辅助常数,i = 0,1,…,m - 1。初始的探查位置为 T[h’(k)]。后续的探查位置 要加上一个偏移量,该偏移量以二次的方式 依赖于探查序号i。这种探查方法的效果 要比 线性探查好得多(连续被占用的槽 就会变得越来越长的情况 会缓解)
2、如果 两个关键字的初始探查位置 相同,那么它们的探查序列 也是相同的,这是因为 h(k1, 0) = h(k2, 0)蕴涵着 h(k1, i) = h(k2, i)。
这一性质 可导致 一种轻度的群集,称为二次群集。像在线性探查中一样,初始探查位置 决定了 整个序列,这样 也仅有m个不同的探查序列被用到
4.3 双重散列
1、双重散列 是用于 开放寻址法的最好方法之一,因为 它所产生的排列 具有随机选择排列的许多特性。散列函数
初始探查位置为 T[h1(k)],后续的探查位置 是前一个位置 加上偏移量h2(k)模m。因此,不像 线性探查 或 二次探查,这里的探查序列 以两种不同方式 依赖于关键字k,因为 初始探查位置、偏移量 或者 二者 都可能发生变化
2、为了 能查找整个散列表,值h2(k) 必须要 与表的大小m 互素(两个整数的最大公约数为1)。有一种简便的方法 确保这个条件成立,就是取m为2的幂,并设计 一个总产生奇数的h2。另一种方法是 取m为素数,并设计一个 总是返回较m小的正整数的函数h2
如果 k = 123456,m = 701,m’ = 700,则有 h1(k) = 80,h2(k) = 257
当m为素数 或者 2的幂时,双重散列法中 用到了 Θ(m2) 种探查序列,而 线性探查 或 二次探查中 用了 Θ(m) 种
因为 每一对可能的 (h1(k), h2(k)) 都会产生 一个不同的探查序列。因此,对于m的每一种可能取值,双重散列的性能 看起来 就非
常接近“理想的”均匀散列的性能
尽管 除素数和2的幂以外的m值 在理论上 也能用于双重散列中,但是在实际中,要高效
地产生 h2(k) 确保使其与m互素 很困难。部分原因是 这些数的相对密度 ɸ(m) / m 可能比较小
3、开放寻址散列的分析:像在链接法中的分析 一样,开放寻址法的分析 也是 以散列表的装载因子 α = n / m 来表达的
当然,使用开放寻址法,每个槽中 至多只有一个元素,因而 n <= m,也就意味着 α ≤ 1
每一种 探查序列 都是等可能的:
给定一个装载因子为 a = n/m ≤ 1 的开放寻址散列表,并假设 是均匀散列的,则 对于一次不成功的查找,其期望的探查次数 至多为 1 / (1 - a)
证:在不成功的查找中,除了 最后一次探查,每一次 探查都要检查 一个被占用 但并不包含 所求关键字的槽,最后检查的槽 是空的。先定义 随机变量X 为一次不成功的探查次数,再定义事件 Ai(i = 1, 2, …) 为 第i次探查 且探查到的是 一个已经被占用的槽。事件 {X>=i} 即为 事件 A1∩A2∩…∩Ai - 1的交集
由于 有n个元素和m个槽,所以 Pr{A1} = n / m。在前j - 1次探查到的 都是已经占用槽的前提下,第j次探查 且探查到的仍是 已占用槽的概率是 (n - j + 1) / (m - j + 1)。因为要在 (m - (j - 1)) 个未探查的槽中,查找 余下的 (n - (j - 1)) 个元素中的某一个。注意到 n<m,对于 所有j(0 <= j < m),就有 (n - j) / (m - j) <= n/m。
等比计算公式
4、假设 采用的是 均匀散列,平均情况下,向一个装载因子为α的开放寻址散列表中 插入一个元素 至多需要做 1/(1 - α) 次探查
证明:只有当表中 有空槽时,才可以 插入新元素,故 α<1。插入一个关键字 要先做一次 不成功的查找,然后 将该关键字置入 第一个遇到的空槽中,所以 跟不成功的查找一样,期望的探查次数 至多为 1/(1 - α)
5、对于一个装载因子 为 α<1 的开放寻址散列表,一次成功查找中的探查期望数 至多为
假设采用均匀散列,且表中的每个关键字被查找的可能性 是相同的
证明:根据4,如果 k是第 i+1 个被插入表中的关键字,则 对k的一次查找中,探查的期望次数 至多为 1/(1 - i / m) = m / (m - i),对散列表中 所有n个关键字 求平均,则得到一次成功查找的 探查期望次数为
综合 3,5:
当装载因子为3/4 和 7/8 时,一次不成功查找 的探查期望数上界 分别为4和8,一次成功查找 的探查期望数上界 分别为 4/3 ln4
和 8/7 ln8
6、写出 HASH-DELETE 的伪代码;修改 HASH-INSERT,使之能处理特殊值 DELETED
HASH-DELETE(T, k)
for i = 0 to m-1
j = h(k, i)
if T[j] == k
T[j] = DELETED
return
HASH-INSERT(T, k)
i = 0
repeat
j = h(k, i)
if T[j] == NIL or T[j] == DELETED // 区别
T[j] = k
return j
else i = i + 1
until i == m
error "hash table overflow"
5、完全散列
1、使用散列技术 通常 是个好的选择,不仅是 因为它有优异的平均情况性能,而且 当关键字集合是静态时,散列技术 也能提供出色的最坏情况性能。所谓静态,就是指 一旦各关键字 存入表中,关键字集合 就不再变化了
2、一种散列方法 称为完全散列,如果 该方法进行查找时,能在 最坏情况下 用 O(1) 次访存完成
采用两级的散列方法 来设计完全散列方案,在每级上 都使用全域散列
第一级与 带链接的散列表基本上是一样的:利用 从某一全域散列函数族中 仔细选出的一个 散列函数h,将n个关键字 散列到 m个槽中
然后 采用了 一个较小的二次散列表 Sj 及 相关的散列函数 hj,利用 精心选择的散列函数hj,可以确保 在第二级上 不出现冲突
为了确保 在第二级上 不出现冲突,需要 让散列表 Sj 的大小 mj 为散列到槽j中的关键字数 nj 的平方,尽管 mj 对 nj 的这种二次依赖 看上去可能使得总体存储需求很大,通过 适当地选择 第一级散列函数,可以将 预期使用的总体存储空间 限制为 O(n)
3、如果 从一个全域散列函数类中 随机选出 散列函数h,将 n个关键字 存储在 一个大小为 m = n2 的散列表中,那么表中 出现冲突的概率 小于 1/2
证明 共有 Cn2 对关键字 可能发生冲突;如果A 是从一个全域散列函数类H 中随机选出,那么 每一对关键字冲突的概率为 1 / m。当 m = n2 时,期望的冲突次数为
运用 马尔可夫不等式,
4、下面的定理 和 一个推论给出了所有二级散列表的大小加起来后 的期望值的界,第二个推论 给出了所有二级散列表的大小加起来后超过线性时的概率的一个上界(实际上,后面的证明中,超过线性是指等于或大于4n)
1)定理:如果 从某一个全域散列函数类中 随机选出 散列函数h,用它将n个关键字 存储到 一个大小为 m = n 的散列表中,则有
这里 nj 为散列 到槽j中的关键字数
证明:从下面的恒等式开始,这个等式 对任何非负的整数a成立
所以有
里面涉及到 加法原理
2)推论1:如果 从某一全域散列函数类中 随机选出散列函数h,用它 将n个关键字 存储到 一个大小为 m = n 的散列表中,并将每个二次散列表的大小 设置为 mj = (nj)2(j = 0,1,…,m-1),则 在一个完全散列方案中,存储 所有二次散列表 所需的存储总量的期望值 小于2n
证明:由 (1)
3)推论2:如果 从某一全域散列函数类中 随机选出散列函数h,用它 将n个关键字 存储到 一个大小为 m = n 的散列表中,并将 每个二级散列表的大小 置为 mj = (nj)2 (j = 0, 1, …, m - 1),则 用于存储 所有二级散列表的存储总量 等于 或 大于 4n的概率小于 1/2
证明:用 马尔可夫不等式,即 Pr{X >= t} <= E[X] / t。并将 入 推论1中 不等式
从 推论2 可得,只需 从全域散列函数类中 随机选出 几个散列函数,尝试几次 就可以快速找到 一个所需存储量 较为合理的函数