淺談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
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...