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