简介:这份资源是东软集团IA事业部2007年编码考试C语言试题C卷,面向准备技术面试、刷题巩固C语言基础的开发者与在校学生,核心考察数组或链表等数据结构设计、售票规则的条件判断以及文件读写操作。压缩包内共1个doc文档,约54KB,完整收录了火车站售票编程题的题干、座位布局示意、售票优先级规则说明,以及可直接复用的GetRequest与OutputResult函数参考实现。题目要求模拟一节80座车厢的售票流程,涵盖单张随机出票、两张优先相邻、三四张优先同间隔等场景,并需从request.txt读取请求、向result.txt追加结果。已有232人学习该试题,读者可借此练习数据结构建模、循环与条件分支组织、文件句柄静态管理等技能,并对照编码规范要求打磨代码风格,是检验C语言基本功与工程习惯的实用练习材料。
1. 一份十七年前的 C 语言编码考题,为什么今天还值得拆
前阵子整理旧资料,翻出一份 2007 年某公司 IA 事业部的编码考试题 C 卷,题目是「火车站售票程序」。乍看像课后作业,细读才发现它把 C 语言面试里最容易翻车的几块——数组状态建模、文件读写、静态变量生命周期、优先级规则落地——全塞进了一道 40 分的题里。它不考算法花活,考的是你能不能把一段带业务规则的流程写成结构清楚、能跑、结果对的代码。这份资源适合两类人:一是准备 C 语言笔试、想找一道「有真实业务约束」的练手题的人;二是带新人、想找一个能讲清数据结构与文件操作配合的案例的工程师。下面按「题目到底要什么 → 怎么建模 → 怎么实现 → 坑在哪」拆开讲,代码可以直接抄去跑。
2. 把售票规则翻译成数据结构:80 个座位怎么存
这道题的第一个分水岭不在写循环,而在建模。题目给的座位布局是 1 到 80,按间隔分组:1、2、5、6 在一个间隔,3、4、7、8 在另一个间隔,往后每 8 个座位重复这个模式。很多人上来就开一个int seat[80],存 0/1 表示是否售出,然后写一堆if判断相邻和同间隔,写到后面自己都绕晕。问题出在:座位号是「一维」的,但业务规则是「二维」的——相邻关系和间隔归属都不是靠座位号加减能直接算出来的。
2.1 座位编号与间隔的映射关系
先把布局规律抽出来。观察题目图:第 1 排是 1、2、过道、3、4;第 2 排是 5、6、过道、7、8;以此类推。也就是说,每 8 个座位构成一个「大块」,块内偏移 0、1 是左侧相邻对,偏移 2、3 是右侧相邻对,偏移 4、5 是下一排左侧相邻对,偏移 6、7 是下一排右侧相邻对。而「一个间隔」指的是同一排左右两侧那 4 个座位,即偏移 {0,1,2,3} 或 {4,5,6,7}。
用座位号n(从 1 开始)反推:
- 块索引
block = (n - 1) / 8 - 块内偏移
off = (n - 1) % 8 - 相邻对:
off为偶数时,n与n+1相邻(因为 0-1、2-3、4-5、6-7 成对) - 间隔编号:
section = block * 2 + (off >= 4 ? 1 : 0)
这套映射是整道题的地基。我一般会先写一个小的验证程序,把 1 到 80 全部打印出(座位号, 相邻伙伴, 间隔号),肉眼核对几组再往下写。别嫌麻烦,这一步省下来,后面调试相邻判断时你会花三倍时间还找不着北。
2.2 状态数组与辅助索引的设计
数据结构我推荐两个数组配合:
#define SEAT_COUNT 80 #define SECTION_COUNT 40 /* seatState[i] = 0 未售,1 已售,下标 0 对应座位 1 */ int seatState[SEAT_COUNT]; /* sectionSeat[s][k] 存第 s 个间隔内第 k 个座位的座位号,k 取 0..3 */ int sectionSeat[SECTION_COUNT][4];seatState负责 O(1) 查某座位卖没卖;sectionSeat是预计算好的「间隔 → 座位号」反查表,初始化时一次性填好,后面判断「某间隔还剩几张、剩的是哪几张」就不用每次现算。这是典型的空间换清晰度:多花 160 个 int,换来售票逻辑里少写两层嵌套取模。
初始化代码:
void InitLayout(void) { for (int n = 1; n <= SEAT_COUNT; n++) { int block = (n - 1) / 8; int off = (n - 1) % 8; int section = block * 2 + (off >= 4 ? 1 : 0); int posInSection = off % 4; /* 间隔内位置 0..3 */ sectionSeat[section][posInSection] = n; seatState[n - 1] = 0; } }参数说明:block是 8 座大块编号,off是块内偏移,section落在 0..39,posInSection把偏移 0..3 和 4..7 都压回 0..3,正好对应间隔内四个位置。跑完这段,sectionSeat[0]应该是{1,2,3,4},sectionSeat[1]是{5,6,7,8},可以拿这个当自检点。
2.3 相邻关系的判定,别用座位号硬减
相邻只有一种情况:同一间隔内,位置 0 和 1 相邻,位置 2 和 3 相邻。注意题目明确说 1 和 5 不算相邻、2 和 3 不算相邻——2 和 3 虽然座位号连着,但分属左右两侧,中间隔着过道。所以判定相邻绝不能写abs(a-b)==1,必须回到间隔内位置:
int IsAdjacent(int a, int b) { int sa = SectionOf(a), sb = SectionOf(b); if (sa != sb) return 0; int pa = PosInSection(a), pb = PosInSection(b); return (pa / 2) == (pb / 2); /* 0-1 一组,2-3 一组 */ }SectionOf和PosInSection就是 2.1 里那两个公式的封装。这样写的好处是规则和代码一一对应,将来布局变了只改映射函数,判定逻辑不动。血泪经验:考场上有人图快直接a+1==b,结果 2 和 3 被判成相邻,售 2 张票时优先规则全乱,输出和标准答案对不上还查不出原因。
3. 售票主流程:四种票数、三级优先级的落地
建模清楚后,主流程其实是「按票数分派到不同策略函数」。题目规则可以归纳成一张优先级表,先把它写死在心里,再落代码。
| 请求票数 | 第一优先 | 兜底策略 | 不足时 |
|---|---|---|---|
| 1 张 | 任意未售座位 | 无 | 无(总有空位或全满) |
| 2 张 | 同一间隔内相邻对 | 任意两张未售 | 不足 2 张则提示不足 |
| 3 张 | 同一间隔内 3 张 | 任意 3 张未售 | 不足 3 张则提示不足 |
| 4 张 | 同一间隔整 4 张 | 任意 4 张未售 | 不足 4 张则提示不足 |
注意题目对 3、4 张的表述:「优先售一个间隔内的票;如果剩余票数不足,就提示不足,不售票。如果不能满足优先原则,则售任何位置的票。」这里有个容易读错的点——「剩余票数不足」指的是全车厢剩余票数不足请求数,不是「某间隔内不足」。间隔内凑不齐 3 张,属于「不能满足优先原则」,应该走兜底随便卖,而不是拒绝。这个区分直接决定边界用例过不过。
3.1 售 1 张与售 2 张:先找相邻对
售 1 张最简单,从头扫第一个未售即可,题目说「随便售出一张」,不要求随机,顺序扫完全可接受,还便于结果复现。
售 2 张的核心是找相邻对。遍历 40 个间隔,每个间隔检查 (位置0,1) 和 (位置2,3) 两组,只要两张都未售就成对卖出:
int SellTwo(int *out) { for (int s = 0; s < SECTION_COUNT; s++) { for (int g = 0; g < 2; g++) { int a = sectionSeat[s][g * 2]; int b = sectionSeat[s][g * 2 + 1]; if (!seatState[a - 1] && !seatState[b - 1]) { seatState[a - 1] = seatState[b - 1] = 1; out[0] = a; out[1] = b; return 2; } } } return SellAny(2, out); /* 兜底:任意两张未售 */ }g取 0 时检查位置 0-1,取 1 时检查位置 2-3,正好覆盖间隔内两组相邻对。找不到就落到SellAny。这里有个隐含约定:优先规则只要求「存在相邻就卖相邻」,没要求卖哪一对,所以顺序遍历第一个命中的即可,结果确定、好对答案。
3.2 售 3 张与售 4 张:整间隔优先
3 张和 4 张共用「整间隔」思路,区别只在需要几张。先统计全车厢剩余总数,不够就直接返回不足;够的话再找间隔:
int SellInSection(int need, int *out) { int remain = CountRemain(); if (remain < need) return -1; /* 全车不足,提示不售 */ for (int s = 0; s < SECTION_COUNT; s++) { int freeCnt = 0, tmp[4], t = 0; for (int k = 0; k < 4; k++) { int n = sectionSeat[s][k]; if (!seatState[n - 1]) { tmp[t++] = n; freeCnt++; } } if (freeCnt >= need) { /* 该间隔能凑齐 */ for (int i = 0; i < need; i++) { seatState[tmp[i] - 1] = 1; out[i] = tmp[i]; } return need; } } return SellAny(need, out); /* 优先原则不满足,兜底 */ }参数need传 3 或 4。freeCnt >= need是关键:售 3 张时,一个间隔剩 3 张或 4 张都算满足优先;售 4 张时必须剩满 4 张。tmp暂存该间隔的空位号,命中后按序卖出。找不到合适间隔就SellAny兜底,注意此时全车剩余已确认够need,兜底一定能成。
3.3 主循环与文件接口的对接
题目给了GetRequest和OutputResult两个函数,直接复用即可,主循环就是把它们串起来:
int main(void) { InitLayout(); int req, seats[4]; while (GetRequest(&req)) { int sold = 0; switch (req) { case 1: sold = SellOne(seats); break; case 2: sold = SellTwo(seats); break; case 3: case 4: sold = SellInSection(req, seats); break; default: continue; } if (sold < 0) { /* 不足:按题目要求提示,不写座位 */ printf("余票不足,本次请求 %d 张未售出\n", req); continue; } OutputResult(seats, sold); } return 0; }GetRequest内部用静态变量缓存了整个request.txt的内容,第一次调用时一次性读完再关闭文件,后续从内存数组取。这个设计有个前提:输入文件里最多 80 个请求,且用 0 作为结束标记。你调试时如果发现读到一半就停了,先检查文件里是不是混进了 0,或者请求数超过 80 被截断——这是这份框架自带的边界,不是你的逻辑错。
4. 文件读写与静态变量:框架里藏着的几个雷
题目把GetRequest和OutputResult的实现直接给了,说「可以直接使用」,但「能用」和「用对」是两回事。这段框架代码里有几处细节,不提前说清楚,调试时会怀疑人生。
4.1 GetRequest 的静态缓存机制
bool GetRequest(int *p) { static FILE* fp = fopen("d:\\request.txt", "r"); static int nIndex = 0; static int inputData[80] = {0}; static bool isfirst = true; int i; if (isfirst) { for (i = 0; i < 80; i++) { if (fscanf(fp, "%d,", &inputData[i]) == EOF) break; } isfirst = false; fclose(fp); } if (inputData[nIndex] == 0 || nIndex == 80) return false; *p = inputData[nIndex++]; return true; }三个static各司其职:fp只打开一次,inputData缓存全部请求,isfirst保证只读一遍文件。fscanf的格式串是"%d,",注意那个逗号——它要求数字后面跟逗号。题目说输入可以用空格、制表符、换行分隔,但这段代码只认逗号,格式串里的逗号会「吃掉」一个非数字字符,空格换行也能凑合过,但如果文件里是1 2 3这种纯空格分隔,fscanf读第一个1后期待逗号,遇到空格会匹配失败,后续读取就错位了。稳妥做法是把格式串改成"%d",让fscanf自动跳过所有空白符。这是我在类似题目上翻过的车:框架给的代码不一定适配你的测试文件,读进来全是 0 或乱序,先怀疑格式串。
4.2 OutputResult 的追加模式与格式
void OutputResult(int * SeatNum, int Count) { FILE* fp = fopen("d:\\result.txt", "a"); int i; fprintf(fp, "====================================\n"); for (i = 0; i < Count; i++) { fprintf(fp, "%d,", SeatNum[i]); } fprintf(fp, "\n"); fclose(fp); }"a"是追加模式,每次售票结果接在文件末尾,不会覆盖历史记录,这符合「多次售票、逐次记录」的要求。但要注意:追加模式不会自动清空旧文件,你反复跑程序,result.txt会越积越长,对答案时容易看串行。我的习惯是每次测试前手动删掉result.txt,或者在main开头用"w"模式打开一次再关掉,把文件清空。另外fopen返回值没判空,如果d:\盘不存在或没写权限,fp是 NULL,fprintf直接崩。加一行if (!fp) { perror("result.txt"); return; }能省不少排查时间。
4.3 路径写死带来的移植问题
框架里路径是d:\\request.txt和d:\\result.txt,写死在函数内部。换台机器没有 D 盘,程序静默失败——GetRequest里fopen返回 NULL,fscanf(NULL, ...)是未定义行为,可能直接段错误,也可能读到垃圾。题目说「根据实际情况,可以修改给出的程序」,那就大方改:把路径提成宏或全局变量,或者改成相对路径request.txt,把文件放可执行文件同目录。改的时候注意GetRequest里fp是静态初始化,路径必须在编译期确定,用宏最省事:
#define REQ_PATH "request.txt" #define RES_PATH "result.txt"然后把两处fopen的参数换掉。这一步改动小,但能让程序在你自己的机器上跑起来,不然连第一步都迈不出去。
5. 避坑与排查:五个真实会卡住你的点
5.1 现象:售 2 张时把 2 和 3 当成相邻卖了
原因:相邻判定用了abs(a-b)==1或a+1==b,没考虑过道分隔。2 和 3 座位号连续但分属左右两侧,题目明确不算相邻。
解决:回到间隔内位置判定,用 2.3 的IsAdjacent,位置 0-1、2-3 才成对。写完拿 1 到 8 号座位两两跑一遍,确认只有 (1,2)(3,4)(5,6)(7,8) 返回真。
5.2 现象:售 3 张时某间隔只剩 2 张,程序直接提示不足不卖了
原因:把「间隔内凑不齐」误判成「全车不足」。题目说的是全车剩余不足才拒售,间隔内不够属于「不能满足优先原则」,应走兜底随便卖。
解决:先CountRemain()判全车总数,够就继续找间隔;找不到合适间隔调SellAny(need, out),不要提前 return 不足。
5.3 现象:result.txt 里结果越跑越多,对不上标准输出
原因:OutputResult用追加模式,旧结果没清。反复调试时历史记录堆叠。
解决:每次运行前清空结果文件,或在main开头以"w"打开一次result.txt再关闭。测试脚本里加一句rm -f result.txt也行。
5.4 现象:GetRequest 读到第二个请求就返回 false
原因:输入文件里混入了 0,而框架用inputData[nIndex] == 0当结束标记;或者请求数超过 80,nIndex == 80触发停止。
解决:检查request.txt内容,确保只有 1 到 4 的数字、没有 0;请求数控制在 80 以内。如果业务上真需要 0,得改框架的结束判定逻辑,但考试场景按题目约定来即可。
5.5 现象:程序在别人机器上直接崩溃,自己机器正常
原因:路径写死d:\,对方没有 D 盘,fopen返回 NULL 未判空,后续文件操作是未定义行为。
解决:路径改成相对路径或宏配置,两处fopen后都加判空,失败时打印perror并退出。这是移植性最基本的后悔药,早加早省心。
6. 用状态快照验证结果:一个能省半天调试的习惯
这道题最容易出的错不是编译不过,而是「跑起来了、结果看着像对的、其实某条规则错了」。光靠肉眼扫result.txt很难发现,尤其售 3 张兜底和间隔优先混在一起时。我一般会加一个调试开关,每次售票后打印全车厢状态快照,用固定输入跑一遍,人工核对前几轮,确认规则无误再关掉。
快照函数这样写:
void DumpState(void) { for (int s = 0; s < SECTION_COUNT; s++) { printf("间隔%2d: ", s); for (int k = 0; k < 4; k++) { int n = sectionSeat[s][k]; printf("%2d[%c] ", n, seatState[n - 1] ? 'X' : 'O'); } printf("\n"); } printf("剩余: %d\n\n", CountRemain()); }X表示已售,O表示未售,每行一个间隔,正好对应题目图的布局。拿一份手写的小输入,比如2 2 3 4 1,预期是:第一次卖 (1,2),第二次卖 (3,4)(同间隔相邻对优先),第三次请求 3 张时第一个间隔已满、第二个间隔 {5,6,7,8} 全空,应卖 (5,6,7),第四次请求 4 张时第二个间隔只剩 8 号一张,凑不齐 4 张,走兜底从后面间隔随便卖 4 张。对着快照一步步看,哪条规则没生效一目了然。
验证时重点盯三个边界:全车只剩 1 张时请求 2 张(应提示不足)、某间隔剩 3 张时请求 4 张(应兜底而非拒售)、请求 4 张但全车只剩 3 张(应提示不足)。这三个用例过了,规则实现基本就稳了。从那以后我每次写这类带优先级的模拟题,都强制先写快照再写主逻辑,调试时间至少砍一半。希望这个习惯也能帮到你,这份考题的完整框架和输入样例,拿去照着跑一遍,比看十篇讲解都管用。
本文还有配套的精品资源,点击获取