散列表查找
2013-09-21 19:49
288 查看
哈希表(hash)又称散列表,是除顺序表存储结构、链表存储结构和索引表存储结构之外的又一种存储线性表的存储结构。设要存储的对象为n个,在内存中长度为m的连续存储单元,对象的关键字key为索引,通过hash函数hash(key)映射到相应的存储空间存储。
hash的两个主要问题:(1)设计好的hash函数;(2)设计好的冲突解决办法。
散列函数的构造方法
那么怎么样才是好的hash函数,一句话,保证散列之后的地址呈均匀分布,不要产生冲突,不要产生堆积。
(1)直接定址法:f(key) = a*key+b,这种定址法由于是线性的,不存在hash冲突。但是我们需要事先知道关键字的分布情况,适合关键字范围较小且几乎连续的情况。
(2)数字分析法:如果我们的关键字是位数较多的数字,而且某几位固定,例如我们的电话号码。那么我们就可以分析数字的规律,取不固定的且分布均匀的那几位做hash。这里用到了一个抽取的方法,这也是在计算hash中经常用到的。这种方法适合关键字的某几位分布均匀,而某几位不均匀。
(3)平方取中法:方法很简单,假设关键字是1234,平方后为1522756,我们可以抽取中间的三位数作为hash,即227。平方取中法适合不知道关键字的分布情况,而位数又不大。
(4)折叠法:将关键字从左到右分割成位数相等的部分然后求和,例如9876543210,我们可以987+654+321+0=1962,最后去962作为hash值。折叠法需要不知道关键字分布情况,适合关键字位数较多的情况。
(5)除留余数法:f(key)=key mod p;根据经验,若散列表的长度为m,通常p取小于或等于表长的最小质数比较好。
(6)随机数法:选择一个随机数,取关键字的随机函数值为它的散列地址。f(key)=rangdom(key),random是随机函数。
想起了一个例子:求一个单词的兄弟单词,我们可以a,b,c.....赋值为一个素数,然后key(abc) = 2*3*5 = 30, key(bac) = 30,然后通过 key%m存到相应的链表中去,这里用链地址法。
Hash冲突的影响因素
(1)与装填因子a有关,装填因子定义为已存入的记录n与hash地址空间m的比值:a = n/m,a是介于0和1之间的。a越接近1,说明你的hash表装得越满,空间利用率越高,但是冲突可能性更大。a越小,空间越空,发生冲突可能性越低,但是空间利用低。
(2)采用的hash函数,选择合适的hash函数,使你的hash地址呈均匀分布。
(3)好的冲突解法方法。
Hash冲突解决办法
(1)开放定址法:在开放定址方法中,用到四类方法:
线性探测法,这种方法只是向后探测,容易堆积,有时候可能前面位置很空。
平方探测法,左右探测,但是每次不是加减1,而是按照1,4,9...这样来加减,我们可以推导一下,不用求平方而是每次加2*i-1或者减2*i-1.
二次hash,双hash,hash冲突利用另一个hash函数继续hash。最终的表达式可以表述:p = (hash1(key) + hash2(key)) %m,注意这里是冲突了采用第二个hash2函数哈。
随机探测法,在冲突时,我们用一个随机数去定位,p = (hash(key)+rand())%m,注意这里的随机数是伪随机数,因为我们在查找的时候要对应起来。
(2)链地址法:此时每个关键字保存的是一个链表。在标准库里面的hash_set、hash_map、hash_multiset、hash_multimap就是用到了这个方法。
(3)公共溢出区法:将冲突的hash放在一个专门放冲突的区域,当查找的时候,如果关键字不匹配,说明冲突,去溢出区找。
Hash表实现
hash表实现包括建表,查找,删除,插入操作。注意我们这里要用到懒惰的删除方法,就是不能真正的删除一个元素,而是标记为删除。因为真正删除的话会影响到hash表的查找操作,进而影响插入操作。
运行结果:
注意:当你在插入操作时,发现你的hash表满了,不应该是不处理,而是生成一个更大的空间,生成更大的空间以后,你所有的记录都要重新按照新的size去hash。而且这里也不是简单的除以一个size,应该是除以一个素数,那么你就应该去找这个素数。这些上面程序都没有考虑进去,只是一个demo。
Hash性能分析
(1)平均成功查找长度
(2)不成功平均查找长度
hash的两个主要问题:(1)设计好的hash函数;(2)设计好的冲突解决办法。
散列函数的构造方法
那么怎么样才是好的hash函数,一句话,保证散列之后的地址呈均匀分布,不要产生冲突,不要产生堆积。
(1)直接定址法:f(key) = a*key+b,这种定址法由于是线性的,不存在hash冲突。但是我们需要事先知道关键字的分布情况,适合关键字范围较小且几乎连续的情况。
(2)数字分析法:如果我们的关键字是位数较多的数字,而且某几位固定,例如我们的电话号码。那么我们就可以分析数字的规律,取不固定的且分布均匀的那几位做hash。这里用到了一个抽取的方法,这也是在计算hash中经常用到的。这种方法适合关键字的某几位分布均匀,而某几位不均匀。
(3)平方取中法:方法很简单,假设关键字是1234,平方后为1522756,我们可以抽取中间的三位数作为hash,即227。平方取中法适合不知道关键字的分布情况,而位数又不大。
(4)折叠法:将关键字从左到右分割成位数相等的部分然后求和,例如9876543210,我们可以987+654+321+0=1962,最后去962作为hash值。折叠法需要不知道关键字分布情况,适合关键字位数较多的情况。
(5)除留余数法:f(key)=key mod p;根据经验,若散列表的长度为m,通常p取小于或等于表长的最小质数比较好。
(6)随机数法:选择一个随机数,取关键字的随机函数值为它的散列地址。f(key)=rangdom(key),random是随机函数。
想起了一个例子:求一个单词的兄弟单词,我们可以a,b,c.....赋值为一个素数,然后key(abc) = 2*3*5 = 30, key(bac) = 30,然后通过 key%m存到相应的链表中去,这里用链地址法。
Hash冲突的影响因素
(1)与装填因子a有关,装填因子定义为已存入的记录n与hash地址空间m的比值:a = n/m,a是介于0和1之间的。a越接近1,说明你的hash表装得越满,空间利用率越高,但是冲突可能性更大。a越小,空间越空,发生冲突可能性越低,但是空间利用低。
(2)采用的hash函数,选择合适的hash函数,使你的hash地址呈均匀分布。
(3)好的冲突解法方法。
Hash冲突解决办法
(1)开放定址法:在开放定址方法中,用到四类方法:
线性探测法,这种方法只是向后探测,容易堆积,有时候可能前面位置很空。
平方探测法,左右探测,但是每次不是加减1,而是按照1,4,9...这样来加减,我们可以推导一下,不用求平方而是每次加2*i-1或者减2*i-1.
二次hash,双hash,hash冲突利用另一个hash函数继续hash。最终的表达式可以表述:p = (hash1(key) + hash2(key)) %m,注意这里是冲突了采用第二个hash2函数哈。
随机探测法,在冲突时,我们用一个随机数去定位,p = (hash(key)+rand())%m,注意这里的随机数是伪随机数,因为我们在查找的时候要对应起来。
(2)链地址法:此时每个关键字保存的是一个链表。在标准库里面的hash_set、hash_map、hash_multiset、hash_multimap就是用到了这个方法。
(3)公共溢出区法:将冲突的hash放在一个专门放冲突的区域,当查找的时候,如果关键字不匹配,说明冲突,去溢出区找。
Hash表实现
hash表实现包括建表,查找,删除,插入操作。注意我们这里要用到懒惰的删除方法,就是不能真正的删除一个元素,而是标记为删除。因为真正删除的话会影响到hash表的查找操作,进而影响插入操作。
#include <iostream> #include <cassert> using namespace std; enum State{Null,Delete,Occupy}; struct HashNode{ State flag; int key; HashNode(int k=0,State f=Null):flag(f),key(k){} }; class HashTable{ friend ostream& operator<<(ostream &out,const HashTable& h) { for(int i=0; i<h.size; ++i){ out<<"("<<h.hash[i].key<<","; switch(h.hash[i].flag){ case 0 :out<<"NULL"<<")";break; case 1 :out<<"Del"<<")";break; default:out<<"Occupy"<<")";break; } } return out; } public: HashTable():size(0),hash(NULL){} HashTable(int size){ this->size = size; hash = new HashNode[size]; assert(hash); } //这里省略复制构造函数和operator=重载 ~HashTable() { if(!hash)delete[] hash; } bool InsertNode(int key) { HashNode newNode(key,Occupy); int begin = key % size;//计算地址 int i = begin; while(hash[i].flag!=Null || hash[i].flag==Delete){ i = (i + 1)%size;//线性探测 if(i == begin) return false;//说明hash表是满的,这里我们不做处理,处理的话可以申请2倍空间 } hash[i] = newNode; return true; } bool SearchNode(int key)//这里应该要返回索引的 { int begin = key % size; int i = begin; while(hash[i].flag!=Occupy && hash[i].key!= key){ i = (i+1)%size; if(i == begin)return false; } if(hash[i].flag==Occupy && hash[i].key== key) return true; else return false; } bool DeleteNode(int key) { int begin = key % size; int i = begin; while(hash[i].flag!=Occupy && hash[i].key!= key){ i = (i+1)%size; if(i == begin)return false; } hash[i].flag = Delete; return true; } private: HashNode *hash; int size; }; int main() { HashTable myHash(6); cout<<"---------Hash-----------"<<endl; myHash.InsertNode(15); myHash.InsertNode(21); myHash.InsertNode(17); myHash.InsertNode(18); cout<<myHash<<endl; cout<<"---------DeleteNode------"<<endl; myHash.DeleteNode(15); cout<<myHash<<endl; cout<<"---------FindNode--------"<<endl; if(myHash.SearchNode(21)) cout<<"find key="<<21<<endl; system("pause"); return 0; }
运行结果:
注意:当你在插入操作时,发现你的hash表满了,不应该是不处理,而是生成一个更大的空间,生成更大的空间以后,你所有的记录都要重新按照新的size去hash。而且这里也不是简单的除以一个size,应该是除以一个素数,那么你就应该去找这个素数。这些上面程序都没有考虑进去,只是一个demo。
Hash性能分析
(1)平均成功查找长度
(2)不成功平均查找长度
相关文章推荐
- 储存每一个单词W以及W的所有前缀,特定方的方向执行一次扫描的时候,如果被查找的单词作为前缀不在散列表中,那么在这个方向上可以及早终止
- 数据结构——线性表——散列存储结构——哈希表知识点总结 原创 2017年05月14日 10:08:40 散列(hashing)是一种重要的存储方法,也是一种常见的查找方法。 基本思想:以结点的
- 散列表的查找代码实现
- 散列(Hash)建立和查找(面试常考)
- 闭散列表的查找、插入和删除操作的完整C代码
- 散列查找的查找插入及冲突处理方法
- 查找----散列查找
- 数据结构课设(散列表的设计与实现---电话号码查找系统)
- 散列查找--解决冲突的方法
- 散列查找
- 散列表(二)冲突处理的方法之链地址法的实现: 哈希查找
- 散列表的查找
- 查找----深入探索散列查找
- 开散列表的查找、插入、删除操作的完整C代码
- 数据结构 学习笔记(完):散列查找:散列(哈希)表,散列函数的构造,冲突处理,性能分析
- 散列表查找(哈希表)的基本操作 (完整代码)
- 【数据结构----笔记2】查找算法之【哈希查找或散列查找】
- 查找五:散列表查找
- 散列表查找概述
- 散列表查找的性能分析