写在前面
上一篇数据结构算法学习-1. 查找(Search)概论与三种顺序查找算法 之后还有线性索引查找与二分树查找,由于目前首先接触到了哈希表的题目,所以跳跃一下,首先来学习一下哈希表相关知识。
哈希表概述
之前的思路是要查找一个记录,都要将不同关键字对应的记录与要查找的记录做“比较”,如果比较之后,二者相同说明,查找到了所需记录结果,如果所有记录都不相同,说明查找失败。但哈希表的思路是直接通过关键字key得到要查找的记录,或者记录储存的内存位置。更加类似一种函数的思想。已知关键字与记录之间某种对应关系,就可以直接通过关键字来获得要查找的记录了。用公式描述如下:
存储位置 = f(关键字)
要实现这种思路,需要用一种新的存储技术——散列技术。
散列技术: 散列技术是在记录的存储位置和它的关键字之间建立一个确定的对应关系f,使得每个关键字key对应一个存储位置f(key)。查找时,根据这个确定的对应关系找到给定值key的映射f(key),若查找集合中存在这个记录,则必定在f(key)的位置上。
因此,这种对应关系f就是散列函数,又称哈希(Hash)函数。采用散列技术将记录存储在一块连续的存储空间中,这块练习存储空间成为散列表或哈希表(Hash)。关键字对应的记录存储位置成为散列地址。
散列技术是一种存储方法,也是一种查找方法。它与线性表、树、图等结构不同,这些结构的数据元素之间都存在某种逻辑关系,可以通过连线图示表达,但散列技术的记录之间不存在什么逻辑关系,它只与关键字有关。散列主要是面向查找到存储结构。
散列技术最适合的求解问题就是查找与给定值相等的记录。
散列函数的构造方法
一个合适的散列函数有两个原则:
- 计算简单。
- 散列地址分布均匀。
下面介绍几种常用的散列函数构造方法,这些方法本质都是将原来数值按照某种规律变成另一个数字,其过程很像加密。
1.直接定址法。
直接定址法就是取关键字的某个线性函数值作为散列地址。即
f(key) = a*key+b (a、b为常数)
直接定址法的优点是简单、均匀、不会产生冲突。但是需要事先直到关键字的分布情况,适合查找表较小而且连续的情况。
2.数字分析法。
数字分析法就是根据某些关键字本身就具有特定的含义,比较适合关键字位数较多。比如生活中常见:手机号码、身份证号码、等。数字分析法利用抽取方法,来使用关键字的一部分来计算散列存储位置的方法。数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布而且关键字的若干位分布较为均匀,比较适合该法。
3.平方取中法。
如果关键字是数字,那么就可以将关键字平方,然后取平方运算结果中间的若干位位数作为散列地址。该方法使用与不知道关键字分布情况,而且位数不是很大的情况。
4.折叠法
折叠法是将关键字从左到右分割成位数相等的及部分,然后将这若干部分叠加求和,然后按照散列表长,取后几位作为散列地址。折叠法适合事先不知道关键字的分布情况,而且关键字位数较多的情况。
5.除留余数法
该方法是最常用的构造散列函数的方法,对散列表长位m的散列函数公式位:
f(key)=key mod p (p <= m)
mod是取模(求余数)的意思,该法不仅可以直接对关键字取模,也可以结合上述诸多方法,折叠、平方取中后取模。该方法p的选择至关重要。根据前辈的经验,若散列表表长为m,通常p为小于或等于表厂的最小质数或不包含小于20质因子的合数。
6.随机数法
选择一个随机数,取关键字的随机函数值为其散列地址。即:
f(key)=random(key)
这里random是随机函数。当关键字的长度不等时,可以采用这个方法。
处理散列冲突的方法
开放地址法 : 一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。其公式是:
fi(key) = (f(key)+di) MOD m (di=1,2,3,......,m-1)
再散列函数法: 事先准备多个散列函数,每当散列地址发生冲突时,就换一个散列函数计算。这种方法使得关键字不产生聚集,也相应的增加了计算的时间。
链地址法: 将所有关键字为同义词的记录存储再一个单链表中,这种表为同义词指标,在散列表中只存储所有同义词指标的头指针。那么,就不存在什么冲突换址的问题,无论有多少冲突,都只是在当前位置给单链表增加结点的问题。
公共溢出区法: 为所有冲突的关键字建立一个公共的溢出区来存放。那么采用这种方法之后,在查找时,先对基本表进行查找,如果查找失败,就去溢出表进行顺序查找。
散列表查找算法实现
首先定义一个散列表的结构与一些相关的常数。HashTable就是散列表结构。elem是一个动态数组。
#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; //除留余数法
}
初始化完成后,对散列表进行插入操作,假设关键字集合为:{12,67,56,16,53,25,48,67,15,94,23,44,74}。
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[*arrd] == NULLKEY || *addr == Hash(key)){
return UNSUCCESS;
}
}
return SUCCESS;
}
写在后面
对于哈希表目前希望做个了解,毕竟每天用map要是还不知道map里面到底是什么东西的话,就太有失水准了。先写这么多,主要还是对知识点的整理。有空可以去看看C++中关于map的源代码。但愿能看懂。