栈是一种常用数据结构,其特性是FILO(first in last out),其基本概念这里不做介绍,相信都学过了。直接食用java中已经封装好Stack<>类。
栈的效率:入栈出栈复杂度为O(1),不需要比较和移动操作。
案例1:单词逆序
比如,输入alphago,要求逆向输出其结果:ogahpla。可以用栈来解决这类问题。
String word = "alphago";
Stack stack = new Stack();
for(int i=0;i/**
* 这种写法有bug,问题在哪?
for(int i=0;i
while(stack.size()!=0){
System.out.print(stack.pop());
}
案例2:分隔符匹配
比如有字符串a{b[c(d)c]b}a,如何检测其中的分隔符是一一对应的。用栈来实现也是最简单的。
分析:检测到字符直接不管,检测到左分隔符入栈,检测到右分隔符,从栈中弹出一个符号,如果匹配则继续,如果不匹配或者没有则报错。若有没有匹配的左分隔符也报错。
public static void main(String[] args) {
// TODO Auto-generated method stub
String word = "a{b[c(d)c]b}a";
int result = check(word);
if(result==-1){
System.out.println("match success");
}else{
System.out.println("match fail: index = "+result+",character is '"+word.charAt(result)+"'");
}
}
/**
* 匹配错误返回第一个不匹配的位置
* 匹配正确则返回-1
*/
private static int check(String word){
Stack stack = new Stack();
for(int i=0;ichar ch = word.charAt(i);
switch(ch){
case '{':
case '[':
case '(':
stack.push(ch);
break;
case '}':
case ']':
case ')':
if(stack.isEmpty()){
return i;
}else{
char p = stack.pop();
if((ch=='}' && p!='{')
|| (ch==']' && p!='[')
||(ch==')' && p!='(')){
return i;
}
}
break;
}
}
if(!stack.isEmpty()){
char c = stack.get(0);
return word.indexOf(c);
}
return -1;
}
案例3:我觉得这个案例最经典了。如何计算表达式的值。
比如任意一个表达式(1+2*3)/7-1,如何计算出他的正确答案。
分析:这道题光用栈还不够,需要清楚计算的方式
首先,得将中缀表达式变为后缀表达式。
然后,计算后序表达式的值。
1)中缀变后缀
首先借助一个树型图来理解中缀和后缀
在网上找了个图,如下:
对这个二叉树做一次中根遍历(即根放在中间,从左到右),可以得到表达式3+2*9-6/4,也就是我们常见的表达式。所以算数表达式都是以中缀表达式出现的。同样,我们可以得到他的后缀表达式,进行一次后跟遍历(即根放在最后,从左到右),可以得到329*+64/-。这就是由中缀变后缀了。同理,我们的表达式画个图,然后也可以写出后缀表达式。
程序实现可以利用栈,情况比较复杂分几种来描述
a. 3+2*9-6/4,*号优先级更高
读取 | 操作 | 栈中 | 输出 |
---|---|---|---|
3 | 3是数字,输出 | $ | 3 |
+ | +是计算字符,而栈中此时无元素,入栈 | $+ | 3 |
2 | 2是数字,输出 | $+ | 32 |
* | 是计算字符,而栈中有元素,弹出+ ,比较和+优先级,优先级>+,先将+入栈,再将入栈 | $+,* | 32 |
9 | 9是数字,输出 | $+,* | 329 |
- | -是计算字符,而栈中有元素,弹出* ,比较-和优先级,-优先级<=,说明前面部分的计算结束了,全部弹出并按顺序输出直到遇到左括号或没了为止,并将-入栈 | $- | 329*+ |
6 | 6是数字,输出 | $- | 329*+6 |
/ | /是计算字符,而栈中有元素,弹出- ,比较/和-优先级,/优先级>-,先将-入栈,再将/入栈 | $-,/ | 329*+6 |
4 | 4是数字,输出 | $-,/ | 329*+64 |
end | 弹出栈中剩余元素,输出 | $ | 329*+64/- |
b. 带括号的表达式(1+2*3)/7-1
读取 | 操作 | 栈中 | 输出 |
---|---|---|---|
( | (是左括号,入栈 | $( | |
1 | 1是数字,输出 | $( | 1 |
+ | +是计算字符,而栈中此时有元素,弹出(,发现不是计算符号,先将(入栈,再将+入栈 | $(,+ | 1 |
2 | 2是数字,输出 | $(,+ | 12 |
* | 是计算字符,而栈中此时有元素,弹出+,>+,先将+入栈,再将*入栈 | $(,+,* | 12 |
3 | 3是数字,输出 | $(,+,* | 123 |
) | )是右括号,说明此阶段计算结束,依次弹出栈顶元素并输出,直到弹出(丢弃 | $ | 123*+ |
/ | /是计算字符,而栈中无元素,入栈 | $/ | 123*+ |
7 | 7是数字,输出 | $/ | 123*+7 |
- | -是计算字符,而栈中有元素,弹出/,-<=/,上一阶段计算结束,依次弹出并输出直至遇到左括号或结束,将-入栈 | $- | 123*+7/ |
1 | 1是数字,输出 | $- | 123*+7/1 |
end | 弹出栈中剩余元素,输出 | $ | 123*+7/1- |
2)后缀表达式求值
为什么要先变后缀?因为变成后缀的过程中可以处理掉括号,最后利用栈可以求值。比如329*+64/-,我们将所有数字压入栈中,每次碰到符号,则弹出左操作数和右操作数,进行一次计算,然后循环这个过程,就可以算出整个表达式。
329*+64/-
读取 | 操作 | 栈中 |
---|---|---|
3 | 3是数字,入栈 | $3 |
2 | 2是数字,入栈 | $3,2 |
9 | 9是数字,入栈 | $3,2,9 |
* | *是计算符号,弹出栈顶元素9为右操作数,再次弹出栈顶元素2为左操作数计算2*9=18,将结果压入栈中 | $3,18 |
+ | +是计算符号,弹出栈顶元素18为右操作数,再次弹出栈顶元素3为左操作数计算3+18=21,将结果压入栈中 | $21 |
6 | 6是数字,入栈 | $21,6 |
4 | 4是数字,入栈 | $21,6,4 |
/ | /是计算符号,弹出栈顶元素4为右操作数,再次弹出栈顶元素6为左操作数计算6/4=1.5,将结果压入栈中 | $21,1.5 |
- | -是计算符号,弹出栈顶元素1.5为右操作数,再次弹出栈顶元素21为左操作数计算21-1.5=19.5,将结果压入栈中 | $19.5 |
end | 将栈中结果pop出来即可,结果是19.5 | $ |
如果是中缀表达式则无法完成这个过程,因为后缀表达式可以保证计算符号一定有两个操作数在他前面。
假定都是个位数运算,这样可以用String来获取每个字符
/**
* 获取后缀表达式
* 如果错误,返回null
*/
private static String getPostfixExpression(String expression){
StringBuilder sb = new StringBuilder();
Stack stack = new Stack();
for(int i=0;ichar c = expression.charAt(i);
if(Character.isDigit(c)){ //数字直接输出
sb.append(c);
}else if(c=='+' || c=='-'){
if(stack.isEmpty()){
stack.push(c);
}else{
//pop所有并输出,如果遇到左括号则停止,把左括号重新入栈
while(!stack.isEmpty()){ //阶段结束标志1
char p1 = stack.pop();
if(p1!='('){
sb.append(p1);
}else{ //阶段结束标志2
stack.push(p1);
break;
}
}
stack.push(c);
}
}else if(c=='*'||c=='/'){
if(stack.isEmpty()){
stack.push(c);
}else{
char p = stack.pop();
if(p=='*'||p=='/'){
sb.append(p);
//pop所有并输出,如果遇到左括号则停止,把左括号重新入栈
while(!stack.isEmpty()){ //阶段结束标志1
char p1 = stack.pop();
if(p1!='('){
sb.append(p1);
}else{ //阶段结束标志2
stack.push(p1);
break;
}
}
stack.push(c);
}else{
stack.push(p);
stack.push(c);
}
}
}else if(c=='('){ //左括号直接入栈
stack.push(c);
}else if(c==')'){
//记录是否遇到左括号
boolean match = false;
//pop所有并输出,如果遇到左括号则停止,把左括号舍弃
while(!stack.isEmpty()){
char p = stack.pop();
if(p=='('){
match = true;
break;
}else{
sb.append(p);
}
}
if(!match){
return null;
}
}else{ //有非法字符
return null;
}
}
//结束之后,剩下的出栈
while(!stack.isEmpty()){
sb.append(stack.pop());
}
return sb.toString();
}
/**
* 计算表达式结果
*/
private static float getResult(String expression){
Stack stack = new Stack();
for(int i=0;ichar c = expression.charAt(i);
if(Character.isDigit(c)){ //数字入栈
stack.push((float)(c-'0'));
}else{ //过滤后剩下的肯定是计算符号
if(stack.size()<2){
System.out.println("表达式不正确");
return 0;
}else{
float rightNum = stack.pop();
float leftNum = stack.pop();
float result = 0;
switch(c){
case '+':
result = (leftNum+rightNum);
break;
case '-':
result = (leftNum-rightNum);
break;
case '*':
result = (leftNum*rightNum);
break;
case '/':
result = (leftNum/rightNum);
break;
}
stack.push(result);
}
}
}
return stack.pop();
}
上一节:数据结构与算法之一(三种简单排序)
下一节:数据结构与算法之三(栈和队列的java实现)