拆解複雜問題:實現一個計算器

咱們最終要實現的計算器功能以下:python

一、輸入一個字符串,能夠包含+ - * /、數字、括號以及空格,你的算法返回運算結果。git

二、要符合運算法則,括號的優先級最高,先乘除後加減。算法

三、除號是整數除法,不管正負都向 0 取整(5/2=2,-5/2=-2)。編程

四、能夠假定輸入的算式必定合法,且計算過程不會出現整型溢出,不會出現除數爲 0 的意外狀況。數據結構

好比輸入以下字符串,算法會返回 9:app

3 * (2-6 /(3 -7))框架

能夠看到,這就已經很是接近咱們實際生活中使用的計算器了,雖然咱們之前確定都用過計算器,可是若是簡單思考一下其算法實現,就會大驚失色:編程語言

一、按照常理處理括號,要先計算最內層的括號,而後向外慢慢化簡。這個過程咱們手算都容易出錯,況且寫成算法呢!ide

二、要作到先乘除,後加減,這一點教會小朋友還不算難,但教給計算機恐怕有點困難。函數

三、要處理空格。咱們爲了美觀,習慣性在數字和運算符之間打個空格,可是計算之中得想辦法忽略這些空格。

我記得不少大學數據結構的教材上,在講棧這種數據結構的時候,應該都會用計算器舉例,可是有一說一,講的真的垃圾,不知道多少將來的計算機科學家就被這種簡單的數據結構勸退了。

那麼本文就來聊聊怎麼實現上述一個功能完備的計算器功能,關鍵在於層層拆解問題,化整爲零,逐個擊破,相信這種思惟方式能幫你們解決各類複雜問題。

下面就來拆解,從最簡單的一個問題開始。

1、字符串轉整數

是的,就是這麼一個簡單的問題,首先告訴我,怎麼把一個字符串形式的整數,轉化成 int 型?

string s = "458";

int n = 0;
for (int i = 0; i < s.size(); i++) {
    char c = s[i];
    n = 10 * n + (c - '0');
}
// n 如今就等於 458

這個仍是很簡單的吧,老套路了。可是即使這麼簡單,依然有坑:(c - '0')的這個括號不能省略,不然可能形成整型溢出

由於變量c是一個 ASCII 碼,若是不加括號就會先加後減,想象一下s若是接近 INT_MAX,就會溢出。因此用括號保證先減後加才行。

2、處理加減法

如今進一步,若是輸入的這個算式只包含加減法,並且不存在空格,你怎麼計算結果?咱們拿字符串算式1-12+3爲例,來講一個很簡單的思路:

一、先給第一個數字加一個默認符號+,變成+1-12+3

二、把一個運算符和數字組合成一對兒,也就是三對兒+1-12+3,把它們轉化成數字,而後放到一個棧中。

三、將棧中全部的數字求和,就是原算式的結果。

PS:我認真寫了 100 多篇原創,手把手刷 200 道力扣題目,所有發佈在 labuladong的算法小抄,持續更新。建議收藏,按照個人文章順序刷題,掌握各類算法套路後投再入題海就如魚得水了。

咱們直接看代碼,結合一張圖就看明白了:

int calculate(string s) {
    stack<int> stk;
    // 記錄算式中的數字
    int num = 0;
    // 記錄 num 前的符號,初始化爲 +
    char sign = '+';
    for (int i = 0; i < s.size(); i++) {
        char c = s[i];
        // 若是是數字,連續讀取到 num
        if (isdigit(c)) 
            num = 10 * num + (c - '0');
        // 若是不是數字,就是遇到了下一個符號,
        // 以前的數字和符號就要存進棧中
        if (!isdigit(c) || i == s.size() - 1) {
            switch (sign) {
                case '+':
                    stk.push(num); break;
                case '-':
                    stk.push(-num); break;
            }
            // 更新符號爲當前符號,數字清零
            sign = c;
            num = 0;
        }
    }
    // 將棧中全部結果求和就是答案
    int res = 0;
    while (!stk.empty()) {
        res += stk.top();
        stk.pop();
    }
    return res;
}

我估計就是中間帶switch語句的部分有點很差理解吧,i就是從左到右掃描,signnum跟在它身後。當s[i]遇到一個運算符時,狀況是這樣的:

因此說,此時要根據sign的 case 不一樣選擇nums的正負號,存入棧中,而後更新sign並清零nums記錄下一對兒符合和數字的組合。

另外注意,不僅是遇到新的符號會觸發入棧,當i走到了算式的盡頭(i == s.size() - 1),也應該將前面的數字入棧,方便後續計算最終結果。

至此,僅處理緊湊加減法字符串的算法就完成了,請確保理解以上內容,後續的內容就基於這個框架修修改改就完事兒了。

3、處理乘除法

其實思路跟僅處理加減法沒啥區別,拿字符串2-3*4+5舉例,核心思路依然是把字符串分解成符號和數字的組合。

好比上述例子就能夠分解爲+2-3*4+5幾對兒,咱們剛纔不是沒有處理乘除號嗎,很簡單,其餘部分都不用變,在switch部分加上對應的 case 就好了:

for (int i = 0; i < s.size(); i++) {
    char c = s[i];
    if (isdigit(c)) 
        num = 10 * num + (c - '0');

    if (!isdigit(c) || i == s.size() - 1) {
        switch (sign) {
            int pre;
            case '+':
                stk.push(num); break;
            case '-':
                stk.push(-num); break;
            // 只要拿出前一個數字作對應運算便可
            case '*':
                pre = stk.top();
                stk.pop();
                stk.push(pre * num);
                break;
            case '/':
                pre = stk.top();
                stk.pop();
                stk.push(pre / num);
                break;
        }
        // 更新符號爲當前符號,數字清零
        sign = c;
        num = 0;
    }
}

乘除法優先於加減法體如今,乘除法能夠和棧頂的數結合,而加減法只能把本身放入棧

如今咱們思考一下如何處理字符串中可能出現的空格字符。其實也很是簡單,想一想空格字符的出現,會影響咱們現有代碼的哪一部分?

// 若是 c 非數字
if (!isdigit(c) || i == s.size() - 1) {
    switch (c) {...}
    sign = c;
    num = 0;
}

顯然空格會進入這個 if 語句,可是咱們並不想讓空格的狀況進入這個 if,由於這裏會更新sign並清零nums,空格根本就不是運算符,應該被忽略。

那麼只要多加一個條件便可:

if ((!isdigit(c) && c != ' ') || i == s.size() - 1) {
    ...
}

好了,如今咱們的算法已經能夠按照正確的法則計算加減乘除,而且自動忽略空格符,剩下的就是如何讓算法正確識別括號了。

4、處理括號

處理算式中的括號看起來應該是最難的,但真沒有看起來那麼難。

爲了規避編程語言的繁瑣細節,我把前面解法的代碼翻譯成 Python 版本:

def calculate(s: str) -> int:

    def helper(s: List) -> int:
        stack = []
        sign = '+'
        num = 0

        while len(s) > 0:
            c = s.pop(0)
            if c.isdigit():
                num = 10 * num + int(c)

            if (not c.isdigit() and c != ' ') or len(s) == 0:
                if sign == '+':
                    stack.append(num)
                elif sign == '-':
                    stack.append(-num)
                elif sign == '*':
                    stack[-1] = stack[-1] * num
                elif sign == '/':
                    # python 除法向 0 取整的寫法
                    stack[-1] = int(stack[-1] / float(num))                    
                num = 0
                sign = c

        return sum(stack)
    # 須要把字符串轉成列表方便操做
    return helper(list(s))

這段代碼跟剛纔 C++ 代碼徹底相同,惟一的區別是,不是從左到右遍歷字符串,而是不斷從左邊pop出字符,本質仍是同樣的。

那麼,爲何說處理括號沒有看起來那麼難呢,由於括號具備遞歸性質。咱們拿字符串3*(4-5/2)-6舉例:

calculate(3*(4-5/2)-6)
= 3 * calculate(4-5/2) - 6
= 3 * 2 - 6
= 0

能夠腦補一下,不管多少層括號嵌套,經過 calculate 函數遞歸調用本身,均可以將括號中的算式化簡成一個數字。換句話說,括號包含的算式,咱們直接視爲一個數字就好了

如今的問題是,遞歸的開始條件和結束條件是什麼?遇到(開始遞歸,遇到)結束遞歸

def calculate(s: str) -> int:

    def helper(s: List) -> int:
        stack = []
        sign = '+'
        num = 0

        while len(s) > 0:
            c = s.pop(0)
            if c.isdigit():
                num = 10 * num + int(c)
            # 遇到左括號開始遞歸計算 num
            if c == '(':
                num = helper(s)

            if (not c.isdigit() and c != ' ') or len(s) == 0:
                if sign == '+': ...
                elif sign == '-': ... 
                elif sign == '*': ...
                elif sign == '/': ...
                num = 0
                sign = c
            # 遇到右括號返回遞歸結果
            if c == ')': break
        return sum(stack)

    return helper(list(s))

你看,加了兩三行代碼,就能夠處理括號了,這就是遞歸的魅力。至此,計算器的所有功能就實現了,經過對問題的層層拆解化整爲零,再回頭看,這個問題彷佛也沒那麼複雜嘛。

PS:我認真寫了 100 多篇原創,手把手刷 200 道力扣題目,所有發佈在 labuladong的算法小抄,持續更新。建議收藏,按照個人文章順序刷題,掌握各類算法套路後投再入題海就如魚得水了。

5、最後總結

本文借實現計算器的問題,主要想表達的是一種處理複雜問題的思路。

咱們首先從字符串轉數字這個簡單問題開始,進而處理只包含加減法的算式,進而處理包含加減乘除四則運算的算式,進而處理空格字符,進而處理包含括號的算式。

可見,對於一些比較困難的問題,其解法並非一蹴而就的,而是步步推動,螺旋上升的。若是一開始給你原題,你不會作,甚至看不懂答案,都很正常,關鍵在於咱們本身如何簡化問題,如何以退爲進。

退而求其次是一種很聰明策略。你想一想啊,假設這是一道考試題,你不會實現這個計算器,可是你寫了字符串轉整數的算法並指出了容易溢出的陷阱,那起碼能夠得 20 分吧;若是你可以處理加減法,那能夠得 40 分吧;若是你能處理加減乘除四則運算,那起碼夠 70 分了;再加上處理空格字符,80 有了吧。我就是不會處理括號,那就算了,80 已經很 OK 了好很差。

相關文章
相關標籤/搜索