栈和队列
本章思维导图:
注:本章思维导图对应的.xmind
和.png
文件都已同步导入至资源,可免费查看
文章目录
- 栈和队列
- 1. 适配器
- 2. 栈 stack
- 2.1 概念及结构
- 2.2 使用
- 2.3 模拟实现
- 3. 队列 queue
- 3.1 普通队列 queue
- 3.1.1 概念及结构
- 3.1.2 使用
- 3.1.3 模拟实现
- 3.2 优先队列 priority_queue
- 3.2.1 概念及结构
- 3.2.2 使用
- 3.2.3 模拟实现
- 4. 双端队列 deque
- 4.1 概念及结构
- 4.2 使用
- 4.3 优缺点和适用情况
1. 适配器
和以往的vector
、string
等容器不同,我们今天所要讲的stack
、queue
等并不是所谓的容器,而是适配器
适配器是一种设计模式,它可以将类的接口处理转换为用户希望得到的另一种接口
2. 栈 stack
2.1 概念及结构
-
栈:是一种特殊的线性表,其只允许在固定的一端进行插入和删除元素的操作。进行数据插入和删除操作的一端称为栈顶,另一端为栈底。
-
栈中的数据元素遵守后进先出原则
-
压栈:栈的插入操作称为进栈/压栈/入栈,其位置在栈顶
-
出栈:栈的删除操作称为出栈,其位置也在栈顶
2.2 使用
栈的使用需要包含头文件:
<stack>
stack
是一个模板类,其具体声明如下:
template <class T, class Container = deque<T> > class stack;
T
即为stack存储数据的具体类型- 而
Container
则为stack所适配的容器,deque
为stack默认的适配容器
其包含如下的常见操作:
函数接口 | 接口说明 |
---|---|
void push (const value_type& val); | 数据val入栈 |
void pop(); | 出栈顶数据 |
value_type& top(); const value_type& top() const; | 返回栈顶元素 |
size_type size() const; | 返回栈的大小 |
bool empty() const; | 判断栈是否为空 |
我们可以用一道经典例题来对stack
栈的使用进行熟悉:👉最小栈
参考代码:
class MinStack {
public:
MinStack() {
}
void push(int val) {
_st.push(val);
if (_minst.empty() || val <= _minst.top())
_minst.push(val);
}
void pop() {
int pop_val = _st.top();
_st.pop();
if (pop_val == _minst.top())
_minst.pop();
}
int top() {
return _st.top();
}
int getMin() {
return _minst.top();
}
stack<int> _st; //普通栈
stack<int> _minst; //存储最小元素的栈
};
2.3 模拟实现
stack
栈是一个适配器,因此和以往我们模拟实现vector
等容器不同,并不需要从头到尾全部自己实现一遍,只需要利用现有的容器复用适配就好了。
由于stack
是一个先入后出(FILO)的结构,因此进行适配的容器也必须要满足以下的操作:
push_back()
,尾插pop_back()
,尾删size()
,返回数据个数empty()
,判断是否为空back()
,返回尾部数据
namespace TEST
{
template<class T, class container = deque<T>>
class stack
{
public:
void push(const T& val)
{
_con.push_back(val);
}
void pop()
{
_con.pop_back();
}
T& top()
{
return _con.back();
}
const T& top() const
{
return _con.back();
}
bool empty()
{
return _con.empty();
}
size_t size()
{
return _con.size();
}
private:
container _con;
};
}
3. 队列 queue
3.1 普通队列 queue
3.1.1 概念及结构
- 队列:只允许在一端进行入数据操作,在另一端进行删除数据操作的特殊线性表
- 遵循先进先出的原则
FIFO
- 入队列:进行插入操作的一段叫做队尾
- 出队列:进行删除操作的一段叫做队头
3.1.2 使用
使用时需要包含头文件
<queue>
queue
是一个模板类,其具体的声明如下
template <class T, class Container = deque<T> > class queue;
T
即为queue存储数据的具体类型- 而
Container
则为queue所适配的容器,deque
为queue默认的适配容器
其包含如下的常见操作:
函数接口 | 接口说明 |
---|---|
void push (const value_type& val); | 队尾入队 |
void pop(); | 队头出队 |
value_type& front(); const value_type& front() const; | 返回队头数据 |
value_type& back(); const value_type& back() const; | 返回队尾数据 |
bool empty() const; | 判断队列是否为空 |
size_type size() const; | 返回队列大小 |
同样,我们用一道例题来熟悉队列queue的操作:👉两个队列实现栈
示例代码:
class MyStack {
public:
MyStack() {
}
void push(int x) {
q_in.push(x);
}
int pop() {
//将入队队列数据导入至辅助队列,留下最后一个数据,即出队数据
while (q_in.size() > 1)
{
int val = q_in.front();
q_in.pop();
q_out.push(val);
}
int ret = q_in.front();
q_in.pop();
//将辅助队列的数据导回入队队列
while (!q_out.empty())
{
int val = q_out.front();
q_out.pop();
q_in.push(val);
}
return ret;
}
int top() {
return q_in.back();
}
bool empty() {
return q_in.empty();
}
queue<int> q_in; //入队队列
queue<int> q_out; //辅助出队队列
};
3.1.3 模拟实现
同样,queue
也是一个适配器,只需要对符合条件的容器进行复用适配即可
适配容器需要满足以下接口:
push_back()
,尾插pop_front()
,头删size()
,返回数据个数empty()
,判断是否为空back()
,返回尾部数据front()
,返回头部数据
所以我们知道,容器vector
不能作为queue
的适配容器
namespace TEST
{
template<class T, class container = deque<T>>
class queue
{
public:
void push(const T& val)
{
_con.push_back(val);
}
void pop()
{
_con.pop_front();
}
T& back()
{
return _con.back();
}
T& front()
{
return _con.front();
}
const T& back() const
{
return _con.back();
}
const T& front() const
{
return _con.front();
}
bool empty()
{
return _con.empty();
}
size_t size()
{
return _con.size();
}
private:
container _con;
};
}
3.2 优先队列 priority_queue
3.2.1 概念及结构
优先队列实际上就是数据结构中的堆,如果对堆的结构性质及其用法不太了解,强烈建议先看看👉数据结构——堆
3.2.2 使用
使用时需要包含头文件:
<queue>
priority_queue
是一个模板类,其具体的声明如下:
template <class T, class Container = vector<T>,
class Compare = less<typename Container::value_type> > class priority_queue;
-
T
即为priority_queue存储数据的具体类型 -
而
Container
则为priority_queue所适配的容器,vector
为priority_queue默认的适配容器 -
Compare
是一个仿函数,确定这个堆是大堆还是小堆。priority_queue默认的是大堆
其包含如下常见操作:
函数接口 | 接口说明 |
---|---|
void push (const value_type& val); | 添加数据 |
void pop(); | 删除堆顶元素 |
const value_type& top() const; | 返回堆顶元素 |
size_type size() const; | 返回堆的大小 |
bool empty() const; | 判断堆是否为空 |
同样,我们通过一道例题来熟悉priority_queue的使用:👉数组中第K大的元素
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
//利用数组的前K个数构建小堆
priority_queue<int, vector<int>, greater<int> > pq(nums.begin(), nums.begin() + k);
//遍历剩下的数据
//如果数据比堆顶元素大,就删除堆顶元素,再将新的数据入堆
for (int i = k; i < nums.size(); i++)
{
if (nums[i] > pq.top())
{
pq.pop();
pq.push(nums[i]);
}
}
//最后大小为K的小堆最大的K个数字
//堆顶就是第K大的数据
return pq.top();
}
};
3.2.3 模拟实现
这里我们先来讲一讲什么是仿函数
通俗的来讲,仿函数其实就是重载了运算符
()
的类对象因为这个类对象重载了
()
,所以我们可以像调用函数一样来调用这个类的某个功能。所以我们称其为仿函数举个例子:
template <class T> struct Less { bool operator() (const T& a, const T& b) { return a < b; } }; int main() { Less<int> le; cout << le(1, 4) << endl; return 0; }
这里我们定义了一个类模板
Less
,在这个里面重载了运算符()
用来确定两个数的大小关系之后我们就可以用
Less
实例化出的对象来调用()
的重载,也就可以像使用函数一样来使用这个类了。
priority_queue
是一个适配器,进行适配的容器需要满足以下条件:
push_back()
,尾插pop_back()
,尾删size()
,返回数据个数empty()
,判断是否为空back()
,返回尾部数据
namespace TEST
{
//确定小于关系的仿函数
template<class T>
struct less
{
bool operator() (const T& a, const T& b)
{
return a < b;
}
};
//确定大于关系的仿函数
template<class T>
struct greater
{
bool operator() (const T& a, const T& b)
{
return a > b;
}
};
template<class T, class container = vector<T>, class compare = less<T> >
class priority_queue
{
public:
//向上调整
void adjust_up(int child)
{
int parent = (child - 1) / 2;
while (child > 0)
{
if (_comp(_con[parent], _con[child]))
{
swap(_con[child], _con[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
break;
}
}
//向下调整
void adjust_down(int parent)
{
int child = parent * 2 + 1;
while (child < _con.size())
{
if (child + 1 < _con.size() && _comp(_con[child], _con[child + 1]))
++child;
if (_comp(_con[parent], _con[child]))
{
swap(_con[child], _con[parent]);
parent = child;
child = parent * 2 + 1;
}
else
break;
}
}
void push(const T& val)
{
_con.push_back(val);
adjust_up(_con.size() - 1);
}
void pop()
{
swap(_con[0], _con[_con.size() - 1]);
_con.pop_back();
adjust_down(0);
}
const T& top() const
{
return _con.front();
}
size_t size() const
{
return _con.size();
}
bool empty() const
{
return _con.empty();
}
private:
container _con;
compare _comp;
};
}
注:博主模拟实现priority_queue
时由于使用了运算符[]
来进行数据的随机访问,从而维护堆的结构,由于list
容器并没有支持[]
的重载,因此博主模拟的priority_queue
并不适配list
。但是标准库的priority_queue
实现起来更加复杂,会用make_heap、push_heap和pop_heap
等算法函数,因此可以适配list
容器。
4. 双端队列 deque
4.1 概念及结构
双端队列是一个支持从头部或尾部插入删除数据的数据结构
其底层上并不是队列,而是一段段连续的小空间拼接而成,如图所示:
从上图中我们可以看出:
deque
由中控器管理着数据,其实际上是一个指针数组,其每个指针指向一段段连续的小空间- 缓冲区一段段的小空间存储着真实的数据,当一段小空间存满时就会开辟新的小空间
deque
的在头部和尾部插入、删除的效率都很高,因为它不需要挪动其余元素deque
支持随机访问,但需要根据每一小段的长度进行计算索引所在的位置,效率较低deque
在遍历数据时需要不断判断是否到了每一小段的边界,效率较低
4.2 使用
使用时需要包含头文件:
<deque>
其是一个类模板,其具体声明如下:
template < class T, class Alloc = allocator<T> > class deque;
其具体使用和vector
类似,但多了push_front()
和pop_front()
,具体的使用可以查看👉操作手册
4.3 优缺点和适用情况
优点:
- 和
vector
相比,它支持数据在头部的插入和删除,而且效率高- 和
list
相比,它的空间利用率更高
缺点:
deque
最致命的缺点便是它在随机访问数据时需要通过计算才能得到具体的位置,同时遍历访问数据时需要不断判断是否到了小段区域的边界,效率较低
适用情形:
- 在实际中,如果要运用到线性结构,一般都会涉及到数据的遍历,因此一般都会使用
vector
和list
- 但是在**
stack
和queue
的实现中,由于二者都只会对数据的头部或尾部做处理**,不会对数据进行随机访问和遍历的操作,这就完美的迎合了deque
的优点,并避开了其缺点,这也是为什么stack
和queue
的默认适配容器为deque
- 而优先队列
priority_queue
由于要维护堆的结构,需要随机访问数据,因此其默认适配容器为vector