散列表
散列表,又叫哈希(hash)表是字典的一种实现形式,它通过散列函数将关键字映射到表中的某个位置存储,并以同样的方式访问,查询的时间复杂度是O(1)
散列函数
定义: 用来将关键字映射到表中某一位置的函数
问题: 由于关键字集合比散列表地址集合大得多,因此,难免会出现某些关键字对应同一个散列表地址的情况,此时就产生冲突
解决冲突的办法:
1.我们希望这个散列函数能将关键字比较均匀地映射到表中
2.人为制定解决冲突的策略
性质:
1.散列函数的定义域是关键字全体,值域是散列表地址全体
2.散列函数计算出的映射地址应该尽可能地均匀分布在整个地址空间
3.散列函数应是简单的,要快速计算出结果
常见的散列函数实现方法:
1.除留余数法
我们选取某个数p,把关键字除以该数得到的余数作为存储的地址,也就是(m是关键码集合的长度):
这里的p一般取不大于m的最大质数,或者取不含20以内质因子的合数
同时,如果key是十进制数,p要避免取10的幂,否则,地址就会被压缩在0~9这个狭小空间中,冲突量会很大,2进制同理
总之,p的选取就是为了让冲突尽可能小
2.数字分析法
设有n个d位数,每位可能有r种不同的符号,根据关键字集合,把n,r,d确定后,我们选取这r种符号分布比较均匀的若干位作为散列地址,我们可以用下式计算均匀度(k代表不同的位):
但是,这种方法完全依赖于关键字集合,换一个关键字集合,散列地址就要重算,因此不常用
3.平方取中法
平方取中法需要计算构成关键字标识符的内码的平方,所谓内码就是某种字符编码,它包含ASCII码并且其他诸如汉字等特殊符号也有对应编码,我们取内码平方后的中间若干位作为散列地址,由于内码通常用八进制表示,因此,我们通常取散列地址为8的某次幂,比如,散列地址总数m=8的r次方,那么我们就取中间r位
4.折叠法
折叠法要求我们先将关键字分成位数相等的几部分,最后一部分位数可以短一点,然后按下面两种之一的叠加方法获取到散列地址:
(1)移位法:把划分好的各部分竖着排最后一位对齐相加
(2)分解法:排法与上面一种一样,只是奇数排保持正序,偶数排需要反序,最后对齐相加
应用以上这些方法求出的地址如果是r位,那么也就是说,地址这个整数的范围是(010^r-1),表空间不可能这么大,因此我们需要规定一个比例因子(01之间的一个数),将地址乘上比例因子得到表中地址
处理冲突的必要性:
由于不论我们如何选取散列函数,冲突无法避免,因此我们必须解决人为规定冲突处理办法,我们对散列表加以改造,如果散列表HT原本有m个地址,我们把它们视为m个桶,每个桶可以装相同的关键字的元素,这些散列地址相同的元素又叫做同义词,下面介绍两大冲突处理方法:
闭散列法
定义: 闭散列法,又叫开地址法,所有的桶直接放在散列表数组中,把数组看作环形结构,每个桶只能放一个元素,桶的下标就是数组的下标,我们通过散列函数将关键字映射为数组的某个位置,如果这个位置的桶是空的,就把该元素放进去,否则我们就需要找“下一个”空的桶,把元素放进去,如果没有空桶,则说明表满了,存入失败,找“下一个“空桶主要有下面的3种方法:
介绍之前,我们先搭一个散列表的基本结构:
#define MAXCAP 200
struct Hashtable{
int bucket[MAXCAP]; //hash桶
int st[MAXCAP];//记录桶被装入元素的个数
int keypos[MAXCAP];//记录某个key的值被存入了哪个桶
int curnum; //已经装了元素的桶个数
};
线性探查法
所谓线性探查法,其实就是,如果遇到冲突,就直接线性地找冲突点后面的桶,遇到空的就放,一个空的都没遇到就存入失败
我们用一个pair类型变量存储元素,第一关键字存储元素关键字,第二关键字存值
我们来实现以下线性探查法的插入函数:
Hashtable hst;
//我们选择199这个质数作为除数,使用除留余数法
bool insert(pair<int,int>a){
if(hst.curnum==MAXCAP){
return false;
}
int adr=a.first%199;//获取插入位置
int i=adr;
while(true){
if(hst.st[i]==0){
break;
}
if(hst.st[hst.keypos[a.first]]!=0){
cout<<"Error:插入了重复的key!"<<endl;
return false;
}
i=(i+1)%MAXCAP;
}
hst.bucket[i]=a.second;
hst.keypos[adr]=i;
hst.st[i]++;
return true;
}
int search(int key){
return hst.bucket[hst.keypos[key%199]];
}
int main(){
if(insert({1,2})){
cout<<"插入成功"<<endl;
}
cout<<search(1)<<endl;
return 0;
}
经过测试,结果正确,我们也发现了,散列表查询的时间复杂度是O(1)
二次探查法
线性探查法完全按照线性搜索,时间复杂度最坏是O(n),这还是有很大优化空间的,我们来看一下二次探查法
所谓二次探查法,寻找下一个”空桶“的公式如下:
这里的m有要求,表示哈希表的长度,并且必须是值符合4k+3的质数
二次探查法的探查序列如下:
H0就是第一次通过散列函数算出来的那个桶的下标
我们具体通过代码看一下二次探查法是如何实现的:
#define MAXCAP 199 //注意我们这次选择了199
Hashtable hst;
//我们选择199这个质数作为除数,使用除留余数法
bool insert(pair<int,int>a){
if(hst.curnum==MAXCAP){
return false;
}
int adr=a.first%199;//获取插入位置
int i=adr;
int k=0,odd=0,save=i;
while(true){
if(hst.st[i]==0){
break;
}
if(hst.st[hst.keypos[a.first]]!=0){
cout<<"Error:插入了重复的key!"<<endl;
return false;
}
if(odd==0){
k++;
i=(save+k*k)%MAXCAP;
odd=1;
}
else{
i=(save-k*k)%MAXCAP;
odd=0;
if(i<0){
i=i+MAXCAP;
}
}
if(i==save){
return false;
}
}
hst.bucket[i]=a.second;
hst.keypos[adr]=i;
hst.st[i]++;
return true;
}
int search(int key){
return hst.bucket[hst.keypos[key%199]];
}
int main(){
if(insert({2,4})){
cout<<"插入成功"<<endl;
}
cout<<search(2)<<endl;
return 0;
}
经过检测,结果正确,二次探查法优越就优越在它在已经装满的桶数量小于总容量一半时,新的数据一定可以插入,并且任何一个位置不会被探查两次,因为它不会让关键字聚在一边
双散列法
双散列法就是当一个散列函数算出的地址发生冲突之后,用另一个散列函数再算一次,双散列完了之后还有冲突,那么此方法就无能为力了,还有一种叫做再散列法的方法,就是双散列遇到冲突就继续再用一个散列函数算,具体公式如下:
这里的Hash1,表示用第一个散列函数算出的地址,i是冲突次数,Hash2是用第二个散列函数算出的地址,Hi则是最终的地址,这种方法对于Hash2的选取要求较高,因此不常用
注意: 闭散列法不能随意删除元素,我们如果要删除,应当使用一个标记表明删除即可,不能物理删除,否则会影响整个散列表
总结: 闭散列法的三种方法都各有缺陷,因此在实际使用时闭散列法并不常用
开散列法
定义: 所谓的开散列法,其实就是,当出现冲突时,把冲突的那个元素作为一个链表节点接在与之冲突的元素地址之后,也就是说,此时哈希桶中就不是存一个元素了,而是存储了一系列同义词,这种方式也是散列表最为常用的处理冲突的方式,示意图如下:
这种方式的hash函数就没必要特意设计了,直接设为Hash(x)=x%m(m是表长)即可
我们用开散列法实现一下散列表:
#include<iostream>
using namespace std;
#define MAXCAP 200
struct Node{
int key,val;
Node* next;
Node():next(NULL){}
Node(int k,int v):key(k),val(v),next(NULL){}
};
struct Hashtable{
Node* bucket[MAXCAP]; //hash桶
};
Hashtable hst;
void insert(pair<int,int>a){
int adr=a.first%MAXCAP;
Node* y=new Node(a.first,a.second);
if(hst.bucket[adr]==nullptr){
hst.bucket[adr]=y;
return;
}
y->next=hst.bucket[adr]->next;
hst.bucket[adr]->next=y;
}
void remove(int key){
int adr=key%MAXCAP;
Node* p=hst.bucket[adr];
Node* pre=new Node();
pre->next=p;
while(p!=nullptr){
if(p->key==key){
break;
}
p=p->next;
pre=pre->next;
}
pre->next=p->next;
delete p;
}
int getvalue(int key){
int adr=key%MAXCAP;
Node* p=hst.bucket[adr];
while(p!=nullptr){
if(p->key==key){
break;
}
p=p->next;
}
if(p==nullptr){
return 0;//没有对应的值就返回0
}
return p->val;
}
int main(){
insert({2,4});
insert({7,8});
insert({3,6});
remove(3);
cout<<getvalue(2)<<endl;
cout<<getvalue(7)<<endl;
cout<<getvalue(3)<<endl;
return 0;
}