
🦌云深麋鹿
专栏 :C++ | 用C语言学数据结构 | Java

回顾:上一篇我们结束了容器list,接下来这篇文章让我们进入到新的容器stack和queue,体会新的设计思路吧~
放个目录
- [一 stack的使用](#一 stack的使用)
-
- [1.1 push](#1.1 push)
- [1.2 empty-top-pop](#1.2 empty-top-pop)
- [1.3 swap](#1.3 swap)
- [二 queue的使用](#二 queue的使用)
-
- [2.1 push](#2.1 push)
- [2.2 empty-pop&front-back](#2.2 empty-pop&front-back)
- [2.3 swap](#2.3 swap)
- [三 相关题目](#三 相关题目)
- [四 容器适配器](#四 容器适配器)
-
- [4.1 什么是容器适配器](#4.1 什么是容器适配器)
- [4.2 用C++写一个stack类](#4.2 用C++写一个stack类)
-
- [4.2.1 接口public](#4.2.1 接口public)
- [4.2.2 成员变量private](#4.2.2 成员变量private)
- [4.2.3 测试](#4.2.3 测试)
-
- (1)测试一
- [(2) 测试二](#(2) 测试二)
- [4.2.5 为什么容器适配器不提供迭代器?](#4.2.5 为什么容器适配器不提供迭代器?)
- [4.3 用C++写一个queue类](#4.3 用C++写一个queue类)
- [4.4 容器:deque](#4.4 容器:deque)
-
- [4.4.1 简要说明](#4.4.1 简要说明)
- [4.4.2 deque到底是怎么实现的呢?](#4.4.2 deque到底是怎么实现的呢?)
- [4.4.3 总结deque的缺点](#4.4.3 总结deque的缺点)
- [五 优先级队列](#五 优先级队列)
-
- [5.1 使用](#5.1 使用)
- [5.2 上个题目](#5.2 上个题目)
- [5.3 模拟实现](#5.3 模拟实现)
-
- [5.3.1 默认模板参数](#5.3.1 默认模板参数)
- [5.3.2 push-top-pop](#5.3.2 push-top-pop)
- [5.3.3 top](#5.3.3 top)
- [六 仿函数](#六 仿函数)
-
- [6.1 写一个仿函数](#6.1 写一个仿函数)
- [6.2 使用场景](#6.2 使用场景)
-
- [6.2.1 改一个排序算法](#6.2.1 改一个排序算法)
- [6.2.2 当priority_queue里存指针](#6.2.2 当priority_queue里存指针)
一 stack的使用

简单用俩接口,熟悉一下。
1.1 push
cpp
stack<int> s;
s.push(1);
s.push(2);
s.push(3);
s.push(4);
调试:

1.2 empty-top-pop
cpp
while (!s.empty()) {
cout << s.top() << " ";
s.pop();
}
cout << endl;
- 上一段代码我们数据入栈。
- 这一段代码:stack没空就进入while循环,把数据依次出栈。
运行:

1.3 swap
cpp
stack<int> s1;
s1.push(1);
s1.push(2);
s1.push(3);
s1.push(4);
stack<int> s2;
s2.push(5);
s2.push(6);
s2.push(7);
s2.push(8);
s1.swap(s2);
调试:

继续运行:


二 queue的使用
2.1 push
cpp
queue<int> q;
q.push(1);
q.push(2);
q.push(3);
q.push(4);
调试:

2.2 empty-pop&front-back
cpp
while(!q.empty()){
cout << "front:" << q.front() << "back:" << q.back() << endl;
q.pop();
}
push的代码搁上面。
运行:

2.3 swap
cpp
queue<int> q1;
q1.push(1);
q1.push(2);
q1.push(3);
q1.push(4);
queue<int> q2;
q2.push(5);
q2.push(6);
q2.push(7);
q2.push(8);
q1.swap(q1);
调试:

swap后:

三 相关题目
3.1 最小栈
双栈思路
一个栈是正常输入栈,一个栈存历史最小值。
cpp
class MinStack {
public:
void push(int val) {
_pushStack.push(val);
if(_minStack.empty() || val <= _minStack.top()){
_minStack.push(val);
}
}
void pop() {
if(_pushStack.top() == _minStack.top()){
_minStack.pop();
}
_pushStack.pop();
}
int top() {
return _pushStack.top();
}
int getMin() {
return _minStack.top();
}
private:
stack<int> _pushStack;
stack<int> _minStack;
};
具体如图:

3.2 栈的压入、弹出序列
直接模拟进栈出栈
cpp
bool IsPopOrder(vector<int>& pushV, vector<int>& popV) {
stack<int> st;
size_t pushi = 0;
size_t popi = 0;
while(pushi < pushV.size()){
st.push(pushV[pushi++]);
while(!st.empty() && st.top() == popV[popi]){
st.pop();
++popi;
}
}
return st.empty();
}
- 用一个栈 st 来模拟。
- pushi 和 popi 依次遍历 pushV 和 popV。
- 依次把 pushV 里的数据入栈。
- 同时检查:若栈顶元素和 popV 遍历到的元素相等,则出栈。
- 最后 st 空了,说明是正确的出栈顺序。
3.3 逆波兰式表达式
思路:借助一个栈求值。
cpp
int evalRPN(vector<string>& tokens) {
stack<int> st;
int num1 = 0;
int num2 = 0;
for(auto& e:tokens){
if(e == "+" || e == "-" || e == "*" || e == "/"){
num2 = st.top();
st.pop();
num1 = st.top();
st.pop();
switch(e[0]){
case '+':
st.push(num1+num2);
break;
case '-':
st.push(num1-num2);
break;
case '*':
st.push(num1*num2);
break;
case '/':
st.push(num1/num2);
break;
default:
break;
}
}
else{
st.push(stoi(e));
}
}
return st.top();
}
- 借助栈st。
- num1和num2为操作数。
- e遍历tokens。
- 若e遍历到操作数,则转换(stoi)后,入栈。
- 若e遍历到操作符,则出栈俩操作数,计算后(把中间值)入栈。
- 最后栈里剩下的值就是最后算出的结果。
3.4 二叉树层序遍历
3.4.1 思路1:用一个队列给每个结点标记层数
cpp
vector<vector<int>> levelOrder(TreeNode* root) {
vector<vector<int>> vv;
if(root == nullptr){
return vv;
}
queue<TreeNode*> nodeQ;
queue<int> levelQ;
nodeQ.push(root);
levelQ.push(0);
while(!nodeQ.empty()){
TreeNode* father = nodeQ.front();
nodeQ.pop();
int fatherLevel = levelQ.front();
levelQ.pop();
if(vv.size() < fatherLevel + 1){
vv.push_back(vector<int>());
}
vv[fatherLevel].push_back(father->val);
if(father->left){
nodeQ.push(father->left);
levelQ.push(fatherLevel+1);
}
if(father->right){
nodeQ.push(father->right);
levelQ.push(fatherLevel+1);
}
}
return vv;
}
- 先创建一个vector<vector> vv,后续往里push_back值,最后返回。
- 参数检查,若参数不合法,则直接返回空的vv。
- 造俩queue,一个 nodeQ 放结点,一个 levelQ 放层数。
- 先把 root 入栈,层数为0。
- 进入while循环:① 存储我们要处理的father结点及father层数,存储完了就可以出栈了。
- ② 若 vv 中没有这一层的 vector,则push_back一个;直接往这一层的 vector 里push_back事先存储好的father结点的值。
- ③ 把father结点的左右孩子(如有)入栈。
- 最后返回vv。
3.4.2 思路2:用一个变量存储该层个数
cpp
vector<vector<int>> levelOrder(TreeNode* root) {
vector<vector<int>> vv;
queue<TreeNode*> nodeQ;
if(root){
nodeQ.push(root);
}
while(!nodeQ.empty()){
int levelSize = nodeQ.size();
vv.push_back(vector<int>());
size_t vvi = vv.size() - 1;
while(levelSize--){
TreeNode* father = nodeQ.front();
vv[vvi].push_back(father->val);
nodeQ.pop();
if(father->left){
nodeQ.push(father->left);
}
if(father->right){
nodeQ.push(father->right);
}
}
}
return vv;
}
- vector<vector> vv为返回值,nodeQ 为放结点的队列。
- 先处理root,若不为空则进栈。
- 进入while循环,一次循环处理一层:1) levelSize 存储当前层数个数(即为当前 nodeQ 元素个数)。
- 2) 往vv中push_back一个vector,为当前层遍历结果存储位置。
- 3) vvi 存储当前层数。
- 4) 进入第二个while循环,一次循环处理一个 father结点,整个循环过程结束就处理完了这一层结点:
- ① 一次出一个nodeQ队头元素,即为father结点。
- ② 把当前 father 结点的值放入返回对象vector里,在队列nodeQ那边就可出栈了。
- ③ father结点 如有 左右孩子,则依次入栈。
- ④ 若这一层结点都处理完毕(即 levelSize == 0),则循环结束。
- 5) 直到队列 nodeQ 为空,最后一层处理完毕,循环体结束。
后置--就是有n次循环n次,前置--就是有n次循环n-1次
四 容器适配器
4.1 什么是容器适配器
- 适配器是一种设计模式,容器适配器是适配器的一种。
- 容器适配器把容器转换成为我们想要的容器。
4.2 用C++写一个stack类
cpp
template<typename T,class Container = std::list<T>>
这里以 list 为默认模板参数。
4.2.1 接口public
cpp
bool empty() const {
return _con.empty();
}
size_t size() const{
return _con.size();
}
const T& top() const {
return _con.back();
}
void push(const T& val) {
_con.push_back(val);
}
void pop() {
_con.pop_back();
}
void swap(Container& con2) {
_con.swap(con2);
}
4.2.2 成员变量private
cpp
Container _con;
4.2.3 测试
(1)测试一
cpp
myStack<int, std::list<int>> st;
st.push(1);
st.push(2);
st.push(3);
while (!st.empty()) {
cout << st.top() << " ";
st.pop();
}
cout << endl;
运行结果:

(2) 测试二
cpp
myStack<int> s1;
s1.push(1);
s1.push(2);
s1.push(3);
s1.push(4);
myStack<int> s2;
s2.push(5);
s2.push(6);
s2.push(7);
s2.push(8);
s1.swap(s2);
调试:

swap后:

4.2.5 为什么容器适配器不提供迭代器?
- 容器适配器的目的是限制底层容器的功能,以强制实施特定的访问规则:如果提供迭代器,用户就可以随机访问。
- 提供迭代器破坏了容器适配器对底层容器的封装。
- 避免非法操作。
4.3 用C++写一个queue类
cpp
template<typename T, class Container = std::list<T>>
依旧以 list 为默认模板参数。
4.3.1 接口public
cpp
bool empty() const {
return _con.empty();
}
size_t size() const {
return _con.size();
}
const T& front() const {
return _con.front();
}
const T& back() const {
return _con.back();
}
void push(const T& val) {
_con.push_back(val);
}
void pop() {
_con.pop_front();
}
void swap(myQueue& myQ2) {
_con.swap(myQ2._con);
}
4.3.2 成员变量private
cpp
Container _con;
4.3.3 测试
(1)测试一
cpp
myQueue<int> mQ;
mQ.push(1);
mQ.push(2);
mQ.push(3);
while(!mQ.empty()){
cout << "front:" << mQ.front() << "; back:" << mQ.back() << endl;
mQ.pop();
}
运行:

(2)测试二
cpp
myQueue<int> q1;
q1.push(1);
q1.push(2);
q1.push(3);
q1.push(4);
myQueue<int> q2;
q2.push(5);
q2.push(6);
q2.push(7);
q2.push(8);
q1.swap(q2);
调试:

swap后:

库里面不支持vector底层实现queue
4.4 容器:deque


4.4.1 简要说明
双端队列。
- 既有list的优点:插入删除数据效率高,没有扩容开销。
- 也有vector的优点:尾插尾删效率高,随机访问效率高,CPU高速缓存命中率高。
4.4.2 deque到底是怎么实现的呢?
结构
中控指针数组+多个buffer。
有个手搓简图:

中控指针数组从中间开始放,这样有头插的余地,也有尾插的余地。
4.4.3 总结deque的缺点
- 中间位置insert和erase效率很低。
- 没有vector和list那么极致的优点。
- 所以适合做stack&queue的默认适配器。(呼应4.4开头两张截图)
五 优先级队列

也是容器适配器。
5.1 使用
cpp
priority_queue<int> pq;
pq.push(1);
pq.push(2);
pq.push(3);
pq.push(4);
while (!pq.empty()) {
cout << "top:" << pq.top() << endl;
pq.pop();
}
运行:

- 可以观察到,pop和top取优先级高的(底层默认是大堆)。
改成小堆
cpp
priority_queue<int, vector<int>,greater<int>> pq;
运行:

- 可以观察到,pop和top取优先级低的。
5.2 上个题目
215. 数组中的第K个最大元素 - 力扣(LeetCode)
用优先级队列就简单很多啦。
cpp
int findKthLargest(vector<int>& nums, int k) {
priority_queue<int> pq(nums.begin(),nums.end());
while(--k){
pq.pop();
}
int ret = pq.top();
return ret;
}
5.3 模拟实现
5.3.1 默认模板参数
默认使用vector,相对而言效率更高。
cpp
template<typename T,class Container = std::vector<T>,class Compare>
5.3.2 push-top-pop
cpp
void push(const T& val) {
_con.push_back(val);
AdjustUp(_con.size() - 1);
}
push完了要向上调整。
写一个AdjustUp,先复习一下:
cpp
father = (child - 1) / 2;
left = father * 2 + 1;
left = father * 2 + 2;
代码:
cpp
void AdjustUp(size_t child) {
while (child > 0) {
size_t parent = (child - 1) / 2;
if (_con[parent] < _con[child]) {
std::swap(_con[parent], _con[child]);
child = parent;
}
else {
break;
}
}
}
测试代码照旧,运行:

5.3.3 top
- 为什么stack的top有两个版本?但是priority_queue只有const版本。
priority_queue的特性需要保证优先级最高(或最低)的排在最前面,所以不提供可以更改top的接口。
六 仿函数
6.1 写一个仿函数
cpp
template<class T>
class myGreater {
public:
bool operator()(const T& a, const T& b) const {
return a > b;
}
};
特点
- 仿函数是一个类。
- 仿函数也可以是一个模板。
- 这个类的对象可以像函数一样使用,就可以叫仿函数。
- 本质上是重载( )。
6.2 使用场景
6.2.1 改一个排序算法
(1)改代码
从C语言阶段找过来一段代码:
cpp
void insertSort(int* arr,int size){
int end = 0;
int tmp = 0;
for (int i = 0;i < size - 1;++i) {
end = i;
tmp = end + 1;
while (end >= 0) {
if (arr[end] > arr[tmp]){
sort_Swap(&arr[end], &arr[tmp]);
--end;
--tmp;
}else{
break;
}
}
}
}
改巴改巴,加上Compare模板参数:
cpp
template<class Compare = myGreater<int>>
void insertSort(std::vector<int>& v)
{
Compare com;
size_t size = v.size();
for (size_t i = 0;i < size - 1;++i)
{
int end = i;
int tmp = end + 1;
while (end >= 0)
{
if (com(v[end], v[tmp]))
{
std::swap(v[end], v[tmp]);
--end;
--tmp;
}
else
{
break;
}
}
}
}
(2)测试代码(默认升序)
cpp
std::vector<int> v = { 2,3,1,6,61,35,1 };
insertSort(v);
for (auto i : v)
{
std::cout << i << " ";
}
cout << endl;
运行:

(3)测试代码(降序)
cpp
insertSort<myLess<int>>(v);
运行:

6.2.2 当priority_queue里存指针
cpp
myPriority_queue<int*> pq;
int a = 3;
int b = 2;
pq.push(&a);
pq.push(&b);
while (!pq.empty()) {
cout << "top:" << pq.top() << endl;
pq.pop();
}
运行,默认是大堆:

但是小的在前面,这里比较的是地址大小。
我们需要比较地址上存的值,所以再写一个仿函数:
cpp
template<class T>
class myLess<T*> {
public:
bool operator()(const T* const a, const T* const b) const {
return *a < *b;
}
};
再次运行:

容器stack&queue 的学习就到这里,下一篇 模板 不久后就会更出来啦~

