1. 程式人生 > >C#中經常使用容器的使用與底層數據結構

C#中經常使用容器的使用與底層數據結構

取消 移除 insert asp 占用空間 裝箱 想要 list() eric

從使用的頻率一個個來簡單說一下。

Array/ArrayList/List/LinkedList

Array

數組在C#中最早出現的。在內存中是連續存儲的,所以它的索引速度非常快,並且賦值與改動元素也非常easy。


string[] s=new string[2]; 

//賦值 
s[0]="a"; 
s[1]="b"; 
//改動 
s[1]="a1"; 




可是數組存在一些不足的地方。在數組的兩個數據間插入數據是非常麻煩的,並且在聲明數組的時候必須指定數組的長度。數組的長度過長,會造成內存浪費,過段會造成數據溢出的錯誤。假設在聲明數組時我們不清楚數組的長度。就會變得非常麻煩。


針對數組的這些缺點,C#中最先提供了ArrayList對象來克服這些缺點。

底層數據結構就是數組。


ArrayList
ArrayList是命名空間System.Collections下的一部分。在使用該類時必須進行引用,同一時候繼承了IList接口。提供了數據存儲和檢索。ArrayList對象的大小是依照當中存儲的數據來動態擴充與收縮的。所以。在聲明ArrayList對象時並不須要指定它的長度。

ArrayList list1 = new ArrayList(); 

//新增數據 
list1.Add("cde"); 
list1.Add(5678); 

//改動數據 
list[2] = 34; 

//移除數據 
list.RemoveAt(0); 

//插入數據 
list.Insert(0, "qwe"); 


從上面樣例看。ArrayList好像是攻克了數組中全部的缺點,為什麽又會有List?
我們從上面的樣例看,在List中。我們不僅插入了字符串cde。並且插入了數字5678。

這樣在ArrayList中插入不同類型的數據是同意的。

由於ArrayList會把全部插入當中的數據當作為object類型來處理,在我們使用ArrayList處理數據時。非常可能會報類型不匹配的錯誤,也就是ArrayList不是類型安全的。在存儲或檢索值類型時通常發生裝箱和取消裝箱操作,帶來非常大的性能耗損。
裝箱與拆箱的概念:
簡單的說:
裝箱:就是將值類型的數據打包到引用類型的實例中
比方將string類型的值abc賦給object對象obj


String i=”abc”; 
object obj=(object)i; 



拆箱:就是從引用數據中提取值類型
比方將object對象obj的值賦給string類型的變量i

object obj=”abc”; 
string i=(string)obj; 


裝箱與拆箱的過程是非常損耗性能的。


底層數據結構就是數組。相似於C++裏面沒有泛型的Vector。


泛型List

由於ArrayList存在不安全類型與裝箱拆箱的缺點。所以出現了泛型的概念。List類是ArrayList類的泛型等效類。它的大部分使用方法都與ArrayList相似。由於List類也繼承了IList接口。

最關鍵的差別在於,在聲明List集合時,我們同一時候須要為其聲明List集合內數據的對象類型。


比方:

List<string> list = new List<string>(); 
//新增數據 
list.Add(“abc”); 
//改動數據 
list[0] = “def”; 
//移除數據 
list.RemoveAt(0); 



上例中。假設我們往List集合中插入int數組123。IDE就會報錯。且不能通過編譯。這樣就避免了前面講的類型安全問題與裝箱拆箱的性能問題了。


底層數據結構就是數組。

相似於C++裏面的Vector。


LinkedList

用雙鏈表實現的List。特點是插入刪除快,查找慢

LinkedList<T> 提供 LinkedListNode<T> 類型的單獨節點,因此插入和移除的運算復雜度為 O(1)。


能夠移除節點,然後在同一列表或其它列表中又一次插入它們。這樣在堆中便不會分配額外的對象。

由於該列表還維護內部計數。因此獲取 Count 屬性的運算復雜度為 O(1)。
LinkedList<T> 對象中的每一個節點都屬於 LinkedListNode<T> 類型。由於 LinkedList<T> 是雙向鏈表,因此每一個節點向前指向 Next 節點,向後指向 Previous 節點。

LinkedList<string> list = new LinkedList<string>();
list.AddFirst("Data Value 1");
list.AddLast("Data Value 6");

關於List和LonkedList的一個性能比較

添加 刪除
在List<T>中添加、刪除節點的速度。大體上快於使用LinkedList<T>時的同樣操作。將 List<T>.Add方法和LinkedList<T>的Add*方法相比較。真正的性能差別不在於Add操作,而在LinkedList<T>在給GC(垃圾回收機制)的壓力上。一個List<T>本質上是將其數據保存在一個堆棧的數組上。而LinkedList<T>是將其全部節點保存在堆棧上(人家是一個,我是一系列)。這就使得GC須要很多其它地管理堆棧上LinkedList<T>的節點對象。

註意,List<T>.Insert*方法比在LinkedList<T>中使用Add*方法在不論什麽地方加入一個節點可能要慢。然而。這個依賴於List<T>插入對象的位置。Insert方法必須使全部在插入點後面的元素往後移動一位。假設新元素被插在List<T>最後或接近最後的位置,那麽相對於GC維護LinkedList<T>節點的總的開銷來說,其開銷是能夠被忽略的。

索引
還有一個List<T>性能優於LinkedList<T>的地方是你在使用索引進行訪問的時候。

在List<T>中,你能夠使用索引值(indexer)直接定位到某個詳細的元素位置。

而在LinkedList<T>中,卻沒有這種奢侈品。在LinkedList<T>中,你必須通過Previous或Next屬性遍歷整個List,直到找到你想要的節點。




HashSet/HashTable/Dictionary

這三個容器的底層都是Hash表。


HashSet

MSDN非常easy的解釋:表示值的集。


HashSet<T> 類提供了高性能的集運算。一組是一個集合,不包括不論什麽反復的元素,且的元素順序不分先後。

用了hash table來儲存數據。是為了用O(n)的space來換取O(n)的時間。也就是查找元素的時間是O(1)。

它包括一些集合的運算

HashSet<T> 提供了很多數學設置操作比如,組加入 (聯合),並設置減法。下表列出了所提供 HashSet<T> 操作和及其數學等效項。



UnionWith - Union 或將其設置的加入
IntersectWith - 交集
ExceptWith - Set 減法
SymmetricExceptWith - 余集


列出的集操作中,除了 HashSet<T> 類還提供了方法來確定 set 是否相等、 重疊的集,以及一組是否為子集或還有一個集的超集。

example

 HashSet<int> evenNumbers = new HashSet<int>();
HashSet<int> oddNumbers = new HashSet<int>();

for (int i = 0; i < 5; i++)
{
     // Populate numbers with just even numbers.
      evenNumbers.Add(i * 2);
     // Populate oddNumbers with just odd numbers.
     oddNumbers.Add((i * 2) + 1);
}
 // Create a new HashSet populated with even numbers.
HashSet<int> numbers = new HashSet<int>(evenNumbers);
 numbers.UnionWith(oddNumbers);


HashTable

表示依據鍵的哈希代碼進行組織的鍵/值對的集合。

Hashtable是System.Collections命名空間提供的一個容器,用於處理和表現相似key/value的鍵值對,當中key通常可用來高速查找,同一時候key是區分大寫和小寫。value用於存儲相應於key的值。Hashtable中key/value鍵值對均為object類型,所以Hashtable能夠支持不論什麽類型的key/value鍵值對.

他內部維護非常多對Key-Value鍵值對,其還有一個相似索引的值叫做散列值(HashCode),它是依據GetHashCode方法對Key通過一定算法獲取得到的。全部的查找操作定位操作都是基於散列值來實現找到相應的Key和Value值的

當前HashTable中的被占用空間達到一個百分比的時候就將該空間自己主動擴容。在.net中這個百分比是72%,也叫.net中HashTable的填充因子為0.72。

比如有一個HashTable的空間大小是100。當它須要加入第73個值的時候將會擴容此HashTable.

這個自己主動擴容的大小是多少呢?答案是當前空間大小的兩倍最接近的素數,比如當前HashTable所占空間為素數71,假設擴容,則擴容大小為素數131.

 Hashtable openWith = new Hashtable();

// Add some elements to the hash table. There are no 
// duplicate keys, but some of the values are duplicates.
openWith.Add("txt", "notepad.exe");
openWith.Add("bmp", "paint.exe");
openWith.Add("dib", "paint.exe");
openWith.Add("rtf", "wordpad.exe");



HashTable也有Boxing和Unboxing的開銷。

然後就有了


Dictionary

Dictionary也是鍵值容器。存入對象是須要與[key]值一一相應的存入該泛型。相對於HashTable,相似於List和ArrayList的關系。它是類型安全的。


Dictionary<string, string> myDic = new Dictionary<string, string>();
myDic.Add("aaa", "111");
myDic.Add("bbb", "222");
myDic.Add("ccc", "333");
myDic.Add("ddd", "444");


小結


數組的容量是固定的,您僅僅能一次獲取或設置一個元素的值,而ArrayList或List<T>的容量可依據須要自己主動擴充、改動、刪除或插入數據。
數組能夠具有多個維度,而 ArrayList或 List< T> 始終僅僅具有一個維度。可是,您能夠輕松創建數組列表或列表的列表。特定類型(Object 除外)的數組 的性能優於 ArrayList的性能。 這是由於 ArrayList的元素屬於 Object 類型;所以在存儲或檢索值類型時通常發生裝箱和取消裝箱操作。

只是,在不須要又一次分配時(即最初的容量十分接近列表的最大容量),List< T> 的性能與同類型的數組十分相近。
在決定使用 List<T> 還是使用ArrayList 類(兩者具有相似的功能)時,記住List<T> 類在大多數情況下運行得更好並且是類型安全的。

假設對List< T> 類的類型T 使用引用類型,則兩個類的行為是全然同樣的。可是,假設對類型T使用值類型,則須要考慮實現和裝箱問題。



所以基本不怎麽用ArrayList.


還要註意的一點

在單線程的時候使用Dictionary更好一些。多線程的時候使用HashTable更好。



由於HashTable能夠通過Hashtable tab = Hashtable.Synchronized(new Hashtable());獲得線程安全的對象。

最後貼一個SOF上面的一個關於Dictionary和hashtable的問題...


Why is Dictionary preferred over hashtable?


FWIW, a Dictionary is a hash table.

If you meant "why do we use the Dictionary class instead of the Hashtable class?

", then it‘s an easy answer: Dictionary is a generic type, Hashtable is not. That means you get type safety with Dictionary, because you can‘t insert any random object into it, and you don‘t have to cast the values you take out.




參考

裝箱和取消裝箱(C# 編程指南)- https://msdn.microsoft.com/zh-cn/library/yz2be5wk.aspx

C#中數組、ArrayList和List三者的差別 - https://www.google.com.hk/webhp?sourceid=chrome-instant&ion=1&espv=2&ie=UTF-8#newwindow=1&safe=strict&q=C%23+hashset+%E5%BA%95%E5%B1%82

C#中經常使用容器的使用與底層數據結構