C#集合:List、Queue、Stack和Set

.NET Core提供了一些基本的具体集合类,这些类实现了一系列集合接口。和集合接口一样,每一种集合类型都可以选择使用泛型或非泛型进行实现。在灵活性和性能方面,泛型类更具有优势,而它们的非泛型冗余版本则是为了向后兼容。这与集合接口不同,非泛型集合接口在某些情形下是有其作用的。而泛型List类是最常用的。

1. List<T>和ArrayList

泛型List和非泛型ArrayList类都提供了一种可动态调整大小的对象数组实现,是集合类中使用最广泛的。ArrayList实现了IList,而List<T>既实现了IList又实现了IList<T>。与数组不同,所有的接口都是公开实现的,而且其方法如Add和Remove也都是公开可用的。

List<T>ArrayList在内部都维护着一个对象数组,并在超出容量时替换为一个更大的数组。在集合中追加元素的效率很高(因为数组末尾一般都有空闲的位置),而插入元素的速度会慢一些(因为插入位置之后的所有元素都必须向后移动才能留出插入空间),移除元素同样速度较慢(尤其是移除起始元素时)。

List<T>ArrayList具有可以接受现有集合的构造器,它们会将现有集合中的每一个元素都复制到新的List<T>ArrayList中。

public class List<T> : ICollection<T>, IEnumerable<T>, IEnumerable, IList<T>, 
    IReadOnlyCollection<T>, IReadOnlyList<T>, ICollection, IList
{
        public List();
        public List(IEnumerable<T> collection);
        public List(int capacity);

        // 追加、插入
        public void Add(T item);
        public void AddRange(IEnumerable<T> collection);
        public void Insert(int index, T item);
        public void InsertRange(int index, IEnumerable<T> collection);
        
        // 删除
        public bool Remove(T item);
        public void RemoveAt(int index);
        public void RemoveRange(int index, int count);
        public int RemoveAll(Predicate<T> match);
        
        // 索引
        public T this[int index] { get; set; }
        public List<T> GetRange(int index, int count);
        public List<T>.Enumerator GetEnumerator();
        
        // 复制、转换
        public T[] ToArray();
        public void CopyTo(T[] array, int arrayIndex);
        public void CopyTo(T[] array);
        public void CopyTo(int index, T[] array, int arrayIndex, int count);
        public ReadOnlyCollection<T> AsReadOnly();
        public List<TOutput> ConvertAll<TOutput>(Converter<T, TOutput> converter);
        
        public void Reverse(int index, int count);
        public void Reverse();
        public int Capacity { get; set; }
        public void TrimExcess();
        public void Clear();
}

除了上述成员,List<T>还提供了Array类中所有搜索和排序方法的实例版本。

List<string> words = new List<string>();    

words.Add ("melon");
words.Add ("avocado");
words.AddRange (new[] { "banana", "plum" } );
words.Insert (0, "lemon");                           // 开头插入
words.InsertRange (0, new[] { "peach", "nashi" });   // 开头插入

words.Remove ("melon");
words.RemoveAt (3);                         // 删除第四个
words.RemoveRange (0, 2);                   // 从第一个开始删除2个

words.RemoveAll (s => s.StartsWith ("n"));  // 删除开头有n的

Console.WriteLine (words [0]);                          // first word
Console.WriteLine (words [words.Count - 1]);            // last word
foreach (string s in words) Console.WriteLine (s);      // all words
List<string> subset = words.GetRange (1, 2);            // 2nd->3rd words

string[] wordsArray = words.ToArray();    

// 将前两个元素复制到现有数组的末尾
string[] existing = new string [1000];
words.CopyTo (0, existing, 998, 2);

List<string> upperCaseWords = words.ConvertAll (s => s.ToUpper());
List<int> lengths = words.ConvertAll (s => s.Length);

使用非泛型的ArrayList类往往需要进行烦琐的转换,如下所示:

ArrayList al = new ArrayList();
al.Add ("hello");
string first = (string) al [0];
string[] strArr = (string[]) al.ToArray (typeof (string));

编译器无法验证这些转换,所以像int first = (int)al[0];这个例子会在运行时出错。

如果导入了System.Linq命名空间,那么可以先调用Cast再调用ToList将一个ArrayList转换为一个泛型List。Cast和ToList都是System.Linq.Enumerable类的扩展方法。

ArrayList al = new ArrayList();
al.AddRange (new[] {1,5,9});
List<int> list = al.Cast<int>().ToList();

2. LinkedList<T>

LinkedList<T>是一个泛型的双向链表。
双向链表是一系列相互引用的节点,每一个节点都引用前一个节点、后一个节点以及实际存储的数据元素。它的主要优点是元素总能够高效地插入到链表的任意位置,因为插入节点只需要创建一个新节点,然后修改引用值。然而查找插入节点的位置会比较慢,因为链表本身并没有直接索引的内在机制。必须遍历每一个节点,并且无法执行二分搜索。
image

LinkedList<T>实现了IEnumerable<T>ICollection<T>(及它们的非泛型版本),但是没有实现IList<T>,因为它不支持索引访问。链表节点是通过下面的类实现的:

public sealed class LinkedListNode<T>
{
    public LinkedListNode(T value);
    public LinkedList<T>? List { get; }
    public LinkedListNode<T>? Next { get; }
    public LinkedListNode<T>? Previous { get; }
    public T Value { get; set; }
    public ref T ValueRef { get; }
}

当添加一个节点时,可以指定它相对于其他节点的位置,或者指定它位于链表的开始/结束位置。可以使用以下方法为LinkedList<T>添加节点:

    public void AddAfter(LinkedListNode<T> node, LinkedListNode<T> newNode);
    public LinkedListNode<T> AddAfter(LinkedListNode<T> node, T value);

    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 void AddLast(LinkedListNode<T> node);
    public LinkedListNode<T> AddLast(T value);

LinkedList<T>内部的一些字段记录了链表元素的个数以及链表的头部和尾部。可以通过下面的公有属性访问这些信息:

    public LinkedListNode<T>? Last { get; }
    public LinkedListNode<T>? First { get; }
    public int Count { get; }

也支持以下搜索方法:

    public bool Contains(T value);
    public LinkedListNode<T>? Find(T value);
    public LinkedListNode<T>? FindLast(T value)

最后,可以将LinkedList<T>的元素复制到一个数组中,以便支持索引处理。Linked-List<T>也支持foreach语句所需的枚举器:

    public void CopyTo(T[] array, int index);
    public LinkedList<T>.Enumerator GetEnumerator();

以下代码演示了LinkedList<String>的用法:

var tune = new LinkedList<string>();
tune.AddFirst ("do");                            // do
tune.AddLast ("so");                             // do - so

tune.AddAfter (tune.First, "re");                // do - re- so
tune.AddAfter (tune.First.Next, "mi");           // do - re - mi- so
tune.AddBefore (tune.Last, "fa");                // do - re - mi - fa- so

tune.RemoveFirst();                              // re - mi - fa - so
tune.RemoveLast();                               // re - mi - fa

LinkedListNode<string> miNode = tune.Find ("mi");
tune.Remove (miNode);                            // re - fa
tune.AddFirst (miNode);

3. Queue<T>和Queue

Queue<T>Queue是先进先出(FIFO)的数据结构。
它们提供了Enqueue(将一个元素添加到队列末尾)和Dequeue(取出并删除队列的第一个元素)方法。它们还包括一个只返回而不删除队列第一个元素的Peek方法以及一个Count属性(可在取出元素前检查该元素是否存在于队列中)。

public class Queue<T> : IEnumerable<T>, IEnumerable, IReadOnlyCollection<T>, ICollection
{
    public Queue();
    public Queue(IEnumerable<T> collection);
    public Queue(int capacity);

    public int Count { get; }
    public void Clear();
    public bool Contains(T item);
    public void CopyTo(T[] array, int arrayIndex);
    public T Dequeue(); // 取出并删除队列的第一个元素
    public void Enqueue(T item); // 将一个元素添加到队列末尾
    public Queue<T>.Enumerator GetEnumerator();
    public T Peek();
    public T[] ToArray();
    public void TrimExcess();
}

虽然队列是可枚举的,但是它并没有实现IList<T>IList,因为我们无法直接通过索引访问其成员。然而,可以使用ToArray方法将其中的元素复制到一个数组中,而后进行随机访问:

    var q = new Queue<int>();
    q.Enqueue (10);
    q.Enqueue (20);
    int[] data = q.ToArray();         // 转为一个数组
    Console.WriteLine (q.Count);      // "2"
    Console.WriteLine (q.Peek());     // "10"
    Console.WriteLine (q.Dequeue());  // "10"
    Console.WriteLine (q.Dequeue());  // "20"
    Console.WriteLine (q.Dequeue());  // 抛出一个异常(队列为空)

队列的实现和泛型List类相似,都在内部使用了一个可根据需要进行大小调整的数组。队列具有一个直接指向头部和尾部元素的索引,因此其入队和出队的操作速度非常快。

4. Stack<T>和Stack

Stack<T>Stack是后进先出(LIFO)的数据结构。
它们提供了Push(向栈的顶部添加一个元素)和Pop(从栈顶取出并删除一个元素)方法,也提供了一个只读取而不删除元素的Peek方法、Count属性,以及可以导出数据并进行随机访问的ToArray方法:

public class Stack<T> : IEnumerable<T>, IEnumerable, IReadOnlyCollection<T>, ICollection
{
    public Stack();
    public Stack(IEnumerable<T> collection);
    public Stack(int capacity);
    public int Count { get; }
    public void Clear();
    public bool Contains(T item);
    public void CopyTo(T[] array, int arrayIndex);
    public Stack<T>.Enumerator GetEnumerator();
    public T Peek(); // 从栈顶取出一个元素
    public T Pop(); // 从栈顶取出并删除一个元素
    public void Push(T item);  // 向栈的顶部添加一个元素
    public T[] ToArray();
    public void TrimExcess();
}

使用方法:

    var s = new Stack<int>();
    s.Push(1);
    s.Push(2);
    s.Push(3);
    Console.WriteLine(s.Count);  // 输出 3
    Console.WriteLine(s.Peek()); // 1,2,3 输出 3
    Console.WriteLine(s.Pop());  // 1,2 输出 3
    Console.WriteLine(s.Pop());  // 1 输出 2
    Console.WriteLine(s.Pop());  // <empty> 输出 1
    Console.WriteLine(s.Pop());  // 报错

栈和Queue<T>List<T>一样,其内部也是用一个可以根据需要调整大小的数组实现的。

5. BitArray

BitArray是一个压缩保存bool值的可动态调整大小的集合。
由于它使用一位(而不是一般的一字节)来存储一个bool值,因此比起简单的bool数组和以bool为类型参数的泛型List,BitArray具有更高的内存使用效率。BitArray的索引器可以读写每一位:

    var bits = new BitArray(2);
    bits[1] = true;

它提供了四种按位操作的运算符方法:And、Or、Xor和Not。除最后一个方法外,其他的方法都接受一个BitArray作为参数:

    bits.Xor(bits); // 与自身按位异或
    Console.WriterLine(bits[1]); // false

6. HashSet<T>和SortedSet<T>

HashSet<T>SortedSet<T>分别是在.NET Framework 3.5和4.0版本新增的泛型集合类型。
它们都具有以下特点:

  • 它们的Contains方法均使用散列查找,因而执行速度很快。
  • 它们都不保存重复元素,并且都忽略添加重复值的请求。
  • 无法根据位置访问元素。SortedSet<T>按一定顺序保存元素,而HashSet<T>则不是。

HashSet<T>是通过使用只存储键的散列表实现的,而SortedSet<T>则是通过一个红黑树实现的。

两个集合都实现ICollection<T>并提供了一些常用的方法,例如Contains、Add和Remove。此外还提供了一个基于谓词的删除元素的方法:RemoveWhere。

    var letters = new HashSet<char> ("the quick brown fox");
    // 是否包含某些成员
    Console.WriteLine (letters.Contains ('t'));      // true
    Console.WriteLine (letters.Contains ('j'));      // false

真正有意思的方法是集合的各种操作。以下集合操作是破坏性的,即它们会修改集合:

    // 将第二个集合的所有元素添加到原始集合上(不包含重复元素)
    public void UnionWith(IEnumerable<T> other);  
    // 将不属于两个集合共有的元素删除
    public void IntersectWith(IEnumerable<T> other); 
    // 删除源集合中的指定元素
    public void ExceptWith(IEnumerable<T> other); 
    // 删除两个集合中共有的元素
    public void SymmetricExceptWith(IEnumerable<T> other);

下面的方法仅仅对集合进行查询,因而是非破坏性的:

    public bool IsProperSubsetOf(IEnumerable<T> other);
    public bool IsProperSupersetOf(IEnumerable<T> other);
    public bool IsSubsetOf(IEnumerable<T> other);
    public bool IsSupersetOf(IEnumerable<T> other);
    public bool Overlaps(IEnumerable<T> other);
    public bool SetEquals(IEnumerable<T> other);

HashSet<T>SortedSet<T>均实现了IEnumerable<T>,因此也可以使用另外一种Set类型或者集合类型作为集合操作方法的参数。
SortedSet<T>拥有HashSet<T>的所有成员。除此之外,还有如下的成员:

    public virtual SortedSet<T> GetViewBetween(T? lowerValue, T? upperValue);
    public IEnumerable<T> Reverse();
    public T? Min { get; }
    public T? Max { get; }

SortedSet<T>的构造器还可接受一个可选的IComparer<T>参数(而非一个相等比较器)。
下面的例子会把相同的字符加载到SortedSet<char>中,获得f与j之间的字符。

    var letters = new SortedSet<char> ("the quick brown fox");
    foreach (char c in letters) 
        Console.Write (c);    //  bcefhiknoqrtuwx
    foreach (char c in letters.GetViewBetween ('f', 'i'))
        Console.Write (c);    //  fhi
posted @ 2022-08-29 17:46  一纸年华  阅读(1966)  评论(0编辑  收藏  举报