C# 有關List的Contains與Equals方法

【以下內容僅為本人在學習中的所感所想,本人水平有限目前尚處學習階段,如有錯誤及不妥之處還請各位大佬指正,請諒解,謝謝!】

 

!!!觀前提醒!!!

【本文內容可能較為複雜,雖然我已經以較為清晰的方式展現我的思想,但可能依舊容易引起思維混亂,若感到混亂或不舒服的情況,可直接轉跳至文末的總結處;也可以先看完結論再來閱讀文章】

 

引言

List作為一種動態存儲結構,可以代替數組,還可以將其當作鏈表使用。本文將介紹C#中List的相關內容,重點介紹其包含的Contains與Equals方法,並針對集合的比較與去重進行分析,提供可行的解決方案。

起因

題目鏈接:6049. 含最多 K 個可整除元素的子數組 – 力扣(LeetCode) (leetcode-cn.com)

 

實際上就是在一個數組的所有非空「子數組」中,統計其中滿足要求的子數組的數量。

經過與結果

想法:枚舉每一個子數組,判斷其是否符合要求;若符合則判斷該子數組是否應經被統計過;未統計則加入,已統計則跳過。

結果:(我真的是匪夷所思啊!!!)

 

【註:將HashSet換為List運行結果相同】      當換用字符串為元素後,結果卻沒有問題且AC通過

 

第一部分 關於List

【註:此部分屬於介紹內容,非本文重點,可轉跳至第二部分

在分析上面問題之前,我們先對List有一個認知。List是一個C#中最常見的可伸縮數組組件,即動態數據存儲結構。我們常常用它來替代數組,並且因為它的長度是動態的,所以我們在寫的時候不用手動去分配的大小;甚至有時我們也會拿它當鏈表使用。

屬性

(一)Count

定義:獲取 List<T>中包含的元素數。

(二)Capacity

(1)定義:獲取或設置該內部數據結構在不調整大小的情況下能夠容納的元素總數。

(2)可能觸發的異常:

ArgumentOutOfRangeException:Capacity 被設置為一個小於現有長度的值。

OutOfMemoryException:系統上沒有足夠的可用內存。

小結

(1)Capacity是可能需要調整大小之前存儲的元素數目,以4為基數,每次遞增4;Count是實際位於其中List<T>元素的數目。

(2)Capacity始終大於等於Count。如果在添加元素時Count超過Capacity,則會在添加元素前自動重新分配Capacity的值;在清空List<T>或刪除元素後Capacity的值不會變化。

(3)可通過TrimExcess()方法刪除多餘的預留空間,即使Capacity的值等於Count的值。

(3)獲取此屬性的值的運算時間複雜度為 O(1)。

(三)Item[index]

(1)定義:獲取或設置指定索引處的元素。

(2)可能觸發的異常:

ArgumentOutOfRangeException:index小於0或大於等於 Count。

常用方法

(一)添加

(1)Add(T)   將對象添加到末尾

(2)AddRange(List<T>)   將集合添加到末尾【註:List<T>非空】

(3)Insert(int index, T)   將元素插入指定索引處

(4)InsertRange(int index, List<T>)   將集合插入指定索引處【註:List<T>非空】

(二)刪除

(1)Remove(T)    移除指定對象的第一個匹配項

(2)RemoveAll(Predicate<T> match)     移除與match相匹配的所有元素

(3)RemoveAt(int num)   移除指定s索引處的元素

(4)RemoveRange(int index, int count)     移除指定範圍內的元素

(三)查找

(1)Find(Predicate<T> match)   搜索並返回第一個與match相匹配的元素

(2)FindIndex(Predicate<T> match) 返回第一個與match相匹配的元素的索引值

(3)FindIndex(int startIndex, Predicate<T> match)   從startIndex開始搜索,返回第一個與match相匹配的元素的索引值

(4)FindIndex(int startIndex, int count, Predicate<T> match) 從startIndex開始搜索count位,返回第一個與match相匹配的元素的索引值

【註:FindLast、FindLastIndex從後往前搜索,其餘部分相同】

(5)IndexOf(T)   搜索指定對象,返回第一個匹配項從零開始的索引

(6)Find(T, int index)  在[index, count)範圍內搜索指定對象,返回第一個匹配項從零開始的索引

(7)Find(T, int index, int count) 在[index, index + count]範圍內搜索指定對象,返回第一個匹配項從零開始的索引

(四)排序

(1)Sort()   使用默認比較器(升序)對List進行排序

(2)Sort(IComparer<T>)  使用指定比較器對List進行排序

(3)Sort(int index, int count, IComparer<T>) 在指定範圍內使用指定比較器(不寫,則為默認比較器)對List進行排序

 

第二部分 關於List之間的包含與比較

(一)包含——Contains

註:元素之間的比較較為簡單,在此不做敘述

C#中變量可分為值類型和引用類型。值類型儲存在棧中,引用類型儲存在堆中,引用類型的內存地址儲存在棧中。其中:泛型List的Contains在比較值類型時,直接比較值,但在比較引用類型時,比較的是引用地址

雖然明白了Contains的比較原理,但這時候又出現了一個問題:List<T>是引用類型,string也是引用類型,那為什麼在判斷string是否相同時可以達到預期效果而判斷List<T>時不能達到預期效果呢?針對這個問題,我們直接查看一下這兩個類型所聲明的變量的內存地址。

(1)   泛型類型為List<T>此時,內部儲存的元素為集合

 

可以發現:

 a. 兩個字符串值相同,則其內存地址相同

    b.  兩個List<T>值相同,但其內存地址不同

    c. 當泛型類型為集合時,即使兩個集合中儲存的元素值相同且元素內存地址相同,但這兩個集合本身的內存地址不同,即list1的地址不同於list2。而根據Contains的查找規律,無法將這兩個集合視為同一個集合。

所以我們所認為的集合相同是內部元素相同,而C#所認為的集合相同是內存地址的相同。

2)泛型類型為string此時,內部儲存的元素為字符串

內部元素不同:

 

內部元素相同:

 

可以發現:

 a. 兩個字符串值相同,則其內存地址相同

 b. 當泛型類型為字符串時,只有兩個字符串的內存地址相同,即str1的地址等同於str2,所以可將這兩個元素視為同一個元素。

(3)泛型類型為自定義類型此時,內部儲存的元素為自定義對象

 

通過上面這個例子可以發現:

 a. 兩個自定義對象值相同,但其內存地址不同

 b. 對象再傳遞時,傳遞的是內存地址

 c. 當泛型類型為自定義對象類型時,只有兩個對象的內存地址相同時,才判定這兩個對象是同一個對象。

小結

至此,我們得出List. Contains()方法的執行原理:

 a. 在查找是否包含時,查找的是針對定義的泛型T,所存儲的對象,與對象中再存儲的元素無關。

 b. 在查找是否包含值類型時,直接比較值;查找引用類型時,先比較內存地址,若內存地址相同則返回True;否則再比較值。

 

(二)比較——Equals

1)泛型類型為List<T>此時,內部儲存的元素為集合

 

 

2)泛型類型為string此時,內部儲存的元素為字符串

內部元素不同:

 

內部元素相同:

 

直接使集合本身相同:

 

(3)泛型類型為自定義類型此時,內部儲存的元素為自定義對象

 

 

 

小結

 a. 通過以上兩個例子,我們可以總結出List. Equals()方法的執行原理:在比較值類型時,直接比較值;在比較引用類型時,只比較內存地址。

回到開頭的題目

為什麼第一種方法是錯的,因為執行Contains操作的對象是是兩個集合,雖然兩個集合內部存儲元素相同,但兩個集合本身的內存地址是不同的,所以總會被判定為是不同的元素,因而不能用於判重;

第二種方法使用字符串,操作對象即為字符串,相同的字符串值,內存地址相同,所以可以用來判重。

總結

 a. 不論是Contains方法還是Equals方法,首先要明確操作的對象,即要查找或比較的是哪兩個對象。

 b. 確定對象後,判斷其內存地址是否相同,相同返回True,不同返回False。

 c. 一般地,引用類型傳遞時,傳遞的是內存地址,且相同值的引用類型內存地址不同特別地,當字符串值相同時,內存地址也相同。

【感謝您可以抽出時間閱讀到這裡,第一篇博客可能會有許多不妥之處;受限於水平,許多地方可能存在錯誤,還請各位大佬留言指正,請見諒,謝謝!】