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

来源:互联网 发布:蜗居海藻知乎 编辑:程序博客网 时间:2024/06/05 02:37

在看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]-1bool 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<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

0 0