11.2 散列表
11.2-1 假设用一个散列函数h将n个不同的关键字散列到一个长度为m的数组T中。假设采用的是简单均匀散列,那么期望的冲突数是多少?更准确地,集合{{k,l}:k ≠ l,且h(k) = h(l)}基的期望值是多少?
在简单均匀散列的情况下,假设我们有n个不同的关键字,将它们散列到一个长度为m的数组T中。每个关键字被散列到m个槽中的任意一个槽上是等概率的。
令C表示冲突的数量,即有两个不同的关键字散列到相同的槽中。我们可以使用指示器随机变量来表示冲突情况。定义指示器变量I_{kl},如果关键字k和关键字l发生冲突(即h(k) = h(l)),则I_{kl} = 1,否则为0。
我们可以写出冲突数C的表达式:
C=求和I_kl (1<=k<l<=n)
期望的冲突数E[C] 是指冲突数C的期望值。由于期望是线性的,我们可以将期望移到求和符号中:
E[C]=E[求和I_kl (1<=k<l<=n)]
然后,我们可以使用简单均匀散列的假设,即每对关键字发生冲突的概率是相同的:
E[C]=求和E[I_kl] (1<=k<l<=n)
因为每对关键字发生冲突的概率相同,所以我们只需要计算一对关键字的冲突概率,并乘以所有可能的一对关键字的数量。有n*(n-1)/2 种可能的一对关键字,所以:
E[C] = n*(n-1)/2 * E[I_12]
现在,考虑任意一对关键字k和l,它们发生冲突的概率是散列函数将它们映射到相同槽的概率。由于采用简单均匀散列,槽的选择是均匀的,所以任意一对关键字发生冲突的概率是 1/m。因此:
E[C]=n*(n-1)/2 * (1/m)
这就是期望的冲突数。
11.2-2 对于一个用链接法解决冲突的散列表,说明将关键字5,28,19,15,20,33,12,17,10插入到该表中的过程。设该表中有9个槽位,并设其散列函数为h(k) = k mod 9。
0:
1: 10 -> 19 -> 28
2: 20
3: 12
4:
5: 5
6: 33 -> 15
7:
8: 17
11.2-3 Marley教授做了这样一个假设,即如果将链模式改动一下,使得每个链表都能保持已排好序的顺序,散列的性能就可以有较大的提高。Marley教授的改动对成功查找、不成功查找、插入和删除操作的运行时间有何影响?
如果我们有 n 个元素和 m 个槽位,那么每个槽位上链表的平均长度是 m/n。这个值可以被视为加载因子(α)。
成功搜索(预期运行时间):
在排序链表中,成功搜索的预期运行时间是 Θ(1+log(α))。这是因为可以使用二分搜索在 log(α)的时间内找到目标元素,并且可能需要额外的常数时间来遍历链表。
失败搜索(预期运行时间):
对于在排序链表中的失败搜索,预期运行时间同样是 Θ(1+log(α))。这是因为失败搜索的过程类似于成功搜索,涉及使用二分搜索。
插入(预期运行时间):
在排序链表中进行插入的预期运行时间是 Θ(1+α)。这是因为在最坏的情况下,可能需要遍历整个链表找到插入位置,需要α的时间。
删除(预期运行时间):
同样地,在排序链表中进行删除的预期运行时间也是 Θ(1+α)。在最坏的情况下,可能需要遍历链表找到要删除的元素,需要α的时间。
11.2-4 说明在散列表内部,如何通过将所有未占用的槽位链接成一个自由链表,来分配和释放元素所占的存储空间。假定一个槽位可以储存一个标志、一个元素加上一个或两个指针。所有的字典和自由链表操作均应具有O(1)的期望运行时间。该自由链表需要是双向链表吗?或者,是不是单链表就足够了呢?
描述的方法涉及在哈希表的每个槽位中使用一个标志来指示相应的元素是否包含值。此外,一个双向链表被用作空闲链表。搜索操作保持不变,具有期望的 O(1)时间复杂度。我们来详细解释一下插入和删除的步骤:
插入操作:
要插入元素 x,首先检查 T[h(x.key)] 是否为空闲(标志为 0)。
如果为空闲,删除 T[h(x.key)] 并将 T[h(x.key)] 的标志更改为 1。
如果不为空闲,将 x.key 插入到存储在那里的双向链表的开头。
删除操作:
要删除元素 x,首先检查 x.prev 和 x.next 是否为 NIL。
如果是,这意味着在删除 x 后链表将为空。
在这种情况下,将 T[h(x.key)] 插入到空闲链表中,将 T[h(x.key)] 的标志更新为 0,并从其所存储的链表中删除 x。
由于从单链表中删除元素的操作不是 O(1),因此必须使用双向链表以有效地在删除过程中更新指针。
11.2-5 假设将一个具有n个关键字的集合存储到一个大小为m的散列表中。试说明如果这些关键字均来源与全域U,且|U|> n*m,则U中还有一个大小为n的子集,其由散列到同一槽位中的所有关键字构成,使得链接法散列的查找时间最坏情况下为θ(n)。
在这个问题中,考虑到散列表的冲突解决方法是链接法,即将散列到同一槽位的关键字放在一个链表中。大小为 n 的子集是指一个由 n 个关键字组成的集合,这个集合中的关键字都会散列到同一个槽位上,形成一个链表。
为了解释为什么存在这样一个大小为 n 的子集,考虑如果每个槽位只有 n - 1 个元素散列到它,那么整个全域的大小就只能是 (n - 1)*m,其中 m 是槽位的数量。这是因为每个槽位最多只能容纳 n - 1 个不同的关键字,如果超过这个数量,就会有关键字发生冲突,散列到同一个槽位上。因此,全域的大小受到了限制。
因此,为了确保全域大小大于 n * m,必然存在一个大小为 n 的子集,其中的关键字都会散列到同一个槽位上。
最坏情况的搜索时间发生在所有放入散列表的元素都属于这个大小为 n 的子集,它们都散列到同一个槽位上,导致形成的链表长度为 n,从而使得搜索的时间复杂度为线性。
11.2-6 假设将n个关键字存储到一个大小为m且通过链接法解决冲突的散列表中,同时已知每条链的长度。包括其中最长链的长度L,请描述从散列表的所有关键字中均匀随机地选择某一元素并在O(L*(1+1/α))的期望时间内返回该关键字的过程。
这段描述的过程是通过在散列表中均匀随机选择一个位置,然后从选定位置对应的链表中随机选择一个元素,以概率 1/(m*L)返回任意关键字。
具体描述如下:
- 从散列表的 m 个位置中随机选择一个位置 k。
- 记 n_k为散列表中第 k 个位置上链表的长度(即存储的元素个数)。
- 随机选择一个从 1 到链表长度 L 之间的数 x。
- 如果 x < n_k,则返回链表中第 x 个元素;否则,重复上述过程。
在该过程中,任何一个散列表中的元素都有相同的概率 1/(m*L) 被返回,因此能够保证均匀随机地选择一个元素。
为了计算期望步数 E[X],定义随机变量 X 表示执行上述过程的次数,p 为在每次尝试中返回元素的概率。根据描述,有E[X] = p(1+α) + (1-p)(1+E[X]))。解方程得到 E[X] = α+ 1/p。
p(1+α) 表示成功找到元素的情况。在这种情况下,我们期望执行 1+α 步找到元素。
1:表示成功找到目标元素后需要的额外步数,即找到元素本身的一步。
α:表示在选择链表和选择元素的过程中,期望的额外步数。
(1−p)(1+E[X]) 表示没有成功找到元素的情况。在这种情况下,我们没有找到元素,但我们需要再执行 1+E[X] 步来继续尝试找到元素。
1:表示这次失败的尝试
E[X]:表示在剩余的尝试中的期望步数
概率 p 返回元素的概率是 n/(m*L),因此 1/p = (m*L)/n。代入得到 E[X] = α + L/α。由于 n/(m*L) = α/L,因此 E[X] = L(1 + 1/α)。
最后的结论是 E[X] = L(1 + 1/α),其中α是加载因子,而α<= L。