STL之stack和queue
1. stack的介绍和使用
1.1 stack的介绍
stack是一种容器适配器,专门用在具有后进先出操作的上下文环境中,其删除只能从容器的一端进行 元素的插入与提取操作。
stack是作为容器适配器被实现的,容器适配器即是对特定类封装作为其底层的容器,并提供一组特定的成员函数来访问其元素,将特定类作为其底层的,元素特定容器的尾部(即栈顶)被压入和弹出。
stack的底层容器可以是任何标准的容器类模板或者一些其他特定的容器类,这些容器类应该支持以下 操作:
- empty:判空操作
- back:获取尾部元素操作
- push_back:尾部插入元素操作
- pop_back:尾部删除元素操作
标准容器vector、deque、list均符合这些需求,默认情况下,如果没有为stack指定特定的底层容器, 默认情况下使用deque。
1.2 stack的使用
函数说明 | 接口说明 |
---|---|
stack() | 构造空的栈 |
empty() | 检测stack是否为空 |
size() | 返回stack中元素的个数 |
top | 返回栈顶元素的引用 |
push() | 将元素val压入stack中 |
pop | 将stack中尾部的元素弹出 |
1.3 stack的相关习题
最小栈
class MinStack {
public:
MinStack() {}
void push(int val) {
_st.push(val);
if(_minst.empty() || val <= _minst.top())
{
_minst.push(val);
}
}
void pop() {
if(_minst.top() == _st.top())
{
_minst.pop();
}
_st.pop();
}
int top() {
return _st.top();
}
int getMin() {
return _minst.top();
}
private:
stack<int> _st;
stack<int> _minst;
};
栈的弹出、压入序列
代码:
class Solution {
public:
bool IsPopOrder(vector<int> pushV,vector<int> popV) {
stack<int> st;
int popi = 0;//记录已经出栈的元素个数
int size = pushV.size();
for(auto e : pushV)
{
st.push(e);
while(!st.empty() && popV[popi] == st.top())
{
popi++;
st.pop();
}
}
return st.empty();
}
};
逆波兰表达式
代码:
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<int> st;
for(const auto& str : tokens)
{
if(str == "+" || str == "-"
|| str == "*" || str == "/")
{
int right = st.top();
st.pop();
int left = st.top();
st.pop();
switch(str[0])
{
case '+':
st.push(left + right);
break;
case '-':
st.push(left - right);
break;
case '*':
st.push(left * right);
break;
case '/':
st.push(left / right);
break;
default :
break;
}
}
else
{
st.push(stoi(str));
}
}
return st.top();
}
};
1.4 stack的模拟实现
template<class T, class Container = deque<T>>
class stack
{
public:
void push(const T& x)
{
_con.push_back(x);
}
void pop()
{
_con.pop_back();
}
const T& top()
{
return _con.back();
}
size_t size()
{
return _con.size();
}
bool empty()
{
return _con.empty();
}
private:
Container _con;
};
2. queue的介绍和使用
2.1 queue的介绍
队列是一种容器适配器,专门用于在FIFO上下文(先进先出)中操作,其中从容器一端插入元素,另一端 提取元素。
队列作为容器适配器实现,容器适配器即将特定容器类封装作为其底层容器类,queue提供一组特定的 成员函数来访问其元素。元素从队尾入队列,从队头出队列。
底层容器可以是标准容器类模板之一,也可以是其他专门设计的容器类。该底层容器应至少支持以下操 作:
- empty:检测队列是否为空
- size:返回队列中有效元素的个数
- front:返回队头元素的引用
- back:返回队尾元素的引用
- push_back:在队列尾部入队列
- pop_front:在队列头部出队列
标准容器类deque和list满足了这些要求。默认情况下,如果没有为queue实例化指定容器类,则使用标 准容器deque。
2.2 queue的使用
函数声明 | 接口说明 |
---|---|
queue() | 构造空的队列 |
empty() | 检测队列是否为空,是返回true,否则返回false |
size() | 返回队列中有效元素的个数 |
front | 返回队头元素的引用 |
back() | 返回队尾元素的引用 |
push() | 在队尾将元素val入队列 |
pop() | 将队头元素出队列 |
2.3 queue的模拟实现
template<class T, class Container = deque<T>>
class queue
{
public:
void push(const T& x)
{
_con.push_back(x);
}
void pop()
{
_con.pop_front();
}
const T& front()
{
return _con.front();
}
const T& back()
{
return _con.back();
}
size_t size()
{
return _con.size();
}
bool empty()
{
return _con.empty();
}
private:
Container _con;
};
3. priority_queue的介绍和使用
3.1 priority_queue的介绍
- 优先队列是一种容器适配器,根据严格的弱排序标准,它的第一个元素总是它所包含的元素中最大的。
- 此上下文类似于堆,在堆中可以随时插入元素,并且只能检索最大堆元素(优先队列中位于顶部的元 素)。
- 优先队列被实现为容器适配器,容器适配器即将特定容器类封装作为其底层容器类,queue提供一组特 定的成员函数来访问其元素。元素从特定容器的“尾部”弹出,其称为优先队列的顶部。
- 底层容器可以是任何标准容器类模板,也可以是其他特定设计的容器类。容器应该可以通过随机访问迭 代器访问,并支持以下操作:
- empty():检测容器是否为空
- size():返回容器中有效元素个数
- front():返回容器中第一个元素的引用
- push_back():在容器尾部插入元素
- pop_back():删除容器尾部元素
- 标准容器类vector和deque满足这些需求。默认情况下,如果没有为特定的priority_queue类实例化指定容器类,则使用vector。
- 需要支持随机访问迭代器,以便始终在内部保持堆结构。容器适配器通过在需要时自动调用算法函数 make_heap、push_heap和pop_heap来自动完成此操作。
3.2 priority_queue的使用
优先级队列默认使用vector作为其底层存储数据的容器,在vector上又使用了堆算法将vector中元素构造成堆的结构,因此priority_queue就是堆,所有需要用到堆的位置,都可以考虑使用priority_queue。
注意: 默认情况下priority_queue是大堆。
函数声明 | 接口说明 |
---|---|
priority_queue()/priority_queue(first,last) | 构造一个空的优先级队列 |
empty() | 检测优先级队列是否为空,是返回true,否则返回 false |
top() | 返回优先级队列中最大(最小元素),即堆顶元素 |
push(x) | 在优先级队列中插入元素x |
pop() | 删除优先级队列中最大(最小)元素,即堆顶元素 |
【注意】
-
默认情况下,priority_queue是大堆。
void test_priority_queue() { priority_queue<int> pq; pq.push(0); pq.push(8); pq.push(7); pq.push(9); pq.push(6); pq.push(3); pq.push(5); pq.push(4); while (!pq.empty()) { cout << pq.top() << " "; pq.pop(); } cout << endl; }
运行截图:
问题:优先级队列默认传的是less仿函数,底层是一个大堆。
想控制成小堆,传greater仿函数,底层是一个小堆。
void test_priority_queue() { priority_queue<int, vector<int>, greater<int>> pq;//中间的vector是容器,即指定实例化优先级队列的容器,默认就是vector,priority_queuez pq.push(0); pq.push(8); pq.push(7); pq.push(9); pq.push(6); pq.push(3); pq.push(5); pq.push(4); while (!pq.empty()) { cout << pq.top() << " "; pq.pop(); } cout << endl; }
运行截图:
3.3 priority_queue的模拟实现
//仿函数/函数对象:对象可以像调用函数一样去使用
template<class T>
struct less
{
bool operator()(const T& x, const T& y)
{
return x < y;
}
};
template<class T>
struct greater
{
bool operator()(const T& x, const T& y)
{
return x > y;
}
};
//优先级队列
template<class T, class Container = vector<T>, class Compare = less<T>>
class priority_queue
{
public:
void AdjustUp(size_t child)
{
size_t parant = (child - 1) / 2;
Compare com;
while (child > 0)
{
if (com(_con[parant], _con[child]))
{
swap(_con[parant], _con[child]);
child = parant;
parant = (child - 1) / 2;
}
else
{
break;
}
}
}
void AdjustDown(size_t parant)
{
size_t child = parant * 2 + 1;
Compare com;
while (child < _con.size())
{
if (child + 1 < _con.size() && com(_con[child], _con[child + 1]))
{
++child;
}
if (com(_con[parant], _con[child]))
{
swap(_con[child], _con[parant]);
parant = child;
child = parant * 2 + 1;
}
else
{
break;
}
}
}
void push(const T& x)
{
_con.push_back(x);
AdjustUp(_con.size() - 1);
}
void pop()
{
assert(!_con.empty());
swap(_con[0], _con[_con.size() - 1]);
_con.pop_back();
AdjustDown(0);
}
const T& top()
{
return _con[0];
}
bool empty()
{
return _con.empty();
}
size_t size()
{
return _con.size();
}
private:
Container _con;
};
注意:此处我们自己实现了仿函数,如果我们自己并不想实现,可以使用库中的仿函数,但是要注意包含functional头文件。
注意:也可以在priority类中定义Compare类型的成员变量,如下所示:
class priority
{
public:
priority(const Compare& comFunc = Compare())
:_com(comFunc)
{}
private:
Container _con;
Compare _com;
}
这样就可以直接在AdjustDown成员函数中直接使用_com(a[child], [parant])了,不需要每次都在函数体内创建临时变量了。
问:仿函数有什么优势?
答:很多场景替代的是函数指针,但是函数指针使用较为复杂,仿函数就相对简单许多。如果我们想要使用函数指针,那么定义priority_queue类型的变量时的代码要进行像这样的修改:
priority_queue<int, vector<int>, bool(*)(int, int)> pq(ComIntLess);//ComIntLess是一个函数,类型是bool(*)(int, int)
注意:区分priority_queue中的less、greater和sort函数中的greater、less。
priority_queue<int, vector<int>, less<int>> q; sort(v.begin(), v.end(), less<int>());
问:为什么一个是
less<int>
,一个是less<int>()
?答:因为前者是在类的模板参数中,后者是在函数的参数列表中。实际上后者是一个匿名对象,等价于下面的两行代码:
less<int> l; sort(v.begin(), v.end(), l());//因为存在less类型对象的()运算符的重载,所以可以这么使用
3.4 了解deque
deque的大致结构:
vector
优点:
- 适合尾插尾删,可以随机访问
- CPU高速缓存命中高
缺点:
- 不适合头部或中部插入删除,效率低,需要挪动数据
- 扩容有一定的性能消耗,还可能存在一定程度的空间浪费(删除元素不释放空间,且扩容时可能存在空间浪费)
list
优点:
- 任意位置插入删除效率高
- 按需申请和释放空间
缺点:
- 不支持随机访问
- CPU高速缓存命中低
deque
优点:
- 头部和尾部插入数据效率比较高
- 支持随机访问
- 扩容代价小
- CPU高速缓存命中高
缺点:
- 中部数据插入效率低
- 虽然支持随机访问,但是效率比vector而言还是有差距,频繁随机访问效率较低
问:什么场景下适合用deque?
答:进行大量的头尾插入删除,偶尔随机访问。
4. 反向迭代器的模拟实现
反向迭代器和正向迭代器相比,除了++和–时的方向不一样,其它操作基本一致。
STL源码中反向迭代器的实现:
代码:
ReverseIterator.h
template<class Iterator, class Ref, class Ptr>
struct Reverse_iterator
{
Iterator _it;
typedef Reverse_iterator<Iterator, Ref, Ptr> Self;
Reverse_iterator(Iterator it)
:_it(it)
{}
Ref operator*()
{
Iterator tmp = _it;
return *(--tmp);
}
Ptr operator->()
{
return &(operator*());
}
Self operator++()
{
--_it;
return *this;
}
Self operator--()
{
++_it;
return *this;
}
bool operator!=(const Self& s)
{
return _it != s._it;
}
};
mylist.h
class mylist
{
typedef list_node<T> Node;
public:
typedef __list_iterator<T, T&, T*> iterator;
typedef __list_iterator<T, const T&, const T*> const_iterator;
//反向迭代器适配支持
typedef Reverse_iterator<iterator, T&, T*> reverse_iterator;
typedef Reverse_iterator<const_iterator, const T&, const T*> const_reverse_iterator;
reverse_iterator rbegin()
{
return reverse_iterator(end());
}
reverse_iterator rend()
{
return reverse_iterator(begin());
}
const_reverse_iterator rbegin()const
{
return const_reverse_iterator(end());
}
const_reverse_iterator rend()const
{
return const_reverse_iterator(begin());
}
}
注意:在互相引入的时候,注意域的限制。
问:如果我们只传一个迭代器类型,不传T&、T*这两种类型,我们该如何实现反向迭代器?
答:以List为例(代码只显示比之前添加的):
template<class T, class Ref, class Ptr>
struct __list_iterator
{
//只传迭代器的情况
typedef Ptr pointer;
typedef Ref reference;
}
template<class T>
class mylist
{
//只传一种迭代器的情况
typedef Reverse_iterator<iterator> reverse_iterator;
typedef Reverse_iterator<const_iterator> const_reverse_iterator;
reverse_iterator rbegin()
{
return reverse_iterator(end());
}
reverse_iterator rend()
{
return reverse_iterator(begin());
}
const_reverse_iterator rbegin()const
{
return const_reverse_iterator(end());
}
const_reverse_iterator rend()const
{
return const_reverse_iterator(begin());
}
}
//ReverseIterator.h文件
//只传迭代器的情况
template<class Iterator>
struct Reverse_iterator
{
Iterator _it;
typedef Reverse_iterator<Iterator> Self;
Reverse_iterator(Iterator it)
:_it(it)
{}
//类模板、模板虚拟类型,没有实例化之前不能去它里面找内嵌定义的类型(只有Iterator::reference)
//类模板实例化,找出来的也是虚拟类型,后期无法处理
//所以需要在Iterator::reference的前面加上一个typename,来告诉编译器后面这一串是一个类型,等iterator
//实例化之后再去里面去找这个内嵌类型
//T&
typename Iterator::reference operator*()
{
Iterator tmp = _it;
return *(--tmp);
}
//T*
typename Iterator::pointer operator->()
{
return &(operator*());
}
Self operator++()
{
--_it;
return *this;
}
Self operator--()
{
++_it;
return *this;
}
bool operator!=(const Self& s)
{
return _it != s._it;
}
};
当然,上面代码中ReverseIterator.h文件中的代码也可以进行下面的修改:
template<class Iterator>
struct Reverse_iterator
{
Iterator _it;
typedef Reverse_iterator<Iterator> Self;
typedef typename Iterator::reference reference;
typedef typename Iterator::pointer pointer;
Reverse_iterator(Iterator it)
:_it(it)
{}
//类模板、模板虚拟类型,没有实例化之前不能去它里面找内嵌定义的类型(只有Iterator::reference)
//类模板实例化,找出来的也是虚拟类型,后期无法处理
//所以需要在Iterator::reference的前面加上一个typename,来告诉编译器后面这一串是一个类型,等iterator
//实例化之后再去里面去找这个内嵌类型
//T&
reference operator*()
{
Iterator tmp = _it;
return *(--tmp);
}
//T*
pointer operator->()
{
return &(operator*());
}
Self operator++()
{
--_it;
return *this;
}
Self operator--()
{
++_it;
return *this;
}
bool operator!=(const Self& s)
{
return _it != s._it;
}
};
注意:上面的这种修改方式,vector的模拟实现中无法完成,为什么呢?因为我们传过来的
Iterator
这种类型,在List中是一种自定义类型(__list_iterator
),我们可以在这个自定义类型中使用typedef进行重定义,但是在vector的模拟实现中,iterator是一种原生指针类型,解决方式有两种,第一种,就像list一样,定义一种vector的迭代器类型(模仿__list_iterator),就像下面的代码:template<class T> struct __vector_iterator {}
另一种方式就是使用迭代器萃取(特化)的方式(比较复杂,不做学习)。
typename的使用举例
要求:能够打印出一个链表中的各种元素,使用之前自己实现的链表
template<class T>
void print_list(const mylist<T>& v)
{
typename mylist<T>::const_iterator cit = v.begin();
//mylist<T>::const_iterator还没有实例化(因为mylist的模板参数没有确定),所以暂时是一个虚拟类型,此处用typename,告诉编译器后面这一串是一个类型,先让它编译过去,等实例化了之后再去取它的类型
//当然,上面的这传类型代码我们也可以用之前学过的一个关键字auto来替代
//auto cit = v.begin();
//问:为什么此处用auto是可以的?
//答:因为我们用了auto之后,编译器知道后面的这个标识符是一个变量名,auto所代表的是一个类型,然后编译器再根据v.begin()返回的类型来自动进行推导,进而得出变量cit的类型
while (cit != v.end())
{
cout << *cit << " ";
++cit;
}
cout << endl;
}
注意:只要是我们使用类模板中的类型来进行定义一个变量时,要在类型名的前面加上一个typename!