☰
数据结构实战:用队列模拟买房排队全流程
2026/10/14 22:03:57 网站建设 项目流程

拿到“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); }

这段代码看着没有问题,但如果你把两个判断条件反过来写,就会出现逻辑错误。比如你先判断预算够不够,再判断房源余量,那么“预算够但没有房”和“预算不够但有房”会被混为一类,输出信息不够准确。更隐蔽的问题是,如果题目要求“目标户型售罄时,可以买备选户型”,判断次序就更加关键:必须先用目标户型尝试,失败后再用备选户型,而不是一上来就检查备选。

我给出一个带备选户型版本的判断顺序:

  1. 先看目标户型是否有余量且预算足够,满足则成交;
  2. 否则,看备选户型是否有余量且预算足够,满足则买备选;
  3. 两个都不满足,客户离开。

这个顺序是严格按照现实业务逻辑来的:客户优先选择自己最喜欢的户型,买不到才会退而求其次。

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”,然后拿着日志模拟一遍整个买房流程。这个方法看起来原始,但排查逻辑错误极其高效。很多时候,你以为自己的队列出了问题,其实只是判断分支写错了顺序。

这道作业题的价值并不在于让你背下队列的八个方法,而是在于训练一种能力——把现实世界里复杂、动态、多约束的业务过程,翻译成一个只有入队、出队、取队头三种操作的简洁模型。能顺利完成这种翻译,数据结构这门课的核心目标也就达到了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询