[翻译]C#数据结构与算法 – 第二章(Part1)
第2章 Arrays与ArrayLists
数组是最常见的数据结构,出现在几乎所有的编程语言中。在C#中使用数据本质上是创建了一个System.Array类型的数组对象。System.Array类是所有数组的抽象基类型,它提供了一系列的方法用于完成如排序与查找之类在过去程序员不得不手工实现的任务。
C#中数组的一个有趣的替代者是ArrayList类。ArrayList是一个可以根据需要自动增加存储空间的数组。针对你不能准确的决定数组的最终大小或数组的大小在程序的生命周期中会改变很多次的情况,arraylist是一个比array更好的选择。
在本章中,我们将快速浏览C#中数组的基本使用,然后我们将转向更高级的话题,包括对ArrayList类对象的拷贝,克隆,测验质量及使用它们的静态方法。
数组基础
数组是基于索引的集合数据。数组既可以是内置类型也可以是用户自定义类型。事实上,把数组中数据看成是对象可能是最简单的。C#中的数据本身也是对象,因为它们继承自System.Array类。既然数组是System.Array类的实例,使用数组时你可以使用System.Array类的方法与属性。
声明与初始化数组
数组使用下面的语法声明:
type[] array-name;
type是数组元素的数据类型,例如:
1 string[] names;
第二行是必需的,其用来初始化数组(因为其是System.Array类型的对象)并决定数组的大小,下面代码初始化了刚声明的names数组:
1 names = new string[10];
并预留了10个字符串的内存空间。
在需要的时候你可以像下面这样将两个语句合并为一行:
1 string[] names = new string[10];
有时候你想将声明,初始化,赋值数组的操作放在一个语句中,在C#中,你可以通过初始化列表来完成。
1 int[] numbers = new int[] { 1, 2, 3, 4, 5 };
这个数字的列表,称作初始化列表,其放于一对花括号之间,元素之间以逗号分隔。当使用这种技术声明一个数组时,你不需要指定元素的数目,编译器由初始化列表的项目的数目来推断这个数值。
设置与访问数组元素
存储于数组的元素既可以直接访问也可以调用Array类的SetValue方法。直接访问通过赋值语句中左边的索引关联指定位置的数组元素。
1 names[2] = "Raymond"; 2 sales[19] = 23123;
SetValue方法提供了一个更面向对象的方式设置数组元素的值,此方法接受两个参数,一个索引值及一个元素值。
1 names.SetValue("Raymond", 2); 2 sales.SetValue(23123, 19);
数组元素既可通过索引直接访问也可以通过GetValue方法访问。GetValue方法接受单一参数 - 一个索引值。
1 myName = names[2]; 2 monthSales = (int)sales.GetValue(19);
使用一个for循环遍历一个数组的元素是很常见的。程序员在编写循环语句时常犯的一个错误有两个,其一是硬编码循环的上限值(这是一个错误,因为如果数组是动态的,他的上界会改变)。另一个错误是调用一个函数访问每一次循环的迭代循环的上界。
1 for (int i = 0; i <= sales.GetUpperBound(0); i++) 2 totalSales = totalSales + sales[i];
检索数组元数据的方法及属性
Array类提供了几个属性用于检查一个数组的元数据。
-
Length: 返回数组中所有维度的全部元素的个数。
-
GetLength: 返回数组指定维度的元素的个数。
-
Rank: 返回数组的维度。
-
GetType: 返回当前数组实例的类型。
Length属性用来统计多维数组元素的个数,其返回那个数组中准确的元素数目。另外,你也可以使用GetUpperBound方法得到的值加上1得到数组元素数。
Length返回数组元素的总数目,GetLength方法统计数组中单个维度的元素数目。这个方法与Rank属性一起使用,可以用来在运行时没有丢失数据的风险下调整数组的大小,这项技术将在本章后面部分讨论。
GetType方法用来在你不确定数组类型的情况下明确数组的数据类型,如将数组作为参数传给一个方法时。在下面的代码片段中,我们创建了Type类型的变量,可以使用其IsArray方法判断对象是否为一数组。如果对象是数组,代码返回数组的数据类型。
1 int[] numbers; 2 numbers = new int[] { 0, 1, 2, 3, 4, }; 3 Type arrayType = numbers.GetType(); 4 if (arrayType.IsArray) 5 Console.WriteLine("The array type is: {0}", arrayType); 6 else 7 Console.WriteLine("Not an array"); 8 Console.Read();
GetType方法不仅返回数组的类型,同时让我们知道对象本质是一个数组。如下是上述代码的输出:
The array type is: System.Int32[]
方括号指示了对象是一个数组。同样注意在显示数据类型时使用的格式。我们不得不这样做,因为我们不能将Type数据转型为string以便连接在待显示字符串的尾部。
多维数组
到目前我们只讨论了一位数组。在C#中,数组可以多达32维,虽然超过三维的数组很少见(且令人迷惑)。
声明多维数组是通过指定数组每个维度的上界,二维数组声明:
1 int[,] grades = new int[4, 5];
声明了一个由4行5列元素组成的数组。二维数组常用作建模矩阵,你也可以声明一个多维数组而不指定维度的界。完成这样的工作,你可以使用逗号指定维度数。例如:
1 double[,] Sales;
声明了一个二维数组,反之:
1 double[, ,] sales;
声明了一个三维数组。当你在不提供维度的上界的情况下声明数组后,你需要在稍后重新使用界值定义数组:
1 Sales = new double[4, 5];
多维数组可以使用初始化列表完成初始化。看下面的语句:
1 int[,] grades = new int[,]{{1,82,74,89,100}, 2 {2,93,96,85,86}, 3 {3,83,72,95,89}, 4 {4,91,98,79,88} 5 };
首先,注意数组的上界没有被指定。当你使用初始化列表初始化数组时。你不能指定数组的界。编译器由初始化列表中的数据计算出每个维度的上界。初始化列表本身以花括号标记,就像数组的每一行。一行中各个元素均以逗号分隔。
访问多维数组的元素同访问一维数组的元素相似。你可以使用传统的数组访问技术。
1 grade = grades[2, 2]; 2 grades[2, 2] = 99;
或者你可以使用Array类的方法:
1 grade = grades.GetValue(2, 2);
你不可以使用SetValue方法对一个多维数组赋值,因为这个方法只接受两个参数:一个值与一个单一索引。(原文错误)
在多维数组的所有元素上执行计算是很常见的操作,虽然大部分情况下这些操作基于存储于数组行中或存储于数组列中的值。
使用grades数组,如果数组每一行是一个学生记录,我们可以计算这个年级中学生的平均成绩。代码如下:
1 int[,] grades = new int[,]{{1,82,74,89,100}, 2 {2,93,96,85,86}, 3 {3,83,72,95,89}, 4 {4,91,98,79,88} 5 }; 6 7 int last_grade = grades.GetUpperBound(1); 8 double average = 0.0; 9 int total; 10 int last_student = grades.GetUpperBound(0); 11 for (int row = 0; row <= last_student; row++) 12 { 13 total = 0; 14 for (int col = 0; col <= last_grade; col++) 15 total += grades[row, col]; 16 average = total / last_grade; 17 Console.WriteLine("Average: " + average); 18 }
参数数组
许多方法的定义需要接受一系列的数值作为参数。但是有些情况下写一个可以接受数目可变的参数方法定义,可以使用一种称作参数数组的构造。
参数数组是一个使用params关键字定义的用于方法的参数列表。下面的方法定义允许任意数量的值作为参数提供给方法,函数的返回值是传入方法的数值的和。
1 static int sumNums(params int[] nums) 2 { 3 int sum = 0; 4 for (int i = 0; i <= nums.GetUpperBound(0); i++) 5 sum += nums[i]; 6 return sum; 7 }
该方法可以使用下面两种方式调用:
1 total = sumNums(1, 2, 3); 2 total = sumNums(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
当你在方法定义中使用参数数组,作为参数数组形式的参数必须放在参数列表的最后一位以便编译器可以正确处理参数列表。否则,编译器不能知道参数数组元素的结束位置,也就没法获得方法中其它参数的开始位置。
不规则数组
当你创建一个多维数组,你通常创建了一个每一行有相同数目元素的结构。例如,下列数组的声明。
1 int[,] sales = new int[12, 30]; //Sales for each day of each month
这个数组假定每一行(月份)拥有相同数目的元素(日),然而我们知道一些月份有30天,一些有31天,还有一个月份有29天。使用刚才声明的这个数组,将会产生一些空元素。对这个数组还算不上一个问题,但是使用一个大的多的数组时会浪费许多空间。
这个问题的解决方案就是使用一个不规则数组代替一个二维数组。不规则数组是一个由数组组成的数组,也即数组的每一行又由一个数组组成。不规则数组的每一维都是一个一维数组。我们称其"不规则"数组的原因是其每一行的元素个数可能不同。一个不规则数组的图像将不是正方形或矩形,反之会有一个形状不规则的边。
不规则数组的声明方法是在数组变量名称的后面放置两对方括号。第一对方括号中的数代表数组的行数,第二对方括号留空来为每一行中的一维数组留出空间。一般情况下,行数在声明语句中使用初始化列表设置,如下:
1 int[][] jagged = new int[12][];
这个语句看起来很奇怪,但是当你将其分开来看将会明白。这个交错数组是一个有12个元素的整型数组,其中每一个元素又是一个整型数组。初始化列表实际上用来初始化数组的每一行,这表示数组12个元素的每一个子元素即一行子数组中的每一个元素都被初始化为默认的值。
一旦一个不规则数组被声明,每一个单独的行数组的元素都可以被赋值。下面的代码段向不规则数组赋值:
1 jagged[0][0] = 23;
2 jagged[0][1] = 13;
3 //...
4 jagged[7][5] = 45;
第一对方括号指示行号,第二对方括号指示行数组的元素。上文代码中,第一行语句访问第一行数组的第一个元素,第二行语句访问第一行数组的第二个元素,第三条语句访问第八行数组的第六个元素。
举一个使用不规则数组的例子,下面的程序创建了一个名为sales(记录两个月中每周的销售情况)的数组,将销售数字赋给其元素,然后遍历数组来计算数组中存储的两个月中每周的平均销售记录。
1 using System; 2 class class1 3 { 4 static void Main() 5 { 6 int[] Jan = new int[31]; 7 int[] Feb = new int[29]; 8 //注:此处使用原文的写法无法编译通过,特改写为C#3.0的初始化列表写法。 9 int[][] sales = new int[][] { Jan, Feb }; 10 int month, day, total; 11 double average = 0.0; 12 sales[0][0] = 41; 13 sales[0][1] = 30; 14 sales[0][0] = 41; 15 sales[0][1] = 30; 16 sales[0][2] = 23; 17 sales[0][3] = 34; 18 sales[0][4] = 28; 19 sales[0][5] = 35; 20 sales[0][6] = 45; 21 sales[1][0] = 35; 22 sales[1][1] = 37; 23 sales[1][2] = 32; 24 sales[1][3] = 26; 25 sales[1][4] = 45; 26 sales[1][5] = 38; 27 sales[1][6] = 42; 28 for (month = 0; month <= 1; month++) 29 { 30 total = 0; 31 for (day = 0; day <= 6; day++) 32 { 33 total += sales[month][day]; 34 } 35 average = total / 7; 36 Console.WriteLine("Average sales for month:" + month + ": " + average); 37 } 38 } 39 }
接下来要翻译的部分介绍了ArrayList类,本书中很少有涉及到C#2.0出现的泛型类,对于这点我将在整理完此书后,根据相应的章节补充一下泛型的内容,并且将有些程序使用C#3.0重新实现。