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

在看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[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

猜你喜欢

转载自blog.csdn.net/u012891242/article/details/48712443