数据结构——哈希表

306 阅读4分钟

数据结构——哈希表

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第2天,点击查看活动详情

哈希表的概念

哈希表是一种高效的查找结构,它利用记录的关键字确定其存储位置,具体表现为把关键字K映射到一个有限的连续的地址集D,这种光系H可以表示为:

H(key):KDkeyKH(key) : K→D ,key∈K

H称为哈希函数散列函数。按哈希函数构建的表称为哈希表

冲突与同义词

在插入关键字时,可能会遇到该关键字的哈希值与已有关键字哈希值相同的情况,即发生了冲突

H(key1)=H(key2)key1key2H(key1) = H(key2), key1≠key2

哈希函数值相同的关键字称为同义词

在实际情况中,关键字集通常是大于哈希地址集的,哈希冲突是必须要解决的问题。根据一定的哈希函数和处理冲突的方法把关键字集K映射到一个有限的连续的地址集D,并以表的形式记录的过程称为哈希造表散列,所得存储位置称哈希地址或散列地址

哈希函数的构造方法

哈希函数的构造方法应该注意两个原则:

  1. 计算过程尽量简单
  1. 哈希函数尽量均匀。

常见的构造方法有以下这些

  • 直接定址法
  • 除留余数法
  • 数字分析法
  • 折叠法
  • 平方取中法

直接定址法

最简单的做法是直接用关键字作为哈希地址,也可以通过对线性函数的操作获得地址,如:

H(key)=a×key+bH(key) = a × key + b

除留余数法

可以将关键字求模

H(key)=keyH(key) = key % p (pump) %p

不仅可以对关键字直接取模,也可以在折叠、平方取中等运算后再取模。

平方取中法

先取关键字的平方,在选取结果的中间若干位作为哈希地址

处理冲突的方法

链地址法

链地址法是处理冲突较为便利的方法。链地址法将关键字为同义词的记录放在一个带有头指针的单链表中。若哈希表地址区间长度为l,我们就可以将哈希表定义为一个由l个头指针组成的指针数组T,只要是哈希地址同为i,都将其插入T[i]为头指针的单链表中,这样就可以解决冲突。

\

开放定址法

开放定址法是在哈希表的地址空间内解决冲突,实现上会比链地址法复杂不少。插入时若发生冲突,就会利用某种探测方法得到另一个空闲地址,若不冲突,则插入,否则求下一个地址,查找的探测过程与插入相同。

线性探测法

思想:将哈希表看成一个循环的空间,线性探测法的探测地址序列可表示为:

Hi = (H(key)+i) %m 1≤i≤m-1

Hi表示出现冲突时,第i次探测的地址空间

二次探测法

二次探测法的探测不是连续的,而是跳跃的,可以减少记录堆集从而方便下一次的插入。二次探测法的探测地址序列可表示为: Hi = (H(key)+di) %m 1≤i≤m-1 di = 1^2, -1^2, 2^2, -2^2,…, k^2, -k^2 (k≤m/2)

链地址哈希表的实现

  • 链地址哈希表的类型定义

    • typedef struct Node {
           RcdType r;
           struct Node *next;
       } Node;
      typedef struct {
            Node **rcd;  //指针类型,大小动态分配    
            int size;        // 哈希表容量
            int count;     // 当前表中含有的记录个数
            int (*hash)(KeyType key, int hashSize);  // 函数指针变量,用于选取的哈希函数
      } HashTable; 
      
  • 链地址哈希表的接口

    •   Status InitHash(HashTable &H, int size, int (hash)(KeyType,int)); // 初始化哈希表 Status DestroyHash(HashTable &H); // 销毁哈希表 Node SearchHash(HashTable H, KeyType key); // 查找 Status InsertHash(HashTable &H, RcdType e); // 插入 Status deleteHash(HashTable &H, KeyType key, RcdType &e); // 删除

链地址哈希表的初始化

Status InitHash(HashTable &H, int size, int (*hash)(KeyType,int))  {    //初始化哈希表
  int i;
 H.rcd = (Node**)malloc(size*sizeof(Node*));
  if(NULL==H.rcd) return OVERFLOW;
  for(i=0; i<size; i++) H.rcd[i] = NULL;
  H.size = size;   H.hash = hash;   H.count = 0;
  return OK;
}

链地址哈希表的查找操作

int hash(int key, int hashSize) { 
      return (3*key) % hashSize;
}

Node* SearchHash(HashTable &H, int key) { 
   int p = H.hash(key, H.size);
   Node* np;
   for(np=H.rcd[p]; np!=NULL; np=np->next)
      if(np->r.key==key) return np;
   return NULL;
}

链地址哈希表的插入操作

Status InsertHash(HashTable &H, RcdType e) { // 在哈希表H中插入记录e
   int p;   Node *np;
   if((np=SearchHash(H, e.key))==NULL) { // 查找不成功时插入到表头
      p = H.hash(e.key, H.size);
      np = (Node*)malloc(sizeof(Node));      if(NULL== np)return OVERFLOW;
      np->r = e;   np->next = H.rcd[p];   H.rcd[p] = np;     H.count++;     return OK;
   }   else   return ERROR;
}

开放定址哈希表的实现

  • 开放定址哈希表的类型定义

    • typedef struct {
        RcdType *rcd;  // 记录存储基址,动态分配数组
           int size;        // 哈希表容量
        int count;     // 当前表中含有的记录个数
           int *tag;          //标记, 0:空; 1:有效;-1:已删除
           int (*hash)(KeyType key, int hashSize); // 函数指针变量,选取的哈希函数
          void (*collision)(int &hashValue, int hashSize); 
               // 函数指针变量,用于处理冲突的函数
      } HashTable; 
      

开放定址哈希表的初始化操作

void collision(int &hashValue, int hashSize) {
     hashValue = (hashValue +1)% hashSize;
}

Status InitHash(HashTable &H, int size, int (*hash)(KeyType, int),
                            void(*collision( int &,int)) {
   int i;
   H.rcd = (RcdType*)malloc(size*sizeof(RcdType));
   H.tag = (int*)malloc(size*sizeof(int));//为记录和标记域分配空间
   if(NULL==H.rcd || NULL==H.tag) return OVERFLOW;
   for(i=0; i<size; i++) H.tag[i] = 0; // 将哈希表中标记域赋值为空标记
   H.size = size;   H.count = 0;  H.hash = hash;  H. collision = collision ;
   return OK;
}

标记0 1 -1分别表示空闲、存在、已删除

开放定址哈希表的查找操作

Status SearchHash (HashTable H, KeyType key, int &p, int &c) {
   p = H.hash(key, H.size); // 求得哈希地址
   while((1==H.tag[p] && H.rcd[p].key!=key) || -1==H.tag[p])) {
      H.collision(p, H.size);  c++; 
   } // 求得下一探测地址p
   if(H.rcd[p].key == key && 1==H.tag[p] ) 
      return SUCCESS; 
   else return UNSUCCESS; 
}

开放定址哈希表的插入操作

调用查找函数,查找成功,不需插入,返回-1;否则插入并返回冲突次数

int InsertHash(HashTable &H, RcdType e) { // 在哈希表H中插入记录e。
   int c=0, j;
   if(SUCCESS == SearchHash(H, e.key, j, c)) return -1;
   else {
      H.rcd[j] = e;  //将记录e插入位置j
      H.tag[j] = 1;  //设置对应的标志位为1
      ++H.count;  //哈希表记录个数加1
      return c; //返回查找时发生冲突的次数
   } 
}

开放定址法删除记录的处理

从哈希表中删除一个记录时,不是直接将其单元置为空闲,而是要打上删除标记,因为空闲单元是查找失败的条件,这样可能会使查找提前结束。

Status deleteHash(HashTable &H, KeyType key, RcdType &e) {
   int j, c;
   if(UNSUCCESS == SearchHash(H, key, j, c))
      return UNSUCCESS;  // 表中没有与 key 相同关键字的记录
   else {
      e = H.rcd[j];  // 被删除的记录
      H.tag[j] = -1;  // 删除标记
      H.count--;  //哈希表记录个数减1
      return SUCCESS;
   }
}