数组
Casey Reas 和 Ben Fry
数组这个词指的是一个结构化的分组或一个令人印象深刻的数字:“自助晚餐提供一系列选择”,“波士顿市面临一系列问题。”在计算机编程中,数组是一组以相同名称存储的数据元素。可以创建数组来保存任何类型的数据,每个元素都可以单独分配和读取。可以有数字、字符、句子、布尔值等数组。数组可以存储复杂形状的顶点数据、键盘最近的击键或从文件读取的数据。例如,一个数组可以存储五个整数(1919、1940、1975、1976、1990),即辛辛那提红军赢得世界大赛冠军的年份,而不是定义五个单独的变量。我们将此数组称为“dates”,并按顺序存储值:
数组元素是从零开始编号的,一开始看起来很混乱,但对于许多编程语言来说,这是一个重要的细节。第一个元素位于位置[0],第二个元素位于位置[1],依此类推。每个元素的位置由其与数组开头的偏移量决定。第一个元素位于位置[0],因为它没有偏移量;第二个元素位于位置[1],因为它从开始位置偏移了一个位置。数组中的最后一个位置是通过从数组长度中减去1来计算的。在本例中,最后一个元素位于位置[4],因为数组中有五个元素。
数组可以使编程任务更容易。虽然不必使用它们,但它们可以成为管理数据的宝贵结构。让我们从一组数据点开始构建条形图。
下面的示例演示了使用数组的一些好处,比如避免了在单个变量中存储数据点的繁琐工作。因为图表有10个数据点,所以将这些数据输入到程序中需要创建10个变量或使用一个数组。左边的代码演示了如何使用单独的变量。右边的代码显示了如何将数据元素逻辑地分组到一个数组中。
int x0 = 50; int[] x = { 50, 61, 83, 69, 71,
int x1 = 61; 50, 29, 31, 17, 39 };
int x2 = 83;
int x3 = 69;
int x4 = 71;
int x5 = 50;
int x6 = 29;
int x7 = 31;
int x8 = 17;
int x9 = 39;
使用我们所知道的无数组绘制,需要10个变量来存储数据;每个变量都用于绘制单个矩形。这很乏味:
int x0 = 50;
int x1 = 61;
int x2 = 83;
int x3 = 69;
int x4 = 71;
int x5 = 50;
int x6 = 29;
int x7 = 31;
int x8 = 17;
int x9 = 39;
fill(0);
rect(0, 0, x0, 8);
rect(0, 10, x1, 8);
rect(0, 20, x2, 8);
rect(0, 30, x3, 8);
rect(0, 40, x4, 8);
rect(0, 50, x5, 8);
rect(0, 60, x6, 8);
rect(0, 70, x7, 8);
rect(0, 80, x8, 8);
rect(0, 90, x9, 8);
相反,下面的示例演示如何在程序中使用数组。使用for循环按顺序访问每个条的数据。下面几页将更详细地讨论数组的语法和用法。
int[] x = {
50, 61, 83, 69, 71, 50, 29, 31, 17, 39
};
fill(0);
// 每次通过for循环读取一个数组元素
for (int i = 0; i < x.length; i++) {
rect(0, i*10, x[i], 8);
}
定义数组
数组的声明与其他数据类型类似,但它们用方括号[和]区分。声明数组时,必须指定它存储的数据类型。(每个数组只能存储一种类型的数据。)声明数组后,必须使用关键字new创建它,就像处理对象一样。这个额外的步骤分配计算机内存中的空间来存储数组的数据。创建数组后,可以指定值。有不同的方法来声明、创建和分配数组。在下面解释这些差异的示例中,将创建一个包含五个元素的数组,并用值19、40、75、76和90填充该数组。请注意,创建和分配数组元素的每种技术与setup()相关的方式不同。
int[] data; //声明
void setup() {
size(100, 100);
data = new int[5]; // 创建
data[0] = 19; // 分配
data[1] = 40;
data[2] = 75;
data[3] = 76;
data[4] = 90;
}
int[] data = new int[5]; // 声明,创建
void setup() {
size(100, 100);
data[0] = 19; // 分配
data[1] = 40;
data[2] = 75;
data[3] = 76;
data[4] = 90;
}
int[] data = { 19, 40, 75, 76, 90 }; // 声明,创建,分配
void setup() {
size(100, 100);
}
尽管前面的三个示例都以不同的方式定义数组,但它们都是等价的。它们显示了在定义数组数据时所允许的灵活性。有时,程序将使用的所有数据在开始时都是已知的,并且可以立即分配。在其他时候,数据是在代码运行时生成的。使用这些技术,每个草图都可以有不同的处理方法。
数组也可以用于不包含setup()和draw()的程序中,但需要声明、创建和分配三个步骤。如果数组不与这些函数一起使用,则可以按照以下示例中所示的方式创建和分配它们。
int[] data; // 声明
data = new int[5]; // 创建
data[0] = 19; // 分配
data[1] = 40;
data[2] = 75;
data[3] = 76;
data[4] = 90;
int[] data = new int[5]; // 声明,创建
data[0] = 19; // 分配
data[1] = 40;
data[2] = 75;
data[3] = 76;
data[4] = 90;
int[] data = { 19, 40, 75, 76, 90 }; // 声明,创建,分配
读取数组元素
定义并分配值后,可以在代码中访问和使用数组的数据。使用数组变量的名称访问数组元素,然后在元素位置的方括号中读取
int[] data = { 19, 40, 75, 76, 90 };
line(data[0], 0, data[0], 100);
line(data[1], 0, data[1], 100);
line(data[2], 0, data[2], 100);
line(data[3], 0, data[3], 100);
line(data[4], 0, data[4], 100);
请记住,数组中的第一个元素位于0位置。如果试图访问位于数组边界之外的数组成员,程序将终止并给出ArrayIndexOutOfBoundsException。
int[] data = { 19, 40, 75, 76, 90 };
println(data[0]); // 输出19
println(data[2]); // 输出75
println(data[5]); // 提示错误,因为序号是从0开始排序
长度字段存储数组中的元素数。此字段存储在数组中,并使用点运算符访问(第363-379页)。下面的示例演示如何利用它。
int[] data1 = { 19, 40, 75, 76, 90 };
int[] data2 = { 19, 40 };
int[] data3 = new int[127];
println(data1.length); // 输出5个
println(data2.length); // 输出2个
println(data3.length); // 输出127个
通常,for循环用于访问数组元素,特别是对于大型数组。下面的示例绘制与代码28-09相同的行,但使用for循环遍历数组中的每个值。
int[] data = { 19, 40, 75, 76, 90 };
for (int i = 0; i < data.length; i++) {
line(data[i], 0, data[i], 100);
}
for循环还可用于将数据放入数组中。例如,它可以计算一系列数字,然后将每个值赋给数组元素。以下示例将sin()函数中的值存储在setup()中的数组中,然后将这些值显示为draw()中直线的笔划值。
float[] sineWave;
void setup() {
size(100, 100);
sineWave = new float[width];
for (int i = 0; i < sineWave.length; i++) {
// 用sin()中的值填充数组
float r = map(i, 0, width, 0, TWO_PI);
sineWave[i] = abs(sin(r));
}
}
void draw() {
for (int i = 0; i < sineWave.length; i++) {
// 将笔划值设置为从数组中读取的数字
stroke(sineWave[i] * 255);
line(i, 0, i, height);
}
}
记录数据
作为如何使用数组的一个示例,本节将演示如何使用数组存储来自鼠标的数据。pmouseX和pmouseY变量存储前一帧的光标坐标,但是没有内置的方法来访问前一帧的光标值。在每一帧中,mouseX、mouseY、pmouseX和pmouseY变量都被替换为新的数字,并且它们以前的数字被丢弃。创建数组是存储这些值历史记录的最简单方法。在下面的示例中,mouseY中最近的100个值存储在屏幕上,并显示为从屏幕左到右边缘的一行。在每一帧,数组中的值向右移动,最新的值添加到开头。
int[] y;
void setup() {
size(100, 100);
y = new int[width];
}
void draw() {
background(204); // 从头到尾读取数组
// 开始避免覆盖数据
for (int i = y.length-1; i > 0; i--) {
y[i] = y[i-1];
}
// 在开头添加新值
y[0] = mouseY;
// 将每对值显示为一行
for (int i = 1; i < y.length; i++) {
line(i, y[i], i-1, y[i-1]);
}
}
同时对mouseX和mouseY值应用相同的代码来存储光标的位置。每帧显示这些值都会在光标后面创建一条轨迹。
int num = 50;
int[] x = new int[num];
int[] y = new int[num];
void setup() {
size(100, 100);
noStroke();
fill(255, 102);
}
void draw() {
background(0);
// 将值右移
for (int i = num-1; i > 0; i--) {
x[i] = x[i-1];
y[i] = y[i-1];
}
// 将新值添加到数组的开头
x[0] = mouseX;
y[0] = mouseY;
// 画圆圈
for (int i = 0; i < num; i++) {
ellipse(x[i], y[i], i/2.0, i/2.0);
}
}
下面的示例生成与上一个示例相同的结果,但使用了更有效的技术。程序不会在每帧中移动数组元素,而是将新数据写入下一个可用的数组位置。数组中的元素在写入后保持在相同的位置,但每帧的读取顺序不同。读取从最旧数据元素的位置开始,一直到数组的末尾。在数组的末尾,使用%运算符(第57页)返回到开头。这种技术,通常被称为环形缓冲区,特别适用于较大的数组,以避免不必要的数据复制,从而减慢程序的速度。
int num = 50;
int[] x = new int[num];
int[] y = new int[num];
int indexPosition = 0;
void setup() {
size(100, 100);
noStroke();
fill(255, 102);
}
void draw() {
background(0);
x[indexPosition] = mouseX;
y[indexPosition] = mouseY;
// 在0和元素数之间循环
indexPosition = (indexPosition + 1) % num;
for (int i = 0; i < num; i++) {
// 将数组位置设置为读取
int pos = (indexPosition + i) % num;
float radius = (num-i) / 2.0;
ellipse(x[pos], y[pos], radius, radius);
}
}
数组函数
处理提供一组有助于管理阵列数据的函数。这里只介绍了其中的四个功能,但在软件附带的处理参考资料中有更多说明。
函数的作用是:将数组扩展一个元素,将数据添加到新位置,并返回新数组:
String[] trees = { "ash", "oak" };
append(trees, "maple"); // 不正确!不更改数组
printArray(trees); // 打印[0]“ash”、[1]“oak”
println();
trees = append(trees, "maple"); // 在结尾加上“maple”
printArray(trees); // Prints [0] "ash", [1] "oak", [2] "maple"
println();
// 将“beech”添加到trees数组的末尾,并创建一个新的
// 存储更改的数组
String[] moretrees = append(trees, "beech");
// Prints [0] "ash", [1] "oak", [2] "maple", [3] "beech"
printArray(moretrees);
函数的作用是:通过删除最后一个元素,将数组减少一个元素,并返回缩短的数组:
String[] trees = { "lychee", "coconut", "fig" };
trees = shorten(trees); // 从数组中删除最后一个元素
printArray(trees); // Prints [0] "lychee", [1] "coconut"
println();
trees = shorten(trees); // 从数组中删除最后一个元素
printArray(trees); // Prints [0] "lychee"
函数的作用是:增加数组的大小。它可以扩展到特定的大小,或者如果未指定大小,则数组的长度将加倍。如果一个数组需要有许多额外的元素,那么使用expand()将大小加倍要比使用append()一次连续添加一个值快。下面的示例将新的mouseX值保存到每帧的数组中。当数组变满时,数组的大小将加倍,新的mouseX值将继续填充放大的数组。
int[] x = new int[100]; //存储x坐标的数组
int count = 0; // 存储在数组中的位置
void setup() {
size(100, 100);
}
void draw() {
x[count] = mouseX; // 为阵列指定新的x坐标
count++; // 增加计数器
if (count == x.length) { // 如果x数组已满,
x = expand(x); // x的两倍大小
println(x.length); // 将新大小写入控制台
}
}
不能使用赋值运算符复制数组值,因为它们是对象。将元素从一个数组复制到另一个数组的最常用方法是使用特殊函数或在for循环中单独复制每个元素。函数的作用是将一个数组的全部内容复制到另一个数组中。数据从用作第一个参数的数组复制到用作第二个参数的数组。两个数组的长度必须相同,才能在此处显示的配置中工作。
String[] north = { "OH", "IN", "MI" };
String[] south = { "GA", "FL", "NC" };
arrayCopy(north, south); // 从北方数组复制到南数组
printArray(south); // Prints [0] "OH", [1] "IN", [3] "MI"
println();
String[] east = { "MA", "NY", "RI" };
String[] west = new String[east.length]; // 创建新数组
arrayCopy(east, west); // 从东方数组复制到西方阵
printArray(west); // Prints [0] "MA", [1] "NY", [2] "RI"
可以编写新函数来对数组执行操作,但数组的行为与int和char等数据类型不同。与对象一样,当数组用作函数的参数时,数组的地址(内存中的位置)将被传输到函数中,而不是实际的数据。不会创建新数组,在函数中所做的更改会影响用作参数的数组。
在下面的示例中,data[]数组用作halve()的参数。数据[]的地址传递给halve()函数中的d[]数组。因为d[]和data[]的地址相同,所以它们都指向相同的数据。在第14行对d[]所做的更改修改了setup()块中的data[]的值。不使用draw()函数,因为只进行一次计算,并且不会将任何内容绘制到显示窗口。
float[] data = { 19.0, 40.0, 75.0, 76.0, 90.0 };
void setup() {
halve(data);
println(data[0]); // Prints "9.5"
println(data[1]); // Prints "20.0"
println(data[2]); // Prints "37.5"
println(data[3]); // Prints "38.0"
println(data[4]); // Prints "45.0"
}
void halve(float[] d) {
for (int i = 0; i < d.length; i++) { // 对于每个数组元素,
d[i] = d[i] / 2.0; // 将值除以2
}
}
在不修改原始数组的情况下更改函数中的数组数据需要一些额外的代码行。在下面的示例中,数组作为参数传递到函数中,创建一个新数组,将原始数组中的值复制到新数组中,对新数组进行更改,最后返回修改后的数组。
float[] data = { 19.0, 40.0, 75.0, 76.0, 90.0 };
float[] halfData;
void setup() {
halfData = halve(data); // 运行halve()函数
println(data[0], halfData[0]); // Prints "19.0, 9.5"
println(data[1], halfData[1]); // Prints "40.0, 20.0"
println(data[2], halfData[2]); // Prints "75.0, 37.5"
println(data[3], halfData[3]); // Prints "76.0, 38.0"
println(data[4], halfData[4]); // Prints "90.0, 45.0"
}
float[] halve(float[] d) {
float[] numbers = new float[d.length]; // 创建新数组
arrayCopy(d, numbers);
for (int i = 0; i < numbers.length; i++) { // 对于每个元素,
numbers[i] = numbers[i] / 2.0; // 将值除以2
}
return numbers; // 返回新数组
}
对象数组
使用对象数组在技术上类似于使用其他数据类型的数组,但它提供了一种惊人的可能性,可以根据需要创建任意多个自定义设计类的实例。与所有数组一样,对象数组与带有方括号和字符的单个对象不同。但是,由于每个数组元素都是一个对象,因此必须使用关键字new创建每个元素,然后才能使用它。使用对象数组的步骤如下:
- 声明数组
- 创建数组
- 在数组中创建每个对象
在下面的示例中将这些步骤转换为代码。它使用第371页中的Ring类,所以复制或重新键入它。此代码创建一个rings[]数组来容纳50个Ring对象。在setup()中为rings[]数组分配内存空间,并创建每个Ring对象。第一次按下鼠标按钮时,将打开第一个环形对象,并将其x和y变量指定给光标的当前值。每次按下鼠标按钮时,都会打开一个新环并在随后的trip through draw()中显示。当数组中的最后一个元素被创建时,程序会跳回到数组的开头,为之前的环分配新的位置
Ring[] rings; // 声明数组
int numRings = 50;
int currentRing = 0;
void setup() {
size(100, 100);
rings = new Ring[numRings]; // 创建数组
for (int i = 0; i < rings.length; i++) {
rings[i] = new Ring(); //创建每个对象
}
}
void draw() {
background(0);
for (int i = 0; i < rings.length; i++) {
rings[i].grow();
rings[i].display();
}
}
//单击以创建新环
void mousePressed() {
rings[currentRing].start(mouseX, mouseY);
currentRing++;
if (currentRing >= numRings) {
currentRing = 0;
}
}
class Ring {
float x, y; // X坐标,y坐标
float diameter; // 环的直径
boolean on = false; // 打开和关闭显示器
void start(float xpos, float ypos) {
x = xpos;
y = ypos;
diameter = 1;
on = true;
}
void grow() {
if (on == true) {
diameter += 0.5;
if (diameter > 400) {
on = false;
diameter = 1;
}
}
}
void display() {
if (on == true) {
noFill();
strokeWeight(4);
stroke(204, 153);
ellipse(x, y, diameter, diameter);
}
}
}
下一个例子需要第363页的Spot类。与前面的示例不同,变量值在setup()中生成,并通过对象的构造函数传递到每个数组元素中。数组中的每个元素都以一组唯一的x坐标、直径和速度值开始。由于对象的数量取决于显示窗口的宽度,因此在程序知道数组的宽度之前,无法创建数组。因此,数组是在setup()之外声明的,以使其成为全局的(参见第12页),但它是在setup内部创建的,在定义了显示窗口的宽度之后。
Spot[] spots; //声明数组
void setup() {
size(700, 100);
int numSpots = 70; // 对象数
int dia = width/numSpots; // 计算直径
spots = new Spot[numSpots]; // 创建数组
for (int i = 0; i < spots.length; i++) {
float x = dia/2 + i*dia;
float rate = random(0.1, 2.0);
// 创建每个对象
spots[i] = new Spot(x, 50, dia, rate);
}
noStroke();
}
void draw() {
fill(0, 12);
rect(0, 0, width, height);
fill(255);
for (int i=0; i < spots.length; i++) {
spots[i].move(); // 移动每个对象
spots[i].display(); // 显示每个对象
}
}
class Spot {
float x, y; // X坐标,y坐标
float diameter; // 圆的直径
float speed; // 每帧移动的距离
int direction = 1; // 运动方向(1向下,-1向上)
// 构造器
Spot(float xpos, float ypos, float dia, float sp) {
x = xpos;
y = ypos;
diameter = dia;
speed = sp;
}
void move() {
y += (speed * direction);
if ((y > (height - diameter/2)) || (y < diameter/2)) {
direction *= -1;
}
}
void display() {
ellipse(x, y, diameter, diameter);
}
}
使用对象数组使我们有机会使用称为增强for循环的代码结构访问每个对象,以简化代码。与本章前面使用的for循环不同,增强的循环自动逐个遍历数组中的每个元素,而无需定义启动和停止条件。增强循环的结构是通过声明数组元素的数据类型、要分配给数组中每个元素的变量名以及数组的名称。例如,代码28-25中的For循环被重写如下:
for (Spot s : spots) {
s.move();
s.display();
}
数组中的每个对象依次分配给变量s,因此第一次通过循环时,代码s.move()为数组中的第一个元素运行move()方法,然后下一次通过循环时,s.move()为数组中的第二个元素运行move()方法,等。块中的两个语句对数组的每个元素运行,直到数组结束。这种访问对象数组中每个元素的方法用于本书的其余部分。
二维阵列
数据也可以从具有多个维度的数组中存储和检索。使用本章开头的示例,将图表的数据点放入二维数组中,其中
添加灰色值:
二维数组本质上是一维数组的列表。它必须先声明,然后创建,然后才可以像在1D数组中一样分配值。以下语法将上图转换为代码:
int[][] x = { {50, 0}, {61,204}, {83,51}, {69,102}, {71, 0},
{50,153}, {29, 0}, {31,51}, {17,102}, {39,204} };
println(x[0][0]); // Prints "50"
println(x[0][1]); // Prints "0"
println(x[4][2]); // 错误!这个元素在数组之外
println(x[3][0]); // Prints "69"
println(x[9][1]); // Prints "204"
这个草图显示了它是如何组合在一起的。
/
int[][] x = { {50, 0}, {61,204}, {83,51}, {69,102},
{71, 0}, {50,153}, {29, 0}, {31,51},
{17,102}, {39,204} };
void setup() {
size(100, 100);
}
void draw() {
for (int i = 0; i < x.length; i++) {
fill(x[i][1]);
rect(0, i*10, x[i][0], 8);
}
}
通过外推这些技术,可以继续制作3D和4D阵列。然而,多维数组可能会令人困惑,通常最好维护多个一维或二维数组。