在看Hector Garcia-Molina,Jeffrey D.Ullman,Jennifer Widom等人写的《数据库系统实现》的时候,
第14.3节介绍了两种可以动态扩充容量的哈希算法。
1.Extensible Hash Tables
2.Linear Hash Tables(以下简称LHT)
第一种方法有其局限性,具体可以去看书,本文主要介绍第二种方法。
哈希表的主要用途,就是根据一个搜索关键字(Search Key)来搜索符合这个关键字的记录。
假设数据库里存了许多学生的信息,那么,可以把学号当做Search Key 来建立一个哈希表,
每个记录在哈希表中的存储:(这个记录的Search Key , 指向这个记录的指针)
这样,给定学号,就可以利用哈希表快速找这个学号对应的学生的信息。
假设总记录数为 n.
用平衡树可以以Search key为关键字,建立一颗二叉搜索树,达到插入复杂度O(log2(n)),查询复杂度O(log2(n)),空间复杂度O(n)。
而使用哈希表,可以做到平均插入复杂度O(1),平均查询复杂度O(1),空间复杂度O(n).
Linear Hash Tables 是一种动态扩展空间的哈希表,会随着插入的元素的增多而自动扩展空间。
这个算法,将n条记录装进N个桶中,使得每个桶中的元素个数较少,从而达到快速查询的目的。
几个状态变量的解释:
P:平均每个桶能装的元素个数(P为常量,在整个算法过程中不变)
E : 当前使用哈希值的最低的 E 位来进行分配(会随着N的增大而增大)。
R:实际装入的元素个数
N:当前使用的桶(Bucket)的个数(随着R的增大而增大)
LHT要时刻保证两条性质:
性质一: R/N <= P 也就是,每个桶的平均元素个数一定要小于预先设定的P,不符合时,要增加N
性质二:2^(E-1) <= N < 2^E
这里使用一个32位的哈希函数,对于每个Key,生成一个32位的哈希值
//
int hash(int x){//32位哈希函数
return x*2654435769;
}
LHT的插入:
假定LHT中的每个元素为(Hash,Key,Value) 一般情况下 Hash = hash(Key)
给定了(Key,Value)需要插入到哈希表中,第一步算出Hash=hash(Key);
然后,选择加入的桶的编号为currentHash(Hash); (桶的编号为0,1,2,...,N-1)
先来看看currentHash函数: 其中mask是掩码,mask[E]=2^E -1 即,低E位都是1,其余位都是零,跟Hash做按位异或,就取了Hash的低E 位。
//
int currentHash(int Hash){//当前哈希值
Hash=Hash&mask[E];
return Hash < N ? Hash : Hash&mask[E-1];
}
假设Hash写成二进制之后,低E位从高到低为a1,a2,...,aE (比如 20 = 1 0 1 0 0)
设 X = Hash & mask[E] ;即取了Hash的低E位。
那么,如果X小于N,那么就直接把该元素放进编号为X的桶中。
如果 N <= X < 2^E 那么,此时a1一定等于1,由于目前不存在编号为X的桶,
所以将a1置零,得到Y,即代码中的Hash&mask[E-1],将该元素放进编号为Y的桶中。
LHT的调整:
这样,就实现了将32位哈希值,均匀的放入N个桶中。
插入操作本身很简单,但是插入操作完成后,R会增加1,然后就有可能破坏前面提到的2个性质。
于是要调用调整函数,来维持两个性质:
性质一:R/N <= P
于是,插入之后,检查性质一有没有被破坏,如果有,就要增加N来让性质一仍然成立。
N增加1之后,首先性质二可能不再满足,若N>=2^E 则E也要加1,使得性质二满足.
令旧N为增加1之前的N。
除此之外,新增了一个编号为旧N的桶,假设 旧N的二进制表示为a1,a2,...,aE,那么a1一定是1。
注意到原本前E位哈希值为1,a2,a3,...,aE的元素,被放置进了编号为0,a2,...,aE的桶中,
但是实际上,这些元素应该放在编号为旧N的桶中。
于是,需要遍历编号为(0,a2,...,aE)的桶,拿回原本属于旧N桶的元素。
换句话说,就是对 编号为 旧N&mask[E-1] 的桶中的元素进行重新分配
查找就很简单了,直接根据哈希值找到对应的桶,然后在桶中搜索一遍有没有元素的Key等于给定的Key.
下面代码中,每个桶用链表来实现。
复杂度分析:
插入的复杂度分两部分:
1.插入操作,由于是无序链表,直接在表头插入即可,单词操作复杂度O(1)
2.调整操作,由R/N <= P 并且得到 N >= R/P 于是,N取R/P即可,开始时N=1,
于是,所有操作结束之后,N达到了R/P ,又因为每次调整操作N会加1,所以调整次数一共是R/P次。
所有操作结束之后,R=n(总个数),所以调整次数是 n/P
每次调整,需要遍历一个链表,平均复杂度是平均的链表长度,也就是P。
相乘得到 n/P*P = n ,所以,总共的调整操作是O(n)的,所以平均每次插入操作调整是O(1)的。
于是,插入操作的平均复杂度是O(1)的。
查找操作也需要遍历一个链表,平均需要访问P个元素,因为P是预先固定的常量,所以复杂度O(1)。
并且,P越小,查找操作就越快。
书上说,P的取值,一般为一个Block中可以存下的记录数量*0.8左右。因为数据库中,磁盘信息是按照Block来读取的,
所以要尽可能减少读取Block的次数。
这就实现了动态扩容的哈希表。
//
const double P=1.0;//平均每个桶装的元素个数的上限 ,实测貌似1.0效果比较好
int E;//目前使用了哈希值的前 E 位来分组
int R;//实际装入本哈希表的元素总数
int N;//目前使用的桶的个数
/*
操作过程中,始终维护两个性质
1. R/N <= P 可以推出 max(N) = max(R/P) = maxn/P 所以,所需链表的个数为 maxn/P
2. 2^(E-1) <= N < 2^E
*/
int p2[33];//记录2的各个次方 p2[i]=2^i
int mask[33]; //记录掩码 mask[i]=p2[i]-1
bool ERROR;//错误信息
//
int hash(int x){//32位哈希函数
return x*2654435769;
}
bool hashEq(int x,int y){//判断x与y在当前条件下属不属于一个桶
return (x&mask[E])==(y&mask[E]);
}
//
int currentHash(int Hash){//当前哈希值
Hash=Hash&mask[E];
return Hash < N ? Hash : Hash&mask[E-1];
}
struct ListNode{//链表节点定义
int Hash;//32位哈希值,根据Key计算,通常为 hash(Key)
int Key;//键值,唯一
int Value;//键值Key对应的值
ListNode *next;//指向链表中的下一节点,或者为空
//构造函数
ListNode(){}
ListNode(int H,int K,int V):Hash(H),Key(K),Value(V){}
};
struct List{//链表定义
ListNode *Head;//头指针
//构造函数 析构函数
List():Head(NULL){}
~List(){clear();}
//插入函数
void Insert(int H,int K,int V){
Insert(new ListNode(H,K,V));
}
void Insert(ListNode *temp){
temp->next=Head;
Head=temp;
}
//转移函数
void Transfer(int H,List *T){//将本链表中,Hash值掩码之后为H的元素加入到链表T中去。
ListNode *temp,*p;
while(Head && hashEq(Head->Hash,H)){
temp=Head;
Head=Head->next;
T->Insert(temp);
}
p=Head;
while(p&&p->next){
if(hashEq(p->next->Hash,H)){
temp=p->next;
p->next=p->next->next;
T->Insert(temp);
}
else p=p->next;
}
}
//寻找函数
int Find(int Key){
ERROR=false;
ListNode *temp=Head;
while(temp){
if(temp->Key==Key) return temp->Value;
temp=temp->next;
}
return ERROR=true;
}
//显示函数
void Show(){
ListNode *temp=Head;
while(temp){
printf("(%d,%d) ",temp->Key,temp->Value);
temp=temp->next;
}
}
//释放申请空间
void clear(){
while(Head){
ListNode *temp=Head;
Head=Head->next;
delete temp;
}
}
}L[100000];
//初始化
void Init(){
p2[0]=1;
for(int i=1;i<=32;++i) p2[i]=p2[i-1]<<1;
for(int i=0;i<=32;++i) mask[i]=p2[i]-1;
E=1;N=1;R=0;L[0]=List();
}
//调整
void Adjust(){
while((double)R/N > P){
//将属于N的信息加入List[N]
L[N&mask[E-1]].Transfer(N,&L[N]);
//更正 N 和 E
if(++N >= p2[E]) ++E;
L[N]=List();
}
}
//插入
void Insert(int Hash,int Key,int Value){
//插入元素
L[currentHash(Hash)].Insert(Hash,Key,Value);
++R;
//调整 N 和 E
Adjust();
}
//寻找
int Find(int Hash,int Key){
return L[currentHash(Hash)].Find(Key);
}
//释放所有
void FreeAll(){
for(int i=0;i
www.csdn.net