1. 程式人生 > >資料結構與演算法之二(棧常見案例)

資料結構與演算法之二(棧常見案例)

棧是一種常用資料結構,其特性是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();
    }