中缀表达式转后缀表达式两位数_[数据结构]表达式树——手动eval()

08cf16cc9168d20ad8c7159677c41395.png

0x00 Prologue

过春节啦!祝大家新年快乐哇~有一段时间没有写文章了,前几天正好把寒假的作业写了一些,今天更一篇依旧是介绍树形数据结构的文章吧....这次看一个没有Zkw线段树那么复杂的数据结构——表达式树(Expression Tree)。

本篇文章将对:

  1. 表达式树的工作原理
  2. 表达式树建立步骤
  3. 表达式树求值

进行介绍。文章代码使用JavaScript(最近某个项目需要学习,正好用一用:P),但是这个语言吧语法还是非常简单易懂的...并且我会在需要解释的地方打上注释:D。

另外,文章会涉及一点非常简单的词法分析器的介绍。


0x01 表达式树能干啥?长啥样?

Q: 给定一个合法的表达式字符串"(1+2)*(5-3)",计算表达式的值。

<del>A: eval()!!(拉出去打死</del>

如果使用一些没有eval()的语言怎么搞?用表达式树!

<del>建一下,玩一年,只要998,年底清仓大甩卖,表达式树带回家</del>

表达式树就是将一个表达式解析成一棵满二叉树(如果所有合法操作符都是进行二元计算),且所有非叶结点都是符号结点(+-*/),叶子结点都是数字结点。对于(1+2)*(5-3)我们就可以构造出一棵这样的表达式树:

4ed07d7402f990527251fdaf12c14ef3.png
(1+2)*(5-3)的表达式树

可以看出来,对表达式树进行后序遍历的结果就是原表达式的逆波兰式(后缀表达式)。对表达式树进行中序遍历的结果就是我们所常见表达式(除去括号)。


0x02 从字符串建立表达式树

0. 规定表达式组成成分

这里为了简化理解,就不介绍产生式了...这一部分就是规定什么能出现在将被evaluate的字符串中,定义符号集以及对应的Token。这里我们的计算器支持基础四则运算,取模以及位运算(and, or, xor, shl, shr),这些符号的token我们定义为OPERATOR;除了符号还有参与运算的数字, 我们将数字分为两种:INTEGERDOUBLE。那么我们就可以定义一个这样的类:

const TokenType = {
    OPERATOR: 'OPERATOR', 
    INT: 'INTEGER', 
    DOUBLE: 'DECIMAL',
}
// 这里是可以作为关键字开头的符号表
// 这样做为了方便之后判断Token...如果有更好的实现方法
// 求大佬们赐教
const operator_list = {
    '+': 0, '-': 0, '*': 0, '/': 0, '%': 0,
    '&': 0, '|': 0, '^': 0, '**': 0, '<': 0, '>': 0, '~': 0, '(': 0, ')': 0
}

class Token{
    constructor(value, tag){
        this.value = value; // 对应值
        this.tag = tag; // Token类型(TokenType)
    }
}
  1. 词法分析

为了方便建树时识别每个符号的含义,我们首先对表达式字符串进行词法分析。这个过程会将字符串中的元素拆分成一个一个的Token。例如+是一个运算符,我们可以定义为OPERATOR。我们将编写一个简单的词法分析器(Lexer)对表达式字符串进行词法分析。

c61d81a943f1c03b9828652e155f2c39.png
Lexer的作用

(上图是对表达式(1+2)*3.5进行词法分析后的结果。 )

实现一个Lexer并不难,我们只需要将字符串从左向右扫一遍,将不同类别的词素分离开即可。例如:当前位置的符号是>,那么我们可以猜测这有可能是shr符号;这时向后看一位,如果是>那么就可以确定这是要shr了 。

为了方便我们实现这个Lexer,首先我们写一个Reader类,用于逐位读取一个可以用下标操作符的对象(用于读取输入的表达式和之后的Token列表):

class Reader{ // Reader 职阶
    constructor(input_data){
        this.seq = input_data;
        this.cursor = 0;
        this.urng = input_data.length;
    }
    next() {
        return this.seq[this.cursor++]; // 返回当前位置的字符并将游标向后移动1
    }
    cursor_data() {
        return this.seq[this.cursor] // 返回当前游标所指的数据
    }
    has_next() {
        return this.cursor < this.urng; // 调用next()是否还有数据
    }
    peek() {
        return this.seq[this.cursor + 1]; // 向后看一位
    }
}

写好Reader类之后,我们再实现Lexer:

var reader;

class Lexer{ // Lexer 职阶
    constructor(input_expreesion){
        reader = new Reader(input_expreesion);
    }
    parse(){
        var token_list = []; // 保存解析出的Token,Token列表
        let last_token = () => {return token_list[token_list.length - 1]}; // 返回上一个解析的Token
        let is_digit = (x) => {return x >= '0' && x <= '9'};
        let is_escape = (x) => {return x === ' ' || x === 'n' || x === 't'};
        function readNum() {
            var ans = {
                v: '', t: TokenType.INT
            };
            // 读取剩余的数字或小数点
            while(reader.has_next() && (is_digit(reader.cursor_data()) || reader.cursor_data() === '.' || is_escape(reader.cursor_data()))){
                if(is_escape(reader.cursor_data())) {
                    // 感谢@被子飞了提醒...这里需要把cursor向后移动一位
                    reader.next();
                    continue;
                };
                ans.v += reader.next();
            }
            if (reader.has_next() && reader.cursor_data() === 'e' && (reader.peek() === '-' || reader.peek() === '+')){
                ans.v += reader.next() + reader.next();
                while(reader.has_next() && is_digit(reader.cursor_data())){
                    ans.v += reader.next();
                }
            } // 科学计数
            if(ans.v.search('.') !== -1 || ans.v.search('e-') !== -1){
                ans.t = TokenType.DOUBLE;
            }
            return ans;
        }
        while (reader.has_next()){
            var cur = reader.next();
            if(is_escape(cur)) continue;
            if(is_digit(cur)) {
                var ret = readNum();
                token_list.push(new Token(cur + ret.v, ret.t));
            }else if(cur in operator_list){
                var prev = last_token();
                if(typeof prev !== 'undefined') prev = prev.tag;
                if(cur === '-'){ // 分两种情况,1. 负数 2. 减法
                    if (typeof prev !== 'undefined' && (prev === TokenType.INT || prev === TokenType.DOUBLE)){
                        token_list.push(new Token(cur, TokenType.OPERATOR)); // 作为运算符处理
                    }else{
                        var ret = readNum();
                        cur += ret.v;
                        token_list.push(new Token(cur, ret.t));
                    }
                }else if(cur === '~'){
                    var ret = readNum();
                    token_list.push(new Token(cur + ret.v, TokenType.INT));
                }else if((cur === '*' || cur === '<' || cur === '>') && reader.has_next() && reader.cursor_data() === cur){
                    cur += reader.next() // 当前位置和上一个位置是同一个Operator,两个组合成指定Operator(** << >>)
                    token_list.push(new Token(cur, TokenType.OPERATOR));
                }else{
                    token_list.push(new Token(cur, TokenType.OPERATOR));
                }
            }else{
                throw "Error character @ " + (reader.cursor - 1) + " " + cur;
            }
        }
        return token_list;
    }
}

至此,一个简单的Lexer就编写完毕了。可以通过console.log(new Lexer("(1+2)*3"))看看解析是否正确。

e.g: (1.25+3e-2+1e+3)*5

61f7fd481966ab2d4376bf7b5b08174b.png
Tokenize: (1.25+3e-2+1e+3)*5

0x03 通过Token列表建立表达式树 & 求值

这时我们的Reader又派上用场了<del>(上吧!Reader,ATK +450↑)</del>。我们建立表达式树的时候也需要从左向右扫一遍我们的Token们。我们需要两个栈(Stack)存储两类数据:

  • Number Stack (数字栈)
  • Operator Stack (符号栈)

事实上,这里的数字栈并不只存数字,而是存可被求值的结点

为了方便之后的求值,我们定义两种表达式树节点:

  • 常数结点
  • 操作符结点

其中常数结点存储一个单独的值value和一个求值方法eval()(直接返回value);操作符结点分为opfi,和se,分别是操作符,左表达式,右表达式。注意左右表达式既可以是操作符结点也可以是常数结点。操作符结点也有一个求值方法eval(),判断op并返回相应值:

常数结点:

class ConstNode{
    constructor(v){
        this.value = v;
    }
    eval(){
        return this.value;
    }
}

操作符结点:

class OperatorNode{
    constructor(op, fi, se){
        this.op = op;
        this.fi = fi;
        this.se = se;
    }
    eval(){
        switch(this.op){
            case '+':
                return this.fi.eval() + this.se.eval();
            case '-':
                return this.fi.eval() - this.se.eval();
            case '*':
                return this.fi.eval() * this.se.eval();
            case '/':
                return this.fi.eval() / this.se.eval();
            case '%':
                return this.fi.eval() % this.se.eval();
            case '**':
                return this.fi.eval() ** this.se.eval();
            case '<<':
                return this.fi.eval() << this.se.eval();
            case '>>':
                return this.fi.eval() >> this.se.eval();
            case '&':
                return this.fi.eval() & this.se.eval();
            case '|':
                return this.fi.eval() | this.se.eval();
            case '^':
                return this.fi.eval() ^ this.se.eval();
            default:
                break;
        }
    }
}

(Tips: 这里使两种结点求值方法名相同就是为了方便整棵树的求值)

我们定义一个addNode()函数,这个函数会从数字栈中取出栈顶的两个元素,然后再从符号栈中取出栈顶符号,最后将刚刚取出的符号和两个表达式元素放入一个符号结点,加入到数字栈当中作为一个新的“数字”(可被求值对象)。

当从左向右扫描的时候如果遇到的Token是INTEGER或者DOUBLE,我们就直接将其处理一下(parseInt()parseFloat())放入数字栈中;如果遇到的是OPERATOR,我们按照运算优先级进行操作。这里不需要明确写出运算优先级,我们只需要一直执行addNode()直到遇到了比当前运算符优先级低的符号为止(比如遇到了*/,那么我们就一直执行addNode()直到遇到+, -, &, |, ^),最后再把当前运算符放入符号栈。

class ExpressionTreeConstructor{
    constructor(tokenList){
        reader = new Reader(tokenList);
        this.operator = []; // 符号栈
        this.num = []; // 数字栈
    }
    addNode(){
        var se = this.num.pop(); // LIFO, 第二个在前面
        var fi = new ConstNode(0);
        if(this.num.length !== 0){
            fi = this.num.pop();
        }
        var op = this.operator.pop(); // 取出栈顶符号
        this.num.push(new OperatorNode(op, fi, se)); // 将三个元素打包成符号结点放入数字栈中
    }
    get opTop(){
        return this.operator[this.operator.length - 1]
    }
    build_tree(){
        while(reader.has_next()){
            var cur = reader.next();
            console.log(this.num, cur.value);
            switch (cur.tag) {
                case TokenType.INT:
                    if (cur.value.startsWith('~')) {
                        this.num.push(new ConstNode(~parseInt(cur.value.slice(1))));
                    } else {
                        this.num.push(new ConstNode(parseInt(cur.value)));
                    }
                    break;
                case TokenType.DOUBLE:
                    if (cur.value.startsWith('~')) { // 这个应该没啥意义...
                        this.num.push(new ConstNode(~parseFloat(cur.value.slice(1))));
                    } else {
                        this.num.push(new ConstNode(parseFloat(cur.value)));
                    }
                    break;
                case TokenType.OPERATOR:
                    switch(cur.value){
                        case '(':
                            this.operator.push('('); // 这个符号优先级最低,直接加入符号栈
                            break;
                        case ')':
                            while(this.operator.length && this.opTop !== '('){ // 将括号之间的全都处理一遍
                                this.addNode();
                            }
                            this.operator.pop(); // 弹掉栈顶的'('
                            break;
                        case '%':
                        case '**':
                            while (this.operator.length && this.opTop === cur.value) {
                                // 取模与exponential比其他运算符优先级高,所以遇到这两个符号时只能处理剩余的取模和exp
                                this.addNode(); 
                            }
                            this.operator.push(cur.value); // 将当前符号加入符号栈
                            break;
                        case '*':
                        case '/':
                            while (this.operator.length && (this.opTop === '*' || this.opTop === '/')) {
                                this.addNode(); // 乘除
                            }
                            this.operator.push(cur.value);
                            break;
                        case '+':
                        case '-':
                            while (this.operator.length && this.opTop !== '(' && this.opTop !== '|' && this.opTop !== '&' && this.opTop !== '^') {
                                // 处理到这些符号就停止,位运算符号优先级低,不能同时加入树中
                                this.addNode();
                            }
                            this.operator.push(cur.value);
                            break;
                        case '|':
                        case '&':
                        case '^':
                        case '<<':
                        case '>>':
                            while(this.operator.length && this.opTop !== '('){
                                this.addNode();
                            }
                            this.operator.push(cur.value);
                            break;
                        default:
                            throw "Error Operator " + cur.value;
                    }
                default:
                    break;
            }
        }
        while(this.operator.length){
            this.addNode(); // 用剩余的结点把数字栈里解析好的结点组合起来
        }
        return this.num[0]; // 最后返回根结点
    }
}

我们可以来看一下(1.25+3e-2+1e+3)*5的建树结果:

e5eaccc5b0662ede9e021b1a025b20d3.png
对(1.25+3e-2+1e+3)*5建立表达式树

求值就非常简单了,我们已经定义了eval()函数,只需要再调用一下就行。

至此我们的表达式树从简单的词法分析到建立就全部完成了。完整代码如下:

const TokenType = {
    OPERATOR: 'OPERATOR', 
    INT: 'INTEGER', 
    DOUBLE: 'DECIMAL',
}

const operator_list = {
    '+': 0, '-': 0, '*': 0, '/': 0, '%': 0,
    '&': 0, '|': 0, '^': 0, '**': 0, '<': 0, '>': 0, '~': 0, '(': 0, ')': 0
}

class Token{
    constructor(value, tag){
        this.value = value;
        this.tag = tag;
    }
}

class Reader{
    constructor(input_data){
        this.seq = input_data;
        this.cursor = 0;
        this.urng = input_data.length;
    }
    next() {
        return this.seq[this.cursor++];
    }
    cursor_data() {
        return this.seq[this.cursor]
    }
    has_next() {
        return this.cursor < this.urng;
    }
    peek() {
        return this.seq[this.cursor + 1];
    }
}

var reader;

class Lexer{
    constructor(input_expreesion){
        reader = new Reader(input_expreesion);
    }
    parse(){
        var token_list = [];
        let last_token = () => {return token_list[token_list.length - 1]};
        let is_digit = (x) => {return x >= '0' && x <= '9'};
        let is_escape = (x) => {return x === ' ' || x === 'n' || x === 't'};
        function readNum() {
            var ans = {
                v: '', t: TokenType.INT
            };
            while(reader.has_next() && (is_digit(reader.cursor_data()) || reader.cursor_data() === '.' || is_escape(reader.cursor_data()))){
                if(is_escape(reader.cursor_data())) continue;
                ans.v += reader.next();
            }
            if (reader.has_next() && reader.cursor_data() === 'e' && (reader.peek() === '-' || reader.peek() === '+')){
                ans.v += reader.next() + reader.next();
                while(reader.has_next() && is_digit(reader.cursor_data())){
                    ans.v += reader.next();
                }
            }
            if(ans.v.search('.') !== -1 || ans.v.search('e-') !== -1){
                ans.t = TokenType.DOUBLE;
            }
            return ans;
        }
        while (reader.has_next()){
            var cur = reader.next();
            if(is_escape(cur)) continue;
            if(is_digit(cur)) {
                var ret = readNum();
                token_list.push(new Token(cur + ret.v, ret.t));
            }else if(cur in operator_list){
                var prev = last_token();
                if(typeof prev !== 'undefined') prev = prev.tag;
                if(cur === '-'){
                    if (typeof prev !== 'undefined' && (prev === TokenType.INT || prev === TokenType.DOUBLE)){
                        token_list.push(new Token(cur, TokenType.OPERATOR));
                    }else{
                        var ret = readNum();
                        cur += ret.v;
                        token_list.push(new Token(cur, ret.t));
                    }
                }else if(cur === '~'){
                    var ret = readNum();
                    token_list.push(new Token(cur + ret.v, TokenType.INT));
                }else if((cur === '*' || cur === '<' || cur === '>') && reader.has_next() && reader.cursor_data() === cur){
                    cur += reader.next()
                    token_list.push(new Token(cur, TokenType.OPERATOR));
                }else{
                    token_list.push(new Token(cur, TokenType.OPERATOR));
                }
            }else{
                throw "Error character @ " + (reader.cursor - 1) + " " + cur;
            }
        }
        return token_list;
    }
}

class OperatorNode{
    constructor(op, fi, se){
        this.op = op;
        this.fi = fi;
        this.se = se;
    }
    eval(){
        switch(this.op){
            case '+':
                return this.fi.eval() + this.se.eval();
            case '-':
                return this.fi.eval() - this.se.eval();
            case '*':
                return this.fi.eval() * this.se.eval();
            case '/':
                return this.fi.eval() / this.se.eval();
            case '%':
                return this.fi.eval() % this.se.eval();
            case '**':
                return this.fi.eval() ** this.se.eval();
            case '<<':
                return this.fi.eval() << this.se.eval();
            case '>>':
                return this.fi.eval() >> this.se.eval();
            case '&':
                return this.fi.eval() & this.se.eval();
            case '|':
                return this.fi.eval() | this.se.eval();
            case '^':
                return this.fi.eval() ^ this.se.eval();
            default:
                break;
        }
    }
}

class ConstNode{
    constructor(v){
        this.value = v;
    }
    eval(){
        return this.value;
    }
}

class ExpressionTreeConstructor{
    constructor(tokenList){
        reader = new Reader(tokenList);
        this.operator = [];
        this.num = [];
    }
    addNode(){
        var se = this.num.pop();
        var fi = new ConstNode(0);
        if(this.num.length !== 0){
            fi = this.num.pop();
        }
        var op = this.operator.pop();
        this.num.push(new OperatorNode(op, fi, se));
    }
    get opTop(){
        return this.operator[this.operator.length - 1]
    }
    build_tree(){
        while(reader.has_next()){
            var cur = reader.next();
            switch (cur.tag) {
                case TokenType.INT:
                    if (cur.value.startsWith('~')) {
                        this.num.push(new ConstNode(~parseInt(cur.value.slice(1))));
                    } else {
                        this.num.push(new ConstNode(parseInt(cur.value)));
                    }
                    break;
                case TokenType.DOUBLE:
                    if (cur.value.startsWith('~')) {
                        this.num.push(new ConstNode(~parseFloat(cur.value.slice(1))));
                    } else {
                        this.num.push(new ConstNode(parseFloat(cur.value)));
                    }
                    break;
                case TokenType.OPERATOR:
                    switch(cur.value){
                        case '(':
                            this.operator.push('(');
                            break;
                        case ')':
                            while(this.operator.length && this.opTop !== '('){
                                this.addNode();
                            }
                            this.operator.pop();
                            break;
                        case '%':
                        case '**':
                            while (this.operator.length && this.opTop === cur.value) {
                                this.addNode();
                            }
                            this.operator.push(cur.value);
                            break;
                        case '*':
                        case '/':
                            while (this.operator.length && (this.opTop === '*' || this.opTop === '/')) {
                                this.addNode();
                            }
                            this.operator.push(cur.value);
                            break;
                        case '+':
                        case '-':
                            while (this.operator.length && this.opTop !== '(' && this.opTop !== '|' && this.opTop !== '&' && this.opTop !== '^') {
                                this.addNode();
                            }
                            this.operator.push(cur.value);
                            break;
                        case '|':
                        case '&':
                        case '^':
                        case '<<':
                        case '>>':
                            while(this.operator.length && this.opTop !== '('){
                                this.addNode();
                            }
                            this.operator.push(cur.value);
                            break;
                        default:
                            throw "Error Operator " + cur.value;
                    }
                default:
                    break;
            }
        }
        while(this.operator.length){
            this.addNode();
        }
        return this.num[0];
    }
}
var tokens = new Lexer('(1.25+3e-2+1e+3)*5').parse();
console.log(tokens);
console.log(new ExpressionTreeConstructor(tokens).build_tree().eval())

0x04 Epilogue

表达式树是一个不太被常用,但是实现起来并不困难的树形结构 ... 做一个计算器可以用到它。比如前段时间打发时间写的用于长度单位换算的小REPL:

AD1024/UnitEquation​github.com
9533585cfaa9e4c89c884877f2fd3baa.png

当然,如果你想做一个编程语言...emmmm 有点困难,因为AST比这个要复杂很多,而且计算表达式比较simple,不需要复杂的parser,但是编程语言嘛...parser复杂很多。这里有个计算器的小REPL(应该还有bug),就是尝试不使用parser搞一个简单的编程语言:

AD1024/Calculator​github.com
9533585cfaa9e4c89c884877f2fd3baa.png

最后祝大家新年快乐,狗年大吉,万事如意~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值