ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

C语言编码考试题拆解:火车站售票程序的规则优先级与数据结构设计

C语言编码考试题拆解:火车站售票程序的规则优先级与数据结构设计

简介:这是东软集团IA事业部07年编码考试C编程赛题,面向需要强化C语言基本功、数据结构与文件操作能力的开发者。题目以火车站售票程序为实战场景,要求模拟一节80座车厢的售票流程:定义座位状态数据结构,实现一次最多售4张票的规则,其中2张票优先相邻座位、3至4张票优先同一间隔,无法满足时再随机出票,并能够反复从文件读取购票需求、将售票结果写入文件。资源包含完整试题文档,共1个doc文件,压缩包大小54KB,其中涵盖题目背景、五道编程题分值分布、售票规则总则及程序要求,并附有GetRequest与OutputResult两个关键函数的参考实现,可直接用于搭建程序框架。文档同时注明了输入输出文件默认路径及调试用request.txt的用法,便于按真实考试条件自行验证。已有232人学习浏览,适合备考企业级C语言编码测试,也可作为数据结构与文件操作的实战练手素材。通过研读该试题,能够系统梳理座位状态管理、优先规则判断及文件解析的完整编码思路。

1. 这道C语言编码考试题,考的是四件能直接照做的事

第一次拿到这套早年某企业IA事业部的C语言编码考试题时,很多备考的人会误判:火车站售票程序?把80个座位卖了不就行了?等真正动手才发现,题目用一段很短的描述,同时压了四个考点——数据结构怎么设计、相邻与间隔怎么判定、三种售票规则的优先级怎么落地、文件读写接口怎么接。更反直觉的是,这套题总分40分,编码规范分含在每一题里,最多能扣除题目分数的三分之一。也就是说,程序能跑只算及格,规则分支写对、代码风格干净才是拿分关键。这篇文章就把这套题的考点分布、高频代码写法、答题顺序全部拆开,适合正在备考C语言笔试机试或技术面试的读者直接背下来练。

2. 考点与分值:40分里规范能扣13分,数据结构和规则占大头

2.1 40分的构成:为什么说“能跑”只是及格

题目原文写得很清楚:编程题总计40分,规范方面分数包含在每题分数中。很多人把注意力全放在“实现功能”上,忽略了最后那句“程序要求逻辑清楚、结构良好,符合编码规范要求,否则扣分,最多扣除题目分数的三分之一”。三分之一是什么概念?按照40分估算,规范扣分上限在13分左右。如果数据结构杂乱、函数职责混乱、变量命名随意,哪怕运行结果全对,也可能只拿到27分上下。反过来,功能全对加上规范良好,这题就是拉开差距的题目。

参考往年类似编码题的评分倾向,可以做一个相对合理的分值拆解,虽然不是原卷官方细则,但足够用来分配作答时间:

考察模块对应题目要求参考分值
座位数据结构定义与初始化第1条“定义存储车厢座位信息的数据结构和相关状态信息”8分
三种售票规则的优先级第2条“模拟售票过程”核心逻辑18分
文件读取与结果输出GetRequest / OutputResult 的正确使用6分
多次售票主循环与终止条件第2条“程序运行允许多次售票”4分
编码规范与结构第3条,最多扣除题目分数的三分之一上限约13分
运行结果正确第4条“程序能够运行,运行结果正确”包含在功能分中

从这个拆解能看出一件事:规则优先级那18分是命脉,而文件输入输出虽然只占6分,但它连接着主循环和题目给定的两个函数,一旦接错,规则写得再好也输出不出去。备考时应该把“数据结构—规则—文件—主循环”当成一条流水线来练,而不是分开背几个孤立的语法点。

2.2 从题目描述反推判分人会看什么

题目给了三个很具体的信号。第一,座位号从1到80,分为40个间隔,每个间隔包含两排座位。这不是普通的编号题,判分人一定会验证你的间隔划分是否符合“1、2、5、6一个间隔,3、4、7、8一个间隔”的规律。第二,GetRequest和OutputResult的实现在下页,可以直接使用。这说明这两个函数是送分工具,正确调用即可,不要去改动它们的数据读取方式,更不要自己重新写一套文件解析逻辑。第三,输入文件默认为d:\request.txt,输出文件默认为d:\result.txt,但“根据实际情况可以修改”。这意味着判分环境可能换路径,所以代码里不应该把路径写死到字符串拼接的角落,而应该让文件路径集中在程序开头,方便替换。

再往深一层想,判分人最常做的验证动作有三个:用题目附带的request.txt跑一遍,看输出文件是否生成且格式正确;连续多次请求,看是否出现重复售票;故意构造“请求3张但全车厢只剩2张”的输入,看程序是否提示不足且不售票。其中一个高频陷阱是,很多人会在“售3张或4张原则”里漏掉兜底逻辑。原文说的是“优先售一个间隔内的票;如果剩余票数不足,就提示不足,不售票。如果不能满足优先原则,则售任何位置的票”。注意这里的层次:总余票不够才提示不足,总余票够但同间隔凑不齐,要走“售任何位置”的兜底分支。这个分支在判分时经常被单独构造用例来测,比如请求4张,车厢里没有任何一个间隔剩4个空座,但全局仍有5张余票,正确输出应当是卖出4个任意位置,而不是拒绝售票。

2.3 一次完整的输入输出推演

结合题目给定的GetRequest实现,输入文件的格式是1到4之间的数字,中间可以用空格、制表符、换行符、换页符和回车符分割,读到0或文件结束时停止。假设request.txt内容为:

1 2 3 4 2 2 3 4 0

程序第一次读取到1,售1张;第二次读取到2,售2张;以此类推,读到0时停止售票。OutputResult每次会向result.txt追加一行分隔线加一行座位号,分隔线是20个等号,座位号用逗号结尾。需要注意的是,result.txt以追加方式打开,所以同一程序多次运行,结果会不断累积。如果要重新测试,应该先手动清空result.txt,否则会看到上次运行的历史记录,容易误判。

判分场景里还有一类边界是座位卖光后再来请求:此时GetRequest继续返回1到4,但车厢内余票为0,程序必须先检查“剩余票数是否足够请求数”,不足则提示且不调用OutputResult。很多人会在sell函数内部边卖边发现没有空座,结果把已经卖出的座号也写进了输出数组,最后输出了一组不完整的座位号。这类错误在笔试机器判分里是必扣分项,提前用“先统计全局余票,不够直接拒绝”的方式即可绕过。

3. 高频考点:80个座位的建模与“间隔/相邻”判定

3.1 三种数据结构方案,为什么推荐结构体数组

座位信息的存储方式,直接决定后续规则实现的复杂度。第一种是int seat[80],用0和1表示空和已售,简单但语义弱,打印调试时不直观。第二种是位标记,用80个bit表示状态,省空间但对初学者来说读写过于绕。第三种是结构体数组,每个座位有编号和状态两个字段,代码可读性最好,扩展时还能再挂“是否靠窗”“是否过道”等信息。本题只有80个座位,内存不是瓶颈,可读性和正确性才是重点,所以推荐结构体数组。

typedef struct Seat { int seatId; /* 座位号,从1到80 */ int sold; /* 0表示未售,1表示已售 */ } Seat; Seat seats[80]; void InitSeats(void) { int i; for (i = 0; i < 80; i++) { seats[i].seatId = i + 1; seats[i].sold = 0; } }

这段代码有三个细节。第一,seatId从1开始,和题目座位号对齐,避免“数组下标0对应座位1”的换算错误。第二,将所有座位初始化为未售,这个动作必须发生在任何售票逻辑之前。第三,用一个独立的初始化函数,而不是在main里散落循环,是为了满足“结构良好、符合编码规范”的扣分条款。面试官最想看到的是,初始化、状态查询、状态更新被拆成不同的小函数,而不是所有逻辑堆在一个while循环里。

3.2 相邻判定的正确公式:2和3不算相邻

题目对“相邻”有明确定义:1和2、3和4、5和6、7和8等,且特意强调“2和3不算相邻”。这句话很容易被忽略。如果直接写成“座位号相差1就算相邻”,2和3会被误判为相邻,而它们实际上分属两个不同的相邻对。

相邻对的规律是:座位号除以2之后落在同一个“对”里。1和2的(座号-1)/2都等于0,3和4的(座号-1)/2都等于1,5和6的(座号-1)/2都等于2。所以判断函数可以写成:

int IsAdjacent(int a, int b) { return (a - 1) / 2 == (b - 1) / 2; }

这个函数的参数单位是真实座位号,不是数组下标。如果你把数组下标直接传进去,比如seats[0]对应座位1、seats[1]对应座位2,那么(0-1)/2和(1-1)/2不同,会错误地认为1和2不相邻。写函数时一定在调用入口换算成座位号。另一个常见误用是直接判断a和b的差值等于1,这会把2和3、4和5也判成相邻,和题目规则冲突。

3.3 间隔判定的公式:先把布局图读明白

原题给出的布局可以简化为四行座位:第一行1、5、9直到77,第二行2、6、10直到78,过道,第三行3、7、11直到79,第四行4、8、12直到80。所谓“间隔”指的是过道同一侧、相邻两排的四个座位。验证例子:1、2、5、6属于同一间隔;3、4、7、8属于同一间隔;9、10、13、14属于同一间隔。规律是每隔两个“相邻对”换一次侧边,再每隔四个座位换一排。

int IntervalOf(int seat) { int idx = seat - 1; int row = idx / 4; /* 第几排,0到19 */ int col = idx % 4; /* 0、1在过道一侧,2、3在另一侧 */ return (row / 2) * 2 + (col >= 2); }

调用示例:座位1,idx为0,row为0,col为0,返回0;座位5,idx为4,row为1,col为0,(1/2)*2+0等于0,和座位1同间隔;座位3,idx为2,row为0,col为2,返回1;座位8,idx为7,row为1,col为3,返回1。这四个例子覆盖了首排、第二排、过道两侧,能验证公式是否写对。实际考试时可以把这个推导过程写在注释里,再用一个断言式的测试代码验证几个已知间隔,确保不把间隔和“相邻对”混淆。

间隔判定的价值在于售3张或4张票的优先逻辑:遍历0到39号间隔,统计某个间隔内4个座位的空座数,够数就取。如果代码里没有一个可靠的IntervalOf函数,这一步就只能硬编码映射表,40个间隔写出来既冗长又容易漏,不推荐。

4. 高频考点:三种售票规则的实现顺序与兜底分支

4.1 售1张与售2张:相邻优先和“随便”的差别

售1张票的原则是“随便售出一张没有售出的票”。这里的“随便”在实际判分里等价于“任意合法空座”,不强制要求随机。写代码时用遍历找第一个空座即可,刻意引入rand()随机函数反而可能导致输出不稳定,增加自查难度。售2张票的原则是“优先售2张相邻的票,如果没有就随便售出两张票”。所谓相邻只认相邻对,即座位号(2k+1, 2k+2)这一对,其中k从0到39。

int SellOne(int *out, int *count) { int i; for (i = 0; i < 80; i++) { if (seats[i].sold == 0) { seats[i].sold = 1; out[(*count)++] = seats[i].seatId; return 1; } } return 0; } int SellTwoAdjacent(int *out, int *count) { int k; for (k = 0; k < 40; k++) { int a = 2 * k + 1; int b = 2 * k + 2; if (seats[a - 1].sold == 0 && seats[b - 1].sold == 0) { seats[a - 1].sold = 1; seats[b - 1].sold = 1; out[(*count)++] = a; out[(*count)++] = b; return 1; } } return 0; }

SellOne和SellTwoAdjacent都返回int,用1表示成功、0表示失败,主函数可以根据返回值决定是否调用OutputResult。SellTwoAdjacent里有个细节:a和b是座位号,访问seats数组时必须减1;同时要保证“两个都空”再一起售出,不能先卖出a再发现b已售,那样会造成只卖出一张的脏状态。如果SellTwoAdjacent返回0,才进入“随便售两张”的兜底,此时同样不能重复选择同一个座位,要维护一个局部计数器,找到两个不同空座。

4.2 售3张或4张:优先同间隔,失败后不拒绝而是兜底

这是整道题逻辑最重的部分。原文规则分两层:第一层,“优先售一个间隔内的票”;第二层,“如果剩余票数不足,就提示不足,不售票;如果不能满足优先原则,则售任何位置的票”。正确的实现顺序应当是:先统计全局余票,全局余票不足请求数,直接提示不售;全局够时,遍历所有间隔找空座数大于等于请求数的间隔;找不到这样的间隔,就兜底从任意位置凑够请求数。

int SellByInterval(int need, int *out, int *count) { int g; for (g = 0; g < 40; g++) { int base = (g / 2) * 8 + (g % 2) * 2 + 1; int cand[4]; int freeCount = 0; int i; cand[0] = base; cand[1] = base + 1; cand[2] = base + 4; cand[3] = base + 5; for (i = 0; i < 4; i++) { if (seats[cand[i] - 1].sold == 0) { freeCount++; } } if (freeCount >= need) { for (i = 0; i < 4 && *count - (out - (int*)0) < need; i++) { if (seats[cand[i] - 1].sold == 0) { seats[cand[i] - 1].sold = 1; out[(*count)++] = cand[i]; } } return 1; } } return 0; }

注意cand数组的生成逻辑:base的公式由第3章的IntervalOf反推而来,也可以不用公式,直接维护一个二维数组intervalSeats[40][4],把所有间隔的四个座位号预先填好。笔试时间紧张时,预填表比公式更稳,缺点是占篇幅。SellByInterval只负责“优先”分支,找不到符合要求的间隔时就返回0,由调用方决定是否进入兜底。

兜底函数就是普通的“从前往后收集need个空座”,结构类似SellOne但需要收集多个:

int SellAny(int need, int *out, int *count) { int i; for (i = 0; i < 80; i++) { if (seats[i].sold == 0) { seats[i].sold = 1; out[(*count)++] = seats[i].seatId; need--; if (need == 0) { return 1; } } } return 0; }

SellAny有一段容易出错的边界:循环里先标记已售,再递减need,如果need减到0就返回。如果剩余空座总数恰好等于need,循环自然结束并返回1;如果总数小于need,说明调用方在“全局余票检查”那里漏了判断,sell函数会边走边卖,最后卖出部分座位却返回0,主函数又不会调用OutputResult,导致部分座位被标记已售但结果没有输出。这是典型的脏状态,自测时要把“余票不足”的输入也跑一遍,确认输出文件没有任何新增内容。

4.3 主循环与给定文件函数的正确接法

题目提供GetRequest和OutputResult这两个函数,主循环的写法直接决定调试效率。GetRequest内部用静态变量缓存了输入文件的所有数据,每次调用取一个数,取到0或读满80个数字后返回false。OutputResult每次以追加方式打开result.txt。主流程建议先做全局余票检查,再按需求数分发到对应的卖票函数。

int main(void) { int req = 0; InitSeats(); while (GetRequest(&req)) { int out[4]; int count = 0; int totalFree = 0; int i; for (i = 0; i < 80; i++) { if (seats[i].sold == 0) totalFree++; } if (totalFree < req) { printf("Insuffcient tickets\n"); continue; } if (req == 1) { SellOne(out, &count); } else if (req == 2) { if (!SellTwoAdjacent(out, &count)) { SellAny(2, out, &count); } } else { if (!SellByInterval(req, out, &count)) { SellAny(req, out, &count); } } if (count > 0) { OutputResult(out, count); } } return 0; }

这段代码里有几个值得向面试官解释的设计。第一,totalFree每次循环重新统计,而不是维护一个全局计数器,虽然效率低,但能避免售出座位后忘记递减计数器导致的逻辑错误,80个座位的规模完全够用。第二,count在进入每个分支前都被重置为0,out数组长度固定为4,因为题目限定每次最多售4张。第三,只有count大于0才调用OutputResult,保证“余票不足”的场景不会往输出文件里写空行。

GetRequest和OutputResult的使用有个隐藏考点:不要试图“修复”它们的实现。比如GetRequest用fscanf读入时把逗号当作格式分隔符,而输入文件实际可能以空格和换行分隔。fscanf的格式串“%d,”会在读完数字后尝试匹配逗号,匹配失败并不会导致本次读取失败,所以这个实现能够兼容空格制表符换行符等常见分隔。如果你自己重写一个读取函数,反而可能破坏“0表示停止”的约定,或者引入文件指针重复打开的问题。

5. 易错点:五个失分点与对应的正确写法

5.1 把2和3当成相邻,邻座规则直接错一半

错在哪:有考生把“相邻”理解为“座位号连续”,于是SellTwoAdjacent里判断条件写成abs(a - b) == 1,导致2和3被配成一对。

为什么错:题目明确说“2和3不算相邻”,相邻只发生在(1,2)、(3,4)、(5,6)这种奇数在前、偶数在后的组合里。如果按绝对值差1判断,卖出2、3这一对时,位置上是背对背的两个座,不符合“相邻”的判定标准,这种输出在判分时属于规则错误。

怎么答对:用(座号-1)/2是否相等来判断。1和2的(1-1)/2与(2-1)/2都是0,3和4的(3-1)/2与(4-1)/2都是1,7和8的(7-1)/2与(8-1)/2都是3,而2和3的(2-1)/2等于0、(3-1)/2等于1,自然判为不相邻。

5.2 间隔公式在“3、4、7、8”上验证失败还硬写

错在哪:写出一个间隔函数后,只用1、2、5、6验证,发现返回0就收工,没有继续验证3、4、7、8。

为什么错:车厢的间隔划分不是简单的“每四个座一组”。如果公式写成(seat-1)/4,座位1、2、5、6会被拆成两组,因为它们对应的数组下标0、1属于同一组,而下标4、5属于下一组。所以只验证第一个间隔会掩盖公式错误。

怎么答对:把三个已知样本全部跑一遍:1、2、5、6要返回同一个间隔值,3、4、7、8要返回另一个间隔值,9、10、13、14要返回第三个间隔值。最好的办法是写一个InitIntervalTable,提前把40个间隔的座位号映射填入二维数组,运行时直接查表,彻底绕开公式推导风险。

5.3 售3张或4张找不到同间隔就直接拒绝

错在哪:SellByInterval返回0后,主函数直接continue,不调用任何兜底卖票逻辑。

为什么错:题目说的是“如果剩余票数不足,就提示不足不售票;如果不能满足优先原则,则售任何位置的票”。“剩余票数不足”指全局余票小于请求数,而“不能满足优先原则”指全局余票够但没有任何一个间隔能凑齐请求数。这两种情况性质不同,后者仍然要卖出任意位置的票。

怎么答对:在主函数里先算totalFree,totalFree小于req才拒绝;SellByInterval失败后紧接着调用SellAny(req, out, &count)。为了验证这条逻辑,可以构造这样的输入:先把某些座位手动售出,使得某个请求为4的时刻,所有间隔最多只剩3个空座,但全局还有6张票,此时程序必须卖出4张任意座位。

5.4 输出文件用“w”覆盖,或忽略了GetRequest的越界隐患

错在哪:自己写输出函数时用fopen("d:\result.txt", "w"),第二次售票的结果把第一次覆盖;或者在使用GetRequest读取时没有注意到它内部用静态数组保存80个数,当nIndex等于80时仍然先访问inputData[80]再判断条件。

为什么错:题目给出的OutputResult用的是“a”追加模式,覆盖写会让多轮售票结果只剩最后一轮,判分时只能看到一次输出。越界问题属于原题给定实现的一个隐患,虽然实际输入文件不太可能正好80个非零数字,但严格来说nIndex等于80时访问inputData[80]已经是数组越界,属于未定义行为。

怎么答对:输出模式保持附录里的“a”,不要动;如果自己要审查GetRequest,可以把条件顺序改成if (nIndex >= 80 || inputData[nIndex] == 0),先判断下标再取值。这道题考的是编码规范里的边界意识,能指出这个隐患并在注释里说明,比闷头调用更能体现功底。

5.5 兜底卖任意座位时把同一个座位卖两次

错在哪:SellAny函数边遍历边标记已售,但没有跳过那些在SellByInterval里已标记的座位,导致out数组里出现重复座位号。

为什么错:SellByInterval在发现间隔内空座够数时会直接把前几个空座标记为已售,并把座号写入out。如果这个函数没有正确返回,或者调用方在失败后又在没清空out的情况下追加SellAny,就会重复卖出。还有一种情况是SellAny的内部循环没有区分“当前座位是否已售”,用未售判断时写成了if (seats[i].seatId),等于没有判断状态。

怎么答对:统一封装一个标记函数MarkSold(seatId),所有售卖分支都通过它更新状态;SellAny循环里严格判断sold字段,并且在每次售卖前把count重置为0。自测方法:连续请求50次以上,最后检查输出数组里80个座位号是否正好覆盖1到80且没有重复,这一步可以用一个简单的计数数组来验证。

提示:把上面五条整理成一份自测清单,每次写完代码先过一遍清单再跑数据,能省下大量调试时间。

6. 40分钟应试答题顺序:从读题到自测的固定流程

6.1 前5分钟:把规则翻译成伪代码

拿到题先不要急着开写。第一件事,画出四行座位布局,在纸上标出1、2、5、6和3、4、7、8两个间隔,推导间隔公式。第二件事,把题目里的三个优先级写成三行伪代码:1张随便;2张先相邻对再随便;3或4张先同间隔再随便,只有总余票不足才拒绝。这两步做完,整个程序的结构已经在脑中了,剩下的30多分钟只是把伪代码翻译成C语言。

6.2 第6到30分钟:按下层到上层的顺序写代码

先写InitSeats和IsAdjacent、IntervalOf这两个判定函数,再写SellOne、SellTwoAdjacent、SellByInterval、SellAny四个售座函数,最后写main主循环。这个顺序的好处是每个函数都能独立验证。例如写完IsAdjacent立刻测试(1,2)返回1、(2,3)返回0;写完SellTwoAdjacent立刻构造一个只有这两座空着的场景跑一遍。每完成一个函数就做一次最小验证,而不是全部写完再统一调试,否则出错了要同时排查五个函数,难度成倍增加。函数里所有输出参数统一采用(int *out, int *count)的形式,out数组由调用方提供,长度固定为4,count记录实际卖出数量,这个接口设计也能直接在面试时讲给面试官听,展示你的模块化意识。

6.3 最后5分钟:用三组输入做回归自测

第一组输入是“1 2 3 4 2 2 3 4 0”,用来验证基本流程和输出文件追加效果。第二组输入是“4 4 4 4 4 0”,用来验证每次请求4张时是否优先落在同一间隔;当连续请求多轮后,某些间隔空座不足时是否自动改为任意位置兜底。第三组输入是“1 1 1 1”(后面不跟0),让程序把座位卖到接近售罄,再补一个“2”请求,验证余票不足时是否提示并不输出。三组跑完,如果输出文件里的座位号没有重复、格式每行以逗号结尾、分隔线数量等于成功售票次数,这道题就稳了。

提示:这三组输入建议直接保存成三个request文件,调试时只改程序顶部的路径宏,不要每次手动重建输入。

最后说一个身边的真实教训。A同学当年考这套题,前30分钟就把主体代码写完了,剩下10分钟一直在调输出格式,结果忽略了“售4张且无同间隔可满足”的兜底分支,判分用例正好打在这个分支上,白白丢了一档分。从那以后,我每次做这类“规则优先”的题目,都会强制自己按“全局余票检查—优先分支—兜底分支”的顺序在纸上写一遍,再对照题目原文逐句核对,避免把“优先”当成“唯一”。这套题不难,难的是把规则当成代码里的分支而不是作文里的描述。希望这篇拆解能帮你在笔试机试和面试手写代码时少踩几个坑,拿到该拿的每1分。

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

返回列表