Leetcode记录:栈和队列
一些理论基础
- C++中stack是容器么?
- 我们使用的stack是属于哪个版本的STL?
- 我们使用的STL中stack是如何实现的?
- stack 提供迭代器来遍历stack空间么?
栈和队列是STL(C++标准库)里面的两个数据结构。C++标准库是有多个版本的,要知道我们使用的STL是哪个版本,才能知道对应的栈和队列的实现原理。三个最为普遍的STL版本:
- HP STL 其他版本的C++ STL,一般是以HP STL为蓝本实现出来的,HP STL是C++ STL的第一个实现版本,而且开放源代码。
- P.J.Plauger STL 由P.J.Plauger参照HP STL实现出来的,被Visual C++编译器所采用,不是开源的。
- 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;
}
中缀表达式转换为后缀表达式
中缀表达式转换为后缀表达式(逆波兰表达式)的过程可以通过使用一个操作符栈和输出队列来实现。转换过程如下:
- 初始化:创建一个空的操作符栈和一个空的输出队列。
- 从左到右扫描中缀表达式的每个符号:
- 如果当前符号是操作数(数字或变量),则将其直接添加到输出队列中。
- 如果当前符号是左括号 (,则将其压入操作符栈。
- 如果当前符号是右括号 ),则将栈顶的操作符弹出并添加到输出队列中,直到遇到左括号为止。此时,将左括号从栈中弹出并丢弃。
- 如果当前符号是操作符(如 +, -, *, / 等),则:
- 如果操作符栈为空,或者栈顶为左括号 (,则直接将当前操作符压入栈。
- 如果当前操作符的优先级高于栈顶操作符的优先级,也将当前操作符压入栈。
- 否则,将栈顶操作符弹出并添加到输出队列中,重复上述步骤,直到当前操作符可以压入栈为止。
- 表达式扫描完成后:将栈中剩余的所有操作符依次弹出并添加到输出队列中。
#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;
}