一般想法
理想的散列表数据结构只不过是一个含有关键字的具有固定大小的数组。典型情况下,一个关键字就是一个带有相关值(例如工资信息)的字符串。我们把表的大小记作 tableSize,并将其理解为散列数据结构的一部分,而不仅仅是浮动于全局的某个遍历。通常的习惯是让表从0到tableSize-1变化。
将每个关键字映射到从0到tableSize-1这个范围中的某个数,并且放到适当的单元中。这个映射叫作散列函数(hash function),理想情况下它应该运算简单并且应该保证任何两个不同的关键字映射到不同的单元。不过,这是不可能的,因为单元的数目是有限的,而关键字实际上是无穷无尽的。因此我们寻找一个散列函数,该函数要在单元之间均匀地分配关键字
0 | |
1 | |
2 | |
3 | john 25000 |
4 | phil 31250 |
5 | |
6 | dave 27500 |
7 | mary 28200 |
8 | |
9 |
在这个例子中,john散列到3,phil散列到4,dave散列到6,mary散列到7。当两个关键字散列到同一个值时称为冲突(collision),如何处理冲突是我们之后文章里要讨论的问题。
散列函数
如果输入的关键字是整数,则一般合理的方法就是直接返回“key mod tableSize”的结果,除非key碰巧具有某些不理想的性质。这种情况下散列函数的选择要仔细考虑,例如表的大小是10,而关键字都是以0为个位,比如70,20,240都会散列到0。此时上述标准的散列函数就是个不好的选择。好的办法通常是保证表的大小是素数。当输入的关键字是随机整数时,散列函数不仅算起来简单而且关键字的分配也很均匀。
通常,关键字是字符串。
一种选择方法是把字符串中字符的ASCII码值加起来,散列函数如下
typedef unsigned int Index;
Index Hash(const char* key, int tableSize)
{
unsigned int hashVal = 0;
while (*key != '\0')
hashVal += *key++;
return hashVal % tableSize;
}
这种散列函数实现起来简单而且能够很快算出答案。不过如果表很大则函数将不会很好地分配关键字。例如,设tableSize=10007(10 007是素数),并设所有的关键字至多8个字符长,由于char形量的值最多是127,因此只能散列到0-1016之间,其中1016=127×8。显然这不是一种均匀的分配
另一个散列函数如下
Index Hash(const char* key, int tableSize)
{
return (key[0] + 27 * key[2] + 729 * key[2] % tableSize);
}
这个散列函数假设key至少有两个字符外加NULL结束符。值27表示英文字母表的字母个数外加一个空格,而729=27²。该函数只考察前三个字符,假如它们是随机的而表的大小还是10007,那么我们就会得到一个合理的均衡分配。可不巧的是英文不是随机的。虽然3个字符(忽略空格)有26³=17576种可能的组合,但查验词汇量足够大的联机词典却揭示:3个字符的不同组合数实际只有2851种。即使这些组合的散列没有冲突,也不过只有表的28%被真正散列到。所以当散列表足够大时这个函数还是不合适的。
如下是散列函数的第三种尝试。这个散列函数涉及关键字中的所有字符,并且一般可以分布得很好(它计算)
Index Hash(const char* key, int tableSize)
{
unsigned int hashVal = 0;
while (*key != '\0')
hashVal = (hashVal << 5) + *key++;
return hashVal % tableSize;
}
程序根据Horner法则计算一个(32的)多项式函数。例如计算的另一种方式是借助于公式进行。Horner法则将其扩展到用于n次多项式。
我们之所以用32代替27,是因为用32作乘法不是真的去乘,而是移动二进制的5位。这个散列函数就表的分布而言未必是最好的,但是确实具有及其简单的优点。