面試題解法二:逆波蘭表達式計算'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
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...