ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

电梯模拟系统数据结构设计与LOOK调度算法C语言实现

电梯模拟系统数据结构设计与LOOK调度算法C语言实现 简介这是一份以电梯模拟为课题的数据结构课程设计/毕业设计报告适合计算机相关专业学生用于课程设计参考、算法设计练习与答辩准备。文档基于经典《数据结构C语言版》教材完整覆盖问题分析、系统分析、概要设计、详细设计到运行测试等环节将电梯调度抽象为多个队列与栈结构展示了链队列和乘客栈在模拟系统中的应用思路。资源包内仅包含1个doc格式文档整体约523KB内容紧凑但结构完整便于直接阅读和打印提交。该报告已被139人浏览学习可作为独立完成课程设计、撰写设计报告的参照模板。通过研读其中的需求分析、数据结构定义与算法流程可帮助巩固数据结构知识提升独立分析问题和编码调试的能力。1. 数据结构电梯模拟别把它当成一道画图题毕业设计里的“数据结构电梯模拟”核心不是画一台会动的电梯而是用数据结构与算法回答一个具体问题面对不同楼层、不同方向的乘客请求电梯怎么决策才公平又高效。这个题目在数据结构C语言版的课程设计、考研408复习里都是熟面孔因为它能把队列、栈、位图、状态机串成一条完整的仿真链路。你要交付的不是动画而是一套调度内核参数可调、结果可复现、指标可对比最后能支撑一份数据扎实的实验报告。适合正在做课程设计或毕设的学生也适合拿它练离散事件模拟手感的工程师。理解这一点后面所有设计都不会跑偏。2. 拆解一个电梯模拟系统数据结构选型与核心结构体2.1 电梯模拟是在模拟什么三张表和一个状态机电梯模拟去掉界面动效之后剩下的是一个离散事件系统。常见做法是把系统拆成“楼层外呼表、轿厢内呼表、电梯本体”三个对象。楼层外呼表记录每一层有没有人按上行、下行轿厢内呼表记录电梯里乘客按下的目标楼层电梯本体则是一个有限状态机在待机、运行、开门三个状态之间迁移。这三张表对应到数据结构上非常直接。外呼和内呼都是“动态增删的目标集合”核心操作是插入、删除、查询最近目标电梯本体则要求方向与状态不能互相矛盾。搭建这个目标模型是整份毕业设计的地基。地基没选好后面的调度算法写得再漂亮也跑不出可靠数据。状态机里IDLE 与 RUN 之间的迁移只由方向刷新函数触发DOOR 与 RUN 之间由到站事件和关门事件触发。有人会把开门也建模成多个状态比如开门中、等待、关门中。对毕设来说拆成单个 DOOR 周期加一个门计时器就够了粒度太细反而容易在门状态里插入调度逻辑埋下后面讲的时序 bug。模型的粒度要和验证目标匹配你要研究的是调度策略不是门机的电气时序。2.2 用位图还是链表组织楼层请求选型理由与 C 语言落地外呼请求的数据组织方式我一般先排除链表。链表插入和删除灵活但查找“上方最近的目标楼层”要遍历所有节点而且删除一个目标要先找到它遍历成本在频繁到达的请求流里会被放大。《大话数据结构》里讲线性表时反复用链表举例但电梯这个场景里链表不是最优解。用优先队列可以快速取最近目标可一旦要“取消某个楼层的外呼”比如电梯已经停靠服务完优先队列只有懒惰删除逻辑绕容易留 bug。位图是和电梯调度最契合的一组。每一层占一个 bit位 1 表示“这个楼层有请求”。查找最近目标时从当前楼层沿方向逐位扫描循环层数就是楼层数常数小。而且楼层数在 12 层以内时一个 uint64_t 就装下全部目标更新和检查都只要一次与或运算。三种结构对比如下。数据结构插入删除指定目标找最近目标实际踩坑链表O(1)O(n)O(n)删除要遍历方向扫描要重写优先队列O(logn)懒惰删除或标记麻烦O(logn)“取消目标”的状态管理复杂位图O(1)O(1)至多 O(楼层数)楼层超过位宽要分段所以这套系统的数据组织我推荐位图。选型理由将来可以直接写进数据结构实验报告的复杂度分析一节逐项对比 O(1) 和 O(n)比空谈“我用了队列”扎实得多。2.3 核心结构体定义与初始化可以直接抄的 C 代码下面这组结构体是整套模拟的地基可以直接建一个新文件存起来#define MAX_F 12 // 楼层数改成 20 也够但别超过 63 #define MAX_PEOPLE 13 // 轿厢核载测试满载时可临时改成 2 #define FLOOR_TIME 5 // 单层运行步数一个步长代表 0.5 秒 #define DOOR_TIME 20 // 开关门周期单位是步 typedef enum { IDLE 0, RUN 1, DOOR 2 } ElevState; typedef enum { DIR_UP 1, DIR_DOWN -1, DIR_STOP 0 } Dir; typedef struct { int cur_floor; // 当前所在楼层1 到 MAX_F Dir dir; // 当前方向 ElevState status; // 待机 / 运行 / 开门 int people; // 轿厢人数用于满载逻辑 uint64_t stops; // 内呼位图bit(k) 1 表示目标楼层 k1 } Elevator; typedef struct { uint64_t up_bits; // 各楼层上行外呼位图 uint64_t down_bits; // 各楼层下行外呼位图 } CallBoard; typedef struct { int floor; // 外呼楼层 int dir; // 1 上行0 下行 int arrive_step; // 请求生成时的仿真步数 int serve_step; // 乘客被接上时的仿真步数0 表示未服务 } CallRec;这里有两个关键约定。楼层从 1 开始编号位图第 0 位对应 1 层所以1ULL (floor - 1)直接做映射uint64_t最大覆盖 63 层普通课程设计完全够用。如果题目硬性要求 64 层以上就把位图改成两个uint64_t的数组查找循环从段内扫描改成分段跨扫描这是少数需要动结构的地方。CallRec是给统计模块用的记录每个外呼的生成时间和服务时间最后算平均等待全看它。初始化函数长这样指针传进来一次清干净void init_system(Elevator *e, CallBoard *cb) { e-cur_floor 1; e-dir DIR_STOP; e-status IDLE; e-people 0; e-stops 0; cb-up_bits 0; cb-down_bits 0; }这段初始化有个值得注意的约定电梯初始停在 1 层、方向 DIR_STOP。很多实现喜欢把初始方向设成 DIR_UP结果模拟一开始电梯自动升到顶层再回来白跑一趟第一批乘客的等待时间也被抬高了。初始方向应该由第一个外呼的方向决定而不是拍脑袋。这也是后面调度函数要在 IDLE 状态下先找第一个目标的原因。3. 调度算法怎么选FCFS、SSTF 与 LOOK 的参数与实现调度算法是这份毕业设计真正出分的地方。数据结构课里的队列、排序、栈都在这里体现得淋漓尽致而电梯和操作系统里的磁盘寻道是同一套数学模型都在一个一维地址空间里响应离散请求都要在效率与公平之间做取舍。算法选型可以先想清楚指标再落到代码。3.1 三种调度策略的定性对比公平、效率与饥饿先列一张对比表表里的结论是代表性结果不是绝对定论。策略核心思想平均等待最坏等待数据结构典型问题FCFS按到达顺序逐一响应中长队列电梯在低层和高层之间来回空跑SSTF每次响应最近请求较好无上界优先队列或链表饥饿远端请求长时间不被服务SCAN/LOOK沿方向扫描到头返回好有上界位图加方向状态参数不配合会空跑FCFS 最容易实现一个先进先出队列就能跑但它完全不顾电梯当前的位置。请求一多电梯就在两端之间来回摆动平均等待看着还行最坏等待非常难看。SSTF 的思路是贪心每次去最近的目标平均等待确实好但贪心带来饥饿只要高层持续有新请求低层请求永远等不到头这在答辩时会被一眼问穿。SCAN 也叫电梯算法先运动到边界再返回顺路处理请求。LOOK 是它的改进版方向不变但当前方向没有请求时立即转向不空跑到边界。我推荐课程设计和毕设都做 LOOK 变体理由有三个实现不比 SCAN 复杂统计指标比 SCAN 好看答辩时你能讲清楚 SCAN 与 LOOK 的差异这在《数据结构与算法分析》那类教材里是标准考点讲出来是加分项。3.2 LOOK 算法的 C 语言实现方向位图扫描与转向判断LOOK 的决策函数就两件事沿当前方向找下一站找不到才转向。先写找下一站int find_next_stop(CallBoard *cb, Elevator *e) { if (e-dir DIR_STOP) return 0; // 待机状态不搜索 int step (e-dir DIR_UP) ? 1 : -1; for (int f e-cur_floor step; f 1 f MAX_F; f step) { if (should_stop(cb, e, f)) return f; // 找到第一个该停的楼层 } return 0; // 当前方向上没有目标 }这里的核心是should_stop它决定“顺路”的语义。int should_stop(CallBoard *cb, Elevator *e, int floor) { uint64_t bit 1ULL (floor - 1); if (e-stops bit) return 1; // 有内呼必停 if (e-people MAX_PEOPLE) return 0; // 满载只认内呼不认外呼 if (e-dir DIR_UP (cb-up_bits bit)) return 1; if (e-dir DIR_DOWN (cb-down_bits bit)) return 1; return 0; }should_stop是整段代码里最容易漏条件的地方。第一行处理内呼第二行处理满载第三第四行处理同向外呼。注意满载判断必须先返回 0否则电梯会在根本进不了人的楼层不断开关门平均等待直接崩坏。方向判断里不允许停反方向的请求否则电梯会被同一层的对向请求拽住造成经典的“来回开门”现象。转向逻辑单独放一个函数门周期结束后调用void plan_next(CallBoard *cb, Elevator *e) { int next find_next_stop(cb, e); if (next 0 e-dir ! DIR_STOP) { e-dir (e-dir DIR_UP) ? DIR_DOWN : DIR_UP; next find_next_stop(cb, e); // LOOK反方向再找一次 } if (next 0) { e-dir DIR_STOP; // 全局空闲进入待机 e-status IDLE; } else { e-status RUN; // 继续跑或掉头跑 } }这段实现有个容易被忽略的细节转向后立即再找一次目标。LOOK 不是 SCAN不需要跑到楼层边界如果反向也没有目标就置 DIR_STOP。同时要确认find_next_stop不会把当前楼层当作目标起始值是cur_floor step所以停在 6 层时不会自己服务自己避免刚关门又判定“本层有请求”的循环。3.3 必调参数表单层耗时、开关门、限载与到达率调度逻辑跑通后参数标定决定模拟数据可信不可信。我常用的参数表如下参数宏含义建议值调参说明MAX_F楼层总数12不要超过位图位宽改大要换分段位图MAX_PEOPLE核载人数13测试满载时要临时调小到 23FLOOR_TIME单层运行步数5每步 0.5 秒则单层 2.5 秒想模拟加减速就在每程头尾各加 2 步DOOR_TIME开关门周期20约 10 秒偏保守演示时可调到 8到达率每步新请求概率0.020.05太小看不出调度差异太大会持续满载这里重点说两个容易被忽略的点。第一FLOOR_TIME 如果设成 1电梯每秒过一层开关门 20 步就显得特别长调度算法会退化成“谁离门近谁被接”对比实验基本失效建议保持 46让运行时间在总耗时里占主导。第二到达率 0.02 在 10000 步仿真里会产生约 200 个请求正好够算统计平均值如果调到 0.1系统每 10 步就来一个请求电梯一直满载这时只能看到满载下的性能不是算法本身的差异。4. 仿真时钟与数据驱动让模拟可复现、可测量调度内核写好后很多人会急着画界面。我的习惯相反先做可复现的数据驱动内核界面只是内核的显示器。这样调算法时不用盯着动画猜看几行统计输出就能定位问题。4.1 时间片步进还是事件驱动两条路线怎么选仿真时钟有两种推进方式。时间片步进法每过一个固定步长就刷新所有对象的状态简单直观和界面刷新天然同步适合 12 层、2 部电梯以内的小规模模拟。事件驱动法则只在事件发生时跳转时钟效率高但需要维护一个事件优先队列适合大规模请求或纯理论分析。课程设计和本科毕设我一般建议时间片步进因为答辩要演示动画步进法能保证界面和逻辑同一节奏。事件驱动也有它的位置。如果题目要求对比几万条请求下的调度效率步进法每步都要扫描所有电梯大量时间花在“没有事件发生的空步”上这时候把乘客到达、门开关完成、电梯到站都放进优先队列时钟直接跳到下一个事件跑一组对比实验能快一个数量级。两条路线都能做但别混着用既按步进扫状态又插入事件会同时继承两者的时序坑。4.2 仿真主循环与统计指标最小可跑 C 主循环下面这个主循环是时间片步进的骨架放在 2.3 的结构体后面就是最小可运行版本#define SIM_STEPS 20000 // 仿真总步数每步 0.5 秒 #define ARRIVE_P 0.03 // 每步生成一个新请求的概率 int main(void) { Elevator e; CallBoard cb; init_system(e, cb); srand(42); // 固定随机种子否则结果不可复现 for (int step 1; step SIM_STEPS; step) { // 1) 随机生成外呼请求登记到位图和统计数组 if (rand() (int)(ARRIVE_P * RAND_MAX)) { int floor rand() % MAX_F 1; int dir (floor 1) ? 1 : (floor MAX_F) ? 0 : rand() % 2; if (dir) cb.up_bits | 1ULL (floor - 1); else cb.down_bits | 1ULL (floor - 1); record_call(floor, dir, step); // 写入 CallRec } // 2) 电梯运行中按 FLOOR_TIME 步进到下一层 if (e.status RUN) { if (runtime_clock FLOOR_TIME) { runtime_clock 0; e.cur_floor (e.dir DIR_UP) ? 1 : -1; if (should_stop(cb, e, e.cur_floor)) { e.status DOOR; door_clock DOOR_TIME; mark_served(e.cur_floor, step); // 结算等待时间 } } } // 3) 开门计时结束后清理位图并刷新方向 if (e.status DOOR) { if (--door_clock 0) { clear_arrive(cb, e); // 清本层内呼和双向外呼 plan_next(cb, e); // 转向判断进入 RUN 或 IDLE } } } print_stats(); // 平均等待、最长等待、满载率 return 0; }主循环按“生成请求、推进运行、处理门状态”三段组织顺序不能调换。先生成请求再推进电梯保证“本步新到的请求不会被本步电梯响应”这符合现实里按钮按下和电梯到站是同一瞬间的并发关系如果先推进电梯再生成请求同一时刻的请求会被推迟一个步长统计结果整体偏移。边界楼层的方向做了截断1 层只有上行顶层只有下行对应真实电梯按钮的设计。统计指标主推三个平均等待时间、最长等待时间、满载率。等待时间在mark_served里计算(serve_step - arrive_step) * 0.5单位是秒满载率在开门时累计people MAX_PEOPLE的次数除以总开门次数。这三个指标足够支撑实验报告平均等待看整体效率最长等待看公平性满载率看系统容量。只报平均值的话答辩老师基本会追问“最坏情况是多少”。4.3 输出 CSV 而不是人眼 log一张表撑起实验报告正文如果你手头那份题目文档是 .doc 格式的毕业设计模板别被章节结构吓住。它要求的通常就是问题定义、数据结构设计、算法描述、参数标定、测试对比这几节而 CSV 汇总表正好一张一张往里贴。很多人喜欢在控制台打一堆 “UP! DOWN! OPEN!” 日志看着热闹真到写实验报告时无从下手。我建议内核只输出两种东西每 500 步一行采样结束时一行汇总。采样用来画等待时间随负载变化的曲线汇总用来做策略对比表。格式用 CSV导入表格软件直接用。抽样输出长这样// 每 500 步打一行后面跟 4 个 CSV 字段 if (step % 500 0) { printf(%d,%.2f,%.2f,%.2f\n, step, avg_wait_sec(), max_wait_sec(), load_factor()); }avg_wait_sec和max_wait_sec统计当前已完成服务的外呼load_factor统计当前轿厢人数与限载的比值。答辩时的实验报告不用贴几十页日志只需要导出一张 5 行对比表同样 200 个请求、同样边界条件下FCFS、SSTF、LOOK 的平均等待和最长等待分别是多少再附一条等待时间随到达率变化的曲线这就是完整的数据支撑。5. 电梯模拟避坑实录5 个让模型翻车的隐蔽问题下面这五条是我在类似调度场景里反复踩过的坑每一条都按“现象、原因、解决”讲清楚做的时候对照检查能省下大量排错时间。5.1 电梯刚关门就响应新外呼门状态被跳过事件在排队现象门动画还没播完电梯已经重新启动或者恰好关门那一帧又来一个同方向请求电梯立刻再次开门乘客在短时间里进出两趟。原因主循环在 DOOR 状态里也调用了plan_next或者door_clock减到 0 前一帧就被当作门已结束门的真实语义被压缩成一个瞬间。解决门状态必须单独占一个完整阶段只有door_clock递减到 0 的那一帧才能调用plan_next同时clear_arrive要在门结束的那一刻执行把开门期间累计到位的请求统一清理。这个时序改成“开门期间可累计请求、关门瞬间统一清理”后同层反向请求也能一次合并不会再出现刚关门又开门的翻车场面。5.2 同层对向请求等到超时位图清理的时机错了现象6 层有人按上行、有人按下行。电梯下行到 6 层开门下人关上门走了上行那位一直等到超时过了很久电梯又下行经过 6 层还是不停。原因一种实现是到站后只清当前方向的位反方向请求永远留给反方向电梯但单梯模型里反方向请求要等电梯转向后才能响应转向如果发生在其它楼层本层请求就被悬空。另一种实现是到站后把该层所有外呼都清掉结果把还没上车的反向请求误删了。解决以“一次开门周期为原子操作”清理开门期间本层两个方向的位都可以清因为这一周期内的进出乘客已经完成关门后到达的新请求才作为下一次目标。这个语义既不会误删也不会漏接。5.3 顶层底层重复停靠边界层同时命中两段行程现象电梯到达顶层后又立刻在顶层开门一次日志里出现“15 层停、15 层停”的连续记录底层也有类似情况。原因边界楼层既是当前行程的终点又是反向行程的起点。转向后如果find_next_stop的起始楼层写成了cur_floor而不是cur_floor step电梯会立刻认为自己又到了一个新目标。解决find_next_stop的起始楼层必须跳过当前层转向只改变方向不清除当前层新累计的请求位。还有一个关联隐患是位图溢出MAX_F超过 31 时1 (floor - 1)在 32 位 int 里直接越界请求位被写进符号位甚至被清零解决是全程使用uint64_t并在结构体注释里写明楼层上限。5.4 随机数不固定调参像玄学结果不可复现现象参数一点没改两次运行的平均等待差出两秒答辩时演示一次通过自己回去调参又复现不出来。原因srand(time(NULL))让每次仿真的请求序列都不一样统计波动被当成算法差异。解决仿真程序默认固定种子随机种子作为命令行参数传入每一次实验都记录种子值和参数组合把单次运行的随机性和策略对比的系统性差异分开。我习惯在测试时先跑固定种子定位 bug确认没有逻辑问题再换 5 个种子做统计平均。这个习惯能救回大量排错时间是我在这类题目里吃的最大一堑也是最好的后悔药。5.5 满载期间层层停外呼没分内呼优先级现象轿厢显示满载但电梯还是在每个有外呼的楼层开门楼层没人能上去等待时间全花在无效开关门上。原因should_stop里只判断了是否有外呼没有判断轿厢是否还能上人。解决满载时只响应内呼外呼位图全部忽略。为了验证这个分支测试阶段把MAX_PEOPLE临时设成 2让满载频繁触发再检查should_stop是否真的返回 0。这个分支平时很少触发但一旦触发就是性能黑洞而且界面动画看不出来只能靠统计满载率曲线判断。6. 验收技巧固定剧本与统计断言把电梯模拟调成可提交状态随机请求适合做整体统计但不适合定位 bug。我常用的验收手段是准备几个“请求剧本”一个模拟早高峰上行潮汐一个模拟下班下行潮汐一个模拟随机交叉。每个剧本把请求逐行写进文件程序启动时读文件而不是靠随机数生成。剧本格式一行一条请求依次是楼层、方向、到达步数。1 1 10 6 1 40 10 0 120 12 0 380 3 1 900运行时用命令行参数指定剧本和种子跑完直接断言三项指标。比如早高峰剧本要求最大等待不超过 30 秒满载率不超过 90%断言失败就说明这个场景下调度策略有问题而不是数据波动。固定剧本还有一个额外好处对拍调试时FCFS 和 LOOK 跑同一份剧本差异一眼可见比两边各跑 5000 个随机请求再比平均值直观得多。编译和回归可以用一条命令收拢gcc -stdc11 -O2 -o elevator main.c stats.c ./elevator --sceneudp_peak.txt --seed42 --algolook --csvout.csv--scene指定剧本文件--seed固定随机种子--algo切换 FCFS 或 LOOK--csv输出统计结果。把这条命令写进工程的 check 脚本答辩前每次改动都跑一遍回归。别把界面当主要调试入口界面是给答辩老师看的不是给你定位问题用的数据驱动内核对调时才是真正能干活的状态。往后做多梯扩展时思路也别变把外呼按方向和楼层分区每部电梯维护各自的位图用负载均衡定期重新分配区域比所有电梯全局抢同一批请求可靠得多。我做这个题目时走过的弯路正好相反先把界面画得漂漂亮亮再往里塞调度逻辑结果算法跑起来只能用眼睛观察平均等待到底几秒全靠估算。后来把内核抽成命令行程序固定种子加剧本两天就把平均等待从 9 秒压到 4 秒实验报告的数据全部从 CSV 里直接取。这套流程后来被我用到所有仿真类项目里先数据后界面最后才是文档。希望帮到你。本文还有配套的精品资源点击获取
返回列表