Hash表的出现主要是为了对内存中数据的快速、随机的访问。

它主要有三个关键点:Hash表的大小、Hash函数、冲突的解决hash表的实现)


1、Hash表的大小

   Hash表的大小一般是定长的,如果太大,则浪费空间,如果太小,冲突发生的概率变大,体现不出效率。所以,选择合适的Hash表的大小是Hash表性能的关键。

   对于Hash表大小的选择通常会考虑两点:

   第一,确保Hash表的大小是一个素数。常识告诉我们,当除以一个素数时,会产生最分散的余数,可能最糟糕的除法是除以2的倍数,因为这只会屏蔽被除数中的位。由于我们通常使用表的大小对hash函数的结果进行模运算,如果表的大小是一个素数,就可以获得最佳的结果。

   第二,创建大小合理的hash表。这就涉及到hash表的一个概念:装填因子。设装填因子为a,则:

a=表中记录数/hash表表长

通常,我们关注的是使hash表的平均查找长度最小,而平均查找长度是装填因子的函数,而不是表长n的函数。a的取值越小,产生冲突的机会就越小,但如果a取值过小,则会造成较大的空间浪,通常,只要a的取值合适,hash表的平均查找长度就是一个常数,即hash表的平均查找长度为O(1)。

    当然,根据不同的数据量,会有不同的哈希表的大小。对于数据量时多时少的应用,最好的设计是使用动态可变尺寸的哈希表,那么如果你发现哈希表尺寸太小了,比如其中的元素是哈希表尺寸的2倍时,我们就需要扩大哈希表尺寸,一般是扩大一倍。
         下面是哈希表尺寸大小的可能取值:
     17,            37,          79,        163,          331,  
    673,           1361,        2729,       5471,         10949,        
   21911,          43853,      87719,      175447,      350899,
  701819,         1403641,    2807303,     5614657,     11229331,   
 22458671,       44917381,    89834777,    179669557,   359339171,  
718678369,      1437356741,  2147483647


2、Hash函数

一个好的hash函数一般具有以下两个特点:

    第一,速度快,

    第二,能够将散列键均匀的分布在整个表中,保证不会产生聚集。

通常,hash函数具有如下形式:

hash-key = calculated-key % tablesize

上一节主要讨论了一下tablesize,为了提高散列键的离散程度,tablesize通常取素数。一般而言,没有绝对好的hash函数,hash函数的好坏很大程度上依赖于输入键的结构,人们讨论的最多的一般都是输入键为普通字符串的情况。这里也以字符串为例讨论如何一步步优化hash函数。

    当键是字符串时,一种选择策略是简单的将字符串中每个字符的ASCII码加起来,代码如下:   

    unsigned int hash(const char *key, unsigned int tableSize)  
    {  
        unsigned int hashVal;  
      
        while(*key != '\0')  
            hashVal += *key++;  
      
        return (hashVal % tableSize);  
    }

     上面的hash函数实现简单而且能够很快的算出答案,不过,如果表很大,则函数就不能很好的分配键,例如,设tableSize=10949,并设所有的键至多8个字符长,由于ASCII字符的值最多是127,因此hash函数只能在0~1016之间取值,其中1016为127*8,显然这不是一种均匀的分配。

    下面的hash函数对针对上面的缺点进行了改进。

    unsigned int hash(const char *key, unsigned int tableSize)  
    {  
        return (key[0] + 27*key[1] + 729*key[2]) % tableSize;  
    }

     这个hash函数假设key至少有3个字符。值27表示英文字母表的字母个数外加一个空格,而729是27的平方。虽然该函数只考察了前三个字符,但是,假如字符出现的概率是随机的,而表的大小还是10949,那么我们就会得到一个合理的均衡分布。可是,英文不是随机的。虽然3个字符有 26*26*26=17567种可能的组合,但查验词汇量足够大的联机词典却揭示出:3个字母的不同组合数实际上只有2851种。即使这些组合没有冲突, 也不过只有表的28%被真正散列到。因此,虽然容易计算,但是当hash表足够大的时候,这个函数还是不合适。

     针对以上缺点,进一步改进:

unsigned int hash(const char *key,unsigned int tableSize)  
{  
    unsigned int hashVal;  
  
    while(*key != '\0')  
        hashVal = (hashVal << 5) + *key++;  
  
    return (hashVal % tableSize);  
}

         这个hash函数涉及键中的所有字符,并且一般可以分布的很好,它计算了字符串的如下值:


    在计算该值时,利用了Horner法则,例如计算 hash=a+32b+32*32c 的另一种方式是借助公式:hash=((c)*32+b)*32+a。Horner法则将其扩展到用于n次多项式。该算法通过将乘法运算转换为位运算保证了hash函数快速的特点。

       下面给出实际字符串hash应用中使用的很广的一个hash函数:ELFhash

    unsigned long ELfHash(const unsigned char * key)  
    {  
        unsigned long h = 0, g;  
      
        while(*key)  
        {  
            h = (h << 4) + *key++;//把h左移4位加上该字符赋给h  
            if(g = h & 0xF0000000)//取h的高四位赋给g  
                h ^= g >> 24;//如果g不为0,让h和g的高八位异或再赋给h  
      
            h &= ~g;//对g取反并与h相与赋给h  
        }  
        return h;  
    }


3、Hash冲突的解决

    为提高hash表查找性能,除了考虑选择合适的hash表表长和完美的hash函数外,还必须考虑hash表处理冲突的能力。当hash函数对两个不同的数据项产生了相同的hash值时,冲突就产生了。对于冲突的处理,通常采用的方法可以分为三类:

(1)线性再散列法,简单的按顺序遍历hash表,寻找下一个可用的槽;

(2)非线性再散列法,计算一个新的hash值;

(3)外部拉链法,将hash表中的每个槽当作具有相同hash值的数据项所组成链表的头部,hash表将发生冲突的项添加到同一个链表中。

下面对这三种方法分别介绍。

1.线性再散列法

       线性再散列法是形式最简单的处理冲突的方法。插入元素时,如果发生冲突,算法会简单的遍历hash表,直到找到表中的下一个空槽,并将该元素放入该槽 中。查找元素时,首先散列值所指向的槽,如果没有找到匹配,则继续遍历hash表,直到:(1)找到相应的元素;(2)找到一个空槽(指示查找的元素不存 在);(3)整个hash表遍历完毕(指示该元素不存在并且hash表是满的)。下表显示了以线性再散列法将{89,18,49,58,69}5个元素插 入hash表的过程。(hash函数为:hash(X)=X mod 10;hash表长一般用素数,这里为了说明方便取表长为10)


         第一次冲突发生在插入关键字49时,它被放在下一个空闲地址,即地址0。关键字58依次和18,89,49发生冲突,试选三次之后才找到一个空单元。对 69的冲突用类似的方法处理。从以上过程可以看出,只要表中有空闲单元,总可以找到,但这里选择步长为1,将会在hash表中产生聚集,即:即使hash 表相对较空,还是会在某些区域形成一些区块,这些区块中的任何活动都将设计更大的步长。但如果以5或更大的值作为步长,可以迅速地从拥挤区域移开,从而减 少聚集现象的发生。事实上,只要hash表长和检查槽的步长是互质的,那么表中的每个槽都会被检查到。

       线性再散列法有两个缺点:第一,不能从表中删除元素,因为相应的单元可能已经引起过冲突,元素绕过它存到了别处,例如,如果我们删除了18,那么其他的元素都会找不到。如果确实需要删除,可以采用懒惰删除的方法。第二,当表被填满时性能下降明显。

2.非线性再散列法

        线性再散列法是从冲突位置开始,采用一个步长以顺序方式遍历hash表,来查找一个可用的槽,从上面的讨论可以看出,它容易产生聚集现象。非线性再散列法 可以避免遍历散列表,它会计算一个新的hash值,并通过它跳转到表中一个完全不同的部分。它的思想就是:通过跳转到表中不同的部分,从而避免相似值的聚 集,如果再散列函数跳转到的槽已经被占用了,则继续执行新一轮的再散列和跳转。

    例如,还是上面的例子,如果再散列函数是hash(X)=R-(X mod R),其中R为小于hash表长的素数,如果我们选择R=7,则下表显示了插入与前面相同的关键字的结果。


        第一个冲突发生在49被插入的时候, hash(49)=7-0=7,故49被插入到位置6。Hash(58)=7-2=5,于是58被插入到位置3。最后69产生冲突,从而被插入到距离为hash(69)=7-6=1的地方。

        非线性再散列法也有不能从表中删除元素的缺点。

        无论是使用线性再散列法还是非线性再散列法,只有在散列表不会接近填满的情况下,才能使用再散列。当散列表的负载因子增大时,再散列所花费的时间也会显著增加。通过以上讨论可以看出,再散列方法适用于表负载较低并且不太可能执行删除操作的情况。

3.外部拉链法

        外部拉链法是将hash表看作是一个链表数组,表中的每个槽要不为空,要不指向hash到该槽的表项的链表。可以通过把元素添加到链表中来解决冲突。同 样,可以通过从链表中删除元素来执行删除操作。因此,解决冲突的代价不会超过向链表中添加一个节点,不需要执行再散列。在再散列中,表项的最大数量是由表 中槽的原始数量确定的,与之不同的是,外部拉链法可以容纳的元素于将在内存中存放的元素一样多。

        外部拉链法的原则是:hash表的大小一般与预料的元素个数差不多。

        假设有一个表长为10的hash表,给出10个关键字为前10个自然数的平方,hash函数为hash(X)=X mod 10,下图就是对应的外部拉链法的hash表。


          外部拉链法的平均查找时间是对链表的查找时间加上1,这个1是最初的定位hash表槽。外部拉链法的缺点是:它需要稍微多一些的空间来实现,因为添加任何 元素都需要添加指向节点的指针,并且每次探查也要花费稍微多一点的时间,因为它需要间接引用指针,而不是直接访问元素。由于今天的内存成本很低并且可以使 用非常快的CPU,所以这些缺点都是微不足道的。因此,实际使用hash表时,一般都是使用拉链法来解决hash冲突。


4、hash表的实现
由于实际应用中,拉链法实现hash表用的比较多,这里也以拉链法来实现hash表。

 /* 
     * hashTable.h 
     * 
     *  Created on: Nov 30, 2011 
     *      Author: Liam Q 
     */  
    #ifndef HASHTABLE_H_  
    #define HASHTABLE_H_  
      
    //定义DEBUG_MSG用于调试输出  
    #define DEBUG_MSG  
    //#define DEBUG_MSG(args)   printf args;    printf("\n");  
      
    typedef char * ElemType;//对字符串进行hash  
      
    typedef struct ListNode//链表中节点  
    {  
        ElemType elem;  
        struct ListNode * next;  
    }ListNode, *Position;  
      
    typedef Position List;//链表  
      
    typedef struct HashTbl//hash表  
    {  
        int tableSize;  
        List * theLists;  
    }HashTbl, *HashTable;  
      
    HashTable initTable(int tableSize);  
    void destroyTable(HashTable hashTable);  
    Position find(HashTable hashtable, ElemType elem);  
    void insert(HashTable hashTable, ElemType elem);  
      
    #endif /* HASHTABLE_H_ */  
      
      
    /* 
     * hashTable.c 
     * 
     *  Created on: Nov 30, 2011 
     *      Author: Liam Q 
     */  
    #include <stdio.h>  
    #include <stdlib.h>  
    #include <math.h>  
    #include <string.h>  
    #include "hashTable.h"  
      
    #define MIN_TABLE_SIZE 10//hash表最小大小  
      
    static int isPrime(int a)  
    {  
        int i;  
        int b = (int)sqrt((double)a);  
        for(i = 2; i <= b; i++)  
            if(a % i == 0)  
                return 0;  
        return 1;  
    }  
      
    static int nextPrime(int a)  
    {  
        while(!isPrime(a++));  
        return --a;  
    }  
      
    HashTable initTable(int tableSize)  
    {  
        HashTable hashTable;  
        int i;  
      
        if(tableSize < MIN_TABLE_SIZE)  
        {  
            printf("Table size is too small\n");  
            return NULL;  
        }  
      
        hashTable = (HashTable)malloc(sizeof(HashTbl));  
        if(hashTable == NULL)  
            exit(-1);  
        hashTable->tableSize = nextPrime(tableSize);  
      
        hashTable->theLists = (List *)malloc(hashTable->tableSize * sizeof(List));  
        if(!hashTable->theLists)  
            exit(-1);  
      
        for(i = 0; i < hashTable->tableSize; i++)  
        {  
            hashTable->theLists[i] = (List)malloc(sizeof(ListNode));  
            if(!hashTable->theLists[i])  
                exit(-1);  
            else  
                hashTable->theLists[i]->next = NULL;  
        }  
      
        return hashTable;  
    }  
      
    void destroyTable(HashTable hashTable)  
    {  
        int i;  
        List l, tmp;  
      
        if(hashTable == NULL)  
            return;  
        if(hashTable->theLists)  
        {  
            for(i = 0; i < hashTable->tableSize; i++)  
            {  
                l = hashTable->theLists[i];  
                while(l != NULL)  
                {  
                    tmp = l;  
                    l = l->next;  
                    free(tmp);  
                }  
                hashTable->theLists[i] = NULL;  
            }  
            free(hashTable->theLists);  
        }  
      
        free(hashTable);  
    }  
      
    static int hash(ElemType elem, int tableSize)  
    {  
        unsigned int hashVal = 0;  
      
        while(*elem != '\0')  
            hashVal = (hashVal << 5) + *elem++;  
      
        return hashVal % tableSize;  
    }  
      
    Position find(HashTable hashTable, ElemType elem)  
    {  
        Position p;  
        List l;  
      
        l = hashTable->theLists[hash(elem, hashTable->tableSize)];  
        p = l->next;  
        while(p != NULL && strcmp(p->elem, elem) != 0)  
            p = p->next;  
      
        return p;  
    }  
      
    void insert(HashTable hashTable, ElemType elem)  
    {  
        Position p, pNewCell;  
        List l;  
      
        p = find(hashTable, elem);  
        if(p == NULL)  
        {  
            pNewCell = (List)malloc(sizeof(ListNode));  
            if(pNewCell == NULL)  
                exit(-1);  
            else  
            {  
                l = hashTable->theLists[hash(elem, hashTable->tableSize)];  
                pNewCell->elem = (char *)malloc(strlen(elem) * sizeof(char));  
                strcpy(pNewCell->elem, elem);  
                pNewCell->next = l->next;  
                l->next = pNewCell;  
                DEBUG_MSG(("%d-----%s\n",hash(elem, hashTable->tableSize),pNewCell->elem));  
            }  
        }  
    }  
      
      
    /* 
     * demo.c .利用上面实现的hash表读取文本文件中单词,并将其放入hash表 
     *  
     *  Created on: Nov 30, 2011 
     *      Author: Liam Q 
     */  
    #include <stdio.h>  
    #include <stdlib.h>  
    #include <string.h>  
    #include <ctype.h>  
      
    #include "hashTable.h"  
      
    #define HASH_SIZE 500  
    #define MAXWORD 64  
      
    void createHashTable(HashTable hashTable)  
    {  
        char *fileName="Makefile.common";  
        FILE * fp;  
        char word[MAXWORD];  
        char c;  
        int i;  
      
        if((fp = fopen(fileName, "r")) == NULL)  
        {  
            fprintf(stderr,"can't open %s\n",fileName);  
            exit(-1);  
        }  
      
        c = ' ';  
        while(!feof(fp))  
        {  
            while(c != EOF && isspace(c))  
                c = fgetc(fp);  
            i = 0;  
            while(c != EOF && !isspace(c))  
            {  
                word[i++] = c;  
                c = fgetc(fp);  
            }  
            if(c == EOF)  
                break;  
            word[i] = '\0';  
            while(i >= 0 && ispunct(word[--i]))  
                word[i] = '\0';  
            insert(hashTable,word);  
        }  
    }  
      
    int main()  
    {  
        HashTable hashTable;  
        char word[MAXWORD];  
      
        hashTable = initTable(HASH_SIZE);  
        createHashTable(hashTable);  
        while(1)  
        {  
            printf("Search string:");  
            fgets(word, sizeof(word), stdin);  
            word[strlen(word) - 1] = '\0';  
            DEBUG_MSG(("%s",word));  
            printf("%s\n",find(hashTable,word)?"find":"not find");  
        }  
        destroyTable(hashTable);  
        hashTable = NULL;  
    }