文章目录
- stack和queue的使用和相关接口
-
- [1.1. stack 的本质](#1.1. stack 的本质)
- [1.2. stack 的常用接口](#1.2. stack 的常用接口)
- 1.3,stack相关算法题
- [2.1. queue 的本质](#2.1. queue 的本质)
- [2.2. queue 的常用接口](#2.2. queue 的常用接口)
- 2.3,相关经典算法题
- stack和queue的底层实现
-
- 容器适配器
- [vector 和 list 优缺点总结](#vector 和 list 优缺点总结)
- [deque 双端队列(了解即可,但必须要了解)](#deque 双端队列(了解即可,但必须要了解))
-
- [1 deque的原理介绍](#1 deque的原理介绍)
- [2, deque的迭代器设计](#2, deque的迭代器设计)
-
- [1、deque 迭代器包含四部分](#1、deque 迭代器包含四部分)
- 3,deque的缺陷------不适合遍历
- 4,为什么选择deque作为stack和queue的底层默认容器
- priority_queue优先级队列
- 总结与复习
- 三、总结与回顾
-
- 一、容器适配器
- 二、为什么默认底层容器不同?
- [三、deque 为什么适合作为 stack 和 queue 的底层?](#三、deque 为什么适合作为 stack 和 queue 的底层?)
- [四、priority_queue 本质](#四、priority_queue 本质)
- 五、仿函数(重点)
- 六、什么时候需要自己写仿函数?
- 下期预告
- 模版进阶
- 结语

◆ 博主: @此生决int
分享编程知识!深挖底层原理!持续原创更新!
热门专栏:
📌 本系列知识点前后关联较强,建议按照专栏顺序阅读哦
本系列主要面向的阅读人群
1,学完 C 语言,准备系统学习 C++ 的同学
2,已经学习过 C++,希望重新梳理知识体系的同学
3,想深入理解 C++ 设计思想与底层原理,而不仅仅停留在语法层面的同学
上期回顾
list------链表
stack和queue的使用和相关接口
1.1. stack 的本质
stack 是一种:后进先出(LIFO) 的线性结构。

1.2. stack 的常用接口
| 接口 | 作用 |
|---|---|
stack() |
构造空栈 |
empty() |
判断是否为空 |
size() |
返回元素个数 |
top() |
返回栈顶元素引用 |
push() |
压栈 |
pop() |
弹栈 |
| 因为我们之前在数据结构部分已经学习过了,所以,这里就不再赘述了! |
1.3,stack相关算法题
最小栈⭐⭐
题目链接
题目描述
设计一个支持 push、pop、top 操作,并能在 O(1) 时间内获取栈中最小元素的栈。

解题思路
使用两个栈维护数据。一个栈正常存储所有元素,另一个栈专门维护当前最小值。当插入的新元素小于等于当前最小值时,将其压入最小栈;弹出元素时,如果弹出的正是当前最小值,则最小栈同步弹出即可。
解题代码
cpp
class MinStack {
public:
MinStack() {
}
void push(int value) {
// 当前元素不大于最小值时,同时进入最小栈
if (minst.empty() || value <= minst.top())
{
minst.push(value);
}
st.push(value);
}
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; // 最小栈,维护当前最小值
};
没懂?看看大神的解题代码!!
大神解题代码
cpp
class MinStack {
public:
MinStack() {
}
void push(int val) {
st.push(val);
// minSt 的栈顶始终对应当前最小值
if (minSt.empty())
minSt.push(val);
else
minSt.push(min(val, minSt.top()));
}
void pop() {
st.pop();
minSt.pop();
}
int top() {
return st.top();
}
int getMin() {
return minSt.top();
}
private:
stack<int> st;
stack<int> minSt;
};
栈的压入、弹出序列⭐⭐
题目链接
题目描述
输入两个整数序列,第一个序列表示栈的压入顺序,判断第二个序列是否可能是该栈的弹出顺序。假设压入栈的所有数字均不相等。

解题思路
利用栈模拟整个压栈、出栈过程。依次将压栈序列入栈,每次入栈后,只要栈顶元素等于当前待弹出的元素,就不断弹栈。最后若弹出序列全部匹配,则说明该弹出序列合法。
解题代码
cpp
class Solution {
public:
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
* @param pushV int整型vector
* @param popV int整型vector
* @return bool布尔型
*/
bool IsPopOrder(vector<int>& pushV, vector<int>& popV) {
// 模拟压栈、出栈过程
int i = 0, j = 0;
stack<int> st;
while (i < pushV.size())
{
// 当前元素入栈
st.push(pushV[i]);
// 只要栈顶等于当前待弹出的元素,就持续出栈
while (!st.empty() && st.top() == popV[j])
{
st.pop();
j++;
}
i++;
}
// 所有元素都成功匹配,则说明弹出序列合法
return j == popV.size();
}
};
没懂?看看大神的解题代码!!
大神解题代码
cpp
class Solution {
public:
bool IsPopOrder(vector<int>& pushV, vector<int>& popV) {
stack<int> st;
int j = 0;
for (int x : pushV)
{
// 模拟入栈
st.push(x);
// 能出栈就一直出栈
while (!st.empty() && st.top() == popV[j])
{
st.pop();
j++;
}
}
// 栈为空说明所有元素都已正确匹配
return st.empty();
}
};
逆波兰表达式求值⭐⭐
题目链接
题目描述
根据逆波兰表达式(后缀表达式)计算表达式的值。运算符仅包含 +、-、*、/,保证表达式合法。

解题思路
利用栈模拟计算过程。遇到数字直接入栈,遇到运算符则弹出栈顶两个元素进行运算,再将结果压回栈中,最终栈顶元素就是答案。
解题代码
cpp
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<int> st;
for (auto& it : tokens)
{
// :&& 优先级高于 ||,应加括号保证整体判断正确
if (st.size() >= 2 &&
(it == "+" || it == "-" || it == "*" || it == "/"))
{
// 注意先弹出的是第二个操作数
int b = st.top();
st.pop();
int a = st.top();
st.pop();
int ret = 0;
char ch = it[0];
switch (ch)
{
case '+':
ret = a + b;
break;
case '-':
ret = a - b;
break;
case '*':
ret = a * b;
break;
case '/':
ret = a / b;
break;
}
st.push(ret);
}
else
{
// 数字直接入栈
st.push(stoi(it));
}
}
return st.top();
}
};
没懂?看看大神的解题代码!!
大神解题代码
cpp
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<int> st;
for (auto& s : tokens)
{
// 数字直接入栈
if (s != "+" && s != "-" && s != "*" && s != "/")
{
st.push(stoi(s));
}
else
{
// 注意出栈顺序:先右操作数,再左操作数
int b = st.top();
st.pop();
int a = st.top();
st.pop();
if (s == "+") st.push(a + b);
else if (s == "-") st.push(a - b);
else if (s == "*") st.push(a * b);
else st.push(a / b);
}
}
return st.top();
}
};
2.1. queue 的本质
queue 是一种: 先进先出(FIFO) 的线性结构。

2.2. queue 的常用接口
| 接口 | 作用 |
|---|---|
queue() |
构造空队列 |
empty() |
判断是否为空 |
size() |
返回元素个数 |
front() |
返回队头元素引用 |
back() |
返回队尾元素引用 |
push() |
在队尾入队 |
pop() |
在队头出队 |
2.3,相关经典算法题
二叉树的层序遍历⭐⭐⭐
题目链接
题目描述
给你二叉树的根节点 root,返回其节点值的层序遍历结果,即逐层从左到右访问所有节点。

解题思路
利用队列实现 BFS。每次记录当前队列中的节点个数,这些节点正好是一层,依次取出并将其左右孩子加入队列,同时保存下一层节点的值,直到队列为空。
解题代码
cpp
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right)
* : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
// 一层不是一个父节点的孩子,而是队列中当前所有节点的孩子。
vector<vector<int>> levelOrder(TreeNode* root) {
if (root == nullptr)
return {};
vector<vector<int>> ret;
queue<TreeNode*> q;
q.push(root);
// 第一层只有根节点
ret.push_back({root->val});
while (!q.empty())
{
vector<int> a;
// 当前队列中的所有节点就是同一层
size_t _size = q.size();
while (_size--)
{
TreeNode* tmp = q.front();
// 下一层左孩子
if (tmp->left)
{
q.push(tmp->left);
a.push_back(tmp->left->val);
}
// 下一层右孩子
if (tmp->right)
{
q.push(tmp->right);
a.push_back(tmp->right->val);
}
q.pop();
}
// 下一层存在节点才加入答案
if (!a.empty())
ret.push_back(a);
}
return ret;
}
};
没懂?看看大神的解题代码!!
大神解题代码
cpp
class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
if (root == nullptr)
return {};
vector<vector<int>> ret;
queue<TreeNode*> q;
q.push(root);
while (!q.empty())
{
int sz = q.size();
vector<int> level;
// 一次处理一整层
while (sz--)
{
TreeNode* cur = q.front();
q.pop();
// 当前层节点加入答案
level.push_back(cur->val);
// 左右孩子进入下一层
if (cur->left) q.push(cur->left);
if (cur->right) q.push(cur->right);
}
ret.push_back(level);
}
return ret;
}
};
stack和queue的底层实现
容器适配器
由于stack和queue的特性,我们其实只需要对像vector和list等容器规定只能尾插尾删就可以实现stack,我们数据结构里面也是这么做的,所以,我们想到可以直接这样来实现栈
cpp
template<class T>
class stack
{
public:
void push(const T& x)
{
v.push_back(x);
}
void pop()
{
v.pop_back();
}
......
private:
vector v;
};
这里的vector呢也可以换成list 等等,进而,我们可以加入模版
cpp
template<class T, class Container >
class stack
{
public:
void push(const T& x)
{
_con.push_back(x);
}
void pop()
{
_con.pop_back();
}
const T& top() const
{
return _con.back();
}
size_t size() const
{
return _con.size();
}
bool empty() const
{
return _con.empty();
}
private:
Container _con;
};
同理,我们也可以实现队列
cpp
template<class T, class Container >
class queue
{
public:
void push(const T& x)
{
_con.push_back(x);
}
void pop()
{
_con.pop_front();
}
const T& front() const
{
return _con.front();
}
const T& back() const
{
return _con.back();
}
size_t size() const
{
return _con.size();
}
bool empty() const
{
return _con.empty();
}
private:
Container _con;
};
我们可以在这里给上缺省参数:
cpp
template<class T, class Container=vector<T>>
这里, vector和list都可以,但是你会发现库里面 给的是 template<class T, class Container =deque<T>>
deque双端队列!!vector和list的缝合怪
好,再了解deque之前,我们先来总结一下vector和list他们各自的优缺点!
vector 和 list 优缺点总结
vector
由于vector是连续存储,空间连续
| 优点 | 缺点 |
|---|---|
✅ 支持随机访问(O(1)) |
❌ 中间或头部插入、删除效率低(O(n)) |
✅ 尾插、尾删效率高(均摊 O(1)) |
❌ 扩容需要重新申请空间,导致迭代器失效 |
| ✅ CPU 缓存命中率高,遍历速度快 | - |
list
| 优点 | 缺点 |
|---|---|
✅ 任意位置插入、删除效率高(已知位置为 O(1)) |
❌ 不支持随机访问,只能顺序遍历(O(n)) |
| ✅ 不需要连续空间,不会整体扩容 | ❌ 每个节点额外存储两个指针,空间开销大 |
| ✅ 插入、删除不会导致其他节点迭代器失效(被删除节点除外) | ❌ 缓存命中率低,遍历速度慢 |
那么,我们可不可以把他们的优缺点都结合起来 形成一个新的结构呢?可以------deque双端队列!
deque 双端队列(了解即可,但必须要了解)
1 deque的原理介绍
deque(双端队列):是一种双开口的"连续"空间的数据结构,双开口的含义是:可以在头尾两端进行插入和删除操作,且时间复杂度为O(1),与vector比较,头插效率高,不需要搬移元素;与list比较,空间利用率比较高。

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

2, deque的迭代器设计

1、deque 迭代器包含四部分
由上图可以看出:STL 中 deque 的迭代器一般包含四个成员:
| 成员 | 含义 |
|---|---|
| first | 当前 Buffer 的起始位置 |
| last | 当前 Buffer 的结束位置(尾后位置) |
| cur | 当前元素位置 |
| node | 当前 Buffer 在中控数组中的位置 |
那deque是如何借助其迭代器维护其假想连续的结构呢?

总之,数据一般存在中控数组的中间,如果头插,就在中控数组的左边(前面)新增一个数组,尾插就是直接在中控数组的右边(后面)新增一个数组,每次都是新增一个buff数组,不需要频繁的扩容!
注意:deque的尾插尾删,头插头删都非常厉害,\[\]的效率略低于vector,但是,他的insert和erase非常非常慢!但一般也不会用!
3,deque的缺陷------不适合遍历
deque有一个致命缺陷:不适合遍历 ,因为在遍历时,deque的迭代器要频繁的去检测其是否移动到某段小空间的边界,导致效率低下,而序列式场景中,可能需要经常遍历,因此在实际中,需要线性结构时,大多数情况下优先考虑vector和list,deque的应用并不多 ,而目前能看
到的一个应用就是,STL用其作为stack和queue的底层数据结构。但是,他作为stack和queue的底层数据结构刚刚好,十分合适!!!
4,为什么选择deque作为stack和queue的底层默认容器
- stack和queue不需要遍历 (因此stack和queue没有迭代器),只需要在固定的一端或者两端进
行操作。 - 在stack中元素增长时,deque比vector的效率高(扩容时不需要搬移大量数据);queue中的元素增长时,deque不仅效率高,而且内存使用率高
- stack只需要尾插尾删,queue只需要尾插头删,都不需要遍历
priority_queue优先级队列
1,介绍+使用
优先级队列就是我们学习过的------堆,默认是大堆!堆------完全二叉树,底层是数组!他和stack和queue一样,是容器适配器!标准容器类vector和deque满足这些需求。但是vector的【】的效率更高,所以,默认是vector、
1,接口
| 函数声明 | 接口说明 |
|---|---|
priority_queue() / priority_queue(first, last) |
构造一个空的优先级队列,或使用区间 [first, last) 中的元素构造优先级队列 |
empty() |
检测优先级队列是否为空 |
top() |
返回堆顶元素 |
push(x) |
插入元素 x |
pop() |
删除堆顶元素 |
| 同时,库里面还提供了一些与堆相关的算法: |
2,堆相关算法(就是不用,了解即可)

| 堆算法 | 接口说明 |
|---|---|
make_heap(first, last) |
将区间 [first, last) 中的元素构造成一个堆(默认大堆) |
push_heap(first, last) |
将区间最后一个元素插入到堆中,并重新调整为堆结构 |
pop_heap(first, last) |
将堆顶元素移动到区间末尾,并重新调整剩余元素为堆结构(注意:不会真正删除元素) |
sort_heap(first, last) |
对堆进行排序,最终得到升序序列(默认大堆) |
is_heap(first, last) |
判断指定区间是否满足堆结构,满足返回 true,否则返回 false |
is_heap_until(first, last) |
返回区间中第一个不满足堆性质的位置,若整个区间都是堆,则返回 last |
注意:
- push_heap()、pop_heap()、sort_heap() 都要求区间已经是一个合法的堆。
- pop_heap() 不会删除元素,只是把堆顶交换到末尾,真正删除需要再调用容器的 pop_back()。
- 默认使用 < 比较,即构造大根堆;传入比较器(如 greater)即可构造小根堆。
2,优先级队列的底层实现
按照我们刚刚学习stack和queue的一样;我们也可以这样实现优先级队列
cpp
template<class T, class Container = std::vector<T> >
class priority_queue
{
public:
//默认大的权重大,默认是大堆,
void AdjustUp(int child)
{
}
void AdjustDown(int parent)
{
}
void push(const T& x)
{
_con.push_back(x);
AdjustUp(_con.size() - 1);
}
void pop()
{
swap(_con[0], _con[_con.size() - 1]);
_con.pop_back();
AdjustDown(0);
}
T& top()
{
return _con[0];
}
size_t size()
{
return _con.size();
}
bool empty()
{
return _con.empty();
}
private:
Container _con;
//size_t _size=0;
};
但是,我们会发现,如果这样实现的话就是堆是大堆还是小堆就固定了,非常不好。所以,C++提供了仿函数来解决这类问题,那么,什么是仿函数呢?
可以稍微润色一下,保持你一直以来简洁、偏教程的风格:
仿函数
仿函数(Functor) :本质上是一个类 ,只要该类重载了 operator() ,那么它的对象就可以像函数 一样被调用,因此称为函数对象(Function Object)。
写法
一个经典的仿函数如下:
cpp
template<class T>
class Lesser
{
public:
bool operator()(const T& x, const T& y)
{
return x < y;
}
};
使用时与普通函数几乎没有区别:
cpp
Lesser<int> less;
cout << less(1, 2) << endl; // 输出 1(true)
STL 中大量使用仿函数作为比较规则 ,例如
sort、set、map、priority_queue等容器和算法都支持传入自定义仿函数。
那加入了仿函数之后,我们的AdjustUp和AdjustDown函数就可以写成
cpp
void AdjustUp(int child)
{
int paret = (child-1) / 2;
//没有写仿函数的版本:while (child >= 0 && _con[child] > _con[parent])
//while (child >= 0 && _con[parent] < _con[child] )
while (child >= 0 && com(_con[parent] , _con[child]))//因为默认是lesser,所以, com(_con[parent] , _con[child])返回真就是_con[parent] < _con[child]
{
swap(_con[child], _con[parent]);
child = parent;
parent = (child - 1) / 2;
}
}
//前提是左右子树都是大根堆堆
void AdjustDown(int parent)//默认是从堆顶开始调整,调整到堆底,
{
int child = parent * 2 + 1;
while (child < _con.size())
{//没有写仿函数的时候:
//if (child + 1 < _con.size() && _con[child + 1] > _con[child])//同理,因为默认是lesser仿函数这里要放小于号魔所以,要换一下位置,
//if (child + 1 < _con.size() && _con[child] < _con[child + 1])
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;
}
}
什么时候要自己写仿函数呢?
1、类类型不支持比较大小,即类没有重载 <、> 等运算符
2、支持比较大小,但是比较的逻辑不是你想要的
例如,下面的 Date 类已经重载了 < 和 >,默认按照年月日进行比较:
cpp
class Date
{
public:
bool operator<(const Date& d) const
{
return (_year < d._year)
|| (_year == d._year && _month < d._month)
|| (_year == d._year && _month == d._month && _day < d._day);
}
bool operator>(const Date& d) const
{
return (_year > d._year)
|| (_year == d._year && _month > d._month)
|| (_year == d._year && _month == d._month && _day > d._day);
}
private:
int _year;
int _month;
int _day;
};
但是当我
cpp
bit::priority_queue<Date*> q;
默认情况下,比较的是指针地址:
cpp
p1 < p2
而不是 Date 对象本身的大小。
显然,我们真正想比较的是日期,因此需要自己编写仿函数:
cpp
class DateLess
{
public:
bool operator()(Date* p1, Date* p2)
{
return *p1 < *p2;
}
};
使用时只需要将仿函数作为第三个模板参数传入即可:
cpp
bit::priority_queue<Date*, vector<Date*>, DateLess> q;
总结与复习
三、总结与回顾
一、容器适配器
✅ stack、queue、priority_queue 都属于 容器适配器(Container Adapter)。
它们本身并不负责存储数据,而是在已有容器的基础上,限制接口,形成新的数据结构。
例如:
stack:只能尾插、尾删(LIFO)queue:只能尾插、头删(FIFO)priority_queue:底层采用堆实现,每次访问堆顶元素
二、为什么默认底层容器不同?
| 容器 | 默认底层 |
|---|---|
stack |
deque |
queue |
deque |
priority_queue |
vector |
原因:
- deque 支持高效头尾插删,非常适合
stack和queue。 - priority_queue 底层采用堆实现,而堆本身就是建立在数组上的,因此默认选择
vector。
三、deque 为什么适合作为 stack 和 queue 的底层?
相比 vector:
- 不需要频繁整体扩容
- 头插、头删效率高
相比 list:
- 支持随机访问
- 空间利用率更高
但是:
deque 并不适合频繁遍历,因此 STL 几乎只把它作为 stack 和 queue 的默认底层容器。
四、priority_queue 本质
priority_queue 本质就是堆。
常用接口:
cpp
push()
pop()
top()
empty()
五、仿函数(重点)
仿函数就是:
重载了
operator()的类,其对象可以像函数一样使用。
STL 中大量使用仿函数作为比较规则。
例如:
cpp
less<T> // 大根堆(默认)
greater<T> // 小根堆
也可以自己编写仿函数,实现自己的比较规则。
六、什么时候需要自己写仿函数?
一般有两种情况:
- 类类型没有重载比较运算符。
- 默认比较规则不能满足需求。
下期预告
模版进阶
结语
** 本文到此结束,感谢大家的阅读!如果觉得本文对你有所帮助,欢迎点赞、收藏、关注,也欢迎在评论区一起交流讨论。**
** 也欢迎订阅我的**
深入理解 C++系列:从语法入门到底层原理,系统掌握现代 C++
算法系列:从入门到精通,蓝桥杯、ACM、LeetCode 与面试算法全路线
快速复习系列:知识梳理、查漏补缺,考前冲刺必备
Java 速通系列:已学 C 语言,快速上手 Java,轻松备战期末考试
** 愿每一次敲下键盘,都比昨天更进一步!** ** 愿每一行代码落下,都让未来多一种可能!**