您的位置:首页 > 理论基础 > 数据结构算法

数据结构教材中hash的使用及一些基本概念和方法

2012-08-14 22:40 531 查看
哈希表

在树和线性表的各种结构中,记录在结构中的相对位置是随机的,和记录的关键字之间不存在一种确定的关系。因此在关键字中查找记录是基于一种比较的策略。在顺序查找中,比较的结果有等与不等,折半,二叉排序树的查找有大于,小于,等于三种。查找的效率依赖于进行比较的次数。

比较理想的情况就是不通过比较来获取记录,即通过记录的关键字和记录的存储地址建立一种对应关系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("\n\n"); 

 
   printf("请输入带查找记录的关键字:"); 

   scanf("%d",&k); 

   j=searchHash(h,
k, p,
c); 
   if(j ==
SUCCESS) 
      printHash(p,
h.elem[p]); 

   else{ 
      printf("没找到\n"); 

      printf("\n\n"); 

      r[i].key =
k;
      r[i].value =
k;
      j=insertHash(h,
r[i]);//插入第N个记录 

   }
   printf("按哈希地址的顺序遍历重建后的哈希表\n"); 

   traverseHashTable(h); 

   printf("\n\n"); 

 
   printf("请输入带查找记录的关键字:"); 

   scanf("%d",&k); 

   j=searchHash(h,k,p,c); 

   if(j ==
SUCCESS) 
      printHash(p,
h.elem[p]); 

   else 
      printf("没找到\n"); 

   printf("\n\n"); 

 
   destroyHashTable(h); 

   printf("哈希表销毁成功!"); 

   printf("\n\n"); 

 
   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("\n\n");
   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("\n\n"); 

      if(j ==
DUPLICATE)
        printf("表中已有关键字尾%d的记录无法再插入记录(%d%d)\n",r[i].key,r[i].key,r[i].value); 

   } 
 
   printf("按哈希地址的顺序遍历哈希表\n"); 

   traverseHashTable(h); 

   printf("\n\n"); 

 
   printf("请输入带查找记录的关键字:"); 

   scanf("%d",&k); 

   j=searchHash(h,
k, p,
c); 
   if(j ==
SUCCESS) 
      printHash(p,
h.elem[p]); 

   else{ 
      printf("没找到\n"); 

      printf("\n\n"); 

      r[i].key =
k;
      r[i].value =
k;
      j=insertHash(h,
r[i]);//插入第N个记录 

   }
   printf("按哈希地址的顺序遍历查询后的哈希表\n"); 

   traverseHashTable(h); 

   printf("\n\n"); 

 
   printf("请输入带查找记录的关键字:"); 

   scanf("%d",&k); 

   j=searchHash(h,k,p,c); 

   if(j ==
SUCCESS) 
      printHash(p,
h.elem[p]); 

   else 
      printf("没找到\n"); 

   printf("\n\n"); 

 
   destroyHashTable(h); 

   printf("哈希表销毁成功!"); 

   printf("\n\n"); 

 
   return 0; 
}

如果明天有空的话,我还会继续改进,采用类的方式来完成它,将方法封装到类里面去使用,这样全局变量就不必要了,很多东西都可以省掉了。还有后继的采用链地址来处理冲突的方法也可以采纳,打算自己构造一个链表或者采用系统提供的list来完成它。

 
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: