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) 最高频
素数筛 / 数论 偶发 偶发

结论:

  1. 面向对象 + 栈队列 = 客观题性价比最高的两块,概念背熟就能拿分,且不需要写代码能力。
  2. 动态规划在客观题里只考"是什么",不考"怎么写"——背住两个性质和与递归/贪心/分治的区别即可。
  3. 编程题的 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;   // ⭐ 静态成员必须在类"外"定义并初始化

⭐ 必背结论

  1. 创建对象时自动调用构造函数;没有手写构造函数时,编译器会自动生成一个默认构造函数。
  2. 析构函数不是必须手写的——不写,编译器也会生成默认的。
  3. 静态数据成员属于类,被所有对象共享,必须在类外单独初始化。
  4. 构造函数与类名同名,没有返回值类型(连 void 都不写)。
  5. 析构函数名 = ~ + 类名,同样无返回值,且不能带参数。

三、🔴 虚函数与运行时多态(最难,也是必考)

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++ 中类的内部可以嵌套定义类 正确 类内可以再定义类(嵌套类)

五、随堂小测

  1. 下列不是面向对象三大特性的是( )
    A. 封装 B. 继承 C. 多态 D. 递归
  2. 关于静态数据成员,正确的是( )
    A. 每个对象各存一份 B. 必须在类外定义初始化 C. 只能用对象访问 D. 必须在构造函数里赋值
  3. 父类指针指向子类对象,函数未声明为 virtual,调用结果是( )
    A. 子类版本 B. 父类版本 C. 编译报错 D. 运行报错
  4. 判断:析构函数可以带参数。( )
  5. 判断:class A { public: class B {}; }; 这种写法在 C++ 中合法。( )
答案
  1. D(递归是算法思想)
  2. B(A 错:全类共享一份;C 错:可用 类名::成员 访问;D 错:类外初始化)
  3. B(静态绑定,按指针类型调用父类的版本)
  4. 错误(析构函数不能带参数,也不能重载)
  5. 正确(嵌套类是合法的)

第 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

🔴 三个必错点

  1. 队满不能写成 rear == front(那是队空)。
  2. 队满不能写成 (rear + 1) == front(漏了取模,绕圈时会错)。
  3. 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 被撤销掉了)。

七、随堂小测

  1. 栈的特点是( )A. 先进先出 B. 后进先出 C. 随机 D. 按大小
  2. 链式栈栈顶为 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;
  3. 循环队列容量 N,队满条件是( )
    A. rear == front B. (rear+1)%N == front C. rear%N == front D. (rear+1) == front
  4. 判断:用栈实现十进制转二进制时,应该先 pop() 再输出 top()。( )
  5. 判断:stack 容器可以用 front() 访问栈顶。( )
答案
  1. B
  2. B
  3. B
  4. 错误(先 top() 输出,再 pop())
  5. 错误(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 题答案)

  • 中序:根在中间被访问
  • 后序:根在最后被访问

记忆法:看"根"字在口诀里的位置——"根左右"→ 先序;"左根右"→ 中序;"左右根"→ 后序。

四、已知两种遍历求第三种(🔴 难点)

方法三步:

  1. 定根:先序的第一个是根;后序的最后一个是根。
  2. 分段:在中序里找到这个根,根左边 = 左子树,根右边 = 右子树。
  3. 递归:对左右两段重复上面两步。

✏️ 例题:中序 BADC,后序 BDCA,求先序。

  • 后序最后一位 A → 根是 A
  • 中序中 A 左边是 B(左子树),右边是 DC(右子树)
  • 后序中对应左子树段是 B,右子树段是 DC
  • 右子树段 DC:后序最后一位 C 是根;中序 DC 中 C 左边是 D
  • 所以:A 的左孩子 B,右孩子 C,C 的左孩子 D
  • 先序 = ABCD

⭐ 结论:必须已知"中序"才能唯一确定一棵树。只有先序+后序不行(除非是满二叉树等特殊情形)。

五、随堂小测

  1. 深度为 4 的满二叉树共有( )个节点。A. 15 B. 16 C. 31 D. 8
  2. 二叉树中叶子数为 8,则度为 2 的节点数为( )A. 7 B. 8 C. 9 D. 无法确定
  3. 后序遍历中,根节点被访问的顺序是( )A. 第一个 B. 中间 C. 最后一个 D. 不一定
  4. 判断:已知先序和后序遍历,一定能唯一确定一棵二叉树。( )
  5. 判断:一棵有 100 个节点的完全二叉树,其深度为 7。( )
答案
  1. A(2^4-1=15)
  2. A(n0=n2+1 → n2=7)
  3. C
  4. 错误(必须已知中序;先序+后序不唯一)
  5. 正确(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)

五、随堂小测

  1. 权值集合 {5, 1, 6, 2} 构造哈夫曼树,WPL 为( )A. 25 B. 26 C. 28 D. 30
  2. 有 8 个叶子的哈夫曼树,总节点数为( )A. 15 B. 16 C. 17 D. 无法确定
  3. 判断:哈夫曼树中可能存在度为 1 的节点。( )
  4. 判断:满二叉树一定是完全二叉树。( )
  5. 二叉排序树的中序遍历结果是( )A. 递增 B. 递减 C. 无序 D. 先增后减
答案
  1. A — 合并:1+2=3,3+5=8,8+6=14 → WPL = 3+8+14 = 25
  2. A — 2n-1 = 15
  3. 错误(哈夫曼树没有度为 1 的节点)
  4. 正确
  5. A

第 5 讲 模块三:哈夫曼编码与格雷码

考纲原文

(3)理解哈夫曼编码、格雷编码相关原理并能进行简单应用。

一、哈夫曼编码

是什么:一种变长编码——出现频率高的字符用短编码,频率低的用长编码,使总长度最短。

怎么来:对每个字符建哈夫曼树,从根出发,走左边记 0、走右边记 1,到达叶子的路径就是该字符的编码。

⭐ 两个关键性质

  1. 前缀码性质:任何一个字符的编码,都不是另一个字符编码的前缀。
    → 所以解码时不会产生歧义,可以"边读边解"。
  2. 总编码长度 = 哈夫曼树的 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。

三、随堂小测

  1. 哈夫曼编码属于( )A. 定长编码 B. 变长前缀码 C. 非前缀码 D. 加密编码
  2. 哈夫曼编码的总长度等于哈夫曼树的( )A. 节点数 B. 深度 C. WPL D. 叶子数
  3. 十进制 6 的 4 位格雷码是( )A. 0110 B. 0101 C. 1010 D. 0011
  4. 判断:格雷码中相邻两个编码只有一位不同。( )
  5. 判断:哈夫曼编码中,出现频率越高的字符,编码长度越短。( )
答案
  1. B
  2. C
  3. B — 6 = 0110₂,6>>1 = 0011₂,0110 XOR 0011 = 0101
  4. 正确
  5. 正确(频率高 → 离根近 → 路径短)

第 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 跳过父亲即可)。

六、随堂小测

  1. BFS 使用的数据结构是( )A. 栈 B. 队列 C. 优先队列 D. 数组
  2. 用栈实现的非递归 DFS,若"先压右后压左",输出顺序等价于( )A. 先序 B. 中序 C. 后序 D. 层序
  3. 无权图中求两点最短路径,应选( )A. DFS B. BFS C. 两者一样 D. 都不行
  4. 判断:DFS 可以用递归实现,递归的本质就是系统调用栈。( )
  5. 判断:BFS 找到目标时,可以保证是层数最浅的那个。( )
答案
  1. B
  2. A
  3. B
  4. 正确
  5. 正确(BFS 按层推进,第一次遇到即最浅)

第 7 讲 模块五:简单动态规划

考纲原文

(5)掌握简单动态规划的算法思想,能够使用代码解决相应的一维动态规划问题和简单背包问题。

一、⭐ 动态规划的两大性质(2024.06 第 11 题,答案:重叠子问题)

性质 含义
最优子结构 大问题的最优解,可以由小问题的最优解拼出来
重叠子问题 计算过程中,同一个小问题被反复计算多次

🔴 正是因为有"重叠子问题",才需要把答案存下来(记忆化 / DP 数组)。
干扰项:分治法(子问题不重叠,如归并排序)、贪心策略、回溯算法。

二、DP 四步法

  1. 定义状态:dp[i] 表示什么?(这一步最关键)
  2. 写转移方程:dp[i] 怎么由前面的推出来?
  3. 定边界:dp[0]、dp[1] 是多少?
  4. 定遍历方向:从前往后还是从后往前?

三、🔴 递归 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++) 允许同一物品被重复放

六、随堂小测

  1. 动态规划的两个重要性质是最优子结构和( )A. 重叠子问题 B. 分治 C. 贪心 D. 回溯
  2. 爬楼梯问题,递归解法的复杂度约为( )A. O(n) B. O(n^2) C. O(2^n) D. O(log n)
  3. 0/1 背包用一维数组时,容量应( )A. 正序 B. 倒序 C. 都行 D. 随机
  4. 判断:贪心算法一定能得到全局最优解。( )
  5. 判断:分治算法的子问题通常是重叠的。( )
答案
  1. A
  2. C
  3. B
  4. 错误(贪心只在满足贪心选择性质时才最优)
  5. 错误(分治的子问题不重叠,重叠是 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 条(答案附后)

先盖住答案自测,错的题号圈起来。

面向对象

  1. 面向对象三大特性是封装、继承、多态。( )
  2. 创建对象时会自动调用构造函数。( )
  3. 一个类必须手动定义析构函数。( )
  4. 静态数据成员属于整个类,被所有对象共享。( )
  5. 静态数据成员可以在类内直接初始化。( )
  6. 析构函数可以带参数。( )
  7. C++ 中类内部可以嵌套定义类。( )
  8. 父类指针指向子类对象时,非虚函数按指针类型调用。( )

栈与队列

  1. 栈是先进先出。( )
  2. 队列是先进先出。( )
  3. 栈常用于括号匹配和撤销操作。( )
  4. 循环队列队满条件是 rear == front。( )
  5. 循环队列通常牺牲一个存储单元来区分队空和队满。( )
  6. stack 容器可以用 front() 访问栈顶。( )
  7. 用栈做进制转换时,应该先读 top() 再 pop()。( )

树

  1. 二叉树第 i 层最多有 2^(i-1) 个节点。( )
  2. 深度为 5 的满二叉树有 32 个节点。( )
  3. 二叉树中叶子数等于度为 2 的节点数加 1。( )
  4. 先序遍历第一个访问的是根节点。( )
  5. 中序遍历中根节点第一个被访问。( )
  6. 已知先序和后序遍历就能唯一确定一棵二叉树。( )
  7. 满二叉树一定是完全二叉树。( )
  8. 完全二叉树一定是满二叉树。( )
  9. 哈夫曼树中可能存在度为 1 的节点。( )
  10. 二叉排序树的中序遍历结果是递增序列。( )

编码与算法

  1. 哈夫曼编码是一种变长前缀码。( )
  2. 哈夫曼编码本质上使用了贪心策略。( )
  3. 格雷码中相邻两个编码只有一位不同。( )
  4. 动态规划的两个必要条件是最优子结构和重叠子问题。( )
  5. 递归代码短,所以执行效率一定比动态规划高。( )
答案与速查
题 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 客观题答题技巧

  1. 概念题先排除"张冠李戴"项。六级的干扰项大量来自其他级别或相邻知识点(如把"分治/贪心"混进面向对象特性)。看到"贪心、动态规划、回溯"出现在问"面向对象"的题里,直接排除。

  2. 计算题一律动手推,不靠直觉。循环队列的 front/rear、哈夫曼 WPL、遍历序列——在草稿纸上画表,3 步以内几乎不会错。

  3. 代码输出题抓"关键一行"。

    • 看有没有 virtual → 决定多态行为
    • 看循环方向(正序/倒序)→ 决定 0/1 还是完全背包
    • 看 top() 和 pop() 的先后 → 决定输出是否错位
    • 看 push 左右孩子的顺序 → 决定 DFS 输出序列
  4. "下列说法错误的是"——先找四个里最绝对的那个。含"一定""必然""都"的选项,错误概率高。

  5. 判断题注意"必须""一定""只能"。这类绝对化表述,多半是错的(如第 3 题"必须手动定义析构函数")。

  6. 50 分客观题的目标:及格线 60 分只需客观题拿 35 + 编程题拿 25。客观题是六级最稳的得分来源,性价比远高于死磕编程题满分。


posted @ 2026-10-02 15:50  kkman2000  阅读(34)  评论(0)    收藏  举报