奖学金OJ题复盘:多关键字排序与C++ sort比较函数详解

刷学校OJ的基础题库时,有一道题让我记忆特别深——题号排在九十几,名字就叫“奖学金”。单看题面,它只是很普通的成绩排序:给你一批学生的学号、语文、数学、英语成绩,按“总分优先、语文次之、学号最后”的规则排出前几名。结果我却连交三次才完全通过,而且前两次挂得都挺丢人。

后来我越琢磨越觉得,这道题几乎是为“多关键字排序”量身定制的入门题,把C++里结构体、sort、比较函数、边界条件这些基础功全串起来了。这篇文章就从头到尾把它拆一遍,适合刚学完结构体和排序、准备机考、或者正在刷基础题库的朋友参考。我会把完整可AC的写法给出来,也会把题目里那些“没人明说但有坑”的细节摊开讲清楚。

1. 奖学金题的题面逻辑与核心考点

1.1 输入输出格式到底长什么样

这种题在各个OJ平台上的版本略有差异,但大体逻辑是一致的。常见输入格式是:第一行一个整数n,表示学生人数;接下来n行,每行四个整数,分别是学号、语文成绩、数学成绩、英语成绩。输出一般要求按排名输出前五名,每行一个学生,输出学号和总分,空格隔开。

有的版本会把数学、英语换成别的科目,有的会在成绩后面加一个“德育分”之类的字段;但只要排序规则还是“总分优先、同分比语文、再同分比学号”,那核心解法就不会变。

我一般会把输入样例先在本地跑一遍,再提交到OJ。很多同学直接跳过样例只看题面,其实样例里藏着不少信息——尤其是“学号是否要升序输出”“总分相同到底先比哪一科”,样例里基本都会体现。

1.2 排序规则的三种等价理解方式

“先按总分从高到低,总分相同按语文从高到低,语文也相同按学号从小到大。”这句话就是整道题的灵魂。

第一种理解方式是最朴素的:定义一个大排序函数,里面依次比较总分、语文、学号。

第二种理解方式是把它看成字典序的变体:如果把每个学生看成一个三元组(-总分,-语文,学号),那么目标就是让三元组按字典序从小到大排列。为什么取负号?因为“从高到低”等价于“取负号后的从小到低”。这种理解在某些语言里写起来很方便,但C++里直接用自定义比较函数更直白。

第三种理解方式是“两两比较”:任何时候要判断学生A是否该排到学生B前面,就只看这三个条件。这个视角最贴近比较函数本身,也是写代码时唯一需要关心的。

1.3 为什么这道题总能出现在基础题库

因为它是典型的“复合排序”,难度刚刚好。只用冒泡排序也能做,但代码会写得比较啰嗦;用sort配合自定义比较函数,就能把“逻辑”和“排序算法”解耦,让初学者意识到:排序不一定非要自己从头实现,关键是定义清楚“谁在谁前面”。

这道题还顺便考察了结构体的使用。有些人习惯开三个平行数组分别存学号、语文、总分,排序时手动交换三个数组;那样做也能通过,但代码的可读性和可维护性会差很多。结构体把学生的所有属性打包在一起,交换一次就是交换一个完整对象,这才符合正常的建模思维。

2. 用结构体和自定义比较函数解决问题

2.1 结构体设计:别让总分成为“临时计算项”

我第一次写这题时,脑子一热,结构体里只存了学号、语文、数学、英语,总分每次比较的时候现算:a.Chinese + a.Math + a.English。代码能跑,但丑,而且容易出现低级错误,比如总分算错、类型溢出、重复计算的性能浪费。

正确的做法是在读入每个学生数据后,立刻把总分算好存进结构体里。这样比较函数只需要读一个total字段,既快又清晰。

#include <cstdio> #include <vector> #include <algorithm> struct Student { int id; int chinese; int math; int english; int total; };

从设计角度讲,这个结构体把“一个学生”完整地表示了出来。如果后续要扩展——比如加一个“奖学金等级”字段,或者追加一门“实验成绩”——都只需要在结构体里加字段,排序逻辑几乎不受影响。

2.2 cmp函数与严格弱序:sort的安全底线

C++的std::sort要求比较函数满足“严格弱序”(strict weak ordering)。这句话听起来吓人,其实要记住的就一点:比较函数回答“a是否应该排在b前面”,当两个元素相等或等价时,必须返回false。

最经典的错误是这样的:

bool cmp(const Student& a, const Student& b) { if (a.total != b.total) { return a.total > b.total; } if (a.chinese != b.chinese) { return a.chinese > b.chinese; } return a.id >= b.id; // 错误:相等时返回true }

当两个学生连学号都相同时(理论上不会发生,但逻辑上存在这种可能),这个函数在a和b上会返回true,在b和a上也会返回true。sort内部一旦发现“a排在b前面”且“b排在a前面”,整个排序结果就会乱掉,轻则输出顺序不对,重则在特定数据下崩溃。

所以写比较函数的铁律是:相等的情况永远返回false,只写<或>,别写>=或<=。对于学号升序,写a.id < b.id,不写a.id <= b.id。

2.3 完整可AC的代码和逐行说明

下面这段代码是我最终提交的版本,输入输出用scanf/printf而不是cin/cout,纯粹是习惯问题,在基础题里两者都不太可能超时。

#include <cstdio> #include <vector> #include <algorithm> struct Student { int id; int chinese; int math; int english; int total; }; bool cmp(const Student& a, const Student& b) { if (a.total != b.total) { return a.total > b.total; // 第一关键字:总分降序 } if (a.chinese != b.chinese) { return a.chinese > b.chinese; // 第二关键字:语文降序 } return a.id < b.id; // 第三关键字:学号升序 } int main() { int n; scanf("%d", &n); std::vector<Student> students(n); for (int i = 0; i < n; ++i) { scanf("%d%d%d%d", &students[i].id, &students[i].chinese, &students[i].math, &students[i].english); students[i].total = students[i].chinese + students[i].math + students[i].english; } std::sort(students.begin(), students.end(), cmp); int cnt = n < 5 ? n : 5; // 不足5人时全部输出 for (int i = 0; i < cnt; ++i) { printf("%d %d\n", students[i].id, students[i].total); } return 0; }

这里有两个细节值得强调。

第一,std::vector<Student> students(n)是C++11之后的写法,会创建n个默认构造的Student对象,然后逐个读入数据。如果OJ环境比较老,不支持C++11,可以改成Student students[1005]这样的固定大小数组。

第二,cnt = n < 5 ? n : 5这个处理非常必要。很多题目数据保证n不小于5,所以有人会直接写for (int i = 0; i < 5; ++i),一旦哪天遇到n=3的边界数据就直接越界访问。基础题里养成“取min”的习惯,后面做大题会少踩很多坑。

2.4 三种排序写法的取舍:不止一种AC姿势

除了自定义cmp配合sort,这道题还有另外两种常见写法。

第一种是重载小于运算符:

bool operator<(const Student& a, const Student& b) { if (a.total != b.total) return a.total > b.total; if (a.chinese != b.chinese) return a.chinese > b.chinese; return a.id < b.id; }

然后直接std::sort(students.begin(), students.end())。这种写法的好处是“一个学生怎么比”这件事直接被固化进类型里,以后任何地方排序都不需要再传cmp。坏处是如果你对同一个类型有不同的排序需求(比如按数学成绩排),重载运算符就不够灵活,还得绕回去写cmp。

第二种是stable_sort逐级排序:

std::stable_sort(students.begin(), students.end(), [](const Student& a, const Student& b) { return a.id < b.id; }); std::stable_sort(students.begin(), students.end(), [](const Student& a, const Student& b) { return a.chinese > b.chinese; }); std::stable_sort(students.begin(), students.end(), [](const Student& a, const Student& b) { return a.total > b.total; });

思路很巧妙:先用学号升序排一次,再用语文降序稳定排,最后用总分降序稳定排。stable_sort的“稳定”保证了每次排序都不会打乱上一次排序建立的相对顺序。最终效果和一次性cmp完全一致。

但这种写法有个前提逻辑:最后一次是关键优先级最高的关键字,最早排的是优先级最低的关键字。很多人一上来就把顺序搞反,结果排完发现和预期正好反了。我建议刚开始刷题时直接用cmp,等彻底理解了稳定性,再尝试这种花式写法。

3. 我连交三次才过的那些坑

3.1 比较函数里的“等于”是隐形炸弹

我第一次提交,就是把学号比较写成了return a.id >= b.id。样例数据里所有学号都不同,所以样例顺利通过;一旦遇到有两个学生总分、语文都相同的情况,排序顺序就开始随机,五个学生里有三个位置错乱,直接WA。

这个教训让我明白了一个道理:OJ的样例覆盖不了所有数据组合。比较函数不是“样例没过才改”,而是要自问一句:当两个元素在前两个关键字上都相等时,我的第三关键字能给出唯一确定的结果吗?

检查方法很简单:把cmp函数里的每个分支都试想一遍,a和b完全相等时返回什么?如果返回了true,立刻改掉。

3.2 语文降序和学号升序的方向容易写反

第二关键字是语文从高到低,所以是a.chinese > b.chinese;第三关键字是学号从低到高,所以是a.id < b.id。这两个方向相反,特别容易手滑。

我的记忆方法是把不等号读数读出来:>表示“大的排前面”,<表示“小的排前面”。先看题目要求“高分的在前”还是“低分的在前”,再决定符号。别凭感觉写,写完回头把这三个分支大声念一遍:“总分大的在前;总分相同则语文大的在前;语文也相同则学号小的在前。”念完就知道符号对不对。

3.3 前五名的输出边界:n小于5时不越界

第二次提交WA的原因不是排序,而是我直接写死了for (int i = 0; i < 5; ++i)。当时觉得题目都说“评选奖学金”,怎么可能人数少于5。结果有个测试点偏偏就n=3,程序虽然没崩,但越界读取了vector后面未初始化的内存,输出了几个乱码数字。

从此我养成了一个习惯:凡是从一个集合里取“前k个”,一律先判断集合大小和k的大小关系。代码里写int cnt = n < 5 ? n : 5;或者int cnt = std::min(n, 5);,一劳永逸。

3.4 空白字符:scanf永远比你想的更宽容

有同学用cin读int,遇到换行或空格都没问题;但如果你混用scanf和getchar,就很容易踩中“读到了多余换行”的坑。

比如有人想用getchar把每行末尾的换行吃掉,结果吃掉了下一行的负数符号或数字第一位,程序直接读入错误数据。我忠告是:读纯数字序列,只用scanf或cin,完全不需要手动处理换行符,它们会自动跳过空白字符。省下的精力足够多检查两遍比较函数了。

4. 只取前五名的优化:局部排序与堆思路

4.1 这题真的需要优化吗?

先说结论:如果n不超过几千,sort全排是绝对够用的,没必要瞎折腾。奖学金这道题在绝大多数OJ平台上n都在几千以内,O(n log n)的sort跑起来感觉不到任何耗时。

但“只取前五名”这个条件确实给了我们一个优化空间。假设某天n变成了十万甚至百万,目标只是要排名最靠前的5个人,全排序的时间复杂度是O(n log n),而理论下限应该是O(n log 5),也就是O(n)。当n足够大时,这个差距是实实在在的。

4.2 partial_sort:一个函数解决“部分有序”

C++标准库提供了一个很方便的算法叫std::partial_sort,它接受三个迭代器:first、middle、last。调用结束后,[first, middle)范围内存放的是整个范围内“按cmp排序后最小的那一批元素”,且这批元素内部已经有序;后面的元素顺序则不保证。

放在这道题里,就是:

int k = std::min(n, 5); std::partial_sort(students.begin(), students.begin() + k, students.end(), cmp); for (int i = 0; i < k; ++i) { printf("%d %d\n", students[i].id, students[i].total); }

注意cmp的定义还是“总分高排前面”,所以partial_sort放在开头的正是总分最高的k个人。我在本地拿一亿条随机数据测过一次,partial_sort比全排快了不止一个数量级,内存占用也差不多。作为扩展知识点,这个函数值得记住;但写题过程中如果n很小,正常sort就完了。

4.3 自己维护大小为5的容器也可以

如果你完全不用STL,还有一个手写的思路:维护一个大小为5的数组,初始放前5个学生,每次来一个新学生,就在这5个里找到排名最低的那个,如果新学生比它排名高就替换。这样一趟扫过去只需要O(n*5),本质上是“选择排序”的变体。

但我不推荐在基础题阶段这么写,原因很简单:逻辑紧凑容易出错,而且还要处理n不到5的情况。用sort或partial_sort,代码更清晰,出错的概率更低。除非你正在学习“如何手写TopK”,否则没必要给自己加戏。

5. 从奖学金题延伸出来的普适排序经验

5.1 一个通用的多关键字比较模板

奖学金题解决之后,我把这类问题抽象成了一个模板,后面遇到任何“按多个字段排名”的需求,直接往里套:

bool cmp(const Item& a, const Item& b) { // 从优先级最高的关键字开始,逐级判断 if (a.key1 != b.key1) { return a.key1 > b.key1; // 把实际升降序规则填在这里 } if (a.key2 != b.key2) { return a.key2 > b.key2; } // 最后一个关键字:建议总有一个唯一标识字段(如id)来兜底 return a.id < b.id; }

这套模板有一个额外的好处:最后一个“兜底字段”可以让排序结果变成完全确定的。即使前面所有关键字都相等,也能稳定地决定先后顺序,杜绝随机性。在OJ排错时,一个“每次运行结果都一致”的程序,debug难度会显著降低。

5.2 同一题的不同版本怎么快速迁移

东华OJ的96题是个基础版,但其他OJ平台有进阶版:有的要求输出“所有获得奖学金的学生”,名额不再是固定的5,而是“总分不低于某个分数线”;有的要求额外输出各科平均分;有的把学号换成了字符串,甚至换成了包含字母的“准考证号”。

碰到这些变体,处理思路完全一样:第一,搞清楚排序的优先级;第二,把额外的输出字段加进结构体;第三,改输出格式。至于排序核心,几乎可以原封不动搬过去。字符串学号的比较稍微注意一下,用std::string直接支持<和>比较,或者用C风格字符串的strcmp。

5.3 机考和面试里识别排序题的方式

很多人觉得这类题太简单,不屑于复盘。但实际机考中,排序题常被包装成“统计前几名”“找出排行榜前K人”“筛选满足条件并按规则输出”。识别方法就一句话:只要题目里出现“按XX排序、再按XX排序、取前几位”,不管描述得多么花哨,底子大概率还是多关键字排序加TopK。

我的个人习惯是:先把比较规则用中文写在草稿纸或注释里,再翻译成cmp代码。中文写“总分从高到低”,代码就是a.total > b.total;中文写“学号从低到高”,代码就是a.id < b.id。看着注释写代码,比对着题面来回翻快得多,也少犯方向性错误。

最后再说一个我自己的小技巧:刷这类基础题时,做完AC之后别急着看下一题,花五分钟把cmp函数里的条件调换顺序,故意写错一版再提交一次,观察WA的输出,能帮助你理解sort为何依赖严格弱序。这种“主动犯错”的训练方式,比连做十道同样难度的题更能沉淀长期经验。

相关新闻

Linux驱动开发:中断与poll机制如何协作,select唤醒链路拆解

Linux驱动开发:中断与poll机制如何协作,select唤醒链路拆解

都说中断和poll机制是驱动开发的两座大山&#xff0c;但把它们分开看其实都不算难&#xff0c;真正让新手抓狂的是“中断来了之后&#xff0c;应用层的select/poll到底是怎么被唤醒的”这条链路。我在做某个模拟项目的外设驱动时&#xff0c;把这条链路完整捋了一遍&#xff0c…

2026/10/20 18:40:44 阅读更多 →
对话式智能求职助手:从搜索到对话原生的交互设计实战

对话式智能求职助手:从搜索到对话原生的交互设计实战

1. 从"投递无门"到"对话即服务"&#xff1a;这个项目到底在解决什么问题 求职这件事&#xff0c;做过的人都懂——不是没有岗位&#xff0c;而是信息太散、匹配太糙、反馈太慢。招聘平台上的岗位列表动辄几十页&#xff0c;筛选条件点了一堆&#xff0c;推…

2026/10/20 18:40:44 阅读更多 →
Bi-RRT算法实现机器人路径规划:从原理到Matlab代码落地

Bi-RRT算法实现机器人路径规划:从原理到Matlab代码落地

1. 项目整体设计与核心思路路径规划这个方向&#xff0c;我在实际项目中接触过不少&#xff0c;从早期的栅格法、人工势场法&#xff0c;到后来的RRT系列、PRM系列&#xff0c;再到深度强化学习那一套&#xff0c;兜兜转转一大圈。如果让我给刚入门机器人路径规划的开发者推荐一…

2026/10/20 18:40:44 阅读更多 →

最新新闻

博科48000交换机安装维护实战指南:端口规划与Zoning配置

博科48000交换机安装维护实战指南:端口规划与Zoning配置

简介&#xff1a;本资源是博科&#xff08;Brocade&#xff09;光纤通道SAN交换机的官方级安装与维护实操手册&#xff0c;面向企业IT运维工程师、存储网络架构师及SAN系统实施人员&#xff0c;聚焦FC-SAN环境下的设备部署、冗余规划、微码升级与日常维护等核心运维场景。文档共…

2026/10/21 0:10:20 阅读更多 →
Java对接支付宝对账单下载接口:从调用到解析入库的完整实践

Java对接支付宝对账单下载接口:从调用到解析入库的完整实践

1. 对账单下载这件事&#xff0c;为什么值得单独拎出来讲做过支付对接的人都有一个共识&#xff1a;支付主链路&#xff08;下单、支付、回调&#xff09;反而是最简单的部分&#xff0c;真正让人头疼的是对账。下单支付是实时链路&#xff0c;出了问题用户马上就能反馈&#x…

2026/10/21 0:10:20 阅读更多 →
SpringBoot+Vue+MySQL社区流浪动物救助系统开发全解析

SpringBoot+Vue+MySQL社区流浪动物救助系统开发全解析

简介&#xff1a;这是一套基于Java、SpringBoot、Vue与MySQL实现的社区流浪动物救助系统完整毕业设计项目&#xff0c;面向计算机相关专业学生&#xff0c;适用于毕业设计、课程设计或期末大作业。系统后端采用SpringBoot框架&#xff0c;前端使用Vue.js&#xff0c;数据库脚本…

2026/10/21 0:10:20 阅读更多 →
STM32寄存器白话手册:从硬件开关视角理解位操作

STM32寄存器白话手册:从硬件开关视角理解位操作

1. 为什么“寄存器手册”从来不是一本能直接读完的书&#xff1f;刚接触STM32那会儿&#xff0c;我桌上常年摊着三份文档&#xff1a;ST官方的《Reference Manual》&#xff08;RM&#xff09;、《Datasheet》&#xff08;DS&#xff09;&#xff0c;还有一本被翻得卷了边的《C…

2026/10/21 0:10:20 阅读更多 →
STM32+华为云IoT+Qt智能家居跨平台实战:低功耗、高可靠、真可用

STM32+华为云IoT+Qt智能家居跨平台实战:低功耗、高可靠、真可用

1. 这套系统到底在解决什么真实问题&#xff1f;——从“能连上”到“真好用”的断层很多人第一次听说“STM32华为云IoTQt”这个组合时&#xff0c;第一反应是&#xff1a;又一个技术堆砌的Demo&#xff1f;确实&#xff0c;网上太多项目只停留在“LED灯亮了”“温湿度数据上云…

2026/10/21 0:10:20 阅读更多 →
ADC采样精度与取平均实战:从原理到配置的避坑指南

ADC采样精度与取平均实战:从原理到配置的避坑指南

ADC 这个模块&#xff0c;说起来简单&#xff0c;做起来坑多。很多人第一次接触模数转换&#xff0c;觉得不就是把模拟电压读进来变成数字嘛&#xff0c;配置一下寄存器、开个通道、读个值就完事了。但真正到了项目里&#xff0c;你会发现读回来的数跳得厉害、精度对不上、采样…

2026/10/21 0:09:19 阅读更多 →

日新闻

统一内存+量化:锐龙AI Max PRO 400如何本地跑320B大模型

统一内存+量化:锐龙AI Max PRO 400如何本地跑320B大模型

这段时间硬件圈和AI圈都在传一个事&#xff1a;AMD锐龙AI Max PRO 400系列把最高192GB统一内存搬进了PC形态的产品里&#xff0c;按宣传口径&#xff0c;本地跑320B参数的大家伙不再是纸面概念。我第一反应倒不是“内存真大”&#xff0c;而是这话背后的含金量——它把过去只能…

2026/10/21 0:00:11 阅读更多 →
AI助手深度使用体验:对话质量、长文本处理与指令遵循如何留住用户

AI助手深度使用体验:对话质量、长文本处理与指令遵循如何留住用户

1. 从“封号重开”这个行为说起&#xff1a;用户到底在追求什么“封号重开”这四个字&#xff0c;放在任何产品语境里都带着一股决绝的味道。一个账号从零开始积累使用记录、调教偏好、沉淀工作流&#xff0c;背后是大量的时间和精力成本。正常情况下&#xff0c;没有人愿意主动…

2026/10/21 0:00:11 阅读更多 →
ROS中YOLOv3工程落地:通信同步、坐标变换与性能调优

ROS中YOLOv3工程落地:通信同步、坐标变换与性能调优

1. 为什么在ROS里硬刚YOLOv3不是“炫技”&#xff0c;而是工程落地的必经之路我第一次在某高校机器人实验室看到那台搭载Jetson TX2的移动底盘原地打转、反复识别错垃圾桶时&#xff0c;就意识到&#xff1a;单纯跑通darknet_ros demo和真正让YOLOv3在ROS里“干活”&#xff0c…

2026/10/21 0:00:11 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型&#xff08;GMM&#xff09;的语音识别系统&#xff0c;很多人第一反应就是一堆数学公式&#xff0c;觉得门槛很高。但如果你真的在MATLAB里把它落地跑通&#xff0c;会发现核心其实就是三件事&#xff1a;把声音变成特征向量&#xff0c;用GMM给每个类别…

2026/10/20 10:02:13 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人&#xff0c;但落到具体操作上&#xff0c;无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程&#xff1a;对图像做反白、改调色板、彩色转灰度&#xff0c;最后把像素数据导出成txt文件。单看每一…

2026/10/20 20:43:59 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词&#xff0c;你会发现一个很有意思的现象&#xff1a;每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型&#xff0c;有人指的是运动控制系统&#xff0c;还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/20 2:50:50 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 10:02:08 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:45 阅读更多 →