開講。
我們知道Dictionary的最大特點就是可以通過任意類型的key尋找值。而且是通過索引,速度極快。
該特點主要意義:數組能通過索引快速尋址,其他的集合基本都是以此為基礎進行擴展而已。 但其索引值只能是int,某些情境下就顯出Dictionary的便利性了。
那么問題就來了--C#是怎么做的呢,能使其做到泛型索引。
我們關注圈中的內容,這是Dictionary的本質 --- 兩個數組,。這是典型的用空間換取時間的做法。
先來說下兩數組分別代表什么。
1- buckets,int[] ,水桶!不過我覺得用倉庫更為形象。eg: buckets = new int[3]; 表示三個倉庫,i = buckets [0] ,if i = -1 表示該倉庫為空,否則表示第一個倉庫存儲着東西。這個東西表示數組entries的索引。
2- entries , Entry<TKey,TValue>[] ,Entry是個結構,key,value就是我們的鍵值真實值,hashCode是key的哈希值,next可以理解為指針,這里先不具體展開。
[StructLayout(LayoutKind.Sequential)]
private struct Entry
{
public int hashCode;
public int next;
public TKey key;
public TValue value;
}
先說一下索引,如何用人話來解釋呢?這么說吧,本身操作系統只支持地址尋址,如數組聲明時會先存一個header,同時獲取一個base地址指向這個header,其后的元素都是通過*(base+index)來進行尋址。
基於這個共識,Dictionary用泛型key索引的實現就得想方設法把key轉換到上面數組索引上去。
也就是說要在記錄的存儲位置和它的關鍵字之間建立一個確定的對應關系 f,使每個關鍵字和結構中一個惟一的存儲位置相對應。
因而在查找時,只要根據這個對應關系 f 找到給定值 K 的函數值 f(K)。若結構中存在關鍵字和 K 相等的記錄。在此,我們稱這個對應關系 f 為哈希 (Hash) 函數,按這個思想建立的表為哈希表。
回到Dictionary,這個f(K)就存在於key跟buckets之間:
dic[key]加值的實現:entries數組加1,獲取i-->key-->獲取hashCode-->f(hashCode)-->確定key對應buckets中的某個倉庫(buckets對應的索引)-->設置倉庫里的東西(entries的索引 = i)
dic[key]取值的實現:key-->獲取hashCode-->f(hashCode)-->確定key對應buckets中的某個倉庫(buckets對應的索引)--> 獲取倉庫里的東西(entries的索引i,上面有說到)-->真實的值entries[i]
上面的流程中只有一個(f(K)獲取倉庫索引)讓我們很難受,因為不認識,那現在問題變成了這個f(K)如何實現了。
實現:
` int index = hashCode % buckets.Length;
這叫做除留余數法,哈希函數的其中一種實現。如果你自己寫一個MyDictionary,可以用其他的哈希函數。
舉個例子,假設兩數組初始大小為3, this.comparer.GetHashCode(4) & 0x7fffffff = 4:
Dictionary<int, string> dic = new Dictionary<int, string>();
dic.Add(4, "value");
i=0,key=4--> hashCode=4.GetHashCode()=4--> f(hashCode)=4 % 3 = 1-->第1號倉庫-->東西 i = 0.
此時兩數組狀態為:
取值按照之前說的順序進行,仿佛已經完美。但這里還有個問題,不同的key生成的hashCode經過f(K)生成的值不是唯一的。即一個倉庫可能會放很多東西。
C#是這么解決的,每次往倉庫放東西的時候,先判斷有沒有東西(buckets[index] 是否為 -1),如果有,則進行修改。
如再:
dic.Add(7, "value");
dic.Add(10, "value");
f(entries[1]. hashCode)=7 % 3 = 1也在第一號倉庫,則修改buckets[1] = 1。
同時修改entries[1].next = 0;//上一個倉庫的東西
f(entries[2].hashCode)=10 % 3 = 1也在第一號倉庫,則再修改buckets[1] = 2。
同時修改entries[1].next = 1;//上一個倉庫的東西
這樣相當於1號倉庫存了一個單向鏈表,entries:2-1-0。
成功解決。
這里有人如果看過這些集合源碼的話知道數組一般會有一個默認大小(當然我們初始化集合的時候也可以手動傳入capacity),總之,Length不可能無限大。
那么當集合滿的時候,我們需對集合進行擴容,C#一般直接Length*2。那么buckets.Length就是可變的,上面的f(K)結果也就不是恆定的。
C#對此的解決放在了擴容這一步:
可以看到擴容實質就是新開辟一個更大空間的數組,講道理是耗資源的。所以我們在初始化集合的時候,每次都給定一個合適的Capacity,講道理是一個老油條該干的事兒。
上面說的這就是所謂“用空間換取時間的做法”,兩個數組存了一個集合,而集合中我們最關心的value仿佛是個主角,一堆配角作陪。
現在看下源碼實現:
索引器取值:
具體實現:
1,2,3,4,5就是本文的重點。基本都講到了,其中4 ,5 -- (this.entries[i].hashCode == num) && this.comparer.Equals(this.entries[i].key, key):確定唯一key value對的條件,hashCode相等,key也得相等。
說明hashCode也有相等的情況,其實這里 (this.entries[i].hashCode == num)這個條件可以省略,因為如果key Equal則hashCode 肯定相等。當然&&符號會先計算第一個條件,比較hashCode快得多,先過濾掉一大部分元素,最后再用Equals比較確定。
也就是
hash code 是整數,相等判斷的性能高。
hash code 相等才做較慢的鍵相等判斷。
這是一種性能優化。
Thanks All.
歡迎討論~
感謝閱讀~
個人公眾號: