数据结构(邓俊辉)学习笔记】词典 04—— 排解冲突(2)

1.平方试探

在这里插入图片描述

在上一节,我们已经介绍了排解散列冲突的基本方法,大体说来,无非封闭定址和开放定址两类大的层。相对而言,后者的物理结构更为紧凑,因此在性能上略具优势。对于大规模的数据更是如此。

然而我们也看到,对于其中典型的线性试探策略而言,往往存在大量本不该发生的冲突,如何就此作出改进?这是接下来这一节的主题。实际上线性试探的问题根源在于大部分的试探位置都集中于某一个相对很小的局部。

因此解决这个问题的钥匙也就是适当的拉开各自试探的间距。而所谓的平方试探就是这一思路的具体体现。

所以平方试探,顾名思义也就是每次试探的位置不是简单的以线性递增,而是以平方数为间距。也就是说,如果必要继续试探,那么第一个位置的间距应该是 1 的平方。如果仍有必要,第二个位置间距应该是 2 平方以至 3 的平方,4 的平方,诸如此类,整个试探的过程也可以通过这幅图来示意。

不妨假设首先散列映射到的是编号为 0 的这个位置。如果不能在此命中,我们接下来将要试探的将是与之间距为 1 的平方,也就是紧邻的这个桶单元。当然就这一步而言,与线性刺探没有区别。

然而接下来的情况就大不相同了。如果我们在 1 号位置仍未命中,接下来第二次试探的位置,其间距将是 2 的平方,也就是4。以下类推第三次试探,如果有必要,其对应的间距应该是 3 的平方 9 以及第四次 4 的平方 16。

当然,所有的试探位置都是相对于 M 取模之后的。如此可以保证所有的试探位置都在这个封闭的散列空间之内。那么这种排解冲突的方法的确优于此前的线性试探策略吗?

2. 一利一弊

在这里插入图片描述
是的,相对于线性试探,平方探测的确可以在很大程度上缓解数据聚集的现象。、

这得益于我们此前的构思。仍然从技术图上我们可以看出,按照这种策略,相邻试探位置之间的间距将按一个算数技术不断地递增。

也就是说,一旦发生冲突,这种策略将会聪明的以一种不断增加的速度跳离这个是非之地,理论上更为详细的分析以及实验的统计都证明了这一点。当然,任何事情都是一利一弊,为此我们也可能需要付出一定的代价,因为相对于线性试探策略,平方试探策略将在一定程度上破坏数据访问的局部性。在某些时候可能会导致 IO 访问的激增,在一些极端的情况下,系统的缓存功能也将失效。 不过好消息是,在通常的情况下,问题还不算很严重。

我们不妨就此做一个估算。我们知道在通常的情况下,缓存页面的规模都在若干个 KB 左右,不失一般性,这里不妨就取做一个 KB。
  ~  
如果我们桶单元,只记录相应的引用,那么大致只需要4个字节,每一个缓存页面都足以容纳至少256个桶单元,也就是16的平方。也就是说如果我们需要做一次额外的 I/O 兑换,必须连续的发生16次冲突。
  ~  
果真如此,我们只能说自己的运气还糟糕了。

就这个意义而言,我们也可以说平方探测所增加的试探位置间距是适度的。当然试探位置间距的加大又会引来一个附加的问题,你能看出来吗?

没错,从直观上看,这样一种方式已经不再是逐个的去试探。因此我们或许会怀疑是否会出现这样一种歧义的现象,也就是在散列表中明明还存在空桶,但是按照这种策略却永远不能发现。

3. 至多半截

在这里插入图片描述
很不幸,我们刚才所设想的坏情况竟然的确可能发生。

比如这就是一个例子,这个散列表的容量取做12,不失一般性,假设就从 0 号桶单元开始,连续的发生足够多次的冲突,根据平方试探的规则,我不难确认各次试探的具体味置。
  ~  
当然在课后也可以对此做一验算,你会发现我们试探的足迹只会涉及到其中的四个单元。
  ~  
也就说,尽管在此时由高达2/3比例的桶都是空的,我们竟然没有办法找到他们。当然也可能会抱怨这里并没有按照常规将表长取作为一个素数。

是的,借助数论的知识不难证明,只要表长 M 是合数,这种情况就必然可能发生。

那么将表长改为素数就足够了吗?我们再来看这样的一个例子,当然这是一个反例。这次我们将表长取作11,没问题,这是一个素数。

接下来依然不失一般性,假设从0号桶开始相继的发生足够多次的冲突。
  ~  
自然,我们同样可以根据平方试探的规则,计算出所有可能抵达的桶单元编号。尽管我们这里直接给出了结果,但我还是建议你在课后就此做一验算。如果不发生意外,我想你和我的结论应该是一样的。

也就是说,按照这种规则,无论试探多少次,我们足迹只能涉及到这11个桶中的6个,而不是全部,换而言之,即便此时有多达接近一半的桶都是空的,我们却无法找到并利用它们。当然,情况还不是糟糕透顶,因为我们发现,毕竟前 6 次试探所经过的桶必然都是互异的,这是一个巧合吗?

不是。同样的,稍微运用一些数论的工具就不难证明只要表长 M 是素数,平方试探的足迹就恰好会遍及其中的M/2 上整个桶。因为一般的素数 M 都是基数,所以这个比例刚刚超过50%。

没错50%。这恰恰是一个重要的分水岭,这也是情况可能糟糕到的最坏程度。而关于这一点的正面结论是,只要我们的表长是个素数,而且能够保持增填因子不超过50%,就一定不会发生刚才所说的负面情况,否则就未必能够保证。

以下我们就来给出证明。

4. M + Lambda

在这里插入图片描述
我们采用反证法,假设在表长为素数,同时装填因子不超过50%的情况下,居然在0与M/2 的上整之间存在两个互异的整数 a 和 b,而它们所对应的位置彼此冲突。

那么转到数论的语言,所谓的冲突,也就是 a 的平方和 b 的平方同属于某个关于 m 的同余类。经过简单的代数变换,我们就可以推知 b 与 a 之和与 b 与 a 之差的乘积能够整除 M。

然而我们说这是不可能的。原因在于,无论是 b - a 亦或 b + a,在数值上都必然是介于 0 和 M 之间。而如此也进一步的意味着 b + a 至少是 2 ,也就是说 b + a 居然是 M 的一个非平凡的因子。这于我们关于 M 是素数的假设是相悖的。
在这里插入图片描述
好了,我们现在可以得出一个结论,只要将散列表的长度取作素数,同时将装填因子控制在50%以下,那么就能保证在起始于任何位置的平方探测序列中,前面的 M/2 取上整的位置,必然是彼此互异的。 而接下来的另一半位置呢?则未必能够保证。

当然,包括你在内,强调和追求效率的我们或许不会满足于这样的效率。那么后一半的空间可否同样地利用起来呢?好消息是可以,这也是我们的下一个主题。

5. 双蜓点水

如果需要进一步的提高装填因子,不妨可以考虑将常规的平方试探拓展为所谓的双向平方试探。

具体来说,一旦发生冲突,则交替地向前、向后,以递增的平方数为间隔逐一的试探。整个试探的过程,以及对应的试探链,可以由这副图来表示。
  ~  在这里插入图片描述> 假设最初的散列映射位置在这 0 ,接下来如果从这个位置开始,持续地发生冲突,那么第一个试探的位置应该是这 1 。如果依然冲突,那么下一个试探的位置应该是这个 -1,第三次试探的位置在这 4,第四次在这 -4,依此类推。

在这里插入图片描述

如果将双向试探的位置罗列出来,大致应该是这样。从居中的 0 号单元开始,向后转入相比而言的 1 号桶,再转向 -1 号桶,再转向 +4号桶,再转向 -4 号桶,以及 +9 号桶,-9 号桶,依此类推。
  ~  
当然具体的桶单元编号还需对 M 取模。
  ~  
对于此前那个长度为 11 的散列表而言,这种方法非常有效。因为如果像这样将各次试探的位置具体列出来,我们就会发现,前11次试探的位置不多不少恰好覆盖了 0 到 10 这11个桶。也就说只要证件因子还没有达到100%,仍有空桶,那么按照这种方式进行试探,就一定能找到一个空桶。
  ~  
你也不难发现,对于其他的一些素数表长,也有这种好现象。比如考察长度为 7 的散列表,可以看到,如果也是采用双向平方探测,那么前7次试探的位置也恰好覆盖了0-6这七个单元。

尽管已经看到了这样的两个好的实例,我们还是有理由怀疑其中存在着运气的成分。我们知道此时的查找链,实际上是由正向和逆向两个子查找链依次交错而构成。在讨论单向平方探测时,我们已经证明,在正向的子查找链中,前 M/2 上整的位置必然彼此互异。而由对称性,逆向的子查找链,也应该在内部具有这样的性质。那么在这两个子查找链之间,除了公共的起点0,是否还有其他公共的单元呢?如果没有,那么他们的总长就应该恰好是 M,也就是说它们的并集将完整地覆盖所有的桶。

我们刚才已经看到在 M 等于11时,情况的确如此,正向的查找链与逆向查找链之间没有任何公共元素。而 M 等于7时,情况也是如此。然而正如我们刚才所担忧的那样,情况并非总是如此。
  ~  
比如 M = 13 时,我们会在两个子查找链中同时发现4 9 3 等等。而在做过进一步的观察之后,你甚至会发现此时的正向子查找链与逆向子查找链居然是由同样的六个数组成的,我们也不能找到更多这样的反利。
  ~  
最简单的莫如 M = 5,你会发现它的正向子查找链与逆向子查找链也是由同样的两个数组成。

以上这些实例告诉我们,对于双向平方试探法而言,采用某些素数表长,可以行之有效,而采用另外一些素数的表长却非常糟糕。

那么同样是素数,这两种类型之间有什么区别呢?我们在相应的设计散列表时,又该如何取舍呢?

6. 4K + 3

在这里插入图片描述

是的,除了2之外,所有的素数无非两类,取决于他们关于4的模余。有些素数关于4的模余为3,比如3自己,以及我们刚才举例的7和11,也包括19,23,31,诸如此类。当然剩下来的那些关于4的模余只能是1,比如我们同样曾经举过例的5和13,以及17 29等等。

经过这样的分类,你或许能发现什么?是的,刚才我们所举的两个好例子,也就是7和11,关于4的模余都是3。而糟糕的那两个实例,也就是5和13都属于模 4 余 1 的那类,实际上在使用双向平方探测时,我们给你的建议是应该将表长 M 取做那类模 4 余 3 的素数,这样就可以确保查找链的前 M 项必然是互异的。

如果没有提前分析过以上的实例,而是直接给出这个建议,你或许会觉得一头雾水。是的,这个建议听起来的确有些蹊跷。然而,你的问题或许还不止于此。是的,还有另一方面,也就是那些模 4 余 1 的素数就注定不能使用吗?

下面我们就一并来回答这正与反两个孪生的问题。需要说明的是这个证明多少需要用到一些数学。所以如果你对数学不是很感兴趣,那么我的建议是,你不妨直接记一下我们刚才所给的这个建议。

7. 双平方定理

在这里插入图片描述

以上,针对双向平衡试探法,我们所给建议是将表长 M 取作形如 4K + 3 的素数。为了证明这个建议有效性和必要性,我们需要用到所谓的双平方定理。这个精妙的定理出自著名的费马之手。

在这个定理中,费马指出:任何一个素数 P 若能表示为一对整数的平方和,其充要条件是这个素数关于 4 的模余应该是 1,而不是 3。 当然,相关的结论也可以推广到一般的整数。为此我们需要借助这个恒等式,通过一些基本的代数变换,不难证明这一点。

当然,你不妨先通过这个实例做一验证,这个恒等式告诉我们什么呢?它告诉我们,如果有两个自然数分别可以表示为一对自然数的平方和,那么它们的乘积也可以表示为一对自然数的平方和。

由以上等式不难推知,任何一个自然数 n 若能分解为一对整数的平方和,当且仅当在它的素因数分解式中,每一个模 4 余 3 的素因子,本身都是偶数次方。

我们来看一个实例,比如考察810,对于所有的自然数一样,它也有一个唯一的素因数分解式,其中只有 3 是模4余3的,而且它是4次方,符合偶数的要求。
  ~  
我们现在就来看看,如何借助上面的恒等式将它表示为一对整数的平方和。首先那个讨厌的2,在此同样可以分解为 1 和 1 的平方和,而3的 4 次方本身就是 9 的平方。而 5 根据费马定理,它必然可以分解,具体的可以分解为1的平方加上2的平方。
  ~  
这样我们就向前迈了一步,接下来考察这个乘积。由我们恒等式,可以进一步的得到 1 的平方加上 1 的平方,再乘以9的平方以及18的平方。
  ~  
现在只需套用刚才的恒等式,就可进一步地得到,9+18 也就是27的平方,再加上9与18的差,也就是9的平方。没错,810确实可以分解为这样的平方和形式。

8. 泾渭分明

在这里插入图片描述

还是回到我们的散列,依然考察双向平方试探策略。假设按照我们所给的建议,表长 M 取做模 4 余 3 的一个素数,可以证明在这样的条件下,任何一个试探序列的前 M 步都不会发生重复和冲突

我们采用反正法,假设在正向试探子序列中的第 a 步与逆向试探子序列中的第 b 步是彼此冲突的。当然,从标号上看,他们都不会小于 1 ,也不会超过 M/2的下整。那么什么是冲突呢?

翻译成数论的语言也就是说,在关于表长 M 模余的意义下,负的 b 平方与正的 a 平方彼此同余,稍等整理,也就是 a 的平方与 b 的平方之和能够整除 M。我们将注意力就放在这个平方和上,并将其记作 n,于是 M 必然是 n 的素因子之一。

既然作为给定条件,素因子 M 是模 4 余 3 的,所以根据费马双平方定理的推论,n 不仅能够被 M 整除,而且可以被 M 的平方整除。这就意味着 a 平方与 b 平方之和至少是 m 平方。

然而,根据 b 和 a 的取值上限,这一关系是断乎不可能成立的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值