代码随想录算法训练营第十一天 | 20. 有效的括号、1047. 删除字符串中的所有相邻重复项、150. 逆波兰表达式求值
20. 有效的括号
题目链接:20. 有效的括号
注:由于栈结构的特殊性,非常适合做对称匹配类的题目。
考察对 Stack 的操作
思路:首先分析不匹配的情况有哪些,如下各图所示:
- 字符串里左侧的括号多余:
- 括号数量正确,但无法匹配:
- 字符串里右侧的括号多余:
对于以上三种情况,判断的方法如下:
第一种情况:已经遍历完了字符串,但是栈不为空,说明有相应的左括号没有右括号来匹配,即左括号多余,所以 return false。
第二种情况:遍历字符串匹配的过程中,发现栈里没有要匹配的字符,所以 return false。
第三种情况:遍历字符串匹配的过程中,栈已经为空了,没有匹配的字符了,说明右括号没有找到对应的左括号,即右括号多余,所以 return false。
若字符串遍历完之后,栈是空的,就说明全部匹配成功。
class Solution {
public boolean isValid(String s) {
// 剪枝:若字符串中字符总数为奇数,则一定不匹配
if (s.length() % 2 != 0) return false;
Stack<Character> stack = new Stack<>();
for (int i = 0; i < s.length(); i++) {
if (s.charAt(i) == '(') { // 遇到左侧括号,则向栈中压入对应的右侧括号
stack.push(')');
} else if (s.charAt(i) == '[') {
stack.push(']');
} else if (s.charAt(i) == '{') {
stack.push('}');
} else if (stack.empty() || s.charAt(i) != stack.peek()) {
// 字符串还未遍历完但栈已空(情况三) 或 遍历到的字符与栈顶元素不同(情况二)
// 注:须先判断是否为空,否则可能对空栈进行 peek() 操作,导致异常
return false;
} else {
// 遇到字符串中的右侧括号与栈顶元素相同,说明当前位置匹配成功,
// 弹出栈顶元素,开始匹配下一个字符
stack.pop();
}
}
// 字符串遍历结束,但栈不为空(情况一)
return stack.empty();
}
}
1047. 删除字符串中的所有相邻重复项
题目链接:1047. 删除字符串中的所有相邻重复项
注:为什么栈适合做类似于消除的操作?因为栈帮助记录了遍历数组当前元素时,前一个元素是什么。
考察对栈的理解和操作
思路:在删除相邻重复项的时候,其实就是要知道当前遍历的这个元素,在前一位是不是遍历过一样数值的元素,因此就用到栈来存放之前遍历过的元素。当遍历当前的各个元素时,去栈里查找是不是遍历过相同数值的相邻元素,然后再去做对应的消除操作。当遍历结束后,从栈中弹出剩余元素,因为从栈里弹出的元素是倒序的,所以再对字符串进行反转,就得到了最终的结果。
解法一: 使用 Deque 作为堆栈。
因为 ArrayDeque 会比 LinkedList 在除了删除元素这一点外会快一点。
(参考:https://stackoverflow.com/questions/6163166/why-is-arraydeque-better-than-linkedlist)
class Solution {
public String removeDuplicates(String s) {
ArrayDeque<Character> deque = new ArrayDeque<>();
char ch;
for (int i = 0; i < s.length(); i++) {
ch = s.charAt(i);
if (deque.isEmpty() || ch != deque.peek()) {
deque.push(ch);
} else {
deque.pop();
}
}
String result = "";
while (!deque.isEmpty()) {
result = deque.pop() + result;
}
return result;
}
}
解法二:拿字符串直接作为栈,省去了栈还要转为字符串的操作。
class Solution {
public String removeDuplicates(String s) {
// 将 res 当做栈
// 也可以用 StringBuilder 来修改字符串,速度更快
// StringBuilder res = new StringBuilder();
StringBuffer res = new StringBuffer();
// top为 res 的长度
int top = -1;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
// 当 top > 0,即栈中有字符时,当前字符如果和栈中字符相等,弹出栈顶字符,同时 top--
if (top >= 0 && res.charAt(top) == c) {
res.deleteCharAt(top);
top--;
// 否则,将该字符入栈,同时 top++
} else {
res.append(c);
top++;
}
}
return res.toString();
}
}
题外话:关于递归
递归的实现就是:每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中,然后递归返回的时候,从栈顶弹出上一次递归的各项参数,所以这就是递归为什么可以返回上一层位置的原因。
此外,一种常见的错误就是栈溢出,系统输出的异常是Segmentation fault(当然不是所有的Segmentation fault 都是栈溢出导致的) 。如果使用了递归,就要想一想是不是无限递归了,那么系统调用栈就会溢出。
注:而且在企业项目开发中,尽量不要使用递归!在项目比较大的时候,由于参数多,全局变量等等,使用递归很容易判断不充分 return 的条件,非常容易无限递归(或者递归层级过深),造成栈溢出错误(这种问题还不好排查!)
150. 逆波兰表达式求值
题目链接:150. 逆波兰表达式求值
注:逆波兰表达式相当于是二叉树中的后序遍历,即用后序遍历的方式把二叉树序列化。
考察对栈的理解和操作
思路:题目已给出解题方法——遇到数字则入栈;遇到运算符则取出栈顶两个数字进行计算,并将结果压入栈中。
class Solution {
public int evalRPN(String[] tokens) {
Deque<Integer> stack = new LinkedList();
for (String s : tokens) {
if ("+".equals(s)) { // leetcode 内置 jdk 的问题,不能使用 == 判断字符串是否相等
stack.push(stack.pop() + stack.pop);
} else if ("-".equals(s)) { // 减号要注意顺序问题
stack.push(-stack.pop() + stack.pop);
} else if ("*".equals(s)) {
stack.push(stack.pop() * stack.pop);
} else if ("/".equals(s)) { // 除号要注意顺序问题
int temp1 = stack.pop();
int temp2 = stack.pop();
stack.push(temp2 / temp1);
} else {
stack.push(Integer.valueOf(s));
}
}
return stack.pop();
}
}