栈,队列这些数据结构在理解其原理上,比较简单,实现一个简单的队列也不是难事。但当仅仅学习完这些简单的基础之后,关于队列真正在实际的应用,还是很抽象,生疏。对于我等初学者来说,事件驱动编程的设计和思想,一时还是难以完全接受的,下边是我学习过程中的疑问,以及思考。
这是我的学习地址:实验楼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();
}
}
}