第 2.3 章:函数与数组
📝 前置条件: 第 2.1 章和第 2.2 章(变量、循环、if/else)
随着程序越来越大,你需要组织代码的方式(函数)和存储数据集合的方式(数组和向量)。本章介绍这两者——竞赛编程中最重要的两个工具。
2.3.1 函数——是什么,为什么需要
🍕 食谱类比
📄 查看代码:🍕 食谱类比
函数就像一份披萨食谱:
- 输入(参数): 食材——面粉、奶酪、番茄
- 过程(函数体):烹饪步骤
- 输出(返回值):做好的披萨
就像你可以用一份食谱做很多张披萨,
你可以用不同的输入多次调用一个函数。
pizza("薄底", "培根") → 一张披萨
pizza("厚底", "蘑菇") → 另一张披萨
没有函数,如果你需要在程序的五个地方计算「这个数是不是质数」,你就得把同样的 10 行代码复制粘贴五次。然后如果发现了 bug,就要在五个地方全部修复!
什么时候写函数
以下情况使用函数:
- 程序中某段逻辑重复了 3 次以上
- 某段代码做的是一件清晰命名的事(如「检查是否质数」「计算距离」)
- 你的
main变得太长、不好读
函数的基本语法
返回类型 函数名(参数1类型 参数1, 参数2类型 参数2, ...) {
// 函数体
return 值; // 必须与返回类型匹配;void 函数可省略
}
你的第一批函数
📄 查看代码:你的第一批函数
#include <bits/stdc++.h>
using namespace std;
// ---- 函数定义(必须在使用前定义,或使用前置声明)----
// 接收一个整数,返回它的平方
int square(int x) {
return x * x;
}
// 接收两个整数,返回较大的那个
int maxOf(int a, int b) {
if (a > b) return a;
else return b;
}
// void 函数:执行某件事但不返回值
void printSeparator() {
cout << "====================\n";
}
// ---- 主函数 ----
int main() {
cout << square(5) << "\n"; // 以 x=5 调用 square,打印 25
cout << square(12) << "\n"; // 以 x=12 调用 square,打印 144
cout << maxOf(7, 3) << "\n"; // 打印 7
cout << maxOf(-5, -2) << "\n"; // 打印 -2
printSeparator(); // 打印分隔线
cout << "完成!\n";
printSeparator();
return 0;
}
🤔 为什么函数要在 main 之前?
C++ 从上到下读取你的文件。当它看到 square(5) 这样的调用时,需要已经知道 square 是什么。如果 square 在 main 之后定义,编译器会说「我从没听说过 square!」
方案一: 把所有函数定义在 main 之上(最简单)。
方案二: 使用函数原型——前置声明,告诉编译器「这个函数存在,我之后再定义」:
📄 C++ 完整代码
#include <bits/stdc++.h>
using namespace std;
int square(int x); // 原型——只有签名,没有函数体
int maxOf(int a, int b); // 原型
int main() {
cout << square(5) << "\n"; // OK!编译器知道 square 存在
return 0;
}
// 完整定义可以在 main 之后
int square(int x) {
return x * x;
}
int maxOf(int a, int b) {
return (a > b) ? a : b;
}
2.3.2 void 函数 vs 返回值函数
void 函数:执行操作,不返回值
📄 查看代码:void 函数:执行操作,不返回值
// void 函数执行某个动作
void printLine(int n) {
for (int i = 0; i < n; i++) {
cout << "-";
}
cout << "\n";
}
// 调用 void 函数——直接调用,不要尝试接收返回值
printLine(10); // 打印:----------
printLine(20); // 打印:--------------------
返回值函数:计算并返回一个值
// 返回 x 的绝对值
int absoluteValue(int x) {
if (x < 0) return -x;
return x;
}
// 调用返回值函数——把结果存入变量或直接使用
int result = absoluteValue(-7);
cout << result << "\n"; // 7
cout << absoluteValue(-3) << "\n"; // 3(直接使用)
多个 return 语句
函数可以有多个 return 语句——执行到第一个就停止:
string classify(int n) {
if (n < 0) return "负数"; // n < 0 时从这里退出
if (n == 0) return "零"; // n == 0 时从这里退出
return "正数"; // 其他情况从这里退出
}
cout << classify(-5) << "\n"; // 负数
cout << classify(0) << "\n"; // 零
cout << classify(3) << "\n"; // 正数
2.3.3 按值传递 vs 按引用传递
向函数传递变量时有两种方式。理解这一点至关重要。
按值传递(默认):函数得到一个副本
📄 查看代码:按值传递(默认):函数得到一个副本
void addOne_byValue(int x) {
x++; // 修改的是局部副本——原变量不变
cout << "函数内部:" << x << "\n"; // 6
}
int main() {
int n = 5;
addOne_byValue(n);
cout << "函数之后:" << n << "\n"; // 还是 5!原变量未改变
return 0;
}
可以把它想成复印件:函数对复印件进行操作,对复印件的修改不影响原件。
按引用传递(&):函数操作原变量
📄 查看代码:按引用传递(&):函数操作原变量
void addOne_byRef(int& x) { // & 表示「原变量的引用」
x++; // 直接修改原变量
cout << "函数内部:" << x << "\n"; // 6
}
int main() {
int n = 5;
addOne_byRef(n);
cout << "函数之后:" << n << "\n"; // 现在是 6!原变量被改变了
return 0;
}
什么时候用哪种
| 用按值传递,当…… | 用按引用传递,当…… |
|---|---|
| 函数不应修改原变量 | 函数需要修改原变量 |
| 小类型(int、double、char) | 需要返回多个值 |
| 你想要安全性(无副作用) | 大型对象(避免昂贵的复制) |
通过引用返回多个值
C++ 函数只能 return 一个值。但可以通过引用参数「返回」多个值:
📄 C++ 函数只能 `return` 一个值。但可以通过引用参数「返回」多个值:
// 同时计算商和余数
void divmod(int a, int b, int& quotient, int& remainder) {
quotient = a / b;
remainder = a % b;
}
int main() {
int q, r;
divmod(17, 5, q, r); // q 和 r 被函数修改
cout << "17 / 5 = " << q << " 余 " << r << "\n";
// 打印:17 / 5 = 3 余 2
return 0;
}
2.3.4 递归
递归函数是调用自身的函数。它非常适合可以拆解成同类问题的更小版本的问题。
经典示例:阶乘
5! = 5 × 4 × 3 × 2 × 1 = 120
= 5 × (4!) ← 同样的问题,更小的输入!
💡 递归思维三步法:
- 找「自相似性」: 原问题能否拆解为同类型的更小问题?5! = 5 × 4!,4! 和 5! 是同一类型 ✓
- 确定边界条件: 最小的情况是什么?0! = 1,不能再拆解
- 写递推步骤: n! = n × (n-1)!,用更小的输入调用自身
这个思维过程在**图论算法(第 5.1 章)和动态规划(第 6.1-6.3 章)**中会反复用到。
int factorial(int n) {
if (n == 0) return 1; // 边界条件:停止递归
return n * factorial(n - 1); // 递推步骤:缩小到更小的问题
}
追踪 factorial(4):
factorial(4)
= 4 * factorial(3)
= 4 * (3 * factorial(2))
= 4 * (3 * (2 * factorial(1)))
= 4 * (3 * (2 * (1 * factorial(0))))
= 4 * (3 * (2 * (1 * 1))) ← 边界条件!
= 4 * (3 * (2 * 1))
= 4 * (3 * 2)
= 4 * 6
= 24 ✓
每个递归函数都需要:
- 边界条件 —— 停止递归(防止无限递归)
- 递推步骤 —— 用更小的输入调用自身
🐛 常见 Bug: 忘记边界条件 → 无限递归 → 「栈溢出」崩溃!
2.3.5 数组——固定大小的集合
🏠 邮箱类比
数组就像街道上一排邮箱:
- 所有邮箱大小相同(相同类型)
- 每个都有门号(下标,从 0 开始)
- 可以通过门号直接找到任意邮箱
图示:数组内存布局
数组在内存中是连续存储的,每个元素紧挨着前一个,因此支持 O(1) 随机访问。
数组基础
📄 查看代码:数组基础
#include <bits/stdc++.h>
using namespace std;
int main() {
// 声明一个有 5 个整数的数组(元素未初始化——是垃圾值!)
int arr[5];
// 逐一赋值
arr[0] = 10;
arr[1] = 20;
arr[2] = 30;
arr[3] = 40;
arr[4] = 50;
// 同时声明和初始化
int nums[5] = {1, 2, 3, 4, 5};
// 全部初始化为零
int zeros[100] = {}; // 所有 100 个元素 = 0
int zeros2[100];
fill(zeros2, zeros2 + 100, 0); // 另一种方式
// 访问和打印
cout << arr[2] << "\n"; // 30
// 遍历数组
for (int i = 0; i < 5; i++) {
cout << nums[i] << " "; // 1 2 3 4 5
}
cout << "\n";
return 0;
}
🐛 差一错误——头号数组 Bug
数组是从 0 开始索引的:如果你声明 int arr[5],合法下标是 0, 1, 2, 3, 4,不存在 arr[5]!
📄 C++ 完整代码
int arr[5] = {10, 20, 30, 40, 50};
// 错误:循环从 i=0 到 i=5 包含——下标 5 不存在!
for (int i = 0; i <= 5; i++) { // BUG:<= 5 应改为 < 5
cout << arr[i]; // i=5 时崩溃或打印垃圾值
}
// 正确:循环从 i=0 到 i=4(i < 5 确保 i 永远到不了 5)
for (int i = 0; i < 5; i++) { // i 的值:0, 1, 2, 3, 4 ✓
cout << arr[i]; // 始终有效
}
这就是「差一错误」——越过末尾一个元素。这是竞赛编程中最常见的数组 bug。
🤔 为什么从 0 开始? C++ 从 C 继承了这个设计,而 C 的设计贴近硬件。下标实际上是从数组起点的偏移量。第一个元素在偏移量 0 处(从起点无偏移)。
大数组用全局变量
main 里的局部变量存在「栈」上,空间有限(约 1-8 MB)。竞赛编程中 N 最大可达 10^6,需要用全局数组(存在不同的内存区域,空间大得多):
📄 C++ 完整代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1000001; // 最大长度 + 1(常见惯例)
int arr[MAXN]; // 全局声明——大数组安全
// 全局数组会自动初始化为 0!
int main() {
int n;
cin >> n;
for (int i = 0; i < n; i++) {
cin >> arr[i];
}
return 0;
}
⚡ 专业技巧: 全局数组会自动初始化为 0。局部数组不会——它们在赋值前包含垃圾值!
2.3.6 常见数组算法
求和、最大值、最小值
📄 查看代码:求和、最大值、最小值
int n;
cin >> n;
vector<int> arr(n); // 马上学 vector;用法与数组相同
for (int i = 0; i < n; i++) cin >> arr[i];
// 求和
long long sum = 0;
for (int i = 0; i < n; i++) sum += arr[i];
cout << "总和:" << sum << "\n";
// 最大值(初始化为第一个元素!)
int maxVal = arr[0];
for (int i = 1; i < n; i++) {
if (arr[i] > maxVal) maxVal = arr[i];
}
cout << "最大值:" << maxVal << "\n";
// 最小值(同理)
int minVal = arr[0];
for (int i = 1; i < n; i++) {
minVal = min(minVal, arr[i]); // min() 是内置函数
}
cout << "最小值:" << minVal << "\n";
复杂度分析:
- 时间:O(N)——每个算法只需一次遍历
- 空间:O(1)——只需几个额外变量(不计输入数组本身)
翻转数组
int arr[] = {1, 2, 3, 4, 5};
int n = 5;
// 从两端向中间交换元素
for (int i = 0, j = n - 1; i < j; i++, j--) {
swap(arr[i], arr[j]); // swap() 是内置函数
}
// arr 现在是 {5, 4, 3, 2, 1}
复杂度分析:
- 时间:O(N)——每对元素交换一次,共 N/2 次
- 空间:O(1)——原地交换,不需要额外数组
二维数组
二维数组就像一张表格或网格,非常适合地图、棋盘、矩阵:
📄 二维数组就像一张表格或网格,非常适合地图、棋盘、矩阵:
int grid[3][4]; // 3 行 4 列
// 填入 i * 10 + j
for (int r = 0; r < 3; r++) {
for (int c = 0; c < 4; c++) {
grid[r][c] = r * 10 + c;
}
}
// 打印
for (int r = 0; r < 3; r++) {
for (int c = 0; c < 4; c++) {
cout << grid[r][c] << "\t";
}
cout << "\n";
}
输出:
0 1 2 3
10 11 12 13
20 21 22 23
2.3.7 向量(vector)——动态数组
数组有一个大限制:大小必须在编译时确定(或事先声明得足够大)。向量解决了这个问题——在程序运行时可以按需增减大小。
数组 vs 向量对比
| 特性 | 数组 | 向量 |
|---|---|---|
| 大小 | 编译时固定 | 运行时可增减 |
| 读 N 个元素 | 必须硬编码或用 MAXN | push_back(x) 自然适用 |
| 内存位置 | 栈(快速,有限) | 堆(稍慢,无限制) |
| 语法 | int arr[5] | vector<int> v(5) |
| 竞赛编程首选 | 固定大小的简单情况 | 大多数问题 |
向量基础
📄 查看代码:向量基础
#include <bits/stdc++.h>
using namespace std;
int main() {
// 创建空向量
vector<int> v;
// 用 push_back 在末尾添加元素
v.push_back(10); // v = [10]
v.push_back(20); // v = [10, 20]
v.push_back(30); // v = [10, 20, 30]
// 按下标访问(与数组相同,从 0 开始)
cout << v[0] << "\n"; // 10
cout << v[1] << "\n"; // 20
// 常用函数
cout << v.size() << "\n"; // 3(元素个数)
cout << v.front() << "\n"; // 10(第一个元素)
cout << v.back() << "\n"; // 30(最后一个元素)
cout << v.empty() << "\n"; // 0(false——不为空)
// 删除最后一个元素
v.pop_back(); // v = [10, 20]
// 清空所有元素
v.clear(); // v = []
cout << v.empty() << "\n"; // 1(true——现在为空)
return 0;
}
创建带初始值的向量
vector<int> zeros(10, 0); // 十个 0:[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
vector<int> ones(5, 1); // 五个 1:[1, 1, 1, 1, 1]
vector<int> primes = {2, 3, 5, 7, 11}; // 从列表初始化
vector<int> empty; // 空向量
遍历向量
📄 查看代码:遍历向量
vector<int> v = {10, 20, 30, 40, 50};
// 方法一:下标循环(与数组相同)
for (int i = 0; i < (int)v.size(); i++) {
cout << v[i] << " ";
}
cout << "\n";
// 方法二:范围 for 循环(更简洁,推荐)
for (int x : v) {
cout << x << " ";
}
cout << "\n";
// 方法三:引用范围 for(需要修改时使用)
for (int& x : v) {
x *= 2; // 将每个元素就地翻倍
}
🤔 为什么下标循环用
(int)v.size()?v.size()返回的是无符号整数。把int i和无符号值比较,C++ 有时会产生意外行为(尤其是i为负时)。转成(int)是安全习惯。
USACO 中使用向量的标准模式
int n;
cin >> n;
vector<int> arr(n); // 创建大小为 n 的向量
for (int i = 0; i < n; i++) {
cin >> arr[i]; // 读入每个位置
}
// 现在处理 arr...
sort(arr.begin(), arr.end()); // 升序排序
二维向量
int rows = 3, cols = 4;
vector<vector<int>> grid(rows, vector<int>(cols, 0)); // 3×4 的全 0 网格
// 访问:grid[r][c]
grid[1][2] = 42;
cout << grid[1][2] << "\n"; // 42
2.3.8 向函数传递数组和向量
数组
向函数传递数组时,函数收到的是第一个元素的指针。函数内部的修改会影响原数组:
📄 向函数传递数组时,函数收到的是第一个元素的指针。**函数内部的修改会影响原数组:**
void fillSquares(int arr[], int n) { // 数组参数的 arr[] 语法
for (int i = 0; i < n; i++) {
arr[i] = i * i; // 修改的是原数组!
}
}
int main() {
int arr[5] = {0};
fillSquares(arr, 5);
// arr 现在是 {0, 1, 4, 9, 16}
for (int i = 0; i < 5; i++) cout << arr[i] << " ";
cout << "\n";
return 0;
}
向量
向量传递给函数时默认是复制(大向量很慢!)。用 & 按引用传递:
📄 向量传递给函数时默认是**复制**(大向量很慢!)。用 `&` 按引用传递:
// 按值传递——复制整个向量(大向量时很慢)
void printVec(vector<int> v) {
for (int x : v) cout << x << " ";
}
// 按引用传递——不复制,可修改原向量(用于输出参数)
void sortVec(vector<int>& v) {
sort(v.begin(), v.end());
}
// 按 const 引用传递——不复制,不可修改(只读时最佳)
void printVecFast(const vector<int>& v) {
for (int x : v) cout << x << " ";
}
⚡ 专业技巧: 对于只读(不修改)的向量参数,始终写
const vector<int>&。这既避免了复制,也向读者表明函数不会修改向量。
⚠️ 第 2.3 章常见错误
| # | 错误 | 示例 | 错在哪里 | 修复方法 |
|---|---|---|---|---|
| 1 | 数组越界差一 | 数组大小为 n 时访问 arr[n] | 合法下标是 0 到 n-1,arr[n] 越界 | 用 i < n 而不是 i <= n |
| 2 | 忘记递归边界条件 | int f(int n) { return n*f(n-1); } | 永不停止,导致栈溢出崩溃 | 加上 if (n == 0) return 1; |
| 3 | 递归传入非法参数(如负数) | factorial(-1) | 边界条件只处理 n == 0;负值导致无限递归 → 栈溢出 | 调用前确保输入在合法范围;或在函数入口加防御:if (n < 0) return -1; |
| 4 | 向量按值传递性能问题 | void f(vector<int> v) | 复制整个向量,N 大时极慢 | 用 const vector<int>& v |
| 5 | 局部数组未初始化 | int arr[100]; sum += arr[50]; | 局部数组不会自动清零,包含垃圾值 | 用 = {} 初始化或使用全局数组 |
| 6 | main 内数组太大 | int main() { int arr[1000000]; } | 超过栈内存限制(通常 1-8 MB),程序崩溃 | 把大数组放在 main 外(全局) |
| 7 | 函数定义在调用之后 | main 调用 square(5) 但 square 定义在 main 下面 | 编译器无法识别未定义的函数 | 在 main 之前定义函数,或使用函数原型 |
本章总结
📌 核心要点
| 概念 | 要点 | 为什么重要 |
|---|---|---|
| 函数 | 定义一次,随处调用 | 减少重复代码,提高可读性 |
| 返回类型 | int、double、bool、void | 不同场景用不同返回类型 |
| 按值传递 | 函数得到副本,原变量不变 | 安全,无副作用 |
按引用传递(&) | 函数操作原变量 | 可修改原变量,避免复制大对象 |
| 递归 | 函数调用自身,必须有边界条件 | 分治、回溯、DP 的基础 |
| 数组 | 固定大小,从 0 开始,O(1) 随机访问 | 竞赛编程中最基本的数据结构 |
| 全局数组 | 避免栈溢出,自动初始化为 0 | N 超过 10^5 时必须用全局数组 |
vector<int> | 动态数组,可变大小 | 竞赛编程首选数据容器 |
push_back / pop_back | 在末尾增/删 | O(1) 操作,构建动态集合的主要方式 |
| 前缀和 | O(N) 预处理,O(1) 查询 | 区间求和的核心技巧,第 3.2 章深入讲解 |
❓ 常见问题
Q1:数组和向量哪个更好?
A:竞赛编程中两者都常用。经验法则:大小固定且已知时,全局数组最简单;大小动态变化或需要传给函数时,用
vector。很多选手默认用vector,因为它更灵活、更不容易出错。
Q2:递归深度有限制吗?会崩溃吗?
A:有。每次函数调用都在栈上分配空间,默认栈大小约 1-8 MB。实际上,约 10^4 到 10^5 层递归是安全的。超出后程序会以「栈溢出」崩溃。竞赛中如果递归深度可能超过 10^4,考虑改用迭代(循环)方式。
Q3:什么时候用按引用传递(&)?
A:两种情况:① 需要在函数内部修改原变量;② 参数是大对象(如
vector或string),想避免复制开销。对于int、double这样的小类型,复制开销可忽略不计,按值传递即可。
Q4:函数可以返回数组或向量吗?
A:数组不能直接返回,但
vector可以!vector<int> solve() { ... return result; }完全有效。现代 C++ 编译器会优化返回过程(称为 RVO),实际上不会复制整个向量。
Q5:为什么前缀和数组多一个索引?prefix[n+1] 而不是 prefix[n]?
A:
prefix[0] = 0是一个「哨兵值」,使得公式prefix[R+1] - prefix[L]在所有情况下都有效。没有这个哨兵,查询 [0, R] 时 L=0 需要特殊处理。这是一个非常常见的编程技巧:用额外的哨兵值简化边界处理。
🔗 与后续章节的联系
- 第 3.1 章(STL 核心用法)将介绍
sort、binary_search、pair等工具,让你用一行代码完成本章手动实现的操作 - 第 3.2 章(前缀和)将深入探讨题目 2.3.10 中引入的前缀和技术,包括二维前缀和和差分数组
- 第 5.1 章(图的基础)将以 2.3.4 节的递归基础为起点,讲解 DFS 和 BFS 等图遍历算法
- 第 6.1-6.3 章(动态规划):「把大问题拆解成小问题」的核心思想与递归密切相关;本章的递归思维是重要的基础
- 本章学到的函数封装和数组/向量操作将在后续每一章中持续使用
练习题
🌡️ 热身题
热身 2.3.1 — 平方函数
编写一个函数 int square(int x),返回 x²。在 main 中读取一个整数并打印其平方。
样例输入: 7 → 样例输出: 49
💡 题解(点击展开)
思路: 把函数写在 main 之上,用输入调用它。
#include <bits/stdc++.h>
using namespace std;
int square(int x) {
return x * x;
}
int main() {
int n;
cin >> n;
cout << square(n) << "\n";
return 0;
}
关键点:
- 函数定义在
main之上,编译器才能识别 return x * x;——C++ 计算x * x并返回结果- 若 x 可能很大(如最大 10^9),用
long long(x² 最大 10^18)
热身 2.3.2 — 两数之大
编写函数 int myMax(int a, int b),返回两个整数中较大的。在 main 中读取两个整数并打印较大值。
样例输入: 13 7 → 样例输出: 13
💡 题解(点击展开)
思路: 比较 a 和 b,返回较大的那个。
#include <bits/stdc++.h>
using namespace std;
int myMax(int a, int b) {
if (a > b) return a;
return b;
}
int main() {
int a, b;
cin >> a >> b;
cout << myMax(a, b) << "\n";
return 0;
}
关键点:
- C++ 有内置的
max(a, b)函数——但自己写一遍有助于理解概念 - 三目运算符替代方案:
return (a > b) ? a : b;
热身 2.3.3 — 倒序数组
声明一个恰好有 5 个整数的数组:{1, 2, 3, 4, 5}。倒序打印(不需要输入)。
期望输出:
5 4 3 2 1
💡 题解(点击展开)
思路: 从下标 4 循环到 0(倒序)。
#include <bits/stdc++.h>
using namespace std;
int main() {
int arr[5] = {1, 2, 3, 4, 5};
for (int i = 4; i >= 0; i--) {
cout << arr[i];
if (i > 0) cout << " ";
}
cout << "\n";
return 0;
}
关键点:
- 从下标
n-1 = 4循环到0(包含),使用i-- if (i > 0) cout << " "避免末尾多一个空格——不过对 USACO 来说末尾空格通常可以接受
热身 2.3.4 — 向量求和
创建一个向量,用 push_back 依次加入 10、20、30、40、50,然后打印它们的总和。
期望输出: 150
💡 题解(点击展开)
思路: 创建空向量,push 5 个值,循环求和。
#include <bits/stdc++.h>
using namespace std;
int main() {
vector<int> v;
v.push_back(10);
v.push_back(20);
v.push_back(30);
v.push_back(40);
v.push_back(50);
long long sum = 0;
for (int x : v) {
sum += x;
}
cout << sum << "\n";
return 0;
}
关键点:
- 范围 for
for (int x : v)遍历所有元素 - 一行替代方案:
accumulate(v.begin(), v.end(), 0LL)
热身 2.3.5 — Hello N 次
编写一个 void 函数 sayHello(int n),打印「Hello!」恰好 n 次。读取 n 后从 main 调用它。
样例输入: 3
样例输出:
Hello!
Hello!
Hello!
💡 题解(点击展开)
思路: 一个内含 for 循环的 void 函数。
#include <bits/stdc++.h>
using namespace std;
void sayHello(int n) {
for (int i = 0; i < n; i++) {
cout << "Hello!\n";
}
}
int main() {
int n;
cin >> n;
sayHello(n);
return 0;
}
关键点:
void表示函数不返回任何值——不需要return 值;(可以用裸return;提前退出)sayHello参数中的n是main中n的独立副本(按值传递)
🏋️ 核心练习题
题目 2.3.6 — 倒序输出 读取 N(1 ≤ N ≤ 100),然后读取 N 个整数,倒序打印。
样例输入:
5
1 2 3 4 5
样例输出: 5 4 3 2 1
💡 题解(点击展开)
思路: 存入向量,然后从最后一个下标到第一个倒序打印。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int n;
cin >> n;
vector<int> arr(n);
for (int i = 0; i < n; i++) {
cin >> arr[i];
}
for (int i = n - 1; i >= 0; i--) {
cout << arr[i];
if (i > 0) cout << " ";
}
cout << "\n";
return 0;
}
关键点:
vector<int> arr(n)创建大小为 n 的向量(初始全为零)- 就像数组一样读入
arr[i] - 从
n-1循环到0(包含)打印
题目 2.3.7 — 动态平均值 读取 N(1 ≤ N ≤ 100),然后逐个读取 N 个整数。每读入一个整数后,打印当前已读所有整数的平均值(保留 2 位小数)。
样例输入:
4
10 20 30 40
样例输出:
10.00
15.00
20.00
25.00
💡 题解(点击展开)
思路: 维护累计和,每次新读入后除以已读个数。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int n;
cin >> n;
long long sum = 0;
for (int i = 1; i <= n; i++) {
int x;
cin >> x;
sum += x;
double avg = (double)sum / i;
cout << fixed << setprecision(2) << avg << "\n";
}
return 0;
}
关键点:
- 每次新元素读入后更新
sum;i是已读元素个数 (double)sum / i——先强转为 double 再除,得到小数结果fixed << setprecision(2)强制输出恰好 2 位小数
题目 2.3.8 — 频率统计 读取 N(1 ≤ N ≤ 100)个整数,每个整数在 1 到 10 之间。打印 1 到 10 中每个值出现的次数。
样例输入:
7
3 1 2 3 3 1 7
样例输出:
1 appears 2 times
2 appears 1 times
3 appears 3 times
4 appears 0 times
5 appears 0 times
6 appears 0 times
7 appears 1 times
8 appears 0 times
9 appears 0 times
10 appears 0 times
💡 题解(点击展开)
思路: 用数组(或向量)作为「计数器」——下标 1 到 10 存储对应值的计数。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int n;
cin >> n;
int freq[11] = {}; // 下标 0-10;我们用 1-10。全部初始化为 0。
for (int i = 0; i < n; i++) {
int x;
cin >> x;
freq[x]++; // 值 x 的计数加一
}
for (int v = 1; v <= 10; v++) {
cout << v << " appears " << freq[v] << " times\n";
}
return 0;
}
关键点:
freq[x]++是非常常见的模式——用值作为频率数组的下标- 声明
freq[11],下标 0-10,使freq[10](表示值 10)有效 int freq[11] = {}——= {}将所有元素初始化为零
题目 2.3.9 — 两数之和
读取 N(1 ≤ N ≤ 100)个整数和目标值 T。如果数组中任意两个不同元素相加等于 T,打印 YES,否则打印 NO。
样例输入:
5 9
1 4 5 6 3
(N=5,T=9,然后是数组)
样例输出: YES(因为 4+5=9 或 3+6=9)
💡 题解(点击展开)
思路: 检查所有满足 i < j 的对 (i, j),若任意一对之和等于 T,打印 YES。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int n, t;
cin >> n >> t;
vector<int> arr(n);
for (int i = 0; i < n; i++) cin >> arr[i];
bool found = false;
for (int i = 0; i < n && !found; i++) {
for (int j = i + 1; j < n; j++) {
if (arr[i] + arr[j] == t) {
found = true;
break;
}
}
}
cout << (found ? "YES" : "NO") << "\n";
return 0;
}
关键点:
- 内层循环从
j = i + 1开始,避免同一元素使用两次,也避免重复检查相同的对 break+ 外层循环的&& !found条件确保找到匹配后立即停止- 这是 O(N²)——对 N ≤ 100 完全够用。N 最大 10^5 时,需要用集合(第 3.1 章)
题目 2.3.10 — 前缀和 读取 N(1 ≤ N ≤ 1000),然后读取 N 个整数。再读取 Q 个查询(1 ≤ Q ≤ 1000),每个查询有两个整数 L 和 R(0-indexed,包含两端)。对每个查询打印下标 L 到 R 的元素之和。
样例输入:
5
1 2 3 4 5
3
0 2
1 3
2 4
样例输出:
6
9
12
💡 题解(点击展开)
为什么不对每次查询直接求和? 暴力法:每次查询从 L 循环到 R,时间复杂度 O(N),所有查询总计 O(N×Q)。当 N=10^5,Q=10^5 时是 10^{10} 次操作——远超时限。
优化思路: 预处理一次,O(N);之后每次查询只需 O(1)。总计 O(N+Q),快得多!这就是前缀和的核心思想(第 3.2 章深入讲解)。
思路: 构建前缀和数组,prefix[i] = arr[0..i-1] 的和。则 L 到 R 的和 = prefix[R+1] - prefix[L],每次查询 O(1)。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int n;
cin >> n;
vector<long long> arr(n), prefix(n + 1, 0);
for (int i = 0; i < n; i++) {
cin >> arr[i];
prefix[i + 1] = prefix[i] + arr[i]; // 构建前缀和
}
// prefix[0] = 0
// prefix[1] = arr[0]
// prefix[2] = arr[0] + arr[1]
// prefix[i] = arr[0] + arr[1] + ... + arr[i-1]
int q;
cin >> q;
while (q--) {
int l, r;
cin >> l >> r;
// l 到 r(含)的和 = prefix[r+1] - prefix[l]
cout << prefix[r + 1] - prefix[l] << "\n";
}
return 0;
}
关键点:
prefix[i]= 前i个元素之和(prefix[0] = 0 是哨兵)- arr[L..R] 的和 =
prefix[R+1] - prefix[L]——减去 L 之前的部分 - 用样例验证:arr=[1,2,3,4,5],prefix=[0,1,3,6,10,15]。查询 [0,2]:prefix[3]-prefix[0]=6-0=6 ✓
复杂度分析:
- 时间:O(N + Q)——预处理 O(N) + 每次查询 O(1) × Q
- 空间:O(N)——前缀和数组占 N+1 的空间
💡 暴力 vs 优化: 暴力 O(N×Q) vs 前缀和 O(N+Q)。N=Q=10^5 时,前者需 10^{10} 次操作(超时),后者只需 2×10^5 次操作(瞬间)。
🏆 挑战题
挑战 2.3.11 — 数组旋转 读取 N(1 ≤ N ≤ 1000)和 K(0 ≤ K < N),再读取 N 个整数。打印向右旋转 K 位后的数组(最后 K 个元素移到最前面)。
样例输入:
5 2
1 2 3 4 5
样例输出: 4 5 1 2 3
💡 题解(点击展开)
思路: 新数组在位置 i 的元素原来在位置 (i - K + N) % N。等价地,从下标 N-K 开始打印,循环回绕。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int n, k;
cin >> n >> k;
vector<int> arr(n);
for (int i = 0; i < n; i++) cin >> arr[i];
// 从下标 (n - k) % n 开始打印 n 个元素,循环回绕
for (int i = 0; i < n; i++) {
int idx = (n - k + i) % n;
cout << arr[idx];
if (i < n - 1) cout << " ";
}
cout << "\n";
return 0;
}
关键点:
- 右旋 K 位:后 K 个元素先输出,再输出前 N-K 个
(n - k + i) % n将新位置i映射到旧位置——% n处理回绕- 验证:n=5,k=2。i=0: idx=(5-2+0)%5=3 → arr[3]=4。i=1: idx=4 → arr[4]=5。i=2: idx=0 → arr[0]=1。正确!
挑战 2.3.12 — 合并有序数组 读取 N₁,然后 N₁ 个已排序的整数;读取 N₂,然后 N₂ 个已排序的整数。打印合并后的有序数组。
样例输入:
3
1 3 5
4
2 4 6 8
样例输出: 1 2 3 4 5 6 8
💡 题解(点击展开)
思路: 双指针——一个指向每个数组的当前位置。每步取两个当前元素中较小的。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int n1;
cin >> n1;
vector<int> a(n1);
for (int i = 0; i < n1; i++) cin >> a[i];
int n2;
cin >> n2;
vector<int> b(n2);
for (int i = 0; i < n2; i++) cin >> b[i];
// 双指针合并
int i = 0, j = 0;
vector<int> result;
while (i < n1 && j < n2) {
if (a[i] <= b[j]) {
result.push_back(a[i++]); // 取 a,i 前进
} else {
result.push_back(b[j++]); // 取 b,j 前进
}
}
// 某个数组可能还有剩余元素
while (i < n1) result.push_back(a[i++]);
while (j < n2) result.push_back(b[j++]);
for (int idx = 0; idx < (int)result.size(); idx++) {
cout << result[idx];
if (idx < (int)result.size() - 1) cout << " ";
}
cout << "\n";
return 0;
}
关键点:
- 双指针
i和j同步扫描数组a和b - 始终取当前较小的元素——维持有序性
- while 循环结束后,某个数组可能还有剩余元素——直接追加
挑战 2.3.13 — 气味距离 (USACO Bronze 风格)
N 头奶牛站成一排,每头奶牛有位置 p[i] 和气味半径 s[i]。如果两头奶牛之间的距离不超过它们半径之和,它们就能互相闻到对方。读取 N,然后 N 对(位置,半径),打印能互相闻到的奶牛对数。
样例输入:
4
1 2
5 1
8 3
15 1
样例输出: 1
(对 (1,2):距离=|5-8|=3,半径和=1+3=4,3≤4,YES。其余对距离均超过半径和,总计 1。)
💡 题解(点击展开)
思路: 检查所有满足 i < j 的对,对每对计算距离并与半径和比较。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int n;
cin >> n;
vector<long long> pos(n), rad(n);
for (int i = 0; i < n; i++) {
cin >> pos[i] >> rad[i];
}
int count = 0;
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
long long dist = abs(pos[i] - pos[j]);
long long sumRad = rad[i] + rad[j];
if (dist <= sumRad) {
count++;
}
}
}
cout << count << "\n";
return 0;
}
关键点:
- 检查满足 i < j 的对,避免同一对计数两次
abs(pos[i] - pos[j])计算位置间的绝对距离- 使用
long long以防位置和半径较大