【Cpp】第九章-STL_stack类和queue类

stack类和queue类

  stackqueue以及priority_queue(优先级队列)是STL中三大容器适配器,将其称为容器适配器是因为其在底层只是对现有容器进行的了封装而并没有重新实现。因此在容器适配器中都有让传入容器的模板参数。

容器适配器

  适配器是一种设计模式,在GOF的《设计模式:可复用面向对象软件的基础》中是这样说的:将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。而容器适配器就是将常见容器的接口进行封装,使之成为我们需要的结构。因此在容器适配的模板参数中我们可以传入我们想要使用的容器作为模板参数进行封装,但是作为适配器的实现容器它们也需要满足一定的要求才可以作为模板参数。

1
2
3
4
5
//Container就是底层实现的容器的模板参数
template <class T, class Container = deque<T> > class queue;
template <class T, class Container = deque<T> > class stack;
template <class T, class Container = vector<T>,
class Compare = less<typename Container::value_type> > class priority_queue;

stack类

  stack和我们数据结构中的栈实现了同样的功能,后进先出是其最大的特点。它只能从容器的一端进行元素的插入和提取,来满足栈的相关功能。
  它底层的容器可以是标准中的容器类也可以是其他的容器类,但是无论如何这些容器类必须满足以下要求。
  1、empty(),判空操作。
  2、back(),获取尾部元素。
  3、push_back(),尾插。
  4、pop_back(),尾删。
  满足这些要求的容器类才可以被传入,如果没有传入容器的话则默认使用deque

常用接口

1
2
3
4
5
6
7
8
9
10
stack(const container_type &ctnr = container_type()); //构造空的栈
bool empty() const; //检测stack是否为空
size_type size(); //const 返回stack中元素的个数
value_type &top(); //返回栈顶元素的引用
const value_type &top() const; //返回栈顶元素的const引用
void push(const value_type &val); //将元素val压入stack中
void pop(); //将stack中尾部的元素弹出
template <class... Args>
void emplace(Args &&... args); //(C++11) 在stack的栈顶构造元素
void swap(stack &x); //(C++11) 交换两个栈中的元素

栈的应用

最小栈

  力扣:
https://leetcode-cn.com/problems/min-stack/submissions/
  这道题就是用栈区实现一个可以返回当前栈中最小值的栈,方法有很多种,这里只提供一种方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class MinStack {
public:
/** initialize your data structure here. */
MinStack()
:_min(INT_MAX)
{}

void push(int x) {
if(x < _min)
{
_min = x;
}
_elem.push_back(x);
}

void pop() {
if(top() == _min)
{
_min = INT_MAX;
for(int i = 0; i < _elem.size() - 1; i++)
{
if(_elem[i] < _min)
{
_min = _elem[i];
}
}
}
_elem.pop_back();
}

int top() {
return _elem[_elem.size() - 1];
}

int getMin() {
return _min;
}
private:
std::vector<int> _elem;
int _min;
};

/**
* Your MinStack object will be instantiated and called as such:
* MinStack* obj = new MinStack();
* obj->push(x);
* obj->pop();
* int param_3 = obj->top();
* int param_4 = obj->getMin();
*/

栈的压入、弹出序列

  牛客网:
https://www.nowcoder.com/practice/d77d11405cc7470d82554cb392585106?tpId=13&&tqId=11174&rp=1&ru=/activity/oj&qru=/ta/coding-interviews/question-ranking
  这道题用到了栈的弹出序列是否匹配压入序列的算法,大致思路是用一个栈进行模拟,并且设立用于分别遍历弹出和压入序列的下标,只要栈顶元素不等于当前遍历到的弹出序列的元素,就将压入序列当前元素压入栈,并且遍历压入序列的下一个元素,如果相等,则将栈顶元素弹出,并且遍历弹出序列的下一个元素。如果当弹出序列还没有遍历完毕而压入序列也始终找不到下一个可以和弹出序列进行匹配的元素则不匹配,返回false,如果弹出序列遍历完毕,则表示匹配,返回true。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Solution {
public:
bool IsPopOrder(vector<int> pushV,vector<int> popV) {
int pushIndex = 0;
int popIndex = 0;
//用栈进行模拟
stack<int> s;
//遍历弹出序列
while(popIndex < popV.size())
{
//栈为空或者栈顶元素不等于当天弹出序列遍历到的元素
while(s.empty() || s.top() != popV[popIndex])
{
//压入序列还有元素则压入
if(pushIndex < pushV.size())
{
s.push(pushV[pushIndex]);
pushIndex++;
}
//压入序列全部压入无法完成匹配
else
{
return false;
}
}
//栈顶元素和弹出序列当前元素相同则往后继续遍历
s.pop();
popIndex++;
}
return true;
}
};

逆波兰表达式求值

  力扣:
https://leetcode-cn.com/problems/evaluate-reverse-polish-notation/submissions/
  逆波兰表达式也成为后缀表达式,这里的逆波兰表达式求值由于不涉及括号所以比较简单,我们只需要将遇到的操作数放入栈,遇到操作符则取出栈顶两个元素进行计算,然后将结果再次放入栈最后取出栈中最后一个元素就是答案。不过这里要注意负数的处理,以及从栈中取出的两个操作数哪个是前操作数哪个是后操作数的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<int> s;
for(int i = 0; i < tokens.size(); i++)
{
if(tokens[i][0] <= '9' && tokens[i][0] >= '0')
{
s.push(atoi(tokens[i].c_str()));
}
//要处理负数
else if(tokens[i][0] == '-' && tokens[i][1] != '\0')
{
string str = tokens[i].substr(1);
int temp = -1 * atoi(str.c_str());
s.push(temp);
}
else
{
int num1 = s.top();
s.pop();
int num2 = s.top();
s.pop();
switch(tokens[i][0])
{
case '+':
s.push(num2 + num1);
break;
case '-':
s.push(num2 - num1);
break;
case '*':
s.push(num2 * num1);
break;
case '/':
s.push(num2 / num1);
break;
}
}
}
return s.top();
}
};

queue类

  和stack相反,queue有着先进先出的特性,这使得他能够按照顺序完成某一功能。它从容器的一端插入另一端删除,因此它的模板参数中的容器要求有以下功能:
  1、empty(),判空
  2、size(),长度
  3、back(),获取尾部元素
  4、front(),获取头部元素
  5、push_back(),尾插
  6、pop_front(),头删
  如果没有传入容器的话则默认使用deque

常用接口

1
2
3
4
5
6
7
8
9
10
11
12
queue(const container_type &ctnr = container_type()); //构造空的队列
bool empty() const; //检测队列是否为空,是返回true,否则返回false
size_type size() const; //返回队列中有效元素的个数
value_type &front(); //返回队头元素的引用
const value_type &front() const; //返回队头元素的const引用
value_type &back(); //返回队尾元素的引用
const value_type &back() const; //返回队尾元素的cosnt引用
void push(value_type & val); //在队尾将元素val入队列
void pop(); //将队头元素出队列
template <class... Args>
void emplace(Args && ... args)(C++ 11); //在队尾构造元素
void swap(queue & x); //交换两个队列中的元素

队列的应用

用队列实现栈

  力扣:
https://leetcode-cn.com/problems/implement-stack-using-queues/
  利用栈进行层序遍历类似于bfs,即广度优先搜索,我们在进行遍历时需要将每一个结点的儿子都压入栈,只要栈不为空就一直遍历下去。但是这个题要求要区分每一层,则我们需要在遍历每一层前都拿到当前队列中结点的个数,则是这一层要遍历的结点数,我们这一层就只遍历这些结点即可。我们甚至由此可以得出N叉树的层序遍历,都可以用类似的思路。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
vector<vector<int>> res;
queue<TreeNode*> q;
//根结点为空直接返回
if(root == nullptr)
{
return res;
}
q.push(root);
while(!q.empty())
{
vector<int> line;
int size = q.size();
for(int i = 0; i < size; i++)
{
TreeNode* node = q.front();
if(node->left != nullptr)
{
q.push(node->left);
}
if(node->right != nullptr)
{
q.push(node->right);
}
q.pop();
line.push_back(node->val);
}
res.push_back(line);
}
return res;
}
};

priority_queue类

  priority_queue称为优先级队列,它的出队元素永远是队列中优先级最高的那一个,它的底层是用一个堆来完成的,堆顶就是优先级最高的元素。
  优先级队列在每次插入删除元素后都会分别调用算法push_heap()pop_heap()重新建堆,并且还会和容器中的push_back()pop_back()结合。同时优先级队列中有一个Compare的模板参数,用于传入一个仿函数,建堆会用这个仿函数按照严格弱序进行建堆。
  所谓仿函数则是一个类中重载了operator()使其实例化的对象可以像函数一样进行调用,它的特点是可以使类作为模板参数一部分传入模板内,并且使其实例化出的对象可以当作函数使用。标准中给了两个类提供了仿函数的功能,lessgreater
  如果在优先级队列中存放自定义类型的数据,要求自定义类型必须重载operator<operator>取决于Compare的类型,这两个重载在仿函数中将会进行调用用于判断和比较。
  同样的优先级队列中的容器也必须满足一些要求才能完成功能:
  1、empty(),判空
  2、size(),长度
  3、front(),获取头部元素
  4、push_back(),尾插
  5、pop_back(),尾删
  容器还需要支持随机访问迭代器,以便始终在内部保持堆结构。
  为什么是这些接口?front()用于在建堆和堆调整后可以获得堆顶元素,即优先级最高的元素,即top()push()在给堆插入元素时是从容器的尾部插入,因此需要push_back()进行尾插,尾插后再用push_heap()调整堆;pop()在出队时要先调用pop_heap(),这个接口会将堆顶元素与堆的最后一个元素交换位置,并且对堆的除最后一个元素进行调整,这个过程类似于堆排序。之后再调用容器的pop_back()即可将原本堆顶优先级最高的元素出队。

常用接口

1
2
3
4
5
6
7
8
9
10
priority_queue(const Compare &x = Compare(),
const Container &y = Container()); //构造一个空的优先级队列
template <class InputIterator>
priority_queue(InputIterator first, InputIterator last,
const Compare &comp = Compare(),
const Container &ctnr = Container()); //用[first, last)区间中的元素构造优先级队列
bool empty() const; //检测优先级队列是否为空,是返回true,否则返回false
const value_type &top() const; //返回优先级队列中最大(最小元素),即堆顶元素
void push(const T &x); //在优先级队列中插入元素x
void pop(); //删除优先级队列中最大(最小)元素,即堆顶元素

  默认情况下,类模板中Compare的类型为less,实例化出来的对象重载时判断的为是否小于,因此会创建大堆,队列出队返回的值也是队中优先级最高的。

优先级队列的应用

  优先级队列底层是堆的实现,因此优先级队列擅长于结局堆能够解决的问题,例如TopK问题。

数组中的第K个最大的元素

  力扣:
https://leetcode-cn.com/problems/kth-largest-element-in-an-array/
  这道题就利用优先级队列找到第k个大的元素即可,也可以先排序后再找。

1
2
3
4
5
6
7
8
9
10
11
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
priority_queue<int> que(nums.begin(), nums.end());
for(int i = 0; i < k - 1; i++)
{
que.pop();
}
return que.top();
}
};

前K个高频元素

力扣:
https://leetcode-cn.com/problems/top-k-frequent-elements/
  这道题做起来不难,但是是对优先级队列较为综合的运用,我们要考虑到TOPK问题中找前K大要用小根堆,还要考虑到要将出现次数与元素本身联系起来要在优先级队列中存储pair

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Solution {
public:
vector<int> topKFrequent(vector<int>& nums, int k) {
map<int, int> m;
for(int i = 0; i < nums.size(); i++)
{
m[nums[i]]++;
}
//topK问题求前K大一定要用小根堆,维护K个数据
//用大根堆要放入全部数据费时费空间
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> que;
for(auto e : m)
{
if(que.size() >= k)
{
if(e.second > que.top().first)
{
que.pop();
que.push(make_pair(e.second, e.first));
}
}
else
{
que.push(make_pair(e.second, e.first));
}
}
vector<int> res(k);
while(!que.empty())
{
res[que.size() - 1] = que.top().second;
que.pop();
}
return res;
}
};

实现

  依旧是实现常用接口,了解底层原理。

stack类的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#pragma once
#include <iostream>
#include <deque>
template<class T, class Container = std::deque<T>>
class Stack
{
public:
void Push(const T& data)
{
_con.push_back(data);
}
void Pop()
{
_con.pop_back();
}
bool Empty()
{
return _con.empty();
}
size_t Size()
{
return _con.size();
}
T& Top()
{
return _con.back();
}
const T& Top() const
{
return _con.back();
}
private:
Container _con;
};

queue的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#pragma once
#include <iostream>
#include <deque>
template<class T, class Container = std::deque<T>>
class Queue
{
public:
void Push(const T& data)
{
_con.push_back(data);
}
void Pop()
{
_con.pop_front();
}
size_t Size()
{
return _con.size();
}
bool Empty()
{
return _con.empty();
}
T& Back()
{
return _con.back();
}
const T& Back() const
{
return _con.back();
}
T& Front()
{
return _con.front();
}
const T& Front() const
{
return _con.front();
}
private:
Container _con;
};

priority_queue的实现

  在SGI版本的priority_queue中建堆以及调整堆都是调用的algorithm中的算法函数完成的,但是在模拟实现中我们完全自己实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
#pragma once
#include <iostream>
#include <vector>
#include <functional>
#include <algorithm>
template<class T>
struct Less
{
bool operator()(const T& obj1, const T& obj2)
{
return obj1 < obj2;
}
};
template<class T>
struct Greater
{
bool operator()(const T& obj1, const T& obj2)
{
return obj1 > obj2;
}
};

template<class T, class Container = std::vector<T>, class Compare = Less<T>>
class Priority_queue
{
public:
//向下调整
void AdjustDown(size_t parent)
{
size_t child = parent * 2 + 1;
Compare com;
while(child < _con.size())
{
if(child + 1 < _con.size() && com(_con[child], _con[child + 1]))
{
child++;
}
if(com(_con[parent], _con[child]))
{
std::swap(_con[parent], _con[child]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
//向上调整
void AdjustUp(size_t child)
{
size_t parent = (child - 1) / 2;
Compare com;
while(child > 0)
{
if(com(_con[parent], _con[child]))
{
std::swap(_con[parent], _con[child]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
void Push(const T& data)
{
_con.push_back(data);
//模拟push_heap()的功能
AdjustUp(_con.size() - 1);
}
void Pop()
{
//模拟pop_heap()的功能
std::swap(_con[0], _con[_con.size() - 1]);
_con.pop_back();
AdjustDown(0);
}
size_t Size()
{
return _con.size();
}
bool Empty()
{
return _con.empty();
}
T& Top()
{
return _con.front();
}
const T& Top() const
{
return _con.front();
}
//private:
Container _con;
};

-------------本文结束感谢您的阅读!-------------
记录学习每一分,感谢您的赞助