有趣的演算法(五) ——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