☰
火车排号问题:用栈模拟判断出站序列合法性与卡特兰数应用
2026/10/19 8:23:39 网站建设 项目流程

火车排号问题最常出现在数据结构期末模拟卷的第二题位置,题面讲的是一个火车编组站只有一条死胡同轨道,编号从 1 到 n 的列车按顺序开进来,让你判断给定的出站序列是否真的能实现。我第一次在试卷上看到这题时,第一反应是"这不是考全排列吗",后来才明白它真正考的是栈——进站顺序固定、只有一个缓冲区、先进去的车会被后进去的车堵住,这其实就是教科书上"栈"的现场版。这篇文章打算把这道题从题面拆解、手推模拟、代码实现到高频变体一次讲透,期末复习的同学可以按这个顺序重新过一遍,正在学栈的大一同学也能把它当成一个完整案例来参考。

1. 题目到底在说什么:一个开进死胡同的列车编组场

1.1 一句人话版题面

题目通常是这样描述的:一个车站只有一条轨道,这条轨道一端是入口,另一端也是出口,也就是说列车只能从同一端进、同一端出,轨道内部没有任何岔路可以超车。

现在有 n 节车厢,编号分别是 1 到 n。它们严格按照编号从小到大的顺序驶向车站入口,也就是先进站的是 1 号,然后是 2 号,再是 3 号,依次类推。但不要求每节车厢一到入口就必须立刻进站,它可以稍微等一等,让后面的车厢先进去;同样,已经进入轨道的车厢也可以选择随时从出口驶出。

题目会给你一个长度为 n 的出站序列,比如 3 2 1 4 5,问你:能不能通过合理安排每节车厢的进站时机和出站时机,让列车最终真的按照这个序列从另一端驶出?

如果能够做到,输出 YES;做不到,输出 NO。

这个例子里的 3 2 1 4 5 是能做到的:1 号先进,2 号进,3 号进,3 号立刻出,2 号出,1 号出,最后 4 号 5 号按顺序直接穿过。整个过程就像一辆接一辆车开进一条死胡同,后面的车不出,前面的车就别想走。

1.2 为什么这道题一定会用栈

把轨道抽象出来,你会发现它完全符合栈的三个特性:后进先出、只能操作顶部元素、容量有限。

进站操作对应 push,出站操作对应 pop,而"当前停在轨道出口处的车厢"就是栈顶元素。整道题没有一个地方明确写着"请使用栈",但只要你把物理场景翻译成数据结构语言,所有规则都严丝合缝地对上了。

这也是出题人喜欢把模板题"包装"成工程场景的原因。剥掉题面中的火车、轨道、编组场这些词,剩下的是一个再朴素不过的问题:给定入栈顺序 1 到 n,判断某个出栈序列是否合法。

1.3 最常见的认知误区:它不是考全排列

我见过不少同学拿到这题的第一反应是枚举所有可能的出站顺序,然后拿题目给的序列去比对。如果 n 只有三四个,这种暴力思路还能跑;一旦 n 到了几百甚至几千,枚举 2^n 种进出时机、n! 种排列就已经完全离谱了。

但更根本的问题在于,把思路局限在"全排列"上会让人忽略这道题的本质:它在考栈操作的合法性,而不是考排列组合的数量。

举个例子,出站序列 3 1 2 和 3 2 1 都是 1 2 3 的排列,但前者做不到,后者做得到。原因后面会详细推演,这里先记住一个结论:合法的出站序列一定是合法栈操作的输出序列,不是随便一个排列都可以。

另一类误区是认为"只要序列整体呈现某种规律就行",比如以为必须是倒序、或者必须是原序。这也是错的。n = 5 时,像 2 5 4 3 1 这种看似"半乱序"的序列其实也是合法的,只要你能在纸上完整模拟一遍。

1.4 用"堵车"来理解栈顶的约束

想快速理解这道题,可以把轨道想象成一条只能倒车的窄巷子:最先进去的车停在巷子最里面,后进去的车停在巷子口,后进去的车不先出来,里面的车就只能干等。

因此,一旦某节车厢选择进站,它出来的时候就必须被它之后进站的车"压制"着。说得专业一点:在任意时刻,能够出站的车只有栈顶那一节;栈顶不满足出站序列的要求时,你只能继续让新车入栈,而不能跳过栈顶去拿更里面的车厢。

这个约束就是整道题的灵魂。理解了它,后面的模拟逻辑就顺理成章了。

2. 手推栈模拟:合法出站与非法出站各跑一遍

2.1 先定义好三根"指针"

在做任何代码之前,先把三个核心概念定下来。第一个是"当前进站车厢编号 cur",因为进站顺序固定为 1 到 n,所以 cur 的初始值是 1,每入栈一节车厢就加 1。第二个是"出站序列指针 idx",它指向出站序列中当前需要匹配的位置,初始为 0。第三个是栈本身,栈底到栈顶保存着已经进站但还没有出站的车厢。

模拟的策略非常简单:不断尝试让当前车厢出站。能出站的情况只有两种,第一,当前要进站的车厢编号恰好等于出站序列需要的编号,那就直接进站并立刻出站,等价于 cur 和 idx 同时前进;第二,当前栈顶元素恰好等于出站序列需要的编号,那就弹出栈顶,idx 前进。

如果这两种情况都不满足,就把当前车厢压入栈中,cur 前进,等待后续匹配。当所有车厢都已经进过站,也就是 cur 大于 n,而出站序列还没匹配完,此时如果栈顶仍然不等于目标编号,就说明没有任何操作可以继续了,序列非法。

2.2 合法序列 3 2 1 4 5 的完整推演

以 n = 5、出站序列 3 2 1 4 5 为例,跟着流程走一遍。

  • 第 1 步:cur = 1,目标 3。1 不等于 3,且栈为空,压入 1。栈内:1。
  • 第 2 步:cur = 2,目标 3。2 不等于 3,栈顶 1 不等于 3,压入 2。栈内:1 2。
  • 第 3 步:cur = 3,目标 3。cur 等于目标,进站即出站,cur 变为 4,idx 指向 2。栈内:1 2。
  • 第 4 步:cur = 4,目标 2。cur 不等于 2,但栈顶是 2,弹出 2,idx 指向 3。栈内:1。
  • 第 5 步:cur = 4,目标 1。cur 不等于 1,栈顶是 1,弹出 1,idx 指向 4。栈内为空。
  • 第 6 步:cur = 4,目标 4。cur 等于目标,进站即出站,cur 变为 5,idx 指向 5。
  • 第 7 步:cur = 5,目标 5。cur 等于目标,进站即出站,idx 指向 6,匹配结束。

整个过程没有任何一步卡住,所以 3 2 1 4 5 是合法序列。注意这里的一个关键点:1 号车厢很早就进站了,但它一直等到 2 号和 3 号都出完才出来,因为它在栈底,被上面两节车厢堵住,这是完全符合物理直觉的。

2.3 非法序列 5 4 1 2 3 究竟卡在哪里

再来看一个非法序列:5 4 1 2 3。

  • 第 1 步:cur = 1,目标 5。压入 1。
  • 第 2 步:cur = 2,目标 5。压入 2。
  • 第 3 步:cur = 3,目标 5。压入 3。
  • 第 4 步:cur = 4,目标 5。压入 4。
  • 第 5 步:cur = 5,目标 5。5 进站即出站,cur = 6,idx 指向 4。栈内:1 2 3 4。
  • 第 6 步:cur = 6,目标 4。cur 已经大于 n,没有新车可进。栈顶是 4,等于目标,弹出 4,idx 指向 1。栈内:1 2 3。
  • 第 7 步:cur = 6,目标 1。栈顶是 3,不等于 1,也没有新车能入栈,模拟卡死,输出非法。

问题出在最后一步:1 号车厢虽然在栈里面,但它是最早入栈的,此时上面压着 2 号和 3 号。想让它出站,必须先让 2 号和 3 号出站,可出站序列要求 1 先出,这就形成了一辆被堵死的火车。整个序列只能作废。

2.4 判定条件的本质:只有一种情况会让模拟卡死

把上面两个例子放在一起,就能提炼出唯一的失败条件:当所有车厢都已经进过站,栈顶元素依然不等于出站序列当前需要的编号。

为什么这是唯一失败条件?因为在其他任何时刻,你都至少还有三种选择之一:要么当前车厢正好可以被直接出站,要么栈顶能满足要求,要么还能把新车压进栈里等待后续。只有"新车已经全部进场、栈顶又不匹配"这一个瞬间,所有路径都被封死,游戏结束。

这个结论非常重要,它是后面代码实现里 while 循环退出条件和 ok 标志位的直接依据。理解了它,你甚至不需要背代码,现场都能把逻辑写出来。

3. 从手推变成代码:数组栈实现和三类 WA 现场

3.1 一份可以直接跑的 C 代码

有了上面的模拟思路,代码写起来非常自然。数据结构考试通常允许用 C/C++ 写,我习惯用数组模拟栈,完整代码是这样:

#include <stdio.h> #define MAXN 100005 int target[MAXN]; int stack[MAXN]; int main() { int n; scanf("%d", &n); for (int i = 0; i < n; i++) { scanf("%d", &target[i]); } int top = 0; // 栈顶指针,指向栈中元素数量 int cur = 1; // 下一节要进站的车厢编号 int idx = 0; // 出站序列当前匹配位置 int ok = 1; while (idx < n) { if (cur <= n && cur == target[idx]) { // 当前车厢正好是需要的,进站后立即出站 cur++; idx++; } else if (top > 0 && stack[top - 1] == target[idx]) { // 栈顶车厢正是需要的,弹出 top--; idx++; } else if (cur <= n) { // 暂时匹配不上,先压栈 stack[top++] = cur++; } else { // 所有车都进过站,栈顶却匹配不上 ok = 0; break; } } printf("%s\n", ok ? "YES" : "NO"); return 0; }

这段代码的核心就是那四分支判断。很多人会纠结分支顺序,其实顺序是固定的:先判断"当前进站车厢等于目标",再判断"栈顶等于目标",最后才"压栈"。如果一上来就压栈,就等于假设当前车厢一定要先进站,这会丢失"进站后立即出站"的情况,导致把合法序列误判成非法。

3.2 为什么我坚持用数组而不是库里的栈

不少同学会问:C++ 里不是有 std::stack 吗,直接用不好吗?如果你只是为了 AC,当然可以。但在考试和平时训练里,我更推荐数组模拟,原因有三个。

第一个原因是中间态可见。数组本质上允许你随机访问栈内任意位置,打印调试时可以直接把 stack[0] 到 stack[top-1] 全部输出,一眼就能看出是哪节车厢堵住了问题。用库里的栈,只能看到栈顶,排错效率低很多。

第二个原因是逻辑更贴合手推过程。手推时我们会说"栈内:1 2 3",数组模拟可以直接打印出来,和草稿纸上的过程一一对应。用库栈时反而要不停地 pop 才能看到内部,容易打乱调试节奏。

第三个原因是考题环境的不确定性。有些在线评测系统或者学校机房环境老旧,C++ 版本兼容性一般,而纯 C 数组栈在任何环境里都能编译运行,不会因为标准库差异出问题。

3.3 边界用例:n 很小的时候反而最容易错

我见过很多同学大用例全对、小用例翻车,问题往往出在边界条件上。把常见的极端情况整理成一张表,复习时直接过一遍:

用例出站序列预期结果模拟中的关键点
n = 11YES只有一个车厢,直接进站即出站
n = 1无输入非法实际题面会保证长度,但代码要能处理
n = 31 2 3YES每节车都进站即出站
n = 33 2 1YES全部压栈后从栈顶依次弹出
n = 33 1 2NO3 出站后,栈顶是 2,1 被压在下面
n = 52 5 4 3 1YES2 先出,5 进站后 4 3 依次压栈再出

最后一行的 2 5 4 3 1,手动推一遍就能体会"半乱序"才是这类题的常态。很多同学只记住了全倒序合法,遇到这种混合序列就懵了,其实是没把模拟过程走熟。

3.4 三类典型 WA 以及排查思路

如果提交后答案错误,大概率是下面三种情况之一。

第一类是目标序列没有先校验。题目说的是"1 到 n 的一个排列",但有些题的输入并没有保证这一点,比如出现重复数字、或者出现大于 n 的数字。如果不在模拟前检查"序列元素是否恰好是 1 到 n 的排列",代码可能对某些非法输入给出错误输出。稳妥做法是加一个标记数组统计每个数字出现次数。

第二类是循环结束条件写错。有人会把循环写成 while (cur <= n || top > 0),这在某些题目里没问题,但会破坏四分支结构的简洁性。我的建议是坚持以 idx < n 作为唯一循环条件,所有失败情况在循环体内处理,这样逻辑更不容易漏。

第三类是输出格式问题。题目要求输出 YES 还是 Yes,输出 Legal 还是 OK,每次都要先看清楚。这种错误最冤枉,属于"样例过了、格式跪了"。建议把输出语句单独写,避免在复杂表达式里拼字符串出错。

4. 一道题背后的一串题:卡特兰数与高频变体

4.1 合法的火车出站序列一共有多少种

很多期末卷不会只满足于让你判断一个序列是否合法,还会追加一问:n 节车厢进站后,总共可能有多少种不同的合法出站序列?这个问题的答案就是卡特兰数。

n 个元素入栈出栈,合法出栈序列的数量是第 n 个卡特兰数,公式为 C_n = C(2n, n) / (n + 1)。n = 3 时是 5,n = 4 时是 14,n = 5 时是 42。

为什么是这个数?把每节车厢的"入栈"记作 +1,"出栈"记作 -1,整个过程的任意前缀都要满足"入栈次数始终不少于出栈次数",因为栈不能弹出空元素。这等价于从坐标原点出发、每次向右上或右下走一步、但永远不落到 x 轴以下的一条路径,恰好就是卡特兰数的经典模型。

这个结论对备考很有用。比如某道题要求你输出所有合法序列或者统计数量,你可以先用卡特兰数算一个预期值来验证自己程序的正确性,能省下大量调试时间。

4.2 变体一:进站顺序不再是 1 到 n

有些题目会把"进站顺序固定为 1 到 n"改成"给定一个任意的进站序列",再给你一个出站序列,问能不能通过栈操作实现。

比如进站顺序是 2 5 3 4 1,出站序列是 5 4 3 2 1。这不代表答案一定是 YES,需要重新模拟。代码改动很小:把前面代码里的 cur 从"当前编号"改成"进站序列数组的下标指针",每次压栈时压入的是进站序列此时的元素,而不是 cur 本身。

我见过有的同学碰到这种变体就慌了,觉得题目变难了。其实完全没有,核心的判定逻辑一模一样,只是"进站车厢编号"换成"进站序列当前指向的车厢"而已。理解这一点,就能以不变应万变。

4.3 变体二:车厢重排问题——缓冲轨道不是栈而是队列

还有一个出场率很高的亲戚题,叫做车厢重排问题。题面是:入口处停着一列乱序车厢,比如 5 3 1 4 2,出口方向要求按 1 到 n 的顺序驶出。中间有若干条缓冲轨道,轨道两端都可以操作。

这里的"两端都可以操作"很重要,它意味着每条缓冲轨道是队列而不是栈。策略是把每节车厢放到某条合适轨道的末尾,让每条轨道内部的队列保持单调性质,最终按顺序驶出。

这种题和你今天做的"火车排号"有本质区别:栈只能从一头进出,队列可以从两头进出;栈靠"后进先出"约束,队列靠"先进先出"约束。如果你把今天的栈模型硬套到车厢重排上,十有八九会写错。

复习时可以把两种模型对照着做,一道判断序列合法性,一道计算最少需要的缓冲轨道数,一个题组就把栈和队列两个知识点全串起来了。

4.4 变体三:顺带求栈的最小容量

第三种变体更隐蔽。题目可能不问"序列是否合法",而是问:给定一个合法出站序列,车站轨道至少需要能同时停多少节车厢?

解法就是在模拟过程中记录栈的最大深度。每压入一节车厢,就统计一下当前 top 的值,最后取最大值输出。

有时候题目还会加上容量限制,比如"轨道最多只能停 3 节车厢",判断序列是否还能实现。那就更简单了,在压栈分支里加一句 if (top + 1 > limit) 直接判非法。

这个变体让我联想到实际工程里的缓冲区设计。很多调度系统的核心问题,就是"在保证处理顺序正确的前提下,缓冲区最小该设多大"。把栈最大深度算出来,本质上就是在做缓冲区容量的静态分析。

5. 考场上的 30 秒速判与自查清单

5.1 不写代码,怎么快速判断一个序列合不合法

考试时间紧张时,有时候你不需要在代码里模拟,只用在草稿纸上人工判断。这里分享一个我常用的快速规则。

从左到右看整个出站序列,遇到一个比较大的数 x 出站时,关注它后面所有比 x 小的数。这些数在被 x 挡住之前,一定都已经在栈里了。x 出站后,它们只能按照"后进先出"的栈顺序依次弹出,所以它们在出站序列里必须呈现严格递减的顺序。

举个例子,判断 4 2 5 3 1 是否合法:第一个出站的是 4,后面比 4 小的数有 2、3、1,但实际顺序是 2、3、1,不是严格递减,因为 2 在 3 前面却比 3 小,这就违反了栈序,直接判非法。

再验证一个合法例子 2 5 4 3 1:第一个出站的是 2,后面比 2 小的只有 1,没问题;第二个出站的是 5,后面比 5 小的是 4、3、1,顺序是 4、3、1,递减,没问题;继续检查 4,后面比 4 小的是 3、1,递减;最终合法。

这个规则在手推和检查代码时非常快。它本质上就是栈的"后进先出"约束在数列上的一种表现,不需要记忆复杂公式,只需要反复朗读一句话:大数先走后,小数段必须倒着走。

5.2 出题人藏在题面里的三个小改动

考场上最怕的不是题目难,而是题面里藏着不显眼的小改动,让你把旧题直接套上去。针对火车排号这个模型,我总结过三个高频改动。

第一个改动是"可以延迟进站"还是"到站必须立刻进站"。如果是后者,那问题就退化成:按顺序进站,每一辆要么直接出,要么压栈,判定的本质还是栈,但可操作空间更小。

第二个改动是"轨道是死胡同"还是"轨道两端都能进出"。前者是栈模型,后者是队列模型,解题思路完全不同。看到"两端"两个字一定要立刻警惕,这不是同一个题。

第三个改动是输出要求的侧重点。有的题要求输出是 YES/NO,有的要求输出最小栈容量,有的要求输出全部合法序列。提前看清楚最后一问,能避免写完主程序才发现漏了第二问的尴尬。

5.3 从踩坑经验里总结的自查清单

每次做完这类题,我都会按下面的清单过一遍,基本能堵住大部分疏漏。

  • 序列本身必须是 1 到 n 的排列,重复或越界要提前拦截。
  • while 循环的唯一推进条件设置为出站序列指针,而不是栈空或新车指针。
  • 分支顺序固定:当前进站等于目标,然后栈顶等于目标,最后压栈。
  • 所有车厢都进过站后,若栈顶还不匹配,果断判非法。
  • 若题目附带求栈最大容量,在压栈分支更新最大值。
  • 输出前确认是 YES/NO、Legal/Illegal 还是数字形式。

这套清单看起来琐碎,但每次考试前扫一眼,真的能拦下很多低级错误。尤其是分支顺序这一条,我见过太多同学因为把"压栈"写在前面,把 1 2 3 这种原序序列都判成了非法。

说实话,火车排号这类题在数据结构里属于典型的"模型一眼看穿、代码量不大、细节决定成败"的题目。我自己最大的体会是:不要急着写代码,先在草稿纸上用三根指针完整推两个序列,一个合法一个非法,推完再动手写循环。这个习惯帮我避免过很多次逻辑混乱。如果你在复习这套期末模拟卷,建议把这道题当作栈章节的"标准例题"反复温习,因为它把所有栈的核心特性都浓缩在了一个画面里:一列火车开进一条死胡同,出不来的车,只能等上面的车先走。

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

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

立即咨询