栈和队列理论基础
栈提供push和pop等接口,所有元素必须符合先进后出规则,所以栈不提供走访功能,也不提供迭代器(iterator)。不像是set或者map提供迭代器iterator来遍历所有元素。
栈是以底层容器完成其所有的工作,对外提供统一的接口,底层容器是可插拔的(也就是说我们可以控制使用哪种容器来实现栈的功能)。
所以STL中栈往往不被归类为容器,而被归类为container adapter(容器适配器)。
那么问题来了,STL中栈是用什么容器实现的?
栈的内部结构,栈的底层实现可以是vector,deque,list 都是可以的,主要就是数组和链表的底层实现。
我们常用的SGI STL,如果没有指定底层实现的话,默认是以deque为缺省情况下栈的底层结构。
deque是一个双端队列,只要封住一端,只开通另一端就可以实现栈的逻辑了。
SGI STL中队列底层实现缺省情况下一样使用deque实现的。
我们也可以指定vector为栈的底层实现,初始化语句如下:
std::stack<int, std::vector<int>> third; // 使用vector为底层容器的栈
刚刚讲过栈的特性,对应的队列的情况是一样的。
队列中先进先出的数据结构,同样不允许有遍历行为,不提供迭代器, SGI STL中队列一样是以deque为缺省情况下的底部结构。
也可以指定list为起底层实现,初始化queue的语句如下:
std::queue<int, std::list<int>> third; // 定义以list为底层容器的队列
所以STL 队列也不被归类为容器,而被归类为container adapter(容器适配器)。
让我们来看到两个题目来巩固下栈和队列的基础。
用栈实现队列
- 实现思路:我们需要两个栈来模拟队列,一个入栈和一个出栈。入栈的作用是来存放
push进来的队尾元素,出栈的作用是返回或pop队头元素。如果出栈为空,则需要把入栈的元素全部导入进出栈中,要是全为空,即队列空。 - 力扣题目链接
- 解法:
class MyQueue {
public:
stack<int> stIn, stOut; //默认底层deque实现
MyQueue(){}
void push(int x) { stIn.push(x);}
//注意弹出先判断出栈是否为空
int pop() { //应该是要判断队列空的情况,但交由应用者考虑
if(stOut.empty())
while(!stIn.empty()){ //注意下面的套路操作
stOut.push(stIn.top());
stIn.pop();
}
int result=stOut.top();
stOut.pop();
return result;
}
//注意代码复用
int peek() {
int result=this->pop();
stOut.push(result);
return result;
}
bool empty() { return stIn.empty() && stOut.empty();}
};
- 时间复杂度:
push和empty为O(1),pop和peek为O(n) - 空间复杂度: O(n)
用队列实现栈
- 实现思路:队列的特点是先进先出,用队列来模拟栈,要
pop的元素自然是队尾,我们可以想到把队列除了最后一个元素依次出队,最后pop的元素自然是栈顶元素 - 力扣题目链接
- 解法:
class MyStack {
public:
queue<int> q;
MyStack() {}
void push(int x) { q.push(x);}
//注意除了最后一个元素,其余元素出队挪到队尾
int pop() {
for(int i=0; i<q.size()-1; i++){
q.push(q.front());
q.pop();
}
int result=q.front();
q.pop();
return result;
}
//只获取栈顶直接返回队尾(用到了一点点双端队列的性质)
int top() { return q.back();}
bool empty() { return q.empty();}
};
- 时间复杂度:
pop为O(n),其他为O(1) - 空间复杂度: O(n)
栈经典题目
有效的括号
- 实现思路:需要一个字符栈,遇见左括号就
push进对应的右括号,遇见右括号就判断括号类型是否相同,相同则弹出。若到最后栈不空或者不匹配返回false,否则返回true。 - 力扣题目链接
- 解法:
bool isValid(string s) {
//奇数是不可能匹配的
if(s.size()%2) return false;
stack<char> st;
for(int i=0; i<s.size(); i++){
if(s[i]=='(') st.push(')');
else if(s[i]=='[') st.push(']');
else if(s[i]=='{') st.push('}');
else if(st.empty() || s[i]!=st.top()) return false;
else st.pop();
}
return st.empty();
}
- 时间复杂度: O(n)
- 空间复杂度: O(n)
删除字符串中的所有相邻重复项
- 这个题目看似非常复杂,像消消乐一样,如果用字符串当栈来实现(栈底是指向首部,栈顶指向尾部),问题便迎刃而解了。
- 力扣题目链接
- 解法:
string removeDuplicates(string s) {
string result;
for(char c:s){
if(result.empty() || result.back()!=c) result.push_back(c);
else result.pop_back();
}
return result;
}
- 时间复杂度: O(n)
- 空间复杂度: O(1),返回值不计空间复杂度
逆波兰表达式求值
-
逆波兰表达式是一种后缀表达式,主要有以下两个优点:
- 去掉括号后表达式无歧义,上式即便写成 1 2 + 3 4 + * 也可以依据次序计算出正确结果。
- 适合用栈操作运算:遇到数字则入栈;遇到运算符则取出栈顶两个数字进行计算,并将结果压入栈中。
-
因此逆波兰表达式对计算机非常友好,展现了计算机思考的方式。
-
解法:
int evalRPN(vector<string>& tokens) {
stack<long> st; //力扣修改了后台测试数据
for(auto token:tokens)
if(token=="+"||token=="-"||token=="*"||token=="/"){
long num1=st.top();
st.pop();
long num2=st.top();
st.pop();
if(token=="+") st.push(num2+num1);
else if(token=="-") st.push(num2-num1); //注意顺序
else if(token=="*") st.push(num2*num1);
else if(token=="/") st.push(num2/num1); //注意顺序
} else st.push(stol(token));
return st.top();
}
- 时间复杂度: O(n)
- 空间复杂度: O(n)
队列经典题目
滑动窗口最大值
- 实现思路:这道题被标记为困难,emm,自己先用暴力解法实现了一遍,超时!好吧,屈服了,这道题想要考察的知识点是单调队列,要用双端队列实现。其实队列没有必要维护窗口里的所有元素,只需要维护有可能成为窗口里最大值的元素就可以了,同时保证队列里的元素数值是由大到小的。
- 力扣题目链接
- 解法:
class Solution {
private:
class MyQueue{ //单调队列
public:
deque<int> q; //双端队列
void pop(int value){
if(!q.empty() && value==q.front())
q.pop_front();
}
void push(int value){
while(!q.empty() && value>q.back()) q.pop_back();
q.push_back(value);
}
int front(){ return q.front();}
};
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
vector<int> result;
MyQueue q;
for(int i=0; i<k; i++) q.push(nums[i]);
result.push_back(q.front());
for(int i=k; i<nums.size(); i++){
q.pop(nums[i-k]);
q.push(nums[i]);
result.push_back(q.front());
}
return result;
}
};
- 时间复杂度: O(n)
- 空间复杂度: O(k)
前 K 个高频元素
- 实现思路:首先自然是统计元素的个数,然后构造小根堆,即放入
priority_queue中,自定义比较函数(采用仿函数方法),输出到数组中即可。 - 注意:大根堆是<,小根堆是>正好相反,自己的理解是是要进来的元素是和堆顶元素比较,拿小根堆来举例,要是进来的元素>堆顶,就把堆顶踢出,进来的元素自顶向下调整,出来新的小堆顶。
- 力扣题目链接
- 解法:
#define pii pair<int,int>
vector<int> topKFrequent(vector<int>& nums, int k) {
//1.map记录元素出现的次数
unordered_map<int,int> map;
for(auto num:nums) map[num]++;
//2.利用优先队列,将出现次数排序
//自定义优先队列的比较方式,仿函数+小顶堆
struct myComparison{
bool operator()(pii& lhs, pii& rhs){
return lhs.second>rhs.second;
}
};
//创建优先队列,遍历map
priority_queue<pii, vector<pii>, myComparison> pq;
for(auto& it:map){
pq.push(it);
if(pq.size()>k) pq.pop();//每次弹出堆顶小元素,剩下的就是大元素
}
//将结果导出
vector<int> result;
while(!pq.empty()){
result.emplace_back(pq.top().first);//比push_back节省些时间
pq.pop();
}
return result;
}
- 时间复杂度: O(nlog k)
- 空间复杂度: O(n)
参考资料
[1] 代码随想录
[2] Leetcode题解