给定一个只包括 '('
,')'
,'{'
,'}'
,'['
,']'
的字符串 s
,判断字符串是否有效。有效字符串需满足:
左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
每个右括号都有一个对应的相同类型的左括号。
判断括号的有效性可以使用「栈」这一数据结构来解决。我们遍历给定的字符串 s。当我们遇到一个左括号时,我们会期望在后续的遍历中,有一个相同类型的右括号将其闭合。由于后遇到的左括号要先闭合,因此我们可以将这个左括号放入栈顶。
当我们遇到一个右括号时,我们需要将一个相同类型的左括号闭合。此时,我们可以取出栈顶的左括号并判断它们是否是相同类型的括号。如果不是相同的类型,或者栈中并没有左括号,那么字符串 s 无效,返回 False。为了快速判断括号的类型,我们可以使用哈希表存储每一种括号。哈希表的键为右括号,值为相同类型的左括号。
在遍历结束后,如果栈中没有左括号,说明我们将字符串 s 中的所有左括号闭合,返回 True,否则返回 False。注意到有效字符串的长度一定为偶数,因此如果字符串的长度为奇数,我们可以直接返回 False,省去后续的遍历判断过程。
class Solution {
public:
bool isValid(string s) {
int len_s = s.size();
if(len_s%2==1){
return false;
}
stack<char> char_stack;
unordered_map<char,char> pairs={
{')','('},
{']','['},
{'}','{'}
};
for(int i=0;i<len_s;i++){
if(pairs.find(s[i])!=pairs.end()){
if(char_stack.empty() || char_stack.top()!=pairs[s[i]]){
return false;
}
char_stack.pop();
}else{
char_stack.push(s[i]);
}
}
return char_stack.empty();
}
};
题意其实就像我们在写代码的过程中,要求括号的顺序是一样的,有左括号,相应的位置必须要有右括号。如果还记得编译原理的话,编译器在 词法分析的过程中处理括号、花括号等这个符号的逻辑,也是使用了栈这种数据结构。再举个例子,linux系统中,cd这个进入目录的命令我们应该再熟悉不过了。这个命令最后进入a目录,系统是如何知道进入了a目录呢 ,这就是栈的应用。
由于栈结构的特殊性,非常适合做对称匹配类的题目。首先要弄清楚,字符串里的括号不匹配有几种情况。建议在写代码之前要分析好有哪几种不匹配的情况,如果不在动手之前分析好,写出的代码也会有很多问题。先来分析一下 这里有三种不匹配的情况,
第一种情况,字符串里左方向的括号多余了 ,所以不匹配。
第二种情况,括号没有多余,但是 括号的类型没有匹配上。
第三种情况,字符串里右方向的括号多余了,所以不匹配。
对应三种情况的解法:
第一种情况:已经遍历完了字符串,但是栈不为空,说明有相应的左括号没有右括号来匹配,所以return false
第二种情况:遍历字符串匹配的过程中,发现栈里没有要匹配的字符。所以return false
第三种情况:遍历字符串匹配的过程中,栈已经为空了,没有匹配的字符了,说明右括号没有找到对应的左括号return false
那么什么时候说明左括号和右括号全都匹配了呢,就是字符串遍历完之后,栈是空的,就说明全都匹配了。还有一些技巧,在匹配左括号的时候,右括号先入栈,就只需要比较当前元素和栈顶相不相等就可以了,比左括号先入栈代码实现要简单的多了!
class Solution {
public:
bool isValid(string s) {
int len_s = s.size();
if(len_s%2==1){
return false;
}
stack<char> char_stack;
// unordered_map pairs={
// {')','('},
// {']','['},
// {'}','{'}
// };
// for(int i=0;i
// if(pairs.find(s[i])!=pairs.end()){
// if(char_stack.empty() || char_stack.top()!=pairs[s[i]]){
// return false;
// }
// char_stack.pop();
// }else{
// char_stack.push(s[i]);
// }
// }
// return char_stack.empty();
for(int i=0;i<len_s;i++){
if(s[i]=='('){
char_stack.push(')');
}else if(s[i]=='{'){
char_stack.push('}');
}else if(s[i]=='['){
char_stack.push(']');
}else if(char_stack.empty() || char_stack.top()!=s[i]){
return false;
}else{
char_stack.pop();
}
}
return char_stack.empty();
}
};
S
,重复项删除操作会选择两个相邻且相同的字母,并删除它们。在 S 上反复执行重复项删除操作,直到无法继续删除。在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。充分理解题意后,我们可以发现,当字符串中同时有多组相邻重复项时,我们无论是先删除哪一个,都不会影响最终的结果。因此我们可以从左向右顺次处理该字符串。
而消除一对相邻重复项可能会导致新的相邻重复项出现,如从字符串 abba 中删除 bb 会导致出现新的相邻重复项 aa 出现。因此我们需要保存当前还未被删除的字符。一种显而易见的数据结构呼之欲出:栈。我们只需要遍历该字符串,如果当前字符和栈顶字符相同,我们就贪心地将其消去,否则就将其入栈即可。
C++ 代码中,由于 std::string 类本身就提供了类似「入栈」和「出栈」的接口,因此我们直接将需要被返回的字符串作为栈即可。对于其他的语言,如果字符串类没有提供相应的接口,则需要在遍历完成字符串后,使用栈中的字符显式地构造出需要被返回的字符串。
class Solution {
public:
string removeDuplicates(string s) {
string char_stack;
for(int index=0;index<s.size();index++){
if(!char_stack.empty()&&s[index]==char_stack.back()){
char_stack.pop_back();
}else{
char_stack.push_back(s[index]);
}
}
return char_stack;
}
};
时间复杂度:O(n),其中 n 是字符串的长度。我们只需要遍历该字符串一次。空间复杂度:O(n) 或 O(1),取决于使用的语言提供的字符串类是否提供了类似「入栈」和「出栈」的接口。注意返回值不计入空间复杂度。
我们在删除相邻重复项的时候,其实就是要知道当前遍历的这个元素,我们在前一位是不是遍历过一样数值的元素,那么如何记录前面遍历过的元素呢?所以就是用栈来存放,那么栈的目的,就是存放遍历过的元素,当遍历当前的这个元素的时候,去栈里看一下我们是不是遍历过相同数值的相邻元素。然后再去做对应的消除操作。 如动画所示:
从栈中弹出剩余元素,此时是字符串ac,因为从栈里弹出的元素是倒序的,所以再对字符串进行反转一下,就得到了最终的结果。
class Solution {
public:
string removeDuplicates(string s) {
stack<char> temp_stack;
for(char ch:s){
if(temp_stack.empty() || temp_stack.top()!=ch){
temp_stack.push(ch);
}else{
temp_stack.pop();
}
}
string res="";
while(!temp_stack.empty()){
res=res+temp_stack.top();
temp_stack.pop();
}
reverse(res.begin(),res.end());
return res;
}
};//超出内存限制
给你一个字符串数组 tokens
,表示一个根据 逆波兰表示法(后缀表达式) 表示的算术表达式。请你计算该表达式。返回一个表示表达式值的整数。
有效的算符为 '+'
、'-'
、'*'
和 '/'
。每个操作数(运算对象)都可以是一个整数或者另一个表达式。两个整数之间的除法总是 向零截断 。表达式中不含除零运算。输入是一个根据逆波兰表示法表示的算术表达式。答案及所有中间计算结果可以用 32 位 整数表示。
逆波兰表达式严格遵循「从左到右」的运算。计算逆波兰表达式的值时,使用一个栈存储操作数,从左到右遍历逆波兰表达式,进行如下操作:
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<int> str_stack;
for(int i=0;i<tokens.size();i++){
string &tok = tokens[i];
if(isNumber(tok)){
str_stack.push(atoi(tok.c_str()));
}else{
int num2 = str_stack.top();
str_stack.pop();
int num1 = str_stack.top();
str_stack.pop();
switch(tok[0]){
case '+':
str_stack.push(num1+num2);
break;
case '-':
str_stack.push(num1-num2);
break;
case '*':
str_stack.push(num1*num2);
break;
case '/':
str_stack.push(num1/num2);
break;
}
}
}
return str_stack.top();
}
bool isNumber(string &tok){
if(tok=="+" || tok=="-" || tok=="*" || tok=="/")
return false;
return true;
}
};
时间复杂度:O(n),其中 n 是数组 tokens 的长度。需要遍历数组 tokens 一次,计算逆波兰表达式的值。空间复杂度:O(n),其中 n 是数组 tokens 的长度。使用栈存储计算过程中的数,栈内元素个数不会超过逆波兰表达式的长度。
递归就是用栈来实现的。所以栈与递归之间在某种程度上是可以转换的!那么来看一下本题,其实逆波兰表达式相当于是二叉树中的后序遍历。 大家可以把运算符作为中间节点,按照后序遍历的规则画出一个二叉树。但我们没有必要从二叉树的角度去解决这个问题,只要知道逆波兰表达式是用后序遍历的方式把二叉树序列化了,就可以了。
本题中每一个子表达式要得出一个结果,然后拿这个结果再进行运算,那么这岂不就是一个相邻字符串消除的过程。
习惯看到的表达式都是中缀表达式,因为符合我们的习惯,但是中缀表达式对于计算机来说就不是很友好了。
尝试按照 popped 中的顺序模拟一下出栈操作,如果符合则返回 true,否则返回 false。这里用到的贪心法则是如果栈顶元素等于 popped 序列中下一个要 pop 的值,则应立刻将该值 pop 出来。
使用一个栈 st 来模拟该操作。将 pushed 数组中的每个数依次入栈,同时判断这个数是不是 popped 数组中下一个要 pop 的值,如果是就把它 pop 出来。最后检查栈是否为空。
class Solution {
public:
bool validateStackSequences(vector<int>& pushed, vector<int>& popped) {
stack<int> temp_stack;
int n=popped.size();
int j=0;
for(int i=0;i<pushed.size();i++){
temp_stack.push(pushed[i]);
while(!temp_stack.empty() && j<n && temp_stack.top()==popped[j]){
temp_stack.pop();
j++;
}
}
return temp_stack.empty();
}
};
时间复杂度:O(N)。将所以元素一遍入栈,一遍出栈,需要 O(2N)。空间复杂度:O(N)。使用了辅助栈 temp_stack
。