c# 圖解泛型List , HashTable和Dictionary


前輩在代碼中使用了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反倒效率可能高一些。具體情況,再具體研究吧,沒有一概而論。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM