近期从新开始学习计算机基础方面的东西,好比计算机组成原理
、网络原理
、编译原理
之类的东西,目前正好在学习编译原理
,开始对这一块的东西感兴趣,可是理论的学习有点枯燥无味,决定换种方式,那就是先实践、遇到问题尝试解决,用实践推进理论。本来打算写个中文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 ) -> )` } // ...
该文章存在一些问题:
该实现也存在一些问题:
思考:
()
的处理或许可使用递归的方式,进入()
以后从新开始一个新的表达式解析总之:文章到此为止,有不少不够详细的地方还请见谅,多多交流,共同成长。