[C++] queue simulation problem

queue simulation problem

12.7.1 ATM problems

Heather Bank intends to open an automated teller machine (ATM) at the Food Heap supermarket. Food Heap supermarket managers are concerned that queues to use ATMs will disrupt traffic at the supermarket and want to limit the number of people waiting in line. Heather Bank wants to estimate how long customers wait in line. To write a program to simulate this situation, so that supermarket managers can understand the possible impact of ATM.

A representative of Heather Bank said: Usually, one-third of customers only need one minute to get service, one-third of customers need two minutes, and another third of customers need three minutes. Also, the timing of customer arrival is random, but the number of customers using the ATM each hour is fairly constant.

Design a class to represent the customer, and simulate the interaction between the customer and the queue.
insert image description here

12.7.2 Queue class

Features of the Queue class:

  • Queues store ordered sequences of items;
  • There is a limit to the number of items the queue can hold;
  • Should be able to create empty queues;
  • It should be possible to check if the queue is empty;
  • It should be possible to check if the queue is full;
  • It should be possible to add items at the end of the queue;
  • It should be possible to remove items from the head of the queue;
  • It should be possible to determine the number of items in the queue.

12.7.3 Interface of the Queue class

Judging from the characteristics of the above classes how to define the public interface of the Queue class:

class Queue{
    enum{Q_SIZE = 10};
    public:
    Queue(int qs = Q_SIZE);
    ~Queue();
    bool isEmpty()const;
    bool isFull()const;
    int queueCount()const;
    bool enqueue(const Item &item);
    bool dequeue(Item &item);
}

12.7.4 Implementation of the Queue class

Once the interface is determined, it can be implemented. First, you need to decide how to represent the queue data. One way is to use new to dynamically allocate an array that contains the desired number of elements. However, for queue operations, arrays are not very suitable. For example, after deleting the first element of the array, you need to move all the remaining elements forward; otherwise, you need to do some more laborious work, such as treating the array as a loop. However, a linked list works well for queues. A linked list consists of a sequence of nodes. Each node contains the information to be saved in the linked list and a pointer to the next node. For the queue here, the data part is a value of Item type, so the following structure can be used to represent the node:

struct Node{
    Item item;
    struct Node * next;
}

To follow a linked list, the address of the first node must be known. You can make a data member of the Queue class point to the starting position of the linked list. Specifically, this is all the information needed to find any node along a chain of nodes. However, since the queue always appends new items
to the tail, it is convenient to include a data member pointing to the last node (see Figure 12.9). In addition, data members can be used to keep track of the maximum number of items the queue can store and the current number of items. So, the private part of the class declaration looks like this:

insert image description here

class Queue{
    private:
    struct Node{ Item item; struct Node * next;};
    enum(Q_SIZE = 10);
    NODE * front;
    NODE * rear;
    int items;
    const int qsize;
}

Only constructors can use this initializer-list syntax. For non-static const class members, you must use this syntax. This syntax must also be used for class members that are declared as references. This is because references, like const data, can only be initialized when they are created.

Data members are initialized in the order in which they appear in the class declaration, regardless of the order in which they are listed in the initializer. (this point of view is doubtful)

The codes of isempty(), isfull(), and queuecount() are very simple. If items is 0, the queue is empty; if items is equal to qsize, the queue is full. To know the number of items in the queue, just return the value of items.

Adding items to the end of the queue (enqueue) is cumbersome:

bool Queue::enqueue(const Item & item)
{
	if (isfull())
		return false;
	Node * add = new Node; //创建节点
	//如果失败,new将抛出std::bad_alloc的异常
	add->item = item; //设置节点指针
	add->next = NULL;
	items++;
	if (front == NULL) //如果队列为空
		front = add; //将元素放在队首
	else
		rear->next = add; //否则放在队尾
	rear = add; //设置尾指针指向新节点
	return true;
}

insert image description here

The method goes through the following stages:

1) If the queue is full, end.

2) Create a new node. If new cannot create a new node, it throws an exception, and the program terminates unless code to handle the exception is provided.

3) Put the correct value in the node. The code copies the Item value to the node's data section and sets the node's next pointer to NULL. This prepares the node to be the last item in the queue.

4) Increment the item count (items) by 1.

5) Append the node to the tail of the queue. This has two parts. First, connect the node with another node in the list. This is done by pointing the next pointer of the current tail node to the new tail node. The second part is to set the member pointer rear of the Queue to point to the new node, so that the queue can directly access the last node. If the queue is empty, the front pointer must also be set to point to the new node (if there is only one node, it is both the head and tail of the queue).

Deleting the first item in the queue (dequeuing) also requires multiple steps to complete:

bool Queue::dequeue(Item & item)
{
	if (front == NULL)
		return false;
	item = front->item; //将队首元素赋值给item
	items--;
	Node * temp = front; //保存队首元素位置
	front = front->next; //将front设置成指向下一个节点
	delete temp; //删除以前的第一个节点
	if (items == 0)
		rear = NULL;
	return true;
}

insert image description here

Need to go through the following stages:

1) If the queue is empty, end

2) The first item of the queue is provided to the calling function by copying the data part from the current front node into the reference variable passed to the method.

3) Decrement the item count (items) by 1.

4) Save the position of the front node for later deletion.

5) Dequeue the node. This is done by setting the Queue member pointer front to point to the next node, whose position is given by front->next.

6) To save memory, delete the previous first node.

7) If the linked list is empty, set rear to NULL.

Step 4 is essential because step 5 will remove information about the previous first node position.

12.7.5 Are other functions required?

The class constructor doesn't use new, so at first glance, it seems that you don't need to pay attention to the special requirements imposed on the class due to the use of new in the constructor. Because adding an object to the queue will call new to create a new node. By deleting nodes, the dequeue() method can indeed clear nodes, but this does not guarantee that the queue will be empty when it expires. Therefore, the class needs an explicit destructor - a function that deletes any remaining nodes.

The following is an implementation that starts from the head of the linked list and deletes each node in it in turn:

Queue::~Queue()
{
	Node * temp;
	while (front != NULL) //确定queue不为空
	{
		temp = front; //保存前一个元素
		front = front->next; //重置下一个元素
		delete temp; //删除前一个元素
	}
}

Classes that use new usually need to contain explicit copy constructors and assignment operators that perform deep copies, is this also the case?

The first question to answer is, is the default member replication appropriate? the answer is negative.

Copying the members of the Queue object will generate a new object that points to the original head and tail of the linked list. Therefore, adding an item to the copied Queue object modifies the shared linked list. Doing so will have very serious consequences. Worse, only the copy's tail pointer gets updated, which corrupts the linked list from the original object's point of view. Obviously, to clone or copy a queue, you must provide a copy constructor and an assignment constructor that performs a deep copy.

Of course, this raises the question: why copy the queue at all? Maybe you want to save snapshots of the queue at different stages of the simulation, or maybe you want to feed two different policies the same input. In fact, it is very useful to have an operation that splits the queue, supermarkets often do this when opening additional checkouts. Likewise, it may be desirable to combine two queues into one or to truncate a queue.

But suppose the simulation here does not implement the above functionality. Can't these problems be ignored and existing methods used? sure. However, at some point in the future, it may be necessary to use the queue again and require replication. Also, you might forget not to provide proper code for the copy. In this case, the program will compile and run, but the result will be messy and even crash. Therefore, it is better to provide a copy constructor and assignment operator, although they are not currently required.

Fortunately, there's a little trick that can avoid all this extra work and keep your program from crashing. This is all it takes to define the desired method as a pseudo-private method:

class Queue
{
private:
	Queue(const Queue &q) : qsize(0) { } //先发制人的定义 
	Queue & operator=(const Queue &q) { return *this; }
	...
};

This does two things:

1) It avoids default method definitions that would otherwise be automatically generated.

2) Because these methods are private, they cannot be widely used.

If nip and tuck were Queue objects, the compiler would not allow this:

Queue snick(nip); //错误,不被允许
tuck = nip; //错误,不被允许

C++11 provides another way to disable methods - using the keyword delete.

When an object is passed by value (or returned), the copy constructor will be called. If you follow the convention of preferring passing objects by reference, you won't have any problems. The copy constructor is also used to create other temporary objects, but there are no operations in the Queue definition that lead to the creation of temporary objects, such as overloading the addition operator.

12.7.6 Customer class

Next you need to design the client class. Typically, an ATM customer has many attributes, such as name, account, and account balance. However, the only attributes that the simulation needs to use here are when a customer enters the queue and how long it takes for a customer to transact. When the simulation generates a new customer, the program creates a new customer object and stores the customer's arrival time and a randomly generated transaction time in it. When the customer arrives at the head of the queue, the program will record the time at this time and subtract it from the time of entering the queue to obtain the customer's waiting time. The following code demonstrates how to define and implement the Customer class:

class Customer
{
private:
	long arrive; //顾客到达时间
	int processtime; //顾客进行时间
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;
}

The default constructor creates an empty client. The set() member function sets the arrival time as a parameter, and sets the processing time as a random value from 1 to 3.

queue.h

#ifndef QUEUE_H_
#define QUEUE_H_
//这个队列包含Customer元素
class Customer
{
private:
	long arrive; //顾客到达时间
	int processtime; //顾客进行时间
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:
	//类中嵌套结构声明
	struct Node { Item item; struct Node * next; };
	enum { Q_SIZE = 10 };
	//私有成员
	Node * front; //队首指针
	Node * rear; //队尾指针
	int items; //队列中当前元素个数
	const int qsize; //队列中最大元素个数
	//避免本来自动生成的默认方法定义
	Queue(const Queue &q) : qsize(0) { }
	Queue & operator=(const Queue &q) { return *this; }
public:
	Queue(int qs = Q_SIZE); //创建一个qs大小队列
	~Queue();
	bool isempty() const;
	bool isfull() const;
	int queuecount() const;
	bool enqueue(const Item &item); //在队尾添加元素
	bool dequeue(Item &item); //在队首删除元素
};
#endif // !QUEUE_H_

queue.cpp

#include "queue.h"
#include <cstdlib>

Queue::Queue(int qs) : qsize(qs)
{
	front = rear = NULL;
	items = 0;
}

Queue::~Queue()
{
	Node * temp;
	while (front != NULL) //确定queue不为空
	{
		temp = front; //保存前一个元素
		front = front->next; //重置下一个元素
		delete temp; //删除前一个元素
	}
}

bool Queue::isempty() const
{
	return items == 0;
}

bool Queue::isfull() const
{
	return items == qsize;
}

int Queue::queuecount() const
{
	return items;
}

//入队
bool Queue::enqueue(const Item & item)
{
	if (isfull())
		return false;
	Node * add = new Node; //创建节点
	//如果失败,new将抛出std::bad_alloc的异常
	add->item = item; //设置节点指针
	add->next = NULL;
	items++;
	if (front == NULL) //如果队列为空
		front = add; //将元素放在队首
	else
		rear->next = add; //否则放在队尾
	rear = add; //设置尾指针指向新节点
	return true;
}

//出队
bool Queue::dequeue(Item & item)
{
	if (front == NULL)
		return false;
	item = front->item; //将队首元素赋值给item
	items--;
	Node * temp = front; //保存队首元素位置
	front = front->next; //将front设置成指向下一个节点
	delete temp; //删除以前的第一个节点
	if (items == 0)
		rear = NULL;
	return true;
}

//设置处理时间为1~3的随机值
void Customer::set(long when)
{
	processtime = std::rand() % 3 + 1;
	arrive = when;
}

12.7.7 ATM simulation

Now you have the tools you need to simulate an ATM. The program allows the user to enter 3 numbers: the maximum length of the queue, the duration of the program simulation (in hours), and the average number of customers per hour. The program will use loops - each loop represents one minute. In the cycle of each minute, the program will complete the following work:
1) Determine whether a new customer has come. If it comes, and the queue is not full at this time, add it to the queue, otherwise reject the customer to enqueue.
2) If no customer is conducting a transaction, the first customer of the queue is picked. Determine how long this customer has been waiting, and set the wait_time counter to the processing time required for new customers.
3) If the client is being processed, decrement the wait_time counter by 1.
4) Record various data, such as the number of customers who get service, the number of customers who are rejected, the accumulated time of waiting in line and the accumulated queue length, etc.

When the simulation loop ends, the program will report various statistical results.

How the program determines whether a new customer arrives: Assuming an average of 10 customers arrive per hour, that equates to one customer every 6 minutes. The program will calculate this value and save it in the min_per_cust variable. However, exactly one customer every 6 minutes is not realistic, what we really want is a more random process - but one customer every 6 minutes on average.

The program uses the following function to determine if a customer has arrived during the loop:

bool newcustomer(double x)
{
	return (std::rand() * x / RAND_MAX < 1); 
}

It works like this: the value RAND_MAX is defined in the cstdlib file and is the maximum value that the rand() function may return (0 is the minimum value). Assuming that the average time between arrivals of customers x is 6, the value of rand()*x/RAND_MAX will be between 0 and 6. Specifically, on average every 6 times, this value will be less than 1. However, this function may cause customers to arrive at intervals of 1 minute and 20 minutes at other times. This method, although clumsy, can make the actual situation different from the regular arrival of a customer every 6 minutes. The above will not work if the average time between arrivals of customers is less than 1 minute, but the simulation is not designed for this case. If you really need to handle this situation, it's better to increase the time resolution, say each loop represents 10 seconds.

main.cpp

If you run the simulation program for a long time, you can know the long-term average; if you run the simulation program for a short time, you will only know the short-term changes.

#include <iostream>
#include <cstdlib>
#include <ctime>
#include "queue.h"
const int MIN_PER_HR = 60;

bool newcustomer(double x);

int main()
{
	using std::cin;
	using std::cout;
	using std::endl;
	using std::ios_base;
	
	std::srand(std::time(0)); //随机初始化

	cout << "Case Study: Bank of Heather Automatic Teller\n";
	cout << "Enter maximum size of queue: ";
	int qs;
	cin >> qs;
	Queue line(qs); //队列中能装下的人数

	cout << "Enter the number of simulation hours: ";
	int hours; //模拟小时
	cin >> hours;
	//模拟将每分钟运行 1 个周期 
	long cyclelimit = MIN_PER_HR * hours;

	cout << "Enter the average number of customer per hour: ";
	double perhour; //每小时顾客到达平均个数
	cin >> perhour;
	double min_per_cust; //平均间隔时间
	min_per_cust = MIN_PER_HR;

	Item temp; //创建一个customer对象
	long turnaways = 0; //队满,拒绝入队
	long customers = 0; //加入队列
	long served = 0; //
	long sum_line = 0; //排队等候累积的队列长度
	int wait_time = 0; //等待ATM空闲时间
	long line_wait = 0; //排队等候累积的时间
	//运行这个模拟
	for (int cycle = 0; cycle < cyclelimit; cycle++)
	{
		if (newcustomer(min_per_cust))
		{
			if (line.isfull())
				turnaways++;
			else
			{
				customers++;
				temp.set(cycle); //cycle = time of arrival
				line.enqueue(temp); //加入新顾客
			}
		}
		if (wait_time <= 0 && !line.isempty())
		{
			line.dequeue(temp); //下一个顾客
			wait_time = temp.ptime(); //等待时间
			line_wait += cycle - temp.when();
			served++;
		}
		if (wait_time > 0)
			wait_time--;
		sum_line += line.queuecount();
	}
	//打印结果
	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 = 客户到达的平均间隔时间
bool newcustomer(double x)
{
	return (std::rand() * x / RAND_MAX < 1); //如果顾客到达的平均时间间隔少于1分钟,则返回真
}

Guess you like

Origin blog.csdn.net/weixin_43717839/article/details/130087834