C#中常用容器的使用与底层数据结构

从使用的频率一个个来简单说一下。

Array/ArrayList/List/LinkedList

Array

数组在C#中最早出现的。在内存中是连续存储的,所以它的索引速度非常快,而且赋值与修改元素也很简单。

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很简单的解释:表示值的集。


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

阅读更多
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页