C# HashTable深度解析

先例舉幾個問題:1,Hashtable爲什麼速度查詢速度快,而添加速度相對慢,且其添加和查詢速度之比相差一個數量等級?
 
                           2,裝填因子( Load Factor)是什麼,hashtable默認的裝填因子是多少?
 
                           3,hashtable裏的元素是順序排序的嗎?
 
                           4,hashtable內部的數據桶(數組)默認長度多少,其長度爲什麼只能是素數?
 
Hashtable中的數據實際存儲在內部的一個數據桶裏(bucket結構數組),其和普通的數組一樣,容量固定,根據數組索引獲取值。
 
下面從正常使用Hashtable場景看內部是如何實現的,內部都做了哪些工作。
 
一,new一個Hashtable,Hashtable ht=new Hashtable();
 
Hashtable有多個構造函數,常用的是無參構造函數:Hashtable ht=new Hashtable(),在new一個hashtable時,其內部做了如下工作:調用Hashtable(int capacity,float loadFactor),其中capacity爲:0,loadFactor爲:1,然後初始化bocket數組大小爲3,裝載因子爲0.72(該值是微軟權衡後給的值),如下圖所示,該圖截取Reflector
 
 
 
二,向Hashtable添加一個元素,ht.Add("a","123")
 
   1,判斷當前Hashtable :ht的元素個數與bucket數組之比是否超過裝載因子0.72,
 
       1)小於0.72:對a進行哈希取值,然後將得到的值與bucket數組長度進行取模計算,將取模的結果插入到bucket數組對應索引,將“123”賦值其value.
 
            因爲哈希值可能會重複(不明白的百度一下),從而導致地址衝突,Hashtable 採用的是 "開放定址法" 處理衝突, 具體行爲是把 HashOf(k) % Array.Length 改爲 (HashOf(k) + d(k)) % Array.Length , 得出另外一個位置來存儲關鍵字 "a" 所對應的數據, d 是一個增量函數. 如果仍然衝突, 則再次進行增量, 依此循環直到找到一個 Array 中的空位爲止。
 
       2)  大於0.72:對bucket數組進行擴容,a, 新建一個數組(該數組的大小爲兩倍於當前容量最小的素數,比如當前數組長度是3,那麼新數組長度爲7)。
 
                                                           b,將原來數組元素拷貝到新的數組中,因爲bocket數組長度變了,所以需要對所有key重新哈希計算(這是影響hashtable性能的重要因素)。
 
                     c, 進行上面a步驟。
 
   
 
三,通過key獲取Hashtable對應的value,var v=ht["a"];
 
    1) 計算"a"的哈希值。
 
    2)將計算的結果與bocket數組長度進行取模計算,因爲哈希值可能會衝突,所以類似定位索引上的key可能與輸入的key不相同,這時繼續查找下一個位置。。。。。
 
    3)取模計算結果即是存儲在bocket數組上"123"的索引。
 
 
 
Hashtable還有很多方法,比如Clear ,Remove ,ContainsKey,ContainsValue等方法,因篇幅有限這裏就不一一介紹了。
 
 
 
  寫到這裏來回答一下篇幅開頭的幾個問題。
 
1,Hashtable查詢速度快是因爲內部是基於數組索引定位的,稍微消耗性能的是取KEY的哈希值,添加性能相對查詢慢是因爲:a,添加元素時可能會地址衝突,需要重新定位地址 。 b,擴容後 數組拷貝,重新哈希計算舊數組所有key。
 
2, 裝填因子是Hashtable“已存元素個數/內部bucket數組長度”,這個比值太大會造成衝突概率增大,太小會造成空間的浪費。默認是0.72,該值是微軟經過大量實驗得出的一個比較平衡的值,裝填因子範圍 0.1<loadFactor<1,否則拋出ArgumentOutOfRangeException異常。
 
3,不是順序的(各位看了文章應該知道爲什麼不是順序的了吧?)
 
4,默認長度是3,我看的是.net framework 4.5版本反編譯的代碼,其他版本的.net framework不確定是不是這個值。爲什麼擴容的數組長度一定要是素數呢?因爲素數有一個特點,只能被自己和1整除,如果不是素數那麼在進行取模計算的時候可能會出現多個值。


===========================================================

C#中實現了哈希表數據結構的集合類有:
(1) System.Collections.Hashtable
(2) System.Collections.Generic.Dictionary<TKey,TValue>
前者爲一般類型的哈希表,後者是泛型版本的哈希表。Dictionary和Hashtable之間並非只是簡單的泛型和非泛型的區別,兩者使用了完全不同的哈希衝突解決辦法。Dictionary我已經做了動態演示程序,使用的是Window應用程序。雖然Dictionary相對於Hashtable來說,更優美、漂亮,但總覺得如果不給Hashtable也配上動態演示程序,也是一種遺憾。這次使用了Silverlight來製作,原因很簡單,它可以掛在網上讓大家很方便地觀看。
先來看看效果,這裏需要注意,必須安裝Silverlight 2.0 RTW 才能正常運行遊戲,下載地址:http://www.microsoft.com/silverlight/resources/install.aspx?v=2.0
程序中的鍵編輯框中只接受整數,因爲整數的哈希碼就是整數本身,這可以讓大家更直觀地查看哈希表的變化。如果輸入了非法字符,則會從0至999中隨機抽取一個整數進行添加或刪除操作。

最新發現不登錄博客園的用戶無法直接看到Silverlight,如果是這樣,請移步到以下網址觀看動畫:

http://www.bbniu.com/matrix/ShowApplication.aspx?id=148

 

8.3 哈希衝突解決方法

 

哈希函數的目標是儘量減少衝突,但實際應用中衝突是無法避免的,所以在衝突發生時,必須有相應的解決方案。而發生衝突的可能性又跟以下兩個因素有關:

(1)       裝填因子α:所謂裝填因子是指合希表中已存入的記錄數n與哈希地址空間大小m的比值,即 α=n / m ,α越小,衝突發生的可能性就越小;α越大(最大可取1),衝突發生的可能性就越大。這很容易理解,因爲α越小,哈希表中空閒單元的比例就越大,所以待插入記錄同已插入的記錄發生衝突的可能性就越小;反之,α越大,哈希表中空閒單元的比例就越小,所以待插入記錄同已插入記錄衝突的可能性就越大;另一方面,α越小,存儲窨的利用率就越低;反之,存儲窨的利用率就越高。爲了既兼顧減少衝突的發生,又兼顧提高存儲空間的利用率,通常把α控制在0.6~0.9的範圍之內,C#的HashTable類把α的最大值定爲0.72。

(2)       與所採用的哈希函數有關。若哈希函數選擇得當,就可使哈希地址儘可能均勻地分佈在哈希地址空間上,從而減少衝突的發生;否則,就可能使哈希地址集中於某些區域,從而加大沖突發生的可能性。

衝突解決技術可分爲兩大類:開散列法(又稱爲鏈地址法)和閉散列法(又稱爲開放地址法)。哈希表是用數組實現的一片連續的地址空間,兩種衝突解決技術的區別在於發生衝突的元素是存儲在這片數組的空間之外還是空間之內:

(1)       開散列法發生衝突的元素存儲於數組空間之外。可以把“開”字理解爲需要另外“開闢”空間存儲發生衝突的元素。

(2)       閉散列法發生衝突的元素存儲於數組空間之內。可以把“閉”字理解爲所有元素,不管是否有衝突,都“關閉”於數組之中。閉散列法又稱開放地址法,意指數組空間對所有元素,不管是否衝突都是開放的。

 

8.3.1  閉散列法(開放地址法)

閉散列法是把所有的元素存儲在哈希表數組中。當發生衝突時,在衝突位置的附近尋找可存放記錄的空單元。尋找“下一個”空位的過程稱爲探測。上述方法可用如下公式表示:

hi=(h(key)+di)%m      i=1,2,…,k (k≤m-1)

其中h(key)爲哈希函數;m爲哈希表長;di爲增量的序列。根據di取值的不同,可以分成幾種探測方法,下面只介紹Hashtable所使用到的雙重散列法。

  • 雙重散列法

雙重散列法又稱二度哈希,是閉散列法中較好的一種方法,它是以關鍵字的另一個散列函數值作爲增量。設兩個哈希函數爲:h1和h2,則得到的探測序列爲:

(h1(key)+h2(key))%m,(h1(key)+2h2(key))%m,(h1(key)+3h2(key))%m,…

其中,m爲哈希表長。由此可知,雙重散列法探測下一個開放地址的公式爲:

(h1(key) + i * h2(key)) % m     (1≤i≤m-1)

定義h2的方法較多,但無採用什麼方法都必須使h2(key)的值和m互素(又稱互質,表示兩數的最大公約數爲1,或者說是兩數沒有共同的因子,1除外)才能使發生衝突的同義詞地址均勻地分佈在整個哈希表中,否則可能造成同義詞地址的循環計算。若m爲素數,則h2取1至m-1之間的任何數均與m互素,因此可以簡單地將h2定義爲:

h2(key) = key % (m - 2) + 1

 

 8.4 剖析System.Collections.Hashtable

 

萬物之母object類中定義了一個GetHashCode()方法,這個方法默認的實現是返回一個唯一的整數值以保證在object的生命期中不被修改。既然每種類型都是直接或間接從object派生的,因此所有對象都可以訪問該方法。自然,字符串或其他類型都能以唯一的數字值來表示。也就是說,GetHashCode()方法使得所有對象的哈希函數構造方法都趨於統一。當然,由於GetHashCode()方法是一個虛方法,你也可以通過重寫這個方法來構造自己的哈希函數。

 

8.4.1  Hashtable的實現原理

Hashtable使用了閉散列法來解決衝突,它通過一個結構體bucket來表示哈希表中的單個元素,這個結構體中有三個成員:

(1)       key :表示鍵,即哈希表中的關鍵字。

(2)       val :表示值,即跟關鍵字所對應值。

(3)       hash_coll :它是一個int類型,用於表示鍵所對應的哈希碼。

int類型佔據32個位的存儲空間,它的最高位是符號位,爲“0”時,表示這是一個正整數;爲“1”時表示負整數。hash_coll使用最高位表示當前位置是否發生衝突,爲“0”時,也就是爲正數時,表示未發生衝突;爲“1”時,表示當前位置存在衝突。之所以專門使用一個位用於存放哈希碼並標註是否發生衝突,主要是爲了提高哈希表的運行效率。關於這一點,稍後會提到。

Hashtable解決衝突使用了雙重散列法,但又跟前面所講的雙重散列法稍有不同。它探測地址的方法如下:

h(key, i) = h1(key) + i * h2(key)

其中哈希函數h1和h2的公式如下:

h1(key) = key.GetHashCode()

h2(key) = 1 + (((h1(key) >> 5) + 1) % (hashsize - 1))

由於使用了二度哈希,最終的h(key, i)的值有可能會大於hashsize,所以需要對h(key, i)進行模運算,最終計算的哈希地址爲:

哈希地址 = h(key, i) % hashsize

【注意】:bucket結構體的hash_coll字段所存儲的是h(key, i)的值而不是哈希地址。

哈希表的所有元素存放於一個名稱爲buckets(又稱爲數據桶) 的bucket數組之中,下面演示一個哈希表的數據的插入和刪除過程,其中數據元素使用(鍵,值,哈希碼)來表示。注意,本例假設Hashtable的長度爲11,即hashsize = 11,這裏只顯示其中的前5個元素。

(1)       插入元素(k1,v1,1)和(k2,v2,2)。

由於插入的兩個元素不存在衝突,所以直接使用h1(key) % hashsize的值做爲其哈希碼而忽略了h2(key)。其效果如圖8.6所示。

 

(2)      插入元素(k3,v3,12)

     新插入的元素的哈希碼爲12,由於哈希表長爲11,12 % 11 = 1,所以新元素應該插入到索引1處,但由於索引1處已經被k1佔據,所以需要使用h2(key)重新計算哈希碼。

h2(key) = 1 + (((h1(key) >> 5) + 1) % (hashsize - 1))

h2(key) = 1 + ((12 >> 5) + 1) % (11 - 1)) = 2

新的哈希地址爲 h1(key) + i * h2(key) = 1 + 1 * 2 = 3,所以k3插入到索引3處。而由於索引1處存在衝突,所以需要置其最高位爲“1”。

(10000000000000000000000000000001)2 = (-2147483647)10

最終效果如圖8.7所示。

 

(3)       插入元素(k4,v4,14)

k4的哈希碼爲14,14 % 11 = 3,而索引3處已被k3佔據,所以使用二度哈希重新計算地址,得到新地址爲14。索引3處存在衝突,所以需要置高位爲“1”。

(12)10 = (00000000000000000000000000001100)2   高位置“1”後

(10000000000000000000000000001100)2 = (-2147483636)10

最終效果如圖8.8所示。

 

(4)       刪除元素k1和k2

Hashtable在刪除一個存在衝突的元素時(hash_coll爲負數),會把這個元素的key指向數組buckets,同時將該元素的hash_coll的低31位全部置“0”而保留最高位,由於原hash_coll爲負數,所以最高位爲“1”。

(10000000000000000000000000000000)2 = (-2147483648)10

單憑判斷hash_coll的值是否爲-2147483648無法判斷某個索引處是否爲空,因爲當索引0處存在衝突時,它的hash_coll的值同樣也爲-2147483648,這也是爲什麼要把key指向buckets的原因。這裏把key指向buckets並且hash_coll值爲-2147483648的空位稱爲“有衝突空位”。如圖8.8所示,當k1被刪除後,索引1處的空位就是有衝突空位。

Hashtable在刪除一個不存在衝突的元素時(hash_coll爲正數),會把鍵和值都設爲null,hash_coll的值設爲0。這種沒有衝突的空位稱爲“無衝突空位”,如圖8.9所示,k2被刪除後索引2處就屬於無衝突空位,當一個Hashtable被初始化後,buckets數組中的所有位置都是無衝突空位。

 

哈希表通過關鍵字查找元素時,首先計算出鍵的哈希地址,然後通過這個哈希地址直接訪問數組的相應位置並對比兩個鍵值,如果相同,則查找成功並返回;如果不同,則根據hash_coll的值來決定下一步操作。當hash_coll爲0或正數時,表明沒有衝突,此時查找失敗;如果hash_coll爲負數時,表明存在衝突,此時需通過二度哈希繼續計算哈希地址進行查找,如此反覆直到找到相應的鍵值表明查找成功,如果在查找過程中遇到hash_coll爲正數或計算二度哈希的次數等於哈希表長度則查找失敗。由此可知,將hash_coll的高位設爲衝突位主要是爲了提高查找速度,避免無意義地多次計算二度哈希的情況。

 

8.4.2  Hashtable的代碼實現

哈希表的實現較爲複雜,爲了簡化代碼,本例忽略了部分出錯判斷,在測試時請不要設key值爲空。

 

  1. using System;
  2. public class Hashtable
  3. {
  4. private struct bucket
  5. {
  6. public Object key; //鍵
  7. public Object val; //值
  8. public int hash_coll; //哈希碼
  9. }
  10. private bucket[] buckets; //存儲哈希表數據的數組(數據桶)
  11. private int count; //元素個數
  12. private int loadsize; //當前允許存儲的元素個數
  13. private float loadFactor; //填充因子
  14. //默認構造方法
  15. public Hashtable() : this(0, 1.0f) { }
  16. //指定容量的構造方法
  17. public Hashtable(int capacity, float loadFactor)
  18. {
  19. if (!(loadFactor >= 0.1f && loadFactor <= 1.0f))
  20. throw new ArgumentOutOfRangeException(
  21. "填充因子必須在0.1~1之間");
  22. this.loadFactor = loadFactor > 0.72f ? 0.72f : loadFactor;
  23. //根據容量計算表長
  24. double rawsize = capacity / this.loadFactor;
  25. int hashsize = (rawsize > 11) ? //表長爲大於11的素數
  26. HashHelpers.GetPrime((int)rawsize) : 11;
  27. buckets = new bucket[hashsize]; //初始化容器
  28. loadsize = (int)(this.loadFactor * hashsize);
  29. }
  30. public virtual void Add(Object key, Object value) //添加
  31. {
  32. Insert(key, value, true);
  33. }
  34. //哈希碼初始化
  35. private uint InitHash(Object key,int hashsize,
  36. out uint seed,out uint incr)
  37. {
  38. uint hashcode = (uint)GetHash(key) & 0x7FFFFFFF; //取絕對值
  39. seed = (uint)hashcode; //h1
  40. incr = (uint)(1 + (((seed >> 5)+1) % ((uint)hashsize-1)));//h2
  41. return hashcode; //返回哈希碼
  42. }
  43. public virtual Object this[Object key] //索引器
  44. {
  45. get
  46. {
  47. uint seed; //h1
  48. uint incr; //h2
  49. uint hashcode = InitHash(key, buckets.Length,
  50. out seed, out incr);
  51. int ntry = 0; //用於表示h(key,i)中的i值
  52. bucket b;
  53. int bn = (int)(seed % (uint)buckets.Length); //h(key,0)
  54. do
  55. {
  56. b = buckets[bn];
  57. if (b.key == null) //b爲無衝突空位時
  58. { //找不到相應的鍵,返回空
  59. return null;
  60. }
  61. if (((b.hash_coll & 0x7FFFFFFF) == hashcode) &&
  62. KeyEquals(b.key, key))
  63. { //查找成功
  64. return b.val;
  65. }
  66. bn = (int)(((long)bn + incr) %
  67. (uint)buckets.Length); //h(key+i)
  68. } while (b.hash_coll < 0 && ++ntry < buckets.Length);
  69. return null;
  70. }
  71. set
  72. {
  73. Insert(key, value, false);
  74. }
  75. }
  76. private void expand() //擴容
  77. { //使新的容量爲舊容量的近似兩倍
  78. int rawsize = HashHelpers.GetPrime(buckets.Length * 2);
  79. rehash(rawsize);
  80. }
  81. private void rehash(int newsize) //按新容量擴容
  82. {
  83. bucket[] newBuckets = new bucket[newsize];
  84. for (int nb = 0; nb < buckets.Length; nb++)
  85. {
  86. bucket oldb = buckets[nb];
  87. if ((oldb.key != null) && (oldb.key != buckets))
  88. {
  89. putEntry(newBuckets, oldb.key, oldb.val,
  90. oldb.hash_coll & 0x7FFFFFFF);
  91. }
  92. }
  93. buckets = newBuckets;
  94. loadsize = (int)(loadFactor * newsize);
  95. return;
  96. }
  97. //在新數組內添加舊數組的一個元素
  98. private void putEntry(bucket[] newBuckets, Object key,
  99. Object nvalue, int hashcode)
  100. {
  101. uint seed = (uint)hashcode; //h1
  102. uint incr = (uint)(1 + (((seed >> 5) + 1) %
  103. ((uint)newBuckets.Length - 1))); //h2
  104. int bn = (int)(seed % (uint)newBuckets.Length);//哈希地址
  105. do
  106. { //當前位置爲有衝突空位或無衝突空位時都可添加新元素
  107. if ((newBuckets[bn].key == null) ||
  108. (newBuckets[bn].key == buckets))
  109. { //賦值
  110. newBuckets[bn].val = nvalue;
  111. newBuckets[bn].key = key;
  112. newBuckets[bn].hash_coll |= hashcode;
  113. return;
  114. }
  115. //當前位置已存在其他元素時
  116. if (newBuckets[bn].hash_coll >= 0)
  117. { //置hash_coll的高位爲1
  118. newBuckets[bn].hash_coll |=
  119. unchecked((int)0x80000000);
  120. }
  121. //二度哈希h1(key)+h2(key)
  122. bn = (int)(((long)bn + incr) % (uint)newBuckets.Length);
  123. } while (true);
  124. }
  125. protected virtual int GetHash(Object key)
  126. { //獲取哈希碼
  127. return key.GetHashCode();
  128. }
  129. protected virtual bool KeyEquals(Object item, Object key)
  130. { //用於判斷兩key是否相等
  131. return item == null ? false : item.Equals(key);
  132. }
  133. //當add爲true時用作添加元素,當add爲false時用作修改元素值
  134. private void Insert(Object key, Object nvalue, bool add)
  135. { //如果超過允許存放元素個數的上限則擴容
  136. if (count >= loadsize)
  137. {
  138. expand();
  139. }
  140. uint seed; //h1
  141. uint incr; //h2
  142. uint hashcode = InitHash(key, buckets.Length,out seed, out incr);
  143. int ntry = 0; //用於表示h(key,i)中的i值
  144. int emptySlotNumber = -1; //用於記錄空位
  145. int bn = (int)(seed % (uint)buckets.Length); //索引號
  146. do
  147. { //如果是有衝突空位,需繼續向後查找以確定是否存在相同的鍵
  148. if (emptySlotNumber == -1 && (buckets[bn].key == buckets) &&
  149. (buckets[bn].hash_coll < 0))
  150. {
  151. emptySlotNumber = bn;
  152. }
  153. if (buckets[bn].key == null) //確定沒有重複鍵才添加
  154. {
  155. if (emptySlotNumber != -1) //使用之前的空位
  156. bn = emptySlotNumber;
  157. buckets[bn].val = nvalue;
  158. buckets[bn].key = key;
  159. buckets[bn].hash_coll |= (int)hashcode;
  160. count++;
  161. return;
  162. }
  163. //找到重複鍵
  164. if (((buckets[bn].hash_coll & 0x7FFFFFFF)==hashcode) &&
  165. KeyEquals(buckets[bn].key, key))
  166. { //如果處於添加元素狀態,則由於出現重複鍵而報錯
  167. if (add)
  168. {
  169. throw new ArgumentException("添加了重複的鍵值!");
  170. }
  171. buckets[bn].val = nvalue; //修改批定鍵的元素
  172. return;
  173. }
  174. //存在衝突則置hash_coll的最高位爲1
  175. if (emptySlotNumber == -1)
  176. {
  177. if (buckets[bn].hash_coll >= 0)
  178. {
  179. buckets[bn].hash_coll |= unchecked((int)0x80000000);
  180. }
  181. }
  182. bn = (int)(((long)bn + incr) % (uint)buckets.Length);//二度哈希
  183. } while (++ntry < buckets.Length);
  184. throw new InvalidOperationException("添加失敗!");
  185. }
  186. public virtual void Remove(Object key) //移除一個元素
  187. {
  188. uint seed; //h1
  189. uint incr; //h2
  190. uint hashcode = InitHash(key, buckets.Length,out seed, out incr);
  191. int ntry = 0; //h(key,i)中的i
  192. bucket b;
  193. int bn = (int)(seed % (uint)buckets.Length); //哈希地址
  194. do
  195. {
  196. b = buckets[bn];
  197. if (((b.hash_coll & 0x7FFFFFFF) == hashcode) &&
  198. KeyEquals(b.key, key)) //如果找到相應的鍵值
  199. { //保留最高位,其餘清0
  200. buckets[bn].hash_coll &= unchecked((int)0x80000000);
  201. if (buckets[bn].hash_coll != 0) //如果原來存在衝突
  202. { //使key指向buckets
  203. buckets[bn].key = buckets;
  204. }
  205. else //原來不存在衝突
  206. { //置key爲空
  207. buckets[bn].key = null;
  208. }
  209. buckets[bn].val = null; //釋放相應的“值”。
  210. count--;
  211. return;
  212. } //二度哈希
  213. bn = (int)(((long)bn + incr) % (uint)buckets.Length);
  214. } while (b.hash_coll < 0 && ++ntry < buckets.Length);
  215. }
  216. public override string ToString()
  217. {
  218. string s = string.Empty;
  219. for (int i = 0; i < buckets.Length; i++)
  220. {
  221. if (buckets[i].key != null && buckets[i].key != buckets)
  222. { //不爲空位時打印索引、鍵、值、hash_coll
  223. s += string.Format("{0,-5}{1,-8}{2,-8}{3,-8}\r\n",
  224. i.ToString(), buckets[i].key.ToString(),
  225. buckets[i].val.ToString(),
  226. buckets[i].hash_coll.ToString());
  227. }
  228. else
  229. { //是空位時則打印索引和hash_coll
  230. s += string.Format("{0,-21}{1,-8}\r\n", i.ToString(),
  231. buckets[i].hash_coll.ToString());
  232. }
  233. }
  234. return s;
  235. }
  236. public virtual int Count //屬性
  237. { //獲取元素個數
  238. get { return count; }
  239. }
  240. }

HashtableArrayList的實現有似的地方,比如兩者都是以數組爲基礎做進一步地抽象而來,兩者都可以成倍地自動擴展容量。


HashTable<T>泛型類的代碼實現

開放地址法  Xn=(Xn-1 +b ) % size

理論上b要和size是要精心選擇的,不過我這邊沒有做特別的處理,101的默認size是從c#源代碼中抄襲的。。。。

代碼儘量簡單一點是爲了理解方便

hashtable快滿的時候擴展一倍空間,數據和標誌位還有key 這三個數組都要擴展

刪除的時候不能直接刪除元素,只能打一個標誌(因爲用了開放地方方法)

目前只支持string和int類型的key(按位131進制)

非線程安全- 因爲這是範例代碼

支持泛型

  1. public class Hashtable<T>
  2. {
  3. public Hashtable()
  4. {
  5. this.dataArray = new T[this.m];
  6. this.avaiableCapacity = this.m;
  7. this.keyArray = new int[this.m];
  8. for (int i = 0; i < this.keyArray.Length; i++)
  9. {
  10. this.keyArray[i] = -1;
  11. }
  12. this.flagArray = new bool[this.m];
  13. }
  14. private int m = 101;
  15. private int l = 1;
  16. private int avaiableCapacity;
  17. private double factor = 0.35;
  18. private T[] dataArray;
  19. private int[] keyArray;
  20. private bool[] flagArray;
  21. public void Add(string s, T item)
  22. {
  23. if (string.IsNullOrEmpty(s))
  24. {
  25. throw new ArgumentNullException("s");
  26. }
  27. if ((double)this.avaiableCapacity / this.m < this.factor)
  28. {
  29. this.ExtendCapacity();
  30. }
  31. var code = HashtableHelper.GetStringHash(s);
  32. this.AddItem(code, item, this.dataArray, code, this.keyArray, this.flagArray);
  33. }
  34. public T Get(string s)
  35. {
  36. if (string.IsNullOrEmpty(s))
  37. {
  38. throw new ArgumentNullException("s");
  39. }
  40. var code = HashtableHelper.GetStringHash(s);
  41. return this.GetItem(code, this.dataArray, code, this.keyArray, this.flagArray);
  42. }
  43. private void ExtendCapacity()
  44. {
  45. this.m *= 2;
  46. this.avaiableCapacity += this.m;
  47. T[] newItems = new T[this.m];
  48. int[] newKeys = new int[this.m];
  49. bool[] newFlags = new bool[this.m];
  50. for (int i = 0; i < newKeys.Length; i++)
  51. {
  52. newKeys[i] = -1;
  53. }
  54. for (int i = 0; i < this.dataArray.Length; i++)
  55. {
  56. if (this.keyArray[i] >= 0 && !this.flagArray[i])
  57. {
  58. //var code = HashtableHelper.GetStringHash(s);
  59. this.AddItem(
  60. this.keyArray[i],
  61. this.dataArray[i],
  62. newItems,
  63. this.keyArray[i],
  64. newKeys,
  65. this.flagArray);
  66. }
  67. }
  68. this.dataArray = newItems;
  69. this.keyArray = newKeys;
  70. this.flagArray = newFlags;
  71. // throw new NotImplementedException();
  72. }
  73. private int AddItem(int code, T item, T[] data, int hashCode, int[] keys, bool[] flags)
  74. {
  75. int address = code % this.m;
  76. if (keys[address] < 0)
  77. {
  78. data[address] = item;
  79. keys[address] = hashCode;
  80. this.avaiableCapacity--;
  81. return address;
  82. }
  83. else if (keys[address] == hashCode)
  84. {
  85. if (flags[address])
  86. {
  87. flags[address] = false;
  88. data[address] = item;
  89. return address;
  90. }
  91. throw new ArgumentException("duplicated key");
  92. }
  93. else
  94. {
  95. int nextAddress = address + this.l; //open addressing Xn=Xn-1 + b
  96. return this.AddItem(nextAddress, item, data, hashCode, keys, flags);
  97. }
  98. }
  99. private T GetItem(int code, T[] data, int hashCode, int[] keys, bool[] flags)
  100. {
  101. int address = code % this.m;
  102. if (keys[address] < 0)
  103. {
  104. return default(T);
  105. }
  106. else if (keys[address] == hashCode)
  107. {
  108. if (flags[address])
  109. {
  110. return default(T);
  111. }
  112. return data[address];
  113. }
  114. else
  115. {
  116. int nextAddress = address + this.l; //open addressing Xn=Xn-1 + b
  117. return this.GetItem(nextAddress, data, hashCode, keys, flags);
  118. }
  119. }
  120. public void Delete(string s)
  121. {
  122. if (string.IsNullOrEmpty(s))
  123. {
  124. throw new ArgumentNullException("s");
  125. }
  126. var code = HashtableHelper.GetStringHash(s);
  127. this.DeleteItem(code, this.dataArray, code, this.keyArray, this.flagArray);
  128. }
  129. private void DeleteItem(int code, T[] data, int hashCode, int[] keys, bool[] flags)
  130. {
  131. int address = code % this.m;
  132. if (keys[address] < 0)
  133. {
  134. return;
  135. //not exist
  136. }
  137. else if (keys[address] == hashCode)
  138. {
  139. if (!this.flagArray[address])
  140. {
  141. flags[address] = true;
  142. this.avaiableCapacity++;
  143. }
  144. }
  145. else
  146. {
  147. int nextAddress = address + this.l; //open addressing Xn=Xn-1 + b
  148. this.DeleteItem(nextAddress, data, hashCode, keys, flags);
  149. }
  150. }
  151. }
  152. public class HashtableHelper
  153. {
  154. public static int GetStringHash(string s)
  155. {
  156. if (string.IsNullOrEmpty(s))
  157. {
  158. throw new ArgumentNullException("s");
  159. }
  160. var bytes = Encoding.ASCII.GetBytes(s);
  161. int checksum = GetBytesHash(bytes, 0, bytes.Length);
  162. return checksum;
  163. }
  164. public static int GetBytesHash(byte[] array, int ibStart, int cbSize)
  165. {
  166. if (array == null || array.Length == 0)
  167. {
  168. throw new ArgumentNullException("array");
  169. }
  170. int checksum = 0;
  171. for (int i = ibStart; i < (ibStart + cbSize); i++)
  172. {
  173. checksum = (checksum * 131) + array[i];
  174. }
  175. return checksum;
  176. }
  177. public static int GetBytesHash(char[] array, int ibStart, int cbSize)
  178. {
  179. if (array == null || array.Length == 0)
  180. {
  181. throw new ArgumentNullException("array");
  182. }
  183. int checksum = 0;
  184. for (int i = ibStart; i < (ibStart + cbSize); i++)
  185. {
  186. checksum = (checksum * 131) + array[i];
  187. }
  188. return checksum;
  189. }
  190. }


解決哈希(HASH)衝突的主要方法

 


雖然我們不希望發生衝突,但實際上發生衝突的可能性仍是存在的。當關鍵字值域遠大於哈希表的長度,而且事先並不知道關鍵字的具體取值時。衝突就難免會發 生。另外,當關鍵字的實際取值大於哈希表的長度時,而且表中已裝滿了記錄,如果插入一個新記錄,不僅發生衝突,而且還會發生溢出。因此,處理衝突和溢出是 哈希技術中的兩個重要問題。
1、開放定址法
     用開放定址法解決衝突的做法是:當衝突發生時,使用某種探查(亦稱探測)技術在散列表中形成一個探查(測)序列。沿此序列逐個單元地查找,直到找到給定 的關鍵字,或者碰到一個開放的地址(即該地址單元爲空)爲止(若要插入,在探查到開放的地址,則可將待插入的新結點存人該地址單元)。查找時探查到開放的 地址則表明表中無待查的關鍵字,即查找失敗。
注意:
①用開放定址法建立散列表時,建表前須將表中所有單元(更嚴格地說,是指單元中存儲的關鍵字)置空。
②空單元的表示與具體的應用相關。
     按照形成探查序列的方法不同,可將開放定址法區分爲線性探查法、線性補償探測法、隨機探測等
(1)線性探查法(Linear Probing)
該方法的基本思想是:
    將散列表T[0..m-1]看成是一個循環向量,若初始探查的地址爲d(即h(key)=d),則最長的探查序列爲:
        d,d+l,d+2,…,m-1,0,1,…,d-1
     即:探查時從地址d開始,首先探查T[d],然後依次探查T[d+1],…,直到T[m-1],此後又循環到T[0],T[1],…,直到探查到T[d-1]爲止。
探查過程終止於三種情況:
     (1)若當前探查的單元爲空,則表示查找失敗(若是插入則將key寫入其中);
    (2)若當前探查的單元中含有key,則查找成功,但對於插入意味着失敗;
     (3)若探查到T[d-1]時仍未發現空單元也未找到key,則無論是查找還是插入均意味着失敗(此時表滿)。
利用開放地址法的一般形式,線性探查法的探查序列爲:
        hi=(h(key)+i)%m 0≤i≤m-1 //即di=i
用線性探測法處理衝突,思路清晰,算法簡單,但存在下列缺點:
① 處理溢出需另編程序。一般可另外設立一個溢出表,專門用來存放上述哈希表中放不下的記錄。此溢出表最簡單的結構是順序表,查找方法可用順序查找。
② 按上述算法建立起來的哈希表,刪除工作非常困難。假如要從哈希表 HT 中刪除一個記錄,按理應將這個記錄所在位置置爲空,但我們不能這樣做,而只能標上已被刪除的標記,否則,將會影響以後的查找。
③ 線性探測法很容易產生堆聚現象。所謂堆聚現象,就是存入哈希表的記錄在表中連成一片。按照線性探測法處理衝突,如果生成哈希地址的連續序列愈長 ( 即不同關鍵字值的哈希地址相鄰在一起愈長 ) ,則當新的記錄加入該表時,與這個序列發生衝突的可能性愈大。因此,哈希地址的較長連續序列比較短連續序列生長得快,這就意味着,一旦出現堆聚 ( 伴隨着衝突 ) ,就將引起進一步的堆聚。
(2)線性補償探測法
線性補償探測法的基本思想是:
將線性探測的步長從 1 改爲 Q ,即將上述算法中的 j = (j + 1) % m 改爲: j = (j + Q) % m ,而且要求 Q 與 m 是互質的,以便能探測到哈希表中的所有單元。
【例】 PDP-11 小型計算機中的彙編程序所用的符合表,就採用此方法來解決衝突,所用表長 m = 1321 ,選用 Q = 25 。

(3)隨機探測
隨機探測的基本思想是:
將線性探測的步長從常數改爲隨機數,即令: j = (j + RN) % m ,其中 RN 是一個隨機數。在實際程序中應預先用隨機數發生器產生一個隨機序列,將此序列作爲依次探測的步長。這樣就能使不同的關鍵字具有不同的探測次序,從而可以避 免或減少堆聚。基於與線性探測法相同的理由,在線性補償探測法和隨機探測法中,刪除一個記錄後也要打上刪除標記。

2、拉鍊法
(1)拉鍊法解決衝突的方法
     拉鍊法解決衝突的做法是:將所有關鍵字爲同義詞的結點鏈接在同一個單鏈表中。若選定的散列表長度爲m,則可將散列表定義爲一個由m個頭指針組成的指針數 組T[0..m-1]。凡是散列地址爲i的結點,均插入到以T[i]爲頭指針的單鏈表中。T中各分量的初值均應爲空指針。在拉鍊法中,裝填因子α可以大於 1,但一般均取α≤1。
【例】設有 m = 5 , H(K) = K mod 5 ,關鍵字值序例 5 , 21 , 17 , 9 , 15 , 36 , 41 , 24 ,按外鏈地址法所建立的哈希表如下圖所示:
          解決哈希(HASH)衝突的主要方法
(2)拉鍊法的優點
與開放定址法相比,拉鍊法有如下幾個優點:
拉鍊法處理衝突簡單,且無堆積現象,即非同義詞決不會發生衝突,因此平均查找長度較短;
②由於拉鍊法中各鏈表上的結點空間是動態申請的,故它更適合於造表前無法確定表長的情況;
③開放定址法爲減少衝突,要求裝填因子α較小,故當結點規模較大時會浪費很多空間。而拉鍊法中可取α≥1,且結點較大時,拉鍊法中增加的指針域可忽略不計,因此節省空間;
④在用拉鍊法構造的散列表中,刪除結點的操作易於實現。只要簡單地刪去鏈表上相應的結點即可。而對開放地址法構造的散列表,刪除結點不能簡單地將被刪結 點的空間置爲空,否則將截斷在它之後填人散列表的同義詞結點的查找路徑。這是因爲各種開放地址法中,空地址單元(即開放地址)都是查找失敗的條件。因此在 用開放地址法處理衝突的散列表上執行刪除操作,只能在被刪結點上做刪除標記,而不能真正刪除結點。

(3)拉鍊法的缺點
     拉鍊法的缺點是:指針需要額外的空間,故當結點規模較小時,開放定址法較爲節省空間,而若將節省的指針空間用來擴大散列表的規模,可使裝填因子變小,這又減少了開放定址法中的衝突,從而提高平均查找速度。

========================

哈希法又稱散列法、雜湊法以及關鍵字地址計算法等,相應的表稱爲哈希表。這種方法的基本思想是:首先在元素的關鍵字k和元素的存儲位置p之間建立一個對應關係f,使得p=f(k)f稱爲哈希函數。創建哈希表時,把關鍵字爲k的元素直接存入地址爲f(k)的單元;以後當查找關鍵字爲k的元素時,再利用哈希函數計算出該元素的存儲位置p=f(k),從而達到按關鍵字直接存取元素的目的。

   當關鍵字集合很大時,關鍵字值不同的元素可能會映象到哈希表的同一地址上,即 k1k2 ,但 Hk1=Hk2),這種現象稱爲衝突,此時稱k1k2同義詞。實際中,衝突是不可避免的,只能通過改進哈希函數的性能來減少衝突。

綜上所述,哈希法主要包括以下兩方面的內容:

 1)如何構造哈希函數

 2)如何處理衝突。

8.4.1   哈希函數的構造方法

    構造哈希函數的原則是:函數本身便於計算;計算出來的地址分佈均勻,即對任一關鍵字kf(k) 對應不同地址的概率相等,目的是儘可能減少衝突。

下面介紹構造哈希函數常用的五種方法。

1 數字分析法

      如果事先知道關鍵字集合,並且每個關鍵字的位數比哈希表的地址碼位數多時,可以從關鍵字中選出分佈較均勻的若干位,構成哈希地址。例如,有80個記錄,關鍵字爲8位十進制整數d1d2d3…d7d8,如哈希表長取100,則哈希表的地址空間爲:00~99。假設經過分析,各關鍵字中 d4d7的取值分佈較均勻,則哈希函數爲:h(key)=h(d1d2d3…d7d8)=d4d7。例如,h(81346532)=43h(81301367)=06。相反,假設經過分析,各關鍵字中 d1d8的取值分佈極不均勻, d都等於5d都等於2,此時,如果哈希函數爲:h(key)=h(d1d2d3…d7d8)=d1d8,則所有關鍵字的地址碼都是52,顯然不可取。

2 平方取中法

當無法確定關鍵字中哪幾位分佈較均勻時,可以先求出關鍵字的平方值,然後按需要取平方值的中間幾位作爲哈希地址。這是因爲:平方後中間幾位和關鍵字中每一位都相關,故不同關鍵字會以較高的概率產生不同的哈希地址。

例:我們把英文字母在字母表中的位置序號作爲該英文字母的內部編碼。例如K的內部編碼爲11E的內部編碼爲05Y的內部編碼爲25A的內部編碼爲01, B的內部編碼爲02。由此組成關鍵字“KEYA”的內部代碼爲11052501,同理我們可以得到關鍵字“KYAB”、“AKEY”、“BKEY”的內部編碼。之後對關鍵字進行平方運算後,取出第7到第9位作爲該關鍵字哈希地址,如圖8.23所示。

 

 

關鍵字

內部編碼

內部編碼的平方值

H(k)關鍵字的哈希地址

KEYA

11050201

122157778355001

778

KYAB

11250102

126564795010404

795

AKEY

01110525

001233265775625

265

BKEY

02110525

004454315775625

315

8.23平方取中法求得的哈希地址

3 分段疊加法

      這種方法是按哈希表地址位數將關鍵字分成位數相等的幾部分(最後一部分可以較短),然後將這幾部分相加,捨棄最高進位後的結果就是該關鍵字的哈希地址。具體方法有摺疊法移位法。移位法是將分割後的每部分低位對齊相加,摺疊法是從一端向另一端沿分割界來回摺疊(奇數段爲正序,偶數段爲倒序),然後將各段相加。例如:key=12360324711202065,哈希表長度爲1000,則應把關鍵字分成3位一段,在此捨去最低的兩位65,分別進行移位疊加和摺疊疊加,求得哈希地址爲105907,如圖8.24所示。

 

 

  2   3                    1   2   3

  0   3                    3   0   6

  4   7                    2   4   7

  1   2                    2   1   1

+   0   2   0               +  0   2   0

        ————————            —————————

        1   1   0   5                    9   0   7

 

a)移位疊加                    (b) 摺疊疊加

 

                      8.24 由疊加法求哈希地址

 

4 除留餘數法

假設哈希表長爲mp爲小於等於m的最大素數,則哈希函數爲

hk=k  %  p ,其中%爲模p取餘運算。

例如,已知待散列元素爲(18756043549046),表長m=10p=7,則有

    h(18)=18 % 7=4    h(75)=75 % 7=5    h(60)=60 % 7=4   

    h(43)=43 % 7=1    h(54)=54 % 7=5    h(90)=90 % 7=6   

    h(46)=46 % 7=4

此時衝突較多。爲減少衝突,可取較大的m值和p值,如m=p=13,結果如下:

    h(18)=18 % 13=5    h(75)=75 % 13=10    h(60)=60 % 13=8    

    h(43)=43 % 13=4    h(54)=54 % 13=2    h(90)=90 % 13=12   

    h(46)=46 % 13=7

此時沒有衝突,如圖8.25所示。

 

     1      2     3     4     5      6     7     8     9     10     11    12

 

 

 

54

 

43

18

 

46

60

 

75

 

90

                      8.25  除留餘數法求哈希地址

 

5 僞隨機數法

    採用一個僞隨機函數做哈希函數,即h(key)=random(key)

在實際應用中,應根據具體情況,靈活採用不同的方法,並用實際數據測試它的性能,以便做出正確判定。通常應考慮以下五個因素 

l         計算哈希函數所需時間 (簡單)。

l         關鍵字的長度。

l         哈希表大小。

l         關鍵字分佈情況。

l         記錄查找頻率

8.4.2   處理衝突的方法

   通過構造性能良好的哈希函數,可以減少衝突,但一般不可能完全避免衝突,因此解決衝突是哈希法的另一個關鍵問題。創建哈希表和查找哈希表都會遇到衝突,兩種情況下解決衝突的方法應該一致。下面以創建哈希表爲例,說明解決衝突的方法。常用的解決衝突方法有以下四種:

1.         開放定址法

這種方法也稱再散列法其基本思想是:當關鍵字key的哈希地址p=Hkey)出現衝突時,以p爲基礎,產生另一個哈希地址p1,如果p1仍然衝突,再以p爲基礎,產生另一個哈希地址p2,直到找出一個不衝突的哈希地址pi 將相應元素存入其中。這種方法有一個通用的再散列函數形式:

          Hi=Hkey+di% m   i=12…,n

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