由一道我喜欢的题引出的思考,parser?

题目LeetCode 736 hard

给你一个类似 Lisp 语句的字符串表达式 expression,求出其计算结果。

表达式语法如下所示:

表达式可以为整数,let 表达式,add 表达式,mult 表达式,或赋值的变量。表达式的结果总是一个整数。
(整数可以是正整数、负整数、0)
let 表达式采用 "(let v1 e1 v2 e2 ... vn en expr)" 的形式,其中 let 总是以字符串 "let"来表示,接下来会跟随一对或多对交替的变量和表达式,也就是说,第一个变量 v1被分配为表达式 e1 的值,第二个变量 v2 被分配为表达式 e2 的值,依次类推;最终 let 表达式的值为 expr表达式的值。
add 表达式表示为 "(add e1 e2)" ,其中 add 总是以字符串 "add" 来表示,该表达式总是包含两个表达式 e1、e2 ,最终结果是 e1 表达式的值与 e2 表达式的值之 和 。
mult 表达式表示为 "(mult e1 e2)" ,其中 mult 总是以字符串 "mult" 表示,该表达式总是包含两个表达式 e1、e2,最终结果是 e1 表达式的值与 e2 表达式的值之 积 。
在该题目中,变量名以小写字符开始,之后跟随 0 个或多个小写字符或数字。为了方便,"add" ,"let" ,"mult" 会被定义为 "关键字" ,不会用作变量名。
最后,要说一下作用域的概念。计算变量名所对应的表达式时,在计算上下文中,首先检查最内层作用域(按括号计),然后按顺序依次检查外部作用域。测试用例中每一个表达式都是合法的。有关作用域的更多详细信息,请参阅示例。

 
示例 1:

输入:expression = "(let x 2 (mult x (let x 3 y 4 (add x y))))"
输出:14
解释:
计算表达式 (add x y), 在检查变量 x 值时,
在变量的上下文中由最内层作用域依次向外检查。
首先找到 x = 3, 所以此处的 x 值是 3 。
示例 2:

输入:expression = "(let x 3 x 2 x)"
输出:2
解释:let 语句中的赋值运算按顺序处理即可。
示例 3:

输入:expression = "(let x 1 y 2 x (add x y) (add x y))"
输出:5
解释:
第一个 (add x y) 计算结果是 3,并且将此值赋给了 x 。
第二个 (add x y) 计算结果是 3 + 2 = 5 。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/parse-lisp-expression

 

思考过程:

对于不清楚lisp,不清楚编译原理parser的同学也不影响,我们只要看题目定义就行,现在定义了这门语言的部分语法,你所要做的就是用你擅长的语言去解析它的三种表达式,add,mult,let,我们一步一步来

首先,加入让你设计一个加法计算器你会怎么做呢例如 1+1=?

你肯定会想这多么简单呀,直接提取'1' '+' '1' 这三个字符,然后将数字部分加起来就可以了,没错就是这样.

那 多个数相加呢 如1 + 1 +1 呢,你得想法应该是没变的,只是解析的过程你会发现有一个'+'其实是多余的,只需要 + 1 1 1 就可以了,这个其实就是前缀表达式,更重要的是可以方便计算

然后你再看看题目中的几个表达式(add e1 e2)不就是(+ 1 1)吗,好了我们继续,提升点难度,并且用add 来替代+

(add (add 1 1)  2) 就是先计算前面的+ 1 1 再将结果和后面的2进行相加,聪明的你可能会想到用栈,或者递归的方式,递归的规律你会发现这个(+ 1 1)本质也是个数,也是个加法计算式,

栈实现则就更加简单了,从左到右入栈,遇到"(add" 记录一下,当遇到对应的")"时,便进行一次计算

我们先写一个转换器,将元素按照我们想要的样式存起来,如也可以不用这步,直接取元素的时候入栈出栈,但是这样虽然会增加一次遍历但是待会方便复用且明朗一点:

先定义关键字,包含后面会用到的:

    static final String ADD = "(add";
    static final String MULT = "(mult";
    static final String LET = "let";
    static final String END = ")";
    static final String SPACE = " ";
/**
     * 如将(add 1 (add 1 1)) 等抽离出来 (add , 1 ,(add,1,1,),),为了明朗,部分api的复杂度较高,lexer处理
     */
    static List<String> convert(String exp) {
        List<String> list = new ArrayList<>();
        while (!exp.isEmpty()) {
            String e = null;
            if (exp.startsWith(ADD)) {
                e = ADD;
            } else if (exp.startsWith(END)) {
                e = END;
            } else {
                // 为数字
                // 非最后参数,那么就是空格结尾
                int endSpace = exp.indexOf(SPACE);
                // 后面都没有空格了说明快结尾了
                if (endSpace < 1) {
                    endSpace = Integer.MAX_VALUE;
                }
                // 最后参数就是括号")"结尾
                int endBrackets = exp.indexOf(END);
                e = exp.substring(0, Math.min(endSpace, endBrackets));
            }
            list.add(e);
            exp = exp.substring(e.length()).trim();
        }
        return list;
    }

测试一下这个方法,我们可以弄个复杂的"(add (add (add 1 1) (add (add 1 1) 1)) (add 2 1))",输出如下就没问题了,注意空格处理,这边都是按照题目正确的显示要求来的

 

然后开始我们核心的parse的流程,采用我们上面所说的迭代(采用栈),和递归来别实现,这边我们将解析和先计算放在一块,注意注释:

    static int parseAndEvaluate(String exp){
        Deque<String> stack = new ArrayDeque<>();
        // 将exp整理好元素
        List<String> tokenList = convert(exp);
        for(String e : tokenList) {
            if (e.equals(END)) {
                // 取出元素进行计算,取到"(add"为止
                int a = 0;
                String b = stack.pop();
                while (!b.equals(ADD)) {
                    int c = Integer.parseInt(b);
                    // 计算
                    a+=c;
                    // 继续取数据元素
                    b = stack.pop();
                }
                stack.push(String.valueOf(a));
            } else {
                // 添加元素
                stack.push(e);
            }
        }
        // exp数据正确的话,栈里面只会有一个元素就是最终的结果
        return Integer.parseInt(stack.pop());
    }

 

 

测试一下"(add (add (add 1 1) (add (add 1 1 1) 1)) (add 2 1 1))",题目只规定了add有两个参数,我们这直接实现了对更多参数的支持

 

关于递归的实现,直接处理tokenList也是可以的,但是我们想换个方式,我们尝试去构建一个数据结构,Enode,怎么去构建呢,我们观察(add  1 1) ,这个整个整体就可以看成是一个Enode,主要包括三部分,add 1 1,add 作为操作符,这个"1"可能也是(add 1 1),也就是一个Enode,那么此时我们便可以开始定义这个具体的数据结构:

class Enode {
    // type 为add 或者 mult时 按照题目设计eNodes 长度为2,但是我们这边直接处理成支持多参数
    // type 为let 时,eNodes,按照题目设计长度不定
    // type 为 变量 或者 值时,eNodes为空 ,content为变量名或者值
    String type;
    List<Enode> eNodes;
    // 内容,如 a b c, 1 2 3
    String content;

    // 局部变量,add,mult 时有值,通过let操作来存储
    Map<String, Integer> envMap;
}

聪明的你会想到这不就是n叉树的数据结构,envMap我们先忽略.

树的节点数据结构设计好了,我们便可以开始构建这个树了,可以调用convert得到的tokenList来帮助我们进行树的构建,或者在convert的时候直接构建:

    Enode getType(String e) {
        Enode node = new Enode();
        // 非值类型
        if (e.startsWith("(add") || e.startsWith("(mult") || e.startsWith("(let")) {
            if (e.startsWith("(add")) {
                node.type = "(add";
            } else if (e.startsWith("(mult")) {
                node.type = "(mult";
            } else if (e.startsWith("(let")) {
                node.type = "(let";
            }
            List<Enode> list = new ArrayList<>();
            node.eNodes = list;
            e = e.substring(node.type.length(), e.length() - 1).trim();
            for (int i = 0; i < e.length(); i++) {
                if (e.charAt(i) == ' ') continue;
                String ex = "" + e.charAt(i);
                int c = i;
                if (ex.charAt(0) == '(') {
                    int left = 1;
                    for (int j = c + 1; j < e.length(); j++) {
                        i++;
                        ex += e.charAt(j);
                        if (e.charAt(j) == '(') {
                            left++;
                        } else if (e.charAt(j) == ')') {
                            left--;
                            if (left == 0) {
                                break;
                            }
                        }
                    }
                } else {
                    for (int j = c + 1; j < e.length(); j++) {
                        i++;
                        if (e.charAt(j) == ' ') {
                            break;
                        } else {
                            ex += e.charAt(j);
                        }
                    }
                }
                list.add(getType(ex));
            }
            return node;
        }
        try {
            // 值类型
            Integer.parseInt(e);
            node.type = "val";
        } catch (Exception ex) {
            // 变量,这边使用了异常处理,不建议,可以使用正则进行类型判断,为了方便
            node.type = "par";
        }
        node.content = e;
        return node;
    }

关于let,let我们在进行赋值运算时,我们这边是用一个envMap来对变量进行一个kv的维护,当进入到下一个环境时,会将envMap拷贝带过去,若是子环境重新进行了赋值,也不会影响父环境的envMap,下面是计算的过程,就是进行多叉树的遍历,

这边是用递归进行处理:

    int evaluateNode(Enode node) {
        switch (node.type) {
            case "val":
                return Integer.parseInt(node.content);
            case "par":
                return node.envMap.get(node.content);
            case "(add":
                int sum = 0;
                for (int i = 0; i < node.eNodes.size(); i++) {
                    Enode node2 = node.eNodes.get(i);
                    node2.envMap = node.envMap;
                    sum += evaluateNode(node2);
                }
                return sum;
            case "(mult":
                int mult = 1;
                for (int i = 0; i < node.eNodes.size(); i++) {
                    Enode node2 = node.eNodes.get(i);
                    node2.envMap = node.envMap;
                    mult *= evaluateNode(node2);
                }
                return mult;
            case "(let":
                Map<String, Integer> envMap = new HashMap<>();
                if (node.envMap != null) {
                    envMap.putAll(node.envMap);
                }
                for (int i = 0; i < node.eNodes.size() - 1; i += 2) {
                    Enode node2 = node.eNodes.get(i + 1);
                    node2.envMap = envMap;
                    envMap.put(node.eNodes.get(i).content, evaluateNode(node2));
                }
                Enode result = node.eNodes.get(node.eNodes.size() - 1);
                result.envMap = envMap;
                // System.out.println(envMap);
                return evaluateNode(result);
        }
        return 0;
    }

最后编写调用函数,进行测试:

    public int evaluate(String e) {
        Enode root = getType(e);
        return evaluateNode(root);
    }

,这边我们相较原题目,增加add,mult多参数的支持

谢谢大家,后面写了不算很细致,后面会写一些相关的,进行补充,大家相互学习,新年快乐!

 

posted @ 2022-02-02 22:34  junlancer  阅读(36)  评论(0编辑  收藏  举报