mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4mobile wallpaper 5mobile wallpaper 6
5272 字
14 分钟
C++进阶知识点CSP实战详解
2026-03-31

C++ 进阶知识点实战详解(新手指南)#

本文专为刚入门的学习者设计,旨在帮助你把学到的知识真正用到实际编程中。
每个知识点都会先讲是什么,再讲为什么(实战中有什么用),最后用带详细自然语言说明的代码展示怎么做每一步都会解释清楚目的、效果和含义
对于二叉树、进制转换等概念性较强的内容,我们只讲原理和应用,不写代码,避免干扰。


1. 代码复杂度分析——帮你在实战中写出高效的代码#

1.1 什么是复杂度?#

复杂度就是衡量程序运行时间(时间复杂度)和占用内存(空间复杂度)的指标。用 大 O 符号 表示,它描述当输入数据量(用 (n) 表示)变得很大时,程序增长的速度。

1.2 为什么实战中要关心?#

  • 用户等待时间:如果程序太慢,用户会受不了。
  • 服务器成本:低效的算法会消耗更多 CPU 和内存,增加服务器费用。
  • 面试/竞赛:复杂度分析是考核重点。

1.3 常见复杂度及实战例子#

复杂度名称实战场景例子
(O(1))常数阶根据索引取数组元素arr[5]
(O(\log n))对数阶在有序数据中查找二分查找(如电话簿查找)
(O(n))线性阶遍历列表求数组总和
(O(n \log n))线性对数阶排序快速排序、归并排序(如排序 100 万条记录)
(O(n^2))平方阶双层循环冒泡排序、简单的图形碰撞检测
(O(2^n))指数阶穷举所有子集旅行商问题的暴力解法(n=20 就爆了)

实战选择

  • 处理 10 万条数据,(O(n^2)) 会慢到无法接受,而 (O(n \log n)) 通常没问题。
  • 写代码前先估算数据量,选择合适算法。

2. 前缀和与后缀和——快速回答“区间和”问题#

2.1 实战场景#

  • 股票分析:快速计算某段时间内的总收益。
  • 游戏统计:查询玩家某几天的得分总和。
  • 地图区域:二维前缀和用于计算矩形区域内的像素总和。

2.2 前缀和详解(一步步来)#

问题:给你一个数组 arr,要频繁查询子数组 [l, r] 的和。如果每次都重新加一遍,每次查询是 (O(n)),太慢。前缀和可以把每次查询降到 (O(1))。

逻辑

  1. 创建一个新数组 prefix,长度比原数组多 1,prefix[0] = 0
  2. prefix[i] 表示原数组前 i 个元素的和(即 arr[0] + arr[1] + ... + arr[i-1])。
  3. 那么子数组 arr[l] + ... + arr[r] 的和 = prefix[r+1] - prefix[l]

为什么?
因为 prefix[r+1]arr[0]arr[r] 的和,减去 arr[0]arr[l-1] 的和(即 prefix[l]),就剩下 arr[l]arr[r] 的和。

代码(每一步都有详细说明)

#include <vector>
using namespace std;
// 这个函数会返回一个前缀和数组,之后用这个数组可以快速算出任何区间的和
vector<int> buildPrefixSum(const vector<int>& arr) {
// 先拿到原数组有多少个数字
int n = arr.size();
// 新建一个数组,长度比原数组多 1,所有元素初始为 0
// 我们把这个数组叫 prefix(前缀和数组)
vector<int> prefix(n + 1, 0);
// 开始循环,i 从 0 走到 n-1,代表原数组的每一个位置
for (int i = 0; i < n; i++) {
// prefix[i+1] 要存储“原数组从开头到第 i 个数字的和”
// 这个和等于“前 i 个数字的和”加上“第 i 个数字”
// 前 i 个数字的和正好就是 prefix[i]
// 第 i 个数字就是 arr[i]
// 所以公式就是 prefix[i+1] = prefix[i] + arr[i]
prefix[i + 1] = prefix[i] + arr[i];
}
// 循环结束后,prefix 数组就准备好了,把它返回给调用者
return prefix;
}
// 这个函数用前缀和数组快速求出原数组中从 l 到 r 的区间和
int rangeSum(const vector<int>& prefix, int l, int r) {
// 根据数学推导:arr[l] + ... + arr[r] = prefix[r+1] - prefix[l]
// 因为 prefix[r+1] 包含了从 0 到 r 的所有数字和
// 减去 prefix[l](包含从 0 到 l-1 的所有数字和)
// 剩下的就是从 l 到 r 的和
return prefix[r + 1] - prefix[l];
}

使用示例

vector<int> arr = {3, 1, 4, 1, 5}; // 原数组
vector<int> pre = buildPrefixSum(arr); // 调用函数,得到前缀和数组
cout << rangeSum(pre, 1, 3) << endl; // 输出 1+4+1 = 6

2.3 后缀和#

后缀和是从当前位置到末尾的和。常用于需要“从后往前”累加的场景,比如统计某天之后的总销量。

逻辑

  • suffix[i] 表示 arr[i] + arr[i+1] + ... + arr[n-1]
  • 从右向左递推:suffix[i] = arr[i] + suffix[i+1],并令 suffix[n] = 0 方便边界。

代码(思路与前缀和对称):

vector<int> buildSuffixSum(const vector<int>& arr) {
int n = arr.size();
// 后缀和数组长度也是 n+1,最后一个位置 suffix[n] 设为 0
vector<int> suffix(n + 1, 0);
// 从最后一个元素开始往前推
for (int i = n - 1; i >= 0; i--) {
// suffix[i] = 当前元素 arr[i] + 它后面所有元素的和(即 suffix[i+1])
suffix[i] = suffix[i + 1] + arr[i];
}
return suffix;
}
// 查询从下标 l 到末尾的和
int suffixSum(const vector<int>& suffix, int l) {
return suffix[l];
}

3. 二分查找——在有序数据中快速定位#

3.1 实战场景#

  • 查找电话簿中的联系人(已排序)。
  • 在已排序的订单列表中查找某价格附近的订单
  • 游戏排行榜中查找某个分数的排名

3.2 标准二分查找:在区间 [l, r] 中找 x#

逻辑(核心思想:每次砍掉一半):

  1. 确定当前查找的区间是 [l, r],初始 l = 0,r = n-1。
  2. 只要 l <= r,就继续:
    • 取中间位置 mid = (l + r) / 2(为防止大数溢出,写为 l + (r-l)/2)。
    • 比较 arr[mid]x
      • 相等 → 找到,返回 mid
      • 小于 → 说明 x 在右边,所以 l = mid + 1
      • 大于 → 说明 x 在左边,所以 r = mid - 1
  3. 循环结束还没找到,返回 -1。

代码(每一步都解释清楚)

// 在数组 arr 的闭区间 [l, r] 中查找 x,返回下标,找不到返回 -1
int binarySearch(const vector<int>& arr, int x, int l, int r) {
// 只要左指针没有超过右指针,就说明还有元素没检查
while (l <= r) {
// 计算中间位置的下标。用 l + (r-l)/2 可以避免 (l+r) 可能太大而溢出
int mid = l + (r - l) / 2;
// 情况1:中间的元素正好等于我们要找的 x
if (arr[mid] == x) {
return mid; // 找到了,直接返回下标
}
// 情况2:中间的元素比 x 小,说明 x 一定在右半部分
else if (arr[mid] < x) {
l = mid + 1; // 把左边界移到 mid 的右边,继续在右半部分找
}
// 情况3:中间的元素比 x 大,说明 x 一定在左半部分
else {
r = mid - 1; // 把右边界移到 mid 的左边,继续在左半部分找
}
}
// 循环结束还没找到,说明 x 不在数组中
return -1;
}

3.3 lower_bound:找第一个 ≥ x 的位置#

场景:有重复元素,想知道第一个不小于 x 的元素下标。例如:[1,2,2,2,3] 中找第一个 ≥ 2 的位置,答案是 1(因为 arr[1]=2)。

逻辑(使用左闭右开区间 [l, r)):

  • 初始 l = 0,r = n。
  • 当 l < r 时:
    • mid = l + (r-l)/2
    • 如果 arr[mid] >= x,则第一个 ≥ x 的位置可能在 mid 或左边,所以 r = mid。
    • 否则 arr[mid] < x,则答案一定在右边,所以 l = mid + 1。
  • 循环结束时,l 就是第一个 ≥ x 的位置(可能为 n,表示所有元素都小于 x)。

代码

int lowerBound(const vector<int>& arr, int x) {
int l = 0, r = arr.size(); // 区间是 [l, r),一开始包含所有元素
while (l < r) {
int mid = l + (r - l) / 2;
// 如果中间元素 >= x,那么第一个 >= x 的位置不可能在 mid 右边
// 它可能是 mid 本身,也可能在左边,所以把右边界缩小到 mid
if (arr[mid] >= x) {
r = mid;
} else {
// 中间元素 < x,那么第一个 >= x 的位置一定在 mid 右边
l = mid + 1;
}
}
// 循环结束时 l 和 r 相等,就是我们要的位置
return l;
}

3.4 upper_bound:找第一个 > x 的位置#

场景:第一个大于 x 的元素下标。例如 [1,2,2,2,3] 中找第一个 > 2 的位置,答案是 4(arr[4]=3)。

逻辑:与 lower_bound 类似,只是条件改为 arr[mid] > x

int upperBound(const vector<int>& arr, int x) {
int l = 0, r = arr.size();
while (l < r) {
int mid = l + (r - l) / 2;
// 如果中间元素 > x,那么第一个 > x 的位置可能在 mid 或左边
if (arr[mid] > x) {
r = mid;
} else {
// 中间元素 <= x,那么第一个 > x 的位置一定在 mid 右边
l = mid + 1;
}
}
return l;
}

实战技巧

  • 利用 lower_bound 和 upper_bound 可以快速得到等于 x 的元素范围:[lower_bound(x), upper_bound(x))
  • 在 STL 中,std::lower_boundstd::upper_bound 直接可用,返回迭代器。

4. 结构体——把相关数据打包成整体#

4.1 实战场景#

  • 游戏开发:每个敌人有坐标、血量、类型等属性。
  • 数据库记录:学生信息(姓名、学号、成绩)。
  • 网络编程:消息结构(类型、长度、数据)。

4.2 结构体定义与使用(逐句解释)#

#include <iostream>
#include <string>
using namespace std;
// 定义一个结构体,名字叫 Student
struct Student {
// 下面是成员变量,它们会出现在每个 Student 对象中
string name; // 学生的名字,是一个字符串
int age; // 学生的年龄,是一个整数
double score; // 学生的成绩,是一个小数(浮点数)
// 这是构造函数,专门用来创建 Student 对象的
// 它接收三个参数:n, a, s,分别对应名字、年龄、成绩
// 后面的 : name(n), age(a), score(s) 叫做初始化列表
// 意思是把参数 n 赋值给 name,a 赋值给 age,s 赋值给 score
Student(string n, int a, double s) : name(n), age(a), score(s) {}
// 这是一个成员函数,用来显示学生的信息
// 末尾的 const 表示这个函数不会修改任何成员变量
// 这样即使一个对象是 const 的,也可以调用这个函数
void display() const {
// 输出学生信息,用空格隔开
cout << name << " " << age << " " << score << endl;
}
};

如何使用

int main() {
// 创建一个 Student 对象,名字叫 stu1,调用构造函数给它赋值
Student stu1("张三", 20, 95.5);
// 调用 stu1 的 display 函数,它会输出 "张三 20 95.5"
stu1.display();
// 结构体的成员默认是公开的,所以可以直接修改
// 把 stu1 的成绩改成 98.0
stu1.score = 98.0;
// 再次显示,现在成绩变了
stu1.display();
// 也可以直接用花括号创建对象,这是 C++11 的列表初始化
Student stu2 = {"李四", 22, 88.0};
stu2.display();
return 0;
}

实战中:结构体经常配合 vector 使用,例如 vector<Student> 存储全班学生,方便排序、查找。


5. 进制转换与编码(纯概念,实战中用于理解计算机底层)#

5.1 十进制转二进制#

方法:除 2 取余,倒序排列。
例子:13 ÷ 2 = 6 余 1(最低位),6 ÷ 2 = 3 余 0,3 ÷ 2 = 1 余 1,1 ÷ 2 = 0 余 1(最高位) → 1101₂。

5.2 二进制转十进制#

方法:按权展开。
例子:1101₂ = 1×2³ + 1×2² + 0×2¹ + 1×2⁰ = 8+4+0+1 = 13。

5.3 N 进制转十进制#

方法:将每一位数字乘以 N 的相应次幂,然后相加。
例子:八进制 237 = 2×8² + 3×8¹ + 7×8⁰ = 128+24+7 = 159。

5.4 原码、反码、补码(计算机如何表示整数)#

  • 原码:最高位为符号位(0 正 1 负),其余位为数值的绝对值。如 +5 = 00000101,-5 = 10000101。
  • 反码:正数同原码,负数符号位不变,其余位取反。如 -5 = 11111010。
  • 补码:正数同原码,负数反码 +1。如 -5 = 11111011。

为什么用补码?
因为补码可以将减法统一为加法,且只有一个 0,硬件实现简单。
实战意义:理解补码有助于排查数据溢出、位运算错误等问题。


6. 位运算与排列组合——高效处理集合与状态#

6.1 常用位运算技巧#

操作作用实战场景
x & 1判断奇偶快速判断数字是奇数还是偶数
x >> 1除以 2二分查找、递归折半
x << 1乘以 2快速计算 2 的幂
x & -x获取最低位 1树状数组(Fenwick Tree)
x & (x-1)清除最低位 1统计二进制中 1 的个数

6.2 用位运算枚举子集(排列组合)#

实战场景:在状态压缩动态规划(如旅行商问题、子集背包)中,用整数表示集合。

逻辑:假设有 n 个元素,每个元素选或不选,可以用一个 n 位的二进制数 mask 表示。第 i 位为 1 表示选择元素 i。
遍历所有子集:for (int mask = 0; mask < (1 << n); mask++)

代码(逐行解释)

#include <iostream>
using namespace std;
int main() {
int n = 3; // 假设集合包含数字 {0, 1, 2}
// 总共有 2 的 n 次方个子集,这里 n=3,所以 2³=8 个子集
// mask 从 0 到 7(二进制 000 到 111)
for (int mask = 0; mask < (1 << n); mask++) {
cout << "{ ";
// 检查 mask 的每一位,i 从 0 到 n-1
for (int i = 0; i < n; i++) {
// 如果 mask 的第 i 位是 1,说明这个子集包含元素 i
// mask >> i 把 mask 右移 i 位,然后 & 1 取出最低位
if ((mask >> i) & 1) {
cout << i << " "; // 输出这个元素
}
}
cout << "}\n";
}
return 0;
}

实战技巧:这种技巧常用于需要枚举所有组合的暴力解法或 DP 预处理。


7. 动态规划(DP)——实战中解决最优化问题#

7.1 什么是动态规划?#

动态规划是把大问题拆成小问题,记住小问题的答案,避免重复计算的一种方法。
实战场景:最短路径、背包问题、文本编辑距离、股票买卖等。

7.2 经典入门:斐波那契数列(帮助理解 DP 思想)#

问题:求第 n 个斐波那契数(0,1,1,2,3,5,8,…)。

暴力递归(慢)

int fib(int n) {
if (n <= 1) return n;
return fib(n-1) + fib(n-2);
}

为什么慢?因为重复计算了很多子问题,例如 fib(5) 要算两次 fib(3)

动态规划(自底向上)

int fib(int n) {
// 如果 n 是 0 或 1,直接返回 n 本身
if (n <= 1) return n;
// 创建一个数组 dp,大小是 n+1,用来存放每个位置的斐波那契数
vector<int> dp(n + 1);
// 初始条件:第 0 个斐波那契数是 0,第 1 个是 1
dp[0] = 0;
dp[1] = 1;
// 从 i=2 开始,一直计算到 i=n
for (int i = 2; i <= n; i++) {
// 斐波那契数的定义:当前数等于前两个数之和
dp[i] = dp[i-1] + dp[i-2];
}
// 最后 dp[n] 就是我们要的答案
return dp[n];
}

逻辑:从最小问题开始,逐步推到目标,每个子问题只计算一次,时间复杂度 O(n)。

实战:这个思想可以推广到很多问题,比如爬楼梯、最小花费爬楼梯等。

7.3 实战案例:0/1 背包问题#

问题:有 n 个物品,每个物品有重量 w 和价值 v,背包容量 W,求能装的最大总价值(每个物品最多选一次)。

DP 思路

  • 定义 dp[w] 表示容量为 w 的背包能装的最大价值。
  • 初始化 dp[0..W] = 0
  • 对于每个物品 i,我们考虑要不要把它放进背包。如果放,那么容量 w 下,价值为 dp[w - w_i] + v_i。取 dp[w] 和这个值的较大者。
  • 为了避免一个物品被重复使用,必须从大到小遍历 w

代码(每一步都有详细说明)

int knapsack(int W, const vector<int>& wt, const vector<int>& val) {
int n = wt.size(); // 一共有多少个物品
// dp 数组,长度是 W+1,下标表示容量,值表示该容量下的最大价值
// 一开始什么物品都没放,价值都是 0
vector<int> dp(W + 1, 0);
// 外层循环:依次考虑每个物品
for (int i = 0; i < n; i++) {
// 内层循环:从最大容量 W 往下遍历到当前物品的重量 wt[i]
// 为什么要从大到小?因为如果从小到大,同一个物品| 方法 | 作用 | 说明 |
|------|------|------|
| `stack<T> st;` | 创建一个空的栈,元素类型为 `T` | 例如 `stack<int> st;` |
| `st.push(x);` | 将元素 `x` 压入栈顶 | 无返回值 |
| `st.pop();` | 弹出栈顶元素(删除) | 无返回值;**必须先确保栈非空**,否则未定义行为 |
| `st.top();` | 返回栈顶元素的引用 | 不删除元素;**必须先确保栈非空** |
| `st.empty();` | 判断栈是否为空 | 返回 `bool`,空为 `true` |
| `st.size();` | 返回栈中元素的个数 | 返回 `size_t` 类型 |
可能会被多次使用(完全背包)
// 这里每个物品只能用一次,所以必须从大到小,保证更新时用到的 dp[w - wt[i]] 是没放过当前物品的状态
for (int w = W; w >= wt[i]; w--) {
// 对于当前容量 w,有两种选择:
// 1. 不装这个物品,价值保持原来的 dp[w]
// 2. 装这个物品,价值 = dp[w - wt[i]] + val[i](腾出 wt[i] 空间,加上物品价值)
// 我们取两者中较大的一个
dp[w] = max(dp[w], dp[w - wt[i]] + val[i]);
}
}
// 最终 dp[W] 就是容量为 W 时能获得的最大价值
return dp[W];
}

例子:W=5,物品1(2,3),物品2(3,4),物品3(4,5)

  • 处理物品1后:dp[2]=3, dp[3]=3, dp[4]=3, dp[5]=3
  • 处理物品2:w=5: dp[5]=max(3, dp[2]+4=7)=7; w=4: dp[4]=max(3, dp[1]+4=4)=4; w=3: dp[3]=max(3, dp[0]+4=4)=4
  • 处理物品3:w=5: dp[5]=max(7, dp[1]+5=5)=7; w=4: dp[4]=max(4, dp[0]+5=5)=5
    最终 dp[5]=7,选物品1和物品2(总重5,价值7)。

实战中:背包问题衍生出无数变种,如完全背包、多重背包,但核心都是状态转移。


8. 栈——后进先出的数据结构#

8.1 实战场景#

  • 函数调用(系统栈)。
  • 表达式求值(中缀转后缀)。
  • 括号匹配
  • 撤销操作(Undo)。 | 方法 | 作用 | 说明 | |------|------|------| | stack<T> st; | 创建一个空的栈,元素类型为 T | 例如 stack<int> st; | | st.push(x); | 将元素 x 压入栈顶 | 无返回值 | | st.pop(); | 弹出栈顶元素(删除) | 无返回值;必须先确保栈非空,否则未定义行为 | | st.top(); | 返回栈顶元素的引用 | 不删除元素;必须先确保栈非空 | | st.empty(); | 判断栈是否为空 | 返回 bool,空为 true | | st.size(); | 返回栈中元素的个数 | 返回 size_t 类型 |

8.2 C++ 栈的使用(头文件 <stack>#

#include <stack>
#include <iostream>
using namespace std;
int main() {
// 创建一个存放 int 类型数据的栈
stack<int> st;
// 入栈:把 10 放到栈顶
st.push(10);
// 入栈:把 20 放到栈顶,现在栈顶是 20
st.push(20);
// 获取栈顶元素:top() 返回栈顶的值,但不删除它
int topVal = st.top(); // 现在 topVal = 20
cout << "栈顶元素是:" << topVal << endl;
// 弹出栈顶:删除栈顶元素,没有返回值
st.pop(); // 现在栈顶变成了 10
// 判断栈是否为空:empty() 返回 true 表示空,false 表示非空
if (st.empty()) {
cout << "栈为空" << endl;
} else {
cout << "栈不为空" << endl;
}
// 获取栈中元素个数:size() 返回当前栈里的元素数量
int sz = st.size(); // 此时只有一个元素 10,所以 sz = 1
cout << "栈的大小是:" << sz << endl;
return 0;
}

关键点

  • pop() 不返回元素,必须先 top() 获取再 pop()
  • 使用前必须确保栈非空,否则 top()pop() 会导致程序崩溃。

8.3 实战应用:括号匹配(详细逻辑)#

问题:判断字符串中的括号是否合法,如 "([{}])" 合法,"([)]" 不合法。

思路

  1. 遍历字符串的每个字符。
  2. 如果遇到左括号 ( [ {,就将其压入栈。
  3. 如果遇到右括号 ) ] },则检查:
    • 如果栈为空,说明没有对应的左括号,非法。
    • 否则弹出栈顶元素,检查是否与当前右括号匹配。不匹配则非法。
  4. 遍历结束后,如果栈为空,则所有括号都匹配;否则有未匹配的左括号,非法。

代码(逐行解释)

bool isValid(string s) {
// 创建一个字符栈,用来存放左括号
stack<char> st;
// 遍历字符串中的每一个字符
for (char c : s) {
// 如果当前字符是左括号之一,就把它压入栈中
if (c == '(' || c == '[' || c == '{') {
st.push(c);
} else {
// 否则,当前字符是右括号
// 首先检查栈是否为空:如果栈空,说明没有左括号来匹配,直接返回 false
if (st.empty()) return false;
// 取出栈顶的左括号(但不删除,先看看)
char top = st.top();
// 然后把这个左括号弹出(因为已经匹配上了)
st.pop();
// 检查当前右括号是否和弹出的左括号匹配
// 如果匹配,继续循环;如果不匹配,返回 false
if ((c == ')' && top != '(') ||
(c == ']' && top != '[') ||
(c == '}' && top != '{')) {
return false;
}
}
}
// 所有字符处理完后,如果栈为空,说明所有括号都匹配了;否则有未匹配的左括号
return st.empty();
}

实战中:这个例子展示了栈的典型用法——处理嵌套结构。


##队列(Queue)

8.4 队列——先进先出的数据结构#

8.4.1 实战场景#

  • 任务调度:操作系统按顺序执行进程。
  • 消息队列:生产者消费者模型。
  • 广度优先搜索(BFS):层序遍历图或树。 | 方法 | 作用 | 说明 | |------|------|------| | queue<T> q; | 创建一个空的队列,元素类型为 T | 例如 queue<int> q; | | q.push(x); | 将元素 x 插入队尾 | 无返回值 | | q.pop(); | 删除队首元素 | 无返回值;必须先确保队列非空 | | q.front(); | 返回队首元素的引用 | 不删除元素;必须先确保队列非空 | | q.back(); | 返回队尾元素的引用 | 不删除元素;必须先确保队列非空 | | q.empty(); | 判断队列是否为空 | 返回 bool,空为 true | | q.size(); | 返回队列中元素的个数 | 返回 size_t 类型 |

8.4.2 C++ 队列的使用(头文件 <queue>#

#include <queue>
#include <iostream>
using namespace std;
int main() {
// 创建一个存放 int 类型数据的队列
queue<int> q;
// 入队:把 10 放到队列尾部
q.push(10);
// 入队:把 20 放到队列尾部,现在队首是 10,队尾是 20
q.push(20);
// 获取队首元素:front() 返回队首的值,但不删除它
int frontVal = q.front(); // frontVal = 10
cout << "队首元素是:" << frontVal << endl;
// 获取队尾元素:back() 返回队尾的值,也不删除
int backVal = q.back(); // backVal = 20
cout << "队尾元素是:" << backVal << endl;
// 出队:删除队首元素,没有返回值
q.pop(); // 现在队首变成 20
// 判断队列是否为空:empty() 返回 true 表示空,false 表示非空
if (q.empty()) {
cout << "队列为空" << endl;
} else {
cout << "队列不为空" << endl;
}
// 获取队列中元素个数:size() 返回当前队列里的元素数量
int sz = q.size(); // 此时只有一个元素 20,所以 sz = 1
cout << "队列的大小是:" << sz << endl;
return 0;
}

关键点

  • pop() 不返回元素,必须先 front() 获取再 pop()
  • 使用前必须确保队列非空,否则 front()back()pop() 会导致程序崩溃。

9. 递归——函数自己调用自己#

9.1 什么是递归?#

递归是一种编程技巧,函数在内部调用自身。适合解决可以分解为相似子问题的问题。

9.2 实战场景#

  • 树/图的遍历(如文件系统遍历)。
  • 分治算法(如快速排序、归并排序)。
  • 回溯算法(如八皇后、迷宫求解)。

9.3 递归的两个核心#

  1. 终止条件:什么时候不再递归,直接返回结果。
  2. 递归调用:如何把问题缩小,并调用自身。

9.4 实战例子:阶乘(帮助理解递归流程)#

// 计算 n! 的递归函数
int factorial(int n) {
// 终止条件:如果 n 是 0 或 1,阶乘就是 1
// 此时不再调用自己,直接返回 1
if (n <= 1) return 1;
// 递归调用:n! = n * (n-1)!
// 这里调用自己,传入 n-1,让问题规模缩小
return n * factorial(n - 1);
}

执行过程(factorial(3)):

  1. 调用 factorial(3):n=3 >1,执行 return 3 * factorial(2)
  2. 调用 factorial(2):n=2 >1,执行 return 2 * factorial(1)
  3. 调用 factorial(1):n=1,满足终止条件,返回 1
  4. 回到 factorial(2):2 * 1 = 2,返回 2
  5. 回到 factorial(3):3 * 2 = 6,返回 6

实战注意

  • 递归深度过大会导致栈溢出(例如递归 10 万层)。
  • 可以改用循环或尾递归优化(C++ 不保证优化)。
  • 很多树形结构天然适合递归(如遍历目录)。

10. 二叉树(只讲概念,实战中用于层次化数据)#

10.1 什么是二叉树?#

二叉树是一种树形结构,每个节点最多有两个子节点,分别称为左子节点和右子节点。

10.2 为什么需要二叉树?#

  • 快速查找:二叉搜索树(BST)可以在 (O(\log n)) 时间内完成查找、插入、删除。
  • 层次化数据:如文件系统、HTML 文档结构。
  • 表达式解析:编译器用语法树表示表达式。

10.3 常见的遍历方式(概念)#

  • 前序遍历:先访问根节点,再递归遍历左子树,最后递归遍历右子树。
  • 中序遍历:先遍历左子树,再访问根节点,最后遍历右子树。在二叉搜索树中,中序遍历得到升序序列。
  • 后序遍历:先遍历左子树,再遍历右子树,最后访问根节点。常用于释放树的内存。
  • 层序遍历:从上到下、从左到右逐层访问(用队列实现)。

实战应用

  • 前序遍历:复制一棵树。
  • 中序遍历:输出排序后的元素(二叉搜索树)。
  • 后序遍历:删除树、计算表达式值。
  • 层序遍历:打印树的层次结构、求树宽。

(二叉树的具体代码实现会在后续课程中讲解,这里只需理解概念和用途。)


11. 动态数组 vector——实战中最常用的容器#

11.1 什么是 vector?#

vector 是 C++ 标准库提供的动态数组,可以自动扩容,支持随机访问,是处理可变长度数据的首选。

11.2 实战场景#

  • 存储不定数量的数据(如用户输入)。
  • 作为数组替代品,避免手动管理内存。
  • 与算法库配合,进行排序、查找等操作。

vector 常用操作速查表#

操作函数/方法说明时间复杂度
创建空 vectorvector<T> v;创建一个空的动态数组O(1)
创建指定大小vector<T> v(n);创建 n 个元素,默认值为 T()O(n)
创建并初始化vector<T> v(n, val);创建 n 个元素,每个初始化为 valO(n)
列表初始化vector<T> v = {a,b,c};用花括号列表初始化O(n)
尾部添加元素v.push_back(x);在末尾添加一个元素均摊 O(1)
删除尾部元素v.pop_back();删除末尾元素(无返回值)O(1)
访问元素(下标)v[i]不检查边界,越界未定义O(1)
安全访问v.at(i)检查边界,越界抛异常O(1)
获取第一个元素v.front()返回第一个元素的引用O(1)
获取最后一个元素v.back()返回最后一个元素的引用O(1)
元素个数v.size()返回当前元素个数O(1)
容量v.capacity()返回当前分配的内存可容纳元素数O(1)
判空v.empty()返回是否为空O(1)
清空v.clear()删除所有元素,size=0,capacity 可能不变O(n)
预分配空间v.reserve(n)确保至少能容纳 n 个元素,避免多次扩容O(n)
调整大小v.resize(n, val)改变 size 为 n,新增元素用 val 填充O(n)
遍历(下标)for (int i=0; i<v.size(); ++i) ...传统方式O(n)
遍历(范围 for)for (auto x : v) ...C++11 简洁方式O(n)
排序sort(v.begin(), v.end())需包含 <algorithm>O(n log n)
反转reverse(v.begin(), v.end())需包含 <algorithm>O(n)
二分查找(需排序)lower_bound(v.begin(), v.end(), x)返回第一个 ≥ x 的迭代器O(log n)

11.3 常用操作(逐行解释)#

#include <vector>
#include <iostream>
using namespace std;
int main() {
// 创建一个空的 vector,里面存放 int 类型的数据
vector<int> v1;
// 创建一个有 5 个元素的 vector,每个元素默认是 0
vector<int> v2(5);
// 创建一个有 5 个元素的 vector,每个元素初始化为 10
vector<int> v3(5, 10);
// 直接用花括号初始化,里面放 1,2,3,4
vector<int> v4 = {1, 2, 3, 4};
// 在 v1 的尾部添加一个元素 100
v1.push_back(100);
// 再添加一个元素 200
v1.push_back(200);
// 删除尾部元素(删除 200)
v1.pop_back();
// 通过下标访问元素(不检查边界,如果下标越界会导致未定义行为)
int a = v1[0]; // a = 100
// 通过 at() 访问元素(会检查边界,如果越界会抛出异常)
int b = v1.at(0); // b = 100
// 获取第一个元素
int first = v1.front(); // first = 100
// 获取最后一个元素
int last = v1.back(); // last = 100(因为只有一个元素)
// 获取当前 vector 中元素的个数
int size = v1.size(); // size = 1
// 获取当前 vector 的容量(已经分配的内存能容纳多少个元素,可能大于 size)
int cap = v1.capacity(); // 可能大于 1
// 判断 vector 是否为空
bool empty = v1.empty(); // false,因为有一个元素
// 清空 vector 中的所有元素(size 变为 0,但容量可能不变)
v1.clear();
// 预分配空间:确保至少能容纳 100 个元素,避免多次扩容
v1.reserve(100);
// 遍历 vector 的两种方式
// 方式1:用下标
for (int i = 0; i < v1.size(); i++) {
cout << v1[i] << " ";
}
// 方式2:范围 for 循环(C++11)
for (int x : v1) {
cout << x << " ";
}
return 0;
}

11.4 与算法库配合(排序、查找)#

#include <algorithm>
vector<int> v = {5, 2, 8, 1, 9};
// 排序:将 v 中所有元素从小到大排列
sort(v.begin(), v.end()); // 现在 v 变成 {1, 2, 5, 8, 9}
// 反转:将 v 中所有元素的顺序颠倒
reverse(v.begin(), v.end()); // 现在 v 变成 {9, 8, 5, 2, 1}
// 二分查找:返回第一个 >= 5 的位置(下标)
int pos = lower_bound(v.begin(), v.end(), 5) - v.begin(); // pos = 2(因为 v[2]=5)

实战注意

  • 频繁在中间插入/删除时,vector 效率低(O(n)),可考虑 listdeque
  • vector<bool> 是特化版本,占用位而不是字节,可能不符合预期,一般用 vector<char> 代替。

12. 深度优先搜索(DFS)与广度优先搜索(BFS)——图/树的遍历#

12.1 图的表示(邻接表)#

实战中,图常用邻接表存储:vector<vector<int>> graphgraph[u] 包含所有与 u 相邻的顶点。

int n = 5; // 顶点数 0~4
vector<vector<int>> graph(n);
// 添加无向边 0-1
graph[0].push_back(1);
graph[1].push_back(0);

12.2 深度优先搜索(DFS)——一条路走到黑#

实战场景

  • 检测连通分量。
  • 拓扑排序。
  • 寻找路径。
  • 回溯算法(如八皇后)。

递归实现(逐行解释)

// 创建一个布尔数组 visited,大小等于顶点数,初始全为 false
// visited[u] 表示顶点 u 是否已经被访问过
vector<bool> visited(n, false);
// DFS 函数,参数 u 是当前正在访问的顶点
void dfs(int u) {
// 标记当前顶点 u 为已访问
visited[u] = true;
// 这里可以执行对 u 的操作,比如输出它
cout << u << " ";
// 遍历所有与 u 相邻的顶点
for (int v : graph[u]) {
// 如果邻居 v 还没有被访问过,就递归访问它
if (!visited[v]) {
dfs(v);
}
}
// 当所有邻居都处理完后,函数返回,回溯到上一层
}

执行过程:从起点出发,一直深入直到无法继续,然后回溯到上一个节点继续。

非递归实现(用栈)(可选了解):

void dfsIter(int start) {
// visited 数组记录是否访问过
vector<bool> visited(n, false);
// 创建一个栈,存放待访问的顶点
stack<int> st;
st.push(start);
while (!st.empty()) {
int u = st.top(); st.pop(); // 取出栈顶元素
if (visited[u]) continue; // 如果已经访问过,跳过
visited[u] = true; // 标记已访问
cout << u << " "; // 处理当前顶点
// 将所有未访问的邻居压入栈
for (int v : graph[u]) {
if (!visited[v]) st.push(v);
}
}
}

12.3 广度优先搜索(BFS)——层层推进#

实战场景

  • 无权图的最短路径。
  • 层序遍历树。
  • 二分图判定。

队列实现(逐行解释)

#include <queue>
void bfs(int start) {
// visited 数组记录是否已访问
vector<bool> visited(n, false);
// 队列,用来存储待访问的顶点,先进先出
queue<int> q;
// 把起点放入队列
q.push(start);
// 标记起点已访问
visited[start] = true;
while (!q.empty()) {
// 取出队首元素
int u = q.front(); q.pop();
// 处理当前顶点 u(比如输出)
cout << u << " ";
// 遍历 u 的所有邻居
for (int v : graph[u]) {
// 如果邻居还没访问过
if (!visited[v]) {
// 标记为已访问,并加入队列尾部
visited[v] = true;
q.push(v);
}
}
}
}

如果需要记录最短距离

vector<int> dist(n, -1); // dist[u] 表示从起点到 u 的最短距离,-1 表示未访问
queue<int> q;
dist[start] = 0; // 起点到自己的距离是 0
q.push(start);
while (!q.empty()) {
int u = q.front(); q.pop();
for (int v : graph[u]) {
if (dist[v] == -1) { // 如果 v 还没被访问过
dist[v] = dist[u] + 1; // 距离 = 父节点距离 + 1
q.push(v);
}
}
}

12.4 DFS vs BFS 实战选择#

需求推荐算法
找一条路径(不要求最短)DFS(代码简单)
找最短路径(无权图)BFS
检测是否有环DFS(用颜色标记)
拓扑排序DFS(后序)或 BFS(Kahn)
层序遍历树BFS
穷举所有解(回溯)DFS

总结#

本文从实战角度出发,详细讲解了 12 个 C++ 进阶知识点,每个都包含:

  • 概念:是什么。
  • 实战场景:在哪里用。
  • 代码实现:每一步都有自然语言说明,逻辑清晰。
  • 注意事项:避免踩坑。

学习这些知识后,你可以:

  • 分析算法效率,写出高性能代码。
  • 用前缀和快速处理区间查询。
  • 用二分查找在有序数据中闪电定位。
  • 用结构体管理复杂数据。
  • 理解进制与编码,减少低级错误。
  • 用位运算高效处理集合。
  • 用动态规划解决最优化问题。
  • 用栈处理嵌套结构。
  • 用递归简化分治与回溯。
  • 掌握二叉树概念,为树结构打基础。
  • 用 vector 轻松管理动态数组。
  • 用 DFS/BFS 遍历图与树。

下一步建议:动手实现这些代码,尝试修改参数、增加功能,遇到问题用调试器单步执行,观察变量变化。编程是实践的艺术,多敲代码才能真正内化为技能。

分享

如果这篇文章对你有帮助,欢迎分享给更多人!

C++进阶知识点CSP实战详解
http://blog.mcstarland.top/posts/cppp/
作者
MEMZGBL
发布于
2026-03-31
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

封面
Sample Song
Sample Artist
封面
Sample Song
Sample Artist
0:00 / 0:00