集合
- 概述
- 集合接口和类型
- 列表(ArrayList, List)
- 队列(Queue)
- 栈(Statck)
- 链表(LinkedList)
- 有序表(SortedList)
- 字典
- Lookup类
- 其他字典类
- HashSet(不重复项的无序列表)
- 位数组
- BitArray
- BitVector32
- 性能
概述
数组和Array类。数组的大小是固定的。如果元素个数是动态的,就应该使用集合类。
List和ArrayList是与数组相当的集合类。还有其他类型的集合:队列、栈、链表和字典。
集合:
- 集合接口和类型
- 列表
- 队列
- 栈
- 链表
- 有序表
- 字典
- Lookup
- HashSet
- 位数组
- 性能
集合接口和类型
集合类可以组合为集合,存储 Object 类型的元素和泛型集合类。在 CLR 2.0 之前,不存在泛型。现在泛型集合类通常是集合的首选类型。泛型集合类是类型安全的,如果使用值类型,是不需要装箱操作的。如果要在集合中添加不同类型的对象,且这些对象不是相互派生的,例如在集合中添加 int和 string 对象,就只需基于对象的集合类。另一组集合类是专用于特定类型的集合,例如
StringCollection 类专用于 string 类型。
列表(ArrayList, List)
.NET Framework 为动态列表提供了类 ArrayList 和 List。System.Collections.Generic 命名空间中的类 List的用法非常类似于System.Collections 命名空间中的 ArrayList 类。这个类实现了 IList、ICollection 和 IEnumerable 接口。
- 调用默认的构造函数,就可以创建列表对象。在泛型类 List中,必须在声明中为列表的值指定类型。下面的代码说明了如何声明一个包含 int 的 List和一个包含 Racer 元素的列表。ArrayList是一个非泛型列表,可以将任意 Object 类型作为其元素。
- 使用默认的构造函数创建一个空列表。元素添加到列表中后,列表的容量就会扩大为可接纳 4 个元素。如果添加了第 5 个元素,列表的大小就重新设置为包含 8 个元素。如果 8 个元素还不够,列表的大小就重新设置为 16。每次都会将列表的容量重新设置为原来的 2 倍。
ArrayList objectList = new ArrayList();
List<int> intList = new List<int>();
List<Racer> racers = new List<Racer>();
创建了一个容量为 10 个元素的集合。如果该容量不足以容纳要添加的元素,就把集合的大小重新设置为 20,或 40,每次都是原来的 2 倍。
ArrayList objectList = new ArrayList(10);
List<int> intList = new List<int>(10);
使用Capacity属性可以获取和设置集合的容量:
objectList.Capacity = 20;
intList.Capactity = 20;
容量与集合中的个数不同。集合中元素个数可以用Count属性读取。当然,容量总是大于元素个数。只要不把元素添加到列表中,元素个数就是 0。
Console.WriteLine(intList.Count); // 输出元素个数
如果已经将元素添加到列表中,且不希望添加更多的元素,就可以调用TrimExcess()方法,去除不需要的容量。但是,重新定位是需要时间的,所以如果元素个数超过了容量的 90%,TrimExcess()方法将什么也不做。
intList.TrimExcess(); // 去除不需要的容量。
ArrayList arrayList = new ArrayList();
arrayList.Add(1);
arrayList.Add(2);
arrayList.Add("Hello");
arrayList.Add(3);
arrayList.Add(4);
arrayList.Add("World");
arrayList.Add(5);// TODO:变量数组列表
Debug.WriteLine("遍历数组列表:");
foreach (var item in arrayList)
{ Debug.WriteLine(item);
}Debug.WriteLine("下标访问数组列表:");
Debug.WriteLine(arrayList[0]);
Debug.WriteLine(arrayList[1]);
Debug.WriteLine(arrayList[2]);
Debug.WriteLine(arrayList[3]);
Debug.WriteLine(arrayList[4]);arrayList.Remove(3); // 移除元素3
arrayList.RemoveAt(3); // 移除第3个元素
Debug.WriteLine("移除元素3,移除第3个元素");
foreach (var item in arrayList)
{Debug.WriteLine(item);
}arrayList.AddRange(new int[] { 10, 11, 12, 13, 14, 15 });
arrayList.RemoveRange(3, 2);int[] ints = new int[] { 34, 54, 43, 21, 66 };
arrayList.InsertRange(2, ints); // 第2个位置插入数组
foreach (var item in arrayList)
{Debug.WriteLine(item);
}//arrayList.Sort(); // 排序
//arrayList.Reverse(); // 反序foreach (var item in arrayList)
{Debug.WriteLine(item);
}//arrayList.Clear(); // 清空
//arrayList.Capacity = 10;
//Debug.WriteLine(arrayList.Count);
队列(Queue)
队列是其元素以先进先出(FIFO)的方式来处理的集合。先放在队列中的元素会先读取。队列的例子有在机场排的队、人力资源部中等待处理求职信的队列、打印队列中等待处理的打印任务、以循环方式等待 CPU 处理的线程。另外,还常常有元素根据其优先级来处理的队列。例如,在机场的队列中,商务舱乘客的处理要优先于经济舱的乘客。这里可以使用多个队列,一个队列对应一个优先级。在机场,这是很常见的,因为商务舱乘客和经济舱乘客有不同的登记队列。打印队列和线程也是这样。可以为一组队列建立一个数组,数组中的一项代表一个优先级。在每个数组项中,都有一个队列,其处理按照 FIFO 的方式进行。
Queue<string> queueStr = new Queue<string>();
Queue<int> queueInt = new Queue<int>(10);queueStr.Enqueue("Hi");
queueStr.Enqueue("Hello");
queueStr.Enqueue("World");
Debug.WriteLine(queueStr.Peek());
Debug.WriteLine(queueStr.Dequeue());
Debug.WriteLine(queueStr.First());
Debug.WriteLine(queueStr.Last());
queueStr.Clear();queueInt.Enqueue(12);
queueInt.Enqueue(23);
queueInt.Enqueue(25);
queueInt.Enqueue(65);
foreach (var item in queueInt)
{Debug.WriteLine(item);
}for (int i = 0; i < queueInt.Count; i++)
{Debug.WriteLine(queueInt.Dequeue());
}Debug.WriteLine(queueInt.Count);
栈(Statck)
栈是与队列非常类似的另一个容器,只是要使用不同的方法访问栈。最后添加到栈中的元素会最先读取。栈是一个后进先出(LIFO)容器。
Stack<char> alphabet = new Stack<char>();alphabet.Push('A');alphabet.Push('B');alphabet.Push('C'); foreach (char c in alphabet){Debug.Write(c+",");}Debug.WriteLine(alphabet.Pop());foreach (char c in alphabet){Debug.Write(c + ",");}
链表(LinkedList)
- LinkedList 集合类没有非泛型集合的类似版本。LinkedList 是一个双向链表,其元素指向它前面和后面的元素。
- 链表的优点是,如果将元素插入列表的中间位置,使用链表会非常快。在插入一个元素时,只需修改上一个元素的 Next 引用和下一个元素的 Previous 引用,使它们引用所插入的元素。在 List 和ArrayList 类中,插入一个元素,需要移动该元素后面的所有元素。
- 当然,链表也有缺点。链表的元素只能一个接一个地访问,这需要较长的时间来查找位于链表中间或尾部的元素。
- 链表不仅能在列表中存储元素,还可以给每个元素存储下一个元素和上一个元素的信息。这就是LinkedList 包含 LinkedListNode 类型的元素的原因。使用 LinkedListNode 类,可以获得列表中的下一个元素和上一个元素。表 10-5 描述了 LinkedListNode 的属性。
LinkedList<string> linkedList = new LinkedList<string>();linkedList.AddLast("Apple");linkedList.AddLast("Huawei");linkedList.AddFirst("Xiaomi");linkedList.AddLast("Huawei");foreach (var item in linkedList){Debug.WriteLine(item);}var node = linkedList.Find("Apple"); // 查找元素linkedList.AddAfter(node, "Apple After"); // 在指定节点后添加元素linkedList.AddBefore(node, "Apple Before"); // 在指定节点前添加元素foreach (var item in linkedList){Debug.WriteLine(item);}Debug.WriteLine(linkedList.ElementAt(0)); // 访问第0个元素Debug.WriteLine(linkedList.ElementAt(3)); // 访问第3个元素
有序表(SortedList)
- 如果需要排好序的表,可以使用 SortedList<TKey, TValue>。这个类按照键给元素排序。
SortedList<string, int> sortedList = new SortedList<string, int>();
sortedList.Add("one", 111);
sortedList.Add("two", 222);
sortedList.Add("three", 333);
sortedList.Add("four", 444);
sortedList.Add("five", 555);
foreach (KeyValuePair<string, int> kvp in sortedList)
{Debug.WriteLine($"key:{kvp.Key}, value:{ kvp.Value}");
}// 通过key获取值
int value_one = sortedList["one"];// 通过索引获取Key
int value = sortedList.Values[2];
string key_two = sortedList.Keys[2];Debug.WriteLine(value_one);
Debug.WriteLine(key_two);sortedList.Remove("one"); // 删除键值对
字典
- 字典表示一种非常复杂的数据结构,这种数据结构允许按照某个键来访问元素。字典也称为映射或散列表。字典的主要特性是能根据键快速查找值。也可以自由添加和删除元素,这有点像 List,但没有在内存中移动后续元素的性能开销。
- 键的类型:用作字典中键的类型必须重写 Object 类的 GetHashCode()方法。只要字典类需要确定元素的位置,就要调用 GetHashCode()方法。GetHashCode()方法返回的 int 由字典用于计算放置元素的索引。这里不介绍这个算法。我们只需知道,它涉及到素数,所以字典的容量是一个素数。
GetHashCode()方法的实现代码必须满足如下要求:
● 相同的对象应总是返回相同的值。
● 不同的对象可以返回相同的值。
● 应执行得比较快,计算的开销不大。
● 不能抛出异常。
● 应至少使用一个实例字段。
● 散列码值应平均分布在 int 可以存储的整个数字区域上。
● 散列码最好在对象的生存期中不发生变化。
提示:
字典的性能取决于 GetHashCode()方法的实现代码。
Lookup类
Dictionary<TKey, TValue>只为每个键支持一个值。新类 Lookup<TKey, TElement>是.NET 3.5 中新增的,它类似于 Dictionary<TKey, TValue>,但把键映射到一个值集上。这个类在程序集 System.Core中实现,用 System.Linq 命名空间定义。
其他字典类
Dictionary<TKey, TValue>是 Framework 中的一个主要字典类,还有其他一些类,当然也有一些非泛型的字典类。
HashSet(不重复项的无序列表)
.NET 3.5 在 System.Collections.Generic 命名空间中包含一个新的集合类:HashSet。这个集合类包含不重复项的无序列表。这种集合称为"集(set)"。集是一个保留字,所以该类有另一个名称HashSet。这个名称很容易理解,因为这个集合基于散列值,插入元素的操作非常快,不需要像List类那样重排集合。
位数组
BitArray
-
如果需要处理许多位,就可以使用类 BitArray 和结构 BitVector32。BitArray 位于命名空间System.Collections,BitVector32 位于命名空间 System.Collections.Specialized。这两种类型最重要的区别是,BitArray 可以重新设置大小,如果事先不知道需要的位数,就可以使用 BitArray,它可以包含非常多的位。BitVector32 是基于栈的,因此比较快。BitVector32 仅包含 32位,存储在一个整数中。
-
类 BitArray 是一个引用类型,包含一个 int 数组,每 32 位使用一个新整数。
BitVector32
如果事先知道需要的位数,就可以使用 BitVector32 结构替代 BitArray。BitVector32 效率较高,因为它是一个值类型,在整数栈上存储位。一个整数可以存储 32 位。如果需要更多的位,就可以使用多个 BitVector32 值或 BitArray。BitArray 可以根据需要增大,但 BitVector32 不能。
性能
许多集合类都提供了相同的功能,例如,SortedList 与 SortedDictionary 的功能几乎完全相同。但是,其性能常常有很大区别。一个集合使用的内存少,另一个集合的元素检索速度快。在 MSDN 文档中,集合的方法常常有性能提示,给出了以大 O 记号表示的操作时间:
- O(1):表示无论集合中有多少数据项,这个操作需要的时间都不变。
- O(n):表示对于集合中的每个元素,需要增加的时间量都是相同的。
- O(log n):表示操作需要的时间随集合中元素的增加而增加,但每个元素需要增加的时间不是线性的,而是呈对数曲线。