数据结构与算法——散列表(C++)

1 散列表简介

  • 散列表是词典的一种形态,散列表通过“循值”访问数据,其动态特性与静态特性都十分优秀
  • 散列表具支持多个词条拥有相同的关键码
  • 散列表的关键是散列函数的设计,随机性强、规律性弱的散列函数最好

散列表的支持的接口如下:

操作接口功能描述
get(key)若词典存在以key为关键码的词条,返回该词条的数据对象;否则返回NULL
put(key, value)插入词条(key, value),并报告是否成功
remove(key)若词典存在以key为关键码的词条,删除之并返回true;否则返回false

2 散列表包含的头文件

实现散列表需要包含一些头文件,包括"dictionary.h", “bitmap.h”, “entry.h”。其中,"dictionary.h"是词典的类,供散列表头文件改写函数接口;"bitmap.h"是位图的头文件,功能是为懒惰删除标志提高操作函数;"entry.h"包含一个含有(key, value)的结构体。

这三个头文件的源文件如下:

//"dictionary.h"
#pragma once

template <typename K, typename V> struct Dictionary { //词典Dictionary模板类
   virtual int size() const = 0; //当前词条总数
   virtual bool put ( K, V ) = 0; //插入词条(禁止雷同词条时可能失败)
   virtual V* get ( K k ) = 0; //读取词条
   virtual bool remove ( K k ) = 0; //删除词条
};
//"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 "../dsa_bintree_20200720/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_s ( M, N, 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" );  }
};
//"entry.h"
#pragma once

template <typename K, typename V> struct Entry { //词条模板类
   K key; V value; //关键码、数值
   Entry ( K k = K(), V v = V() ) : key ( k ), value ( v ) {}; //默认构造函数
   Entry ( Entry<K, V> const& e ) : key ( e.key ), value ( e.value ) {}; //基于克隆的构造函数
   bool operator< ( Entry<K, V> const& e ) { return key <  e.key; }  //比较器:小于
   bool operator> ( Entry<K, V> const& e ) { return key >  e.key; }  //比较器:大于
   bool operator== ( Entry<K, V> const& e ) { return key == e.key; } //判等器:等于
   bool operator!= ( Entry<K, V> const& e ) { return key != e.key; } //判等器:不等于
}; //得益于比较器和判等器,从此往后,不必严格区分词条及其对应的关键码

3 散列表

散列表头文件如下所示,支持remove,get与put的函数实现。两种不同的查找函数probe4Hit与probe4Free分别应用于remove & get和put的场合。

#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 ); //释放原桶数组——由于其中原先存放的词条均已转移,故只需释放桶数组本身
}

4 散列表的测试

散列表的测试主函数如下所示,注释中的结果显示测试成功。

/*
 * 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;
}

5 散列表的应用——桶排序

5.1 桶排序

以下是桶排序的原理实现,简单的程序实现桶排序。

/*
 * This program is a test for bucket sort
 * instance:{ 9, 1, 2, 8, 5, 6, 3, 7, 4}
 * author@Ripples
 * 20200803
 */
#include <iostream>
using namespace std;
void bucket_sort( int x[], int M , int (&arr)[10]);
int main(){
    int a[] = { 9, 1, 2, 8, 5, 6, 3, 7, 4, 0};
    int b[10];
    bucket_sort( a, 10, b);
}

void bucket_sort( int x[], int M , int (&arr)[10]){
    int len = 10;
    //依次放入桶单元
    for( int i = 0; i < len; ++i){
        int r = x[i] % M;
        arr[r] = x[i];
    }
    //从桶单元取出
    for( int j = 0; j < 10; j ++){
        cout << arr[j] << endl;//0 1 ,..., 8 9
    }
}

当测试序列中存在重复数字时,需要在桶中安放一个列表,用于存储重复的数字并输出。

/*
 * This program is a test for bucket sort
 * instance:{ 9, 1, 2, 2, 5, 3, 3, 7, 4, 0}
 * author@Ripples
 * 20200804
 */
#include <vector>
#include <iostream>
#include "../dsa_list_20200715/list.h"
using namespace std;

void bucket_sort( int x[], int M, vector<int> &b);
int main(){
    int a[] = { 9, 1, 2, 2, 5, 3, 3, 7, 4, 0};
    vector<int> b;
    bucket_sort( a, 10, b);
    for( int i = 0; i < 10; ++i){
        cout << b[i] << endl;//0 1 2 2 3 3 4 5 7 9 
    }
}

void bucket_sort( int x[], int M, vector<int> &b){
    vector<List<int>> vec(10);
    int len = 10;
    //依次放入桶单元
    for( int i = 0; i < len; ++i){
        int r = x[i] % M;

        //列表插入
        vec[r].insertAsLast( x[i] );
    }
    //从桶单元取出
    for( int j = 0; j < 10; j ++){
        ListNodePosi(int) p = vec[j].first();
        while( p != vec[j].trailer ){
            b.push_back( p->data);
            p = p->succ;
        }
    }
}

5.2 基数排序

对于三位数的数集,分别针对个位、十位与百位做过一趟桶排序,即可得到正确的排序结果。

6 总结

散列表完成了高效的“循值访问”,在实际中具有深远的应用意义。目前只能算入了个门,将来要在学习和应用中深刻自己对散列表的认识与理解。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值