题目:有效的括号
-
给定一个只包括
'('
,')'
,'{'
,'}'
,'['
,']'
的字符串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目录呢 ,这就是栈的应用。
-
由于栈结构的特殊性,非常适合做对称匹配类的题目。首先要弄清楚,字符串里的括号不匹配有几种情况。建议在写代码之前要分析好有哪几种不匹配的情况,如果不在动手之前分析好,写出的代码也会有很多问题。先来分析一下 这里有三种不匹配的情况,
-
第一种情况,字符串里左方向的括号多余了 ,所以不匹配。
-
第二种情况,括号没有多余,但是 括号的类型没有匹配上。
扫描二维码关注公众号,回复: 16675108 查看本文章 -
第三种情况,字符串里右方向的括号多余了,所以不匹配。
-
-
对应三种情况的解法:
-
第一种情况:已经遍历完了字符串,但是栈不为空,说明有相应的左括号没有右括号来匹配,所以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<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(); 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 的长度。使用栈存储计算过程中的数,栈内元素个数不会超过逆波兰表达式的长度。
-
递归就是用栈来实现的。所以栈与递归之间在某种程度上是可以转换的!那么来看一下本题,其实逆波兰表达式相当于是二叉树中的后序遍历。 大家可以把运算符作为中间节点,按照后序遍历的规则画出一个二叉树。但我们没有必要从二叉树的角度去解决这个问题,只要知道逆波兰表达式是用后序遍历的方式把二叉树序列化了,就可以了。
-
本题中每一个子表达式要得出一个结果,然后拿这个结果再进行运算,那么这岂不就是一个相邻字符串消除的过程。
-
习惯看到的表达式都是中缀表达式,因为符合我们的习惯,但是中缀表达式对于计算机来说就不是很友好了。
题目: 栈的压入、弹出序列
- 输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如,序列 {1,2,3,4,5} 是某栈的压栈序列,序列 {4,5,3,2,1} 是该压栈序列对应的一个弹出序列,但 {4,3,5,1,2} 就不可能是该压栈序列的弹出序列。
题解
-
尝试按照 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
。