news 2026/10/2 20:43:10

电梯调度系统设计:循环链表与结构体数组的工程实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
电梯调度系统设计:循环链表与结构体数组的工程实践

简介:本资源是一份面向计算机专业本科生的数据结构课程设计报告,聚焦电梯模拟系统开发,旨在通过真实项目实践深化对栈、队列、链表等核心数据结构的理解与应用能力。报告完整覆盖系统分析、概要设计、详细实现、运行测试及总结反思全过程,含7章内容,从抽象数据类型定义、状态机建模(Opening/Opened/Closing/Closed/Waiting七态)、多层等候队列与乘客栈设计,到C语言编码规范、时间单位模拟(0.1秒粒度)及超时放弃机制,均体现扎实的算法设计与工程实现能力。资源为单个Word文档(.doc),大小523KB,结构清晰、图文结合,含目录、参考文献及严蔚敏《数据结构》配套引用,适合作为课程设计范本或毕业设计参考。目前已有138人学习下载,可直接用于教学复现、代码调试与设计思路借鉴。

1. 为什么用链表和队列写电梯调度,比直接套Unity动画更扛毕业答辩?

“毕业设计-数据结构电梯模拟.doc”——这个标题在高校计算机/软件工程/自动化专业毕设选题池里,常年稳居「看似简单、实则暗坑密布」TOP3。它不是让你做个会动的电梯GIF,而是用线性表、栈、队列、优先队列(堆)甚至图结构,把楼层请求、轿厢状态、调度策略全部建模成可验证、可调试、可量化性能的数据流。我带过7届毕设,见过太多同学前期用PyGame画个方块上下跳,答辩时被问“你这调度逻辑在哪?响应时间怎么算?多部电梯协同怎么避免死锁?”当场卡壳。真正能过审、拿高分、还能塞进简历的版本,核心就三点:请求队列用循环链表实现动态插入删除、轿厢状态用结构体封装、调度算法必须支持至少两种策略(FCFS + SCAN)并能输出每步决策日志。适合大三下刚学完《数据结构(C语言版)》严蔚敏第2章到第4章的同学——不考你OpenGL渲染,但考你能不能把课本里的“链表插入”“队列判空”“堆调整”真刀真枪落地成可运行、可截图、可讲清每行代码作用的控制台程序。别急着装VS或配环境,先想清楚:你模拟的是单梯还是群控?是纯逻辑调度,还是带物理延迟?这些决定你后面80%的代码结构。


2. 用循环链表+结构体搭出电梯系统骨架:从需求到内存布局

电梯模拟的本质,是管理两类动态实体:乘客请求(外部输入)和轿厢状态(内部载体)。它们的生命周期、增删频率、访问模式完全不同——这就决定了数据结构选型不能拍脑袋。我们不用STL容器(答辩时解释不清底层),也不硬套树或图(过度设计),就用最基础、教材里讲透的带头结点的循环单链表存请求队列,用结构体数组存多部电梯状态。下面拆解为什么这么选,以及怎么落地。

2.1 请求队列为什么必须是循环链表?而不是数组或普通链表

数组的问题太明显:最大请求数固定(比如预设100个),但实际运行中可能瞬间涌进50个请求,也可能半小时没新请求——空间浪费或溢出;普通单链表删除首结点后需特殊处理头指针,而电梯调度中“服务完当前请求即删除首结点”是高频操作,循环链表的tail->next = head特性让首尾操作统一,代码健壮性翻倍。更重要的是:循环链表天然支持SCAN算法中的“方向扫描”逻辑——当轿厢向上运行时,只处理current_floor < target_floor的请求;向下时反之。遍历链表时只需判断p->floor > current或p->floor < current,无需额外索引维护。

// 请求结点定义(C语言) typedef struct RequestNode { int floor; // 目标楼层(1~N) int time_arrived; // 到达时间(模拟时钟tick) int priority; // 优先级(可扩展:老人/紧急请求加权) struct RequestNode* next; } RequestNode; // 循环链表头结点(不存实际请求,仅作哨兵) RequestNode* request_head = NULL; // 初始化:创建带头结点的循环链表 void init_request_queue() { request_head = (RequestNode*)malloc(sizeof(RequestNode)); if (!request_head) exit(1); request_head->next = request_head; // 自循环 }

提示:request_head本身不存请求数据,它的next指向第一个真实请求结点。这样插入/删除时无需判断空链表特例,所有操作统一为head->next = new_node或p->next = p->next->next。

2.2 轿厢状态用结构体数组而非链表:为什么物理属性必须静态分配?

一部电梯的属性是固定的:当前楼层、运行方向(UP/DOWN/STOP)、载客量、开关门状态、是否故障……这些字段数量确定、访问频繁(每tick都要读写),且电梯总数在程序启动时已知(如3部)。用链表管理轿厢会导致:① 频繁malloc/free影响实时性;② 遍历查找某部电梯耗时O(n);③ 答辩时被问“你怎么保证多部电梯状态同步更新?”很难自圆其说。结构体数组直接按索引访问,O(1)定位,内存连续利于CPU缓存——这才是工业级嵌入式思维(哪怕你只是跑控制台)。

// 电梯状态结构体 typedef struct Elevator { int id; // 电梯编号(0,1,2...) int current_floor; // 当前楼层(1~20) int direction; // 方向:1=UP, -1=DOWN, 0=STOP int load_count; // 当前载客数(≤额定容量) int capacity; // 额定载客量(如12人) int is_door_open; // 1=开门中,0=关门/运行中 int next_target; // 下一个目标楼层(用于SCAN算法决策) } Elevator; // 全局电梯数组(假设3部电梯) #define ELEVATOR_COUNT 3 Elevator elevators[ELEVATOR_COUNT]; // 初始化所有电梯 void init_elevators() { for (int i = 0; i < ELEVATOR_COUNT; i++) { elevators[i].id = i; elevators[i].current_floor = 1; // 默认停靠1楼 elevators[i].direction = 0; elevators[i].load_count = 0; elevators[i].capacity = 12; elevators[i].is_door_open = 0; elevators[i].next_target = 0; } }

注意:next_target字段是SCAN算法的核心——它不等于“最近请求楼层”,而是根据当前方向扫描到的第一个有效请求。比如轿厢在5楼向上运行,请求队列有3楼、7楼、9楼,则next_target=7(跳过3楼,因方向不符)。

2.3 请求插入与电梯状态更新:两个关键函数的边界逻辑

插入请求不是简单malloc+link,必须考虑时间戳排序和重复楼层合并。例如同一秒内多个请求发往8楼,应合并为1个请求(载客量+1),而非插入3个结点——这直接影响调度公平性。电梯状态更新则要防“超速”:每tick只允许移动1层(模拟物理限制),且开门/关门需占用2个tick(否则答辩时被质疑“门怎么瞬移?”)。

// 插入请求:按time_arrived升序插入,同楼层请求合并 void insert_request(int floor, int time_arrived) { RequestNode* new_node = (RequestNode*)malloc(sizeof(RequestNode)); new_node->floor = floor; new_node->time_arrived = time_arrived; new_node->priority = 0; // 默认优先级 RequestNode* p = request_head; // 找到插入位置(按time_arrived升序) while (p->next != request_head && p->next->time_arrived < time_arrived) { p = p->next; } // 检查是否与p->next同楼层(合并逻辑) if (p->next != request_head && p->next->floor == floor) { // 同楼层请求,载客量隐式+1(实际可扩展为count字段) free(new_node); // 丢弃新结点,只更新原结点语义 return; } new_node->next = p->next; p->next = new_node; } // 更新电梯状态:每tick调用一次 void update_elevator(int eid) { Elevator* e = &elevators[eid]; if (e->is_door_open) { e->is_door_open = 0; // 门关闭 return; } if (e->next_target == 0) { e->direction = 0; return; // 无目标,停止 } // 移动1层(物理约束) if (e->direction == 1 && e->current_floor < MAX_FLOOR) { e->current_floor++; } else if (e->direction == -1 && e->current_floor > 1) { e->current_floor--; } // 到达目标楼层:开门 if (e->current_floor == e->next_target) { e->is_door_open = 1; // 此处应触发“服务完成”逻辑:从请求队列删除该请求 remove_request_by_floor(e->next_target); e->next_target = 0; // 清空目标 } }

关键细节:remove_request_by_floor()函数必须遍历链表找到并删除第一个匹配楼层的结点(非全部),因为同一楼层可能有多个不同时刻的请求。删除后需重新计算该电梯的next_target——这是SCAN算法“方向保持”的关键。


3. FCFS与SCAN双调度算法实现:从伪代码到可验证的C函数

毕业设计答辩最常被追问的,不是“你画的电梯动没动”,而是“你的调度策略怎么选?有没有对比数据?”。只实现一种算法(比如只做FCFS)会被认为工作量不足;堆砌三四种又容易逻辑混乱。FCFS(先来先服务)+ SCAN(电梯算法)是黄金组合:前者验证基础链表操作正确性,后者体现数据结构应用深度。重点不是算法多炫酷,而是你能说清“为什么SCAN比FCFS平均等待时间少23%”——这需要你真跑出数据。

3.1 FCFS算法:用链表遍历实现最朴素的公平性

FCFS本质就是按请求到达时间顺序服务。难点在于:如何保证“服务完一个请求后,下一个请求一定是链表中时间戳最小的那个”?答案是——根本不需要排序!循环链表的插入已按time_arrived升序,首结点永远是最老请求。所以FCFS调度器只需:① 取head->next的楼层作为目标;② 让最近的空闲电梯前往;③ 服务完成后删除该结点。注意:这里“最近电梯”指abs(e.current_floor - target)最小者,而非简单选1号电梯。

// FCFS调度主逻辑(每tick调用) void fcfs_schedule() { if (request_head->next == request_head) return; // 队列空 int target_floor = request_head->next->floor; int best_eid = -1; int min_distance = INT_MAX; // 找离target_floor最近的空闲电梯(direction==0) for (int i = 0; i < ELEVATOR_COUNT; i++) { if (elevators[i].direction == 0) { int dist = abs(elevators[i].current_floor - target_floor); if (dist < min_distance) { min_distance = dist; best_eid = i; } } } if (best_eid != -1) { elevators[best_eid].next_target = target_floor; elevators[best_eid].direction = (target_floor > elevators[best_eid].current_floor) ? 1 : -1; } }

逻辑说明:elevators[i].direction == 0表示电梯静止,可接受新任务。min_distance初始化为INT_MAX(需#include <limits.h>),避免未赋值导致的随机行为。此函数不处理“多请求并发”场景(如同时3个请求),但已满足毕设基础要求。

3.2 SCAN算法:用方向状态机解决“饿死”问题

SCAN算法核心是方向保持:电梯向上走到顶层才转向,向下走到底层才转向。但纯SCAN有缺陷——如果高层长期无请求,低层请求会“饿死”。因此工业实现必加LOOK优化(检测到无更高请求时提前转向)。我们的简化版SCAN+LOOK这样设计:① 维护全局scan_direction(1/-1);② 向上扫描时,找floor > current_floor的最小楼层;③ 向下扫描时,找floor < current_floor的最大楼层;④ 若当前方向无请求,则转向。

// SCAN调度主逻辑(每tick调用) void scan_schedule() { if (request_head->next == request_head) return; // 全局扫描方向(初始为UP) static int scan_direction = 1; int target_floor = -1; RequestNode* p = request_head->next; if (scan_direction == 1) { // 向上扫描 // 找第一个floor > current_floor的请求 while (p != request_head) { if (p->floor > elevators[0].current_floor) { // 以0号电梯为参考(可扩展为选最优) target_floor = p->floor; break; } p = p->next; } // 若没找到,转向 if (target_floor == -1) scan_direction = -1; } else { // 向下扫描 // 找最后一个floor < current_floor的请求(需遍历到底) RequestNode* last_valid = NULL; while (p != request_head) { if (p->floor < elevators[0].current_floor) { last_valid = p; } p = p->next; } if (last_valid) target_floor = last_valid->floor; else scan_direction = 1; // 无更低请求,转向上 } if (target_floor != -1) { // 分配给空闲电梯(同FCFS逻辑) int best_eid = find_idle_elevator(target_floor); if (best_eid != -1) { elevators[best_eid].next_target = target_floor; elevators[best_eid].direction = scan_direction; } } } // 辅助函数:找离target最近的空闲电梯 int find_idle_elevator(int target_floor) { int best_eid = -1; int min_dist = INT_MAX; for (int i = 0; i < ELEVATOR_COUNT; i++) { if (elevators[i].direction == 0) { int dist = abs(elevators[i].current_floor - target_floor); if (dist < min_dist) { min_dist = dist; best_eid = i; } } } return best_eid; }

参数说明:scan_direction声明为static,保证状态跨tick保持。find_idle_elevator()复用FCFS逻辑,避免代码重复。此处以elevators[0]为参考点是简化处理,实际可遍历所有电梯找最优——但毕设阶段,清晰比完美重要。

3.3 算法切换与性能对比:用日志文件验证你的选择

答辩时老师会问:“你凭什么说SCAN比FCFS好?”光嘴说没用,必须有数据。我们在主循环里加计时器和统计:记录每个请求的wait_time = service_time - arrive_time,运行1000tick后计算平均等待时间、最长等待时间、电梯空闲率。关键是要把日志输出到文件,答辩时直接展示Excel图表。

// 全局统计变量 long long total_wait_time = 0; int request_count = 0; int max_wait_time = 0; // 在服务完成时(remove_request_by_floor后)调用 void record_service_time(int floor, int arrive_time, int service_time) { int wait_time = service_time - arrive_time; total_wait_time += wait_time; request_count++; if (wait_time > max_wait_time) max_wait_time = wait_time; } // 运行结束后输出统计 void print_statistics() { FILE* f = fopen("elevator_stats.txt", "w"); if (!f) return; fprintf(f, "Algorithm: %s\n", using_scan ? "SCAN" : "FCFS"); fprintf(f, "Total Requests: %d\n", request_count); fprintf(f, "Average Wait Time: %.2f ticks\n", (double)total_wait_time / request_count); fprintf(f, "Max Wait Time: %d ticks\n", max_wait_time); fclose(f); }

血泪经验:service_time必须是请求被实际服务完成的tick数(即电梯到达该楼层并开门的时刻),不是开始移动的时刻。很多同学错把current_floor == target当作服务完成,忽略了开门耗时——这会导致平均等待时间虚低20%,答辩时被当场指出。


4. 避坑指南:答辩老师最爱问的5个致命问题及现场解决方案

写完代码跑通只是第一步,毕设答辩的“死亡提问”往往来自实现细节的疏漏。以下是我在7届答辩中高频出现的5个问题,附真实现象、根因分析和30秒内可执行的修复方案——不是理论,是能立刻改代码、重编译、再演示的救命补丁。

4.1 现象:电梯在2楼和3楼之间反复横跳,无法停稳

原因:update_elevator()中判断“到达目标楼层”的条件是e->current_floor == e->next_target,但若电梯从1楼向上,next_target=3,则经过2楼时current_floor=2却未触发任何逻辑,继续冲到3楼;而3楼服务完成后next_target被清零,下次调度又可能设为2楼,导致来回震荡。
解决:在update_elevator()移动楼层后,立即检查是否越过目标。增加如下逻辑:

// 在e->current_floor++或--之后插入 if ((e->direction == 1 && e->current_floor >= e->next_target) || (e->direction == -1 && e->current_floor <= e->next_target)) { e->current_floor = e->next_target; // 强制对齐目标楼层 e->is_door_open = 1; remove_request_by_floor(e->next_target); e->next_target = 0; }

4.2 现象:多部电梯抢同一个请求,导致请求被删除两次

原因:FCFS调度中,find_idle_elevator()返回电梯ID后,多个调度器(如FCFS和SCAN并存)可能同时为不同电梯分配同一请求。
解决:请求分配必须原子化。在insert_request()时为每个结点加is_allocated标志位,分配前检查并置位:

// RequestNode结构体新增字段 int is_allocated; // 在分配前检查 if (!p->is_allocated) { p->is_allocated = 1; // 分配逻辑... }

注意:remove_request_by_floor()需改为只删除is_allocated==1的结点,避免误删。

4.3 现象:程序运行10分钟后内存暴涨,最后崩溃

原因:insert_request()不断malloc,但remove_request_by_floor()只free部分结点,未释放整个链表内存。循环链表删除结点后,free(p)未置p=NULL,后续野指针访问。
解决:严格遵循“申请-释放”配对。在remove_request_by_floor()中:

RequestNode* prev = request_head; RequestNode* p = request_head->next; while (p != request_head) { if (p->floor == floor && p->is_allocated) { prev->next = p->next; free(p); // 释放后必须break,否则p成为野指针 p = NULL; // 防止后续使用 break; } prev = p; p = p->next; }

4.4 现象:SCAN算法在顶层不停顿,直接掉头向下

原因:scan_schedule()中“转向判断”逻辑错误——当向上扫描到顶层(如20楼)时,代码未检测p->floor > current_floor是否还有结点,而是直接转向,导致错过顶层请求。
解决:转向前必须确认当前方向无有效请求。修改SCAN向上扫描段:

if (scan_direction == 1) { int found = 0; p = request_head->next; while (p != request_head) { if (p->floor > elevators[0].current_floor && !p->is_allocated) { target_floor = p->floor; found = 1; break; } p = p->next; } if (!found) scan_direction = -1; // 确认无请求才转向 }

4.5 现象:生成的elevator_stats.txt里平均等待时间为0或负数

原因:service_time变量未初始化,或arrive_time大于service_time(如请求插入时time_arrived设为0,但服务发生在第500tick,而service_time被误赋为局部变量未赋值)。
解决:所有时间变量必须显式初始化。在main()开头:

int global_tick = 0; // 全局时钟 // 每次插入请求时:insert_request(floor, global_tick); // 每次服务完成时:record_service_time(floor, arrive_time, global_tick);

关键:global_tick在主循环每轮++,确保单调递增。arrive_time必须是插入时的global_tick值,不可用time(NULL)——那会引入毫秒级误差,导致负等待时间。


5. 让答辩老师眼前一亮的3个进阶技巧:从“能跑”到“值得写进简历”

做到上面四章,你的毕设已稳过。但如果想拿优秀、想放进技术简历、想让面试官追问细节,得加点“料”。这些不是炫技,而是体现你真正理解数据结构如何服务于工程问题。我当年带的学生用这三条,3个拿了校级优秀毕设,2个拿到大厂实习offer——因为面试官看到的不是“学生作业”,而是“准工程师的系统思维”。

5.1 用优先队列(小根堆)替代链表排序:把O(n)插入降到O(log n)

当前FCFS的插入是O(n)遍历找位置,当请求量超过500/秒时(模拟高流量场景),链表性能断崖下跌。而优先队列(堆)天生适合按时间排序:插入O(log n),取最小O(1)。C语言没有内置堆,但用数组实现小根堆只要60行代码。关键是重定义堆元素比较逻辑——不是比楼层,而是比time_arrived:

// 堆节点定义(复用RequestNode,但用数组存) typedef struct { int floor; int time_arrived; int priority; } HeapNode; HeapNode heap[MAX_REQUESTS]; int heap_size = 0; // 上浮:新元素插入末尾后,向上调整 void heap_up(int idx) { while (idx > 0) { int parent = (idx - 1) / 2; if (heap[parent].time_arrived <= heap[idx].time_arrived) break; swap(&heap[parent], &heap[idx]); idx = parent; } } // 插入:O(log n) void heap_insert(int floor, int time_arrived) { if (heap_size >= MAX_REQUESTS) return; heap[heap_size].floor = floor; heap[heap_size].time_arrived = time_arrived; heap[heap_size].priority = 0; heap_up(heap_size); heap_size++; } // 取最小:O(1) HeapNode heap_top() { return heap[0]; } // 删除最小:O(log n) void heap_pop() { if (heap_size == 0) return; heap[0] = heap[--heap_size]; heap_down(0); }

为什么值得做?因为答辩时你可以指着heap_insert()说:“老师,我把请求插入从O(n)优化到O(log n),在1000请求压力下,调度延迟从120ms降到8ms——这是数据结构课上‘堆’章节的直接应用。” 这比“我用了链表”有力得多。

5.2 实现“电梯健康度”监控:用滑动窗口统计空闲率

老师喜欢问:“你的系统鲁棒吗?如果一部电梯故障,怎么处理?” 光说“我加个if判断”不够。真正的工程思维是量化监控。我们用长度为100的滑动窗口,记录每部电梯过去100tick的空闲状态(direction==0为1,否则0),实时计算空闲率:

// 每部电梯的滑动窗口 #define WINDOW_SIZE 100 int idle_window[ELEVATOR_COUNT][WINDOW_SIZE]; int window_ptr[ELEVATOR_COUNT] = {0}; // 当前写入位置 // 每tick更新窗口 void update_idle_window(int eid) { int is_idle = (elevators[eid].direction == 0) ? 1 : 0; idle_window[eid][window_ptr[eid]] = is_idle; window_ptr[eid] = (window_ptr[eid] + 1) % WINDOW_SIZE; } // 计算空闲率(%) int get_idle_rate(int eid) { int sum = 0; for (int i = 0; i < WINDOW_SIZE; i++) { sum += idle_window[eid][i]; } return (sum * 100) / WINDOW_SIZE; } // 故障判定:空闲率持续>95%且载客量=0,视为疑似故障 int is_elevator_faulty(int eid) { return (get_idle_rate(eid) > 95 && elevators[eid].load_count == 0); }

这招的杀伤力在于:答辩时你打开控制台,实时显示Elevator 0: Idle Rate=87% | Faulty? NO,老师会立刻意识到——你做的不是玩具,是可运维的系统。而且is_elevator_faulty()可以触发告警,比如自动将请求重定向到其他电梯。

5.3 输出可视化调度日志:用ANSI颜色码让控制台变“仪表盘”

答辩演示时,满屏黑白文字会让老师失去耐心。用ANSI转义序列给关键信息上色,成本几乎为零,但体验提升巨大:

// 定义颜色宏 #define RED "\x1b[31m" #define GREEN "\x1b[32m" #define YELLOW "\x1b[33m" #define BLUE "\x1b[34m" #define RESET "\x1b[0m" // 日志打印 printf(GREEN "[TICK %d] " RESET, global_tick); printf(YELLOW "Elevator %d " RESET, eid); printf(RED "→ Floor %d " RESET, target); printf(BLUE "(WAIT: %d)" RESET, wait_time); printf("\n");

效果:电梯移动用绿色,目标楼层用红色,等待时间用蓝色——一眼看出系统瓶颈。这不需要任何第三方库,Windows Terminal和Linux终端都支持。我学生曾用这招,让答辩老师主动说:“这个颜色设计很专业,能快速定位问题。”

最后说句实在的:我当年写这个毕设时,也以为“能动就行”,结果第一次答辩被问“你的链表删除逻辑在并发下安全吗”,答不上来。后来重写时,把remove_request_by_floor()加了互斥锁(即使单线程也模拟),把日志输出改成CSV格式供Excel分析——这些细节没让我多拿1分,但让我在实习面试时,被问到“你做过什么系统级设计”时,能掏出这份代码讲3分钟。希望帮到你。

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

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

SystemVerilog关联数组方法实战:exists、遍历与性能避坑

关联数组在 SystemVerilog 里算不上什么新鲜语法&#xff0c;但真到写验证环境的时候&#xff0c;它出现的频率高得离谱——寄存器模型的地址映射、覆盖率 bin 的命中计数、scoreboard 里按 transaction id 归档的数据、参考模型里按地址索引的存储&#xff0c;几乎都是它。IEE…

作者头像 李华