<C++>:队列queue(STL queue)用法

前言

人生如逆旅,我亦是行人。


队列Queue

  • 队列是一种类型的容器的适配器,FIFO(先进先出),其中元素被插入到所述容器的一端,并从其另一端进行提取操作。
  • 队列被实现为容器的适配器,其是使用特定容器类封装到对象内部,作为其底层的容器类,提供了一个特定的一组成员函数来访问它的元素。
#include <queue>

std::queue<int> myQueue;
只能访问 queue 容器适配器的第一个和最后一个元素。只能在容器的末尾添加新元素,只能从头部移除元素。也就是传说中的“出队入队”操作,先进先出(FIFO)

下图展示了一个 queue 容器及其一些基本操作:
在这里插入图片描述


  • queue 的生成方式和 stack(栈) 相同,下面展示如何创建一个保存字符串对象的 queue:
std::queue<std::string> words;
  • 也可以使用拷贝构造函数:
std::queue<std::string> copy_words {words}; // A duplicate of words

stack、queue 这类适配器类都默认封装了一个 deque 容器,也可以通过指定第二个模板类型参数来使用其他类型的容器:

std::queue<std::string, std::list<std::string>>words;
底层容器必须提供这些操作:front()、back()、push_back()、pop_front()、empty() 和 size()。

queue操作

queue 和 stack 有一些成员函数相似,但在一些情况下,工作方式有些不同:

  • front():返回 queue 中第一个元素的引用。如果 queue 是常量,就返回一个常引用;如果 queue 为空,返回值是未定义的。
  • back():返回 queue 中最后一个元素的引用。如果 queue 是常量,就返回一个常引用;如果 queue 为空,返回值是未定义的。
  • push(const T& obj):在 queue 的尾部添加一个元素的副本。这是通过调用底层容器的成员函数 push_back() 来完成的。
  • push(T&& obj):以移动的方式在 queue 的尾部添加元素。这是通过调用底层容器的具有右值引用参数的成员函数 push_back() 来完成的。
  • pop():删除 queue 中的第一个元素。
  • size():返回 queue 中元素的个数。
  • empty():如果 queue 中没有元素的话,返回 true。
  • emplace():用传给 emplace() 的参数调用 T 的构造函数,在 queue 的尾部生成对象。
  • swap(queue<T> &other_q):将当前 queue 中的元素和参数 queue 中的元素交换。它们需要包含相同类型的元素。也可以调用全局函数模板 swap() 来完成同样的操作。

queue 模板定义了拷贝和移动版的 operator=(),对于所保存元素类型相同的 queue 对象,它们有一整套的比较运算符,这些运算符的工作方式和 stack 容器相同。

和 stack 一样,queue 也没有迭代器。访问元素的唯一方式是遍历容器内容,并移除访问过的每一个元素。例如:

std::deque<double> values {1.5, 2.5, 3.5, 4.5}; 
std::queue<double> numbers(values);
while (!numbers. empty())
{
    std ::cout << numbers. front() << " "; // Output the 1st element 
    numbers. pop();  // Delete the 1st element
}
std::cout << std::endl;

用循环列出 numbers 的内容,循环由 empty() 返回的值控制。调用 empty() 可以保证我们能够调用一个空队列的 ftont() 函数。如代码所示,为了访问 queue 中的全部元素,必须删除它们。如果不想删除容器中的元素,必须将它们复制到另一个容器中。如果一定要这么操作,我们可能需要换一个容器。


1、队列初始化

std::deque<int> mydeck(3, 100);        // 双端队列里初始化3个元素,都是100
std::list<int> mylist(2, 200);         // list 容器里初始化2个元素,都是200
std::queue<int> first;                 // 初始化一个空队列
std::queue<int> second(mydeck);        // 复制 mydeck 的内容初始化队列
std::queue<int, std::list<int> > third; // 初始化空队列,底层使用 list 容器
std::queue<int, std::list<int> > fourth(mylist);    // 复制 mylist 的内容初始化队列,底层使用 list 容器
std::cout << "size of first: " << first.size() << std::endl; // 0
std::cout << "size of second: " << second.size() << std::endl; // 3
std::cout << "size of third: " << third.size() << std::endl; // 0
std::cout << "size of fourth: " << fourth.size() << std::endl; // 2

2、判空

std::queue<int> myqueue1;
bool empty1 = myqueue1.empty(); // true
std::queue<int> myqueue2({100,100});
bool empty2 = myqueue2.empty(); // false

3、获得元素的个数

std::queue<int> myints;
std::cout << "0. size: " << myints.size() << std::endl; // 输出:0
for (int i = 0; i < 5; i++) myints.push(i);
std::cout << "1. size: " << myints.size() << std::endl; // 输出:5
myints.pop();
std::cout << "2. size: " << myints.size() << std::endl; // 输出:4

4、返回队头元素

头元素就是最先加入队列的元素,这个元素也是下次 Pop 出队的元素。

std::queue<int> myqueue3;
myqueue3.push(77);
myqueue3.push(66);
int& a1 = myqueue3.front(); // 77
int a2 = myqueue3.front(); // 77
myqueue3.front() = 88; // 给头元素77赋值为88
std::cout << "front:" << myqueue3.front() << std::endl; // 输出:88

5、返回末尾元素引用

末尾元素就是最后加入队列的元素,这个元素也是最新push入队的元素。

std::queue<int> myqueue4;
myqueue4.push(77);
myqueue4.push(66);
int& b1 = myqueue4.back(); // 66
int b2 = myqueue4.back(); // 66
myqueue4.back() = 33; // 给末尾元素66赋值为33
std::cout << "front:" << myqueue4.front() << std::endl; // 输出:33

6、出队/入队

std::queue<int> myqueue5;
myqueue5.push(55); // 无返回值,入队了一个55,size()==1
myqueue5.push(45); // size()==2
myqueue5.pop(); // 无返回值,出队了一个55,size()==1

7、(C++11)另一种入队,其底层容器调用了emplace_back方法。

myqueue5.emplace(45);

8、(C++11)交换

std::queue<int> teeth;
teeth.emplace(4); teeth.emplace(7);
std::queue<int> bags;
bags.emplace(4); bags.emplace(7); bags.emplace(7);
bags.swap(teeth);
std::cout << teeth.size() << std::endl; //输出:3
std::cout << bags.size() << std::endl; //输出:2

9、运算符 = != > >= < <=

// [==]当两个队列front()内容一致,返回true
std::queue<int> q1, q2;
bool ret = q1 == q2; // ret为true

// [!=]当两个队列元素front()不相等,返回true
std::queue<int> q3, q4;
q3.push(1);
bool ret2 = q3 != q4; // ret2为true

// [>]左边队列的front()的元素大于右边队列pop的元素,则返回true.
std::queue<int> q5, q6;
q5.push(1); q5.push(2); q5.push(2);
q6.push(0); q6.push(2);
bool ret3 = q5 >= q6; // ret3为true,因为1大于0

queue 容易的实际使用

这里汇集了一些使用 queue 容器的示例。这是一个用 queue 模拟超市运转的程序。结账队列的长度是超市运转的关键因素。它会影响超市可容纳的顾客数——因为太长的队伍会使顾客感到气馁,从而放弃排队。在很多情形中——医院可用病床数会严重影响应急处理设施的运转,也会产生同样的队列问题。我们的超市模拟是一个简单模型,灵活性有限。

  • 在头文件 customer.h 中定义一个类来模拟顾客:
//根据客户的结账时间定义客户
#ifndef CUSTOMER_H
#define CUSTOMER_H

// #include<vcruntime.h>

class Customer
{

protected:
    unsigned int service_t {};    //结账时间

private:
    /* data */

public:
    Customer(unsigned int st=10) :service_t {st}{}

    //减少结账的剩余时间
    Customer& time_decrement()
    {
        if(service_t > 0)
            service_t--;
        return *this;
    }
    bool done() const
    {
        return service_t == 0;
    }
};

#endif 
/*这里只有一个成员变量:service_t(服务时间):用来记录顾客的结账需要的时间。每过一分钟,
会调用一次 :time_decrement() 函数,这个函数会减少 service_t 的值,它可以反映顾客结账所
花费的时间。当 service_t 的值为 0 时,成员函数 done() 返回 true。*/
  • 超市的每个结账柜台都有一队排队等待的顾客。Checkout.h 中定义的 Checkout 类如下:
// 超市结账-维护和处理队列中的客户
#ifndef CHECKOUT_H
#define CHECKOUT_H

#include <queue> // 添加队列容器的头文件
#include "Customer.h"

class Checkout
{
    
private:
    std::queue<Customer> customers; // 等待结帐的顾客队伍
public:
    void add(const Customer& customer) 
    { 
        customers.push(customer); 
    }
    size_t qlength() const 
    { 
        return customers.size(); 
    }

    // 将时间增加一分钟
    void time_increment()
    {
        if (!customers.empty())
        { // There are customers waiting...
            if (customers.front().time_decrement().done())  // 如果顾客结账完成
                customers.pop(); // 将客户移除,也就是出队操作
        }
    };

    bool operator < (const Checkout& other) const 
    { 
        return qlength() < other.qlength(); 
    }
    bool operator > (const Checkout& other) const 
    { 
        return qlength() > other.qlength(); 
    }
};
#endif

/* 1、queue 容器是 Checkout 唯一的成员变量,用来保存等待结账的 Customer 对象。成员函数 add() 可以向队列中添加新顾客。
      只能处理队列中的第一个元素。 每过一分钟,调用一次 Checkout 对象的成员函数 ,
   2、time_increment(},它会调用第一个 Customer 对象的成员函数 time_decrement() 来减少剩余的服务时间,然后再调用成员
      函数 done()。如果 done() 返回 true,表明顾客结账完成,因此把他从队列中移除。Checkout 对象的比较运算符可以比较队
      列的长度。 */
  • 主文件 main.cpp
//模拟具有多个收银台的超市结账时间
#include <iostream>  // For standard streams:标准输入输出流
#include <iomanip>   // For stream manipulators:流的操纵者
#include <vector>    // For vector container:为向量的容器
#include <string>    // For string class:对于字符串的类
#include <numeric>   // For accumulate():积累
#include <algorithm> // For min_element & max_element:
#include <random>    // For random number generatio:对于随机数的生成

#include "customer.h"
#include "checkout.h"

// using namespace std;

using std::string;
using distribution = std::uniform_int_distribution<>;

//服务时间的输出直方图
void histogram(const std::vector<int> &v, int min)
{
    string bar(60, '*'); //用一排星号表示收银台

    for (size_t i{}; i < v.size(); i++)
    {
        /* code */
        std::cout << std::setw(3) << i + min << "" //服务时间等于索引(index)加上最小值(min)
                   << std::setw(4) << v[i] << ""    //没有输出的出现
                   << bar.substr(0, v[i])           //
                   << (v[i] > static_cast<int>(bar.size()) ? "……" : " ")
                   << std::endl;
    }
}

int main()
{
    std::random_device random_n;

    //设置最小和最大的结帐期-时间单位:分钟
    int service_t_min{2}, service_t_max{15};
    distribution service_t_d{service_t_min, service_t_max};

    //在商店开业时设置最小和最大的顾客数量
    int min_customers{15}, max_customers{20};
    distribution n_1st_customers_d{min_customers, max_customers};

    //设定客人到达的最小和最大间隔时间
    int min_arr_interval{1}, max_arr_interval{5};
    distribution arrival_interval_d{min_arr_interval, max_arr_interval};

    size_t n_checkouts{}; //结账柜台的数量

    std::cout << "请输入超市中结账柜台的数量:";
    std::cin >> n_checkouts;

    if (n_checkouts == 0)
    {
        /* code */
        std::cout << "结账柜台的数量必须大于0,至少设置为1" << std::endl;
        n_checkouts = 1;
    }

    std::vector<Checkout> checkouts{n_checkouts};
    std::vector<int> service_times(service_t_max - service_t_min + 1);

    //添加商店开门时等待的顾客
    int count{n_1st_customers_d(random_n)};
    std::cout << "商店开门时就在等待的客户:" << count << std::endl;
    int added{};
    int service_t{};
    while (added++ < count)
    {
        /* code */
        service_t = service_t_d(random_n);
        std::min_element(std::begin(checkouts), std::end(checkouts))->add(Customer(service_t));
        ++service_times[service_t - service_t_min];
    }

    size_t time{};                //存储时间
    const size_t total_time{600}; //模拟持续时间:分钟
    size_t longest_q{};           //存储最长的结帐队列长度

    //直到下一位顾客到来的周期
    int new_cust_interval{arrival_interval_d(random_n)};

    //运行以total_time分钟为周期的存储模拟
    while (time < total_time)           // 随时间变化的模拟循环
    {
        time++;                         // 增加一分钟

        // 当到达间隔为0时,新客户到达
        if (--new_cust_interval == 0)
        {
            service_t = service_t_d(random_n);              // 随机客户服务时间
            std::min_element(std::begin(checkouts), std::end(checkouts))->add(Customer(service_t));
            ++service_times[service_t - service_t_min];     // 记录服务时间

            // 出现的最长队列的更新记录
            for (auto &checkout : checkouts)
            {
                longest_q = std::max(longest_q, checkout.qlength());
            }    
            new_cust_interval = arrival_interval_d(random_n);
        }
        // 更新结帐时间—服务于每个队列中的第一个客户
        for (auto &checkout : checkouts)
            checkout.time_increment();
    }

    std::cout << "最大的队列长度 = " << longest_q << std::endl;
    std::cout << "\n 服务时间直方图:\n";

    histogram(service_times, service_t_min);

    std::cout << "\n 今天所有的顾客数量: "
              << std::accumulate(std::begin(service_times), std::end(service_times), 0)
              << std::endl;
}


注:

直接使用 using 指令可以减少代码输入,简化代码。

在头文件后面加入以下代码:

using namespace std;

顾客服务次数记录在 vector 中。服务时间减去 service_times 的最小值可以用来索引需要自增的 vector 元素,这导致 vector 的第一个元素会记录下最少服务时间的发生次数。histogram() 函数会以水平条形图的形式生成每个服务时间出现次数的柱状图。

输入的唯一数字是 checkouts。此处选择将模拟持续时间设置为 600 分钟,也可以用参数输入这个时间。main() 函数生成了顾客服务时间,超市开门时等在门外的顾客数,以及顾客到达时间间隔的分布对象。它表明顾客在同一时间到达。我们可以轻松地将这个程序扩展为每次到达的顾客数是一个处于一定范围内的随机数。

顾客总是可以被分配到最短的结账队列。通过调用 min_elemeiit() 算法可以找到最短的 Checkout 对象队列。这会使用<运算符比较元素,但是这个算法的另一个版本有第三个参数可以指定比较函数。在这次模拟开始前,当超市开门营业时,在门外等待的顾客的初始 序列被添加到 Checkout 对象中,然后服务时间记录被更新。

模拟在 while 循环中进行。在每次循环中,time 都会增加 1 分钟。在下一个顾客到达期间,new_cust_interval 会在每次循环中减小,直到等于 0。用新的随机服务时间生成新的顾客,然后把它加到最短的 Checkout 对象队列中。这个时候也会更新变量 longest_q,因为在添加新顾客后,可能出现新的最长队列。然后调用每个 Checkout 对象的 time_increment() 函数来处理队列中的第一个顾客。


  • 结果:在这里插入图片描述
    输入表示有 3 个结账柜台。将它们设为 2 个时,最长队列的长度达到 42——已经长到会让顾客放弃付款。还可以做更多改进,让模拟更加真实。均匀分配并不符合实际,例如,顾客通常成群结队到来。可以增加一些其他的因素,比如收银员休息时间、某个收银员生病工作状态不佳,这些都会导致顾客不选择这个柜台结账。

猜你喜欢

转载自blog.csdn.net/WandZ123/article/details/124954292