类应用——队列模拟

背景:Heather银行打算在Food Heap超市开设一个ATM。Food Heap超市的管理者担心排队等待使用ATM的人流会干扰超市的交通,希望限制排队等待的人数。Heather银行希望对顾客排队等待时间进行估测。

任务:自行设计一个队列的类和顾客的类,顾客到达的时间随机,并且编写一个程序来模拟顾客和队列的交互。

思路:首先设计队列类,队列是一个尾进头出的结构,若采用数组来表示队列,删除队列头部元素,需要将整体的元素往前移动一格,操作较为繁琐,使用链表的数据结构来表示队列,删除队列首元素时,只需针对单个元素,增加队列元素时,只需要改变地址。链表与数组相比,链表查找元素较为麻烦,增减元素较为方便。

队列类的设计
1、首先设计类的公有接口

  • 队列所能容纳的项目数应该要有限制,因此应该在类中设计一个const属性来存储队列所能存储的最大数目。
  • 队列能够创建空队列,所以在队列类的构造函数中应当设置初始化属性的参数
  • 能够检查队列是否为满,所以应该设置一个bool函数,还想到要有一个类中现有的items的属性,通过这个items的大小与最大数目来进行比较从而判断队列是否为满
  • 检查队列是否为空,与队列是否为满相类似。
  • 需要一个函数成员能够返回当前队列中的数目
  • (重点)需要一个插入队列的成员函数和删除队列成员的成员函数,涉及到链表的删除和添加,因此在类中应该各设置一个指向头部和指向尾部的指针。
class Quene
{
public:
    Quene(int qs);
    ~Quene();
    bool isempty() const;
    bool isfull() const;
    int countQuene() const;
    bool enquene(const Item& item);
    bool dequene(Item item);

2、类的私有部分声明
除上述提到的私有部分以外,针对链表数据结构,应该设计一个结点结构,结点结构包含数值和指向下一个结点的地址。

struct Node
{
    Item item;
    Node* next;
}

class Quene
{
public:
    Node* first;
    Node* rear;
    int items;
    const int Maxitems;

上述代码引出一个问题:是应该在类中声明结构体,还是在全局中进行声明。全局中进行声明的话,作用域为整个文件,若其他头文件也声明了这个结点定义,当一起包含时,容易出现冲突。若在类中声明结构,则其作用域为类,则不用担心冲突问题。若声明在私有部分,则只能类内进行使用,若在公有部分声明,要在类外使用,只需Quene::Node。

最后队列的类公有成员和私有成员声明:

class Quene
{
private:
    struct Node
    {
        Item item;
        Node* next;
    }
    Node* first;
    Node* rear;
    int items;
    const int Maxitems;
public:
    Quene(int qs);
    ~Quene();
    bool isempty() const;
    bool isfull() const;
    int countQuene() const;
    bool enquene(const Item& item);
    bool dequene(Item item);

3、类的实现

Quene::Quene(int qs):Maxitems(qs)
{
    first=NULL;
    rear=NULL;
    items=0;
}

类的有参构造函数需要初始化类中的私有属性,在流程进入函数块中时,程序将会率先为属性分配内存,因此若在函数块中常规为const常量赋值是不允许的,对于必须在进程进入函数块前,创建常量的同时,初始化常量,因此需要这段代码:Quene(int qs):Maxitems(qs),这种操作被称为成员初始化列表。初始化列表的操作不限于const量,对于一般属性也可以这样操作,但是有一个限制条件为,只能在构造函数中使用。

bool Quene::isempty()
{
    return (items == 0);
}
bool Quene::isfull()
{
    return(items == Maxitems);
}
int Quene::CountQuene()
{
    return items;
}

上述三种类内方法实现较为简单,对于这样简单的类内方法实现,最好使用内联函数的表达形式,即在类内方法声明时,直接在后面加上定义,这样可以提升整体的程序运行速度,但是会增加内存的使用,所以内联函数更适用于函数体实现代码较少的情况。

队列的插入方法实现(重点)
队列的插入与链表的增加结点的方法一致

graph TD
    A[检查队列是否满员] -->|已经满员| C(返回false)
    A[检查队列是否满员] -->|未满员|B(新建一个结点)
    B --> D(设置结点的item与next值)
    D --> E(判断first是否为NULL)
    E --> |是| I(将first指针指向新结点)
    E --> |否| F(将上一个结点的next指向新建结点)
    I --> G
    F --> G(将rear指针指向这个结点)
    G --> H(items++)
bool Quene::enquene(const Item& item)
{
    if (isfull())
        return false;
    Node * add = new Node;
    add->item = item;
    add->next = NULL;
    if (front == NULL)
        front = add;
    else
        rear->next = add;
    rear = add;
    items++
    return true;
}

队列的删除方法实现(重点)
思路与增加结点的方法一致

graph TD
    A[判断队列是否为空] -->|否| B(将first指向结点的地址赋值给新结点指针)
    A[判断队列是否为空] -->|是| G(返回false)
    B --> C{将第二个结点的地址赋给first}
    C --> D(delete掉刚刚指向的新结点,并将items--)
    D --> E(判断此时items是否为0)
    E -->|是| F(将rear指针指向NULL)
bool Quene::dequene(Item& item)
{
    if (isempty())
        return false;
    item = front->item;
    Node* temp = front;
    front = front->next;
    delete temp;
    items--;
    if (items == 0)
        rear = NULL;
    return true;}

析构函数的实现(重点)
虽然已经有删除队列的成员函数,但是依然无法保证队列过期时,在堆区中所创建的内存全部释放,因此析构函数应该将所有结点一一删除。

Quene::~Quene()
{
    Node* temp;
    while (front != NULL)
    {
        temp = front;
        front = front->next;
        delete temp;
    }
}

4、补充设计
思考该队列类是否能使用默认的复制函数操作,当使用默认的复制函数操作时,会将头指针和尾指针同时复制给新对象,因此在操作新对象时,也会改变原对象的队列,更为严重的是,原对象的rear指针和first指针不会相应发生改变,因此在不必须要重载复制函数和深拷贝=函数时,将这两个函数放在私有部分,依靠编译器来避免发生错误。


顾客类的设计

  • 能够初始化顾客的属性
  • 属性设置为入队时间以及处理事务时间
  • 能够给顾客的属性赋值
  • 类外能够获得顾客的到达时间属性和处理时间属性
  • 处理时间为1~3s,由系统随机生成
    1、顾客类的声明

    class Customer
    {
    private:
      long arrive;
      int processtime;
    public:
      Customer()
      {
          arrive = 0;
          processtime = 0;
      }
      void setTime(long when);
      long when() const{ return arrive; }
      int processTime() const{ return processtime; }
    };

    2、顾客类的实现

void Customer::setTime(long when)
{
    arrive = when;
    processtime = std::rand() % 3 + 1;
}

ATM的模拟
本次模拟,程序允许输入三个数:队列的最大长度,程序模拟的时间(小时),平均每小时的顾客数,另外程序将使用循环的方式,每循环一次代表过去一分钟,就是相当于该程序的刷新率为1分钟一次,程序需要完成以下工作:

  1. 需要判断是否有新顾客来。
  2. 如果没有客户在交易,那么选取队列中的第一个客户,率先确定客户已经等候的时间,将wait_time计数器设置为新客户所需要的处理时间。
  3. 如果有客户在交易,则将wait_time计数器减1。
  4. 需要记录获得服务的顾客数目、被拒绝的客户数目、排队等候的累计时间以及累计的队列长度等。

如何判断有新顾客
假设我们输入的是平均每小时的顾客数为10,也就是说每6分钟就会有一个顾客,但是为了使模拟更加的真实,我们想要把这个时间随机化,也就是说可能两个顾客的间隔时间为3分钟也有可能4分钟5分钟。代码如下

bool newCustomer(int x)
{
    return (rand()*x/RAND_MAX<1);
}

rand()为一个返回任意数值的函数,RAND_MAX为rand()所能取的最大值,该式子将会随机返回0~6中的任意一个数字,这个程序相对来说比较笨拙,但是能够模拟出新顾客随机出现的情况。可以通过提高每循环一次,所经历的时间来提高准确性。

现在编写执行程序代码
1、准备工作阶段

srand(time(NULL));//随机数种子
    
    cout << "Case Study:Bank of Heather Automatic Teller" << endl;
    cout << "Enter maximum size of quene: ";
    int qs;//输入队列所能存储的最大值
    cin >> qs;
    Quene line(qs);//通过有参构造,将输入的最大值赋值给Maxitems

    cout << "Enter the number if simulation hours:";
    int hours;
    cin >> hours;//输入本次模拟所需要的时间
    long cyclelimit = MIN_PR_HR * hours;//总分钟数

    cout << "Enter the average number of customers per hour:";
    double perhour;
    cin >> perhour;//输入平均每小时的新顾客
    double min_per_cust;
    min_per_cust = MIN_PR_HR / perhour;//min_per_cust为平均多少分钟来一个客人

    Item temp;
    long turnaways = 0;//因队伍人数满,而被回绝的人数
    long customers = 0;//进入队列的客户总人数
    long served = 0;//参与交易的客户总人数
    long sum_line = 0;//队伍长度的累加值
    int wait_time = 0;//等待时间
    long line_wait = 0;//队伍中等待时间的累加

2、循环部分代码

    for (int cycle = 0; cycle < cyclelimit; cycle++)
    {
        if (newCustomer(min_per_cust))//判断是否有新顾客来
        {
            if (line.isfull())//若队列满了,则给turnways++
                turnaways++;
            else
            {
                customers++;//否则给customers++
                temp.setTime(cycle);//将顾客到达的时间赋值给属性arrive
                line.enquene(temp);//将temp顾客加入到队列中
            }
        }
        if (wait_time <= 0 && !line.isempty())//判断柜台是否有人在交易
        {
            line.dequene(temp);//选取队列头第一个客户
            wait_time = temp.processTime();//将该客户的交易时间赋给wait_time
            line_wait += cycle - temp.when();//求出累计的等待时间
            served++;//将服务的总客户加1
        }
        if (wait_time > 0)//若wait_time>0,则代表目前还有人在交易
            wait_time--;
        sum_line += line.CountQuene();//将每个时间里队伍里的人数累加

3、展示部分的代码
展示部分的代码即将各个量打印出来即可。

总结:这是一道在C++primer plus上的示例,第一次看的时候觉得很迷糊,所以现在将它记录在思否上。

你可能感兴趣的:(c++)