news 2026/10/2 2:45:36

GESP四级排序题详解:C++ sort自定义比较器与稳定排序避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
GESP四级排序题详解:C++ sort自定义比较器与稳定排序避坑指南

GESP四级第二题,六个字“排序”,每年都能让一批同学从信心满满到怀疑人生。今年6月的这道题,实际上并不复杂,核心就是排序规则的理解 + C++ sort 的自定义比较器。如果你今早在考场上用了 sort,却因为比较函数写反或者没搞懂“分数相同按学号从小到大”这个稳定排序要求,最后样例都过不去,那这篇文章就是写给你的。

我不喜欢讲空话,直接还原题目、拆思路、给代码、讲避坑,一条龙讲完。不说官方题库原话,但题目结构和考点,跟我在备考阶段反复练的那一类完全一致。

1. 题目还原与考点定位

1.1 题目描述(还原)

根据考生回忆和历年出题风格,这道题大致长这样:

某班有 n(2 ≤ n ≤ 10000)个学生。每个学生有一个学号 id 和一个考试成绩 score(0 ≤ score ≤ 100)。请你按照以下规则对全班学生排序:

  1. 成绩高的排在前面;
  2. 如果成绩相同,学号小的排在前面。

输入:第一行一个整数 n,接下来 n 行,每行两个整数 id 和 score。 输出:排序后每行输出一个学生的学号和成绩,用空格隔开。

样例输入:

3 2 90 1 90 3 80

样例输出:

1 90 2 90 3 80

题干很短,读着也不难,但越是这种看起来没坑的题,越容易在小细节上砸锅。

1.2 这题到底在考什么

GESP四级大纲里,排序算法和 STL 是必考内容。这道题表面上是排序,实际上考了四件事:

  • 能不能看懂排序规则:主关键字是成绩降序,次关键字是学号升序。
  • 会不会写自定义比较器:也就是 C++ sort 的第三个参数。
  • 懂不懂稳定排序和不稳定排序的区别:如果你不知道 sort 是不稳定的,就可能因为用错函数而丢分。
  • 知不知道结构体的基本用法:把关联的两个数据打包成一个整体,而不是拆成两个平行数组排序。

很多同学一看到“排序”两个字,脑子里就只有sort(a, a+n)这个不带条件的一键排序。但遇到多关键字,就必须额外给它一套“规则”,这就是自定义比较器登场的地方。

1.3 样例验证

我们先拿样例过一遍,等会代码写出来还要靠它验证。

输入三行数据,原始顺序是:

  • id=2, score=90
  • id=1, score=90
  • id=3, score=80

按规则,90分那两个并列最高,id没有顺序要求的话谁排前面都行。但规则明确说了,成绩一样就按学号升序。id=1 < id=2,所以输出必须是1 90然后2 90,最后是3 80。这个样例点,专门用来检验你排序后相同成绩学生的相对顺序是否满足要求。

2. 完整思路拆解:从排序规则到比较器

2.1 排序规则的数学表达

写代码之前,先把规则翻译成逻辑表达式。假设有两个学生 A 和 B,成绩分别是 scoreA、scoreB,学号分别是 idA、idB。

A 应该排在 B 前面(也就是 A 小于 B,在 sort 的比较逻辑里返回 true)需要满足:

  • scoreA > scoreB,或者
  • scoreA == scoreB 并且 idA < idB

对应代码就是:

bool cmp(Student a, Student b) { if (a.score != b.score) return a.score > b.score; return a.id < b.id; }

这里有一个初学者特别容易踩的坑:把第一个条件写成return a.score < b.score。那样就变成从小到大排序了,样例第二个点是 3 80,它会被排到最前面,直接离谱。

2.2 为什么要自定义比较函数

sort(a, a+n)默认是升序,也适用于 int、double 这些内置类型,但它完全不了解“学生成绩”这种东西。sort 的默认规则是“a 小于 b 就返回 true,否则返回 false”。对于两个Student结构体对象,它不知道什么叫“大于”,更不知道什么叫“成绩相同比学号”。

自定义比较器cmp,本质是给 sort 提供一本“规则手册”,告诉它两个学生谁应该排在前面。sort 内部的排序算法(一般是内省排序)在整个排序过程中会反复调用这个cmp,来决定是否交换两个元素的位置。

提示:cmp必须是一个严格弱序(strict weak ordering)。也就是说,它需要满足三个基本性质:

  • 反自反性:同一元素比较,cmp(a, a)必须返回 false。
  • 非对称性:cmp(a, b)和cmp(b, a)不能同时为 true。
  • 传递性:如果cmp(a, b)且cmp(b, c),则必须cmp(a, c)。

只要你的比较规则自洽,基本都能满足。但如果你写出return a.score >= b.score;这种带等号的比较器,就是自毁长城,后面详说。

2.3 两种数据结构存储:struct vs pair

这道题可以把学生信息存成结构体,也可以用pair<int, int>,但是两种写法有细微差别。

写法一:struct

struct Student { int id; int score; };

清晰、可读性强,自定义比较器里访问字段也舒服。缺点是代码量稍多。

写法二:pair

pair<int, int>默认的比较规则是先比较 first,再比较 second。如果我们直接存pair<score, id>,那 sort 会先按分数升序,分数相同再按学号升序。这不是我们要的分数降序,所以要把分数存成负数,或者用pair<id, score>然后用特殊比较器。存负分数能利用默认规则,但代码语义不够直观:

vector<pair<int, int>> v; // pair<score取负, id> v.push_back({-90, 2}); sort(v.begin(), v.end()); // 默认升序:-90 < -80,相当于90排在80前面

这种写法很巧妙,很多竞赛选手爱用,但考场上一紧张容易弄混。我更推荐 struct + 显式比较器,逻辑清楚,不容易翻车。

2.4 比较函数的三个“不要”

虽然目录看着像是废话,但真的有人死在下面这三件事上:

  • 不要写>=或<=。sort的比较器如果对两个等价元素返回 true,会破坏内部排序的假设,导致未定义行为,轻则结果诡异,重则直接崩溃。记住,比较相等要返回 false。
  • 不要把主次关键字搞反。有些人先比较 id 再比较 score,结果成绩相同的排对了,但不同成绩的顺序完全乱了。
  • 不要试图在排序时“保留原顺序”。如果你因为 sort 不稳定而想依赖输入顺序,那是不靠谱的,要么用stable_sort,要么把输入相对顺序作为一个额外关键字存入结构体。

3. 参考代码与逐行解析

3.1 第一种写法:struct + 自定义比较器

直接上完整可编译代码,我在考场上用的就是这种思路,大众、稳定、好解释:

#include <bits/stdc++.h> using namespace std; struct Student { int id; int score; }; bool cmp(const Student &a, const Student &b) { if (a.score != b.score) return a.score > b.score; return a.id < b.id; } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n; cin >> n; vector<Student> students(n); for (int i = 0; i < n; i++) { cin >> students[i].id >> students[i].score; } sort(students.begin(), students.end(), cmp); for (const Student &s : students) { cout << s.id << ' ' << s.score << '\n'; } return 0; }

逐行说明:

  • #include <bits/stdc++.h>是包含全部 STL 头文件的万能头,GESP 评测环境普遍支持,考试时用它能省很多心。
  • struct Student定义两个成员。结构体就是打包数据的最小单位,比开两个数组id[]和score[]更不容易弄错对应关系。
  • cmp接收两个const Student&,加const和引用是习惯,避免拷贝也能防止意外修改原对象。
  • sort的第三个参数写上cmp,sort 内部就用它来比较元素。经过排序后,students容器里的元素顺序就变成了题目要求的顺序。
  • 输出用'\n'而不是endl,endl会强制刷新输出缓冲区,循环次数一多会拖慢程序。

3.2 第二种写法:stable_sort + pair

如果你更清楚稳定排序的概念,也可以这么写:

#include <bits/stdc++.h> using namespace std; struct Student { int id; int score; int order; // 记录输入顺序 }; bool cmp(const Student &a, const Student &b) { if (a.score != b.score) return a.score > b.score; if (a.id != b.id) return a.id < b.id; return a.order < b.order; }

等等,题目里成绩相同已经规定按学号升序,而学号是唯一的,所以实际上不需要 order。但我想借此说明一个更常用的技巧:如果你想把“输入顺序”作为一个兜底条件保留,就加一个 order 成员记录输入序号。这条在处理“排序后保持原相对顺序”的需求时特别有用。

还有一种做法是用stable_sort,这个算法基于归并排序,排序时如果两个元素等价(比较结果相等),它们会保持排序前的相对顺序。不过在这道题里,由于学号唯一,成绩相同时学号大小已经唯一决定顺序,所以sort也不会乱。但如果你遇到“成绩相同就按输入顺序排”这种题,那就必须用stable_sort,或者手动记录原始下标。

3.3 输入输出性能优化细节

题目的 n 上限是 10000,理论上用cin >>和cout <<也能过,但你不知道评测机运行环境如何。稳妥起见,我总会加上三行代码:

ios::sync_with_stdio(false); cin.tie(nullptr);

第一行关闭 C 和 C++ 标准流之间的同步,让 cin 不再和 scanf 共用一个缓冲区;第二行解除 cin 与 cout 的绑定,避免每次读入前强制刷新输出。这两个操作能让 cin/cout 的吞吐量明显提升。如果你用 scanf/printf,不需要这两行,但也别和 cin/cout 混用,混用的后果是顺序错乱或者性能下降,具体原理属于 C++ 标准流缓冲区问题,这里不多展开。

还有一点:读入学生信息时用的cin >> students[i].id >> students[i].score;会自动跳过空白字符,所以输入里多几个空格或换行都没关系,别自己去做无谓的scanf格式校验。

4. 运行过程与边界测试

4.1 样例走查

用我们上面的代码跑一遍样例:

  1. 读入 n=3。
  2. 结构体 vector 里依次存入 (2,90), (1,90), (3,80)。
  3. sort 调用 cmp 进行排序:
    • 比较 (2,90) 和 (1,90):score 相等,id 2 > 1,所以 (2,90) 不排在前面,(1,90) 需要前置。
    • 比较 (1,90) 和 (3,80):90 > 80,(1,90) 前置。
    • 最终顺序 (1,90), (2,90), (3,80)。
  4. 输出格式完全一致。

4.2 边界数据构造与测试

考试不只考样例,边界条件才是杀招。我给你列几个典型的边界用例,你可以粘贴到本地跑一遍:

用例1:n=2,两个学生成绩相同

2 5 88 2 88

期望输出:

2 88 5 88

这个用例专门测试“次关键字学号升序”。如果你的代码只按成绩排,不管学号,两行输出会跟原始输入顺序一致(因为 sort 不稳定),运气不好时可能输出 5 88 在 2 88 前面。

用例2:成绩跨度最大

4 100 100 99 0 1 50 2 50

期望输出:

100 100 1 50 2 50 99 0

这里有两个 50 分的,学号 1 和 2,必须按 1 2 的顺序,不能变成 2 1。

用例3:所有学生成绩完全相同

3 10 60 20 60 30 60

期望输出就是按学号升序。这个用例测的是比较器在 score 都相等时,能否完全依赖 id 排序。如果比较函数里写成了return a.score > b.score;而没有后续 id 比较,那么这 3 个元素的相对顺序在 sort 里是不确定的。

用例4:n 的最小值 2

这个几乎所有人都会过,但恰好是很多同学不会循环的起点。如果你把循环从 i=1 开始,读 n=2 时会漏一个,输出就缺行。注意循环写for (int i = 0; i < n; i++),别自作聪明。

4.3 大数据量与复杂度验证

我构造了一个 n=10000 的随机数据在本地跑,程序运行时间几乎可以忽略。时间复杂度是 O(n log n),这是排序算法的理论下限附近的常见复杂度。空间复杂度是 O(n),存储所有学生信息。

具体到 sort 的内省排序,平均时间复杂度 O(n log n),最坏 O(n log n),它结合了快速排序、堆排序和插入排序的优点。对于 n=10000,sort 处理起来毫无压力。而如果这道题你手写了一个冒泡排序,最坏情况 O(n^2),n=10000 时就是一亿次比较,在评测机里极可能超时,这也是GESP四级喜欢考察“能直接用STL就用STL”的原因。

5. 常见错误与避坑经验

5.1 比较器写反/搞错降序升序

我见过最多的错误是:

bool cmp(Student a, Student b) { return a.score < b.score; // 从小到大 }

或者把成绩降序写成:

return a.score > b.score ? true : false;

这种写法本身没问题,但后面接 id 时容易把逻辑写乱。真正稳妥的顺序是:先写不相等的情况,再写相等的情况。别为了省一行代码把两个条件合并。

错误范例:

return a.score > b.score || (a.score == b.score && a.id < b.id);

我知道很多人爱这么写,要求也符合,但在新手阶段,分开写更不容易出错。分开写的代码,即使将来改成别的排序规则,也一眼能看出主次关键字。

5.2 忽略 sort 的不稳定性造成顺序错乱

很多人把“sort”等同于“稳定排序”,这是错误认知。C++ 的sort是不稳定排序,它不能保证等价元素的相对顺序。什么叫等价元素?在我们这道题里,如果学号唯一,实际上没有完全等价的元素,因为学号总能区分顺序。但如果题目改成“如果得分相同,按输入顺序先后排列”,你再用 sort,它可能把输入顺序打乱。

这时候有两个选择:

  • 用stable_sort替换sort,它基于归并排序,保证等价元素保持原有顺序。
  • 或者给每个元素记录一个唯一的 order 字段,作为最后一个比较关键字。这样即使用不稳定 sort,也等价于稳定排序。

我个人倾向第二种,因为stable_sort的时间复杂度虽然是 O(n log n),但常数比sort大,数据量大时会有感知。更重要的是,通过记录 order,你还能顺便掌握多关键字排序的精髓,一举两得。

5.3 输入输出效率导致超时

n=10000,一般情况下 cin 不会超时,但GESP评测机“卡IO”的情况也不是没出现过。有同学用cin >> n;然后循环cout << endl;,在 n 比较大的时候,endl不断刷新缓冲区,性能急剧下降。这不是技术问题,是细节问题。

我的习惯是:在文件开头写上ios::sync_with_stdio(false);和cin.tie(nullptr);,输出统一用'\n'。这一点不管你参加什么C++考试,都是好习惯。

5.4 错误使用相等元素的比较

再强调一次:cmp返回 true 表示 a 应该排在 b 前面,返回 false 表示不一定。如果你写了return a.score >= b.score;,当两个分数相等时,cmp(a,b) 和 cmp(b,a) 都返回 true,这就违反了严格弱序的反对称性原则。sort 内部算法碰到这种情况会行为异常,结果可能是一团乱序。

很多同学自以为看懂了比较器,其实只看了眼“大于号”和“小于号”。要判断一个比较器是否合格,可以把它放到 set 或 priority_queue 中使用,如果出现莫名其妙的重复或顺序错乱,那就是比较器有问题。最简单的自查方法:对两个相同 key 的数据 a 和 b,手动算一遍cmp(a,b)和cmp(b,a),如果两个都是 true,立刻重写。

6. 从这道题延伸:GESP四级排序题还能怎么考

6.1 字符串排序

四级考试另一大经典就是字符串排序。比如给你 n 个字符串,按长度从小到大排,长度相同按字典序升序。这题同样用自定义比较器,区别是要处理string类型的比较。string类型自带length()和<运算符,所以代码写起来更简单:

bool cmp(const string &a, const string &b) { if (a.length() != b.length()) return a.length() < b.length(); return a < b; }

再进阶一点,题目可能要求忽略大小写排序,那就需要额外写一个toLower函数,或者用 C++ 标准库的tolower。这种字符串排序考的就是你对 STL 字符串容器的熟悉程度,以及处理局部比较规则的能力。

6.2 结构体多关键字排序

多关键字排序是四级排序题里最常见的配方。常见的规则组合有:

  • 总分降序,总分相同语文降序,语文相同数学降序,再相同学号升序。
  • 日期排序,先年再月再日。
  • 学生信息表,按班级升序,班级相同按成绩降序。

无论规则多复杂,比较器和我们的 cmp 函数思路完全一样:把每个关键字按照优先级依次比较,某个关键字能分出高下就立刻返回结果,否则继续比较下一个。写的时候记住“先主后次,相等再比下一个”这个口诀。

6.3 手写排序算法

虽然四级不强制要求手写排序算法,但大纲明确覆盖冒泡排序、选择排序、插入排序这些基础排序。像这种“第二题排序”的题目,如果你只用 sort,是可能被扣过程分的。我这里说的“过程分”不是指考试评分,而是指你学习算法时的自我要求。

比如选择排序,每一轮找到剩余元素中最小(或最大)的元素,交换到前面。它是不稳定排序,但如果你利用“稳定化技巧”,比如只交换相邻元素或者在比较相等时不交换,就可以变成稳定的。考试时如果要求写出算法过程,你应该能手动模拟冒泡排序的每一轮交换。

6.4 排序和其他知识结合

排序经常跟贪心、二分、前缀和等搭配出题。比如“最小移动次数使数组有序”“合并区间”“求逆序对”等,都是排序的延展。回到这道题,它本身很基础,但你要是能把它背后的比较器原理吃透,那么后面遇到“自定义对象装入优先队列”“按字典序拼接字符串得到最小数字”这类题时,思路会轻松不少。

比如有一个经典面试题:给定一组非负整数,将它们拼接起来得到一个最大的数。解法是自定义排序规则:两个数 a 和 b,比较字符串 a+b 和 b+a 哪个大。这本质上用的还是 cmp 函数,只不过比较的是拼接后的字符串。如果没学会自定义比较器,这类题会无从下手。

最后,再分享一点考场经验

我每次参加这类编程考试,都会在写排序题时主动做三件事:第一,先在草稿纸上把排序规则写成if...else伪代码,避免直接写 cmp 时脑子乱。第二,代码写完立刻拿样例走一遍,重点看相同分数学生的顺序对不对。第三,故意构造一个“最阴间”的边界用例,比如所有分数都相同,或者 n 最小的用例,测试输出是否稳定。

这三步看着浪费时间,但能把失误概率压到最低。尤其是这道“第二题排序”,它往往是整张卷子区分度的分水岭:写对了,后面的大题心态就稳;写错了,则可能连锁影响到后面的发挥。希望明白这些坑之后,你再遇到类似的题目,能像条件反射一样写出正确的比较器,稳稳拿下这些分数。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 2:44:46

混合模型时间序列预测实战:LSTM与Transformer融合路径

简介&#xff1a;这份资源面向具备一定深度学习基础、希望上手时间序列预测实战的开发者与学习者&#xff0c;聚焦LSTM与Transformer混合模型的构建与落地。内容围绕如何将LSTM的局部序列建模能力与Transformer自注意力机制的全局依赖捕捉相结合&#xff0c;用于处理金融、气象…

作者头像 李华
网站建设 2026/10/2 2:44:20

CSDN博客插图尺寸调整全攻略:从拖拽到源码的四种方法

写技术博客这几年&#xff0c;我最头疼的往往不是文章内容本身&#xff0c;而是配图排版。很多人应该都有过这种经历&#xff1a;辛辛苦苦写了几千字&#xff0c;插入一张运行效果截图&#xff0c;结果图片尺寸要么大得撑满整个版面&#xff0c;要么小得看不清细节&#xff0c;…

作者头像 李华
网站建设 2026/10/2 2:44:01

Python金融风控建模实战:从特征工程到模型监控的完整链路

简介&#xff1a;这份资源面向金融风控方向的初学者与在校学生&#xff0c;提供一套基于机器学习的Python大数据风控建模完整实战项目&#xff0c;可用于毕业设计、期末大作业或课程设计场景。压缩包共106个文件&#xff0c;约20.13MB&#xff0c;以40个py源码文件为核心&#…

作者头像 李华
网站建设 2026/10/2 2:43:42

SSM+Vue交通规则考试系统:从数据库设计到部署实战

每年这个时候&#xff0c;都有一批人对着毕设题目发愁。如果你拿到的是“基于SSMVue的交通规则考试系统”这个题&#xff0c;恭喜你&#xff0c;这套组合拳在毕设圈里属于最稳的一类&#xff1a;后端是SpringSpringMVCMyBatis这套老牌SSM组合&#xff0c;前端是Vue&#xff0c;…

作者头像 李华
网站建设 2026/10/2 2:43:39

基于YOLOv8的西红柿成熟度检测系统:从数据集训练到PyQt5界面部署

简介&#xff1a;本资源是一套基于YOLOv8深度学习框架的西红柿成熟度检测系统&#xff0c;面向计算机、人工智能、自动化等专业的在校学生、教师及企业开发者&#xff0c;也适合作为毕设、课程设计或实战演示项目。系统通过PyQt5构建图形化界面&#xff0c;可对西红柿进行成熟与…

作者头像 李华
网站建设 2026/10/2 2:43:36

DeepSeek Harness实战:从Agent到Vibe Coding工作流

不是工具不够强&#xff0c;是用法太粗糙最近的 AI 编程圈&#xff0c;几乎每天都能看到“Vibe Coding 已死”“Copilot 已经落后”这类说法。但在大量真实开发讨论里&#xff0c;我发现一个更普遍的问题&#xff1a;很多人根本不是在用 DeepSeek Harness&#xff0c;而是把它当…

作者头像 李华