背景:Heather银行打算在Food Heap超市开设一个ATM。Food Heap超市的管理者担心排队等待使用ATM的人流会干扰超市的交通,希望限制排队等待的人数。Heather银行希望对顾客排队等待时间进行估测。
任务:自行设计一个队列的类和顾客的类,顾客到达的时间随机,并且编写一个程序来模拟顾客和队列的交互。
思路:首先设计队列类,队列是一个尾进头出的结构,若采用数组来表示队列,删除队列头部元素,需要将整体的元素往前移动一格,操作较为繁琐,使用链表的数据结构来表示队列,删除队列首元素时,只需针对单个元素,增加队列元素时,只需要改变地址。链表与数组相比,链表查找元素较为麻烦,增减元素较为方便。
队列类的设计
1、首先设计类的公有接口
- 队列所能容纳的项目数应该要有限制,因此应该在类中设计一个const属性来存储队列所能存储的最大数目。
- 队列能够创建空队列,所以在队列类的构造函数中应当设置初始化属性的参数
- 能够检查队列是否为满,所以应该设置一个bool函数,还想到要有一个类中现有的items的属性,通过这个items的大小与最大数目来进行比较从而判断队列是否为满
- 检查队列是否为空,与队列是否为满相类似。
- 需要一个函数成员能够返回当前队列中的数目
- (重点)需要一个插入队列的成员函数和删除队列成员的成员函数,涉及到链表的删除和添加,因此在类中应该各设置一个指向头部和指向尾部的指针。
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++)
队列的删除方法实现(重点)
思路与增加结点的方法一致
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分钟一次,程序需要完成以下工作:
- 需要判断是否有新顾客来。
- 如果没有客户在交易,那么选取队列中的第一个客户,率先确定客户已经等候的时间,将wait_time计数器设置为新客户所需要的处理时间。
- 如果有客户在交易,则将wait_time计数器减1。
- 需要记录获得服务的顾客数目、被拒绝的客户数目、排队等候的累计时间以及累计的队列长度等。
如何判断有新顾客
假设我们输入的是平均每小时的顾客数为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上的示例,第一次看的时候觉得很迷糊,所以现在将它记录在思否上。