关于数据结构与算法的相关知识,请查看文章《数据结构与算法基础知识文章汇总》。
一、散列技术
散列技术是记录的位置和它的关键字key之间建立一个确定的对应关系f,使得每个关键字key对应一个存储位置f(key)。查找时,根据关键字key与存储位置之间的对应关系f(key),求得关键字key在内存中的存储位置,如果找到的位置的数据刚好就是关键字key,则查找成功。
假如有如下数据:
10、22、34、43、9、81、47、56
现在需要我们设计一个公式,求得上面的每个在数组中的存储位置,而且存储的位置在数组中是相对均匀的。并且这个公式不能太复杂,要相对简单。
- 1.所求的公式即为
哈希公式;- 2.存储时,把数据代入哈希公式,即求得存储的位置,即
哈希地址;- 3.查找时,把数据代入哈希公式,求得存储的
位置。获取存储位置的数据与要查找的数据进行比较,如果相等,则查找成功;- 4.这样一种通过公式求解存储和查找位置的技术即为
散列技术,散列也称为哈希。
二、哈希公式的设计
哈希公式的设计要遵循简单的原则,并且数据存储的位置要相对均匀,下面介绍几种哈希公式设计的方法。
1.直接定址法
- 1.假如要存储的数据如上图所示,左边表示存储的是数据是年龄和人数之间的关系;
- 2.右边表示存储的数据是出生年份与人数之间的关系;
- 3.分析数据发现,年龄是依次递增的,出生年份也是依次递增的,它们和存储位置之间存在着某种线性关系;
- 4.于是,就可以以年龄和出生年份为key,设计一个哈希算法;
- 5.比如,存储位置与年龄之间的哈希算法为:地址 = 年龄;则存储位置与出生年份之间的哈希算法为:地址 = 出生年份 - 1980。
- 6.在存储数据时,就可以用哈希公式计算出存储位置,查找时也能使用哈希公式快速查找到数据。
直接定址法的要求存储位置和关键字key之间存在线性关系:
2.数字分析法
- 1.假如存储的数据是手机号码,前面7位相同的可能性比较大,后面四位相同的可能性比较小,所以可以根据手机号码后四位来设计一个哈希公式;
- 2.再比如人的出生年月日,同一年出生的人很多,同一天月日出生的人较少,所以可以根据月日来设计哈希式;
- 3.再比如人的身份证,身份证后4位相同的人很少,也可以根据身份证后4位来设计哈希公式;
- 4.数据分析法就是
找出数据的规律,尽可能找到差异较大的数据来设计哈希公式。
3.平方取中法
假如要为下面的数据设计哈希公式:
1234、1322,4322,3421,6693,9993,7852,8721
我们可以对上面的数据求平方,再取平方后的数据的中间部分来构造哈希公式。
比如,1234的平方为1522756,227或者27作为要存储的位置。
4.折叠法
折叠法就是将关键字从左到右分割成位数相等的几部分,再对分割后的数据进行操作,最终求得哈希地址。
5.除留余数法
- 1.假设要存储的数据如上,可以分别对上在面的关键字进行数组长度的
取模处理,从而得到数据要存储的位置,即哈希地址;- 2.上面的
数组长度为12,所以关键字模以12,则关键字12的哈希地址为12%12 = 0,25%12 = 1,38%12 = 2。所以12、25、38的哈希地址为0、1、2.
公式:
6.随机数法
选择一随机函数,取关键字的随机值作为散列地址,即f(key)=random(key)其中random为随机函数,通常用于关键字长度不等的场合。
哈希公式的设计要具体分析数据后进行设计,可以选择上面介绍的直接定址法、数字分析法、平方取中法、折叠法、除留余数法或随机数法来进行设计。哈希公式的设计要具体问题,具体分析。
三、解决哈希冲突的常见方法
假设要存储的数据如下图,图中的数据使用除留余数据法得到的哈希地址,现在要将24这个数据存进入,但24的通过除留余数法得到的哈希地址为:24%12 = 0,但0的位置已经存了12了,不能再存24了,这样的情况称为哈希冲突。
即发生了哈希冲突,就要解决哈希冲空。即如果求得的存储位置已经存了其他数据了,就要再次查找没有存储数据的位置。下面介绍几种解决哈希冲突的几种方法。
1.开放定址法
如果使用除留余数法后求得的哈希地址发生了
哈希冲突,则将关键字key + 某个常数,然后再执行哈希公式,直到找到没有发生哈希冲突的地址为止;
2.再散列函数法
可以多设计几个
哈式公式(散列函数),如果发生哈希冲突,再使用其他的哈希公式求哈希地址。
3.链地址法
将所有的关键字为同义词的记录存储在一个单向链表中,称之为同义词字表。如下图:
- 1.
12和48进行除留余数后的哈希地址都是0,所以12和48就是单向链表的形式存储在下标为0的数组的位置所对应的链表中;- 2.查找的时候,求得
哈希地址后,要再查找单向链表中是否有对应数据,如果有,则查找成功。
4.公共溢出法
如果
除留余数法求得的哈希地址发生了哈希冲突,则再申请一个数组来保存数。这种方法会造成大量的空间浪费,不建议使用。
四、散列查找的示例代码
对以下数据设计哈希公式:
{12,67,56,16,25,37,22,29,15,47,48,34}
1.状态值定义
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
2.数据结构设计
typedef struct
{
//数据元素存储基址,动态分配数组
int *elem;
//当前数据元素个数
int count;
}HashTable;
int m=0; /* 散列表表长,全局变量 */
3.初始化散列表
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;
}
4.哈希公式设计
int Hash(int key)
{
//除留余数法
return key % m;
}
5.插入数据
//3. 插入关键字进散列表
void InsertHash(HashTable *H,int key)
{
//① 求散列地址
int addr = Hash(key);
//② 如果不为空,则冲突
while (H->elem[addr] != NULLKEY)
{
//开放定址法的线性探测
addr = (addr+1) % m;
}
//③ 直到有空位后插入关键字
H->elem[addr] = key;
}
6.查找数据
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;
}
7.调试代码
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);
}
五、总结
散列查找就是设计一个哈希公式(散列函数),使得存储数据和查找数据时能快速定位到数据的一种查找方法。