《C++ Primer Plus》12.7 队列模拟 学习笔记

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!
时间: 2024-11-06 05:06:14

《C++ Primer Plus》12.7 队列模拟 学习笔记的相关文章

c++ primer(第五版)学习笔记及习题答案代码版(第十四章)重载运算与类型转换

笔记较为零散,都是自己不熟悉的知识点. 习题答案至于一个.h 和.cc 中,需要演示某一题直接修改 #define NUM****, 如运行14.30题为#define NUM1430: Alice Emma has long flowing red hair. Her Daddy says when the wind blows through her hair, it looks almost alive, like a fiery bird in flight. A beautiful f

c++ primer(第五版)学习笔记及习题答案代码版(第十一章)关联容器

笔记较为零散,都是自己不熟悉的知识点. 习题答案至于一个.cc 中,包含Chapter7.h头文件,读入文件包括./test ./rules .需要演示某一题直接修改 #define NUM****, 如运行11.23题为#define NUM1123: chapter 11 1.  关联容器不支持顺序容器的位置相关的操作,例如push_front或push_back.原因是关联容器中元素是根据关键字存储的,这些操作对 关联容器没有意义.而且关联容器也不支持构造函数或插入操作这些接收一个元素值和

c++ primer(第五版)学习笔记及习题答案代码版(第六章)函数

笔记较为零散,都是自己不熟悉的知识点. 习题答案至于一个.cc 中,编译需要包含Chapter6.h头文件. 需要演示某一题直接修改 #define NUM***, 如运行6.23题为#define NUM623: chapter 6 1. 形参初始化的机理与变量初始化一样. 当形参是引用类型时,它对应的实参被引用传递或者函数被传引用调用. 2. const和实参 void fcn(const int i){ /*fcn能够读取i,但是不能向i写值*/} void fcn(int i){ /*.

[matlab]Monte Carlo模拟学习笔记

理论基础:大数定理,当频数足够多时,概率可以逼近频率,从而依靠频率与$\pi$的关系,求出$\pi$ 所以,rand在Monte Carlo中是必不可少的,必须保证测试数据的随机性. 用蒙特卡洛方法进行计算机模拟的步骤:[1] 设计一个逻辑框图,即模拟模型.[2] 根据流程图编写程序,模拟随机现象.可通过具有各种概率分布的模拟随机数来模拟随机现象.[3] 分析模拟结果,计算所需要结果. ex1.投针试验求$\pi$ %蒲丰投针实验的计算机模拟 format long; %设置15位显示精度 a=

《C++ Primer Plus》16.4 泛型编程 学习笔记

STL是一种泛型编程(generic programming).面向对象编程关注的是编成的数据方面,而泛型编程关注的是算法.它们之间的共同点是抽象和创建可重用代码,单他们的理念决然不同.泛型编程旨在编写独立于数据类型的代码. 16.4.1 为何使用迭代器理解迭代器是理解STL的关键所在.模板使得算法独立于存储的数据类型,而迭代其使算法独立于使用的容器类型.因此,它们都是STL通用方法的重要组成部分.为了解为何需要迭代器,我们来看如何为两种不同数据表现实现find函数,然后来看如何推广这种方法.首

《C++ Primer Plus》第4章 学习笔记

数组.结构和指针是C++的3中符合类型.数组可以在一个数据对象中存储多个同种类型的值.通过使用索引或下标,可以访问数组中各个元素.结构可以将多个不同类型的值存储在同一个数据对象中,可以使用成员关系运算符(.)来访问其中的成员.使用结构的第一步是创建结构模板,它定义结构存储了那些成员.模板的名称将称为新类型的标识符,然后就可以声明这种类型的结构变量.共用体可以存储一个值,但是这个值可以是不同的类型,成员名指出了使用的模式.指针是被设计用来存储地址的变量.我们说,指针指向它存储的地址.指针声明指出了

《C++ Primer Plus》第6章 学习笔记

使用引导程序选择不同操作的语句后,程序和编程将更有趣.C++提供了if 语句 .if else 语 句 和 switch 语句来管理选项.if 语句使程序有条件地执行语句或语句块,也就是说,如果满足特定的条件,程序将执行特定的语句或语句块 .if else 语句程序选择执行两个语句或语句块之一.可以在这条语句后再加上if else, 以提供一系列的选项.switch语句引导程序执行一系列选项之一.C++还提供了帮助决策的运算符.第 5 章讨论了关系表达式, 这种表达式对两个值进行比较.if 和

2016.12.15网络编程学习笔记

---恢复内容开始--- TCP/IP协议族已经帮我们解决了这个问题,网络层的"ip地址"可以唯一标识网络中的主机,而传输层的"协议+端口"可以唯一标识主机中的应用程序(进程).利用三元组(ip地址,协议,端口)就可以标识网络的进程.TCP/IP协议的应用程序通常采用应用编程接口:UNIX  BSD的套接字(socket),来实现网络进程之间的通信,网络中进程通信是无处不在,这就是我为什么说"一切皆socket".socket起源于Unix,而U

《C++ Primer Plus》14.3 多重继承 学习笔记

多重继承(MI)描述的是有多个直接基类的类.与单继承一样,共有MI表示的也是is-a关系.例如,可以从Awiter类和Singer类派生出SingingWaiter类:class SingingWaiter : public Waiter, public Singer {...};MI可能会给程序员带来很多新问题.其中两个主要的问题是:从两个不同的基类继承同名方法:从两个或更多相关基类那里继承同一个类的多个实例.在下面的例子中,我们将定义一个抽象基类Worker,并使用它派生出Waiter类和S