|
| 1 | +### 题目描述 |
| 2 | + |
| 3 | +这是 LeetCode 上的 **[736. Lisp 语法解析](https://leetcode.cn/problems/parse-lisp-expression/solution/by-ac_oier-i7w1/)** ,难度为 **困难**。 |
| 4 | + |
| 5 | +Tag : 「DFS」、「模拟」、「哈希表」 |
| 6 | + |
| 7 | + |
| 8 | + |
| 9 | +给你一个类似 `Lisp` 语句的字符串表达式 `expression`,求出其计算结果。 |
| 10 | + |
| 11 | +表达式语法如下所示: |
| 12 | + |
| 13 | +* 表达式可以为整数,`let` 表达式,`add` 表达式,`mult` 表达式,或赋值的变量。表达式的结果总是一个整数。 |
| 14 | +* (整数可以是正整数、负整数、0ドル$) |
| 15 | +* `let` 表达式采用 `"(let v1 e1 v2 e2 ... vn en expr)"` 的形式,其中 `let` 总是以字符串 `"let"`来表示,接下来会跟随一对或多对交替的变量和表达式,也就是说,第一个变量 `v1`被分配为表达式 `e1` 的值,第二个变量 `v2` 被分配为表达式 `e2` 的值,依次类推;最终 `let` 表达式的值为 `expr` 表达式的值。 |
| 16 | +* `add` 表达式表示为 `"(add e1 e2)"` ,其中 `add` 总是以字符串 `"add"` 来表示,该表达式总是包含两个表达式 `e1`、`e2` ,最终结果是 `e1` 表达式的值与 `e2` 表达式的值之 和 。 |
| 17 | +* `mult` 表达式表示为 `"(mult e1 e2)"` ,其中 `mult` 总是以字符串 `"mult"` 表示,该表达式总是包含两个表达式 `e1`、e2,最终结果是 `e1` 表达式的值与 `e2` 表达式的值之 积 。 |
| 18 | +* 在该题目中,变量名以小写字符开始,之后跟随 0ドル$ 个或多个小写字符或数字。为了方便,`"add"` ,`"let"` ,`"mult"` 会被定义为 `"关键字" ,不会用作变量名。 |
| 19 | +* 最后,要说一下作用域的概念。计算变量名所对应的表达式时,在计算上下文中,首先检查最内层作用域(按括号计),然后按顺序依次检查外部作用域。测试用例中每一个表达式都是合法的。有关作用域的更多详细信息,请参阅示例。 |
| 20 | + |
| 21 | +示例 1: |
| 22 | + |
| 23 | +输入:expression = "(let x 2 (mult x (let x 3 y 4 (add x y))))" |
| 24 | +输出:14 |
| 25 | +解释: |
| 26 | +计算表达式 (add x y), 在检查变量 x 值时, |
| 27 | +在变量的上下文中由最内层作用域依次向外检查。 |
| 28 | +首先找到 x = 3, 所以此处的 x 值是 3 。 |
| 29 | +示例 2: |
| 30 | +``` |
| 31 | +输入:expression = "(let x 3 x 2 x)" |
| 32 | + |
| 33 | +输出:2 |
| 34 | + |
| 35 | +解释:let 语句中的赋值运算按顺序处理即可。 |
| 36 | +``` |
| 37 | +示例 3: |
| 38 | +``` |
| 39 | +输入:expression = "(let x 1 y 2 x (add x y) (add x y))" |
| 40 | + |
| 41 | +输出:5 |
| 42 | + |
| 43 | +解释: |
| 44 | +第一个 (add x y) 计算结果是 3,并且将此值赋给了 x 。 |
| 45 | +第二个 (add x y) 计算结果是 3 + 2 = 5 。 |
| 46 | +``` |
| 47 | + |
| 48 | +提示: |
| 49 | +* 1ドル <= expression.length <= 2000$ |
| 50 | +* `exprssion` 中不含前导和尾随空格 |
| 51 | +* `expressoin` 中的不同部分(`token`)之间用单个空格进行分隔 |
| 52 | +* 答案和所有中间计算结果都符合 `32-bit` 整数范围 |
| 53 | + |
| 54 | +--- |
| 55 | + |
| 56 | +### DFS 模拟 |
| 57 | + |
| 58 | +今天身体不舒服,不写太详细,题目不难,大家结合代码看吧。 |
| 59 | + |
| 60 | +设计函数 `int dfs(int l, int r, Map<String, Integer> map)` 函数,代表计算 $s[l...r]$ 的结果,答案为 `dfs(0,n-1,map)`,其中 $n$ 为字符串的长度。 |
| 61 | + |
| 62 | +根据传入的 $[l, r]$ 是否为表达式分情况讨论: |
| 63 | + |
| 64 | +* 若 $s[l] = (,ドル说明是表达式,此时从 $l$ 开始往后取,取到空格为止(假设位置为 `idx`),从而得到操作 `op`(其中 `op` 为 `let`、`add` 或 `mult` 三者之一),此时 `op` 对应参数为 $[idx + 1, r - 1],ドル也就是需要跳过位置 $r$(即跳过 `op` 对应的 `)` 符号); |
| 65 | + |
| 66 | + 再根据 `op` 为何种操作进一步处理,我们设计一个「传入左端点找右端点」的函数 `int getRight(int left, int end)`,含义为从 `left` 出发,一直往右找(不超过 `end`),直到取得合法的「子表达式」或「对应的值」。 |
| 67 | + |
| 68 | + ```Java |
| 69 | + // 对于 getRight 函数作用,给大家举个 🌰 理解吧,其实就是从 left 出发,找到合法的「子表达式」或「值」为止 |
| 70 | + |
| 71 | + // (let x 2 (mult x (let x 3 y 4 (add x y)))) |
| 72 | + // a b |
| 73 | + // 传入 a 返回 b,代表 [a, b) 表达式为 (mult x (let x 3 y 4 (add x y))) |
| 74 | + |
| 75 | + // (let x 2 (mult x (let x 3 y 4 (add x y)))) |
| 76 | + // ab |
| 77 | + // 传入 a 返回 b,代表 [a, b) 表达式为 x |
| 78 | + ``` |
| 79 | + |
| 80 | +* 否则 $s[l...r]$ 不是表达式,通过判断 $s[l...r]$ 是否有被哈希表记录来得知结果:若在哈希表中有记录,结果为哈希表中的映射值,否则结果为本身所代表的数值。 |
| 81 | + |
| 82 | +需要注意,根据「作用域」的定义,我们不能使用全局哈希表,而要在每一层递归时,传入 `clone` 出来的 `map`。 |
| 83 | + |
| 84 | +代码: |
| 85 | +```Java |
| 86 | +class Solution { |
| 87 | + char[] cs; |
| 88 | + String s; |
| 89 | + public int evaluate(String _s) { |
| 90 | + s = _s; |
| 91 | + cs = s.toCharArray(); |
| 92 | + return dfs(0, cs.length - 1, new HashMap<>()); |
| 93 | + } |
| 94 | + int dfs(int l, int r, Map<String, Integer> map) { |
| 95 | + if (cs[l] == '(') { |
| 96 | + int idx = l; |
| 97 | + while (cs[idx] != ' ') idx++; |
| 98 | + String op = s.substring(l + 1, idx); |
| 99 | + r--; // 判别为 "(let"、"(add" 或 "(mult" 后,跳过对应的 ")" |
| 100 | + if (op.equals("let")) { |
| 101 | + for (int i = idx + 1; i <= r; ) { |
| 102 | + int j = getRight(i, r); |
| 103 | + String key = s.substring(i, j); |
| 104 | + if (j >= r) return dfs(i, j - 1, new HashMap<>(map)); |
| 105 | + j++; i = j; |
| 106 | + j = getRight(i, r); |
| 107 | + int value = dfs(i, j - 1, new HashMap<>(map)); |
| 108 | + map.put(key, value); |
| 109 | + i = j + 1; |
| 110 | + } |
| 111 | + return -1; // never |
| 112 | + } else if (op.equals("add")) { |
| 113 | + int j = getRight(idx + 1, r); |
| 114 | + int a = dfs(idx + 1, j - 1, new HashMap<>(map)), b = dfs(j + 1, r, new HashMap<>(map)); |
| 115 | + return a + b; |
| 116 | + } else { |
| 117 | + int j = getRight(idx + 1, r); |
| 118 | + int a = dfs(idx + 1, j - 1, new HashMap<>(map)), b = dfs(j + 1, r, new HashMap<>(map)); |
| 119 | + return a * b; |
| 120 | + } |
| 121 | + } else { |
| 122 | + String cur = s.substring(l, r + 1); |
| 123 | + if (map.containsKey(cur)) return map.get(cur); |
| 124 | + return Integer.parseInt(cur); |
| 125 | + } |
| 126 | + } |
| 127 | + int getRight(int left, int end) { |
| 128 | + int right = left, score = 0; |
| 129 | + while (right <= end) { |
| 130 | + if (cs[right] == '(') { |
| 131 | + score++; right++; |
| 132 | + } else if (cs[right] == ')') { |
| 133 | + score--; right++; |
| 134 | + } else if (cs[right] == ' ') { |
| 135 | + if (score == 0) break; |
| 136 | + right++; |
| 137 | + } else { |
| 138 | + right++; |
| 139 | + } |
| 140 | + } |
| 141 | + return right; |
| 142 | + } |
| 143 | +} |
| 144 | +``` |
| 145 | +* 时间复杂度:除对表达式进行递归划分以外,还存在对 `map` 的每层拷贝操作,复杂度为 $O(n^2)$ |
| 146 | +* 空间复杂度:$O(n)$ |
| 147 | + |
| 148 | +--- |
| 149 | + |
| 150 | +### 最后 |
| 151 | + |
| 152 | +这是我们「刷穿 LeetCode」系列文章的第 `No.736` 篇,系列开始于 2021年01月01日,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。 |
| 153 | + |
| 154 | +在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。 |
| 155 | + |
| 156 | +为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。 |
| 157 | + |
| 158 | +在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。 |
| 159 | + |
0 commit comments