只要在洛谷刷过入门题单,十有八九会撞到 P1427 这道题。题面很短,短到很多新手扫一眼就动手:输入一串整数,以 0 结尾,把 0 之前的这些整数倒着输出。可别小看这道“水题”,我在给身边人答疑时见过太多种翻法——有人把结束标志 0 也输出了,有人把数组开成 a[100] 却忘了下标从 0 开始,也有人用递归时把输出写在递归调用之前,结果输出的还是正序。这篇博文不打算只贴一份能通过的代码,而是把这道题背后的“为什么”掰开讲清楚:为什么倒着输出要用到栈的思路,为什么递归也能做,以及提交到洛谷之前有哪些细节值得确认。适合刚学数组、刚准备开始刷题的朋友阅读,算法老手可以直接跳到第 5 节看延伸玩法。
1. P1427 到底考的是什么:先读懂题面的三种信息
1.1 “以 0 结尾”是一句需要拆解的约束
很多人在做入门题时不会逐字读题,但竞赛题里每句话都可能有坑。P1427 这句话的意思是:输入不一定在一行,也不一定会提前告诉你总共有多少个数字,而是用一个特殊值 0 来充当“到此为止”的哨兵。读到 0 之后,后面的内容不用再管。举个最简单的例子:如果输入是 3 8 5 0,那么有效数字只有 3 8 5,输出应当是 5 8 3。如果题目不保证 0 后面没有内容,严谨的程序也应该读到 0 就停止,不再处理后面的 9。
这种“哨兵值”在很多输入场景里都会出现,比如链表结尾的 NULL、字符串结尾的 \0,本质上都是用特殊标记代替“数量”。明白这一点之后,你就不容易犯“把 0 也当成有效数字”的错误。很多初学者看到一长串输入就直接用 for 循环固定次数读取,那是因为习惯了“先给 n 再给数据”的题目;遇到 P1427 这种“哨兵式输入”,代码结构必须改成 while 加 break。
1.2 倒序输出的本质是“后进先出”
如果输入顺序是 a1 a2 a3 ... an,要求的输出顺序是 an ... a3 a2 a1,也就是最后一个读到的数字最先输出。这与现实中的栈完全一致:盘子一个个往上放,拿走的时候先拿最上面的。于是 P1427 就成了很多人接触“栈”概念的启蒙题。当然,题目本身并不强制你用栈,数组加循环也能轻松完成;但如果你能从一开始就把“倒序输出”和“后进先出”联系起来,后面学括号匹配、表达式求值、DFS 回溯输出路径时会省力很多。
1.3 为什么这道题适合新手入门
它同时涉及了循环输入、条件判断、数组下标操作、逆序遍历四个基础能力。很多人写循环写得顺,但一遇到“不知道输入多少个”就发怵;这道题正好补这个盲区。另外它的数据范围很小,不会出现超时、爆栈这些问题,因此你可以放心地把精力放在“理解思路”而不是“极限优化”上。换句话说,这是一道用来建立信心的题,也是用来发现自己细节短板的题。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 三种解法与背后的实现逻辑
2.1 数组法:最直白的“先存起来再说”
思路:开一个数组,循环读入数字,如果是 0 就停;否则存进数组,下标加一。输入结束后,从最后一个下标往前遍历,依次输出。
cpp复制#include <bits/stdc++.h>
using namespace std;
int a[105];
int main() {
int x, n = 0;
while (cin >> x) {
if (x == 0) break;
a[n++] = x;
}
for (int i = n - 1; i >= 0; --i) {
cout << a[i];
if (i > 0) cout << ' ';
}
cout << '\n';
return 0;
}
这段代码有三个关键点。一是 n++ 先取下标再自增,所以第一次读到的数字放在 a[0],读完后 n 恰好等于数字个数。二是 for (int i = n - 1; i >= 0; --i) 中必须写成 i >= 0,写成 i > 0 会漏掉第一个数,写成 i < n 的人更是直接顺序输出了。三是输出空格时我用 if (i > 0) 判断,让最后一个数字后面不带空格;如果只写 cout << a[i] << " ",OJ 通常也不判错,但养成不留尾空格的习惯更稳。
数组法的复杂度是 O(n) 时间、O(n) 空间。对这道题来说绰绰有余。唯一需要留意的就是数组容量,稍后在第 4 节详细说。
2.2 栈:顺着“后进先出”的自然表达
既然我们已经发现倒序输出就是后进先出,那么直接使用栈这种数据结构,代码会更贴近思路本身。
cpp复制#include <bits/stdc++.h>
using namespace std;
int main() {
stack<int> s;
int x;
while (cin >> x) {
if (x == 0) break;
s.push(x);
}
while (!s.empty()) {
cout << s.top();
s.pop();
if (!s.empty()) cout << ' ';
}
cout << '\n';
return 0;
}
读到一个数就压栈,读到 0 后停止;输出时不断取栈顶,然后弹出。注意栈顶元素是最后一次压进去的,因此第一次输出的一定是最后一个有效数字。很多初学者会问:“既然数组也能做,为什么还要学栈?”我的回答是:如果题目一开始就明确告诉你最多 100 个数,数组完全够用;但如果输入规模不确定,或者以后做表达式求值这类题时,栈可以帮你省掉一堆下标计算。用这道入门题把栈的 push、pop、top、empty 四个操作练熟,性价比很高。
2.3 递归:不建数组,用系统栈完成倒序
还有一类更巧妙的做法:递归。递归的本质是函数一层层调用,系统会用栈记录每一层的局部状态;等到递归返回时,后调用的函数先返回,这本身就是逆序。
cpp复制#include <bits/stdc++.h>
using namespace std;
void reverseRead() {
int x;
cin >> x;
if (x == 0) return;
reverseRead();
cout << x << ' ';
}
int main() {
reverseRead();
cout << '\n';
return 0;
}
这个写法读入 x 后不马上输出,而是先递归调用自己,去读下一个数字;直到读到的这一层数字是 0,递归开始一层层返回,这时才会执行之前那些 cout << x 语句。举例来说,输入 3 8 5 0,调用过程是先读到 3,再去读 8,再去读 5,再去读 0 之后开始返回,于是依次输出 5、8、3。代码短是它最大的优点,但缺点也很明显:如果输入的数字很多,递归层数深,程序可能栈溢出。本题目数字个数通常不超过 100,递归完全没问题;换成百万级数据就必须用循环栈了。
2.4 三种方案怎么选
我通常建议新手优先掌握数组法,因为这是后面几乎所有题目的通用能力;第二遍练习时再试试栈和递归,这样一道题能顶三道题用。三种方案的时间和空间复杂度都是 O(n),没有本质差别,差别只在于代码可读性和适用场景。如果你想要一个看起来更“专业”的答案,直接提交栈版本也不会错;但我的经验是,练习阶段最好别只满足于“过了”,要能解释得清边界条件为什么这样写。
3. 多语言实现与提交细节
3.1 C 语言的输入输出姿势
如果你用 C,套路差不多,只是读入用 scanf,输出用 printf:
c复制#include <stdio.h>
int a[105];
int main() {
int x, n = 0;
while (scanf("%d", &x) == 1 && x != 0) {
a[n++] = x;
}
for (int i = n - 1; i >= 0; --i) {
printf("%d%c", a[i], i ? ' ' : '\n');
}
return 0;
}
注意 scanf("%d", &x) == 1 表示成功读入了 1 个整数,这样写比直接 while (scanf("%d", &x) != EOF) 更严格,能避免读入失败时还执行循环体。还有一个细节:%d 输入时会自动跳过空白字符,因此无论这些数字是在同一行还是跨了多行,scanf 都能连续读取,不需要自己处理换行。printf 里的 %c 我用了三目运算符,最后一个数字后面输出换行,其他数字后面输出空格,这样输出格式和样例保持一致。
3.2 Java 提交时要注意类名和输出拼接
Java 提交到洛谷时,最容易被忽略的是代码最外层类名必须叫 Main,否则编译会直接报错。下面是基于数组的参考实现:
java复制import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int[] a = new int[105];
int n = 0;
while (in.hasNextInt()) {
int x = in.nextInt();
if (x == 0) break;
a[n++] = x;
}
StringBuilder sb = new StringBuilder();
for (int i = n - 1; i >= 0; --i) {
if (sb.length() > 0) sb.append(' ');
sb.append(a[i]);
}
System.out.println(sb);
}
}
我不建议在高频循环里用 System.out.print 逐个打数字,因为频繁输出会产生额外开销,入门题无所谓,但以后做大数据量题目会吃亏。比较规范的做法是先用 StringBuilder 拼好整行,再一次性 println。sb.length() > 0 这个判断也能自然地避免末尾空格,思路和 C++ 里的空格标记完全一致。
3.3 Python 的两种常见写法
Python 写法最简洁,但要注意“一行读完”和“跨行读取”的差别。如果测评数据全部放在一行里,直接这样做即可:
python复制nums = list(map(int, input().split()))
nums.pop()
print(' '.join(map(str, reversed(nums))))
所有数字读完后,最后一个一定是 0,用 pop() 去掉它,剩下就是有效数字。但对于输入可能跨行的情况,我一般写一个生成器逐行处理:
python复制import sys
def read_numbers():
for line in sys.stdin:
for x in map(int, line.split()):
yield x
arr = []
for x in read_numbers():
if x == 0:
break
arr.append(x)
print(' '.join(map(str, arr[::-1])))
这段代码的好处是遇到 0 立即停止,就算后面还有多余内容也不会被读进数组。arr[::-1] 是切片逆序,map(str, ...) 把每个整数转成字符串,最后用空格拼接,格式不会出错。如果你已经比较熟悉 Python,也可以尝试用 arr.reverse() 原地反转后再输出,效果一样。
3.4 要不要纠结 IO 性能
很多题解里会写 ios::sync_with_stdio(false); cin.tie(nullptr);,目的是让 C++ 的 cin/cout 与 C 标准库不同步,从而提速。P1427 的数据量很小,不写也完全能过;但如果你做数据量大的题目,最好记住这两行。同理,Java 的 Scanner 在十万级以上的输入会变慢,可以用 BufferedReader 替代,但这个阶段不用过度优化。先把逻辑写对,再考虑速度。
4. 新手最容易踩的坑:P1427 提交避雷指南
4.1 结束标志 0 到底要不要输出
这是最常见的错误。有人看到样例输出里只有数字,不会犯;但自己写的时候,容易把循环写成 while (cin >> x) { a[n++] = x; } 然后忘记遇到 0 时 break,最后把 0 也压进数组,输出时就多了一个 0。我在帮别人 debug 时经常遇到这种版本:题目要求“不包括最后一个 0”,但代码把 0 当成了普通数字。想避免这个问题,读入循环里第一件事就是判断 if (x == 0) break;,再决定要不要存入数组。栈法也一样,压栈之前先判断。
4.2 数组容量到底开多大
一般题目会说“数字个数不超过 100”或类似范围。如果开 a[100],下标是 0 到 99,刚好能装下 100 个数字吗?如果有效数字正好 100 个,能装下,因为最后一个有效数字放在 a[99]。但很多新手会把 n 当成“需要申请的容量”,看到 n <= 100 就开 a[100],然后在代码里 a[n] 操作,越界了。稳妥的做法是开 a[105] 或更大,多出来的几个位置不花什么成本,却能避免焦虑。数组索引从 0 开始的基本功,说多重要都不夸张。
4.3 末尾空格和换行的区别
大多数 OJ 的判题程序会忽略行末空格,所以输出 5 8 3 (末尾多个空格)一般也能 AC。但有些题目或平台会严格比对,而且输出了多余空格会让你自己看样例时觉得奇怪。我习惯在循环里加一个“是否第一个输出”的判断,或者用前面的 if 控制。至于换行,最后的 cout << '\n' 或 puts("") 是加分项,不是强制项,做了总比不做好。这里的关键是:不要在数字之间多加一个逗号或分号。
4.4 cin 和 scanf 混用的隐患
C++ 里 cin/cout 默认会和 C 的 scanf/printf 保持同步,混用一般不会出太大问题;但你要是写了 ios::sync_with_stdio(false),再混用 C 的输入输出,顺序就可能错乱。建议要么全用 C++ 风格,要么全用 C 风格。对这道题,没有任何必要混用,选一个顺手即可。这个习惯从新手期养成,后面处理大量输入时会少掉很多玄学问题。
4.5 常见错误速查表
我把 P1427 里最常遇到的几类问题整理成一个表,方便你在本地自查:
| 错误现象 | 可能原因 | 解决思路 |
|---|---|---|
| 输出多了个 0 | 循环没判断终止值 | 读到 x 后先判断 x == 0,再决定是否入数组/栈 |
| 数组越界 | 容量开太小 | 根据题面范围多开 3~5 个元素 |
| 输出顺序反了 | 循环正向输出 | 倒序遍历,或使用栈 |
| 末尾带空格 | 输出语句直接拼接 | 用分隔标志或判断下标 |
| 提交编译失败 | 类名/主函数名不规范 | Java 类名固定写 Main,C++ 必须有 main 函数 |
4.6 搜索编号别搜出幻觉
最后说个题外话。有不少人找题解时会搜“P14258 题解”,这可能是因为 P1427 和 P1428 两题长得像,被并在一起输入。P1428 是“小鱼比可爱”,是让小鱼统计左边有多少条鱼比自己可爱,和本题倒序输出不是一回事。如果你搜索洛谷题解,建议完整输入“P1427 小鱼的数字游戏”,或者直接进原题页面看题号。别小看这种细节,搜索能力在刷题过程中真的会影响效率。
5. 从 P1427 延伸出去:一道入门题能带出多少东西
5.1 逆序输出的核心限制:输入流无法回退
标准输入流是单向的,正常情况下你不能“倒回去”再读一遍之前的数字。所以想倒序输出,你必须把已经读到的内容留下来,留到哪里都行——数组、栈、递归的系统栈,本质上都是一种存储。如果不允许用额外数据结构,只能在读完之后,用双指针把数组原地反转,再正序输出。理解了这一层,你就能明白为什么“倒序输出”和“栈”是天然绑定的:不是题目要求你用栈,而是倒序本身就暗含了后进先出的顺序。
5.2 哨兵值输入与“给定 n”输入的区别
以 0 结束是“哨兵式输入”,常见于字符串(用 \0 结尾)、链表(用 NULL 结尾)和竞赛题(用 -1、0 等特殊标记)。另一种输入模式是“第一行给一个 n,后面再给 n 个数”,两者的代码结构完全不同。前者适合 while (cin >> x && x != 0),后者适合 cin >> n; for (int i=0; i<n; ++i) cin >> a[i];。能把两种模式分清楚,做算法题的底子就稳了一小块。以后读到“以 -1 结束”“以空行结束”时,处理逻辑也是同一套思路。
5.3 后续适合顺势刷的几类题
做完 P1427 后,如果觉得不过瘾,可以顺着栈的方向去找几道经典练习,比如括号匹配、括号序列合法性判断、单调栈求下一个更大元素。另外一个常见搜索词是“洛谷动态规划题单”,有些刚入门的同学想直接挑战 DP,其实不太建议;像 P1427 这种数组和顺序问题都还没理顺的话,直接上 DP 很容易打击信心。先把基础题稳定吃透,再一层层往上走,后面的题单会更容易接住。
5.4 用一组测试数据自测
我在本地写完代码后,一定会准备几组数据自测:
- 单个有效数字:输入
7 0,输出7 - 只有一个 0:输入
0,输出为空行 - 有负数的情况:输入
-1 2 0,输出2 -1 - 跨行输入:第一行
1 2,第二行3 0,输出3 2 1
很多时候你以为代码逻辑没问题,跑一下边缘样例就露馅了。用这四组测试,基本能把数组、循环、终止条件的问题都覆盖到。
最后分享一个我的小习惯:做这道题时我会把三种解法都写在同一个文件夹里,然后逐个提交,看洛谷给出的“通过”状态。不是为了炫技,而是想让自己记住“同一道题可以有多种解法”的感觉。很多算法题不是只有唯一解,早一点习惯从多个角度想问题,后面刷题会越来越顺。如果你正卡在某个入门题上,不妨先放下“抄题解”的念头,按这个思路画一下输入输出流程,八成就能自己写出来了。
