IEnumerable表明对象是不确定类型的集合并支持简单迭代,是不是定长根本不关心...
IEnumerable<T>表明对象是指定类型的集合并支持简单迭代,是不是定长根本不关心...
ICollection是IEnumerable接口的派生接口,表明对象是不确定类型的集合并支持简单迭代,而且定义了集合的大小、枚举数和同步方法,这里的大小是指可以是定长的也可以是不定长的...
IList是ICollection和IEnumerable的派生接口,表明对象是不确定类型的集合并支持简单迭代,而且定义了集合的大小、枚举数和同步方法,还可以按照索引单独访问,这里的大小是指可以是定长的也可以是不定长的...
ArrayList类是IList接口的实现,表明对象是不确定类型的大小可按需动态增加的数组...
List<T>类是IList<T>接口的实现,是ArrayList类的泛型等效类并增强了功能,表明对象是可通过索引访问的对象的强类型列表...在.NET 2.0以上可以完全代替ArrayList,就是说ArrayList已经被淘汰...
而动态数组和链表在本质上是不同的...在.NET 2.0以上有双向链表LinkedList<T>泛型类,它也是继承自ICollection<T>,IEnumerable<T>,ICollection,IEnumerable...
C# 集合类 Array Arraylist List Hashtable Dictionary Stack Queue
1.数组是固定大小的,不能伸缩。虽然System.Array.Resize这个泛型方法可以重置数组大小,
但是该方法是重新创建新设置大小的数组,用的是旧数组的元素初始化。随后以前的数组就废弃!而集合却是可变长的
2.数组要声明元素的类型,集合类的元素类型却是object.
3.数组可读可写不能声明只读数组。集合类可以提供ReadOnly方法以只读方式使用集合。
4.数组要有整数下标才能访问特定的元素,然而很多时候这样的下标并不是很有用。集合也是数据列表却不使用下标访问。
很多时候集合有定制的下标类型,对于队列和栈根本就不支持下标访问!
ArrayList 是数组的复杂版本。ArrayList 类提供在大多数 Collections 类中提供但不在 Array 类中提供的一些功能。例如:
Array 的容量是固定的,而 ArrayList 的容量是根据需要自动扩展的。如果更改了 ArrayList.Capacity 属性的值,则自动进行内存重新分配和元素复制。
ArrayList 提供添加、插入或移除某一范围元素的方法。在 Array 中,您只能一次获取或设置一个元素的值。
使用 Synchronized 方法可以很容易地创建 ArrayList 的同步版本。而 Array 将一直保持它直到用户实现同步为止。
ArrayList 提供将只读和固定大小包装返回到集合的方法。而 Array 不提供。
另一方面,Array 提供 ArrayList 所不具有的某些灵活性。例如:
可以设置 Array 的下限,但 ArrayList 的下限始终为零。
Array 可以具有多个维度,而 ArrayList 始终只是一维的。
特定类型(不包括 Object)的 Array 的性能比 ArrayList 好,这是因为 ArrayList 的元素属于 Object 类型,所以在存储或检索值类型时通常发生装箱和取消装箱。
要求一个数组的大多数情况也可以代之以使用 ArrayList。它更易于使用,并且通常具有与 Object 类型的数组类似的性能。
Array 位于 System 命名空间中;ArrayList 位于 System.Collections 命名空间中。
//数组
int[] intArray1;
//初始化已声明的一维数组
intArray1 = new int[3];
intArray1 = new int[3]{1,2,3};
intArray1 = new int[]{1,2,3};
//ArrayList类对象被设计成为一个动态数组类型,其容量会随着需要而适当的扩充
方法
1:Add()向数组中添加一个元素,
2:Remove()删除数组中的一个元素
3:RemoveAt(int i)删除数组中索引值为i的元素
4:Reverse()反转数组的元素
5:Sort()以从小到大的顺序排列数组的元素
6:Clone()复制一个数组
//ArrayList动态数组 定义 赋值 输出 ArrayList可以不用指定维数 可动态赋值 赋不同类型值
ArrayList arrayList1 = new ArrayList();
arrayList1.
arrayList1.Add("a");
arrayList1.Add(1);
arrayList1.Add("b");
Response.Write(arrayList1[1]);
//Array数组类 所有数组的基类 定义 赋值 输出 Array的容量是固定的 先指定大小 在赋值
Array arrayList2 = Array.CreateInstance(typeof(string), 6);
arrayList2.SetValue("a", 0);
arrayList2.SetValue("b", 1);
Response.Write(arrayList2.GetValue(1));
//数组 定义 赋值 输出 先指定大小 在赋值
string[] arrayList;
arrayList=new string[]{"A","B","C","D"};
arrayList[0] = "abcde";
arrayList[2] = "1234";
arrayList.SetValue("dd", 3);
Response.Write(arrayList[0]);
//哈希表
Hashtable abc = new Hashtable();
abc.Add("1", "34");
if (abc.Contains("1"))
{
Response.Write(abc["1"]);
}
//声明一个二维数组
int[,] cells=int[3,3];
//初始化一个二维整数数组
int[,] cells={{1,0,2},{1,2,0},{1,2,1}};
//List
可通过索引访问的对象的强类型列表。提供用于对列表进行搜索、排序和操作的方法
在决定使用 List 还是使用 ArrayList 类(两者具有类似的功能)时,记住 List 类在大多数情况下执行得更好并且是类型安全的。如果对 List 类的类型 T 使用引用类型,则
两个类的行为是完全相同的。但是,如果对类型 T 使用值类型,则需要考虑实现和装箱问题。
如果对类型 T 使用值类型,则编译器将特别针对该值类型生成 List 类的实现。这意味着不必对 List 对象的列表元素进行装箱就可以使用该元素,并且在创建大约 500 个列表
元素之后,不对列表元素装箱所节省的内存将大于生成该类实现所使用的内存。
//Dictionary
表示键和值的集合。Dictionary遍历输出的顺序,就是加入的顺序,这点与Hashtable不同
//SortedList类
与哈希表类似,区别在于SortedList中的Key数组排好序的
//Hashtable类
哈希表,名-值对。类似于字典(比数组更强大)。哈希表是经过优化的,访问下标的对象先散列过。如果以任意类型键值访问其中元素会快于其他集合。
GetHashCode()方法返回一个int型数据,使用这个键的值生成该int型数据。哈希表获取这个值最后返回一个索引,表示带有给定散列的数据项在字典中存储的位置。
//Stack类
栈,后进先出。push方法入栈,pop方法出栈。
Queue类
队列,先进先出。enqueue方法入队列,dequeue方法出队列。
-------------------------------------------------------------
//Dictionary
System.Collections.DictionaryEntry dic=new System.Collections.DictionaryEntry("key1","value1");
Dictionary<int, string> fruit = new Dictionary<int, string>();
//加入重复键会引发异常
fruit.Add(1, "苹果");
fruit.Add(2, "桔子");
fruit.Add(3, "香蕉");
fruit.Add(4, "菠萝");
//因为引入了泛型,所以键取出后不需要进行Object到int的转换,值的集合也一样
foreach (int i in fruit.Keys)
{
Console.WriteLine("键是:{0} 值是:{1}",i,fruit);
}
//删除指定键,值
fruit.Remove(1);
//判断是否包含指定键
if (fruit.ContainsKey(1))
{
Console.WriteLine("包含此键");
}
//清除集合中所有对象
fruit.Clear();
}
//ArrayList
System.Collections.ArrayList list=new System.Collections.ArrayList();
list.Add(1);
list.Add(2);
for(int i=0;i<list.Count;i++)
{
System.Console.WriteLine(list[i]);
}
//List
//声明一个List对象,只加入string参数
List<string> names = new List<string>();
names.Add("乔峰");
names.Add("欧阳峰");
names.Add("马蜂");
//遍历List
foreach (string name in names)
{
Console.WriteLine(name);
}
//向List中插入元素
names.Insert(2, "张三峰");
//移除指定元素
names.Remove("马蜂");
//HashTable
System.Collections.Hashtable table=new System.Collections.Hashtable();
table.Add("table1",1);
table.Add("table2",2);
System.Collections.IDictionaryEnumerator d=table.GetEnumerator();
while(d.MoveNext())
{
System.Console.WriteLine(d.Entry.Key);
}
//Queue
System.Collections.Queue queue=new System.Collections.Queue();
queue.Enqueue(1);
queue.Enqueue(2);
System.Console.WriteLine(queue.Peek());
while(queue.Count>0)
{
System.Console.WriteLine(queue.Dequeue());
}
//SortedList
System.Collections.SortedList list=new System.Collections.SortedList();
list.Add("key2",2);
list.Add("key1",1);
for(int i=0;i<list.Count;i++)
{
System.Console.WriteLine(list.GetKey(i));
}
//Stack
System.Collections.Stack stack=new System.Collections.Stack();
stack.Push(1);
stack.Push(2);
System.Console.WriteLine(stack.Peek());
while(stack.Count>0)
{
System.Console.WriteLine(stack.Pop());
}
最近研究Nhibernate,看着样例代码一知半解,苦恼万分,发现中间又引用了一个Iesi.Collections,不禁要产生疑问--为什么要专门引用一个集合类程序集?这个程序集里的集合数组类与.net自带的有什么不一样?结果此问题一出就一发不可收拾,扪心自问冒出了一大堆的问题--.net有哪些集合类?array和ArrayList有什么区别?Hashtable与集合有什么不一样?....等等.这时才意识到,如果对.net本身提供的集合类不了解,根本不会明白引用Iesi.Collections的用意.
由<<CLR via C#>>的书中所说:"所有的数组(如int[],FileStream[],object[])都隐式派生自System.Array,所有数组都隐式实现IEnumerable,ICollection,IList",所以先从Array开始研究,用Reflector工具找到Array源代码,发现除去一堆的静态方法如Sort,Find,BinarySearch以外,几乎就不剩什么东西了.
其中比较重要的一点是Array仅仅用显示接口实现了一个私有IList.Add方法,这意味着:Array实例没有办法调用Add方法,一旦初始化以后,长度不可变.
同样通过源代码可以看到ArrayList和Array的区别,ArrayList内置了一个Array变量 _items(代码中红色标出),
也就是说:ArrayList是Array的一层封装,实际数据操作还是针对Array.
ArrayList有Add方法,当Add方法发现内部的object[]容量已满时,便会调用一个方法自动扩充object[]容量,既然ArrayList的实质是操作object[],而Array长度不可变,那么如何扩充?其实说白了,就是通过调用EnsureCapacity方法再创建一个更长的object[]数组,然后把原数组复制到新的数组中.
ArrayList的很多方法如Sort,Indexof,内部都是调用了Array的静态方法,如IndexOf方法:
.net2.0以后出现了泛型,目的是为了避免装箱拆箱操作造成的性能损失.ArrayList对应的泛型集合就是List<T>.
由于泛型的出现,List<T>内部操作的不再是object[],而是T[],提供的很多方法如Sort,IndexOf等,同ArrayList类一样,内部也是调用Array的静态方法来操作数组.
因为Array的局限性,List<T>的一些方法会用到循环,如Find方法:
其中Predicate<T>是一个委托,在.net内部定义,需要一个类型为T的参数,返回值为bool.
知道这些可以避免我们把这些方法用在自己写的循环中,造成性能损失.
Array,ArrayList和List<T>在使用上如何选择呢?由于泛型可以避免装箱拆卸的性能损失,2.0以后的泛型集合基本上能淘汰以前的非泛型集合,所以ArrayList不做考虑,只比较Array和List<T>.
标准答案:确定了Array大小的时候,选用Array,不确定大小时,使用List<T>.
个人观点:如果数组是1维的,使用List<T>和Array之间的性能损失微乎其微,而List<T>可以带来更大的灵活性,因为很多时候想法和业务逻辑是多变的,建议多用List<T>.
刚写完后,查了博客园,发现这个问题在园子里已经被说的很透彻了,给出链接重新理解List<T>.
今天来探究哈希表,.net内置的哈希表容器是Hashtable类,而Dictionary<TKey,TValue>是对应的泛型哈希表.
一般我们实例化ArrayList或List<T>的时候,如果不指定容量,则其内部是赋值为一个静态的空数组。当有添加操作时,会实例化为一个长度为4的数组,如果容量满了以后,再添加,就会自动扩充为两倍的容量。
哈希表也有一个类似的情况,new Hashtable()如果不指定长度,则会将内置bucket数组的长度默认指定为11。如果给定一个值如new Hashtable(20),也并不会将bucket数组长度设置为20,而是循环内部一个素数数组,设置为刚好大于20的素数(也叫质数),此处是23。可知,哈希表内部存取数组长度总是一个素数。相关代码:
从源码可以得出一个结论:Hashtable总是创建一个比我们预想中要大一些的数组。
Hashtable内部包含了一个bucket类型的数组变量,bucket是一个结构,用来保存Key,Value和哈希值。
从上面代码可以看出Hashtable跟ArrayList,List<T>一样,内部都是用Array数组进行存储数据。既然同样是数组,Hashtable和ArrayList存取方式有什么不同?
当Hashtable添加数据时,(例如:hash.Add(key)) 首先调用当前键值key的GetHashCode()方法,得到哈希值(该值为Int32),取该哈希值的绝对值跟内部数组(上面代码的buckets)的总容量进行余运算(就是'%'操作),得到一个小于总容量的值,把该值当做数组序号,存在对应的位置,通过Key获取数组序号的过程叫映射。
当然,两个不同的key值有可能的得到相同的序号,这叫哈希冲突,在此不做分析。
下面是部分源码:
从Hashtable里取值的,也是通过映射Key值,找到数组中对应的存放索引位置,直接位置取出值,整个过程无需循环,时间复杂度为O(1),而ArrayList查找一个值则需要循环整个数组去逐个匹配,时间复杂度为O(n),由此可以看到哈希表在查询中的优势。
既然Hashtable本身也是数组存储,查询速度又快,那还要ArrayList做什么?存储数据的时候都用Hashtable不就行了?有句话说,XX打开了一扇门,就必然要关闭很多窗,现在分析一下Hashtable关闭的那些窗。
首先,哈希表不能有重复键值。
其次,哈希表会占用更多内存空间。
第一点显而易见,现在分析第二点。
我们知道ArrayList和List<T>在添加数值时,如果容量不足,会新建一个二倍容量的数组,然后把原数组数据复制过去,达到容量扩充的目的,所以使用ArrayList和List <T>的时候,建议在实例化方法里指定容量。
那么Hashtable在添加操作时碰到内部数组容量不足会怎么做?
其实处理方法和ArrayList基本相似,也是二倍扩充,但略有不同。不同点在于:
首先,扩充的大小比二倍略大。上面在实例化Hashtable那一部分中提到,由于哈希算法本身的特点,数组长度总是一个素数,所以当需要扩充时,新的数组长度是刚好大于原数组二倍长度的素数,也就是说略大于二倍。
其次,数组在未存满的情况下就要扩充。Hashtable有一个加载因子为0.72f。实际能存储的数据量等于内部Array长度* 0.72f。也就是说一个长度为11的数组,当存了7个数据以后,就不能再存储,需要要扩充容量。代码如下:
也就是说在Hashtable中,假如初始化了100个位置,只有72个可用,其他28个不能用,但仍然要占着地方。
由此我们可以知道,如果保存的数据量相同,Hashtable要比ArrayList占用更多的内存空间。
.net 2.0之后出现了泛型,泛型避免了装箱拆箱造成的损失,在大多数情况下,使用泛型集合要比传统的集合有较高的性能。List<T>对应ArrayList,Dictionary<TKey,TValue>对应Hashtable。
Dictionary<TKey,TValue>除了泛型带来的优势以外,还在哈希值算法,哈希冲突检测上做了改进,也没有了100个位置28个不能用的空间浪费。所以当碰到哈希表使用时,优先考虑Dictionary<TKey,TValue>。
最后总结:哈希表在查询方面有明显优势,但会占用多一点的内存空间,当业务中的集合有大量的查找操作时,优先考虑哈希表,一旦决定使用哈希表,优先考虑泛型哈希表。
.Net3.5之后出现了HashSet<T>,硬翻译过来就是“哈希集合”,跟“哈希”两字挂钩说明这种集合的内部实现用到了哈希算法,用Reflector工具就可以发现,HashSet<T>和Dictionary<TKey,TValue>使用了相同的存储方式和哈希冲突算法,那么,它跟Dictionary<TKey,TValue>和Hashtable在使用上到底有什么不同?
HashSet<T>是一个Set集合,虽然List、Collection也叫集合,但Set集合和它们却大有不同。
HashSet<T>提供了和“Set集合运算”相关的方法,如:
IntersectWith (IEnumerable<T> other) (交集)
public void IntersectWithTest() { HashSet<int> set1 = new HashSet<int>() { 1, 2, 3 }; HashSet<int> set2 = new HashSet<int>() { 2, 3, 4 }; set1.IntersectWith(set2); foreach (var item in set1) { Console.WriteLine(item); } //输出:2,3 }
UnionWith (IEnumerable<T> other) (并集)
public void UnionWithTest() { HashSet<int> set1 = new HashSet<int>() { 1, 2, 3 }; HashSet<int> set2 = new HashSet<int>() { 2, 3, 4 }; set1.UnionWith(set2); foreach (var item in set1) { Console.WriteLine(item); } //输出:1,2,3,4 }
ExceptWith (IEnumerable<T> other) (排除)
public void ExceptWithTest() { HashSet<int> set1 = new HashSet<int>() { 1, 2, 3 }; HashSet<int> set2 = new HashSet<int>() { 2, 3, 4 }; set1.ExceptWith(set2); foreach (var item in set1) { Console.WriteLine(item); } //输出:1 }
这些对集合的操作是List<T>、Hashtable和Dictionary<TKey,TValue>所缺少的,但是伴随着Linq和扩展方法的出现,.net 3.5为泛型集合提供了一系列的扩展方法,使得所有的泛型集合具备了set集合操作的能力。
例如与HashSet的IntersectWith 方法对应的扩展方法是IEnumerable<T> 的Intersect,两者的区别是:
HashSet<T>.IntersectWith 是对当前集合进行修改,没有返回值;
IEnumerable<T>.Intersect并不修改原集合,而是返回了一个新的集合。
实例代码如下:
public void IntersectTest() { HashSet<int> set1 = new HashSet<int>() { 1, 2, 3 }; HashSet<int> set2 = new HashSet<int>() { 2, 3, 4 }; IEnumerable<int> set3=set1.Intersect(set2); foreach (var item in set1) { Console.WriteLine(item); } foreach (var item in set3) { Console.WriteLine(item); } //输出:o //set1 : 1,2,3 //set3 : 2,3 }
IEnumerable<T> 其他的扩展方法也是一样,都是不改变调用方法的数组,而是产生并返回新的IEnumerable<T>接口类型的数组,当然你可以通过ToArray,ToList,ToDictionary将返回值转换成你想要的集合类型。
至于如何使用这两种集合操作方式,要取决于你的习惯和业务需求。
在3.5之前,想用哈希表来提高集合的查询效率,只有Hashtable和Dictionary<TKey,TValue>两种选择,而这两种都是键-值方式的存储。但有些时候,我们只需要其中一个值,例如一个Email集合,如果用泛型哈希表来存储,往往要在Key和Value各保存一次,不可避免的要造成内存浪费。而HashSet<T>只保存一个值,更加适合处理这种情况。
此外,HashSet<T>的Add方法返回bool值,在添加数据时,如果发现集合中已经存在,则忽略这次操作,并返回false值。而Hashtable和Dictionary<TKey,TValue>碰到重复添加的情况会直接抛出错误。
从使用上来看,HashSet<T>和线性集合List<T>更相似一些,但前者的查询效率有着极大的优势。假如,用户注册时输入邮箱要检查唯一性,而当前已注册的邮箱数量达到10万条,如果使用List<T>进行查询,需要遍历一次列表,时间复杂度为O(n),而使用HashSet<T>则不需要遍历,通过哈希算法直接得到列表中是否已存在,时间复杂度为O(1),这是哈希表的查询优势,在上一篇中已提到。
HashSet<T>是Set集合,它只实现了ICollection接口,在单独元素访问上,有很大的限制:
跟List<T>相比,不能使用下标来访问元素,如:list[1] 。
跟Dictionary<TKey,TValue>相比,不能通过键值来访问元素,例如:dic[key],因为HashSet<T>每条数据只保存一项,并不采用Key-Value的方式,换句话说,HashSet<T>中的Key就是Value,假如已经知道了Key,也没必要再查询去获取Value,需要做的只是检查值是否已存在。
所以剩下的仅仅是开头提到的集合操作,这是它的缺点,也是特点。
综上可知,HashSet<T>是一个Set集合,查询上有较大优势,但无法通过下标方式来访问单个元素,这点会让用惯了List<T>的人(我就是),用起来很不顺手。
HashSet<T>有别于其他哈希表,具有很多集合操作的方法,但优势并不明显,因为.net 3.5之后扩展方法赋予了泛型集合进行集合操作的能力,但扩展方法的集合操作往往返回新的集合,在使用习惯上,我个人更偏爱HashSet<T>的操作方式。
链表是数据结构中存储数据的一种形式,我们经常使用的List<T>,ArrayList,Hashtable等容器类,存取操作时是用数组Array来保存,ListDictionary和LinkedList<T>不用Array,而是用链表的形式来保存。
以ListDictionary为例,在源码中,看不到Array类型的的变量,取而代之的是一个DictionaryNode类型的变量,查看该类的源码会发现,只包含一个key,一个value,和一个DictionaryNode类型的next变量,DictionaryNode的代码如下:
private class DictionaryNode { public object key; public ListDictionary.DictionaryNode next; public object value; }
添加数据的时候,直接把当前节点的next变量赋值为新的节点,这样一个节点扣一个节点,就有了链的形式。
在链表中查找数据时,如调用Contains(object key) :bool 方法,需要从链表的头节点依次遍历,逐个匹配,所以时间复杂度为O(n),和List<T>,ArrayList相比,在查询效率上并没有太大的区别。
那么链表的优势在哪里呢?答案是,节省内存空间。
在之前的文章有提到过,线性表和哈希表初始化时会将内部Array数组默认一个大小,List<T>的初始值为4,Hashtable的为11,当添加数据碰到容量不足时,会将当前数组扩充2倍,这种做法不可避免要造成浪费。而链表不用数组保存,用节点相连,实实在在,添加几个节点,就占用几个节点的内存,相对于线性表和哈希表,链表没有浪费,因而占用内存空间较少。
除了节省空间以外,链表还有一个优点,那就是插入数据的灵活性。
可惜这一点在ListDictionary中并没有体现,每次添加数据,ListDictionary都要遍历整个链表,来确保没有重复节点,导致每次添加都要循环一次,添加数据的时间复杂度和查询数据的时间复杂度都为O(n),比线性表和哈希表要慢的多。
在.net的集合容器中,有一个名为HybridDictionary的类,充分利用了Hashtable查询效率高和ListDictionary占用内存空间少的优点,内置了Hashtable和ListDictionary两个容器,添加数据时内部逻辑如下:
当数据量小于8时,Hashtable为null,用ListDictionary保存数据。
当数据量大于8时,实例化Hashtable,数据转移到Hashtable中,然后将ListDictionary置为null。
HybridDictionary的Add方法的代码如下:
public void Add(object key, object value) { if (this.hashtable != null) { this.hashtable.Add(key, value); } else if (this.list == null) { this.list = new ListDictionary(this.caseInsensitive ? StringComparer.OrdinalIgnoreCase : null); this.list.Add(key, value); } else if ((this.list.Count + 1) >= 9) { //当数据量大于8时,则调用该方法,实例化Hashtable,转移数据,清空list this.ChangeOver(); this.hashtable.Add(key, value); } else { this.list.Add(key, value); } }
HybridDictionary类也进一步说明出了链表ListDictionary的特点:相对于Hashtable,占用内存较少,但随着数据量的增加,查询效率远不及Hashtable。
LinkedList是泛型链表,也是用节点存取,节点类型为LinkedListNode<T> ,与ListDictionary的节点不同的是,LinkedListNode<T>有next和prev两个指向,说明LinkedList<T>是双向链表,而ListDictionary是单向链表,代码如下:
public sealed class LinkedListNode<T> { // Fields internal T item; internal LinkedList<T> list; internal LinkedListNode<T> next; internal LinkedListNode<T> prev; ...... }
除了节省内存空间外,链表的另一个优点--插入数据的灵活性,在LinkedList<T>中完全体现出来,共有4个不同位置的添加数据的方法,分别为链头插入,链尾插入,节点前插入,节点后插入。
每种插入方法又分别有两种插入模式:
1、直接插入LinkedListNode<T>,没有返回值。
2、直接插入T类型的值,返回插入完成后的节点。
四种位置,两种模式,一共就有8个插入数据的方法,运用这些方法,可以在添加数据时灵活控制链表中数据的顺序,这个优势是线性表和哈希表所不能比的。代码如下:
public LinkedListNode<T> AddAfter(LinkedListNode<T> node, T value); public void AddAfter(LinkedListNode<T> node, LinkedListNode<T> newNode); public void AddBefore(LinkedListNode<T> node, LinkedListNode<T> newNode); public LinkedListNode<T> AddBefore(LinkedListNode<T> node, T value); public void AddFirst(LinkedListNode<T> node); public LinkedListNode<T> AddFirst(T value); public LinkedListNode<T> AddLast(T value); public void AddLast(LinkedListNode<T> node);
此外,由于LinkedList<T>是双向链表,在查询数据方面提供了“从前往后”和“从后往前”两个查询方法,所以虽然理论上链表的时间复杂度为O(n),根据自己在插入数据时对顺序的把握,结合这两个方法,可以相对提高查询效率。
public LinkedListNode<T> Find(T value);//从前往后查 public LinkedListNode<T> FindLast(T value);//从后往前查
相对于线性表和哈希表,链表比较节省内存空间。
ListDictionary在每次添加数据时都要遍历链表,效率较低,数据量较大且插入频繁的情况下,不宜选用。
泛型链表LinkedList<T>在保证节省内存空间的同时,在添加数据的顺序方面有极大的灵活性,加上泛型本身避免装箱拆箱的优点,需要用链表的时候,应优先考虑泛型链表。
无论是常用的List<T>、Hashtable还是ListDictionary<TKey,TValue>,在保存值的时候都是无序的,而今天要介绍的集合类SortedList和SortedList<TKey,TValue>在保存值的时候是有序保存。
一个集合有序,意味着什么?意味着可以利用一些算法来提高遍历集合时的效率,最常见的就是运用二分查找法,SortedList集合的核心就是运用二分查找。
SortedList保存数据时和哈希表一样用Key-Value的方式进行存储,但不同的是,它把Key和Value分别保存在两个object[]数组中,每次插入删除操作都会保持这两个object[]大小的同步性。
SortedList在初始化时如果不指定大小,则会给一个默认的十六进制值0x10(16),在添加操作中,如果容量不足则会自动扩充为2倍容量,这些与ArrayList和Hashtable相同。
SortedList的独特之处在于它保证数据的有序性,这点是如何保证呢?
原来,在Add(key,value)方法中,SortedList会首先用二分查找插入的key值,如果有重复项,则报错,如果没有重复项,则根据key值大小,比较得出在集合中的位置,然后插入到该位置中,代码如下:
public virtual void Add(object key, object value) { if (key == null) { throw new ArgumentNullException("key", Environment.GetResourceString("ArgumentNull_Key")); } //调用Array的静态方法进行二分查找。 int index = Array.BinarySearch(this.keys, 0, this._size, key, this.comparer); if (index >= 0) { throw new ArgumentException(Environment.GetResourceString("Argument_AddingDuplicate__", new object[] { this.GetKey(index), key })); } this.Insert(~index, key, value); }
二分查找的时间复杂度为O(LogN),所以SortedList的Add方法的时间复杂度为O(LogN),这一点略逊于ArrayList和Hashtable,后两者添加操作的时间复杂度都为O(1),这也是“有序”所付出的代价。
在查询数据时,SortedList会先通过二分查找法,找到key值所在object[]数组的序号,然后根据该序号去保存value的object[]数组中直接取值,代码如下:
public virtual object this[object key] { get { //IndexOfKey使用Array.BinarySearch进行二分查找; int index = this.IndexOfKey(key); if (index >= 0) { return this.values[index]; } return null; } ...... }
由于二分查找的关系,可以看出SortedList通过object查询操作的时间复杂度为O(logN),这一点强于ArrayList的O(n),逊于Hashtable的O(1)。
SortedList也可以通过序号下标来获取值,这种方式的复杂度为O(1),获取单个元素的方式在下面提到。
SortedList获取集合中单个元素的方式非常灵活,ArrayList只能通过int型的下标序号来获取,Hashtable只能object型的Key值来匹配,而SortedList既可以用object型的key获取,也可以用int型的序号来获取。
public void SortedListTest() { ArrayList arrayList = new ArrayList(); arrayList.Add("a"); Console.WriteLine(arrayList[0]); //output: a Hashtable hash = new Hashtable(); hash.Add("a", "aaa"); Console.WriteLine(hash["a"]); //output: aaa SortedList sortlist = new SortedList(); sortlist.Add("a", "aaa"); sortlist.Add("b", "bbb"); Console.WriteLine(sortlist["b"]); Console.WriteLine(sortlist.GetByIndex(0)); //output: bbb // aaa }
SortedList<TKey,TValue>是SortedList对应的泛型集合,除了免装箱拆箱优势和一个TryGetValue方法外,就没有什么太大差别。
SortedList保证集合中数据的有序性,有两种方式来获取单个元素,较为灵活。
添加操作比ArrayList,Hashtable略慢;查找、删除操作比ArrayList快,比Hashtable慢。
当然,如果使用,则优先选择泛型集合SortedList<TKey,TValue>。
从类名就可以看出SortedDictionary<TKey,TValue>和上篇介绍的SortedList一样,都是有序集合,但从类内部的存储结构上看,两者有很大区别,SortedList内部用数组保存,只能算是有序线性表,而SortedDictionary<TKey,TValue>的内部结构是红黑树。
园子里有不少关于红黑树的好文章,已将红黑树分析的很透彻。所以这里不讨论红黑树的结构原理,而讨论SortedDictionary和SortedList有什么差异?何时应该选择使用SortedDictionary?
SortedDictionary内部结构是红黑树,红黑树是平衡二叉树的一种,SortedList是有序线性表,内部结构是Array,运用了二分查找法提高效率。从两者查找、插入、删除操作的时间复杂度来看,都为O(LogN),分辨不出优劣,但内部结构的不同导致了实际操作中的性能差异。
由于SortedList用数组保存,每次进行插入操作时,首先用二分查找法找到相应的位置,得到位置以后,SortedList会把该位置以后的值依次往后移一个位置,空出当前位,再把值插入,这个过程中用到了Array.Copy方法,而调用该方法是比较损耗性能的,代码如下:
private void Insert(int index, TKey key, TValue value) { ...... if (index < this._size) { Array.Copy(this.keys, index, this.keys, index + 1, this._size - index); Array.Copy(this.values, index, this.values, index + 1, this._size - index); } ...... }
SortedDictionary在添加操作时,只会根据红黑树的特性,旋转节点,保持平衡,并没有对Array.Copy的调用。
现在我们用数据测试一下:循环一个int型、容量为100000的随机数组,分别用SortedList和SortedDictionary添加。(代码中的CodeTimer类,来自老赵的文章。)
public void SortedAddInTest() { Random random = new Random(); int array_count = 100000; List<int> intList = new List<int>(); for (int i = 0; i <= array_count; i++) { int ran = random.Next(); intList.Add(ran); } SortedList<int, int> sortedlist_int = new SortedList<int, int>(); SortedDictionary<int, int> dic_int = new SortedDictionary<int, int>(); CodeTimer.Time("sortedList_Add_int", 1, () => { foreach (var item in intList) { if (sortedlist_int.ContainsKey(item) == false) sortedlist_int.Add(item, item); } }); CodeTimer.Time("sortedDictionary_Add_int", 1, () => { foreach (var item in intList) { if (dic_int.ContainsKey(item) == false) dic_int.Add(item, item); } }); }
结果跟之前分析的一样,为:
sortedList_Add_int
Time Elapsed: 4,311ms
CPU Cycles: 8,249,183,130
Gen0: 0
Gen1: 0
Gen2: 0
sortedDictionary_Add_int
Time Elapsed: 217ms
CPU Cycles: 278,164,530
Gen0: 1
Gen1: 1
Gen2: 0
由此可以看出:在大量添加操作的情况下,SortedDictionary性能优于SortedList。
两者的查询操作中,时间复杂度都为O(LogN),且源码中也没有额外的操作造成性能损失,那么他们在查询操作中性能如何?继续上面一个例子进行测试。
public void SortedAddInTest() { ...... CodeTimer.Time("sortedList_Search_int", 1, () => { foreach (var item in intList) { sortedlist_int.ContainsKey(item); } }); CodeTimer.Time("sortedDictionary_Search_int", 1, () => { foreach (var item in intList) { dic_int.ContainsKey(item); } }); }
结果为:
sortedList_Search
Time Elapsed: 602ms
CPU Cycles: 1,156,460,630
Gen0: 0
Gen1: 0
Gen2: 0
sortedDictionary_Search
Time Elapsed: 667ms
CPU Cycles: 1,256,685,950
Gen0: 0
Gen1: 0
Gen2: 0
可以得出:两者在循环10w次的情况下,仅仅相差几十毫秒,可以看出,两者的查询操作性能相差不大。
从添加操作例子可以看出,由于SortedList内部使用数组进行存储数据,而数组本身的局限性使得SortedList大部分的添加操作都要调用Array.Copy方法,从而导致了性能的损失,这种情况同样存在于删除操作中。
SortedList每次删除操作都会将删除位置后的值往前挪动一位,以填补删除位置的空白,这个过程刚好跟添加操作反过来,同样也需要调用Array.Copy方法,相关代码如下。
public void RemoveAt(int index) { ...... if (index < this._size) { Array.Copy(this.keys, index + 1, this.keys, index, this._size - index); Array.Copy(this.values, index + 1, this.values, index, this._size - index); } ...... }
情况跟添加操作一样,所以先在这里预测一下:在大量删除操作的情况下时,SortedDictionary的性能优于SortedList。
让我们继续上面的测试代码来验证这一点。
public void SortedDictionaryTest() { //....... CodeTimer.Time("sortedList_Delete_String", 1, () => { foreach (var item in temp_List) { sortedlist.Remove(item); } }); CodeTimer.Time("sortedDictionary_Delete_String", 1, () => { foreach (var item in temp_List) { dic.Remove(item); } }); }
结果跟之前预测的一样,SortedDictionary的性能较好,如下:
sortedList_Delete
Time Elapsed: 13,346ms
CPU Cycles: 25,040,378,250
Gen0: 0
Gen1: 0
Gen2: 0
sortedDictionary_Delete
Time Elapsed: 731ms
CPU Cycles: 1,335,367,350
Gen0: 0
Gen1: 0
Gen2: 0
SortedDictionary内部用红黑表存储数据,SortedList用数组存储数据,两者的查询效率差不多,但由于数组本身的限制,在大量添加删除操作的情况下,SortedDictionary的性能优于SortedList,而SortedList又存在二倍扩充的问题,在内存占用上也处于劣势。(这两者的添加删除操作因Array.Copy造成的性能差异也同样存在于泛型链表LinkedList<T>和线性表中,我之前关于链表的文章里忘记分析这一块了,^_^)
此处我有了一个迷惑,既然SortedDictionary的性能全面优于SortedList,那SortedList存在的意义是什么?我找来找去只发现SortedList的一个优点就两种获取单个元素的方式--key和index,这点在上篇文章也有提到,难道SortedList的优点只有这个?