概述
哈希表
在树和线性表的各种结构中,记录在结构中的相对位置是随机的,和记录的关键字之间不存在一种确定的关系。因此在关键字中查找记录是基于一种比较的策略。在顺序查找中,比较的结果有等与不等,折半,二叉排序树的查找有大于,小于,等于三种。查找的效率依赖于进行比较的次数。
比较理想的情况就是不通过比较来获取记录,即通过记录的关键字和记录的存储地址建立一种对应关系f。这样的话,直接通过这个对应关系f就可以找到给定关键字K的存储地址f(K),我们称这个对应关系f为哈希函数。根据这个函数建立的表为哈希表。
例如:假设一个学校的学生的学号是唯一的,位数为8位。学号的前四位一样,只有后四位不一样。那么我们可以将学生的学号作为记录的关键字,令hash函数为取学号的后四位。
这样例如一个学生的学号为08509046,那么对应的这个学生的记录的地址f(Key) = 9046,这样如果建立一个具有10000个元素的数组,对于任意给定的学生的学号,我们都能根据学号的后四位获取这个学生记录的地址,其实这就是一种Hash的映射。也许可能会有这样的一种情况,另一个学校的学生后四位有可能有相同的,例如08509046,08519046,这样在进行映射的时候,这两个记录都会映射到地址9046,这样不同的关键字通过hash函数的映射具有相同的hash地址,我们称这种现象叫做冲突。对于不同类型的记录,我们首先要选取一个相对好的Hash函数来减少冲突的产生,同时我们必须明白,不可能完全的消除冲突,只有去解决冲突。下面将讲述几种常见的构造Hash函数的方法以及解决冲突产生的方法。
常见的构造hash函数的方法:
1, 直接定址法
取关键字的某一个线性函数作为哈希地址,即H(key)= a * key + b;
可以看出直接定址法采用的是线性函数,即不同key会对应不同的H(key),这样的Hash函数不会有冲突的产生,不过实际中应用很少。
2, 数字分析法
对关键字进行分析,例如上文提到的学生学号的后四位不同,前四位相同,那么我们可以取后四位作为Hash地址。这种情况建立在对全部记录的关键字都是事先知道的情况下。
3, 平方取中法
取关键字的平方后的中间几位作为hash地址,这种很常用。主要是因为通常情况下,我们并不知道关键字的全部情况,选取哪几位也不一定合适,选取一个数平方后的中间几位数和每一位都是相关的。选取的位数由表长决定
4, 折叠法
将关键字拆分成位数相同的几部分,然后将这几个部分求和,舍去进位作为哈希地址。适用于关键字的位数很多而且数字分布均匀的情况
5, 除留余数法
取关键字被某个不大于哈希表表长m的数字p除后所得的余数作为哈希地址,即
H(key) = key MOD p (p<= m)
此种方法简单而且很常用。可以对key直接取模,也可以在平方,或者折叠之后取模。根据经验知:p为质数或者不包含小于20的质因数的合数
6, 随即函数法
H(key) = random(key) 取关键字的随机函数作为它的hash地址
“冲突”是指由关键字得到的hash地址上已有记录,而“处理冲突”是指另外寻找一个空的哈希地址。处理冲突的过程中,可以得到一个地址序列Hi ,i=1,2,3..k,如果出现了冲突,那么首先寻找哈希地址H1,如果H1上仍然有记录,那么寻找H2,依次类推,直到Hk不发生冲突,则Hk即为此记录的hash地址。
常用的处理冲突的方法:
1, 开放定址法
Hi = (H(key) + di) MOD m I = 1, 2,…k(k <= m-1)
其中:H(key)为上文中所采用的某一种hash函数,m为hash表长,di为增量数列,可有下列3种取法:(1)di = 1,2,3…m-1,称为线性探测再散列;(2)di = 1,-1,4,-4,9,-9…k*k,-k*k(k <= m/2)称为二次探测再散列;(2)di= 伪随机数列,称为伪随机探测再散列。(此法简单易理解易用,很常用)
说着比较抽象,举例如下:
长度为11的哈希表中已有关键字17,60,29的记录(哈希函数为H(key) = key MOD 11),它们的hash地址分别为6,5,7.现在有第四个记录,其关键字为38,由哈希函数得到hash地址为5,与60产生冲突。若采用线性探测再散列,根据“处理冲突”的策略,首先寻找哈希地址H1,即下一个hash地址6,仍然冲突;继续求下一个hash地址7,仍然冲突;直到hash地址为8的位置为“空”时止,处理冲突的过程结束,将记录填入到表中序号为8的位置。若用二次探测再散列,则应放入4的位置,若用伪随机序列,则放入对应的伪随机散列的地址。
2, 再散列法
Hi = RHi(key) i=1,2,3…k
其中RHi是不同的hash函数,如果产生冲突,采用下一个hash函数,直到不再产生冲突为止。(此法不是很常用,计算时间要多一些)
3, 链地址法
将产生冲突的关键字记录存储在一个线性表中。例如hash地址为i的记录都插入到头指针为ChainHash[i]的链表中,可以插入到表头或者表尾,也可以插入到中间,保证关键字有序(此法很常用,结构有点复杂)。
4, 建立一个公共溢出区
首先创建一个基本表HashTable[0…m-1]为基本表,另设定向量OverTable[0….v]为溢出表,所有与基本表中关键字冲突的记录都通通放在溢出表中。这样当进行搜索的时候如果出现了冲突就需要顺序搜索溢出表了,效率不高(当关键字冲突出现少时候有效)。
以下代码是采用开放定址法进行hash映射的代码,代码参考了数据结构的教材,以及网上流传的一些结构,自己进行了一些bug的修改和逻辑的纠正。当判断一个位置没有元素本程序是采用那个位置上元素的key为0进行判断的,这个我打算对元素再加上一个state的参数,来表明当前位置是不是已经被占用了,如果占用对应元素的state为1,没有占用state为0.现在先列出数据结构书籍上采用的一种方式,解决冲突采用的是开放定制法的线性探测再散列,网上流传的几份代码的线性探测再散列,个人认为有些问题,主要是探测的位置不是线性的,而是跳跃的,很是费解。
#include<iostream>
using namespace std;
#define NULLKEY 0//表示此位置无记录
#define EQ(a,b) ((a)==(b))
//int hashSize[] ={5,7,11,17,23,37,47,61,79,109};
int hashSize[]={11,19,29,37};//哈希表容量递增表一个合适的素数序列
int m = 0;//hash的表的长度
typedef enum
{
DUPLICATE = 1,//元素已经存在
SUCCESS = 0,
FAILURE = -1
};
typedef int KeyType;
typedef struct
{
KeyType key;
int value;
}ElemType;//hash表的元素类型
typedef struct
{
ElemType* elem;//数据元素的基地址
int count;//数据元素的个数
int sizeIndex;//hash表的容量
}HashTable;
int initHashTable(HashTable&H)
{
H.sizeIndex =0;
m = hashSize[H.sizeIndex];
int size = hashSize[H.sizeIndex];
H.elem = new ElemType[size];
if(!H.elem)
{
cout<<"内存分配失败"<<endl;
return FAILURE;
}
H.count = 0;//hash的当前数据元素个数初始化
for (int i = 0; i < size; ++i)
{
H.elem[i].key = NULLKEY;//hash表的内容初始化
}
return SUCCESS;
}
//hash函数
unsigned int Hash(KeyType key)
{
return key % m;
}
//线性探测再散列
void collision(int&pos, int&col)
{
++col;//冲突次数增
pos = pos + 1;
}
int searchHash(HashTableH, KeyTypekey, int&pos, int&col)
{
pos = Hash(key);
while(H.elem[pos].key != NULLKEY&& !EQ(H.elem[pos].key,key))
{
collision(pos,col);//把处理冲突的方法封装了,返回下一个搜索的位置
cout<<"pos"<<pos<<endl;
}
if(EQ(H.elem[pos].key,key))
{
return SUCCESS;//找到元素
}
return FAILURE;//没有找到元素
}
int insertHash(HashTable&H,ElemTypee);
//重建hash表,增长表长。新建一个表,赋值为NULLKEY,逐个插入元素
int recreateHashTable(HashTable&H)
{
int count = H.count;
ElemType* elem= new ElemType[H.count];
for(int i =0,j=0; i< m; ++i){
if(H.elem[i].key != NULLKEY)
elem[j++] = H.elem[i];
}
H.sizeIndex++;
m = hashSize[H.sizeIndex];
H.count = 0;
if(H.elem){
delete H.elem;
H.elem = NULL;
}
H.elem = new ElemType[m];
if(H.elem == NULL){
cout<<"内存分配失败"<<endl;
return FAILURE;
}
for (int i = 0; i < m; ++i)
{
H.elem[i].key = NULLKEY;
}
for (int i = 0; i < count; ++i)
{
insertHash(H, elem[i]);
}
return SUCCESS;
}
int insertHash(HashTable&H,ElemTypee)
{
int c = 0, p = 0;
if(searchHash(H,e.key,p,c) == SUCCESS)
return DUPLICATE;
if (c < hashSize[H.sizeIndex] / 2)
{
H.elem[p] = e;
H.count++;
return SUCCESS;
}
else{//冲突次数过多
recreateHashTable(H);//重建hash表后继续插入操作
insertHash(H,e);
return SUCCESS;
}
}
void printHash(int p,ElemType r)
{
printf("address=%d(%d %d)n",p,r.key,r.value);
}
void traverseHashTable(HashTableH)
{
cout<<"哈希地址~"<<m-1<<endl;;
for(int i=0;i<m;i++)
{
if(H.elem[i].key!=NULLKEY)//有数据
printf("address=%d (%d %d)n",i,H.elem[i].key,H.elem[i].value);
}
}
int destroyHashTable(HashTable&H)
{
if(H.elem)
{
delete H.elem;
H.elem = NULL;
}
H.sizeIndex =0;
H.count = 0;
return SUCCESS;
}
#define N 10
int main()
{
//6,5,7,5,1,2,3,4,5,2,
ElemType r[N]={{17,1},{60,2},{29,3},{38,4},{1,5},{2,6},{3,7},{4,8},{60,9},{13,10}};
HashTable h;
int i,p,c;
int j;
KeyType k;
initHashTable(h);
for(i=0;i<N;i++)//插入前N-1个记录
{
j=insertHash(h, r[i]);
if(j == DUPLICATE)
printf("表中已有关键字尾%d的记录无法再插入记录(%d%d)n",r[i].key,r[i].key,r[i].value);
}
printf("按哈希地址的顺序遍历哈希表n");
traverseHashTable(h);
printf("nn");
printf("请输入带查找记录的关键字:");
scanf("%d",&k);
j=searchHash(h, k, p, c);
if(j == SUCCESS)
printHash(p, h.elem[p]);
else{
printf("没找到n");
printf("nn");
r[i].key = k;
r[i].value = k;
j=insertHash(h, r[i]);//插入第N个记录
}
printf("按哈希地址的顺序遍历重建后的哈希表n");
traverseHashTable(h);
printf("nn");
printf("请输入带查找记录的关键字:");
scanf("%d",&k);
j=searchHash(h,k,p,c);
if(j == SUCCESS)
printHash(p, h.elem[p]);
else
printf("没找到n");
printf("nn");
destroyHashTable(h);
printf("哈希表销毁成功!");
printf("nn");
return 0;
}
以下代码是经过给元素加入一个状态state标记来标明此位置是否已经被占用,相对来说更安全一些,不过相应的元素的占用空间会增多,毕竟每一个元素都加入了一个状态标记。
#include<iostream>
using namespace std;
#define NULLELEMENT 0//表示此位置无记录
#define HASELEMENT 1//此位置有记录
#define EQ(a,b) ((a)==(b))
//int hashSize[] ={5,7,11,17,23,37,47,61,79,109};
int hashSize[]={11,19,29,37};//哈希表容量递增表一个合适的素数序列
int m = 0;//hash的表的长度
typedef enum
{
DUPLICATE = 1,//元素已经存在
SUCCESS = 0,
FAILURE = -1
};
typedef int KeyType;
typedef struct
{
KeyType key;
int state;//状态为,表明此位置不存在元素,与NULLELEMENT作用一致
int value;
}ElemType;//hash表的元素类型
typedef struct
{
ElemType* elem;//数据元素的基地址
int count;//数据元素的个数
int sizeIndex;//hash表的容量
}HashTable;
int initHashTable(HashTable&H)
{
H.sizeIndex =0;
m = hashSize[H.sizeIndex];
int size = hashSize[H.sizeIndex];
H.elem = new ElemType[size];
if(!H.elem)
{
cout<<"内存分配失败"<<endl;
return FAILURE;
}
H.count = 0;//hash的当前数据元素个数初始化
for (int i = 0; i < size; ++i)
{
H.elem[i].state = NULLELEMENT;//hash表的内容初始化
}
return SUCCESS;
}
//hash函数
unsigned int Hash(KeyType key)
{
return key % m;
}
//线性探测再散列
void collision(int&pos, int&col)
{
++col;//冲突次数增
pos = pos + 1;
}
int searchHash(HashTableH, KeyTypekey, int&pos, int&col)
{
pos = Hash(key);
while(H.elem[pos].state != NULLELEMENT&& !EQ(H.elem[pos].key,key))
{
collision(pos,col);//把处理冲突的方法封装了,返回下一个搜索的位置
}
if(EQ(H.elem[pos].key,key))
{
return SUCCESS;//找到元素
}
return FAILURE;//没有找到元素
}
int insertHash(HashTable&H,ElemTypee);
void traverseHashTable(HashTableH);
//重建hash表,增长表长。新建一个表,赋值为NULLELEMENT,逐个插入元素
int recreateHashTable(HashTable&H)
{
int count = H.count;
ElemType* elem= new ElemType[H.count];
for(int i =0,j=0; i < m; ++i){
if(H.elem[i].state != NULLELEMENT)
elem[j++] = H.elem[i];
}
H.sizeIndex++;
m = hashSize[H.sizeIndex];
H.count = 0;
if(H.elem){
delete H.elem;
H.elem = NULL;
}
H.elem = new ElemType[m];
if(H.elem == NULL){
cout<<"内存分配失败"<<endl;
return FAILURE;
}
for (int i = 0; i < m; ++i)
{
H.elem[i].state = NULLELEMENT;
}
for (int i = 0; i < count; ++i)
{
insertHash(H, elem[i]);
}
printf("重建表后,遍历哈希表n");
traverseHashTable(H);
printf("nn");
return SUCCESS;
}
int insertHash(HashTable&H,ElemTypee)
{
int c = 0, p = 0;
if(searchHash(H,e.key,p,c) == SUCCESS)
return DUPLICATE;
e.state = HASELEMENT;
if (c < hashSize[H.sizeIndex] / 2)
{
H.elem[p] = e;
H.count++;
return SUCCESS;
}
else{//冲突次数过多
recreateHashTable(H);//重建hash表后继续插入操作
insertHash(H,e);
return SUCCESS;
}
}
void printHash(int p,ElemType r)
{
printf("address=%d(%d %d)n",p,r.key,r.value);
}
void traverseHashTable(HashTableH)
{
cout<<"哈希地址~"<<m-1<<endl;;
for(int i=0;i<m;i++)
{
if(H.elem[i].state!=NULLELEMENT)//有数据
printf("address=%d(%d %d %d)n",i,H.elem[i].key,H.elem[i].state,H.elem[i].value);
}
}
int destroyHashTable(HashTable&H)
{
if(H.elem)
{
delete H.elem;
H.elem = NULL;
}
H.sizeIndex =0;
H.count = 0;
return SUCCESS;
}
#define N 10
int main()
{
//6,5,7,5,1,2,3,4,5,2,
ElemType r[N]={{17,1},{60,2},{29,3},{38,4},{1,5},{2,6},{3,7},{4,8},{60,9},{13,10}};
HashTable h;
int i,p,c;
int j;
KeyType k;
initHashTable(h);
for(i=0;i<N;i++)//插入前N个记录
{
j=insertHash(h, r[i]);
printf("按哈希地址的顺序遍历哈希表n");
traverseHashTable(h);
printf("nn");
if(j == DUPLICATE)
printf("表中已有关键字尾%d的记录无法再插入记录(%d%d)n",r[i].key,r[i].key,r[i].value);
}
printf("按哈希地址的顺序遍历哈希表n");
traverseHashTable(h);
printf("nn");
printf("请输入带查找记录的关键字:");
scanf("%d",&k);
j=searchHash(h, k, p, c);
if(j == SUCCESS)
printHash(p, h.elem[p]);
else{
printf("没找到n");
printf("nn");
r[i].key = k;
r[i].value = k;
j=insertHash(h, r[i]);//插入第N个记录
}
printf("按哈希地址的顺序遍历查询后的哈希表n");
traverseHashTable(h);
printf("nn");
printf("请输入带查找记录的关键字:");
scanf("%d",&k);
j=searchHash(h,k,p,c);
if(j == SUCCESS)
printHash(p, h.elem[p]);
else
printf("没找到n");
printf("nn");
destroyHashTable(h);
printf("哈希表销毁成功!");
printf("nn");
return 0;
}
如果明天有空的话,我还会继续改进,采用类的方式来完成它,将方法封装到类里面去使用,这样全局变量就不必要了,很多东西都可以省掉了。还有后继的采用链地址来处理冲突的方法也可以采纳,打算自己构造一个链表或者采用系统提供的list来完成它。
最后
以上就是清新小蘑菇为你收集整理的数据结构教材中hash的使用及一些基本概念和方法的全部内容,希望文章能够帮你解决数据结构教材中hash的使用及一些基本概念和方法所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复