概述
散列表,又称哈希表,hash表。散列表是一种特殊的数据结构,它同数组、链表以及二叉排序树等相比较有很明显的区别,它能够快速定位到想要查找的记录,而不是与表中存在的记录的关键字进行比较来进行查找。这个源于散列表设计的特殊性,它采用了函数映射的思想将记录的存储位置与记录的关键字关联起来,从而能够很快速地进行查找。
设计思想
Hash表采用一个映射函数 f : key —> address 将关键字映射到该记录在表中的存储位置,从而在想要查找该记录时,可以直接根据关键字和映射关系计算出该记录在表中的存储位置,通常情况下,这种映射关系称作为Hash函数,而通过Hash函数和关键字计算出来的存储位置(注意这里的存储位置只是表中的存储位置,并不是实际的物理地址)称作为Hash地址。
哈希函数
1)直接定址法
取关键字或者关键字的某个线性函数为Hash地址,即Hash(key)=a*key+b
2)除留余数法
如果知道Hash表的最大长度为m,可以取不大于m的最大质数p,然后对关键字进行取余运算,Hash(key)=key%p。
在这里p的选取非常关键,p选择的好的话,能够最大程度地减少冲突,p一般取不大于m的最大质数。
3)平方取中法
对关键字进行平方运算,然后取结果的中间几位作为Hash地址。假如有以下关键字序列{421,423,436},平方之后的结果为{177241,178929,190096},那么可以取{72,89,00}作为Hash地址。
4)折叠法
将关键字拆分成几部分,然后将这几部分组合在一起,以特定的方式进行转化形成Hash地址。假如知道图书的ISBN号为8903-241-23,可以将Hash(key)=89+03+24+12+3作为Hash地址。
冲突处理策略
无论利用上述哪种hash函数计算hash地址,难免会产生不同元素的hash地址一样,那么这样就产生了冲突,那么如何避免冲突是一件很关键的事。下面有两种方式解决冲突:开放定址法与分离链接法(链地址法)。由于篇幅问题,这篇博文主要介绍开放定址法。
开放定址法
当一个关键字和另一个关键字发生冲突时,使用某种探测技术在Hash表中形成一个探测序列,然后沿着这个探测序列依次查找下去,当碰到一个空的单元时,则插入其中。基本公式为:hash(key) = (hash(key)+di)mod TableSize。其中di为增量序列,TableSize为表长。根据di的不同我们又可以分为线性探测,平方(二次)探测,双散列探测。
1)线性探测
以增量序列 1,2,……,(TableSize -1)循环试探下一个存储地址,即di = i。如果table[index+di]为空则进行插入,反之试探下一个增量。但是线性探测也有弊端,就是会造成元素聚集现象,降低查找效率。具体例子如下图:
特别对于开放定址法的删除操作,不能简单的进行物理删除,因为对于同义词来说,这个地址可能在其查找路径上,若物理删除的话,会中断查找路径,故只能设置删除标志。
//插入函数,利用线性探测法
bool Insert_Linear_Probing(int num){
//哈希表已经被装满,则不在填入
if(this->size == this->length){
return false;
}
int index = this->hash(num);
if(this->data[index] == MAX){
this->data[index] = num;
}else{
int i = 1;
//寻找合适位置
while(this->data[(index+i)%this->length] != MAX){
i++;
}
index = (index+i)%this->length;
this->data[index] = num;
}
if(this->delete_flag[index] == 1){//之前设置为删除
this->delete_flag[index] = 0;
}
this->size++;
return true;
}
2)平方探测
以增量序列1,-1,4,-4…且q ≤ TableSize/2 循环试探下一个存储地址。
3)双散列探测
di 为i*h2(key),h2(key)是另一个散列函数。探测序列成:h2(key),2h2(key),3h2(key),……。对任意的key,h2(key) ≠ 0 !探测序列还应该保证所有的散列存储单元都应该能够被探测到。选择以下形式有良好的效果:
h2(key) = p - (key mod p)
其中:p < TableSize,p、TableSize都是素数。
例子
全部代码:这里只利用线性探测思想解决冲突。其他2种节约时间未实现。
#include <iostream>
#include <cmath>
#include <cstring>
using namespace std;
const int MAX = 65535;
class HashTable{
private:
int length; //哈希表表长
int size; //哈希表中已经填入的元素
int* data; //元素表
int Max_Prime; //最大素数
int* delete_flag; //删除标志
public:
//构造函数
HashTable(int length){
this->length = length;
this->data = new int[this->length];
this->Max_Prime = this->getMaxPrime();
this->delete_flag = new int[this->length];
memset(this->delete_flag,0,sizeof(this->delete_flag[0])*this->length);
for(int i = 0 ; i < this->length ; i++){
//初始化最大值,表示未插入
this->data[i] = MAX;
}
}
//判断素数函数
bool isPrime(int num){
bool flag = true;
if(num <= 1){
flag = false;
}else if( num == 2){
flag = true;
}else{
for(int i = 2 ; i < num-1 ; i++){
//num能被i整除
if(num % i == 0){
flag = false;
break;
}
}
}
return flag;
}
//寻求小于表长的最大素数
int getMaxPrime(){
for(int i = this->length-1 ; i >= 0 ; i--){
if(this->isPrime(i)){
return i;
}
}
}
//hash函数
int hash(int num){
return num%this->Max_Prime;
}
//插入函数,利用线性探测法
bool Insert_Linear_Probing(int num){
//哈希表已经被装满,则不在填入
if(this->size == this->length){
return false;
}
int index = this->hash(num);
if(this->data[index] == MAX){
this->data[index] = num;
}else{
int i = 1;
//寻找合适位置
while(this->data[(index+i)%this->length] != MAX){
i++;
}
index = (index+i)%this->length;
this->data[index] = num;
}
if(this->delete_flag[index] == 1){//之前设置为删除
this->delete_flag[index] = 0;
}
this->size++;
return true;
}
//建表函数
void Create_Linear_Probing(int* num , int size){
for(int i = 0 ; i < size ; i++){
this->Insert_Linear_Probing(num[i]);
}
}
//查找函数
int Find_Linear_Probing(int num){
int flag = -1;
int index = this->hash(num);
if(this->data[index] == num && this->delete_flag[index] == 0){
flag = index;
}else{
int i = 1;
while(1){
if(this->delete_flag[index+i] == 0){//未设置为已删除
if(this->data[index+i] != num){
//不等于无穷大,也不等于num,则偏移量加1
i++;
}else{//等于num时
flag = index+i;
break;
}
}else{//设置为已删除
i++;
}
}
}
return flag;
}
//线性探测的删除函数
bool Delete_Linear_Probing(int num){
int index = this->Find_Linear_Probing(num);
if(index == -1){
return false;
}else{
this->delete_flag[index] = 1;//设置为已删除
this->size--;
return true;
}
}
void Print(){
cout<<"下标\t";
for(int i = 0 ; i < this->length ; i++){
printf("%7d ",i);
}
cout<<endl<<"元素\t";
for(int i = 0 ; i < this->length ; i++){
if(this->delete_flag[i] == 0){
printf("%7d ",this->data[i]);
}else{
printf("%7d ",MAX);
}
}
cout<<endl;
}
};
int main()
{
cout<<"请输入哈希表的表长:"<<endl;
int length,size;
int* data;
cin>>length;
cout<<"请输入数组长度:"<<endl;
cin>>size;
data = new int[size];
cout<<"请初始化数组:"<<endl;
for(int i = 0 ; i < size ; i++){
cin>>data[i];
}
HashTable hashtable(length);
hashtable.Create_Linear_Probing(data,size);
cout<<"哈希表为:"<<endl;
hashtable.Print();
int num;
cout<<"请输入删除的元素:"<<endl;
cin>>num;
cout<<"删除前哈希表为:"<<endl;
hashtable.Print();
if(hashtable.Delete_Linear_Probing(num)){
cout<<"删除后哈希表为:"<<endl;
hashtable.Print();
}else{
cout<<"哈希表没有"<<num<<"这个元素"<<endl;
}
cout<<"请输入插入的元素:"<<endl;
cin>>num;
cout<<"插入前哈希表为:"<<endl;
hashtable.Print();
if(hashtable.Insert_Linear_Probing(num)){
cout<<"插入后哈希表为:"<<endl;
hashtable.Print();
}else{
cout<<"哈希表已满,无法插入"<<num<<"这个元素"<<endl;
}
return 0;
}
截图1:
截图2: