【数据结构与算法分析】第五章 散列
散列是一种用于以常数平均时间执行插入、删除和查找技术。
1.散列函数
每个关键字被映射到从0到TableSize-1这个范围中的某个数,并放到适当的单元中。(TableSize为散列表的大小,表的大小为素数)
一种简单的散列函数:把字符串的ASCII码值加起来
typedef unsigned int Index;
Index Hsh(const char *key,int TableSize)
{
unsigned int HashVal=0;
while(*key!=0)
HashVal+=*key++;
return HashVal % TableSize;
}
一种好的散列函数:Horner法则
Index Hash(const char *key,int TableSize)
{
unsigned int HashVal=0;
while(*key!=0)
HashVal=(HashVal<<5)+*key++;//向左移动5位,即乘以32
return HashVal % TableSize;
}
如果当一个元素被插入时另一个元素已经存在(散列值相同),那么就产生一个冲突,消除冲突的方法:分离链接法和开放定址法
2.分离链接法
将散列到同一值得所有元素保留到一个表中。这些表都有表头。
例:散列表大小为10,散列函数就是Hash(key,10)= key % 10,每个散列值均有一个表,如key=16,则散列值为6,key=36,散列值为6,他们的处于同一链表中

分离链接散列表的类型声明:
#ifndef _Hash_H
struct ListNode;
typedef struct ListNode *Position;
struct HashTb;
typedef struct HashTb *HashTable;
HashTable InitializeTable(int TableSize);
void DestroyTable(HashTable H);
Position Find(ElementType Key,HashTable H);
void Insert(ElementType Key,HashTable H);
ElementType Retrieve(Position P);
#endef
struct ListNode
{
ElementType Element;
Position Next;
}
typedef Position List;
struct HashTb
{
int TableSize;
List *TheLists;
}
分离链接散列表的初始化例程:
HashTable InitializeTable(int TableSize)
{
HashTable H;
int i;
if(TableSize < MinTableSize)
{
Error("Table size too small");
return NULL;
}
H=malloc(sizeof(struct HashTb));//分配列散表空间
if(H=NULL)
FatalError("out of space");
H->TableSize=NextPrime(TableSize);//设置表的大小为一个素数
H->TheLists=malloc(sizeof(List)*H->TableSize);//分配链表的空间
if(H->TheLists==NULL)
FatalError("out of space");
for(i=0;i<H->TableSize;i++)//分配每个链表的表头空间
{
H->TheLists[i] = malloc(sizeof(struct ListNode));
if(H->TheLists[i]==NULL)
FatalError("out of space");
else
H->TheLists[i]->Next = NULL;
}
return H;
}
分离链接散列表的Find例程:
Position Find(ElementType Key,HashTable H)
{
Posion P;
List L;
L=H->TheLists[Hash(key,H->TableSize)];//散列值相同的表,L为表头
P=L->Next;//表的第二个元素的位置
while(P!=NULL&&P->Element!=key)//如果有第二个元素
P=P->Next;
return P;//返回P的值,为NULL则无重复,否则重复
}
分离链接散列表的Insert例程:
void Insert(ElementType Key,HashTable H)
{
Position Pos,NewCell;
List L;
Pos = Find(Key,H);//查看是否存在散列值
if(Pos==NULL)//散列值不存在
{
NewCell=malloc(sizeof(struct ListNode));//分配链表中一个元素的内存
if(NewCell==NULL)
FatalError("out of space");
else
{
L=H->TheLists[Hash(Key,H->TableSize)];//散列表下的一个链表,L为表头
NewCell->Next=L->Next;//
L->Next=NewCell;//向链表中插入一个值
NewCell->Element=Key;//向前插入,插入链表中的最前位置
}
}
}
插入如图所示:插入值为56
3.开放定址法
定义散列表的装填因子为散列表中的元素个数与散列表大小的比值,分离链接散列表的装填因子为1.
开放定址散列法不需要使用链表,所需要的表较大,其装填因子低于0.5,三个开放定址的冲突解决方法:线性探测法,平方探测法,双散列
开放定址散列表的类型声明:
#ifndef _HashQuad_H
typedef unsigned int Index;
typedef Index Position;
struct HashTb;
typedef struct HashTb *HashTable;
HashTable InitializeTable(int TableSize);
void DestroyTable(HashTable H);
Position Find(ElementType Key,HashTable H);
void Insert(ElementType Key,HashTable H);
ElementType Retrieve(Position P,HashTable H);
HashTable Rehash(HashTable H);
#endef
enum KindOfEntry{Legitimate,Empty,file};
struct HashEntry
{
ElementType Element;
enum KindOfEntry Info;
};
typedef struct HashEntry cell;
struct HashTb
{
int TableSize;
Cell *TheCells;
}
初始化开放定址散列表:
HashTable InitializeTable(int TableSize)
{
HashTable H;
int i;
if(TableSize < MinTableSize)
{
Error("tablesize too small");
return NULL;
}
H=malloc(sizeof(struct HashTb));//为散列表分配内存
if(H==NULL)
FatalError("out of space");
H->TableSize=NextPrime(TableSize);//设置散列表的大小为一个素数
H->TheCells=malloc(sizeof(Cell)*H->TableSize);//分配散列表的表项单元的数组
if(H->TheCell==NULL)
FatalError("out of space");
for(i=0;i<H->TableSize;i++)//将每个表项单元的数组设置为Empty
H->TheCells[i].Info=Empty;
return H;
}
3.1线性探测法
无冲突时Hash(X)=X mod TableSize
有冲突时,F(i)=i,F为冲突解决方法
例:将关键字{89,18,49,58,69}插入到一个散列表中

插入89时,无冲突,正常插入
插入18时,无冲突,正常插入
插入49时,与89产生一个冲突,F(1)=1,放入9下一个空闲位置,即地址0
插入58时,与18产生冲突,往下一个地址移动依次与89,49冲突,然后找到空地址,即地址1
插入69时,与89产生冲突,依次向后一个一个找空位置,直到找到地址2
3.2平方探测法
无冲突时Hash(X)=X mod TableSize
有冲突时,F(i)=i*i,F为冲突解决方法
例:将关键字{89,18,49,58,69}插入到一个散列表中

插入89时,无冲突,正常插入
插入18时,无冲突,正常插入
插入49时,与89产生一个冲突,F(1)=1,放入9下一个空闲位置,即地址0
插入58时,与18产生冲突,F(1)=1,探测相邻的单元,发生冲突,F(2)=2*2=4,探测18后的第四个位置,即位置2,找到空地址
插入69时,与89产生冲突,F(1)=1,探测相邻的单元,发生冲突,F(2)=2*2=4,探测89后的第四个位置,即位置3,找到空地址
使用平方探测散列法的Find例程:
Position Find(Element key,HashTable H)
{
Position CollitionPos;
int CollitionNum;
CollitionNum=0;
CollitionPos=Hash(key,H->TableSize);//返回散列值
while(H->TheCells[CollitionPos].Info!=Empty&&H->TheCells[CollitionPos].Element!=key)//如果冲突,1次:CollitionPos=1,2次CollitionPos=4,3次:CollitionPos=9...
{
CollitionPos +=2 * ++CollitionNum - 1;//公式:F(i)=F(i-1)+2i-1
if(CollitionPos>=H->TableSize)
CollitionPos-=H->TableSize;
}
return CollitionPos;//返回key应插入的位置
}
使用平方探测散列法插入:
void Insert(ElementType key,HashTable H)
{
Position Pos;
Pos=Find(key,H);//Pos为应该插入的位置
if(H->TheCells[Pos].Info!=Legitimate)//合法
{
H->TheCells[Pos].Info=Legistimate;
H->TheCells[Pos].Element=key;
}
}
3.3双散列
无冲突时Hash(X)=X mod TableSize
有冲突时,F(i)=i*hash2(X),hash2(X)=R-(X mod R),R为小于TableSize的素数,产生第一个冲突时,F(1)=hash2(X),产生第二个冲突时,F(2)=2hash2(X)...
例:将关键字{89,18,49,58,69}插入到一个散列表中,选择R为7

插入89时,无冲突,正常插入
插入18时,无冲突,正常插入
插入49时,与89冲突,hash2(49)=7-(49 % 7)=7-0=7,探测89后的第7个位置,即位置6,检测位置为空,则将49插入
插入58时,与18冲突,hash2(58)=7-2=5,探测18后的第5个位置,即位置3,检测到位置为空,则将58插入
插入69时,与89冲突,hash2(69)=7-6=1,探测89后的第一个位置,即位置0,检测到位置为空,则将69插入
插入60时,与69冲突,hash2(60)=7-4=3,探测69后的第3个位置,即位置位置3,检测到冲突,2hash(2)=6,探测69后的第6个位置,即位置6,产生冲突,3hash2(60)=9,检测到冲突,然后检测为2
4.再散列
建立另一个大约两倍大的表(而且使用一个相关的新散列函数),扫描整个原始散列表,计算每个元素的新散列值并将其插入到新表中。
例:将13、15、24和6插入大小为7的开放定址散列表中,使用线性探测法解决冲突,插入结果如图所示

若将23插入表中,如图

插入23后的表将有超过70%的单元是满的,表填的过满,所有我们将建立一个新的表,取表的大小为17(原表两倍后的第一个素数),新的散列函数为hash(X)=X mod 17,扫描原表,将元素6,15,23,24,13插入新表中.

本文深入探讨了散列技术,包括散列函数的设计原则及其在分离链接法和开放定址法中的应用。通过具体实例,详细介绍了线性探测、平方探测及双散列等冲突解决方法。

1257

被折叠的 条评论
为什么被折叠?



