news 2026/10/28 2:12:07

C语言实战:火车售票系统数据结构与优先级算法详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C语言实战:火车售票系统数据结构与优先级算法详解

简介:这份资源是东软集团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第二轮「先统计再出票」的顺序必须保留,换成结构体后如果图省事边扫边标记,回滚逻辑会跟着变复杂。我当初重构一个类似模块时就栽在这,统计和出票揉在一起,测试用例一多,状态对不上,查了大半天才发现是半途失败没回滚。从那以后我每次动这类「先判断后修改」的逻辑,都强制先把候选集收全、数量确认无误再统一落状态,宁可多一个临时数组,也不让中间态漏出去。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/28 2:11:45

Maximo数据库配置核心原理与configdb执行规范

简介&#xff1a;本资源是一份面向Maximo初学者与实施人员的系统性入门培训文档&#xff0c;聚焦资产管理系统的核心配置逻辑与实操基础。文档深入解析数据库配置&#xff08;含对象建模、主对象定义、持久性与层级分类&#xff09;及属性配置&#xff08;如类型长度、等同属性…

作者头像 李华
网站建设 2026/10/28 2:11:45

超市管理系统课程设计:七张表与八大模块的数据库实战

简介&#xff1a;这份Java超市管理系统实训报告面向计算机相关专业学生与Java初学者&#xff0c;服务于课程设计、数据库原理实践与小型超市管理场景&#xff0c;帮助读者完成从需求分析到系统测试的完整设计流程。压缩包内共1个doc文档&#xff0c;约411KB&#xff0c;内容涵盖…

作者头像 李华
网站建设 2026/10/28 2:11:08

DZ/T 0097—2021工程地质调查规范落地指南

简介&#xff1a;本资源为中华人民共和国地质矿产行业最新标准《DZ/T 0097—2021 工程地质调查规范&#xff08;1:50 000&#xff09;》正式PDF版&#xff0c;面向工程地质、岩土工程、勘察设计及自然资源系统技术人员&#xff0c;解决1:50 000比例尺区域调查中任务界定、复杂度…

作者头像 李华
网站建设 2026/10/28 2:11:03

线性分类器:深度学习的数学骨架与梯度传播原点

简介&#xff1a;本资源是一份面向深度学习初学者与进阶学习者的系统性理论入门文档&#xff0c;聚焦线性分类器、前馈神经网络与卷积神经网络三大核心模块&#xff0c;帮助读者扎实掌握模型背后的数学原理与实现逻辑。文档结构严谨&#xff0c;覆盖从数据准备、假设函数构建&a…

作者头像 李华
网站建设 2026/10/28 2:10:57

OSG 3.6.5+OSGEarth 3rdParty 编译配置与避坑

简介&#xff1a;面向 OSG 3.6.5 与 osgEarth 2.10 开发者的第三方依赖库合集&#xff0c;主要解决在 Visual Studio 2019 环境下编译 OpenSceneGraph 与 osgEarth 时依赖缺失、版本不匹配、构建过程冗长的问题。全部依赖均已完成 64 位预编译&#xff0c;下载并解压后即可配置…

作者头像 李华
网站建设 2026/10/28 2:10:52

PaddleOCR离线打包exe实战:模型内置与PyInstaller避坑指南

简介&#xff1a;这份资源面向需要在无 Python 环境下完成文字识别的开发者与运维人员&#xff0c;提供一套基于 PaddleOCR 的离线 exe 工具打包成品&#xff0c;解决现场部署时依赖缺失、无法直接调用 OCR 接口的问题&#xff0c;适合具备基础 Python 与打包经验的中级读者参考…

作者头像 李华