数据结构基础(18) --哈希表的设计与实现

哈希表

    根据设定的哈希函数 H(key)所选中的处理冲突的方法,将一组关键字映射到一个有限的、地址连续的地址集 (区间) 上,并以关键字在地址集中的“映像”作为相应记录在表中的存储位置,如此构造所得的查找表称之为“哈希表”。

 

构造哈希函数的方法

1. 直接定址法(数组)

  哈希函数为关键字的线性函数H(key) = key 或者 H(key) = a*key + b

  此法仅适合于:地址集合的大小 == 关键字集合的大小

 

2. 数字分析法

  假设关键字集合中的每个关键字都是由 s 位数字组成 (u1, u2, …, us),分析关键字集中的全体, 并从中提取分布均匀的若干位或它们的组合作为地址。

  此方法仅适合于:能预先估计出全体关键字的每一位上各种数字出现的频度。

 

3. 平方取中法

  以关键字的平方值的中间几位作为存储地址。求“关键字的平方值”的目的是“扩大差别” ,同时平方值的中间各位又能受到整个关键字中各位的影响。

  此方法适合于:关键字中的每一位都有某些数字重复出现频度很高的现象。

 

4. 折叠法

  将关键字分割成若干部分,然后取它们的叠加和为哈希地址。有两种叠加处理的方法:移位叠加和间界叠加。

  此方法适合于:关键字的数字位数特别多;

 

5. 除留余数法

  设定哈希函数为:{H(key) = key % p | 其中,p≤m(表长)并且p 应为不大于 m 的素数或是不含 20 以下的质因子}

   为什么要对 p 加限制?

    例如:给定一组关键字为:12, 39, 18, 24, 33,21,若取 p=9, 则他们对应的哈希函数值将为:3, 3, 0, 6, 6, 3;

  可见,若 p 中含质因子 3, 则所有含质因子 3 的关键字均映射到“3 的倍数”的地址上,从而增加了“冲突”的可能。

 

6. 随机数法

  设定哈希函数为:H(key) = Random(key)其中,Random 为伪随机函数;

  通常,此方法用于对长度不等的关键字构造哈希函数。

  

(如果关键字并不是数字, 则还需先对其进行数字化处理。) 

实际造表时,采用何种构造哈希函数的方法取决于建表的关键字集合的情况(包括关键字的范围和形态),总的原则是使产生冲突的可能性降到尽可能地小(下面我们将以除留余数法构造哈希函数)。

 

处理冲突的方法

  “处理冲突” 的实际含义是:为产生冲突的地址寻找下一个哈希地址。

1. 开放定址法

  为产生冲突的地址 H(key) 求得一个地址序列:{ H0, H1, …, Hs|1≤ s≤m-1}

  其中:  H0 = H(key)

        Hi = ( H(key) + di ) % m {i=1, 2, …, s}

 对增量 di  有三种取法:

   1) 线性探测再散列
      di = c * i   最简单的情况  c=1

   2) 平方探测再散列
      di = 1^2, -1^2, 2^2, -2^2, …,

   3) 随机探测再散列
      di 是一组伪随机数列或者di=i×H2(key) (又称双散列函数探测)

  注意:增量 di 应具有“完备性”,即:产生的 Hi 均不相同,且所产生的s(m-1)个 Hi 值能覆盖哈希表中所有地址。则要求: 

    ※ 平方探测时的表长 m 必为形如 4j+3 的素数(如: 7, 11, 19, 23, … 等);

    ※ 随机探测时的 m 和 di 没有公因子。

 

2. 链地址法(又称拉链法)

   将所有哈希地址相同的记录都链接在同一链表中(我们将采用的方法)。

 

哈希表的设计与实现

[cpp] view plain copy
  1. //哈希表设计  
  2. template <typename HashedObj>  
  3. class HashTable  
  4. {  
  5. public:  
  6.     typedef typename vector<HashedObj>::size_type size_type;  
  7.   
  8. public:  
  9.     explicit HashTable(int tableSize = 101)  
  10.         : theList(tableSize), currentSize(0) {}  
  11.     ~HashTable()  
  12.     {  
  13.         makeEmpty();  
  14.     }  
  15.   
  16.     //判断元素x是否存在于哈希表中  
  17.     bool contains(const HashedObj &x) const;  
  18.   
  19.     void makeEmpty();  
  20.     bool insert(const HashedObj &x);  
  21.     bool remove(const HashedObj &x);  
  22.   
  23. private:  
  24.     vector< list<HashedObj> > theList;  
  25.     size_type currentSize;  
  26.   
  27.     void rehash();  
  28.     int myHash(const HashedObj &x) const;  
  29. };  

哈希函数

[cpp] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. //如果关键字并不是数字, 则需先对其进行数字化处理  
  2. template <typename Type>  
  3. int hash(Type key)  
  4. {  
  5.     return key;  
  6. }  
  7. template<>  
  8. int hash<const string &>(const string &key)  
  9. {  
  10.     int hashVal = 0;  
  11.     for (size_t i = 0; i < key.length(); ++i)  
  12.     {  
  13.         hashVal = 37 * hashVal * key[i];  
  14.     }  
  15.   
  16.     return hashVal;  
  17. }  
  18.   
  19. //哈希函数  
  20. template <typename HashedObj>  
  21. int HashTable<HashedObj>::myHash(const HashedObj &x) const  
  22. {  
  23.     //首先对key进行数字化处理  
  24.     int hashVal = hash(x);  
  25.     //计算哈希下标  
  26.     hashVal = hashVal % theList.size();  
  27.     if (hashVal < 0)  
  28.         hashVal += theList.size();  
  29.   
  30.     return hashVal;  
  31. }  

哈希表的插入

[cpp] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. //插入  
  2. template <typename HashedObj>  
  3. bool HashTable<HashedObj>::insert(const HashedObj &x)  
  4. {  
  5.     //首先找到应该插入的桶(链表)  
  6.     list<HashedObj> &whichList = theList[ myHash(x) ];  
  7.     //哈希表中已经存在该值了  
  8.     if (find(whichList.begin(), whichList.end(), x) != whichList.end())  
  9.         return false;  
  10.   
  11.     //插入桶中  
  12.     whichList.push_back(x);  
  13.     //如果此时哈希表已经"满"了(所存储的元素个数 = 哈希表的槽数)  
  14.     //装载因子 == 1, 为了获取更好的性能, 再哈希  
  15.     if (++ currentSize > theList.size())  
  16.         rehash();  
  17.   
  18.     return true;  
  19. }  

再哈希

[cpp] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. //判断是否是素数  
  2. bool is_prime(size_t n)  
  3. {  
  4.     if (n == 1 || !n)  
  5.         return 0;  
  6.     for (size_t i = 2; i*i <= n; i++)  
  7.         if (!(n%i))  
  8.             return 0;  
  9.     return 1;  
  10. }  
  11. //寻找下一个素数  
  12. size_t nextPrime(size_t n)  
  13. {  
  14.     for (size_t i = n; ; ++i)  
  15.     {  
  16.         if (is_prime(i))  
  17.             return i;  
  18.     }  
  19.   
  20.     return -1;  
  21. }  
[cpp] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. //再哈希  
  2. template <typename HashedObj>  
  3. void HashTable<HashedObj>::rehash()  
  4. {  
  5.     vector< list<HashedObj> > oldList = theList;  
  6.   
  7.     //以一个大于原表两倍的第一个素数重新设定哈希桶数  
  8.     theList.resize( nextPrime(2*theList.size()) );  
  9.     //将原表清空  
  10.     for (typename vector< list<HashedObj> >::iterator iter = theList.begin();  
  11.             iter != theList.end();  
  12.             ++ iter)  
  13.         iter -> clear();  
  14.   
  15.     //将原表的数据插入到新表中  
  16.     for (size_type i = 0; i < oldList.size(); ++i)  
  17.     {  
  18.         typename list<HashedObj>::iterator iter = oldList[i].begin();  
  19.         while (iter != oldList[i].end())  
  20.         {  
  21.             insert(*iter ++);  
  22.         }  
  23.     }  
  24. }  

哈希表的查找

    查找过程和造表过程一致。假设采用开放定址处理冲突,则查找过程为:对于给定值 K, 计算哈希地址 i = H(K),若 r[i] = NULL  则查找不成功,若 r[i].key = K  则查找成功否则 “求下一地址 Hi” ,直至 r[Hi] = NULL  (查找不成功)或r[Hi].key = K  (查找成功) 为止。

    而我们采用比较简单的链地址法(也称拉链法的查找实现):

[cpp] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. //查找:判断哈希表中是否存在该元素  
  2. template <typename HashedObj>  
  3. bool HashTable<HashedObj>::contains(const HashedObj &x) const  
  4. {  
  5.     const list<HashedObj> &whichList = theList[ myHash(x) ];  
  6.     if (find(whichList.begin(), whichList.end(), x) != whichList.end())  
  7.         return true;  
  8.   
  9.     return false;  
  10. }  

哈希表查找的分析:

  从查找过程得知,哈希表查找的平均查找长度实际上并不等于零。决定哈希表查找的ASL的因素

   1)选用的哈希函数;

   2)选用的处理冲突的方法;

   3)哈希表饱和的程度,装载因子 α=n/m 值的大小(n:记录数,m:表的长度)

  一般情况下,可以认为选用的哈希函数是“均匀”的,则在讨论ASL时,可以不考虑它的因素。

  因此,哈希表的ASL是处理冲突方法和装载因子的函数。可以证明,查找成功时有下列结果

线性探测再散列:

 

随机探测再散列:

 

链地址法

 

 

   从以上结果可见:哈希表的平均查找长度是装载因子的函数,而不是 n 的函数;这说明,用哈希表构造查找表时,可以选择一个适当的装填因子,使得平均查找长度限定在某个范围内(这是哈希表所特有的特点).


哈希表的删除操作

[cpp] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. //删除  
  2. template <typename HashedObj>  
  3. bool HashTable<HashedObj>::remove(const HashedObj &x)  
  4. {  
  5.     list<HashedObj> &whichList = theList[ myHash(x) ];  
  6.     typename list<HashedObj>::iterator iter = find(whichList.begin(), whichList.end(), x);  
  7.     //没有找到该元素  
  8.     if (iter == whichList.end())  
  9.         return false;  
  10.   
  11.     whichList.erase(iter);  
  12.     -- currentSize;  
  13.   
  14.     return true;  
  15. }  

清空哈希表

[cpp] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. //清空哈希表  
  2. template <typename HashedObj>  
  3. void HashTable<HashedObj>::makeEmpty()  
  4. {  
  5.     for (typename vector< list<HashedObj> >::iterator iter = theList.begin();  
  6.             iter != theList.end();  
  7.             ++ iter)  
  8.     {  
  9.         iter -> clear();  
  10.     }  
  11. }  

1-测试代码

[cpp] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. int main()  
  2. {  
  3.     HashTable<int> iTable;  
  4.     // 1 2 3 4 5 6 7 8 9 10  
  5.     for (int i = 0; i < 10; ++i)  
  6.         iTable.insert(i+1);  
  7.   
  8.     for (int i = 0; i < 10; ++i)  
  9.         if (iTable.contains(i+1))  
  10.             cout << i << ": contains..." << endl;  
  11.         else  
  12.             cout << i << ": not contains" << endl;  
  13.     cout << endl;  
  14.   
  15.     //1 2  
  16.     for (int i = 0; i < 10; ++i)  
  17.         iTable.remove(i+3);  
  18.   
  19.     for (int i = 0; i < 10; ++i)  
  20.         if (iTable.contains(i))  
  21.             cout << i << ": contains..." << endl;  
  22.         else  
  23.             cout << i << ": not contains" << endl;  
  24.     cout << endl;  
  25.   
  26.     // 6 8  
  27.     iTable.makeEmpty();  
  28.     iTable.insert(6);  
  29.     iTable.insert(8);  
  30.     for (int i = 0; i < 10; ++i)  
  31.         if (iTable.contains(i))  
  32.             cout << i << ": contains..." << endl;  
  33.         else  
  34.             cout << i << ": not contains" << endl;  
  35.   
  36.     return 0;  
  37. }  

2-各类算法复杂度的比较

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章