多阶哈希表
前面笔者的博客已经介绍了哈希表中解决哈希冲突的开放定制法和拉链法,无意中接触到了多阶哈希这种数据结构,同样也可以解决哈希冲突的问题。经过各种资料和源码的阅读,今天拿出来与大家分享。
多阶哈希表结构
从多阶哈希这个名字就可以听出,多阶哈希在结构上应该像楼梯一样有多阶。没错,正如我们所想的,多阶哈希的结构如下图
通过上面的图我们发现,多阶哈希并不是每一阶都是一样长的,而是从上往下逐渐减小类型于锯齿状,每一阶的长度都是一个素数,并且多阶哈希的每一阶的每个节点是一个HashNode,每个节点的前四个字节是一个int类型的Key。
那么问题来了,每一阶的长度到底是怎么定义的呢?实际上,假设你希望你的多阶哈希有10阶,现在你想映射1000个数据,这10阶哈希的长度从上往下就是小于1000最大的10个素数,通过素数集中的算法得到的10个素数分别是: 997 991 983 977 971 967 953 947 941 937. 可见虽然是锯齿数组,但各层之间的差别并不是很多。
现在相信你已经明白了多阶哈希的基本结构,但是我相信有很多读者都会有一个刚开始同样困扰笔者的问题,这些锯齿状数组是怎么组织在一起的呢?
解答:俗话说,不要被事物表面现象所迷惑,你现在所看到的实际上是多阶哈希的逻辑结构(就像开了美颜一样),多阶哈希真正的物理结构依旧是一个连续的数组,他只不过是通过控制下标的方式来访问不同的层级。
多阶哈希表源码刨析
废话不多说,上面讲了基本的结构,现在我们就基于这个基本结构来刨析一下多阶哈希的源码:(不要觉得看源码恶心,其实读懂意思会有一种豁然开朗的感觉)
定义多阶哈希类
modTable[lines]每次都会根据模板参数在后面的getMode()函数中进行初始化
//maxLine最大一行最多有多少节点,lines是有多少行
template<typename valueType, unsigned long maxLine, int lines>
class hash_shm
{
public:
int find(unsigned long _key); //如果_key在表中,返回0,并设置lastFound位置,否则返回-1
int remove(unsigned long _key); //如果_key不在表中,返回-1,否则删除节点,设置节点key 0并返回0
//将节点插入表中,如果_key存在,返回1;如果插入成功,返回0;如果失败,返回-1
int insert(unsigned long _key, const valueType &_value);
void clear(); //移除所有的数据
public:
double getFullRate()const; //使用空间的比率
public:
//使用共享内存的起始位置和空间大小,如果空间不够,程序将退出
hash_shm(void *startShm, unsigned long shmSize = sizeof(hash_node)*maxLine*lines);
//使用共享内存键,它将获得共享内存,如果失败,退出
hash_shm(key_t shm_key);
~hash_shm(){} //销毁
private:
void *mem; //用于存储运行时数据共享内存的空间的起始位置指针
unsigned long memSize; //共享内存的大小,类似于vector的capacity
unsigned long modTable[lines]; //通过这张表获取最大的lines个素数
unsigned long maxSize; //表的大小,类似于vector的size
unsigned long *currentSize; //表的当前大小,表示现在表中有效的数据个数
void *lastFound; //记录最后查找的位置
struct hash_node{ //哈希表的节点
unsigned long key; //当key==0时,节点为空
valueType value; //名称-值对
};
private:
bool getShm(key_t shm_key); //获取构造函数使用的共享内存
void getMode(); //获取构造函数使用的最大素数blow maxLine
void *getPos(unsigned int _row, unsigned long _col);//得到(row,col)的位置
};
多阶哈希内存管理
下面这三个函数看不懂没关系,你只需要知道这三个函数是在进行哈希的内存初始化,就好比给vector开空间,设置size,而多阶哈希中*currentSize就是当前哈希表中的有效数据
template<typename vT, unsigned long maxLine, int lines>
bool hash_shm<vT, maxLine, lines>::getShm(key_t shm_key)
{
int shm_id = shmget(shm_key, memSize, 0666);
if (shm_id == -1) //check if the shm exists
{
shm_id = shmget(shm_key, memSize, 0666 | IPC_CREAT);//create the shm
if (shm_id == -1){
cerr << "Share memory get failed\n";
return false;
}
}
mem = shmat(shm_id, NULL, 0); //mount the shm
if (int(mem) == -1){
cerr << "shmat system call failed\n";
return false;
}
return true;
}
template<typename vT, unsigned long maxLine, int lines>
hash_shm<vT, maxLine, lines>::hash_shm(void *startShm, unsigned long shmSize)
{
if (startShm != NULL){
cerr << "Argument error\n Please check the shm address\n";
exit(-1);
}
getMode();//获取使用的最大素数blow maxLine
maxSize = 0;
for (int i = 0; i<lines; i++) //计算最大尺寸
maxSize += modTable[i];
if (shmSize<sizeof(hash_node)*(maxSize + 1)){ //检查共享内存大小
cerr << "Not enough share memory space\n";
exit(-1);
}
memSize = shmSize;
if (*(currentSize = (unsigned long *)((long)mem + memSize))<0)
*currentSize = 0;;
}
template<typename vT, unsigned long maxLine, int lines>
hash_shm<vT, maxLine, lines>::hash_shm(key_t shm_key)
{ //获得共享内存
getMode();
maxSize = 0;
for (int i = 0; i<lines; i++)
maxSize += modTable[i];
memSize = sizeof(hash_node)*maxSize;
if (!getShm(shm_key)){
exit(-1);
}
if (*(currentSize = (unsigned long *)((long)mem + memSize))<0)
*currentSize = 0;
}
设置成员变量modTable[lines]的值
注意多阶哈希最长的一阶必须超过5
template<typename vT, unsigned long maxLine, int lines>
void hash_shm<vT, maxLine, lines>::getMode()
{ //采用 6n+1 6n-1 素数集中原理
if (maxLine<5){ exit(-1); }
unsigned long t, m, n, p;
int i, j, a, b, k;
int z = 0;
for (t = maxLine / 6; t >= 0, z<lines; t--)
{
i = 1; j = 1; k = t % 10;
m = 6 * t; /**i,j的值 是是否进行验证的标志也是对应的6t-1和6t+1的素性标志**/
if (((k - 4) == 0) || ((k - 9) == 0) || ((m + 1) % 3 == 0))j = 0;/*此处是简单验证6*t-1,6*t+1 是不是素数,借以提高素数纯度**/
if (((k - 6) == 0) || ((m - 1) % 3 == 0))i = 0; /***先通过初步判断去除末尾是5,及被3整除的数***/
for (p = 1; p * 6 <= sqrt(m + 1) + 2; p++)
{
n = p * 6; /**将6*p-1和6*p+1看作伪素数来试除*****/
k = p % 10;
a = 1; b = 1; /**同样此处a,b的值也是用来判断除数是否为素数提高除数的素数纯度**/
if (((k - 4) == 0) || ((k - 9) == 0))a = 0;
if (((k - 6) == 0))b = 0;
if (i){ /*如果i非零就对m-1即所谓6*t-1进行验证,当然还要看除数n+1,n-1,素性纯度*/
if (a){ if ((m - 1) % (n + 1) == 0)i = 0; } /***一旦被整除就说明不是素数故素性为零即将i 赋值为零***/
if (b){ if ((m - 1) % (n - 1) == 0)i = 0; }
}
if (j){ /**如果j非零就对m+1即所谓6*t+1进行验证,当然还要看除数n+1,n-1,素性纯度*/
if (a){ if ((m + 1) % (n + 1) == 0)j = 0; } /***一旦被整除就说明不是素数故素性为零即将j 赋值为零***/
if (b){ if ((m + 1) % (n - 1) == 0)j = 0; }
}
if ((i + j) == 0)break; /**如果已经知道6*t-1,6*t+1都不是素数了那就结束试除循环***/
}
if (j){ modTable[z++] = m + 1; if (z >= lines)return; }
if (i){ modTable[z++] = m - 1; if (z >= lines)return; }
}
}
多阶哈希的插入和获取pos
如果多阶哈希当前的位置已经存在为key的数据那么直接返回,如果当前位置有数据但是不为key,那么就到下一阶查看,如果找到空位置,放入并且返回。这里其实会有每一阶相应的位置都满的情况我们后面进行讨论
template<typename vT, unsigned long maxLine, int lines>
int hash_shm<vT, maxLine, lines>::insert(unsigned long _key, const vT &_value)
{
if (find(_key) == 0)return 1; //表示key已经存在
unsigned long hash;
hash_node *pH = NULL;
for (int i = 0; i<lines; i++){
hash = (_key + maxLine) % modTable[i];
pH = (hash_node *)getPos(i, hash);
if (pH->key == 0){ //寻找要插入的位置
pH->key = _key;
pH->value = _value;
(*currentSize)++;//有效数据个数++
return 0;
}
}
return -1; //all the appropriate position filled
}
template<typename vT, unsigned long maxLine, int lines>
void *hash_shm<vT, maxLine, lines>::getPos(unsigned int _row, unsigned long _col)
{
unsigned long pos = 0UL;
for (int i = 0; i<_row; i++) //calculate the positon from the start
pos += modTable[i];
pos += _col;
if (pos >= maxSize)return NULL;
return (void *)((long)mem + pos*sizeof(hash_node));
}
多阶哈希的查找
这个很简单,迭代着去每一阶查找就好。
template<typename vT, unsigned long maxLine, int lines>
int hash_shm<vT, maxLine, lines>::find(unsigned long _key)
{
unsigned long hash;
hash_node *pH = NULL;
for (int i = 0; i<lines; i++)
{
hash = (_key + maxLine) % modTable[i]; //calculate the col position
pH = (hash_node *)getPos(i, hash);
if (pH->key == _key){
lastFound = pH;
return 0;
}
}
return -1;
}
多阶哈希的清空
这里贴这个代码是希望大家能更好的理解为什么前面说多阶哈希在物理结构上实际也是连续的。
template<typename vT, unsigned long maxLine, int lines>
void hash_shm<vT, maxLine, lines>::clear()
{
memset(mem, 0, memSize);
*currentSize = 0;
}
多阶哈希表小结
说到这里相信你对多阶哈希有了更深的理解,但是这里依旧存在一些问题,如果哈希冲突的特别厉害,比如某个下标出现哈希冲突数据堆积现象,但是你多阶哈希阶数有限,这样就可能会导致很多数无法映射到表中。如果你将表阶数设置的非常多,那么会影响搜索的效率,设置太少又会使负载率大大减少。简单的比方,你将哈希阶数设置为20,你仔细思考如果在AVL树种搜索20次可以搜索多少数据。这里一起来看看数据统计的结果。
通过表我们发现,HASH层数的增多,负载能力可以逐渐提高,但是依旧有数据无法放入且效率可能得不到保障。
多阶哈希和拉链法比较
-
hash冲突处理方式都非常简单. 开链法是找到位置他们的查找时间复杂度常规情况下都是O(1). 开链极端才会O(N)
-
拉链法有多个桶(链表),使得空间利用率很高,多阶哈希并不需要一个很大的桶来减少冲突.
-
开链法的增容做的事情将所有节点都要重新挂. 但是多阶哈希可以动态增长空间,不断加入新的一阶,且对原来的数据没有影响.
-
相对来说,多阶哈希是提前开辟好空间,而哈希桶是来一个添加一个,所以多阶哈希比较占用空间.
关于多阶哈希的讲解就到这里,如果读者有什么不懂和文中有什么错误尽管指出。