本文要模拟队列,写了四个半小时,发现了自己的很多问题
给定三个参数:队列长度,模拟时间(单位:小时),和平均每小时的到达顾客数目
程序输出五项参数:平均队列长度,顾客平均等待时间,被接受的顾客数目,被服务的顾客数目,被拒绝的顾客数目。
完全是随机模拟,所以相同输入不一定得到相同输出。
队列类
我在写队列类和顾客类时,发现自己的最大问题是还是很难把抽象和细节完全分开,但是不分开就会让人觉得非常不对劲,感觉搅和在一起,很容易出问题。比如写队列的时候,我设置私有成员时设置的是指向Customer类的指针,且没有想到把顾客和next指针封装为一个抽象节点!!!但是这一步的封装是非常重要的,之前用C写队列模拟的时候,也是用了结构体把内容和next指针封装为更抽象的Node, 并且结构体里面仍然要抽象,不要直接写顾客类,而是写Item item,这样才足够抽象,因此足够通用,比如你想把队列中的内容改为数据包,而非顾客,那么只需要把Node结构体的Item改为数据包类,而非顾客类。
类声明(头文件)
//queue.h
#ifndef QUEUE_H_
#define QUEUE_H_
#include "customers.h"
typedef unsigned int uint;
typedef Customers Item;
class Queue{
private:
enum {MAXLEN = 10};
const uint queueSize = MAXLEN;//使用初始化成员列表的构造函数会覆盖类内初始化;
struct Node{Item item; Node * next;};//也可写struct Node *,这行只是结构声明,并非成员
Node * front;//队列的头指针,指向第一个节点(顾客)
Node * rear;//指向当前节点(顾客),即队列尾部最后一个节点(顾客)
uint items;//队列长度
public:
Queue(uint qSize = MAXLEN);
//Queue(const Queue &);//复制构造函数
~Queue();
void initialize();
bool isempty() const;
bool isfull() const;
bool enqueue(const Item & item);//把item放入队列尾部
bool dequeue(Item & item);//把队首元素拿到item中
uint len() const;
Queue & operator=(const Queue &);//重载赋值运算符成员函数
};
#endif
类定义(方法文件)
用成员初始化列表初始化const变量和常量,用于初始化const类成员
复制构造函数也用了成员初始化列表(只有构造函数可以使用这种语法)
//queue.cpp
#include <iostream>
#include "customers.h"
#include "Queue.h"
typedef unsigned int uint;
typedef Customers Item;
//使用成员初始化列表语法初始化queueSize,因为queueSize是const变量,只能在声明时赋值
//所以queueSize必须在执行函数体之前(即创建对象时就赋值)
Queue::Queue(uint qSize):queueSize(qSize)
{
//queueSize = qSize;//报错
initialize();
std::cout << "A " << queueSize << " size queue is created!\n";
}
/*
//成员初始化列表并不是只能用于常量,const变量和类的引用成员
//(因为引用本质是const指针变量,所以也必须初始化时候赋值),也可以用于普通变量
//但是只有构造函数可以使用这种语法
Queue(uint qSize) : queueSize(qsize), items(0), front(NULL), rear(nullptr)
{}
*/
//成员初始化列表以初始化const的queueSize
Queue::Queue(const Queue & q):queueSize(q.queueSize)
{
std::cout << "copy constructor\n";
if (q.front == NULL)//q是空队列,一定要检查是否是空队列才可以用->访问其指向的内容
initialize();
else{
front->item = (q.front)->item;
front->next = (q.front)->next;
rear->item = (q.rear)->item;
rear->next = (q.rear)->next;
items = q.items;
}
}
Queue::~Queue()
{
//构造函数虽然没用new,但是析构函数还是要用,因为程序结束时队列的长度不一定为0
Item item;
while (rear != NULL)//or while (!items)
{
dequeue(item);
}
}
void Queue::initialize()
{
items = 0;
front = rear = 0;//空指针,nullptr
}
bool Queue::isempty() const
{
return (items == 0);
}
bool Queue::isfull() const
{
return (items == queueSize);
}
bool Queue::enqueue(const Item & item)
{
if (isfull())
{
std::cout << "queue is already full!\n";
return false;
}
Node * node = new Node;//创建node时会调用顾客类的默认构造函数,创建顾客
node->item = item;
node->next = 0;
++items;
if (front == NULL)//不用isempty(),因为上一句增加了items
{
front = rear = node;
}
else
{
rear->next = node;
}
rear = node;
//std::cout << items << " customers left in queue!\n";
return true;
}
bool Queue::dequeue(Item & item)
{
if (isempty())
{
std::cout << "queue is already empty!\n";
return false;
}
item = front->item;
Node * temp = front;
front = front->next;
delete temp;//不能delete front
--items;
if ( items == 0)
rear = 0;
//std::cout << items << " customers left in queue!\n";
return true;
}
uint Queue::len() const
{
return items;
}
Queue & Queue::operator=(const Queue & q)
{
std::cout << "operator member function\n";
if (q.front == NULL)
initialize();
else{
front->item = (q.front)->item;
front->next = (q.front)->next;
rear->item = (q.rear)->item;
rear->next = (q.rear)->next;
items = q.items;
}
return *this;
}
顾客类
类声明(头文件)
顾客类很简单,只需要记录他们的等待时间和到达时间,我由于没想到模拟细节所以没想到到达时间
//customers.h
#ifndef CUSTOMERS_H_
#define CUSTOMERS_H_
typedef unsigned int uint;
class Customers{
private:
uint processing_time;//处理时间
uint arrive_time;//到达时间
public:
Customers();
~Customers();
void set_process();
void set_arrive(uint);
uint when() const
{return arrive_time;}
uint how_long() const
{return processing_time;}
};
#endif
类定义(方法文件)
我之前在set_process()方法中还写了std::srand(std::time(0));,于是仿真结果很奇怪,不太对劲,之前也犯过这个错误,种子在整个程序中只设置一次,最好在测试程序中设置,不要在方法中还设置一次,这样中途种子变了,产生的所有数字就来自于不同随机种子,不再是均匀分布了,毕竟计算机产生的是伪随机数,即只要种子一样,每次都从同一波数中取数。
每个顾客的处理时间可取1, 2, 3分钟,均匀概率分布。
//customers.cpp
#include <cstdio>
#include <ctime>
#include <iostream>
#include "customers.h"
typedef unsigned int uint;
Customers::Customers()
{
arrive_time = 0;
set_process();
//std::cout << "process time (min): " << processing_time << '\n';
}
Customers::~Customers()
{}
void Customers::set_process()
{
processing_time = std::rand() % 3 + 1;//均匀分布
}
void Customers::set_arrive(uint t)
{
arrive_time = t;
}
主程序(测试程序 / 驱动程序)
每个时间单位要做的事情:
-
判断有无新顾客到来。只要队列未满,就接受顾客,否则拒绝。
-
被接受的顾客数大于等于被服务的顾客数,差值是模拟时间到了后队列中的剩余人数,所以差值小于等于队列长度。
-
平均队列长度等于每一个时间单位的队列长度之和除以模拟时间单位总数。本文设置时间分辨率为1分钟,即时间单位为1min,每分钟程序检查是否有新顾客到来,是否服务等等。
-
平均等待时间等于队列所有被服务顾客的等待时间之和除以被服务顾客的数量。
我把模拟队列的部分封装起来了,留出了接口,即三个输入参数,而输出参数在函数内打印输出。主程序只负责读取用户的输入,并且我用while设置为了循环多次输入。
程序结尾我展示了复制构造函数和赋值运算符成员函数的运行,说明写的没错,注意这两个函数一定要先检查传入的队列是否是空队列,否则会直接访问空指针指向的内容,造成程序错误。
判断有无新顾客到来非常巧妙,比随机设置用户的处理时间还巧妙。
//main.cpp
#include <iostream>
#include <cstdlib>
#include <ctime>
#include "Queue.h"
#include "customers.h"
void simulate(uint QueueSize, uint hours, uint num_customers);
const uint MIN_PER_HR = 60;
bool isNewcustomer(double x);
void eatline();
int main()
{
uint QueueSize;
uint hours;
uint num_per_hour;
std::cout << "Enter queue size (0 to quit):\n";
std::cin >> QueueSize;
eatline();
while (QueueSize)
{
std::cout << "Enter simulating hours:\n";
std::cin >> hours;
eatline();
std::cout << "Enter average number of customers per hour:\n";
std::cin >> num_per_hour;
eatline();
std::srand(std::time(0));
simulate(QueueSize, hours, num_per_hour);
std::cout << "\nEnter queue size (0 to quit):\n";
std::cin >> QueueSize;
eatline();
}
std::cout << "Bye!\n";
Queue q1(10);//默认构造函数
Queue q2(q1);//复制构造函数
Queue q3 = q2;//复制构造函数
Queue q4;//默认构造函数
q4 = q3;//赋值运算符成员函数
return 0;
}
void simulate(uint QueueSize, uint hours, uint num_per_hour)
{
double MIN_PER_CUST = MIN_PER_HR / num_per_hour;//平均几分钟到达一个顾客
Queue line(QueueSize);
uint reject = 0;//拒绝的顾客数目
uint customers = 0;//进入队列的顾客数量
uint served = 0;//仿真期间服务的顾客数量
uint sum_line = 0;//每一秒队列长度的和,用于计算平均队列长度
uint wait_time = 0;//到ATM空闲的剩余时间
uint line_wait = 0;//顾客们在队列的总时间(不算接受服务)
Item temp;
//仿真时间分辨率为1min,即每分钟检查是否有新顾客,ATM是否空闲等
uint i;
uint cyclelimit = hours * MIN_PER_HR;
for (i = 0; i < cyclelimit; ++i){
if (isNewcustomer(MIN_PER_CUST)){
if (!line.isfull()){
temp.set_process();
temp.set_arrive(i);
line.enqueue(temp);
++customers;
}
else{
++reject;
}
}
if (wait_time <= 0 && !line.isempty()){
line.dequeue(temp);
wait_time = temp.how_long();
++served;
line_wait += i- temp.when();
}
if (wait_time > 0){
--wait_time;
}
sum_line += line.len();
}
//reports
if (customers > 0){
std::cout << "Customers accepted: " << customers << '\n';
std::cout << " Customers served: " << served << '\n';
std::cout << " turnaways: " << reject << '\n';
std::cout << "average queue size: " << double (sum_line) / i << '\n';
std::cout << " average wait time: " << double (line_wait) / served << " minutes" << '\n';
}
else
std::cout << "No customers!\n";
}
bool isNewcustomer(double x)
{
return (std::rand() * x / RAND_MAX < 1);//概率:1/x
}
void eatline()
{
while (std::cin.get() != '\n')
;
}
输出
输出几项结果的冒号对齐,便于观看。
相同输入的结果比较相近,但还是会有较大变动,排队时间波动很大。
每小时人数由15变为30后,排队时间不是加倍,而是增大了十几倍。
Enter queue size (0 to quit):
10
Enter simulating hours:
100
Enter average number of customers per hour:
15
A 10 size queue is created!
Customers accepted: 1517
Customers served: 1516
turnaways: 0
average queue size: 0.160667
average wait time: 0.635224 minutes
Enter queue size (0 to quit):
10
Enter simulating hours:
100
Enter average number of customers per hour:
15
A 10 size queue is created!
Customers accepted: 1507
Customers served: 1507
turnaways: 0
average queue size: 0.166333
average wait time: 0.662243 minutes
Enter queue size (0 to quit):
10
Enter simulating hours:
100
Enter average number of customers per hour:
30
A 10 size queue is created!
Customers accepted: 2900
Customers served: 2899
turnaways: 48
average queue size: 3.86217
average wait time: 7.9931 minutes
Enter queue size (0 to quit):
20
Enter simulating hours:
100
Enter average number of customers per hour:
30
A 20 size queue is created!
Customers accepted: 2966
Customers served: 2946
turnaways: 51
average queue size: 9.94433
average wait time: 20.091 minutes
Enter queue size (0 to quit):
10
Enter simulating hours:
4
Enter average number of customers per hour:
30
A 10 size queue is created!
Customers accepted: 130
Customers served: 121
turnaways: 13
average queue size: 6.325
average wait time: 12.124 minutes
Enter queue size (0 to quit):
10
Enter simulating hours:
4
Enter average number of customers per hour:
30
A 10 size queue is created!
Customers accepted: 134
Customers served: 124
turnaways: 0
average queue size: 3.0875
average wait time: 5.28226 minutes
Enter queue size (0 to quit):
10
Enter simulating hours:
4
Enter average number of customers per hour:
30
A 10 size queue is created!
Customers accepted: 115
Customers served: 108
turnaways: 7
average queue size: 5.75417
average wait time: 12.2407 minutes
Enter queue size (0 to quit):
10
Enter simulating hours:
4
Enter average number of customers per hour:
30
A 10 size queue is created!
Customers accepted: 129
Customers served: 121
turnaways: 12
average queue size: 6.00833
average wait time: 11.314 minutes
Enter queue size (0 to quit):
10
Enter simulating hours:
4
Enter average number of customers per hour:
30
A 10 size queue is created!
Customers accepted: 116
Customers served: 113
turnaways: 0
average queue size: 1.92917
average wait time: 3.95575 minutes
Enter queue size (0 to quit):
0
Bye!
A 10 size queue is created!
copy constructor
copy constructor
A 10 size queue is created!
operator member function