淺談C# Dictionary實現原理

来源:https://www.cnblogs.com/xiaomowang/archive/2020/03/04/12405639.html
-Advertisement-
Play Games

使用C#已經有好多年頭了,然後突然有一天被問到C#Dictionary的基本實現,這讓我反思到我一直處於拿來主義,能用就好,根本沒有去考慮和學習一些底層架構,想想令人頭皮發麻。下麵開始學習一些我平時用得理所當然的東西,今天先學習一下字典,Dictionary 一、Dictionary源碼學習 Dic ...


使用C#已經有好多年頭了,然後突然有一天被問到C#Dictionary的基本實現,這讓我反思到我一直處於拿來主義,能用就好,根本沒有去考慮和學習一些底層架構,想想令人頭皮發麻。下麵開始學習一些我平時用得理所當然的東西,今天先學習一下字典,Dictionary

一、Dictionary源碼學習

Dictionary實現我們主要對照源碼來解析,目前對照的源碼版本是.Net Framwork4.8,源碼地址

這邊主要介紹Dictionary中幾個比較關鍵的類和對象,然後跟著代碼來走一遍插入、刪除和擴容的流程。

1、Entry結構體

首先,我們引入Entry這樣一個結構體,它的定義如下麵代碼所示,這是Dictionary中存放數據的最小單位,調用Add(Key,Value)方法添加的元素都會被封裝在這樣的一個結構體中。

1         private struct Entry 
2         {
3             public int hashCode;    // Lower 31 bits of hash code, -1 if unused
4             public int next;        // Index of next entry, -1 if last
5             public TKey key;           // Key of entry
6             public TValue value;         // Value of entry
7         }

2、其他關鍵私有變數

1 private int[] buckets; // Hash桶
2 private Entry[] entries; // Entry數組,存放元素
3 private int count; // 當前entries的index位置
4 private int version; // 當前版本,防止迭代過程中集合被更改
5 private int freeList; // 被刪除Entry在entries中的下標index,這個位置是空閑的
6 private int freeCount; // 有多少個被刪除的Entry,有多少個空閑的位置
7 private IEqualityComparer<TKey> comparer; // 比較器
8 private KeyCollection keys; // 存放Key的集合
9 private ValueCollection values; // 存放Value的集合

3、Dictionary的構造

 1         private void Initialize(int capacity)
 2         {
 3             int prime = HashHelpers.GetPrime(capacity);
 4             this.buckets = new int[prime];
 5             for (int i = 0; i < this.buckets.Length; i++)
 6             {
 7                 this.buckets[i] = -1;
 8             }
 9             this.entries = new Entry<TKey, TValue>[prime];
10             this.freeList = -1;
11         }

我們看到,Dictionary在構造的時候做了以下幾件事:

1、初始化一個this.buchkets=new int[prime]

2、初始化一個this.entries=new Entry<TKey,TValue>[prime]

3、Bucket和entries的容量都為大於字典容量的一個最小的質數

其中this.buckets主要用來進行Hash碰撞,this.entries用來存儲字典的內容,並且標識下一個元素的位置。

4、Dictionary——Add操作

我們以Dictionary<int,string>為例,來展示一下Dictinoary如何添加元素:

首先,我們構造一個,容量為6:

Dictionary<int, string> test = new Dictionary<int, string>(6);

 

Test.Add(4,"4")

 

根據Hash演算法:4.GetHashCode()%7=4,因此碰撞到buckets中下表為4的槽上,此時由於Count為0,因此元素放在Entries中第0個元素上,添加後,Count變為1

 

 

 

Test.Add(11,"11")

 

 根據Hash演算法,11.GetHashCode()%7=4,因此再次碰撞到Buckets中下標為4的槽上,由於此槽上的值已經不是-1,此時Count=1,因此把這個新加的元素放到entries中下標為1的數組中,並且讓Buckets槽指向下表為1的entries中,下標為1的entry之下下表為0的entries。

 

 

Test.Add(18,"18")
Test.Add(19,"19")

 

 

 

 

 5、Dictionary——Remove操作

Test.Remove(4)

 

我們刪除元素時,通過一次碰撞,並且沿著鏈表尋找3次,找到key為4的元素所在的位置,刪除當前元素,並且把FreeList的位置指向當前刪除元素的位置,FreeCount置為1。

 

 刪除的數據會形成一個FreeList的鏈表,添加數據的時候,優先向FreeList鏈表中添加數據,FreeList為空則按照count一次排序。

6、Dictionary——Resize操作(擴容)

有細心的小伙伴可能看過Add操作後就想問了,buckets、entries不就是兩個數組麽,那萬一數組放滿了怎麼辦?接下來就是我要介紹的Resize(擴容)這樣一種操作,對我們的buckets、entries進行擴容。

6.1 擴容操作的觸發條件

首先我們需要直到在什麼情況下,會發生擴容操作:

第一種情況自然就是數組已經滿了,沒有辦法繼續存放新的元素,如下圖所示。

 第二種,Dictionary中發生的碰撞次數太多,會嚴重影響性能,也會出發擴容操作。

Hash運算會不可避免的產生衝突,Dictionary中使用拉鏈發來解決衝突的問題,但是,大家看下圖中的這種情況,所有的元素都剛好落在buckets[3]上面,結果就導致了時間複雜度O(n),查找性能會下降:

 

 6.2 擴容操作如何進行

為了給大家演示清楚,模擬了以下這種數據結構,大小為2的Dictionary,假設碰撞的閾值為2;現在出發Hash碰撞擴容。

1、申請兩倍於現在大小的buckets、entries

2、將現有的元素拷貝到新的entries

3、如果時Hash碰撞擴容,使用新HashCode函數重新計算Hash值

4、對entries每個元素bucket=newEntries[i].hashCode%newSize確定新buckets位置

5、重建hash鏈,newEntries[i].next=buckets[bucket];buckets[bucket]=i;

關註點

對於Dictionary的實現原理,其中有兩個關鍵的演算法,1、Hash演算法。2、用於對應Hash碰撞衝突解決演算法。

二、Hash演算法

Hash演算法是一種術宇摘要演算法,它將能不定長度的二進位數據集給映射到一個較短的二進位長度數據集。

實現了Hash演算法的函數我們叫它Hash函數,Hash函數有以下幾點特征。

1、相同的數據進行Hash運算,得到的結果一定是相同的,HashFunc(key1)==HashFunc(key1)

2、不同的數據進行Hash運算,其結果也可能會相同,(Hash會產生碰撞)。key1!=key2=>HashFunc(key1)==HashFunc(key2)。

3、Hash運算是不可逆的,不能由key獲取原始的數據,key1=>hashCode但是hashCode==>key1

 

 關於Hash碰撞下圖很清晰的就解釋了,可從圖中得知Sandra Dee 和 John Smith 通過hash運算後,落到了02位置,產生了碰撞和衝突。

 

 常見的構造Hash函數的演算法有以下幾種。

1、直接定址法:取keyword或者keyword的某個線性函數值為散列地址,即H(key)=key或者H(key)=a·key+b,當中a和b為常數(這樣的散列函數叫做自身函數)。這個的應用就是,比如我們世界地圖的掩碼,直接用坐標x*1000+坐標y,得到key。

2、數字分析法:找出數字的規律,儘可能利用這些數據來構造衝突幾率較低的散列地址。分析一組數據,比方一組員工的出生年月日,這時,我們發現出生年月日的前幾位數字大體相同,這種話,出現衝突的幾率就會非常大,可是我們發現年月日的後幾位表示月份和詳細日期的數字區別非常大,假設用後面的數字來構造散列地址,則衝突幾率就會明顯減少。

3、平方取中法:取keyword平方後的中間幾位作為散列地址。

4、摺疊法:將keyword切割成位數同樣的幾部分,最後一部分分數能夠不同,然後取這及部分的疊加和(去除進位)作為散列地址。

5、隨機數法:選擇一隨機函數,取keyword的隨機值作為散列地址,通常適用於keyword長度不同的場合。

6、除留餘數法:取keyword被某個不大於散列表表長m的數p除後所得的餘數為散列地址。即H(key)=key MOP p ,  p<=m。不僅能夠對keyword直接取模,也可在摺疊、平方取中等運算後取模,對p的選擇非常重要,一般取素數或m,若p選的不好,容易產生碰撞。

三、Hash桶演算法

說到Hash演算法大家就會想到Hash表,一個Key通過Hash函數運算後可快速的得到hashCode,通過hashCode的映射可以直接Get到Value。但是hashCode一般取值都是非常大的。經常是2^32以上,不可能對每個hashCode都指定一個映射。因為這樣的一個問題,所以人們就將生成的HashCode以分段的形式來映射,把每一段稱之為一個Bucket(桶),一般常見的Hash桶就是直接對結果取餘。

假設將生成的hashCode可能取值有2&32個,然後將其切分成一段一段,使用8個桶來映射,那麼就可以通過bucketIndex=HashFunc(key1)%8 這樣一個演算法來確定這個hashCode映射到具體哪個桶中。

Dictionary就是這用的哈希桶演算法。

int hashCode =comparer.GetHashCode(key)&0x7FFFFFFF;
int targetBucket = hashCode %buckets.Length;

四、Hash碰撞衝突解決演算法

對於一個hash演算法,不可避免地會產生衝突,那麼產生衝突以後如何處理,是一個很關鍵的地方,目前常見的衝突解決演算法有拉鏈法(Dictionary實現採用的)、開放定址法、再Hash法、公共溢出分區法。

1、拉鏈法(開散列):將產生衝突的元素建立一個單鏈表,並將頭指針地址存儲之Hash表對應桶的位置,這樣定位到Hash表桶的位置後通過遍歷單鏈表的形式來查找元素。

2、開放定址法(閉散列):當發生哈希衝突時,如果哈希表未被裝滿,說明再哈希表中必然還有空位置,那麼可以把key存放到衝突位置中的“下一個”空位置中去。

3、再Hash法:顧名思義就是將key使用其他的Hash函數再次Hash,直到找到不衝突的位置為止。

拉鏈法:

 

 開放地址法:

假設現在有一個關鍵碼集合(1、4、5、6、7、9),哈希結構的容量為10,哈希函數為Hash(key)=key%10。將所有關鍵碼插入到該哈希結構中,如圖:

 

 假如仙子啊有一個關鍵碼24要插入該結構中,使用哈希函數求得哈希地址為24,但是該地址已經存放了元素,此時發生哈希衝突。

線性探測:從發生哈希衝突的位置開始,一次向後探測,直到找到下一個空位置為止,例如上面的地址,插入關鍵碼24時,進行線性探測,插入後如下圖:

 

 限制:

1、用該方法需要關鍵碼必須為整形才能被模,所以我們需要實現將非整形轉化為整形。

2、模的數值最好為素數,需要我們創建一個素數表。

3、增容問題。

好了,關於Dictionary的相關知識,就先介紹到這裡了。

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 什麼是Websocket 我們在傳統的客戶端程式要實現實時雙工通訊第一想到的技術就是socket通訊,但是在web體系是用不了socket通訊技術的,因為http被設計成無狀態,每次跟伺服器通訊完成後就會斷開連接。 在沒有websocket之前web系統如果要做雙工通訊往往使用http long p ...
  • 以上是代碼 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace ConsoleApplication ...
  • 讓 mac 本地和自己的 github 網站建立連接(ssh) 下載安裝 git 網址: https://git-scm.com/downloads 查看安裝是否成功: git -version $ git version git version 2.15.1 (Apple Git-101) che ...
  • 帶著問題去思考,大家好! 問題1:HTTP請求和返回相應的HTTP響應信息之間發生了什麼? 1:首先是最底層,托管層,位於WebAPI和底層HTTP棧之間 2:其次是 消息處理程式管道層,這裡比如日誌和緩存。OWIN的引用是將消息處理程式管道的一些功能下移到棧下端的OWIN中間件了。 3:控制器處理 ...
  • 參考文檔: https://jingyan.baidu.com/article/ed15cb1bb5c3411be369819d.html https://blog.csdn.net/hzw19920329/article/details/53156015 https://blog.csdn.net ...
  • 《深入理解 C#》 (第2版) [作者] (英) Jon Skeet[譯者] (中) 周靖 朱永光 姚琪琳[出版] 人民郵電出版社[版次] 2012年01月 第1版[印次] 2012年01月 第1次 印刷[定價] 79.00元 【關於本書】 具體地說, C# 作為一種語言,它的基礎是各種各樣的 “框 ...
  • IP明細參考 先找到國內所有的IP "http://ipblock.chacuo.net/view/c_CN" 執行腳本 IIS白名單設置 powershell 如果嫌棄太慢,就直接改配置文件 先用腳本導入幾個,然後找到以下配置文件,找到需要修改的位置 加上一下內容 參考 "https://deja ...
  • 假定一個場景,開始做開發的你,領導走到你的面前說道:“小伙子,看了簡歷和最近的工作表現,很不錯,現在交給一個任務,開發一個簡單的CMS後端介面吧,前端有人配合你”,當時你內心讀白:“CMS什麼東西,還好我可以百度,但我要在哪個項目上開搞啊”,這時的領導又說道:“項目你自己建立,然後上傳git就行了” ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...