近期從新開始學習計算機基礎方面的東西,好比計算機組成原理
、網絡原理
、編譯原理
之類的東西,目前正好在學習編譯原理
,開始對這一塊的東西感興趣,可是理論的學習有點枯燥無味,決定換種方式,那就是先實踐、遇到問題嘗試解決,用實踐推進理論。本來打算寫個中文JS解析的,可是好像有點難,須要慢慢實現,因而就找個簡單的來作,那就是解析一下四則運算,就有了這個項目,聲明:這是一個很簡單的項目,這是一個很簡單的項目,這是一個很簡單的項目。其中用到的詞法分析、語法分析、自動機都是用簡單的方式實現,畢竟比較菜。html
實現功能:前端
+-*/
正整數運算()
既然說很簡單,那無論用到的理論和實現的方式都必定要都很簡單,實現這個效果一共須要克服三個問題:git
*/()
的優先級大於+-
。+
後面好比跟隨數字
或者(
(這裏將-
看成操做,而不是符號)。若是沒有優先級問題,那實現一個計算十分的簡單,好比下面的代碼能夠實現一個簡單的加減或者乘除計算(10之內,超過一位數會遇到問題2,這裏先簡單一點,避過問題2):github
let calc = (input) => { let calMap = { '+': (num1, num2) => num1 + num2, '-': (num1, num2) => num1 - num2, '*': (num1, num2) => num1 * num2, '/': (num1, num2) => num1 / num2, } input = [...input].reverse() while (input.length >= 2) { let num1 = +input.pop() let op = input.pop() let num2 = +input.pop() input.push(calMap[op](num1, num2)) } return input[0] } expect(calc('1+2+3+4+5-1')).toEqual(14) expect(calc('1*2*3/3')).toEqual(2)
算法步驟:算法
將輸入打散成一個棧,由於是10之內的,因此每一個數只有一位:後端
input = [...input].reverse()
每次取出三位,若是是正確的輸入,則取出的三位,第一位是數字,第二位是操做符,第三位是數字:網絡
let num1 = +input.pop() let op = input.pop() let num2 = +input.pop()
根據操做符作運算後將結果推回棧中,又造成了這麼一個流程,一直到最後棧中只剩下一個數,或者說每次都要取出3個數,因此若是棧深度<=2,那就是最後的結果了:函數
while (input.length >= 2) { // ...... input.push(calMap[op](num1, num2)) }
動畫演示:單元測試
可是如今須要考慮優先級,好比*/
的優先級大於+-
,()
的運算符最高,那如何解決呢,其實都已經有解決方案了,我用的是後綴表達式
,也叫逆波蘭式
學習
1+1
表示成11+
。1+1
表示成+11
,這裏不作深刻逆波蘭式
能夠參考下列文章
在逆波蘭式子中,1+1*2
能夠轉化爲112*+
代碼演示:
let calc = (input) => { let calMap = { '+': (num1, num2) => num1 + num2, '-': (num1, num2) => num1 - num2, '*': (num1, num2) => num1 * num2, '/': (num1, num2) => num1 / num2, } input = [...input].reverse() let resultStack = [] while (input.length) { let token = input.pop() if (/[0-9]/.test(token)) { resultStack.push(token) continue } if (/[+\-*/]/.test(token)) { let num1 = +resultStack.pop() let num2 = +resultStack.pop() resultStack.push(calMap[token](num1, num2)) continue } } return resultStack[0] } expect(calc('123*+')).toEqual(7)
轉化以後計算步驟以下:
初始化一個棧
let resultStack = []
每次從表達式中取出一位
let token = input.pop()
若是是數字,則推入棧中
if (/[0-9]/.test(token)) { resultStack.push(token) continue }
若是是操做符,則從棧中取出兩個數,作相應的運算,再將結果推入棧中
if (/[+\-*/]/.test(token)) { let num1 = +resultStack.pop() let num2 = +resultStack.pop() resultStack.push(calMap[token](num1, num2)) continue }
若是表達式不爲空,進入步驟2,若是表達式空了,棧中的數就是最後的結果,計算完成
while (input.length) { // ... } return resultStack[0]
動畫演示:
轉化成逆波蘭式以後有兩個優勢:
(1+2)*(3+4)
,能夠轉化爲12+34+*
,按照逆波蘭式運算方法便可完成運算這是問題1的最後一個小問題了,這個問題的實現過程以下:
let parse = (input) => { input = [...input].reverse() let resultStack = [], opStack = [] while (input.length) { let token = input.pop() if (/[0-9]/.test(token)) { resultStack.push(token) continue } if (/[+\-*/]/.test(token)) { opStack.push(token) continue } } return [...resultStack, ...opStack.reverse()].join('') } expect(parse(`1+2-3+4-5`)).toEqual('12+3-4+5-')
準備兩個棧,一個棧存放結果,一個棧存放操做符,最後將兩個棧拼接起來上面的實現能夠將1+2-3+4-5
轉化爲12+3-4+5-
,可是若是涉及到優先級,就無能爲力了,例如
expect(parse(`1+2*3`)).toEqual('123*+')
1+2*3
的轉化結果應該是123*+
,但其實轉化的結果倒是123+*
,*/
的優先級高於+
,因此,應該作以下修改
let parse = (input) => { input = [...input].reverse() let resultStack = [], opStack = [] while (input.length) { let token = input.pop() if (/[0-9]/.test(token)) { resultStack.push(token) continue } // if (/[+\-*/]/.test(token)) { // opStack.push(token) // continue // } if (/[*/]/.test(token)) { while (opStack.length) { let preOp = opStack.pop() if (/[+\-]/.test(preOp)) { opStack.push(preOp) opStack.push(token) token = null break } else { resultStack.push(preOp) continue } } token && opStack.push(token) continue } if (/[+\-]/.test(token)) { while (opStack.length) { resultStack.push(opStack.pop()) } opStack.push(token) continue } } return [...resultStack, ...opStack.reverse()].join('') } expect(parse(`1+2`)).toEqual('12+') expect(parse(`1+2*3`)).toEqual('123*+')
*/
的時候,取出棧頂元素,判斷棧中的元素的優先級低是否低於*/
,若是是就直接將操做符推入opStack
,而後退出,不然一直將棧中取出的元素推入resultStack
。if (/[+\-]/.test(preOp)) { opStack.push(preOp)// 這裏用了棧來作判斷,因此判斷完還得還回去... opStack.push(token) token = null break }else { resultStack.push(preOp) continue }
token && opStack.push(token) continue
+-
的時候,由於已是最低的優先級了,因此直接將全部的操做符出棧就好了if (/[+\-]/.test(token)) { while (opStack.length) { resultStack.push(opStack.pop()) } opStack.push(token) continue }
到這裏已經解決了+-*/
的優先級問題,只剩下()
的優先級問題了,他的優先級是最高的,因此這裏作以下修改便可:
if (/[+\-]/.test(token)) { while (opStack.length) { let op=opStack.pop() if (/\(/.test(op)){ opStack.push(op) break } resultStack.push(op) } opStack.push(token) continue } if (/\(/.test(token)) { opStack.push(token) continue } if (/\)/.test(token)) { let preOp = opStack.pop() while (preOp !== '('&&opStack.length) { resultStack.push(preOp) preOp = opStack.pop() } continue }
+-
的時候,再也不無腦彈出,若是是(
就不彈出了while (opStack.length) { let op=opStack.pop() if (/\(/.test(op)){ opStack.push(op) break } resultStack.push(op) } opStack.push(token)
(
的時候,就推入opStack
if (/\(/.test(token)) { opStack.push(token) continue }
)
的時候,就持續彈出opStack
到resultStack
,直到遇到(
,(
不推入resultStack
if (/\)/.test(token)) { let preOp = opStack.pop() while (preOp !== '('&&opStack.length) { resultStack.push(preOp) preOp = opStack.pop() } continue }
完整代碼:
let parse = (input) => { input = [...input].reverse() let resultStack = [], opStack = [] while (input.length) { let token = input.pop() if (/[0-9]/.test(token)) { resultStack.push(token) continue } if (/[*/]/.test(token)) { while (opStack.length) { let preOp = opStack.pop() if (/[+\-]/.test(preOp)) { opStack.push(preOp) opStack.push(token) token = null break } else { resultStack.push(preOp) continue } } token && opStack.push(token) continue } if (/[+\-]/.test(token)) { while (opStack.length) { let op = opStack.pop() if (/\(/.test(op)) { opStack.push(op) break } resultStack.push(op) } opStack.push(token) continue } if (/\(/.test(token)) { opStack.push(token) continue } if (/\)/.test(token)) { let preOp = opStack.pop() while (preOp !== '(' && opStack.length) { resultStack.push(preOp) preOp = opStack.pop() } continue } } return [...resultStack, ...opStack.reverse()].join('')
動畫示例:
如此,就完成了中綴轉後綴了,那麼整個問題1就已經被解決了,經過calc(parse(input))
就能完成中綴=>後綴=>計算
的整個流程了。
雖然上面已經解決了中綴=>後綴=>計算
的大問題,可是最基礎的問題還沒解決,那就是輸入問題,在上面問題1的解決過程當中,輸入不過是簡單的切割,並且還侷限在10之內。而接下來,要解決的就是這個輸入的問題,如何分割輸入,達到要求?
解決方式1:正則,雖然正則能夠作到以下,作個簡單的demo
仍是能夠的,可是對於以後的語法檢測之類的東西不太有利,因此不太好,我放棄了這種方法
(1+22)*(333+4444)`.match(/([0-9]+)|([+\-*/])|(\()|(\))/g) // 輸出 // (11) ["(", "1", "+", "22", ")", "*", "(", "333", "+", "4444", ")"]
解決方法2:逐個字符分析,其大概的流程是
while(input.length){ let token = input.pop() if(/[0-9]/.test(token)) // 進入數字分析 if(/[+\-*/\(\)]/.test(token))// 進入符號分析 }
接下來試用解決方案2來解決這個問題:
當咱們分割的時候,並不單純保存值,而是將每一個節點保存成一個類似的結構,這個結構可使用對象表示:
{ type:'', value:'' }
其中,type
是節點類型,能夠將四則運算中全部可能出現的類型概括出來,個人概括以下:
TYPE_NUMBER: 'TYPE_NUMBER', // 數字 TYPE_LEFT_BRACKET: 'TYPE_LEFT_BRACKET', // ( TYPE_RIGHT_BRACKET: 'TYPE_RIGHT_BRACKET', // ) TYPE_OPERATION_ADD: 'TYPE_OPERATION_ADD', // + TYPE_OPERATION_SUB: 'TYPE_OPERATION_SUB', // - TYPE_OPERATION_MUL: 'TYPE_OPERATION_MUL', // * TYPE_OPERATION_DIV: 'TYPE_OPERATION_DIV', // /
value
則是對應的真實值,好比123
、+
、-
、*
、/
。
若是是數字,則繼續往下讀,直到不是數字爲止,將這過程中全部的讀取結果放到value
中,最後入隊。
if (token.match(/[0-9]/)) { let next = tokens.pop() while (next !== undefined) { if (!next.match(/[0-9]/)) break token += next next = tokens.pop() } result.push({ type: type.TYPE_NUMBER, value: +token }) token = next }
先定義一個符號和類型對照表,若是不在表中,說明是異常輸入,拋出異常,若是取到了,說明是正常輸入,入隊便可。
const opMap = { '(': type.TYPE_LEFT_BRACKET, ')': type.TYPE_RIGHT_BRACKET, '+': type.TYPE_OPERATION_ADD, '-': type.TYPE_OPERATION_SUB, '*': type.TYPE_OPERATION_MUL, '/': type.TYPE_OPERATION_DIV } let type = opMap[token] if (!type) throw `error input: ${token}` result.push({ type, value: token, })
這樣就完成了輸入的處理,這時候,其餘的函數也須要處理一下,應爲輸入已經從字符串變成了tokenize
以後的序列了,修改完成以後就是能夠calc(parse(tokenize()))
完成一整套騷操做了。
語法檢測要解決的問題其實就是判斷輸入的正確性,是否知足四則運算的規則,這裏用了相似狀機的思想,不過簡單到爆炸,而且只能作單步斷定~~
定義一個語法表,該表定義了一個節點後面能夠出現的節點類型,好比,+
後面只能出現數字
或者(
之類。
let syntax = { [type.TYPE_NUMBER]: [ type.TYPE_OPERATION_ADD, type.TYPE_OPERATION_SUB, type.TYPE_OPERATION_MUL, type.TYPE_OPERATION_DIV, type.TYPE_RIGHT_BRACKET ], [type.TYPE_OPERATION_ADD]: [ type.TYPE_NUMBER, type.TYPE_LEFT_BRACKET ], [type.TYPE_OPERATION_SUB]: [ type.TYPE_NUMBER, type.TYPE_LEFT_BRACKET ], [type.TYPE_OPERATION_MUL]: [ type.TYPE_NUMBER, type.TYPE_LEFT_BRACKET ], [type.TYPE_OPERATION_DIV]: [ type.TYPE_NUMBER, type.TYPE_LEFT_BRACKET ], [type.TYPE_LEFT_BRACKET]: [ type.TYPE_NUMBER, type.TYPE_LEFT_BRACKET ], [type.TYPE_RIGHT_BRACKET]: [ type.TYPE_OPERATION_ADD, type.TYPE_OPERATION_SUB, type.TYPE_OPERATION_MUL, type.TYPE_OPERATION_DIV, type.TYPE_RIGHT_BRACKET ] }
這樣咱們就能夠簡單的使用下面的語法斷定方法了:
while (tokens.length) { // ... let next = tokens.pop() if (!syntax[token.type].includes(next.type)) throw `syntax error: ${token.value} -> ${next.value}` // ... }
對於()
,這裏使用的是引用計數,若是是(
,則計數+1
,若是是)
,則計數-1
,檢測到最後的時候斷定一下計數就行了:
// ... if (token.type === type.TYPE_LEFT_BRACKET) { bracketCount++ } // ... if (next.type === type.TYPE_RIGHT_BRACKET) { bracketCount-- } // ... if (bracketCount < 0) { throw `syntax error: toooooo much ) -> )` } // ...
該文章存在一些問題:
該實現也存在一些問題:
思考:
()
的處理或許可使用遞歸的方式,進入()
以後從新開始一個新的表達式解析總之:文章到此爲止,有不少不夠詳細的地方還請見諒,多多交流,共同成長。