面試題解法二:逆波蘭表達式計算'1 + (5 - 2) * 3'

来源:https://www.cnblogs.com/rynxiao/archive/2018/03/20/8605740.html
-Advertisement-
Play Games

昨天發了一個面試題: "關於一道面試題【字元串 '1 + (5 2) 3',怎麼算出結果為10,'eval'除外】" ,受到了各位大大的指點,用一個比較簡單的解法就能夠計算出來,因此自己在下班後按照各位的指點又實現了一遍,這裡貼出來供大家參考。 瞭解首碼、中綴、尾碼表達式 關於概念這裡簡單貼一下,想 ...


昨天發了一個面試題:關於一道面試題【字元串 '1 + (5 - 2) * 3',怎麼算出結果為10,'eval'除外】,受到了各位大大的指點,用一個比較簡單的解法就能夠計算出來,因此自己在下班後按照各位的指點又實現了一遍,這裡貼出來供大家參考。

瞭解首碼、中綴、尾碼表達式

關於概念這裡簡單貼一下,想瞭解更多的可以自行Google

  • 首碼表達式:是一種沒有括弧的算術表達式,與中綴表達式不同的是,其將運算符寫在前面,操作數寫在後面。為紀念其發明者波蘭數學家Jan Lukasiewicz,首碼表達式也稱為“波蘭式”。例如,- 1 + 2 3,它等價於1-(2+3)。
  • 中綴表達式:是一個通用的算術或邏輯公式表示方法, 操作符是以中綴形式處於操作數的中間(例:3 + 4),中綴表達式是人們常用的算術表示方法。
  • 尾碼表達式:指的是不包含括弧,運算符放在兩個運算對象的後面,所有的計算按運算符出現的順序,嚴格從左向右進行,尾碼表達式也稱為“逆波蘭式”。例如:1 2 3 4 + * + 5 +

註: 與首碼表達式(例:+ 3 4)或尾碼表達式(例:3 4 +)相比,中綴表達式不容易被電腦解析,但仍被許多程式語言使用,因為它符合人們的普遍用法。

中綴表達式如何轉換為尾碼表達式以及運算

一、 將中綴表達式轉換成尾碼表達式演算法:

  1. 從左至右掃描一中綴表達式。
  2. 若讀取的是操作數,則判斷該操作數的類型,並將該操作數存入操作數堆棧
  3. 若讀取的是運算符
  • 該運算符為左括弧"(",則直接存入運算符堆棧。
  • 該運算符為右括弧")",則輸出運算符堆棧中的運算符到操作數堆棧,直到遇到左括弧為止。
  • 該運算符為非括弧運算符:
    • 若運算符堆棧棧頂的運算符為括弧,則直接存入運算符堆棧。
    • 若比運算符堆棧棧頂的運算符優先順序高或相等,則直接存入運算符堆棧。
    • 若比運算符堆棧棧頂的運算符優先順序低或者優先順序相等,則輸出棧頂運算符到操作數堆棧,直到比運算符堆棧棧頂的運算符優先順序低或者為空時才將當前運算符壓入運算符堆棧。
  1. 當表達式讀取完成後運算符堆棧中尚有運算符時,則依序取出運算符到操作數堆棧,直到運算符堆棧為空。

二、逆波蘭表達式求值演算法:

  1. 迴圈掃描語法單元的項目。
  2. 如果掃描的項目是操作數,則將其壓入操作數堆棧,並掃描下一個項目。
  3. 如果掃描的項目是一個二元運算符,則對棧的頂上兩個操作數執行該運算。
  4. 如果掃描的項目是一個一元運算符,則對棧的最頂上操作數執行該運算。
  5. 將運算結果重新壓入堆棧。
  6. 重覆步驟2-5,堆棧中即為結果值。

看上面的概念我都看暈了,接下來以一個例子講解:

1 + 2 * (3 + 4) + 5
originArr      代表字元串轉化為數組之後的數組
operatorArr    代表運算符數組
reverseArr     代表尾碼表達式數組
下麵是一步一步的過程

originArr:   ["1","+","2","*","(","3","+","4",")","+","5"]

operatorArr: []
reverseArr:  ["1"]
 
operatorArr: ["+"]
reverseArr:  ["1"]

operatorArr: ["+"]
reverseArr:  ["1","2"]

operatorArr: ["+","*"]
reverseArr:  ["1","2"]

operatorArr: ["+","*","("]
reverseArr:  ["1","2"]

operatorArr: ["+","*","("]
reverseArr:  ["1","2","3"]

operatorArr: ["+","*","(","+"]
reverseArr:  ["1","2","3"]

operatorArr: ["+","*","(","+"]
reverseArr:  ["1","2","3","4"]

operatorArr: ["+","*"]
reverseArr:  ["1","2","3","4","+"]

operatorArr: ["+"]
reverseArr:  ["1","2","3","4","+","*","+"]

operatorArr: ["+"]
reverseArr:  ["1","2","3","4","+","*","+","5"]

operatorArr: []
reverseArr:  ["1","2","3","4","+","*","+","5","+"]

更多的可以參看小茗同學的這篇文章 或者 逆波蘭表達式

實現過程

這裡直接貼代碼,在代碼中有詳細的解析

const ADD = '+';                // 加常量
const SUB = '-';                // 減常量
const MUL = '*';                // 乘常量
const DIV = '/';                // 除常量
const MOD = '%';                // 取餘常量
const priorityMap = {
    '+': 1, 
    '-': 1,
    '*': 2,
    '/': 2,
    '%': 2
};

const str = '1 + 2';
const str2 = '1 + 2 - 3';
const str3 = '1 + 2 + 3 / 4';
const str4 = '1 + 2 + 3 / 4 % 5';
const str5 = '1 + 2 * (3 + 4) + 5';
const str6 = '(1 + 2) * (3 + 4) + 5';
const str7 = '((1 + 2) + 3 / (4 % 6)) * 6';

/**
 * 獲取逆波蘭數組
 * @param  string str 運算字元串
 * @return Array     逆波蘭數組
 */
function reversePolish(str) {
    str = str.replace(/\s*/g, '');
    const originArr = str.split('');
    // 保存最終逆波蘭數組的數組
    let reverseArr = [];
    // 保存運算符的數組
    let operatorArr = [];

    originArr.forEach(origin => {
        // 如果是數字,則直接push最終逆波蘭的數組
        if (!isNaN(Number(origin))) {
            reverseArr.push(origin);
        } else {
            // 如果運算符數組為空,說明還沒有遇到運算符
            // 直接push進棧
            if (operatorArr.length === 0) {
                operatorArr.push(origin);
            } else {
                // 進行比較,決定是入棧還是出棧
                // 1. '*/%'這三類因為具有最高優先順序,所以直接push
                // 2. '('因為不和誰進行比較,也可以直接push
                const originPriority = priorityMap[origin];
                if (originPriority === 2 || origin === '(') {
                    operatorArr.push(origin);
                } else {
                    // 獲取運算符中是否存在了'(',為後面的判斷作准備
                    const lastBracketIndex = operatorArr.lastIndexOf('(');
                    // 如果迴圈到了')',說明運算數組中必定存在一個'('
                    // 則直接截取從最後到'('的數組,直接push進返回結果的數組中
                    if (origin === ')') {
                        const includeLeftBracketArr = operatorArr.splice(lastBracketIndex).slice(1).reverse();
                        reverseArr = reverseArr.concat(includeLeftBracketArr);
                    } else {
                        // 否則,我只需要比較運算數組中最後一個運算符就好
                        // 如果迴圈出的運算符的優先順序大於或者等於最後一個運算符的優先順序,那麼直接push
                        const topOperator = operatorArr[operatorArr.length - 1];
                        if (originPriority >= priorityMap[topOperator]) {
                            operatorArr.push(origin);
                        } else {
                            // 否則,就需要判斷運算符數組中是否已經存在了'('
                            // 如果存在'(', 則我只需要截取到'('的數組就可以了
                            // 如果不存在,我只需要將整個運算符數組進行拼接就好,因為迴圈出來的運算符的優先順序肯定是小於或者等於運算符數組中的優先順序的
                            if (lastBracketIndex !== -1) {
                                const includeLeftBracketArr = operatorArr.splice(lastBracketIndex + 1).reverse();
                                reverseArr = reverseArr.concat(includeLeftBracketArr);
                            } else {
                                reverseArr = reverseArr.concat(operatorArr.reverse());
                                operatorArr = [];
                            }
                            // 最後,把這個運算符push進棧
                            operatorArr.push(origin);
                        }
                    }
                }
            }
        }
    });

    // 最後,如果運算符中還有運算符,進行拼接就好了
    if (operatorArr.length > 0) {
        reverseArr = reverseArr.concat(operatorArr.reverse());
        operatorArr = [];
    }

    return reverseArr;
}

/**
 * 真正的計算過程
 * @param  string left  左邊的數字字元串
 * @param  string right 右邊的數字字元串
 * @param  string opr   運算符
 * @return number       結果
 */
function cacl(left, right, opr) {
    left = Number(left);
    right = Number(right);
    switch(opr) {
        case MUL:
            return left * right;
        case DIV:
            return left / right;
        case MOD: 
            return left % right;
        case ADD:
            return left + right;
        case SUB:
            return left - right;
        default: 
            return 0;
    }
}


/**
 * 計算逆波蘭數組中的值
 * @param  string str 運算字元串
 * @return number     結果
 */
function myEval(str) {
    const reversePolishArr = reversePolish(str);
    const tempArr = [];
    reversePolishArr.forEach(origin => {
        // 數字直接push
        if (!isNaN(Number(origin))) {
            tempArr.push(origin);
        } else {
            // 如果遇到運算符,則pop出前兩個數
            // 根據運算符得出結果後再push
            const num1 = tempArr.pop();
            const num2 = tempArr.pop();
            const result = cacl(num2, num1, origin);
            tempArr.push(result);
        }
    });
    return tempArr[0];
}

console.time('myEval');
console.log('myEval: ',  myEval(str));
console.log('myEval: ',  myEval(str2));
console.log('myEval: ',  myEval(str3));
console.log('myEval: ',  myEval(str4));
console.log('myEval: ',  myEval(str5));
console.log('myEval: ',  myEval(str6));
console.log('myEval: ',  myEval(str7));
console.timeEnd('myEval')

console.time('eval');
console.log('eval: ',  eval(str));
console.log('eval: ',  eval(str2));
console.log('eval: ',  eval(str3));
console.log('eval: ',  eval(str4));
console.log('eval: ',  eval(str5));
console.log('eval: ',  eval(str6));
console.log('eval: ',  eval(str7));
console.timeEnd('eval')

運行時間:

runTime

因為換了臺電腦,所以原生eval對比上一篇文章中有比較大的影響,但是就時間的對比來說還是有接近3倍左右的差距。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • DOM操作:創建元素、查找元素、添加新元素、替換、移除、複製、移動 ...
  • <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scal ...
  • 一、script引入(聯繫使用,小型項目) 直接下載並用 <script> 標簽引入,Vue 會被註冊為一個全局變數。 <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js"></script> 二、Vue 提供一個官方命令 ...
  • $("body").on("click", ".submit_btn", function() { if (!$("#tel").val()) { alert("手機號不能為空"); $(this).attr("disabled", true); $(this).css({ background: ...
  • 最近閱讀《高性能JavaScript》時,在書中的“達夫設備“ 。 對此,有些感悟,同時有些疑問,希望看到的朋友,能幫忙解釋下,在此先提前感謝了。 1. 先說自己的理解吧: ”達夫設備“的目的是減少迭代次數,提高迴圈的效率,減少時間,提升性能。 感受:感覺代碼的優化,真的需要用工匠精神來雕琢,代碼的 ...
  • 本文原創 如轉載請註明出處!!! 本博客地址http://www.cnblogs.com/we-jack 本文原創,如果有同樣需求的小伙伴請第一時間聯繫我 或者在留言區留言 上次為大家提供了3D模型的展示之後 發現網上有很多想要計算3D模型錶面積和體積的需求 那麼經過掉了幾百根頭髮的艱辛歷程之後 終 ...
  • 最近使用websocket加ECharts做了一個實時監控的功能,發現了一個比較嚴重的問題,就是瀏覽器運行一段時間就會非常卡,之前在ECharts官網運行官方實例“動態數據 + 時間坐標軸”時,也遇到了同樣的情況,只是當時沒有當回事,現在來看原來是記憶體泄漏的問題。那麼是什麼原因導致的記憶體泄漏呢? 通 ...
  • 前端測試存在的問題 在講Sinon之前,我們得先講一下在學習了Mocha、chai以及enzyme之後,我們的前端測試還存在的一些問題。 比如前臺測試需要與後臺交互,獲取後臺數據後再根據相應數據進行測試。 又比如一個函數測試依賴另一個函數,我們可以根據測試的目的去模擬另一個函數,講兩者的測試分開,從 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...