您的位置:首页 > 理论基础 > 数据结构算法

数据结构分析之线性哈希表(Linear Hash Tables)

2015-09-24 18:47 337 查看
在看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位的哈希值

<span style="font-size:14px;">//
int hash(int x){//32位哈希函数
return x*2654435769;
}</span>


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 位。

<span style="font-size:14px;">//
int currentHash(int Hash){//当前哈希值
Hash=Hash&mask[E];
return Hash < N ? Hash : Hash&mask[E-1];
}</span>
假设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

L[N&mask[E-1]].Transfer(N,&L
);
//更正 N 和 E
if(++N >= p2[E])	 ++E;
L
=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<N;++i)	L[i].clear();
}
//显示
void ShowList(){
OUT3(E,R,N);
for(int i=0;i<N;++i){
printf("%d:",i);
L[i].Show();
printf("\n");
}
}
/*
使用上述模板需要知道的外部函数:
void Init() :初始化  在所有操作之前运行
void FreeAll():全部释放  在所有操作之后运行
void Insert(Hash,Key,Value):加入元素,这里的Hash是32位Hash ,一般取Hash=hash(Key)
int Find(Hash,Key):找到键值Key对应的Value
调用Find之后,若全局变量ERROR为true 则表示没有找到,此时返回值无效,否则返回值为Value
void ShowList():显示所有桶的元素
*/


www.csdn.net
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: