跳转至

C1-08 数组

数组声明、下标、循环读入与基本遍历

学习目标:理解数组是“装着一排同类型数据的盒子”;掌握一维数组的声明、初始化和元素访问;理解下标从 0 开始;能够用 for 循环完成数组的读入、输出、求和、求最大值;养成先看数据范围、再检查下标边界的编程习惯。

一、为什么需要数组

1.只用变量会遇到什么问题

前面我们学习了变量和循环。比如,读入 n 个数并求和,可以边读边加:

int n;
cin >> n;

int sum = 0;
for (int i = 0; i < n; i++) {
    int x;
    cin >> x;
    sum += x;
}

cout << sum << endl;

这个程序可以求和,但是每个数读进来以后就被处理掉了。如果题目要求:

  • 先读入一组数,再从后往前输出;
  • 先求平均值,再统计大于平均值的数;
  • 读入以后多次查询某个位置的数;

就必须把这些数保存下来。

如果要保存 5 个数,可以定义:

int a1, a2, a3, a4, a5;

如果要保存 100 个数,就要定义 100 个变量。这样的写法既长又容易出错。

2.数组是什么

数组是一组相同类型数据的集合。可以把它想象成一排连续的抽屉:

┌──────┬──────┬──────┬──────┬──────┐
│ a[0] │ a[1] │ a[2] │ a[3] │ a[4] │
└──────┴──────┴──────┴──────┴──────┘
  • 每个抽屉只能放一种类型的数据;
  • 每个抽屉都有一个编号,这个编号叫下标;
  • 一个数组名可以管理很多个数据;
  • 通过“数组名 + 下标”可以找到其中某一个元素。

第 7 课解决的是“读入一个、处理一个”;本课学习“先把数据存起来,再反复使用”。

3.本课学习范围

本课只学习一维数组:

  • 数组的声明;
  • 数组的下标;
  • 数组的初始化;
  • 用 for 循环读入和输出;
  • 遍历数组求和、求最大值;
  • 数组逆序输出。

二维数组、字符串数组和更复杂的数据结构放在后续课程学习。

二、一维数组的声明

1.基本语法

类型 数组名[大小];

例如:

int a[5];          // 能存 5 个 int
double score[10];  // 能存 10 个 double
char letters[26];  // 能存 26 个 char

数组声明有三个要素:

要素 作用 示例
类型 决定每个元素保存什么数据 int、double、char
数组名 给这一排数据起一个名字 a、score
大小 表示最多能放多少个元素 5、100

2.数组大小要提前准备

定义数组时,先根据题目给出的最大数据个数,提前准备好一块足够大的空间。最直接的写法,是把具体数字写在方括号中:

int a[105];

如果题目保证 n 不超过 100,可以多开 5 个位置,写成 int a[105]。多出的几个位置可以看作“保险空间”,但实际使用时仍然只使用前 n 个元素。

题目如果说:

n 不超过 100

就可以准备:

int a[105];       // 100 个数据 + 5 个保险位置

当程序中有多个长度相同的数组时,为了以后修改大小更方便,可以把数组长度写成一个常量:

const int N = 1005;
int a[N], b[N], c[N];

以后如果题目范围发生变化,只需要修改 N 这一行,三个数组会一起改变大小。

现在的 C++ 标准已经支持把运行时读入的 n 用作数组长度,例如:

int n;
cin >> n;
int a[n];

不过,这种写法会让数组大小依赖运行时数据,不适合作为信奥入门阶段的固定写法。为了代码更规范、更容易迁移,仍然建议提前准备常量大小的数组:

const int N = 1005;
int a[N];

无论采用哪种声明方式,都必须保证实际使用的元素个数不超过数组容量。

3.数组不是“所有位置都必须使用”

int a[105];

这表示数组最多可以存 105 个整数。如果题目实际输入 n = 5,我们只使用:

a[0], a[1], a[2], a[3], a[4]

剩下的位置暂时不用。

三、数组下标:从 0 开始

1.下标和第几个元素

如果定义:

int a[5];

数组中有 5 个元素,但下标不是 1 到 5,而是:

a[0]  a[1]  a[2]  a[3]  a[4]
第1个  第2个  第3个  第4个  第5个

数组下标从 0 开始。

大小为 N 的数组,有效下标范围是:

0 到 N - 1

所以:

  • 第 1 个元素是 a[0];
  • 第 2 个元素是 a[1];
  • 第 n 个元素是 a[n - 1];
  • 最后一个元素是 a[N - 1]。

2.访问数组元素

数组元素的访问格式是:

数组名[下标]

示例:

int a[5];

a[0] = 10;   // 给第 1 个元素赋值
a[1] = 20;   // 给第 2 个元素赋值
a[4] = 50;   // 给第 5 个元素赋值

cout << a[0] << endl;  // 输出 10
cout << a[4] << endl;  // 输出 50

数组元素和普通变量一样,可以出现在赋值、输入、输出和计算中:

a[2] = a[0] + a[1];

cin >> a[3];

cout << a[3] << endl;

3.数组越界

对于 int a[5],有效下标只有 0、1、2、3、4。

下面的写法是错误的:

a[5] = 100;  // 越界,数组没有第 6 个位置

a[5] 并不是“第 5 个元素”,而是下标为 5 的元素,也就是第 6 个位置。C++ 通常不会自动提醒你越界,程序可能出现乱码、运行错误或其他不可预测的结果。

重点记忆: 数组大小是 N,最大下标是 N - 1,不是 N。

4.用表格理解下标

int a[4] = {7, 3, 9, 2};
下标 a[0] a[1] a[2] a[3]
元素值 7 3 9 2
第几个 第 1 个 第 2 个 第 3 个 第 4 个

四、数组初始化

1.完整初始化

声明数组时,可以直接给元素赋初值:

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

对应关系是:

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

2.部分初始化

如果只写出一部分初值,前面的元素按顺序赋值,没有写出的元素自动补 0:

int a[5] = {10, 20};

实际结果是:

a[0] = 10
a[1] = 20
a[2] = 0
a[3] = 0
a[4] = 0

这条规则很重要:花括号中没有写出的元素,会自动初始化为 0。

3.全部初始化为 0

常见写法:

int a[100] = {};

空花括号表示“没有指定任何初值”,因此所有元素都会自动初始化为 0。

有时候会看到下面的写法:

int a[100] = {0};

但要注意,int a[100] = {0}; 从语法本质上属于部分初始化:它只明确写出了第一个初值 0,其余没有写出的元素再按照规则自动补 0。因为补出来的也是 0,所以最终效果仍然是整个数组为 0。

4.不初始化的数组

int a[100];

如果这是一个局部数组,里面的值可能是内存中原来留下的内容,直接读取会得到不确定的结果,犹如“开盲盒”,开到什么就是什么:

cout << a[0] << endl;  // 不要这样直接读取a[0]并输出,会输出不确定的值

如果数组后面会通过 cin 给每个位置赋值,那么可以不先清零:

int a[100];

for (int i = 0; i < n; i++) {
    cin >> a[i];
}

如果数组中有些位置可能暂时不赋值,建议使用:

int a[100] = {};

五、用 for 循环读入数组

数组下标是连续的整数 0、1、2、...,这和 for 循环非常适合配合。

1.从下标 0 开始:读入模板

下面把数组定义、读入和输出放在一起。数组实际使用的位置是 a[0] 到 a[n - 1]:

const int N = 105;
int a[N];

int n;
cin >> n;

for (int i = 0; i < n; i++) {
    cin >> a[i];
}

for (int i = 0; i < n; i++) {
    cout << a[i] << " ";
}
cout << endl;

循环变量 i 同时充当数组下标:

第 1 轮:i = 0,读入 a[0]
第 2 轮:i = 1,读入 a[1]
第 3 轮:i = 2,读入 a[2]
……
最后一轮:i = n - 1,读入 a[n - 1]

2.从下标 1 开始:另一种读入模板

也可以把第一个数据放在 a[1] 中,实际使用的位置是 a[1] 到 a[n]:

const int N = 105;
int a[N];

int n;
cin >> n;

for (int i = 1; i <= n; i++) {
    cin >> a[i];
}

for (int i = 1; i <= n; i++) {
    cout << a[i] << " ";
}
cout << endl;

两种写法都可以,关键是让循环起点、循环条件和数组下标保持一致。后面的数组遍历、求和、求最值和逆序输出例子,均以从下标 0 开始为例。

3.完整示例:读入并原样输出

#include <iostream>
using namespace std;

const int N = 105;
int a[N];

int main() {
    int n;
    cin >> n;

    for (int i = 0; i < n; i++) {
        cin >> a[i];
    }

    for (int i = 0; i < n; i++) {
        cout << a[i] << " ";
    }

    cout << endl;
    return 0;
}

输入:

5
10 20 30 40 50

输出:

10 20 30 40 50

4.为什么条件是 i < n

当 n = 5 时,实际使用的下标是:

0、1、2、3、4

所以循环应写成:

for (int i = 0; i < n; i++)

如果误写成:

for (int i = 0; i <= n; i++)

那么 i 会取到 5,程序会访问 a[5],这就越界了。

在从下标 0 开始的写法中,i < n 正好访问 a[0] 到 a[n - 1],共 n 个元素。

4.输入和数组的对应关系

输入:

4
8 6 5 4

读入过程:

循环变量 i 读入的数据 保存位置
0 8 a[0]
1 6 a[1]
2 5 a[2]
3 4 a[3]

六、用 for 循环输出数组

1.正序输出

for (int i = 0; i < n; i++) {
    cout << a[i] << " ";
}
cout << endl;

这里 i 从 0 走到 n - 1,所以输出顺序是:

a[0] → a[1] → a[2] → …… → a[n-1]

2.倒序输出(逆序输出)

for (int i = n-1; i >=0; i--) {
    cout << a[i] << " ";
}
cout << endl;

这里 i 从 n-1 走到 0,所以输出顺序是:

a[n-1] → a[n-2] → a[n-3] → …… → a[0]

逆序循环的两个常见错误:

  • 错误一:假设从下标 0 开始存储,访问时却从 i = n 开始,第一次访问的就是 a[n],已经越界。正确起点是 i = n - 1;
  • 错误二:假设从下标 0 开始存储,for循环的判断条件写作 i > 0 ,这会漏掉 a[0]。正确条件是 i >= 0。

3.输出数组中指定位置的元素

题目中的“第几个”通常从 1 开始数,而数组下标从 0 开始。因此,正数第 k 个和倒数第 k 个的写法如下:

正数第 k 个 倒数第 k 个
a[k - 1] a[n - k]
第 1 个是 a[0] 倒数第 1 个是 a[n - 1]
第 n 个是 a[n - 1] 倒数第 n 个是 a[0]

例如:

// 假设数组 a[i] 从下标 0 开始存储
int k;
cin >> k;

cout << a[k - 1] << endl;  // 正数第 k 个
cout << a[n - k] << endl;  // 倒数第 k 个

如果题目大量使用“第 1 个、第 2 个、……、第 n 个”这样的位置说法,而且程序主要按位置访问,也可以考虑从下标 1 开始存放数据。这样正数第 k 个可以直接写成 a[k],倒数第 k 个可以写成 a[n - k + 1],不需要做 -1 的转换。

例如:

// 假设数组 a[i] 从下标 1 开始存储
int k;
cin >> k;

cout << a[k] << endl;  // 正数第 k 个
cout << a[n - k + 1] << endl;  // 倒数第 k 个

七、数组遍历:求和与求最值

把数组中的元素一个一个访问,这个过程叫作遍历数组。

1.遍历求和

int sum = 0;

for (int i = 0; i < n; i++) {
    sum += a[i];
}

cout << sum << endl;

执行过程:

sum = 0
→ 加上 a[0]
→ 加上 a[1]
→ 加上 a[2]
→ ……
→ 加上 a[n-1]

求和时,累加器要从 0 开始,因为 0 不会改变第一次加法的结果。

完整程序:

#include <iostream>
using namespace std;

int a[105];

int main() {
    int n;
    cin >> n;

    for (int i = 0; i < n; i++) {
        cin >> a[i];
    }

    int sum = 0;
    for (int i = 0; i < n; i++) {
        sum += a[i];
    }

    cout << sum << endl;
    return 0;
}

输入:

5
1 2 3 4 5

输出:

15

2.遍历求最大值

求最大值时,可以使用“打擂台”的方法。先选一个数当作擂主,再让后面的元素依次来挑战它:

  1. 先让第一个元素 a[0] 当擂主;
  2. 从第二个元素 a[1] 开始逐个比较;
  3. 如果当前元素比擂主大,就让它成为新的擂主;
  4. 所有元素都比较完后,最后的擂主就是最大值。

例如,数组中的数据是:

3  7  2  9  4

打擂台的过程如下:

比较对象 比较前的擂主 比较结果 比较后的擂主
初始 a[0] = 3 — 先让 3 当擂主 3
a[1] = 7 3 7 更大,换擂主 7
a[2] = 2 7 2 不够大,擂主不变 7
a[3] = 9 7 9 更大,换擂主 9
a[4] = 4 9 4 不够大,擂主不变 9

因此,最大值是 9。把这个过程写成代码:

int maxValue = a[0];

for (int i = 1; i < n; i++) {
    if (a[i] > maxValue) {
        maxValue = a[i];
    }
}

cout << maxValue << endl;

这里从 i = 1 开始,是因为 a[0] 已经先当过一次擂主,不需要再次和自己比较。

初始擂主的其他写法

上面使用 a[0] 作为初始最大值,最稳妥,因为不需要提前知道数据的正负和范围。也可以先准备一个“比所有数据都小”的数:

#include <climits>

int maxValue = INT_MIN;

如果题目数据范围明确,也可以使用常见的保险初值:

int maxValue1 = -1000000000;              // int 常用极小值
int maxValue2 = -1e9;                    // int 常用写法
long long maxValue3 = -1000000000000000000LL; // long long 常用极小值
long long maxValue4 = -1e18;             // long long 常用写法

使用这些初值时,必须确认题目中的所有数据确实不会比初值更小。求最小值时则相反,可以使用 INT_MAX,或者根据数据范围使用 1e9、1e18 等极大值。

求最值的检查重点:

  1. 数组不能为空,至少要有一个元素,才能使用 a[0] 作为初值;
  2. 用 a[0] 当擂主时,循环从 i = 1 开始;
  3. 如果使用 0 作为初值,遇到全负数时可能得到错误答案;
  4. 使用 INT_MIN、-1e9 或 -1e18 时,要和题目的数据类型、数据范围对应。

例如数组为:

-8 -3 -10 -5

过程如下:

当前元素 maxValue 比较前 比较结果 更新后
初始 a[0] = -8 — — -8
a[1] = -3 -8 -3 更大 -3
a[2] = -10 -3 -10 不更大 -3
a[3] = -5 -3 -5 不更大 -3

最后最大值是 -3。

3.遍历求最小值

求最小值只需要改变比较方向:

int minValue = a[0];

for (int i = 1; i < n; i++) {
    if (a[i] < minValue) {
        minValue = a[i];
    }
}

cout << minValue << endl;

4.统计某个数出现的次数

数组可以让数据被反复使用。例如,统计 x 出现了几次:

int x;
cin >> x;

int count = 0;

for (int i = 0; i < n; i++) {
    if (a[i] == x) {
        count++;
    }
}

cout << count << endl;

这里的 count 是计数器,必须在循环外初始化为 0。

八、综合例题:读入、求和、求最大值

1.题目

输入一个正整数 n,再输入 n 个整数。请输出这 n 个整数的总和和最大值。

2.分析

需要完成三件事:

  1. 读入 n 个数并保存到数组;
  2. 遍历数组求和;
  3. 遍历数组求最大值。

同一数组可以被遍历多次,这正是“先存下来”的价值。

3.参考代码

#include <iostream>
using namespace std;

int a[105];

int main() {
    int n;
    cin >> n;

    for (int i = 0; i < n; i++) {
        cin >> a[i];
    }

    int sum = 0;
    for (int i = 0; i < n; i++) {
        sum += a[i];
    }

    int maxValue = a[0];
    for (int i = 1; i < n; i++) {
        if (a[i] > maxValue) {
            maxValue = a[i];
        }
    }

    cout << sum << " " << maxValue << endl;
    return 0;
}

输入:

5
3 7 2 9 4

输出:

25 9

九、常见错误与注意事项

编号 错误写法或想法 后果 改正
E1 int a[5]; 却访问 a[5] 数组越界,结果不可预测 大小为 5 时,下标只能是 0 到 4
E2 for (int i = 0; i <= n; i++) 多访问一次 a[n] 正序遍历通常写 i < n
E3 逆序从 i = n 开始 第一次就访问 a[n] 从 i = n - 1 开始
E4 逆序条件写成 i > 0 漏掉第一个元素 a[0] 写成 i >= 0
E5 int a[n]; 虽然现在的 C++ 标准已经支持 数组大小依赖运行时数据,代码不便于统一修改和迁移 仍建议根据最大范围提前准备 int a[105] 或 const int N = 1005; int a[N];
E6 数组未初始化就直接读取 可能读到不确定的值 使用 int a[105] = {};,或先给每个位置输入
E7 初始值个数超过数组大小 编译错误或警告 初始值不能多于数组元素个数
E8 求和时 sum 没有初始化 结果不确定 int sum = 0;
E9 求最大值时把 maxValue 设为 0 全部为负数时结果错误 使用 a[0] 作为初始最大值
E10 求最大值从 i = 0 开始但逻辑混乱 可能重复处理或写错初值 maxValue = a[0] 后从 i = 1 开始
E11 把“第 k 个”直接写成 a[k] 位置是否错位 需要看存储的起始下标,若从下标 0 开始存储,则第 k 个元素通常是 a[k - 1],若从下标 1 开始存储,则第 k 个元素通常是 a[k]
E12 只读入数据但没有保存 后面无法逆序或二次处理 先写入 a[i],再遍历数组