前輩在代碼中使用了HashTable,由於我用的比較少,不能理解,為什么不用Dictionary?看了源碼以及查閱資料,總結如下:
首先看看它們的繼承體系:
我把list<T>的繼承體系也一並畫出來,因為c#集合中List<T>和Dictionary<T>這兩種數據結構實在太常用了。從上圖中可以看到Dictionary和HashTable都繼承於IDictionary。既然父輩都相同,那么注定會有很多相似的地方。那么它們又會有哪些不同呢?
這個還得研究源碼,先看看HashTable:
1 private struct bucket { 2 public Object key; 3 public Object val; 4 public int hash_coll; // Store hash code; sign bit means there was a collision. 5 } 6 7 private bucket[] buckets;
HashTable 定義了一個結構體數組,hash_coll里面存儲了hash code。那么hash code又是什么東西呢?hash code其實類似於索引。還記得int[],按順序存儲,我們必須知道它確切的存儲位置,即在數組中的索引。在HashTable中,Key的類型是object,所以理論上可以是任意類型,但是我們實際上最常用的是Int和String類型。因此,HashTable是可以按字符串索引的。歸根結底,微軟擴展了數組,自定義了一個數組。這就帶來了一個問題。什么問題?存儲問題。以前的數組存儲,我們按數字索引存儲。現在呢,我們按key存儲,如何按key存儲?這就需要一個方法,把key映射到數組的不同位置上,並且不能重復。我們把這個映射方法稱為散列函數GetHashCode。如果hash code出現重復了,我們稱為哈希碰撞或者哈希沖突。產生沖突當然需要解決了。解決這一沖突的簡單辦法,便是不斷地嘗試其它位置,直到沖突解決。想想我們中午去飯店吃飯的時候,總要找個座位,這個座位必須是空的才行,如果發現這個座位有人,那么我們再去尋找其它的座位。如果所有的座位都滿了,我們只能等待別人讓出座位。程序若發現數組中的大部分位置都被占了,那么會擴展這個數組,否則會影響性能,總不能把時間花在找座位上。如下圖所示:
Dictionary的內部存儲結構:
1 private struct Entry { 2 public int hashCode; // Lower 31 bits of hash code, -1 if unused 3 public int next; // Index of next entry, -1 if last 4 public TKey key; // Key of entry 5 public TValue value; // Value of entry 6 } 7 8 private int[] buckets; 9 private Entry[] entries;
從結構體的定義中,我們可以看出,Dictionary比HashTable多了一個next字段。那么這個next字段是做什么用的?
Dictionary處理哈希沖突的方法,是把具有相同的哈希值的元素放到一個邏輯鏈表里面。那么next字段正是指向下一個元素的索引。這種處理沖突的方法跟化學當中的同位素還是有點相似的。我們把不同的元素放到數組中,每個元素的同位素放到自己的邏輯鏈表里。具體如何實現,我們看源碼:
1 private void Insert(TKey key, TValue value, bool add) { 2 3 if( key == null ) { 4 ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key); 5 } 6 7 if (buckets == null) Initialize(0); 8 int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF; 9 int targetBucket = hashCode % buckets.Length; 10 int index; 11 if (freeCount > 0) { 12 index = freeList; 13 freeList = entries[index].next; 14 freeCount--; 15 } 16 else { 17 if (count == entries.Length) 18 { 19 Resize(); 20 targetBucket = hashCode % buckets.Length; 21 } 22 index = count; 23 count++; 24 } 25 26 entries[index].hashCode = hashCode; 27 entries[index].next = buckets[targetBucket]; 28 entries[index].key = key; 29 entries[index].value = value; 30 buckets[targetBucket] = index; 31 version++; 32
我把插入字典的核心代碼貼出來。這段代碼,不畫圖不太好理解。首先解釋一下,entries是存放元素的數組,buckets也是數組,記錄entries數組的索引。假設我們數組大小為5,hashcode的取值范圍在1-30之間。
圖1為數組的初始狀態:buckets一開始全部為-1,entries為空數組
圖1
圖2:插入hashcode為9的元素
9%5=4,所以buckets[4]=0,記錄第一元素的索引值。
圖2
圖3:插入第二個元素,hashcode=26,26%5=1,所以buckets[1]=1
圖3
圖4:插入第三個元素,hashcode=25,25%5=0,所以buckets[0]=2
圖5:插入第四個元素,hashcode=10, 10%5=0,所以buckets[0]=3
注意:第三個元素指向了第二個元素,因為buckets[0]同時記錄了元素2和元素3,所以發生了沖突,此時用到了元素的鏈表來記錄所有沖突的元素。
圖6:插入第五個元素,hashcode=5,5%5=0,所以buckets[0]=4
發現了嗎?如果發生沖突,新的元素,總是指向前一任。所謂的元素的鏈表,不是真實的鏈表結構存儲的,而是邏輯上,用Next記錄前任元素的索引值罷了,還是用的同一個數組。
好了,Dictionary和HashTable是同源,它們實現了自己的哈希算法。至於兩者之間的效率,那得具體看情況了。對於含有大量裝箱拆箱的操作,那當然了用泛型字典合適。對於數據量比較小的字符串處理,用HashTable反倒效率可能高一些。具體情況,再具體研究吧,沒有一概而論。