先说明一下,她们两个属于不同的范畴,双散列属于开放定址法,仍是一种解决冲突的策略。而再散列是为了解决插入操作运行时间过长、插入失败问题的策略。简而言之,她们的区别在于:前者让散列表做的“对”(把冲突元素按规则安排到合理位置),后者让散列表具有了可扩充性,可以动态调整(不用担心填满了怎么办)。
双散列
我们来考察最后一个冲突解决方法,双散列(double hashing)。常用的方法是让F(i)= i * hash2( x ),这意思是用第二个散列函数算出x的散列值,然后在距离hash2( x ),2hash2( x )的地方探测。hash2( x )作为关键,必须要合理选取,否则会引起灾难性的后果——各种撞车。这个策略暂时不做过多分析了。
再散列
之前说过,对于使用平方探测法的闭散列里,如果元素填的太满的话后续插入将耗费过长的时间,甚至可能Insert失败,因为这里面会有太多的移动和插入混合操作。怎么办呢?一种解决方法是建立另外一个大约两倍大的表,再用一个新的散列函数,扫描整个原始表然后按照新的映射插入到新的表里。
再散列的目的是为了后续的插入方便。
比如我们把{6, 15, 23, 24,6}插入到Size=7的闭散列里,Hash(x)= x % 7,用线性探测的方法解决冲突,会得到这样一个结果;
现在还剩23,把这个插入之后,整个表里就填满了70%以上:
于是我们要建立一个新的表,newSize=17,这是离原规模2倍大小的最近素数。新的散列函数是Hash( x ) = x % 17。扫描原来的表,把所有元素插入到新的表里,得到这个:
这一顿操作就是再散列。可以看出这会付出很昂贵的代价:运行时间O(N),不过庆幸的是实际情况里并不会经常需要我们再散列,都是等快填满了才做一次,所以还没那么差。得说明一下,这种技术是对程序员友好而对用户不友好的。因为如果我们把这种结构应用于某个程序,那并不会有什么显著的效果,另一方面,如果再散列作为交互系统的一部分运行,可能使用户感到系统变慢。所以到底用不用还是要权衡一番的,运行速度不敏感的场景就可以用,方便自己,因为这个技术把程序员从对表规模的担心中解放出来了。
具体实现可以用平方探测以很多种方式实现
只要表有一半满了就做
只有当插入失败时才做(这种比较极端)
途中策略:当表到达某个装填因子时再做。
由于随着装填因子的增加,表的性能会有所下降,所以第三个方法或许是最好的。再散列把程序员从对表规模的担心中解放出来了,这一点的重要之处在于在复杂程序中散列表不可能一开始就做得很大,然后高枕无忧。因为我们也不知道多大才够用,所以能使她动态调整这个特性就很有必要了。实现的时候也比较简单
HashTable Rehash(HashTable H) { int i,OldSize; Cell *OldCells; OldCells=H->TheCells; OldSize=H->TableSize; //新建一个原规模*2的表 H=Init(OldSize<<1); //扫描原表,重新插入到新表里 for (i=0; i<OldSize; i++) { if (OldCells[i].Info==Legitimate) { Insert(OldCells[i].value, H); } } free(OldCells); return H; }