之前我们说过,对于需要动态维护的散列表 冲突是不可避免的,无论你的散列函数设计的有多么精妙。因此我们解决的重要问题就是:一旦发生冲突,我们该如何加以排解?
我们在这里讨论最常见的两种方法:分离链接法和开放定址法。本篇探讨前者,下一篇讨论后者。
分离链接法
解决冲突的第一种方法通常叫做分离链接法(separatechaining),做法是将散列到同一个值的所有元素保留到一个链表中。那……为什么要这么做呢?保留到数组中不行么?下面我们来分析一下。
我们先从最初的思路说起,所谓的冲突形象来说就是一山不容二虎,倘若的确有两只老虎呢?答:用铁丝网将这座山分成两部分,两只老虎各居一侧,这是最朴素的办法了,这种思路也就是多槽位法(multipleslots)。如果此前的桶单元对应于山,那么每一个槽位(slot)就对应于在这个山中用铁丝网分割出的一个子区域。
对于这个散列表,每一个横条就是一个一个又一个的桶单元。在这里,我们将每个桶单元都继续细分为ABCD,4个槽位,每个桶内部的这些槽位就可以用来存放彼此冲突的若干个词条。
具体看一个例子吧,比如这就是一个长度为23的散列表,其中每一个桶都被分成了3个槽位
往里面放入数据之后变成这样:
可以看到这里尽管有些词条的确会彼此冲突,但依然可以在对应的桶中和平共处,被分隔开。当然,查找过程需要多出一步:除了需要根据关键码确定对应的桶单元地址,还需要在桶中遍历所有的槽位——直到找到目标or失败。不过只要槽位数量不多,就还能保证O(1)的效率。
但是!有一个显而易见的问题。。。。
找到对应的地址之后,遍历到哪算完啊,我还得往前扫描多久啊?问题就在这:每一个桶具体应该细分为多少个槽位,在事先几乎是无法预测的。如果分的过细就会造成空间上的浪费,而反过来,无论分的多细,在极端的情况下,仍有可能在某个特定的桶中发生大规模的冲突。那么面临这一两难的抉择该如何破解呢?
多槽位法在空间和时间效率上的两难处境,我在学习向量(动态数组)的时候也遇到过,那时的解决办法就是用列表(这里就采用指针链表实现)。
新的策略如这幅图所示:如果这个长条是整个散列表,那么其中的每一个单元都将各自拥有一个对应的列表,而每一个列表都可以用来存放一组彼此冲突的词条。那么答案就水落石出了——将相互冲突的词条串接起来,也就是所谓的separate chaining。举个例子:
这里我们假设关键字是前10个完全平方数,hash(x)=x%10,这里Size不是素数,只是为了简便。
相对于多槽位法,独立链法的优势非常明显:除了最初的表头,我们无需预留任何更多的空间,甚至如果空间很紧,更可取的方法是避免使用这些表头。而且表的长度可以根据需要自由的伸缩,只要系统的资源足够,任意多次的冲突都可以解决。得益于我们之前实现的表结构,我们只需寥寥几句即可实现相应的散列表结构。
下面来谈谈实现策略。