stack和queue
1、stack
1.1 stack的介绍
- stack是一种容器适配器,专门设计用于先出环境(后进先出),即只从容器一端插入和提取元素。
- stack以容器适配器的形式实现,容器适配器是使用特定容器类封装对象作为底层容器的类,提供特定的成员函数集以访问其元素。元素是从特定容器的尾部(栈顶)推送/弹出的,这被称为堆叠顶部。
- stack的底层容器可以是任何标准容器类模板,也可以是其他专门设计的容器类。这些应支持以下操作:
- empty:判空
- size:大小
- back:获取尾部元素
- push_back:尾部插入
- pop_back:尾部删除
1.2 stack的使用
| 函数说明 | 接口说明 |
|---|---|
| stack() | 构造空的栈 |
| empty() | 检测 stack 是否为空 |
| size() | 返回 stack 中元素的个数 |
| top() | 返回栈顶元素的引用 |
| push() | 将元素 val 压入 stack 中 |
| pop() | 将 stack 中尾部的元素弹出 |
1.2.1 最小栈

思路:用两个栈实现,其中一个栈_st用来正常存储数据,另一个栈_minst用来存储最小的数据。具体实现是往_st中插入数据时判断,如果当前插入的数据val小于等于_minst栈顶的数据,就将val插入到_minst这个栈中。否则直接将数据插入_st中。在pop数据时,先取_st的栈顶元素和_minst的栈顶元素进行比较,如果两者相等,那就同时取_st和_minst的栈顶元素,要获取堆栈中的最小元素直接返回_minst的栈顶元素即可
c
class MinStack
{
public:
MinStack() {}
void push(int val)
{
_st.push(val);
if(_minst.empty() || val <= _minst.top())
{
_minst.push(val);
}
}
void pop()
{
if(_st.top() == _minst.top())
{
_minst.pop();
}
_st.pop();
}
int top()
{
return _st.top();
}
int getMin()
{
return _minst.top();
}
private:
stack<int> _st;
stack<int> _minst;
};
1.2.2 栈的压入、弹出序列

本题思路是先定义一个栈st,然后给栈中入一个数据,接着取栈顶的数据和出栈序列popV当前位置进行比较,如果不相等则继续从入栈序列pushV中拿数据往st中入,如果相等就出栈,最后判断栈st是否为空,如果为空说明出站序列正确,如果不为空说明出站序列有问题
c
class Solution {
public:
bool IsPopOrder(vector<int>& pushV, vector<int>& popV)
{
stack<int> st;
size_t push_pos = 0, pop_pos = 0;
while(push_pos < pushV.size())
{
//先入一个元素
//入栈序列入栈
st.push(pushV[push_pos++]);
if(st.top() != popV[pop_pos])
{
//不匹配继续入数据
continue;
}
//栈顶尝试跟出数据序列匹配
while(!st.empty() && st.top() == popV[pop_pos])
{
//出数据
st.pop();
pop_pos++;
}
}
return st.empty();
}
};
1.2.3 逆波兰表达式求值

逆波兰表达式也叫后缀表达式,中缀表达式是我们平时最常见的,例如:10*2-8,变成后缀表达式后得到:10 2 * 8 -,操作顺序不变,操作符按优先级重排。这道题要求对后缀表达式进行求解,可以借助栈,遇到操作数入栈,遇到操作符从栈中出两个元素进行运算,将运算结果继续入栈。最终栈顶的元素就是整个逆波兰表达式的结果
c
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<int> st;
for(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.2.4 用栈实现队列

将一个栈作为输入栈,用于压入push传入的数据,另一个栈作为输出栈,用于pop和peek,输入栈和入队顺序一致,输出栈和出队顺序一致
c
typedef int STDataType;
typedef struct Stack {
STDataType* arr;
int top; //指向栈顶的位置
int capacity;//栈的容量
}ST;
//栈的结构
//初始化
void StackInit(ST* ps) {
ps->arr = NULL;
ps->top = ps->capacity = 0;
}
//栈的销毁
void StackDestroy(ST* ps) {
if (ps->arr) {
free(ps->arr);
}
ps->arr = NULL;
ps->top = ps->capacity = 0;
}
//入栈---栈顶
void StackPush(ST* ps, STDataType x) {
assert(ps);
if (ps->top == ps->capacity) {
int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
STDataType* tmp = (STDataType*)realloc(ps->arr, newCapacity * sizeof(STDataType));
if (tmp == NULL) {
perror("realloc fail!");
exit(1);
}
ps->arr = tmp;
ps->capacity = newCapacity;
}
ps->arr[ps->top++] = x;
}
//判断栈是否为空
bool StackEmpty(ST* ps) {
assert(ps);
return ps->top == 0;
}
//出栈---栈顶
void StackPop(ST* ps) {
assert(!StackEmpty(ps));
--ps->top;
}
//取栈顶元素
//数据有效个数不减少
//只取栈顶数据 不删除数据
STDataType StackTop(ST* ps) {
assert(!StackEmpty(ps));
return ps->arr[ps->top - 1];
}
//获取栈中的有效元素个数
int StackSize(ST* ps) {
return ps->top;
}
//入队 往pushST中插入数据
//出队 popST不为空从popST中删除数据 否则将pushST中的数据导入popST中再出数据
//取队头元素 popST不为空从popST中取数据 否则将pushST中的数据导入popST中再取数据
typedef struct {
ST pushST;
ST popST;
}MyQueue;
MyQueue* myQueueCreate() {
MyQueue* pq = (MyQueue*)malloc(sizeof(MyQueue));
StackInit(&pq->pushST);
StackInit(&pq->popST);
return pq;
}
void myQueuePush(MyQueue* obj, int x) {
//入队 往pushST中插入数据
StackPush(&obj->pushST, x);
}
int myQueuePop(MyQueue* obj) {
//出队 popST不为空从popST中删除数据 否则将pushST中的数据导入popST中再出数据
if (StackEmpty(&obj->popST)) {
//将pushST中的数据导入popST中
while (!StackEmpty(&obj->pushST))
{
StackPush(&obj->popST, StackTop(&obj->pushST));
StackPop(&obj->pushST);
}
}
int top=StackTop(&obj->popST);
StackPop(&obj->popST);
return top;
}
int myQueuePeek(MyQueue* obj) {
if (StackEmpty(&obj->popST)) {
//将pushST中的数据导入popST中
while (!StackEmpty(&obj->pushST))
{
StackPush(&obj->popST, StackTop(&obj->pushST));
StackPop(&obj->pushST);
}
}
return StackTop(&obj->popST);
}
bool myQueueEmpty(MyQueue* obj) {
return StackEmpty(&obj->pushST) && StackEmpty(&obj->popST);
}
void myQueueFree(MyQueue* obj) {
StackDestroy(&obj->pushST);
StackDestroy(&obj->popST);
free(obj);
obj = NULL;
}
2、queue
2.1 queue的介绍
- 队列是一种容器适配器,专门设计用于先进先出环境下,即将元素插入容器的一端,从另一端提取。
- 队列实现为容器适配器,这些类使用特定容器类的封装对象作为底层容器,提供特定的成员函数集以访问其元素。元素从不队尾入队列,从队头出队列。
- 底层容器可以是标准容器类模板或其他专门设计的容器类。该底层容器应至少支持以下操作:
- empty:检查队列是否为空
- size:返回队列中有效元素个数
- front:返回队头元素的引用
- back:返回队尾元素的引用
- push_back:在队列尾部入队列
- pop_front:在队列头部出队列
2.2 queue的使用
| 函数声明 | 接口说明 |
|---|---|
| queue() | 构造空的队列 |
| empty() | 检测队列是否为空,是返回true,否则返回false |
| size() | 返回队列中有效元素的个数 |
| front() | 返回队头元素的引用 |
| back() | 返回队尾元素的引用 |
| push() | 在队尾将元素val入队列 |
| pop() | 将队头元素出队列 |
2.2.1 二叉树的层序遍历

二叉树的层序遍历可以借助队列实现,先让根节点入队列,首先取出队头元素,把队头删除,左孩子不为空时让左孩子入队列,右孩子不为空时让右孩子入队列,充当根节点
c
class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
vector<vector<int>> vv;
// 1. 定义队列
queue<TreeNode*> q;
// 2. 如果根不为空,入队
if(root) q.push(root);
// 3. 初始化当前层节点数
int levelSize = q.size();
while (!q.empty())
{
vector<int> v;
// 控制一层一层出
while (levelSize--)
{
TreeNode* front = q.front();
q.pop();
v.push_back(front->val);
if (front->left)
q.push(front->left);
if (front->right)
q.push(front->right);
}
vv.push_back(v);
// 当前层出完了,下一层节点都进队列了,队列数据个数就是下一层的节点个数
levelSize = q.size();
}
return vv; // 4. 必须返回结果
}
};
3、模拟实现
3.1 stack模拟实现
c
template<class T, class Container = deque<T>>
class stack
{
public:
void push(const T& x)
{
_con.push_back(x);
}
void pop()
{
_con.pop_back();
}
size_t size() const
{
return _con.size();
}
bool empty() const
{
return _con.empty();
}
const T& top() const
{
return _con.back();
}
T& top()
{
return _con.back();
}
private:
Container _con;
};
注意:
- 因为stack可以由vector或list或deque多种容器作为内核实现,所以将容器写成一个模版参数Container,让用户自己选择使用哪种容器
- stack可以用vector或者list实现
3.2 queue模拟实现
c
template<class T, class Container = deque<T>>
class queue
{
public:
void push(const T& x)
{
_con.push_back(x);
}
void pop()
{
//_con.erase(_con.begin());
_con.pop_front();
}
size_t size() const
{
return _con.size();
}
bool empty() const
{
return _con.empty();
}
const T& front() const
{
return _con.front();
}
T& front()
{
return _con.front();
}
const T& back() const
{
return _con.back();
}
T& back()
{
return _con.back();
}
private:
Container _con;
};
注意:不能借助vector实现,因为出队列相当于删除vector中的第一个元素,效率低
4、deque的介绍
4.1 deque的原理
deque是一种双端开口的数据结构,可以在头尾两端进行插入和删除,时间复杂度为O(1),与vector相比,头插效率高,与list相比,空间利用率高

deque并不是真正连续的空间,而是由一段段连续的小空间拼接而成的,实际deque类似于一个动态的二维数组,其底层结构如下图所示:

双端队列底层是一段假象的连续空间,实际是分段连续的,为了维护其"整体连续"以及随机访问的假象,落在了deque的迭代器身上 ,因此deque的迭代器设计就比较复杂,如下图所示:

4.2 deque的缺陷
- 与vector比较 ,deque的优势是:头部插入和删除时,不需要搬移元素,效率特别高 ,而且在扩容时,也不需要搬移大量的元素,因此其效率是必vector高的。
- 与list比较 ,其底层是连续空间,空间利用率比较高,不需要存储额外字段。
- 但是,deque有一个致命缺陷:不适合遍历,因为在遍历时,deque的迭代器要频繁的去检测其是否移动到某段小空间的边界,导致效率低下 ,而序列式场景中,可能需要经常遍历,因此在实际中,需要线性结构时,大多数情况下优先考虑vector和list ,deque的应用并不多,而目前能看到的一个应用就是,STL用其作为stack和queue的底层数据结构。
4.3 为什么选择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的效率高(扩容时不需要搬移大量数据);queue中的元素增长时,deque不仅效率高,而且内存使用率高。
5、priority_queue
5.1 priority_queue的介绍
-
优先队列是一种容器适配器,专门设计使其第一个元素总是其所包含元素中最大的一个,符合某种严格的弱排序标准。
-
这种上下文类似于堆,元素可以随时插入,且只能检索最大堆元素(优先队列顶部的那个)。
-
优先队列以容器适配器的形式实现,这些类使用特定容器类的封装对象作为底层容器,queue提供特定的成员函数集以访问其元素。元素是从特定容器的"尾部"弹出的,这被称为优先队列的顶部。
-
底层容器可以是任何标准容器类模板,也可以是其他专门设计的容器类。容器应通过随机访问迭代器访问,并支持以下操作:
- empty():检查是否为空
- size():返回容器中有效元素个数
- front():返回容器中第一个元素的引用
- push_back():在容器尾部插入元素
- pop_back():删除容器尾部元素
-
标准容器类vector和deque满足这些要求。默认情况下,如果没有为特定的priority_queue类实例化指定容器类,则使用vector。
-
为了始终保持堆结构,需要支持随机访问迭代器。这由容器适配器自动调用算法函数make_heap、push_heap和pop_heap来自动完成此操作(即存在建堆算法、向上向下调整算法)
5.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是大堆。如果在priority_queue中放自定义类型的数据,用户需要在自定义类型中提供>或<的重载
5.2.1 数组中的第k个最大元素

c
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
// 将数组中的元素先放入优先级队列中
priority_queue<int> p(nums.begin(), nums.end());
// 将优先级队列中前k-1个元素删除掉
for (int i = 0; i < k - 1; ++i)
{
p.pop();
}
return p.top();
}
};
5.3 priority_queue模拟实现
5.3.1 仿函数
仿函数是个类,其内部重载了operator()函数,可以使一个类的对象当做是一个函数去用,和正常函数一样,因此叫做仿函数。
eg:
c
#include <iostream>
using namespace std;
// 定义一个仿函数类
struct Add {
// 重载 () 运算符
int operator()(int a, int b) {
return a + b;
}
};
int main() {
Add add; // 创建仿函数对象
// 像函数一样使用
int res = add(10, 20);
cout << "结果:" << res << endl; // 输出 30
return 0;
}
仿函数一般都是类模板,可以支持不同类型的变量
priority_queue的实现:本质是堆
c
#pragma once
#include<vector>
namespace central
{
template<class T, class Container = vector<T>, class Compare = Less<T>>
class priority_queue
{
public:
priority_queue() = default;
template <class InputIterator>
priority_queue(InputIterator first, InputIterator last)
:_con(first, last)
{
// 向下调整建堆
for (int i = (_con.size()-1-1)/2; i >= 0; i--)
{
adjust_down(i);
}
}
void push(const T& x)
{
_con.push_back(x);
adjust_up(_con.size() - 1);
}
void pop()
{
swap(_con[0], _con[_con.size() - 1]);
_con.pop_back();
adjust_down(0);
}
const T& top()
{
return _con[0];
}
bool empty() const
{
return _con.empty();
}
size_t size() const
{
return _con.size();
}
private:
void adjust_up(int child)
{
Compare com;
int parent = (child - 1) / 2;
while (child > 0)
{
//if (_con[child] > _con[parent])
//if (_con[parent] < _con[child])
if (com(_con[parent], _con[child]))
{
swap(_con[child], _con[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
void adjust_down(int parent)
{
Compare com;
size_t child = parent * 2 + 1;
while (child < _con.size())
{
// 选出大的那个孩子
if (child + 1 < _con.size() && com(_con[child], _con[child + 1]))
++child;
if (com(_con[parent], _con[child]))
{
swap(_con[child], _con[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
private:
Container _con;
};
}