.NET各种集合分类总结 本文不定期修改补充 属于收集整理用

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方法,一旦初始化以后,长度不可变.

int IList.Add(object value) { throw new NotSupportedException(Environment.GetResourceString("NotSupported_FixedSizeCollection")); }


同样通过源代码可以看到ArrayList和Array的区别,ArrayList内置了一个Array变量 _items(代码中红色标出),

也就是说:ArrayList是Array的一层封装,实际数据操作还是针对Array.

[Serializable, ComVisible(true), DebuggerDisplay("Count = {Count}"), DebuggerTypeProxy(typeof(ArrayListDebugView))] public class ArrayList : IList, ICollection, IEnumerable, ICloneable { // Fields private const int _defaultCapacity = 4private object[] _itemsprivate int _size; // Methods   ........... }


ArrayList有Add方法,当Add方法发现内部的object[]容量已满时,便会调用一个方法自动扩充object[]容量,既然ArrayList的实质是操作object[],而Array长度不可变,那么如何扩充?其实说白了,就是通过调用EnsureCapacity方法再创建一个更长的object[]数组,然后把原数组复制到新的数组中.

ArrayList的很多方法如Sort,Indexof,内部都是调用了Array的静态方法,如IndexOf方法:

public virtual int IndexOf(object value) { return Array.IndexOf(this._items, value, 0this._size); }


.net2.0以后出现了泛型,目的是为了避免装箱拆箱操作造成的性能损失.ArrayList对应的泛型集合就是List<T>.

由于泛型的出现,List<T>内部操作的不再是object[],而是T[],提供的很多方法如Sort,IndexOf等,同ArrayList类一样,内部也是调用Array的静态方法来操作数组.

因为Array的局限性,List<T>的一些方法会用到循环,如Find方法:

public T Find(Predicate<T> match) { if (match == null) { ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match); } for (int i = 0; i < this._size; i++) { if (match(this._items[i])) { return this._items[i]; } } returndefault(T); }


其中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>是对应的泛型哈希表.

哈希表-Hashtable的实例

一般我们实例化ArrayList或List<T>的时候,如果不指定容量,则其内部是赋值为一个静态的空数组。当有添加操作时,会实例化为一个长度为4的数组,如果容量满了以后,再添加,就会自动扩充为两倍的容量。

哈希表也有一个类似的情况,new Hashtable()如果不指定长度,则会将内置bucket数组的长度默认指定为11。如果给定一个值如new Hashtable(20),也并不会将bucket数组长度设置为20,而是循环内部一个素数数组,设置为刚好大于20的素数(也叫质数),此处是23。可知,哈希表内部存取数组长度总是一个素数。相关代码:

public Hashtable(int capacity, float loadFactor) { ......
//默认值为11 int num2 = (num > 11.0) ? HashHelpers.GetPrime((int) num) : 11;this.buckets = new bucket[num2];
 
...... } [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] internal static int GetPrime(int min) { ...... //循环素数数组找出刚好大于min的素数 for (int i = 0; i < primes.Length; i++) { int num2 = primes[i]; if (num2 >= min) { return num2; } }
 
...... } //初始化素数数组 static HashHelpers() { primes = new int[] { 37110x110x170x1d0x250x2f0x3b0x470x590x6b0x830xa30xc50xef0x1250x1610x1af0x2090x2770x2f90x3970x44f0x52f0x63d,0x78b0x91d0xaf10xd2b0xfd10x12fd0x16cf0x1b650x20e30x27770x2f6f0x38ff0x446f0x521f0x628d0x76550x8e010xaa6b0xcc890xf5830x126a70x1619b0x1a8570x1fd3b0x263150x2dd670x3701b0x420230x4f3610x5f0ed0x721250x88e310xa443b0xc51eb0xec8c10x11bdbf0x154a3f0x198c4f0x1ea8670x24ca190x2c25c10x34fa1b0x3f928f0x4c49870x5b8b6f0x6dda89 }; }

从源码可以得出一个结论:Hashtable总是创建一个比我们预想中要大一些的数组。

哈希表-Hashtable的存取方式

Hashtable内部包含了一个bucket类型的数组变量,bucket是一个结构,用来保存Key,Value和哈希值。

private struct bucket { public object key; public object val; public int hash_coll; } public class Hashtable //相关接口 { private IEqualityComparer _keycomparer; private object _syncRoot; private bucket[] buckets;
  //.... }

从上面代码可以看出Hashtable跟ArrayList,List<T>一样,内部都是用Array数组进行存储数据。既然同样是数组,Hashtable和ArrayList存取方式有什么不同?

当Hashtable添加数据时,(例如:hash.Add(key)) 首先调用当前键值key的GetHashCode()方法,得到哈希值(该值为Int32),取该哈希值的绝对值跟内部数组(上面代码的buckets)的总容量进行余运算(就是'%'操作),得到一个小于总容量的值,把该值当做数组序号,存在对应的位置,通过Key获取数组序号的过程叫映射。

当然,两个不同的key值有可能的得到相同的序号,这叫哈希冲突,在此不做分析。

下面是部分源码:

private void Insert(object key, object nvalue, bool add) {
  ......
uint num3 = this.InitHash(key, this.buckets.Length, out num, out num2); intnum4 = 0int index = -1;
  //num6就是通过余计算得到的数组序号。 int num6 = (int) (num % this.buckets.Length);
  ...... } private uint InitHash(object key, int hashsize, out uint seed,out uint incr) {   //获取key在CLR中的HashCode值,并取绝对值。 uint num = (uint) (this.GetHash(key) & 0x7fffffff); seed = num;
  //此处针对哈希冲突 incr = 1 + ((uint) (((seed >> 5) + 1) % (hashsize - 1))); return num; }


从Hashtable里取值的,也是通过映射Key值,找到数组中对应的存放索引位置,直接位置取出值,整个过程无需循环,时间复杂度为O(1),而ArrayList查找一个值则需要循环整个数组去逐个匹配,时间复杂度为O(n),由此可以看到哈希表在查询中的优势。

既然Hashtable本身也是数组存储,查询速度又快,那还要ArrayList做什么?存储数据的时候都用Hashtable不就行了?有句话说,XX打开了一扇门,就必然要关闭很多窗,现在分析一下Hashtable关闭的那些窗。

首先,哈希表不能有重复键值。

其次,哈希表会占用更多内存空间。

第一点显而易见,现在分析第二点。

哈希表-Hashtable添加时的内部扩充方式

我们知道ArrayList和List<T>在添加数值时,如果容量不足,会新建一个二倍容量的数组,然后把原数组数据复制过去,达到容量扩充的目的,所以使用ArrayList和List <T>的时候,建议在实例化方法里指定容量。

那么Hashtable在添加操作时碰到内部数组容量不足会怎么做?

其实处理方法和ArrayList基本相似,也是二倍扩充,但略有不同。不同点在于:

首先,扩充的大小比二倍略大。上面在实例化Hashtable那一部分中提到,由于哈希算法本身的特点,数组长度总是一个素数,所以当需要扩充时,新的数组长度是刚好大于原数组二倍长度的素数,也就是说略大于二倍。

其次,数组在未存满的情况下就要扩充。Hashtable有一个加载因子为0.72f。实际能存储的数据量等于内部Array长度* 0.72f。也就是说一个长度为11的数组,当存了7个数据以后,就不能再存储,需要要扩充容量。代码如下:

public Hashtable(int capacity, float loadFactor) { ...... //初始状态loafFactor=1f this.loadFactor = 0.72f * loadFactor; ...... int num2 = (num > 11.0) ? HashHelpers.GetPrime((int) num) : 11this.buckets = new bucket[num2];
//实际可加载量 this.loadsize = (int) (this.loadFactor * num2); ...... }
//Hashtable的Add方法内部调用了Insert方法 private void Insert(object key, object nvalue, bool add) { ...... if (this.count >= this.loadsize) {
     //内部容量扩充的方法 this.expand(); } ...... }

也就是说在Hashtable中,假如初始化了100个位置,只有72个可用,其他28个不能用,但仍然要占着地方。

由此我们可以知道,如果保存的数据量相同,Hashtable要比ArrayList占用更多的内存空间。

哈希表-泛型Dictionary<TKey,TValue>和Hashtable比较

.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>对集合运算的操作

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将返回值转换成你想要的集合类型。

至于如何使用这两种集合操作方式,要取决于你的习惯和业务需求。

HashSet<T>的特点

在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>的不能做的事情

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),比线性表和哈希表要慢的多。

HybridDictionary-结合链表和哈希表的特点扬长避短

在.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<T>

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集合的核心就是运用二分查找。

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获取单个元素的灵活性

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),分辨不出优劣,但内部结构的不同导致了实际操作中的性能差异。

SortedDictionary和SortedList性能比较--插入

由于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。

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次的情况下,仅仅相差几十毫秒,可以看出,两者的查询操作性能相差不大。

SortedDictionary和SortedList性能比较--删除

从添加操作例子可以看出,由于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的优点只有这个?


原文地址:https://www.cnblogs.com/happycat1988/p/2386050.html