1.概述
散列技术是在记录的储存位置和它的关键字之间建立一个确定的对应关系f,使得每个关键字key对应一个存储位置f(key)。
这种对应关系 f 称为散列函数,又称为哈希(Hash)函数。
采用散列技术将记录存储在一块连续的存储空间中,这块连续存储空间称为散列表或哈希表。那么关键字对应的记录存储位置我们称为散列地址。
散列表的查找步骤:
(1)在存储记录时,通过散列函数计算出记录的散列地址,并按此散列地址存储该记录。
(2)在查找记录时,我们通过同样的是散列函数计算记录的散列地址,并按此散列地址访问该记录。
散列技术既是一种存储方法,也是一种查找方法。
2.散列函数的构造方法
好的散列函数要求:(1)计算简单,至少散列函数的计算时间不应该超过其他查找技术与关键字比较的时间;(2)计算出的散列地址分布均匀,这样可以保证存储空间的有效利用,并减少为处理冲突而耗费的时间。
(1) 直接定址法
取关键字或关键字的某个线性函数值为散列地址。即H(key) = a·key + b,其中a和b为常数。
(2) 数字分析法
假设某公司的员工登记表以员工的手机号作为关键字。手机号一共11位。前3位是接入号,对应不同运营商的子品牌;中间4位表示归属地;最后4位是用户号。不同手机号前7位相同的可能性很大,所以可以选择后4位作为散列地址,或者对后4位反转(1234 -> 4321)、循环右移(1234 -> 4123)、循环左移等等之后作为散列地址。
抽取方法是使用关键字的一部分来计算散列存储位置的方法,这在散列函数中是常常用到的手段。
数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的若干位分布比较均匀,就可以考虑这个方法。
(3)平方取中法
假设关键字是1234、平方之后是1522756、再抽取中间3位227,用作散列地址。
平方取中法比较适合于不知道关键字的分布,而位数又不是很大的情况。
(4)折叠法
折叠法将关键字从左到右分割成位数相等的几部分,最后一部分位数不够时可以短些,然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。
比如关键字是9876543210,散列表表长是3位,将其分为四组,然后叠加求和:987 + 654 + 321 + 0 = 1962,取后3位962作为散列地址。
折叠法事先不需要知道关键字的分布,适合关键字位数较多的情况。
(5)除留余数法
f(key) = key mod p (p≤m),m为散列表长。这种方法不仅可以对关键字直接取模,也可在折叠、平方取中后再取模。根据经验,若散列表表长为m,通常p为小于或等于表长(最好接近m)的最小质数,可以更好的减小冲突。
此方法为最常用的构造散列函数方法。
(6)随机数法
f(key) = random(key),这里random是随机函数。当关键字的长度不等时,采用这个方法构造散列函数是比较合适的。
实际应用中,应该视不同的情况采用不同的散列函数。如果关键字是英文字符、中文字符、各种各样的符号,都可以转换为某种数字来处理,比如其unicode编码。
下面这些因素可以作为选取散列函数的参考:(1)计算散列地址所需的时间;(2)关键字长度;(3)散列表大小;(4)关键字的分布情况;(5)查找记录的频率。综合这些因素,才能决策选择哪种散列函数最合适。
3.处理散列冲突的方法两个关键字key1 != key2,但是却有 f(key1)==f(key2),这种现象我们称为冲突,并把key1和key2称为这个散列函数的同义词(synonym)。
常用的处理冲突的方法:
(1)开放定址法
开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。它的公式是:
Hi=(H(key) + di) MOD m, i=1,2,…, k(k<=m-1),其中H(key)为散列函数,m为散列表长,di为增量序列,可有下列三种取法:
1. di=1,2,3,…, m-1,称线性探测再散列;
2. di=1^2, (-1)^2, 2^2,(-2)^2, (3)^2, …, ±(k)^2,(k<=m/2)称二次探测再散列;
3. di=伪随机数序列,称伪随机探测再散列。
(2)再散列函数法
Hi=RHi(key), i=1,2,…,k RHi均是不同的散列函数,即在同义词产生地址冲突时计算另一个散列函数地址,直到冲突不再发生,这种方法不易产生“聚集”,但增加了计算时间。
(3)链地址法
当存储结构是链表时,多采用拉链法,用拉链法处理冲突的办法是:把具有相同散列地址的关键字(同义词)值放在同一个单链表中,称为同义词链表。有m个散列地址就有m个链表,同时用指针数组T[0..m-1]存放各个链表的头指针,凡是散列地址为i的记录都以结点方式插入到以T[i]为指针的单链表中。T中各分量的初值应为空指针。
例如,按上面例9.4所给的关键字序列,用拉链法构造散列表如图所示。
用拉链法处理冲突,虽然比开放定址法多占用一些存储空间用做链接指针,但它可以减少在插入和查找过程中同关键字平均比较次数(平均查找长度),这是因为,在拉链法中待比较的结点都是同义词结点,而在开放定址法中,待比较的结点不仅包含有同义词结点,而且包含有非同义词结点,往往非同义词结点比同义词结点还要多
4.散列表的查找实现
代码:
<span style="font-family:SimSun;font-size:18px;">#include <stdio.h>
#include <stdlib.h>
#define OK 1
#define ERROR 0
typedef int Status;
//定义一个散列表的结构和相关的常数
#define SUCCESS 1
#define UNSUCCESS 0
#define HASHSIZE 12 //定义散列表长为数组的长度
#define NULLKEY -32768
typedef struct
{
int *elem; //数据元素存储基址,动态分配数组
int count; //当前数据元素个数
}HashTable;
int m=0; // 散列表表长,全局变量
//初始化散列表
Status InitHashTable(HashTable *H)
{
int i;
m=HASHSIZE;
H->count=m;
H->elem = (int *)malloc(m*sizeof(int));
for(i=0;i<m;i++)
H->elem[i]=NULLKEY;
return OK;
}
//散列函数
int Hash(int key)
{
return key % m; //除留余数法
}
//插入关键字进散列表
void InsertHash(HashTable *H,int key)
{
int addr = Hash(key); //求散列地址
while(H->elem[addr] != NULLKEY) //如果不为空,则冲突
addr = (addr+1) % m;
H->elem[addr] = key;
}
//散列表查找关键字
Status SearchHash(HashTable H, int key,int *addr)
{
*addr = Hash(key);
while(H.elem[*addr] !=key)
{
*addr=(*addr+1)%m;
if(H.elem[*addr]==NULLKEY||*addr==Hash(key))
return UNSUCCESS;
}
return SUCCESS;
}
int main()
{
int arr[HASHSIZE]={12,67,56,16,25,37,22,29,15,47,48,34};
int i,p,key,result;
HashTable H;
key=39;
InitHashTable(&H);
for(i=0;i<m;i++)
InsertHash(&H,arr[i]);
result=SearchHash(H,key,&p);
if (result)
printf("查找 %d 的地址为:%d \n",key,p);
else
printf("查找 %d 失败。\n",key);
for(i=0;i<m;i++)
{
key=arr[i];
SearchHash(H,key,&p);
printf("查找 %d 的地址为:%d \n",key,p);
}
return 0;
}
</span>