GESP七级备考:吃透类、树、图、哈希表四大核心 1. 七级到底考什么先把目标拆明白GESP七级卡住了不少同学。很多人在四级、五级一路顺利考过来到了七级突然觉得吃力原因很简单七级是第一个真正要求“系统掌握数据结构”的级别。类、树、图、哈希表这四个词几乎就是七级考纲的四大支柱。拿我自己带学生备考的经验来说这一级不是靠刷几道题就能蒙混过关的它考察的是你能不能把一个实际问题抽象成正确的数据结构再写出能跑通的代码。理解了这一点复习方向就对了。先说考试本身。GESP全称是“青少年编程能力等级考试”由中国计算机学会主办七级对应的是“熟练掌握一门编程语言并可运用数据结构解决复杂问题”的水平。考试以C为主题型包括单选题、程序阅读题、程序完善题和编程题。前面的客观题部分会直接考察概念判断后面的编程题才是重头戏四道大题里通常会有两道以上涉及树、图或哈希表。如果能把“类、树、图、哈希表”这四个专项吃透七级证书基本上就握在手里了。我见过太多同学栽在同一个地方基础语法都会但一到“设计一个类来解决某个问题”就懵一到“把树的遍历和递归结合起来”就乱。问题不在于题目难而在于没有建立体系。七级的考察特点是“概念实现应用”三层递进不光是让你背定义还要你在规定时间内把它实现出来。所以这篇复习思路我按这四个板块逐一拆开讲每个板块都告诉你重点在哪、坑在哪、应该怎么练。2. 类C面向对象的“地基”2.1 先搞懂类的封装与访问控制七级考试对“类”的要求不是让你写一个能运行的小程序那么简单。它考察的是你对面向对象三大特性——封装、继承、多态——的实际运用能力。先看封装。封装就是把数据成员设为私有通过公有成员函数来访问。很多同学不理解为什么要这么绕直接把所有成员都设为public不就完了考试里确实有人这么写但程序完善题和阅读题经常专门考察访问控制的作用。比如题目会给你一个类私有成员是int score;然后问你外部能不能直接通过obj.score赋值。如果你平时写代码把所有东西都暴露在外面这种题一眼就会踩坑。你需要清楚private成员只能被本类的成员函数和友元访问protected成员能被派生类访问public成员才是对外的接口。七级的阅读题很喜欢在继承的场景下出访问权限的判断题这是第一个容易丢分的地方。再说一个实用技巧写类的时候数据成员全部私有对外暴露的接口用公有函数。这不是教条而是为了日后维护方便。如果有一天你想修改数据的存储方式比如把int改成long long只需要改类内部实现外部调用代码统统不用动。考试不会考你“为什么”但你自己写编程题的时候这种设计习惯能帮你少出bug毕竟程序完善题里也经常出现“成员函数应该设为公有还是私有”的选择。2.2 构造函数、析构函数与拷贝控制构造函数是七级最爱考的点之一尤其是初始化列表。我见过不少同学在构造函数里写this-x x;这没错但效率上有差异。用初始化列表Node(int val) : data(val), next(nullptr) {}成员在进入函数体之前就被初始化了如果放到函数体内赋值对基本类型影响不大但对对象成员就有性能损失。考试不会直接考性能但程序填空题里经常需要你补全初始化列表的写法。拷贝构造函数也不可忽视。七级题目里常见的一个场景是你定义了一个类里面有一个指针成员然后把一个对象复制给另一个对象。如果没写拷贝构造函数编译器会生成默认的浅拷贝两个对象的指针指向同一块内存析构时就会double free。考试可能会给你一段代码让你判断运行结果或指出错误。判断方法就一句话只要类里有裸指针raw pointer你就必须考虑深拷贝或者改用智能指针。析构函数的作用是释放资源。有的同学觉得析构函数不写也没关系反正程序结束操作系统会回收内存。但考试不这么考程序完善题会给你一个类让你补全析构函数来释放动态分配的内存漏了就是编译能过但运行崩。平时练习时一定要养成“看到new就要想到delete看到成员里有指针就要想到析构函数”的反射。2.3 运算符重载与STL联动如果说类的构造函数是基础那运算符重载就是七级的分水岭。为什么重要因为STL里的排序、查找、优先级队列大量依赖运算符重载或者自定义比较函数。比如你定义了一个结构体struct Student { string name; int score; };然后想按分数从高到低排序。如果直接sort(v.begin(), v.end())编译器不知道怎么比较两个Student对象。这时候要么给结构体重载运算符要么写一个自定义比较函数或lambda表达式。代码示例struct Student { string name; int score; // 重载小于号便于sort排序 bool operator(const Student other) const { return score other.score; // 分数高的排前面 } };程序填空题经常会挖掉这里的运算符重载部分让你补全。你需要掌握的不只是语法还有const修饰符。看到成员函数后面带const表示这个函数不会修改对象状态能对常量对象调用。有些同学补全重载时忘写const导致sort内部对常量引用调用比较函数时报错这就是细节丢分。另外operator重载在哈希表场景下也有用因为自定义类型作为unordered_set的键时需要提供哈希函数和相等判断。七级对哈希表的考察经常和类结合出题这部分在下面哈希表章节再细说。2.4 类板块的复习建议复习“类”这一块我建议按三步走先抄一遍基本的类定义模板熟悉构造函数、析构函数、拷贝构造的写法再做十几道简单封装练习比如写一个复数类、写一个分数类实现加减乘除和输出最后做包含“类STL容器”的综合题比如用类存储学生信息用vector管理按需求排序和查找。踩过的坑要记录。我强烈建议备考阶段准备一个错题本不需要多精美但要把每个语法细节错误记录下来。比如“忘写const”、“初始化列表顺序和成员声明顺序不一致导致警告”、“重载时返回值写反”。这些坑在考试中极其常见因为程序阅读题非常喜欢放“有bug但能运行”的代码考察你能否发现逻辑问题。3. 树从递归到迭代一步都不能少3.1 树的存储结构孩子表示法其实够用了树的定义考察的是基础概念树由结点和边组成有根结点除根外每个结点有且仅有一个父结点没有环。这个定义在客观题中会以各种变形出现比如判断无向图是不是树、判断某个结构能否称为二叉树。你需要记住二叉树是每个结点最多有两个子结点的有序树左右子树不能交换。有些题目会故意把二叉树和有左、右之分的“有序树”混在一起稍不留意就会选错。在代码实现层面树的存储主要有三种方式父亲数组、孩子数组、链式存储。很多同学一提到树就想到指针和结点但在算法竞赛和GESP考试里用数组模拟树的场景非常多。链式存储方便直观数组模拟效率高、好调试。例如用vectorint children[N]存储每个结点的所有子结点或者用int parent[N]存父结点。如果只是静态的树数组方式就够用且不容易出错。七级的程序阅读题会给出一棵树然后用邻接表或孩子表示法存储让你根据遍历代码判断输出顺序。如果看不懂存储结构后面读代码全是抓瞎。所以第一步是把存储结构吃透给定一个结点编号你能快速说出它的父结点是谁、子结点有哪些、层数是多少。3.2 二叉树的三种遍历与递归思维二叉树的先序、中序、后序遍历是七级必考内容但这里有个误区很多人只是背“先访问根、再访问左子树、最后访问右子树”这种口诀碰上简单的题能对一但需要你“根据中序先序还原二叉树”就傻了。七级对这种知识点的考察往往不是让你写出递归函数本身而是让你用它解决更复杂的问题。比如表达式树的构建问题就是七级热词里经常出现的一个考点。先序/中序/后序对应表达式的三种Polish表达方式先序对应前缀表达式中序对应中缀表达式后序对应后缀表达式逆波兰式。题目可能会给你一个字符串表示的后缀表达式让你构造表达式树然后输出其中序遍历结果。这类题考察的不只是遍历而是你对递归过程和栈的掌握。递归代码其实很简洁void inorder(Node* root) { if (root nullptr) return; inorder(root-left); cout root-val ; inorder(root-right); }但考试要求你理解每一层递归调用时发生了什么。我教学时常用一个比喻递归调用就像你走进一个套娃每一层都先处理左半边处理完回来后再打印自己然后再去处理右半边。如果你能把这个“栈式”的执行过程画出来程序阅读题基本不会错。3.3 从树的遍历到图搜索的过渡题目经常把“树的遍历”和“图的遍历”联系在一起考察。比如给你一棵二叉树要求你层序遍历输出每一层的结点。层序遍历的实现要借助队列先把根结点入队然后循环出队一个结点访问它再把它的左孩子、右孩子依次入队。这个算法其实和图的广度优先搜索BFS完全一致。相较于递归层序遍历更容易理解因为它是“一层一层来”的。有些同学总觉得递归很玄学这种时候我建议用层序来理解先序先序就是“遇到一个结点就处理然后从左往右依次深入”。先序也可以用栈迭代实现但七级不要求必须用迭代。不过掌握迭代没坏处因为程序阅读题可能会给出非递归版本的遍历代码考察你是否能看懂栈的使用。需要特别注意的是,树的直径和树的重心这类进阶知识点,七级可能涉及但不一定是重点。更多是把树当作基础结构考察建树、遍历、计算深度或子树大小。这些计算都要用后序遍历的思路先处理子树再汇总到父结点。我给学生总结的口诀是“问子树问题用后序问层数问题用层序问路径问题先想深搜”。3.4 二叉搜索树BST专题二叉搜索树是七级考试的一个核心考点。BST的性质很简单左子树所有结点的值都小于根结点右子树所有结点的值都大于根结点。插入和删除操作以及“判断一棵树是否为BST”都是高频考点。这里有一个经典陷阱判断BST不能只比较当前结点和左右孩子的值而要保证整棵子树都满足范围约束。很多同学只判断“左孩子小于父结点、右孩子大于父结点”结果对于退化链式的树会误判。正确的做法是递归时携带一个范围(minVal, maxVal)结点值必须落在这个范围内。在C里可以用long long类型的边界初始值来避开INT_MAX的细节问题。BST的删除是最麻烦的尤其是删除有两个孩子的结点。标准做法是用右子树的最小值结点来替代被删结点。七级不一定会直接考删除的实现代码但程序填空里可能让你补全“寻找后继结点”的函数。你需要熟悉BST的中序遍历结果是升序序列这个性质在很多题里是解题的钥匙。4. 图存储、遍历与最短路径4.1 邻接矩阵还是邻接表先看数据范围图的存储是图论题的第一步,也是最容易被忽视的一步。七级的图论题目通常不会给太大的数据范围但你要能根据数据规模选择正确的存储方式否则要么内存爆掉要么超时。邻接矩阵int graph[N][N]实现简单判断两个点是否直接有边是O(1)但空间复杂度是O(n²)。如果n是1000矩阵就是4MBint类型还能接受如果n是100000矩阵的空间就是10¹⁰量级直接内存爆炸。邻接表用vectorint adj[N]存储每个结点的相邻结点空间复杂度是O(nm)更适合稀疏图。判断两点相邻需要遍历邻接表但GESP的数据规模下完全够用。代码习惯上我建议优先写邻接表。原因不只是内存问题更关键的是遍历时方便用for (int v : adj[u])就能轻松访问u的所有邻居。而邻接矩阵遍历时每次都要判断if (graph[u][v])代码冗余且容易出bug。考试程序填空题通常给邻接表形式你得看懂vectorint adj[N]的操作。4.2 BFS和DFS理解“栈”与“队列”的本质区别图的深度优先搜索DFS和广度优先搜索BFS是七级必考内容。前者基于栈递归也隐含栈后者基于队列。很多同学问这两个遍历方式到底什么时候用哪个我的经验是求最短步数用BFS求连通性和路径枚举用DFS。BFS有个特性容易被忽略当图中所有边的权值都相等或者说每条边的代价都为1时BFS第一次访问某个结点时走过的路径一定是最短路径。这个性质在“走迷宫求最少步数”这类题中是核心解法。有些题要求输出路径本身你需要在BFS时记录每个结点的前驱结点最后从终点回溯到起点。七级编程题中出现过类似的迷宫问题代码模板要背熟。DFS则更灵活可以回溯、可以搜索所有可能路径。对初学者来说DFS在码量上更少但容易超时尤其是不加剪枝的时候。考试中如果有DFS的代码它通常会用visited数组防止重复访问。不要忘记在回溯时重置visited状态否则搜索路径会漏掉很多情况。这个“回溯重置”的细节是程序完善题中常见的填空点。4.3 最短路径Dijkstra和Floyd的适用场景七级对最短路径的要求通常是掌握Dijkstra迪杰斯特拉算法和Floyd弗洛伊德算法。这里最需要分清适用场景Dijkstra算法单源最短路径要求边权非负。复杂度O(n²)的朴素实现或者O((nm)log n)的优先队列优化版本。Floyd算法多源最短路径即求出任意两点间的最短距离。复杂度O(n³)适合n在300以内的稠密图。我见过不少同学对Floyd又爱又恨代码短、好背但一言不合就用Floyd结果n稍微大一点就超时。考试编程题如果你用Floyd能过说明出题人放水了但如果数据范围到1000以上Floyd基本不可能通过。学习原则是先判断n的范围再决定用什么算法。Dijkstra朴素版的核心循环是“每次从未访问的结点中选一个距离最小的用它去松弛其它结点”。选最小这一步如果线性扫描是O(n)用优先队列就是O(log n)。GESP七级对优先队列优化的要求并不高但作为加分项理解它没坏处。程序填空常考的版本不一定让你写优先队列但会让你补全“松弛”部分if (dist[v] dist[u] w) dist[v] dist[u] w;。4.4 最小生成树与拓扑排序七级的进阶门槛最小生成树MST的理论其实不难在带权无向连通图中找一棵包含所有结点的树使得所有边的权值和最小。两个经典算法是Prim和Kruskal。Kruskal的实现更直观把所有边按权值从小到大排序从小到大选边如果这条边的两个端点不在同一个集合就选中它用并查集判断是否在同一集合。这个算法和并查集的结合是考试的热门考点因为程序填空题特别适合把“判断是否连通”挖空。拓扑排序则针对有向无环图DAG。算法实现也很简单统计每个结点的入度把所有入度为0的结点入队然后循环弹出队首结点把它所有出边指向的结点的入度减1如果某个结点入度变为0就入队。如果最终出队结点的数量不等于总结点数说明图里有环。这个“判断是否有环”的应用经常出现在任务调度、课程安排类的题目中。七级的图论题从实践来看常见的出题方式是这样的读入一个图求最短路径或判断连通性输出特定结果。把这些算法的模板代码自己手写过一遍比看十遍教程都管用。我给学生的要求是每个算法都能在5分钟内无错默写出来做到这种熟练度考试才能游刃有余。5. 哈希表空间换时间的艺术5.1 哈希函数与冲突处理概念题的高频来源哈希表Hash Table是一种用哈希函数将关键字映射到表中位置的数据结构。核心思想是空间换时间通过一个函数直接计算出关键字的存储位置使得查询的平均时间复杂度能达到O(1)。但哈希函数的设计并不是完美的两个不同的关键字可能映射到同一个位置这就叫“冲突”。七级对哈希表概念的考察主要集中在冲突解决方法。常用的有开放定址法线性探测、平方探测和链地址法。其中链地址法在C STL中就是unordered_map和unordered_set的实现基础。概念题可能会问你线性探测遇到散列地址被占用时往后找一个空位链地址法则是每个桶挂一个链表冲突的元素挂在同一个桶的链表后面。我建议把哈希表和类结合起来复习。比如定义了一个struct Key { int x, y; };作为unordered_mapKey, int的键时需要自己提供哈希函数对象和一个operator。GESP七级程序填空题确实有可能出这种“为自定义类型提供哈希函数”的题目所以不要以为只是背概念就完事。5.2 unordered_map 和 map 的区别别选错容器很多初学者分不清map和unordered_map的区别这很正常因为它们用法几乎一样。但底层实现完全不同map是红黑树结构内部有序插入删除查找都是O(log n)而unordered_map是哈希表内部无序平均复杂度O(1)。题目要求按某种顺序输出时用map更方便题目只要求快速查重或计数时用unordered_map更快。考试中一个实用的技巧是如果你需要频繁地判断某个元素是否存在或者统计元素出现次数unordered_map是你的首选。看这段代码unordered_mapint, int cnt; for (int x : nums) { cnt[x]; } // 遍历统计结果 for (auto [key, val] : cnt) { cout key - val endl; }这种“计数查重”的模式在编程题里极其常见比如“给一个数组找出出现次数超过一半的数”、“判断两个数组是否有公共元素”。掌握了unordered_map的基本操作这些题就是送分题。5.3 哈希表的经典应用离散化与查重哈希表在竞赛中的经典应用之一是离散化。离散化并不复杂当数据范围很大但数据个数较少时把数据映射为连续的较小的整数。举个简单例子有100个1到10⁹之间的数你不需要开一个长度为10⁹的数组而是把它们排序去重后映射为1到100的编号然后再用数组或树状数组处理。在C里可以用unordered_map完成这个映射。另一个高频应用是查重。比如“给定n个数判断是否有重复元素”你当然可以用双重循环O(n²)实现但n如果到10⁵就会超时。用unordered_set逐个插入如果插入时发现元素已经存在说明有重复O(n)就能解决。这类题在七级里可能会隐藏在更长的题目描述中但核心就是哈希表查重。哈希表也有一个重要缺陷它的遍历顺序是不确定的。如果你用哈希表存了一批元素然后要求按从小到大输出直接遍历unordered_map是不行的需要把键拷贝到vector里再排序。这个考点经常出现在“哈希表加排序”的综合题中程序完善题会告诉你“从小到大输出结果”你要能反应过来遍历哈希表之前需要先对键进行排序。6. 七级备考的避坑指南与刷题规划6.1 高频易错点全是过来人的经验整理一下我在实际教学中看到的七级考生反复出现的错误每条都是真实踩出来的坑类的成员变量没有初始化。定义了类对象后基本类型的成员变量初始值是未定义的。因此构造函数里务必初始化所有成员否则程序运行结果不确定。七级程序阅读题很可能利用这一点设置陷阱。树递归没有终止条件。写树的遍历时忘了判断root nullptr导致空指针解引用崩溃。这种错误最常见于“补全代码后整体阅读”的场景。图遍历漏掉visited标记。无向图在DFS/BFS时如果不对已访问结点做标记会无限循环。无向图里“两个相邻结点互相访问”是最典型的死循环。Dijkstra的优先队列不用pair且排序方向写反。priority_queue默认是最大堆要算最短路需要用的是greater比较器或者直接存pairint,int让默认按第一个元素排序,但一定要记得把距离放在first位置。哈希表冲突处理不清。同一道题里既要按输入顺序又要求快速查重不要试图用unordered_map保持顺序该用map或者vector二分。Kruskal算法忘记按边权排序。最小生成树的核心就是先排序再并查集。忘记排序相当于白写。6.2 考试现场的调试技巧考试不比平时练习没有IDE的强力调试工具时更要靠“肉眼调试”。我的建议是平时刷题就养成两个习惯第一小数据量用手算。写完代码先别急着交自己造一个最小的测试用例比如n3的小图手动模拟一遍算法过程然后跟代码输出对比。很多逻辑错误在n3时就暴露了。第二输出中间变量。在关键节点打印调试信息比如树的递归进入和退出、Dijkstra每次松弛后dist数组的变化。GESP是IOI赛制允许你多次提交所以调试信息可以保留到你确定正确为止。还有一个小技巧先把暴力解法写对。如果题目一下子想不出最优算法先写一个暴力的、一定能过的版本确认自己的理解无误后再优化。七级考试中一些编程题数据范围并不大暴力算法甚至可能直接拿满分。不要因为觉得“暴力不好看”就不写过了才是硬道理。6.3 两周冲刺复习计划根据我的经验七级的有效备考周期大概需要四到六周如果是冲刺模式至少保证两周完整的时间。我给出一个两周计划供大家参考第1-3天类与面向对象每天6-8道编程题重点练构造函数、运算符重载、STL结合。第4-6天树每天8-10道题涵盖建树、三种遍历、BST操作、表达式树。第7-9天图每天8-10道题覆盖邻接表建立、BFS/DFS、Dijkstra、Kruskal、拓扑排序。第10-11天哈希表每天4-6道题练习unordered_map计数、查重、自定义哈希函数。第12-13天综合模拟每天做一套完整的七级模拟卷记录得分和耗时。第14天只看错题本和模板代码不再做新题保持手感。这个计划的核心是“每天手写模板”不要只看不练。类、树、图、哈希表的代码模板加起来不超过300行但每一个模板你都得背到肌肉记忆的程度。考场上有时候拼的不是聪明而是熟练度。写在最后的一点体会带过这么多轮备考我自己的感受是GESP七级真正卡的并不是智商而是踏实的积累。类、树、图、哈希表这四个板块每一个都不是孤立的类会渗透到所有STL使用中树和图的遍历核心都是搜索哈希表则是一种贯穿始终的优化思想。你越早把它们串成一张网后面准备八级甚至更高级别时就越轻松。最后分享一个对我自己很管用的小习惯每天晚上睡觉前在脑子里过一遍当天写过的代码结构想想某棵树的前序遍历应该怎么走某个图用邻接表怎么存某个哈希表冲突了怎么处理。这种“睡前复盘法”不需要额外花时间但坚持两周之后你对这些数据结构的理解会明显更扎实。祝备考顺利七级一次过。