ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

数据结构实战:用队列模拟买房排队全流程

数据结构实战:用队列模拟买房排队全流程 拿到“w11必做题1买房必做2队列”这个题目描述时我的第一反应是这标题写得也太随手了活像备忘录里的一条待办。但等你真正坐下来读题会发现这恰好是数据结构课程里最值得认真做的一类练习——它用“买房排队”这个生活场景把队列Queue这个基础数据结构考得明明白白。这篇文章我打算直接按“业务拆解 → 数据结构选型 → 完整实现 → 踩坑复盘 → 工程扩展”的顺序来讲。不管你是正在赶作业的学生还是想复习队列应用细节的开发者都可以照着这份思路把代码写出来并且能解释清楚每个设计决策的理由。尤其是那些OJ上不报错、但代码评审一眼就能看出来的问题我会单独拉一章来讲。1. “买房”到底在考什么先读懂业务再谈代码1.1 一个售楼处选房的完整业务过程题目叫“买房”但具体到代码层面它到底想模拟什么我给你还原一个最常见的出题版本。假设售楼处有若干套房子分为几种户型。比如A户型还剩2套B户型还剩1套C户型已经售罄。然后有若干个客户按照到达顺序一个接一个地进入选房区。每位客户身上带着三个关键属性姓名、心理预算、偏好的户型。流程是客户走到前台说出自己想要的户型如果这个户型还有余量并且客户预算足够那么成交该户型余量减一如果目标户型已经售罄或者预算不够客户可以选择放弃也可以考虑备选户型具体看题目规则不管成交还是放弃这个客户处理完之后下一位客户才能上来。最后程序要输出什么一般是两类结果一类是每套房子的成交情况另一类是每个客户最终买到了什么、等待了多长时间。有些版本还要求统计成交率、平均等待时长。注意这里最核心的约束是“按照到达顺序一个接一个”。这就是一个标准的先进先出过程。你不需要把客户按预算排序也不需要按姓氏排序甚至不能因为某个客户出价高就让他插队。先来后到就是唯一的规则。1.2 为什么这题的核心不是“排序”而是“排队”很多同学看到“买房”两个字第一反应是这不就是个排序题吗把客户按预算从高到低排把房子从贵到便宜排然后逐个匹配。这一步其实已经掉进陷阱了。如果题目没有明确说“出价高者优先”那买房就是先到先得。两个客户同时看中同一套房先到的客户预算不足后到的客户预算反而够了你能让后到的客户直接买走吗真实业务里不可以因为客户是按顺序接待的。也就是说顺序是绝对刚性的预算只能决定这个客户买不买得起不能决定他排在哪。那用数组模拟行不行当然行但不优雅。如果你用一个数组存放所有客户然后每次从头处理处理完一个就把后面的元素整体前移每次删除的时间复杂度是 O(n)处理 n 个客户就是 O(n²)。数据量小还能忍数据量一大就明显超时。队列的好处是入队和出队都是 O(1)整个流程跑下来是 O(n)这才符合“模拟业务过程”的本意。我习惯用一个简单的对比来看待这类问题数组适合“随机访问”栈适合“后到先处理”队列适合“先到先处理”。买房选房这个场景天然就和队列绑定。所以这道题真正考的是你能不能把一个业务过程翻译成数据结构操作。2. 队列实现选型链式队列和循环数组队列怎么选2.1 两种存储结构的核心差异确定了要用队列紧接着就面临选型问题。数据结构课本里教过两种队列实现方式链式队列和循环数组队列。课堂上两种都要求会写但实际做题时选哪个取决于输入规模。链式队列的优点是容量可以动态增长不需要事先知道客户总数。缺点也很明显每个节点需要额外存一个指针字段内存开销略大操作时还要小心指针指错。循环数组队列的优点是内存紧凑、操作简单性能通常会比链式略好一点。但它要求你提前知道最大容量。如果题目给了明确的人数上限比如“客户总数不超过10000”那用循环数组非常省心。我通常这样判断如果输入第一行给了总人数 n或者题目说明了规模上限我优先用循环数组队列如果题目是一个流式的输入不知道什么时候结束我就用链式队列。下面是一个简单的对比表方便你快速决策维度链式队列循环数组队列容量动态增长需要预知上限内存开销每个节点多一个指针固定数组略省内存入队/出队复杂度O(1)O(1)实现难度中等需处理指针较低但要注意满/空判断适用场景未知输入量、频繁增删已知规模、OJ时间要求严格2.2 一个最小可用的队列接口设计无论你选哪种实现方式我都强烈建议按照“接口思维”来写队列不要让业务代码直接操作队列的内部节点。这也是这道题隐藏的加分点代码结构清晰。以链式队列为例我会把业务字段和队列节点分开设计。一个客户节点可以这样定义typedef struct Customer { char name[32]; int budget; // 心理预算 int prefer; // 偏好户型编号 int arrive_time; // 到达时间用于统计等待时长 } Customer; typedef struct QNode { Customer data; struct QNode *next; } QNode; typedef struct Queue { QNode *front; QNode *rear; int size; } Queue;这里把业务数据和队列指针分离开好处是日后如果要改队列的数据类型不用动主逻辑。然后实现四个最基本的方法初始化、判空、入队、出队、取队头。注意一个细节取队头是读但不删除出队是删除但不返回数据。这样拆分是为了避免你在出队时还要考虑要不要释放内存的问题。void initQueue(Queue *q) { q-front q-rear NULL; q-size 0; } int isEmpty(Queue *q) { return q-size 0; } void enQueue(Queue *q, Customer c) { QNode *node (QNode *)malloc(sizeof(QNode)); node-data c; node-next NULL; if (q-rear NULL) { q-front q-rear node; } else { q-rear-next node; q-rear node; } q-size; } Customer getFront(Queue *q) { return q-front-data; } Customer deQueue(Queue *q) { Customer c q-front-data; QNode *tmp q-front; q-front q-front-next; if (q-front NULL) { q-rear NULL; } free(tmp); q-size--; return c; }这段代码里的关键点有两个。第一出队的时候必须特判“出队后队列变空”的情况把 rear 也置为 NULL否则后面再入队时 rear 还指向一个已经释放的节点就会出现野指针。第二如果你实际上不需要出队后返回的数据可以直接调用 getFront 拿到队头再调用 deQueue 删除业务逻辑会更清晰。3. 买房主流程实现入队、出队、成交判断的分工与合作3.1 输入阶段与主循环的整体结构队列写完之后接下来就是业务主流程。这个主流程本身并不复杂核心就是一句话把所有客户按顺序入队然后循环处理队头客户直到队列为空。我先把整体骨架罗列出来int main() { Queue q; initQueue(q); int n; scanf(%d, n); for (int i 0; i n; i) { Customer c; scanf(%s %d %d %d, c.name, c.budget, c.prefer, c.arrive_time); enQueue(q, c); } while (!isEmpty(q)) { Customer c getFront(q); // 1. 判断客户是否买得起、目标户型是否有余量 // 2. 成交或放弃 // 3. 记录结果 deQueue(q); } // 输出结果 }为什么不写成一边出队一边判断也就是直接调用 deQueue 拿到客户数据再判断因为可能出队之后你才发现这个客户还需要和其他状态项比如备选方案交互如果一开始就把数据取走队列的完整性就没有意义了。保持“先查看队头再决定是否出队”的顺序可以让逻辑更接近真实业务。3.2 分支条件的判断次序很重要我给一个最简单的版本每个客户只有一个目标户型如果目标户型售罄或预算不足客户直接离开。while (!isEmpty(q)) { Customer c getFront(q); if (house_count[c.prefer] 0 c.budget house_price[c.prefer]) { house_count[c.prefer]--; printf(%s bought house %d\n, c.name, c.prefer); } else { printf(%s left\n, c.name); } deQueue(q); }这段代码看着没有问题但如果你把两个判断条件反过来写就会出现逻辑错误。比如你先判断预算够不够再判断房源余量那么“预算够但没有房”和“预算不够但有房”会被混为一类输出信息不够准确。更隐蔽的问题是如果题目要求“目标户型售罄时可以买备选户型”判断次序就更加关键必须先用目标户型尝试失败后再用备选户型而不是一上来就检查备选。我给出一个带备选户型版本的判断顺序先看目标户型是否有余量且预算足够满足则成交否则看备选户型是否有余量且预算足够满足则买备选两个都不满足客户离开。这个顺序是严格按照现实业务逻辑来的客户优先选择自己最喜欢的户型买不到才会退而求其次。3.3 统计指标等待时间与成交数据的处理不少题目要求输出平均等待时间或者判断“谁的等待时间最长”。这时候你就要在客户节点里记录到达时间然后在轮到他处理时计算等待时间。int wait_time current_time - c.arrive_time;这里有几个细节很容易出错。第一current_time 是什么如果题目描述的是“每个客户处理需要固定时间”那 current_time 就是从头开始累计的处理时间如果题目描述的是“当前时刻”那 current_time 可能已经给定。一定要仔细读题。第二如果用 int 统计总等待时间客户数量超过一定规模时会溢出。虽然业务上单个人等待时间不会很长但总和可能很大。习惯上用 long 来存累计等待时间更稳妥。第三如果你需要知道“最后买走某套房子的客户是谁”建议在房源结构里维护一个买家字段而不是等到最后再回头查。4. 实测踩坑复盘这题最容易扣分的四个细节4.1 坑一把“先来后到”实现成“排序后处理”这个坑我见过太多次。题目刚发下来有同学一看“买房”立刻联想到贪心算法把客户按预算从高到低排序优先处理出价高的人。这种方案在某些“拍卖型”题目里是对的但在这道“排队型”题目里直接挂掉。原因很简单业务规则决定顺序算法不能擅自改变顺序。如果题目明确说“客户按到达顺序依次选房”那到达顺序就是一条不可违反的铁律。你在设计算法时第一步应该是找“顺序约束”而不是找“最优策略”。怎么检查你有没有踩这个坑把输入数据打乱顺序再跑一遍如果结果变了说明你的算法依赖输入顺序之外的某种排序规则那就要警惕了。4.2 坑二链式队列出队后没有释放旧节点这属于典型的内存管理问题。OJ上跑的话通常不会爆出内存泄漏的报错最多就是内存占用偏高。但如果是人工代码评审这一步基本要被扣分。错误写法通常是这样的Customer deQueue(Queue *q) { QNode *tmp q-front; q-front q-front-next; return tmp-data; // 忘了 free(tmp) }每次出队少 free 一次跑一万个客户就泄漏一万个节点。更麻烦的是如果出队的是最后一个节点你还要把 rear 置为 NULL否则后续入队会访问到原本已经释放的内存。这两个问题经常同时出现。我建议你写完队列之后专门写一个“只有三个客户”的小测试逐一入队再逐一出队配合内存检测工具检查是否有泄漏。这一步花费的时间很少但能避免很多隐性扣分。4.3 坑三循环数组队列的“满”和“空”判断搞反如果你选用循环数组队列最常见的问题就是“队满”和“队空”的判断条件互相混淆。经典的实现方式是让数组保留一个空位用 front 和 rear 的相对位置来判断队列空front rear队列满(rear 1) % capacity front很多同学会把队满条件写成 front rear导致一个满队列被误判为空然后死循环。还有同学在入队时没有正确处理取模操作比如q-rear q-rear 1; // 错误没有取模 q-data[q-rear] c;这条代码在连续入队几次之后rear 会直接越界访问数组尾部。正确写法是q-rear (q-rear 1) % capacity; q-data[q-rear] c;取模这件事特别容易在“看起来没报错”的状态下出错因为数组越界访问不一定立刻崩溃可能只是偷偷覆盖了相邻内存等到输出统计结果时才变得莫名其妙。4.4 坑四判断次序写反导致已售罄户型被再次卖出这个坑往往出现在稍微复杂的题目版本里。比如客户可以先判断“预算够不够”再判断“有没有房”。如果预算不够程序可能直接输出离开但忽略了房源已经被前面的人全部买完此时应该输出“售罄”而不是“预算不足”。反过来如果先判断“有没有房”再判断“预算够不够”那么在房源还有、但预算不够的情况下应该继续尝试备选户型而不是直接放弃。换句话说判断分支的先后顺序直接决定了业务逻辑是否自洽。我的建议是把所有可能的判断条件列出来模拟一个客户走完全部流程再决定代码顺序。你可以在代码里加一条注释把判断顺序写出来// 1. 目标户型是否售罄 // 2. 预算是否足够 // 3. 备选户型是否售罄 // 4. 备选户型是否买得起这看起来是小事但在写复杂版本时这一步能帮你减少无数调试时间。4.5 如何用边界测试验证正确性作业提交之前我强烈建议你构造几组边界测试用例不要只拿题目给的示例跑一遍就完事。下面是我常用的几组用例类型输入特征期望结果空输入客户总量 n 0不报错输出为空单个客户只有一位客户房源充足该客户正常成交房源归零所有户型数量都是 0所有客户离开预算不足客户预算低于任何户型价格客户全部离开顺序竞争两客户抢同一户型先到的预算不足先到的放弃后到的成交不能交换顺序备选逻辑首选户型售罄备选有余量客户买备选户型这些测试用例构造起来很快但价值很高。很多隐藏逻辑错误比如“判断次序写反”“顺序被排序破坏”在常规测试里看不出来边界测试一跑就现出原形。5. 从课堂作业到工程实践队列的三种进阶变形5.1 优先队列当VIP客户可以插队时“买房排队”这个场景在真实世界中往往不是绝对公平的。有的题目会加一条规则VIP客户可以优先进场。这时候你再用普通队列就不合适了。优先队列和非优先队列的区别在于出队的不一定是入队最早的而是优先级最高的。在C语言里你可以用堆Heap来实现在C里可以直接用 priority_queue。这个数据结构在算法题里非常常见核心思路是在入队时就把顺序优先级排好出队时直接取堆顶。如果你做完这道“买房队列”的必做题我建议你顺手把同样场景改成“VIP优先”再写一遍。你会发现业务规则一变数据结构选择也跟着变这才是真正的能力考察点。5.2 双端队列当客户可能反悔时有些模拟场景中客户可能会“撤销刚才的选房”或者新来的客户需要排到最前面。这时双端队列 deque 就派上用场了它允许你在队头和队尾两端都能入队、出队。在“买房”这个业务里怎么用比如客户选完房后突然决定反悔退房出来后重新排队到末尾这个过程需要从队尾入队而如果题目有“老客户优先”的规则那就可能需要从队头插入。双端队列能同时覆盖这两种需求抽象能力比普通队列强很多。5.3 队列在真实系统中的同构应用学完这道作业题你会发现队列的应用范围远比“模拟排队”广泛。举几个例子消息队列生产者把消息入队消费者按顺序出队保证消息不丢失、不重复任务调度操作系统中的就绪队列CPU按时间片轮转处理任务网络请求服务器接收大量并发请求往往先放入请求队列再逐个处理打印任务多个文档发送到打印机打印机按提交顺序逐个打印。这些场景的共同点都是“先来先服务”和买房排队本质上是同构的。你在未来工作中很可能遇到比这道题大得多的数据量但抽象思路完全一样。最后再分享一个实际经验。在写这类“场景模拟”题目时我建议你养成一个习惯在每一步关键操作前后打印日志比如“XX入队当前队头是XX”然后拿着日志模拟一遍整个买房流程。这个方法看起来原始但排查逻辑错误极其高效。很多时候你以为自己的队列出了问题其实只是判断分支写错了顺序。这道作业题的价值并不在于让你背下队列的八个方法而是在于训练一种能力——把现实世界里复杂、动态、多约束的业务过程翻译成一个只有入队、出队、取队头三种操作的简洁模型。能顺利完成这种翻译数据结构这门课的核心目标也就达到了。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表