目录
- LeetCode #20:Valid Parentheses 有效的括号
- LeetCode #150:Evaluate Reverse Polish Notation 逆波兰表达式求值
- LeetCode #232:Implement Queue using Stacks 用栈实现队列
- LeetCode #225:Implement Stack using Queues 用队列实现栈
- LeetCode #239 :Sliding Window Maximum 滑动窗口最大值
- LeetCode #1047:Remove All Adjacent Duplicates in String 删除字符串中的所有相邻重复项
本系列文章仅是 GitHub 大神 @halfrost 的刷题笔记 《LeetCode Cookbook》的提纲以及示例、题集的 C++转化。原书请自行下载学习。
本篇文章涉及新手应该优先刷的几道经典栈和队列算法题,以后会更新“二刷”“三刷”等等。
LeetCode #20:Valid Parentheses 有效的括号
给定一个只包括 '('
,')'
,'{'
,'}'
,'['
,']'
的字符串 s
,判断字符串是否有效。
有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
- 每个右括号都有一个对应的相同类型的左括号。
括号匹配是使用栈解决的经典问题。我们可以维护一个栈,遍历字符串,遇到左括号就入栈,遇到右括号则取出栈顶元素,判断栈顶元素与右括号是否匹配。
其中,有两种特殊情况也会返回 false
:
- 遍历完字符串,栈不为空,说明左括号多了。
- 遇到右括号,但此时栈为空,说明右括号多了。
class Solution {
public:
bool isValid(string s) {
//字符串有奇数个元素,必然不匹配
if (s.size() % 2 == 1) return false;
//初始化栈
stack<char> stk;
//括号对映射
unordered_map<char, char> pairs = {
{')', '('},
{']', '['},
{'}', '{'}
};
for (char c : s) {
//如果是左括号,压入栈
if (pairs.find(c) == pairs.end()) stk.push(c);
//在遍历过程中,碰到空栈或者括号不匹配,返回 false
else if (stk.empty() || pairs[c] != stk.top()) return false;
//如果是右括号,并且与栈顶左括号匹配,弹出栈顶元素
else stk.pop();
}
//全部遍历完,如果栈为空,返回 true,栈不为空,返回 false
return stk.empty();
}
};
LeetCode #150:Evaluate Reverse Polish Notation 逆波兰表达式求值
根据逆波兰表示法,求表达式的值。
有效算符包括 +
-
*
/
。运算对象可以是整数,可以是其他逆波兰表达式。
逆波兰表达式发明出来就是为了方便计算机运用「栈」进行表达式运算(计算机普遍采用栈式结构,执行的是先入后出的操作)的,其运算规则如下:
- 按顺序遍历逆波兰表达式中的字符,如果是数字,则放入栈。
- 如果是运算符,则将栈顶的两个元素(先出栈的数在运算符右侧,后出栈的在运算符左侧)拿出来进行运算,再将结果放入栈。
- 对于减法和除法,栈顶第二个数是被除(减)数。
试举一例,a + b
的逆波兰表达式为 a b +
。而逆波兰表达式有以下优点:
- 去掉括号后表达式无歧义,
(1 + 2) * (3 + 4)
即便写成1 2 + 3 4 + *
也可以依据次序计算出正确结果。 - 适合用栈操作运算。
class Solution {
public:
int evalRPN(vector<string>& tokens) {
//初始化栈
stack<int> stk;
unordered_map<string, function<int(int, int)>> ops = {
{"+", [](int a, int b) { return a + b; }},
{"-", [](int a, int b) { return a - b; }},
{"*", [](int a, int b) { return a * b; }},
{"/", [](int a, int b) { return b != 0 ? a / b : throw runtime_error("Division by zero"); }}
};
for (const string& token : tokens) {
//如果当前 token 不是操作符,转换为整数并入栈
if (token != "+" && token != "-" && token != "*" && token != "/") stk.push(stoi(token));
else { //如果当前token是操作符
//先出栈的数在操作符右侧
int right = stk.top(); stk.pop();
//后出栈的数在操作符左侧
int left = stk.top(); stk.pop();
//进行相应运算,运算完的数值入栈
stk.push(ops[token](left, right)); //经典的条件判断语句效率更高,可采用
}
}
return stk.top();
}
};
LeetCode #232:Implement Queue using Stacks 用栈实现队列
仅使用两个栈实现先入先出队列,队列支持一般队列支持的所有操作:
void push(int x)
:将元素x
推到队列的末尾。int pop()
:从队列的开头移除并返回元素。int peek()
:返回队列开头的元素。boolean empty()
:如果队列为空,返回true
;否则,返回false
。
队列是一种先入先出(FIFO)的数据结构,而栈是一种后入先出(LIFO)的数据结构,一个栈绝对满足不了队列的 FIFO 特性。因此,我们需要两个栈——输入栈 s1
和输出栈 s2
,输入栈用以反转元素的入队顺序,元素只能从输入栈进入( push
);输出栈用以存储元素的正常顺序,元素只能从输出栈出来(pop
、peek
)。
当调用 push()
让元素入队时,只要把元素压入栈 s1
即可;当调用 pop()
让元素出队时,若输出栈 s2
不为空,直接弹出栈顶元素,若 s2
为空,则把输入栈 s1
的所有元素压入输出栈 s2
中,再弹出 s2
栈顶元素。
其余的函数调用较为简单,empty()
判断队列是否为空,如果两个栈都为空则队列为空,反之亦然;peek()
方法则与 pop()
思路一致,只要查看 s2
栈顶元素即可。
class MyQueue {
private:
stack<int> s1, s2;
public:
MyQueue() {
}
void push(int x) {
s1.push(x);
}
int pop() {
//保证 s2 非空
peek();
int poppedValue = s2.top();
s2.pop();
return poppedValue;
}
int peek() {
if (s2.empty())
while (!s1.empty()) {
//把 s1 元素压入 s2
s2.push(s1.top());
s1.pop();
}
return s2.top();
}
bool empty() {
return s1.empty() && s2.empty();
}
};
LeetCode #225:Implement Stack using Queues 用队列实现栈
用队列实现栈的下列操作:
void push(int x)
:元素x
入栈。int pop()
:移除并返回栈顶元素。int peek()
:返回栈顶元素。boolean empty()
:栈为空,返回true
,否则返回false
。
建一个主队列和一个辅助队列,每次入栈操作时,将新元素添加到辅助队列,再依次将主队列的元素出队列,依次加入辅助队列,最后将主队列与辅助队列互换。
class MyStack {
private:
queue<int> q1; //主队列,模拟栈的行为
queue<int> q2; //辅助队列
public:
MyStack() {
}
void push(int x) {
q2.push(x); //先将新元素推入辅助队列
while (!q1.empty()) {
q2.push(q1.front()); //将主队列的所有元素移到辅助队列
q1.pop();
}
//交换两个队列
queue<int> temp = q1;
q1 = q2;
q2 = temp;
}
int pop() {
int topElement = q1.front(); //获取栈顶元素
q1.pop(); //移除栈顶元素
return topElement;
}
int top() {
return q1.front(); //返回栈顶元素,但不移除它
}
bool empty() {
return q1.empty(); //如果主队列为空,则栈为空
}
};
LeetCode #239 :Sliding Window Maximum 滑动窗口最大值
大小为 k
的滑动窗口从整数数组 nums
的最左侧移到最右侧,只能看到滑动窗口中的 k
个数字,窗口每次向右移动一位。
返回滑动窗口的最大值。
这是用队列解决的经典滑动窗口问题。一般的队列是限制仅在队尾插入、在队首进行删除操作的线性表,而双端队列则是放开了这个限制,在两端都可以进行入队和出队操作,类似于栈和队列的结合体。存在两种受限的双端队列,一种是输出受限的双端队列,允许在一端进行入队和出队,但在另一端只允许入队;另一种则是输入受限的双端队列,允许在一端进行入队和出队,但在另一端只允许出队。
进一步地,输出受限的双端队列中有一种特殊的形式——队列里的各元素之间的关系具有单调性,即单调队列,也是解决本问题的关键。单调队列这一数据结构,既能够维护队列元素先进先出的时间顺序,又能够正确维护队列中所有元素的最值。
我们维护一个单调递减的单调队列(队列只存储数组对应的索引值),较大的元素必然比较小的元素晚弹出队列,从而只在队列中保留可能成为窗口最大元素的元素,去掉不可能成为窗口中最大元素的元素。
首先,初始化一个单调队列和结果数组,并注意参数 k
不合法的情况:
if (nums.empty() || k <= 0 || k > nums.size()) return {};
deque<int> dq; //双端队列,保存当前窗口最大值的数组位置
vector<int> res(nums.size() - k + 1, 0); //结果数组,初始化大小为nums.size() - k + 1
对于每一个新进入的元素,只要大于等于先前的队尾元素,队尾元素就直接弹出,不予考虑,直到把所有小于等于新元素的队列元素全部弹出;同时新元素加入队列:
//保证队列从大到小,如果队尾元素小于等于当前元素,则弹出队尾
while (!dq.empty() && nums[dq.back()] <= nums[i]) dq.pop_back();
dq.push_back(i); //添加当前值的索引到队列
维护队列的单调性,确保队首是当前窗口中的最大值,并弹出超出当前窗口索引的队首元素:
if (dq.front() <= i - k) dq.pop_front(); // i - k + 1 是当前窗口的起始索引
窗口的滑动是通过循环的迭代和队列的更新来隐式地实现的,i
的每次迭代都会使得窗口右移一格。遍历完整个窗口后记录最大值到结果数组 res
中,在下次迭代中滑动窗口:
if (i >= k - 1) res[i - k + 1] = nums[dq.front()];
最终的代码实现如下:
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
if (nums.empty() || k <= 0 || k > nums.size()) return {};
deque<int> dq; //双端队列,保存当前窗口最大值的数组位置
vector<int> res(nums.size() - k + 1, 0); //结果数组,初始化大小为nums.size() - k + 1
for (int i = 0; i < nums.size(); ++i) {
//保证队列从大到小,如果队尾元素小于等于当前元素,则弹出队尾
while (!dq.empty() && nums[dq.back()] <= nums[i]) dq.pop_back();
dq.push_back(i); //添加当前值的索引到队列
//如果队列首部的索引已经超出窗口范围,则弹出
if (dq.front() <= i - k) dq.pop_front();
//当窗口大小达到 k 时,记录当前窗口的最大值
if (i >= k - 1) res[i - k + 1] = nums[dq.front()];
}
return res;
}
};
LeetCode #1047:Remove All Adjacent Duplicates in String 删除字符串中的所有相邻重复项
小写字母组成字符串 s
,重复项删除操作会选择两个相邻且相同的字母,并删除它们。
在 s
上反复执行重复项删除操作,直到无法继续删除。
维护一个栈,从左到右依次扫描字符串,比较当前字符与栈顶元素:
- 若相同,则栈顶元素出栈,当前字符不入栈。
- 若不同,则当前字符入栈。
为了方便处理字符串,我们直接使用 C++ 中的 string
类来模拟栈,push_back()
方法添加字符到字符串的末尾(相当于入栈), pop_back()
方法移除字符串末尾的字符(相当于出栈),empty()
方法检查字符串(栈)是否为空,以及 back()
方法访问字符串(栈)的最后一个元素(栈顶元素)。
class Solution {
public:
string removeDuplicates(string s) {
string res; //使用 string 作为栈
for (char c : s) {
//当栈不为空且栈顶元素等于当前字符时,弹出栈顶元素
if (!res.empty() && res.back() == c) res.pop_back();
else res.push_back(c); //否则,将当前字符入栈
}
return res;
}
};