數(shù)據(jù)結構期末作業(yè):從隊列到仿真的完整實現(xiàn)指南)
簡介這份資源是面向計算機專業(yè)學生與數(shù)據(jù)結構初學者的課程作業(yè)參考包圍繞「銀行排隊系統(tǒng)」這一經典場景重點演示隊列FIFO結構及VIP與普通用戶雙隊列的優(yōu)先級調度設計。包內共9個文件以cpp源碼、txt數(shù)據(jù)文件、cbp工程配置及exe可執(zhí)行程序為主另含編譯中間產物與項目布局文件壓縮包約1.03MB可直接用Code::Blocks打開運行。源碼中涵蓋入隊出隊、多隊列優(yōu)先級判斷、文件讀取用戶信息與循環(huán)模擬服務流程等核心邏輯適合對照理解STL queue容器的實際用法并作為期末作業(yè)的排錯與結構參考。目前已有3367人學習下載便于讀者快速搭建可運行示例、驗證優(yōu)先級處理思路并查漏補缺。1. 銀行排隊系統(tǒng)一份數(shù)據(jù)結構期末作業(yè)的完整落地思路銀行排隊系統(tǒng)這個題目幾乎每年都會出現(xiàn)在數(shù)據(jù)結構期末作業(yè)的選題清單里。表面上看它只是隊列的增刪改查但真正動手做過的同學都清楚難點根本不在隊列本身而在于怎么把隨機到達的客戶、多個服務窗口、等待時間統(tǒng)計這幾件事組織成一個能跑、能出數(shù)據(jù)、能講清楚的結構。我見過太多作業(yè)最后只交了一個Queue類加幾個cout答辯時被問一句平均等待時間怎么算的就卡住了。這份方案要解決的就是這個問題把銀行排隊系統(tǒng)當成一個完整的小型仿真系統(tǒng)來設計從需求拆解、數(shù)據(jù)結構選型到模塊實現(xiàn)、指標統(tǒng)計再到最后的驗收答辯給出一條能直接照著走的路徑。適合正在趕數(shù)據(jù)結構期末作業(yè)、課程設計的學生也適合想拿一個經典案例練手隊列與仿真的開發(fā)者。2. 需求拆解與數(shù)據(jù)結構選型為什么不能只用一個隊列2.1 先把業(yè)務場景翻譯成可計算的需求銀行排隊系統(tǒng)的原始描述通常很模糊模擬客戶排隊辦理業(yè)務。要把它變成能寫代碼的需求我一般會先問四個問題客戶怎么來、窗口怎么服務、排隊規(guī)則是什么、最后要輸出什么。這四個問題對應四組需求??蛻舻竭_是隨機的通常假設單位時間內到達人數(shù)服從泊松分布或者簡化為每個時間片以概率 p 到達一個客戶。服務時間也是隨機的常見做法是給每個客戶隨機生成一個業(yè)務辦理時長或者按業(yè)務類型存取款、開戶、轉賬給不同的時長區(qū)間。排隊規(guī)則默認是先到先服務也就是標準隊列語義但如果要做進階版可以加入 VIP 客戶優(yōu)先這就變成了優(yōu)先隊列。輸出指標一般包括總客戶數(shù)、平均等待時間、最大等待時間、各窗口服務人數(shù)、客戶平均逗留時間。把這四組需求寫清楚作業(yè)的評分點基本就浮出來了。導師看的不只是你有沒有用隊列而是你有沒有把隨機性和統(tǒng)計這兩件事做對。很多作業(yè)翻車就翻在只做了確定性的入隊出隊沒有任何隨機模擬也沒有任何統(tǒng)計輸出看起來像隊列的單元測試而不是一個系統(tǒng)。2.2 核心結構隊列為主輔助結構補齊主結構毫無疑問是隊列??蛻舻竭_時入隊窗口空閑時從隊頭取人這是最基礎的 FIFO 語義。但只有隊列是不夠的實際實現(xiàn)里通常還要配幾個輔助結構。窗口狀態(tài)需要一個數(shù)組或列表來維護每個窗口記錄當前是否忙碌和當前客戶還需服務多久。事件處理需要一個時間驅動的循環(huán)每個時間片推進一次檢查有沒有新客戶到達、有沒有窗口完成服務。統(tǒng)計部分需要累加器記錄每個客戶的到達時刻和開始服務時刻兩者相減就是等待時間。如果做 VIP 版本主結構就要從普通隊列換成優(yōu)先隊列通常用堆實現(xiàn)優(yōu)先級按客戶等級排序同級再按到達時間排序。這里有個容易忽略的點優(yōu)先隊列的穩(wěn)定很重要如果兩個 VIP 客戶等級相同必須先服務先到的那個否則統(tǒng)計結果會失真。標準堆不保證穩(wěn)定性通常的做法是在優(yōu)先級里加入到達時間作為次級鍵。下面是一個簡化的結構定義用 Python 寫邏輯清晰換成 C 或 Java 也容易遷移from dataclasses import dataclass, field from collections import deque import random dataclass class Customer: cid: int # 客戶編號 arrive_time: int # 到達時刻 service_duration: int # 需要服務時長 start_time: int -1 # 開始服務時刻-1 表示尚未開始 level: int 0 # 0 普通1 VIP dataclass class Window: wid: int busy_until: int 0 # 當前客戶服務結束時刻 served_count: int 0 # 已服務人數(shù)這段代碼的關鍵在于Customer同時攜帶了到達時刻和服務時長Window用busy_until表示占用狀態(tài)而不是布爾值。用結束時刻代替是否忙碌的好處是時間推進時只需要比較當前時刻和busy_until不用額外維護倒計時邏輯更干凈。start_time初始化為 -1 是為了區(qū)分還沒開始服務和在時刻 0 開始服務統(tǒng)計等待時間時不會算錯。2.3 時間驅動還是事件驅動這是選型里最容易被低估的一個決策。時間驅動是每個時間片推進一格檢查所有窗口和到達事件事件驅動是維護一個事件優(yōu)先隊列每次直接跳到下一個事件發(fā)生的時刻。兩種都能做對但適用場景不同。時間驅動的優(yōu)點是直觀、好調試每個時間片的狀態(tài)都能打印出來適合作業(yè)演示和答辯講解。缺點是如果服務時長跨度很大會有大量空轉時間片。事件驅動效率高但實現(xiàn)復雜度上一個臺階需要維護到達事件和離開事件兩類事件還要處理同一時刻多個事件的順序。我的建議是期末作業(yè)默認用時間驅動把時間片設成 1 分鐘或 1 個時間單位客戶到達概率和服務時長都按這個粒度生成。這樣代碼量可控答辯時也容易畫時間軸解釋。如果導師明確要求性能或者要做大規(guī)模仿真再上事件驅動。選型不是越復雜越好而是要和你的演示能力匹配。3. 總體架構與模塊劃分四個模塊撐起一個可演示的系統(tǒng)3.1 分層結構輸入層、仿真層、統(tǒng)計層、輸出層一個能拿得出手的銀行排隊系統(tǒng)代碼不應該全堆在一個main里。我通常按四層來切輸入層負責讀取參數(shù)窗口數(shù)、仿真總時長、到達概率、服務時長范圍仿真層負責時間推進和隊列調度統(tǒng)計層負責收集指標輸出層負責打印結果和生成報告。這樣切的好處是每一層都能單獨測試。輸入層可以用固定參數(shù)跑仿真層可以注入確定性的隨機種子復現(xiàn)結果統(tǒng)計層可以喂假數(shù)據(jù)驗證公式輸出層可以單獨調格式。答辯時如果被問到某個指標怎么來的你能直接指到統(tǒng)計層的某個函數(shù)而不是在一大坨代碼里翻。模塊之間的數(shù)據(jù)流是單向的輸入層產出配置對象仿真層消費配置并產出客戶記錄列表統(tǒng)計層消費客戶記錄算出指標輸出層消費指標和記錄生成展示。單向數(shù)據(jù)流意味著任何一層出問題都不會污染其他層排查時可以先看客戶記錄對不對再看統(tǒng)計公式對不對最后看輸出格式。3.2 仿真主循環(huán)的設計要點仿真層的核心是一個按時間推進的主循環(huán)。每個時間片做三件事處理新到達、處理窗口完成、推進時間。順序很重要如果先推進時間再處理到達會導致客戶到達時刻偏移如果先處理窗口完成再處理到達同一時刻的空閑窗口可能錯過剛到達的客戶。正確的順序通常是先檢查窗口完成釋放窗口再處理新到達入隊然后嘗試把隊頭客戶分配給空閑窗口最后時間加一。這個順序保證了同一時刻釋放的窗口能立刻服務同一時刻到達的客戶符合現(xiàn)實直覺。def simulate(config, seed42): random.seed(seed) # 固定種子保證結果可復現(xiàn) queue deque() windows [Window(i) for i in range(config.window_count)] customers [] cid 0 for t in range(config.total_time): # 1. 釋放已完成服務的窗口 for w in windows: if w.busy_until t: w.busy_until 0 # 2. 新客戶到達 if random.random() config.arrive_prob: c Customer(cid, t, random.randint(*config.service_range)) cid 1 queue.append(c) customers.append(c) # 3. 空閑窗口取隊頭 for w in windows: if w.busy_until 0 and queue: c queue.popleft() c.start_time t w.busy_until t c.service_duration w.served_count 1 return customers, windows這段主循環(huán)里busy_until t的判斷讓窗口在服務結束的那個時間片就能被釋放而不是等到下一個時間片。random.seed(seed)是血淚經驗沒有固定種子每次跑出來的平均等待時間都不一樣答辯時沒法復現(xiàn)導師會懷疑你的統(tǒng)計有問題。固定種子后同一組參數(shù)永遠得到同一組結果調試和演示都穩(wěn)。參數(shù)說明arrive_prob是每個時間片的到達概率取值 0 到 1通常設 0.3 到 0.5 比較接近真實銀行網(wǎng)點的忙碌程度service_range是服務時長的閉區(qū)間比如 (2, 8) 表示每個客戶需要 2 到 8 個時間單位total_time是仿真總時長設 480 可以模擬一個 8 小時工作日的分鐘級仿真。3.3 統(tǒng)計模塊指標公式要經得起追問統(tǒng)計模塊是整個作業(yè)的黑匣子也是最容易被追問的地方。核心指標有四個公式必須寫清楚。平均等待時間等于所有客戶的start_time - arrive_time之和除以客戶總數(shù)。注意只統(tǒng)計真正被服務過的客戶如果仿真結束時隊列里還有沒被服務的客戶要么排除要么單獨標注不能混進平均值。最大等待時間是所有等待時間取最大值用來反映最壞體驗。平均逗留時間等于start_time - arrive_time service_duration的平均值也就是客戶從進店到離開的總時長。窗口利用率等于窗口忙碌時間片總數(shù)除以窗口數(shù)乘以總時長。這里有個常見錯誤把start_time為 -1 的客戶也算進平均等待時間結果出現(xiàn)負數(shù)或者異常大的值。正確的做法是先過濾start_time 0的客戶再統(tǒng)計。另一個錯誤是窗口利用率的分母算錯應該是窗口數(shù) × 仿真時長不是客戶數(shù) × 平均服務時長后者在客戶沒排滿時會高估利用率。def compute_stats(customers, windows, total_time): served [c for c in customers if c.start_time 0] waits [c.start_time - c.arrive_time for c in served] stays [c.start_time - c.arrive_time c.service_duration for c in served] busy_slots sum(w.served_count * 0 for w in windows) # 占位實際按記錄累加 return { total_customers: len(customers), served_customers: len(served), avg_wait: sum(waits) / len(waits) if waits else 0, max_wait: max(waits) if waits else 0, avg_stay: sum(stays) / len(stays) if stays else 0, unserved: len(customers) - len(served), }這段統(tǒng)計代碼里served的過濾是關鍵unserved單獨輸出是為了讓報告完整——如果仿真結束時還有大量客戶沒被服務說明窗口數(shù)或仿真時長設置不合理這個數(shù)字本身就是結論的一部分。busy_slots那行是占位實際實現(xiàn)里應該在仿真循環(huán)中累加每個窗口的忙碌時間片而不是事后用served_count反推因為不同客戶的服務時長不同反推會失真。4. 關鍵實現(xiàn)細節(jié)從能跑到跑得對4.1 隨機數(shù)生成的可復現(xiàn)性前面提過固定種子這里展開說。Python 的random模塊是全局狀態(tài)如果仿真函數(shù)里調用了其他也使用random的代碼種子會被干擾。穩(wěn)妥的做法是給仿真單獨創(chuàng)建一個random.Random(seed)實例所有隨機調用都走這個實例不碰全局狀態(tài)。rng random.Random(seed) if rng.random() config.arrive_prob: duration rng.randint(*config.service_range)這樣即使外部代碼也在用隨機數(shù)你的仿真結果依然可復現(xiàn)。C 里對應的是std::mt19937配std::uniform_real_distributionJava 里是new Random(seed)思路一致。這個細節(jié)在答辯時如果主動提出來是很加分的——說明你考慮過實驗的可重復性而不是碰運氣跑出一個好看的數(shù)字。4.2 邊界情況空隊列、零窗口、仿真結束殘留空隊列是常態(tài)窗口空閑但隊列為空時不能popleft否則拋異常。零窗口是非法配置輸入層要校驗窗口數(shù)至少為 1。仿真結束殘留是容易被忽略的循環(huán)結束后隊列里可能還有客戶這些客戶的等待時間無法計算但數(shù)量要報告出來。還有一種邊界是同一時間片多個客戶到達。如果到達概率模型是每個時間片最多一個客戶就不會出現(xiàn)但如果改成泊松分布一個時間片可能到達多個。這時入隊順序要按到達順序不能亂。我的做法是到達事件生成一個列表按順序append到隊列保證 FIFO 語義不被破壞。4.3 輸出格式讓結果一眼能看懂輸出層不要只打印一堆數(shù)字。我一般會輸出三塊配置摘要窗口數(shù)、仿真時長、到達概率、服務時長范圍、核心指標表總客戶、已服務、未服務、平均等待、最大等待、平均逗留、各窗口服務人數(shù)。如果有余力再加一個按時間段的等待時間分布比如每 60 個時間單位統(tǒng)計一次平均等待能看出高峰期和低谷期的差異。表格用 Markdown 或純文本對齊都行關鍵是列名清晰、單位明確。答辯時導師掃一眼就能抓到重點比讓他自己從日志里找數(shù)字強得多。5. 避坑與常見問題五條最容易翻車的地方5.1 現(xiàn)象平均等待時間出現(xiàn)負數(shù)或異常大值原因統(tǒng)計時沒有過濾start_time -1的未服務客戶-1 - arrive_time產生負數(shù)或者某個客戶的start_time被錯誤地設成了 0 而實際到達時刻很大。解決統(tǒng)計前強制過濾start_time 0并在仿真循環(huán)里確保start_time只在真正分配窗口時賦值不要在客戶創(chuàng)建時給默認值 0。5.2 現(xiàn)象窗口利用率超過 100%原因分母算錯用了客戶數(shù) × 平均服務時長而不是窗口數(shù) × 仿真時長或者忙碌時間片重復累加同一個時間片被多個窗口記了多次。解決在仿真循環(huán)里按窗口逐個累加忙碌時間片每個時間片每個窗口最多記一次分母固定為window_count * total_time。5.3 現(xiàn)象每次運行結果都不一樣無法復現(xiàn)原因使用了全局隨機狀態(tài)或者沒有固定種子或者種子被其他模塊干擾。解決仿真函數(shù)內部創(chuàng)建獨立的Random(seed)實例種子作為參數(shù)傳入并記錄在輸出里保證任何人用同樣的種子和參數(shù)都能得到同樣的結果。5.4 現(xiàn)象VIP 客戶和普通客戶順序錯亂原因優(yōu)先隊列的優(yōu)先級只按等級排序同級客戶沒有按到達時間排序導致后到的同級 VIP 插隊。解決優(yōu)先級設計成元組(-level, arrive_time)等級高的在前同級按到達時間早的在前堆比較時自然保證穩(wěn)定性。5.5 現(xiàn)象仿真結束時大量客戶未服務指標失真原因窗口數(shù)太少或仿真時長太短或者到達概率設得過高系統(tǒng)長期過載。解決這不是代碼 bug是參數(shù)問題。在輸出里明確報告未服務人數(shù)并在報告里討論參數(shù)敏感性——比如把窗口數(shù)從 2 加到 4觀察平均等待時間的變化這本身就是一份有分析深度的作業(yè)。6. 驗收與答辯怎么把這份作業(yè)講成一個方案驗收階段導師通常不會逐行看代碼而是問三個層次的問題結構對不對、指標怎么算、結論有沒有依據(jù)。對應地答辯準備也分三層。第一層是結構演示。準備一張模塊關系圖手畫或文檔里畫都行說清楚輸入、仿真、統(tǒng)計、輸出四層各自負責什么數(shù)據(jù)怎么流動。被問到為什么用隊列不用棧時答 FIFO 符合先到先服務語義棧是后進先出會讓先來的客戶永遠排不上這是業(yè)務語義決定的不是隨便選的。第二層是指標追問。把四個核心指標的公式寫在紙上能當場推導。特別準備好回答未服務客戶怎么處理和窗口利用率分母是什么這兩個高頻問題。如果做了 VIP 版本準備好解釋優(yōu)先級的穩(wěn)定性設計。第三層是結論分析。這是拉開差距的地方。不要只報一個平均等待時間要給出參數(shù)對比。比如固定其他參數(shù)把窗口數(shù)從 2 增加到 3、4記錄平均等待時間的變化做成一張小表窗口數(shù)平均等待最大等待未服務人數(shù)218.6471236.221041.890這張表能直接支撐一個結論窗口數(shù)從 2 加到 3 收益最大從 3 加到 4 邊際收益遞減。這種分析不需要復雜代碼只需要把仿真跑幾遍、結果填進表里但它讓作業(yè)從實現(xiàn)了隊列變成用隊列回答了一個問題。答辯時還有一個技巧主動說出你的假設和局限。比如本仿真假設服務時長均勻分布實際銀行可能更接近指數(shù)分布假設客戶不會中途離開實際可能存在放棄排隊的情況。主動暴露局限比被問出來強說明你知道邊界在哪。我自己的習慣是每次交這類仿真作業(yè)前都會用同一組參數(shù)連跑三次確認結果完全一致再換兩組參數(shù)各跑一次確認趨勢合理。這個三次復現(xiàn)加兩組對比的流程幫我擋掉過好幾次隨機種子沒固定、統(tǒng)計公式寫反的低級錯誤。從那以后我每次做仿真類作業(yè)都強制走一遍這個檢查希望幫到你。本文還有配套的精品資源點擊獲取