散列表--双散列、再散列与可扩散列

        双散列

我们将要考察的最后一个冲突解决方法是双散列(double hashing)。对于双散列一种流行的选择是F(i)=i\cdot hash_2(X),意思是说冲突解决函数中又有一个对应X的散列函数,并在距离hash_2(X),2hash_2(X)等处探测。hash_2(X)选择不好将会是灾难性的,例如插入99,对于通常的选择hash_2(X)=Xmod 9将不起作用,当冲突发生时,冲突函数的值hash_2(X),2hash_2(X),...都将是0值,解决不了冲突。因此函数一定不要算得0值。

        诸如hash_2(X)=R-(XmodR)这样的函数将起到良好的作用,其中R为小于tableSize的素数。使用双散列时保证表的大小为素数时重要的,假设选取R为7,表大小为10。若想把23插入表中而与某个数发生冲突之后,hash_2(23)=7-2=5,且该表大小为10,因此我们只有一个备选位置,而这个位置已经使用了。因此,如果表的大小不是素数,那么备选单元就有可能提前用完。然而如果双散列正确实现,则模拟表明,预期的探测次数几乎和随机冲突解决方法的情形相同。双散列理论上很有吸引力,不过相比之下平方探测不需要使用第二个散列函数,从而在实践中可能更简单并且更快。       

        再散列

        对于使用平方探测的开放定址散列法,如果表的元素填得太满,那么操作的运行时间将开始消耗过程,且Insert操作可能失败。这可能发生在有太多的移动和插入混合的场合。此时一种解决方法是建立另外一个大约两倍大(原表两倍大的下一个素数)的表(而且使用一个相关的新散列函数,扫描整个原始散列表,计算每个(未删除的)元素的新散列值并将其插入到表中。整个操作就叫作再散列(rehashing),显然这是一种非常昂贵的操作,其运行时间为O(N),因为有N个元素要再散列而表的大小约为2N,不过由于不是经常发生,因此实际效果没有那么差。

        添加到每个插入上的花费基本上是一个常数开销(这是新表要做成老表约两倍大的原因,这样一次插入就不会存在很多冲突增加运行时间从而基本上是常数开销)。如果这种数据结构是程序的一部分,那么其效果是不显著的。另一方面,如果再散列作为交互系统的一部分运行,那么其插入引起再散列的不幸的用户将会感到速度减慢。

        再散列可以用平方探测以多种方法实现。一种做法是只要表填满一半就再散列。另一种极端的方法是只有当插入失败时才再散列。第三种方法是途中(middle-of-the-road)策略:当表到达某一个装填因子时进行再散列。由于随着装填因子的增加,表的性能的确有下降,因此,以好的截止手段实现的第三种策略,可能是最好的策略。

        再散列还可以用在其他的数据结构中。例如队列,再散列的实现很简单,

        如下:

HashTable ReHash(HashTable h)
{
	if (h == NULL)
		return NULL;

	int i;
	int oldSize = h->tableSize;
	Cell* oldCells = h->theCells;
	h = InitializeTable(2 * oldSize);
	for (i = 0; i < oldSize; i++)
	{
		if (oldCells[i].info == Legitimate)
			Insert(h, oldCells[i].element);
	}
	free(oldCells);
	return h;
}

        可扩散列

        最后来讨论数据量太大以至于装不进主存的情况,此时主要考虑的是检索数据所需的磁盘存取次数。

        我们假设在任意时刻有N个记录要存储,N的值随时间变化。此外最多可把M个记录放入一个磁盘区块。现在设M=4。

        如果使用开放定址散列法或分离链接散列法,那么主要的问题在于,在一次Find操作期间,冲突可能引起多个区块被考察,甚至对于理想分布的散列表也在所难免。不仅如此,当表变得过满的时候,必须执行执行代价巨大的再散列这一步,它需要O(N)次磁盘访问。

        一种聪明的选择叫作可扩散列(extendible hashing),它允许用两次磁盘访问执行一次Find。插入操作也需要很少的磁盘访问。

        我们知道B树具有深度O(log_{M/2}N)。随着M的增加,B树的深度降低。理论上我们可以选择使得B树深度为1的M。此时在第一次以后的任何Find都将花费一次磁盘访问,因为据推测根节点可能存在主存中。这种方法的问题在于分支系数(branching factor)太高,以至于为了确定数据在哪片树叶上要进行大量的处理工作。如果运行这一步的时间可以减缩,那么我们就有一个实际的方案,这正是可扩散列使用的策略。

        现在假设我们的数据由几个6位二进制数组成。“树”的根含有四个指针,它们由这些数据的前两位确定。每片树叶有最多M=4个元素。现在举个例子,碰巧这里每片树叶中数据的前两位都是相同的。为了更正式用D代表根所使用的位数,有时称其为目录(directory),于是,目录中的项数为2^Dd_L为树叶L所有元素共有的最高位的位数。d_L将依赖于特定的树叶,因此d_L\leqslant D

         设欲插入关键字100100。它将进入第三片树叶,但是第三片树叶已经满了,没有空间存放它。因此我们将这片树叶分裂成两片树叶,它们由前三位确定。这需要将目录的大小增加到3,变化如图所示

目录大小变为3,项也随之变为8项,因为多加一位有0有1两种,所以每一项变成两项。

        如果插入关键字000000,那么第一片树叶就要分裂,生成d_L=3的两片树叶。由于D=3,故在目录中所作的唯一变化是000和001指针的更新,而目录位数不增加。

        这个非常简单的方法提供了对大型数据库Insert操作和Find操作的快速存取时间。这里还有一些重要细节我们尚未考虑。

  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值