問題來源
全日制steam小遊戲『24點』實現過程中遇到的問題,現在,通過某種方法,後端能得到一個字符串表達式,含有四則運算和括號。要求結算該表達式得到結果。對於計算結果,若爲整數,直接以整數結果表示;若爲分數則以分數結果表示,不能轉成浮點數。
解決思路
分解問題
其實是一個經典的算法問題以及一個分數運算問題:
- 解析字符串表達式,表達式中含有+、-、*、/、( 和 )
- 將一般的數值運算轉爲分數運算,其中,值得注意的是 分數的約分 邏輯
解析字符串表達式
首先,忽略分數計算的問題,解析字符串表達式,這個算是一個經典問題,大學時期應該都有了解,這裏提供一個leetcode題目作爲參考: leetcode - 227. 基本計算器2 Basic Calculator II 。在這個題目的基礎上,增加解析括號的要求,即爲本小節需要解決的問題。
表達式中含有四則運算和括號,因此存在運算優先級的問題,爲了減少遞歸,對於乘除法沒有執行。
先來一個字符串表達式解析的流程圖。
分數運算
分數運算沒有複雜的算法,實際上就是新增一個 Fraction類 ,Fraction存儲分子和分母,所有的從字符串表達式中解析得到的數字都不直接參與字符串解析的算法,而是 先初始化爲 Fraction 的對象 ,再進行四則運算,因此,只要理清楚 分數的四則運算和 約分 邏輯即可。其中,約分邏輯需要注意效率和正確性,因此下文也給出了筆者的約分流程圖。
以下是Fraction分式類的類圖,以及約分流程圖。
方法或屬性 | 說明 |
---|---|
Fraction::num | 存儲分子 |
Fraction::den | 存儲分母 |
Fraction::rule(Fraction $exp, int $op) | 當前對象與$exp做$op運算,並將結果更新至當前對象 |
Fraction::ruleString(Fraction $exp, int $op) | 當前對象與$exp做$op運算,並將結果更新至當前對象 |
Fraction::sum(Fraction $exp) | 當前對象與$exp做加法運算,並將結果更新至當前對象 |
Fraction::sub(Fraction $exp) | 當前對象與$exp做減法運算,並將結果更新至當前對象 |
Fraction::mul(Fraction $exp) | 當前對象與$exp做乘法運算,並將結果更新至當前對象 |
Fraction::div(Fraction $exp) | 當前對象與$exp做除法運算,並將結果更新至當前對象 |
Fraction::reduction() | 對當前對象執行約分邏輯 |
Fraction::reciprocal() | 對當前對象求倒數 |
Fraction::isProper() | 對當前對象判斷是否爲真分數 |
Fraction::getFloat() | 返回當前對象的浮點數值 |
Fraction::getAuto() | 當前分式能表達爲整數則表達爲整數,否則返回分數字符串 |
注意:實際代碼較該流程有優化,可參考最後php代碼中,Fraction::reduction()
方法的具體邏輯。
代碼實現
最後,慣例,看不懂上面的圖文的,來看代碼吧~~~
php實現
兩個類,一個 Fraction 存儲分式,Caculator用於計算,裏面僅一個靜態方法Caculator::caculate()用於計算字符串表達式。
/**
* Class Fraction
* 分式計算器
*/
class Fraction
{
const OP_TYPE_SUM = 1;
const OP_TYPE_SUB = 2;
const OP_TYPE_MUL = 3;
const OP_TYPE_DIV = 4;
const OP_TYPE_STR_SUM = '+';
const OP_TYPE_STR_SUB = '-';
const OP_TYPE_STR_MUL = '*';
const OP_TYPE_STR_DIV = '/';
const OP_TYPES = [self::OP_TYPE_STR_SUM, self::OP_TYPE_STR_SUB, self::OP_TYPE_STR_DIV, self::OP_TYPE_STR_MUL];
/**
* @var int 分子
*/
public $num = 0;
/**
* @var int 分母
*/
public $den = 1;
public function __construct(int $num = 0, int $den = 1)
{
$this->num = $num;
$this->den = $den;
}
/**
* 可選四則運算
*
* @param Fraction $exp
* @param $op
* @throws \Exception
*/
public function rule(Fraction $exp, int $op)
{
switch ($op) {
case self::OP_TYPE_SUM:
$this->sum($exp);
break;
case self::OP_TYPE_SUB:
$this->sub($exp);
break;
case self::OP_TYPE_MUL:
$this->mul($exp);
break;
case self::OP_TYPE_DIV:
$this->div($exp);
break;
default:
throw new \Exception('OPERATOR TYPE IS FAIL');
break;
}
}
/**
* 可選四則運算
*
* @param Fraction $exp
* @param $op
* @throws \Exception
*/
public function ruleStr(Fraction $exp, string $op)
{
switch ($op) {
case self::OP_TYPE_STR_SUM:
$this->sum($exp);
break;
case self::OP_TYPE_STR_SUB:
$this->sub($exp);
break;
case self::OP_TYPE_STR_MUL:
$this->mul($exp);
break;
case self::OP_TYPE_STR_DIV:
$this->div($exp);
break;
default:
throw new \Exception('OPERATOR TYPE IS FAIL');
break;
}
}
/**
* 加法
*
* @param Fraction $exp
*/
public function sum(Fraction $exp)
{
// 通分
$num1 = $this->num * $exp->den;
$num2 = $exp->num * $this->den;
$den = $this->den * $exp->den;
$this->num = $num1 + $num2;
$this->den = $den;
// $this->reduction();
}
/**
* 減法
*
* @param Fraction $exp
*/
public function sub(Fraction $exp)
{
// 通分
$num1 = $this->num * $exp->den;
$num2 = $exp->num * $this->den;
$den = $this->den * $exp->den;
$this->num = $num1 - $num2;
$this->den = $den;
// $this->reduction();
}
/**
* 乘法
*
* @param Fraction $exp
*/
public function mul(Fraction $exp)
{
$this->num *= $exp->num;
$this->den *= $exp->den;
// $this->reduction();
}
/**
* 除法
*
* @param Fraction $exp
* @throws \Exception
*/
public function div(Fraction $exp)
{
// 除某個數即乘以其倒數
if (!$exp->den) {
throw new \Exception('算式非法');
}
$this->mul($exp->reciprocal());
}
/**
* 約分當前分數
* 性能考慮,不在每次運算後約分
*
* @todo 存在可優化的方向,直接打表,每次求最大公約數時,直接查表得到
*/
public function reduction()
{
$isProper = $this->isProper();
// 能直接整除時,直接處理,節省時間
if ($isProper && $this->den && !($this->num % $this->den)) {
$this->num /= $this->den;
$this->den = 1;
return;
} elseif ($this->num && !($this->den % $this->num)) {
$this->den /= $this->num;
$this->num = 1;
return;
}
// 約分
// 從較小者的1/2開始,循環至2,若存在能同時將兩者整除
// 則同時對分子分母執行整除操作
// 由大向小執行,直接避免了由小往大執行時可能出現的當前數約分後依然能再次約分的情況
// 如 i=2時, 8/i = 4,此時還要再判斷一次4能否被i整除,能的話需要再執行一次
$min = $isProper ? $this->num : $this->den;
for ($i = intval($min / 2); $i > 1;) {
if (!($this->num % $i || $this->den % $i)) {
$this->num /= $i;
$this->den /= $i;
// 約分後,依然從較小者的1/2開始向前遍歷,減少運算次數
$min = $isProper ? $this->num : $this->den;
$i = intval($min / 2);
} else {
$i--;
}
}
}
/**
* 是否真分數
* 分母是否大於分子
*
* @return bool
*/
public function isProper()
{
return $this->num < $this->den;
}
/**
* 求倒數
*
* @return static
*/
public function reciprocal()
{
$tmp = $this->num;
$this->num = $this->den;
$this->den = $tmp;
return $this;
}
/**
* 獲取小數
* 結果
*
* @return float|int
*/
public function getFloat()
{
return $this->num / $this->den;
}
public function getAuto()
{
$this->reduction();
if ($this->den === 1) {
return $this->num;
} elseif ($this->den === 0) {
return null;
}
return $this->num . '/' . $this->den;
}
}
class Calculator
{
/**
* @var string 表達式的原始字符串
*/
protected $expRaw = '';
public function __construct($raw)
{
$this->expRaw = $raw;
}
/**
* @param string $raw
* @return Fraction
* @throws Exception
*/
public static function calculate(string $raw)
{
$rawLen = strlen($raw);
$numFraction = new Fraction();
$curResFraction = new Fraction();
$resFraction = new Fraction();
$op = '+';
for ($i = 0; $i < $rawLen; ++$i) {
// 遍歷字符串
$c = $raw[$i];
if ($c >= '0' && $c <= '9') {
$numFraction->num = $numFraction->num * 10 + $c - '0';
} elseif ($c == '(') {
$j = $i;
$cnt = 0;
// 搜尋下一個括號
for (; $i < $rawLen; ++$i) {
if ($raw[$i] == '(') ++$cnt;
if ($raw[$i] == ')') --$cnt;
if ($cnt == 0) break;
}
$numFraction = self::calculate(substr($raw, $j + 1, $i - $j - 1));
} elseif ($c == '+' || $c == '-' || $c == '*' || $c == '/') {
$curResFraction->ruleStr($numFraction, $op);
if ($c == '+' || $c == '-') {
$resFraction->ruleStr($curResFraction, '+');
$curResFraction = new Fraction();
}
$op = $c;
$numFraction = new Fraction();
}
if ($i == $rawLen - 1) {
$curResFraction->ruleStr($numFraction, $op);
$resFraction->ruleStr($curResFraction, '+');
break;
}
}
$resFraction->reduction();
return $resFraction;
}
}
// 調用實例
$startTime = microtime(true);
$fraction = Calculator::calculate('(((11*13)/2)/11)');
//echo microtime(true) - $startTime;
//exit;
echo $fraction->num . '/' . $fraction->den . PHP_EOL;
echo $fraction->getFloat() . PHP_EOL;
echo $fraction->getAuto() . PHP_EOL;