C++初阶—stack和queue类
第一章:stack的介绍和使用
1.1 stack的介绍
容器适配器不同于之前的数据容器直接管理数据,栈和队列是让其他容器管理数据,在此基础上封装封装、转换出需要的东西。
1. stack是一种容器适配器,专门用在具有后进先出操作的上下文环境中,其删除只能从容器的一端进行元素的插入与提取操作。
2. stack是作为容器适配器被实现的,容器适配器即是对特定类封装作为其底层的容器,并提供一组特定的成员函数来访问其元素,将特定类作为其底层的,元素特定容器的尾部(即栈顶)被压入和弹出。
3. stack的底层容器可以是任何标准的容器类模板或者一些其他特定的容器类,这些容器类应该支持以下操作:
- empty:判空操作
- back:获取尾部元素操作
- push_back:尾部插入元素操作
- pop_back:尾部删除元素操作
4. 标准容器vector、deque、list均符合这些需求,默认情况下,如果没有为stack指定特定的底层容器,默认情况下使用deque。
1.2 stack的使用
函数说明 | 接口说明 |
stack() | 构造空的栈 |
empty() | 检测stack是否为空 |
size() | 返回stack中元素的个数 |
top() | 返回栈顶元素的引用 |
push() | 将元素val压入stack中 |
pop() | 将stack中尾部的元素弹出 |
#include <iostream>
#include <stack>
using namespace std;
void test_stack1() {
stack<int> st;
st.push(1);
st.push(2);
st.push(3);
st.push(4);
while (!st.empty()) {
cout << st.top() << " ";
st.pop();
}
cout << endl;
}
int main() {
test_stack1();
return 0;
}
1.2.1 155. 最小栈 - 力扣(LeetCode)
class MinStack {
public:
//不需要写构造函数,不写编译器会默认生成,
//默认生成的构造函数对内置类型不做处理,对自定义类型会去调用该自定义类型的默认构造函数
//留下该构造函数且什么都不写也可以,因为不写所有成员会走初始化列表
//初始化列表也会去调用成员的默认构造
//思路:
//创建两个栈,_st正常存储入栈所有数据,_minst只有为空或入栈数据小于等于其栈顶数据才入栈
//注意入栈数据等于_minst栈顶数据时也要入栈。
//比如连续入栈数据是3且该数据是最小元素,如果_minst只入其中一个,那么pop之后,最小元素就丢失了
MinStack() {}
void push(int val) {
_st.push(val);//_st正常入栈
//_minst为空 或 入栈数据小于等于其栈顶数据才入栈
if (_minst.empty() || val <= _minst.top())
_minst.push(val);
}
void pop() {
//_minst栈顶数据和_st相等时才出栈
if (_st.top() == _minst.top())
_minst.pop();
_st.pop();//_st正常出栈
}
int top() {
return _st.top();
}
int getMin() {
return _minst.top();
}
stack<int> _st;
stack<int> _minst;
};
1.2.2 栈的压入、弹出序列_牛客题霸_牛客网
#include <stack>
#include <vector>
class Solution {
public:
//假设入栈数据是1,2,3,4,5,但并不一定是按顺序入栈,即入栈序列并不是有序
//所以不能依据入栈序列有序去找规律
//思路:
//1.入栈序列先入栈 结束条件:入栈序列走到尾
//2.栈顶数据和出栈序列比较
// a.如果匹配,出栈序列++,出栈顶元素,继续比较,直到栈为空,或者不匹配
// b.如果不匹配,返回步骤1
bool IsPopOrder(vector<int>& pushV, vector<int>& popV) {
// //自己的方法
// stack<int> pushst;
// size_t i = 0;//pushV的下标
// size_t j = 0;//popV的下标
// while (i < pushV.size()) {
// pushst.push(pushV[i]);
// ++i;
// //判断栈顶元素是否与出栈序列当前元素相等时要注意:
// //当连续比较并出栈时可能出现栈为空的情况,此时不能再取栈顶元素
// while (!pushst.empty() && pushst.top() == popV[j]) {
// pushst.pop();
// ++j;
// }
// }
// if (pushst.empty())
// return true;
// else
// return false;
//老师的方法
stack<int> st;
size_t pushi = 0, popi = 0;
while (pushi < pushV.size()) {
st.push(pushV[pushi++]);
while (!st.empty() && st.top() == popV[popi]) {
st.pop();
++popi;
}
}
return st.empty();
}
};
1.2.3 150. 逆波兰表达式求值 - 力扣(LeetCode)
class Solution {
public:
//中缀表达式转后缀表达式方法:
//1.遇到操作数输出
//2.遇到操作符:(第二步是个循环)
// a.栈为空,入栈
// b.栈不为空,跟栈顶操作符比较,优先级比栈顶操作符高则入栈
// (优先级高不能运算,因为后面操作符优先级可能更高)
// c.栈不为空,跟栈顶操作符比较,优先级比栈顶操作符低或相等,则出栈顶操作符
// (后一运算符确定前一运算符的优先级,后面优先级比前面低或相等才能计算)
//3.遇到括号:递归解决子问题,即再创建一个栈来处理运算符(处理规则跟步骤2一样)
//4.遍历完表达式,依次出栈中所有操作符
//运算符优先级可以单独写一个函数,使用switch case来实现
//该题思路:
//1.操作数入栈
//2.操作符,取栈顶的两个操作数运算,运算结果在入栈
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();
//虽然操作符只有一个,但类型是string,而switch条件和case后只能跟整形常量表达式,
//所以需要取字符操作(char也是整型)
//写法一:
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;
}
}
else
st.push(stoi(str));
}
return st.top();
}
};
1.3 stack的模拟实现
Stack.h
#pragma once
#include <vector>
#include <list>
#include <deque>
namespace bit {
原先学习过的方式
//template <class T>
//class stack {
//private:
// T* _a;
// size_t _size;
// size_t _capacity;
//};
//复用其他数据容器的方式,比如vector,list。这种方式叫容器适配器
//通过Container控制底层容器是哪个,不论底层容器是哪个,都可以适配出后进先出的栈
//template <class T, class Container = deque<T> >
//template <class T, class Container = deque<T> >
//库里面实现的带缺省参数,该缺省参数是双端队列(双端队列不是队列)
//双端队列deque支持头插、头删、尾插、尾删、任意位置插入删除、下标随机访问(具备vector+list的功能)
//虽然功能齐全但不是每方面都很强
//连续的物理空间
//优点:极致高效下标随机访问
//缺点:1.扩容(可能需要拷贝数据,消耗大)、2.中间或头部插入删除,效率低(需要挪动数据)
//非连续的物理空间
//优点:效率的任意位置插入删除数据、按需申请释放
//缺点:不支持下标随机访问
//deque框架
//有多个子数组(也称buffer,一般开辟10个元素的空间)存储数据。指向这些子数组的指针存在一个中控数组(本质是指针数组)中,
//并且这些数组指针并不是从中控数组头开始存储,而是中间。
//如果子数组中的数据满了。
//尾插:在最后一个子数组后再开辟一个数组,并将数据存到第一个位置。
//头插:在第一个子数组前在开辟一个数组,并将数据存到最后一个位置。
//只有中控数组满了才需要扩容,而且这个代价比较低,因为都是指针
//中间位置插入删除数据,是整体挪动数据 还是对单个buffer数组扩容或控制数据个数?
//都可以,看选择牺牲什么
//1.如果中间插入、删除选择调整子数组大小,那么会影响下标运算符重载的实现。
//假设每个buffer是一样大的,那么[]就会快很多。假设buffer大小都是10,i/=10就能算出在第几个buffer,再用i%=10就找到在该buffer的第几个
//2.选择整体挪动数据效率低
//一般还是选择每个buffer大小一样实现,因为顺序表中间插入和删除不多
template <class T, class Container = deque<T>>
class stack {
public:
//栈要实现的几个核心接口就是push、pop、top等
//可以使用:deque、vector、list作为底层容器
void push(const T& x) { _con.push_back(x); } //默认尾作为栈顶
void pop() { _con.pop_back(); }//尾删就是pop
T& top() { return _con.back(); }//取尾数据就是top
bool empty() { return _con.empty(); }
size_t size() { return _con.size(); }
private:
Container _con;//模版参数定义一个对象
};
}
Stack Class.cpp
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <string>
#include <deque>
#include <algorithm>
using namespace std;
#include "Stack.h"
class Solution {
public:
int evalRPN(vector<string>& tokens) {
//bit::stack<int, vector<int>> st;
//bit::stack<int, list<int>> st;
bit::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;
}
}
else
st.push(stoi(str));
}
return st.top();
}
};
int main() {
vector<string> v = { "4","13","5","/","+" };
cout << Solution().evalRPN(v) << endl;//6
return 0;
}
第二章:queue的介绍和使用
2.1 queue的介绍
翻译:
1. 队列是一种容器适配器,专门用于在FIFO上下文(先进先出)中操作,其中从容器一端插入元素,另一端提取元素。
2. 队列作为容器适配器实现,容器适配器即将特定容器类封装作为其底层容器类,queue提供一组特定的成员函数来访问其元素。元素从队尾入队列,从队头出队列。
3. 底层容器可以是标准容器类模板之一,也可以是其他专门设计的容器类。该底层容器应至少支持以下操作:
- empty:检测队列是否为空
- size:返回队列中有效元素的个数
- front:返回队头元素的引用
- back:返回队尾元素的引用
- push_back:在队列尾部入队列
- pop_front:在队列头部出队列
4. 标准容器类deque和list满足了这些要求。默认情况下,如果没有为queue实例化指定容器类,则使用标准容器deque。
2.2 queue的使用
函数声明 | 接口说明 |
queue() | 构造空的队列 |
empty() | 检测队列是否为空,是返回true,否则返回false |
size() | 返回队列中有效元素的个数 |
front() | 返回队头元素的引用 |
back() | 返回队尾元素的引用 |
push() | 在队尾将元素val入队列 |
pop() | 将队头元素出队列 |
2.2.1 102. 二叉树的层序遍历 - 力扣(LeetCode)
class Solution {
public:
// 最终输出的vector<vector<int>>可以理解为二维数组,其中每个元素vector<int>可以理解为一维数组(即二叉树每层的数据)
// 思路:
// 层序遍历就是一层一层输出数据
// 每一层数据用队列存储,因为队列是先进先出,且每输出一个节点,就将该节点的左右子节点入队列
// 创建一个levelSize变量用来存储每层有多少个数据,即levelSize是多少,就输出多少次数据
vector<vector<int>> levelOrder(TreeNode* root) {
queue<TreeNode*> q; // 创建队列对象存储每个二叉树节点指针,存储节点指针才能访问其左右子节点
int levelSize = 0; // 当前层的节点个数
if (root) { // 根节点不为空才向队列中插入数据
q.push(root);
levelSize = 1;
}
vector<vector<int>> vv; // 最终的输出
while (!q.empty()) { // 队列不为空就继续,第一次循环队列中有根节点
vector<int> v; // 每层的数据
// 每次取队头数据插入到储存每层数据vector中,同时将每次的队头左右子节点插入队列
while (levelSize--) { // 每层有多少数据就循环几次并输出数据
// 每次取队头的数据插入到存储每层数据的vector中
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);
}
// 每层数据个数就是队列数据个数
// 因为levelSize是每层数据个数,循环levelSize次就是每层的数据都输出完了(在此过程中也将下层的数据也插入队列了)
// 剩下的数据就是下一层的,即剩下的数据个数就是队列的size,也是下一层的数据个数
levelSize = q.size();
vv.push_back(v);
}
return vv;
}
};
2.3 queue的模拟实现
queue.h
#pragma once
#include <vector>
#include <list>
namespace bit {
template <class T, class Container = deque<T>>
class queue {
public:
//vector不能适配队列,因为头删效率低
void push(const T& x) { _con.push_back(x); }
void pop() { _con.pop_front(); }
const T& front() { return _con.front(); }
const T& back() { return _con.back(); }
bool empty() { return _con.empty(); }
size_t size() { return _con.size(); }
private:
Container _con;//模版参数定义一个对象
};
}
Queue Class.cpp
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <string>
#include <deque>
#include <algorithm>
using namespace std;
#include "queue.h"
void test_op1() { //验证deque直接排序的效率
srand((int)time(0));
const int N = 1000000;
deque<int> dq;
vector<int> v;
for (int i = 0; i < N; ++i) {
auto e = rand();
dq.push_back(e);
v.push_back(e);
}
// 排序
int begin1 = clock();
sort(v.begin(), v.end());
int end1 = clock();
int begin2 = clock();
sort(dq.begin(), dq.end());
int end2 = clock();
printf("vector sort:%d\n", end1 - begin1);//此方法更快
printf("deque sort:%d\n", end2 - begin2);
}
void test_op2() { //验证deque导入vector后排序的效率
srand((int)time(0));
const int N = 1000000;
deque<int> dq1;
deque<int> dq2;
for (int i = 0; i < N; ++i) {
auto e = rand();
dq1.push_back(e);
dq2.push_back(e);
}
// 排序
int begin1 = clock();
sort(dq1.begin(), dq1.end());
int end1 = clock();
int begin2 = clock();
vector<int> v(dq2.begin(), dq2.end());
sort(v.begin(), v.end());
dq2.assign(v.begin(), v.end());
int end2 = clock();
printf("deque sort:%d\n", end1 - begin1);
printf("deque copy vector sort:%d\n", end2 - begin2);//此方法更快
}
//通过上面排序消耗时间的例子可知,实际中deque不常用。下标随机访问还是用vector,头尾插入、删除deque还可以,略优于vector和list
//deque优势:头尾插入删除 大于 [] 远大于 中间插入删除
void test_queue() {
bit::queue<int> q;
q.push(1);
q.push(2);
q.push(3);
q.push(4);
q.push(5);
while (!q.empty()) {
cout << q.front() << " ";
q.pop();
}
cout << endl;
}
int main() {
test_op1();
test_op2();
test_queue();
return 0;
}
第三章:priority_queue的介绍和使用
3.1 priority_queue的介绍
翻译:
1. 优先队列是一种容器适配器,根据严格的弱排序标准,它的第一个元素总是它所包含的元素中最大的。
2. 此上下文类似于堆,在堆中可以随时插入元素,并且只能检索最大堆元素(优先队列中位于顶部的元素)。
3. 优先队列被实现为容器适配器,容器适配器即将特定容器类封装作为其底层容器类,queue提供一组特定的成员函数来访问其元素。元素从特定容器的“尾部”弹出,其称为优先队列的顶部。
4. 底层容器可以是任何标准容器类模板,也可以是其他特定设计的容器类。容器应该可以通过随机访问迭代器访问,并支持以下操作:
- empty():检测容器是否为空
- size():返回容器中有效元素个数
- front():返回容器中第一个元素的引用
- push_back():在容器尾部插入元素
- pop_back():删除容器尾部元素
5. 标准容器类vector和deque满足这些需求。默认情况下,如果没有为特定的priority_queue类实例化指定容器类,则使用vector。
6. 需要支持随机访问迭代器,以便始终在内部保持堆结构。容器适配器通过在需要时自动调用算法函数make_heap、push_heap和pop_heap来自动完成此操作。
3.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() | 删除优先级队列中最大(最小)元素,即堆顶元素 |
3.2.1 215. 数组中的第K个最大元素 - 力扣(LeetCode)
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
//思路:
//使用优先级队列建大堆,k是几pop几次,剩下堆中的堆顶元素就是结果
priority_queue<int> pq;//优先级队列默认大堆
//将vector数据导入优先级队列方法一:
for(auto e : nums)
pq.push(e);
//将vector数据导入优先级队列方法二:
priority_queue<int> pq(nums.begin(), nums.end());
while (--k) //应该pop k-1次,所以是前置--
pq.pop();
return pq.top();
}
};
【注意】
1. 默认情况下,priority_queue是大堆。
仿函数
//其功能主要为了替代函数指针
//class Less { //不一定要加模板
//public:
// bool operator() (int x, int y) { return x < y; }
//};
template <class T>//有模板更方便
class Less {
public:
bool operator() (const T& x, const T& y) { return x < y; }
};
template <class T>//有模板更方便
class Greater {
public:
bool operator() (const T& x, const T& y) { return x > y; }
};
void test_priority_queue() { //优先级队列不需要单独头文件,包含在queue里
//插入删除数据的效率 logN
//优先级队列是用vector适配生成,除了vector还可以用deque,因为需要支持[]的容器
bit::priority_queue<int> q; //默认大堆 //5 4 3 1
//bit::priority_queue<int, vector<int>, greater<int>> q;//1 3 4 5
q.push(3);
q.push(1);
q.push(5);
q.push(4);
while (!q.empty()) { //适配器都没有迭代器
cout << q.top() << " ";
q.pop();
}
cout << endl;
//可以理解为默认底层是大堆,每次取堆顶数据
//less是大堆,greater是小堆
int a[] = { 1,2,6,2,1,5,9,4 };
sort(a, a + 8);//默认升序
sort(a, a + 8, greater<int>());//降序
//上方这里greater<int>带(),而优先级队列不带的原因:
//在优先级队列中,bit::priority_queue<int, vector<int>, greater<int>> q; <>里面是类模板参数,要传的是类型
//传进去后在类里面定义对象
//在sort函数中需要传的是函数参数,所以传对象,即传了一个匿名对象
}
int main() {
//Less less;//无模版版本。函数对象,实质是对象,但可以像函数那样使用
Less<int> less;//有模板版本
cout << less(2, 3) << endl;
cout << less.operator()(2, 3) << endl;//上方的显示调用
return 0;
}
2. 如果在priority_queue中放自定义类型的数据,用户需要在自定义类型中提供> 或者< 的重载。
//演示仿函数的第二个用法
class Date {
public:
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(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);
}
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);
}
friend ostream& operator<<(ostream& _cout, const Date& d);
private:
int _year;
int _month;
int _day;
};
//友元最好在全局定义,否则有些编译器可能编译不通过
ostream& operator<<(ostream& _cout, const Date& d) {
_cout << d._year << "-" << d._month << "-" << d._day;
return _cout;
}
struct PDataCompare {
bool operator()(Date* p1, Date* p2) { return *p1 < *p2; }
};
int main() {
bit::priority_queue<Date> q1;
q1.push(Date(2018, 10, 29));
q1.push(Date(2018, 10, 28));
q1.push(Date(2018, 10, 30));
cout << q1.top() << endl;//默认大堆 2018-10-30
//bit::priority_queue<Date*> q2;
//上方实例化对象的方式会导致结果在三个日期中不停变换
//因为优先级队列里面存的是指针,所以 q2.top() 返回的是一个指针。
//这里优先级队列的比较是基于 地址的大小,而不是对象的值,这就导致了输出的是内存地址较大的对象。
//因为 new Date(...) 每次都会分配新的内存(且地址大小与开辟先后顺序无关),内存地址(指针)会不断变化。
bit::priority_queue<Date*, vector<Date*>, PDataCompare> q2;//正确方式
q2.push(new Date(2018, 10, 29));
q2.push(new Date(2018, 10, 28));
q2.push(new Date(2018, 10, 30));
cout << *(q2.top()) << endl;
return 0;
}
3.3 priority_queue的模拟实现
priority_queue.h
#pragma once
#include <vector>
namespace bit {
template <class T, class Container = vector<T>, class Compare = Less<T>>
class priority_queue {
public:
priority_queue() {} //有了下方带参数的构造函数(编译器不在自动生成默认构造函数),需要一个默认构造函数
template <class InputIterator>
priority_queue(InputIterator first, InputIterator last)
:_con(first, last) {
//向下调整建堆
for (int i = (_con.size() - 2) / 2; i >= 0; --i)
adjust_down(i);
}
//优先级队列不需要写构造函数和析构函数等,自动生成的各种默认函数会调用实例化后的容器
//目前是大堆的向上调整算法,如果要小堆的算法需要改代码
//C语言的qsort通过函数指针来控制升序或降序,但是函数指针不好用
void adjust_up(int child) {
Compare com;
int parent = (child - 1) / 2;
while (child > 0) {
//if (_con[parent] < _con[child]) {
//下方等价上方,priority_queue这个类模板的模版参数Less是个仿函数(类模版)
//仿函数可以像函数那样使用,Less这个仿函数的作用是,如果 参数1<参数2 则返回真
//所有再写一个Greater仿函数就可以改为 如果 参数1>参数2 则返回真
//这样就可以通过仿函数来控制父子节点的大小比较从而控制大小堆
if (com(_con[parent], _con[child])) {
swap(_con[parent], _con[child]);
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() && _con[child] < _con[child + 1])//如果右子节点存在且比左子结点大
if (child + 1 < _con.size() && com(_con[child], _con[child + 1]))
++child;//换成右子节点
//if (_con[parent] < _con[child]) {
if (com(_con[parent], _con[child])) {
swap(_con[parent], _con[child]);
parent = child;
child = parent * 2 + 1;
}
else
break;
}
}
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() { return _con.empty(); }
size_t size() { return _con.size(); }
private:
Container _con;
};
}
第四章:容器适配器
4.1 什么是适配器
适配器是一种设计模式(设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结),该种模式是将一个类的接口转换成客户希望的另外一个接口。
4.2 STL标准库中stack和queue的底层结构
虽然stack和queue中也可以存放元素,但在STL中并没有将其划分在容器的行列,而是将其称为容器适配器,这是因为stack和队列只是对其他容器的接口进行了包装,STL中stack和queue默认使用deque,比如:
4.3 deque的简单介绍(了解)
4.3.1 deque的原理介绍
deque(双端队列):是一种双开口的"连续"空间的数据结构,双开口的含义是:可以在头尾两端进行插入和删除操作,且时间复杂度为O(1),与vector比较,头插效率高,不需要搬移元素;与list比较,空间利用率比较高。
deque并不是真正连续的空间,而是由一段段连续的小空间拼接而成的,实际deque类似于一个动态的二维数组,其底层结构如下图所示:
双端队列底层是一段假象的连续空间,实际是分段连续的,为了维护其“整体连续”以及随机访问的假象,落在了deque的迭代器身上,因此deque的迭代器设计就比较复杂,如下图所示:
那deque是如何借助其迭代器维护其假想连续的结构呢?
4.3.2 deque的缺陷
与vector比较,deque的优势是:头部插入和删除时,不需要搬移元素,效率特别高,而且在扩容时,也不需要搬移大量的元素,因此其效率是必vector高的。
与list比较,其底层是连续空间,空间利用率比较高,不需要存储额外字段。
但是,deque有一个致命缺陷:不适合遍历,因为在遍历时,deque的迭代器要频繁的去检测其是否移动到某段小空间的边界,导致效率低下,而序列式场景中,可能需要经常遍历,因此在实际中,需要线性结构时,大多数情况下优先考虑vector和list,deque的应用并不多,而目前能看到的一个应用就是,STL用其作为stack和queue的底层数据结构。
4.4 为什么选择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不仅效率高,而且内存使用率高。
结合了deque的优点,而完美的避开了其缺陷。
作业
1. 以下是一个二叉树的遍历算法,queue是FIFO队列,请参考下面的二叉树,根节点是root,正确的输出是( )
queue.push(root);
while (!queue.empty()) {
node = queue.top();
queue.pop();
output(node->value) //输出节点对应数字
if (node->left)
queue.push(node->left);
if (node->right)
queue.push(node->right);
}
A.1376254
B.1245367
C.1234567
D.1327654
答案:C
分析:此题是一个层次遍历的伪代码
2. 一个栈的输入顺序是a,b,c,d,e则下列序列中不可能是出栈顺序是( )
A.e,d,a,c,b
B.a,e,d,c,b
C.b,c,d,a,e
D.b,c,a,d,e
答案:A
分析:首先此题要保证入栈的顺序不能改变,其次,某个字母出栈前,必须把其栈顶的元素都要出栈
A:e要先出栈,就必须把a b c d e 全部入栈,然后e才能出栈,对于e d 的出栈没有问题,只是a要出栈,就必须c d 先出栈后,才能轮到a出栈,因此A是不可能得到的出栈顺序
3. 下列代码的运行结果是( )
int main() {
queue<char> Q;
char x, y;
x = 'n'; y = 'g';
Q.push(x); Q.push('i'); Q.push(y);
Q.pop(); Q.push('r'); Q.push('t'); Q.push(x);
Q.pop(); Q.push('s');
while (!Q.empty()) {
x = Q.front();
Q.pop();
cout << x;
};
cout << y;
}
A.gstrin
B.grtnsg
C.srting
D.stirng
答案:B
分析:Q.push(x); Q.push('i'); Q.push(y); 入队数据为:nig 左边对头,右边队尾
Q.pop(); Q.push('r'); Q.push('t'); Q.push(x); n出队,rtn入队,队里数据为:igrtn
Q.pop(); Q.push('s'); i出队,s入队,队里数据为:grtns
while (!Q.empty()) {} 队不空,在出队打印为:grtns
cout << y; 最后在打印一个g
故答案为 : B
4. 下列代码的运行结果是( )
int main() {
stack<char> S;
char x, y;
x = 'n';
y = 'g';
S.push(x); S.push('i'); S.push(y);
S.pop(); S.push('r'); S.push('t'); S.push(x);
S.pop(); S.push('s');
while (!S.empty()) {
x = S.top();
S.pop();
cout << x;
};
cout << y;
}
A.gstrin
B.string
C.srting
D.stirng
答案:B
分析:S.push(x); S.push('i'); S.push(y); 入栈了字母“nig” 左边栈底 右边栈顶
S.pop(); S.push('r'); S.push('t'); S.push(x); 字母g出栈,然后入栈字母“rtn”,此时栈数据 为"nirtn"
S.pop(); S.push('s'); 字母n出栈,s入栈,最终的栈数据为nirts
while (!S.empty()) {} 栈不空出栈打印,按相反顺讯出栈,所以打印结果为:strin
cout << y; 最后还打印了字母g
所以答案为B
5. 225. 用队列实现栈 - 力扣(LeetCode)
//方法一:两个队列
class MyStack {
public:
queue<int> q1;
queue<int> q2;
MyStack() {}
void push(int x) {
if (!q1.empty())
q1.push(x);
else
q2.push(x);
}
int pop() {
//创建了 q1 和 q2 的副本,而不是修改原来的队列,导致 pop() 没有真正删除元素。
// queue<int> EmptyQ(q1);
// queue<int> NonEmptyQ(q2);
//这里 NonEmptyQ 和 EmptyQ 本来就是 q1 和 q2 的引用,不能重新赋值!
//引用一旦绑定,就不能更改绑定的对象,所以必须改成正确的交换逻辑。
// queue<int>& EmptyQ = q1;
// queue<int>& NonEmptyQ = q2;
queue<int>& NonEmptyQ = q1.empty() ? q2 : q1;
queue<int>& EmptyQ = q1.empty() ? q1 : q2;
if (!q1.empty()) {
NonEmptyQ = q1;
EmptyQ = q2;
}
while (NonEmptyQ.size() > 1) {
EmptyQ.push(NonEmptyQ.front());
NonEmptyQ.pop();
}
int top = NonEmptyQ.front();
NonEmptyQ.pop();
return top;
}
int top() {
if (!q1.empty())
return q1.back();
else
return q2.back();
}
bool empty() {
return q1.empty() && q2.empty();
}
};
//方法二:一个队列
class MyStack {
public:
queue<int> q;
MyStack() {}
//在插入新元素后,除新元素外依次出队并重新入队,使得新插入元素变为队列 front(即栈顶)。
void push(int x) {
q.push(x);
int size = q.size();
while (size > 1) {
q.push(q.front());
q.pop();
size--;
}
}
int pop() {
int top = q.front();
q.pop();
return top;
}
int top() {
return q.front();
}
bool empty() {
return q.empty();
}
};
6. 232. 用栈实现队列 - 力扣(LeetCode)
class MyQueue {
public:
stack<int> pushst;
stack<int> popst;
MyQueue() {}
void push(int x) {
pushst.push(x);
}
int pop() {
int front = peek();
popst.pop();
return front;
}
int peek() {
if (popst.empty()) {
while (!pushst.empty()) {
popst.push(pushst.top());
pushst.pop();
}
}
return popst.top();
}
bool empty() {
return pushst.empty() && popst.empty();
}
};
7. 下列代码的运行结果是( )
int main() {
priority_queue<int> a;
priority_queue<int, vector<int>, greater<int> > c;
priority_queue<string> b;
for (int i = 0; i < 5; i++) {
a.push(i);
c.push(i);
}
while (!a.empty()) {
cout << a.top() << ' ';
a.pop();
}
cout << endl;
while (!c.empty()) {
cout << c.top() << ' ';
c.pop();
}
cout << endl;
b.push("abc");
b.push("abcd");
b.push("cbd");
while (!b.empty()) {
cout << b.top() << ' ';
b.pop();
}
cout << endl;
return 0;
}
A.4 3 2 1 0 0 1 2 3 4 cbd abcd abc
B.0 1 2 3 4 0 1 2 3 4 cbd abcd abc
C.4 3 2 1 0 4 3 2 1 0 abc abcd cbd
D.0 1 2 3 4 4 3 2 1 0 cbd abcd abc
答案:A
分析:优先级队列默认情况为:大堆,这是解题的关键
priority_queue<int> a; //a是大堆
priority_queue<int, vector<int>, greater<int> > c; //c指定了比较规则,是小堆
priority_queue<string> b; //b是大堆
因此分别建堆的过程是建立a大堆,c小堆,b大堆
所以出队的顺序就按照其优先级大小出队,所以答案为A
8. STL中的priority_queue使用的底层数据结构是什么( )
A.queue
B.heap
C.deque
D.vector
答案:D
9. 以下说法正确的是( )
A.deque的存储空间为连续空间
B.list迭代器支持随机访问
C.如果需要高效的随机存取,还要大量的首尾的插入删除则建议使用deque
D.vector容量满时,那么插入元素时只需增加当前元素个数的内存即可
答案:C
A.deque底层总体为不连续空间
B.不支持,因为底层是一个个的不连续节点
C.正确
D.一般会以容量的2倍扩充容量,这是为了减少扩容的次数,减少内存碎片
10. 假设cont是一个Container 的示例,里面包含数个元素,那么当CONTAINER为:
1.vector 2.list 3.deque 会导致下面的代码片段崩溃的Container 类型是( )
int main() {
Container cont = { 1, 2, 3, 4, 5 };
Container::iterator iter, tempIt;
for (iter = cont.begin(); iter != cont.end();) {
tempIt = iter;
++iter;
cont.erase(tempIt);
}
}
A.1, 2
B.2, 3
C.1, 3
D.1, 2, 3
答案:C
分析:此题主要考察cont.erase(tmpit)删除数据之后,迭代器失效相关问题
本题重点要关注的是底层实现
vector、deque底层都是用了连续空间,所以虽然++iter迭代器了,但是erase(tempit)以后底层是连续空间,删除会挪动数据,最终导致iter意义变了,已失效了。
而list,不是连续空间,删除以后tempIt虽然失效了,但是不影响iter。
11. 仿函数比起一般函数具有很多优点,以下描述错误的是( )
A.在同一时间里,由某个仿函数所代表的单一函数,可能有不同的状态
B.仿函数即使定义相同,也可能有不同的类型
C.仿函数通常比一般函数速度快
D.仿函数使程序代码变简单
答案:C
A.仿函数是模板函数,可以根据不同的类型代表不同的状态
B.仿函数是模板函数,可以有不同类型
C.仿函数是模板函数,其速度比一般函数要慢,故错误
D.仿函数在一定程度上使代码更通用,本质上简化了代码