棧,隊列這些數據結構在理解其原理上,比較簡單,實現一個簡單的隊列也不是難事。但當僅僅學習完這些簡單的基礎之後,關於隊列真正在實際的應用,還是很抽象,生疏。對於我等初學者來說,事件驅動編程的設計和思想,一時還是難以完全接受的,下邊是我學習過程中的疑問,以及思考。
這是我的學習地址:實驗樓https://zhuanlan.zhihu.com/p/21571038
歡迎朋友們指出錯誤,一起學習,分享,交流!!!
首先,問題情景。
某個銀行從早上八點開始服務並只服務到中午十二點就停止營業。假設當天銀行只提供了 w 個服務窗口進行服務,問:
- 平均每分鐘有多少個顧客抵達銀行?
- 平均每個顧客佔用服務窗口時間是多少?
首先,我們來分析銀行的排隊邏輯。我們去銀行辦理業務,首先會去取號機取號,然後等待相應的窗口呼你的號,也就是說,在你領取你的號之後,你並不知道你排的是哪個窗口。實際上,在銀行的排隊系統中,所有的用戶(VIP除外)都是排在一個隊列上的,這和買火車票,食堂打飯的排隊方式不一樣。只有一個客戶隊列,而窗口服務完畢客戶之後,從客戶隊列中調取客戶到窗口。
到此,我們整個的排隊模型就變成了:
所以我們需要這樣的幾個基礎部件
- 服務窗口類(會被創建 w 個)//抽象窗口
- 顧客隊列類(只會被創建一個)//抽象客戶排的隊
- 顧客結構(包含兩個隨機屬性: 到達時間, 服務時間)//抽象辦理業務的客戶
因爲顧客的結構是連接顧客隊列以及服務窗口之間的信息,所以,我們首先可以設計我們的顧客數據結構,因爲主要的數據操作部分由服務窗口完成,所以我們用簡單的結構體來表述顧客的存儲信息。
如下(customer一直拼寫錯了,湊活着看吧。。。。)
<span style="font-size:18px;">typedef struct costomer{
//顧客的數據結構
//顧客是隊列的數據存儲基礎,所以操作上沒要求,用結構體就ok
int arrive_time;//顧客的隨機到達時間
int duration;//顧客業務的隨機耗費時間
costomer * next;//隊列我們用鏈表實現,所以節點</span>
<span style="font-size:18px;"> // 結構體的默認構造函數???
costomer(int arrive_time = 0,int duration = Random::uniform(RANDOM_PARAMETER)) :arrive_time(arrive_time),
duration(duration) ,next(nullptr){}
//在結構體的構造函數中,實現對duration的隨機數的生成
} Costomer;</span>
關於結構體的構造函數我也是第一次見到,不過真的相見恨晚!!!(大神們見笑啦)所以,有了顧客的數據結構之後,顧客排隊隊列便很容易實現啦!!!
下邊,便開始設計我們的窗口類,
窗口類的數據基礎主要有這兩部分】
1.存儲要處理的用戶信息
2.當前窗口的工作狀態,忙碌?空閒?
相應的在這兩個數據基礎之上,還需要一些相應的類方法
在類定義之前,我們給出窗口狀態的枚舉,這也是我們在編程中很值得學習的一個技巧吧(我直接用的0,1,自愧不如)
//窗口狀態的枚舉
enum Win_Status {
SERVICE,//服務中0
IDLE//空閒1
};
下邊是窗口的類定義,因爲類方法簡單,所以寫成內聯函數的形式
//工作窗口類定義
class ServiceWindows {
private:
Costomer costomer;//存儲處理客戶的信息
Win_Status status;//表示窗口狀態
public:
ServiceWindows()//構造函數
{
status = IDLE;//初始的時候空閒
}
void setBusy()//窗口設置爲繁忙
{
status = SERVICE;
}
void setIdle()//窗口設置爲空閒
{
status = IDLE;
}
inline void serveCustomer(Costomer &customer) {//讀取新客戶業務
costomer = customer;
}
bool IsIdle()
{
if (status == IDLE)
return true;
else
return false;
}
int getArriveTime()
{
return costomer.arrive_time;
}
int getDurationTime()
{
return costomer.duration;
}
};
到此,我們的基本部件就已經準備好了,就好像,我們買回了基本的電腦部件,但能不能真的跑起來,還得需要我們去把這些組件組裝起來。
我也是第一次聽說事件驅動編程這種說法,起初解決這個如何讓系統跑起來的問題的時候,自然而然的想到了利用while()循環,但在實際的操作中,發現挺難,很多東西不好兼顧(肯定有可以實現的大神,虛心求教!!!)然後仔細的研究了這個牛叉叉的事件驅動,貌似window系統便用到了這樣的編程思想。一想,很nice呀,一學多用呀。
那正經的,什麼事事件驅動編程呢?
度娘說:http://baike.baidu.com/view/8835457.htm
因爲官方的話,大家都可以自己百度到,那我來表達我自己的理解吧。
事件驅動編程,我的理解就是,以讀取事件爲開始,並循環的讀取時間列表中的事件,並隨之分析事件的類型,做出相應的響應,直到時間列表爲空,終止程序。
前邊說過,我們實現了程序的幾個基礎的部件,但這些都是靜態的,需要我們在他們之間搭上一些方法。
下邊是我自己對這個程序如何動起來的理解
首先,事件驅動編程,我們需要一個按照事件發生的時間先後順序排序的時間列隊,程序跑起來的過程就是程序不斷讀取這些時間並做出相應的過程。
1、確定響應事件的元素
2、爲指定元素確定需要響應的事件類型
3、爲指定元素的指定事件編寫相應的事件處理程序
4、將事件處理程序綁定到指定元素的指定事件
我們分析下,這個程序中會有幾個事件。
兩個,1.用戶到達(到達時間) 2.用戶離開(離開時間),所以對於這兩種不同的事件,我們需要設計環環相扣的處理方法。
具體如下:
1.在銀行剛啓動的時候,我們將一條用戶到達的默認事件壓入事件隊列。
2.隨後讀取這個事件,並分析這個事件的類型(到此初始化結束)
3.如果是用戶到達事件
1.那麼用戶數目++(因爲問題有需要我們統計這個)
2.隨後,產生下一個用戶到達的隨機事件,並在此基礎上生成下一個用戶到達的事件,按時間順序放入到事件隊列中。(理解這裏存在的事件傳動)
3.然後檢查是否有空的窗口,如果有,就從等待的用戶隊列的對頭調一個用戶到這個窗口。並且隨機生成這個用戶離開的時間,在這個時間的基礎上產生這個用戶離開的時間愛,放入到事件隊列中(這很重要,用戶進入窗口,伴隨着他離開事件的生成。)
``````````````````````````分割線·······················
4.如果事件類型是離開呢?
1.計算用戶的staytime(問題的需要呀)
2.查看如果客戶的等待隊列中還有人,就將客戶調到窗口來!(進窗口了,別忘了生成他的離開事件)
3.如果等待隊列沒人,就把窗口設置爲等待狀態。
說了這麼多,是不是很迷糊呢。。。在上碼子之前總結一下難以理解的地方吧
1.靜態:首先,用戶隊列,窗口類,等等,這些都是基本的部件,是靜態的,但是是基礎。
2.動態:我們引入了事件隊列(不正經的隊列)這麼個玩意,用事件來驅動程序的運行,循環的讀取事件隊列中的事件,直到事件隊列爲空,則over。
3.傳動:傳動也是靠時間來實現的。比如 。1.處理用戶到達事件的時候,會生成下一個用戶到達的隨機時間,並且在這個時間的基礎上,形成下一個用戶的到達事件,並將之加入到事件隊列中,從而實現事件隊列的擴充(因爲這是一個模擬的程序嘛) 2.當有用戶出隊進入窗口的時候,在此之後,就會隨機生成其離開的隨機時間,由此產生這個客戶離開的隨機事件,並將之加入到事件隊列中。
4.終止:關於程序的終止,便是事件隊列的空爲終止。那從3.傳動中看,事件列表會一直得到補充呀,沒錯。所以程序有變量銀行的營業時間,在每次事件入隊的時候,都要判斷,事件的時間是否超出了營業時間,從而停止事件的輸入,實現終止。
ok,差不多啦,該上新鮮的碼子啦,讀碼字應該比讀我的文字爽吧。
大神勿嘲諷呦。
#include<iostream>
#include <cstdlib>
#include <cmath>
#include<deque>
#include<ctime>
#define RANDOM_PARAMETER 100//生成隨機數的區間0-99
using namespace std;
//大大的疑問????把函數放在類裏???
class Random {//隨機數生成類
public:
// [0, 1) 之間的服從均勻分佈的隨機值???
static double uniform(double max = 1) {
return ((double)std::rand() / (RAND_MAX))*max;
}
};
typedef struct costomer{
//顧客的數據結構
//顧客是隊列的數據存儲基礎,所以操作上沒要求,用結構體就ok
int arrive_time;//顧客的隨機到達時間
int duration;//顧客業務的隨機耗費時間
costomer * next;
// 結構體的默認構造函數???
costomer(int arrive_time = 0,int duration = Random::uniform(RANDOM_PARAMETER)) :arrive_time(arrive_time),
duration(duration) ,next(nullptr){}
//在結構體的構造函數中,實現對duration的隨機數的生成
} Costomer;
//窗口狀態的枚舉
enum Win_Status {
SERVICE,//服務中0
IDLE//空閒1
};
//工作窗口類定義
class ServiceWindows {
private:
Costomer costomer;//存儲處理客戶的信息
Win_Status status;//表示窗口狀態
public:
ServiceWindows()//構造函數
{
status = IDLE;//初始的時候空閒
}
void setBusy()//窗口設置爲繁忙
{
status = SERVICE;
}
void setIdle()//窗口設置爲空閒
{
status = IDLE;
}
inline void serveCustomer(Costomer &customer) {//讀取新客戶業務
costomer = customer;
}
bool IsIdle()
{
if (status == IDLE)
return true;
else
return false;
}
int getArriveTime()
{
return costomer.arrive_time;
}
int getDurationTime()
{
return costomer.duration;
}
};
//設計事件表,即,事件的數據結構
struct Event {
int occur_time;//事件發生的時間,用於之後的事件的排序
//描述時間的類型,-1表示到達,》=0表示離開,並且表示相應的窗口編號
int EventType;
Event * next;
//所以,又是結構體的構造函數?
Event(int time = Random::uniform(RANDOM_PARAMETER) ,int type = -1):occur_time(time),EventType(type)
,next(nullptr) {}
};
//可插入隊列的的實現
template<class T>
class Queue {
private:
T * front;
T * rear;//頭指針and尾指針
public:
Queue();//構造函數,帶有頭節點的
~Queue();//析構函數
void clearQueue();//清空隊列
T* enqueue(T & join);//入隊
T * dequeue();//出隊
T * orderEnqueue(Event& event);//只適用於事件入隊
int length();//獲得隊列長度
};
//系統隊列的設計
class QueueSystem {
private:
int total_service_time;//總的服務時間
int total_costomer;//總的服務顧客總數
int total_stay_time;//總的等待時間
int windows_number;//窗口數目
int avg_stay_time;//平均時間
int avg_costomers;//平均顧客數目
ServiceWindows* windows;//創建服務窗口數組的指針
Queue<Costomer> customer_list;//客戶排隊等待的隊列
Queue<Event> event_list;//時間隊列????
Event* current_event;//事件指針
double run();// 讓隊列系統運行一次
void init();// 初始化各種參數
void end();// 清空各種參數
int getIdleServiceWindow();// 獲得空閒窗口索引
void customerArrived();// 處理顧客到達事件
void customerDeparture();// 處理顧客離開事件
public:
// 初始化隊列系統,構造函數
QueueSystem(int total_service_time, int window_num);
// 銷燬,析構函數
~QueueSystem();
// 啓動模擬,
void simulate(int simulate_num);
inline double getAvgStayTime() {
return avg_stay_time;
}
inline double getAvgCostomers() {
return avg_costomers;
}
};
int main()
{
srand((unsigned)std::time(0)); // 使用當前時間作爲隨機數種子
int total_service_time = 240; // 按分鐘計算
int window_num = 4;
int simulate_num = 100000; // 模擬次數????這是幹嘛用的???
QueueSystem system(total_service_time, window_num);//構建這個系統,初始化
system.simulate(simulate_num);//開啓模擬???這又是神馬意思
cout << "The average time of customer stay in bank: "
<< system.getAvgStayTime() << endl;
cout << "The number of customer arrive bank per minute: "
<< system.getAvgCostomers() << endl;
getchar();
return 0;
}
template<class T>
Queue<T>::Queue()
{
front = new T;//有一個頭節點的鏈表
if (!front)
exit(1);//內存分配失敗,終止程序
rear = front;
front->next = nullptr;//頭節點
}
template<class T>
Queue<T>::~Queue()//析構函數,清空鏈表,釋放頭節點
{
clearQueue();
delete front;//釋放頭節點內存
}
template<class T>
void Queue<T>::clearQueue()
{
T *temp_node;
//清空鏈表的時候用頭節點往前邊推進,知道最後的NULL,這個方法比較巧妙
while (front->next) {
temp_node = front->next;
front->next = temp_node->next;
delete temp_node;
}
this->front->next = NULL;
this->rear = this->front;
}
template<class T>
T * Queue<T>::enqueue(T & join)
{//從隊尾加入
T * new_node= new T;
if (!new_node)
exit(1);
*new_node = join;
new_node->next = nullptr;
rear->next = new_node;
rear = rear->next;
return front;//返回頭指針,
}
template<class T>
T * Queue<T>::dequeue()//注意,這裏實現的不是刪除節點,而是將節點從鏈表拆除,拿走使用
{
if (!front->next)//空,全面的錯誤檢查
return nullptr;
T * temp = front->next;
front->next = temp->next;//將首節點拆除,以便於後來帶走
if (!front->next)//錯誤預警,判斷是不是拿走的是不是最後一個元素
rear = front;
return temp;//返回出隊的元素指針,在這裏不釋放。
}
template<class T>
int Queue<T>::length()
{
T *temp_node;
temp_node = this->front->next;
int length = 0;
while (temp_node) {
temp_node = temp_node->next;
++length;
}
return length;
}
template<class T>
T * Queue<T>::orderEnqueue(Event & event)//對於事件列表,要按照時間的順序插入
{
Event* temp = new Event;
if (!temp) {
exit(-1);
}
*temp = event;//賦值
// 如果這個列表裏沒有事件, 則把 temp 事件插入
if (!front->next) {
enqueue(*temp);
delete temp;
return front;
}
// 按時間順序插入
Event *temp_event_list = front;
// 如果有下一個事件,且下一個事件的發生時間小於要插入的時間的時間,則繼續將指針後移
while ( temp_event_list->next && temp_event_list->next->occur_time < event.occur_time) {
temp_event_list = temp_event_list->next;
}//最終得到的temp_event_list的下一個是時間大於新輸入event的,所以應該插入在temp_event_list之後
// 將事件插入到隊列中
temp->next = temp_event_list->next;
temp_event_list->next = temp;
// 返回隊列頭指針
return front;
}
/*
我們來看入隊方法和出隊方法中兩個很關鍵的設計:
入隊時儘管引用了外部的數據,但是並沒有直接使用這個數據,反而是在內部新分配了一塊內存,再將外部數據複製了一份。
出隊時,直接將分配的節點的指針返回了出去,而不是拷貝一份再返回。
在內存管理中,本項目的代碼使用這樣一個理念:誰申請,誰釋放。
隊列這個對象,應該管理的是自身內部使用的內存,釋放在這個隊列生命週期結束後,依然沒有釋放的內存。
*/
QueueSystem::QueueSystem(int total_service_time, int window_num):
total_service_time(total_service_time),
windows_number(window_num),
total_stay_time(0),
total_costomer(0)
{//構造函數
windows = new ServiceWindows[windows_number];//創建 num 個工作窗口
}
QueueSystem::~QueueSystem()
{
delete [] windows ;//釋放窗口內存
}
void QueueSystem::simulate(int simulate_num)//這個地方一直沒搞懂,模擬?
{
double sum = 0;//累計模擬次數????
//這個循環可以說是這個系統跑起來運行的發動機吧
for (int i = 0; i != simulate_num; ++i) {
// 每一遍運行,我們都要增加在這一次模擬中,顧客逗留了多久
sum += run();
}
/*模擬結束,進行計算,類似覆盤*/
// 計算平均逗留時間
avg_stay_time = (double)sum / simulate_num;
// 計算每分鐘平均顧客數
avg_costomers = (double)total_costomer / (total_service_time*simulate_num);
}
// 系統開啓運行前, 初始化事件鏈表,第一個時間一定是到達事件,所以採用默認構造就ok
void QueueSystem::init() {
Event *event = new Event;//創建一個默認的事件,到達。
current_event = event;//並且是當前事件
}
// 系統開始運行,不斷消耗事件表,當消耗完成時結束運行
double QueueSystem::run() {
init();//在這裏初始化????
while (current_event) {
// 判斷當前事件類型
if (current_event->EventType == -1) {
customerArrived();//事件類型爲-1,處理客戶到達事件
}
else {
customerDeparture();//處理客戶離開事件
}
delete current_event;//處理完畢,釋放當前的事件
// 從事件表中讀取新的事件
current_event = event_list.dequeue();//出隊列,
};
end();//結束
// 返回顧客的平均逗留時間
return (double)total_stay_time / total_costomer;
}
// 系統運行結束,將所有服務窗口置空閒。並清空用戶的等待隊列和事件列表????
void QueueSystem::end() {
// 設置所有窗口空閒
for (int i = 0; i != windows_number; ++i) {
windows[i].setIdle();
}
// 顧客隊列清空
customer_list.clearQueue();
// 事件列表清空
event_list.clearQueue();
}
// 處理用戶到達事件
void QueueSystem::customerArrived() {
total_costomer++;//用戶數目++
// 生成下一個顧客的到達事件
int intertime = Random::uniform(100); // 下一個顧客到達的時間間隔,我們假設100分鐘內一定會出現一個顧客
// 下一個顧客的到達時間 = 當前時間的發生時間 + 下一個顧客到達的時間間隔
int time = current_event->occur_time + intertime;
Event temp_event(time);//結構體構造函數,參數爲到達時間,然後業務時間在構造函數中生成
// 如果下一個顧客的到達時間小於服務的總時間,就把這個事件插入到事件列表中
if (time < total_service_time) {
event_list.orderEnqueue(temp_event);
} // 否則不列入事件表,且不加入 cusomer_list
// 同時將這個顧客加入到 customer_list 進行排隊
// 處理當前事件中到達的顧客
Costomer *customer = new Costomer(current_event->occur_time);
if (!customer) {
exit(-1);
}
customer_list.enqueue(*customer);//將的用戶加入列表
// 如果當前窗口有空閒窗口,那麼直接將隊首顧客送入服務窗口
int idleIndex = getIdleServiceWindow();
if (idleIndex >= 0) {
customer = customer_list.dequeue();//客戶指針
windows[idleIndex].serveCustomer(*customer);//將客戶信息傳遞給空閒的窗口處理
windows[idleIndex].setBusy();//窗口設置爲忙碌
// 顧客到窗口開始服務時,就需要插入這個顧客的一個離開事件到 event_list 中
// 離開事件的發生時間 = 當前時間事件的發生時間 + 服務時間
Event temp_event(current_event->occur_time + customer->duration, idleIndex);
event_list.orderEnqueue(temp_event);//將離開的事件按照時間的先後插入事件鏈表
}
delete customer;//釋放已經傳遞到窗口的客戶信息
}
//獲取空閒窗口的序號
int QueueSystem::getIdleServiceWindow() {
for (int i = 0; i != windows_number; ++i) {//遍歷查找
if (windows[i].IsIdle()) {
return i;
}
}
return -1;
}
// 處理用戶離開事件
void QueueSystem::customerDeparture() {
// 如果離開事件的發生時間比總服務時間大,我們就不需要做任何處理
if (current_event->occur_time < total_service_time) {
// 顧客總的逗留時間 = 當前顧客離開時間 - 顧客的到達時間
total_stay_time += current_event->occur_time - windows[current_event->EventType].getArriveTime();
// 如果隊列中有人等待,則立即服務等待的顧客
//把窗口交給排隊中的新的客戶
if (customer_list.length()) {
Costomer *customer;
customer = customer_list.dequeue();
windows[current_event->EventType].serveCustomer(*customer);
// 因爲有新的客戶進入櫃檯,所以要爲這個新的客戶編寫離開事件事件,並送到事件列表中
Event temp_event(
current_event->occur_time + customer->duration,
current_event->EventType
);
event_list.orderEnqueue(temp_event);
delete customer;
}
else {
// 如果隊列沒有人,且當前窗口的顧客離開了,則這個窗口是空閒的
windows[current_event->EventType].setIdle();
}
}
}