简介:银行排队系统实验报告是一份面向计算机专业学生的C语言数据结构课程设计资料,以队列为核心模拟银行多窗口排队场景,帮助学习者掌握如何将离散事件仿真转化为可运行的程序,并理解平均逗留时间的计算逻辑。资源为单个doc文档,压缩包仅191KB,内容完整,无需额外安装环境即可查阅,适合正在完成同类实验或复习队列应用的学习者。报告涵盖设计目的、仪器环境、导航图、程序分析及主要模块源程序,包括主菜单的switch控制、客户到达时VIP与普通用户的队列分配、客户离开时的记录更新、业务与排队查询等,同时配合时间函数模拟真实流逝过程,展示了从问题建模到算法表达的全流程。已有900余人浏览学习,尤其适合需要对照完整实验报告撰写思路、参考C语言队列实现或排查程序细节的读者。
1. 银行排队系统的实验报告到底在报告什么:先抓住三个核心指标
银行排队系统实验报告,本质上是回答“这个网点高峰时段该开几个窗口”的数据分析报告。你手头有的是一段真实观测得到的客户到达记录和服务时间记录,要做的是用排队论公式或者离散事件仿真,推演出客户平均等待时长、队列长度、窗口忙闲率三个指标,最后给出“三窗口够用、四窗口浪费”这类可执行的结论。我经手这类项目时最大的感触是:公式和代码都只是工具,真正决定报告价值的是参数标定和稳态判断这两件事,做对了,结论就立得住。这份报告的读者一般分两类——做课程设计或毕业设计的学生,以及银行网点里做运营分析的人,前者需要可复现的推导过程,后者需要能直接指导排班的数字。下面按实验的推进顺序,把从建模、仿真、数据采集到结论落地的一整套做法讲清楚。
2. 先用排队论把底账算清:M/M/c模型公式与参数标定
2.1 到达率λ与服务率μ:先定参数再谈模型
任何一份银行排队系统实验报告,最早的步骤都是把模型参数标定清楚。M/M/c 模型是排队论里最经典的服务台模型:第一个 M 表示客户到达过程服从泊松流,即到达间隔服从负指数分布;第二个 M 表示服务时间服从负指数分布;c 是服务窗口数量。模型的输入只有两个率——客户到达率 λ 和单窗口服务率 μ。λ 的单位通常写作“人/分钟”,μ 是“人/分钟”,服务时间均值就是 1/μ。
标定 λ 最常见的方式是统计高峰时段的到店人数。但这里有一个非常隐蔽的坑——到达率的单位时间跨度必须和排队公式里的口径一致。例如你统计到 9:30 到 10:30 来了 54 人,那 λ 就是 54/60 = 0.9 人/分钟,而不是 54 人/小时直接代入公式。μ 的标定更依赖数据来源:如果叫号系统能导出每笔业务的受理时长,直接用均值倒数;如果只能靠人工掐表,就要把客户在窗口的“业务办理时间”和“递单、签字、打印凭证”合并计为完整服务时间,别只记电脑操作那一段。
另一个标定细节是区分平峰和高峰。银行网点一天之内到达率波动很大,如果你把 10:00-11:00 的早高峰和 14:00-15:00 的平峰混在一起估计 λ,算出来的窗口数是错的。我在实际项目里通常把观测时间切成 10 分钟一段,分别统计每段的到达人数,再挑高峰段做模型参数。如果你手上只有一整天的总人数,那宁可把它当成平峰模型来分析,也别硬套高峰场景。
2.2 M/M/c稳态公式:给出一个具体可验算的数字
当系统满足 ρ = λ / (cμ) < 1 时,排队系统存在稳态,也就是队列长度不会无限积累。ρ 叫服务强度,代表窗口整体的忙闲程度。比如 λ = 0.9 人/分钟、μ = 0.4 人/分钟、c = 3 时,ρ = 0.9 / (3 × 0.4) = 0.75,说明三个窗口合计有 75% 的时间在忙。
M/M/c 模型的核心公式只有四条,依次计算空闲概率 P0、平均队列长度 Lq、平均等待时间 Wq 和平均逗留时间 W:
P0 = [ ∑_{n=0}^{c-1} (cρ)^n / n! + (cρ)^c / (c! × (1−ρ)) ]^(−1)
Lq = P0 × ρ × (cρ)^c / (c! × (1−ρ)^2)
Wq = Lq / λ
W = Wq + 1/μ
用上面的参数手动验算一遍:cρ = 0.9 / 0.4 = 2.25,P0 的分母是 2.25^0/0! + 2.25^1/1! + 2.25^2/2! + 2.25^3/(6×(1−0.75)),也就是 1 + 2.25 + 2.53125 + 7.59375 = 13.375,P0 ≈ 0.0748。代入 Lq 公式得到约 1.70 人,Wq = 1.70 / 0.9 ≈ 1.89 分钟,加上服务时间均值 1/μ = 2.5 分钟,逗留时间约 4.39 分钟。
这个验算结果一定要保留在报告里。它不但是模型推导的证据,更是你后面仿真器输出的校准基准。如果仿真程序写对了,同一组参数下的平均等待时间应该在 1.89 分钟附近;差太多,说明代码有 bug 或者预热期设置有问题。我习惯在实验报告的正文里放一个“参数—结果对照表”,把 λ、μ、c 以及 Wq、Lq 的理论值和仿真值并排写,一眼就能看到是否符合预期。
2.3 灵敏度分析:窗口数量决策的证据链
实验报告里最有力的一节内容就是灵敏度分析——每次只改变一个参数,观察 Wq 的变化趋势。以 λ 为例,μ = 0.4、c = 3 时,λ 从 0.7 升到 0.9,Wq 还比较温和;但当 λ 接近 1.1 时,ρ = 1.1/1.2 ≈ 0.917,排队长度会以非线性方式暴涨,Wq 可能冲到 10 分钟以上。这就是为什么业务人员常说“网点看着人不多,排队却特别久”——服务强度一旦逼近 0.9,等待时间对到达率极为敏感,半小时多来七八个人就能让排队翻倍。
窗口数量 c 的灵敏度分析更能直接支撑结论。同样 λ = 0.9、μ = 0.4,开 2 个窗口时 ρ = 1.125,系统不进稳态,排队无限堆积;开 3 个窗口 Wq ≈ 1.89 分钟;开 4 个窗口 ρ = 0.5625,Wq 可能掉到十几秒。这样的对照表放在报告里,决策者自然能看到“三窗口是性价比拐点”。注意分析时不要贪多,逐项做 3 到 5 组参数就够,太多反而稀释重点。
3. 手写一个银行排队离散事件仿真器:从事件循环到指标输出
3.1 为什么自己写仿真而不是直接上现成的仿真库
M/M/c 公式给的是理想化模型的精确解,但真实网点有叫号过号重取、贵宾插队、窗口临时关停这些不规则行为,公式兜不住。实验报告的扎实程度,取决于有没有一份能复现的仿真代码。业界做排队仿真的常见工具是 SimPy 这类离散事件仿真库,用它写排队模型很快,但它把事件调度和资源管理封装在内部,变成黑匣子;一旦你要解释“为什么等待时间是 1.9 分钟而不是 0.8 分钟”,就得翻开库源码去查它内部怎么分配资源的。自己在 200 行内写一个事件驱动仿真器,每个状态变化都暴露在眼前,实验报告里可以直接贴代码,答辩时每一步都能讲清楚。
我推荐持续采用“下一事件优先”的事件调度方式。它的核心是维护一个按时间排序的事件堆,每次从堆里弹出最早发生的事件(要么是客户到达,要么是服务完成),更新系统状态,然后生成后续事件。这种方式不会浪费时间扫描没有事件发生的空闲时间片,仿真几万个事件也就是毫秒级,比固定步长推进快一个数量级。
3.2 最小实现:一把事件堆、一张窗口时间表、一个客户等待队列
下面这段代码是我给实验报告准备的模板,去掉了多余的功能,保留了最核心的排队逻辑。它接收四个参数:到达率、服务率、窗口数量、仿真总时长,输出平均等待时间、平均队列长度和窗口忙碌比例。
import heapq import random from collections import deque def bank_queue_sim(arrival_rate, service_rate, num_windows, total_minutes=480, warmup_minutes=60, seed=42): random.seed(seed) # 事件堆: (时间戳, 事件类型, 客户id, 窗口id) # 事件类型 0=到达, 1=服务完成 events = [] heapq.heappush(events, (0.0, 0, None, None)) # next_free[i] 表示第 i 个窗口下一次空闲的时刻 next_free = [0.0] * num_windows # 客户等待队列, 存 (客户id, 到达时刻) waiting = deque() wait_times = [] # 预热期之后的等待时长记录 queue_len_samples = [] # 每次事件发生后的队列长度采样 customer_id = 0 clock = 0.0 served = 0 while events: t, etype, cid, wid = heapq.heappop(events) if t > total_minutes: break clock = t # 采样预热期之后的队列长度 if clock >= warmup_minutes: queue_len_samples.append(len(waiting)) if etype == 0: # 客户到达事件 customer_id += 1 free_win = None # 找一个已经空闲的窗口 for i, ft in enumerate(next_free): if ft <= clock: free_win = i break if free_win is not None: # 有窗口空闲, 立刻服务 if clock >= warmup_minutes: wait_times.append(0.0) svc_time = random.expovariate(service_rate) next_free[free_win] = clock + svc_time heapq.heappush(events, (clock + svc_time, 1, customer_id, free_win)) else: # 所有窗口都在忙, 进等待队列 waiting.append((customer_id, clock)) # 生成下一位客户的到达时间 inter_arrival = random.expovariate(arrival_rate) heapq.heappush(events, (clock + inter_arrival, 0, None, None)) else: # 服务完成事件: 释放窗口 next_free[wid] = clock if waiting: _, arrive_t = waiting.popleft() if clock >= warmup_minutes: wait_times.append(clock - arrive_t) svc_time = random.expovariate(service_rate) next_free[wid] = clock + svc_time heapq.heappush(events, (clock + svc_time, 1, None, wid)) served += 1 avg_wait = sum(wait_times) / len(wait_times) if wait_times else 0.0 avg_queue = sum(queue_len_samples) / len(queue_len_samples) if queue_len_samples else 0.0 # 粗略窗口忙碌比例: 仿真结束时仍在忙的窗口占比 busy_ratio = sum(1 for ft in next_free if ft > total_minutes) / num_windows return { "avg_wait_min": avg_wait, "avg_queue_len": avg_queue, "arrivals": customer_id, "served": served, "busy_ratio": busy_ratio, } if __name__ == "__main__": r = bank_queue_sim(arrival_rate=0.9, service_rate=0.4, num_windows=3, total_minutes=480, warmup_minutes=60, seed=42) print(r)这段代码的逻辑说明,按三个数据结构展开:事件堆events决定下一个该处理什么;next_free记录每个窗口的释放时刻,用于判断新客户能否立即上柜;waiting是真正的队列,存的是客户到达时刻。事件堆中事件的排序依靠 Python 元组的自然顺序,也就是先按时间戳排列,时间相同时按类型和客户 id 排列,这保证了调度的确定性。
参数说明写清楚才能让代码被别人复现:arrival_rate和service_rate必须用相同的单位(分钟),total_minutes是仿真总时长,warmup_minutes是系统预热时间,预热期内不采样;seed是随机种子,不固定跑两次结果就完全不一样。我一般在实验报告里把这段代码的参数逐个列成表,并写出推荐范围:warmup_minutes建议取 1/(cμ−λ) 的 3 到 5 倍量级,本例约 10~16 分钟,稳妥起见取 60;total_minutes至少 480 分钟,否则有效样本太少。
3.3 运行结果与忙闲率的精确统计方式
用 λ=0.9、μ=0.4、c=3 运行上面的代码,得到的 avg_wait_min 大约在 1.8 到 2.1 分钟之间,avg_queue_len 在 1.6 到 1.9 之间,和第 2 章理论值 Wq=1.89、Lq=1.70 对得上。注意随机种子不同,结果会有一点点波动,但不会偏离理论值太远。如果仿真出来的平均等待时间超过 2.5 分钟,优先检查预热期是否太短、总时长是否够、随机种子是否固定这三件事。
上面代码里的busy_ratio用的是粗略算法——仿真结束时窗口是否还有未完成工作。这个算法有个缺陷:它只看最后一瞬间,不是整段仿真的真实平均忙闲率。更严谨的做法是事件驱动的时间加权平均:每次弹出事件时,把当前时刻与上一个事件时刻的差值乘以这段时间内忙碌窗口数,累加起来,最后除以有效仿真时长。改进后的代码片段如下:
# 在事件循环中维护两个变量: last_clock 和 busy_window_time last_clock = 0.0 busy_window_time = 0.0 # 每次从事件堆弹出事件后、处理业务前执行: if clock >= warmup_minutes: busy_windows = sum(1 for ft in next_free if ft > clock) busy_window_time += (clock - last_clock) * busy_windows last_clock = clock # 仿真结束时: busy_ratio = busy_window_time / (total_minutes - warmup_minutes) / num_windows这段替换把窗口忙闲率的统计细化到每个时间区间,和理论 ρ 的偏差能控制在 1% 以内。报告里写对忙闲率的定义尤其重要,因为银行运营方很在意人力利用率:忙闲率 75% 意味着窗口有四分之一时间是空的,他们可能会据此裁掉一个窗口——如果你统计口径有误,这个决策就是错的。
4. 现场数据怎么采、怎么拟合成模型参数:一份可抄的流程
4.1 观测方法:一个人、一个表、三个时刻
报告的说服力最终靠数据。没有真实数据,这套模型只是作业题;有真实数据,才叫实验。数据采集我建议盯住三个时刻:客户到达的时刻、开始办业务的时刻、办结离开的时刻。有叫号系统的网点,后台流水里往往已经有取号时刻和叫号时刻,重点补记业务结束时刻就行;没有叫号系统的,用手机录一段大堂视频,回放时逐条记录,成本低还不容易漏人。
观测时段的选择直接影响结论。我建议至少测两段:一段高峰(比如工作日上午 9:30—11:00),一段平峰(下午 14:00—15:30)。每段连续测 60 到 90 分钟,能积累 50 条以上服务记录就够做分布拟合。如果只测 20 分钟就想拟合一个分布,后面 K-S 检验大概率通不过,不是你数据不好,是样本量不支持。采样时还要注意把 ATM 区、理财区和现金柜分开记录,因为这三块的服务时间分布差异很大,混在一起拟合出来的服务率不伦不类。
清洗规则要提前定好:到达后等不及离开的客户记为“弃等”,算入报告结论,但不进服务时间拟合;中途去上厕所被叫号跳过窗口的,按过号重排处理,服务时间只记实际办理段。清洗规则写进报告附录,别人复核数据时才看得懂你的口径。
4.2 用Python做分布检验:指数分布假设能不能成立
原始数据清洗完后,转成两个数组——到达间隔和服务时间。到达间隔是相邻两个客户到达时刻的差值;服务时间是“服务开始时刻减去服务完成时刻”。这两个数组分别用scipy.stats.expon拟合,再做 K-S 检验判断是否服从指数分布。K-S 检验的原假设是“样本服从指定分布”,p 值大于 0.05 就说明没有足够证据拒绝指数分布假设,M/M/c 模型用起来是安全的;p 值小于 0.05 则要小心,后面我会讲对应的处理方案。
代码如下:
import numpy as np from scipy import stats # 假设已经清洗并读入了两个数组, 单位都是分钟 arrival_intervals = np.array([...]) service_times = np.array([...]) # 指数分布的率参数 = 1/均值 lambda_est = 1.0 / arrival_intervals.mean() mu_est = 1.0 / service_times.mean() print(f"到达率 λ = {lambda_est:.3f} 人/分钟") print(f"服务率 μ = {mu_est:.3f} 人/分钟") # K-S 检验 ks1, p1 = stats.kstest(arrival_intervals, "expon", args=(0, 1/lambda_est)) ks2, p2 = stats.kstest(service_times, "expon", args=(0, 1/mu_est)) print(f"到达间隔 K-S={ks1:.4f}, p={p1:.4f}") print(f"服务时长 K-S={ks2:.4f}, p={p2:.4f}")这段代码背后有两个注意点。第一,stats.expon的loc参数固定为 0,因为到达间隔和服务时间为负数没有意义,location 偏移会掩盖真实的分布特征。第二,拟合和检验用的是同一份数据,严格说存在过拟合风险,但实验报告场景下这样做是业界主流做法,因为样本量有限。如果拿到 200 条以上数据,可以按 7:3 拆成拟合集和验证集,更讲究。p 值如果刚刚大于 0.05(比如 0.06),不要急着高兴,那说明样本量撑不起检验功效,多补几组数据再看。
4.3 数据不服从指数分布时怎么办
真实数据经常不给面子。银行的服务时间往往不是纯指数分布——有些简单业务(取号、改密码)特别快,复杂业务(开户、理财产品)又特别慢,混合起来会出现双峰甚至长尾。此时继续硬套 M/M/c 有两个后果:理论公式算出的 Wq 会严重高估或低估真实排队,报告结论自然立不住。常见做法是把到达过程和服务过程的经验分布直接灌进第 3 章的仿真器,把负指数分布的抽样函数替换成经验分布抽样,比如用numpy.random.choice配合观测值集合做 bootstrap 抽样。这样做模型假设更贴近实际,报告的专业性反而提升一个档次。
5. 排队实验的五个常见坑:现象、原因、解法都在这里
5.1 单位不统一,公式算出离谱答案
现象:套 M/M/c 公式时平均等待时间算出 300 分钟,或者 Wq 是负数,怎么检查公式都没错。
原因:λ 用了“人/小时”,μ 用了“人/分钟”,带入 ρ 时直接差了 60 倍,服务强度超过 1,公式全部失真。
解决:写报告前先建一个单位声明表,把 λ、μ、Wq、Lq 的单位全部统一成“分钟”体系。我在实验报告的参数表里会加一行粗体注释:λ 单位是 人/分钟,μ 单位是 人/分钟,服务时间均值 1/μ 单位是 分钟。仿真代码和理论公式都用同一套单位,就不会出现这个低级但致命的错误。
5.2 不检查服务强度 ρ,仿真结果发散了还不知道
现象:仿真跑完,平均等待时间随总仿真时长一直涨,十分钟涨一点,三十分钟又涨一点,不稳定。
原因:λ/(cμ) ≥ 1,客户到达速度大于窗口服务能力,队列无限累积,系统不存在稳态。
解决:任何仿真代码跑之前,先算一次 ρ。ρ 大于 0.85 要警惕,大于 1 直接加窗口或降到达率。实验报告如果想讨论过负荷场景,比如网点突发大客流,那就单开一节写“过负荷状态分析”,说明这是非稳态场景,故意设置为 ρ>1,指标会随时间发散,不能用稳态排队论公式。我在实际分析中会把稳态工作点(ρ=0.75)和过负荷节点(ρ=1.08)分开呈现,这样业务方才能看出什么时候会崩。
5.3 预热期数据混入统计,仿真结果虚低
现象:理论 Wq=1.9 分钟,仿真跑出来 0.6 分钟,而且怎么调参数都偏低。
原因:系统初始是空队列、全空闲窗口,从 0 开始建立起稳态需要一段时间,把冷启动阶段的 0 等待时刻平均了进去,整体等待时间被拉低。
解决:设置warmup_minutes至少为 30 分钟,保险一点取 60 分钟。判断是否进入稳态可以看队列长度的时间序列图——如果曲线在某个水平附近上下波动,说明进出平衡,可以采样了;如果曲线单调向上,说明还没稳或者 ρ 本身就不小于 1。一个快捷经验法则是预热时长取 1/(cμ−λ) 的 3 到 5 倍,本组参数算出来 10 到 16 分钟左右,取 60 分钟绝对够。
5.4 单次仿真当结论,随机波动让数字忽高忽低
现象:同一组参数跑两次,第一次 Wq=1.9,第二次 Wq=4.7,报告里不知道写哪个。
原因:仿真是随机抽样过程,没有固定随机种子时,每次运行都是一条不同的样本路径,单次运行的结果方差很大。要是赶上前半段恰好连续来了十几个客户,等待时间就会被堆得很高。
解决:固定 seed,或者干脆跑多次。实验报告里我会用 20 个不同种子各跑一遍,输出“均值±标准差”:
results = [] for s in range(20): r = bank_queue_sim(0.9, 0.4, 3, total_minutes=480, warmup_minutes=60, seed=s) results.append(r["avg_wait_min"]) mean_wait = sum(results) / len(results) std_wait = (sum((x - mean_wait) ** 2 for x in results) / len(results)) ** 0.5 print(f"平均等待 = {mean_wait:.2f} ± {std_wait:.2f} 分钟")报告正文里只写多次运行均值,把 20 次运行的完整结果放附录,让读者自行复核。单次运行的数字只能出现在草稿里,放进正式报告等于给了评审律师一个挑刺的机会。
5.5 分布拟合通不过,数据质量背锅
现象:K-S 检验 p 值小于 0.001,指数分布假设被明确拒绝,报告写不下去。
原因:数据采集时把“到达时刻”记成了“排队开始时刻”,或者把“服务完成时刻”和“客户离开时刻”混为一谈,导致时序错位。还有一种常见情况是中途丢弃了大量过号数据,样本里只剩很长的服务时间,分布自然不对。
解决:回到原始流水重新清洗,严格定义三个时刻。服务开始时刻用叫号时刻,服务结束时刻用窗口呼叫下一位客户的时刻。如果数据清洗后 p 值还是小于 0.05,就放弃 M/M/c 公式,改用第 4.3 节的经验分布驱动仿真。这不算结论失败——报告里写清楚“服务时间不服从指数分布,采用经验分布仿真”,反而比硬凑公式严谨得多。
6. 实验结果怎么落成一页汇报:从窗口数量到验证口径
实验报告收尾时不写空泛总结,而是直接给结论和验证方法。我常用的做法是把仿真结果画成一条“平均等待时间 vs 窗口数量”的折线。以 λ=0.9、μ=0.4 为例,2 个窗口时系统不稳定,3 个窗口 Wq 约 1.9 分钟,4 个窗口 Wq 掉到 0.3 分钟以内。折线图上标注一条“目标服务水平线”,例如银行内部 SLA 要求“高峰时段平均等待不超过 3 分钟”,那结论就是 3 个窗口满足要求,4 个窗口提升不大但人力成本增加 33%。如果换成 λ=1.1 的高峰预测,3 个窗口 Wq 逼近 10 分钟,4 个窗口才是达标方案。这就把“开几个窗口”的决策从拍脑袋变成了可量化的公式。
验证口径是报告里最能体现专业功底的部分。仿真结果要先和第 2 章 M/M/c 理论值做对照,偏差控制在 5% 到 10% 以内,用以确认代码没有逻辑错误。然后做敏感性验证——把 λ 上下浮动 10%,重新跑仿真,看窗口结论是否翻转。如果 λ 从 0.9 涨到 1.0 后三窗口 Wq 超过 5 分钟,那原结论就要从“三窗口可行”改为“三窗口仅在平峰可行,高峰预留应急窗口”。最后可以做一次简单的随机性检验:用不同 seed 跑 10 次,确认中位数和均值差异不超过 0.2 分钟,说明单次运行不是偶然。
我个人习惯在报告最后写出“局限与边界”这一段,用三到五句话说明:数据只覆盖了工作日高峰,周末和节假日不在模型范围内;模型假设窗口服务不中断,真实网点午休和交接班需要额外建模;弃等客户没有进入服务时间统计,实际客户感知可能比 Wq 更敏感。写这一段不是为了自我批评,而是让业务方知道哪些结论能放心用,哪些需要在更多数据下复核。经过这么多银行网点排队项目的打磨,我的教训是:排队模型的准头七分在数据采集、三分在仿真实现,公式推导反而是最不容易出错的部分。希望这份从理论到仿真再到数据验证的完整路径能帮到你,让你的实验报告不止停在纸面,而是真正成为网点排班决策的支撑。
本文还有配套的精品资源,点击获取