Heather银行打算在Food Heap超市开设一个自动柜员机(ATM)。Food Heap超市的管理者担心排队使用ATM的人流会干扰超市的交通,希望限制排队等待的人数。Heather银行希望对顾客排队等待的事件进行估测。要编写一个程序来模拟这种情况,让超市的管理者可以了解ATM可能招骋的影响。
对于这种问题,最自然的方法是使用顾客对列。队列是一种抽象的数据类型(Abstract Data Type,ADT),可以存储有序的项目序列。新项目被添加在队尾,并可以删除队首的项目。队列有点像栈,单栈在同意段进行添加和删除。这使得栈是一种后进先出(LIFO,last-in,first-out)的结构,而队列是先进先出(FIFO, first-in,first-out)的。从概念上说,队列就好比是收款台或ATM前面排的队,所以对于上述问题,队列非常合适。因此,工程的任务之一是定义一个Queue类。
队列中的项目是顾客。Heather银行的代表介绍:通常,三分之一的顾客只需要一分钟便可获得服务,三分之一的顾客需要两分钟,另外三分之一的顾客需要三分钟。另外,顾客到达的时间是随机的,但每个小时使用自动柜员机的顾客数量相当稳定。工程的另外两项任务是:设计一个表示顾客的类;编写一个程序来模拟顾客和队列之间的交互。
12.7.1 队列类
首先需要设计一个Queue类。这里先列出队列的特征:
* 队列存储有序的项目序列;
* 队列所能容纳的项目数有一定的限制;
* 应当能够创建空队列;
* 应当能构建测队列是否为空;
* 应当能够检查队列是否是满的;
* 应当能够检查队列是否是满的;
* 应当能够在队尾添加项目;
* 应当能够从队首删除项目;
* 应当能够确定队列中项目数。
设计类时,需要开发共有接口和私有实现。
1.Queue类的接口
从队列的调整可知,Queue类的公有接口应该如下:
class Queue
{
enum {Q_SIZE = 10};
private:
// private representation to be developed later
public:
Queue(int qs = Q_SIZE); // create queue with a qs limit
~Queue();
bool isempty() const;
bool isfull() const;
int queuecount() const;
bool enqueue(const Item &item); // add item to end
bool dequeue(Item &item); // remove item from front
};
构造函数创建一个空队列。默认情况下,列最多可存储10个项目,但是可以用显式初始化参数覆盖该默认值:
Queue line1; // queue with 10-item limit
Queue line2(20); // queue with 20-item limit
2.Queue类的实现
确定接口后,便可以实现它。首先,需要确定如何表示队列数据。一种方法是使用new动态分配一个数组,它包佛汉所需的元素数。但是数组比较麻烦。链表能够很好地满足队列的要求。链表由节点序列构成。每一个节点11包含要保存到链表中的信息以及一个指向下一个节点的指针。对于这里的队列来说,数据部分都是一个Item类型的值,因此可以使用下面的结构来表示节点:
struct Node
{
Item item; // data stored in the node
struct Node * next; // pointer to next node
};
类声明的私有部分与下面类似:
class Queue
{
private:
// class scope definitions
// Node is a nested structure definition local to this class
struct Node { Item item; struct Node * next; };
enum {Q_SIZE = 10};
// private class members
Node * front; // pointer to front of Queue
Node * rear; // pointer to rear of Queue
int items; // current number of items in Queue
const int qsize; // maximum number of items in Queue
public:
// ...
};
isempty()、isfull()和queuecount()的代码都非常简单。如果items为0,则队列是空的:如果items等于qsize,则队列是满的。要知道队列中的项目数,只需返回items的值。后面的程序清单12.11列出了这些代码。
将项目添加到队尾(入队)比较麻烦。下面是一种方法:
bool Queue::enqueue(const Item & item)
{
if (isfull())
return false;
Node * add = new Node; // create node
// on failure, new throws std::bad_alloc exception
add->item = item; // set node pointers
add->next = NULL; // or nullptr;
items ++;
if (front = NULL) // if queue is empty,
front = add; // place item at front
else
rear->next = add; // else place at rear
rear = add;
return true;
}
总之,方法需要经过下面几步:
1.如果队列已满,则结束(在这里的实现中,队列的最大长度由用户通过构造函数指定)。
2。创建一个新节点。如果new无法创建新节点,它将引发异常。(这个主题在15章介绍)最终的结果是,除非提供了处理异常的代码,否则程序将终止。
3.在节点中放入正确的值。在这个例子中,代码将Item值复制到节点的数据部分,并将节点的next指针设置为NULL(0或C++11新增的nullptr)。这样就为将节点作为队列中的最后一个项目做好了准备。
4.将项目技术(items)加1。
5.将节点附加到队尾。这包括两个部分。首先,将节点与列表中的另一个节点链接起来。这时通过将当前队尾节点的next指针指向新的队尾节点来完成的。第二部分是将Queue的成员值帧rear指针设置成指向新节点(如果只有一个节点,则它即使对手节点,也是队尾节点)。
删除队首项目(出队)也需要多个步骤才能完成。下面是一种方式:
bool Queue::dequeue(Item & item)
{
if (front == NULL)
return false;
item = front->item; // set item to first item in queue
items --;
Node * temp = front; // save location of first item
front = front->next; // reset front to next item
delete temp; // delete former first item
if (items == 0)
rear == NULL;
return true;
}
总之,需要经过下面几个阶段:
* 1.如果队列为空,则结束。
* 2.将队列的第一个项目提供给调用函数,这时通过将当前front节点中的数据部分复制到专递给方法的引用变量中来实现的。
* 3.将项目及数(items)减1.
* 4.保存front节点的位置,供以后删除。
* 5.让节点出队。这时通过将Queue成员指针front设置成指向下一个节点来完成的,该节点的位置由front->next提供。
* 6.为节省内存,删除以前的第一个节点。
* 7.如果链表为空,则将rear设置为NULL(在这个例子中,将front指针设置成front->next后,它已经是NULL了)。同样,可使用0而不是NULL,也可使用C++11新增的nullptr。
4.是否需要其他类方法
类需要提供一个显式析构函数——该函数删除剩余的所有节点。下面是一种实现,它从链表头开始,一次删除其中的每个节点:
Queue::~Queue()
{
Node * temp;
while (front != NULL) // while queue is not yet empty
{
temp = front; // save address of front item
front = front->next; // reset pointer to next item
delete temp; // delete formet front
}
}
(要克隆或复制队列,必须提供复制构造函数和执行深度复制的复制构造函数。)
12.7.2 Customer类
接下来需要设计客户类。通常,ATM客户有很多属性,例如姓名、账户和账户结余。然而,这里的模拟需要使用的唯一一个属性是客户何时进入队列以及客户交易所需的事件。当模拟生成新客户时,程序将创建一个新的客户对象,并在其中存储客户的到达时间以及一个随机生成的交易师键。当客户到达队首时,程序将记录此事的时间,并将其与进入队列的事件相见,得到客户的等待时间。
下面的代码演示了如何定义和实现Customer类:
class Customer
{
private:
long arrive; // arrival time for customer
int processtime; // processing time for customer
public:
Customer() { arrive = processtime = 0; }
void set(long when);
long when() const { return arrive; }
int ptime() const { return processtime; }
};
void Customer::set(long when)
{
processtime = std::rand() % 3 + 1;
arrive = when;
}
默认构造函数创建一个空客户。set()成员函数将到达时间设置为参数,并将处理时间设置成为1~3之间的一个随机值。
程序清单12.10将Queue和Customer类声明放在了一起,而程序清单12.11列出了方法。
程序清单12.10 queue.h
// queue.h -- interface for a queue #ifndef QUEUE_H_ #define QUEUE_H_ // This queue will contain Customer items class Customer { private: long arrive; // arrival time for customer int processtime; // processing time for customer public: Customer() { arrive = processtime = 0; } void set(long when); long when() const { return arrive; } int ptime() const { return processtime; } }; typedef Customer Item; class Queue { private: // class scope definitions // Node is a nested structure definition local to this c struct Node { Item item; struct Node * next; }; enum {Q_SIZE = 10}; // private class members Node * front; // pointer to front of Queue Node * rear; // pointer to rear of Queue int items; // current number of items in Queue const int qsize; // maximum number of items in Queue // preemptive definitions to prevent public copying Queue(const Queue & q) : qsize(0) {} Queue & operator=(const Queue & q) { return *this; } public: Queue(int qs = Q_SIZE); // create queue with a qs limit ~Queue(); bool isempty() const; bool isfull() const; int queuecount() const; bool enqueue() const; bool enqueue(const Item &item); // add item to end bool dequeue(Item &item); // remove item from front }; #endif // QUEUE_H_
程序清单12.11 queue.cpp
// queue.cpp -- Queue and Customer methods #include "queue.h" #include <cstdlib> // {or stdlib.h} for rand() // Queue methods Queue::Queue(int qs) : qsize(qs) { front = rear = NULL; // or nullptr items = 0; } Queue::~Queue() { Node * temp; while (front != NULL) // while queue is not yet empty { temp = front; // save address of front item front = front->next; // reset pointer to next item delete temp; // delete fromer front } } bool Queue::isempty() const { return items == 0; } bool Queue::isfull() const { return items == qsize; } int Queue::queuecount() const { return items; } // Add item to queue bool Queue::enqueue(const Item & item) { if (isfull()) return false; Node * add = new Node; // create node // on failure, new throws std::bad_alloc exception add->item = item; // set node pointers add->next = NULL; // or nullptr items ++; if (front == NULL) // if queue is empty front = add; // place item at front else rear->next = add; // else place at rear rear = add; // have rear point to new node return true; } // Place front item into item variable and remove from queue bool Queue::dequeue(Item & item) { if (front == NULL) return false; item = front->item; // set item to first item in queue items --; Node * temp = front; // save location of first item front = front->next; // reset front to next item delete temp; // delete former first item if (items == 0) rear = NULL; return true; } // time set to a random value in the range 1 ~ 3 void Customer::set(long when) { processtime = std::rand() % 3 + 1; arrive = when; }
12.7.3 ATM模拟
现在已经拥有模拟ATM所需的工具。程序允许用户输入3个数:队列的最大长度、吃呢供需模拟的持续时间(单位为小时)以及平均每小时的客户数。程序将使用循环——每次循环代表一分钟。在每分钟的循环中,程序将完成下面的工作。
1.判断是否来了新的客户。如果来了,并且此时队列未满,则将它添加到队列中,否则拒绝客户入队。
2.如果没有客户在进行交易,则选取队列的第一个客户。确定1客户的已等候时间,并将wait_time计数器设置为客户的处理时间。
3.如果客户正在处理中,则将wait_time计数器减1。
4.记录各种类数据,如获得服务的客户数目、被拒绝的客户数目、排队等候的累计时间以及累积的队列长度等。
当模拟循环结束时,程序将报告各种统计结果。
一个有趣的问题是,程序如何取诶的那个是否有新的客户到来。假设平均每小时有10名客户到达,选择相当于每6分钟有一名客户。程序将计算这个值,并将它保存在min_per_cust变量中。然而,甘南更好每6分钟来一名客户不太显示,我们真正(至少在大部份时间内)希望的是一个更随机的过程。程序将用函数来确定是否在循环期间有客户到来:
bool newcustomer(double x)
{
return (std::rand() * x / RAND_MAX < 1);
}
程序清单12.12给出了模拟的细节。长时间运行该程序,可以知道长期的平均值;短时间运行该程序类,将只能知道短期的变化。
程序清单12.12 bank.cpp
// bank.cpp -- using the Queue interface // compile with queue.cpp #include <iostream> #include <cstdlib> #include <ctime> #include "queue.h" const int MIN_PER_HR = 60; bool newcustomer(double x); // is there a new customer int main() { using std::cin; using std::cout; using std::endl; using std::ios_base; // setting things up std::srand(std::time(0)); // random initializing of rand() cout << "Case Study: Bank of Heather Automatic Teller\n"; cout << "Enter maximum size of queue:"; int qs; cin >> qs; Queue line(qs); // line queue holds up to qs people cout << "Enter the number of simulation hours:"; int hours; // hours of simulation cin >> hours; // simulation will run 1 cycle per minute long cyclelimit = MIN_PER_HR * hours; // # of cycles cout << "Enter the average number of customers per hour:"; double perhour; // average # of arrival per hour cin >> perhour; double min_per_cust; // average time between arrivals min_per_cust = MIN_PER_HR / perhour; Item temp; // new customer data long turnaways = 0; // turned away by full queue long customers = 0; // joined the queue long served = 0; // served during the simulation long sum_line = 0; // cumulative line length int wait_time = 0; // time until autoteller is free long line_wait = 0; // cumulative time in line // running the simulation for (int cycle = 0; cycle < cyclelimit; cycle ++) { if (newcustomer(min_per_cust)) // have newcomer { if (line.isfull()) turnaways ++; else { customers ++; temp.set(cycle); // cycle = time of arrival line.enqueue(temp); // add newcomer to line } } if (wait_time <= 0 && !line.isempty()) { line.dequeue(temp); // attend next customer wait_time = temp.ptime(); // for wait_time minutes line_wait += cycle - temp.when(); served ++; } if (wait_time > 0) wait_time --; sum_line += line.queuecount(); } // reporting results if (customers > 0) { cout << "customers accepted: " << customers << endl; cout << " customers served: " << served << endl; cout << " turnaways: " << turnaways << endl; cout << "average queue size: "; cout.precision(2); cout.setf(ios_base::fixed, ios_base::floatfield); cout << (double) sum_line / cyclelimit << endl; cout << " average wait time: " << (double) line_wait / served << " minutes\n"; } else cout << "No customers!\n"; cout << "Done!\n"; return 0; } // x = average time, in minutes, between customers // return value is true if customer shows up this minute bool newcustomer(double x) { return (std::rand() * x / RAND_MAX < 1); }
效果:
Case Study: Bank of Heather Automatic Teller Enter maximum size of queue:10 Enter the number of simulation hours:100 Enter the average number of customers per hour:30 customers accepted: 2894 customers served: 2894 turnaways: 84 average queue size: 4.30 average wait time: 8.92 minutes Done!