Hash表
首先说一种数据结构,Hash表,也叫哈希表或者散列表,索引表。它同数组、链表以及二叉排序树等数据结构有很明显的区别,它能够快速定位到想要查找的记录。也就是说,某个元素存储的地址跟这个元素的内容之间存在一种映射。
比如,有一张用户个人信息表,你想查找“李四”这个人,如果是用普通的数组存储,你需要遍历每一个数组元素,时间复杂度是O(n),就算用二叉树,时间复杂度也是O(log n)。但是,如果建立了散列表,你就可以根据“李四”这个值直接找到他的个人信息在整张表中的存储位置。
Hash函数
建立一张哈希表,最重要的就是哈希函数,也叫哈希算法。因为很有可能出现多个值映射到同一个地址的情况,比如“张三”和“李四”都映射到表中的同一个位置。
常见的设计哈希函数的方法有以下几种:
直接定址法
取关键字或者关键字的某个线性函数为Hash地址,即address(key)=a*key+b;如知道学生的学号从2000开始,最大为4000,则可以将address(key)=key-2000作为Hash地址。
平方取中法
对关键字进行平方运算,然后取结果的中间几位作为Hash地址。假如有以下关键字序列{421,423,436},平方之后的结果为{177241,178929,190096},那么可以取{72,89,00}作为Hash地址。
折叠法
将关键字拆分成几部分,然后将这几部分组合在一起,以特定的方式进行转化形成Hash地址。假如知道图书的ISBN号为8903-241-23,可以将address(key)=89+03+24+12+3作为Hash地址。
或者比如上面的用户个人信息表:
address(“张三”)=ASCII(‘Z’)+ASCII(‘S’)=173
address(“李四”)=ASCII(‘L’)+ASCII(‘S’)=159
这种方法也很有可能出现地址冲突的情况,比如“张三”和“周四”的地址就冲突了。
除留取余法
如果知道Hash表的最大长度为m,可以取不大于m的最大质数p,然后对关键字进行取余运算,address(key)=key%p。
在这里p的选取非常关键,p选择的好的话,能够最大程度地减少冲突,p一般取不大于m的最大质数。
哈希表大小的确定
Hash表大小的确定也非常关键,如果Hash表的空间远远大于最后实际存储的记录个数,则造成了很大的空间浪费,如果选取小了的话,则容易造成冲突。在实际情况中,一般需要根据最终记录存储个数和关键字的分布特点来确定Hash表的大小。还有一种情况时可能事先不知道最终需要存储的记录个数,则需要动态维护Hash表的容量,此时可能需要重新计算Hash地址。
最后是关于地址冲突的解决,上面四种方法,地址冲突都难以完全避免,主要有以下两种解决办法:
开放地址法
即当一个关键字和另一个关键字发生冲突时,使用某种探测技术在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插入其中。
链地址法
采用数组和链表相结合的办法,将Hash地址相同的记录存储在一张线性表中,而每张表的表头的序号即为计算得到的Hash地址。如上述例子中,采用链地址法形成的Hash表存储表示为:
哈希表的平均查找长度
Hash表的平均查找长度包括查找成功时的平均查找长度和查找失败时的平均查找长度。
查找成功时的平均查找长度=表中每个元素查找成功时的比较次数之和/表中元素个数;
查找不成功时的平均查找长度相当于在表中查找元素不成功时的平均比较次数,可以理解为向表中插入某个元素,该元素在每个位置都有可能,然后计算出在每个位置能够插入时需要比较的次数,再除以表长即为查找不成功时的平均查找长度。
比如有一组关键字{23,12,14,2,3,5},表长为14,Hash函数为key%11,则关键字在表中的存储如下:
地址 0 1 2 3 4 5 6 7 8 9 10 11 12 13
关键字 23 12 14 2 3 5
比较次数 1 2 1 3 3 2
关键字 0 1 13 25 4 16 6 7 8 9 10 - - -
比较次数 1 7 6 5 4 3 2 1 1 1 1 1 1 1
因此
查找成功时的平均查找长度为(1+2+1+3+3+2)/6=11/6;
查找失败时的平均查找长度为(1+7+6+5+4+3+2+1+1+1+1+1+1+1)/14=38/14;
下面是建立一个哈希表的代码:
#include<stdio.h>
#define DataType int
#define M 30
typedef struct HashNode
{
DataType data; //存储值
int isNull; //标志该位置是否已被填充
}HashTable;
HashTable hashTable[M];
void initHashTable() //对hash表进行初始化
{
int i;
for(i = 0; i<M; i++)
{
hashTable[i].isNull = 1; //初始状态为空
}
}
int getHashAddress(DataType key) //Hash函数
{
return key % 29; //Hash函数为 key%29
}
int insert(DataType key) //向hash表中插入元素
{
int address = getHashAddress(key);
if(hashTable[address].isNull == 1) //没有发生冲突
{
hashTable[address].data = key;
hashTable[address].isNull = 0;
}
else //当发生冲突的时候
{
while(hashTable[address].isNull == 0 && address<M)
{
address++; //采用线性探测法,步长为1
}
if(address == M) //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<M))
{
address++;
}
if( address == M)
address = -1;
return address;
}
int main(int argc, char *argv[])
{
int key[]={123,456,7000,8,1,13,11,555,425,393,212,546,2,99,196};
int i;
initHashTable();
for(i = 0; i<15; i++)
{
insert(key[i]);
}
for(i = 0; i<15; i++)
{
int address;
address = find(key[i]);
printf("%d %d\n", key[i],address);
}
return 0;
}