散列表性质与实现
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函数:直接打印出所建立的散列表。
一个通俗的例子是,为了查找电话簿中某人的号码,可以创建一个按照人名首字母顺序排列的表(即建立人名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; }
相关文章推荐
- 分离链接散列表实现文件C语言
- Java中利用散列表实现股票行情的查询
- 散列表(哈希表)各种方法实现
- STL学习】自己动手C++编程实现hash table(散列表)
- 利用散列表实现的字谜游戏
- 二叉搜索树的性质与实现
- 散列表(二):冲突处理的方法之链地址法的实现(哈希查找)
- 哈希表(散列表)简单实现
- 散列表实现(平方探测法)
- Java中利用散列表实现股票行情的查询
- 图论dijkstra Bellman_Ford与Floyd算法的性质比较与实现
- [C++]数据结构:散列表HashTable的实现与简单应用
- 【STL学习】自己动手C++编程实现hash table(散列表)
- 二叉树的性质及实现代码 !!!!
- HashTable(散列表)的实现代码及测试代码
- 用异或的性质实现简单加密解密
- 用异或的性质实现简单加密解密
- 散列表(三):冲突处理的方法之开地址法(线性探测再散列的实现)
- HashSet和HashMap的底层实现——哈希表、散列表
- Java中链表、堆栈、队列、二叉树、散列表等数据结构的实现