1 std::queue 概述
std::queue 是 C++ 标准模板库(STL)中的一种容器适配器,它提供了队列(Queue)这种数据结构的功能。队列是一种特殊的线性表,它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作。因此,队列具有先进先出(FIFO)的特性。
1.1 std::queue 的内部实现
std::queue 的内部实现通常基于其他容器,如 std::deque(双端队列)或 std::list。这种实现方式使得 std::queue 能够提供队列(Queue)这种数据结构的功能,即只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,从而实现先进先出(FIFO)的特性。
(1)基于 std::deque 的实现:
当 std::queue 基于 std::deque 实现时,其内部存储机制利用了 deque 的双向操作特性。deque 允许在其头部和尾部都进行高效的插入和删除操作,这使得它非常适合作为队列的底层容器。
在这种实现中,std::queue 的 push 操作会将元素添加到 deque 的尾部,而 pop 操作会从 deque 的头部删除元素。front 和 back 操作则分别返回 deque 的头部和尾部元素。
由于 deque 提供了直接访问其头部和尾部的接口,因此这种实现方式通常具有较高的性能。
(2)基于 std::list 的实现:
当 std::queue 基于 std::list 实现时,其内部存储机制利用了 list 的双向链表结构。虽然 list 在任意位置进行插入和删除操作都较为高效,但作为队列使用时,主要的操作还是集中在头部和尾部。
在这种实现中,std::queue 的 push 操作会将元素添加到 list 的尾部,而 pop 操作会从 list 的头部删除元素。与基于 deque 的实现类似,front 和 back 操作分别返回 list 的头部和尾部元素。
然而,需要注意的是,虽然 list 允许在任意位置进行插入和删除操作,但在作为队列使用时,其性能可能不如基于 deque 的实现,因为 deque 在头部和尾部的操作通常更加高效。
注意:没有明确指定 std::queue 的底层容器类型时,它默认使用 std::deque
1.2 std::queue 的性能特点
std::queue 的性能特点主要源于其内部使用的容器类型,通常是 std::deque 或 std::list。
(1)基于std::deque的性能特点:
当 std::queue 基于 std::deque 实现时,它通常能够展现出非常优秀的性能。这是因为 std::deque 是一个双向队列,它允许在两端进行高效的插入和删除操作。因此,在 std::queue 中,使用 push 在尾部插入元素和使用 pop 在头部删除元素的操作通常都非常快。
此外,std::deque 通常是以固定大小的块来存储元素,这种存储方式减少了内存分配和释放的次数,从而提高了性能。这也使得 std::queue 在处理大量元素时能够保持稳定的性能。
(2)基于std::list的性能特点:
虽然 std::queue 也可以基于 std::list 实现,但相比基于 std::deque 的实现,其性能可能稍逊一筹。std::list 是一个双向链表,虽然它在任意位置插入和删除元素都比较高效,但相对于 std::deque,它在头部和尾部进行操作的性能可能稍差。
具体来说,std::list 中的元素是分散在内存中的,这可能导致缓存不命中(cache misses)的问题,从而降低性能。此外,由于链表需要维护指针或迭代器来跟踪元素的位置,这也可能增加一些额外的开销。
(3)时间复杂度:
对于 std::queue 的基本操作,如 push、pop、front 和 back,其时间复杂度通常都是常数时间 O(1)。这意味着无论队列中有多少元素,这些操作所需的时间都是固定的。
然而,需要注意的是,虽然这些基本操作的时间复杂度是常数,但在实际应用中,性能还可能受到其他因素的影响,如内存分配、缓存行为以及并发访问等。
(4)实际应用中的考虑:
在选择使用 std::queue 时,通常不需要过多关心其底层实现和性能特点。因为标准库已经提供了优化过的实现,并且在大多数情况下都能够满足性能需求。然而,在一些对性能要求非常高的场景下,可能需要考虑使用更底层的容器或自定义数据结构来替代 std::queue。
2 std::queue 的基本使用
2.1 std::queue 的声明与初始化
声明
首先,需要包含<queue>头文件以使用 std::queue:
#include <queue>
#include <string>
// 声明一个整数类型的 queue
std::queue<int> vals;
// 声明一个字符串类型 queue
std::queue<std::string> strs;
// 声明一个自定义类型的 queue
struct MyStruct
{
int id;
std::string name;
};
std::queue<MyStruct> myStructs;
初始化
可以使用多种方法来初始化 std::queue。
(1)默认初始化:
如果不提供任何参数,std::queue 会使用默认构造函数进行初始化。这意味着它会使用其底层容器(默认为 std::deque)的默认构造函数。
std::queue<int> q;
(2)使用 std::deque 进行初始化:
虽然 std::deque 不支持初始化列表,但可以使用以初始化列表初始化的 std::deque<int> 来进行初始化。
std::queue<int> q(std::deque<int>{1, 2, 3, 4, 5}); // 使用 std::deque<int> 初始化队列 q
(3)复制另一个队列:
可以使用另一个 std::queue 的副本来初始化一个新的队列。
std::queue<int> q1(std::deque<int>{1, 2, 3, 4, 5});
std::queue<int> q2(q1); // 使用q1的内容初始化q2
(4)移动另一个队列:
C++11 及更高版本还支持移动语义,这意味着可以转移另一个队列的内容来初始化新的队列,而不需要复制元素。
std::queue<int> q1 = {1, 2, 3, 4, 5};
std::queue<int> q2(std::move(q1)); // 使用 q1 的内容(通过移动)初始化 q2,q1 现在为空
(5)指定底层容器:
虽然不常见,但可以通过指定底层容器来初始化 std::queue。这要求提供一个容器对象,该对象将用作队列的底层存储。
std::list<int> l = {1, 2, 3, 4, 5};
std::queue<int, std::list<int>> q(l); // 使用 list l 作为底层容器初始化队列 q
2.2 std::queue 的大小与容量
(1)大小(size)
std::queue 的大小是指队列中当前存储的元素数量。可以使用 std::queue 的 size 成员函数来获取队列的大小。例如:
std::queue<int> q;
q.push(1);
q.push(2);
q.push(3);
std::size_t size = q.size(); // size 现在是 3,因为队列中有 3 个元素
这个例子向队列中添加了三个元素,并使用 size 成员函数获取队列的大小。
(2)容量(capacity)
与 std::vector 或 std::deque 不同,std::queue 没有直接提供获取其“容量”的成员函数。容量通常指的是容器在不进行内存重新分配的情况下可以容纳的元素数量。由于 std::queue 的设计是为了提供队列操作的接口,并且隐藏了其底层容器的实现细节,因此它并不直接暴露底层容器的容量信息。
如果需要了解底层容器的容量信息,可能需要直接操作底层容器,但这通常不是使用 std::queue 的推荐做法,因为它违反了队列的抽象和封装原则。
2.3 std::queue 的构造函数与析构函数
(1)构造函数
std::queue 提供了多个构造函数,以便在不同的情况下灵活地初始化队列。以下是一些主要的构造函数:
默认构造函数:
std::queue<Type> q;
此构造函数创建一个空的队列,其底层容器使用默认构造函数进行初始化。这里的 Type 是队列中元素的类型。
拷贝构造函数:
std::queue<Type> q1(q2);
此构造函数使用另一个队列 q2 的内容来初始化新的队列 q1。它复制 q2 中的所有元素到 q1 中。
移动构造函数(C++11 及更高版本):
std::queue<Type> q1(std::move(q2));
此构造函数通过移动另一个队列 q2 的内容来初始化新的队列 q1。这意味着 q2 在移动操作后不再包含其原始元素,这些元素的所有权现在属于 q1。使用移动构造函数通常比使用拷贝构造函数更高效,因为它可以避免不必要的元素复制。
(2)析构函数
当 std::queue 对象的生命周期结束时,其析构函数会被自动调用。析构函数负责清理队列所占用的资源,包括释放底层容器的内存。注意不需要显式地调用析构函数,因为 C++ 的自动存储期管理会处理这些细节。
例如:
{
std::queue<int> q;
// ... 在这里使用队列 ...
} // 在这里,当 q 离开其作用域时,其析构函数会自动被调用
在上面的代码中,当 q 离开其作用域时,其析构函数会被自动调用,从而释放队列所占用的资源。
3 std::queue 的元素操作
3.1 入队列操作(push)
入队列操作使用 push 成员函数,它接受一个参数,即要添加到队列顶的元素。例如:
std::queue<int> q;
q.push(1); // 将整数 1 压入队列中
q.push(2); // 将整数 2 压入队列中
这个例子创建了一个 int 类型的队列 q,并使用 push 函数将两个整数依次压入队列中。
3.2 出队列操作(pop)
出队列操作使用 pop 成员函数,它移除队列顶的元素,但不返回该元素的值。例如:
std::queue<int> q;
q.push(1);
q.push(2);
q.pop(); // 移除队列顶元素 1,但不返回它
这个例子创建了一个队列 q 并压入两个整数。然后,使用 pop 函数移除了队列头部的元素 1。
3.3 查看队列头部元素(front)
查看队列头部元素使用 front 成员函数,它返回队列头部元素的引用,但不移除该元素。例如:
std::queue<int> q;
q.push(1);
q.push(2);
int frontElement = q.front(); // 获取队列头部元素,此时 frontElement 的值为 1
这个例子创建了一个队列 q 并压入两个整数。然后,使用 front 函数获取了队列头部的元素,并将其值存储在 frontElement 变量中。
3.4 查看队列尾部元素(back)
查看队列尾部元素使用 back 成员函数,它返回队列尾部元素的引用,但不移除该元素。例如:
std::queue<int> q;
q.push(1);
q.push(2);
int backElement = q.back(); // 获取队列尾部元素,此时 backElement 的值为 2
这个例子创建了一个队列 q 并压入两个整数。然后,使用 back 函数获取了队列尾部的元素,并将其值存储在 backElement 变量中。
3.5 检查队列是否为空(empty)
检查队列是否为空使用 empty 成员函数,如果队列为空,则返回 true;否则返回 false。例如:
std::queue<int> q;
bool isEmpty = q.empty(); // isEmpty 的值为 true,因为队列是空的
q.push(1);
isEmpty = q.empty(); // isEmpty 的值为 false,因为队列不再为空
这个例子首先创建了一个空的队列 q,并使用 empty 函数检查其是否为空。然后,压入一个整数并再次检查队列是否为空。
3.6 队列的交换(swap)
可以使用 swap 成员函数来交换两个队列的内容。例如:
std::queue<int> q1, q2;
q1.push(1);
q1.push(2);
q2.push(3);
q2.push(4);
q1.swap(q2); // 交换 q1 和 q2 的内容
在这个例子中,q1 原本包含元素 1 和 2,q2 包含元素 3 和 4。调用 swap 后,q1 将包含元素 3 和 4,而 q2 将包含元素 1 和 2。
3.6 底层容器的访问
虽然直接访问 std::queue 的底层容器通常是不推荐的(因为它破坏了队列的封装性),但 STL 仍然提供了某种程度的访问能力。可以使用 _Get_container 成员函数来获取底层容器的引用。注意:应该非常小心地使用这个功能,并只在确实需要时才使用它。
std::queue<int> q;
auto& underlyingDeque = q._Get_container(); // 获取底层 deque 的引用(注意:这通常不是好的做法)
4 std::queue 的删除操作
std::queue 是一个后进先出(FIFO)的数据结构,其设计初衷是提供基本的队列操作,如 push(压入元素)、pop(弹出元素)、top(查看队列顶元素)等。然而,std::queue 并没有直接提供删除队列中特定元素的操作,这是因为它保持了队列的简单性和一致性。
如果需要删除队列中的特定元素,那么可能需要考虑其他的数据结构,如 std::deque 或 std::list,它们提供了更多的元素操作功能。但如果仍然想要使用 std::queue 并删除其中的元素,那么可以通过以下方式间接实现:
(1)弹出元素直到找到并删除目标元素:
可以通过连续调用 pop 函数,直到找到并删除目标元素。但是,这种方法会破坏队列的结构,因为它会移除队列顶的所有元素,直到找到目标元素为止。这通常不是推荐的做法,因为它违反了队列的 FIFO 原则。
std::queue<int> q;
q.push(1);
q.push(2);
q.push(3);
q.push(4);
std::queue<int> qTmp;
int target = 3;
bool found = false;
while (!q.empty()) {
int front = q.front();
q.pop();
if (front != target) {
qTmp.push(front); // 将非目标元素重新压入队列中
}
}
q.swap(qTmp);
这个例子试图删除值为 3 的元素。通过循环不断地从队列顶弹出元素,检查它是否是想要删除的目标,如果不是,则将其重新压入备用队列中。这种方法效率很低,特别是当队列很大且目标元素靠近队列底时。
(2)使用其他数据结构辅助:
另一种方法是使用一个辅助的数据结构(如 std::vector 或 std::deque)来存储队列中的元素,然后在这个辅助数据结构中删除目标元素,最后再将辅助数据结构中的元素重新压入队列中。这种方法同样会破坏队列的结构,并且效率也不高。
(3)避免需要删除操作:
最好的方法是避免在 std::queue 中进行删除操作。在设计程序时,尽量确保你不需要从队列中删除特定的元素。如果确实需要这种功能,那么可能需要考虑使用其他更适合的数据结构。