![08cf16cc9168d20ad8c7159677c41395.png](https://i-blog.csdnimg.cn/blog_migrate/cf594da156a554890538dba2407a2b4f.jpeg)
0x00 Prologue
过春节啦!祝大家新年快乐哇~有一段时间没有写文章了,前几天正好把寒假的作业写了一些,今天更一篇依旧是介绍树形数据结构的文章吧....这次看一个没有Zkw线段树那么复杂的数据结构——表达式树(Expression Tree)。
本篇文章将对:
- 表达式树的工作原理
- 表达式树建立步骤
- 表达式树求值
进行介绍。文章代码使用JavaScript(最近某个项目需要学习,正好用一用:P),但是这个语言吧语法还是非常简单易懂的...并且我会在需要解释的地方打上注释:D。
另外,文章会涉及一点非常简单的词法分析器的介绍。
0x01 表达式树能干啥?长啥样?
Q: 给定一个合法的表达式字符串"(1+2)*(5-3)",计算表达式的值。
<del>A: eval()!!(拉出去打死</del>
如果使用一些没有eval()
的语言怎么搞?用表达式树!
<del>建一下,玩一年,只要998,年底清仓大甩卖,表达式树带回家</del>
表达式树就是将一个表达式解析成一棵满二叉树(如果所有合法操作符都是进行二元计算),且所有非叶结点都是符号结点(+-*/
),叶子结点都是数字结点。对于(1+2)*(5-3)
我们就可以构造出一棵这样的表达式树:
![4ed07d7402f990527251fdaf12c14ef3.png](https://i-blog.csdnimg.cn/blog_migrate/213c4f206be76659678e16ff759d167c.jpeg)
可以看出来,对表达式树进行后序遍历的结果就是原表达式的逆波兰式(后缀表达式)。对表达式树进行中序遍历的结果就是我们所常见表达式(除去括号)。
0x02 从字符串建立表达式树
0. 规定表达式组成成分
这里为了简化理解,就不介绍产生式了...这一部分就是规定什么能出现在将被evaluate的字符串中,定义符号集以及对应的Token。这里我们的计算器支持基础四则运算,取模以及位运算(and
, or
, xor
, shl
, shr
),这些符号的token我们定义为OPERATOR
;除了符号还有参与运算的数字, 我们将数字分为两种:INTEGER
和DOUBLE
。那么我们就可以定义一个这样的类:
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)
}
}
- 词法分析
为了方便建树时识别每个符号的含义,我们首先对表达式字符串进行词法分析。这个过程会将字符串中的元素拆分成一个一个的Token。例如+
是一个运算符,我们可以定义为OPERATOR
。我们将编写一个简单的词法分析器(Lexer)对表达式字符串进行词法分析。
![c61d81a943f1c03b9828652e155f2c39.png](https://i-blog.csdnimg.cn/blog_migrate/ba4e8c0399f76e1dbd62540ba77269fe.jpeg)
(上图是对表达式(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](https://i-blog.csdnimg.cn/blog_migrate/9650d973b2f0e6e30af3cfb3dca4d00c.jpeg)
0x03 通过Token列表建立表达式树 & 求值
这时我们的Reader
又派上用场了<del>(上吧!Reader,ATK +450↑)</del>。我们建立表达式树的时候也需要从左向右扫一遍我们的Token们。我们需要两个栈(Stack)存储两类数据:
- Number Stack (数字栈)
- Operator Stack (符号栈)
事实上,这里的数字栈并不只存数字,而是存可被求值的结点。
为了方便之后的求值,我们定义两种表达式树节点:
- 常数结点
- 操作符结点
其中常数结点存储一个单独的值value
和一个求值方法eval()
(直接返回value
);操作符结点分为op
,fi
,和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](https://i-blog.csdnimg.cn/blog_migrate/cf1d5d1344329a071c6bf3aab1b5c02c.jpeg)
求值就非常简单了,我们已经定义了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/UnitEquationgithub.com![9533585cfaa9e4c89c884877f2fd3baa.png](https://i-blog.csdnimg.cn/blog_migrate/6ad2dc874b7f6c50e10380a03905a2ce.jpeg)
当然,如果你想做一个编程语言...emmmm 有点困难,因为AST比这个要复杂很多,而且计算表达式比较simple,不需要复杂的parser,但是编程语言嘛...parser复杂很多。这里有个计算器的小REPL(应该还有bug),就是尝试不使用parser搞一个简单的编程语言:
AD1024/Calculatorgithub.com![9533585cfaa9e4c89c884877f2fd3baa.png](https://i-blog.csdnimg.cn/blog_migrate/6ad2dc874b7f6c50e10380a03905a2ce.jpeg)
最后祝大家新年快乐,狗年大吉,万事如意~