《数据结构》学习-- Hash(3) --Open Addressing

前端之家收集整理的这篇文章主要介绍了《数据结构》学习-- Hash(3) --Open Addressing前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

1. 回顾

上一次我们讲了Hash冲突解决方案之开散列(Separate Chaining)。其优点是思路简单,实现也容易。这一回我们介绍另一种Hash冲突解决方案,名为闭散列法,或叫Open Addressing
你可能觉得闭散列和Open有些矛盾。其实,看了Open Addressing的核心思想后,你就明白了。

2. Open Addressing核心思想

Open Addressing思想非常简单。如果第一次Hash寻找到得位置失败,那就不断进行位移,直到找到满足条件的位置。
即:我们不断尝试h0(x),h1(x),h2(x)...这些位置。其中:hi(x) = (Hash(x) + Function(i)) % tableSize。其中Function(0) = 0.

2.1 Linear Open Addressing

顾名思义,就是Function(i) = i。也就是说,如果第一次Hash寻找位置失败,那么就顺序找下去,直到找到一个满足要求的位置为止。
优点:思路简单,而且只要Hash表不满,总能找到满足条件的位置。
缺点:容易产生主聚合效应(primary clustering)。简单来说,就是插入的点容易聚集到一块地方,从而使得第一次Hash到这块范围的数都必须顺序搜索这块范围。根据复杂的计算,我们可以得到,当load factor(此概念在上一章介绍)为0.5时,平均每次插入(等同于非成功寻找)需要位移2.5次,平均每次成功寻找需要位移1.5次。将load factor保证在0.5以下,那么时间是比较理想的。

2.2 Quadratic Open Addressing

顾名思义,就是Function(i) = i^2。简单地计算可以得到:h(i+1)(x) = hi(x) + 2i -1. 另外,只有当load factor小于0.5且Hash表大小为质数时,才能保证每次插入都成功(可以证明,这里略)。
优点:不会产生主聚合效应。
缺点:虽然Quadratic方法不会产生主聚合效应。但会产生次聚合效应(secondary clustering)。即,第一次Hash到同一个位置的点,他们之后的搜索过程都完全一样,需要重复。

3. 延迟删除(lazy deletion)

如果我们需要删除一个值,不能简单的把那个位置的值去掉。简单思索便可明白,因为这个点后面的值可能是通过位移过去的,如果这点被挖空,那么我们想寻找后面的值就变得不可能了。
因此,我们使用一个延迟删除的技术。思想很简单,我们给每个点赋予一个状态,分别是被占用(legitimate),空(empty),被删除(deleted)。初始时所有点都为空,当被插入一个值时将状态设为被占用,但被删除时状态设为被删除。这样的话,如果我们要寻找一个点,只要搜索路径上的点非空,且其值与我们想要搜索的值不同,那么就不断搜索下去,直到找到空点或者相同值得点。(如果觉得拗口,请看下面的代码)。

4. Open Addressing实现

4.1 基本数据结构

  1. enum Kind {LEGITIMATE,EMPTY,DELETED};
  2. struct HashNode{
  3. ElementType elementValue;
  4. enum Kind kind;
  5. };
  6. struct HashTbl{
  7. int tableSize;
  8. int content;
  9. HashNode* table;
  10. };
  11. HashTbl* hashTable;

4.2 初始化

  1. template<class elementtype="">
  2. void HashTable<elementtype>::initialize(HashTbl*& newHashTable,int minSize)
  3. {
  4. int tableSize=nextPrime(minSize);//寻找下一个比minSize大的质数
  5.  
  6. try{
  7. newHashTable=new HashTbl;
  8. }catch(std::bad_alloc&){
  9. errorDisplay("new memory Failed!",__FILE__,__FUNCTION__,__LINE__);//如果new失败,报错
  10. }
  11.  
  12. try{
  13. newHashTable->table=new HashNode[tableSize];
  14. }catch(std::bad_alloc&){
  15. errorDisplay("new memory Failed!",__LINE__);//如果new失败,报错
  16. }
  17.  
  18. for(int i=0;i<tablesize i="" newhashtable-="">table[i].kind=EMPTY;
  19. }
  20.  
  21. newHashTable->tableSize=tableSize;
  22. newHashTable->content=0;
  23. }
  24. </tablesize></elementtype></class>

4.3 寻找Find

Find函数可以说是Open Addressing的关键。
  1. template<class elementtype="">
  2. int HashTable<elementtype>::findInner(HashTbl* _hashTable,ElementType& elementValue)
  3. {
  4. int key=getElementKey(elementValue)%_hashTable->tableSize; //第一次Hash,getElementKey是根据输入数据获得一个初始key值,详细可参考上一章
  5. int hashTimes=0;
  6. while(_hashTable->table[key].kind!=EMPTY && _hashTable->table[key].elementValue!=elementValue){
  7. key=hash2(key,hashTimes)%_hashTable->tableSize; //hash2就是上面所提到的Function,具体见下面
  8. }
  9. return key;
  10. }
  11.  
  12. template<class elementtype="">
  13. bool HashTable<elementtype>::find(ElementType elementValue)
  14. {
  15. int pos=findInner(hashTable,elementValue);
  16. return hashTable->table[pos].kind==LEGITIMATE;
  17. }
  18.  
  19. template<class elementtype="">
  20. int HashTable<elementtype>::hash2(int key,int hashTimes)
  21. {
  22. switch(OPEN_ADDRESS){ //根据不同的Open Addressing方法,选择不同的位移方式
  23. case LINEAR:
  24. return key+hashTimes;
  25. case QUDRATIC:
  26. return key+2*(hashTimes+1)-1;
  27. default:
  28. errorDisplay("OPEN_ADDRESS method error!",__LINE__);
  29. return -1;
  30. }
  31. }
  32. </elementtype></class></elementtype></class></elementtype></class>

4.4 插入Insertion

  1. template<class elementtype="">
  2. bool HashTable<elementtype>::insertInner(HashTbl*& _hashTable,ElementType& elementValue)
  3. {
  4. //rehash
  5. if(loadFactor>MAX_LOAD_FACTOR){ //MAX_LOAD_FACTOR一般取0.5
  6. _hashTable=rehash(_hashTable->tableSize); //rehash的概念在上一章讲过
  7. }
  8. int pos=findInner(_hashTable,elementValue);
  9.  
  10. HashNode& hashNode=_hashTable->table[pos];
  11. if(hashNode.kind==LEGITIMATE) //该值已经存在,无需插入
  12. return false;
  13. else{ //该值不存在,或者已被删除
  14. hashNode.elementValue=elementValue;
  15. hashNode.kind=LEGITIMATE;
  16. _hashTable->content++;
  17. loadFactor=(double)(_hashTable->content)/(double)(_hashTable->tableSize);
  18. return true;
  19. }
  20. }
  21.  
  22. template<class elementtype="">
  23. bool HashTable<elementtype>::insert(ElementType elementValue)
  24. {
  25. return insertInner(hashTable,elementValue);
  26. }
  27. </elementtype></class></elementtype></class>

4.5 删除Remove

  1. template<class elementtype="">
  2. bool HashTable<elementtype>::removeInner(HashTbl* _hashTable,ElementType& elementValue)
  3. {
  4. int pos=findInner(_hashTable,elementValue);
  5. HashNode& hashNode=_hashTable->table[pos];
  6. if(hashNode.kind==LEGITIMATE){ //这个点存在
  7. hashNode.kind=DELETED;
  8. _hashTable->content--;
  9. loadFactor=(double)(_hashTable->content)/(double)(_hashTable->tableSize);
  10. return true;
  11. }
  12. else //这个点不存在,或已被删除
  13. return false;
  14. }
  15.  
  16. template<class elementtype="">
  17. bool HashTable<elementtype>::remove(ElementType elementValue)
  18. {
  19. return removeInner(hashTable,elementValue);
  20. }
  21. </elementtype></class></elementtype></class>

4.6 扩充Hash表 rehash

  1. template<class elementtype="">
  2. class HashTable<elementtype>::HashTbl* HashTable<elementtype>::rehash(int currentSize)
  3. {
  4. HashTbl* newHashTable;
  5. initialize(newHashTable,currentSize*10);//扩充一个比原来大十倍的Hash表,这个数字是我简单设定的,没有经过考量!
  6. loadFactor=(double)(hashTable->content)/(double)(newHashTable->tableSize);
  7.  
  8. for(int i=0;i<hashtable->tableSize;i++){
  9. insertInner(newHashTable,hashTable->table[i].elementValue);
  10. }
  11.  
  12. return newHashTable;
  13. }
  14. </hashtable-></elementtype></elementtype></class>

5. 性能测试

我们创建一个使用Quadratic方式位移的Hash表。初始大小设为1,000,000.然后不断插入10,000个随机数。测试需要多少时间。
  1. int main()
  2. {
  3. HashTable<int> hashTable(1000000,&getElementKey,&isEqual,HashTable<int>::QUDRATIC,0.49);
  4. clock_t start=clock();
  5.  
  6. for(int i=0;i<10000000;i++){
  7. int r=rand();
  8. hashTable.insert(r);
  9. }
  10.  
  11. clock_t finish=clock();
  12. printf("time is %fs\n",(double)(finish-start)/CLOCKS_PER_SEC);
  13.  
  14. return 0;
  15. }
  16. </int></int>

使用clang++编译,O3速度优化。测试结果:
  1. time is 2.239344s
  2. time is 2.059147s
  3. time is 2.318181s

6.总结

这次我们介绍了闭散列法(Open Addressing),实测下来,这种方法比开散列速度更快。个人认为主要原因是避免了内存分配/释放操作这一非常耗时的过程。至此为止,我们已经把主流的Hash方法都介绍了。对于一般的应用基本足够。Hash冲突解决方案,Hash Function的设计,都是需要具体问题具体分析的,没有一个放之四海而皆准的方案,关于这一点我也并没有经验,请大家参考其他资源。最后,在下一章(应该也是最终章)中,将介绍C++ STL,以及Python中的Hash库。敬请期待吧!

猜你在找的数据结构相关文章