ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

C语言实战:火车站售票题的数组建模与规则实现

C语言实战:火车站售票题的数组建模与规则实现

简介:这是东软集团IA事业部2007年C语言编码考试试题,适合正在准备企业编程笔试、想巩固C语言基本功与文件操作能力的开发者。题目要求实现一节80座车厢的火车站售票模拟程序,重点考查数据结构设计、相邻与间隔座位分配逻辑、输入输出文件处理及编码规范。资源内包含完整试卷与可直接参考的GetRequest、OutputResult函数框架,对理解静态变量保持文件句柄、按规则拆分布局、处理边界情况很有帮助。资料为1个doc文档,压缩包约54KB,已吸引232人学习。借助这套真题,读者既能检验自身C语言水平,也可将其作为练习项目,动手搭建座位状态表、实现1至4张票的优先购票策略,并在调试中提升文件读写与异常处理的实战能力。整体内容精炼,特别适合以“火车票购票模拟”为切入点的C语言进阶训练。

1. 这道“火车站售票”C语言题,考的不是你会不会写循环

先说一个高频丢分点:题目里 1、2、5、6 属于同一个间隔,3、4、7、8 属于旁边那个间隔。很多人看到“座位号从 1 到 80”,第一反应就是“连续 4 个座位一组”,结果间隔模型从一开始就错了,后面售票规则的优先级逻辑全部跑偏。

这道某企业事业部早期的 C 语言编码考试题,表面是“模拟火车站售票”,实际考察三件事:一、能不能用合适的数据结构表达 80 个座位的售出状态;二、能不能把“相邻”和“间隔”这两个约束翻译成可查找的逻辑;三、文件读写和函数接口用得到不到位。分值大致是:数据结构与编码规范约 25 分,售票规则约 40 分,主流程和文件操作约 35 分。适合准备企业编码机试的求职者,也适合想练 C 语言状态建模的在读学生。下面直接按考点拆开讲。

2. 底层考点:座位数组与间隔映射,先画清楚再写代码

2.1 数据结构:80 个座位就是“4 列 × 20 行”,间隔用公式算

这一题不需要链表,也不需要位图,一张数组就够。定义int seatState[80],0 表示未售,1 表示已售。为什么不用结构体数组?因为座位本身没有附加属性要存,状态变化也只有“未售 → 已售”一种,用 int 数组最简单,阅卷人一眼能看懂。真正要动脑的是“间隔”怎么算。

车厢布局可以抽象成 4 列 × 20 行。座位编号规律是:每个座位号 n(从 1 开始),先减 1,除以 4 得到行号 row,对 4 取余得到列号 col。于是row = (n-1)/4,col = (n-1)%4。验证一下:座位 1 在第 1 行第 1 列,座位 2 在第 1 行第 2 列,座位 5 在第 2 行第 1 列,座位 3 在第 1 行第 3 列。过道位于第 2 列和第 3 列之间,所以 col 小于 2 的是过道左侧两列,col 大于等于 2 的是过道右侧两列。

间隔定义为“两行 × 两列”的块,一个间隔装 4 个座位,全车共 20 个间隔。计算间隔编号时先算行块,再算左右:行块rowBlock = row / 2;如果 col 小于 2,间隔号 =rowBlock * 2 + 1;否则间隔号 =rowBlock * 2 + 2。按这个公式,座位 1、2、5、6 的间隔号都是 1,座位 3、4、7、8 的间隔号都是 2,和题干完全对得上。这段映射是整道题的地基,考试时可以先把下面的函数写出来:

int GetInterval(int seatNo) { int row = (seatNo - 1) / 4; // 0 开始的行号 int col = (seatNo - 1) % 4; // 0 开始的列号 int rowBlock = row / 2; // 每两行合成一个行块 if (col < 2) { // 过道左侧两列 return rowBlock * 2 + 1; } else { // 过道右侧两列 return rowBlock * 2 + 2; } }

注意这个函数的输入是座位号 1 到 80,输出是间隔号 1 到 20。不要把它和相邻对的编号混在一起,相邻对是 (1,2)、(3,4)、(5,6) 这种两两一组,间隔是四个座位一组,两者维度不同。后续“售 2 张”和“售 3/4 张”分别查这两套结构,写清楚就能避免逻辑纠缠。

2.2 文件接口:GetRequest 读什么、OutputResult 写什么

题目把输入输出函数直接给了,很多考生把这两个函数当成不可改动的黑匣子,实际要用好也有讲究。GetRequest(int *p)的作用是从d:\request.txt里读一次购票请求:每次调用读一个 1 到 4 的数字写入*p,返回 true 表示有请求,返回 false 表示请求读完,主循环就此结束。注意它每次只消费一个请求,所以主循环的执行次数等于文件里的请求个数,而不是只执行一次。

有个细节很容易翻车:原题给出的GetRequest里fscanf格式是"%d,",要求数字之间用逗号分隔。如果request.txt里的数据是“1 2 3 4”这种纯空白分隔格式,函数会在等待逗号时读失败。题目描述说“空格、制表符、换行符、换页符和回车符都可以分割”,又说“供调试使用,请使用题目附带的文件 request.txt”,稳妥做法是统一把格式改成"%d",并在提交前用真实输入文件跑一遍。另一个注意点是GetRequest用 static 修饰文件指针和数组,第一次调用时打开文件并一次性读完数字,之后多次调用从静态数组里取数。如果自己重写这个函数,别在每次调用里重复fopen,否则请求会漏读。

OutputResult(int *SeatNum, int Count)的职责是追加写一行本次售票结果。它的实现每次都把文件按追加方式打开,所以多次售票的结果会叠在一起。为了防止上一次调试的旧数据干扰,main函数开头最好先用"w"方式打开一次d:\result.txt,立即关闭,把文件清空。文件操作另一个常见问题是fopen失败返回 NULL,考试环境里路径写错很常见,至少判断一下返回值并在窗口打印提示,这一两行就能救回规范分。

2.3 主循环骨架:初始化、读请求、调售票、写结果

考试时不要一上来就写规则细节,先把主流程写死。主流程就是四步循环:置所有座位为未售;调用GetRequest读请求;根据请求调对应的售票函数;成功后调OutputResult输出;GetRequest返回 false 就结束循环。这样结构清晰,即使某个规则函数没写完,主流程也能单独跑起来调试。

int main(void) { int request, sellCount; int soldSeats[4]; int i; // 1. 座位全部置为未售 for (i = 0; i < 80; i++) { seatState[i] = 0; } // 2. 清空上次遗留的输出文件 FILE *fp = fopen("d:\\result.txt", "w"); if (fp != NULL) { fclose(fp); } // 3. 循环读请求 while (GetRequest(&request)) { if (request == 1) { sellCount = TrySellOne(soldSeats); } else if (request == 2) { sellCount = TrySellTwo(soldSeats); } else { sellCount = TrySellMore(request, soldSeats); } // 4. 有票才写输出 if (sellCount > 0) { OutputResult(soldSeats, sellCount); } else { printf("本次请求不售票:余票不足\n"); } } return 0; }

这个骨架里,TrySellOne、TrySellTwo、TrySellMore分别对应三类售票规则,每个函数只做一件事:找出座位号、更新状态、返回卖出张数。输出统一交给OutputResult,这样职责清晰,也符合“逻辑清楚、结构良好”的评分要求。编码规范分最多能占到题目分数的三分之一,函数划分离不开:主流程管调度,规则函数管选座,辅助函数管映射,阅卷人一眼就能找到采分点。

3. 高频考点:售 1 张、2 张、3~4 张票的三层规则怎么落地

3.1 售 1 张票:线性扫描第一个空座就可以

题干说“随便售出一张没有售出的票”。这里的“随便”并不等价于“必须用 rand 随机”。绝大多数阅卷标准只要求满足“卖出一张未售票”,按顺序扫描第一个空座也是合法的。用了随机数反而多出三个麻烦:可能重复选到已售座位,需要在循环里重试;测试结果不可复现;还要处理srand种子,忘记写种子会导致每次运行结果都不同。

正确写法是把选座和更新状态放在同一个函数里完成:

int TrySellOne(int out[]) { for (int n = 1; n <= 80; n++) { if (seatState[n - 1] == 0) { seatState[n - 1] = 1; out[0] = n; return 1; } } return 0; }

这个函数返回 1 表示成功,返回 0 表示车厢已满。out数组由调用方提供,函数内只写数据,不负责输出文件,这就是“职责单一”。面试追问“如果 80 个座位全部卖完,GetRequest 还有请求怎么办”时,标准答法是:TrySellOne返回 0,主流程打印“余票不足不售票”,不调用OutputResult,这与后面 3 张、4 张规则里的“不售票”提示逻辑一致。

3.2 售 2 张票:相邻对怎么找才不出错

两张票的规则是“优先 2 张相邻,没有相邻就随便两张”。题干明确说了相邻包括 1 和 2、3 和 4、5 和 6、7 和 8,但不包括 1 和 5、2 和 3。也就是说,相邻对就是按座位编号两两成对:(1,2)、(3,4)、(5,6)……一直到 (79,80)。用数组下标表示就是seatState[0]和seatState[1]成对,seatState[2]和seatState[3]成对。

最容易写错的循环是全范围扫描“相邻 i 和 i+1”。如果从 1 循环到 79,会把 (2,3) 也判成相邻,而题干明确说 2 和 3 不算,因为 2 在过道左侧、3 在过道右侧,两人中间隔着过道,不是相邻。正确实现是步长 2 扫描:

int TrySellTwo(int out[]) { // 优先卖相邻对:1-2, 3-4, 5-6, ... for (int i = 1; i < 80; i += 2) { if (seatState[i - 1] == 0 && seatState[i] == 0) { seatState[i - 1] = 1; seatState[i] = 1; out[0] = i; out[1] = i + 1; return 2; } } // 回退:任意两张未售出的票 int count = 0; for (int n = 1; n <= 80 && count < 2; n++) { if (seatState[n - 1] == 0) { seatState[n - 1] = 1; out[count++] = n; } } return count; }

这里有一个容易被忽略的细节:回退分支同样会把状态置为已售,所以无论走哪个分支,函数返回时座位状态一定是更新的。如果只写“找到相邻对”的逻辑、回退分支忘记置状态,同一批座位会被重复卖出。调试时输出结果每行多出的逗号分隔座位号会暴露这个问题,但最好写的时候就避免。

3.3 售 3 张和 4 张票:间隔优先与余票不足的判定顺序

3 张 4 张是整道题规则最厚的部分,题干三句话对应三个分支:优先售一个间隔内的票;如果剩余票数不足,就提示不足,不售票;如果不能满足优先原则,则售任何位置的票。翻译成代码判定顺序是:先统计全车剩余票数,不足直接返回不售票;再遍历 20 个间隔,找有没有“该间隔剩余座位数 >= 请求数”的;有就选这个间隔里的座位;没有就随便选全车空座。

为什么“剩余票数”要先判?因为题干明确要求“如果剩余票数不足,就提示不足,不售票”,这个分支优先级最高。比如只剩 3 个空座,请求是 4,此时无论如何都不能出票。但如果剩余票数是 4,且没有任何一个间隔能一次性提供 4 个座位,就可以从不同间隔凑出 4 张,这对应“不能满足优先原则,则售任何位置”。

int TrySellMore(int need, int out[]) { int totalFree = 0; int n, k; // 1. 统计全车剩余票数 for (n = 1; n <= 80; n++) { if (seatState[n - 1] == 0) { totalFree++; } } if (totalFree < need) { return 0; // 提示不足,不售票 } // 2. 遍历 20 个间隔,找空座数足够的一个 for (int interval = 1; interval <= 20; interval++) { int freeInInterval[4]; int cnt = GetFreeInInterval(interval, freeInInterval); if (cnt >= need) { for (k = 0; k < need; k++) { seatState[freeInInterval[k] - 1] = 1; out[k] = freeInInterval[k]; } return need; } } // 3. 没有满足条件的间隔,任意位置出票 int count = 0; for (n = 1; n <= 80 && count < need; n++) { if (seatState[n - 1] == 0) { seatState[n - 1] = 1; out[count++] = n; } } return count; }

这里用GetFreeInInterval把某个间隔的所有未售座位号放进数组并返回数量,它和GetInterval配合,避免重复写行列换算。这个辅助函数是本题的一个高频采分点,单独写出来能让阅卷人明确看到“我知道间隔怎么算”。

面试追问通常有两种:一种是“优先间隔存在多个时选哪个”,答案是题干没有规定优先级,取第一个满足条件的间隔即可,代码用 for 循环从头扫,稳定可复现;另一种是“间隔里空 3 个,但请求是 4,能不能从同间隔硬凑”,答案是不能,因为“优先售一个间隔内的票”要求所有座位在同一间隔,该间隔不满足需求就找下一个间隔。答完再结合代码说明,基本就稳了。

4. 真题逐题演练:完整可运行的售票程序与得分注释

4.1 完整代码:从间隔映射到主循环的一键可跑版本

把前面各部分拼成完整程序。这份代码按 C99 标准编译,文件路径保持题目默认的d:\request.txt和d:\result.txt。如果考试环境不允许写盘根目录,改宏定义或直接改字符串即可。先看完整代码,再逐段解释。

#include <stdio.h> #include <stdbool.h> #define SEAT_NUM 80 #define INTERVAL_NUM 20 /* 座位状态:0 未售,1 已售 */ int seatState[SEAT_NUM]; /* 题目提供的读请求接口,修正分隔符为 %d 以兼容空白分割 */ bool GetRequest(int *p) { static FILE *fp = fopen("d:\\request.txt", "r"); static int nIndex = 0; static int inputData[SEAT_NUM] = {0}; static bool isfirst = true; if (isfirst) { for (int i = 0; i < SEAT_NUM; i++) { if (fscanf(fp, "%d", &inputData[i]) == EOF) { break; } } isfirst = false; if (fp != NULL) { fclose(fp); } } if (nIndex < SEAT_NUM && inputData[nIndex] != 0) { *p = inputData[nIndex++]; return true; } return false; } /* 题目提供的写结果接口,追加写一行 */ void OutputResult(int *seatNum, int count) { FILE *fp = fopen("d:\\result.txt", "a"); if (fp == NULL) { return; } fprintf(fp, "====================================\n"); for (int i = 0; i < count; i++) { fprintf(fp, "%d,", seatNum[i]); } fprintf(fp, "\n"); fclose(fp); } /* 座位号到间隔号:1~80 映射到 1~20 */ int GetInterval(int seatNo) { int row = (seatNo - 1) / 4; int col = (seatNo - 1) % 4; int rowBlock = row / 2; if (col < 2) { return rowBlock * 2 + 1; } else { return rowBlock * 2 + 2; } } /* 统计某个间隔内的未售座位,并写入 out 数组 */ int GetFreeInInterval(int interval, int out[]) { int count = 0; for (int n = 1; n <= SEAT_NUM; n++) { if (GetInterval(n) == interval && seatState[n - 1] == 0) { out[count++] = n; } } return count; } /* 售 1 张:任意一张未售 */ int TrySellOne(int out[]) { for (int n = 1; n <= SEAT_NUM; n++) { if (seatState[n - 1] == 0) { seatState[n - 1] = 1; out[0] = n; return 1; } } return 0; } /* 售 2 张:优先相邻对,否则任意两张 */ int TrySellTwo(int out[]) { for (int i = 1; i < SEAT_NUM; i += 2) { if (seatState[i - 1] == 0 && seatState[i] == 0) { seatState[i - 1] = 1; seatState[i] = 1; out[0] = i; out[1] = i + 1; return 2; } } int count = 0; for (int n = 1; n <= SEAT_NUM && count < 2; n++) { if (seatState[n - 1] == 0) { seatState[n - 1] = 1; out[count++] = n; } } return count; } /* 售 3 或 4 张:先判余票,再找间隔,最后任意位置 */ int TrySellMore(int need, int out[]) { int totalFree = 0; for (int n = 1; n <= SEAT_NUM; n++) { if (seatState[n - 1] == 0) { totalFree++; } } if (totalFree < need) { return 0; } for (int interval = 1; interval <= INTERVAL_NUM; interval++) { int freeInInterval[4]; int cnt = GetFreeInInterval(interval, freeInInterval); if (cnt >= need) { for (int k = 0; k < need; k++) { seatState[freeInInterval[k] - 1] = 1; out[k] = freeInInterval[k]; } return need; } } int count = 0; for (int n = 1; n <= SEAT_NUM && count < need; n++) { if (seatState[n - 1] == 0) { seatState[n - 1] = 1; out[count++] = n; } } return count; } int main(void) { int request, sellCount; int soldSeats[4]; int i; for (i = 0; i < SEAT_NUM; i++) { seatState[i] = 0; } // 清空旧输出文件 FILE *fp = fopen("d:\\result.txt", "w"); if (fp != NULL) { fclose(fp); } while (GetRequest(&request)) { if (request == 1) { sellCount = TrySellOne(soldSeats); } else if (request == 2) { sellCount = TrySellTwo(soldSeats); } else { sellCount = TrySellMore(request, soldSeats); } if (sellCount > 0) { OutputResult(soldSeats, sellCount); } else { printf("本次请求不售票:余票不足\n"); } } return 0; }

这段代码覆盖了题干要求的全部功能点。GetRequest的 static 变量只在第一次调用时打开文件,之后从静态数组取数,这是题目给定的设计,不要改成每次调用都读文件。OutputResult的追加写配合 main 开头的清空操作,保证多次调试不会把旧结果混进来。三个TrySell函数各自独立,返回值统一表示“实际卖出的票数”,失败时返回 0,主流程根据返回值决定是否输出,逻辑链路清晰。

调试时可以用一个简单的d:\request.txt,比如依次写入“2 2 4 1”,四行数字用空格或换行分开。第一次请求 2 会卖出 1、2;第二次请求 2 会卖出 3、4;第三次请求 4 会优先找同一间隔,这时 5、6、7、8 所在的间隔 2 还空着,会一次卖出 5、6、7、8;第四次请求 1 会卖 9。把预期结果和result.txt对照,能快速验证相邻对和间隔逻辑。

4.2 逐段得分点:这 5 处位置是阅卷人重点看的地方

编程题给分不是只看最后能不能运行,而是按数据结构和规则拆点。下面这张对照表可以帮你自查,提交前逐项确认,比盲目改代码有效。

位置评分点易错替代写法正确做法
main 开头的清空输出文件操作规范直接用"a"不清旧文件先"w"打开一次再关闭
GetInterval间隔模型正确按n/4连续分组行列换算,两行两列成块
GetFreeInInterval间隔统计在主函数里重复写循环独立函数返回数量和座位号
TrySellTwo 的步长相邻判定正确i = 0; i < 79; i++把 2、3 误判为相邻for (i = 1; i < 80; i += 2)
TrySellMore 的判定顺序规则优先级正确先找间隔再判余票先统计余票,不足直接返回 0

编码规范分占比不低,命名要见名知义。比如totalFree比sum好,freeInInterval比a好;函数之间用空行隔开,核心逻辑加一行注释解释“为什么先判余票”。注释不必写满,但间隔映射、规则优先、不足处理这三处必须写清意图,这就是扣分规则里“逻辑清楚、结构良好”的直接体现。

5. 易错点速查:五个“一看就会、一写就错”的失分点

5.1 错把间隔当成连续 4 个座位一组

错在哪:以为座位 1~4 是间隔 1,5~8 是间隔 2,直接在数组上按每 4 个元素切片。

为什么错:车厢座位是 4 列 × 20 行,间隔由两行两列构成。1、2 在第 1 行左侧,5、6 在第 2 行左侧,所以 1、2、5、6 才同属一个间隔;3、4、7、8 属第二个间隔。按连续 4 个座位分组后,“间隔内优先”的规则就全乱套,3 张 4 张票可能卖出过道两侧的座位。

怎么答对:用行列换算公式GetInterval计算间隔号,并把间隔号和座位号的映射关系用测试用例验证。先手算 1、2、5、6 的间隔号都是 1,再验证 9、10、13、14 的间隔号是 3,确保公式覆盖到边界座位。

5.2 直接套用fscanf("%d,", ...)导致读不到请求

错在哪:题目给的GetRequest里写的是带逗号的格式,有些人原样照抄,输入文件里却是空格或换行分隔。

为什么错:%d,要求读取数字后必须紧跟逗号。如果请求之间是空格,函数会在等逗号时卡住,inputData数组大部分位置保持 0,主循环提前结束或漏读请求。

怎么答对:提交前把格式统一成"%d",兼容空白和逗号两种分隔。如果题目明确要求使用附带文件,先打开 request.txt 看一眼实际内容再决定。改一行代码的事,别让整个程序因为格式问题跑不出结果。

5.3 先输出再更新座位状态,导致同一座位被卖两次

错在哪:在TrySell函数里先调OutputResult(soldSeats, cnt),再置seatState为 1。

为什么错:下一次请求进入时,座位状态还是 0,同一个座位会被再次卖出。尤其是卖 2 张和 3、4 张时,间隔内空座数统计也会跟着错,整个结果文件看起来像“重复出票”,这在铁路售票场景里属于致命逻辑错误。

怎么答对:把状态更新放在函数内部、输出之前完成。每个TrySell函数在确定座位号后立刻置 1,返回值只表示本次卖出的票数,主流程拿到soldSeats再交给OutputResult。状态更新和输出解耦,既符合职责单一,也能避免重复卖票。

5.4 3 张、4 张票余票不足时没有提示,直接随便凑票

错在哪:TrySellMore里先遍历间隔,找不到足够空座就跳到任意位置,没判断全车总余票。

为什么错:题干明确写了“如果剩余票数不足,就提示不足,不售票”。如果只剩 3 个空座、请求是 4 张,正确行为是拒绝售票,而不是从 3 个空座里挑 3 张卖出去,因为这样等于少卖一张、规则解释不通。

怎么答对:在TrySellMore开头先统计totalFree,小于need直接返回 0。主流程看到返回 0 就打印“余票不足不售票”,不调用OutputResult。这样把“不足不售”作为最高优先级分支,后面“间隔优先”和“任意位置”都在余票充足的前提下执行。

5.5 输出文件用追加模式却不清空,旧结果混进新运行

错在哪:每次调试直接运行程序,result.txt用"a"追加,上一次甚至上上次的售票结果全留在文件里。

为什么错:阅卷人或面试官看到的结果文件是多次运行叠加的,无法判断本次运行是否真正符合请求序列。如果第一次运行卖出了 1、2,第二次运行又卖出了 1、2,文件里看起来就像重复出票,规则写得再对也会被误判。

怎么答对:main开头先以"w"方式打开result.txt,立刻关闭,清空文件。之后OutputResult再用"a"追加本次运行的多行结果。这样每次运行都是干净起点,结果文件只包含本次请求对应的售票记录。

6. 答题技巧:把这类规则模拟题压缩成“三步走”模板

这类“规则型模拟题”不只出现在火车站售票这一题上,排号系统、资源分配系统、库存扣减题都是同一个套路。我后来每次遇到这类题,都强制按三步走,很少再在规则翻译上丢分。

第一步,把所有约束写成“约束 → 数据结构”的映射。相邻对就是 (1,2)、(3,4) 这种步长 2 的配对;间隔就是“两行两列”的块,需要行列换算;座位状态就是 0/1 数组。先把这些映射写出来,代码只是翻译。题目里“相邻”和“间隔”是两种不同的结构,不要偷懒合并成一种。

第二步,把规则翻译成独立函数,每个函数只回答一个问题。“售 1 张”回答“找哪个空座”,“售 2 张”回答“有没有相邻对”,“售 3/4 张”回答“先判余票还是先找间隔”。函数之间不共享局部变量,靠参数和返回值通信。试想如果所有规则都写在 main 里,出错了要在一大段代码里找逻辑,而拆成函数后,每个函数都能单独验证。

第三步,用固定输入做 5 次手算测试。我习惯准备一个request.txt,依次写入“2 2 4 1 3”,然后在纸上模拟:第一次卖 1、2,第二次卖 3、4,第三次卖 5、6、7、8,第四次卖 9,第五次只剩 70 多个空位、找一个间隔凑 3 张。跑完程序对照 result.txt,如果前两行和纸面结果不一致,一定是相邻对步长或间隔公式写错了。这一步能拦住 80% 的隐性错误。

规律就一句话:先画模型,再拆函数,最后跑数据。从那以后我每次做这类模拟题,都强制先写一张“约束 → 函数”的映射表再动 main,几乎没在评审规则上再翻过车。这套流程应付企业级编码机试的规则型编程题足够用了,希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进