C++ STL系列学习参考:STL 数据结构与算法__Zwy@的博客-CSDN博客
https://blog.csdn.net/bite_zwy/category_12852141.html
学习C++ STL的三个境界,会用,明理,能扩展,STL中的所有容器都遵循这个规律,下面我们就按照这三个境界来学习stack和queue
参考文档
cplusplus.com/reference/stack/stack/
https://cplusplus.com/reference/stack/stack/
栈是STL中的容器适配器,也是一个类模板,遵循后进先出(LIFO - Last In First Out)的原则,元素只从容器的一端插入和提取。栈是作为容器适配器实现的,这些适配器是使用特定容器类的封装对象作为其底层容器的类,提供一组特定的成员函数来访问其元素。
其中T表示存储的元素类型,Container 表示stack内部底层容器的类型。
value_type(T) 表示存储元素的类型
container_type (Container) 表示底层容器的类型
reference 表示对value_type 元素类型的引用
const_reference 表示对value_type 元素类型的const 引用
size_type 表示无符号整数类型 通常与size_t类型相同
默认构造函数,构造空的stack,这是最基本的构造方式,用于创建一个空的stack。这里的T
是模板参数,代表 stack中存储元素的类型,默认构造函数会初始化stack内部使用的底层容器,将其设置为空状态。
示例代码:
void TestConstructor()
{
//构造空的stack
stack<int> st;
stack<string> st_str;
stack<stack<int>> st_st;
stack<vector<int>> st_vec;
}
使用底层容器初始化的构造函数,这种构造函数允许指定stack的底层容器类型,并使用一个已有的该类型容器来初始化stack。构造函数会将给定容器中的元素按照顺序复制到stack中,并且遵循stack的后进先出原则。
注意,指定的底层容器类型必须支持back(),push_back()和pop_back()操作。
示例代码:
void TestConstructor()
{
// 此时st_v1的底层容器是v1
// 栈顶为5,栈底为1
vector<int> v1= { 1, 2, 3 ,4, 5 };
stack<int,vector<int>> st_v1(v1);
// 此时st_v2的底层容器是v2
//栈顶为stack 栈底为queue
vector<string> v2 = { "queue","stack" };
stack<string, vector<string>> st_v2(v2);
}
拷贝构造函数
示例代码:
void TestConstructor()
{
vector<int> v{ 1,3,5,7,9 };
stack<int,vector<int>> st(v);
//拷贝构造
stack<int,vector<int>> st_cp(st);
}
当使用一个已存在的同类型stack对象来初始化新的stack时,会调用拷贝构造函数。新构造的stack会和被拷贝的stack对象一模一样。
检测stack是否为空,为空返回true,否则返回false
示例代码:
void Testempty()
{
//构造非空的stack
vector<int> v{ 1,3,5,7,9 };
stack<int,vector<int>> st1(v);
if (!st1.empty())
cout << "st1不为空" << endl;
else
cout << "st1为空" << endl;
//构造空的stack
stack<int> st2;
if (!st2.empty())
cout << "st2不为空" << endl;
else
cout << "st2为空" << endl;
}
输出:
返回stack中有效元素的个数
向栈顶压入元素,即压栈
void Test_size_push()
{
vector<int> v{ 1,2,3,4,5};
stack<int,vector<int>> st(v);
cout << "st当前有效数据个数为:" << st.size() << endl;
st.push(6);
st.push(7);
cout << "st当前有效数据个数为:" << st.size() << endl;
}
输出:
获取栈顶元素的引用
移除栈顶的元素
示例代码:
void Test_top_pop()
{
vector<int> v{ 1,2,3,4,5};
stack<int,vector<int>> st(v);
//压栈 栈顶压入元素
st.push(6);
st.push(7);
st.push(8);
cout << "遍历前st有效元素个数:" << st.size() << endl;
//使用top和pop接口遍历stack
cout << "开始遍历st:" << endl;
while (!st.empty())
{
//获取栈顶元素
cout << st.top() << " ";
//移除栈顶元素
st.pop();
}
cout << endl;
cout << "遍历后st有效元素个数:" << st.size() << endl;
}
输出:
交换两个stack的底层容器
示例代码:
void Test_swap()
{
vector<int> v1{ 1,3,5,7 };
stack<int, vector<int>> st1(v1);
vector<int> v2{ 2,4,6,8,10,12 };
stack<int, vector<int>> st2(v2);
//交换前
cout << "swap前st1的size()为:" << st1.size() << endl;
while (!st1.empty())
{
cout << st1.top() << " ";
st1.pop();
}
cout << endl;
st1.swap(st2);
//交换后
cout << "swap后st1的size()为:" << st1.size() << endl;
while (!st1.empty())
{
cout << st1.top() << " ";
st1.pop();
}
cout << endl;
}
输出:
从栈的接口中可以看出,栈实际是一种特殊的vector,因此使用vector完全可以模拟实现stack,
所以我们可以使用vector作为栈底层的容器来模拟实现栈。
示例代码:
#include<iostream>
#include<vector>
using namespace std;
namespace _Zwy
{
template <class T,class Container=vector<T>>
class Stack
{
public:
void push(const T& x)
{
_con.push_back(x);
}
void pop()
{
_con.pop_back();
}
size_t size()
{
return _con.size();
}
const T& top()const
{
return _con.back();
}
bool empty()
{
return _con.empty();
}
private:
Container _con;
};
}
参考文档:
cplusplus.com/reference/queue/queue/
https://cplusplus.com/reference/queue/queue/
队列也是STL中的一种容器适配器,专门用于在 FIFO上下文(先进先出 )中操作,其中从容器一端插入元素,另一端提取元素。队列作为容器适配器实现,容器适配器即将特定容器类封装作为其底层容器类,queue提供一组特定的成员函数来访问其元素。元素从队尾入队列,从队头出队列。
queue的模板参数与stack相同,其中T表示存储的元素类型,Container 表示queue内部底层容器的类型。
queue的成员类型也与stack完全相同
member type | definition | notes |
---|---|---|
value_type | The first template parameter (T) | Type of the elements |
container_type | The second template parameter (Container) | Type of the underlying container |
reference | container_type::reference | usually, value_type& |
const_reference | container_type::const_reference | usually, const value_type& |
size_type | an unsigned integral type | usually, the same as size_t |
value_type(T) 表示存储元素的类型
container_type (Container) 表示底层容器的类型
reference 表示对value_type 元素类型的引用
const_reference 表示对value_type 元素类型的const 引用
size_type 表示无符号整数类型 通常与size_t类型相同
默认构造函数 这是最基本的构造方式,用于构造一个空的queue。其中value_Type表示存储的元素类型。
示例代码:
void TestConstructor()
{
//默认构造函数
queue<int> qi;
queue<string> qs;
queue<vector<int>> qv;
queue<char> qc;
}
使用底层容器初始化的构造函数这种构造函数,允许指定queue
的底层容器类型,并使用一个已有的该类型容器来初始化queue
。Container
必须满足一定的序列容器要求,通常可以是list
、deque
等。构造函数会将给定容器中的元素按照顺序复制到queue
中,并且遵循queue
的先进先出原则。
注意,指定的底层容器类型必须支持front()
、back()
、push_back()
和pop_front()
操作。
示例代码:
void TestConstructor()
{
//使用底层容器初始化的构造函数
list<int> Mylist{ 1,3,5,7,9 };
//使用Mylist作为q1的底层容器
queue<int, list<int>> q1(Mylist);
//使用Myvector作为q2的底层容器
vector<char> Myvector{ 'a','c','e','g' };
queue<char, vector<char>> q2(Myvector);
}
拷贝构造函数,当使用一个已存在的同类型queue
对象(Q
)来初始化新的queue
(q
)时,会调用拷贝构造函数。这个函数会构造一个新的queue
,并将Q
中的元素逐个复制到新的queue
中。新构造的queue会和被拷贝的queue对象一模一样。
示例代码:
void TestConstructor()
{
//拷贝构造函数
list<int> Mylist{ 1,3,5,7,9 };
//使用Mylist构造 q1
queue<int, list<int>> q1(Mylist);
//使用q1拷贝构造q2
queue<int, list<int>> q2(q1);
}
判断queue队列是否为空,为空返回true,否则返回false
示例:
void Testempty()
{
//构造非空的queue
list<int> l{ 1,3,5,7,9 };
queue<int, list<int>> q1(l);
if (!q1.empty())
cout << "q1不为空" << endl;
else
cout << "q1为空" << endl;
//构造空的queue
queue<int> q2;
if (!q2.empty())
cout << "q2不为空" << endl;
else
cout << "q2为空" << endl;
}
输出:
返回当前queue中有效元素的个数
示例:
void Testsize()
{
list<int> l{ 1,3,5,7,9 ,11};
queue<int, list<int>> q1(l);
cout << "q1的有效数据个数size():" << q1.size() << endl;
queue<int> q2;
cout << "q2的有效数据个数size():" << q2.size() << endl;
}
输出:
向队列尾部插入元素,入队列,这个成员函数有效地调用底层容器对象的成员函数push_back。
移除队列头部的元素,出队列,这个成员函数有效地调用底层容器对象的成员函数pop_front。
示例:
void Test_push_pop()
{
//使用list作为底层容器构造queue
list<int> l{ 1,2,3,4,5};
queue<int, list<int>> q(l);
//push元素 入队列
q.push(6);
q.push(7);
cout << "当前队列size为:" << q.size() << endl;
//遍历队列
cout << "开始遍历队列:" << endl;
while (!q.empty())
{
//获取队头元素并删除
cout << q.front() << " ";
q.pop();
}
cout << endl<< "当前队列size为:" << q.size() << endl;
}
输出:
获取队列头部元素的引用,这个成员函数有效地调用底层容器对象的成员函数front
获取队列尾部元素的引用,此成员函数有效地回调底层容器对象的成员函数back
示例:
void Test_front_back()
{
//使用list作为底层容器构造queue
list<int> l{ 1,2,3,4,5};
queue<int, list<int>> q(l);
cout << "初始队头元素front为:" << q.front() << endl;
cout << "初始队尾元素back为:" << q.back() << endl;
//push元素 入队列
q.push(6);
//pop 队头元素出队列
q.pop();
cout << "当前队头元素front为:" << q.front() << endl;
cout << "当前队尾元素back为:" << q.back() << endl;
}
输出:
交换两个queue的底层容器
示例:
void Test_swap()
{
list<int> l1{ 10,9,8,7,6 };
queue<int, list<int>> q1(l1);
list<int> l2{ -1,0,1,2,3,4,5 };
queue<int, list<int>> q2(l2);
//交换前
cout << "swap前q1的size()为:" << q1.size() << endl;
while (!q1.empty())
{
cout << q1.front() << " ";
q1.pop();
}
cout << endl;
q1.swap(q2);
//交换后
cout << "swap后q1的size()为:" << q1.size() << endl;
while (!q1.empty())
{
cout << q1.front() << " ";
q1.pop();
}
cout << endl;
}
输出:
因为queue的接口中存在头删和尾插,因此使用vector来封装效率太低,故可以借助list来模拟实
现queue,list封装效率较高
namespace _Zwy
{
template <class T, class Container = list<T>>
class Queue
{
public:
void push(const T& x)
{
_con.push_back(x);
}
void pop()
{
_con.pop_front();
}
size_t size()
{
return _con.size();
}
const T& front()const
{
return _con.front();
}
const T& back()const
{
return _con.back();
}
bool empty()
{
return _con.empty();
}
private:
Container _con;
};
}
相信大家看到这里,已经对stack和queue的使用和简单的模拟实现有了大概的了解,下面给大家分享几道经典的OJ题,帮助大家在尝试解题中更深刻的理解stack和queue!
https://leetcode.cn/problems/min-stack/description/
题解:
class MinStack
{
public:
void push(int x)
{
// 只要是压栈,先将元素保存到_elem中
_elem.push(x);
// 如果x小于_min中栈顶的元素,将x再压入_min中
if (_min.empty() || x <= _min.top())
_min.push(x);
}
void pop()
{
// 如果_min栈顶的元素等于出栈的元素,_min顶的元素要移除
if (_min.top() == _elem.top())
_min.pop(); _elem.pop();
}
int top() { return _elem.top(); }
int getMin() { return _min.top(); }
private:
// 保存栈中的元素
std::stack<int> _elem;
// 保存栈的最小值
std::stack<int> _min;
};
题解:
class Solution {
public:
bool IsPopOrder(vector<int> pushV, vector<int> popV) {
//入栈和出栈的元素个数必须相同
if (pushV.size() != popV.size())
return false;
// 用s来模拟入栈与出栈的过程
int outIdx = 0;
int inIdx = 0;
stack<int> s;
while (outIdx < popV.size())
{
// 如果s是空,或者栈顶元素与出栈的元素不相等,就入栈
while (s.empty() || s.top() != popV[outIdx])
{
if (inIdx < pushV.size())
s.push(pushV[inIdx++]);
else
return false;
}
// 栈顶元素与出栈的元素相等,出栈
s.pop();
outIdx++;
}
return true;
}
};
3、150. 逆波兰表达式求值 - 力扣(LeetCode)
https://leetcode.cn/problems/evaluate-reverse-polish-notation/description/
题解:
class Solution
{
public:
int evalRPN(vector<string>& tokens) {
stack<int> s;
for (size_t i = 0; i < tokens.size(); ++i)
{
string& str = tokens[i];
// str为数字
if (!("+" == str || "-" == str || "*" == str || "/" == str))
{
s.push(atoi(str.c_str()));
}
else
{
// str为操作符
int right = s.top();
s.pop();
int left = s.top();
s.pop();
switch (str[0])
{
case '+':
s.push(left + right);
break;
case '-':
s.push(left - right);
break;
case '*':
s.push(left * right);
break;
case '/':
// 题目说明了不存在除数为0的情况
s.push(left / right);
break;
}
}
}
return s.top();
}
};
适配器是一种设计模式(设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设
计经验的总结),该种模式是将一个类的接口转换成客户希望的另外一个接口。
这就是大家所熟知的电源适配器,通过适配器,将原本不兼容的插头和插座可以一起工作。容器适配器也是一样的道理。
在STL标准库中stack和queue的底层容器默认情况下为deque,deque(双端队列)也是C++标准库提供的一种序列容器,下面我们来认识deque!
参考文档:
cplusplus.com/reference/deque/deque/
https://cplusplus.com/reference/deque/deque/
deque又叫做双端队列,Double - ended queue, 头文件为<deque>,是一种双开口的"连续"空间的数据结构,双开口的含义是:可以在头尾两端进行插入和删除操作,且时间复杂度为O(1),与vector比较,头插效率高,不需要搬移元素;与list比较,空间利用率比较高。它的元素在内存中不是连续存储的,而是由多个固定大小的数组块组成,这些块在逻辑上是连续的。
deque 并不是真正连续的空间,而是由一段段连续的小空间拼接而成的,实际 deque 类似于一个
动态的二维数组 ,其底层结构如下图所示:
双端队列底层是一段假象的连续空间,实际是分段连续的,为了维护其 “ 整体连续 ” 以及随机访问
的假象,落在了 deque 的迭代器身上, 因此deque 的迭代器设计就比较复杂,如下图所示:
那deque是如何借助其迭代器维护其假想连续的结构呢?
通过对迭代器的特殊封装,
与vector比较,deque的优势是:头部插入和删除时,不需要搬移元素,效率特别高,而且在扩
容时,也不需要搬移大量的元素,因此其效率是必vector高的。与list比较,其底层是连续空间,空间利用率比较高,不需要存储额外字段。
不适合遍历,因为在遍历时,deque的迭代器要频繁的去检测其是否移动到某段小空间的边界,迭代器不仅要记录当前元素在块内的位置,还需要记录当前块的信息。
当迭代器在块内移动时,操作相对简单,但当迭代器移动到块边界时,需要进行块切换操作,这涉及到更新块指针和重新计算元素在新块内的位置等操作,导致效率低下,而序列式场景中,可能需要经常遍历,因此在实际中,需要线性结构时,大多数情况下优先考虑vector和list,deque的应用并不多,而目前能看到的一个应用就是,STL用其作为stack和queue的底层数据结构。
stack是一种后进先出的特殊线性数据结构,因此只要具有push_back()和pop_back()操作的线性结构,都可以作为stack的底层容器,比如vector和list都可以;
queue是先进先出的特殊线性数据 结构,只要具有push_back和pop_front操作的线性结构,都可以作为queue的底层容器,比如 list。
但是STL中对stack和queue默认选择deque作为其底层容器,主要是因为:
1. stack和queue不需要遍历(因此stack和queue没有迭代器),只需要在固定的一端或者两端进 行操作。
2. 在stack中元素增长时,deque比vector的效率高(扩容时不需要搬移大量数据);queue中的元素增长时,deque不仅效率高,而且内存使用率高。 结合了deque的优点,而完美的避开了其缺陷。
1)、STL中stack的模拟实现
#include<deque>
namespace _Zwy
{
template<class T, class Con = deque<T>>
//template<class T, class Con = vector<T>>
//template<class T, class Con = list<T>>
class stack
{
public:
stack()
{}
void push(const T& x)
{
_deq.push_back(x);
}
void pop()
{
_deq.pop_back();
}
T& top()
{
return _deq.back();
}
const T& top()const
{
return _deq.back();
}
size_t size()const
{
return _deq.size();
}
bool empty()const
{
return _deq.empty();
}
private:
Con _deq;
};
}
1)、STL中queue的模拟实现
#include<deque>
namespace _Zwy
{
template<class T, class Con = deque<T>>
//template<class T, class Con = list<T>>
class queue
{
public:
queue()
{}
void push(const T& x)
{
_deq.push_back(x);
}
void pop()
{
_deq.pop_front();
}
T& back()
{
return _deq.back();
}
const T& back()const
{
return _deq.back();
}
T& front()
{
return _deq.front();
}
const T& front()const
{
return _deq.front();
}
size_t size()const
{
return _deq.size();
}
bool empty()const
{
return _deq.empty();
}
private:
Con _deq;
};
}
stack和queue是STL中很基础同时也很重要的容器,是非常重要的数据结构之一,其底层容器deque大家现阶段了解即可,下篇博文将带大家深度剖析deque以及STL中另一个容器适配器priority_queue 优先级队列。敬请期待!
创作不易,还请多多互三支持!!!如上讲解如有不足之处,还请各位大佬评论区斧正!