🔥个人主页:北辰水墨
🔥专栏:C++学习仓
本节内容我们来讲解栈和队列的模拟实现,文末会赋上模拟实现的代码
一、stack的使用和模拟实现
stack适配器的介绍:
1. stack是一种容器适配器,专门用在具有后进先出操作的上下文环境中,其删除只能从容器的一端进行元素的插入与提取操作。
2. stack是作为容器适配器被实现的,容器适配器即是对特定类封装作为其底层的容器,并提供一组特定的成员函数来访问其元素,将特定类作为其底层的,元素特定容器的尾部(即栈顶)被压入和弹出。
3. stack的底层容器可以是任何标准的容器类模板或者一些其他特定的容器类,这些容器类应该支持以下操作
- empty:判空操作
- back:获取尾部元素操作
- push_back:尾部插入元素操作
- pop_back:尾部删除元素操作
4. 标准容器vector、deque、list均符合这些需求,默认情况下,如果没有为stack指定特定的底层容器,默认情况下使用deque。
函数的介绍:
🔥初始化栈:
1.构建一个空栈:
std::stack<int> myst;
2.通过已有的容器,初始化栈:
std::vecotr<int> v={1,2,3,4,5};
srd::stack<int,vector<int>> myst(v);
//stack是适配器,传相应的容器,底层就是用对应的容器实现栈
3.使用拷贝构造函数初始化一个与另一个栈相同的栈:
std::stack<int> originalStack;
// 添加一些元素到 originalStack
std::stack<int> myStack(originalStack);
🔥empty()
检查栈是否为空
🔥size()
返回stack中元素的个数
🔥top()
返回栈顶元素
🔥push()
将元素val压栈
🔥pop()
将stack中尾部的元素弹出
🔥swap()
交换两个栈的所有元素
// stack::swap
#include <iostream> // std::cout
#include <stack> // std::stack
int main ()
{
std::stack<int> foo,bar;
foo.push (10); foo.push(20); foo.push(30);
bar.push (111); bar.push(222);
foo.swap(bar);
std::cout << "size of foo: " << foo.size() << '\n';
std::cout << "size of bar: " << bar.size() << '\n';
return 0;
}
输出:
size of foo: 2
size of bar: 3
例题一:"最小栈"
(点击"最小栈"字体就可以跳转做题)
st1
是一个标准的栈,它用于按照后进先出的顺序存储所有推入的元素st2
是一个辅助栈,它用于跟踪s1
中所有元素的最小值
-
MinStack() {} 不需要自己实现,走初始化列表,是自定义类型,调用他自己stack<int>的默认构造
-
void push(int x)
:在st1
中推入 x。如果st2
为空或者 x 小于等于st2
的栈顶元素,也将 x 推入st2
。这保证st2
的栈顶元素始终是st1
中当前所有元素的最小值 -
void pop()
:从st1
中弹出一个元素。如果st1
的栈顶元素与st2
的栈顶元素相等,说明st1
弹出的元素是当前的最小值,因此也需要在st2
中弹出栈顶元素
class MinStack {
public:
/** initialize your data structure here. */
MinStack() {
}
void push(int x) {
st1.push(x);
if(st2.empty()||st2.top()>=x) st2.push(x);
}
void pop() {
if(st1.top()==st2.top())
{
st2.pop();
}
st1.pop();
}
int top() {
return st1.top();
}
int getMin() {
return st2.top();
}
private:
stack<int> st1;
stack<int> st2;
};
例题二:"验证栈序列"
核心思想:模拟入栈和出栈的过程。
n: 记录poped的下标
pushst:创建一个栈
for循环开始模拟入栈的过程,只要 i 没有大于 pushed.size()
个数就继续循环
while循环:栈不为空&&栈顶元素==poped[n]就进入循环,出栈并且让n++
class Solution {
public:
bool validateStackSequences(vector<int>& pushed, vector<int>& popped) {
stack<int> pushst;
int n = 0;
for(int i=0;i<pushed.size();i++)
{
pushst.push(pushed[i]);
while (!pushst.empty()&&pushst.top() == popped[n]) //这个的顺序不能反,否则当为空的时候,去取栈顶元素,会报错
{
pushst.pop();
n++;
}
}
return pushst.empty();
}
};
栈的模拟实现:
namespace ink
{
template<class T,class Container=vector<int>>
class stack
{
public:
bool empty()
{
return _con.empty();
}
size_t size()
{
return _con.size();
}
//加引用,是为了自定义类型,减少拷贝构造
const T& top() //const 返回的是栈顶元素,用引用返回,外面的就可以接收并且可以修改,我就让它只读
{
return _con.back();
}
void pop()
{
_con.pop_back();
}
void push(T& x)
{
_con.push_back(x);
}
private:
Container _con;
};
}
上面的实现是简单地展示了如何用C++模板和通用编程的原则来定义一个通用的栈类,这个栈类被称为适配器。在这种上下文中,“适配器模式”是一种设计模式的用词。
在容器类库设计中(如标准模板库 STL 中的容器),适配器模式通常用于通过已有的容器类型(如vector
, deque
, list
等),来实现某种特定的抽象数据类型(如栈、队列等)的接口。这样的做法使我们能够重用现有代码,并提供更丰富的操作
- 定义了
stack
模板类,它接收两个模板参数:T
: 栈中元素的类型。Container
: 底层容器的类型,默认是vector<T>
Container
是一个模板参数,它允许我们定义底层数据结构。默认使用std::vector<T>
作为底层容器,但我们可以指定std::deque<T>
、std::list<T>
等容器,这是适配器模式的应用之一,我们可以切换不同的底层实现,不改变栈的接口 (底层千差万别,不改变上层接口)
stack 类包含如下成员函数:
- push: 向栈中添加元素
- pop: 从栈中移除顶部元素
- size: 返回栈中元素的数量
- empty: 检查栈是否为空
- top: 返回栈顶元素的引用
这些成员函数中的每一个都直接调用了底层容器 Container 实例 _con 的相应操作函数,这样 stack 就提供了类似栈的接口
二、queue的使用和模拟实现
queue适配器的介绍
1. 队列是一种容器适配器,专门用于在FIFO上下文(先进先出)中操作,其中从容器一端插入元素,另一端提取元素
2. 队列作为容器适配器实现,容器适配器即将特定容器类封装作为其底层容器类,queue提供一组特定的成员函数来访问其元素。元素从队尾入队列,从队头出队列。
3. 底层容器可以是标准容器类模板之一,也可以是其他专门设计的容器类。该底层容器应至少支持以下操作:
- empty:检测队列是否为空
- size:返回队列中有效元素的个数
- front:返回队头元素的引用
- back:返回队尾元素的引用
- push_back:在队列尾部入队列
- pop_front:在队列头部出队列
4.队列需要支持pop_front的操作,而vector不支持。所以vector不能做queue的底层容器
5.一般我们都是使用deque容器作为queue的底层容器。
deque容器的介绍
deque 成为双端队列,是一种序列容器,在两端都支持高效的元素插入和删除操作。
与 std::vector 相比,std::deque 提供类似的功能,但在许多实现中,deque 是由多个固定大小的数组(通常被称为块或段)组成的动态数组。这允许在两端进行快速的插入和删除操作,而不必像 std::vector 在插入(或删除)元素时将所有元素向前或向后移动。
deque 的主要特点和功能包括:
1. 双端操作:可以在队列的前端和后端进行插入 (push_front, emplace_front) 和删除 (pop_front) 操作
2. 序列访问:可以使用下标操作符 (operator[]) 或一系列迭代器访问 deque 中的元素
3. 迭代器失效:在两端添加或删除元素通常不会使迭代器失效,但是在 deque 中除了首尾外的任何位置插入或删除元素都可能使所有迭代器失效。这取决于具体的实现。
4. 内存分配:deque 不保证所有元素都连续存储,因此不能依赖像 std::vector 那样的内存连续性
5. 性能:在两端插入或删除元素通常是常数时间复杂度 O(1),但是在中间位置插入或删除元素的时间复杂度通常是线性的 O(n),这取决于插入位置与最近端点的距离
vector的优点在于能支持下标随机访问,缺点是头部或中间插入删除的效率低,扩容有消耗
list的优点在于任意位置插入删除的效率都不错,缺点就是不支持下标的随机访问
而deque可以看做vector和list的中和版,既支持下标访问,又支持头插头删
deque并不是真正连续的空间,而是由一段段连续的小空间拼接而成的
std::deque
的常见实现方式是使用一系列的固定大小的数组(称为缓冲区或块),这些数组被指针所管理,这些指针通常保存在一个或多个中央数组中。这种实现允许在 deque
的两端都高效地添加或删除元素,而无需移动所有元素
双端队列底层是一段假象的连续空间,实际是分段连续的,为了维护其“整体连续”以及随机访问的假象,落在了deque的迭代器身上,因此deque的迭代器设计就比较复杂
中控数组满了就扩容,它的消耗会小很多
它的迭代器有四个指针
- start指向指向第一个buff的第一个数据
- finish指向最后一个buff的最后一个数据的下一个位置
- cur指向buff的头节点
- node指回中控数组(为了让迭代器在走完上一层之后可以跳转到下一块空间的首元素)
deque的缺陷
与vector比较,deque的优势是:头部插入和删除时,不需要搬移元素,效率特别高,而且在扩容时,也不需要搬移大量的元素,因此其效率是必vector高的。
与list比较,其底层是连续空间,空间利用率比较高,不需要存储额外字段。
但是,deque有一个致命缺陷:不适合遍历,因为在遍历时,deque的迭代器要频繁的去检测其是否移动到某段小空间的边界,导致效率低下,而序列式场景中,可能需要经常遍历,因此在实际中,需要线性结构时,大多数情况下优先考虑vector和list,deque的应用并不多,而目前能看到的一个应用就是,STL用其作为stack和queue的底层数据结构
为什么选择deque作为stack和queue的底层默认容器?
stack是一种后进先出的特殊线性数据结构,因此只要具有push_back()和pop_back()操作的线性结构,都可以作为stack的底层容器,比如vector和list都可以;queue是先进先出的特殊线性数据结构,只要具有push_back和pop_front操作的线性结构,都可以作为queue的底层容器,比如list。但是STL中对stack和queue默认选择deque作为其底层容器,主要是因为:
- stack和queue不需要遍历(因此stack和queue没有迭代器),只需要在固定的一端或者两端进行操作。
- 在stack中元素增长时,deque比vector的效率高(扩容时不需要搬移大量数据);deque不需要像list那样频繁的开空间,queue/stack中的元素增长时,deque不仅效率高,而且内存使用率高
queue的模拟实现
#include<deque>
#include<list>
namespace own
{
template<class T, class Con = deque<T>>
class queue
{
public:
queue() {}
void push(const T& x) { _c.push_back(x); }
void pop() { _c.pop_front(); }
T& back() { return _c.back(); }
const T& back()const { return _c.back(); }
T& front() { return _c.front(); }
const T& front()const { return _c.front(); }
size_t size()const { return _c.size(); }
bool empty()const { return _c.empty(); }
private:
Con _c;
};
}