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

时间:2021-12-27 19:20:37

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

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