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

哈希表

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

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

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值