.NET中的HashSet及原理解析
在.NET中,System.Collection及System.Collection.Generic命名空間中提供了一系列的集合類(lèi),HashSet定義在System.Collections.Generic中,是一個(gè)不重復(fù)、無(wú)序的泛型集合,本文學(xué)習(xí)下HashSet的工作原理。
哈希表原理
HashSet是基于哈希表的原理實(shí)現(xiàn)的,學(xué)習(xí)HashSet首先要了解下哈希表。
哈希表(hash table, 也叫散列表)是根據(jù)key直接訪問(wèn)存儲(chǔ)位置的數(shù)據(jù)結(jié)構(gòu),它通過(guò)一個(gè)鍵值的函數(shù),將所需查詢(xún)的數(shù)據(jù)映射到表中一個(gè)位置來(lái)訪問(wèn),加快了查找速度。
上述函數(shù)即為哈希函數(shù),哈希函數(shù)應(yīng)盡量計(jì)算簡(jiǎn)單以提高插入、檢索效率;計(jì)算得到的地址應(yīng)盡量分布均勻,以降低哈希沖突;應(yīng)具有較大的壓縮性,以節(jié)省內(nèi)存。常見(jiàn)的哈希函數(shù)構(gòu)造方法有直接定址法、除留余數(shù)法、數(shù)字分析法等。HashSet采用除留余數(shù)法,將元素的HashCode除以某個(gè)常數(shù)(哈希表Size)的余數(shù)作為地址,常數(shù)通常選取一個(gè)素?cái)?shù)。
兩個(gè)相等的對(duì)象的哈希值相同,但兩個(gè)不等的對(duì)象的哈希值是有可能相同的,這就是哈希沖突。處理沖突的方法有開(kāi)放定址法、鏈表法、雙散列法等。HashSet使用鏈表法,將沖突元素放在鏈表中。
哈希表是一種用于高性能集合操作的數(shù)據(jù)結(jié)構(gòu),它有如下特點(diǎn):
- 無(wú)序、不重復(fù);插入、查找時(shí)間復(fù)雜度為O(1);
- 不使用索引;
- 容量不足時(shí)自動(dòng)擴(kuò)容,但擴(kuò)容成本高;
- 可提供很多高性能集合操作,如合并、裁剪等;
HashSet實(shí)現(xiàn)
HashSet內(nèi)置了兩個(gè)數(shù)組,如下。_buckets中存放由哈希函數(shù)計(jì)算得到的索引值,_buckets中的值從1開(kāi)始,因此在使用時(shí)需要-1。該值即為_(kāi)entries數(shù)組的相對(duì)索引,若未發(fā)生沖突,指向的值即為待查找元素的相對(duì)索引。如果發(fā)生了沖突,根據(jù)沖突鏈表也可以快速定位到元素。_entries存放的是Entry對(duì)象,Entry類(lèi)型如下所示。HashCode為元素的哈希值,在查找、插入、刪除、擴(kuò)容等操作時(shí)都會(huì)用到。Value存儲(chǔ)數(shù)據(jù)。Next在不同時(shí)刻有不同的作用,當(dāng)Entry在列表中時(shí),形成沖突鏈表,其N(xiāo)ext指向沖突鏈表的下一元素,鏈表最后一個(gè)元素的Next值為-1;若Entry已被列表刪除,形成空位鏈表,其N(xiāo)ext指向空位鏈表的下一元素,空位鏈表的最后一個(gè)元素值為-2。
HashSet還有幾個(gè)關(guān)鍵成員:_count、_freeList、_freeCount。_count表示添加元素?cái)?shù)量,注意它并不是實(shí)際存儲(chǔ)的元素?cái)?shù)量,因?yàn)樵趧h除元素時(shí)未更新它。_freeList為空位鏈表頭,其值指向被刪除的_entries索引,_entries[_freeList].Next指向下一空位的相對(duì)位置。_freeCount表示空位數(shù)量,列表實(shí)際存儲(chǔ)的元素?cái)?shù)量為_(kāi)count - _freeCount。
private int[]? _buckets; // _entries索引數(shù)組 private Entry[]? _entries; // 實(shí)體數(shù)組 private int _count; // 實(shí)際存儲(chǔ)的元素?cái)?shù)量 private int _freeList; // 空位鏈表頭索引,初始值-1 private int _freeCount; // 空位數(shù)量 private struct Entry { public int HashCode; public int Next; public T Value; } public int Count => _count - _freeCount; // _count不會(huì)記錄被刪除的元素,因此實(shí)際元素?cái)?shù)量為_(kāi)count - _freeCount
HashSet提供了多個(gè)構(gòu)造函數(shù)重載,如果不傳任何參數(shù),不會(huì)初始化_buckets和_entries。當(dāng)添元素時(shí),會(huì)調(diào)用Initialize(0)。Initialize方法接受一個(gè)int參數(shù),該參數(shù)表示需要初始化的列表容量。實(shí)際初始化的列表容量為大于等于該值的最小素?cái)?shù)。取素?cái)?shù)作為列表長(zhǎng)度是因?yàn)樵撝底鳛槭褂贸粲鄶?shù)法構(gòu)造的哈希函數(shù)的除數(shù),對(duì)素?cái)?shù)求余結(jié)果分布更均勻,減少了沖突的發(fā)生。
private int Initialize(int capacity) { int size = HashHelpers.GetPrime(capacity); // 獲取>=capacity的最小素?cái)?shù) var buckets = new int[size]; var entries = new Entry[size]; // ... return size; }
查找元素時(shí),首先調(diào)用元素的GetHashCode方法計(jì)算哈希值,然后調(diào)用GetBucketRef方法執(zhí)行哈希函數(shù)運(yùn)算,獲得索引。GetBucketRef的返回值-1為真實(shí)索引i,若i為-1,則未找到元素。若i>=0,表示列表中存在與待查找元素哈希值相同的元素,但相等的哈希值并不一定表示元素相等,還要進(jìn)一步判斷HashCode,若HashCode相等,再判斷元素是否相等,滿(mǎn)足則查找到元素,返回_entries的索引i。
private int FindItemIndex(T item) { // ... int hashCode = item != null ? item.GetHashCode() : 0; if (typeof(T).IsValueType) { int i = GetBucketRef(hashCode) - 1; // _buckets元素從1開(kāi)始 while (i >= 0) // 存在與item相同的哈希值,不一定存在item { ref Entry entry = ref entries[i]; if (entry.HashCode == hashCode && EqualityComparer<T>.Default.Equals(entry.Value, item)) { return i; // HashCode相等且元素相等,則查找到元素,返回_entries索引 } i = entry.Next; collisionCount++; // ... } } // ... return -1; } private ref int GetBucketRef(int hashCode) { int[] buckets = _buckets!; return ref buckets[(uint)hashCode % (uint)buckets.Length]; // 使用除留余數(shù)法構(gòu)造哈希函數(shù) }
插入元素時(shí),首先會(huì)查找待插入的元素是否存在,HashSet是不重復(fù)的,因此若插入元素已存在會(huì)直接返回false。若不存在元素,則會(huì)尋找存放元素的index。如果存在刪除后的空位,則會(huì)將元素放到_freeList指向的空位上;如果不存在空位,則按_entries順序插入元素。找到index后,即可將元素的HashCode及元素賦值到_entries[index]對(duì)應(yīng)字段,當(dāng)沒(méi)有沖突時(shí),Next值為-1;若存在沖突,則形成鏈表,將其添加到鏈表頭,Next指向沖突的下一位置。
private bool AddIfNotPresent(T value, out int location) { bucket = ref GetBucketRef(hashCode); // bucket為ref int類(lèi)型,若不存在沖突,bucket應(yīng)為0,因?yàn)閕nt默認(rèn)值為0 // ... int index; if (_freeCount > 0) // 存在刪除后的空位,將元素放在空位上 { index = _freeList; _freeCount--; // 更新刪除后空位數(shù)量 _freeList = StartOfFreeList - entries[_freeList].Next; // 更新空位索引 } else // 按_entries順序存儲(chǔ)元素 { int count = _count; if (count == entries.Length) // 容量不足,擴(kuò)容 { Resize(); bucket = ref GetBucketRef(hashCode); } index = count; _count = count + 1; entries = _entries; } { // 賦值 ref Entry entry = ref entries![index]; entry.HashCode = hashCode; entry.Next = bucket - 1; // 若不存在沖突則為-1,否則形成鏈表,指向沖突的下一元素索引 entry.Value = value; bucket = index + 1; // 此處對(duì)bucket賦值,即改變_buckets對(duì)應(yīng)元素,即將以插入元素哈希值為索引的_buckets值指向存放插入元素的_entries的索引+1 _version++; location = index; } // ... return true; }
插入時(shí)若列表容量不足,會(huì)調(diào)用Resize方法進(jìn)行擴(kuò)容。擴(kuò)容后的大小為大于等于原大小2倍的最小素?cái)?shù)。獲取待擴(kuò)容的大小后,以新大小重新分配entries內(nèi)存,并調(diào)用Array.Copy方法將原內(nèi)容拷貝到新位置。由于列表長(zhǎng)度變了,因此哈希值會(huì)變,因此需要更新_buckets的內(nèi)容(_entries索引),同理entry.Next的值也要更新。
private void Resize() => Resize(HashHelpers.ExpandPrime(_count), forceNewHashCodes: false); public static int ExpandPrime(int oldSize) { int num = 2 * oldSize; if ((uint)num > 2146435069u && 2146435069 > oldSize) { return 2146435069; } return GetPrime(num); // 返回原大小2倍的最小素?cái)?shù) } private void Resize(int newSize, bool forceNewHashCodes) { var entries = new Entry[newSize]; Array.Copy(_entries, entries, count); // ... _buckets = new int[newSize]; for (int i = 0; i < count; i++) { ref Entry entry = ref entries[i]; if (entry.Next >= -1) // 更新_buckets內(nèi)容 { ref int bucket = ref GetBucketRef(entry.HashCode); // 獲取以新大小作為除數(shù)的哈希函數(shù)運(yùn)算得到的哈希值 entry.Next = bucket - 1; // 更新Next bucket = i + 1; // 更新_buckets的元素,指向重新計(jì)算的_entry索引+1 } } _entries = entries; }
當(dāng)刪除元素時(shí),首先查找待刪除元素是否存在。若哈希值存在沖突,會(huì)記錄沖突鏈表的上一索引。查找到元素后,需要更新沖突鏈表的指針。刪除元素后,會(huì)更新_freeCount空位數(shù)量,并將刪除元素索引賦值給_freeList,記錄刪除空位,添加到空位鏈表頭,其N(xiāo)ext指向下一空位的相對(duì)位置。插入元素時(shí),會(huì)將元素插入到_freeList記錄的空位索引處,并根據(jù)該空位的Next更新_freeList的值。
public bool Remove(T item) { int last = -1; int hashCode = item != null ? (_comparer?.GetHashCode(item) ?? item.GetHashCode()) : 0; ref int bucket = ref GetBucketRef(hashCode); int i = bucket - 1; while (i >= 0) { ref Entry entry = ref entries[i]; if (entry.HashCode == hashCode && (_comparer?.Equals(entry.Value, item) ?? EqualityComparer<T>.Default.Equals(entry.Value, item))) { // 找到待刪除元素 if (last < 0) // 待刪除元素位于鏈表頭部,更新_buckets元素值指向鏈表下一位置 { bucket = entry.Next + 1; } else // 待刪除元素非鏈表頭,需更新鏈表上一元素Next值 entries[last].Next = entry.Next; entry.Next = StartOfFreeList - _freeList; // 空位鏈表,記錄下一空位索引相對(duì)位置,插入時(shí)根據(jù)該值更新_freeList if (RuntimeHelpers.IsReferenceOrContainsReferences<T>()) entry.Value = default!; _freeList = i; // 記錄刪除元素位置,下次插入元素時(shí),會(huì)插入在此 _freeCount++; // 更新刪除后空位數(shù)量 return true; } last = i; // 存在沖突,記錄鏈表上一位置 i = entry.Next; } return false; }
總結(jié)
通過(guò)上文分析可以看出HashSet是個(gè)設(shè)計(jì)巧妙,使用靈活的數(shù)據(jù)結(jié)構(gòu)?;诠1淼乃枷耄琀ashSet的插入、查找速度很快,只需要簡(jiǎn)單的計(jì)算?;诖?,HashSet也具備了高性能集合運(yùn)算的條件,可以高效執(zhí)行合并、裁剪等運(yùn)算。但這也導(dǎo)致了HashSet無(wú)法存儲(chǔ)重復(fù)元素。刪除時(shí)空位鏈表的設(shè)計(jì)非常巧妙,但也導(dǎo)致了HashSet無(wú)序,也就無(wú)法使用索引。因此,當(dāng)選擇數(shù)據(jù)結(jié)構(gòu)時(shí),如果需要包含重復(fù)元素,或者需要有序,則應(yīng)考慮使用其它數(shù)據(jù)結(jié)構(gòu),如List。
另外,Dictionary與HashSet原理相同,只是HashSet只有Key,沒(méi)有Value。
參考文章
到此這篇關(guān)于.NET中的HashSet的文章就介紹到這了,更多相關(guān).NET中的HashSet內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解ASP.NET Core Web Api之JWT刷新Token
這篇文章主要介紹了詳解ASP.NET Core Web Api之JWT刷新Token,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-11-11asp.net下一個(gè)賬號(hào)不允許多個(gè)用戶(hù)同時(shí)在線(xiàn),重復(fù)登陸的代碼
asp.net下一個(gè)賬號(hào)不允許多個(gè)用戶(hù)同時(shí)在線(xiàn),重復(fù)登陸的代碼,需要的朋友可以參考下。2010-10-10Asp.net6.0?Swagger使用問(wèn)題及解決過(guò)程
這篇文章主要介紹了Asp.net6.0?Swagger使用備忘,文中介紹了在Docker中顯示OpenApiInfo的中文內(nèi)容,顯示xml注釋及如何顯示Header的問(wèn)題,需要的朋友可以參考下2022-05-05ASP.NET FileUpload 上傳圖片實(shí)例
Add a FileUpload control to the aspx page2009-09-09Ajax實(shí)現(xiàn)異步刷新驗(yàn)證用戶(hù)名是否已存在的具體方法
由于要做一個(gè)注冊(cè)頁(yè)面,看到許多網(wǎng)站上都是使用Ajax異步刷新驗(yàn)證用戶(hù)名是否可用的,所以自己也動(dòng)手做一個(gè)小實(shí)例2014-02-02asp.net提取多層嵌套json數(shù)據(jù)的方法
這篇文章主要介紹了asp.net提取多層嵌套json數(shù)據(jù)的方法,結(jié)合實(shí)例形式較為詳細(xì)的分析了asp.net解析json格式數(shù)據(jù)的步驟與相關(guān)操作技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2016-06-06.NET C#創(chuàng)建WebService服務(wù)簡(jiǎn)單實(shí)例
這篇文章主要為大家詳細(xì)介紹了.NET C# 創(chuàng)建WebService服務(wù)簡(jiǎn)單實(shí)例,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-05-05