1 std::stack 概述
std::stack 是 C++ 标准模板库(STL)中的一种容器适配器,它模拟了栈(stack)这种数据结构,即一种后进先出(LIFO,Last In First Out)的数据结构。std::stack被设计为只提供栈的基本操作,包括push(添加元素到栈顶)、pop(从栈顶移除元素)、top(访问栈顶元素)等。
std::stack 的主要特点是其操作的限制性,即只能从栈顶进行存取操作。这种特性使得它在某些特定场景(如函数调用和递归等)中非常有用。在这些场景中,数据需要以特定的顺序进行存储和访问,而 std::stack 的 LIFO 特性正好满足这种需求。
1.1 std::stack 的内部实现
在 std::stack 中,一个底层容器(如 std::deque、std::vector 或 std::list)被用来存储栈中的元素。这个底层容器负责实际存储和管理元素,而 std::stack 则提供了对底层容器的操作接口,使得这些操作符合栈的LIFO特性。
具体来说,std::stack 的 push 操作将新元素添加到底层容器的尾部。由于栈是后进先出的数据结构,因此新添加的元素将位于栈顶。pop 操作则从底层容器的尾部移除元素,也就是移除栈顶的元素。top 操作则返回底层容器尾部的元素,即栈顶元素的引用。
为了实现对底层容器的封装和抽象,std::stack 的成员函数内部调用了底层容器的相应成员函数。这使得用户在使用 std::stack 时无需关心底层容器的具体实现细节,只需通过 std::stack 提供的接口进行操作即可。
注意:虽然 std::stack 的默认底层容器是 std::deque,但用户也可以通过模板参数指定其他类型的底层容器。不同的底层容器具有不同的性能特点和适用场景,因此用户可以根据具体需求选择合适的底层容器来优化 std::stack 的性能。
1.2 std::stack 的性能特点
std::stack 的性能特点主要取决于其底层容器的选择。由于 std::stack 本身是一个容器适配器,它并不直接存储元素,而是依赖于底层容器来执行实际的存储和访问操作。因此,底层容器的性能特性会直接影响到 std::stack 的性能。
在 C++ 标准模板库(STL)中,std::stack 的默认底层容器是 std::deque。std::deque 是一个双向队列,它在内存分配上采用分块策略,能够在头部和尾部进行高效的插入和删除操作。这使得 std::stack 在 push 和 pop 操作时具有优秀的性能,其时间复杂度通常可以达到 O(1),即常数时间复杂度。
然而,需要注意的是,std::stack 的性能并不只受底层容器类型的影响,还与具体的使用场景和操作频率有关。在一些对性能要求极高的场景中,比如高频次地执行 push 和 pop 操作的程序,即使使用性能优秀的底层容器,std::stack 的性能也可能成为瓶颈。在这种情况下,可能需要考虑使用更底层的数据结构或手动优化内存管理来进一步提升性能。
2 std::stack 的基本使用
2.1 std::stack 的声明与初始化
声明
首先,需要包含<stack>头文件以使用 std::stack:
#include <stack>
#include <string>
// 声明一个整数类型的 stack
std::stack<int> vals;
// 声明一个字符串类型 stack
std::stack<std::string> strs;
// 声明一个自定义类型的 stack
struct MyStruct
{
int id;
std::string name;
};
std::stack<MyStruct> myStructs;
初始化
可以使用多种方法来初始化 std::stack。
(1)默认初始化:
如果不提供任何参数,std::stack 会使用默认构造函数进行初始化。这意味着它会使用其底层容器(默认为 std::deque)的默认构造函数。
std::stack<int> s;
(2)使用 std::deque 进行初始化:
虽然 std::stac 不支持初始化列表,但可以使用以初始化列表初始化的 std::deque<int> 来进行初始化。
std::stack<int> s(std::deque<int>{1, 2, 3, 4, 5}); // 使用 std::deque<int> 初始化栈 s
(3)复制另一个栈:
可以使用另一个 std::stack 的副本来初始化一个新的栈。
std::stack<int> s1(std::deque<int>{1, 2, 3, 4, 5});
std::stack<int> s2(s1); // 使用s1的内容初始化s2
(4)移动另一个栈:
C++11 及更高版本还支持移动语义,这意味着可以转移另一个栈的内容来初始化新的栈,而不需要复制元素。
std::stack<int> s1(std::deque<int>{1, 2, 3, 4, 5});
std::stack<int> s2(std::move(s1)); // 使用 s1 的内容(通过移动)初始化 s2,s1 现在为空
(5)指定底层容器:
虽然不常见,但可以通过指定底层容器来初始化 std::stack。这要求提供一个容器对象,该对象将用作栈的底层存储。
std::list<int> l = {1, 2, 3, 4, 5};
std::stack<int, std::list<int>> s(l); // 使用 list l 作为底层容器初始化栈 s
2.2 std::stack 的大小与容量
(1)大小(size)
std::stack 的大小是指栈中当前存储的元素数量。可以使用 std::stack 的 size 成员函数来获取栈的大小。例如:
std::stack<int> s;
s.push(1);
s.push(2);
s.push(3);
std::size_t size = s.size(); // size 现在是 3,因为栈中有 3 个元素
这个例子向栈中添加了三个元素,并使用 size 成员函数获取栈的大小。
(2)容量(capacity)
容量通常指的是底层容器在没有重新分配内存的情况下可以容纳的元素数量。然而,std::stack 本身并不直接提供获取容量的成员函数,因为 std::stack 是一个适配器,它依赖于其底层容器来处理容量和内存分配。
如果需要知道 std::stack 的底层容器的容量,可以使用底层容器的 capacity 成员函数(如果底层容器提供了这样的函数)。但是,直接访问 std::stack 的底层容器通常是不被鼓励的,因为这破坏了栈的封装性。
在实践中,通常不需要关心 std::stack 的容量,因为 std::stack 会自动管理其底层容器的内存分配。当你向栈中添加元素时,如果底层容器的当前容量不足以容纳新元素,它会自动增长。同样地,当从栈中删除元素时,底层容器可能会释放一些未使用的内存,但这取决于底层容器的实现。
如果确实需要管理栈的容量(例如,为了优化内存使用),可能需要直接使用底层容器(如 std::deque 或 std::vector),而不是使用 std::stack。但要注意的是,这样做将失去 std::stack 提供的栈特定操作的便利性。
2.3 std::stack 的构造函数与析构函数
(1)构造函数
std::stack 提供了多个构造函数,以便在不同的情况下灵活地初始化栈。以下是一些主要的构造函数:
默认构造函数:
std::stack<Type> st;
此构造函数创建一个空的栈,其底层容器使用默认构造函数进行初始化。这里的 Type 是栈中元素的类型。
拷贝构造函数:
std::stack<Type> st1(st2);
此构造函数使用另一个栈 st2 的内容来初始化新的栈 st1。它复制 st2 中的所有元素到 st1 中。
移动构造函数(C++11 及更高版本):
std::stack<Type> st1(std::move(st2));
此构造函数通过移动另一个栈 st2 的内容来初始化新的栈 st1。这意味着 st2 在移动操作后不再包含其原始元素,这些元素的所有权现在属于 st1。使用移动构造函数通常比使用拷贝构造函数更高效,因为它可以避免不必要的元素复制。
(2)析构函数
当 std::stack 对象的生命周期结束时,其析构函数会被自动调用。析构函数负责清理栈所占用的资源,包括释放底层容器的内存。注意不需要显式地调用析构函数,因为 C++ 的自动存储期管理会处理这些细节。
例如:
{
std::stack<int> st;
// ... 在这里使用栈 ...
} // 在这里,当 st 离开其作用域时,其析构函数会自动被调用
在上面的代码中,当 st 离开其作用域时,其析构函数会被自动调用,从而释放栈所占用的资源。
3 std::stack 的元素操作
3.1 入栈操作(push)
入栈操作使用 push 成员函数,它接受一个参数,即要添加到栈顶的元素。例如:
std::stack<int> s;
s.push(1); // 将整数 1 压入栈中
s.push(2); // 将整数 2 压入栈中
这个例子创建了一个 int 类型的栈 s,并使用 push 函数将两个整数依次压入栈中。
3.2 出栈操作(pop)
出栈操作使用 pop 成员函数,它移除栈顶的元素,但不返回该元素的值。例如:
std::stack<int> s;
s.push(1);
s.push(2);
s.pop(); // 移除栈顶元素 2,但不返回它
这个例子创建了一个栈 s 并压入两个整数。然后,使用 pop 函数移除了栈顶的元素 2。
3.3 查看栈顶元素(top)
查看栈顶元素使用 top 成员函数,它返回栈顶元素的引用,但不移除该元素。例如:
std::stack<int> s;
s.push(1);
s.push(2);
int topElement = s.top(); // 获取栈顶元素,此时 topElement 的值为 2
这个例子创建了一个栈 s 并压入两个整数。然后,使用 top 函数获取了栈顶的元素,并将其值存储在 topElement 变量中。
3.4 检查栈是否为空(empty)
检查栈是否为空使用 empty 成员函数,如果栈为空,则返回 true;否则返回 false。例如:
std::stack<int> s;
bool isEmpty = s.empty(); // isEmpty 的值为 true,因为栈是空的
s.push(1);
isEmpty = s.empty(); // isEmpty 的值为 false,因为栈不再为空
这个例子首先创建了一个空的栈 s,并使用 empty 函数检查其是否为空。然后,压入一个整数并再次检查栈是否为空。
3.5 栈的交换(swap)
可以使用 swap 成员函数来交换两个栈的内容。例如:
std::stack<int> s1, s2;
s1.push(1);
s1.push(2);
s2.push(3);
s2.push(4);
s1.swap(s2); // 交换 s1 和 s2 的内容
在这个例子中,s1 原本包含元素 1 和 2,s2 包含元素 3 和 4。调用 swap 后,s1 将包含元素 3 和 4,而 s2 将包含元素 1 和 2。
3.6 底层容器的访问
虽然直接访问 std::stack 的底层容器通常是不推荐的(因为它破坏了栈的封装性),但 STL 仍然提供了某种程度的访问能力。可以使用 _Get_container 成员函数来获取底层容器的引用。注意:应该非常小心地使用这个功能,并只在确实需要时才使用它。
std::stack<int> s;
auto& underlyingDeque = s._Get_container(); // 获取底层 deque 的引用(注意:这通常不是好的做法)
4 std::stack 的删除操作
std::stack 是一个后进先出(LIFO)的数据结构,其设计初衷是提供基本的栈操作,如 push(压入元素)、pop(弹出元素)、top(查看栈顶元素)等。然而,std::stack 并没有直接提供删除栈中特定元素的操作,这是因为它保持了栈的简单性和一致性。
如果需要删除栈中的特定元素,那么可能需要考虑其他的数据结构,如 std::deque 或 std::list,它们提供了更多的元素操作功能。但如果仍然想要使用 std::stack 并删除其中的元素,那么可以通过以下方式间接实现:
(1)弹出元素直到找到并删除目标元素:
可以通过连续调用 pop 函数,直到找到并删除目标元素。但是,这种方法会破坏栈的结构,因为它会移除栈顶的所有元素,直到找到目标元素为止。这通常不是推荐的做法,因为它违反了栈的 LIFO 原则。
std::stack<int> s;
s.push(1);
s.push(2);
s.push(3);
s.push(4);
std::stack<int> sTmp;
int target = 3;
bool found = false;
while (!s.empty()) {
int top = s.top();
s.pop();
if (top != target) {
sTmp.push(top); // 将非目标元素重新压入栈中
}
}
s.swap(sTmp);
这个例子试图删除值为 3 的元素。通过循环不断地从栈顶弹出元素,检查它是否是想要删除的目标,如果不是,则将其重新压入备用栈中。这种方法效率很低,特别是当栈很大且目标元素靠近栈底时。
(2)使用其他数据结构辅助:
另一种方法是使用一个辅助的数据结构(如 std::vector 或 std::deque)来存储栈中的元素,然后在这个辅助数据结构中删除目标元素,最后再将辅助数据结构中的元素重新压入栈中。这种方法同样会破坏栈的结构,并且效率也不高。
(3)避免需要删除操作:
最好的方法是避免在 std::stack 中进行删除操作。在设计程序时,尽量确保你不需要从栈中删除特定的元素。如果确实需要这种功能,那么可能需要考虑使用其他更适合的数据结构。