18.5【保姆级教程】用队列进行模拟:从数据结构到现实世界的“预言机”
📢专栏持续更新中!关注博主不迷路,跟着专栏系统学C语言底层开发,从语法入门到工程实战,逐章拆解,保姆级讲解,刚入门的同学跟着学,全程零压力~
上一节我们掌握了抽象数据类型(ADT)的设计方法,学会了把数据结构和操作打包成模块,对外只暴露干净利落的函数接口。今天,我们要把这项技能用在一个非常有趣的地方——用队列来模拟现实生活中的排队场景。
你可能觉得“队列”只是个简单的先进先出(FIFO)数据结构,但它在真实世界中的应用无处不在:银行柜台前的顾客、机场跑道上等待起飞的飞机、操作系统里排队等待CPU处理的任务、你刷短视频时后台的请求队列……掌握了队列模拟,你就能用C语言回答这样的问题:
- 一个咨询摊位平均每小时能接待多少顾客?
- 每位顾客平均要等多久?
- 排队等待的顾客平均有多少人?
- 摊位需要设置多少个座位才够用?
这些问题不需要真的去街上蹲一天——用程序模拟几千个顾客,几分钟就能得到有统计意义的答案。这就是数据结构 + 编程的威力。
本节核心知识点梳理(提前划重点,方便后续对照学习):
- 问题建模:如何把现实中的排队场景抽象为程序中的数据结构;
- 随机数生成:用
rand()模拟顾客到达间隔和服务时长的随机性; - 队列 ADT 的复用:直接使用之前学过的队列模块,不必重复造轮子;
- 模拟循环的核心逻辑:以“分钟”为单位推进仿真时钟,处理到达、服务、离开事件;
- 数据统计与分析:从模拟中提取平均等待时间、平均队列长度、每小时服务人数等指标。
一、问题描述:商业街上的咨询摊位
1.1 场景设定
假设 Sigmund Landers 在商业街设置了一个提供付费建议的摊位。规则如下:
- 顾客可以购买1分钟、2分钟或3分钟的建议(咨询服务时长);
- 为确保商业街人流畅通,每个摊位前排队等待的顾客最多为10人(即队列最大长度为10);
- 顾客是随机出现的,到达间隔不确定;
- 每位顾客的服务时长也是随机选择的(1、2或3分钟);
- 如果顾客到达时队列已满(10人),该顾客直接离开(损失一位顾客)。
我们要回答的核心问题:
- Sigmund 平均每小时能接待多少名顾客?
- 每位顾客平均花在咨询上的时间是多少?
- 排队等待的顾客平均有多少人?
- 有多少顾客因为队列满而流失?
1.2 为什么队列是最合适的数据结构?
这个问题天然符合队列的“先进先出”特性:
- 先来的顾客先接受服务;
- 后来的顾客排在队尾;
- 服务完的顾客从队首离开。
队列在这里就是顾客排队的数字化身。
1.3 模拟策略:以“分钟”为单位推进
我们将模拟时间以分钟为最小单位进行推进。每一分钟里,会发生以下事件:
| 步骤 | 事件 | 模拟操作 |
|---|---|---|
| 1 | 是否有新顾客到达? | 用随机数决定,如果到达且队列未满则入队 |
| 2 | 当前是否有顾客正在接受服务? | 如果是,剩余服务时间减1分钟 |
| 3 | 服务是否刚好完成? | 如果是,让该顾客出队,统计其等待时间 |
| 4 | 如果服务完成且队列非空 | 从队列中取出下一位顾客,开始服务 |
这就是离散事件模拟的核心循环。程序运行几千个这样的“分钟”后,就能得出有统计意义的结论。
二、随机数的艺术:让模拟更真实
2.1 顾客到达的随机性
现实中,顾客不是排着队准时出现的。假设平均每 3 分钟来一位顾客,但实际间隔可能是1分钟、5分钟、甚至连续来两位。我们怎么模拟这种“随机感”?
C 语言的rand()函数生成一个 0 到RAND_MAX之间的伪随机整数。通过与运算,可以得到指定范围内的随机数:
#include<stdlib.h>#include<time.h>// 在 main 开头调用一次,用当前时间做种子,确保每次运行结果不同srand((unsignedint)time(NULL));// 生成 1 到 3 之间的随机整数,代表 1分钟、2分钟 或 3分钟intservice_time=rand()%3+1;2.2 到达概率的设定
假设平均每 3 分钟来一位顾客,则每一分钟有 1/3 的概率来一位新顾客。我们可以这样模拟:
// 每一分钟执行一次:if(rand()%3==0){// 概率约为 1/3// 来了一位新顾客if(queue_size<MAX_QUEUE_SIZE){queue_enqueue(q,service_time);}else{customers_lost++;// 队列满,顾客流失}}2.3 服务时长的随机性
每位顾客购买的建议时长是随机的(1、2或3分钟):
intservice_time=rand()%3+1;// 1, 2, 或 3三、模拟程序的完整实现
3.1 需要统计的数据
在开始编写模拟循环前,先明确我们要收集哪些数据:
| 统计指标 | 变量名 | 含义 |
|---|---|---|
| 接待顾客总数 | total_served | 成功接受服务并离开的顾客数量 |
| 流失顾客总数 | total_lost | 因队列满而直接离开的顾客数量 |
| 总等待时间 | total_wait_time | 所有顾客在队列中等待的分钟数之和 |
| 总服务时间 | total_service_time | 所有顾客接受咨询的分钟数之和 |
| 模拟总时长 | SIMULATION_MINUTES | 模拟多少分钟(如 480 分钟 = 8小时) |
通过这些原始数据,可以计算出:
- 平均每小时接待人数 =
total_served / (SIMULATION_MINUTES / 60) - 平均等待时间 =
total_wait_time / total_served - 平均队列长度 =
total_wait_time / SIMULATION_MINUTES(这段时间内累积的等待分钟数 ÷ 总分钟数,近似等于任一时刻的平均排队人数) - 流失率 =
total_lost / (total_served + total_lost)
3.2 完整代码
#include<stdio.h>#include<stdlib.h>#include<time.h>#defineMAX_QUEUE_SIZE10#defineARRIVAL_PROB3// 平均每3分钟来一位顾客(概率 1/3)#defineSIMULATION_MINUTES480// 模拟8小时(480分钟)// ---------- 队列 ADT 的简单实现(内嵌版本,便于教学) ----------structCustomer{intservice_time;// 该顾客需要的服务时长(1~3分钟)intwait_time;// 该顾客在队列中等待的分钟数(入队时记录当前时间)};structQueue{structCustomeritems[MAX_QUEUE_SIZE];intfront;intrear;intsize;};voidqueue_init(structQueue*q){q->front=0;q->rear=0;q->size=0;}intqueue_is_full(structQueue*q){returnq->size>=MAX_QUEUE_SIZE;}intqueue_is_empty(structQueue*q){returnq->size==0;}intqueue_enqueue(structQueue*q,structCustomerc){if(queue_is_full(q))return-1;q->items[q->rear]=c;q->rear=(q->rear+1)%MAX_QUEUE_SIZE;q->size++;return0;}structCustomerqueue_dequeue(structQueue*q){structCustomerc=q->items[q->front];q->front=(q->front+1)%MAX_QUEUE_SIZE;q->size--;returnc;}// ---------- 模拟主程序 ----------intmain(void){structQueueq;queue_init(&q);srand((unsignedint)time(NULL));// 统计变量inttotal_served=0;// 成功服务的顾客总数inttotal_lost=0;// 因队列满而流失的顾客总数inttotal_wait_time=0;// 所有顾客的总等待分钟数inttotal_service_time=0;// 所有顾客的总服务分钟数intcurrent_service_remaining=0;// 当前正在服务的顾客还剩余多少分钟(0 表示空闲)intcurrent_minute;// 模拟时钟的当前分钟数// 模拟主循环:每分钟迭代一次for(current_minute=1;current_minute<=SIMULATION_MINUTES;current_minute++){// 事件1:是否有新顾客到达?if(rand()%ARRIVAL_PROB==0){// 概率 1/ARRIVAL_PROBif(!queue_is_full(&q)){structCustomerc;c.service_time=rand()%3+1;// 随机 1, 2, 3 分钟c.wait_time=current_minute;// 记录到达时刻,后续计算等待时间queue_enqueue(&q,c);}else{total_lost++;// 队列满了,顾客直接离开}}// 事件2:当前是否有顾客正在接受服务?if(current_service_remaining>0){current_service_remaining--;// 服务时间减少 1 分钟// 事件3:服务是否刚好完成?if(current_service_remaining==0){total_served++;}}// 事件4:如果当前空闲,且队列非空,从队列中取下一位顾客开始服务if(current_service_remaining==0&&!queue_is_empty(&q)){structCustomerc=queue_dequeue(&q);current_service_remaining=c.service_time;total_wait_time+=(current_minute-c.wait_time);// 累计该顾客的等待分钟数total_service_time+=c.service_time;}}// 模拟结束,输出统计结果printf("========== 模拟结果 ==========\n");printf("模拟总时长:%d 分钟(约 %.1f 小时)\n",SIMULATION_MINUTES,SIMULATION_MINUTES/60.0);printf("接待顾客总数:%d 人\n",total_served);printf("流失顾客总数:%d 人(队列满)\n",total_lost);printf("平均每小时接待:%.1f 人\n",(double)total_served/(SIMULATION_MINUTES/60.0));if(total_served>0){printf("每位顾客平均等待时间:%.1f 分钟\n",(double)total_wait_time/total_served);printf("每位顾客平均服务时间:%.1f 分钟\n",(double)total_service_time/total_served);}printf("平均队列长度:%.2f 人\n",(double)total_wait_time/SIMULATION_MINUTES);printf("流失率:%.1f%%\n",(double)total_lost/(total_served+total_lost)*100.0);return0;}3.3 运行结果示例
========== 模拟结果 ========== 模拟总时长:480 分钟(约 8.0 小时) 接待顾客总数:142 人 流失顾客总数:0 人(队列满) 平均每小时接待:17.8 人 每位顾客平均等待时间:0.7 分钟 每位顾客平均服务时间:2.0 分钟 平均队列长度:0.22 人 流失率:0.0%新手重点:因为每次运行用了srand(time(NULL)),结果会稍有不同,但大致趋势一致。这正是模拟的意义——通过反复运行,得出一个统计范围,而不是精确的单一数值。
3.4 代码逐段拆解
① 数据结构定义
structCustomer{intservice_time;// 服务时长intwait_time;// 到达时刻(用于计算等待时间)};wait_time记录的是顾客入队时的分钟数(当前模拟时钟的读数)。当这位顾客最终被服务时,用current_minute - c.wait_time就得到了他在队列中等待的总分钟数。
② 模拟主循环的四步事件
for(current_minute=1;current_minute<=SIMULATION_MINUTES;current_minute++){// 事件1:检查新到达// 事件2:推进当前服务// 事件3:检查服务完成// 事件4:取下一位顾客}这个循环结构是离散事件模拟的通用模板,换个场景同样适用。
③ 统计变量的更新时机
| 变量 | 何时更新 |
|---|---|
total_served | 服务完成时 |
total_lost | 到达但队列满时 |
total_wait_time | 顾客从队列中取出开始服务时 |
total_service_time | 顾客从队列中取出时 |
④ 平均队列长度的计算
平均队列长度 = 总等待分钟数 / 模拟总分钟数这背后的原理是利特尔法则:在一个稳定系统中,平均排队人数 = 顾客到达率 × 平均等待时间。我们的计算方式等价于这个公式,统计意义上合理。
3.5 你可以调整的参数
| 参数 | 当前值 | 如果调整为…… | 会有什么变化? |
|---|---|---|---|
MAX_QUEUE_SIZE | 10 | 5 | 流失率飙升,排队人数减少 |
ARRIVAL_PROB | 3(每3分钟来一位) | 2(每2分钟来一位) | 顾客更多,队列更长,等待更久 |
SIMULATION_MINUTES | 480(8小时) | 2880(48小时) | 样本更大,统计结果更稳定 |
| 服务时长范围 | 1~3分钟 | 1~5分钟 | 每位顾客占用更长时间,队列积压更严重 |
动手试一试:改几个参数重新运行,观察结果如何变化。这就是模拟的魅力——不需要真的去街上开个摊位,就能提前预见各种情况。
四、本章总结(新手必看,快速掌握核心)
| 核心知识点 | 一句话总结 |
|---|---|
| 问题建模 | 把现实排队场景映射为队列数据结构,1分钟为模拟时钟的最小单位 |
| 随机数模拟 | rand() % N控制概率,srand(time(NULL))保证每次运行结果不同 |
| 四步事件循环 | 到达检查 → 服务推进 → 完成处理 → 取下一位,这是离散事件模拟的通用模板 |
| 数据统计 | 在关键事件点累计原始数据,模拟结束后统一计算平均指标 |
| 利特尔法则 | 平均队列长度 = 总等待时间 / 总模拟时间 |
| 参数可调 | 队列容量、到达率、服务时长范围等参数都可以自由调整,观察不同场景 |
✅入门行动清单:
- 把上面的完整代码复制到 VS 中,编译运行,观察输出结果;
- 修改
MAX_QUEUE_SIZE为 5,重新运行,对比流失率的变化; - 修改
ARRIVAL_PROB为 2(顾客来得更频繁),重新运行,观察等待时间的变化; - 尝试将模拟时长设为
60 * 24(模拟一整天),看看结果是否更稳定; - 思考一下:你身边还有哪些场景可以用队列模拟?食堂打饭?地铁安检?试试动手改编代码。
队列模拟的魔法已经在你手里了。它不仅仅是一个数据结构练习,更是把编程能力应用于解决现实问题的思维方式——从观察世界到建模抽象,再到代码实现和数据分析,这条链路是高级程序员的必备素养。
👉关注博主,专栏持续更新,从基础到实战,保姆级讲解 C 语言核心特性与工程技巧。队列之后,我们将进入二叉树的世界——那是数据结构的又一个重要篇章。我们下一节见!
#C语言 #数据结构 #队列 #离散事件模拟 #随机数 #保姆级教程 #新手避坑 #嵌入式开发 #CSDN #C语言实战
🎁欢迎关注公众号,获取更多技术干货!
🚀 C语言宝藏资源包免费送!14 本 C++ 经典书 + 编译工具全家桶 + 高效编程技巧,搭配 C 语言精选书籍、20 + 算法源码 + 项目规范,还有 C51 单片机 400 例实战!从零基础到嵌入式开发全覆盖,学生党、职场人直接抄作业~ 关注文章末尾的博客同名公众号,回复【C 语言】一键解锁全部资源,手慢也有!
