数据结构(严蔚敏P65)中离散事件模拟,将银行业务作为模拟,假设有4个窗口对外接待客户,从早晨银行开门起不断有客户进入银行,每个窗口在某一时刻只能接待一个客户,因此客户人数众多时需要每个窗口前顺序排队,对于刚进入银行的客户,如果某个窗口业务员正空闲,则可上前办理业务;反之,若4个窗口均有客户所占,他便会排在人数最少的队伍后面。现需要编制一个程序以模拟银行这种业务活动并计算一天中客户在银行逗留的平均时间。其中,客户到达时间的间隔在1~5分钟之间,客户的业务办理时间在1~30分钟之间,所有时间使用int型,表示从开始营业始所经历的分钟数。
先将书中所描述的基于事件驱动的思路用C#实现一下:
事件类型,分别表示新客户到达时,及4个窗口有客户离开时:
enum EventType
{
NewArrival = 0,
Win1Over = 1,
Win2Over = 2,
Win3Over = 3,
Win4Over = 4
}
事件描述实体,用来表示事件发生的时间,同时实现了IComparable<T>接口,利于按事件发生时间进行排序
class DiscreteEvent : IComparable<DiscreteEvent>
{
internal int OccurTime;
internal EventType NType;
public int CompareTo(DiscreteEvent other)
{
return OccurTime.CompareTo(other.OccurTime);
}
}
客户结构体,表示客户到达时间,及业务办理时间
struct Customer
{
internal int ArrivalTime;
internal int Duration;
}
银行开始营业,对相关数据进行初始化
void OpenForDay()
{
totalTime = 0;
customerNum = 0;
eventList = new List<DiscreteEvent>();
winQueue = new Queue<Customer>[4];
for (int i = 0; i < 4; i++)
winQueue[i] = new Queue<Customer>();
//产生第一个客户到达事件
DiscreteEvent e = new DiscreteEvent();
e.NType = EventType.NewArrival;
e.OccurTime = 0;
AddEventList(e);
}
模拟银行处理,循环查询事件列表,根据所取事件,判断事件类型,分发给不同函数进行处理
public void Bank_Simulation(int closeTime)
{
this.closeTime = closeTime;
OpenForDay();
while (eventList.Count != 0)
{
DiscreteEvent en = eventList[0];
eventList.RemoveAt(0);
switch (en.NType)
{
case EventType.NewArrival: CustomerArrived(en); break;
default: CustomerDaparture(en); break;
}
}
Console.WriteLine(customerNum + " " + totalTime + " " + (double)totalTime / customerNum);
}
客户达到事件处理函数
void CustomerArrived(DiscreteEvent en)
{
DiscreteEvent e;
customerNum++;
Customer c = new Customer();//生成当前用户
c.ArrivalTime = en.OccurTime;
c.Duration = Random(1, 30);
int min = MiniQueue();//进最小处理队列
winQueue[min].Enqueue(c);
if (winQueue[min].Count == 1)//如果是当前正被柜台处理的,则可以再生成该用户离开事件
{
e = new DiscreteEvent();
e.NType = (EventType)(min + 1);
e.OccurTime = en.OccurTime + c.Duration;
AddEventList(e);
}
int nextArrived = en.OccurTime + Random(1, 5);
if (nextArrived <= closeTime)//银行未关门,则插入下一用户到达事件
{
e = new DiscreteEvent();
e.NType = EventType.NewArrival;
e.OccurTime = nextArrived;
AddEventList(e);
}
}
客户离开事件处理函数
void CustomerDaparture(DiscreteEvent en)
{
int i = (int)en.NType - 1;//第几个窗口离开
Customer c = winQueue[i].Dequeue();
totalTime += en.OccurTime - c.ArrivalTime;//累计客户逗留时间
if (winQueue[i].Count != 0)//计算当前队列第一个客户离开时间,并插入事件列表
{
c = winQueue[i].Peek();
DiscreteEvent e = new DiscreteEvent();
e.NType = (EventType)(i + 1);
e.OccurTime = en.OccurTime + c.Duration;
AddEventList(e);
}
}
上面的AddEventList()函数的作用是将事件插入事件列表,并按事件发生时间的顺序进行排序,这里使用了BinarySearch
void AddEventList(DiscreteEvent e)
{
int index = eventList.BinarySearch(e);
if (index < 0)
index = ~index;
eventList.Insert(index, e);
}
接下来使用面向对象的方式对该场景进行一下模拟:
客户类,除了负责记录客户的到达时间,业务办理时间,还有等待时间,离开时间,逗留时间等一些生成的属性,同时还有一个静态方法,负责迭代产生新客户
class Customer
{
internal int ArrivalTime;
internal int Duration;
internal int WaitTime;
internal int LeaveTime
{
get { return ArrivalTime + WaitTime + Duration; }
}
internal int AllTimeInBank
{
get { return WaitTime + Duration; }
}
internal static IEnumerable <Customer> GenCustomer(int closeTime)
{
int lastestArrivalTime = 0;
while (lastestArrivalTime <= closeTime)
{
Customer c = new Customer();
c.ArrivalTime = lastestArrivalTime;
c.Duration = Random(1, 30);
lastestArrivalTime += Random(1, 5);
yield return c;
}
}
}
这个类比起上一方案上的结构体有两个变化:一是封装,在上述方案中需靠一个全局的变量totalTime去累计所有客户的等待时间,而现在确保了每一个客户有独立的等待时间,从而让客户类具有了状态性;二是职责,原先产生客户的方法是分散在OpenForDay(),CustomerArrived()两个函数中,职责不明朗,现在利用一个静态方法去迭代产生新客户
银行窗口类,状态性的信息包括,当前正在处理的客户currentCustomer,正在排队等待处理的客户队列queue,已处理完毕的客户列表leavelist,及相关的生成属性,此外还包括一些方法,AddNew(Customer customer)负责处理新客户排队到此窗口,BeginDeal(int time)开始处理客户业务,EndDeal(int time)结束处理当前客户业务,Tick(int time)是表示时间片轮询的响应方法
class Window
{
internal int winNo;
Queue<Customer> queue = new Queue<Customer>();
internal List<Customer> leavelist = new List<Customer>();
Customer currentCustomer;
internal int waittingCount
{
get { return queue.Count + (currentCustomer == null ? 0 : 1); }
}
internal bool AllDone
{
get { return totalCustomerNum == leavelist.Count; }
}
internal void AddNew(Customer customer)
{
queue.Enqueue(customer);
}
internal void Tick(int time)//时间片轮询响应
{
if (currentCustomer != null && time == currentCustomer.LeaveTime)//当前客户处理完了
{
EndDeal(time);
}
if (queue.Count != 0 && currentCustomer == null)//处理新客户
{
BeginDeal(time);
}
}
void BeginDeal(int time)
{
currentCustomer = queue.Dequeue();
currentCustomer.WaitTime = time - currentCustomer.ArrivalTime;
}
void EndDeal(int time)
{
leavelist.Add(currentCustomer);
currentCustomer = null;
}
}
这个类是一个全新的类,表示一个银行窗口所具有的状态及相应职责,它负责响应时间片的轮循,然后根据时间去判断客户是否处理完毕,并根据自身客户排列情况去获取一个客户,将上述方案中分散在CustomerArrived(),CustomerDaparture()的职责进行了一下明确
银行模拟类,它的职责包括,AddCustomerToMinWindow()新进的客户分配到最小排队的窗口前,GenCustomers(int closeTime)委托Customer静态方法去产生客户队列,AllWindowEnd(int time, int closeTime)判断银行是否可以停止营业了,Simulation()及Statistics()就是对业务的模拟及统计
public class Bank
{
int closeTime;
Window[] windows;
Queue<Customer> customerList;
public Bank(int windowCount, int closeTime)
{
windows = new Window[windowCount];//初始化银行窗口
for (int i = 0; i < windows.Length; i++)
{
windows[i] = new Window();
windows[i].winNo = i;
}
this.closeTime = closeTime;
GenCustomers(closeTime);//产生将会到来的客户
}
public void Simulation()
{
for (int time = 0; ; time++)//时间的轮循
{
if (customerList.Count != 0 && time == customerList.Peek().ArrivalTime)//客户到了
{
AddCustomerToMinWindow();
}
for (int i = 0; i < windows.Length; i++)//每个窗口进行时间轮循响应
{
windows[i].Tick(time);
}
if (AllWindowEnd(time, closeTime))
break;
}
Statistics();
}
void GenCustomers(int closeTime)
{
customerList = new Queue<Customer>();
foreach (Customer c in Customer.GenCustomer())
customerList.Enqueue(c);
}
void AddCustomerToMinWindow()
{
int min = 0;
for (int i = 1; i < windows.Length; i++)
{
if (windows[i].waittingCount < windows[min].waittingCount)
min = i;
}
windows[min].AddNew(customerList.Dequeue());
}
bool AllWindowEnd(int time, int closeTime)
{
if (time <= closeTime)
return false;
for (int i = 0; i < windows.Length; i++)
if (!windows[i].AllDone) return false;
return true;
}
void Statistics()
{
int customerNum = 0;
int totalTime = 0;
for (int i = 0; i < windows.Length; i++)
{
customerNum += windows[i].totalCustomerNum;
foreach (Customer c in windows[i].leavelist)
{
totalTime += c.AllTimeInBank;
}
}
Console.WriteLine(customerNum + " " + totalTime + " " + (double)totalTime / customerNum);
}
}
同样这个银行类也是上个方案不存在的,它的许多职责是外观性的,比如GenCustomers(int closeTime),实要委托Customer.GenCustomer()去完成,比如判断是否可以停止营业AllWindowEnd(int time, int closeTime),是要委托窗口实例去判断自己有没有处理完业务windows[i].AllDone
两种方案如采用同样的客户队列,将输出相同的结果。