Leetcode刷题笔记Day5:栈和队列

110 阅读6分钟

栈和队列理论基础

栈提供pushpop等接口,所有元素必须符合先进后出规则,所以栈不提供走访功能,也不提供迭代器(iterator)。不像是set或者map提供迭代器iterator来遍历所有元素。

栈是以底层容器完成其所有的工作,对外提供统一的接口,底层容器是可插拔的(也就是说我们可以控制使用哪种容器来实现栈的功能)。

所以STL中栈往往不被归类为容器,而被归类为container adapter(容器适配器)。

那么问题来了,STL中栈是用什么容器实现的?

栈的内部结构,栈的底层实现可以是vectordequelist 都是可以的,主要就是数组和链表的底层实现。

我们常用的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();}
};
  • 时间复杂度: pushempty为O(1), poppeek为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题解