一、定义
散列技术是记录的存储位置和它的关键字之间建立一个确定的对应关系f,使得每个关键字key对应一个存储位置f(key)。查找时,根据这个对应关系找到给定值key的映射f(key)。若查找集合中存在这个记录,则必定在f(key)的位置上。
存储位置 = f (关键字)
我们把这种对应关系f称为散列函数,又称为哈希(Hash)函数。
采用散列技术将记录存储在一块连续的存储空间中,这块连续的存储空间称为散列表或者哈希表。
关键字对应的记录存储位置我们称为散列地址。
二、散列查找步骤
- 在存储时,通过散列函数计算记录的散列地址,并按此散列地址存储该记录。
- 当查找记录时,我们通过同样的散列函数计算记录的散列地址,按此散列地址访问该记录。
散列技术既是一种存储方法,也是一种查找方法。
与线性表、树、图等结构不同的是,线性表、树、图数据元素之间存在某种逻辑关系,可以用连线图表示出来,而散列技术记录的数据元素之间不存在什么逻辑关系,它只与关键字有关联。
我们时常会碰到两个关键字key1不等于key2,但是却有f(key1)=f(key2),这种现象我们称为冲突,并把key1和key2称为这个散列函数的同义词。
三、散列函数的构造方法
1. 直接定址法
我们可以取关键字的某个线性函数值为散列地址:
f(key) = a*key+b(a、b为常数)
优点:简单、均匀、也不会产生冲突。
缺点:需要事先知道关键字的分布情况。
适用范围:查找表较小且连续的情况。
2. 数字分析法
使用关键字的一部分来计算散列存储位置的方法,如电话号码存储,可抽取电话号码的中间几位进行反转/右移/左移等操作,然后将关键字分配到散列表的各位置。
适用范围:通常用于处理关键字位数比较大的情况。
3. 平方取中法
这个方法计算比较简单,假设关键字1234,那么它的平方为1522756,再抽取中间3位就是227,用做散列地址。再比如4321,那么它的平方为18671041,抽取中间3位就可以是671,也可以是710用做散列地址。
适用范围:不知道关键字分布情况,而且位数又不是很大的情况。
折叠法
将关键字从左到右分割成位数相等的几部分(最后一部分位数不够时可以短些),然后将这几部分叠加求和,并按散列表表长,取后几位做为散列地址。
适用范围:事先不需要知道关键字的分布,适合关键字位数较多的情况。
4. 除留余数法
对于散列表长为m的散列函数公式为:
f(key) = key mod p(p<=m)
其中mod是取模的意思。
事实上,这个方法不仅可以对关键字取模,也可以在折叠、平方取中后再取模。
此方法的关键在于如何选择p,p如果选不好,则可能很容易产生同义词。通常p为小于或等于表长的最小质数或不包含小于20质因子的合数。
5. 随机数法
选择一个随机数,取关键字的随机函数值为它的散列地址。
f(key) = random(key)
适用范围:关键字长度不等时,可采用此方法。
四、处理散列冲突的办法
1. 开放定址法
开放定址法是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散表地址总能被找到,并将记录存入。
f(key) = (f(key)+d) mod m (d=1,2,3...m-1)
这种解决冲突的开放定址法称为线性探测法。
2. 再散列函数法
每当发生散列冲突的时候,就换一个散列函数计算,相信总有一个会把冲突解决掉。
f(key) = RH(key)
其中RH是另一个散列函数。
3. 链地址法
将所有关键字的同义词的记录存储在一个单链表中,我们称这种表为同义词子表,在散列表中只存储所有同义词子表的头指针。
此时已经完全不存在冲突问题,无论有多少个冲突,都只是在当前位置给单链表增加结点而已。
链地址法对于可能会造成很多冲突的散列函数来说,提供了绝对不会出现找不到地址的保障,但是查找时需要遍历单链表,增加了性能损耗。
五、散列表查找实现
typedef int Status;
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
#define MAXSIZE 100 //存储空间初始分配量
#define SUCCESS 1
#define UNSUCCESS 0
//定义散列表长为数组的长度
#define HASHSIZE 12
#define NULLKEY -32768
typedef struct
{
//数据元素存储基址,动态分配数组
int *elem;
//当前数据元素个数
int count;
}HashTable;
int m=0; /* 散列表表长,全局变量 */
//1.初始化散列表
Status InitHashTable(HashTable *H)
{
int i;
//① 设置H.count初始值; 并且开辟m个空间
m=HASHSIZE;
H->count=m;
H->elem=(int *)malloc(m*sizeof(int));
//② 为H.elem[i] 动态数组中的数据置空(-32768)
for(i=0;i<m;i++)
H->elem[i]=NULLKEY;
return OK;
}
//2. 散列函数
int Hash(int key)
{
//除留余数法
return key % m;
}
//3. 插入关键字进散列表
void InsertHash(HashTable *H,int key)
{
//① 求散列地址
int addr = Hash(key);
//② 如果不为空,则冲突
while (H->elem[addr] != NULLKEY)
{
//开放定址法的线性探测
addr = (addr+1) % m;
}
//③ 直到有空位后插入关键字
H->elem[addr] = key;
}
//4. 散列表查找关键字
Status SearchHash(HashTable H,int key,int *addr)
{
//① 求散列地址
*addr = Hash(key);
//② 如果不为空,则冲突
while(H.elem[*addr] != key)
{
//③ 开放定址法的线性探测
*addr = (*addr+1) % m;
//④H.elem[*addr] 等于初始值或者循环有回到了原点.则表示关键字不存在;
if (H.elem[*addr] == NULLKEY || *addr == Hash(key))
//则说明关键字不存在
return UNSUCCESS;
}
return SUCCESS;
}
int main(int argc, const char * argv[]) {
// insert code here...
printf("Hello, World!\n");
int arr[HASHSIZE]={12,67,56,16,25,37,22,29,15,47,48,34};
int i,p,key,result;
HashTable H;
//1.初始化散列表
InitHashTable(&H);
//2.向散列表中插入数据
for(i=0;i<m;i++)
InsertHash(&H,arr[i]);
//3.在散列表查找key=39
key=39;
result=SearchHash(H,key,&p);
if (result)
printf("查找 %d 的地址为:%d \n",key,p);
else
printf("查找 %d 失败。\n",key);
//4.将数组中的key,打印出所有在散列表的存储地址
for(i=0;i<m;i++)
{
key=arr[i];
SearchHash(H,key,&p);
printf("查找 %d 的地址为:%d \n",key,p);
}
return 0;
}