简介:这份资源是东软集团IA事业部2007年编码考试C语言试题C卷,面向准备技术面试、刷题巩固C语言基础的开发者与在校学生,核心考察数组或链表等数据结构设计、售票规则的条件判断以及文件读写操作。压缩包内共1个doc文档,约54KB,完整收录了火车站售票编程题的题干、座位布局示意、售票优先级规则说明,以及可直接复用的GetRequest与OutputResult函数框架,便于对照题目要求动手实现。题目要求模拟80个座位、40个间隔的车厢售票流程,处理1至4张票的相邻优先、同间隔优先与随机分配逻辑,并完成request.txt读取与result.txt结果追加。已有232人学习,适合借此练习编码规范、边界场景测试与文件错误处理,检验基本功是否扎实。
1. 从一份十七年前的编码题说起:80 个座位怎么卖票
前阵子帮一个刚入行的朋友翻 C 语言练手题,翻到一份很老的编码考试题,题目本身不复杂,但把「数据结构 + 规则优先级 + 文件读写」三件事压在一道题里,做完之后对 C 语言基本功的检验相当扎实。题目要求模拟一节火车车厢的售票过程:车厢 80 个座位,按间隔分组,每次最多卖 4 张,卖 1 张随便给、卖 2 张优先给相邻的、卖 3 到 4 张优先给同一间隔内的,凑不齐就提示不足不卖。输入从文件读购票请求,输出把售出的座位号追加写回文件。
这道题适合谁?适合刚学完数组、指针、文件操作,想找一个「有真实业务规则、又不是玩具级」的题目练手的人;也适合带新人的开发者,拿它当一次小型的代码规范与逻辑拆解训练。它不涉及任何框架,纯 C,编译即跑,但规则里的优先级判断和边界处理,恰恰是最容易翻车的地方。下面按「题目到底要什么 → 怎么把规则翻译成代码 → 怎么跑起来 → 怎么改」的顺序拆一遍。
2. 题目规则拆解:座位布局与售票优先级怎么落到数据结构上
2.1 座位编号与间隔的映射关系
先把座位布局吃透。题目给的图里,座位号 1 到 80,每 8 个号构成一个「间隔」,一个间隔内是两排各 4 个座位,中间是过道。具体来说,1、2、5、6 在一个间隔,3、4、7、8 也在一个间隔——注意这里「间隔」指的是物理上被过道分隔的一小块区域,而相邻关系只看编号是否连续成对:1 和 2 相邻,3 和 4 相邻,5 和 6 相邻,7 和 8 相邻;1 和 5 不算相邻,2 和 3 也不算相邻。
这个映射关系决定了后面所有判断的写法。最直接的做法是用一个长度 81 的数组(下标 0 不用,1 到 80 对应座位号),元素值表示状态:0 未售,1 已售。相邻判断就退化成「两个下标是否构成 (1,2)(3,4)(5,6)(7,8) 这种配对」,间隔判断则退化成「座位号减一后除以 8 是否相同」。
/* 座位状态:0 未售,1 已售;下标 1..80 对应座位号 */ int seat[81] = {0}; /* 判断两个座位号是否相邻:同一对 (1,2)(3,4)(5,6)(7,8) */ int is_adjacent(int a, int b) { int lo = a < b ? a : b; int hi = a < b ? b : a; /* 相邻对满足:lo 为奇数且 hi = lo + 1,且 lo 落在 1,3,5,7 这类对的起点 */ if (hi != lo + 1) return 0; /* 每 8 个号为一组,组内偏移 0/1、2/3、4/5、6/7 成对 */ return ((lo - 1) % 8) % 2 == 0; } /* 判断两个座位是否在同一间隔:座位号减一后除以 8 相同 */ int same_compartment(int a, int b) { return (a - 1) / 8 == (b - 1) / 8; }is_adjacent里先保证两个号差 1,再用(lo - 1) % 8拿到组内偏移,偏移为偶数说明 lo 是这一对的左座。这样写比硬编码一堆 if 干净,也不容易漏掉 65 和 66 这种大号段。same_compartment更简单,减一除八就是间隔编号,1 到 8 是第 0 间隔,9 到 16 是第 1 间隔,依此类推,一共 10 个间隔。
提示:题目原文说「1,2,5,6 位于一个间隔内,3、4、7、8 也是一个间隔内的座位」,这里两个「间隔」其实是同一个物理隔间的两排。判断「同一间隔」时按 8 个号一组算,正好覆盖这两排,不用再细分。
2.2 售票规则的优先级翻译
规则有四条,翻译成代码时要按请求票数分支:
- 请求 1 张:找任意一个未售座位即可,从 1 到 80 顺序扫,第一个空位就是答案。
- 请求 2 张:先找一对相邻且都未售的座位;找不到再退化成任意两个空位。
- 请求 3 或 4 张:先找一个间隔内剩余空位够数的,把该间隔内的空位凑出来;凑不齐就提示不足、不售票。题目还说「如果不能满足优先原则,则售任何位置的票」,这句和「不足就不售」要分清:3、4 张时优先整间隔,整间隔凑不齐但全场空位够,就随便给;全场空位都不够,才提示不足。
这里有个容易读错的地方:3、4 张的「不足」指的是全场剩余票数不足请求数,而不是「同一间隔内不足」。原文「如果剩余票数不足,就提示不足,不售票」紧跟在「优先售一个间隔内的票」后面,结合「如果不能满足优先原则,则售任何位置的票」,正确理解是:优先整间隔,整间隔不行就全场随便凑,全场都不够才拒绝。这个理解差异直接决定测试用例过不过,我后面在避坑章节还会提。
2.3 请求与结果的读写接口
题目已经给了GetRequest和OutputResult两个函数的实现,可以直接用。GetRequest第一次调用时把d:\request.txt里的数字全部读进静态数组,之后每次返回一个 1 到 4 的请求,读到 0 或读完 80 个就返回 false 停止售票。OutputResult以追加模式打开d:\result.txt,每次写一条分隔线和一行座位号。
这两个函数用了static变量保存文件句柄和读入缓存,属于典型的「一次性初始化 + 多次调用」写法。自己写主循环时不用关心文件打开关闭,只要循环调用GetRequest拿请求、处理、再调OutputResult写结果就行。要注意的是GetRequest里fscanf用的格式是"%d,",意味着输入文件里的数字后面要跟逗号,纯空格分隔会读失败——这是原题实现的一个小坑,调试时输入文件格式必须对齐。
3. 核心实现:主循环、选座算法与文件输出怎么串起来
3.1 主循环骨架
主循环非常短:拿请求、按票数选座、写结果、继续。选座失败(返回 0 张)时打印提示,但不写结果文件,因为题目要求「提示不足,不售票」。
#include <stdio.h> #include <stdbool.h> bool GetRequest(int *p); void OutputResult(int *SeatNum, int Count); int main(void) { int req; int seats[4]; int count; while (GetRequest(&req)) { count = sell_tickets(req, seats); /* 返回实际售出张数,0 表示不足 */ if (count > 0) { OutputResult(seats, count); } else { printf("余票不足,本次请求 %d 张未售出\n", req); } } return 0; }sell_tickets是整道题的核心,按票数分派到不同策略。主循环里没有额外状态,所有座位状态都在全局seat数组里,这样GetRequest的静态缓存和座位状态互不干扰,逻辑最清晰。
3.2 卖 1 张和卖 2 张:顺序扫描 + 相邻配对
卖 1 张最简单,从头扫到尾,遇到 0 就卖。卖 2 张分两步:先扫相邻对,再退化到任意两个空位。
/* 卖 1 张:返回售出的座位号,无票返回 0 */ int sell_one(void) { for (int i = 1; i <= 80; i++) { if (seat[i] == 0) { seat[i] = 1; return i; } } return 0; } /* 卖 2 张:优先相邻,其次任意两个空位 */ int sell_two(int out[2]) { /* 第一轮:找相邻对,按 (1,2)(3,4)... 的顺序扫 */ for (int i = 1; i <= 80; i++) { if (seat[i] == 0 && i + 1 <= 80 && is_adjacent(i, i + 1) && seat[i + 1] == 0) { seat[i] = seat[i + 1] = 1; out[0] = i; out[1] = i + 1; return 2; } } /* 第二轮:任意两个空位 */ int n = 0; for (int i = 1; i <= 80 && n < 2; i++) { if (seat[i] == 0) { seat[i] = 1; out[n++] = i; } } return n; /* 可能只凑到 1 张,说明全场只剩 1 张 */ }第一轮扫描时is_adjacent(i, i + 1)保证了只认 (1,2)(3,4) 这类配对,不会把 (2,3) 当成相邻。第二轮退化逻辑里,如果全场只剩 1 张空位,n会是 1,调用方按实际张数处理即可——题目说「随便售出两张票」,但物理上只剩一张时只能卖一张,这种边界要能兜住,不能越界写out[1]。
3.3 卖 3 张和 4 张:整间隔优先 + 全场兜底
这是最容易写错的一段。思路是:先遍历 10 个间隔,看哪个间隔内空位数够请求数,够就从这个间隔里挑;都不够,再全场扫空位凑;全场空位总数还不够,返回 0。
/* 卖 n 张(n 为 3 或 4):优先同一间隔,其次全场任意,不足返回 0 */ int sell_many(int n, int out[4]) { /* 第一轮:按间隔找,间隔内空位够 n 个就整间隔出票 */ for (int c = 0; c < 10; c++) { int base = c * 8 + 1; int free_cnt = 0; for (int k = 0; k < 8; k++) if (seat[base + k] == 0) free_cnt++; if (free_cnt >= n) { int got = 0; for (int k = 0; k < 8 && got < n; k++) { if (seat[base + k] == 0) { seat[base + k] = 1; out[got++] = base + k; } } return got; } } /* 第二轮:全场空位总数够就随便凑 */ int total = 0; for (int i = 1; i <= 80; i++) if (seat[i] == 0) total++; if (total < n) return 0; /* 全场都不够,提示不足 */ int got = 0; for (int i = 1; i <= 80 && got < n; i++) { if (seat[i] == 0) { seat[i] = 1; out[got++] = i; } } return got; }第一轮里base = c * 8 + 1把间隔编号换算成起始座位号,第 0 间隔是 1 到 8,第 9 间隔是 73 到 80。free_cnt >= n判断这个间隔能不能一次出够票,能就按顺序挑空位。第二轮先统计全场空位总数,不够直接返回 0,够再顺序凑——这个「先统计再出票」的顺序很重要,如果边扫边卖,卖到一半发现不够,座位状态已经被改脏了,回滚很麻烦。
3.4 分派函数与输出格式
把三个策略串起来的分派函数:
int sell_tickets(int req, int out[4]) { switch (req) { case 1: { int s = sell_one(); if (s == 0) return 0; out[0] = s; return 1; } case 2: return sell_two(out); case 3: case 4: return sell_many(req, out); default: return 0; /* 题目说不需容错,这里兜底 */ } }输出由题目给的OutputResult负责,每次写一行====================================再写一行逗号分隔的座位号。追加模式意味着多次运行会不断往同一个文件里加内容,调试时如果发现结果越积越多,先清空d:\result.txt再跑,别以为是程序重复出票。
4. 跑起来:编译、输入文件格式与结果验证
4.1 编译与文件路径
纯 C 代码,没有第三方依赖,Linux 下gcc -std=c99 -Wall -o ticket ticket.c即可,Windows 下用任意 C 编译器建控制台工程。题目默认路径是d:\request.txt和d:\result.txt,在 Linux 或 macOS 上跑要改成当前目录,比如./request.txt,或者干脆把路径抽成宏:
#define REQ_PATH "request.txt" #define RES_PATH "result.txt"改路径时注意GetRequest里fopen失败没有判空,文件不存在会直接崩,调试前先确认输入文件在位。这是原题实现的一个粗糙点,自己改的时候建议补一句if (!fp) { perror("request.txt"); return false; }。
4.2 输入文件格式对齐
GetRequest用fscanf(fp, "%d,", &inputData[i])读数据,格式串里带逗号,所以输入文件必须是1,2,3,4,这种逗号分隔,纯空格或换行分隔会读失败。题目原文说「中间可以以空格、制表符、换行符、换页符和回车符分割」,但给的实现是逗号格式,两者有出入——以给的实现为准,输入文件写成逗号分隔最稳。一个能直接用的测试输入:
1,2,3,4,1,2,3,4,1,2,3,4,1,2,3,4,1,2,3,4,1,2,3,4,1,2,3,4,1,2,3,4,1,2,3,4,1,2,3,4,1,2,3,4,1,2,3,4,1,2,3,4,1,2,3,4,1,2,3,4,1,2,3,4,1,2,3,4,1,2,3,4,1,2,3,4,1,2,3,4,80 个请求循环 1 到 4,正好把 80 个座位卖完,最后几个请求会触发「不足」分支,能同时验证正常出票和拒绝出票两条路径。
4.3 结果验证的检查点
跑完之后看result.txt,重点核对三件事:卖 2 张时是否优先出现 (1,2)(3,4) 这类配对;卖 3、4 张时是否优先集中在同一间隔;总售出数是否不超过 80 且无重复座位号。可以写个小脚本统计:
# 统计结果文件里出现的座位号,检查是否有重复 grep -o '[0-9]\+' result.txt | sort -n | uniq -d没有输出说明无重复。再数一下总行数,和输入请求里成功售出的次数对得上,就基本没问题。如果发现卖 2 张时出现了 2 和 3 这种组合,回去查is_adjacent的组内偏移判断;如果卖 4 张时座位散落在多个间隔,查sell_many第一轮的free_cnt统计是否漏了边界。
5. 避坑与排查:这道题最容易翻车的五个地方
5.1 相邻判断把 (2,3) 当成相邻
现象:卖 2 张时输出里出现 2 和 3、6 和 7 这类组合。原因:只判断了两个号差 1,没判断是否落在同一对里。解决:用((lo - 1) % 8) % 2 == 0限定左座必须是组内偶数偏移,或者直接枚举四对偏移{0,1,2,3,4,5,6,7}里成对的(0,1)(2,3)(4,5)(6,7)。
5.2 卖 3、4 张时把「间隔不足」当成「全场不足」
现象:明明全场还有几十张空位,请求 4 张却提示不足。原因:只检查了单个间隔的空位数,没做全场兜底。解决:按 3.3 的两轮结构写,第一轮整间隔,第二轮全场统计后再出票,两轮都不过才返回 0。
5.3 边扫边卖导致状态脏了
现象:请求 4 张,扫到一半发现凑不齐,返回不足,但前面已经标记售出的座位没回滚,下次请求少了几张票。原因:出票和判断混在一个循环里。解决:先统计够不够,够再统一标记;或者先把候选座位号收进临时数组,确认数量够了再批量置 1。
5.4 输入文件格式不匹配读不到数据
现象:程序一启动就退出,或者只处理了第一个请求。原因:fscanf格式串是"%d,",输入文件用了空格分隔。解决:输入文件统一逗号分隔,或者把格式串改成"%d"并自行跳过非数字字符。改格式串时注意别把逗号读进下一个数。
5.5 输出文件追加导致结果越跑越多
现象:第二次运行后result.txt里出现两轮完整记录,误以为程序重复出票。原因:OutputResult用"a"追加模式,这是题目设计如此。解决:每次测试前手动清空结果文件,或者在主函数开头用"w"模式先清一次再交给OutputResult追加。
6. 二次开发:把这道题改成一个可复用的选座模块
这道题虽然老,但选座逻辑本身有复用价值。真实场景里座位状态往往来自数据库或接口,规则也更复杂(连座、靠窗、家庭座),把题目里的硬编码数组换掉,就能抽出一个轻量的选座核心。我的改法是三步:把seat数组换成结构体数组,带上座位属性;把「相邻」「同间隔」抽成可配置的判定函数;把文件读写换成回调。
typedef struct { int no; /* 座位号 */ int sold; /* 0 未售 1 已售 */ int window; /* 是否靠窗,扩展属性 */ int group; /* 间隔编号,替代 (no-1)/8 硬算 */ } Seat; /* 判定函数指针,方便替换规则 */ typedef int (*AdjacentFn)(const Seat *a, const Seat *b);这样改的好处是,原来写死的(a - 1) / 8变成a->group == b->group,换车厢布局只改初始化数据,不动算法。sell_many里的base = c * 8 + 1也改成按group分组遍历,间隔大小不再固定为 8。验证方法很直接:保留原来的 80 座测试输入,改造前后跑同一份request.txt,对比result.txt的座位号序列,只要规则没变,输出应该逐行一致——这是重构时最省心的回归手段。
| 改造点 | 原实现 | 改造后 | 验证方式 |
|---|---|---|---|
| 座位存储 | int seat[81] | Seat seats[N]结构体数组 | 同一输入输出一致 |
| 间隔判定 | (no-1)/8硬算 | group字段比较 | 换布局只改初始化 |
| 相邻判定 | 固定配对公式 | 函数指针可替换 | 单测覆盖 (2,3) 反例 |
| 输入输出 | 固定文件路径 | 回调或接口注入 | 内存流模拟读写 |
改造时有个细节别丢:sell_many第二轮「先统计再出票」的顺序必须保留,换成结构体后如果图省事边扫边标记,回滚逻辑会跟着变复杂。我当初重构一个类似模块时就栽在这,统计和出票揉在一起,测试用例一多,状态对不上,查了大半天才发现是半途失败没回滚。从那以后我每次动这类「先判断后修改」的逻辑,都强制先把候选集收全、数量确认无误再统一落状态,宁可多一个临时数组,也不让中间态漏出去。希望帮到你。
本文还有配套的精品资源,点击获取