CCF GESP C++ 六级 · 客观题(理论题)知识点汇总学案
CCF GESP C++ 六级 · 客观题知识点汇总学案
范围:单选题(15 题 × 2 分 = 30 分)+ 判断题(10 题 × 2 分 = 20 分),共 50 分
不含:编程题 1 / 编程题 2(另见《编程题备考模板手册》)
依据:CCF GESP 官方认证标准 C++ 六级七条知识点(即考纲) + 历年真题客观题实证
使用说明
| 项目 | 说明 |
|---|---|
| 适用对象 | 已通过 GESP C++ 五级、准备报考六级的中小学生 |
| 配套资源 | ①《GESP C++ 六级编程题备考模板手册》②《洛谷专题练习清单》③ 洛谷有题 https://ti.luogu.com.cn/(客观题专项机刷) |
- 本学案的每一道例题都来自真实真题,不是模拟题。题号已标注场次。
- 每个模块后有 3~5 道自测题,答案在最后。先做完再对答案,对完把错的题号圈起来,第二遍只刷错题。
- 标 🔴 的是高频失分点,考前必须再看一遍。
- 标 ⭐ 的是必背结论,考场上要能不假思索写出来。
关于考频说明
本学案的"权重"依据两条:① CCF 官方大纲七条知识点的表述详略;② 可核验场次真题的实际分布。不做虚构的百分比——凡是给出具体题数的,都注明了场次来源。
第 0 讲 考试概况与一个关键洞察
题型与分值
| 题型 | 题数 | 每题 | 小计 |
|---|---|---|---|
| 单选题 | 15 | 2 分 | 30 分 |
| 判断题 | 10 | 2 分 | 20 分 |
| 编程题 | 2 | 25 分 | 50 分 |
| 合计 | 100 分 / 180 分钟 |
- 通过线:60 分
- 跳级线:90 分(达到 90 分,下次可直接报考八级)
🔴 关键洞察:客观题和编程题考的不是同一批知识点
这是历年真题中反复出现的规律,也是复习最容易走偏的地方:
| 模块 | 客观题(50 分) | 编程题(50 分) |
|---|---|---|
| 面向对象 | 高频(概念题天然适合出选择/判断) | 0 次(不考写代码) |
| 栈 / 队列 / 循环队列 | 高频(指针模拟、下标计算) | 很少单独出题 |
| 树与遍历 | 中高频(性质、节点数、遍历序列) | 最高频 |
| DFS / BFS | 中频(输出序列、比较次数) | 高频 |
| 哈夫曼 / 格雷码 | 中频(WPL 计算、编码长度) | 偶发 |
| 动态规划 | 低频(只考概念:两大性质、递归 vs DP) | 最高频 |
| 素数筛 / 数论 | 偶发 | 偶发 |
结论:
- 面向对象 + 栈队列 = 客观题性价比最高的两块,概念背熟就能拿分,且不需要写代码能力。
- 动态规划在客观题里只考"是什么",不考"怎么写"——背住两个性质和与递归/贪心/分治的区别即可。
- 编程题的 DP 和树要靠刷题,客观题的面向对象和栈队列要靠背。两条腿要分开练。
真题选择题考点分布
| 题号 | 考点 | 归属模块 |
|---|---|---|
| 1 | 面向对象三大原则 | 模块六 面向对象 |
| 2 | static 成员 + 构造/析构计数 | 模块六 面向对象 |
| 3 | 虚函数与运行时多态 | 模块六 面向对象 |
| 4 | 链式栈插入操作 | 模块七 栈 |
| 5 | 栈的特点(LIFO) | 模块七 栈 |
| 6 | 栈应用:十进制转二进制 | 模块七 栈 |
| 7 | 循环队列判满 | 模块七 队列 |
| 8 | 哈夫曼编码最少比特数 | 模块三 编码 |
| 9 | 哪种遍历先访问根 | 模块一 树遍历 |
| 10 | 5 层满二叉树节点数 | 模块一 树性质 |
| 11 | 动态规划两大性质 | 模块五 DP |
| 12 | 递归与 DP 的效率对比 | 模块五 DP |
| 13 | BFS(队列)输出序列 | 模块四 搜索 |
| 14 | DFS(栈)输出序列 | 模块四 搜索 |
| 15 | DFS 比较节点数 | 模块四 搜索 |
该场分布:面向对象 3 题、栈队列 4 题、搜索 3 题、树 2 题、DP 2 题、编码 1 题。
→ 概念型模块(面向对象 + 栈队列)占 7/15,接近一半。
第 1 讲 模块六:面向对象(客观题性价比之王)
考纲原文
(6)掌握面向对象的思想,了解封装、继承、多态的基本概念,并掌握类的创建和基本的使用方法。
一、三大特性(必背,选择题第 1 题常客)
| 特性 | 一句话 | 生活比喻 |
|---|---|---|
| 封装 | 把数据和操作藏在类里,只留接口 | 电视机:你只按遥控器(接口),不需要知道里面电路(实现) |
| 继承 | 子类自动拥有父类的属性和方法 | 学生"继承"人的姓名年龄,再加上自己的学号 |
| 多态 | 同一个调用,不同对象有不同表现 | 同样说"叫",猫喵喵、狗汪汪 |
⭐ 三大特性 = 封装、继承、多态(2024.06 第 1 题答案)
⚠️ 干扰项长什么样:贪心/动态规划/回溯(这是算法,不是面向对象特性)、并发/并行/异步(这是操作系统概念)、递归/循环/分治(这是算法思想)。
二、类与对象的基本语法
class my_class {
public: // 公有:外部可访问
static int count; // 静态成员:属于"类",不属于某个对象
my_class() { count++; } // 构造函数:创建对象时自动调用
~my_class() { count--; } // 析构函数:对象销毁时自动调用
static void print_count() { cout << count << ";"; }
};
int my_class::count = 0; // ⭐ 静态成员必须在类"外"定义并初始化
⭐ 必背结论
- 创建对象时自动调用构造函数;没有手写构造函数时,编译器会自动生成一个默认构造函数。
- 析构函数不是必须手写的——不写,编译器也会生成默认的。
- 静态数据成员属于类,被所有对象共享,必须在类外单独初始化。
- 构造函数与类名同名,没有返回值类型(连
void都不写)。 - 析构函数名 =
~+ 类名,同样无返回值,且不能带参数。
三、🔴 虚函数与运行时多态(最难,也是必考)
class shape {
public:
virtual int area() { cout << "parent area:"; return 0; } // 有 virtual
};
class rectangle : public shape {
public:
int area() { cout << "rectangle area:"; return width * height; }
};
判定规则(一句话):
父类指针指向子类对象时,如果函数是
virtual,调用的是"对象真正的类型"的函数;如果不是virtual,调用的是"指针的类型"的函数。
- 有
virtual→ 动态绑定(运行期决定)→ 调用rectangle::area() - 无
virtual→ 静态绑定(编译期决定)→ 调用shape::area()
2024.06 第 3 题中 area() 带 virtual,pshape 先指向 rectangle 再指向 triangle,输出为
rectangle area:triangle area:
(若去掉 virtual,两次都会输出 parent area:——这正是干扰项 B 的来源。)
四、真题回放
【2024.06 第 2 题】 静态成员计数,输出是什么?
my_class obj1; // count: 0 → 1
my_class::print_count(); // 输出 "1;"
my_class obj2; // count: 1 → 2
obj2.print_count(); // 输出 "2;"
my_class obj3; // count: 2 → 3
obj3.print_count(); // 输出 "3;"
答案:123;(注意分号是 print_count 里输出的一部分)
【2024.06 判断题】
| 题 | 表述 | 答案 | 理由 |
|---|---|---|---|
| 2 | 创建对象时自动调用构造函数;没定义则编译器自动生成默认构造函数 | 正确 | 概念原文 |
| 3 | 定义一个类时必须手动定义析构函数 | 错误 | 编译器会自动生成默认析构函数 |
| 4 | C++ 中类的内部可以嵌套定义类 | 正确 | 类内可以再定义类(嵌套类) |
五、随堂小测
- 下列不是面向对象三大特性的是( )
A. 封装 B. 继承 C. 多态 D. 递归 - 关于静态数据成员,正确的是( )
A. 每个对象各存一份 B. 必须在类外定义初始化 C. 只能用对象访问 D. 必须在构造函数里赋值 - 父类指针指向子类对象,函数未声明为
virtual,调用结果是( )
A. 子类版本 B. 父类版本 C. 编译报错 D. 运行报错 - 判断:析构函数可以带参数。( )
- 判断:
class A { public: class B {}; };这种写法在 C++ 中合法。( )
答案
- D(递归是算法思想)
- B(A 错:全类共享一份;C 错:可用
类名::成员访问;D 错:类外初始化) - B(静态绑定,按指针类型调用父类的版本)
- 错误(析构函数不能带参数,也不能重载)
- 正确(嵌套类是合法的)
第 2 讲 模块七:栈、队列、循环队列
考纲原文
(7)掌握栈、队列、循环队列的基本定义,应用场景和常见操作。
一、栈 Stack —— 后进先出 LIFO
比喻:一摞盘子。只能从最上面放(push)和拿(pop),最后放上去的最先被拿走。
| 操作 | 含义 |
|---|---|
push(x) |
压栈,放到栈顶 |
pop() |
弹栈,移除栈顶 |
top() |
看一眼栈顶(不移除) |
empty() / size() |
判空 / 元素个数 |
⭐ 栈顶是唯一出入口,栈底元素最后才能出来。
典型应用:括号匹配、撤销操作(Ctrl+Z)、进制转换、递归的底层实现、表达式求值。
二、链式栈的插入(🔴 高频,注意指针顺序)
栈顶指针 hs,要插入新节点 s:
s->next = hs; // ① 新节点指向原来的栈顶
hs = s; // ② 栈顶指针移到新节点
顺序绝对不能反——先改 hs 就找不到原来的栈顶了。
(2024.06 第 4 题答案:s->next = hs; hs = s;)
三、栈应用:十进制转二进制(2024.06 第 6 题)
不断 %2 取余、/2 缩小,把余数压栈,最后依次弹出:
while (!bin.empty()) {
cout << bin.top(); // ① 先读栈顶
bin.pop(); // ② 再弹出
}
🔴 顺序陷阱:必须先 top() 再 pop()。反过来的话,第一个数被弹掉后才读,会少输出一位且整体错位。
其他选项错在哪:
bin.back()/bin.front():stack容器没有这两个接口,只有top()。
四、队列 Queue —— 先进先出 FIFO
比喻:排队买奶茶。先来的先买,后来的排在队尾。
五、🔴 循环队列(必考计算题)
为了让数组"绕圈"复用,用取模 % 实现。为避免"队空"和"队满"判别式撞车,故意多开一个空间(capacity = size + 1)。
⭐ 四句口诀,全部背下来
| 项目 | 公式 |
|---|---|
| 队空 | front == rear |
| 队满 | (rear + 1) % capacity == front |
| 入队 | arr[rear] = x; rear = (rear + 1) % capacity; |
| 出队 | x = arr[front]; front = (front + 1) % capacity; |
| 元素个数 | (rear - front + capacity) % capacity |
🔴 三个必错点
- 队满不能写成
rear == front(那是队空)。 - 队满不能写成
(rear + 1) == front(漏了取模,绕圈时会错)。 front指向队头元素,rear指向队尾的下一个空位——所以牺牲了一个格子。
(2024.06 第 7 题答案:return (rear + 1) % capacity == front;)
六、真题回放
【2025.12 第 5 题】 循环队列 N = 6,初始 front = 1, rear = 1,
依次执行:入队、入队、入队、出队、入队、入队。求最终 (front, rear)。
逐步推演:
| 步骤 | 操作 | front | rear |
|---|---|---|---|
| 初始 | — | 1 | 1 |
| 1 | 入队 | 1 | (1+1)%6 = 2 |
| 2 | 入队 | 1 | 3 |
| 3 | 入队 | 1 | 4 |
| 4 | 出队 | (1+1)%6 = 2 | 4 |
| 5 | 入队 | 2 | 5 |
| 6 | 入队 | 2 | (5+1)%6 = 0 |
答案:(2, 0) —— 注意最后一步 rear 从 5 绕回 0,这正是循环队列的考点。
【2025.12 第 4 题】 依次输入 A、B、C、D 后按两次撤销(每次弹出栈顶一个字符),
栈从栈底到栈顶是:A B(C、D 被撤销掉了)。
七、随堂小测
- 栈的特点是( )A. 先进先出 B. 后进先出 C. 随机 D. 按大小
- 链式栈栈顶为
hs,插入s,正确操作是( )
A.hs->next = s;B.s->next = hs; hs = s;C.s->next = hs->next; hs->next = s;D.s->next = hs; hs = hs->next; - 循环队列容量
N,队满条件是( )
A.rear == frontB.(rear+1)%N == frontC.rear%N == frontD.(rear+1) == front - 判断:用栈实现十进制转二进制时,应该先
pop()再输出top()。( ) - 判断:
stack容器可以用front()访问栈顶。( )
答案
- B
- B
- B
- 错误(先
top()输出,再pop()) - 错误(
stack只有top(),front()是queue的接口)
第 3 讲 模块一:树的概念与遍历
考纲原文
(1)掌握树的基本概念,掌握其构造与遍历的相关算法。
一、基本概念(分清"深度"和"高度")
| 术语 | 含义 |
|---|---|
| 根节点 | 最上面、没有父亲的那个 |
| 叶子节点 | 没有孩子的节点 |
| 节点的度 | 它有几个孩子 |
| 深度 / 层次 | 从根往下数,根为第 1 层 |
| 高度 | 从叶子往上数,叶子高度为 1 |
| 树的深度 | 最大层次数 |
🔴 考试默认"根在第 1 层"(不是第 0 层)。看到"5 层"就按 5 层算。
二、⭐ 二叉树必背性质
二叉树必背性质
设深度为 h,节点总数为 n,n0 为叶子数,n2 为度为 2 的节点数。
| 性质 | 公式 | 例子 |
|---|---|---|
| 第 i 层最多节点数 | 2^(i-1) | 第 3 层最多 4 个 |
| 深度 h 的二叉树最多节点数 | 2^h - 1 | h=5 → 31 |
| 叶子与度为 2 的节点关系 | n0 = n2 + 1 | 度为 2 的有 5 个 → 叶子 6 个 |
| n 个节点的完全二叉树深度 | floor(log2 n) + 1 | n=10 → 4 |
满二叉树:每一层都填满。深度 h 的满二叉树节点数恰为 2^h - 1。
【2024.06 第 10 题】 一棵 5 层的满二叉树节点数 = 2^5 - 1 = 31。答案 31。
干扰项 32 是“把根当第 0 层”或“算成 2^5”的典型错误。
三、⭐ 四种遍历(必须会手推)
| 遍历 | 顺序 | 口诀 |
|---|---|---|
| 先序(前序) | 根 → 左 → 右 | 根左右 |
| 中序 | 左 → 根 → 右 | 左根右 |
| 后序 | 左 → 右 → 根 | 左右根 |
| 层序 | 从上到下、从左到右 | 一层一层扫 |
⭐ 第一个访问根节点的遍历是先序遍历(2024.06 第 9 题答案)
- 中序:根在中间被访问
- 后序:根在最后被访问
记忆法:看"根"字在口诀里的位置——"根左右"→ 先序;"左根右"→ 中序;"左右根"→ 后序。
四、已知两种遍历求第三种(🔴 难点)
方法三步:
- 定根:先序的第一个是根;后序的最后一个是根。
- 分段:在中序里找到这个根,根左边 = 左子树,根右边 = 右子树。
- 递归:对左右两段重复上面两步。
✏️ 例题:中序
BADC,后序BDCA,求先序。
- 后序最后一位
A→ 根是 A- 中序中 A 左边是
B(左子树),右边是DC(右子树)- 后序中对应左子树段是
B,右子树段是DC- 右子树段
DC:后序最后一位C是根;中序DC中 C 左边是D- 所以:A 的左孩子 B,右孩子 C,C 的左孩子 D
- 先序 =
ABCD
⭐ 结论:必须已知"中序"才能唯一确定一棵树。只有先序+后序不行(除非是满二叉树等特殊情形)。
五、随堂小测
- 深度为 4 的满二叉树共有( )个节点。A. 15 B. 16 C. 31 D. 8
- 二叉树中叶子数为 8,则度为 2 的节点数为( )A. 7 B. 8 C. 9 D. 无法确定
- 后序遍历中,根节点被访问的顺序是( )A. 第一个 B. 中间 C. 最后一个 D. 不一定
- 判断:已知先序和后序遍历,一定能唯一确定一棵二叉树。( )
- 判断:一棵有 100 个节点的完全二叉树,其深度为 7。( )
答案
- A(2^4-1=15)
- A(n0=n2+1 → n2=7)
- C
- 错误(必须已知中序;先序+后序不唯一)
- 正确(floor(log2 100)+1 = 6+1 = 7)
第 4 讲 模块二:哈夫曼树、完全二叉树、二叉排序树
考纲原文
(2)掌握哈夫曼树、完全二叉树、二叉排序树的相关概念和应用。
一、⭐ 哈夫曼树(最优二叉树)
构造算法(贪心):每次从当前所有节点中挑权值最小的两个合并,新节点权值为二者之和,放回集合,重复直到剩一棵树。
比喻:每次都让最轻的两个人先抬,这样重的人被抬的次数最少,总体最省力。
⭐ 必背性质
| 性质 | 内容 |
|---|---|
| 合并次数 | n 个叶子 → 合并 n-1 次 |
| 总节点数 | 2n - 1(n 个叶子 + n-1 个新增) |
| 度为 1 的节点 | 没有(哈夫曼树中不存在度为 1 的节点) |
| WPL | 带权路径长度 = sum(叶子权值 × 该叶子的深度) |
🔴 WPL 的快捷算法:
WPL = 构造过程中所有"新合并出来的节点权值"之和。
这个技巧比"逐个算深度再乘"快得多,且不容易错。
二、真题回放:哈夫曼编码比特数(2024.06 第 8 题)
对字符串
"classmycls"使用哈夫曼编码,最少需要多少比特?
第一步:统计频率(共 10 个字符)
| 字符 | c | l | a | s | m | y |
|---|---|---|---|---|---|---|
| 频数 | 2 | 2 | 1 | 3 | 1 | 1 |
第二步:模拟合并(每次取最小的两个)
| 轮次 | 取出的两个 | 新节点 | 本轮代价 |
|---|---|---|---|
| 1 | a(1) + m(1) | (am) = 2 | 2 |
| 2 | y(1) + (am)(2) | (yam) = 3 | 3 |
| 3 | c(2) + l(2) | (cl) = 4 | 4 |
| 4 | s(3) + (yam)(3) | (syam) = 6 | 6 |
| 5 | (cl)(4) + (syam)(6) | 根 = 10 | 10 |
第三步:WPL = 所有代价之和
WPL = 2 + 3 + 4 + 6 + 10 = 25
答案:25 比特。
✅ 验算(用深度法交叉检验):
c 深度 2、l 深度 2、s 深度 2、y 深度 3、a 深度 4、m 深度 4
2×2 + 2×2 + 3×2 + 1×3 + 1×4 + 1×4 = 4+4+6+3+4+4 = 25 ✓⚠️ 提示:合并时若出现"多个相同权值",选哪两个不影响 WPL(可以验证),但会影响每个字符的具体编码串。所以题目问"最少比特数/最短总长度"时答案唯一,问"某字符的编码是什么"时要注意。
三、完全二叉树
定义:深度为 h,除第 h 层外每层都满,且最后一层的节点全部靠左连续排列。
⭐ 判定方法(数组表示法):按层序从 1 开始编号,若所有节点的编号恰好是 1 ~ n 连续无空缺,就是完全二叉树。
🔴 BFS 判定代码(2025.12 第 6 题考过这个代码):
bool check(TreeNode* root) {
if (!root) return true;
queue<TreeNode*> q;
q.push(root);
bool hasNull = false; // 是否遇到过空缺
while (!q.empty()) {
TreeNode* node = q.front(); q.pop();
if (node == nullptr) { hasNull = true; } // 标记:出现空缺
else {
if (hasNull) return false; // 🔴 空缺之后又出现节点 → 不是完全二叉树
q.push(node->left);
q.push(node->right);
}
}
return true;
}
一句话:见过空缺之后,不许再有节点。
性质:
- n 个节点的完全二叉树深度 = floor(log2 n) + 1
- 编号为 i 的节点:左孩子 2i,右孩子 2i+1,父亲 floor(i/2)(i>1)
- 满二叉树一定是完全二叉树,反之不一定。
四、二叉排序树 BST
定义:对任意节点,左子树所有值 < 它 < 右子树所有值。
⭐ 最常考的性质:中序遍历二叉排序树,得到递增有序序列。
| 操作 | 复杂度 |
|---|---|
| 查找 / 插入 / 删除(平衡时) | O(log n) |
| 退化成链时 | O(n) |
五、随堂小测
- 权值集合 {5, 1, 6, 2} 构造哈夫曼树,WPL 为( )A. 25 B. 26 C. 28 D. 30
- 有 8 个叶子的哈夫曼树,总节点数为( )A. 15 B. 16 C. 17 D. 无法确定
- 判断:哈夫曼树中可能存在度为 1 的节点。( )
- 判断:满二叉树一定是完全二叉树。( )
- 二叉排序树的中序遍历结果是( )A. 递增 B. 递减 C. 无序 D. 先增后减
答案
- A — 合并:1+2=3,3+5=8,8+6=14 → WPL = 3+8+14 = 25
- A — 2n-1 = 15
- 错误(哈夫曼树没有度为 1 的节点)
- 正确
- A
第 5 讲 模块三:哈夫曼编码与格雷码
考纲原文
(3)理解哈夫曼编码、格雷编码相关原理并能进行简单应用。
一、哈夫曼编码
是什么:一种变长编码——出现频率高的字符用短编码,频率低的用长编码,使总长度最短。
怎么来:对每个字符建哈夫曼树,从根出发,走左边记 0、走右边记 1,到达叶子的路径就是该字符的编码。
⭐ 两个关键性质
- 前缀码性质:任何一个字符的编码,都不是另一个字符编码的前缀。
→ 所以解码时不会产生歧义,可以"边读边解"。 - 总编码长度 = 哈夫曼树的 WPL。
优缺点对比
| 哈夫曼编码 | 定长编码 | |
|---|---|---|
| 长度 | 短(压缩) | 长 |
| 是否前缀码 | 是 | 是 |
| 解码 | 需要码表 | 简单 |
⭐ 哈夫曼编码本质上是贪心策略(2024.06 判断题第 1 题,答案:正确)
理由:每一步都"贪心地"选当前最小的两个合并,局部最优累积成全局最优。
二、格雷码 Gray Code
是什么:一串二进制编码,其中相邻两个编码之间只有 1 位不同。
比喻:电梯楼层显示从 3 楼的 011 跳到 4 楼的 100——用普通二进制三位全变容易出错;格雷码保证每次只变一位,机械开关更可靠。
⭐ 生成公式(必背):第 i 个格雷码
G(i) = i XOR (i >> 1)
即:该数的二进制,与自己右移一位后的结果做异或。
3 位格雷码序列举例:
| 十进制 i | 二进制 | 格雷码 G(i)=i XOR (i>>1) |
|---|---|---|
| 0 | 000 | 000 |
| 1 | 001 | 001 |
| 2 | 010 | 011 |
| 3 | 011 | 010 |
| 4 | 100 | 110 |
| 5 | 101 | 111 |
| 6 | 110 | 101 |
| 7 | 111 | 100 |
⭐ 注意循环性:首尾也算相邻——最后一个 100 与第一个 000 也只差 1 位。
性质:n 位格雷码共有 2^n 个,遍历完 0 ~ 2^n-1。
三、随堂小测
- 哈夫曼编码属于( )A. 定长编码 B. 变长前缀码 C. 非前缀码 D. 加密编码
- 哈夫曼编码的总长度等于哈夫曼树的( )A. 节点数 B. 深度 C. WPL D. 叶子数
- 十进制 6 的 4 位格雷码是( )A. 0110 B. 0101 C. 1010 D. 0011
- 判断:格雷码中相邻两个编码只有一位不同。( )
- 判断:哈夫曼编码中,出现频率越高的字符,编码长度越短。( )
答案
- B
- C
- B — 6 = 0110₂,6>>1 = 0011₂,0110 XOR 0011 = 0101
- 正确
- 正确(频率高 → 离根近 → 路径短)
第 6 讲 模块四:DFS 与 BFS
考纲原文
(4)掌握深度优先搜索算法(DFS)、宽度优先搜索算法(也称广度优先搜索算法,BFS)、二叉树的搜索算法的概念及应用,能够根据现实问题,选择合适的搜索算法。
一、一句话区分
| DFS 深度优先 | BFS 宽度优先 | |
|---|---|---|
| 比喻 | 走迷宫:一条路走到黑,撞墙再回头换岔路 | 水面波纹:一圈一圈向外扩 |
| 用的数据结构 | 栈(或递归) | 队列 |
| 访问顺序 | 沿着深度往下 | 按层从左到右 |
| 在二叉树上 | 输出 = 先序(根左右) | 输出 = 层序 |
| 适合求 | 所有路径、连通性、回溯 | 最短步数(无权)、最少层数 |
| 空间 | O(h) 递归深度 | O(w) 最宽层的宽度 |
二、🔴 用栈实现 DFS(2024.06 第 14 题)
stack<TreeNode*> stk;
stk.push(root);
while (!stk.empty()) {
TreeNode* node = stk.top(); stk.pop();
if (node->val == a) return node;
cout << node->val << " ";
if (node->right) stk.push(node->right); // 先压右
if (node->left) stk.push(node->left); // 再压左
}
⭐ 为什么要"先压右、再压左"?
因为栈是后进先出——后压进去的左孩子先被弹出来。这样才能保证访问顺序是"根 → 左 → 右",即先序遍历。
🔴 反过来写成"先压左、再压右",输出就变成"根 → 右 → 左"(镜像先序),是常见错选。
三、用队列实现 BFS(2024.06 第 13 题)
queue<TreeNode*> q;
q.push(root);
while (!q.empty()) {
TreeNode* node = q.front(); q.pop();
if (node->val == a) return node;
cout << node->val << " ";
if (node->left) q.push(node->left); // 先入左
if (node->right) q.push(node->right); // 再入右
}
队列先进先出,先入的左孩子先出 → 输出是层序(从上到下,每层从左到右)。
四、🔴 "比较了多少个节点"(2024.06 第 15 题)
这类题的做法:按代码一步步模拟,每弹出一个节点并判断 node->val == a 就计一次数,找到就立刻 return,后面的不再算。
- 若目标值就是根 → 只比较 1 个节点
- 找到时提前返回 → 不计入后续节点
- 若目标不存在 → 比较全部节点
五、如何选择
| 问题特征 | 选谁 |
|---|---|
| 求最少步数 / 最短路径(边权相同) | BFS |
| 判断能否到达 / 连通块计数 | 两者都行 |
| 枚举所有方案 / 需要回溯 | DFS |
| 求树中某条特定路径 | DFS |
| 按层统计数据(树的宽度、层平均值) | BFS |
🔴 DFS/BFS 都必须有"访问标记",否则在图上会无限循环(树上因为无环,靠传 fa 跳过父亲即可)。
六、随堂小测
- BFS 使用的数据结构是( )A. 栈 B. 队列 C. 优先队列 D. 数组
- 用栈实现的非递归 DFS,若"先压右后压左",输出顺序等价于( )A. 先序 B. 中序 C. 后序 D. 层序
- 无权图中求两点最短路径,应选( )A. DFS B. BFS C. 两者一样 D. 都不行
- 判断:DFS 可以用递归实现,递归的本质就是系统调用栈。( )
- 判断:BFS 找到目标时,可以保证是层数最浅的那个。( )
答案
- B
- A
- B
- 正确
- 正确(BFS 按层推进,第一次遇到即最浅)
第 7 讲 模块五:简单动态规划
考纲原文
(5)掌握简单动态规划的算法思想,能够使用代码解决相应的一维动态规划问题和简单背包问题。
一、⭐ 动态规划的两大性质(2024.06 第 11 题,答案:重叠子问题)
| 性质 | 含义 |
|---|---|
| 最优子结构 | 大问题的最优解,可以由小问题的最优解拼出来 |
| 重叠子问题 | 计算过程中,同一个小问题被反复计算多次 |
🔴 正是因为有"重叠子问题",才需要把答案存下来(记忆化 / DP 数组)。
干扰项:分治法(子问题不重叠,如归并排序)、贪心策略、回溯算法。
二、DP 四步法
- 定义状态:
dp[i]表示什么?(这一步最关键) - 写转移方程:
dp[i]怎么由前面的推出来? - 定边界:
dp[0]、dp[1]是多少? - 定遍历方向:从前往后还是从后往前?
三、🔴 递归 vs 动态规划(2024.06 第 12 题)
题目给的两个函数:
int jump_recur(int n) { // 递归版
if (n == 1) return 1;
if (n == 2) return 2;
return jump_recur(n-1) + jump_recur(n-2);
}
int jump_dp(int n) { // DP 版
vector<int> dp(n+1);
dp[1] = 1; dp[2] = 2;
for (int i = 3; i <= n; ++i) dp[i] = dp[i-1] + dp[i-2];
return dp[n];
}
| 说法 | 判断 | 理由 |
|---|---|---|
A. jump_recur 采用递归方式 |
✅ 正确 | 函数自己调用自己 |
B. jump_dp 采用动态规划方法 |
✅ 正确 | 用数组保存已算过的值 |
| C. n 较大时递归有大量重复计算,效率低 | ✅ 正确 | 重叠子问题未保存 |
| D. 递归代码量小,执行效率高 | ❌ 错误(本题答案) | 代码量小 ≠ 效率高;递归时间复杂度 O(2^n),DP 是 O(n) |
⭐ 必背:递归代码短,但效率低;DP 代码稍长,但效率高。
效率高不高看的是时间复杂度,不是代码行数。
复杂度对比:
- 递归爬楼梯:O(2^n)(指数级,n=40 就要算很久)
- DP 爬楼梯:O(n)
四、四种算法思想的区别(判断/选择题常客)
| 思想 | 核心 | 子问题是否重叠 | 是否保证最优 |
|---|---|---|---|
| 分治 | 拆成独立子问题再合并 | 不重叠 | 是 |
| 贪心 | 每步选当前最优 | — | 不一定最优 |
| DP | 记录所有子问题的最优解 | 重叠 | 是 |
| 回溯 | 枚举所有可能,不行就退 | 可能重叠 | 是(但慢) |
🔴 DP 与贪心的关键区别:贪心"一条道走到黑不回头",DP"把每种可能都算一遍取最好"。
五、背包方向口诀(客观题偶尔考)
| 类型 | 一维数组容量遍历方向 | 原因 |
|---|---|---|
| 0/1 背包(每件最多 1 次) | 倒序 for (j = V; j >= w; j--) |
防止同一物品被重复放 |
| 完全背包(每件无限次) | 正序 for (j = w; j <= V; j++) |
允许同一物品被重复放 |
六、随堂小测
- 动态规划的两个重要性质是最优子结构和( )A. 重叠子问题 B. 分治 C. 贪心 D. 回溯
- 爬楼梯问题,递归解法的复杂度约为( )A. O(n) B. O(n^2) C. O(2^n) D. O(log n)
- 0/1 背包用一维数组时,容量应( )A. 正序 B. 倒序 C. 都行 D. 随机
- 判断:贪心算法一定能得到全局最优解。( )
- 判断:分治算法的子问题通常是重叠的。( )
答案
- A
- C
- B
- 错误(贪心只在满足贪心选择性质时才最优)
- 错误(分治的子问题不重叠,重叠是 DP 的特征)
附录 A 考前数字速查表(建议单独打印)
2 的幂(背到 2^10)
| n | 2^n | 2^n-1(满二叉树节点数) |
|---|---|---|
| 1 | 2 | 1 |
| 2 | 4 | 3 |
| 3 | 8 | 7 |
| 4 | 16 | 15 |
| 5 | 32 | 31 |
| 6 | 64 | 63 |
| 7 | 128 | 127 |
| 8 | 256 | 255 |
| 9 | 512 | 511 |
| 10 | 1024 | 1023 |
树的核心公式
| 场景 | 公式 |
|---|---|
| 第 i 层最多节点 | 2^(i-1) |
| 深度 h 满二叉树总节点 | 2^h - 1 |
| 叶子 vs 度 2 节点 | n0 = n2 + 1 |
| n 节点完全二叉树深度 | floor(log2 n) + 1 |
| 完全二叉树编号 i 的孩子 | 左 2i,右 2i+1,父 floor(i/2) |
| n 叶子的哈夫曼树总节点 | 2n-1,合并 n-1 次 |
| 哈夫曼 WPL | = 所有合并产生的新节点权值之和 |
循环队列四句话
队空: front == rear
队满: (rear + 1) % capacity == front
入队: arr[rear] = x; rear = (rear + 1) % capacity
出队: x = arr[front]; front = (front + 1) % capacity
个数: (rear - front + capacity) % capacity
遍历口诀
先序:根 左 右 中序:左 根 右 后序:左 右 根 层序:逐层从左到右
复杂度速查
| 算法 | 平均 | 最坏 |
|---|---|---|
| 快速排序 | O(n log n) | O(n^2) |
| 归并排序 | O(n log n) | O(n log n) |
| 埃氏筛 | O(n log log n) | — |
| 线性筛(欧拉筛) | O(n) | — |
| 二叉排序树查找 | O(log n) | O(n)(退化成链) |
| DFS / BFS | O(n) | O(n) |
| 递归爬楼梯 | — | O(2^n) |
附录 B 易错判断题 30 条(答案附后)
先盖住答案自测,错的题号圈起来。
面向对象
- 面向对象三大特性是封装、继承、多态。( )
- 创建对象时会自动调用构造函数。( )
- 一个类必须手动定义析构函数。( )
- 静态数据成员属于整个类,被所有对象共享。( )
- 静态数据成员可以在类内直接初始化。( )
- 析构函数可以带参数。( )
- C++ 中类内部可以嵌套定义类。( )
- 父类指针指向子类对象时,非虚函数按指针类型调用。( )
栈与队列
- 栈是先进先出。( )
- 队列是先进先出。( )
- 栈常用于括号匹配和撤销操作。( )
- 循环队列队满条件是
rear == front。( ) - 循环队列通常牺牲一个存储单元来区分队空和队满。( )
stack容器可以用front()访问栈顶。( )- 用栈做进制转换时,应该先读
top()再pop()。( )
树
- 二叉树第 i 层最多有 2^(i-1) 个节点。( )
- 深度为 5 的满二叉树有 32 个节点。( )
- 二叉树中叶子数等于度为 2 的节点数加 1。( )
- 先序遍历第一个访问的是根节点。( )
- 中序遍历中根节点第一个被访问。( )
- 已知先序和后序遍历就能唯一确定一棵二叉树。( )
- 满二叉树一定是完全二叉树。( )
- 完全二叉树一定是满二叉树。( )
- 哈夫曼树中可能存在度为 1 的节点。( )
- 二叉排序树的中序遍历结果是递增序列。( )
编码与算法
- 哈夫曼编码是一种变长前缀码。( )
- 哈夫曼编码本质上使用了贪心策略。( )
- 格雷码中相邻两个编码只有一位不同。( )
- 动态规划的两个必要条件是最优子结构和重叠子问题。( )
- 递归代码短,所以执行效率一定比动态规划高。( )
答案与速查
| 题 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|
| 答案 | ✓ | ✓ | ✗ | ✓ | ✗ | ✗ | ✓ | ✓ |
| 题 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
|---|---|---|---|---|---|---|---|
| 答案 | ✗ | ✓ | ✓ | ✗ | ✓ | ✗ | ✓ |
| 题 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
|---|---|---|---|---|---|---|---|---|---|---|
| 答案 | ✓ | ✗ | ✓ | ✓ | ✗ | ✗ | ✓ | ✗ | ✗ | ✓ |
| 题 | 26 | 27 | 28 | 29 | 30 |
|---|---|---|---|---|---|
| 答案 | ✓ | ✓ | ✓ | ✓ | ✗ |
容易记反的 7 条(务必再看一遍):
- 3 ✗:析构函数不必手写,编译器会生成默认的
- 5 ✗:静态成员必须在类外定义初始化(C++17 的
inline static除外,考试按传统规则) - 12 ✗:
rear == front是队空,队满要(rear+1)%N == front - 14 ✗:
stack用top(),front()是queue的 - 17 ✗:5 层满二叉树是 31,不是 32
- 21 ✗:必须已知中序
- 30 ✗:递归效率低(O(2^n))
附录 C 客观题答题技巧
-
概念题先排除"张冠李戴"项。六级的干扰项大量来自其他级别或相邻知识点(如把"分治/贪心"混进面向对象特性)。看到"贪心、动态规划、回溯"出现在问"面向对象"的题里,直接排除。
-
计算题一律动手推,不靠直觉。循环队列的 front/rear、哈夫曼 WPL、遍历序列——在草稿纸上画表,3 步以内几乎不会错。
-
代码输出题抓"关键一行"。
- 看有没有
virtual→ 决定多态行为 - 看循环方向(正序/倒序)→ 决定 0/1 还是完全背包
- 看
top()和pop()的先后 → 决定输出是否错位 - 看
push左右孩子的顺序 → 决定 DFS 输出序列
- 看有没有
-
"下列说法错误的是"——先找四个里最绝对的那个。含"一定""必然""都"的选项,错误概率高。
-
判断题注意"必须""一定""只能"。这类绝对化表述,多半是错的(如第 3 题"必须手动定义析构函数")。
-
50 分客观题的目标:及格线 60 分只需客观题拿 35 + 编程题拿 25。客观题是六级最稳的得分来源,性价比远高于死磕编程题满分。

浙公网安备 33010602011771号