🌠 作者:@阿亮joy.
🎆专栏:《吃透西嘎嘎》
🎇 座右铭:每个优秀的人都有一段沉默的时光,那段时光是付出了很多努力却得不到结果的日子,我们把它叫做扎根
目录
- 👉stack 的介绍和使用👈
- stack 的介绍
- stack 的使用
- 1. 最小栈
- 2. 验证栈序列
- 3. 逆波兰表达式求值
- 4. 中缀表达式转为后缀表达式
- 5. 根据中缀表达式构建二叉树
- 6. 中缀表达式求值
- 7. 中缀表达式转化为前缀表达式
- 👉queue 的介绍和使用👈
- queue 的介绍
- queue 的使用
- 👉stack 的模拟实现👈
- 👉queue 的模拟实现👈
- 👉容器适配器👈
- 什么是适配器
- STL标准库中 stack 和 queue 的底层结构
- deque
- 1. deque 的原理介绍
- 2. deque 的缺陷
- 👉总结👈
👉stack 的介绍和使用👈
stack 的介绍
- stack 是一种容器适配器,专门用在具有后进先出操作的上下文环境中,其删除只能从容器的一端进行元素的插入与提取操作。
- stack 是作为容器适配器被实现的,容器适配器即是对特定类封装作为其底层的容器,并提供一组特定的成员函数来访问其元素,将特定类作为其底层的,元素特定容器的尾部(即栈顶)被压入和弹出。
- stack 的底层容器可以是任何标准的容器类模板或者一些其他特定的容器类,这些容器类应该支持以下操作:
- empty:判空操作
- back:获取尾部元素操作
- push_back:尾部插入元素操作
- pop_back:尾部删除元素操作
- 标准容器 vector、deque、list 均符合这些需求,默认情况下,如果没有为 stack 指定特定的底层容器,默认情况下使用 deque。
stack 的使用
函数说明 | 接口说明 |
---|---|
stack() | 构造空的栈 |
empty() | 检测stack是否为空 |
size() | 返回stack中元素的个数 |
top() | 返回栈顶元素的引用 |
push() | 将元素val压入stack中 |
pop() | 将stack中尾部的元素弹出 |
1. 最小栈
设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。
实现 MinStack 类:
- MinStack() 初始化堆栈对象。
- void push(int val) 将元素val推入堆栈。
- void pop() 删除堆栈顶部的元素。
- int top() 获取堆栈顶部的元素。
- int getMin() 获取堆栈中的最小元素。
思路:最小栈MinStack
包含两个栈:一个是存储插入元素的栈_st
,另一个是存储最小值的栈_minst
。插入元素时,必须向栈_st
中插入新元素。对于_minst
来说,如果栈_minst
为空,直接向_minst
插入新元素。如果栈_minst
不为空,当新插入的元素小于或等于_minst
的栈顶元素,那么向_minst
中插入新元素;而新插入的元素大于_minst
的栈顶元素,则向_minst
中插入栈顶元素。那么,此时的_minst
中的栈顶元素就是栈_st
对应位置的最小值。出栈时,让栈_st
和_minst
同时出栈即可。
class MinStack
{
public:
MinStack() {}
void push(int val)
{
_st.push(val);
if(_minst.empty() || val <= _minst.top())
{
_minst.push(val);
}
else
{
_minst.push(_minst.top());
}
}
void pop()
{
_st.pop();
_minst.pop();
}
int top()
{
return _st.top();
}
int getMin()
{
return _minst.top();
}
stack<int> _st;
stack<int> _minst;
};
其实按照上面的思路来解题,栈_minst
中可能会存储着大量的重复的最小值。那么我们可以做一些小小的优化:当新插入元素大于_minst
栈顶元素时,不再向_minst
插入栈顶元素了。按照这种思路的话,出栈也要做相应的改变:当_st
栈顶元素等于_minst
栈顶元素时,_minst
才会出栈。
class MinStack
{
public:
MinStack() {}
void push(int val)
{
_st.push(val);
if(_minst.empty() || val <= _minst.top())
{
_minst.push(val);
}
}
void pop()
{
if(_st.top() == _minst.top())
{
_minst.pop();
}
_st.pop();
}
int top()
{
return _st.top();
}
int getMin()
{
return _minst.top();
}
stack<int> _st;
stack<int> _minst;
};
但是第二种思路面对着大量重复的最小值的场景时,也会存在空间浪费的情况。那么我们还可以进一步的优化,增加多一个计算器并将其封装成一个类struct valCont
,该类的成员变量_val
和_count
。此时,栈_minst
的元素是struct valCount
。当新插入元素等于_minst.top()._val
时,++_minst.top()._count
。当_minst.top()._count == 0
时,则_minst
栈顶元素出栈。
struct valCount
{
int _val;
int _count;
valCount(int val = 0, int count = 0)
{
_val = val;
_count = count;
}
};
class MinStack
{
public:
MinStack() {}
void push(int val)
{
_st.push(val);
if(_minst.empty() || val < _minst.top()._val)
{
_minst.push(valCount(val, 1));
}
else if(val == _minst.top()._val)
{
++_minst.top()._count;
}
}
void pop()
{
if(_st.top() == _minst.top()._val)
{
--_minst.top()._count;
if(_minst.top()._count == 0)
_minst.pop();
}
_st.pop();
}
int top()
{
return _st.top();
}
int getMin()
{
return _minst.top()._val;
}
stack<int> _st;
stack<valCount> _minst;
};
注:struct valCount 最好自己提供默认构造函数,这样就可以支持匿名对象的用法了。以上思路实现的 MinStack 都没有提供构造函数,为什么也有过呢?因为对于自定义类型,编译器会调用自定义类型的默认构造函数。
2. 验证栈序列
给定 pushed 和 popped 两个序列,每个序列中的值都不重复,只有当它们可能是在最初空栈上进行的推入 push 和弹出 pop 操作序列的结果时,返回 true;否则,返回 false 。
思路:给定一个入栈序列,可能会有多个出栈序列。因为会存在边入边出、入几个再出等等情况。本道题是让我们验证出栈序列popped
是不是入栈序列pushed
的出栈序列。那么如何解决这道题呢?这道题可以这样来解决:1. 入栈序列的元素先入栈 2. 栈顶元素和出栈序列的元素比较,相等则栈顶元素出栈并继续比较 3. 当入栈序列的元素已全部入栈,循环结束。如果栈为空或者出栈序列也全部遍历了一次,那么popped
就是pushed
的出栈序列了,否则不是。
class Solution
{
public:
bool validateStackSequences(vector<int>& pushed, vector<int>& popped)
{
stack<int> st;
int i = 0; // 遍历出栈序列popped用的下标
for(auto& e : pushed)
{
st.push(e); // 入栈序列先入栈
// 比较:相同则出栈且继续比较
while(!st.empty() && st.top() == popped[i])
{
st.pop();
++i;
}
}
//return i == popped.size();
return st.empty();
}
};
3. 逆波兰表达式求值
给你一个字符串数组 tokens ,表示一个根据逆波兰表示法表示的算术表达式。
请你计算该表达式。返回一个表示表达式值的整数。
注意:
- 有效的算符为 ‘+’、‘-’、‘*’ 和 ‘/’ 。
- 每个操作数(运算对象)都可以是一个整数或者另一个表达式。
- 两个整数之间的乘法总是 向零截断 。
- 表达式中不含除零运算。
- 输入是一个根据逆波兰表示法表示的算术表达式。
- 答案及所有中间计算结果可以用 32 位 整数表示。
逆波兰表达式是一种后缀表达式,而我们日常学习用到的是中缀表达式。中缀表达式操作符在中间,操作数在两遍;而后缀表达式则操作符在后面,操作数在前面。如:前缀表达式2 + 3 * 4
对应的后缀表达式为2 3 4 * +
。那为什么要有后缀表达式呢?因为计算机是顺序读入表达式的,当读到操作符时,计算机就会去两个操作数进行运算。如果采用的是中缀表达式,计算机将无法识别该操作符后是否存在更高级的操作符。而后缀表达式不会存在这样的情况,可以依据后缀表达式的次序计算出正确结果。
那么解决这道题的思路是什么呢?其实很简单,就是遇到数字则入栈;遇到操作符则取栈顶两个数组进行计算(对于减法和除法,需区分左操作数和右操作数),并将结果压入栈中。遍历结束,栈顶元素就是后缀表达式的计算结果。
class Solution
{
public:
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();
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(); // 后缀表达式的结果
}
};
stoi 函数的第二个参数是一个输出型参数,其值为 str 转化成整型数字时的结束位置的下一个位置,该参数可以设置为nullptr
,表明不关心该值;第三个参数则表示要转化的字符串 str 里的数字是什么进制的。该参数的缺省值为 10,表示 str 里的数字是十进制的。如果该值被设置为 0 ,则需要根据 str 里的标致来判别其数字是什么进制的。
stoi 函数的演示使用
#include <iostream>
#include <string>
using namespace std;
int main()
{
string str_dec = "2001, A Space Odyssey";
string str_hex = "40c3";
string str_bin = "-10010110001";
string str_auto = "0x77";
std::string::size_type sz; // alias of size_t
int i_dec = std::stoi(str_dec, &sz);
int i_hex = std::stoi(str_hex, nullptr, 16); // 十六进制
int i_bin = std::stoi(str_bin, nullptr, 2); // 2进制
int i_auto = std::stoi(str_auto, nullptr, 0); //
cout << str_dec << ": " << i_dec << " and [" << str_dec.substr(sz) << "]\n";
// substr(sz)表示从sz位置取到str_dec的结尾作为子串
cout << str_hex << ": " << i_hex << '\n';
cout << str_bin << ": " << i_bin << '\n';
cout << str_auto << ": " << i_auto << '\n';
return 0;
}
4. 中缀表达式转为后缀表达式
注:该中缀表达式中包含 + - * / ( ) =
等符号,且包含空格。转换得到的后缀表达式也不包含空格。
那么如何将中缀表达式转为后缀表达式呢?
- 从左往右遍历中缀表达式,跳过空格
- 遇到操作数直接输出
- 如果当前操作符为左括号,左括号直接入栈
- 如果当前操作符为右括号,让栈顶到左括号之间的操作符出栈添加到后缀表达式中,括号不出现在后缀表达式中
- 如果当前操作符优先级小于或等于栈顶操作符的优先级,先将栈顶操作符出栈直至栈顶操作符优先级小于当前操作符优先级,再将当前操作符入栈
- 遍历完中缀表达式,将栈中的操作符出栈添加到后缀表达式中
注意:出栈有可能会出多个操作符;栈为空时,操作符一定要入栈。遍历结束时,栈内的操作符要依次出栈。
class Solution
{
public:
vector<string> convertToRPN(vector<string> &expression)
{
vector<string> ret;
stack<string> st; // 存储操作符的栈
for(auto& str : expression)
{
if(str == "(") // 左括号直接入栈
st.push(str);
else if(str == ")")
{
// 左括号上面的操作符全部出栈添加到后缀表达式中
while(st.top() != "(")
{
ret.push_back(st.top());
st.pop();
}
st.pop(); // 左括号出栈但不添加到后缀表达式中
}
else if(str == "+" || str == "-" || str == "*" || str == "/")
{
// 栈顶操作符优先级大于或等于当前操作符,则出栈
while(!st.empty() && compare(st.top(), str))
{
ret.push_back(st.top());
st.pop();
}
st.push(str); // 当前操作符入栈
}
else
{
ret.push_back(str); // 数字直接添加到后缀表达式中
}
}
while(!st.empty())
{
ret.push_back(st.top());
st.pop();
}
return ret;
}
private:
// 比较操作符的优先级:first >= second时,返回 true
bool compare(const string& first, const string& second)
{
if(first == "*" || first == "/")
return true;
else if(first == "(")
return false;
else if(second == "+" || second == "-")
return true;
else
return false;
}
};
5. 根据中缀表达式构建二叉树
其实将中缀表达式构建成二叉树的思路差不多,思路如下:
- 从左往右遍历中缀表达式
- 遇到操作数时,建立新节点存储该操作数并将该节点压入操作数栈中
- 当操作符从操作符栈中出栈时,为该操作符新建一个节点,并从操作数栈中 pop 出两个操作数节点,第一个操作数节点作为操作符节点的右孩子,第二个操作数节点作为操作符节点的左孩子,将新节点压入操作数栈中(注:节点 TreeNode 的值是 string,操作数栈存储的是 TreeNode*)。该过程直至操作符栈的栈顶操作符优先级小于当前操作符的优先级,最后将当前操作符压入操作符栈中。
- 注:操作符优先级关系同将中缀表达式转为后缀表达式的操作符优先级关系
- 当最后一个操作符出栈时,就构成了二叉表达树,且最后一个操作符节点为根节点
- 该二叉表达书的前序遍历就是前缀表达式(波兰表达式),中序遍历就是中缀表达式,后序遍历就是后缀表达式(逆波兰表达式)。
有些计算器就是通过将中缀表达式转化成后缀表达式,再计算后缀表达式求出结果。
6. 中缀表达式求值
给一个用字符串表示的中缀表达式数组,求出这个中缀表达式的值。
表达式只包含整数,+,-,*,/,(,)
思路:
- 从左往右遍历中缀表达式
- 遇到操作数,将其放入操作数栈
stack<int>
中- 遇到左括号,直接将左括号放入操作符栈
stack<string>
中- 遇到右括号,操作符栈的元素出栈直至左括号成为栈顶元素。注意操作符栈元素出栈的过程中,操作数栈也要出两个操作数来进行计算,并将计算结果放回操作数栈中。最后左括号出栈
- 当遇到
+ - * /
操作符时,需比较操作符的优先级。如果栈顶操作符优先级高,则进行计算并将计算结果压入操作数栈中。最后弹出栈顶操作符并将当前操作符压入操作符栈中- 遍历结束,将操作符栈和操作数栈中的元素进行计算。最后操作数栈的栈顶元素就是中缀表达式的计算结果
class Solution
{
public:
int evaluateExpression(vector<string> &expression)
{
if(expression.size() == 0)
return 0;
stack<int> st1; // 操作数栈
stack<string> st2; // 操作符栈
for(auto& str : expression) // 遍历中缀表达式
{
if(str == "(") // 左括号直接入栈
st2.push(str);
else if(str == ")")
{
while(st2.top() != "(")
{
calc(st1, st2); // 操作符边出栈边计算
st2.pop();
}
st2.pop(); // 左括号出栈
}
else if(str == "+" || str == "-" || str == "*" || str == "/")
{
while(!st2.empty() && compare(st2.top(), str))
{
calc(st1, st2);
st2.pop();
}
st2.push(str);
}
else
st1.push(stoi(str)); // 字符串转为操作数
}
// 将两个栈的剩余元素进行计算
while(!st2.empty())
{
calc(st1, st2);
st2.pop();
}
return st1.top();
}
private:
// 比较操作符优先级
bool compare(const string& first, const string& second)
{
if(first == "(") // 栈顶操作符为左括号,当前操作符直接入栈
return false;
else if(first == "*" || first == "/")
return true;
else if(second == "+" || second == "-")
return true;
else
return false;
}
// 计算
void calc(stack<int>& st1, stack<string>& st2)
{
int right = st1.top();
st1.pop();
int left = st1.top();
st1.pop();
switch(st2.top()[0])
{
case '+':
st1.push(left + right);
break;
case '-':
st1.push(left - right);
break;
case '*':
st1.push(left * right);
break;
case '/':
st1.push(left / right);
break;
}
}
};
7. 中缀表达式转化为前缀表达式
给定一个字符串数组,它代表一个表达式,返回该表达式的波兰表达式(去掉括号)。
思路:
- 从右向左遍历中缀表达式
- 遇到数字,直接添加到
vector<string> ret
的末尾- 遇到右括号,入操作符栈
stack<string> st
- 遇到左括号,操作符栈元素出栈并添加到
ret
的末尾中,直至右括号弹出(右括号不添加到ret
中)- 如果遇到操作符,操作符栈弹栈至栈顶操作符不大于当前操作符,所有弹出的操作符依次添加到
ret
的末尾,最后再将该操作符入栈- 出于方便,我们将所有操作符的优先级设置为:
*/
最高,+-
次之,然后是右括号,最后是左括号- 遍历完后,将操作符栈中的元素依次弹出并添加到
ret
的末尾。最后将ret
反转就能得到前置表达式了
#include <iostream>
#include <vector>
#include <string>
#include <stack>
using namespace std;
class Solution
{
public:
vector<string> Convertion(vector<string>& expression)
{
vector<string> ret;
stack<string> st;
for (int i = expression.size() - 1; i >= 0; --i)
{
string& str = expression[i];
if (str == ")") // 右括号直接入栈
st.push(str);
else if (str == "(")
{
while (st.top() != ")")
{
ret.push_back(st.top());
st.pop();
}
st.pop(); // 右括号出栈
}
else if (str == "+" || str == "-" || str == "*" || str == "/")
{
// 栈顶操作符优先级大于当前操作符优先级,则出栈
while (!st.empty() && getPriority(st.top()) > getPriority(str))
{
ret.push_back(st.top());
st.pop();
}
st.push(str);
}
else // 操作数则直接添加到ret的末尾
ret.push_back(str);
}
// 栈中的操作符全部出栈
while (!st.empty())
{
ret.push_back(st.top());
st.pop();
}
reverse(ret.begin(), ret.end()); // 将ret反转得到前缀表达式
return ret;
}
private:
int getPriority(const string& str)
{
if (str == "*" || str == "/")
return 3;
else if (str == "+" || str == "-")
return 2;
else if (str == ")")
return 1;
else
return 0;
}
};
int main()
{
vector<string> expression1 = { "(", "5", "-", "6", ")", "*", "7" };
vector<string> expression2 = { "3", "+", "(", "1", "-", "2", ")" };
vector<string> ret1 = Solution().Convertion(expression1);
vector<string> ret2 = Solution().Convertion(expression2);
for (auto& str : ret1)
{
cout << str << " ";
}
cout << endl;
for (auto& str : ret2)
{
cout << str << " ";
}
cout << endl;
return 0;
}
👉queue 的介绍和使用👈
queue 的介绍
- 队列是一种容器适配器,专门用于在 FIFO 上下文(先进先出)中操作,其中从容器一端插入元素,另一端提取元素。
- 队列作为容器适配器实现,容器适配器即将特定容器类封装作为其底层容器类,queue 提供一组特定的成员函数来访问其元素。元素从队尾入队列,从队头出队列。
- 底层容器可以是标准容器类模板之一,也可以是其他专门设计的容器类。该底层容器应至少支持以下操作:
- empty:检测队列是否为空
- size:返回队列中有效元素的个数
- front:返回队头元素的引用
- back:返回队尾元素的引用
- push_back:在队列尾部入队列
- pop_front:在队列头部出队列
- 标准容器类 deque 和 list 满足了这些要求。默认情况下,如果没有为 queue 实例化指定容器类,则使用标准容器 deque。
queue 的使用
函数声明 | 接口说明 |
---|---|
queue() | 构造空的队列 |
empty() | 检测队列是否为空,是返回 true,否则返回 false |
size() | 返回队列中有效元素的个数 |
front() | 返回队头元素的引用 |
back() | 返回队尾元素的引用 |
push() | 在队尾将元素 val 入队列 |
pop() | 将队头元素出队列 |
👉stack 的模拟实现👈
学习 stack 的模拟实现前,我们需要了解一下上面是设计模式。设计模式是一套被反复使用、多数人知晓的经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。
目前,我们学习到了一种设计模式 — 迭代器模式。迭代器模式不暴露底层的实现细节,封装后提供统一的方式访问容器。而接下来将要学习到的适配器模式就是将已有的东西封装转换成我们想要的东西。
stack 的适配器可以是 vector、list 和 deque,这些容器都支持尾插、尾删、判空和获得尾部元素等操作。stl 中的 stack 和 queue 的默认适配器都是双端队列 deque,而本人设计的 stack 默认适配器为 vector。注:双端队列将会在下面的内容里讲解。
// Stack.h
#pragma once
#include <vector>
#include <list>
#include <deque>
#include <iostream>
using namespace std;
namespace Joy
{
// 默认容器为vector
template <class T, class Container = vector<T> >
class stack
{
public:
void push(const T& val)
{
_con.push_back(val);
}
void pop()
{
_con.pop_back();
}
T& top()
{
return _con.back();
}
const T& top() const
{
return _con.back();
}
bool empty() const
{
return _con.empty();
}
size_t size()
{
return _con.size();
}
private:
Container _con;
};
}
// Test.cpp
#include "Stack.h"
int main()
{
Joy::stack<int> st;
st.push(1);
st.push(2);
st.push(3);
st.push(4);
st.push(5);
while (!st.empty())
{
cout << st.top() << " ";
st.pop();
}
cout << endl;
return 0;
}
👉queue 的模拟实现👈
queue 的适配器需要支持头删、尾插、判空、获得头部元素和尾部元素等操作。因为 vector 没有pop_front
头删接口且 vector 头删效率低,所以本人采用 list 作为 queue 的默认适配器。
// Queue.h
#pragma once
#include <deque>
#include <list>
#include <iostream>
using namespace std;
namespace Joy
{
// 默认适配器为list
template <class T, class Container = list<T> >
class queue
{
public:
void push(const T& val)
{
_con.push_back(val);
}
void pop()
{
_con.pop_front();
}
T& front()
{
return _con.front();
}
const T& front() const
{
return _con.front();
}
T& back()
{
return _con.back();
}
const T& back() const
{
return _con.front();
}
bool empty() const
{
return _con.empty();
}
size_t size()
{
return _con.size();
}
private:
Container _con;
};
}
// Test.cpp
#include "Queue.h"
int main()
{
Joy::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;
return 0;
}
👉容器适配器👈
什么是适配器
适配器是一种设计模式(设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结),该种模式是将一个类的接口转换成客户希望的另外一个接口。
STL标准库中 stack 和 queue 的底层结构
虽然 stack 和 queue 中也可以存放元素,但 STL 并没有将其划分在容器的行列,而是将其称为容器适配器,这是因为 stack 和 queue 只是对其他容器的接口进行了包装,STL 中 stack 和 queue 默认使用 deque。
deque
1. deque 的原理介绍
deque(双端队列):是一种双开口的"连续"空间的数据结构,双开口的含义是:可以在头尾两端进行插入和删除操作,且时间复杂度为 O(1)。与 vector 比较,头插效率高,不需要搬移元素;与 list 比较,空间利用率比较高。
deque 并不是真正连续的空间,而是由一段段连续的小空间拼接而成的,实际 deque 类似于一个动态的二维
数组,其底层结构如下图所示:
那 deque 是如何借助其迭代器维护其假想连续的结构呢?
#include <deque>
#include <iostream>
using namespace std;
int main()
{
deque<int> d;
d.push_back(1);
d.push_back(2);
d.push_back(3);
d.push_back(4);
d.push_front(10);
d.push_front(20);
for (size_t i = 0; i < d.size(); ++i)
{
cout << d[i] << " ";
}
cout << endl;
return 0;
}
2. deque 的缺陷
与 vector 比较,deque 的优势是:头部插入和删除时,不需要搬移元素,效率高.而且在扩容时,也不需要搬移大量的元素,因此其效率是比 vector 高的。
与 list 比较,其底层是连续空间,空间利用率比较高,不需要存储额外字段。
但是,deque 有一个致命缺陷:不适合遍历,因为在遍历时,deque 的迭代器要频繁的去检测其是否移动到某段小空间的边界,导致效率低下。而序列式场景中,可能需要经常遍历,因此在实际中,需要线性结构时,大多数情况下优先考虑 vector 和 list,deque 的应用并不多,而目前能看到的一个应用就是 STL 用其作为 stack 和 queue 的底层数据结构。deque 适用于中间插入删除少、头尾插入删除多、偶尔需要随机访问的场景。
性能对比
#include <deque>
#include <vector>
#include <iostream>
#include <algorithm>
using namespace std;
// N个数据需要排序,vector+ 算法sort deque+ sort
void Test()
{
srand(time(0));
const int N = 1000000;
vector<int> v;
v.reserve(N);
deque<int> dq;
for (int i = 0; i < N; ++i)
{
auto e = rand();
v.push_back(e);
dq.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("dequeue sort:%d\n", end2 - begin2);
}
int main()
{
Test();
return 0;
}
原因:deque 的随机访问效率没有 vector 的随机访问效率高。
为什么选择 deque 作为 stack 和 queue 的底层默认容器
stack 是一种后进先出的特殊线性数据结构,因此只要具有 push_back() 和 pop_back() 操作的线性结构,都可
以作为 stack 的底层容器,比如 vector 和 list 都可以。queue 是先进先出的特殊线性数据结构,只要具有
push_back 和 pop_front 操作的线性结构,都可以作为queue 的底层容器,比如 list。但是 STL 中对 stack 和
queue 默认选择 deque 作为其底层容器,主要是因为结合了 deque 的优点,而完美的避开了其缺陷。
- stack 和 queue 不需要遍历(因此 stack 和 queue 没有迭代器),只需要在固定的一端或者两端进行操作。
- 在 stack 中元素增加时,deque 比 vector 的效率高(扩容时不需要搬移大量数据);queue 中的元素增加时,deque 不仅效率高,而且内存使用率高。
👉总结👈
本篇博客主要讲解了栈的几道经典例题:最小值、验证栈序列、逆波兰表达式求值和将中缀表达式转为后缀表达式、什么是适配器、以适配器模式实现 stack 和 queue 以及双端队列 deque 等等。那么以上就是本篇博客的全部内容了,如果大家觉得有收获的话,可以点个三连支持一下!谢谢大家!💖💝❣️