stack(理解使用和模拟实现)
- 什么是stack
- stack的使用
- 1. stack构造函数
- 2.empty()
- 3.size()
- 3.top()
- 4.push
- 5.emplace
- 6.pop()
- 7.swap
- 模拟实现stack的准备
- 1.什么是容器适配器?
- 2. deque的简单介绍
- 3. deque的缺陷
- 4. 为什么STL中stack和queue默认使用deque?
- 模拟实现stack
- 结语
什么是stack
stack
是一种容器适配器,专门用在具有后进先出操作的上下文环境中,其删除只能从容器的一端进行元素的插入与提取操作。stack
是作为容器适配器被实现的,容器适配器即是对特定类封装作为其底层的容器,并提供一组特定
的成员函数来访问其元素,将特定类作为其底层的,元素特定容器的尾部(即栈顶)被压入和弹出。stack
的底层容器可以是任何标准的容器类模板或者一些其他特定的容器类,这些容器类应该支持以下
操作:
empty:判空操作
back:获取尾部元素操作
push_back:尾部插入元素操作
pop_back:尾部删除元素操作
- 标准容器
vector、deque、list
均符合这些需求,默认情况下,如果没有为stack
指定特定的底层容器,默认情况下使用deque
。
stack的使用
1. stack构造函数
这些是C++标准库中std::stack类的构造函数声明。std::stack是一个适配器容器,它可以使用不同的底层容器来实现栈的功能。这些构造函数声明提供了不同的方式来创建和初始化std::stack对象,可以根据需求选择合适的构造函数。
这里的各种构造函数选项包括不同的初始化方式、移动初始化、使用不同的分配器(allocator)以及结合不同的容器类型等。你可以根据需要选择适当的构造函数来创建std::stack对象。
这些构造函数的使用方式与普通类的构造函数相似,你可以使用这些构造函数来实例化std::stack对象,并为其提供合适的参数,比如底层容器、分配器等。
你可以使用以下构造函数之一:
initialize (1):
std::stack<int> myStack1; // 使用默认构造函数,默认底层容器是 std::deque
move-initialize (2):
std::stack<int> sourceStack;
sourceStack.push(1);
sourceStack.push(2);
sourceStack.push(3);
std::stack<int> myStack2(std::move(sourceStack)); // 使用移动构造函数,sourceStack中的元素移动到myStack2
allocator (3):
std::allocator<int> myAllocator;
std::stack<int, std::vector<int, std::allocator<int>>> myStack3(myAllocator); // 使用指定的分配器
init + allocator (4):
std::vector<int> vec4 = {1, 2, 3, 4, 5};
std::allocator<int> myAllocator4;
std::stack<int, std::vector<int, std::allocator<int>>> myStack4(vec4, myAllocator4); // 使用指定的容器和分配器
move-init + allocator (5):
std::vector<int> vec5 = {5, 4, 3, 2, 1};
std::allocator<int> myAllocator5;
std::stack<int, std::vector<int, std::allocator<int>>> myStack5(std::move(vec5), myAllocator5); // 使用移动构造函数,同时指定容器和分配器
copy + allocator (6):
std::stack<int> sourceStack6;
sourceStack6.push(1);
sourceStack6.push(2);
sourceStack6.push(3);
std::allocator<int> myAllocator6;
std::stack<int, std::vector<int, std::allocator<int>>> myStack6(sourceStack6, myAllocator6); // 复制构造函数,同时指定分配器
move + allocator (7):
std::stack<int> sourceStack7;
sourceStack7.push(7);
sourceStack7.push(8);
sourceStack7.push(9);
std::allocator<int> myAllocator7;
std::stack<int, std::vector<int, std::allocator<int>>> myStack7(std::move(sourceStack7), myAllocator7); // 使用移动构造函数,同时指定分配器
这些示例基于不同的构造函数变体,以不同的方式初始化了 std::stack
对象,包括使用不同的容器类型和分配器。具体的使用情景会根据你的代码需求而有所不同。
2.empty()
bool empty() const;
是 C++ 标准库中 std::stack
类的成员函数之一。它用于判断栈是否为空。
返回类型:bool
,表示栈是否为空,如果栈为空则返回 true
,否则返回 false
。
const
关键字表示这个函数不会修改类的成员变量。
你可以在实际使用中,通过调用这个函数来检查栈是否为空,以便进行相应的操作。以下是一个示例:
#include <iostream>
#include <stack>
int main() {
std::stack<int> myStack;
if (myStack.empty()) {
std::cout << "Stack is empty." << std::endl;
} else {
std::cout << "Stack is not empty." << std::endl;
}
myStack.push(42);
if (myStack.empty()) {
std::cout << "Stack is empty." << std::endl;
} else {
std::cout << "Stack is not empty." << std::endl;
}
return 0;
}
在上述示例中,我们先创建一个空的栈 myStack
,然后通过 empty()
函数来检查栈是否为空。在栈为空时输出 "Stack is empty."
,否则输出 "Stack is not empty."
。之后,我们将一个元素压入栈中,并再次检查栈的状态。输出应该是 "Stack is not empty."
。
3.size()
size_type size() const;
是 C++ 标准库中 std::stack
类的成员函数之一。它用于返回当前栈中元素的数量(大小)。
返回类型:size_type
,通常是一个无符号整数类型,表示栈中元素的数量。
const
关键字表示这个函数不会修改类的成员变量。
你可以在实际使用中,通过调用这个函数来获取栈中元素的数量。以下是一个示例:
#include <iostream>
#include <stack>
int main() {
std::stack<int> myStack;
std::cout << "Initial size: " << myStack.size() << std::endl;
myStack.push(42);
myStack.push(15);
myStack.push(7);
std::cout << "Size after pushing elements: " << myStack.size() << std::endl;
myStack.pop();
std::cout << "Size after popping an element: " << myStack.size() << std::endl;
return 0;
}
在上述示例中,我们首先创建一个空的栈 myStack
,然后使用 size()
函数获取栈的大小。之后,我们将三个元素压入栈中,再次使用 size()
函数获取栈的大小。接着,我们弹出一个元素,并再次使用 size()
函数获取栈的大小。输出应该是栈的大小在不同操作后的变化。
3.top()
reference top();
和 const_reference top() const;
是 C++ 标准库中 std::stack
类的成员函数之一。它们用于获取栈顶元素的引用。
reference top();
:返回栈顶元素的引用。如果需要修改栈顶元素,可以使用这个版本。
const_reference top() const;
:返回栈顶元素的常量引用。如果你不需要修改栈顶元素,而只是需要读取它,可以使用这个版本。
在这两个版本中,const
关键字表示在函数内部不会修改类的成员变量,确保在使用 top()
函数的过程中不会发生意外的修改。
以下是一个示例:
#include <iostream>
#include <stack>
int main() {
std::stack<int> myStack;
myStack.push(42);
myStack.push(15);
// 使用 top() 获取栈顶元素
int topElement = myStack.top();
std::cout << "Top element: " << topElement << std::endl;
// 修改栈顶元素
myStack.top() = 99;
std::cout << "New top element: " << myStack.top() << std::endl;
return 0;
}
在上述示例中,我们首先将两个元素压入栈中,然后使用 top()
函数获取栈顶元素,并输出它。接着,我们使用 top()
函数修改了栈顶元素的值,然后再次使用 top()
函数获取新的栈顶元素,并输出它。注意,为了修改栈顶元素,我们使用了 top()
函数的第一个版本。
4.push
void push(const value_type& val);
和 void push(value_type&& val);
是 C++ 标准库中 std::stack
类的成员函数之一。它们用于将一个新的元素压入栈中。
void push(const value_type& val);
:将传入的值的常量引用const value_type&
压入栈中。这个版本用于传递一个常量引用的值,不会修改传入的值。
void push(value_type&& val);
:将传入的值的右值引用value_type&&
压入栈中。这个版本用于传递一个右值引用的值,通常用于移动语义,可能会修改传入的值。
这两个版本的 push 函数允许你在栈顶添加新的元素。如果需要保持传入值的不变性,可以使用第一个版本;如果你想利用移动语义来避免不必要的复制,可以使用第二个版本。
以下是一个示例:
#include <iostream>
#include <stack>
int main() {
std::stack<int> myStack;
myStack.push(42); // 使用右值,将 42 压入栈中
myStack.push(15);
int newElement = 99;
myStack.push(newElement); // 使用常量引用,将 newElement 压入栈中
std::cout << "Stack size: " << myStack.size() << std::endl;
while (!myStack.empty()) {
std::cout << myStack.top() << " "; // 输出栈顶元素
myStack.pop(); // 弹出栈顶元素
}
return 0;
}
在上述示例中,我们使用不同版本的 push
函数将元素压入栈中,包括使用右值42
和常量引用newElement
。然后,我们循环输出栈中的元素,并弹出栈顶元素,以清空栈。最终输出的应该是栈中元素的值。
5.emplace
template <class... Args> void emplace(Args&&... args);
是 C++ 标准库中 std::stack
类的成员函数之一。它用于在栈顶处构造一个新的元素,使用传递的参数来进行构造。
Args
:可变模板参数,用于传递构造元素所需的参数列表。
args
:完美转发(perfect forwarding)的参数包,用于构造元素。
这个函数利用了完美转发,可以将传入的参数包 args
在构造新元素时进行适当的转发,从而可以使用不同的构造函数来创建元素。
以下是一个示例:
#include <iostream>
#include <stack>
class MyClass {
public:
int value;
MyClass(int v) : value(v) {
std::cout << "Constructor with int: " << value << std::endl;
}
};
int main() {
std::stack<MyClass> myStack;
myStack.emplace(42); // 使用传入的参数构造 MyClass 对象
myStack.emplace(15);
std::cout << "Stack size: " << myStack.size() << std::endl;
while (!myStack.empty()) {
std::cout << myStack.top().value << " "; // 输出栈顶元素的 value
myStack.pop(); // 弹出栈顶元素
}
return 0;
}
在上述示例中,我们首先定义了一个名为 MyClass
的类,该类具有一个带有整数参数的构造函数。然后,我们使用 emplace
函数将不同的整数值作为参数传递给构造函数,以构造 MyClass
对象。最终,我们输出栈中元素的 value
成员,并弹出栈顶元素,以清空栈。
6.pop()
void pop();
是 C++ 标准库中 std::stack
类的成员函数之一。它用于将栈顶元素弹出(删除)。
这个函数没有返回值,它只是从栈中移除栈顶元素。在调用 pop()
函数之前,需要确保栈不为空,否则会导致未定义行为。
以下是一个示例:
#include <iostream>
#include <stack>
int main() {
std::stack<int> myStack;
myStack.push(42);
myStack.push(15);
myStack.push(7);
std::cout << "Stack size before popping: " << myStack.size() << std::endl;
myStack.pop(); // 弹出栈顶元素
std::cout << "Stack size after popping: " << myStack.size() << std::endl;
return 0;
}
在上述示例中,我们首先将三个元素压入栈中,然后输出栈的大小。接着,我们调用 pop()
函数弹出一个栈顶元素,并再次输出栈的大小。输出应该是弹出元素后的栈大小。
7.swap
void swap(stack& x) noexcept(/*see below*/);
是 C++ 标准库中 std::stack
类的成员函数之一。它用于交换两个栈的内容,使得两个栈中的元素互换。
x
:另一个栈对象的引用,与当前栈对象交换内容。
这个函数没有返回值,它会交换当前栈对象和传入的栈对象 x
的内容。需要注意的是,交换操作并不会改变栈的容器类型或分配器,只是交换两个栈中的元素。
关于 noexcept
:noexcept
是异常规范说明符,用于表示函数不会抛出异常。在这个函数的声明中,noexcept
后面可能会有一些条件,表示函数是否在某些情况下可能会抛出异常。如果没有特定的条件,可以简单地写成 noexcept
,表示该函数不会抛出异常。
以下是一个示例:
#include <iostream>
#include <stack>
int main() {
std::stack<int> stack1;
std::stack<int> stack2;
stack1.push(1);
stack1.push(2);
stack1.push(3);
stack2.push(4);
stack2.push(5);
std::cout << "Before swapping:" << std::endl;
std::cout << "Stack 1: ";
while (!stack1.empty()) {
std::cout << stack1.top() << " ";
stack1.pop();
}
std::cout << std::endl;
std::cout << "Stack 2: ";
while (!stack2.empty()) {
std::cout << stack2.top() << " ";
stack2.pop();
}
std::cout << std::endl;
stack1.push(6);
stack1.push(7);
stack1.swap(stack2); // 交换栈的内容
std::cout << "After swapping:" << std::endl;
std::cout << "Stack 1: ";
while (!stack1.empty()) {
std::cout << stack1.top() << " ";
stack1.pop();
}
std::cout << std::endl;
std::cout << "Stack 2: ";
while (!stack2.empty()) {
std::cout << stack2.top() << " ";
stack2.pop();
}
std::cout << std::endl;
return 0;
}
在上述示例中,我们首先创建了两个栈对象 stack1
和 stack2
,然后向它们分别添加一些元素。然后,我们使用 swap
函数交换了这两个栈的内容。在交换后,输出应该显示交换前后栈中元素的变化。
std::stack
的 swap
函数可以在不同的容器类型之间进行交换,但是有一些限制需要注意。
std::stack
的 swap
函数内部会调用其底层容器的 swap
函数来实现元素的交换。因此,如果底层容器类型不同,它们的 swap
函数也必须是兼容的。通常情况下,C++ 标准库中的容器都会提供支持不同容器类型间交换的 swap
函数,但在使用自定义容器或第三方容器库时需要注意。
总之,在标准情况下,如果你使用的是标准库中的容器(如 std::vector、std::deque、std::list
等),那么在不同容器类型之间调用 std::stack
的 swap
函数是安全的。但是,如果使用自定义的容器类型,需要确保这些容器的 swap 函数支持交换到其他容器类型。
模拟实现stack的准备
1.什么是容器适配器?
虽然stack
和queue
中也可以存放元素,但在STL
中并没有将其划分在容器的行列,而是将其称为容器适配器,这是因为stack
和队列只是对其他容器的接口进行了包装,STL
中stack
和queue
默认使用deque
容器适配器(Container Adapters)是 C++ 标准库提供的一种数据结构,它们基于现有的容器类型,提供了特定的接口和功能,以便更方便地实现某些特定的数据结构和算法。容器适配器本质上是对底层容器的封装,提供了不同的数据访问方式,使它们适用于特定的用途。
标准库中提供了三种常用的容器适配器:
stack
:栈适配器,基于底层容器提供了栈数据结构的操作,如压入(push)、弹出(pop)、查看栈顶元素等。默认底层容器是std::deque
,但也可以使用其他支持back()
和push_back()
操作的容器。
queue
:队列适配器,基于底层容器提供了队列数据结构的操作,如入队(push)、出队(pop)、查看队首元素等。默认底层容器是std::deque
,但也可以使用其他支持back()
和push_back()
操作的容器。
priority_queue
:优先队列适配器,基于底层容器提供了优先队列数据结构的操作,支持在插入元素时根据优先级进行排序。默认底层容器是std::vector
,但也可以使用其他支持随机访问和插入操作的容器。
2. deque的简单介绍
deque
(双端队列):是一种双开口的"连续"空间的数据结构,双开口的含义是:可以在头尾两端进行插入和删除操作,且时间复杂度为O(1)
,与vector
比较,头插效率高,不需要搬移元素;与list
比较,空间利用率比较高。
deque
并不是真正连续的空间,而是由一段段连续的小空间拼接而成的,实际deque
类似于一个动态的二维数组,其底层结构如下图所示:
双端队列底层是一段假象的连续空间,实际是分段连续的,为了维护其“整体连续”以及随机访问的假象,落在了deque
的迭代器身上,因此deque
的迭代器设计就比较复杂,如下图所示:
那deque是如何借助其迭代器维护其假想连续的结构呢?
3. deque的缺陷
与vector比较,deque的优势是:头部插入和删除时,不需要搬移元素,效率特别高,而且在扩容时,也不需要搬移大量的元素,因此其效率是必vector
高的。
与list比较,其底层是连续空间,空间利用率比较高,不需要存储额外字段。但是,deque
有一个致命缺陷:不适合遍历,因为在遍历时,deque
的迭代器要频繁的去检测其是否移动到某段小空间的边界,导致效率低下,而序列式场景中,可能需要经常遍历,因此在实际中,需要线性结构时,大多数情况下优先考虑vector
和list
,deque
的应用并不多,而目前能看到的一个应用就是,STL
用其作为stack
和queue
的底层数据结构。
4. 为什么STL中stack和queue默认使用deque?
STL(标准模板库)中的 stack
和 queue
默认使用 std::deque
作为底层容器的原因是出于性能和功能的考虑。
std::deque
(双端队列)是一个双向开口的动态数组,支持在队首和队尾进行高效的插入和删除操作。它的内部实现使得在队首和队尾的操作都能达到接近常数时间复杂度,这使得 std::deque
在作为底层容器时能够提供较好的性能。
对于 stack
和 queue
,它们通常需要在栈顶或队尾进行元素的插入和删除操作,而 std::deque
提供了这种高效的能力。此外,std::deque
也支持在常数时间内对栈顶或队首元素进行访问,这在实现栈和队列的常见操作时非常有用。
虽然 stack
和 queue
默认使用 std::deque
,但你也可以选择通过指定其他容器类型作为底层容器来实现不同的性能和行为。这是通过模板参数来实现的。例如,你可以使用 std::vector
作为底层容器来实现 stack
或 queue
,但需要注意 std::vector
在头部插入元素时可能导致较大的时间复杂度。
总之,std::deque
作为 stack
和 queue
的默认底层容器,具备良好的性能和功能,结合了deque的优点,而完美的避开了其缺陷,能够满足大多数情况下的需求。如果你有特定的需求,可以选择其他底层容器来进行定制。
模拟实现stack
#pragma once
#include <deque>
namespace xzq
{
template<class T, class Container = deque<T>>
class stack
{
public:
void push(const T& x)
{
_con.push_back(x);
}
void pop()
{
_con.pop_back();
}
T& top()
{
return _con.back();
}
const T& top() const
{
return _con.back();
}
bool empty() const
{
return _con.empty();
}
size_t size() const
{
return _con.size();
}
private:
Container _con;
};
}
以下是这个模拟栈的代码实现过程的分析:
头文件包含:代码首先包含了头文件 <deque>
,这是为了使用底层容器 std::deque
。
命名空间定义:代码将 stack
类放置在了命名空间 xzq
下,这是为了避免命名冲突和提供代码的组织结构。
stack 类模板定义:stack
类是一个模板类,有两个模板参数:T
表示栈中存储的元素类型,Container
表示底层容器的类型,默认为 std::deque<T>
。
公共成员函数:
push(const T& x)
:将传入的元素值 x 添加到底层容器的末尾,实现了入栈操作。
pop()
:从底层容器的末尾删除一个元素,实现了出栈操作。
T& top()
和const T& top() const
:分别返回底层容器的末尾元素的引用(允许修改)和常量引用(只读),实现了查看栈顶元素操作。
bool empty() const
:返回底层容器是否为空。
size_t size() const
:返回底层容器中元素的数量。
私有成员变量_con
:这是一个模板类的私有成员变量,用于存储实际的栈元素。其类型是根据模板参数Container
确定的,在实例化时会被替换为具体的容器类型。
结语
有兴趣的小伙伴可以关注作者,如果觉得内容不错,请给个一键三连吧,蟹蟹你哟!!!
制作不易,如有不正之处敬请指出
感谢大家的来访,UU们的观看是我坚持下去的动力
在时间的催化剂下,让我们彼此都成为更优秀的人吧!!!