數據結構基礎(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-各類算法複雜度的比較

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