☰
寻找250:从EOF判停到顺序查找的入门实践
2026/10/14 5:16:05 网站建设 项目流程

天梯赛的L1题单里,“寻找250”可能是最有喜感的一道题。第一次看到这个标题,我第一反应是出题人大概想趁比赛紧张的氛围皮一下。等仔细读完题,发现它正经得很:给一串正整数,数字之间用空格分隔,你要做的是把第一个等于250的数字找出来,输出它在整个序列里的序号,序号从1开始。核心考察点就三个——怎么判断输入结束、怎么顺序去找、怎么保证“第一个”。

别看它简单,我带队伍训练时发现,这道题在L1阶段翻车率一点都不低。翻车的不是新手和老手的某一类人,而是两类人都容易踩坑:新手容易栽在EOF判断上,老手容易栽在输出格式和起始序号上。这篇文章会把这道题完全拆透,讲清楚题面里的每个隐藏考点,给出C、C++、Python三种主流写法的完整代码,再把我实际遇到的高频问题和排查思路整理出来。适合准备参加天梯赛的新手、正在刷基础题的学生,以及想补一补在线评测输入输出基本功的朋友。

1. 先把题目读懂:三个关键词决定成败

很多同学觉得这种题不用读题,看样例就会了。但恰恰是这种心态,最容易丢分。这道题的题面虽然短,里面却藏着三个决定成败的关键词:第一个、250、序号从1开始。任何一条理解偏差,代码写完都过不了。

1.1 “第一个”意味着找到就停

“第一个250”这句话直接决定了循环的终止策略。当你从头开始遍历数字序列时,遇到的第一个等于250的数就是答案,后面还剩多少数字,跟你没有任何关系。所以代码逻辑应该是:读一个数,比较一次,命中即输出,然后立刻结束程序。

这个逻辑用“排队找穿红衣服的人”来类比最好理解。你在队伍里找那个第一个穿红衣服的人,一旦看到了,后面的队伍还需要继续检查吗?不需要了。但如果你要实现的是“最后一个穿红衣服的人”,那逻辑就完全反过来,得把整个队伍走完才能确定。所以,一个“第一个”三个字,直接决定了break该不该出现、出现在哪里。

顺带提醒一点:有些同学写出这样的代码——把每个数都存进数组,遍历完整个数组再输出第一个符合条件的位置。这种方式能过,但没必要。既然只要第一个,找到之后就应该立刻停,既省时间也省代码量。后面我会详细讲解流式处理的写法。

1.2 “序号从1开始”是新手最容易忽略的边界

这是这道题最大的坑,没有之一。题目说“输出它在整个序列里的序号”,这里的约定是第一个数编号为1,第二个为2,以此类推。这是中文题目里最自然的表达方式,但写代码的人往往刚从“数组下标从0开始”的世界里出来,很容易下意识地把计数器初始化为0。

拿一个典型的输入样例来说:

888 666 123 233 250 999 250

正确输出是5,因为从左往右数,250出现在第5个位置。如果你把计数器从0开始初始化,程序会输出4,评测结果直接判错。这个错误非常隐蔽,因为你自己本地跑的时候,如果只测试“250在开头”的数据,初始化为0和初始化为1输出的是同一个结果,根本暴露不了问题。只有当你测试“250在中间或末尾”的样例时,才会发现差了一位。

我的建议是:拿到题目先不要急着写代码,在草稿纸上把“第几个数”的编号标出来,尤其注意第一个数的编号。后面遇到“输出第k个满足条件的元素”“求第k大”这类题目时,这个1-based计数的思维方式会反复用到。

1.3 输入不告诉你个数,这是整道题的题眼

天梯赛L1阶段的很多题目,输入的第一行会先给一个n,告诉你后面有几个数,然后你按n次循环读取就行。但这道题不是,它只给你一行正整数,不告诉你数量。数字与数字之间是空格,最后可能有换行,也可能没有。你怎么知道数据读完了?

答案是通过读取函数的返回值来判断。在C语言里,scanf每成功读入一个整数就返回1;如果读到文件末尾已经没有数据,就返回EOF。C++的cin >> x在读到文件末尾时,表达式的布尔值为false。Python的sys.stdin.read()读到空字符串表示结束。这三种语言的终止判断细节不一样,但本质都是同一个概念:检测输入流的末尾。

这个点是你以后刷所有在线评测题目必须跨过的门槛。L1有一批题目都采用了“无n输入”的设计,比如给一行以某个特殊值结尾的数据,或者给一个不定长的序列。如果你只会写“先读n再循环n次”的固定模式,遇到这种题就会卡住。这道题就是让你提前接受这个训练的好机会。

2. 解题思路与三种语言实现

核心思路一句话就能说清楚:读一个数,判断是不是250,是就输出当前位置并结束,不是就位置加1继续读。这个思路不依赖存储,不需要数组,也不需要预先知道一共有多少个数,是典型的流式处理。

2.1 核心思路:流式查找为什么最合适

我见过一份代码,开了一个很大的数组,比如int a[100000],然后先用一个循环把所有数字读进数组,再用另一个循环找250。这样写当然能过,因为数据量小,内存足够。但我还是推荐流式写法,原因有两个。

第一,代码更短,逻辑更直接。一边读一边判断,命中即停,不需要两轮循环。第二,它培养的是一种处理未知长度输入流的习惯。以后你遇到更复杂的问题,比如从网络流、日志文件、传感器数据里查找某个特征,这些数据源往往不能一次性全部加载到内存里,你只能一个一个读、一个一个判断。流式处理的思维越早建立越好。

用生活里的例子来说,这就像你在机场安检口检查乘客的行李,传送带上一个行李接一个行李过来,你需要做的只是盯着看有没有那个特定标记的箱子。你不会先把所有行李都搬到旁边的空地上,再一件一件重新看一遍。直接看传送带才是效率最高的方式。

2.2 C语言版本与逐行讲解

C语言是参加天梯赛最常用的语言之一,对输入输出的控制最直接。下面是完整的可提交代码:

#include <stdio.h> int main() { int x, pos = 1; while (scanf("%d", &x) != EOF) { if (x == 250) { printf("%d\n", pos); break; } pos++; } return 0; }

逐行拆解一下。pos = 1表示当前读到的这个数是序列中的第几个,从1开始。while (scanf("%d", &x) != EOF)是循环的关键,scanf每次从标准输入读一个整数,成功读到一个就返回1,读不到任何数据时就返回EOF。这个写法既处理了输入结束,又不需要事先知道总数。循环体内先判断当前数是否是250,是就立刻输出pos并break;不是则pos++,继续读下一个数。

这里有个小知识点值得说:scanf读取整数时会自动跳过空格、换行、Tab这些空白字符,所以输入里的空格你完全不用手动处理。很多新手会想着写一句scanf("%d", &x)之后再用getchar()去吃掉一个空格,这是多余的,甚至可能因为换行符的问题引入诡异Bug。记住,scanf配合%d,已经帮你把空白符全部过滤干净了。

2.3 C++版本与cin的bool语义

C++写法更简洁,也是我最推荐的比赛入门语言:

#include <iostream> using namespace std; int main() { int x, pos = 1; while (cin >> x) { if (x == 250) { cout << pos << '\n'; break; } pos++; } return 0; }

while (cin >> x)能作为循环条件,本质是因为C++的operator>>重载后返回的是istream对象,而istream在布尔语境下会被转换成bool类型,当读取到文件末尾时转换结果为false。所以这个循环的语义和C语言里的scanf != EOF完全等价,但写起来更自然。

输出那句我特意用了'\n'而不是endl。两者看起来都是换行,但endl除了换行还会强制刷新输出缓冲区,在大量循环输出时性能比'\n'差一些。虽然这道题只输出一个数,差别可以忽略,但养成用'\n'的习惯是好事。比赛环境下,任何无谓的性能浪费都该避免。

2.4 Python版本与sys.stdin逐行读取

Python写这道题也有天然的简洁优势:

import sys pos = 1 for line in sys.stdin: for x in line.split(): if int(x) == 250: print(pos) exit() pos += 1

for line in sys.stdin会把标准输入按行迭代。每一行拿到之后,用split()按空白分割成字符串列表,再逐个转成整数。注意这里不能用字符串直接和数字比较,'250' == 250永远为False,必须先int(x)。

如果你确定输入就是一行,也可以写得更紧凑:

import sys nums = list(map(int, sys.stdin.read().split())) pos = 1 for x in nums: if x == 250: print(pos) break pos += 1

sys.stdin.read().split()会把整个输入按空白拆成若干段,再用map(int, ...)一次性全转成整数。这种写法更短,但我个人建议初学者用第一种逐行读取的写法,因为它在处理超大输入时更节省内存,而且流式思维和C/C++版本的逻辑也更一致。

2.5 为什么说这道题根本不需要数组

有些同学一看到“一串正整数”就条件反射地想开数组。但你再仔细想一下:找第一个250需要记住前面所有数字吗?不需要。你只需要知道“当前读到的是第几个数”以及“这个数是不是250”,一个位置变量足矣。

不存数组带来的额外好处是空间复杂度只有O(1)。就算250出现在第100万个位置,程序也只用几个变量,不会因为开数组的大小问题翻车。这种“能不存就不存”的优化意识,在算法题里叫“流式处理”,在以后处理大规模数据时非常重要。很多人的代码写得慢,不是因为算法难,而是因为下意识地存了很多根本用不到的数据。

3. 实操验证:从本地跑通到在线提交

代码写出来是一回事,能在本地正确跑起来、能顺利通过在线评测,是另一回事。这里面的细节很多,尤其是新手第一次面对“无n输入”时,本地测试环节就能卡半小时。我把完整流程走一遍。

3.1 本地编译运行与手动输入EOF

假设你用的是C++代码,文件名叫find250.cpp。先在终端里编译:

g++ find250.cpp -o find250

然后运行:

./find250

此时程序在等待标准输入。你可以手动输入样例数据,比如敲:

888 666 123 233 250 999 250

按回车后你会发现程序并没有立刻输出结果,而是还在那等着。很多人到这里就懵了,以为程序死循环了。其实不是,程序是在等你输入“文件结束标志”。在Linux或macOS终端里,按下Ctrl + D;在Windows命令行里,按下Ctrl + Z再回车。输入流关闭后,scanf返回EOF,循环结束,程序才会正常输出5。

这个知识点极其关键。在线评测系统会直接把测试数据作为完整文件喂给你的程序,所以你感觉不到EOF的存在;但你在本地手动测试时,必须自己告诉程序“输入结束了”。没接触过这个操作的人,很容易在第一步就卡住。

3.2 用管道命令快速测试多个样例

手动按Ctrl + D也能测,但效率太低。更推荐用管道方式把输入直接交给程序:

echo "888 666 123 233 250 999 250" | ./find250

这条命令会把echo输出的字符串作为程序的标准输入。当管道里的数据全部读完时,输入流自然结束,程序自动获得EOF,马上输出结果。以后你写任何需要交互输入的程序,都可以用这种方式做自动化测试。

如果你准备了好几组测试数据,建议把它们写进一个文件,比如test1.txt:

888 666 123 233 250 999 250

然后运行:

./find250 < test1.txt

想验证输出是否符合预期,可以加一句diff对比,这是很多老手都在用的方式。先准备一个expected.txt写上预期的输出5,然后:

./find250 < test1.txt | diff - expected.txt

没有输出就说明结果一致,有输出就会列出差异。

3.3 自定义边界用例,别只测样例

只测题目样例远远不够。一道题真正容易错的地方,往往藏在边界用例里。我建议至少准备下面四个测试,分别验证不同的场景:

测试输入预期输出验证点
2501第一个数就命中,验证pos初值
1 250 2502连续出现多个250,验证只取第一个
1 2 3 4 5 2506250在末尾,验证整个流程走完
250 1 2 31再次验证首位置和break时机

特别是第二组“1 250 250”,很多同学如果没有在找到第一个250时立刻break,而是继续遍历,最终输出的就是3而不是2,一测就露馅。不测这一组,你可能一直不知道自己错在哪。

3.4 在线提交时的三条铁律

本地测完之后就是真正的提交环节。在线评测对输出极其严格,我见过太多人在这一步丢分,原因不是算法,而是输出格式。三条铁律记好。

第一,不要输出任何多余内容。提示语、调试信息、额外的空格,全都不能有。你写的printf("结果是:%d\n", pos);一旦出现了“结果是:”这几个字,评测系统就会判你格式错误。评测比对的基本都是标准输出,不会有人帮你忽略多余文字。

第二,末尾一定要换行。printf("%d\n", pos);里的\n是必要的。有些评测系统比较宽松,差一个换行只是警告,但很多系统会直接判错。永远按“输出一个正整数加一个换行符”来写。

第三,不要依赖本地文件。程序里绝对不能写fopen("input.txt", "r")之类的东西,评测时运行的目录里并没有你的文件,也没人帮你把测试数据放到文件里。所有的输入都从标准输入读取,所有的输出都写向标准输出。

4. 常见问题与排查技巧实录

这一节的内容全部来自我实际带队伍和答疑过程中遇到的真实问题。有些问题很隐蔽,网上题解根本不会提,但杀伤力极大。我把它们整理成对照表,再逐个展开讲。

4.1 高频症状对照表

现象可能原因修复思路
本地输入样例后程序没反应没发送EOF,程序还在等待输入Linux/macOS按Ctrl+D,Windows按Ctrl+Z再回车
输出结果比预期少1位置计数器初始化为0把pos初始化为1
找到第一个250后输出却是后面的位置没有在命中后break,还在继续遍历命中后立刻break或直接return
编译报错C语言代码里用了C++语法,或反之检查文件后缀和编译器选项,代码与语言标准匹配
输出前面有奇怪的多余文字写了printf调试信息没删删掉所有非必要输出

4.2 坑一:本地测试看起来像“死循环”

这个坑几乎每个新手都会踩。你在终端里敲入样例数据,按了回车,程序就是不输出。这时候你以为是代码写错了,检查半天发现逻辑没问题,非常打击信心。真相只是程序还在等待你发送EOF。

记住一个判断技巧:如果程序因为死循环卡住,CPU占用率会很高,而且你敲任何额外的数字它都会有反应(还在scanf中)。如果程序只是在安静地等待EOF,它同样也在scanf中等待,但不会消耗CPU。不管哪种情况,先按Ctrl + D试试。养成习惯:本地测完一个样例,要么用管道输入,要么主动发EOF,不要傻等。

4.3 坑二:输出总是比预期少一位

这个坑我在前面已经提到了,但值得单独再说一次。原因就一句话:计数器从0开始了。为什么有人会从0开始?因为数组下标是0-based,习惯了。但题目里“第几个数”的表述是1-based,翻译到代码里,第一个数的序号必须是1。

我给你的排查办法是:找一个“250出现在第2个位置”的测试用例,比如1 250,如果输出是1,那你的位置计数就少了一位,把pos的初始值从0改成1即可。这个用例能在一秒内将问题定位,比盯着代码发呆有效得多。

4.4 坑三:把空白字符当成数据处理

有个别同学不喜欢用scanf("%d")或cin >> x,觉得太高级,非要自己写一个字符一个字符地读,然后拼数字。一旦走这条路,你就必须自己处理空格和换行。输入里数字之间的空格数量是不确定的,可能是1个,可能是多个,行尾可能有空格也可能没有,最后一行可能没有换行符。你手动处理这些空白,等于给自己埋了一堆判断分支。

正确的做法是把空白符的过滤交给库函数。scanf的%d会跳过所有前导空白,cin >> int会跳过所有前导空白,Python的split()更是把所有空白都视为分隔符。用库函数,你的代码就只需要关心数字本身,不用关心数字周围有什么装饰。

4.5 坑四:数据范围虽然小,但别在类型上栽跟头

这道题里每个输入是正整数,具体范围题目通常有限制,一般不会超过int范围。位置计数也不会大到哪里去,用int完全够。但如果遇到更极端的变式,比如数据量上百万,或者要求你累加和,那时候int可能溢出,就要考虑long long了。

我的习惯是:写题之前先看数据范围。位置变量的范围还可以再看一眼输出格式,是要求输出整数还是浮点数,有没有误差要求。这些习惯都能在L1阶段就培养好,到后面做L2、L3时就能少看几次评测结果才能发现问题。

5. 从“寻找250”延伸出的三个变式练习

这道题本身不难,但把它改一改,就能变成一系列考察相同基本功的题目。如果你已经顺利AC了原题,我强烈建议顺手做一下下面三个变式,它们能帮你把“顺序查找”这个基本模型吃透。

5.1 变式一:找最后一个250

题目稍微改一下:输出最后一个250的位置。思路区别在哪里?原题命中即break,现在不能break了,得遍历完整条序列,每次遇到250就更新答案,最后输出记录的最后一个位置。代码的核心变成:

#include <stdio.h> int main() { int x, pos = 1, ans = -1; while (scanf("%d", &x) != EOF) { if (x == 250) { ans = pos; } pos++; } printf("%d\n", ans); return 0; }

这个变式非常经典,它和原题唯一的区别就是“第一个”和“最后一个”在循环控制上的差异。我能想到的坑是:如果题目保证至少有一个250,那么ans一定有值;但如果你改成“可能没有250”的情况,就得考虑ans的初始值该表示什么。很多时候题目会约定找不到时输出什么特殊值,读题时要格外注意。

5.2 变式二:统计250出现了几次,并输出所有位置

这个变式要求你输出250出现的所有位置,那就不能只用一个位置变量从头走到尾了,因为你必须把每个命中的位置都记住。这时数组或vector就派上用场了:

#include <iostream> #include <vector> using namespace std; int main() { int x, pos = 1; vector<int> ans; while (cin >> x) { if (x == 250) { ans.push_back(pos); } pos++; } cout << ans.size() << '\n'; for (int i = 0; i < ans.size(); i++) { if (i) cout << ' '; cout << ans[i]; } cout << '\n'; return 0; }

这里要注意输出格式的细节:一般题目会要求多个数字之间用空格分隔,行尾不能有多余空格。我看到不少人在这个格式上栽跟头。处理办法就是我上面写的,在循环里判断i是否为0,不是0就前面加空格,这样最后一个数后面就没有多余空格了。这个技巧在L1阶段会反复用到,建议直接背下来。

5.3 变式三:输入的不是一串数字,而是一整段文本

再改改:输入一行字符串,可能包含字母、数字、空格和标点,让你找第一个出现的连续子串“250”的位置。这就从数字比较变成了字符串匹配。C语言里可以用strstr找子串首次出现的位置,C++里可以用string::find,Python里可以直接用str.find('250')。

思路虽然不同,但“找第一个出现位置”的框架和原题一脉相承。这个变式是想提醒你:天梯赛L1题目看着千奇百怪,但很多题目内核都差不多,区别只在于数据形态从整数变成了字符。你如果能把原题的模型吃透,遇到字符串版本时就不会慌。

5.4 从这道题里提炼的四个基本功

原题一共训练了四个可迁移的基本功,我总结成一句话:无n输入时的EOF判停、不存储的流式处理、1-based位置计数、命中即停的提前返回。这四个习惯在后面的L1题目里几乎题题都能用上。

比如做数组元素查找类的题,你会用到“命中即停”和“位置计数”;做字符串题时,你会用到“EOF判停”;做数据处理模拟题时,你会用到“流式处理”。把这些基本功练成肌肉记忆,比刷十道题背十个模板要实在得多。我一直跟身边同学说,L1阶段最值得做的就是这类“小题目”,因为它们的考点虽然简单,但都是后面复杂题目的地基。

我个人在实际操作中的体会是,这道题最值得琢磨的其实不是代码,而是读题习惯。很多人被“250”逗笑,忽略了“第几个”是从1开始,也忽略了输入没有显式结束标志。刷题初期不怕题目简单,怕的就是把简单题做错后不复盘。每做完一道L1题,我都会逼自己回答三个问题:题面里有没有容易被忽略的限制条件?循环的终止条件是否依赖EOF或特殊值?输出格式是不是严格符合要求?把这三个问题变成习惯,后面打比赛的路会顺畅非常多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询