1. 程式人生 > 其它 >有趣的演算法(五) ——Dijkstra雙棧四則運算

有趣的演算法(五) ——Dijkstra雙棧四則運算

有趣的演算法(五)——Dijkstra雙棧四則運算

(原創內容,轉載請註明來源,謝謝)

一、概念

近期看到演算法書上,提到dijkstra雙棧的方法,實現輸入一個四則運算的字串,輸出結果。

其實質上,就是利用兩個棧,一個儲存數字,一個儲存運算子,再通過括號進行判定是否需要取出內容。

二、分析

為方便說明,現假設運算的字串為(3*(8-2))。其中,為簡化演算法,假定每兩個數的運算都要加上括號(對於不加括號的演算法,後面會討論到)。

運算的過程如下:

1)初始化兩個棧,分別用於存放運算子和數字。接收這一整串的字串,並從第一個字元開始,遍歷字串。

2)遇到左括號,忽略。

3)遇到數字,存入數字棧;遇到運算子,存入運算子棧。

4)遇到右括號,開始計算,取出數字棧最頂上兩個元素,以及運算子棧最頂上一個元素,用數字棧倒數第二個元素通過運算子和第一個元素進行運算。

5)將計算的結果再壓入數字棧。

6)重複2-5,直到遇到最後一個括號,則計算結束,返回最終數字棧中的唯一元素。

例如上圖,一開始會將3、8、2壓入數字棧,*、-壓入運算子棧。當遇到第一個右括號,則將數字棧的8、2和運算子棧的-彈出,並按照8在前,2在後的順序,運用運算子-,進行計算,得到結果6,再存入數字棧。

則此時,數字棧的順序是3、6,運算子棧是*。再遇到一個右括號,則會計算3*6,將結果18壓入數字棧。最終運算子棧沒有內容,數字棧是唯一的數字。

三、程式設計

1、java實現

1)首先,利用hashset,可以區分數字set和運算子set,針對每一個字元,判斷是否屬於這兩個set,或是否是有括號,並進行相應的操作,壓入棧或者是取出並計算。

如下:

    private static finalHashSet<Character> numStringSet = new HashSet<Character>(){
        {add('0'); add('1'); add('2');add('3'); add('4'); add('5'); add('6'); add('7'); add('8'); add('9');}
    };
    private static finalHashSet<Character> operatorSet = new HashSet<Character>(){
        {add('+'); add('-'); add('*');add('/');}
    };

2)接著,利用兩個stack泛型,一個是Double型別(主要是針對除法),一個是Character型別,用於儲存計算期間的內容。

如下:

         private Stack<Double> doubleStack= new Stack<>();
private Stack<Character> charStack = new Stack<>();

3)接著,針對每個字元,進行判斷,檢視需要何種操作。

    private BooleanneedCalculate(Character ch){
        Boolean res = false;
       if(operatorSet.contains(ch)){
           this.pushChar(ch);//運算子
        }elseif(numStringSet.contains(ch)){
           this.pushDouble(Double.valueOf(String.valueOf(ch)));//數字
        }elseif(ch.equals(')')){
            res = true;//要計算的情況
        }
        return res;
    }

4)最後進行計算,並返回結果。

整體程式碼如下:

注:程式碼已傳到github,https://github.com/linhxx/taskmanagement,就是之前的springboot專案,我計劃將java相關的內容整合到裡面,作為演算法測試模組。

package com.lin.service.algorithm;
import java.util.HashSet;
import java.util.Stack;
public class CalculateService{
    privateStack<Double> doubleStack = new Stack<>();
    privateStack<Character> charStack = new Stack<>();
    private String strCalcu;
    private Integer strLength;
    private static finalHashSet<Character> numStringSet = new HashSet<Character>(){
        {add('0'); add('1');add('2'); add('3'); add('4'); add('5'); add('6'); add('7'); add('8');add('9');}
    };
    private static finalHashSet<Character> operatorSet = new HashSet<Character>(){
        {add('+'); add('-');add('*'); add('/');}
    };
    publicCalculateService(String strCalcu){
        this.strCalcu =strCalcu;
        this.strLength =strCalcu.length();
    }
    private voidpushDouble(Double num){
        doubleStack.push(num);
    }
    private voidpushChar(Character str){
        charStack.push(str);
    }
    private DoublepopDouble(){
        returndoubleStack.pop();
    }
    private CharacterpopChar(){
        returncharStack.pop();
    }
    private Boolean needCalculate(Characterch){
        Boolean res = false;
       if(operatorSet.contains(ch)){
           this.pushChar(ch);//運算子
        }elseif(numStringSet.contains(ch)){
           this.pushDouble(Double.valueOf(String.valueOf(ch)));//數字
        }else if(ch.equals(')')){
            res = true;//要計算的情況
        }
        return res;
    }
    private voidcalculateMiddle(){
        Double sum =this.popDouble();
        Character ch =this.popChar();
        Double num =this.popDouble();
        if(ch.equals('+')){
            sum += num;
        }elseif(ch.equals('-')){
            sum = num - sum;
        }elseif(ch.equals('*')){
            sum *= num;
        }elseif(ch.equals('/') && 0 == sum){
            sum = num / 1;
        }else{
            sum = num / sum;
        }
        this.pushDouble(sum);
    }
    public DoubledealCalculate(){
        for(int i=0;i<this.strLength; i++){
            Character ch =this.strCalcu.charAt(i);
           if(this.needCalculate(ch)){
                this.calculateMiddle();
            }
        }
        returndoubleStack.pop();
    }
}

2、PHP實現

PHP實現上,相對於java,就比較簡單粗暴了。因為php沒有那麼多的規定和資料型別,就是直接用array來存內容,所有的內容都是基於array,不過解答思想上,還是根據雙棧的方式。

         <?php
class CalculateService{
         private$doubleStack = array();
   private $charStack = array();
   private $strCalcu;
   private $strLength;
   private static const $numStringSet = array('0', '1', '2', '3', '4', '5','6', '7', '8', '9');
   private static const $operatorSet = array('+', '-', '*', '/');
   public function __construct($strCalcu){
            $this->strCalcu =$strCalcu;
            $this->strLength =strlen($strCalcu);
   }
   private function push($type, $content){
            array_push($this->$type,$content);
   }
   private function pop($type){
            returnarray_pop($this->$type);
   }
   private function needCalculate($ch){
       $res = false;
       if(in_array($ch, self::$operatorSet)){
            $this->push('charStack', $ch);//運算子
       }else if(in_array($ch, self::$numStringSet)){
            $this->push('doubleStack',$ch);//數字
       }else if(')' == $ch){
            $res = true;//要計算的情況
       }
       return $res;
   }
   private function calculateMiddle(){
       $sum = $this->pop('doubleStack');
       $ch = $this->pop('charStack');
       $num = $this->pop('doubleStack');
       if('+' == $ch){
            $sum += $num;
       }else if('-' == $ch){
            $sum = $num - $sum;
       }else if('*' == $ch){
            $sum *= $num;
       }else if('/' == $ch && 0 == $sum){
            $sum = $num / 1;
       }else{
            $sum = (double)$num / $sum;
       }
       $this->push('doubleStack', $sum);
   }  
   public function dealCalculate(){
       for($i=0; $i<$this->strLength; $i++){
            $ch = $this->strCalcu[$i];
            if($this->needCalculate($ch)){
                $this->calculateMiddle();
            }
       }
       return $this->pop('doubleStack');
   }   
}

四、進階

當前這個演算法,是簡化版的四則運算,有五個待解決的前提。

1、括號問題

由於目前採用判斷有括號的方式,因此,即使1+1,也需要寫成(1+1),否則會返回結果1。這顯然不合理。而且任意兩個數的運算,都需要加括號,無論其優先順序。

要解決1+1必須寫(1+1)的問題,這個較為簡單,需要做兩件事情:

1是在字串都處理結束的時候,檢查兩個棧,是否數字棧只剩1個元素,運算子棧沒有元素。如果不是,則按照倒序,逐個取出數字和運算子進行計算,將棧逐步清空。

2是需要在處理到左括號的時候,也將其存入運算子棧,則當處理到右括號的時候,可以一路追溯到左括號,將一系列的內容都取出進行計算。

2、取數問題

目前是逐個字元取數字,這就造成如果數字超過1位數,例如10,則會被當作1和0分別存入數字棧。

要解決這個問題,也較容易,即當遍歷字串,遍歷到的元素是數字時,先暫存這個數字,再遍歷下一個元素。如果下一個元素還是數字,則和該元素進行字串拼接。直到下一個元素是運算子、括號或者沒有下一個元素。將這一串拼接的數字,轉成double存入數字棧中。

3、負數問題

和取數問題一樣,負數例如-1,會被當作-和1分別存入兩個棧中。

為了解決這個問題,需要在類中,加一個變數,來判斷上一個元素是否是數字。如果上一個元素是數字,則-可以直接進入運算子棧;如果上一個元素不是數字,則需要將-和下一個元素進行計算,可以用0減去下一個數字,將得到的結果存入數字棧。

4、優先順序問題

這個問題較為複雜,當沒有對所有的運算進行括號限制的時候,就需要程式來判斷優先順序問題。

要解決這個問題,需要先建立一個優先順序列表,可以是一個二維陣列,陣列按下標從0開始,每一個數組內的儲存優先順序一致的運算子,且0開始優先順序逐漸減小。

這樣,當取出運算子的時候,就需要在優先順序列表裡面進行判斷,如果不是最高優先順序,還需要再取一個元素,再進行判斷。

5、拓展問題

除了四則運算,如果需要拓展到其他運算,如冪、開方、三角函式、對數等,以及特殊常量π、e等,則需要程式裡面進行相應的定義。可以指定某些字母作為判斷,例如2pow2,可以看作2的平方。

但是,由此,又引申出一個問題,錯誤處理。由於內容都是輸入的,會出現諸多不符合情況的輸入,例如連續兩個運算子、除以0、括號數量不對等問題。因此,還需要加入錯誤處理機制,這個是最為複雜的部分。

——written by linhxx 2017.10.10