Skip to content

第 17 章 从存到算——数组的统计与变换


🏗️ 前情回顾:上一章你学会了用数组把全班 50 个成绩整整齐齐装进一排柜子里——int scores[50],再也不用写 50 个变量名了。但柜子装满了,然后呢?老师走过来:"平均分多少?最高分是谁?90 分以上有几个?把成绩从小到大排好。"——光有柜子不够,你得会"盘"这些数据。

💡 本章要解决的问题数组存下来了——怎么统计、怎么比较、怎么排序、怎么插入删除? 这一章,我们解锁数组的"组合技"——遍历不是目的,遍历只是手段;同一组数据换一种遍历方式,就能回答完全不同的问题。


🎯 本章目标

学完这一章,你能:

  • 用数组求一组数的和、平均值、最大值和最小值
  • 用"桶计数"的思想统计每个数出现了几次
  • 在数组中插入、删除、移动元素
  • 反转数组、给数组去重
  • 认识到同一个数组可以有多种玩法——关键在于"怎么遍历"

📖 故事引入

班长唱票

期末到了,班长小李手里有全班 40 个人的数学成绩单。老师问他三个问题:

  1. "平均分是多少?"
  2. "最高分和最低分是谁?"
  3. "有多少人考了 90 分以上?"

小李没手忙脚乱。他把所有成绩抄在一张纸上,开始唱票——这是他从班级投票里学来的方法。

第一遍唱票——求和、平均:他拿手指从第一个成绩划到最后一个,嘴里念着"85、加 92、加 78……",最后除以 40,平均分出炉。

第二遍唱票——找最值:他又从头扫一遍,这次不看总分,而是盯"最大的那个"。"85?先记着。92?更大,换掉。78?不如 92,跳过……"扫完,最高分出炉。

第三遍唱票——数人头:再扫一遍,每看到一个 ≥90 的就在纸上画一笔"正"字。三遍扫完,人数也出来了。

三次唱票,都是"从头到尾过一遍",但每次脑子里的问题不同,结果就不同。这,就是数组统计的核心秘密——遍历是同一把刀,切不同的菜靠的是不同的刀法


🧱 知识讲解

17.1 求和、均值、最值

这三个操作是数组统计的"老三样",它们有一个共同特征:初始化一个变量,然后遍历数组,逐个更新这个变量

求和

cpp
int a[5] = {89, 92, 78, 95, 88};
int sum = 0;                         // 累加器,从 0 开始
for (int i = 0; i < 5; i++) {
    sum = sum + a[i];               // 或者 sum += a[i];
}
cout << "总分:" << sum << endl;      // 442
cout << "平均:" << sum / 5.0 << endl; // 88.4

求最大值

cpp
int maxVal = a[0];                   // 先假设第一个是最大的
for (int i = 1; i < 5; i++) {        // 从第二个开始比
    if (a[i] > maxVal) {
        maxVal = a[i];               // 谁大就替换
    }
}
cout << "最高分:" << maxVal << endl;  // 95

求最小值:把 > 改成 < 就行。

常见踩坑——最大值初始值设错:

cpp
int maxVal = 0;                    // ❌ 如果数组里全是负数,0 就"假冠军"了
int a[5] = {-10, -5, -20, -8, -15};
for (int i = 0; i < 5; i++) {
    if (a[i] > maxVal) maxVal = a[i];
}
cout << maxVal << endl;            // 输出 0,但正确答案是 -5!

正确:初始值设为 a[0]——拿数组第一个元素当"暂时的冠军":

cpp
int maxVal = a[0];                 // ✅ 用数组自己的元素做起点

17.2 计数统计——桶思想

"有多少人考了 90 分以上?"这个问题本质是计数:设定一个计数器,遍历数组,每遇到一个符合条件的就加一。

cpp
int a[8] = {85, 92, 78, 95, 88, 90, 73, 91};
int cnt = 0;                         // 计数器
for (int i = 0; i < 8; i++) {
    if (a[i] >= 90) {
        cnt++;
    }
}
cout << "90分以上人数:" << cnt << endl; // 4

更进一步的"桶计数":如果想知道"每个分数各有几个人"呢?这时可以开一个计数数组——数组的下标代表"分数",数组的值代表"人数"。

cpp
int scores[10] = {85, 92, 78, 95, 88, 90, 73, 91, 88, 79};
int bucket[101] = {0};               // 成绩 0~100,共 101 个桶

for (int i = 0; i < 10; i++) {
    int s = scores[i];               // 取出第 i 个人的成绩
    bucket[s]++;                     // 在对应的桶里 +1
}

cout << "考 88 分的有 " << bucket[88] << " 人" << endl;  // 2
cout << "考 95 分的有 " << bucket[95] << " 人" << endl;  // 1

这就是桶排序的核心思想——用数组下标天然有序的特性来做统计。后面你会学到完整的桶排序,现在先感受感受。

常见踩坑——计数数组没初始化:

cpp
int bucket[101];                    // ❌ 没初始化!
bucket[s]++;                        // 垃圾值 + 1 = 还是垃圾

正确:

cpp
int bucket[101] = {0};             // ✅ 全部初始化为 0

17.3 数组元素的插入

假设你有一个数组 {10, 20, 30, 40, 50},想在 20 和 30 之间插入一个 25。怎么做?

数组里没有"缝隙",你得先把后面的元素整体往后挪一格,腾出空位,再把新元素放进去。

cpp
int a[10] = {10, 20, 30, 40, 50};    // 容量 10,目前用了 5 个
int n = 5;                            // 当前元素个数

// 在位置 2(下标为 2,即 30 的位置)插入 25
int pos = 2;
int val = 25;

// 第一步:从后往前,把 pos 及之后的元素整体后移
for (int i = n; i > pos; i--) {
    a[i] = a[i - 1];
}
// 第二步:放入新元素
a[pos] = val;
n++;                                  // 元素个数 +1

// 输出:10 20 25 30 40 50
for (int i = 0; i < n; i++) {
    cout << a[i] << " ";
}

🔑 关键细节:移动要从后往前in 递减到 pos+1),如果从前往后移,后面的值还没腾地方就被覆盖了。

常见踩坑——从前往后移:

cpp
// 想在下标 2 处插入 25
for (int i = pos; i < n; i++) {
    a[i + 1] = a[i];                // ❌ 30 把 40 盖了,40 把 50 盖了
}                                    //    最后整段都是 30

正确——从后往前移:

cpp
for (int i = n; i > pos; i--) {
    a[i] = a[i - 1];                // ✅ 50 先挪到空位 5,40 再挪到空位 4……
}

🧠 记忆技巧:排队插队,后面的人先退,才不会踩到前面人的脚。

17.4 数组元素的删除

删除则反过来——把后面的元素整体往前挪一格,盖住要删的那个。

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

// 删除位置 2(即 30)
int pos = 2;

// 从 pos+1 开始,每个元素往前移一位
for (int i = pos; i < n - 1; i++) {
    a[i] = a[i + 1];
}
n--;                                  // 元素个数 -1

// 输出:10 20 40 50
for (int i = 0; i < n; i++) {
    cout << a[i] << " ";
}

17.5 数组反转

{1, 2, 3, 4, 5} 变成 {5, 4, 3, 2, 1}

聪明的方法:首尾交换。让第 0 个和第 n-1 个交换,第 1 个和第 n-2 个交换……一直到中间碰头。

cpp
int a[5] = {1, 2, 3, 4, 5};
int n = 5;

for (int i = 0; i < n / 2; i++) {    // 只需要 n/2 轮
    int temp = a[i];                  // 经典的三步交换
    a[i] = a[n - 1 - i];
    a[n - 1 - i] = temp;
}

// 输出:5 4 3 2 1
for (int i = 0; i < n; i++) {
    cout << a[i] << " ";
}

💡 n / 2 是关键——如果遍历到 n,会把已经换好的又换回去,等于白干。

常见踩坑——循环到 n:

cpp
for (int i = 0; i < n; i++) {       // ❌ i 走到 4 时,把换好的又换了回去
    swap(a[i], a[n - 1 - i]);
}
// 结果:1 2 3 4 5(白干!)

正确:

cpp
for (int i = 0; i < n / 2; i++) {   // ✅ 走到中间就停
    swap(a[i], a[n - 1 - i]);
}

17.6 数组去重

"去重"就是去掉重复的元素,每个值只保留第一次出现的。

思路:用一个新数组存"只出现过一次"的元素。遍历原数组,对每个元素检查它是否已经在新数组里出现过——没出现过就加进去。

cpp
int a[8] = {3, 1, 4, 1, 5, 9, 3, 4};
int n = 8;
int b[8];                             // 存去重后的结果
int m = 0;                            // b 的元素个数

for (int i = 0; i < n; i++) {
    bool found = false;
    for (int j = 0; j < m; j++) {     // 在 b 里找 a[i]
        if (b[j] == a[i]) {
            found = true;
            break;
        }
    }
    if (!found) {
        b[m] = a[i];                  // 没找到就加进去
        m++;
    }
}

// 输出去重结果:3 1 4 5 9
for (int i = 0; i < m; i++) {
    cout << b[i] << " ";
}

🧠 这个去重方法叫"双重循环法",简单直观但效率不高。学到后面你还会遇到更高效的去重方式(比如先排序再去重)。现在先从最朴素的方法开始,建立直觉。


✋ 动手试试

试试 1:定义一个 int 数组 {65, 34, 87, 23, 98, 56, 72, 41},分别求最大值、最小值、平均值。平均保留 2 位小数。

试试 2:模拟一个班级的投票——数组里存 20 个数字(1~5),每个数字代表投给一个候选人。用桶计数统计每个人得了多少票,找出票数最高的候选人。

试试 3:有一个已排序(从小到大)的数组,用户输入一个新数字,把它插入到合适的位置,保持数组仍然有序。提示:先找到插入位置,再后移所有元素。


🦶 你踩过这些坑吗?

  • [ ] 坑 1:遍历时循环条件写 <=——越界访问 arr[n]
  • [ ] 坑 2:求最大值把初始值设成 0——遇到全负数数组就翻车
  • [ ] 坑 3:插入元素时从前往后移——后面的值被覆盖,全变成同一个数
  • [ ] 坑 4:反转数组时循环到 n——把换好的又换回去,白干
  • [ ] 坑 5:计数数组没初始化——读到的都是垃圾值
  • [ ] 坑 6:插入/删除后忘记更新元素个数 n——导致逻辑混乱

📝 练习

基础题

1. 选择题

(1)求数组最大值时,初始值最好设为:
A. 0   B. a[0]   C. 100   D. -1

(2)数组反转时,循环应该进行多少次?
A. n 次   B. n/2 次   C. n-1 次   D. 随便

(3)删除数组某个元素后,元素个数 n 应该:
A. 不变   B. n++   C. n--   D. n = 0

2. 填空题

(1)累加求和的计数器通常初始化为 ____。

(2)在位置 pos 插入元素时,需要把从 ____ 到 ____ 的元素整体后移。

(3)去重时需要判断一个元素是否 ____ 出现过。

(4)桶计数的核心思路是用数组的 ____ 代表要统计的"类别",用数组的 ____ 代表"数量"。

提高题

3. 编程题 — 成绩分析

输入 n 个学生的成绩(0~100),输出:平均分、最高分、最低分、及格人数(≥60)、优秀人数(≥90)。

4. 编程题 — 数组移位

有一个数组,把所有元素循环右移 k 位(k 由用户输入)。比如 {1,2,3,4,5} 右移 2 位变成 {4,5,1,2,3}。(提示:可以借助"反转三次"的技巧:整体反转 → 反转前 k 个 → 反转后 n-k 个。)

5. 编程题 — 有序插入

用户先输入 n 个已经从小到大排好序的整数存入数组,再输入一个新整数 x。把 x 插入到数组中合适的位置,保持数组仍然有序。输出插入后的数组。(提示:先找到 x 应该插入的位置,然后把后面的元素整体后移。)

挑战题

6. 编程题 — 约瑟夫问题简化版

n 个人围成一圈,从第 1 个人开始报数,报到 m 的人出局。下一个人继续从 1 报数。输出出局的顺序。

示例:n=5, m=3 → 出局顺序:3, 1, 5, 2, 4

(提示:用一个 bool 数组标记"是否已出局",用循环模拟绕圈。)

7. 编程题 — 找众数

输入 n 个整数,找出其中出现次数最多的那个数(众数)。如果有多个数出现次数并列最多,输出最小的那个。例如输入 1 3 2 3 1 3,输出 3(出现了 3 次)。


🧠 本章小结

统计三件套:
  求和 sum  →  遍历累加
  最值 max  →  遍历比较(初值设 a[0]!)
  计数 cnt  →  遍历判条件

桶计数:bucket[ 值 ]++  →  数组下标天然有序

插入:从后往前移  →  腾空位  →  放入
删除:从前往后移  →  覆盖  →  n--

反转:首尾交换  →  n/2 轮

去重:遍历原数组  →  检查是否出现过  →  加入新数组

📝 配套练习

共8题。桶计数是本章核心——J0071/J0074/J0077三题从简单计数到逐位统计到频次分布。。★核心(课堂必做) ◆拓展(课后练习)

级别题号链接覆盖知识点
★ 核心J0071https://hydro.ac/d/srqc/p/J0071桶计数、cnt[]作计数器
◆ 拓展J0074https://hydro.ac/d/srqc/p/J0074桶计数、逐位拆解、cnt[10]
◆ 拓展J0078https://hydro.ac/d/srqc/p/J0078状态数组、!翻转、步进遍历
◆ 拓展J0058https://hydro.ac/d/srqc/p/J0058最值、一趟遍历双聚合
◆ 拓展J0076https://hydro.ac/d/srqc/p/J0076滑动窗口、前缀和雏形
◆ 拓展J0077https://hydro.ac/d/srqc/p/J0077桶计数、频次统计
◆ 拓展luogu-P1554https://hydro.ac/p/luogu-P1554桶计数、逐位统计、多区间
◆ 拓展luogu-P5728https://hydro.ac/d/srqc/p/J0107嵌套for、两两比较、分数差

💡 练习建议:先完成 1 道★核心题,确保掌握本章基本方法;再完成 7 道◆拓展题,覆盖不同变式和细节。


配套练习

共8题。桶计数是本章核心——J0071/J0074/J0077三题从简单计数到逐位统计到频次分布。★核心(课堂必做) · ◆拓展(课后练习)

级别题号链接覆盖知识点
★ 核心J0071https://hydro.ac/d/srqc/p/J0071桶计数、cnt[]作计数器
◆ 拓展J0074https://hydro.ac/d/srqc/p/J0074桶计数、逐位拆解、cnt[10]
◆ 拓展J0078https://hydro.ac/d/srqc/p/J0078状态数组、!翻转、步进遍历
◆ 拓展J0058https://hydro.ac/d/srqc/p/J0058最值、一趟遍历双聚合
◆ 拓展J0076https://hydro.ac/d/srqc/p/J0076滑动窗口、前缀和雏形
◆ 拓展J0077https://hydro.ac/d/srqc/p/J0077桶计数、频次统计
◆ 拓展luogu-P1554https://hydro.ac/p/luogu-P1554桶计数、逐位统计、多区间
◆ 拓展luogu-P5728https://hydro.ac/d/srqc/p/J0107嵌套for、两两比较、分数差

练习建议:先在课堂完成 1 道★核心题,掌握本章基本方法;课后完成 7 道◆拓展题,覆盖不同变式和细节。

自查清单

  • [ ] 我能用循环遍历数组,求出一组数的和、平均值、最大/最小值
  • [ ] 我会正确设置最大/最小值的初始值(a[0],不是 0)
  • [ ] 我能用桶数组做计数统计,知道必须初始化为 {0}
  • [ ] 我能在数组中正确插入和删除元素(知道为什么移动方向很重要)
  • [ ] 我能在 O(n) 时间内反转一个数组(循环次数是 n/2)
  • [ ] 我理解"去重"的思路,能用双重循环实现
  • [ ] 我知道插入/删除后要更新元素个数 n

🚀 下章预告

一维数组把全班 50 个人的数学成绩存好了,你也能统计、排序、找最值了。但班主任又来了——"这是全班三门课的成绩单:数学、语文、英语。"你低头一看,这张表有行有列:每行是一个学生,每列是一门科目。一维数组是一条线,装不下带行和列的表格。

下一章,我们给数组加上第二个维度——二维数组,让你像操作 Excel 表格一样,用 a[行][列] 搞定一切矩阵问题!