Dictionary<TKey,TValue>
是平常.net開發中最經常使用的數據類型之一,基本上遇到鍵值對類型的數據時第一反應就是使用這種散列表。散列表特別適合快速查找操做,查找的效率是常數階O(1)。那麼爲何這種數據類型的查找效率可以這麼高效?它背後的數據類型是如何支撐這種查找效率的?它在使用過程當中有沒有什麼侷限性?一塊兒來探究下這個數據類型的奧祕吧。html
本文內容針對的是.Net Framework 4.5.1
的代碼實現,在其餘.Net版本中或多或少都會有些差別,可是基本的原理仍是相同的。算法
本文的內容主要分爲三個部分,第一部分是從代碼的角度來分析並以圖文並茂的方式通俗的解釋Dictionary如何解決的散列衝突並實現高效的數據插入和查找。第二部分名爲「眼見爲實」,因爲第一部分是從代碼層面分析Dictionary的實現,側重於理論分析,所以第二部分使用windbg直接分析內存結構,跟第一部分的理論分析相互印證,加深對於這種數據類型的深刻理解。最後是從數據結構的時間複雜度的角度進行分析並提出了幾條實踐建議。數組
本文內容:數據結構
提到散列表,就不能不提散列衝突。因爲哈希算法被計算的數據是無限的,而計算後的結果範圍有限,所以總會存在不一樣的數據通過計算後獲得的值相同,這就是哈希衝突。(兩個不一樣的數據計算後的結果同樣)。散列衝突的解決方案有好幾個,好比開放尋址法、鏈式尋址法。函數
Dictionary使用的是鏈式尋址法,也叫作拉鍊法。拉鍊法的基本思想是將散列值相同的數據存在同一個鏈表中,若是有散列值相同的元素,則加到鏈表的頭部。一樣道理,在查找元素的時候,先計算散列值,而後在對應散列值的鏈表中查找目標元素。性能
用圖來表達鏈式尋址法的思想:大數據
Dictionary的內部存儲數據主要是依賴了兩個數組,分別是int[] buckets
和Entry[] entries
。其中buckets
是Dictionary全部操做的入口,相似於上文中解說拉鍊法所用的圖中的那個豎着的數據結構。Entry
是一個結構體,用於封裝全部的元素而且增長了next字段用於構建鏈表數據結構。爲了便於理解,下文中截取了一段相關的源代碼並增長了註釋。.net
//數據在Dictionary<TKey,TValue>的存儲形式,全部的鍵值對在Dictionary<TKey,TValue>都是被包裝成一個個的Entry保存在內存中 private struct Entry { public int hashCode; // 散列計算結果的低31位數,默認值爲-1 public int next; // 下一個Entry的索引值,鏈表的最後一個Entry的next爲-1 public TKey key; // Entry對象的Key對應於傳入的TKey public TValue value; // Entry對象的Value對應與傳入的TValue } private int[] buckets; //hashCode的桶,是查找全部Entry的第一級數據結構 private Entry[] entries; //保存真正的數據
下文中以Dictionary<int,string>
爲例,分析Dictionary
在使用過程當中內部數據的組織方式。線程
初始化代碼:3d
Dictionary<int, string> dictionary = new Dictionary<int, string>();
Dictionary
的初始化時,buckets
和entries
的長度都是0。
dictionary.Add(1, "xyxy");
向Dictionary中添加這個新元素大體通過了7個步驟:
當向Dictionary
中添加一個元素後,內部數據結構以下圖(爲了便於理解,圖上將bucket和entries中各個鏈表頭結點用線標出了關聯關係):
dictionary.Add(7, "xyxy");
向Dictionary中添加這個元素大體通過了6個步驟:
當向Dictionary
中添加第二個元素後,內部數據結構是這樣的:
dictionary.Add(2, "xyxy");
向Dictionary添加這個元素通過了以下5個步驟:
當向Dictionary
中添加第三個元素後,內部數據結構:
dictionary.Add(4, "xyxy");
經過前面幾個操做能夠看出,當前數據結構中entries數組中的元素已滿,若是再添加元素的話,會發生怎樣的變化呢?
假如再對於dictionary添加一個元素,原來申請的內存空間確定是不夠用的,必須對於當前數據結構進行擴容,而後在擴容的基礎上再執行添加元素的操做。那麼在解釋這個Add
方法原理的時候,分爲兩個場景分別進行:數組擴容和元素添加。
在發現數組容量不夠的時候,Dictionary
首先執行擴容操做,擴容的規則與該數據類型首次初始化的規則相同,即便用大於原數組長度2倍的第一個素數7
做爲新數組的長度(3*2=6,大於6的第一個素數是7)。
擴容步驟:
Array.Copy(entries, 0, newEntries, 0, count);
)Dictionary
中的元素的hashCode在bucket中的位置(注意新的bucket數組中數值的變化);擴容完成後Dictionary
的內容數據結構:
當前已經完成了entries
和bucket
數組的擴容,有了充裕的空間來存儲新的元素,因此能夠在新的數據結構的基礎上繼續添加元素。
當向Dictionary
中添加第四個元素後,內部數據結構是這樣的:
添加這個新的元素的步驟:
畢竟本文的主題是圖文並茂分析Dictionary<Tkey,Tvalue>
的原理,雖然已經從代碼層面和理論層面分析了Dictionary<Tkey,Tvalue>
的實現,可是若是可以分析這個數據類型的實際內存數據結果,能夠得到更直觀的感覺而且對於這個數據類型可以有更加深刻的認識。因爲篇幅的限制,沒法將Dictionary<Tkey,Tvalue>
的全部操做場景結果都進行內存分析,那麼本文中精選有表明性的兩個場景進行分析:一是該數據類型初始化後添加第一個元素的內存結構,二是該數據類型進行第一次擴容後的數據結構。
執行代碼:
Dictionary<int, string> dic = new Dictionary<int, string>(); dic.Add(1, "xyxy"); Console.Read();
打開windbg附加到該進程(因爲使用的是控制檯應用程序,當前線程是0號線程,所以若是附加進程後默認的不是0號線程時執行~0s
切換到0號線程),執行!clrstack -l
查看當前線程及線程上使用的全部變量:
0:000> !clrstack -l OS Thread Id: 0x48b8 (0) Child SP IP Call Site 0000006de697e998 00007ffab577c134 [InlinedCallFrame: 0000006de697e998] Microsoft.Win32.Win32Native.ReadFile(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte*, Int32, Int32 ByRef, IntPtr) 0000006de697e998 00007ffa96abc9c8 [InlinedCallFrame: 0000006de697e998] Microsoft.Win32.Win32Native.ReadFile(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte*, Int32, Int32 ByRef, IntPtr) 0000006de697e960 00007ffa96abc9c8 *** ERROR: Module load completed but symbols could not be loaded for C:\WINDOWS\assembly\NativeImages_v4.0.30319_64\mscorlib\5c1b7b73113a6f079ae59ad2eb210951\mscorlib.ni.dll DomainNeutralILStubClass.IL_STUB_PInvoke(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte*, Int32, Int32 ByRef, IntPtr) 0000006de697ea40 00007ffa972d39ec System.IO.__ConsoleStream.ReadFileNative(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte[], Int32, Int32, Boolean, Boolean, Int32 ByRef) LOCALS: <no data> <no data> <no data> <no data> <no data> <no data> 0000006de697ead0 00007ffa972d38f5 System.IO.__ConsoleStream.Read(Byte[], Int32, Int32) LOCALS: <no data> <no data> 0000006de697eb30 00007ffa96a882d4 System.IO.StreamReader.ReadBuffer() LOCALS: <no data> <no data> 0000006de697eb80 00007ffa97275f23 System.IO.StreamReader.Read() LOCALS: <no data> 0000006de697ebb0 00007ffa9747a2fd System.IO.TextReader+SyncTextReader.Read() 0000006de697ec10 00007ffa97272698 System.Console.Read() 0000006de697ec40 00007ffa38670909 ConsoleTest.DictionaryDebug.Main(System.String[]) LOCALS: 0x0000006de697ec70 = 0x00000215680d2dd8 0000006de697ee88 00007ffa97ba6913 [GCFrame: 0000006de697ee88]
經過對於線程堆棧的分析很容易看出當前線程上使用了一個局部變量,地址爲:0x000001d86c972dd8
,使用!do
命令查看該變量的內容:
0:000> !do 0x00000215680d2dd8 Name: System.Collections.Generic.Dictionary`2[[System.Int32, mscorlib],[System.String, mscorlib]] MethodTable: 00007ffa96513328 EEClass: 00007ffa9662f610 Size: 80(0x50) bytes File: C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll Fields: MT Field Offset Type VT Attr Value Name 00007ffa964a8538 4001887 8 System.Int32[] 0 instance 00000215680d2ee8 buckets 00007ffa976c4dc0 4001888 10 ...non, mscorlib]][] 0 instance 00000215680d2f10 entries 00007ffa964a85a0 4001889 38 System.Int32 1 instance 1 count 00007ffa964a85a0 400188a 3c System.Int32 1 instance 1 version 00007ffa964a85a0 400188b 40 System.Int32 1 instance -1 freeList 00007ffa964a85a0 400188c 44 System.Int32 1 instance 0 freeCount 00007ffa96519630 400188d 18 ...Int32, mscorlib]] 0 instance 00000215680d2ed0 comparer 00007ffa964c6ad0 400188e 20 ...Canon, mscorlib]] 0 instance 0000000000000000 keys 00007ffa977214e0 400188f 28 ...Canon, mscorlib]] 0 instance 0000000000000000 values 00007ffa964a5dd8 4001890 30 System.Object 0 instance 0000000000000000 _syncRoot
從內存結構來看,該變量中就是咱們查找的Dic存在buckets、entries、count、version等字段,其中buckets和entries在上文中已經有屢次說起,也是本文的分析重點。既然要眼見爲實,那麼buckets和entries這兩個數組的內容究竟是什麼樣的呢?這兩個都是數組,一個是int數組,另外一個是結構體數組,對於這兩個內容分別使用!da
命令查看其內容:
首先是buckets的內容:
0:000> !da -start 0 -details 00000215680d2ee8 Name: System.Int32[] MethodTable: 00007ffa964a8538 EEClass: 00007ffa966160e8 Size: 36(0x24) bytes Array: Rank 1, Number of elements 3, Type Int32 Element Methodtable: 00007ffa964a85a0 [0] 00000215680d2ef8 Name: System.Int32 MethodTable: 00007ffa964a85a0 EEClass: 00007ffa96616078 Size: 24(0x18) bytes File: C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll Fields: MT Field Offset Type VT Attr Value Name 00007ffa964a85a0 40005a2 0 System.Int32 1 instance -1 m_value [1] 00000215680d2efc Name: System.Int32 MethodTable: 00007ffa964a85a0 EEClass: 00007ffa96616078 Size: 24(0x18) bytes File: C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll Fields: MT Field Offset Type VT Attr Value Name 00007ffa964a85a0 40005a2 0 System.Int32 1 instance 0 m_value [2] 00000215680d2f00 Name: System.Int32 MethodTable: 00007ffa964a85a0 EEClass: 00007ffa96616078 Size: 24(0x18) bytes File: C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll Fields: MT Field Offset Type VT Attr Value Name 00007ffa964a85a0 40005a2 0 System.Int32 1 instance -1 m_value
當前buckets中有三個值,分別是:-一、0和-1,其中-1是數組初始化後的默認值,而下表爲1的位置的值0則是上文中添加dic.Add(1, "xyxy");
這個指令的結果,表明其對應的鏈表首節點在entries數組中下標爲0的位置,那麼entries數組中的數值是什麼樣子的呢?
0:000> !da -start 0 -details 00000215680d2f10 Name: System.Collections.Generic.Dictionary`2+Entry[[System.Int32, mscorlib],[System.String, mscorlib]][] MethodTable: 00007ffa965135b8 EEClass: 00007ffa9662d1f0 Size: 96(0x60) bytes Array: Rank 1, Number of elements 3, Type VALUETYPE Element Methodtable: 00007ffa96513558 [0] 00000215680d2f20 Name: System.Collections.Generic.Dictionary`2+Entry[[System.Int32, mscorlib],[System.String, mscorlib]] MethodTable: 00007ffa96513558 EEClass: 00007ffa966304e8 Size: 40(0x28) bytes File: C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll Fields: MT Field Offset Type VT Attr Value Name 00007ffa964a85a0 4003502 8 System.Int32 1 instance 1 hashCode 00007ffa964a85a0 4003503 c System.Int32 1 instance -1 next 00007ffa964a85a0 4003504 10 System.Int32 1 instance 1 key 00007ffa964aa238 4003505 0 System.__Canon 0 instance 00000215680d2db0 value [1] 00000215680d2f38 Name: System.Collections.Generic.Dictionary`2+Entry[[System.Int32, mscorlib],[System.String, mscorlib]] MethodTable: 00007ffa96513558 EEClass: 00007ffa966304e8 Size: 40(0x28) bytes File: C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll Fields: MT Field Offset Type VT Attr Value Name 00007ffa964a85a0 4003502 8 System.Int32 1 instance 0 hashCode 00007ffa964a85a0 4003503 c System.Int32 1 instance 0 next 00007ffa964a85a0 4003504 10 System.Int32 1 instance 0 key 00007ffa964aa238 4003505 0 System.__Canon 0 instance 0000000000000000 value [2] 00000215680d2f50 Name: System.Collections.Generic.Dictionary`2+Entry[[System.Int32, mscorlib],[System.String, mscorlib]] MethodTable: 00007ffa96513558 EEClass: 00007ffa966304e8 Size: 40(0x28) bytes File: C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll Fields: MT Field Offset Type VT Attr Value Name 00007ffa964a85a0 4003502 8 System.Int32 1 instance 0 hashCode 00007ffa964a85a0 4003503 c System.Int32 1 instance 0 next 00007ffa964a85a0 4003504 10 System.Int32 1 instance 0 key 00007ffa964aa238 4003505 0 System.__Canon 0 instance 0000000000000000 value
經過對於entries數組的分析能夠看出,這個數組也有三個值,其中下標爲0的位置已經填入相關內容,好比hashCode爲1,key爲1,其中value的內容是一個內存地址:000001d86c972db0
,這個地址指向的就是字符串對象,它的內容是xyxy
,使用!do
指令來看下具體內容:
0:000> !do 00000215680d2db0 Name: System.String MethodTable: 00007ffcc6b359c0 EEClass: 00007ffcc6b12ec0 Size: 34(0x22) bytes File: C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll String: xyxy Fields: MT Field Offset Type VT Attr Value Name 00007ffcc6b385a0 4000283 8 System.Int32 1 instance 4 m_stringLength 00007ffcc6b36838 4000284 c System.Char 1 instance 78 m_firstChar 00007ffcc6b359c0 4000288 e0 System.String 0 shared static Empty
這次執行的代碼爲:
Dictionary<int, string> dic = new Dictionary<int, string>(); dic.Add(1, "xyxy"); dic.Add(7, "xyxy"); dic.Add(2, "xyxy"); dic.Add(4, "xyxy"); Console.Read();
一樣採起附加進程的方式分析這段代碼執行後的內存結構,本章節中忽略掉如何查找Dictionary變量地址的部分,直接分析buckets數組和entries數組的內容。
首先是buckets數組的內存結構:
0:000> !da -start 0 -details 0000019a471a54f8 Name: System.Int32[] MethodTable: 00007ffcc6b38538 EEClass: 00007ffcc6ca60e8 Size: 52(0x34) bytes Array: Rank 1, Number of elements 7, Type Int32 Element Methodtable: 00007ffcc6b385a0 [0] 0000019a471a5508 Name: System.Int32 MethodTable: 00007ffcc6b385a0 EEClass: 00007ffcc6ca6078 Size: 24(0x18) bytes File: C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll Fields: MT Field Offset Type VT Attr Value Name 00007ffcc6b385a0 40005a2 0 System.Int32 1 instance 1 m_value [1] 0000019a471a550c Name: System.Int32 MethodTable: 00007ffcc6b385a0 EEClass: 00007ffcc6ca6078 Size: 24(0x18) bytes File: C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll Fields: MT Field Offset Type VT Attr Value Name 00007ffcc6b385a0 40005a2 0 System.Int32 1 instance 0 m_value [2] 0000019a471a5510 Name: System.Int32 MethodTable: 00007ffcc6b385a0 EEClass: 00007ffcc6ca6078 Size: 24(0x18) bytes File: C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll Fields: MT Field Offset Type VT Attr Value Name 00007ffcc6b385a0 40005a2 0 System.Int32 1 instance 2 m_value [3] 0000019a471a5514 Name: System.Int32 MethodTable: 00007ffcc6b385a0 EEClass: 00007ffcc6ca6078 Size: 24(0x18) bytes File: C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll Fields: MT Field Offset Type VT Attr Value Name 00007ffcc6b385a0 40005a2 0 System.Int32 1 instance -1 m_value [4] 0000019a471a5518 Name: System.Int32 MethodTable: 00007ffcc6b385a0 EEClass: 00007ffcc6ca6078 Size: 24(0x18) bytes File: C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll Fields: MT Field Offset Type VT Attr Value Name 00007ffcc6b385a0 40005a2 0 System.Int32 1 instance 3 m_value [5] 0000019a471a551c Name: System.Int32 MethodTable: 00007ffcc6b385a0 EEClass: 00007ffcc6ca6078 Size: 24(0x18) bytes File: C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll Fields: MT Field Offset Type VT Attr Value Name 00007ffcc6b385a0 40005a2 0 System.Int32 1 instance -1 m_value [6] 0000019a471a5520 Name: System.Int32 MethodTable: 00007ffcc6b385a0 EEClass: 00007ffcc6ca6078 Size: 24(0x18) bytes File: C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll Fields: MT Field Offset Type VT Attr Value Name 00007ffcc6b385a0 40005a2 0 System.Int32 1 instance -1 m_value
而後是entries的內存結構:
0:000> !da -start 0 -details 00000237effb2fa8 Name: System.Collections.Generic.Dictionary`2+Entry[[System.Int32, mscorlib],[System.String, mscorlib]][] MethodTable: 00007ffa965135b8 EEClass: 00007ffa9662d1f0 Size: 192(0xc0) bytes Array: Rank 1, Number of elements 7, Type VALUETYPE Element Methodtable: 00007ffa96513558 [0] 00000237effb2fb8 Name: System.Collections.Generic.Dictionary`2+Entry[[System.Int32, mscorlib],[System.String, mscorlib]] MethodTable: 00007ffa96513558 EEClass: 00007ffa966304e8 Size: 40(0x28) bytes File: C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll Fields: MT Field Offset Type VT Attr Value Name 00007ffa964a85a0 4003502 8 System.Int32 1 instance 1 hashCode 00007ffa964a85a0 4003503 c System.Int32 1 instance -1 next 00007ffa964a85a0 4003504 10 System.Int32 1 instance 1 key 00007ffa964aa238 4003505 0 System.__Canon 0 instance 00000237effb2db0 value [1] 00000237effb2fd0 Name: System.Collections.Generic.Dictionary`2+Entry[[System.Int32, mscorlib],[System.String, mscorlib]] MethodTable: 00007ffa96513558 EEClass: 00007ffa966304e8 Size: 40(0x28) bytes File: C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll Fields: MT Field Offset Type VT Attr Value Name 00007ffa964a85a0 4003502 8 System.Int32 1 instance 7 hashCode 00007ffa964a85a0 4003503 c System.Int32 1 instance -1 next 00007ffa964a85a0 4003504 10 System.Int32 1 instance 7 key 00007ffa964aa238 4003505 0 System.__Canon 0 instance 00000237effb2db0 value [2] 00000237effb2fe8 Name: System.Collections.Generic.Dictionary`2+Entry[[System.Int32, mscorlib],[System.String, mscorlib]] MethodTable: 00007ffa96513558 EEClass: 00007ffa966304e8 Size: 40(0x28) bytes File: C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll Fields: MT Field Offset Type VT Attr Value Name 00007ffa964a85a0 4003502 8 System.Int32 1 instance 2 hashCode 00007ffa964a85a0 4003503 c System.Int32 1 instance -1 next 00007ffa964a85a0 4003504 10 System.Int32 1 instance 2 key 00007ffa964aa238 4003505 0 System.__Canon 0 instance 00000237effb2db0 value [3] 00000237effb3000 Name: System.Collections.Generic.Dictionary`2+Entry[[System.Int32, mscorlib],[System.String, mscorlib]] MethodTable: 00007ffa96513558 EEClass: 00007ffa966304e8 Size: 40(0x28) bytes File: C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll Fields: MT Field Offset Type VT Attr Value Name 00007ffa964a85a0 4003502 8 System.Int32 1 instance 4 hashCode 00007ffa964a85a0 4003503 c System.Int32 1 instance -1 next 00007ffa964a85a0 4003504 10 System.Int32 1 instance 4 key 00007ffa964aa238 4003505 0 System.__Canon 0 instance 00000237effb2db0 value [4] 00000237effb3018 Name: System.Collections.Generic.Dictionary`2+Entry[[System.Int32, mscorlib],[System.String, mscorlib]] MethodTable: 00007ffa96513558 EEClass: 00007ffa966304e8 Size: 40(0x28) bytes File: C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll Fields: MT Field Offset Type VT Attr Value Name 00007ffa964a85a0 4003502 8 System.Int32 1 instance 0 hashCode 00007ffa964a85a0 4003503 c System.Int32 1 instance 0 next 00007ffa964a85a0 4003504 10 System.Int32 1 instance 0 key 00007ffa964aa238 4003505 0 System.__Canon 0 instance 0000000000000000 value [5] 00000237effb3030 Name: System.Collections.Generic.Dictionary`2+Entry[[System.Int32, mscorlib],[System.String, mscorlib]] MethodTable: 00007ffa96513558 EEClass: 00007ffa966304e8 Size: 40(0x28) bytes File: C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll Fields: MT Field Offset Type VT Attr Value Name 00007ffa964a85a0 4003502 8 System.Int32 1 instance 0 hashCode 00007ffa964a85a0 4003503 c System.Int32 1 instance 0 next 00007ffa964a85a0 4003504 10 System.Int32 1 instance 0 key 00007ffa964aa238 4003505 0 System.__Canon 0 instance 0000000000000000 value [6] 00000237effb3048 Name: System.Collections.Generic.Dictionary`2+Entry[[System.Int32, mscorlib],[System.String, mscorlib]] MethodTable: 00007ffa96513558 EEClass: 00007ffa966304e8 Size: 40(0x28) bytes File: C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll Fields: MT Field Offset Type VT Attr Value Name 00007ffa964a85a0 4003502 8 System.Int32 1 instance 0 hashCode 00007ffa964a85a0 4003503 c System.Int32 1 instance 0 next 00007ffa964a85a0 4003504 10 System.Int32 1 instance 0 key 00007ffa964aa238 4003505 0 System.__Canon 0 instance 0000000000000000 value
從內存的結構來看,擴容後bucket數組中使用了下標爲0、一、2和4這四個位置,entries中使用了0~3存儲了示例中添加的數據,符合前文中理論分析的結果,二者相互之間具備良好的印證關係。
時間複雜度表達的是數據結構操做數據的時候所消耗的時間隨着數據集規模的增加的變化趨勢。經常使用的指標有最好狀況時間複雜度、最壞狀況時間複雜度和均攤時間複雜度。那麼對於Dictionary<Tkey,TValue>
來講,插入和查找過程當中這些時間複雜度分別是什麼樣的呢?
最好狀況時間複雜度:對於查找來講最好的是元素處於鏈表的頭部,查找效率不會隨着數據規模的增長而增長,所以該複雜度爲常量階複雜度,即O(1);插入操做最理想的狀況是數組中有空餘的空間,不須要進行擴容操做,此時時間複雜度也是常量階的,即O(1);
最壞狀況時間複雜度:對於插入來講,比較耗時的操做場景是須要順着鏈表查找符合條件的元素,鏈表越長,查找時間越長(下文稱爲場景一);而對於插入來講最壞的狀況是數組長度不足,須要動態擴容並從新組織鏈表結構(下文稱爲場景二);
場景一中時間複雜度隨着鏈表長度的增長而增長,可是Dictionary
中規定鏈表的最大長度爲100,若是有長度超過100的鏈表就須要擴容並調整鏈表結構,因此順着鏈表查找數據不會隨着數據規模的增加而增加,最大時間複雜度是固定的,所以時間複雜度仍是常量階複雜度,即O(1);
場景二中時間複雜度隨着數組中元素的數量增長而增長,若是原來的數組元素爲n個,那麼擴容時須要將這n個元素拷貝到新的數組中並計算其在新鏈表中的位置,所以該操做的時間複雜度是隨着數組的長度n的增長而增長的,屬於線性階時間複雜度,即O(n)。
綜合場景一和場景二的分析結果得出最壞狀況時間複雜度出如今數據擴容過程當中,時間複雜度爲O(n)。
最好狀況時間複雜度和最壞狀況時間複雜度都過於極端,只能描述最好的狀況和最壞的狀況,那麼在使用過程當中如何評價數據結構在大部分狀況下的時間複雜度?一般對於複雜的數據結構可使用均攤時間複雜度來評價這個指標。均攤時間複雜度適用於對於一個數據進行連續操做,大部分狀況下時間複雜度都很低,只有個別狀況下時間複雜度較高的場景。這些操做存在先後連貫性,這種狀況下將較高的複雜度攤派到以前的操做中,通常均攤時間複雜度就至關於最好狀況時間複雜度。
經過前面的分析能夠看出Dictionary
剛好就符合使用均攤時間複雜度分析的場景。以插入操做爲例,假設預申請的entries
數組長度爲n,在第n+1次插入數據時必然會遇到一次數組擴容致使的時間消耗較高的場景。將此次較爲複雜的操做的時間均攤到以前的n次操做後,每次操做時間的複雜度也是常量階的,所以Dictionary
插入數據時均攤時間複雜度也是O(1)。
首先,Dictionary這種類型適合用於對於數據檢索有明顯目的性的場景,好比讀寫比例比較高的場景。其次,若是有大數據量的場景,最好可以提早聲明容量,避免屢次分配內存帶來的額外的時間和空間上的消耗。由於不進行擴容的場景,插入和查找效率都是常量階O(1),在引發擴容的狀況下,時間複雜度是線性階O(n)。若是僅僅是爲了存儲數據,使用Dictionary並不合適,由於它相對於List<T>
具備更加複雜的數據結構,這樣會帶來額外的空間上面的消耗。雖然Dictionary<TKey,TValue>
的TKey是個任意類型的,可是除非是對於判斷對象是否相等有特殊的要求,不然不建議直接使用自定義類做爲Tkey。
C#中的Dictionary<TKey,TValue>
是藉助於散列函數構建的高性能數據結構,Dictionary
解決散列衝突的方式是使用鏈表來保存散列值存在衝突的節點,俗稱拉鍊法。本文中經過圖文並茂的方式幫助理解Dictionary
添加元素和擴容過程這兩個典型的應用場景,在理論分析以後使用windbg分析了內存中的實際結構,以此加深對於這種數據類型的深刻理解。隨後在此基礎上分析了Dictionary
的時間複雜度。Dictionary
的最好狀況時間複雜度是O(1),最壞狀況複雜度是O(n),均攤時間複雜度是O(1),Dictionary
在大多數狀況下都是常量階時間複雜度,在內部數組自動擴容過程當中會產生明顯的性能降低,所以在實際實踐過程當中最好在聲明新對象的時候可以預估容量,盡力避免數組自動擴容致使的性能降低。