【題解】表達式計算其實很簡單!!遞歸實現

雨神告訴俺們,表達式計算用遞歸最不容易出錯。(確實不遞歸的話需要考慮的情況就很多了,容易錯漏)
本題解參考牛友 id 唯愛奕希依然 的代碼。

題目描述

給出一個表達式,其中運算符僅包含+,-,*,/,^(加 減 乘 整除 乘方)要求求出表達式的最終值

數據可能會出現括號情況,還有可能出現多餘括號情況

數據保證不會出現 231\geq 2^{31} 的答案(ps:說明可以用int存結果)

數據可能會出現負數情況(ps:這裏指的有負數情況下負數會用括號括起來)

輸入描述:

僅一行,即爲表達式

輸出描述:

僅一行,既爲表達式算出的結果

示例

輸入

(2+2)^(1+1)

輸出

16

備註:

表達式總長度 30\leq 30

題目分析

不想看分析的話,題目小結和完整代碼在最後 卑微.jpg

首先考慮沒有括號的情況:比如
如果表達式的輸入不存在括號,就直接按照運算符優先級進行計算就可以了。這裏我們藉助遞歸來簡單化代碼,減少出錯。

注意遞歸的運算使用了棧,即LIFO後入先出的容器。所以運算符優先級最高應該最先計算的^應該最後入棧!!!

代碼:(題解末尾會討論儲存符號的變量初值能否設置爲0的問題,這裏建議暫停思考一下)

string n;//存儲表達式
int cale(int left, int right){
	//計算表達式n中,下標left~right的部分
	int add = 0, mul = 0, power = 0;//記錄三種運算符,它們的下標不可能是0 故可以使用0來初始化 
	for(int i = left; i <= right; i++){
    	switch(n[i]){//記錄各種優先級符號的位置,便於後面計算
	       case '+':
	       case '-':
	           add = i;
	       case '*':
	       case '/':
	           mul = i;
	       case '^':
	           power = i;
	       }
	}
    if(add)//開始計算,注意運算符優先級最低的應該放在最前面
        if(n[add] == '+')
            return cale(l, add-1) + cale(add+1, r);
        else
            return cale(l, add-1) - cale(add+1, r);
    if(mul)
        if(n[mul] == '*')
            return cale(l, mul-1) * cale(mul+1, r);
        else
            return cale(l, mul-1) / cale(mul+1, r);
    if(power)//優先級最高的最後計算
        return pow(cale(l, power-1), cale(power+1, r));
    }

遞歸計算表達式的思想就結束了。是不是超級簡單!!

接下來就是考慮括號的細節問題了,我們以示例爲例:(2+2)^(1+1)
如果存在括號,這一段被括號括起來的表達式(2+2)(1+1)應該最先計算對吧?同樣由於我們使用的是棧,爲了先計算,就應該最後入棧。就是說在代碼處理的時候可以先忽略括號內部表達式的存在,最後再對括號內部進行處理。

所以我們稍微更改一下上面統計符號的方法:只記錄不被括號包住的運算符^

    int cnt = 0, add = 0, mul = 0, power = 0;//cnt記錄括號個數
    for(int i = l; i <= r; i++){
        if(n[i] == '(')
            cnt++;
        else if(n[i] == ')')
            cnt--;//匹配掉前面的括號
        else if(!cnt){  
        //只記錄括號外的符號
            switch(n[i]){
            case '+':
            case '-':
                add = i;
            case '*':
            case '/':
                mul = i;
            case '^':
                power = i;
            }
        }
    }

如上所示,第一次記錄的符號就只有^了,也就是它會。

乘冪運算以後 我們再處理括號裏面的內容對吧?也就是遞歸到只剩下括號裏面的內容的時候:判斷一下當前表達式左右是否爲一對括號,是則去除括號,計算括號的內部:

        if(n[l] == '(' && n[r] == ')')
            return cale(l + 1, r - 1);

但是這個步驟不可以放在遞歸函數的開頭,否則樣例(2+2)^(1+1)的左右括號就會被當成一對給拆成2+2)^(1+1了!!

上述步驟應該只在當前表達式左右括號匹配的情況下才進行。也就是隻在只剩下一對括號包裹一個表達式的(2+2)的時候才進行。那麼如何判斷是否匹配?利用之前的符號記錄吧!因爲我們前面只記錄括號外面的符號,如果括號外面沒有符號,不就說明只剩下一個括號內的表達式了嗎?!:

    if(!cnt && !add && !mul && !power){
        //剩下的式子都包在括號裏,則去掉括號計算。注意這裏的代碼要放在符號記錄之後
        if(n[l] == '(' && n[r] == ')')
            return cale(l + 1, r - 1);
    }

哦哦!不過還存在一種沒有符號記錄且cnt==0的情況:當前表達式只剩下數字。

所以需要在這個條件語句裏面加進去一個語句來返回這個純數字(實際上這個函數纔是我們遞歸的deadline基線條件)

注意純數字有可能有多位,而我們使用的是字符串來儲存多位數字,不可直接返回。應該寫一個函數調用來處理多位數字。

int number(int left, int right){
//返回數組n的下標[left, right]段組成的數字
	int ans = 0;
	for(int i = left; i <= right; i++)
		ans = ans * 10 + n[i] - '0';//高位部分乘以10再加下一個位的數字,注意-'0'將字符n[i]從ascii碼轉爲數字
	return res;
}

程序是不是就完成了呢?我們最後再讀一遍題目確保一下沒有被漏掉的條件。哦哦!!居然還有可能出現多餘的括號。那就去掉多餘括號,就完事了!故記錄符號以後的括號處理完整代碼是這樣的:

    if(cnt > 0 && n[l] == '(')//去掉多餘的左括號
        return cale(l + 1, r);
    else if(cnt < 0 && n[r] == ')')//去掉多餘的右括號
        return cale(l, r - 1);
    else if(!cnt && !add && !mul && !power){
        if(n[l] == '(' && n[r] == ')') //剩下的式子完全包在括號裏
            return cale(l + 1, r - 1);
        return number(l, r);// 或者 只剩下一個數字
    }

思路小結與完整代碼

  1. 遍歷一遍表達式,記錄下括號的匹配情況cnt和括號外的三種優先級的運算符位置 add mul power
  2. 處理括號:
    a. 多餘的括號去掉,遞歸計算去掉以後的部分。
    b. 只剩下一串由括號包裹的表達式的時候也直接去掉括號,遞歸計算裏面的表達式。
    c. 注意 (cnt add mul power均爲0的時候) 這時內部剩下的表達式有可能只有一個數字,用一個函數返回這個數字。
  3. 按照第1步記錄的符號位置,遞歸計算剩下的內容
#include<bits/stdc++.h>
using namespace std;
string n;
int number(int left, int right){
    //返回字符串n中[left,right]段的數字
    int ans = 0;
    for(int i = left; i <= right; i++)
        ans = ans * 10 + n[i] - '0';
    return ans;
}
int cale(int l, int r){
    int cnt = 0, add = 0, mul = 0, power = 0;
    for(int i = l; i <= r; i++){
        if(n[i] == '(')
            cnt++;
        else if(n[i] == ')')
            cnt--;
        else if(!cnt){  //記錄括號外的最後一個符號位置,優先級不同的分開記錄
            switch(n[i]){
            case '+':
            case '-':
                add = i;
            case '*':
            case '/':
                mul = i;
            case '^':
                power = i;
            }
        }
    }
    if(cnt > 0 && n[l] == '(')//去掉多餘的括號
        return cale(l + 1, r);
    else if(cnt < 0 && n[r] == ')')
        return cale(l, r - 1);
    else if(!cnt && !add && !mul && !power){
        //剩下的式子包在括號裏或者只剩下數字,則獲取這個數字
        if(n[l] == '(' && n[r] == ')')
            return cale(l + 1, r - 1);
        return number(l, r);
    }
    if(add)
        if(n[add] == '+')
            return cale(l, add-1) + cale(add+1, r);
        else
            return cale(l, add-1) - cale(add+1, r);
    if(mul)
        if(n[mul] == '*')
            return cale(l, mul-1) * cale(mul+1, r);
        else
            return cale(l, mul-1) / cale(mul+1, r);
    if(power)
        return pow(cale(l, power-1), cale(power+1, r));
}
int main(){
    cin>>n;
    cout<<cale(0,n.length()-1)<<endl;
    return 0;
}

代碼到這裏其實已經可以ac了,但是在聽雨神講這道題的時候評論區裏討論了一下符號標誌到底能不能被初始化爲0。

符號標誌的意思是標記這個符號在字符串中的下標,所以應該被初始化爲一個不可能的位置。但邏輯上說運算符號是不會出現在第一位也就是n[0]的。但是考慮到負數出現在第一位的話,-這個符號可能作爲負號出現在n[0]。比如-2+1這樣的樣例上述代碼是無法正常計算的。俺的代碼能過只是運氣而已。

順帶一提上述代碼並不是遇到負數就掛了,而是遇到位於首位的負號會掛。因爲首位一旦出現負號,則將帶着負號進入number處理函數,故會出錯。但如果表達式帶的是中間的負數(如3+(-2)或者(-2)+1)會被當做表達式處理成0-2=-2。也是僥倖能過的一個原因吧

綜上所述,數組中不可能出現的下標還是標記爲-1吧 檢討.jpg

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章