1. 题目理解与核心思路
1.1 一句话看懂 P1427 到底在考什么
洛谷 P1427“小鱼的数字游戏”是一道经典入门题。题目讲的是小鱼最近迷上了一款数字游戏,会输入一串数字,以 0 结尾,要求把这串数字逆序输出。说白了,就是一个“输入一串以 0 结尾的整数,将它们反向打印出来”的题。
如果你刚开始刷洛谷,这道题几乎是必刷的。它不像后面那些动态规划题单里的题目(比如 P7074、P1792 那种让人挠头的问题),P1427 就是最基础的数组遍历、逆序输出的练手题。但它有个小坑:输入的数字个数是不确定的,以 0 作为结束标志,0 本身不参与逆序输出。这个“以 0 结尾”的处理方式,在 OI(信息学竞赛)题目里非常常见,是一种典型的“哨兵值”设计。
我第一次刷这道题的时候,其实犯了个低级错误:把 0 也存进数组,然后逆序输出时连 0 一起打印了。后来仔细读题才发现,要求是“当读到 0 时结束输入”,也就是 0 只是一个终止标记,并不算在有效数据里。这个细节,就是这道题给新手挖的第一个坑。
1.2 数据范围与输入输出格式,先搞清楚再动手
很多新手一上来就写代码,结果连题目给的输入输出格式都没看清。P1427 的输入是一行若干个整数,用空格隔开,最后以 0 结束。每个数字都是大于 0 的整数(至少样例里是这样),但题目并没有明确说数字的上限,所以稳妥起见,按照 C++ 的 int 范围来存就够了。
输出要求是:将这串数字逆序输出,仍然用空格隔开。注意最后一个数字后面有没有多余空格?洛谷对这个问题通常不卡,但为了养成良好的输出习惯,我建议控制好空格的处理方式:要么第一个输出前不加空格,要么最后一个输出后不加空格,别养成“无脑在后面跟空格”的坏习惯。有些题目的严格判题器会对行末空格敏感,虽然 P1427 实测不敏感,但习惯要趁早养好。
样例是这样的:
text复制输入:
1 2 3 4 5 0
输出:
5 4 3 2 1
就这么简单。但越简单的题,越能看出一个人写代码的规范程度:数组开多大、循环边界怎么写、0 怎么排除、空格怎么控制。这些细节,恰恰是后面做 P1428、P1928 那些题目时同样要用到的基本功。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 多种解法与代码实现
2.1 解法一:数组存储 + 逆序遍历(最直观的思路)
这是最符合直觉的写法:先开一个数组,把读到的每个数字存进去,读到 0 就停止。然后从数组末尾往前遍历,逐个输出。
C++ 写法如下:
cpp复制#include <iostream>
using namespace std;
int a[105]; // 题目没说多少个数字,开大一点,比如 105 足够
int main() {
int n = 0;
while (cin >> a[n] && a[n] != 0) {
n++;
}
for (int i = n - 1; i >= 0; i--) {
cout << a[i];
if (i > 0) cout << " "; // 控制空格,最后一个数字后不输出空格
}
cout << endl;
return 0;
}
这里有几个点值得展开:
第一,数组开多大? 题目没有明确说明最多会输入多少个数字,但作为入门题,数据量很小。开 105、1005 都行。有的同学喜欢开 100000,也可以,但没必要。如果以后做动态规划题单里的题,比如 P7074,那个数据量可能到 10^5 甚至更大,数组就要按题目给的范围精确开。P1427 这种题,开个 105 绰绰有余,但如果你实在不放心,开 1005 也不扣分。这只是内存占用大小的问题,不是正确性问题。
第二,while 循环的写法。 cin >> a[n] && a[n] != 0 这个写法很紧凑:先读一个数存到 a[n],然后判断它是不是 0。如果不是 0,n 自增,继续读;如果是 0,循环结束。注意这时候 0 已经存在 a[n] 里了,但 n 没有自增,所以后面逆序遍历时根本不会碰到它。这个细节很妙,也特别容易写错——如果你写的是 while (cin >> x && x != 0) { a[n++] = x; },那就更直观了,0 根本不会进数组。两种写法都可以,看个人习惯。
第三,空格输出控制。 if (i > 0) cout << " "; 意思是:只要当前输出的不是最后一个数(即不是数组第一个元素),就输出一个空格。这样输出的结果是“5 4 3 2 1”,最后一个 1 后面没有多余空格。有的同学喜欢在循环里写成 cout << a[i] << " ",结果输出变成“5 4 3 2 1 ”,末尾多了一个空格。这道题能过,但到了某些卡格式的题目可能就 WA(Wrong Answer)了。
2.2 解法二:递归实现(理解栈的天然特性)
递归其实是非常契合这道题思路的一种写法。你想,逆序输出本质上就是“递归到最深处,然后回溯时输出”——这正好利用了系统栈(调用栈)的天然后进先出特性。
C++ 递归写法:
cpp复制#include <iostream>
using namespace std;
void solve() {
int x;
cin >> x;
if (x == 0) return; // 读到 0,递归终止
solve(); // 先递归读后面的数
cout << x << " "; // 回溯时输出当前数
}
int main() {
solve();
cout << endl;
return 0;
}
是不是很惊艳?这段代码只有几行,但逻辑非常漂亮:每次读一个数,如果不是 0,就先递归调用自身,等后面的数全部处理完,再输出当前的数。这样最先生效的数字反而最后输出,实现了逆序。
我当初第一眼看到这个写法时,简直拍案叫绝。递归也好、栈也好,本质上就是“先进后出”。这个思路如果能理解透,对后面做表达式求值、括号匹配这类题都非常有帮助。但要注意,递归有深度限制,如果输入数据量特别大(比如几万个数),递归深度过深可能导致栈溢出。P1427 的数据量小,递归没问题;但如果你去写输入 10 万个数字的版本,递归就不合适了,还是用数组或栈更稳。
2.3 解法三:C 语言和 Python 的对应实现
很多同学在洛谷上会同时用 C 和 Python。C 语言写法跟 C++ 几乎一样,只是把 cin/cout 换成 scanf/printf:
c复制#include <stdio.h>
int a[105];
int main() {
int n = 0;
while (scanf("%d", &a[n]) && a[n] != 0) {
n++;
}
for (int i = n - 1; i >= 0; i--) {
printf("%d", a[i]);
if (i > 0) printf(" ");
}
printf("\n");
return 0;
}
Python 写法更简洁。Python 的列表(list)天生支持切片逆序,一行就搞定了:
python复制nums = list(map(int, input().split()))
# 找到第一个 0 的位置,截取有效部分
idx = nums.index(0)
result = nums[:idx][::-1]
print(" ".join(map(str, result)))
这里 nums.index(0) 会返回列表中第一个 0 的下标,nums[:idx] 取出 0 之前的所有数字,[::-1] 实现逆序。最后用空格拼接输出。
不过 Python 版本有个小隐患:如果输入数据非常大,一次性读入并 split 可能会占比较多内存。但 P1427 的数据量很小,完全不用担心。如果你是 Pythoner,平时刷洛谷,这种写法没毛病。
Java 写法也不难:
java复制import java.util.ArrayList;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
ArrayList<Integer> list = new ArrayList<>();
while (true) {
int x = sc.nextInt();
if (x == 0) break;
list.add(x);
}
for (int i = list.size() - 1; i >= 0; i--) {
System.out.print(list.get(i));
if (i > 0) System.out.print(" ");
}
System.out.println();
sc.close();
}
}
ArrayList 动态扩容,不需要提前指定长度,比数组更灵活。如果输入数字特别多,ArrayList 也不会越界。这个写法放在 Java 面试里也是常见的处理方式。
3. 边界情况、易错点与调试经验
3.1 最容易踩的四个坑,我全踩过
第一个坑:把 0 输出出来。 这是 P1427 最典型的错误。很多新手写循环时判断条件写成了 while (cin >> x),没有判断 x 是否为 0,结果 0 被存进了数组,逆序输出时 0 排在最后,输出变成“5 4 3 2 1 0”。洛谷的评测机会直接给你判 WA。解决方法是写入循环条件时就把 x == 0 的情况排除掉。
第二个坑:数组越界。 如果数组只开了 10 个,但输入了 20 个数,程序就崩了。P1427 的数据量比较友好,不会给你来个几千个数,但你要是开 int a[3]; 这种极小数组,撞上数据多一点的测试点就会越界,产生不可预料的错误。我建议是:数组空间宁大勿小,但也不要大到离谱。 根据题目描述合理推断,开 105 或 1005 都是稳的。
第三个坑:循环边界多一位或少一位。 逆序遍历的循环条件是 for (int i = n - 1; i >= 0; i--)。很多同学写成了 i > 0,结果第一个数字丢了;也有写成 i >= 1 的,同样漏掉输出。这是一种非常低级但很容易犯的 off-by-one(差一错误)。我个人的排查习惯是:把样例套进去手算一遍,n=5 时,i 从 4 减到 0,循环执行 5 次,正好输出 5 个数。如果你不确定,就在纸上写一下循环过程,别急着提交。
第四个坑:行末空格。 前面已经提过。洛谷对 P1427 的行末空格不敏感,但你不知道未来做的题目会不会严格。在 P1427 就养成控制空格的意识,后面做其他题时会舒服很多。
3.2 样例过了就真的能 AC 吗?说说测试这件事
很多初学者刷题有个坏习惯:样例测过了,直接提交,被判 WA 后又一脸懵。实际上,洛谷的评测数据远不止题目给出的那一个样例。P1427 这种题,评测数据会覆盖各种情况:
- 只有一个数字就结束,比如输入
5 0,输出应该是5。 - 输入含有多个空格或换行,比如数字之间用换行分隔。
- 数字可能是大的整数,比如
123456 789 0。
所以写完代码后,建议自己编几个边界测试:
| 输入 | 期望输出 | 说明 |
|---|---|---|
5 0 |
5 |
最小规模:只有一个有效数字 |
0 |
(无输出,仅换行) | 极端情况:直接输入 0,没有任何有效数字 |
100 200 300 0 |
300 200 100 |
较大数字的逆序 |
1 0 2 0 |
1 |
这种情况题目没明确说明,但输入以 0 结尾,按逻辑只处理第一个 0 之前的数据 |
特别注意“直接输入 0”这种情况。如果你的代码是用数组存再逆序输出的写法,n = 0,循环 for (int i = n - 1; i >= 0; i--) 会从 -1 开始,直接不执行,输出一个换行就结束了。这没问题。但如果你用的是递归写法,读到 0 直接 return,也不会输出任何数字。都合理。
另外,关于“输入以 0 结尾”的理解,有的同学会问:如果输入是 1 2 0 3 0,后面的 3 算不算?按照题目描述“当读到 0 时结束输入”,程序在第一次读到 0 时就已经停止读取了,后面的 3 0 不会被读入。测试数据里通常不会出现这种歧义情况,但明白这个逻辑有助于你理解“哨兵值”的设计理念。
3.3 为什么“以 0 结尾”是哨兵值设计?
很多同学对这个“以 0 结尾”有点摸不着头脑:为什么不直接告诉我有多少个数字呢?这其实就是 OI 题里常见的“哨兵值”(sentinel)设计思路。程序无法预知用户会输入多少个数,所以用一个不可能出现在正常数据中的特殊值来标记数据结束。
这种设计在文件读取中也经常见到:比如读取一个文件直到 EOF(End of File),EOF 本质上就是一个特殊标记。C 语言里 getchar() 返回 EOF(通常为 -1)表示文件结束,也是哨兵思想。你以后会碰到更多以“输入若干个字符,以 # 结束”“输入直到文件结束”等变体,都是同一个套路。
理解了哨兵值的原理,碰到这类题就不会慌了:判断条件放在读取之后,一旦读到哨兵就 break,而且绝不把哨兵当作有效数据。
4. 从 P1427 出发,说说洛谷刷题的正确姿势
4.1 洛谷的题解区怎么用才能事半功倍?
P1427 作为一道红题(入门难度),题解区里充斥着各种奇奇怪怪的写法:有人用链表,有人用 vector 的 reverse 迭代器,还有人用 bitset 硬搞。我见过最离谱的一个题解,用 100 行代码实现一个栈,就为了逆序输出几个数。我建议你:先看题解,但别看太早。
什么意思?拿到一道题,先自己思考 20 到 30 分钟。实在想不出来,再去看题解。题解的作用不是让你抄代码,而是让你理解别人的思路。尤其像 P1427 这样的入门题,每篇题解都能展示一种不同的思路:数组逆序、递归回溯、栈结构、Python 切片……你在入门阶段接触的解法越多,后面做题的思维就越开阔。
我自己的习惯是:三种主流解法都写一遍。先用数组写一版,AC 了;再用递归写一版,感受一下函数调用的过程;最后用 Python 写一版,看看有没有更简洁的写法。这个过程花费不到半小时,但收获远大于提交一次就完事。
另外,洛谷的“讨论”区也值得逛。有时候你 WA 了,卡了很久不知道哪里有问题,去讨论区搜一下,可能别人早就问过同样的问题,底下还有管理员或者大佬的解答。搜索关键词可以带题目编号,比如搜“P1427 为什么 WA”“P1427 格式错误”,效率比一个人干想高得多。
4.2 从这题开始,建立你自己的刷题清单
刷题这件事,最忌讳的就是东一榔头西一棒槌。今天刷一道 P1427,明天刷一道 P1792,后天又跳到动态规划题单……这样看似在刷题,实际效果很差,因为知识点是零散的,没有形成体系。
我建议你按照难度梯度来。
第一步,把洛谷的【入门】题目过一遍。P1427 就是这一阶段的典型代表,还有 P1001(A+B Problem)、P1000(超级玛丽游戏)这些。这一阶段的核心目标是:熟悉 OJ 的输入输出机制,掌握基本的循环、条件判断、数组操作。
第二步,进入【顺序结构】【分支结构】【循环结构】题单。这些题单是洛谷官方整理的,每道题对应一个语法知识点。刷完这些,你的 C++/Python 基础语法就没什么大问题了。
第三步,再碰【数组】和【字符串】题单。P1427 可以作为数组题单的起点。这之后你可以尝试 P1428(小鱼的游泳课?还是小鱼比可爱?我记得有一道 P1428 是小鱼比可爱),那道题跟 P1427 都带“小鱼”前缀,但考的是数组相邻比较,正好进阶了一下。
第四步,才轮到【算法初步】,比如排序、枚举、模拟。动态规划题单(P7074、P1792 这些)是后面的后面的事,别一上来就挑战动态规划,容易劝退。
我还要多说一句:我发现评论区经常有人问“洛谷该怎么放数据”或者“洛谷小游戏是什么”。这其实反映出很多新手对 OJ 的本地测试流程不熟。所谓“放数据”,通常指的是自己把测试数据放到程序里跑,也就是手动输入样例,或者用输入重定向(freopen)读文件。“洛谷小游戏”则更多是指洛谷社区里的一些娱乐玩法,跟刷题本身关系不大。新手主次分明就好:核心是提升算法能力,娱乐功能不是重点。
4.3 我刷 P1427 之后总结的三个编程好习惯
借这道入门题,我想分享三个任何阶段都用得上的好习惯。
第一个习惯:写代码前先手推样例。 不要急着敲键盘。在纸上把样例的输入过程、循环过程、输出过程一步步写出来。P1427 这个规模,手推只需要几秒钟,但这个习惯能帮你过滤掉至少一半的低级错误。尤其是 off-by-one 这类问题,手推一遍基本上就能发现。
第二个习惯:多写注释,尤其是关键逻辑的注释。 我记得自己刚刷题时觉得写注释很麻烦,后来回看一个月前写的代码,发现自己都看不懂当时为啥那样写了。P1427 虽然简单,但你在递归版里写一行注释“回溯时输出,从而实现逆序”,再过三个月翻出来,一眼就能回忆起来。这道题玩递归的思路很好,不写注释真的太可惜了。
第三个习惯:AC 之后要复盘。 不要 AC 了就欢呼一下切下一题。想一下:最优的复杂度是多少?我的代码有没有更简洁的写法?如果数据量放大 100 倍,我的写法还能 AC 吗?P1427 数据量小,你直接开数组没问题;但如果数据量是 10^8,你就得考虑用输入时直接处理的方式,或者用 deque。这个“如果数据量放大”的思考习惯,会倒逼你深入理解算法的时间复杂度和空间复杂度。
5. 常见问题与自查清单实录
5.1 WA(答案错误)排查思路
很多新手 WA 了之后完全不知道从哪查起。我分享一下我的排查顺序:
第一步:重新审题。再看一遍题目描述,尤其是输入输出格式和输出要求。P1427 的大部分 WA 都源于“把 0 也输出了”或者“格式不对”。这个步骤能解决大约三成的问题。
第二步:自编边界数据。把“只有一个数”“输入为空”“最大规模数据”等极端情况都测一遍。P1427 的话,重点测 0 和 5 0 这两个输入。
第三步:逐行读代码,重点检查循环边界和判断条件。P1427 这种短代码,一眼就能看完,把 while 和 for 的每一行都读一遍,确认变量 n 的值在循环结束时是什么。很多时候问题就藏在 n 是自增了还是没有自增。
第四步:把代码贴到本地 IDE 里,用调试器逐行跑,监控变量的值变化。这一步在 P1427 上可能用不着,但对更复杂的题目非常有效。
5.2 RE(运行时错误)和 TLE(超时)的可能性
P1427 出现 RE 的概率不高,除非你数组开太小,或者递归深度太大。如果你用 C 语言,还有一种 RE 的可能:scanf 返回值没有判断,导致读入失败后依然访问数组。这些在 P1427 这种简单题里不太会出现,但不代表可以完全忽略代码健壮性。
TLE 就更不可能了。这题时间复杂度 O(n),n 最大可能也就是几十。如果你这题能 TLE,那大概率是你写了死循环,比如 while 循环里忘记改变循环变量的值。比如你把 while (cin >> a[n] && a[n] != 0) 写成 while (cin >> a[n] && a[n] != 0);——分号一加,循环体空了,n 永远不变,输入读完了还在死循环,这不 TLE 才怪。这种低级错误,写代码时注意一下就能避免。
5.3 一份可以直接拿来用的自查清单
| 检查项 | 操作 | P1427 对应的要求 |
|---|---|---|
| 输入格式 | 确认能正确结束读取 | 读到 0 时停止,0 不入数组 |
| 输出内容 | 与样例比对 | 输出不含 0 |
| 输出格式 | 空格、换行符合要求 | 数字间空格分隔,最后无多余空格 |
| 边界数据 | 自测极端情况 | 0 和 5 0 都能通过 |
| 数组空间 | 确认不越界 | 按数据量合理预估,预留安全余量 |
| 循环边界 | 手推一遍循环变量 | 从 n-1 到 0 都要输出 |
每次刷完一道题,对照这张表自查一遍。时间长了,很多问题会变成肌肉记忆,写代码时直接就能避开。
6. 写在最后:一道入门题能带给你的,远不止 AC
P1427 这道题,说难吗?真不难。说简单吗?它的递归解法、哨兵值思想、格式控制细节,每一个点都足够延伸出很多讨论。我见过不少同学刷完这道题就匆匆切走了,但我想说:入门题存在的意义,不只是让你通过它,而是让你通过它建立稳健的编程习惯、理解经典的解题范式。
以这道题为例,你至少可以带走三样东西:第一,如何处理“以特殊值结束输入”的读入模式;第二,数组逆序遍历时如何正确控制循环边界;第三,递归函数天然具备“后进先出”的特性,可以用来解决逆序问题。这三样东西,在你日后做字符串反转、链表逆序、括号匹配、表达式求值等题目时,都会反复碰到。
我个人还特别喜欢 P1427 的一点是,它的题面有故事性——“小鱼最近迷上了一款数字游戏”。这种叙事风格让算法题不那么冰冷。刷题本身是枯燥的,但如果每一道题都当成一个需要解决的小谜题,乐趣会多很多。希望这篇解析能帮你顺利 AC 这道题,也希望能帮你打开刷题的正确姿势。刷题的路很长,P1427 只是第一块小小的里程碑,后面的世界还大得很。
