散列的定义
散列技术是在记录的存储位置和它的关键字之间建立一个确定的对应关系f
,使得每个关键字key
对应一个存储位置f(key)
其中的对应关系f称为散列函数
,又称哈希(Hash)函数
,采用散列技术将记录存储在一块连续的存储空间中,这块连续的存储空间被称为散列表
或者哈希表(Hash table)
散列表的查找步骤
- 存储时,通过散列函数计算记录的散列地址,并且按照这个散列地址存储该记录。
- 查找时,通过同样的散列函数计算记录的散列地址,按此散列地址访问该记录。
散列不适用的场合
- 关键字对应多个记录
- 范围查找
哈希函数的常见的构造方法
什么才是一个好的散列函数?
- 计算简单(时间)
- 散列地址分布均匀
直接定值法
取关键字的某个线性函数值为散列地址,优点是简单,均匀,不会产生冲突,但是需要预先知道关键字的分布情况,适合查找表较小且连续的情况。不常用。
数字分析法
适用于关键字是数字如手机号码,对这个号码进行抽取分析。
一般用于关键字位数比较大,同时知道关键字的分布且关键字的若干位分布均匀的情况。
平方取中法
比如:关键字是1234, 12342=1522756 ,在抽取中间的三位即可 :227.
这种方法比较适合不知道关键字的分布,而位数又不是很大的情况。
折叠法
将关键字从左往右分成位数相等的几部分,最后一部分位数不够可以短一些。然后将这几部分叠加求和,并按照散列表的表长,取后几位作为散列地址。
比如关键字是9876543210,散列表表长为三位,那么分成四组,分别是987|654|321|0,叠加求和 987+654+321+0=1962 ,在求后三位得到散列地址为 962
折叠法事先不需要知道关键字的分布,适合关键字位数较多的情况。
除留余数法
这是最常用的方法,对于散列表长为m的散列函数,公式为:
这种方法的关键在于p的选取,通常p为小于或等于表长(最好接近m)的最小质数。
随机数法
这里的random是随机函数,当关键字的长度不等时,采用这个方法比较合适。
选择散列函数的参考
- 计算散列地址所需的时间
- 关键字的长度
- 散列表的大小
- 关键字的分布情况
- 记录查找的频率
哈希的术语
闭哈希法:开放地址法
开哈希法:链地址法(拉链法)
散列冲突
两个关键字
key1≠key2
,但是却有
f(key1)==f(key2)
,这种现象称为冲突,并把key1
和key2
称为这个散列函数的同义词(synonym)
开放定址法
一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到。
这种解决方式称为线性探测法。
这种解决方式称为二次探测法。
一般只要散列表未填满,总是能找到不发生冲突的地址。
再散列函数法
当发生冲突时,换一个散列函数。
链地址法
为什么有冲突就要换地方?
将所有关键字为同义词的记录存储在一个单链表中,这种表为同义词子表,在散列表中只存储所有同义词子表的头指针。无论是多少个冲突,都是在当前位置给单链表增加节点。
缺点是:增加了遍历单链表的性能耗损。
公共溢出区法
这个更好理解,就是将冲突的建立一个公共的溢出区来存放
当查找时,先于基本表进行查找,如果想等,查找成功。不相等,则去溢出表进行顺序查找
代码实现
#include <stdio.h>
#include <malloc.h>
#define SUCCESS 1
#define UNSUCCESS 0
#define HASHSIZE 12
#define NULLKEY -32786
#define OK 1
int m = 0;
typedef struct HashTable
{
int* elem;
int count;
}HashTable;
/**
* @author 韦轩
* @time 2015/09/03
* @brief 初始化散列表
* @param
* @return
*
*/
int InitHashTable(HashTable *hashTable)
{
int i = 0;
m = HASHSIZE;
hashTable->count = m;
hashTable->elem = (int*)malloc(sizeof(int));
for (int i = 0; i < m;i++)
{
hashTable->elem[i] = NULLKEY;
}
return OK;
}
/**
* @author 韦轩
* @time 2015/09/03
* @brief 散列函数
* @param
* @return
*
*/
int Hash(int key)
{
return key % m;
}
/**
* @author 韦轩
* @time 2015/09/03
* @brief 插入
* @param
* @return
*
*/
void InsertHash(HashTable* hashTable, int key)
{
int addr = Hash(key); /*求散列地址*/
while (hashTable->elem[addr] != NULLKEY) /*冲突*/
addr = (addr + 1) % m; /*开地址法的线性探测*/
hashTable->elem[addr] = key;
}
/**
* @author 韦轩
* @time 2015/09/03
* @brief 查找
* @param
* @return
*
*/
int SeachHash(HashTable hashTable, int key, int* addr)
{
*addr = Hash(key);
while (hashTable.elem[*addr] != key)
{
*addr = *(addr + 1) % m;
if (hashTable.elem[*addr] == NULLKEY || *addr == Hash(key))
return UNSUCCESS;
}
return SUCCESS;
}
int main()
{
return 0;
}
性能分析
- 如果没有冲突,那么查找的时间复杂度是 O(1)
平均查找长度取决于什么?
- 散列函数是否均匀
- 处理散列函数的方法 线性探测有可能会产生堆积,没有二次探测好,而链地址法不会产生任何堆积,因而具有更佳的平均查找性能
- 散列表的装填因子
a=填入表中的记录个数散列表的长度
a标志着散列表的装满程度,a越大,产生冲突的可能性就越大。
散列表的平均查找长度取决于装填因子,与集合的个数无关
哈希表其他TIPS
- 哈希查找可以在外存中查找,可以用哈希表映射到文件,分级查找
- 拉链式哈希曼最坏查找时间复杂度是O(n),最坏情况是所有记录的散列值都冲突,这样就退化为线性查找,时间复杂度为O(n),拉链法的缺点是:指针需要额外的空间,故当结点规模较小时,开放定址法较为节省空间,在用拉链法构造的散列表中,删除结点的操作易于实现。
- 开放定址更适合于造表前确定表长的情况,由于拉链法中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况
- 拉链法解决冲突的做法是:将所有关键字为同义词的结点链接在同一个单链表中,拉链法处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短
- 平均查找长度
(19,14,23,1,68,20,84,27,55,11,10,79)散列存储在一个哈希表中,若散列函数为H(key)=key%7,并采用链地址法来解决冲突,则在等概率情况下查找的平均查找长度为:
这些关键字除以7取余后分别得到5,0,2,1,5,6,0,6,6,4,3,2存储结构如下
位置–存储
0—–14-84 //14查找1次,84需要查找2次
1—–1
2—–23-79 //23 查找一次,79查找两次
3—–10
4—–11
5—–19-68 //19查找一次,68查找两次
6—–20-27-55 //20查找一次,27查找两次,55查找3次
总查找次数为1+2+1+1+2+1+1+1+2+1+2+3=18
平均查找长度为18/12=1.5
6. 假定有k个关键字互为同义词,若用线性探测法把这k个关键字存入哈希表中,至少要进行
k(k+1)2
次探测
7. 数组的插入和删除可以 O(1),哈希表支持直接通过关键码得到值 其实数组就是一种哈希表 下标就是关键码 通过下标直接得到值 因此哈希表肯定需要做范围检查也有办法做范围检查的
8. 采用开址定址法解决冲突的哈希查找中,发生集聚的原因主要是解决冲突的算法选择不好。