B 班 第 4 课:数组

适用对象: 已完成第 3 课(循环)的同学 使用方式: 自学讲义。本课延续第 2、3 课的厚度——知识点 3 个大模块 + 8 题练习 + 加餐,做完大约需要 1.5 小时 学完本课,你应该能:

  1. 声明、初始化、读写一个数组(对照 Python 的 list)
  2. 把"读入-处理-输出"三部曲用在数组上,这是以后几乎所有题目的骨架
  3. 综合运用前三课的类型、if、循环,写一道"最高分/人数/平均分"三合一的完整程序

0. 热身:三课知识连连看(10 分钟)

先别急着学新东西,写一个小程序把前三课串起来——读入一个整数,判断奇偶,再用循环把 1 加到它:

#include <bits/stdc++.h>
using namespace std;

int main() {
    int n;
    cin >> n;
    if (n % 2 == 0) {
        cout << n << " 是偶数" << endl;
    } else {
        cout << n << " 是奇数" << endl;
    }
    int sum = 0;
    for (int i = 1; i <= n; i++) sum += i;
    cout << "1 加到 " << n << " 是 " << sum << endl;
    return 0;
}

int 类型(L01)、if/else(L02)、for 循环(L03)——这三课的知识在这一个程序里全用到了。今天要加入第四块拼图:数组


1. 数组是什么:一排连号的盒子(20 分钟)

Python 里你用 list 装一串数据:

Python 你这样写:

a = [0, 0, 0, 0, 0]      # 5 个 0
b = [1, 2, 3, 4, 5]      # 直接给初始值
b[2] = 99                # 改第 3 个(下标从 0 开始)
print(b[0], b[2])

C++ 改成这样:

#include <bits/stdc++.h>
using namespace std;

int main() {
    int a[5] = {0};           // 5 个 0(花括号里写一个 0,其余自动补 0)
    int b[5] = {1, 2, 3, 4, 5};
    b[2] = 99;
    cout << b[0] << " " << b[2] << endl;
    return 0;
}

输出:1 99

只有这几个不同:

⚠️ 本课最大的新规矩:C++ 数组的大小必须是提前写死的常数,声明完就不能再变大。 不像 Python 的 list.append() 无限加长——C++ 也有能动态增长的容器(叫 vector),以后学,现在先学"大小固定"的数组,够用了。

那不知道具体有多少个数据怎么办? 常见做法是先开一个"足够大"的数组,再用一个变量 n 记录"实际用了多少格":

#include <bits/stdc++.h>
using namespace std;

int main() {
    int a[1005];     // 先开一个够大的数组(1000 多个格子)
    int n;
    cin >> n;        // 实际要用几个,读进来存在 n 里
    for (int i = 0; i < n; i++) cin >> a[i];   // 只用前 n 个格子
    for (int i = 0; i < n; i++) cout << a[i] << " ";
    cout << endl;
    return 0;
}

📝 C++ 数组没有 Python 的 len(a) 数组本身不知道自己"装了几个有效数据",必须自己另开一个变量 n 来记账——这是和 Python list 最大的使用习惯差异。

动手检查点 1(5 分钟)

新建 check1.cpp:声明一个 int a[5] = {10, 20, 30, 40, 50};,用 for 循环把 5 个数依次输出(空格分隔)。


2. 数组配合循环:读入-处理-输出三部曲(20 分钟)

几乎所有数组题目都是这三步:

// ① 读入:把数据装进数组
for (int i = 0; i < n; i++) cin >> a[i];

// ② 处理:遍历数组做点什么
for (int i = 0; i < n; i++) { /* ... */ }

// ③ 输出:把结果打印出来
cout << 结果 << endl;

第 3 课你已经用循环写过累加、计数、打擂台三种套路,只是当时数组是"半成品"(照抄了 int a[1005] 却不知道为什么这样写)。现在把那三个套路重新过一遍,这次完全讲清楚:

① 累加(数组求和):

#include <bits/stdc++.h>
using namespace std;

int main() {
    int a[5] = {3, 1, 4, 1, 5};
    int sum = 0;
    for (int i = 0; i < 5; i++) sum += a[i];
    cout << sum << endl;   // 14
    return 0;
}

② 计数(数组里数满足条件的个数):

#include <bits/stdc++.h>
using namespace std;

int main() {
    int a[5] = {3, 1, 4, 1, 5};
    int cnt = 0;
    for (int i = 0; i < 5; i++) {
        if (a[i] % 2 == 1) cnt++;   // 数奇数
    }
    cout << cnt << endl;   // 4
    return 0;
}

③ 打擂台(数组里找最大值):

#include <bits/stdc++.h>
using namespace std;

int main() {
    int a[5] = {3, 1, 4, 1, 5};
    int maxV = a[0];               // 第一个先当擂台冠军
    for (int i = 1; i < 5; i++) {
        if (a[i] > maxV) maxV = a[i];
    }
    cout << maxV << endl;   // 5
    return 0;
}

📝 这三个套路 + "读入-处理-输出"三部曲,组合起来就能解决今天练习里几乎所有的题。


3. 读错误:2 个数组专属的坑(10 分钟)

错误 1:数组越界(第 3 课提过,现在自己写数组更要小心)

int a[5] = {1, 2, 3, 4, 5};
cout << a[5] << endl;   // ← a 只有下标 0~4,a[5] 是数组外面!

a[5] 访问的是数组外面的内存,编译器大概率不报错,但会输出一个乱七八糟的垃圾值,运气不好还会让程序直接崩溃。

📝 数组开了几个,有效下标就是 0大小-1,循环条件永远用 i < n,不要手滑写成 i <= n

错误 2:数组没初始化就用(结果是垃圾值)

int a[5];             // 没写 = {0},也没读入任何数据
cout << a[2] << endl; // 这里可能输出任何数字,每次运行都可能不一样

局部数组(写在 main 函数里的数组)不会自动清零——想要全部是 0,要么写 int a[5] = {0};,要么老老实实用 cin 把每个位置都读进去。

📝 拿到"结果一会儿对一会儿不对"的程序,先检查数组是不是有格子从来没被赋值就被读取了。


4. 本课练习(8 题,35 分钟起步,做不完当课后作业)

怎么用这些练习(比题目本身更重要,先读完这几行)

和第 3 课一样,每道题的帮助分成三层,一层一层点开:

里面是什么 什么时候点开
提示 1:往哪个方向想 只告诉你用哪个套路 + 一个反问,不给代码 自己想满 5 分钟还没头绪
提示 2:关键的那一步 最关键的一两行 + 这题的坑 看了提示 1 又想了 5 分钟还写不动
参考答案 完整代码 提示 2 也试过了,确实写不出来

⚠️ 直接翻到参考答案,等于这道题没做。 你会觉得"我看懂了",然后下次遇到同类题还是写不出来——看懂和写得出来,中间隔着很远的距离。

📝 每点开一层,在纸上记一笔:我是卡在哪儿才看的。 一学期下来,这张纸比任何讲义都准确地告诉你:你到底缺什么。

📝 看完参考答案之后,把它关掉,从空文件重写一遍。 这一步不做,前面全白费。

⚠️ 这一课的八道题里,有六道的坑都在"下标从哪儿开始、到哪儿结束"。 每写完一道,先别急着跑,用手指在纸上点一遍:第一次循环 i 是几、最后一次 i 是几、这两个下标在数组里真的存在吗。


⭐ 基础题 1:声明与输出(文件名 t1.cpp

声明 int a[6] = {2, 4, 6, 8, 10, 12};,用 for 循环输出全部元素(空格分隔)。

输出样例:
2 4 6 8 10 12
提示 1:往哪个方向想

数组配循环,就是第 2 节那个"三部曲"里的输出这一步。

先回答一个问题再动手:这六个盒子的编号是从几到几?

(不是 1 到 6。)

提示 2:关键的那一步
for (int i = 0; i < 6; i++) cout << a[i] << " ";

⚠️ i < 6,不是 i <= 6 六个盒子的编号是 0 1 2 3 4 5——写 <= 6 就会去摸第七个盒子 a[6],那是数组外面,不是你的地盘。

📝 "开了几个就写 i < 几" ——这条从今天起要变成肌肉记忆。

参考答案
#include <bits/stdc++.h>
using namespace std;

int main() {
    int a[6] = {2, 4, 6, 8, 10, 12};
    for (int i = 0; i < 6; i++) cout << a[i] << " ";
    cout << endl;
    return 0;
}

⭐ 基础题 2:读入 n 个数原样输出(文件名 t2.cpp

输入一个整数 n,接着输入 n 个整数,原样输出(空格分隔)。

输入样例:
4
7 3 9 2

输出样例:
7 3 9 2
提示 1:往哪个方向想

第 2 节的"三部曲":读入 → (这题不用处理)→ 输出,两个循环。

动手前先答一个问题:n 是运行的时候才知道的,那数组该开多大?

提示 2:关键的那一步

按题目允许的最大值开死,比如 int a[1005];。开大一点不花钱,n 只负责管循环跑几次:

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

⚠️ 别写 int a[n]; 有的编译器能过,有的过不了,考试的评测机上不一定认——这个写法以后再说。现在一律先开死大小。

参考答案
#include <bits/stdc++.h>
using namespace std;

int main() {
    int n;
    cin >> n;
    int a[1005];
    for (int i = 0; i < n; i++) cin >> a[i];
    for (int i = 0; i < n; i++) cout << a[i] << " ";
    cout << endl;
    return 0;
}

⭐⭐ 实战题 3:逆序输出(文件名 t3.cpp

输入 n 个整数,从后往前输出(不建新数组,直接倒着遍历)。

输入样例:
5
1 2 3 4 5

输出样例:
5 4 3 2 1
提示 1:往哪个方向想

题目已经说了:不建新数组

那么读入的循环一个字都不用改,只改输出的那个循环——改的也只有一件事:从哪儿走到哪儿、往哪边走。

提示 2:关键的那一步
for (int i = n - 1; i >= 0; i--) cout << a[i] << " ";

⚠️ 两头都容易差一个:

📝 拿 n = 1 试一下:只输出一个数才对。

参考答案
#include <bits/stdc++.h>
using namespace std;

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

下标从 n-1(最后一个)倒着数到 0(第一个),不用另开数组存倒序结果。


⭐⭐ 实战题 4:统计区间内的个数(文件名 t4.cpp

输入 n 个整数和两个数 lr,统计数组中有多少个数落在 [l, r] 区间内(含两端)。

输入样例:
6
2 5 8 5 12 5
3 8

输出样例:
4
提示 1:往哪个方向想

计数套路(第 3 课的三套路之一):一个计数器 cnt,符合条件就 cnt++

和第 3 课唯一的差别,是判断条件从"是不是偶数"换成了"在不在区间里"。

⚠️ 注意读入顺序:先读 n 个数,最后才读 lr——照题目给的顺序来。

提示 2:关键的那一步
if (a[i] >= l && a[i] <= r) cnt++;

⚠️ 一个区间要写两个条件,中间用 && 连起来(第 2 课学的)。不能写成 l <= a[i] <= r ——那在 C++ 里是合法的,但算出来的结果不是你想要的。

⚠️ 题目说"含两端",所以是 >=<=,不是 ><

参考答案
#include <bits/stdc++.h>
using namespace std;

int main() {
    int n;
    cin >> n;
    int a[1005];
    for (int i = 0; i < n; i++) cin >> a[i];
    int l, r;
    cin >> l >> r;
    int cnt = 0;
    for (int i = 0; i < n; i++) {
        if (a[i] >= l && a[i] <= r) cnt++;
    }
    cout << cnt << endl;
    return 0;
}

第 2 课学的 x > 0 && x < 10 区间判断,在这道题里配合数组用上了。


⭐⭐ 实战题 5:求和与平均分(文件名 t5.cpp

输入 n 个整数(学生成绩),输出总分和平均分(平均分保留 1 位小数)。

输入样例:
4
80 90 70 100

输出样例:
340 85.0
提示 1:往哪个方向想

累加套路,读入的时候顺手加,一个循环就够了。

⚠️ 真正的麻烦在平均分:第 2 课那个整数除法的坑,在这题上又来了。

先自己想一分钟:340 / 4 你写出来会是多少?如果换成 sum = 341n = 4 呢?

提示 2:关键的那一步
double avg = (double)sum / n;

⚠️ 除号左边必须先变成小数,否则 int / int 先按整数算完(小数部分直接砍掉),再赋值给 double 已经晚了。

📝 double avg = sum / n; 是救不回来的。 判断症状很简单:平均分永远是 .0 结尾。

(double)sum 叫强制类型转换,先照抄;输出用 printf("%.1f", avg) 保留一位小数,也先照抄,格式化输出以后正式学。

参考答案
#include <bits/stdc++.h>
using namespace std;

int main() {
    int n;
    cin >> n;
    int a[1005];
    int sum = 0;
    for (int i = 0; i < n; i++) {
        cin >> a[i];
        sum += a[i];
    }
    double avg = (double)sum / n;   // 除法前转成 double,不然又踩第 2 课的整数除法坑
    printf("%d %.1f\n", sum, avg);
    return 0;
}

(double)sum 是"类型转换"——把 int 强行当成 double 来算,这样除出来才有小数。printf("%.1f", ...) 是保留 1 位小数的写法,先直接照抄,格式化输出以后会正式学。


⭐⭐⭐ 冲刺题 6:山峰数组(文件名 t6.cpp

输入 n 个整数,判断这个数组是不是"山峰形状"——先严格递增一段,到达最高点后再严格递减,最高点前后都至少要有一个数(不能一直增或一直减)。是山峰输出 是山峰,不是输出 不是山峰

输入样例1:      输入样例2:
5               4
1 3 5 2 1       1 2 3 4

输出样例1:      输出样例2:
是山峰          不是山峰
提示 1:往哪个方向想

分两步:先找到最高点,再检查它两边。

⚠️ 但这里有个关键的差别,想清楚了这题就通了一半:这道题要的不是最高的那个"数值",而是最高点在"第几格"。

为什么?因为你接下来要检查的是"它左边那一段"和"它右边那一段"——知道数值没用,得知道位置。

提示 2:关键的那一步

打擂台的时候,擂主记的是下标不是数值:

int peak = 0;
for (int i = 1; i < n; i++) {
    if (a[i] > a[peak]) peak = i;    // 比的是 a[i] 和 a[peak],记的是 i
}

然后三件事都要检查:

  1. ⚠️ peak 不能是第一个也不能是最后一个peak == 0 || peak == n-1 → 不是山峰)。题目要求"最高点前后都至少有一个数"
  2. 1peak:每个都要比前一个大(严格,相等也不行 → <= 就判假)
  3. peak+1n-1:每个都要比前一个小

📝 用一个 bool ok = true; 记结果,任何一条不满足就 ok = false,别一发现不对就急着输出。

参考答案
#include <bits/stdc++.h>
using namespace std;

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

    int peak = 0;               // 打擂台找最高点的下标
    for (int i = 1; i < n; i++) {
        if (a[i] > a[peak]) peak = i;
    }

    bool ok = true;
    if (peak == 0 || peak == n - 1) ok = false;   // 最高点不能是第一个或最后一个
    for (int i = 1; i <= peak; i++) {
        if (a[i] <= a[i - 1]) ok = false;          // 前半段要严格递增
    }
    for (int i = peak + 1; i < n; i++) {
        if (a[i] >= a[i - 1]) ok = false;          // 后半段要严格递减
    }

    cout << (ok ? "是山峰" : "不是山峰") << endl;
    return 0;
}

先打擂台找到最高点的下标(不只是数值,因为要检查它前后的位置),再分别检查前半段递增、后半段递减。


⭐⭐⭐ 冲刺题 7:综合大题——成绩统计(文件名 t7.cpp

输入 n 个学生的成绩,依次输出:最高分、90 分及以上的人数、平均分(保留 1 位小数)。这题会用满本课和前三课的所有知识点。

输入样例:
5
78 92 85 90 60

输出样例:
92 2 81.0
提示 1:往哪个方向想

三个套路一次全用上:累加(总分)、打擂台(最高分)、计数(90 分以上几个人)。

⚠️ 别急着写代码。先在纸上列出你需要哪几个变量、每个的初值是几。 列不出来就说明还没想清楚,这时候敲键盘只会越写越乱。

(提示:一共四个变量,其中三个是"从头攒到尾"的。)

提示 2:关键的那一步

三件事可以分成三个循环写(好懂),也可以合成一个循环(读入的时候顺手全干完)。两种都对,先写好懂的那种。

⚠️ 打擂台的初值必须是 a[0],不能是 0。 这就是第 3 次课改错 3-2 那道题——maxV = 0 在正数数据上永远看不出问题,一遇到全负数就露馅。

⚠️ 所以如果你要合成一个循环,得先把数据读进来才有 a[0] 可用;或者干脆分开写:先读入,再 int maxV = a[0];,再从 i = 1 开始比。

平均分照旧 (double)sum / n

参考答案
#include <bits/stdc++.h>
using namespace std;

int main() {
    int n;
    cin >> n;
    int a[1005];
    int sum = 0;
    for (int i = 0; i < n; i++) {
        cin >> a[i];
        sum += a[i];              // 累加
    }

    int maxV = a[0];
    for (int i = 1; i < n; i++) {
        if (a[i] > maxV) maxV = a[i];   // 打擂台
    }

    int cnt = 0;
    for (int i = 0; i < n; i++) {
        if (a[i] >= 90) cnt++;    // 计数
    }

    double avg = (double)sum / n;
    printf("%d %d %.1f\n", maxV, cnt, avg);
    return 0;
}

int / double(L01)、比较运算(L02)、for 循环(L03)、数组(本课)——一次全用上。这就是"综合"的意思:不是学了新东西就忘了旧的,而是新旧拼在一起解决更大的问题。


⭐⭐⭐ 冲刺题 8:找众数(文件名 t8.cpp,选做)

输入 n 个整数,值的范围是 0~100,输出出现次数最多的那个数(保证答案唯一)。

输入样例:
6
3 1 3 5 3 2

输出样例:
3
提示 1:往哪个方向想

如果你的想法是"拿每个数去和其他所有数比一遍"——能做出来,但太笨了,而且写起来要两层循环套着,容易乱。

换个方向想:题目特意告诉你值的范围是 0~100,这句话是白给的吗?

⚠️ 想象你开一个数组,让"下标就是那个数字本身",格子里放"它出现了几次"。 读到一个 3,就去 3 号格子里加一笔。

那么读完之后,答案在哪儿?

提示 2:关键的那一步
int cnt[101] = {0};        // 下标 0~100,正好盖住题目给的取值范围
...
cnt[x]++;                  // 读到 x,就在 x 号格子上加一笔

统计就这一句,不用任何比较。最后在 cnt 上打擂台,找的是最大值所在的下标(和第 6 题一样,要位置不要数值):

int ans = 0;
for (int v = 0; v <= 100; v++) {
    if (cnt[v] > cnt[ans]) ans = v;
}

⚠️ = {0} 不能省。 写在 main 里的数组不会自动清零(第 3 节「错误 2」讲过),不清零就 cnt[x]++,等于在一堆随机数上加一笔,结果每次运行都可能不一样。

⚠️ 数组要开到 101 不是 100——下标 0100 一共 101 个。

参考答案
#include <bits/stdc++.h>
using namespace std;

int main() {
    int n;
    cin >> n;
    int cnt[101] = {0};
    for (int i = 0; i < n; i++) {
        int x;
        cin >> x;
        cnt[x]++;
    }

    int ans = 0;
    for (int v = 0; v <= 100; v++) {
        if (cnt[v] > cnt[ans]) ans = v;
    }
    cout << ans << endl;
    return 0;
}

这题用了"数组当计数器"的技巧——下标本身就是数值,cnt[x] 直接就是"x 出现了几次",比一个个比较快得多。这个技巧以后会经常用到。


5. 学有余力:字符串其实也是"字符的数组"(加餐,可跳过)

第 1 课学的 string 其实内部就是"字符排成一排",也能用 [] 按下标取出单个字符:

#include <bits/stdc++.h>
using namespace std;

int main() {
    string s = "hello";
    cout << s[0] << " " << s[4] << endl;   // h o
    return 0;
}

s[0] 是第一个字符 hs[4] 是第五个字符 o——和数组的下标规则完全一样。字符串更完整的用法(拼接、查找、二维数组、vector 动态数组)以后学,今天只需要知道"字符串和数组用的是同一套下标语法"。


本课要点速查

Python C++ 备注
a = [0, 0, 0, 0, 0] int a[5] = {0}; 大小是提前写死的常数
a = [1, 2, 3, 4, 5] int a[5] = {1, 2, 3, 4, 5}; 花括号初始化
a[i] a[i] 下标都从 0 开始,读写都行
len(a) 没有! 要自己另开一个变量 n 记录个数
a.append(x) 没有! 数组大小不能动态增长,vector 以后学
s[i](字符串取字符) s[i] string 也能按下标取字符

数组上的三种循环套路:

sum += a[i];                     // ① 累加
if (cond) cnt++;                  // ② 计数
if (a[i] > maxV) maxV = a[i];     // ③ 打擂台

两个数组专属坑:

症状 原因
结果是垃圾值或程序崩溃 数组下标越界(i < n 写成了 i <= n
结果时对时错 数组某些位置从没被赋值就被读取了

本课新符号: int a[大小]、数组下标 [](double) 类型转换、printf("%.1f", ...)


结束前的自我检查

  1. 不看讲义,声明一个装 10 个整数、初始值全是 0 的数组
  2. 向别人解释:为什么 C++ 数组不能像 Python list 一样 append?现在怎么绕过这个限制?
  3. 练习 8 题至少完成到题 7(综合大题),每题样例都亲手验证过
  4. 合上讲义,把"数组求和"和"数组打擂台找最大值"两个模板一次性写对