很多准备GESP四级的朋友,上来第一句就问:“四级到底考什么?是不是把三级语法学完就行?” 我的回答通常比较直接:三级看的是你会不会写C++,四级看的是你会不会用C++解决问题。GESP四级正好卡在“语法熟练”到“算法入门”的分水岭上,过了这道坎,后面五级、六级学栈、队列、树、图才会顺,过不去,刷再多题也是原地踏步。这篇文章我会从四级考纲出发,把排序、结构体、递归、STL、二分、贪心这些高频考点逐个拆开,讲清原理、配上代码、列出避坑清单,再给出一条可以照做的备考路线。不管你是零基础冲刺、还是已经在机构学过一阵子,这篇内容都值得你花半小时认真看完。
1. GESP四级到底考什么:考纲拆解与学习重点
1.1 四级的知识边界:从“语法”到“算法”的真正跨越
如果给GESP各级别画一条能力线,一级到三级基本都在解决“语言问题”:变量、分支、循环、数组、字符串、函数。到了四级,考点开始明显向“算法思维”倾斜,重点集中在几个板块:
- 排序算法:选择排序、冒泡排序、插入排序,以及计数排序。
- 结构体与简单类:结构体定义、数组存储、函数传参,初步接触封装思想。
- 递归与递推:理解递归执行过程,能写经典递归函数;会用递推公式解决简单问题。
- STL基础:重点掌握vector和sort,理解迭代器的基本用法。
- 二分查找:在有序数组中查找目标,理解二分答案的雏形。
- 贪心算法:能用局部最优策略解决简单的区间问题、分配问题。
换句话说,四级不是“更难的语法”,而是“第一批真正能改变程序效率的思路”。同样一道排序题,你可以用冒泡,也可以调用sort,还可以遇到特定数据用计数排序。考的就是你能不能根据数据范围选对方法。
1.2 题型结构与命题趋势:选择、判断、编程三块怎么分配精力
GESP四级试卷由选择题、判断题、编程题三部分组成。选择题和判断题主要覆盖概念细节,比如排序的稳定性、递归的终止条件、结构体成员访问方式、二分查找的边界条件。这些题目平时刷题不容易碰到,必须有意识地背诵和辨析概念。
编程题通常是2到3道,风格非常稳定:一道偏模拟或字符串处理,一道偏排序或结构体应用,一道偏贪心或二分查找。从近年考题看,纯考语法的题目越来越少,取而代之的是把语法藏进场景里,比如用结构体管理学生成绩、用二分查找优化查询次数、用贪心安排活动时间表。所以备考时不要只盯着“能不能编译通过”,更要训练“读完题能快速识别考点”的能力。
1.3 与三级、五级的衔接:为什么四级是关键分水岭
不少同学三级轻松高分,四级却考了两三次才过,原因就是学习方式没切换。三级阶段的作业是“照着模板写代码”,四级阶段的题目则要求你先抽象出数据模型,再设计算法,最后才是写代码。
举一个很实在的例子:同样处理“求n个数中出现次数最多的数”。三级思路是开计数数组硬统计;四级就得多考虑一步——如果数很大、范围很广,计数数组根本开不下怎么办?这时候要么用排序后相邻相等元素统计,要么用结构体加自定义排序解决。这就是四级真正的难点。它不考偏题怪题,却会把简单问题包装成需要你“多想一层”的样子。
2. 排序算法专项:选择、冒泡、插入、计数一次讲透
2.1 三种O(n²)排序的手写能力:必须形成肌肉记忆
GESP四级对排序的要求不是“会用sort就行”,而是要求你能手写选择排序、冒泡排序和插入排序,并且能说清它们的区别。原因很简单:sort内部是优化过的快速排序,虽然快,但考察排序的原理时还是得回到最朴素的实现。
拿冒泡排序举例,最容易被忽视的是内层循环的边界条件。标准写法是这样的:
void bubbleSort(int a[], int n) { for (int i = 0; i < n - 1; i++) { bool swapped = false; for (int j = 0; j < n - i - 1; j++) { if (a[j] > a[j + 1]) { swap(a[j], a[j + 1]); swapped = true; } } if (!swapped) break; // 本轮无交换,说明已经有序 } }这里的swapped标记是一个经典的优化点,也是考卷上喜欢挖坑的地方。如果数据本来就有序,加上这个标记后冒泡排序的最好时间复杂度能降到O(n)。我见过很多学生在笔试题里问“冒泡排序最好情况复杂度”,答案不是O(n²)而是O(n),就是靠这个优化实现的。选择排序和插入排序同样有各自的特征:选择排序不稳定,适合数据量小且不要求稳定性的场景;插入排序稳定,像整理扑克牌一样,在近乎有序的数据上表现奇佳。
2.2 计数排序:唯一一个O(n)但容易写错的排序
计数排序是四级考纲里的“偏门武器”。它不属于比较排序,时间复杂度是O(n+k),k是数据范围。很多学生第一次看到它都觉得神奇,但写的时候又开始乱套。计数排序的核心思想就一句话:用数组下标代表原数组的值,数组里存的是这个值出现的次数。
void countingSort(vector<int>& nums) { if (nums.empty()) return; int maxVal = *max_element(nums.begin(), nums.end()); vector<int> cnt(maxVal + 1, 0); for (int x : nums) cnt[x]++; int idx = 0; for (int i = 0; i <= maxVal; i++) { while (cnt[i] > 0) { nums[idx++] = i; cnt[i]--; } } }需要注意,计数排序只适合数据范围不大的场景,比如成绩、年龄、身高这类有限区间的数据。如果数据范围有负数或者特别大,基础版就行不通了。我建议你在本子上写三遍这个模板,考试时能直接默写,因为编程题如果用上它,效率优势非常明显。
2.3 用sort函数:STL的正确打开方式
四级考试允许使用STL,所以std::sort一定要熟练。至少要掌握三种用法:排序整型数组、排序vector、对结构体按指定规则排序。结构体排序是编程题的高频考法,代码通常长这样:
struct Student { string name; int score; int id; }; bool cmp(Student a, Student b) { if (a.score != b.score) return a.score > b.score; // 分数高的在前 return a.id < b.id; // 分数相同按学号小的在前 } sort(stu, stu + n, cmp);这里面有个初学者易错的坎:比较函数cmp必须满足“严格弱序”,也就是返回值必须表示“a是否应该排在b前面”,不能出现a和b互为true的情况。比如比较分数时,如果你写的是return a.score >= b.score,当两个分数相等时,a>=b和b>=a都成立,sort行为就变成未定义了,程序可能直接崩溃。这种错误在本地编译时不报错,在评测系统里却会让你莫名超时或答案错误,排查起来特别费劲。
2.4 一张表记住四种排序:稳定性和复杂度对照
| 排序方法 | 平均时间复杂度 | 最好情况 | 最坏情况 | 是否稳定 | 适用场景 |
|---|---|---|---|---|---|
| 选择排序 | O(n²) | O(n²) | O(n²) | 不稳定 | 简单但慢 |
| 冒泡排序 | O(n²) | O(n) | O(n²) | 稳定 | 近乎有序数据 |
| 插入排序 | O(n²) | O(n) | O(n²) | 稳定 | 数据量小 |
| 计数排序 | O(n+k) | O(n+k) | O(n+k) | 稳定 | 数据范围小 |
选择排序为什么不稳定?因为它会把后面的小元素直接交换到前面,可能打乱相同元素的相对顺序。这个知识点在选择题里反复出现,记忆方式就是“选择=交换=不稳定”。
3. 函数、递归与递推:打开思路的第一步
3.1 递归三要素与执行过程:从栈的角度理解
递归是GESP四级里很多同学觉得玄学的部分。其实递归的底层就是函数调用栈:每调用一次函数,系统就把当前状态压栈;函数返回时,再从栈里恢复现场。理解这一点后,很多问题都会迎刃而解。
写递归只需要盯住三件事:
- 终止条件:什么时候不再调用自己,直接返回结果。
- 递归关系:当前结果如何由更小规模的问题推导出来。
- 递归参数:每一层调用,参数要朝着“更小”的方向变化。
以经典斐波那契为例:
int fib(int n) { if (n <= 1) return n; // 终止条件 return fib(n - 1) + fib(n - 2); // 递归关系 }这里必须提醒:朴素递归的斐波那契在n稍微大一点时,会指数级膨胀。fib(40)就已经需要跑好几秒,fib(50)基本跑不完了。四级考试如果需要用斐波那契,通常不会让你直接递归算,而是让你用递推数组,或者做记忆化处理。递归能帮你抓思路,但绝不能不管效率直接提交。
3.2 从递归到递推:循环数组替代函数栈
递推和递归的区别用一句话说就是:递归是从上往下拆问题,递推是从下往上推答案。斐波那契用递推写,就是用一个数组滚动保存前两项:
long long f[55]; f[0] = 0; f[1] = 1; for (int i = 2; i <= n; i++) { f[i] = f[i - 1] + f[i - 2]; }这样做的好处是每个子问题只算一次,时间复杂度O(n),而且不存在爆栈风险。类似的,判断质数时也有人喜欢写递归试除,但其实用循环更直接:试除到sqrt(n)就可以停了,在判断质数优化时,常用的模板是for (int i = 2; i * i <= n; i++),注意用i*i时防止int溢出,改用i <= n / i更安全。这些细节都可能在四级选择题里出现。
3.3 经典递归问题速览:汉诺塔、全排列、倒序输出
四级阶段至少要看懂三个经典递归程序:汉诺塔、全排列、倒序输出字符串。汉诺塔的代码非常短,但理解它需要盯住“借助第三个柱子的过程”:
void hanoi(int n, char from, char mid, char to) { if (n == 1) { cout << from << " -> " << to << endl; return; } hanoi(n - 1, from, to, mid); cout << from << " -> " << to << endl; hanoi(n - 1, mid, from, to); }建议你把n=3代入,在纸上画一遍递归树,把所有过程补齐。这一步做完,你的递归就真正通了。全排列问题则更接近四级编程题风格,用递归加回溯,可以输出一个数组的所有排列,这类题本质上是为五级的深度优先搜索打基础。
4. 结构体与类:从面向过程到面向对象的关键跨越
4.1 struct与函数传参:为什么一定要用引用
结构体在四级中的定位是“组织复杂数据”。考纲不要求你把面向对象学得多深,但要会用结构体把相关联的数据捆绑在一起。比如学生信息包含姓名、语文成绩、数学成绩、总成绩,用结构体存储后,一次sort就能按任意维度排序,代码可读性也瞬间提升。
struct传给函数时,默认是值传递,也就是复制一份完整的结构体。如果结构体里成员较多,频繁复制会拖慢程序。所以函数参数里建议写成const Student &s这种形式,既避免拷贝,又防止误改:
void printStu(const Student& s) { cout << s.name << " " << s.score << endl; }很多学生一开始不习惯引用符号,但这是C++相对C语言的一个实用性改进,也是四级常考的概念点,务必掌握。
4.2 class封装与三个最容易犯的错
四级对类的考察比较基础,通常只要求“知道类是什么、能定义简单类”。比如定义一个口算题目的类,包含题目字符串、答案和难度等级。但有几个坑几乎年年有人踩:
- 成员默认是私有的。struct的成员默认public,class的成员默认private,刚切换时经常忘记给数据成员加public,结果主函数里stu.name直接编译失败。
- 构造函数与类同名,没有返回值。写了构造函数后,它会在对象创建时自动执行,适合初始化成员。
- this指针的含义。在成员函数里用this->score来访问成员,可以避免参数名和成员名冲突。
一个简单的类定义可以写成:
class Student { public: Student(string n, int s) : name(n), score(s) {} void print() { cout << name << " " << score << endl; } private: string name; int score; };考试中如果选择题问“private成员能否在外部访问”,答案是否定的。这个“访问权限”概念是四级和三级最大的区别之一,多花几分钟吃透很值得。
4.3 结构体数组与sort搭配:编程题经典套路
模拟考试成绩排名的题目,几乎每年都会以不同马甲出现。套路是固定的:定义结构体、读入数据、写比较函数、sort排序、按格式输出。这里要额外注意并列排名的处理,如果题目要求“分数相同则按学号升序”,那比较函数里一定要写两行判断,而不是只比一个字段。平时都练熟了,考试时就能直接当“默写题”做,把时间留给后面更难的题目。
5. STL实用基础:vector与sort让代码量骤减
5.1 vector:动态数组的使用要点
四级允许使用STL后,vector就成了最常用的容器。它比普通数组灵活的地方在于长度可变,push_back可以动态追加元素。基本用法如下:
vector<int> v; v.push_back(5); v.push_back(3); sort(v.begin(), v.end()); int size = v.size(); for (int i = 0; i < v.size(); i++) { cout << v[i] << " "; }这里有一个经常在选择题里挖坑的点:v.size()返回的是size_t类型,是无符号整数。如果你写for (int i = v.size() - 1; i >= 0; i--),当v为空时,v.size() - 1会变成巨大的正数,循环直接崩溃。安全写法是用for (int i = (int)v.size() - 1; i >= 0; i--),或者干脆用下标逆序遍历时格外小心。这些小坑,笔试时就是送命题。
5.2 string与字符处理的常见操作
四级编程题很大比例会涉及字符串处理。C++的string类封装好了各种常用功能,必须烂熟于心的有:
- 取子串substr(pos, len)
- 查找find,返回子串第一次出现位置,找不到返回string::npos
- 长度length()
- 字符拼接直接用+号
string s = "GESP2025"; string sub = s.substr(4, 4); // "2025" if (s.find("2025") != string::npos) { cout << "found" << endl; }还经常遇到“字符串数组初始化”的题目,vector 可以直接用花括号初始化:vector names = {"Alice", "Bob", "Cindy"};。另外,getline(cin, s)能读入包含空格的整行,而cin >> s只能读到空格为止。读入混合数据时这是最大的坑,我见过无数学生用cin >> s读带空格的名字,结果输出全乱了,所以在读整行前如果有数字输入,记得先加一个cin.ignore()把缓冲区里的换行符清掉。
5.3 四级阶段还需要认识的其他容器吗
map、set、stack、queue这些容器严格来说在四级不是重点,但有些考题会以“阅读理解”的形式出现,比如给一段使用map的代码让你推结果。我的建议是:至少能看懂map是什么——它存储键值对,key唯一,内部自动排序;set存储不重复元素,也有序。不要求你写得熟练,但不能完全看不懂。如果你学有余力,可以提前把stack和queue的常用操作过一遍,这样备考五级时会轻松非常多。
6. 二分查找与贪心思维:四级最“提分”的两把刀
6.1 二分查找的边界问题:死循环的根源
二分查找本身思想不难,难的是写对边界。GESP选择题特别喜欢考“以下哪个是正确写法”,答案差别往往就在一个等号或者一处+1/-1上。我的建议是记住一套“左闭右开”模板,考试时直接套:
int l = 0, r = n; // 查找区间 [l, r) while (l < r) { int mid = (l + r) / 2; if (a[mid] >= target) r = mid; else l = mid + 1; } // 结束后l就是第一个 >= target 的位置补充:这种写法相当于C++里的lower_bound。如果题目要求找大于等于目标值的第一个位置,这个模板直接输出l即可;如果要求找大于目标值的位置,可以把条件改成a[mid] > target。写二分时把人脑“想当然”的细节交给模板,能减少大量隐蔽错误。编程题若明确提示数据有序,或需要频繁查找,就应该条件反射地去想二分。
6.2 贪心算法:局部最优如何凑成全局最优
贪心是四级算法题里区分度最高的部分。它的核心思想是每一步都做当前看起来最好的选择,最终得到全局最优解。但贪心不是“碰运气”,它需要严格证明。四级阶段最常考的贪心场景有:活动安排(按结束时间排序)、接水问题(按时间短者优先)、分糖果问题(先排序再分配)。
活动安排问题的代码思路极简:
struct Act { int s, e; }; bool cmp(Act a, Act b) { return a.e < b.e; // 按结束时间排序 } sort(acts, acts + n, cmp); int ans = 0, lastEnd = 0; for (int i = 0; i < n; i++) { if (acts[i].s >= lastEnd) { ans++; lastEnd = acts[i].e; } }为什么按结束时间排序是对的?因为结束越早,越能给后面的活动留出更多时间。这个直觉看起来简单,但在考场上,不少学生会错选成“按开始时间排序”。备考时多问自己一句“它的反例在哪”,就能避开一类错误。
6.3 贪心的反例意识:不能“看起来对”就写
平时刷题时,遇到贪心题目一定要动手验证小数据。比如“硬币找零问题”在有些硬币面额下贪心不成立,需要动态规划来解决,四级不考动态规划,但会通过选择题让你判断“贪心一定能得到最优解吗”。这种题考察的就是你对贪心局限性的认识。我的经验是:做题前先用暴力枚举跑一个小数据,如果贪心结果和暴力结果一致,再提交贪心代码,这个习惯能让你少交很多次罚时。
7. 备考路线与常见失分点排查
7.1 开发环境选择:Dev-C++还是VSCode
环境对考试不直接加分,但会影响平时练习效率。Dev-C++安装简单,考场上模拟体验真实;VSCode功能更强,适合长期学习,但配置C/C++环境需要花心思,包括安装编译器、配置tasks.json和launch.json。四级阶段我的建议是:用什么顺手就用什么,不要为了“高级”而频繁换环境,真正重要的是把编译运行快捷键练到肌肉记忆。考试前务必用官方模拟系统练一次,熟悉提交和评测流程,很多考生不是不会写,而是不知道输出格式有多严格——多了个空格都可能是零分。
7.2 三轮复习法:基础、真题、错题怎么安排
如果距离考试还有两个月左右,可以参考下面这个节奏:
- 第一轮(约3周):梳理考纲知识点,排序算法手写三遍左右,结构体和STL每天练两个小题,递归递推至少做掉10道经典题。
- 第二轮(约2周):刷近年真题和模拟题,每道编程题写完必须提交评测,不满足于“本地能跑”。
- 第三轮(约1周):整理错题本,把选择题中的概念错题集中背诵,编程题按题型归类重做。
7.3 编程题常见失分点速查表
| 问题类型 | 具体表现 | 解决办法 |
|---|---|---|
| 数组越界 | 开了a[100]却访问a[100] | 多开10个空间,养成习惯 |
| 变量未初始化 | sum、ans忘记赋0 | 定义时同步初始化 |
| 输入输出格式 | 多空格、少换行、输出多余字符 | 按样例严格比对 |
| 结构体比较错误 | 比较函数写了>=号 | 只写严格小于/大于 |
| 二分死循环 | mid更新条件不对 | 套模板,不要临时发挥 |
| 递归爆栈 | 递归层数过深 | 改递推或改成循环 |
| 忘记cin.ignore | getline前残留换行符 | 读入数字后主动清空缓冲 |
7.4 选择题判断题的背诵清单
最后再整理一份考前背诵清单:四种排序的稳定性与时间空间复杂度;递归必须有终止条件,否则栈溢出;结构体与类的区别,以及访问权限默认值;vector的size()是无符号类型;string的find找不到时返回npos;二分查找要求数据有序;贪心算法需要证明局部最优能推出全局最优。每个考点用一句话总结,考前过一遍,选择题拿分就会稳很多。
根据我带学生的实际经验,能一次通过四级的人往往不是刷题最多的,而是每次错题后都能把原因抽象成一句话的那类同学。比如“凡是看到已经有序的数据,就要想到插入排序的O(n)”,“凡是遇到自定义排序,先检查比较器是否符合严格弱序”。备考四级时,你有多少次“原来这里写错了”的顿悟,考试时就有多大把握。这份考点拆解与实操指南,建议你收藏后立刻打开编译器,从第一个排序模板开始,一行一行敲出肌肉记忆。四级的难度不在天上而在脚下,把每个模板吃透、把每类题目的套路写顺,证书自然会来。