哈希表详解

Hash表也称散列表,也有直接译做哈希表,Hash表是一种特殊的数据结构,它同数组、链表以及二叉排序树等相比较有很明显的区别,它可以快速定位到想要查找的记录,而不是与表中存在的记录的关键字进行比较来进行查找。这个源于Hash表设计的特殊性,它采用了函数映射的思想将记录的存储位置与记录的关键字关联起来,从而可以很快速地进行查找。程序员

零、Hash表的设计思想

对于通常的线性表,好比链表,若是要存储联系人信息:编程

张三 13980593357
李四 15828662334
王五 13409821234
张帅 13890583472

那么可能会设计一个结构体包含姓名,手机号码这些信息,而后把4个联系人的信息存到一张链表中。当要查找”李四 15828662334“这条记录是否在这张链表中或者想要获得李四的手机号码时,可能会从链表的头结点开始遍历,依次将每一个结点中的姓名同”李四“进行比较,直到查找成功或者失败为止,这种作法的时间复杂度为O(n)。即便采用二叉排序树进行存储,也最多为O(logn)。假设可以经过”李四“这个信息直接获取到该记录在表中的存储位置,就能省掉中间关键字比较的这个环节,复杂度直接降到O(1)。Hash表就可以达到这样的效果。数组

Hash表采用一个映射函数 f :key —> address 将关键字映射到该记录在表中的存储位置,从而在想要查找该记录时,能够直接根据关键字和映射关系计算出该记录在表中的存储位置,一般状况下,这种映射关系称做为Hash函数,而经过Hash函数和关键字计算出来的存储位置(注意这里的存储位置只是表中的存储位置,并非实际的物理地址)称做为Hash地址。好比上述例子中,假如联系人信息采用Hash表存储,则当想要找到“李四”的信息时,直接根据“李四”和Hash函数计算出Hash地址便可。下面讨论一下Hash表设计中的几个关键问题。数据结构

1、Hash函数的设计

Hash函数设计的好坏直接影响到对Hash表的操做效率。下面举例说明:ide

假如对上述的联系人信息进行存储时,采用的Hash函数为:姓名的每一个字的拼音开头大写字母的ASCII码之和。函数

      address(张三)=ASCII(Z)+ASCII(S)=90+83=173;
    address(李四)=ASCII(L)+ASCII(S)=76+83=159;
    address(王五)=ASCII(W)+ASCII(W)=87+87=174;
    address(张帅)=ASCII(Z)+ASCII(S)=90+83=173;

假如只有这4个联系人信息须要进行存储,这个Hash函数设计的很糟糕。性能

首先,它浪费了大量的存储空间。由于假如采用char型数组存储联系人信息的话,每一个人的信息须要12个字节来存储。
(手机号为11位,数值上为100多亿,2^64 =1.844674407371 * 10^19,2^32 = 4294967296,因此须要64位也就是8个字节来存储手机号。
每一个汉字占两个字节,两个汉字占四个字节。
因此总共须要8 + 4 = 12Byte)
这样的话,至少须要开辟174*12字节的空间。然而空间利用率只有4/174,不到3%。spa

另外,根据Hash函数计算结果以后,address(张三)和address(张帅)具备相同的地址,这种现象称做冲突,对于174个存储空间中只须要存储4条记录就发生了冲突,这样的Hash函数设计是很不合理的。因此在构造Hash函数时应尽可能考虑关键字的分布特色来设计函数使得Hash地址随机均匀地分布在整个地址空间当中。设计

一般有如下几种构造Hash函数的方法:code

1 直接定址法
  取关键字或者关键字的某个线性函数为Hash地址,即address(key)=a*key+b;如知道学生的学号从2000开始,最大为4000,则能够将address(key)=key-2000做为Hash地址。

2 平方取中法
  对关键字进行平方运算,而后取结果的中间几位做为Hash地址。假若有如下关键字序列{421,423,436},平方以后的结果为{177241,178929,190096},那么能够取中间的两位数{72,89,00}做为Hash地址。

3 折叠法
  将关键字拆分红几部分,而后将这几部分组合在一块儿,以特定的方式进行转化造成Hash地址。假如知道图书的ISBN号为8903-241-23,能够将address(key)=89+03+24+12+3做为Hash地址。

4 除留取余法
  若是知道Hash表的最大长度为m,能够取不大于m的最大质数p,而后对关键字进行取余运算,address(key)=key%p。

在这里p的选取很是关键,p选择的好的话,可以最大程度地减小冲突,p通常取不大于m的最大质数。

2、Hash表大小的肯定

Hash表大小的肯定也很是关键,若是Hash表的空间远远大于最后实际存储的记录个数,则形成了很大的空间浪费,若是选取小了的话,则容易形成冲突。在实际状况中,通常须要根据最终记录存储个数和关键字的分布特色来肯定Hash表的大小。还有一种状况时可能事先不知道最终须要存储的记录个数,则须要动态维护Hash表的容量,此时可能须要从新计算Hash地址。

3、冲突的解决

在上述例子中,发生了冲突现象,所以须要办法来解决,不然记录没法进行正确的存储。一般状况下有2种解决办法:

1 开放定址法
  即当一个关键字和另外一个关键字发生冲突时,使用某种探测技术在Hash表中造成一个探测序列,而后沿着这个探测序列依次查找下去,当碰到一个空的单元时,则插入其中。比较经常使用的探测方法有线性探测法,好比有一组关键字{12,13,25,23,38,34,6,84,91},Hash表长为14,Hash函数为address(key)=key%11,当插入12,13,25时能够直接插入,而当插入23时,地址1被占用了,所以沿着地址1依次往下探测(探测步长能够根据状况而定),直到探测到地址4,发现为空,则将23插入其中。

2 链地址法
  采用数组和链表相结合的办法,将Hash地址相同的记录存储在一张线性表中,而每张表的表头的序号即为计算获得的Hash地址。如上述例子中,采用链地址法造成的Hash表存储表示为:

 
hash.jpg

虽然可以采用一些办法去减小冲突,可是冲突是没法彻底避免的。所以须要根据实际状况选取解决冲突的办法。

4、Hash表的平均查找长度

Hash表的平均查找长度包括查找成功时的平均查找长度和查找失败时的平均查找长度。
  查找成功时的平均查找长度=表中每一个元素查找成功时的比较次数之和/表中元素个数;
查找不成功时的平均查找长度至关于在表中查找元素不成功时的平均比较次数,能够理解为向表中插入某个元素,该元素在每一个位置都有可能,而后计算出在每一个位置可以插入时须要比较的次数,再除以表长即为查找不成功时的平均查找长度。

下面的例子有助于理解。

例1

将关键字序列{7, 8, 30, 11, 18, 9, 14}散列存储到散列表中。散列表的存储空间是一个下标从0开始的一维数组,长度为10,即{0, 1,2, 3, 4, 5, 6, 7, 8, 9}。散列函数为: H(key) = (key * 3) % 7,处理冲突采用线性探测再散列法。

求等几率状况下查找成功和查找不成功的平均查找长度。

解:

1 求散列表

H(7) = (7 * 3) % 7 = 0
H(8) = (8 * 3) % 7 = 3
H(30) = 6
H(11) = 5
H(18) = 5
H(9) = 6
H(14) = 0

按关键字序列顺序依次向哈希表中填入,发生冲突后按照“线性探测”探测到第一个空位置填入。
H(7) = 0,key = 7应插在第0个位置,由于第0个位置为空,能够直接插入。
H(8) = 3,key = 8应插在第3个位置,由于第3个位置为空,能够直接插入。
H(30) = 6,key = 30应插在第6个位置,由于第6个位置为空,能够直接插入。
H(11) = 5,key = 11应插在第5个位置,由于第5个位置为空,能够直接插入。
H(18) = 5,key = 18应插在第5个位置,可是第5个位置已经被key=11占据了,因此日后挪一位到第6个位置,可是第6个位置被key=30占据了,再日后挪一位到第7个位置,这个位置是空的,因此key=18就插到这个位置
H(9) = 6,key = 9应插在第6个位置,可是第6个位置已经被key = 30占据,因此须要日后挪一位到第7个位置,可是第7个位置已经被key = 18占据,因此再日后挪移到第8个位置,这个位置是空的,因此key = 9就插到这个位置。
H(14) = 0,key = 14应插在第0个位置,但第0个位置已被key=7占据,因此日后挪移一位到第1个位置,这个位置是空的,因此key=14就插到这个位置。

最终的插入结果以下表所示:

address 0 1 2 3 4 5 6 7 8 9
key 7 14   8   11 30 18 9  

2 求查找成功的平均查找长度

查找7,H(7) = 0,在0的位置,一会儿就找到了7,查找长度为1。
查找8,H(8) = 3,在3的位置,一会儿就找到了8,查找长度为1。
查找30,H(30) = 6,在6的位置,一会儿就找到了30,查找长度为1。
查找11,H(11) = 5,在5的位置,一会儿就找到了11,查找长度为1。
查找18,H(18) = 5,第一次在5的位置没有找到18,第二次日后挪移一位到6的位置,仍没有找到,第三次再日后挪移一位到7的位置,找到了,查找长度为3。
查找9,H(9) = 6,第一次在6的位置没找到9,第二次日后挪移一位到7的位置,仍没有找到,第三次再日后挪移一位到8的位置,找到了,查找长度为3.
查找14,H(14) = 0,第一次在0的位置没找到14,第二次日后挪移一位到1的位置,找到了,查找长度为2。

因此,查找成功的平均查找长度为(1 + 1 + 1 + 1 + 3 + 3 + 2) / 7 = 12 / 7。

3 求查找不成功的平均查找长度

查找不成功,说明要查找的数字确定不在上述的散列表中。
由于这里哈希函数的模为7,因此要查找的数的初始地址只可能位于0~6的位置上。
地址0,到第一个关键字为空的地址2须要比较3次,所以查找不成功的次数为3。好比要查找的数为28,H(28) = (28 * 3) % 7 = 0。即28对应的地址是0,因为存放在0位置的数是7,因此日后挪移一位,发如今1位置存放的数是14,继续日后挪一位,发现位置2上没有数。至此就知道28不在这个哈希表里,即查找28失败。
地址1,到第一个关键字为空的地址2须要比较2次,所以查找不成功的次数为2。
地址2,到第一个关键字为空的地址2须要比较1次,所以查找不成功的次数为1。
地址3,到第一个关键字为空的地址4须要比较2次,所以查找不成功的次数为2。
地址4,到第一个关键字为空的地址4须要比较1次,所以查找不成功的次数为1。
地址5,到第一个关键字为空的地址9须要比较5次,所以查找不成功的次数为5。
好比要查找的数为4,H(4) = (4 * 3) % 7 = 5,因此从地址5开始查找,最终发现地址五、地址六、地址七、地址8上存放的数都不是5,而且地址9的位置上没放数据,至此可知5不在这个哈希表里。
地址6,到第一个关键字为空的地址9须要比较4次,所以查找不成功的次数为4。
因此,查找不成功的平均查找长度为(3 + 2 + 1 + 2 + 1 + 5 + 4)/ 7 = 18 / 7。

5、优缺点

优势:
不论哈希表中有多少数据,查找、插入、删除(有时包括删除)只须要接近常量的时间即0(1)的时间级。实际上,这只须要几条机器指令。
哈希表运算得很是快,在计算机程序中,若是须要在一秒种内查找上千条记录一般使用哈希表(例如拼写检查器)哈希表的速度明显比树快,树的操做一般须要O(N)的时间级。哈希表不只速度快,编程实现也相对容易。
若是不须要有序遍历数据,而且能够提早预测数据量的大小。那么哈希表在速度和易用性方面是无与伦比的。

缺点:
它是基于数组的,数组建立后难于扩展,某些哈希表被基本填满时,性能降低得很是严重,因此程序员必需要清楚表中将要存储多少数据,或者准备好按期地把数据转移到更大的哈希表中,这是个费时的过程。

 

/*采用数组实现哈希表*/ 

#include<stdio.h>
#define DataType int
#define Len 10
 
typedef struct HashNode    
{
    DataType data;    //存储值 
    int isNull;       //标志该位置是否已被填充 
}HashTable;

HashTable hashTable[Len];

void initHashTable()     //对hash表进行初始化 
{
    int i;
    for(i = 0; i<Len; i++)
    {
        hashTable[i].isNull = 1;    //初始状态为空 
    }
}

int getHashAddress(DataType key)    //Hash函数 
{
    return key * 3 % 7;      
}

int insert(DataType key)    
{
    int address = getHashAddress(key);       
    if(hashTable[address].isNull == 1)  //没有发生冲突 
    {
        hashTable[address].data = key;
        hashTable[address].isNull = 0;
    }
    else    //当发生冲突的时候 
    {
        while(hashTable[address].isNull == 0 && address<Len)
        {
            address++;          //采用线性探测法,步长为1 
        }
        if(address == Len)      //Hash表发生溢出 
            return -1;
        hashTable[address].data = key;
        hashTable[address].isNull = 0;
    }
    
    return 0;
}

int find(DataType key)       
{
    int address = getHashAddress(key);
    while( !(hashTable[address].isNull == 0 && hashTable[address].data == key && address<Len))
    {
        address++;
    } 
    
    if( address == Len)
    {
        address = -1;
    }
    
    return address;
}


int main(int argc, char *argv[])
{
    int key[]={7,8,30,11,18,9,14};
    int i;
    initHashTable();
    
    for(i = 0; i<7; i++)
    {
        insert(key[i]);
    }
    
    for(i = 0; i<7; i++)
    {
        int address;
        address = find(key[i]);
        printf("key:%d\t address:%d\n", key[i],address);
    }
    
    return 0;
}
View Code
相关文章
相关标签/搜索