MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C#中的数组与集合类型详解

2024-12-223.2k 阅读

C# 中的数组

数组基础

在 C# 中,数组是一种用于存储多个相同类型元素的固定大小的数据结构。它在内存中是连续存储的,这使得对数组元素的访问非常高效。数组的声明包括数据类型、数组名和方括号 []。例如,声明一个整数数组:

int[] numbers;

这里只是声明了数组变量 numbers,并没有为其分配内存来存储实际的元素。要分配内存并初始化数组,可以使用 new 关键字:

numbers = new int[5];

这行代码创建了一个可以容纳 5 个整数的数组。数组的索引从 0 开始,所以可以通过索引来访问和赋值数组元素:

numbers[0] = 10;
numbers[1] = 20;
numbers[2] = 30;
numbers[3] = 40;
numbers[4] = 50;

也可以在声明数组的同时进行初始化:

int[] numbers = new int[] { 10, 20, 30, 40, 50 };

或者更简洁地:

int[] numbers = { 10, 20, 30, 40, 50 };

多维数组

除了一维数组,C# 还支持多维数组。多维数组有两种类型:矩形数组和交错数组。

矩形数组

矩形数组的每一维都有相同数量的元素,就像一个矩形。声明和初始化一个二维矩形数组的示例如下:

int[,] matrix = new int[3, 4];
matrix[0, 0] = 1;
matrix[0, 1] = 2;
// 其他元素赋值

也可以在声明时初始化:

int[,] matrix = {
    { 1, 2, 3, 4 },
    { 5, 6, 7, 8 },
    { 9, 10, 11, 12 }
};

访问矩形数组元素时,需要提供每一维的索引,例如 matrix[1, 2] 表示第二行第三列的元素。

交错数组

交错数组是数组的数组,即每一维的数组大小可以不同。声明和初始化一个二维交错数组的示例如下:

int[][] jaggedArray = new int[3][];
jaggedArray[0] = new int[2];
jaggedArray[1] = new int[3];
jaggedArray[2] = new int[4];
jaggedArray[0][0] = 1;
jaggedArray[0][1] = 2;
// 其他元素赋值

初始化时也可以这样写:

int[][] jaggedArray = {
    new int[] { 1, 2 },
    new int[] { 3, 4, 5 },
    new int[] { 6, 7, 8, 9 }
};

访问交错数组元素时,先指定外层数组的索引,再指定内层数组的索引,如 jaggedArray[1][2] 表示第二个内层数组的第三个元素。

数组的属性和方法

数组有一些有用的属性和方法。

Length 属性

Length 属性返回数组的总元素个数。对于一维数组,它就是数组的大小;对于多维矩形数组,它是所有维数大小的乘积;对于交错数组,它是外层数组的大小。

int[] numbers = { 1, 2, 3, 4, 5 };
Console.WriteLine(numbers.Length); // 输出 5

int[,] matrix = {
    { 1, 2, 3, 4 },
    { 5, 6, 7, 8 },
    { 9, 10, 11, 12 }
};
Console.WriteLine(matrix.Length); // 输出 12

int[][] jaggedArray = {
    new int[] { 1, 2 },
    new int[] { 3, 4, 5 },
    new int[] { 6, 7, 8, 9 }
};
Console.WriteLine(jaggedArray.Length); // 输出 3

GetLength 方法

GetLength 方法用于获取指定维度的大小。对于一维数组,它等同于 Length 属性。对于多维数组,可以通过传入维度索引来获取相应维度的大小。

int[,] matrix = {
    { 1, 2, 3, 4 },
    { 5, 6, 7, 8 },
    { 9, 10, 11, 12 }
};
Console.WriteLine(matrix.GetLength(0)); // 输出 3,第一维大小
Console.WriteLine(matrix.GetLength(1)); // 输出 4,第二维大小

Clone 方法

Clone 方法用于创建数组的浅表副本。浅表副本意味着新数组中的元素引用与原数组相同(对于引用类型),而对于值类型则是复制值。

int[] numbers = { 1, 2, 3, 4, 5 };
int[] clonedNumbers = (int[])numbers.Clone();

C# 中的集合类型

泛型集合基础

虽然数组在 C# 中很有用,但它们有固定的大小,并且缺乏一些高级的功能。这就是集合类型发挥作用的地方。C# 提供了丰富的泛型集合类型,这些集合类型在 System.Collections.Generic 命名空间中。泛型集合允许指定集合中元素的类型,从而提供类型安全。

List

List<T> 是最常用的泛型集合之一,它表示可动态调整大小的对象列表。

创建和初始化

可以通过以下方式创建和初始化 List<T>

List<int> numbers = new List<int>();
numbers.Add(10);
numbers.Add(20);

也可以在创建时初始化:

List<int> numbers = new List<int> { 10, 20, 30 };

常用操作

  • 添加元素:除了 Add 方法逐个添加元素外,还可以使用 AddRange 方法添加多个元素:
List<int> numbers1 = new List<int> { 1, 2, 3 };
List<int> numbers2 = new List<int> { 4, 5, 6 };
numbers1.AddRange(numbers2);
  • 访问元素:可以像数组一样通过索引访问 List<T> 中的元素:
List<int> numbers = new List<int> { 10, 20, 30 };
int firstNumber = numbers[0];
  • 删除元素Remove 方法用于删除指定的元素,RemoveAt 方法用于删除指定索引处的元素:
List<int> numbers = new List<int> { 10, 20, 30 };
numbers.Remove(20);
numbers.RemoveAt(1);
  • 查找元素Contains 方法用于检查集合中是否包含指定元素,IndexOf 方法用于查找元素的索引:
List<int> numbers = new List<int> { 10, 20, 30 };
bool contains = numbers.Contains(20);
int index = numbers.IndexOf(30);

Dictionary<TKey, TValue>

Dictionary<TKey, TValue> 表示键值对的集合,其中每个键是唯一的。

创建和初始化

Dictionary<string, int> ages = new Dictionary<string, int>();
ages.Add("Alice", 25);
ages.Add("Bob", 30);

或者在创建时初始化:

Dictionary<string, int> ages = new Dictionary<string, int> {
    { "Alice", 25 },
    { "Bob", 30 }
};

常用操作

  • 添加和访问元素:通过键来添加和访问值。如果尝试添加已存在的键,会抛出 ArgumentException
Dictionary<string, int> ages = new Dictionary<string, int> {
    { "Alice", 25 },
    { "Bob", 30 }
};
int aliceAge = ages["Alice"];
ages["Charlie"] = 35;
  • 删除元素:使用 Remove 方法通过键删除键值对:
Dictionary<string, int> ages = new Dictionary<string, int> {
    { "Alice", 25 },
    { "Bob", 30 }
};
ages.Remove("Bob");
  • 检查键是否存在:使用 ContainsKey 方法检查字典中是否包含指定的键:
Dictionary<string, int> ages = new Dictionary<string, int> {
    { "Alice", 25 },
    { "Bob", 30 }
};
bool hasAlice = ages.ContainsKey("Alice");

HashSet

HashSet<T> 表示一个集合,其中的元素是唯一的,没有重复。

创建和初始化

HashSet<int> numbers = new HashSet<int>();
numbers.Add(10);
numbers.Add(20);

也可以在创建时初始化:

HashSet<int> numbers = new HashSet<int> { 10, 20, 30 };

常用操作

  • 添加元素Add 方法用于添加元素,如果元素已存在,则添加操作会失败但不会抛出异常。
HashSet<int> numbers = new HashSet<int> { 10, 20 };
numbers.Add(30);
  • 检查元素是否存在:使用 Contains 方法检查集合中是否包含指定元素:
HashSet<int> numbers = new HashSet<int> { 10, 20 };
bool contains = numbers.Contains(20);
  • 集合操作HashSet<T> 支持一些集合操作,如并集、交集和差集。
HashSet<int> set1 = new HashSet<int> { 1, 2, 3 };
HashSet<int> set2 = new HashSet<int> { 3, 4, 5 };
HashSet<int> union = new HashSet<int>(set1);
union.UnionWith(set2);
HashSet<int> intersection = new HashSet<int>(set1);
intersection.IntersectWith(set2);
HashSet<int> difference = new HashSet<int>(set1);
difference.ExceptWith(set2);

Queue

Queue<T> 表示一个先进先出(FIFO)的集合。

创建和初始化

Queue<int> numbers = new Queue<int>();
numbers.Enqueue(10);
numbers.Enqueue(20);

也可以在创建时初始化:

Queue<int> numbers = new Queue<int>(new int[] { 10, 20, 30 });

常用操作

  • 添加元素:使用 Enqueue 方法将元素添加到队列的末尾。
Queue<int> numbers = new Queue<int>();
numbers.Enqueue(10);
  • 移除元素Dequeue 方法移除并返回队列开头的元素,如果队列为空则抛出 InvalidOperationExceptionPeek 方法返回队列开头的元素但不移除它。
Queue<int> numbers = new Queue<int> { 10, 20, 30 };
int firstNumber = numbers.Dequeue();
int peekedNumber = numbers.Peek();

Stack

Stack<T> 表示一个后进先出(LIFO)的集合。

创建和初始化

Stack<int> numbers = new Stack<int>();
numbers.Push(10);
numbers.Push(20);

也可以在创建时初始化:

Stack<int> numbers = new Stack<int>(new int[] { 10, 20, 30 });

常用操作

  • 添加元素:使用 Push 方法将元素添加到栈的顶部。
Stack<int> numbers = new Stack<int>();
numbers.Push(10);
  • 移除元素Pop 方法移除并返回栈顶的元素,如果栈为空则抛出 InvalidOperationExceptionPeek 方法返回栈顶的元素但不移除它。
Stack<int> numbers = new Stack<int> { 10, 20, 30 };
int topNumber = numbers.Pop();
int peekedNumber = numbers.Peek();

集合类型的选择

在选择使用哪种集合类型时,需要考虑以下因素:

性能

  • List:适合需要频繁插入和删除元素,并且需要通过索引访问元素的场景。它的查找性能相对较低,尤其是对于大型集合,因为查找需要遍历整个列表。
  • Dictionary<TKey, TValue>:适合通过键快速查找值的场景,其查找操作平均时间复杂度为 O(1)。但插入和删除操作可能会有一些开销,特别是在需要重新调整哈希表大小时。
  • HashSet:适合需要确保元素唯一性的场景,添加、删除和查找操作的平均时间复杂度为 O(1)。
  • QueueStack:适合实现特定的队列和栈数据结构,它们的操作(如入队、出队、入栈、出栈)时间复杂度为 O(1)。

内存使用

  • List:在动态添加元素时,可能会多次重新分配内存,导致内存使用不太稳定。
  • Dictionary<TKey, TValue>:由于使用哈希表,会有一些额外的内存开销用于存储哈希桶和相关的元数据。
  • HashSet:类似 Dictionary<TKey, TValue>,也使用哈希表,有一定的内存开销。
  • QueueStack:内存使用相对较为直接,主要取决于存储的元素数量。

功能需求

  • 如果需要按顺序存储元素并且通过索引访问,List<T> 是一个好选择。
  • 如果需要通过键快速查找值,Dictionary<TKey, TValue> 是首选。
  • 如果需要确保元素唯一,HashSet<T> 是合适的。
  • 如果需要实现先进先出或后进先出的逻辑,分别选择 Queue<T>Stack<T>

集合类型与数组的转换

数组转集合

C# 提供了方便的方法将数组转换为集合。例如,可以将数组转换为 List<T>

int[] numbersArray = { 1, 2, 3, 4, 5 };
List<int> numbersList = new List<int>(numbersArray);

也可以将数组转换为 HashSet<T>

int[] numbersArray = { 1, 2, 3, 4, 5 };
HashSet<int> numbersSet = new HashSet<int>(numbersArray);

对于 Dictionary<TKey, TValue>,如果有合适的键值对数组,可以这样转换:

KeyValuePair<string, int>[] pairsArray = {
    new KeyValuePair<string, int>("Alice", 25),
    new KeyValuePair<string, int>("Bob", 30)
};
Dictionary<string, int> agesDictionary = new Dictionary<string, int>(pairsArray);

集合转数组

集合类型也可以很容易地转换为数组。List<T>ToArray 方法:

List<int> numbersList = new List<int> { 1, 2, 3, 4, 5 };
int[] numbersArray = numbersList.ToArray();

HashSet<T> 同样有 ToArray 方法:

HashSet<int> numbersSet = new HashSet<int> { 1, 2, 3, 4, 5 };
int[] numbersArray = numbersSet.ToArray();

对于 Dictionary<TKey, TValue>,可以将键或值转换为数组:

Dictionary<string, int> agesDictionary = new Dictionary<string, int> {
    { "Alice", 25 },
    { "Bob", 30 }
};
string[] keysArray = agesDictionary.Keys.ToArray();
int[] valuesArray = agesDictionary.Values.ToArray();

集合的排序与查找

排序

许多集合类型都支持排序操作。对于 List<T>,可以使用 Sort 方法进行排序。如果 T 实现了 IComparable<T> 接口,Sort 方法会按照该接口定义的比较规则进行排序。例如,对于整数列表:

List<int> numbers = new List<int> { 3, 1, 4, 1, 5 };
numbers.Sort();

如果 T 没有实现 IComparable<T>,可以提供一个实现了 IComparer<T> 接口的比较器。例如,假设有一个自定义的类 Person

class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

class AgeComparer : IComparer<Person>
{
    public int Compare(Person x, Person y)
    {
        return x.Age.CompareTo(y.Age);
    }
}

List<Person> people = new List<Person> {
    new Person { Name = "Alice", Age = 25 },
    new Person { Name = "Bob", Age = 30 },
    new Person { Name = "Charlie", Age = 20 }
};
people.Sort(new AgeComparer());

HashSet<T> 本身不支持直接排序,但可以将其转换为 List<T> 后再进行排序。Dictionary<TKey, TValue> 由于其基于键的无序性,一般不进行整体排序,但可以对其键或值进行排序。

查找

除了前面提到的 ContainsIndexOf 等方法,集合还支持更复杂的查找操作。例如,List<T>Find 方法可以根据指定的条件查找第一个匹配的元素:

List<int> numbers = new List<int> { 10, 20, 30, 40, 50 };
int foundNumber = numbers.Find(n => n > 30);

FindAll 方法可以查找所有匹配条件的元素并返回一个新的 List<T>

List<int> numbers = new List<int> { 10, 20, 30, 40, 50 };
List<int> foundNumbers = numbers.FindAll(n => n > 30);

Dictionary<TKey, TValue> 主要通过键进行查找,但也可以通过遍历值来查找满足特定条件的值。

集合的遍历

foreach 循环

foreach 循环是遍历集合最常用的方式。对于 List<T>

List<int> numbers = new List<int> { 10, 20, 30 };
foreach (int number in numbers)
{
    Console.WriteLine(number);
}

对于 Dictionary<TKey, TValue>,可以遍历键值对:

Dictionary<string, int> ages = new Dictionary<string, int> {
    { "Alice", 25 },
    { "Bob", 30 }
};
foreach (KeyValuePair<string, int> pair in ages)
{
    Console.WriteLine($"Name: {pair.Key}, Age: {pair.Value}");
}

对于 HashSet<T>

HashSet<int> numbers = new HashSet<int> { 10, 20, 30 };
foreach (int number in numbers)
{
    Console.WriteLine(number);
}

使用迭代器

集合类型实现了 IEnumerable<T> 接口,该接口提供了 GetEnumerator 方法,返回一个迭代器。可以手动使用迭代器来遍历集合:

List<int> numbers = new List<int> { 10, 20, 30 };
IEnumerator<int> enumerator = numbers.GetEnumerator();
while (enumerator.MoveNext())
{
    int number = enumerator.Current;
    Console.WriteLine(number);
}

但一般情况下,foreach 循环更简洁和易用,它在编译时会被转换为使用迭代器的代码。

通过深入了解 C# 中的数组和各种集合类型,开发者可以根据具体的需求选择最合适的数据结构,从而提高程序的性能和可读性。无论是简单的数组,还是功能丰富的泛型集合,都在 C# 的编程世界中扮演着重要的角色。