Leetcode记录:栈和队列

一些理论基础

  1. C++中stack是容器么?
  2. 我们使用的stack是属于哪个版本的STL?
  3. 我们使用的STL中stack是如何实现的?
  4. stack 提供迭代器来遍历stack空间么?

栈和队列是STL(C++标准库)里面的两个数据结构。C++标准库是有多个版本的,要知道我们使用的STL是哪个版本,才能知道对应的栈和队列的实现原理。三个最为普遍的STL版本:

  1. HP STL 其他版本的C++ STL,一般是以HP STL为蓝本实现出来的,HP STL是C++ STL的第一个实现版本,而且开放源代码。
  2. P.J.Plauger STL 由P.J.Plauger参照HP STL实现出来的,被Visual C++编译器所采用,不是开源的。
  3. SGI STL 由Silicon Graphics Computer Systems公司参照HP STL实现,被Linux的C++编译器GCC所采用,SGI STL是开源软件,源码可读性甚高。

接下来介绍的栈和队列也是SGI STL里面的数据结构, 知道了使用版本,才知道对应的底层实现。

栈(stack) 提供 push 和 pop 等接口,所有元素必须符合先进后出规则,所以栈不提供走访功能,也不提供迭代器(iterator)。 不像是set 或者map 提供迭代器iterator来遍历所有元素。栈是以底层容器完成其所有的工作,对外提供统一的接口,底层容器是可插拔的(也就是说我们可以控制使用哪种容器来实现栈的功能)。所以STL中栈往往不被归类为容器,而被归类为container adapter(容器适配器)。

那么问题来了,STL 中栈是用什么容器实现的?从下图中可以看出,栈的内部结构,栈的底层实现可以是vector,deque,list,只要支持back,push_back,pop_back都是可以的, 主要就是数组和链表的底层实现。

我们常用的SGI STL,如果没有指定底层实现的话,默认是以deque为缺省情况下栈的底层结构。deque是一个双向队列,只要封住一段,只开通另一端就可以实现栈的逻辑了。SGI STL中 队列底层实现缺省情况下一样使用deque实现的。我们也可以指定vector为栈的底层实现,初始化语句如下:

std::stack<int, std::vector<int> > third;  // 使用vector为底层容器的栈

队列中先进先出的数据结构,同样不允许有遍历行为,不提供迭代器, SGI STL中队列一样是以deque为缺省情况下的底部结构,底部结构需支持front,pop_front和push_back。也可以指定list 为起底层实现,初始化queue的语句如下:

std::queue<int, std::list<int>> third; // 定义以list为底层容器的队列

priority_queue也是容器适配器,同样也不支持iterator。它类似于最大堆(max heap),给定某个严格弱排序后。它的top永远是最大的,数组内部是从小到大排列的,它能pop最大的元素是因为它使用的底层函数是pop_back()。

deque和queue支持front和back,stack和priority_queue支持top。

用栈实现队列

在push数据的时候,只要数据放进输入栈就好,但在pop的时候,操作就复杂一些,输出栈如果为空,就把进栈数据全部导入进来(注意是全部导入),再从出栈弹出数据,如果输出栈不为空,则直接从出栈弹出数据就可以了。

class MyQueue {
public:
    stack<int> stIn;
    stack<int> stOut;
    /** Initialize your data structure here. */
    MyQueue() {

    }
    /** Push element x to the back of queue. */
    void push(int x) {
        stIn.push(x);
    }

    /** Removes the element from in front of queue and returns that element. */
    int pop() {
        // 只有当stOut为空的时候,再从stIn里导入数据(导入stIn全部数据)
        if (stOut.empty()) {
            // 从stIn导入数据直到stIn为空
            while(!stIn.empty()) {
                stOut.push(stIn.top());
                stIn.pop();
            }
        }
        int result = stOut.top();
        stOut.pop();
        return result;
    }

    /** Get the front element. */
    int peek() {
        int res = this->pop(); // 直接使用已有的pop函数
        stOut.push(res); // 因为pop函数弹出了元素res,所以再添加回去
        return res;
    }

    /** Returns whether the queue is empty. */
    bool empty() {
        return stIn.empty() && stOut.empty();
    }
};

用队列实现栈

入栈操作时,首先获得入栈前的元素个数 n,然后将元素入队到队列,再将队列中的前 n 个元素(即除了新入栈的元素之外的全部元素)依次出队并入队到队列,此时队列的前端的元素即为新入栈的元素,且队列的前端和后端分别对应栈顶和栈底。

class MyStack {
public:
    queue<int> q;

    /** Initialize your data structure here. */
    MyStack() {

    }

    /** Push element x onto stack. */
    void push(int x) {
        int n = q.size();
        q.push(x);
        for (int i = 0; i < n; i++) {
            q.push(q.front());
            q.pop();
        }
    }
    
    /** Removes the element on top of the stack and returns that element. */
    int pop() {
        int r = q.front();
        q.pop();
        return r;
    }
    
    /** Get the top element. */
    int top() {
        int r = q.front();
        return r;
    }
    
    /** Returns whether the stack is empty. */
    bool empty() {
        return q.empty();
    }
};

有效的括号

Leetcode 20. 给定一个只包括 ‘(‘,’)’,’{‘,’}’,’[‘,’]’ 的字符串 s ,判断字符串是否有效。

bool isValid(string s) {
    if (s.size() % 2 != 0) return false; // 如果s的长度为奇数,一定不符合要求
    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(']');
        // 第三种情况:遍历字符串匹配的过程中,栈已经为空了,没有匹配的字符了,说明右括号没有找到对应的左括号 return false
        // 第二种情况:遍历字符串匹配的过程中,发现栈里没有我们要匹配的字符。所以return false
        else if (st.empty() || st.top() != s[i]) return false;
        else st.pop(); // st.top() 与 s[i]相等,栈弹出元素
    }
    // 第一种情况:此时我们已经遍历完了字符串,但是栈不为空,说明有相应的左括号没有右括号来匹配,所以return false,否则就return true
    return st.empty();
}

删除字符串中的所有相邻重复项

Leetcode 1047. 给出由小写字母组成的字符串 S,重复项删除操作会选择两个相邻且相同的字母,并删除它们。在 S 上反复执行重复项删除操作,直到无法继续删除。在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。

匹配问题都是栈的强项

用栈存放即可:

string removeDuplicates(string S) {
    stack<char> st;
    for (char s : S) {
        if (st.empty() || s != st.top()) {
            st.push(s);
        } else {
            st.pop(); // s 与 st.top()相等的情况
        }
    }
    string result = "";
    while (!st.empty()) { // 将栈中元素放到result字符串汇总
        result += st.top();
        st.pop();
    }
    reverse (result.begin(), result.end()); // 此时字符串需要反转一下
    return result;

}

中缀表达式转换为后缀表达式

中缀表达式转换为后缀表达式(逆波兰表达式)的过程可以通过使用一个操作符栈和输出队列来实现。转换过程如下:

  1. 初始化:创建一个空的操作符栈和一个空的输出队列。
  2. 从左到右扫描中缀表达式的每个符号:
    1. 如果当前符号是操作数(数字或变量),则将其直接添加到输出队列中。
    2. 如果当前符号是左括号 (,则将其压入操作符栈。
    3. 如果当前符号是右括号 ),则将栈顶的操作符弹出并添加到输出队列中,直到遇到左括号为止。此时,将左括号从栈中弹出并丢弃。
    4. 如果当前符号是操作符(如 +, -, *, / 等),则:
      1. 如果操作符栈为空,或者栈顶为左括号 (,则直接将当前操作符压入栈。
      2. 如果当前操作符的优先级高于栈顶操作符的优先级,也将当前操作符压入栈。
      3. 否则,将栈顶操作符弹出并添加到输出队列中,重复上述步骤,直到当前操作符可以压入栈为止。
  3. 表达式扫描完成后:将栈中剩余的所有操作符依次弹出并添加到输出队列中。
#include <iostream>
#include <stack>
#include <string>
#include <cctype>

using namespace std;

// 判断操作符优先级
int precedence(char op) {
    if(op == '+' || op == '-') 
        return 1;
    if(op == '*' || op == '/')
        return 2;
    if(op == '^')
        return 3;
    return 0;
}

// 判断是否为操作符
bool isOperator(char c) {
    return (c == '+' || c == '-' || c == '*' || c == '/' || c == '^');
}

// 中缀表达式转后缀表达式
string infixToPostfix(string infix) {
    stack<char> s;
    string postfix = "";
    
    for(char& c : infix) {
        if(isdigit(c) || isalpha(c)) {
            postfix += c;  // 如果是操作数,直接添加到后缀表达式中
        } else if(c == '(') {
            s.push(c);  // 左括号直接压栈
        } else if(c == ')') {
            // 右括号,弹出直到遇到左括号
            while(!s.empty() && s.top() != '(') {
                postfix += s.top();
                s.pop();
            }
            s.pop(); // 弹出左括号
        } else if(isOperator(c)) {
            // 操作符,考虑优先级
            while(!s.empty() && precedence(s.top()) >= precedence(c)) {
                postfix += s.top();
                s.pop();
            }
            s.push(c);
        }
    }
    
    // 将剩余的操作符全部弹出
    while(!s.empty()) {
        postfix += s.top();
        s.pop();
    }
    
    return postfix;
}

逆波兰表达式(后缀表达式)

Leetcode 150. 给你一个字符串数组 tokens ,表示一个根据 逆波兰表示法 表示的算术表达式。请你计算该表达式。返回一个表示表达式值的整数。

和删除重复项问题十分类似,使用栈即可。

int evalRPN(vector<string>& tokens) {
    stack<long long> st; 
    for (int i = 0; i < tokens.size(); i++) {
        if (tokens[i] == "+" || tokens[i] == "-" || tokens[i] == "*" || tokens[i] == "/") {
            long long num1 = st.top();
            st.pop();
            long long num2 = st.top();
            st.pop();
            if (tokens[i] == "+") st.push(num2 + num1);
            if (tokens[i] == "-") st.push(num2 - num1);
            if (tokens[i] == "*") st.push(num2 * num1);
            if (tokens[i] == "/") st.push(num2 / num1);
        } else {
            st.push(stoll(tokens[i]));
        }
    }

    int result = st.top();
    st.pop(); // 把栈里最后一个元素弹出(其实不弹出也没事)
    return result;
}

前k个高频元素

Leetcode 347. 给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。

直接使用priority_queue即可,它的实现类似于堆排序,每次返回的是最大的元素。

vector<int> topKFrequent(vector<int>& nums, int k) {
    unordered_map<int,int> mp;
    priority_queue<pair<int,int>> pq;
    for(auto num : nums)
        mp[num]++;
    for(auto it = mp.begin(); it !=mp.end(); ++it){
        pq.emplace(it->second, it->first);
    }
    vector<int> res;
    for(int i = 0; i < k; ++i){
        res.push_back(pq.top().second);
        pq.pop();
    }
    return res;
}