华为OD机试实战:C++实现打印机队列模拟与优先级调度

发布时间:2026/7/17 5:22:10
华为OD机试实战:C++实现打印机队列模拟与优先级调度 1. 项目概述从一道题看华为OD机试的实战逻辑最近在帮几个朋友准备华为OD的机试发现“打印机队列模拟”这道题出现的频率相当高而且它绝不仅仅是一道简单的编程题。很多人一看到“队列”、“模拟”就觉得是数据结构的基础应用上手就写结果要么超时要么逻辑漏洞百出最后只能拿个基础分。这道题真正考察的是一个开发者将现实业务逻辑抽象成清晰、健壮代码的能力这正是华为OD这类大厂机试的核心——他们不只要你会写代码更要你的代码能处理复杂、并发的“脏”数据。简单来说题目场景是这样的你有多个打印机每个打印机有自己的打印队列。打印任务有优先级高优先级的可以插队。但现实情况更复杂打印机有状态忙碌/空闲任务到达时间有先后高优先级任务插队时不能影响已经在打印的任务还需要处理任务取消等边缘情况。用C来实现这个模拟器你需要综合考虑STL容器的选择、多条件排序、时间片模拟、以及边界条件处理。这几乎是一个微型的、面向对象的生产者-消费者模型。如果你正在准备华为OD或其他大厂的机试或者想通过一个综合项目来巩固你的C面向对象和STL实战能力那么把这个模拟器吃透价值远超刷十道LeetCode简单题。接下来我会带你从零开始拆解这道题的所有核心细节、设计思路、以及我踩过的那些坑最终呈现一个可直接运行、逻辑清晰的C实现。2. 核心需求解析与系统设计思路拿到题目第一步不是急着写main函数而是彻底理解需求并把它转化为清晰的设计文档。很多人在这一步就输了。2.1 需求深度拆解隐藏在字面下的“坑”根据常见的题目描述和变体我们可以提炼出以下核心与隐性需求多打印机系统系统中有N台打印机例如5台每台打印机独立运作拥有自己的任务队列。打印任务属性每个任务至少包含任务ID、优先级1-99最高、提交时间、预计耗时。有些变体还会包含任务状态等待、打印中、完成、取消。核心调度规则FIFO基础默认情况下每个打印队列按照任务到达的先后顺序提交时间处理。优先级抢占当一个新的高优先级任务到达某个打印机的队列时它需要插入到队列中所有优先级低于它的任务之前但必须位于任何正在打印或优先级不低于它的任务之后。这是最容易出错的地方。打印机状态机打印机有空闲和忙碌两种状态。只有当打印机空闲时才会从队首取出任务开始打印。打印任务会占用打印机一段固定的时间预计耗时。时间推进模拟这是模拟类题目的关键。我们需要一个全局的“时钟”按时间单位如1秒推进。在每个时间点检查是否有新任务到达并将其分配到某个打印机的队列分配策略可能是轮询、找最短队列等需明确。更新所有打印机的状态如果打印机忙碌则其剩余打印时间减1如果减到0则任务完成打印机变为空闲。对于刚变为空闲的打印机立即检查其队列取出下一个任务开始打印。输入与输出输入通常是一系列按提交时间排序的打印任务。格式可能为[提交时间] [打印机ID] [任务ID] [优先级] [耗时]。输出需要输出每个任务的开始打印时间和完成时间或者最终所有任务的完成情况。设计心得务必在编码前用纸笔画一下任务在队列中插入的动态过程以及打印机状态随时间的变化。我最初就是没理清“优先级插入但不打断当前任务”这个点导致整个调度逻辑写反了。2.2 数据结构选型为什么是vectorqueue加priority_queue这是设计的关键决策点直接决定了代码的效率和清晰度。打印机队列每台打印机一个队列。选择std::queue似乎很自然因为它FIFO。但问题来了高优先级插队怎么办queue不支持中间插入。因此我们需要一个支持在特定位置插入的容器。备选方案1std::deque。双端队列支持头尾快速操作也支持相对较慢的中间插入。对于频繁的队首弹出和队尾插入以及偶尔的中间插入deque是一个不错的选择。备选方案2std::list。链表中间插入和删除是O(1)。但随机访问慢。在我们的场景里主要是顺序处理和按条件插入链表很合适。我的选择我最终选择了**std::vector**来管理每个打印机的等待队列。原因有三1) 任务数量在模拟过程中是可控的2) 我们需要频繁地遍历队列以找到合适的插入位置基于优先级vector的缓存友好性使得遍历速度极快3) 在vector中间插入元素insert虽然平均是O(n)但在我们的场景下由于会维护队列大致有序按优先级和提交时间插入点通常靠近队首或队尾性能可以接受。代码的可读性和遍历效率优先。全局任务列表或事件队列我们需要按提交时间顺序处理任务到达事件。一个常见的做法是使用一个最小堆std::priority_queue作为事件队列按时间排序。但这里输入通常已按时间排序所以简单地用一个vector或list存储所有任务然后按时间顺序处理也可以。任务与打印机类设计// 打印任务类 class PrintJob { public: int jobId; int priority; // 1-9 int submitTime; int duration; int startTime; int endTime; // 构造函数等... }; // 打印机类 class Printer { public: int printerId; enum Status { IDLE, BUSY }; Status status; PrintJob* currentJob; // 当前正在打印的任务 int remainingTime; // 当前任务剩余打印时间 std::vectorPrintJob queue; // 等待队列 // 方法添加任务、开始下一个任务、更新状态等 void addJobToQueue(const PrintJob job); void startNextJob(int currentTime); void update(int currentTime); // 更新打印机状态 };2.3 模拟引擎主循环设计模拟的核心是一个时间驱动的主循环。伪代码如下初始化所有打印机为空闲队列为空。 将所有任务按提交时间排序。 当前时间 t 0。 当还有未处理的任务 或 有打印机忙碌时 1. 处理在时间t到达的所有任务将其添加到对应打印机的队列。 2. 遍历所有打印机 a. 调用 printer.update(t)。内部会减少remainingTime如果完成则设置空闲 b. 如果打印机在update后变为空闲则立即调用 printer.startNextJob(t)。 3. 时间 t。这个循环保证了在每一时刻先处理新任务的到达可能引发队列重排再更新打印机状态并触发新的任务开始。3. 核心模块实现与C代码详解有了清晰的设计我们就可以动手实现了。这里我会分模块给出关键代码并附上详细注释。3.1 数据模型定义#include iostream #include vector #include algorithm #include queue // 用于可能的全局事件队列本例未使用 #include string class PrintJob { public: int jobId; int priority; // 优先级9最高 int submitTime; // 提交时间 int duration; // 需要打印的时间 int startTime; // 开始打印的时间 int endTime; // 完成时间 bool isCompleted; PrintJob(int id, int prio, int submit, int dur) : jobId(id), priority(prio), submitTime(submit), duration(dur), startTime(-1), endTime(-1), isCompleted(false) {} // 用于在队列中排序的比较规则优先级高的在前同优先级则提交时间早的在前 // 注意这个规则用于维护队列顺序与提交时间顺序不同。 static bool compareForQueue(const PrintJob a, const PrintJob b) { if (a.priority ! b.priority) { return a.priority b.priority; // 优先级降序 } return a.submitTime b.submitTime; // 提交时间升序 } }; class Printer { public: int printerId; enum Status { IDLE, BUSY }; Status status; PrintJob* currentJob; // 指向当前正在处理的任务 int remainingTime; // 当前任务剩余时间 std::vectorPrintJob jobQueue; // 等待队列 Printer(int id) : printerId(id), status(IDLE), currentJob(nullptr), remainingTime(0) {} // 关键方法1将新任务添加到队列并按照优先级规则插入 void addJobToQueue(const PrintJob newJob) { // 找到插入位置第一个优先级低于新任务的位置 auto it jobQueue.begin(); while (it ! jobQueue.end()) { // 如果当前任务优先级低于新任务则插入在此位置之前 if (it-priority newJob.priority) { break; } // 如果优先级相同但新任务提交时间更晚则继续往后找保持同优先级下的FIFO // 注意这里假设输入任务按提交时间递增所以新来的同优先级任务时间一定更晚 // 更严谨的做法是比较 submitTime if (it-priority newJob.priority it-submitTime newJob.submitTime) { break; } it; } jobQueue.insert(it, newJob); // 插入后队列整体保持“优先级非递增同优先级下提交时间非递减”的顺序 // 这样队首始终是下一个该被执行的任务 } // 关键方法2如果打印机空闲且队列不为空开始打印下一个任务 void startNextJob(int currentTime) { if (status IDLE !jobQueue.empty()) { // 从队首取任务因为队列已按规则排序好 PrintJob job jobQueue.front(); job.startTime currentTime; job.endTime currentTime job.duration; currentJob job; remainingTime job.duration; status BUSY; // 任务从等待队列移除 jobQueue.erase(jobQueue.begin()); std::cout 时间 currentTime : 打印机 printerId 开始打印任务 job.jobId (优先级: job.priority , 耗时: job.duration ) std::endl; } } // 关键方法3更新打印机状态被主循环在每个时间点调用 void update(int currentTime) { if (status BUSY) { remainingTime--; if (remainingTime 0) { // 当前任务完成 if (currentJob) { currentJob-isCompleted true; std::cout 时间 currentTime : 打印机 printerId 完成打印任务 currentJob-jobId std::endl; } status IDLE; currentJob nullptr; remainingTime 0; // 注意这里不直接调用 startNextJob由主循环统一处理 // 避免在update循环中产生递归调用或状态混乱 } } // 如果状态是IDLE则什么也不做等待主循环调用startNextJob } // 辅助方法检查打印机是否还有任务正在打印或等待 bool hasPendingJobs() const { return (status BUSY) || !jobQueue.empty(); } };3.2 模拟引擎与主循环实现class PrintSystemSimulator { private: std::vectorPrinter printers; std::vectorPrintJob allJobs; // 所有待处理的任务按提交时间排序输入 int currentTime; size_t jobIndex; // 指向 allJobs 中下一个要处理的任务 public: PrintSystemSimulator(int numPrinters) : currentTime(0), jobIndex(0) { for (int i 0; i numPrinters; i) { printers.emplace_back(i 1); // 打印机ID从1开始 } } // 添加任务到系统假设按提交时间顺序添加 void addJob(int jobId, int priority, int submitTime, int duration) { allJobs.emplace_back(jobId, priority, submitTime, duration); // 可以在这里按submitTime排序但假设输入是有序的 } // 运行模拟 void runSimulation() { std::cout 打印机队列模拟开始 std::endl; // 主循环当还有未来任务或打印机有未完成工作时继续 while (jobIndex allJobs.size() || std::any_of(printers.begin(), printers.end(), [](const Printer p) { return p.hasPendingJobs(); })) { // 步骤1处理在当前时间到达的所有任务 while (jobIndex allJobs.size() allJobs[jobIndex].submitTime currentTime) { PrintJob job allJobs[jobIndex]; // 任务分配策略这里采用最简单的轮询找到当前等待队列最短的打印机 // 更复杂的策略可以是根据打印机ID分配或者始终分配给某一台 int targetPrinterId 0; size_t minQueueSize printers[0].jobQueue.size(); for (size_t i 1; i printers.size(); i) { if (printers[i].jobQueue.size() minQueueSize) { minQueueSize printers[i].jobQueue.size(); targetPrinterId i; } } printers[targetPrinterId].addJobToQueue(job); std::cout 时间 currentTime : 任务 job.jobId 提交分配到打印机 (targetPrinterId 1) (优先级: job.priority ) std::endl; jobIndex; } // 步骤2更新所有打印机状态 for (auto printer : printers) { printer.update(currentTime); } // 步骤3检查所有空闲打印机并尝试开始下一个任务 // 注意必须在所有update之后否则刚完成任务的打印机状态还没变为IDLE for (auto printer : printers) { if (printer.status Printer::IDLE) { printer.startNextJob(currentTime); } } // 时间推进 currentTime; } std::cout 模拟结束于时间 (currentTime - 1) std::endl; printStatistics(); } void printStatistics() { std::cout \n 任务统计 std::endl; for (const auto job : allJobs) { std::cout 任务 job.jobId : 提交 job.submitTime , 开始 job.startTime , 完成 job.endTime , 总耗时 (job.endTime - job.submitTime) std::endl; } } };3.3 主函数与测试用例int main() { // 示例创建一个有3台打印机的系统 PrintSystemSimulator sim(3); // 添加测试任务格式 jobId, priority, submitTime, duration // 任务按提交时间顺序添加 sim.addJob(1, 2, 0, 5); // 任务1优先级2时间0提交耗时5 sim.addJob(2, 5, 1, 3); // 任务2优先级5时间1提交耗时3 sim.addJob(3, 2, 2, 4); // 任务3优先级2时间2提交耗时4 sim.addJob(4, 9, 3, 2); // 任务4优先级9最高时间3提交耗时2 sim.addJob(5, 1, 4, 6); // 任务5优先级1最低时间4提交耗时6 sim.addJob(6, 5, 10, 3); // 任务6优先级5时间10提交耗时3 // 运行模拟 sim.runSimulation(); return 0; }4. 关键难点剖析与避坑指南实现过程中有几个地方极易出错也是面试官考察的重点。4.1 优先级插入的精确逻辑这是本题最核心的算法点。规则是高优先级任务可以插队但必须排在所有优先级不低于它的任务之后。注意是“不低于”也就是说如果队列里有一个同优先级的任务新来的同优先级任务应该排在其后除非题目特别说明同优先级按提交时间逆序但通常还是FIFO。我的addJobToQueue方法中的while循环逻辑体现了这一点遍历队列寻找第一个优先级低于新任务的位置。如果优先级相同则比较提交时间找到第一个提交时间晚于新任务的位置以保证同优先级下先来后到。在此位置插入。踩坑记录我最初写的比较逻辑是if (it-priority newJob.priority) break;这会导致同优先级任务插到前面违反了FIFO原则。调试了很久才发现。务必用具体数据如两个同优先级不同时间的任务测试这个边界。4.2 时间片推进与状态更新顺序模拟类题目对事件处理的顺序非常敏感。我的主循环顺序是处理新任务到达因为新任务在currentTime这一刻到达理论上它可以被立即加入队列并影响调度决策。更新打印机状态将当前时间片用于处理正在进行的任务remainingTime--。如果任务在本时间片完成打印机在currentTime结束时变为空闲。调度空闲打印机在currentTime结束时检查那些刚刚变为空闲或本就空闲的打印机从它们的队列中取出下一个任务开始打印。**这个任务的实际开始时间应该是currentTime 1**吗不在我们的模型里我们把一个时间片视为一个“时刻点”。任务在currentTime被决定开始它的startTime就记为currentTime它的结束时间是startTime duration。update函数在下一个时间片才会扣减它的时间。这种“开始时扣费”还是“结束时扣费”的模型需要统一我采用的是前者即startNextJob时设置endTimeupdate只负责递减和检测完成。设计选择另一种常见模型是把时间片视为一个“区间”。在currentTime到currentTime1这个区间内打印机处理任务。那么任务开始时间就是currentTime完成检查在currentTime1时进行。两种模型都可以但必须在注释和代码中保持一致否则计算总耗时会出现差1的错误。4.3 内存管理与对象所有权在上面的代码中我使用了PrintJob* currentJob指向allJobs中的元素。这里存在一个潜在风险如果allJobs是局部变量而Printer对象生命周期更长就会产生悬空指针。在我们的设计里PrintSystemSimulator同时管理allJobs和printers且生命周期一致所以是安全的。更稳健的做法是使用std::shared_ptr或者直接存储任务ID通过ID在allJobs中查找。为了代码简洁和可读性示例采用了指针方式但在实际项目或复杂系统中需要更谨慎。4.4 输入处理与鲁棒性机试题目通常需要从标准输入读取数据。一个健壮的程序必须考虑输入行可能有多余空格。任务可能不按严格时间顺序提交虽然题目常说“按时间递增”但最好排序一下。打印机ID、任务ID等可能不连续。可能有错误数据如优先级超出1-9范围。// 一个更健壮的输入处理示例片段 int main() { int numPrinters, numJobs; std::cin numPrinters numJobs; PrintSystemSimulator sim(numPrinters); std::vectorPrintJob inputJobs; for(int i 0; i numJobs; i) { int submit, printerId, jobId, priority, duration; // 假设输入格式提交时间 打印机ID 任务ID 优先级 耗时 std::cin submit printerId jobId priority duration; // 数据校验 if(priority 1 || priority 9) { std::cerr 警告任务 jobId 优先级 priority 超出范围已调整为合法值。 std::endl; priority std::max(1, std::min(9, priority)); } inputJobs.emplace_back(jobId, priority, submit, duration); } // 按提交时间排序 std::sort(inputJobs.begin(), inputJobs.end(), [](const PrintJob a, const PrintJob b) { return a.submitTime b.submitTime; }); // 添加到模拟器 for(const auto job : inputJobs) { sim.addJob(job.jobId, job.priority, job.submitTime, job.duration); } sim.runSimulation(); return 0; }5. 性能优化与扩展思考对于机试基本功能实现和正确性是第一位的。但如果想拿到高分或者在实际应用中可以考虑以下优化和扩展点。5.1 使用更高效的数据结构我们之前用vector管理队列在中间插入是O(n)。如果任务数量非常大比如数万个并且频繁发生高优先级插队性能可能成为瓶颈。此时可以改用std::list它的插入删除是O(1)。但遍历查找插入位置仍然是O(n)。有没有更好的办法我们可以为每个打印机的等待队列维护一个始终有序的数据结构。这样插入新任务时只需要找到正确位置插入即可查找位置可以用二分查找。std::vector在有序情况下可以用std::lower_bound进行二分查找插入但插入操作本身仍是O(n)的移动元素。std::multiset基于红黑树可以保证插入后自动排序且插入和查找都是O(log n)非常合适。但注意multiset中的元素是常量不能直接修改任务的startTime等属性。这时可以存储任务指针或shared_ptr或者只将任务ID和排序关键信息放入multiset主体数据另存。// 使用multiset优化队列存储任务指针示例 struct QueueCompare { bool operator()(const PrintJob* a, const PrintJob* b) const { if (a-priority ! b-priority) return a-priority b-priority; return a-submitTime b.submitTime; } }; std::multisetPrintJob*, QueueCompare jobQueue; // 等待队列 // 插入jobQueue.insert(job); // 获取下一个任务PrintJob* nextJob *jobQueue.begin(); jobQueue.erase(jobQueue.begin());5.2 支持更复杂的调度策略题目可能要求不同的调度策略例如全局队列所有打印机共享一个任务队列哪个打印机空闲就从全局队列取最高优先级的任务。负载均衡新任务总是分配给当前队列最短的打印机我们的示例实现了简单的轮询找最短队列。优先级队列与饥饿问题如果持续有高优先级任务到达低优先级任务可能永远得不到执行饥饿。可以引入“老化”机制随着等待时间增长逐步提高任务的优先级。实现这些策略意味着要修改任务分配逻辑addJobToQueue可能不再是打印机的方法而是系统级的方法和队列数据结构。5.3 事件驱动模拟 vs 时间步进模拟我们当前实现的是时间步进模拟即无论有没有事件发生时钟都一格一格向前走。这在时间跨度大、事件稀疏时效率很低。更高效的方法是事件驱动模拟。我们维护一个优先队列最小堆里面存放未来将要发生的事件如“任务到达事件”、“任务完成事件”。每次从堆顶取出最近的事件将时钟快进到该事件发生的时间处理该事件并可能产生新的事件如一个任务开始打印会产生一个未来的“完成事件”加入堆中。这样模拟器只在有事件的时间点工作跳过了中间的空闲时间。struct Event { int time; enum Type { JOB_ARRIVAL, JOB_FINISH } type; int printerId; // 相关打印机 int jobId; // 相关任务 // 自定义比较运算符时间小的优先 bool operator(const Event other) const { return time other.time; } }; std::priority_queueEvent, std::vectorEvent, std::greaterEvent eventQueue; // 主循环变为 while(!eventQueue.empty()) { 处理堆顶事件; }事件驱动模拟更复杂但它是工业级离散事件模拟的标准方法如果机试时间充裕且想挑战高分可以尝试实现。6. 调试技巧与常见问题排查即使思路清晰实现过程中也难免遇到bug。以下是我总结的几个调试技巧。6.1 构造极端测试用例不要只用题目给的例子。自己设计一些边界情况单任务测试只有一个任务验证基本流程。优先级全相同所有任务优先级一样应该退化为纯FIFO。持续高优先级连续提交多个高优先级任务看低优先级任务是否会饿死根据题目要求。同时到达多个任务在同一时间点提交到同一打印机检查排序逻辑。空转测试在很长一段时间没有新任务打印机完成工作后应保持空闲时间应正常推进。大时间差任务1在时间0提交耗时100任务2在时间1提交优先级更高。任务2是否能在任务1完成前开始不能因为它到达时打印机正忙且不能抢占正在执行的任务。它应该进入队列等待。6.2 添加详细的日志输出在关键节点打印状态信息是调试模拟程序最有效的方法。我在示例代码中已经加入了一些cout语句。你可以更细化比如在addJobToQueue内部打印插入前后的队列状态在每次时间循环开始打印当前时间和各打印机状态。void debugPrintState(int time) { std::cout \n--- 时间 time --- std::endl; for (const auto printer : printers) { std::cout 打印机 printer.printerId [ (printer.statusPrinter::BUSY?忙碌:空闲) ] ; if(printer.currentJob) { std::cout 正在打印: printer.currentJob-jobId 剩余: printer.remainingTime; } std::cout 队列:; for(const auto job : printer.jobQueue) { std::cout J job.jobId (P job.priority ); } std::cout std::endl; } }6.3 常见Bug与解决方法任务开始时间或结束时间计算错误总是差1。解决方法统一你的时间模型。画一个时间轴明确每个时间点t代表的是时刻还是区间。建议将t视为“时刻”在t时刻决定开始的任务其startTime t预计在t duration时刻完成。在update中remainingTime--表示处理了一个单位时间当remainingTime 0时在当前时刻任务完成。优先级插入导致顺序混乱解决方法单独写一个小程序测试你的addJobToQueue或比较函数。输入一组任务打印插入后的队列顺序与手工计算的结果对比。指针错误或对象切片如果使用指针或引用确保它们指向的对象在整个生命周期内有效。如果直接存储对象注意vector插入删除可能导致迭代器失效。解决方法对于简单模拟可以优先使用对象ID进行关联而非指针。如果要用指针考虑使用std::shared_ptr或确保存储容器如allJobs生命周期覆盖整个模拟过程。模拟无法结束主循环条件有误可能因为某个打印机状态判断错误导致hasPendingJobs()永远返回true。解决方法检查循环条件确保当所有任务都已提交(jobIndex allJobs.size())且所有打印机空闲且队列为空时循环能退出。在循环内打印currentTime和任务索引观察是否在空转。把这个打印机队列模拟题吃透你收获的不仅仅是一道题的解法更是一套处理复杂业务逻辑模拟、设计清晰类结构、编写健壮C代码的实战方法论。在华为OD这类机试中展现出这种系统性的思考和实现能力远比快速写出一个漏洞百出的答案要重要得多。