C++ STL 详解:stack 和 queue 的介绍使用与模拟实现
文章目录
- [C++ STL 详解:stack 和 queue 的介绍使用与模拟实现](#C++ STL 详解:stack 和 queue 的介绍使用与模拟实现)
-
- [stack 的介绍与使用](#stack 的介绍与使用)
-
- [1. stack 是什么](#1. stack 是什么)
- [2. stack 的定义方式](#2. stack 的定义方式)
-
- [2.1 使用默认底层容器定义 stack](#2.1 使用默认底层容器定义 stack)
- [2.2 指定底层容器定义 stack](#2.2 指定底层容器定义 stack)
- [3. stack 的常用接口](#3. stack 的常用接口)
- [4. stack 使用示例](#4. stack 使用示例)
- [queue 的介绍与使用](#queue 的介绍与使用)
-
- [1. queue 是什么](#1. queue 是什么)
- [2. queue 的定义方式](#2. queue 的定义方式)
-
- [2.1 使用默认底层容器定义 queue](#2.1 使用默认底层容器定义 queue)
- [2.2 指定底层容器定义 queue](#2.2 指定底层容器定义 queue)
- [3. queue 的常用接口](#3. queue 的常用接口)
- [4. queue 使用示例](#4. queue 使用示例)
- 容器适配器
-
- [1. 什么是容器适配器](#1. 什么是容器适配器)
- [2. stack 和 queue 为什么叫容器适配器](#2. stack 和 queue 为什么叫容器适配器)
- [3. 默认底层容器 deque](#3. 默认底层容器 deque)
- [stack 的模拟实现](#stack 的模拟实现)
-
- [1. stack 接口和底层容器的对应关系](#1. stack 接口和底层容器的对应关系)
- [2. stack 模拟实现代码](#2. stack 模拟实现代码)
- [3. stack 实现细节说明](#3. stack 实现细节说明)
- [queue 的模拟实现](#queue 的模拟实现)
-
- [1. queue 接口和底层容器的对应关系](#1. queue 接口和底层容器的对应关系)
- [2. queue 模拟实现代码](#2. queue 模拟实现代码)
- [3. queue 实现细节说明](#3. queue 实现细节说明)
- 总结
stack 的介绍与使用
1. stack 是什么
stack 是一种容器适配器,主要用在"后进先出"的场景里
所谓后进先出,也就是 Last In First Out,简称 LIFO
可以把 stack 想成一个只有一端能操作的桶,元素只能从这一端放进去,也只能从这一端拿出来
比如按顺序压入 1、2、3、4,那么取出来的时候顺序就是 4、3、2、1
stack 的特点可以这样记:
- 元素只能从栈顶进入
- 元素也只能从栈顶出去
- 最后放进去的元素最先出来
- 不支持遍历
- 不支持随机访问
- 只暴露栈这种数据结构需要的接口
stack 常见使用场景:
- 函数调用栈
- 表达式求值
- 括号匹配
- 递归转非递归
- 深度优先搜索 DFS
- 浏览器后退记录这类"最近操作优先处理"的场景
使用 stack 需要包含头文件:
cpp
#include <stack>
2. stack 的定义方式
stack 是容器适配器,它自己不直接管理底层存储,而是包装其他容器来使用
2.1 使用默认底层容器定义 stack
cpp
stack<int> st1;
这种写法最常见
如果没有指定底层容器,标准库里的 stack 默认使用 deque 作为底层容器
也就是说:
cpp
stack<int> st1;
大致可以理解成:
cpp
stack<int, deque<int>> st1;
2.2 指定底层容器定义 stack
stack 的第二个模板参数可以指定底层容器
比如使用 vector:
cpp
stack<int, vector<int>> st2;
也可以使用 list:
cpp
stack<int, list<int>> st3;
完整示例:
cpp
#include <iostream>
#include <stack>
#include <vector>
#include <list>
using namespace std;
int main()
{
stack<int> st1; // 默认使用 deque 作为底层容器
stack<int, vector<int>> st2; // 指定 vector 作为底层容器
stack<int, list<int>> st3; // 指定 list 作为底层容器
st1.push(1);
st2.push(2);
st3.push(3);
cout << st1.top() << endl;
cout << st2.top() << endl;
cout << st3.top() << endl;
return 0;
}
注意一点:stack 要求底层容器至少能支持 back、push_back、pop_back、size、empty 这些操作
所以 vector、deque、list 都能作为 stack 的底层容器
3. stack 的常用接口
stack 常用成员函数如下:
| 成员函数 | 作用 |
|---|---|
| empty | 判断栈是否为空 |
| size | 获取栈中有效元素个数 |
| top | 获取栈顶元素 |
| push | 元素入栈 |
| pop | 元素出栈 |
| swap | 交换两个栈中的数据 |
这些接口可以按栈顶来理解:
- push 往栈顶放数据
- pop 从栈顶删数据
- top 查看栈顶数据
- empty 判断有没有数据
- size 看当前有多少个数据
- swap 交换两个 stack 内部维护的数据
注意:pop 只负责删除栈顶元素,不会返回被删除的值
如果想拿到被删除的数据,要先 top,再 pop
cpp
int x = st.top();
st.pop();
4. stack 使用示例
cpp
#include <iostream>
#include <vector>
#include <stack>
using namespace std;
int main()
{
// 指定 vector 作为 stack 的底层容器
stack<int, vector<int>> st;
// 入栈顺序:1 2 3 4
st.push(1);
st.push(2);
st.push(3);
st.push(4);
cout << st.size() << endl; // 4
// stack 是后进先出,所以输出顺序是 4 3 2 1
while (!st.empty())
{
cout << st.top() << " "; // 先看栈顶元素
st.pop(); // 再删除栈顶元素
}
cout << endl;
return 0;
}
输出结果:
text
4
4 3 2 1
这里入栈顺序是 1、2、3、4,最后进入的是 4,所以 4 最先出来
queue 的介绍与使用
1. queue 是什么
queue 也是一种容器适配器,主要用在"先进先出"的场景里
所谓先进先出,也就是 First In First Out,简称 FIFO
可以把 queue 想成排队买票,先排队的人先买票离开,后排队的人后处理
queue 的特点可以这样记:
- 元素从队尾进入
- 元素从队头出去
- 最先进入队列的元素最先出来
- 不支持随机访问
- 不支持遍历
- 只暴露队列需要的接口
queue 常见使用场景:
- 广度优先搜索 BFS
- 任务调度
- 消息队列
- 缓冲区
- 排队模型
- 按到达顺序处理请求的场景
使用 queue 需要包含头文件:
cpp
#include <queue>
2. queue 的定义方式
2.1 使用默认底层容器定义 queue
cpp
queue<int> q1;
如果没有指定底层容器,标准库里的 queue 默认也使用 deque
2.2 指定底层容器定义 queue
queue 也可以指定底层容器
cpp
queue<int, list<int>> q2;
也可以看到这种写法:
cpp
queue<int, deque<int>> q3;
需要注意的是,queue 的底层容器要支持 front、back、push_back、pop_front、size、empty 等操作
所以 deque 和 list 很适合作为 queue 的底层容器
vector 一般不适合作为 queue 的底层容器,因为 queue 出队要从队头删除,需要底层容器支持 pop_front,而 vector 没有 pop_front
所以像下面这种写法在标准库里通常是不能正常满足 queue 适配要求的:
cpp
// 不推荐,也通常不符合 queue 对底层容器的要求
queue<int, vector<int>> q;
如果只是学习模板参数可以这么看,但真正写代码时,queue 常用 deque 或 list
完整示例:
cpp
#include <iostream>
#include <queue>
#include <list>
using namespace std;
int main()
{
queue<int> q1; // 默认使用 deque
queue<int, list<int>> q2; // 指定 list 作为底层容器
q1.push(10);
q2.push(20);
cout << q1.front() << endl;
cout << q2.front() << endl;
return 0;
}
3. queue 的常用接口
queue 常用成员函数如下:
| 成员函数 | 作用 |
|---|---|
| empty | 判断队列是否为空 |
| size | 获取队列中有效元素个数 |
| front | 获取队头元素 |
| back | 获取队尾元素 |
| push | 队尾入队列 |
| pop | 队头出队列 |
| swap | 交换两个队列中的数据 |
这些接口可以按队头和队尾来理解:
- push 往队尾插入数据
- pop 从队头删除数据
- front 查看队头数据
- back 查看队尾数据
- empty 判断队列是否为空
- size 查看队列中有多少个元素
- swap 交换两个队列中的数据
注意:queue 的 pop 也只删除元素,不返回元素
如果想拿到即将出队的数据,要先 front,再 pop
cpp
int x = q.front();
q.pop();
4. queue 使用示例
cpp
#include <iostream>
#include <list>
#include <queue>
using namespace std;
int main()
{
// 指定 list 作为 queue 的底层容器
queue<int, list<int>> q;
// 入队顺序:1 2 3 4
q.push(1);
q.push(2);
q.push(3);
q.push(4);
cout << q.size() << endl; // 4
// queue 是先进先出,所以输出顺序是 1 2 3 4
while (!q.empty())
{
cout << q.front() << " "; // 查看队头元素
q.pop(); // 删除队头元素
}
cout << endl;
return 0;
}
输出结果:
text
4
1 2 3 4
这里入队顺序是 1、2、3、4,最先进队的是 1,所以 1 最先出队
容器适配器
1. 什么是容器适配器
stack 和 queue 都能存放元素,但 STL 并没有把它们当成普通容器,而是把它们叫作容器适配器
适配器这个名字可以这么理解:它本身不重新造一套完整的数据结构,而是在已有容器的基础上包装一层接口,让这个容器表现出某种特定的数据结构行为
比如 stack 想要的是后进先出,所以它只暴露 push、pop、top 等接口
底层容器明明可能有迭代器、下标、insert 等能力,但 stack 不把这些接口暴露出来
queue 也是一样,它只暴露队列需要的 push、pop、front、back 等接口
2. stack 和 queue 为什么叫容器适配器
学过数据结构后会知道,栈和队列既可以用顺序表实现,也可以用链表实现
在 STL 中,stack 和 queue 就是把某个现成容器包装起来,然后提供栈或队列的操作方式
比如定义:
cpp
stack<int, vector<int>> st;
可以理解成:这个 stack 内部用 vector 存数据,但对外只给你栈的接口
再比如:
cpp
queue<int, list<int>> q;
可以理解成:这个 queue 内部用 list 存数据,但对外只给你队列的接口
所以 stack 和 queue 的类模板一般有两个模板参数:
- 第一个模板参数是元素类型
- 第二个模板参数是底层容器类型
也就是类似这样的形式:
cpp
template<class T, class Container = deque<T>>
class stack;
template<class T, class Container = deque<T>>
class queue;
当不指定第二个模板参数时,它们默认使用 deque
3. 默认底层容器 deque
stack 和 queue 默认使用 deque,是因为 deque 同时支持头部和尾部的高效操作
对于 stack 来说,它主要需要:
- push_back
- pop_back
- back
- size
- empty
deque 支持这些接口
对于 queue 来说,它主要需要:
- push_back
- pop_front
- front
- back
- size
- empty
deque 也支持这些接口
所以 deque 很适合作为二者的默认底层容器
stack 的模拟实现
1. stack 接口和底层容器的对应关系
stack 的模拟实现比较直接,因为它只是对底层容器接口做了一层包装
可以先把 stack 的接口和底层容器接口对应起来:
| stack 接口 | 作用 | 底层容器调用 |
|---|---|---|
| push | 元素入栈 | push_back |
| pop | 元素出栈 | pop_back |
| top | 获取栈顶元素 | back |
| size | 获取栈中有效元素个数 | size |
| empty | 判断栈是否为空 | empty |
| swap | 交换两个栈中的数据 | swap |
这里选择模板参数 Container 作为底层容器类型
如果用户不指定 Container,就默认用 std::deque
2. stack 模拟实现代码
cpp
#pragma once
#include <deque>
#include <cstddef>
namespace cl
{
// stack 是容器适配器,底层默认使用 deque
template<class T, class Container = std::deque<T>>
class stack
{
public:
// 元素入栈,栈顶在底层容器尾部
void push(const T& x)
{
_con.push_back(x);
}
// 元素出栈,删除底层容器尾部元素
void pop()
{
_con.pop_back();
}
// 获取栈顶元素,普通对象可读可写
T& top()
{
return _con.back();
}
// 获取栈顶元素,const 对象只能读
const T& top() const
{
return _con.back();
}
// 获取栈中有效元素个数
size_t size() const
{
return _con.size();
}
// 判断栈是否为空
bool empty() const
{
return _con.empty();
}
// 交换两个 stack 的底层容器
void swap(stack<T, Container>& st)
{
_con.swap(st._con);
}
private:
Container _con; // 真正存储数据的底层容器
};
}
3. stack 实现细节说明
这个 stack 的实现重点在于:栈顶放在底层容器的尾部
这样做的原因是,大多数顺序容器和链表容器都支持尾插和尾删
push 的时候调用 push_back,相当于把元素压到栈顶
pop 的时候调用 pop_back,相当于把栈顶元素删除
top 的时候调用 back,拿到的就是最后一个元素,也就是栈顶元素
这个实现不提供迭代器,也不提供下标访问,因为 stack 只关心栈顶
如果底层容器是 vector:
cpp
cl::stack<int, std::vector<int>> st;
那么 push、pop、top 最终都会转成 vector 的 push_back、pop_back、back
如果底层容器是 list:
cpp
cl::stack<int, std::list<int>> st;
那么这些操作最终会转成 list 的 push_back、pop_back、back
queue 的模拟实现
1. queue 接口和底层容器的对应关系
queue 的模拟实现也很直接,本质上也是包装底层容器的接口
queue 的接口和底层容器接口对应关系如下:
| queue 接口 | 作用 | 底层容器调用 |
|---|---|---|
| push | 队尾入队列 | push_back |
| pop | 队头出队列 | pop_front |
| front | 获取队头元素 | front |
| back | 获取队尾元素 | back |
| size | 获取队列中有效元素个数 | size |
| empty | 判断队列是否为空 | empty |
| swap | 交换两个队列中的数据 | swap |
queue 和 stack 最大的区别在于:
- stack 只在一端进出
- queue 从队尾进,从队头出
所以 queue 的底层容器必须支持 pop_front
2. queue 模拟实现代码
cpp
#pragma once
#include <deque>
#include <cstddef>
namespace cl
{
// queue 是容器适配器,底层默认使用 deque
template<class T, class Container = std::deque<T>>
class queue
{
public:
// 队尾入队列
void push(const T& x)
{
_con.push_back(x);
}
// 队头出队列
void pop()
{
_con.pop_front();
}
// 获取队头元素,普通对象可读可写
T& front()
{
return _con.front();
}
// 获取队头元素,const 对象只能读
const T& front() const
{
return _con.front();
}
// 获取队尾元素,普通对象可读可写
T& back()
{
return _con.back();
}
// 获取队尾元素,const 对象只能读
const T& back() const
{
return _con.back();
}
// 获取队列中有效元素个数
size_t size() const
{
return _con.size();
}
// 判断队列是否为空
bool empty() const
{
return _con.empty();
}
// 交换两个 queue 的底层容器
void swap(queue<T, Container>& q)
{
_con.swap(q._con);
}
private:
Container _con; // 真正存储数据的底层容器
};
}
3. queue 实现细节说明
queue 的实现重点在于队头和队尾分工
push 调用 push_back,让新元素进入底层容器尾部
pop 调用 pop_front,让队头元素离开队列
front 调用 front,访问队头元素
back 调用 back,访问队尾元素
因为 queue 需要 pop_front,所以底层容器不能随便选
deque 支持 pop_front,所以适合
list 也支持 pop_front,所以也适合
vector 不支持 pop_front,所以不适合作为标准 queue 的底层容器
总结
stack 和 queue 都不是普通意义上的容器,而是容器适配器
它们的核心思想是:把已有容器包起来,只暴露符合栈或队列语义的接口
stack 重点记这些:
- 后进先出
- 默认底层容器是 deque
- 可以指定 vector、list、deque 等合适容器作为底层容器
- push 对应底层容器的 push_back
- pop 对应底层容器的 pop_back
- top 对应底层容器的 back
- pop 不返回元素,想获取元素要先 top 再 pop
queue 重点记这些:
- 先进先出
- 默认底层容器是 deque
- 常用底层容器是 deque 或 list
- push 对应底层容器的 push_back
- pop 对应底层容器的 pop_front
- front 对应队头元素
- back 对应队尾元素
- pop 不返回元素,想获取元素要先 front 再 pop
模拟实现时,stack 和 queue 本身没有太复杂的底层逻辑
真正干活的是内部的底层容器,stack 和 queue 只是把接口重新包装了一下
所以理解它们的关键,不是背接口,而是明白:
- stack 限制了"只能从一端进出"
- queue 限制了"从一端进,从另一端出"
- 容器适配器的价值就在于隐藏底层容器多余的能力,只留下符合数据结构语义的操作