
很多刚开始刷题的朋友第一道真正让自己有“原来我还能写代码”感觉的题目往往不是Hello World而是像牛客BC95这样的小题。这道题的全名叫“最高分与最低分之差”逻辑一句话就能说清给出一组学生的成绩算出最高分与最低分的差值。用C、Java、Python哪门语言都能写核心代码加起来可能不到十行。但我一直觉得这道看起来“送分”的题恰好是检验输入处理、变量初始化、边界条件这三项基本功的好材料。这篇文章就围绕BC95把从读题到AC的完整链路、代码写法、踩坑记录和引申出的变种思路都整理一遍。不管你是刚学编程的入门选手还是准备笔试面试、想回头巩固基础的老手都能在这里找到一点用得上的东西。1. 题目拆解先搞清楚这题到底在问什么1.1 输入输出和核心逻辑一句话说清但别忽略细节BC95的题意没有任何弯弯绕绕。输入包含两行第一行是一个整数n表示学生人数第二行是n个整数代表每个学生的成绩。输出就是这n个数里最大值减去最小值的差值。比如输入5 99 45 78 67 72那输出就是99减45得到54。这道题输出的值永远是整数不涉及浮点数所以用int类型完全够用。但要注意一个很容易被新手忽略的输入细节第二行的n个整数到底是不是必须写在同一行实际上读入的时候不需要担心这个问题。C语言的scanf的%d在读整数时会自动跳过空格、换行、制表符这些空白字符Python的input()配合split()再把每个元素转成int也天然支持换行。所以无论OJ上给的测试数据是挤在一行还是分成好几行代码都能正确处理。这个特性很多人用了很久都没意识到但它其实是所有标准输入题目的共用前提。1.2 算法流程为什么一趟循环就能同时搞定最大值和最小值这道题的算法思路本质上就是在一组数里做“同时找最大和最小”的遍历。一个最直观的做法是先读入全部成绩存进数组然后第一次循环找最大值第二次循环找最小值最后相减。这个思路很符合直觉绝大多数教材也都是这么教的。不过这道题有一个更好的处理方式不用数组读一个成绩就立刻更新一次最大值和最小值。这样一遍循环读完所有输入的同时两个答案就已经出来了。整个过程类似“一边往框里放鸡蛋一边心里记着目前见过最大的鸡蛋和最小的鸡蛋”而不是把鸡蛋全倒出来再一个个重新看一遍。放完最后一个鸡蛋的时候你自然就知道最大最小是哪两个。两种方式的结果完全一样但后者省掉了数组的存储空间代码也更短。对于BC95这种只要求输出一个极差的题数组其实是多余的。如果题目以后改成“输出所有成绩里第k大的值”或者“还要按顺序输出成绩”那才轮到数组出场。写代码之前先想清楚“最终要保留哪些信息”比闷头写完再回头看重要得多。1.3 时间复杂度与空间复杂度这道题面试官会怎么问作为入门题BC95也经常被用来考察复杂度概念。如果是数组版本一共遍历两次数组时间复杂度是O(n)空间复杂度是O(n)因为要存下n个成绩。如果改成单变量在线更新版本时间复杂度仍然是O(n)但从头到尾只需要两个变量max和min加一个临时变量空间复杂度降到O(1)。n的范围对这道题的影响不大。如果n是100两个版本跑起来肉眼完全看不出区别但如果n变成100万数组版本会多占几MB内存单变量版本却几乎不消耗额外空间。很多初学者习惯性“先存下来再说”等到做大规模数据题时就吃了亏。所以从BC95开始养成“能不存就不存”的意识后面的路会顺很多。2. 代码实现三套语言的写法与背后的取舍2.1 C语言参考写法用第一个元素初始化是正确姿势先看最经典的C语言版本。这里我给出一个支持多组输入直到EOF的写法因为牛客和其他OJ平台上的很多题目都需要这样处理直接养成本能没问题。#include stdio.h int main() { int n; while (scanf(%d, n) ! EOF) { int score; scanf(%d, score); int max score; int min score; for (int i 1; i n; i) { scanf(%d, score); if (score max) { max score; } if (score min) { min score; } } printf(%d\n, max - min); } return 0; }这段代码里最关键的一行就是int max score; int min score;用读进来的第一个成绩同时初始化两个变量。为什么要这么做而不是写成int max 0; int min 100;因为这里默认了成绩一定在0到100之间。如果哪一天输入数据里出现了超过100的成绩比如竞赛里的某次模拟得分是120min 100这个初始化就永远错误会让程序输出错误答案。用第一个元素初始化无论数据范围怎么变都是安全的这是一种更通用的“避免假设”的写法。循环从i 1开始也很重要因为第一个元素已经读过了不需要再和自己比较一遍。很多新手在这里从i 0开始循环结果把第一个成绩读两遍虽然这个错误不影响最终答案但暴露了对循环边界的理解不够清晰。2.2 用while包住scanf到底解决了什么问题很多初次接触OJ的读者会困惑为什么别人的代码里有一个while (scanf(%d, n) ! EOF)而老师上课教的都是直接scanf(%d, n)原因很简单OJ的评测系统会把所有测试数据一次性通过标准输入给程序每个测试用例是一组输入程序需要自己判断还有没有下一组。如果只写scanf(%d, n)程序读完第一组数据就退出后面几组数据根本没有机会被处理自然就会WA。用while (scanf(%d, n) ! EOF)包裹之后只要评测系统还有剩余输入scanf就会返回成功读到的变量个数等到所有数据都被读完scanf返回EOF循环自然结束。如果题目明确只有一组输入单组写法也能通过但多组写法完全涵盖单组输入的情况所以直接养成多组输入的习惯最省事。这个习惯后面做牛客的BC系列、CCF CSP和蓝桥杯题时都非常有用。2.3 Python与Java版本思路相同、语法不同Python版本可以用更简洁的方式来实现同样的逻辑while True: try: n int(input()) nums list(map(int, input().split())) print(max(nums) - min(nums)) except EOFError: break这里input().split()负责把第二行的字符串按空白拆成列表map(int, ...)把每个字符串转成整数再用内置的max()和min()分别取出最大值和最小值。Python内置函数的时间复杂度同样是O(n)并且已经帮你优化好了不需要自己写循环去逐个比较。但要注意Python读入时如果到了文件末尾input()会抛出EOFError异常所以必须用try/except EOFError包起来否则最后一组数据跑完程序会异常退出。这是Python版和C版在输入处理上最不一样的地方。Java版本的写法同样直观用Scanner的hasNext()判断是否还有输入import java.util.Scanner; public class Main { public static void main(String[] args) { Scanner sc new Scanner(System.in); while (sc.hasNext()) { int n sc.nextInt(); int score sc.nextInt(); int max score; int min score; for (int i 1; i n; i) { int x sc.nextInt(); if (x max) max x; if (x min) min x; } System.out.println(max - min); } } }三套语言放到一起对比能看出一个规律核心算法思想完全一样差别只在输入输出API和异常处理的语法上。所以刷题时不需要纠结“哪门语言更好”选自己最熟悉、最容易写出正确答案的那门就行。3. 新手最容易踩的坑从WA到AC的复盘3.1 初始化错误答案全是0的罪魁祸首我第一次在牛客上写这类题的时候曾经连续WA了好几发排查了半天发现根本不是算法问题而是初始化出了岔子。当时的代码大概是这样的int max 0; int min 100;这种写法有一个隐藏的致命问题如果输入的所有成绩都不是0到100之间的数比如全是120分那么min 100的判断会让最小值永远停留在100最终输出的差值就比正确答案小了20。反过来如果成绩里真的出现了0分max 0又会让最大值永远停在0导致差值为负或者错误。后来我养成一个习惯凡是遍历序列求最值的题目全部用序列第一个元素初始化或者用C语言里的INT_MAX和INT_MIN来初始化。例如int max INT_MIN; int min INT_MAX;用这两个极端值初始化的好处是只要遍历还没结束任何正常输入的元素都会覆盖掉初始值不会出现“初始值本身就在数据范围内导致判断失效”的尴尬。这个习惯很值得从一开始就培养。3.2 边界条件没想全n1的时候程序会不会崩边界条件是OJ题里最容易被忽略却也是最容易挂分的地方。BC95最典型的边界情况就是n等于1。如果只有一个学生最高分和最低分是同一个数差值应该是0。用单变量版本时读入第一个数之后max和min已经被赋成同一个值for循环一次都不需要执行最后直接输出0逻辑天然正确。但如果你写的是数组版并且习惯性地在循环结束后用scores[0]来做初始化就要确认n至少是1。万一某个非法测试数据里n为0访问scores[0]就是越界C语言会直接产生不可预期的行为可能在OJ上表现为RE。针对n1这种情况比较好的防御性写法是在读完后加一个判断if (n 0) { printf(0\n); continue; }当然正常题目不会给n0的数据但多写这一行并不会降低性能却能让程序更健壮。真实的工作环境中输入数据永远比OJ上更脏防御性编程本身就是工程师的基本素养。3.3 本地跑得好好的一上OJ就错先检查这三个地方很多初学者在本地编译器里测试样例输出完全正确一提交到OJ就变成WA然后百思不得解。根据我自己的踩坑经历BC95这类入门题最容易在下面三个地方翻车。第一多组输入问题。本地测试时通常只手动输入了一组数据所以单组写法看不出问题而OJ的测试文件里可能有多个测试用例程序只处理了一组就退出。解决方法是固定使用while (scanf(...) ! EOF)或者while (sc.hasNext())这种可以连续读取的模式然后再加一个本地测试文件比如准备一个包含多组数据文本文件用重定向方式运行程序来验证。第二输出格式问题。很多时候题目要求输出后换行printf(%d\n, max - min)里面的\n不能省略。有些评测系统对换行不敏感但另一些会严格比对少了一个换行就是Presentation Error。另外要注意输出不能有多余的提示文字比如“答案是”或者“请输入”这些OJ只认纯数字输出。第三数组越界和变量类型。如果题目给的n有最大值约束而你的数组开得不够大或者成绩超出了int的范围也会导致意想不到的错。BC95本身数据范围不大但养成先看数据范围再选类型的习惯后面做大数题就不会慌。下面这张表可以贴在旁边遇到WA时逐项排查错误现象典型原因解决办法输出结果恒为0max和min初始化成了同一个错误值用第一个元素初始化或使用INT_MIN/INT_MAX只处理了一组数据就退出缺少多组输入循环用while(scanf(...)!EOF)包裹输出格式错误PE缺换行或打印了多余字符严格按题目要求的格式输出程序崩溃REn0时访问了scores[0]增加n0的防御判断结果比预期大或小成绩超出初始化预设范围不要假设成绩在0~100之间4. 把这题吃透之后相关变种与思路迁移4.1 变种一去掉一个最高分、再去掉一个最低分求平均分BC95本身只要求输出极差但在一些考试和面试场景里这道题会进一步升级成“评委打分”问题给出所有评委的评分去掉一个最高分和一个最低分然后计算剩余评分的平均值。这种题目的常见场景是各种比赛里的选手得分目的是去掉极端评分对结果的影响。解题思路是在原来找出最大值和最小值的基础上再多做一件事在遍历过程中顺便把所有成绩累加到一个总和变量里。遍历结束后用sum - max - min得到去掉极端值后的总和再除以n - 2就得到平均分。需要注意的一点是如果成绩允许小数所有变量都要用double或float类型最后用printf(%.2f\n, avg)输出保留两位小数。这个变种和原题最大的区别是“不仅要找最值还要维护累计信息”但整体依然是一趟循环能搞定的复杂度很适合作为从BC95往后再走一步的进阶练习。4.2 变种二从“极差”到“区间次方和”查询问题的进化牛客的题库里有一类名字听起来很高级的题目比如“区间次方和”。这类题的要求通常是给一个数组然后多次询问某个区间内每个元素的k次方之和。第一次看到这种题很多人的第一反应是每次询问都遍历一次区间把区间里的每个数都算一遍次方再加起来。这当然能做对但如果询问次数特别多比如有10万次询问而数组长度也是10万每次遍历区间的总复杂度会变成O(n * m)直接就超时了。从BC95到区间次方和本质上是从“一次遍历求整个序列的极差”到“多次查询任意区间内的聚合结果”的进化。前者的思路是扫一遍就能拿到答案后者的关键则是在正式回答查询之前先做预处理用前缀和或前缀次方和把每个位置之前的累积结果存下来。这样每次查询只需要用presum[r] - presum[l - 1]这种O(1)操作就能拿到答案不用再去遍历。很多人觉得这种题难难的不是代码本身而是思维上的转换——从“临时算”变成“提前算好、随时取用”。我建议刷完BC95之后可以顺着这两个方向多练几题一是区间求和类二是区间最值类。这两个方向能覆盖从入门到进阶的大部分数组处理套路。4.3 变种三定长窗口里的最大最小差还有一个值得关注的变种给定一个数组和一个固定长度的窗口让窗口从左往右滑动每次输出窗口内最大值与最小值的差。这个问题如果沿用BC95的思路每次滑动窗口时重新遍历一遍窗口内的所有元素复杂度是O(n * k)k为窗口长度。数据量一大就不好使了。更高效的做法是用单调队列来维护窗口内的最大值和最小值或者用支持删除操作的优先队列。每次窗口滑动时新元素进队、旧元素出队同时保持队列的单调性这样每个元素最多入队出队一次整体复杂度降到O(n)。这个变种其实已经超出入门题的范围了但它的核心问题和BC95一样仍然是“维护一组数据里的最大值和最小值”。只是从静态数组变成了动态窗口维护的难度也跟着上升。如果BC95你已经可以在五分钟内无脑AC不妨拿这个变种来挑战一下自己。4.4 更远一步极差与数据分布的统计意义从纯编程角度看BC95只是一道遍历求最值的入门题但换个角度想它其实也藏着一点数据统计的影子。极差最大值减最小值是最简单的一种离散程度度量它能够粗略地反映一组数据的波动范围。比如全班成绩最高分98、最低分58极差就是40一眼就能看出成绩差异很大如果最高分95、最低分88极差只有7说明成绩分布相对集中。当然极差的缺点也很明显它只用了两个极端值完全忽略了中间大量数据的分布情况。如果一组数据是1、50、50、50、99极差是98听起来波动很大但实际大部分数据都聚集在50附近。而方差、标准差这些统计量会考虑每个数据与平均值的偏离程度能更准确地刻画离散程度。对编程题来说如果题目要求计算方差就得在求和之外再维护每个数的平方和依然是在一次遍历里能完成的事。明白这道题背后的统计含义对你以后处理数据分析类的编程问题也有帮助。5. 写在最后这道入门题带给我的三点体会刷题数量多了以后我反而越来越觉得BC95这种“简单题”值得认真对待。它不需要复杂的算法也不需要高深的数据结构但它的解题过程几乎涵盖了所有早期编程学习必须跨过的门槛。第一点体会是“读入即处理”。很多时候我们不需要把数据原封不动地保存下来边读边算能让代码更简洁、更省内存。这个意识在BC95里是优化在后续很多题目里就是必须。第二点体会是“初始化的选择最能暴露一个人写代码的思维习惯”。用0初始化、用100初始化、用INT_MIN初始化、用第一个元素初始化这四种选择背后对应的是不同程度的防御意识。刚开始写代码时觉得哪种都行直到因为一个初始化问题WA到怀疑人生才会真正理解为什么要写得更通用、更鲁棒。第三点体会是“简单题和难题之间的距离往往只差一个提问”。BC95求整个数组的极差是O(n)但如果你问一句“如果要在任意子区间里反复求极差怎么办”题目难度立刻就上了一个台阶再问一句“如果窗口是动态滑动的怎么办”就又是一层进阶。算法学习就是这样先做会简单题再学会给自己出变种题一层一层往上走。如果你现在刚刷到牛客BC95建议先自己动手写一遍再故意把初始化改成容易错的写法在本地用极端数据测一测感受一下WA是怎么发生的。等你能把这套流程完整走下来一道题的收获就已经不止是一道题了。