資料結構與演算法之二(棧常見案例)
棧是一種常用資料結構,其特性是FILO(first in last out),其基本概念這裡不做介紹,相信都學過了。直接食用java中已經封裝好Stack<>類。
棧的效率:入棧出棧複雜度為O(1),不需要比較和移動操作。
案例1:單詞逆序
比如,輸入alphago,要求逆向輸出其結果:ogahpla。可以用棧來解決這類問題。
String word = "alphago";
Stack<Character> stack = new Stack<Character>();
for(int i=0;i<word.length();i++){
stack.push(word.charAt(i));
}
/**
* 這種寫法有bug,問題在哪?
for(int i=0;i<stack.size();i++){
Character c = stack.pop();
System.out.print(c);
}*/
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<Character> stack = new Stack<Character>();
for(int i=0;i<word.length();i++){
char 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<Character> stack = new Stack<Character>();
for(int i=0;i<expression.length();i++){
char 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<Float> stack = new Stack<Float>();
for(int i=0;i<expression.length();i++){
char 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();
}