1.平衡符号
编译器需要检查代码的语法错误,但是常常由于缺少一个符号(如一楼一个花括号或者注释起始符)引起编译器上百行的诊断,而真正的错误并没有找出(在这一点上java编译器还是比较可靠的,但不是所有编译器)。在这种情况下一个有用的工具就是检查符号是否有效。于是每一个左花括号,左方括号以及左圆括号必然有右括号与其对应(比如{[]}是合法的,但是{[}]是错误的)。显然为此编写一个大型程序去校验是不值得的,事实上检验这些事情是很容易的,我们就圆括号、方括号和花括号进行一次校验,我们这个简单的算法用到一个栈
class Check {
// 用一个Map来装需要成对的符号
private HashMap<Character, Character> mappings;
// 初始化映射,方便之后读取使用
public Check() {
this.mappings = new HashMap<Character, Character>();
this.mappings.put(')', '(');
this.mappings.put('}', '{');
this.mappings.put(']', '[');
}
public boolean isValid(String s) {
// 初始化一个栈
Stack<Character> stack = new Stack<Character>();
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
// 判断当前字符是否为一个右括号
if (this.mappings.containsKey(c)) {
// 元素出栈,若为空则设置为"#"
char topElement = stack.empty() ? '#' : stack.pop();
//如果括号不能组成对则返回false
if (topElement != this.mappings.get(c)) {
return false;
}
} else {
// 如果为一个做括号则元素入栈
stack.push(c);
}
}
// 如果元素为空,则校验为true
return stack.isEmpty();
}
}
2.后缀表达式
假设这么一个场景:我们在外购物,一些货物会有折扣,我们在使用计算器计算各个物品价格为4.99,5.99,6.99的花费,商品会有9折的折扣。那么输入这些数据的自然方式将是:
4.99+5.99+6.99*0.9=
随着计算器的不同,这个结果或者是所要的答案16.173(顺序执行),或者是科学答案17.271。最简单的计算器都会给出第一个答案,但是许多先进的计算器是知道乘法的优先级高于加法的。
另外一种情况, 如果我们的折扣只适用于4.99和6.99的商品,那么计算顺序为:
4.990.9+5.99+6.990.9=
将在科学计算器中给出正确答案16.772,而在简单计算器上给出错误答案15.724。科学计算器一般包含括号,因此我们总能通过加括号的方式得到正确的答案,但是使用简单计算器需要我们记住中间的结果。
这例典型的计算顺序可以是将4.99和0.9相乘记为A1,然后将5.99与A1相加,再将结果存入A1;我们再将6.99与0.9相乘记为A2,最后将A1和A2相加并把结果存入A1。这样我们可以将这种操作顺序书写如下:
4.99 0.9 * 5.99 + 6.99 0.9 * +
这个记法叫做后缀(postfix)或是逆波兰(reverse Polish)记法,其求值过程就是上述过程,计算这个问题最简单的方法是使用一个栈当见到数时就把它推入栈,在遇到运算符时就将该符号作用于从栈中弹出的两个数上,再将结果推入栈中。下面模拟一下计算6 5 2 3 + 8 * + 3 + *的过程。
计算一个后缀表达式花费的时间是O(N),因为对输入中的每个元素处理都由栈操作组成,花费时间为常数,这样计算的优点是没有必要知道任何优先的规则。这里介绍了后缀表达式顺便再简介一下前缀表达式和中缀表达式,前缀表达式使用非常少,指的是将符号写在适用数字之前比如1-(2+3)的前缀表达式为- 1 + 2 3,将表达式从右往左直接入栈出栈即可。中缀表达式就是我们正规的运算表达式,需要了解的是中缀和后缀的转换。
我们可以用栈将一个标准形式的表达式(中缀表达式)转换成后缀表达式。假设表达式合法为:
a + b * c ( d * e + f ) * g
先说一下思路,计算从一个空的栈开始,当读到一个操作数时,立即把它放到输出中,已经见到过但尚未放到输出的操作符推入栈中。当遇到左括号时也推入栈,如果遇到一个右括号,那么就将栈元素弹出并写在输出中直到遇到一个对应的左括号,但是这个左括号只被弹出并不输出。如果见到其他符号(+,*,(),那么我们从栈中弹出元素直到发现优先级更低的元素为止。这里需要注意:如果处理一个左括号(时,+的优先级最低,而(的优先级最高。当弹栈的工作结束后我们再将操作符压入栈中。最后,如果读到输入的末尾,我们将栈元素弹出直到变为空栈,将符号写在输出中。
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
String s = scanner.nextLine();
ArrayList<Character> outPut = new ArrayList<>();
Stack<Character> characters = new Stack<>();
HashMap<Character, Integer> map = new HashMap<>();
map.put('+',1);
map.put('*',2);
//如果入栈的是‘+’栈顶是‘*’,则‘*’出栈,‘+’再入栈,栈顶为‘+’则出栈,再入栈。遍历判断
// 如果入栈的是‘*’,栈顶为‘*’则出栈,再入栈,如果栈顶为‘(’,直接入栈。遍历判断
// 如果符号的是‘(’,直接入栈。
//如果符号是‘)’,则元素依次出栈,直到栈顶元素为‘(’,‘(’出栈结束。
//如果是其它符号,直接进入数组。
//当遍历完字符串时,元素出栈。
int length = s.length();
// for (int i=0;i<s.length();i++){//这种方式当数据量多的时候消耗时间更多!//详细参考这篇博客https://www.trinea.cn/android/arraylist-linkedlist-loop-performance/
for (int i=0;i<length;i++){
char c = s.charAt(i);
if (c=='+'||c=='*'){
if (characters.isEmpty()||characters.peek()=='('){//"||"只要满足第一个条件,后面的条件就不再判断,而|要对所有的条件进行判断。
characters.push(c);
}else if (map.get(c)<=map.get(characters.peek())){//前者的优先级比后者小或相等
while (!characters.isEmpty()&&map.get(characters.peek())!=null&&map.get(c)<=map.get(characters.peek())){
Character pop = characters.pop();
outPut.add(pop);
}
characters.push(c);
} else {//前者比后者大
characters.push(c);
}
}else if (c=='('){
characters.push(c);
}else if (c==')'){
while (characters.peek()!='('){
Character pop = characters.pop();
outPut.add(pop);
}
//‘(’出去
characters.pop();
}else {
//其它数字
outPut.add(c);
}
}
while (!characters.isEmpty()){
outPut.add(characters.pop());
}
for (int i=0;i<outPut.size();i++){
System.out.print(outPut.get(i));
}
}
输入:a+bc+(de+f)g
输出:abc+def+g