您的位置:首页 > 其它

散列表性质与实现

2014-08-26 15:30 218 查看
散列表(Hash table,也叫哈希表),是根据关键字(Key value)而直接访问在内存存储位置的数据结构。也就是说,它通过把键值通过一个函数的计算,映射到表中一个位置来访问记录,这加快了查找速度。这个映射函数称做散列函数,存放记录的数组称做散列表。

一个通俗的例子是,为了查找电话簿中某人的号码,可以创建一个按照人名首字母顺序排列的表(即建立人名x到首字母F(x)的一个函数关系),在首字母为W的表中查找“王”姓的电话号码,显然比直接查找就要快得多。这里使用人名作为关键字,“取首字母”是这个例子中散列函数的函数法则F(),存放首字母的表对应散列表。关键字和函数法则理论上可以任意确定。

散列表的基本概念

假设某应用要用到一个动态集合,其中每个元素都有一个属于[0..p]的关键字,此处p是一个不太大的数,且没有两个元素具有相同的关键字,则可以用一个数组[p+1]存储该动态集合,并且使用关键字作为数组下标进行直接寻址。这一直接寻址思想在前面的非比较排序中就有所应用。然而,当p很大并且实际要存储的动态集合大小n<<p时,这样一个数组将浪费大部分空间。

散列表(Hash table),α=n/m被定义为散列表的装载因子(n为关键字个数,m为散列表槽的个数)。在散列表中,具有关键字k的元素的下标为h(k),即利用散列函数h,根据关键字k计算出槽的位置。散列函数h将关键字域[0..p]映射到散列表[0..m-1]的槽位上,这里,m可以远小于p,从而缩小了需要处理的下标范围,并相应地降低了空间开销。散列表带来的问题是:两个关键字可能映射到同一个槽上,这种情形称为碰撞。因此,散列函数h应当将每个关键字等可能地散列到m个槽位的任何一个中去,并与其它关键字已被散列到哪一个槽位中无关(均匀散列函数),从而避免或者至少最小化碰撞。



散列函数



多数散列函数都假定关键字域为自然数集。如果所给关键字不是自然数,则必须有一种方法将它们解释为自然数。这里,介绍六种主要的散列函数:



解决碰撞的方法


为了知道碰撞产生的相同散列函数地址所对应的关键字,必须选用另外的散列函数,或者对碰撞结果进行处理。而不发生碰撞的可能性是非常之小的,所以通常对碰撞进行处理。解决碰撞的方法主要有两种:链接法(允许有多个元素散列到一个槽中,用链表链起来)和开放寻址法(连续的检查散列表直到找到放置它的槽)。
l 链接法(chaining):把散列到同一槽中的所有元素都存放在一个链表中。这样插入、删除、搜索操作都转换为List[h(x.key)]链表上的操作。每个槽中有一个指针,指向由所有散列到该槽的元素构成的链表的头。如果不存在这样的元素,则指针为空。在使用中比较喜欢链接法,实现过程可以参考“桶排序”代码。如果散列表中链表是双向的,则删除一个元素x的操作可以在O(1)时间内完成,但多了额外的前驱指针来存储前驱节点位置。
分析:最坏情况下,所有n个关键字都散列到同一个槽中,产生一个长度为n的链表。因此最坏情况下查找的时间O(n)。显然我们不是因为最坏情况性能差我们就不用它,在简单均匀散列(任何一个给定元素等可能的散列到m个槽中的任何一个,且与其他元素被散列到什么位置上无关)假设下,此时每个槽内元素为n/m个,此时期望查找时间为O(1+n/m),即先计算hash散列函数值找到表头,再对链表进行遍历。



l 开放寻址法(open
addressing):所有的元素都存放在散列表中。因此,适用于动态集合大小n不大于散列表大小的情况,即装载因子不超过1。否则,可能发生散列表溢出。在开放寻址中,当要插入一个元素时,可以连续地探查散列表的各项,直到找到一个空槽来放置待插入的关键字。



分析:使用开放寻址法,每个槽中至多只有一个元素,装载因子α=n/m小于等于1,如果符合均匀散列假设,则对于一次查找或插入,期望探测次数最多为1/(1-α)。直观上解释为无论如何都至少进行一次探查,如果碰撞则进行二次探查(需要进行二次探查概率大约为α),如果碰撞需要进行三次探查(概率大约为α^2)···


完全散列



如果某一种散列技术在进行查找时,其最坏情况内存访问次数为O(1)的话,则称其为完全散列(perfect
hashing)。通常利用一种两级的散列方案,每一级上都采用全域散列。为了确保在第二级上不出现碰撞,需要让第二级散列表Sj的大小mj为散列到槽j中的关键字数nj的平方。如果利用从某一全域散列函数类中随机选出的散列函数h,来将n个关键字存储到一个大小为m=n的散列表中,并将每个二次散列表的大小置为mj=nj2
(j=0, 1, …, m-1),则在一个完全散列方案中,表中出现冲突的概率小于0.5,存储所有二次散列表所需的存储总量的期望值小于2n(预期使用总体存储空间限制为O(n)),大于等于4n(超过线性)的概率小于0.5。



总结



C++实现
1.散列表hash函数采用类Java的hashMap原理实现:直接返回x&(hashTable.length-1),初始设定length=16;

2.维持散列表装载因子(元素个数/槽个数)<=0.75,如果大于0.75,则将槽大小翻倍,并重新建立散列表。

3.碰撞后处理的方法采用链接法,直接用链表链起来。

insert函数:插入一个Data类型元素

find函数:成功返回查找到的Data类型元素,未找到返回Data(ID=-1···)的元素

delete函数:在hash表中删除元素,成功返回true。

printHashTable函数:直接打印出所建立的散列表。

/*
链接法实现一个hash类
hash函数使用简单的取余方法
*/
struct Data{
Data(int ID , string name):ID(ID),name(name){}
Data(){}
~Data(){}
int ID;//唯一ID
string name;
friend ostream& operator<<(ostream&out,Data record){//重载输出运算符
out<<" ["<<record.ID<<" "<<record.name<<"] ";
return out;
}
};
class HashTable{
struct List{//链表节点类
List():pNext(NULL){}
~List(){}
List *pNext;
Data data;
};

private:
List *slot;//槽是一个链表数组
int SlotLen;//槽长度
int dataNum;//hash表中含有的数据个数
double loadFactor;//装载因子
void reCreateHash();//当装载因子大于0.75,将槽扩展为原来的两倍,重建hash表
int hash(int x);//hash函数

public:
HashTable();
HashTable(int num[], int numLen);
~HashTable();
Data find(int x);//在hash表中寻找ID号为x的元素,找到了返回name,没找到返回空
void insert(Data x);//在hash表中插入节点x
bool deleteData(Data x);//在hash表中删除节点x,成功返回true
void printHashTable();//打印hashTable
void test();//插入电话号码测试
};
HashTable::~HashTable()
{
delete []slot;
}
int HashTable::hash(int x)
{
//---方法一:采用最简单的取余方法
//return x%SlotLen;
//---方法二:仿照Java中的实现x&(slotLen-1),此时槽起始位16,装载因子超过0.75扩大为2倍
return x&(SlotLen-1);
}
HashTable::HashTable()
{
SlotLen = 16;//初始槽数为16
dataNum = 0;
loadFactor = 0.0;
slot = new List[SlotLen];
}
void HashTable::printHashTable()
{
for(int i=0 ; i<SlotLen ; i++){
List *pHead = &slot[i];
if(pHead->pNext == NULL){
cout<<i<<"  --  "<<endl;
continue;
}
cout<<i<<"  --  ";
pHead = pHead->pNext;//跳到第一个有效数据节点
while(pHead){
cout<<pHead->data<<" ";
pHead = pHead->pNext;
}
cout<<endl;
}
}
void HashTable::reCreateHash()
{
if(loadFactor > 0.75)
{
Data *temp = new Data[dataNum];//存储扫描到的数据
int k=0;
for(int i=0 ; i<SlotLen ; i++){//先将原数据扫描下来
List *pFirst = slot[i].pNext;//第一个有效元素节点
if(pFirst == NULL)
continue;
while(pFirst){
temp[k++] = pFirst->data;
pFirst = pFirst->pNext;
}
}
//---删除原hash表
delete []slot;
SlotLen = SlotLen<<1;//槽数扩大为原来的两倍
slot = new List[SlotLen];
loadFactor = (double)dataNum/SlotLen;//更新装载因子
dataNum = 0;//更新数据计数
//---重建hash表
for(int i=0 ; i<k ; i++){
insert(temp[i]);
}
delete []temp;
}
}
void HashTable::insert(Data x)
{
//---如果装载因子大于0.75重新建立hash表
if(loadFactor > 0.75)
reCreateHash();
//---将数据插入hash表中
int slotIndex = hash(x.ID);//以数据的ID作为hash依据
List *temp = new List;
temp->data = x;
List *pHead = &slot[slotIndex];
while (pHead->pNext)//定位最后一个节点
pHead = pHead->pNext;
pHead->pNext = temp;

dataNum++;
loadFactor = (double)dataNum/SlotLen;
}
bool HashTable::deleteData(Data x)
{
int slotIndex = hash(x.ID);
List*pHead = slot[slotIndex].pNext;
if(pHead == NULL){
return false;
}
List *pPre = pHead;
while(pHead){
if(pHead->data.ID==x.ID && pHead->data.name==x.name){
pPre->pNext = pHead->pNext;
delete pHead;
return true;
}
pPre = pHead;
pHead = pHead->pNext;
}
return false;
}
Data HashTable::find(int x)
{
int slotIndex = hash(x);
List*pHead = slot[slotIndex].pNext;
if(pHead == NULL){
return Data(-1,NULL);
}
while(pHead){
if(pHead->data.ID == x)
return pHead->data;
pHead = pHead->pNext;
}
return Data(-1,NULL);
}
//插入电话号码测试
void HashTable::test()
{
insert(Data(10010,"中国联通"));
insert(Data(10086,"中国移动"));
insert(Data(10000,"中国电信"));
insert(Data(10010,"中国移动"));
insert(Data(13312,"哆啦A梦"));
insert(Data(12222,"大雄"));
insert(Data(14444,"静香"));
insert(Data(15555,"小夫"));
insert(Data(13025,"胖虎"));
insert(Data(13325,"太一"));
insert(Data(13200,"嘉和"));
insert(Data(13318,"美美"));
insert(Data(13698,"漩涡鸣人"));
insert(Data(18956,"佐助"));
insert(Data(30000,"第四代火影"));
insert(Data(40012,"莉娜因巴斯"));
insert(Data(13845,"黑猫警长"));
}
int main()
{
HashTable h;
h.test();
h.printHashTable();
cout<<endl<<endl;
h.deleteData(Data(18956,"佐助"));
h.printHashTable();
return 0;
}



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