9.3 HashTable(散列表)

参考博客:什么是散列表(HashTable)
参考博客:散列表
参考博客:BitMap
参考博客:散列表概念

本章问题

1.什么是散列表,和桶排序的关系是什么

关系,都借助桶来快速查询,散列表(Hashtable),是根据键(Key)而直接访问在内存存储位置的数据结构

2.Bitmap位图是什么

位图图像(bitmap), 亦称为点阵图像或绘制图像,是由称作像素(图片元素)的单个点组成的。这些点可以进行不同的排列和染色以构成图样。当放大位图时,可以看见赖以构成整个图像的无数单个方块。扩大位图尺寸的效果是增大单个像素,从而使线条和形状显得参差不齐。然而,如果从稍远的位置观看它,位图图像的颜色和形状又显得是连续的。

3.如何获得散列码,这里的散列函数是什么

散列码循环左移5位,再累加当前字符
如此所得的散列码,实际上可理解为近似的“多项式散列码”
对于英语单词,"循环左移5位"是实验统计得出的最佳值

4散列表的基本构思是什么

开辟物理地址连续的桶数组ht[],借助散列函数hash( ),将词条关键码key映射为hash(key),从而快速确定待操作词条的物理位置。

5.散列函数的设计原则是什么

(1) 必须具有确定性:无论所含的数据项如何,词条E在散列表中的映射地址 hash(E.key) 必须完全取决于其关键码 E.key。
(2) 映射过程自身不能过于复杂,唯此方能保证散列地址的计算可快速完成,从而保证查询或修改操作整体的O(1)期望执行时间。
(3) 所有关键码经映射后应尽量覆盖整个地址空间 [0, M),唯此方可充分利用有限的散列表空间。也就是说,函数hash()最好是满射。
在此,最为重要的一条原则就是,关键码映射到各桶的概率应尽量接近于1/M ---- 若关键码均匀且独立地随机分布,这也是任意一对关键码相互冲突的概率。就整体而言,这等效于将关键码空间“均匀地”映射到散列地址空间,从而避免导致极端低效的情况。总而言之,随机却强,规律性越弱的散列函数越好。要尽可能地消除导致关键码分布不均匀的因素,最大限度地模拟理想的随机函数,尽最大可能降低冲突发生的概率。

6.常见的散列函数有什么

(1) 除余法(division method)

一种最简单的映射方法,就是将散列长度M取作为素数(质数),并将关键码key映射至key关于M整除的余数。

    hash(key) = key mod M

注意:采用除余法时必须将M选做素数,否则关键码被映射到 [0,M)范围内的均匀度将大幅度降低,发生冲突的概率将随M所含因子的增多而迅速增大。

(2) MAD法(multiply-add-divide method)
以素数为表长的除余法尽管可在一定程度上保证词条的均匀分布,但从关键码空间到散列地址空间映射的角度看,依然残留有某种连续性。为弥补这一不足,可采用MAD法经关键key映射为:

    (a * key + b) mod M ,其中M仍然为素数,a > 0, b > 0,且 a mod M !=0

实际上,除余法也可以看作是MAD法的一种特例(a=1 , b=0).

(3) (伪)随机数法

越是随机、越是没有规律,就越是好的收列函数,按照这一标准,任何一个(伪)随机数发生器,本身即是一个好的散列函数。比如,可直接使用C/C++语言提供的 rand ()函数,将关键码key映射至桶地址:

    rand(key) mod M ,其中rand(key) 为系统定义的第key个(伪)随机数。

这一策略的原理也可理解为,将“设计好散列函数”的任务,转换为“设计好的(伪)随机数发生器”的任务,幸运的是,二者的优化目标几乎是一致的。需特别留意的是,由于不同计算环境所提供的(伪)随机数发生器不尽相同,故在将某一系统中生成的散列表移植到另一系统时,必须格外小心。

7.解决冲突的方法通常有什么

(a) 开散列策略
(1) 多槽位(multiple slots)
最直截了当的一种对策是,将彼此冲突的每一组词条组织为一个小规模的子词典,分别存放于它们共同对应的桶单元中,比如一种简便的方法是,统一将各桶细分为更小的称作槽位 (slot) 的若干单元,每一组槽位可组织为向量或列表。
多槽位法有很多缺陷,
1.绝大多数的槽位通常都处与空i状态。导致装填因子大大降低;
2.很难在事先确定槽位应细分到何种程度,方可保证在任何情况下都够用。
(2) 独立链(separate chaining)
冲突排解的另一策略与多槽位 (multiple slots) 法类似,也令相互冲突的每组词条构成小规模的子词典,只不过采用列表(而非向量) 来实现各子词典。
相对于多槽位法,独立链法可更为灵活地动态调整各子词典的容量,降低空间消耗。但在查找过程中一旦发生冲突,则需要遍历整个列表,导致查找成本的增加,而且系统的缓存功能几乎失效(没有局部性)。
(3) 公共溢出区法
公共溢出区 (overflow area) 法的思路很简单,在原散列表之外另设个词典结构D,一旦在插入词条时发生冲突就将该词条转存至D中,就效果而言,D相当于一个存放冲突词条的公共缓冲池,该方法也因此得名。

(b) 闭散列策略(最常用)

尽管就逻辑结构而言,独立链等策略便捷而紧凑,但绝非上策。比如,因需要引入次级关联结构,实现相关算法的代码自身的复杂程度将加大大增加,反过来,因不能保证物理上的关联性,对于稍大规模的词条集,查找过程中将需做更多的I/O操作。
实际上,仅仅依靠基本的散列表结构,且就地排解冲突,反而是更i的选择。也就是说,新词条与已有词条冲突,则只允许在散列表内部为其寻找另一空桶,如此,各桶并非注定只能放特定的一组词条:从理论上讲,每个桶单元都有可能存放任一词,因为散列地址空间对所有词条开放,故这一新的策略亦称开放定址(open addressing) ,亦称闭散列(closed hashing) ,反之称为开散列。
(1) 线性试探法(linear probing)
若发现桶单元 ht[hash(key)] 已被占用,则转而试探桶单元 ht[hash(key) + 1];若ht[hash(key) +1]也被占用,则继续试探ht[hash(key) + 2,…;如此不断直到发现一个可用空桶。当然,为确保桶地址的合法,最后还需统一对M取模。因此准确地,第i次 试探的桶单元应为:

    ht[ (hash(key) +i) mod M ], i= 1,2,3,....

如此,被试探的桶单元在物理空间上依次连贯,其地址构成等差数列。

8.懒惰删除是什么含义

查找链中任何一环的缺失,都会导致后续词条因无法抵达而丢失,表现为有时无法找到实际已存在的词条。因此若采用开放定址策略,则在执行删除操作时,需同时做特别的调整。简明而有效的方法是,为每个桶另设一个标志位 ,指示该桶尽管目前为空,但此前确曾存放过词条具体地,为删除词条,只需将对应的桶标志位置1,该桶虽不存放任何实质的词条,却依然是查找链上的一环,这一方法称为懒惰删除。

9.BItMap中memcpy_s函数无法使用的原因

查看string.h头文件中无memcpy_s函数,将memcpy_s ( M, N, oldM, oldN );语句改为memcpy ( M, oldM, oldN );
结果: 成功运行

10.散列表是如何找到要插入元素的位置的

通过散列函数找到映射地址,如果该地址不为空,逐步向后依次查找,直到找到第一个为空的位置(无论是否带懒惰删除标志)

11.散列表如何找到要查找元素的位置的

沿查找链线性试探:跳过所有冲突的桶,以及带懒惰删除标记的桶,直到找到关键码位置相同的位置

12.重散列算法是如何运行的

  • 重散列算法:装填因子过大时,采取“逐一取出再插入”的朴素策略,对桶数组扩容

  • 不可简单地(通过memcpy())将原桶数组复制到新桶数组(比如前端),否则存在两个问题:

  • 1)会继承原有冲突;2)可能导致查找链在后端断裂——即便为所有扩充桶设置懒惰删除标志也无济于事

    注意: 利用插入函数逐一插入时,在此过程中会计算散列码

9.3.1 BitMap.h

9.3.1.1 ADT接口

操作 功能 对象
bitmap(int n = 8) 构造函数,默认容量为8位 位图
bitmap(char* file, int n = 8) 构造函数,从指定文件中读取比特图 位图
~bitmap() 析构函数,释放位图空间 位图
init(int n) 初始化位图空间 位图
set(int k) 置位第k个标志位 位图
clear(int k) 复位第k个标志位 位图
test(int k) 取出指定字节中的指定位 位图
dump(char* file) 将位图整体导出至指定的文件 位图
bits2string(int n) 将前n位转换为字符串 位图
expand(int k) 扩容 位图
print(int n) 逐位打印以检验位图内容 位图

9.3.1.2 BitMap.h

//"bitmap.h"
//"../dsa_bintree_20200720/release.h"可见https://blog.csdn.net/weixin_41698717/article/details/107541482
#pragma once

#include <stdlib.h>
#include <stdio.h>
#include <memory.h>
#include <cstring>
#include "release.h"


class Bitmap {
    
     //位图Bitmap类:以空间为补偿,节省初始化时间(既允许插入也允许删除)
private:
   char* M; int N; //比特图所存放的空间M[],容量为N*sizeof(char)*8比特
protected:
   void init ( int n ) {
    
     M = new char[N = ( n + 7 ) / 8]; memset ( M, 0, N ); }
public:
   Bitmap ( int n = 8 ) {
    
     init ( n ); } //按指定或默认规模创建比特图(为测试暂时选用较小的默认值)
   Bitmap ( char* file, int n = 8 ) //按指定或默认规模,从指定文件中读取比特图
   {
    
      init ( n ); FILE* fp = fopen ( file, "r" ); fread ( M, sizeof ( char ), N, fp ); fclose ( fp );  }
   ~Bitmap() {
    
     delete [] M; M = NULL; } //析构时释放比特图空间

   void set   ( int k ) {
    
     expand ( k );        M[k >> 3] |=   ( 0x80 >> ( k & 0x07 ) ); }
   void clear ( int k ) {
    
     expand ( k );        M[k >> 3] &= ~ ( 0x80 >> ( k & 0x07 ) ); }
   bool test  ( int k ) {
    
     expand ( k ); return M[k >> 3] &    ( 0x80 >> ( k & 0x07 ) ); }

   void dump ( char* file ) //将位图整体导出至指定的文件,以便对此后的新位图批量初始化
   {
    
      FILE* fp = fopen ( file, "w" ); fwrite ( M, sizeof ( char ), N, fp ); fclose ( fp );  }
   char* bits2string ( int n ) {
    
     //将前n位转换为字符串——
      expand ( n - 1 ); //此时可能被访问的最高位为bitmap[n - 1]
      char* s = new char[n + 1]; s[n] = '\0'; //字符串所占空间,由上层调用者负责释放
      for ( int i = 0; i < n; i++ ) s[i] = test ( i ) ? '1' : '0';
      return s; //返回字符串位置
   }
   void expand ( int k ) {
    
     //若被访问的Bitmap[k]已出界,则需扩容
      if ( k < 8 * N ) return; //仍在界内,无需扩容
      int oldN = N; char* oldM = M;
      init ( 2 * k ); //与向量类似,加倍策略
      memcpy ( M, oldM, oldN ); delete [] oldM; //原数据转移至新空间
   }
   /*DSA*/
   /*DSA*/   void print ( int n ) //逐位打印以检验位图内容,非必需接口
   /*DSA*/   {
    
      expand ( n ); for ( int i = 0; i < n; i++ ) printf ( test ( i ) ? "1" : "0" );  }
};

9.3.1 HashTable(散列表)

9.3.1.1 ADT接口

操作 功能
HashTable(c ) 创建一个容量不小于c的散列表(为测试暂时选用较小的默认值)
~HashTable( ) 释放桶数组及其中各(非空)元素所指向的词条
get(k ) 读取
probe4Hit( ) 沿关键码k对应的查找链,找到词条匹配的桶
remove( ) 删除
put( ) 插入(禁止雷同词条,故可能失败)
probe4Free( ) 沿关键码k对应的查找链,找到首个可用空桶
rehash( ) 重散列,扩充桶数组容量
hashCode( ) 散列码统一转换方法
size() 返回当前词条数目

9.3.1.2 HashTable类模板

#include "dictionary.h" //引入词典ADT
#include "bitmap.h" //引入位图
#include "entry.h"
template <typename K, typename V> //key、value
class Hashtable : public Dictionary<K, V> {
    
     //符合Dictionary接口的Hashtable模板类
   /*DSA*/friend class UniPrint;
private:
   Entry<K, V>** ht; //桶数组,存放词条指针
   int M; //桶数组容量
   int N; //词条数量
   Bitmap* lazyRemoval; //懒惰删除标记
#define lazilyRemoved(x)  (lazyRemoval->test(x))
#define markAsRemoved(x)  (lazyRemoval->set(x))
protected:
   int probe4Hit ( const K& k ); //沿关键码k对应的查找链,找到词条匹配的桶
   int probe4Free ( const K& k ); //沿关键码k对应的查找链,找到首个可用空桶
   void rehash(); //重散列算法:扩充桶数组,保证装填因子在警戒线以下
public:
   Hashtable ( int c = 5 ); //创建一个容量不小于c的散列表(为测试暂时选用较小的默认值)
   ~Hashtable(); //释放桶数组及其中各(非空)元素所指向的词条
   int size() const {
    
     return N; } // 当前的词条数目
   bool put ( K, V ); //插入(禁止雷同词条,故可能失败)
   V* get ( K k ); //读取
   bool remove ( K k ); //删除
};

9.3.1.3 HashTable(c )

template <typename K, typename V> Hashtable<K, V>::Hashtable ( int c ) {
    
     //创建散列表,容量为
   //M = primeNLT ( c, 1048576, "prime-1048576-bitmap.txt" ); //不小于c的素数M
   M = 7;
   N = 0; ht = new Entry<K, V>*[M]; //开辟桶数组(还需核对申请成功),初始装填因子为N/M = 0%
   memset ( ht, 0, sizeof ( Entry<K, V>* ) *M ); //初始化各桶
   lazyRemoval = new Bitmap ( M ); //懒惰删除标记比特图
   //*DSA*/printf("A bucket array has been created with capacity = %d\n\n", M);
}

9.3.1.4 ~HashTable( )

//析构
template <typename K, typename V> Hashtable<K, V>::~Hashtable() {
    
     //析构前释放桶数组及非空词条
   for ( int i = 0; i < M; i++ ) //逐一检查各桶
      if ( ht[i] ) dtl::release ( ht[i] ); //释放非空的桶
   dtl::release ( ht ); //释放桶数组
   dtl::release ( lazyRemoval ); //释放懒惰删除标记
}

9.3.1.5 get(k )

//查找
template <typename K, typename V> V* Hashtable<K, V>::get ( K k ) //散列表词条查找算法
{
    
      int r = probe4Hit ( k ); return ht[r] ? & ( ht[r]->value ) : NULL;  } //禁止词条的key值雷同

9.3.1.6 probe4Hit( )

template <typename K, typename V> int Hashtable<K, V>::probe4Hit ( const K& k ) {
    
    
   int r = hashCode ( k ) % M; //从起始桶(按除余法确定)出发
   //*DSA*/printf(" ->%d", r);
   while ( ( ht[r] && ( k != ht[r]->key ) ) || ( !ht[r] && lazilyRemoved ( r ) ) )
      r = ( r + 1 ) % M; //沿查找链线性试探:跳过所有冲突的桶,以及带懒惰删除标记的桶
   //*DSA*/printf(" ->%d", r);
   //*DSA*/printf("\n");
   return r; //调用者根据ht[r]是否为空,即可判断查找是否成功
}

9.3.1.7 remove( )

//删除
template <typename K, typename V> bool Hashtable<K, V>::remove ( K k ) {
    
     //散列表词条删除算法
   int r = probe4Hit ( k ); if ( !ht[r] ) return false; //对应词条不存在时,无法删除
   dtl::release ( ht[r] ); ht[r] = NULL; markAsRemoved ( r ); N--; return true;
   //否则释放桶中词条,设置懒惰删除标记,并更新词条总数
}

9.3.1.8 put( )

//插入
template <typename K, typename V> bool Hashtable<K, V>::put ( K k, V v ) {
    
     //散列表词条插入
   if ( ht[probe4Hit ( k ) ] ) return false; //雷同元素不必重复插入
   int r = probe4Free ( k ); //为新词条找个空桶(只要装填因子控制得当,必然成功)
   ht[r] = new Entry<K, V> ( k, v ); ++N; //插入(注意:懒惰删除标记无需复位)
   if ( N * 2 > M ) rehash(); //装填因子高于50%后重散列
   return true;
}

9.3.1.9 probe4Free( )

template <typename K, typename V> int Hashtable<K, V>::probe4Free ( const K& k ) {
    
    
   int r = hashCode ( k ) % M; //从起始桶(按除余法确定)出发
   //*DSA*/printf(" ->%d", r); //首个试探的桶单元地址
   while ( ht[r] ) r = ( r + 1 ) % M; //沿查找链逐桶试探,直到首个空桶(无论是否带有懒惰删除标记)
//*DSA*/   while (ht[r]) { r = (r+1) % M; printf(" ->%d", r); } printf("\n");
   return r; //为保证空桶总能找到,装填因子及散列表长需要合理设置
}

9.3.1.10 rehash( )

//重散列
/******************************************************************************************
 * 重散列算法:装填因子过大时,采取“逐一取出再插入”的朴素策略,对桶数组扩容
 * 不可简单地(通过memcpy())将原桶数组复制到新桶数组(比如前端),否则存在两个问题:
 * 1)会继承原有冲突;2)可能导致查找链在后端断裂——即便为所有扩充桶设置懒惰删除标志也无济于事
 ******************************************************************************************/
template <typename K, typename V> void Hashtable<K, V>::rehash() {
    
    
   int old_capacity = M; Entry<K, V>** old_ht = ht;
   //M = primeNLT ( 2 * M, 1048576, "prime-1048576-bitmap.txt" ); //容量至少加倍
   M = 17;
   N = 0; ht = new Entry<K, V>*[M]; memset ( ht, 0, sizeof ( Entry<K, V>* ) * M ); //新桶数组
   dtl::release ( lazyRemoval ); lazyRemoval = new Bitmap ( M ); //新开懒惰删除标记比特图
   //*DSA*/printf("A bucket array has been created with capacity = %d\n\n", M);
   for ( int i = 0; i < old_capacity; i++ ) //扫描原桶数组
      if ( old_ht[i] ) //将非空桶中的词条逐一
         put ( old_ht[i]->key, old_ht[i]->value ); //插入至新的桶数组
   dtl::release ( old_ht ); //释放原桶数组——由于其中原先存放的词条均已转移,故只需释放桶数组本身
}

9.3.1.11 hashCode( )

static size_t hashCode ( char c ) {
    
     return ( size_t ) c; } //字符
static size_t hashCode ( int k ) {
    
     return ( size_t ) k; } //整数以及长长整数
static size_t hashCode ( long long i ) {
    
     return ( size_t ) ( ( i >> 32 ) + ( int ) i ); }
static size_t hashCode ( char s[] ) {
    
     //生成字符串的循环移位散列码(cyclic shift hash code)
   int h = 0; //散列码
   for ( size_t n = strlen ( s ), i = 0; i < n; i++ ) //自左向右,逐个处理每一字符
      {
    
     h = ( h << 5 ) | ( h >> 27 ); h += ( int ) s[i]; } //散列码循环左移5位,再累加当前字符
   return ( size_t ) h; //如此所得的散列码,实际上可理解为近似的“多项式散列码”
} //对于英语单词,"循环左移5位"是实验统计得出的最佳值

9.3.2 HashTable.h

#include "dictionary.h" //引入词典ADT
#include "bitmap.h" //引入位图
#include "entry.h"

static size_t hashCode ( char c ) {
    
     return ( size_t ) c; } //字符
static size_t hashCode ( int k ) {
    
     return ( size_t ) k; } //整数以及长长整数
static size_t hashCode ( long long i ) {
    
     return ( size_t ) ( ( i >> 32 ) + ( int ) i ); }
static size_t hashCode ( char s[] ) {
    
     //生成字符串的循环移位散列码(cyclic shift hash code)
   int h = 0; //散列码
   for ( size_t n = strlen ( s ), i = 0; i < n; i++ ) //自左向右,逐个处理每一字符
      {
    
     h = ( h << 5 ) | ( h >> 27 ); h += ( int ) s[i]; } //散列码循环左移5位,再累加当前字符
   return ( size_t ) h; //如此所得的散列码,实际上可理解为近似的“多项式散列码”
} //对于英语单词,"循环左移5位"是实验统计得出的最佳值


template <typename K, typename V> //key、value
class Hashtable : public Dictionary<K, V> {
    
     //符合Dictionary接口的Hashtable模板类
   /*DSA*/friend class UniPrint;
private:
   Entry<K, V>** ht; //桶数组,存放词条指针
   int M; //桶数组容量
   int N; //词条数量
   Bitmap* lazyRemoval; //懒惰删除标记
#define lazilyRemoved(x)  (lazyRemoval->test(x))
#define markAsRemoved(x)  (lazyRemoval->set(x))
protected:
   int probe4Hit ( const K& k ); //沿关键码k对应的查找链,找到词条匹配的桶
   int probe4Free ( const K& k ); //沿关键码k对应的查找链,找到首个可用空桶
   void rehash(); //重散列算法:扩充桶数组,保证装填因子在警戒线以下
public:
   Hashtable ( int c = 5 ); //创建一个容量不小于c的散列表(为测试暂时选用较小的默认值)
   ~Hashtable(); //释放桶数组及其中各(非空)元素所指向的词条
   int size() const {
    
     return N; } // 当前的词条数目
   bool put ( K, V ); //插入(禁止雷同词条,故可能失败)
   V* get ( K k ); //读取
   bool remove ( K k ); //删除
};
//构造
int primeNLT ( int c, int n, char* file ) {
    
     //根据file文件中的记录,在[c, n)内取最小的素数
   Bitmap B ( file, n ); //file已经按位图格式,记录了n以内的所有素数,因此只要
   while ( c < n ) //从c开始,逐位地
      if ( B.test ( c ) ) c++; //测试,即可
      else return c; //返回首个发现的素数
   return c; //若没有这样的素数,返回n(实用中不能如此简化处理)
}
template <typename K, typename V> Hashtable<K, V>::Hashtable ( int c ) {
    
     //创建散列表,容量为
   //M = primeNLT ( c, 1048576, "prime-1048576-bitmap.txt" ); //不小于c的素数M
   M = 7;
   N = 0; ht = new Entry<K, V>*[M]; //开辟桶数组(还需核对申请成功),初始装填因子为N/M = 0%
   memset ( ht, 0, sizeof ( Entry<K, V>* ) *M ); //初始化各桶
   lazyRemoval = new Bitmap ( M ); //懒惰删除标记比特图
   //*DSA*/printf("A bucket array has been created with capacity = %d\n\n", M);
}


//析构
template <typename K, typename V> Hashtable<K, V>::~Hashtable() {
    
     //析构前释放桶数组及非空词条
   for ( int i = 0; i < M; i++ ) //逐一检查各桶
      if ( ht[i] ) dtl::release ( ht[i] ); //释放非空的桶
   dtl::release ( ht ); //释放桶数组
   dtl::release ( lazyRemoval ); //释放懒惰删除标记
}
//查找
template <typename K, typename V> V* Hashtable<K, V>::get ( K k ) //散列表词条查找算法
{
    
      int r = probe4Hit ( k ); return ht[r] ? & ( ht[r]->value ) : NULL;  } //禁止词条的key值雷同

template <typename K, typename V> int Hashtable<K, V>::probe4Hit ( const K& k ) {
    
    
   int r = hashCode ( k ) % M; //从起始桶(按除余法确定)出发
   //*DSA*/printf(" ->%d", r);
   while ( ( ht[r] && ( k != ht[r]->key ) ) || ( !ht[r] && lazilyRemoved ( r ) ) )
      r = ( r + 1 ) % M; //沿查找链线性试探:跳过所有冲突的桶,以及带懒惰删除标记的桶
   //*DSA*/printf(" ->%d", r);
   //*DSA*/printf("\n");
   return r; //调用者根据ht[r]是否为空,即可判断查找是否成功
}
//删除
template <typename K, typename V> bool Hashtable<K, V>::remove ( K k ) {
    
     //散列表词条删除算法
   int r = probe4Hit ( k ); if ( !ht[r] ) return false; //对应词条不存在时,无法删除
   dtl::release ( ht[r] ); ht[r] = NULL; markAsRemoved ( r ); N--; return true;
   //否则释放桶中词条,设置懒惰删除标记,并更新词条总数
}
//插入
template <typename K, typename V> bool Hashtable<K, V>::put ( K k, V v ) {
    
     //散列表词条插入
   if ( ht[probe4Hit ( k ) ] ) return false; //雷同元素不必重复插入
   int r = probe4Free ( k ); //为新词条找个空桶(只要装填因子控制得当,必然成功)
   ht[r] = new Entry<K, V> ( k, v ); ++N; //插入(注意:懒惰删除标记无需复位)
   if ( N * 2 > M ) rehash(); //装填因子高于50%后重散列
   return true;
}


template <typename K, typename V> int Hashtable<K, V>::probe4Free ( const K& k ) {
    
    
   int r = hashCode ( k ) % M; //从起始桶(按除余法确定)出发
   //*DSA*/printf(" ->%d", r); //首个试探的桶单元地址
   while ( ht[r] ) r = ( r + 1 ) % M; //沿查找链逐桶试探,直到首个空桶(无论是否带有懒惰删除标记)
//*DSA*/   while (ht[r]) { r = (r+1) % M; printf(" ->%d", r); } printf("\n");
   return r; //为保证空桶总能找到,装填因子及散列表长需要合理设置
}
//重散列
/******************************************************************************************
 * 重散列算法:装填因子过大时,采取“逐一取出再插入”的朴素策略,对桶数组扩容
 * 不可简单地(通过memcpy())将原桶数组复制到新桶数组(比如前端),否则存在两个问题:
 * 1)会继承原有冲突;2)可能导致查找链在后端断裂——即便为所有扩充桶设置懒惰删除标志也无济于事
 ******************************************************************************************/
template <typename K, typename V> void Hashtable<K, V>::rehash() {
    
    
   int old_capacity = M; Entry<K, V>** old_ht = ht;
   //M = primeNLT ( 2 * M, 1048576, "prime-1048576-bitmap.txt" ); //容量至少加倍
   M = 17;
   N = 0; ht = new Entry<K, V>*[M]; memset ( ht, 0, sizeof ( Entry<K, V>* ) * M ); //新桶数组
   dtl::release ( lazyRemoval ); lazyRemoval = new Bitmap ( M ); //新开懒惰删除标记比特图
   //*DSA*/printf("A bucket array has been created with capacity = %d\n\n", M);
   for ( int i = 0; i < old_capacity; i++ ) //扫描原桶数组
      if ( old_ht[i] ) //将非空桶中的词条逐一
         put ( old_ht[i]->key, old_ht[i]->value ); //插入至新的桶数组
   dtl::release ( old_ht ); //释放原桶数组——由于其中原先存放的词条均已转移,故只需释放桶数组本身
}

9.3.2 HashTable.h的测试

/*
 * The program is a test for hashtable
 * author@Ripples
 * 20200803
 */
#include "hashtable.h"
#include <iostream>
using namespace std;
int main(){
    
    
    Hashtable<int, string> ht;
    //测试put
    ht.put( 2, "Kyrie Irving");
    ht.put( 23, "LeBron James" );
    ht.put( 30, "Stephen Curry" );
    //测试get
    cout << *(ht.get( 2 )) << endl;//Kyrie Irving
    //测试remove
    ht.remove( 23 );
    cout << *(ht.get( 30 )) << endl;//Stephen Curry//没有因为删除中间的而导致查找链断裂
    ht.put( 0, "Kyrie Irving");
    ht.put( 1, "Rippes Ding" );
    ht.put( 3, "Rippes Ding" );
    ht.put( 4, "Rippes Ding" );
    //测试rehash(),装载率大于50%则rehash
    cout << *(ht.get( 4 )) << endl;//Rippes Ding
    return 0;
}

猜你喜欢

转载自blog.csdn.net/ZXG20000/article/details/114868636
9.3