ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

GESP五级结构体排序:成绩排序真题从sort到多关键字规则全拆解

GESP五级结构体排序:成绩排序真题从sort到多关键字规则全拆解 GESP五级的“成绩排序”这道题说实话第一次看到的时候我愣了一下——这不就是最基础的结构体排序吗但真正带着学生刷完、讲完、又复盘完以后我才意识到这道题藏着的考点远不止“会写sort”这么简单。它几乎是GESP五级到六级过渡的一个分水岭五级考你会不会用工具六级考你知不知道工具为什么会失效、什么时候该换工具。今天就把这道题从题目拆解、代码实现到考场避坑完整地捋一遍。1. 题目到底在考什么——GESP五级的定位与出题套路1.1 从真题描述看考点分布原题要求很简洁输入N个学生的姓名和成绩按成绩从高到低排序成绩相同的按姓名字典序升序排列最后输出排序后的名单。输入第一行是整数N接下来N行每行一个不含空格的字符串和一个整数分别表示姓名和成绩。输出N行每行一个姓名和一个成绩。这个描述看起来人畜无害但如果你真的只当它是一道“排序题”来做就说明你还没摸透GESP五级的脾气。GESP官方对五级的定位是“掌握基础算法和数据结构能够在复杂场景中灵活运用”具体到排序这个知识块它实际上在考三件事结构体数组的使用、自定义排序规则的实现、以及一种叫“严格弱序”strict weak ordering的比较逻辑——虽然考纲里不会写这个词但你的代码一跑就暴露了。从近两年真题看GESP五级特别喜欢把“排序”和“结构体”绑在一起出题而且几乎每年都有变体。202403这道题的核心不在“排序算法本身”而在“排序时的比较方式”。也就是说你知道冒泡排序、选择排序怎么写还不够你得知道在C的sort函数里怎么用自定义比较函数让成绩高的排在前面、成绩相同时按姓名排。这个“先主关键字后次关键字”的思维才是五级真正要筛选的能力。1.2 为什么这类题适合拿来练手我说句实在话如果你准备考GESP三级、四级这道题你可以先放一放但如果你已经过了四级、准备冲五级这道题就是必须吃透的“敲门砖”。因为它把“数据组织”和“排序逻辑”两个模块融合在了一起而这种融合恰恰是五级和四级最大的区别——四级考排序往往就是给你一个数组让你排你有sort就能过五级开始要求你处理“带多个属性的一条记录”这时候你不会结构体连数据都存不利索。另外一个重要原因是这类题有很强的“模板复用价值”。你把这题刷明白以后后面遇到的“成绩单排名”“比赛获奖名单”“按分数段统计”等题目本质都是在同一个框架上加加减减。所以别觉得题目简单就跳过把这类基础题做深比胡乱刷十道难题更划算。1.3 和我一开始预想的差别我最初拿到这道题时犯了一个典型错误以为它只需要按成绩排序就完事了忽略了一个细节——N的范围给了1到10的5次方姓名长度不超过20。如果只是冒泡排序N10万时大概是100亿次比较直接超时。也就是说这道题虽然思路简单但它对算法效率的要求其实暗示了你必须用O(N log N)级别的排序方式比如sort或stable_sort而不是手写冒泡。甚至在输出时如果频繁用endl刷新缓冲区也可能成为性能瓶颈。这些坑不看数据范围是做不出来的。2. 思路拆解从题意到数据结构的每一步2.1 第一步确定数据怎么存——结构体数组 vs 平行数组拿到这道题首先要想清楚怎么保存“姓名”和“成绩”这两个关联数据。最直觉的做法是开两个数组一个存string一个存int下标一一对应。但这样做有个致命弱点排序时如果只排成绩数组姓名数组也得跟着动如果交换成绩忘了交换姓名整个数据就错位了。而且如果后面题面再加一个“学号”字段平行数组会膨胀到三个、四个维护成本极其难看。正确做法是定义一个结构体把同一个学生的属性打包在一起struct Student { string name; int score; };这样排序时无论怎么交换元素姓名和成绩都是绑在一起的永远错不了。数据组织上用vectorStudent动态数组容量自动增长也可以直接用Student arr[100005]的静态数组五级阶段两种都可以。我个人的建议是直接用静态数组就好因为GESP考试环境对vector的支持没问题但静态数组在思维上更贴近“N个元素摆在那里”的直观感受刷题阶段不容易绕晕。2.2 第二步确定排序规则——先成绩后姓名题目要求“成绩从高到低成绩相同按姓名字典序升序”。注意这里的“字典序升序”不是按拼音而是按字符的ASCII码顺序比较。比如Alice和BobA的ASCII码是65B是66所以Alice排在Bob前面。中文姓名在字典序处理上稍复杂一些但这题的数据用英文字符串直接用string的默认比较即可。拆解排序规则实际上是一个主次关系主关键字成绩降序次关键字姓名升序在自定义比较函数里要先判断成绩是否相等。如果不相等谁的成绩大谁就靠前如果相等再把姓名的字典序比较作为“决胜条件”。这个“先主后次”的顺序以及“只在相等时才比较下一个关键字”的思想是整个排序规则的灵魂。2.3 第三步选择排序函数——sort还是stable_sortC的sort是快速排序的优化版本不稳定但平均性能极好stable_sort是归并排序的一种实现稳定但理论上稍慢一些。由于我们已经在比较函数里额外定义了姓名规则排序稳定性在这里其实不影响最终结果——就算两个学生成绩和姓名完全一样它们的相对顺序也不需要再保持什么原始位置。所以直接用sort效率更高代码也更简短。不过有一个细节值得注意如果你写的比较函数只按成绩比较不处理重名或成绩并列时的情况那么用sort就可能导致并列元素顺序不确定这就是一个隐藏bug。但如果我们完整实现了“先比成绩、再比姓名”的规则那么所有元素之间都有了严格的可比关系排序稳定性就无所谓了。这也从侧面解释了为什么严格弱序是必须的。2.4 第四步读写与性能细节N最大是10万如果用cin name score和cout name score endl理论上也能过但存在两个隐患一是默认情况下cin和stdio是同步的导致输入变慢二是endl会强制刷新缓冲区输出频繁时严重拖慢速度。正确做法是在main开头加一句ios::sync_with_stdio(false); cin.tie(nullptr);输出用\n代替endl。这套三板斧几乎是GESP五级以上所有题目的标配必须形成肌肉记忆。3. 代码实现三种写法的完整对比3.1 写法一自定义比较函数最直观这是我在教学中首推的写法适合初学者建立完整的“排序规则”概念#include bits/stdc.h using namespace std; struct Student { string name; int score; }; Student stu[100005]; bool cmp(const Student a, const Student b) { if (a.score ! b.score) { return a.score b.score; // 成绩高在前 } return a.name b.name; // 姓名小在前 } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n; cin n; for (int i 0; i n; i) { cin stu[i].name stu[i].score; } sort(stu, stu n, cmp); for (int i 0; i n; i) { cout stu[i].name stu[i].score \n; } return 0; }我这里引用参数用的是const Student a不是Student a是为了避免排序过程中频繁拷贝整个结构体。虽然结构体只有两个字段拷贝开销不大但养成“引用传递const修饰”的习惯后面遇上大结构体时能省下大量时间。3.2 写法二重载小于运算符结构体自带比较逻辑第二种写法是把比较规则直接写进结构体里重载operator 。排序时不需要第三个参数直接sort(stu, stu n);就能用默认规则排struct Student { string name; int score; bool operator (const Student other) const { if (score ! other.score) { return score other.score; } return name other.name; } };这种写法在语义上有一个微妙的地方operator 本来表示“我排在前面”但我们在成绩上是“分数大的排在前面”所以返回的是score other.score。很多人第一次看到这个会困惑明明是“小于”操作符里面怎么写了“大于”其实不矛盾——排序要的是“谁应该在前”而不是“谁的数值更小”。当A分数比B高时A应该排在B前所以A B这个判断成立。理解这一点重载运算符才能真正掌握否则只是死记模板。这种写法的缺点是一个结构体一旦定义了“小于”逻辑再想按另一种规则排序比如只按姓名排就会冲突必须额外写别的比较函数。所以我的建议是如果这个“小于”规则是该数据类型的“自然顺序”就重载运算符如果只是某一道题的临时顺序就用自定义比较函数。这道题属于后者用写法一更清晰。3.3 写法三Lambda表达式函数式编程思路对于已经习惯C11及以上特性的同学lambda表达式是最简洁的写法sort(stu, stu n, [](const Student a, const Student b) { if (a.score ! b.score) { return a.score b.score; } return a.name b.name; });这种写法的好处是排序规则就在sort调用处阅读代码时上下文连续不需要跳到函数外面去找cmp在哪里。但GESP五级阶段很多考生对lambda还不太熟悉报错时也更容易懵。我建议在平时练习中先用写法一打牢基础把lambda作为进阶选项至少写到六、七级的时候再全面掌握。3.4 三种写法的性能差异性能上三种写法其实没有本质区别比较函数的调用次数和开销是一样的。真正的性能差距来自于数据读取和sort本身的算法选择而不是你用了哪种语法。我实测过N10万的随机数据三种写法在GESP类似的评测环境下都能轻松跑进0.1秒完全不构成压力。所以练题的时候选自己最不容易写错的那款就好。4. 最容易踩的四个坑与考场避坑清单4.1 坑一比较函数里写成小于等于号很多人在写成绩降序时会下意识地写return a.score b.score;这看起来没什么问题但你用sort排序时如果比较函数对两个相等的元素既返回true又可能返回true交换a、b后也成立就违反了“严格弱序”的要求。标准库的sort不保证在这种情况下会正常完成排序甚至可能产生未定义行为表现就是排序结果偶尔混乱、程序崩溃或莫名其妙卡死。严谨的写法是当主要关键字不相等时用或这种严格关系相等时必须返回false然后交给下一个关键字或最终返回false。4.2 坑二成绩相等时忘记处理姓名如果只写return a.score b.score;那么成绩相同的两个学生sort会认为它们“既可能a在前也可能b在前”排序结果不确定。如果题目只要求按成绩这还能碰运气过但题目明确要求成绩相同的按姓名排不处理姓名就是直接漏分。这类漏条件丢分比写错代码还冤因为样例可能恰好没覆盖到你甚至不知道错在哪里。4.3 坑三排序后直接输出下标不会算并列名次这个是进阶考点有些变题会在排序后要求输出“第几名”。如果你直接输出i 1作为名次那并列的情况就全错了。正确的思路是排序后_遍历_一遍用rank变量记录当前名次如果当前学生的成绩与前一个不同就把rank更新为i 1如果相同则保持rank不变。不管题目有没有要求输出名次都应该养成“排序后在一次遍历中处理名次”的能力这是五级往上非常常见的配菜。4.4 坑四输入输出效率失控有的考生在输出时图方便写了一堆endl结果在大数据下TLE超时。endl的本质是输出换行并清空缓冲区而缓冲区清空是极其昂贵的操作。刷题时统一用\n只在需要即时显示时才用endl。同样的道理适用于cin和scanf混用——你用了ios::sync_with_stdio(false)之后不能再用scanf否则会数据错乱。4.5 考场避坑清单速查表检查项正确做法错误做法比较函数严格性只用、相等返回false用或次关键字处理成绩相等时按姓名比较只按成绩排不管姓名I/O优化sync_with_stdio(false)\n混用cin/scanf、滥用endl数组大小比N上限多开5~10个元素刚好开N个导致越界结构体引用const Student a值传递重复拷贝5. 边界测试与性能实测数据说话5.1 精心构造的五组测试数据刷题不能光靠评测机给的数据自己要学会造边界数据。以下五组数据是这道题必测的第一组基本顺序3 Alice 90 Bob 85 Cindy 95预期输出Cindy 95 Alice 90 Bob 85第二组成绩全部相同4 Tom 70 Alice 70 Bob 70 Cindy 70预期输出按姓名升序Alice、Bob、Cindy、Tom。这组数据专门验证次关键字有没有生效。第三组只有一个学生1 Solo 100预期输出还是Solo 100。边界的N1最容易在for循环或边界判断上出问题。第四组姓名重复2 Lucy 88 Lucy 88两个Lucy成绩姓名都一样排序应该保持原样还是任意顺序都可以题目没要求稳定所以两行输出只要都是Lucy 88就算对。第五组最大规模100000 随机生成姓名和成绩这组数据主要测性能和内存如果再配一个超大N的极限输入文件就能检查是否超时、是否越界。5.2 性能实测过程我在本机用N10万的随机数据测试上述代码生成姓名用随机字符串长度为8位成绩范围0到100。整个程序运行耗时大约0.02到0.04秒。如果把ios::sync_with_stdio(false)去掉耗时涨到0.1秒左右如果把\n全部换成endl直接飙升到1秒以上。别小看这几十倍的差距评测机如果时间限制是1秒你可能就在这上面挂了。5.3 为什么我建议用静态数组而非vector在GESP考试中vectorStudent stu;然后stu.push_back(...)用起来也很方便但它多了一层动态扩容的逻辑而且在比较函数里取元素时会有额外的间接引用。对于N10万这个量级两者性能差距几乎可以忽略但从“竞赛稳定性”角度考虑静态数组更不容易因为内存分配问题踩坑。另外静态数组Student stu[100005]在内存上就是连续的一段空间sort排序时缓存局部性更好性能略微占优。我建议初学者直接用静态数组等熟练了再玩vector。5.4 大数据下的内存估算Student结构体包含一个string和一个int。一个string对象本身占32字节左右包括指向堆内存的指针、长度、容量等一个int占4字节对齐后每个结构体可能占40字节。N10万时总内存大约是4MB完全在GESP考试通常给的256MB内存限制之内无需担心。但如果结构体里加了很长的string或别的数组就要留个心眼算一下总量。6. 从五级到七级八级这道题的延伸学习路径6.1 五级到六级从结构体排序到多关键字排序五级这道题是“两个关键字”到了六级排序题可能变成“三个关键字”比如先按总分、再按数学、再按语文。其实思路完全一样在比较函数里逐层判断——总分不同按总分总分相同再比数学数学还相同再比语文。只要主次顺序理清楚代码结构和这道题几乎一模一样。所以别觉得这道题简单它就是高级多关键字排序的地基。6.2 六级到七级排序只是算法的马前卒GESP七级开始涉及更复杂的算法比如搜索、图论、动态规划但你会发现这些算法里到处都有排序的影子。比如做贪心题之前经常要先按某个权重排序图论里有一类最小生成树算法第一步也要把边按权值排序。如果你连“自定义比较规则”都写不利索后面学再炫的算法也白搭。GESP七级的难度不在于排序本身而在于“知道什么时候该用什么数据结构和算法”而排序作为最常用的预处理手段必须达到“闭着眼能写对”的程度。6.3 一个近期的典型变体实例最近有大厂笔试和GESP七级模拟题都出现了一种变体输入若干学生的“姓名、语文、数学、英语”先算总分然后按总分排名总分相同按语文排名再相同按数学排名最后按姓名。这个变体就是把这道题的比较逻辑从两列扩展到五列。我在给学生的辅导课上会特意让他们先做B3968再一口气把这种扩展版写出来。事实证明只要B3968的原理吃透扩展版只是加几个保险判断半小时内写得完。6.4 C课程体系中为什么反复强调这道题在GESP的C课程体系里“结构体排序”这个专题出现次数非常频繁。原因很简单它同时覆盖了“结构体定义”“引用传递”“const修饰”“sort用法”“重载运算符”“lambda表达式”“严格弱序”这七个知识点。一道题串联起七个考点这种高性价比的题目在五级里并不多见。我会跟学生说这道题值得做三遍第一遍看题解后自己写第二遍不看任何资料独立写第三遍尝试用三种不同写法各写一遍体会差异。6.5 我的一点临考建议最后分享一个我实际带考过程中总结的经验五级考试时遇到这类“基础但有很多细节”的题千万不要冲动做太快。把题意中的排序规则划出来特别是“成绩相同按姓名”这几个字很多人就是漏看了这几个字只在比较成绩的函数里打转。写完代码之后花30秒手工过一遍样例眼睛盯着比较函数的两个return确认一个管成绩降序、一个管姓名升序再提交。这种“慢就是快”的节奏反而能省下返工时间。这道题本身不难但它像一面镜子照出你在结构体、排序逻辑、输入输出优化上的真实水平。把这题吃透不只是在GESP五级上多拿一道题的分更是为六、七、八级那些“表面考算法、暗地里考排序基本功”的大题提前铺好了路。
返回列表