目录
有效的括号
题干
题目:给定一个只包括 ' ( ',' ) ',' { ',' } ',' [ ',' ] ' 的字符串,判断字符串是否有效。
有效字符串需满足:
-
左括号必须用相同类型的右括号闭合。
-
左括号必须以正确的顺序闭合。
-
注意空字符串可被认为是有效字符串。
思路
括号匹配问题是栈的经典应用。
方法一 左括号先入栈:当遇到左括号则压栈,遇到右括号立即查询栈中是否有匹配的左括号有则弹出左括号。如果最后栈空和字符串都遍历完毕则说明匹配成功;如果栈空而字符串还没遍历完说明左括号缺失;如果栈非空而字符串遍历完毕说明右括号缺失;如果左右括号不匹配也是直接return false。
方法二 右括号先入栈:当遍历到左括号时,让和左括号匹配的右括号压栈,这样当遍历到右括号时,只需查询右括号和栈顶元素是否相等即可。如果不相等就说明和之前的左括号并不匹配。而左括号或右括号缺失的情况和方法一相同。
代码
方法一:左括号先入栈
class Solution {
public:
bool isValid(string s) {
stack<char> brackets;
for (char c : s) {
// 遇到左括号压栈
if (c == '(' || c == '[' || c == '{'){
brackets.push(c);
} else{
// 遇到右括号看栈是否为空
// 若栈空说明左括号缺失
if (brackets.empty()){
return false;
} else{
// 若栈非空查看是否匹配
if ((c == ')' && brackets.top() == '(') ||
(c == ']' && brackets.top() == '[') ||
(c == '}' && brackets.top() == '{') ){
// 若匹配则弹栈
brackets.pop();
} else{
// 不匹配直接return
return false;
}
}
}
}
// 最后栈空说明全部匹配完成
if (brackets.empty()){
return true;
} else{
// 栈非空说明右括号缺失
return false;
}
}
};
方法二:右括号先入栈
public:
bool isValid(string s) {
// 要使括号匹配,字符串的长度肯定是2的倍数,即偶数,如果是奇数肯定不匹配
if (s.size()%2 != 0){
return false;
}
stack<char> brackets;
for (char c : s) {
if (c == '('){
brackets.push(')');
} else if (c == '['){
brackets.push(']');
} else if ( c == '{'){
brackets.push('}');
} else{
// 当栈为空 或者 栈顶元素和当前右括号不相等,说明不匹配
if (brackets.empty() || c!=brackets.top()){
return false;
}
// 当栈顶元素和当前右括号相等,说明匹配,弹栈
brackets.pop();
}
}
return brackets.empty();
}
};
删除字符串中的所有相邻重复项
题干
题目:给出由小写字母组成的字符串 S,重复项删除操作会选择两个相邻且相同的字母,并删除它们。在 S 上反复执行重复项删除操作,直到无法继续删除。在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。
示例:
-
输入:"abbaca"
-
输出:"ca"
-
解释:例如,在 "abbaca" 中,我们可以删除 "bb" 由于两字母相邻且相同,这是此时唯一可以执行删除操作的重复项。之后我们得到字符串 "aaca",其中又只有 "aa" 可以执行重复项删除操作,所以最后的字符串为 "ca"。
思路
方法一使用栈空间:遍历字符串,当遇到和之前不同的字母时压栈,当遇到相同字母时弹栈,这样最后栈中剩余的元素就是删除后的结果。但由于栈是先进后出,所以将栈中剩余的元素存储到结果字符串时元素是颠倒的,最后只需反转字符串即可。时间复杂度O(n),空间复杂度O(n)。
方法二用新的字符串当作栈:思路和方法一相同,只不过使用一个新的空字符串来作为栈使用,就不需要后续将栈转为字符串了。时间复杂度O(n),空间复杂度O(1)。
代码
方法一:使用栈空间
class Solution {
public:
string removeDuplicates(string s) {
stack<char> characters;
for (char c : s) {
if (characters.empty() || characters.top() != c){
characters.push(c);
} else{
// 栈顶元素和当前字母相同
characters.pop();
}
}
if (characters.empty()){
return "";
}
s.clear();
// 把栈中剩余的元素都弹出到字符串中
while (!characters.empty()){
s.push_back(characters.top());
characters.pop();
}
// 最后反转字符串
reverse(s.begin(),s.end());
return s;
}
};
方法二:使用字符串当栈
class Solution {
public:
string removeDuplicates(string s) {
string characters = "";
for (char c : s) {
if (characters.empty() || characters.back() != c){
// 当元素不同,存入字符串
characters.push_back(c);
} else{
// 碰到相同元素,弹出字符串最后一个元素。
characters.pop_back();
}
}
return characters;
}
};
逆波兰表达式求值
逆波兰表达式
逆波兰表达式是一种后缀表达式,所谓后缀就是指算符写在后面。
-
平常使用的算式是一种中缀表达式,如
( 1 + 2 ) * ( 3 + 4 )
。 -
该算式的逆波兰表达式写法为
( ( 1 2 + ) ( 3 4 + ) * )
。
逆波兰表达式主要有以下两个优点:
-
去掉括号后表达式无歧义,上式即便写成
1 2 + 3 4 + *
也可以依据次序计算出正确结果。 -
适合用栈操作运算:遇到数字则入栈;遇到算符则取出栈顶两个数字进行计算,并将结果压入栈中
题干
题目:给你一个字符串数组 tokens ,存储着根据 逆波兰表示法 表示的算术表达式。根据 逆波兰表示法,求该表达式的值。有效的运算符包括 + , - , * , / 。每个运算对象可以是整数,也可以是另一个逆波兰表达式。
说明:整数除法只保留整数部分。 给定逆波兰表达式总是有效的。换句话说,表达式总会得出有效数值且不存在除数为 0 的情况。
思路
遍历字符串,遇到数字,则将数字字符串转化为整数压入栈,遇到运算符则取出栈顶两个数字进行计算,并将中间结果压入栈中,遍历完字符串,栈中最后只剩下一个元素即运算结果。
代码
class Solution {
public:
// 定义运算
int operation(string s, int a, int b){
if (s == "+"){
return a+b;
} else if (s == "-"){
return a-b;
} else if (s == "*"){
return a*b;
} else{
return a/b;
}
}
// 求逆波兰表达式的值
int evalRPN(vector<string>& tokens) {
stack<int> num; // 存储操作数和中间运算的结果
for (int i = 0; i < tokens.size(); ++i) {
string s = tokens[i];
if (s == "+" || s == "-" || s == "*" || s == "/"){
// 弹出两个操作数,b是第二个操作数
int b = num.top();
num.pop();
// a 是第一个操作数
int a = num.top();
num.pop();
// 将中间的运算结果压回栈中
num.push(operation(s,a,b));
}else{
// 碰到操作数,将字符串转化为整数压入栈中
num.push(stoi(s));
}
}
int result = num.top(); // 最后栈中剩余的一个元素就是运算结果
return result;
}
};
滑动窗口最大值
题干
题目:给定一个数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。将每次滑动窗口中的最大值存储在数组中返回。
进阶:你能在线性时间复杂度内解决此题吗?
思路
方法一暴力解法:使用两个for循环,一个for循环遍历数组滑动窗口,一个for循环遍历窗口找到最大值,时间复杂度O(n*k),其中n为数组长度,k为窗口长度,超出时间限制,就不放代码了。
方法二使用队列:因为每次窗口向右移动一位,则窗口最左边的元素都会被弹出,符合队列先进先出的原则,因此可以用队列存储窗口中的值。同时为了找到每次窗口的最大值,可以让元素从大到小排列在队列中,最大值即队头元素,但是我们滑动窗口时还需要把窗口要移除的元素弹出,那如何在排过序的队列中找到这个要移除的元素呢?此时就出现了矛盾,因此我们需要思考以下这个问题。
队列是否有必要维护窗口所有的元素?
其实队列没有必要维护窗口里的所有元素,我们要找的是窗口最大值,那么只需要维护有可能成为窗口最大值的元素,并且保证队列里的元素是降序排列即可。因此在遍历数组时,假如遍历到的值为value,value如果比队尾元素小就直接插入队列;而如果比队尾元素大,我们就把队列里所有比 value 值小的元素弹出,因为这些比 value 小的元素都不可能成为最大值。
代码
class Solution {
private:
// 自定义队列
class MyQueue{
public:
deque<int> que;
void pop(int value) {
// 当要弹出的元素为队头元素时,才需要弹出
// 如果要弹出的元素不是队头元素,说明这个元素比队头元素要小,在之前push队头元素时就已经被弹出了
if (!que.empty() && value == que.front()){
que.pop_front();
}
}
void push(int value) {
while (!que.empty() && value > que.back()){
// 从队尾弹出所有比 value 小的元素,让队列里只剩下比 value 大的
// 保持队列的降序排列
que.pop_back();
}
que.push_back(value);
}
int front() {
return que.front();
}
};
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
// 滑动窗口大小为 1,则返回原数组
if (k == 1){
return nums;
}
MyQueue window; // 队列存储窗口可能的最大值
vector<int> result;// 存储每个窗口的最大值
// 先存储前 k 个元素
for (int i = 0; i < k; ++i) {
window.push(nums[i]);
}
result.push_back(window.front()); // 存储第一个窗口的最大值
// 遍历接下来的窗口
for (int i = k; i < nums.size(); ++i) {
// i 指向的是每次右移窗口后要插入的新元素的位置
// 要准备弹出 nums[i-k] 这个元素,插入 nums[i]
window.pop(nums[i-k]);
window.push(nums[i]);
// 储存当前窗口的最大值
result.push_back(window.front());
}
return result;
}
};