目录
本章讨论散列表(hash table)ADT,不过它只支持二叉查找树所允许的一部分操作。
散列表的实现常常叫作散列(hashing)。散列是一种以常数平均时间执行插入、删除和查找的技术。
5.1 一般想法
理想的散列表数据结构只不过是一个含有关键字的具有固定大小的数组。
我们把表的大小记作Table-Size,并将其理解为散列数据结构的一部分而不仅仅是浮动于全局的某个变量。通常的习惯是让表从\(0\)到Table-Size\(-1\)变化。
散列函数(hash function)——将每个关键字映射到从\(0\)到Table-Size\(-1\)这个范围中的某个数,并且放到适当的单元中。
不过一对一对映射是不可能的——单元的数目是有限的,而关键字实际上是无穷无尽的。因此,我们寻找一个散列函数,该函数要在单元之间均匀地分配关键字。
剩下的问题是如何解决当两个关键字散列映射到同一个值的冲突(collision)以及如何确定散列表的大小。
5.2 散列函数
如果输入的关键字是整数,则一般合理的方法就是直接返回Key mod TableSize的结果,除非Key碰巧具有某些不理想的性质。解决方法:保证表的大小是素数——当输入的关键字是随机整数时,散列函数不仅算起来简单而且关键字的分配也很均匀。
如果输入的关键字是字符串——
散列函数返回类型:
typedef unsigned int Index;
第一种方法是把字符串中字符的ASCII码值加起来,但此时对表的大小有要求,取决于字符串的最大字符长,否则无法实现均匀分配。
第一种方法:
Index Hash( const char *Key, int TableSize ) { unsigned int HashVal = 0; while( *Key != '\0' ) HashVal += *Key++; return HashVal % TableSize; }
第二种方法是把字符串中字符在字母表中的位置乘以权重加起来,但此时理论上3个字符有\(26^3=17576\)种可能的组合,实际上3个字母的不同组合数只有2851种,对表的大小依然有要求(不能过大)。
第二种方法:
Index Hash( const char *Key, int TableSize ) { return ( Key[ 0 ] + 27 * Key[ 1 ] + 729 * Key[ 2 ] ) % TableSize; }
第三种方法是计算\(\sum_{i=0}^{KeySize-1}{Key[KeySize-i-1]·32^i}\),并将结果限制在适当的范围内。程序根据Horner法则计算一个(32的)多项式函数(这里之所以用32代替27,是因为用移位代替乘法)。下述程序中while循环里的加法还可以用按位异或来代替。
第三种方法:
Index Hash( const char *Key, int TableSize ) { unsigned int HashVal = 0; while( *Key != '\0' ) HashVal = ( HashVal << 5 ) + *Key++; return HashVal % TableSize; }
在关键词特别长时,一般不使用所有的字符。(只使用奇数位置/偶数位置/……)
设计思想:用计算散列函数节省下的时间来补偿由此产生的对均匀分布的函数的轻微干扰。
剩下的主要编程细节是解决冲突的消除问题。这里讨论其中最简单的两种:分离链接法和开放定址法。