[LeetCode] Parse Lisp Expression 解析Lisp表示式
You are given a string expression
representing a Lisp-like expression to return the integer value of.
The syntax for these expressions is given as follows.
- An expression is either an integer, a let-expression, an add-expression, a mult-expression, or an assigned variable. Expressions always evaluate to a single integer.
- (An integer could be positive or negative.)
- A let-expression takes the form
(let v1 e1 v2 e2 ... vn en expr)
, wherelet
is always the string"let"
, then there are 1 or more pairs of alternating variables and expressions, meaning that the first variablev1
is assigned the value of the expressione1
v2
is assigned the value of the expressione2
, and so on sequentially; and then the value of this let-expression is the value of the expressionexpr
.
- An add-expression takes the form
(add e1 e2)
whereadd
is always the string"add"
, there are always two expressionse1, e2
, and this expression evaluates to the addition of the evaluation ofe1
e2
.
- A mult-expression takes the form
(mult e1 e2)
wheremult
is always the string"mult"
, there are always two expressionse1, e2
, and this expression evaluates to the multiplication of the evaluation ofe1
and the evaluation ofe2
.
- For the purposes of this question, we will use a smaller subset of variable names. A variable starts with a lowercase letter, then zero or more lowercase letters or digits. Additionally for your convenience, the names "add", "let", or "mult" are protected and will never be used as variable names.
- Finally, there is the concept of scope. When an expression of a variable name is evaluated, within the context of that evaluation, the innermost scope (in terms of parentheses) is checked first for the value of that variable, and then outer scopes are checked sequentially. It is guaranteed that every expression is legal. Please see the examples for more details on scope.
Evaluation Examples:
Input: (add 1 2) Output: 3 Input: (mult 3 (add 2 3)) Output: 15 Input: (let x 2 (mult x 5)) Output: 10 Input: (let x 2 (mult x (let x 3 y 4 (add x y)))) Output: 14 Explanation: In the expression (add x y), when checking for the value of the variable x, we check from the innermost scope to the outermost in the context of the variable we are trying to evaluate. Since x = 3 is found first, the value of x is 3. Input: (let x 3 x 2 x) Output: 2 Explanation: Assignment in let statements is processed sequentially. Input: (let x 1 y 2 x (add x y) (add x y)) Output: 5 Explanation: The first (add x y) evaluates as 3, and is assigned to x. The second (add x y) evaluates as 3+2 = 5. Input: (let x 2 (add (let x 3 (let x 4 x)) x)) Output: 6 Explanation: Even though (let x 4 x) has a deeper scope, it is outside the context of the final x in the add-expression. That final x will equal 2. Input: (let a1 3 b2 (add a1 1) b2) Output 4 Explanation: Variable names can contain digits after the first character.
Note:
- The given string
expression
is well formatted: There are no leading or trailing spaces, there is only a single space separating different components of the string, and no space between adjacent parentheses. The expression is guaranteed to be legal and evaluate to an integer. - The length of
expression
is at most 2000. (It is also non-empty, as that would not be a legal expression.) - The answer and all intermediate calculations of that answer are guaranteed to fit in a 32-bit integer.
這道題讓我們解析Lisp語言的表示式,以前聽說過Lisp語言,但是完全沒有接觸過,看了題目中的描述和給的例子,感覺很叼。估計題目只讓我們處理一些簡單的情況,畢竟不可能讓我們寫一個編譯器出來。題目中說了給定的表示式都是合法的,這樣也降低了難度。還有一個好的地方是題目給了充足的例子,讓我們去更好的理解這門新的語言。我們通過分析例子發現,所有的命令都是用括號來包裹的,而且裡面還可以巢狀小括號即子命令。讓我們處理的命令只有三種,add,mult,和let。其中add和mult比較簡單就是加法和乘法,就把後面兩個數字或者子表示式的值加起來或成起來即可。let命令稍稍麻煩一些,後面可以跟好多變數或表示式,最簡單的是三個,一般第一個是個變數,比如x,後面會跟一個數字或子表示式,就是把後面的數字或子表示式的值賦值給前面的變數,第三個位置是個表示式,其值是當前let命令的返回值。還有一個比較重要的特性是外層的變數值不會隨著裡層的變數值改變,比如對於下面這個例子:
(let x 2 (add (let x 3 (let x 4 x)) x))
剛開始x被賦值為2了,然後在返回值表示式中,又有一個add操作,add操作的第一個變數又是一個子表示式,在這個子表示式中又定義了一個變數x,並複製為3,再其返回值表示式又定義了一個變數x,賦值為4,並返回這個x,那麼最內層的表示式的返回值是4,那麼x被賦值為3的那層的返回值也是4,此時add的第一個數就是4,那麼其第二個x是多少,其實這個x並沒有被裡層的x的影響,仍然是剛開始賦值的2,那麼我們就看出特點了,外層的變數是能影響裡層變數的,而裡層變數無法影響外層變數。那麼我們只要在遞迴的時候不加引用就行了,這樣值就不會在遞迴函式中被更改了。
對於這種長度不定且每個可能包含子表示式的題,遞迴是一個很好的選擇,由於需要給變數賦值,所以需要建立一個變數和其值之間的對映,然後我們就要來寫遞迴函數了,最開始我們給定的表示式肯定是有括號的,所以我們先處理這種情況,括號對於我們的解析沒有用,所以要去掉首尾的括號,然後我們用一個變數cur表示當前指向字元的位置,初始化為0,下面要做的就是先解析出命令單詞,我們呼叫一個子函式parse,在parse函式中,簡單的情況就是解析出add,mult,或let這三個命令單詞,我們用一個指標來遍歷字元,當越界或遇到空格就停止,但是如果我們需要解析的是個子表示式,而且裡面可能還有多個子表示式,那麼我們就需要找出最外面這個左括號對應的右括號,因為中間可能還會有別的左右括號,裡面的內容就再之後再次呼叫遞迴函式時處理。判斷的方法就是利用匹配括號的方法,用變數cnt來表示左括號的的個數,初始化為1,當要parse的表示式第一個字元是左括號時,進入迴圈,迴圈條件是cnt不為0,當遇到左括號時cnt自增1,反之當遇到右括號時cnt自減1,每次指標end都向右移動一個,最後我們根據end的位置減去初始時cur的值(儲存在變數t中),可以得到表示式。如果解析出的是命令let,那麼進行while迴圈,然後繼續解析後面的內容,如果此時cur大於s的長度了,說明此時是let命令的最後一個部分,也就是返回值部分,直接呼叫遞迴函式返回即可。否則就再解析下一個部分,說明此時是變數和其對應值,我們要建立對映關係。如果之前解析出來的是add命令,那麼比較簡單,就直接解析出後面的兩個部分的表示式,並分別呼叫遞迴函式,將遞迴函式的返回值累加並返回即可。對於mult命令同樣的處理方式,只不過是將兩個遞迴函式的返回值乘起來並返回。然後我們再來看如果表示式不是以左括號開頭的,說明只能是數字或者變數,那麼先來檢測數字,如果第一個字元是負號或者0到9之間的數字,那麼直接將表示式轉為int型即可;否則的話就是變數,我們直接從雜湊map中取值即可。最後需要注意的就是遞迴函式的引數雜湊map一定不能加引用,具體可以參見上面那個例子的分析,加了引用後外層的變數值就會受內層的影響,這是不符合題意的,參見程式碼如下:
class Solution { public: int evaluate(string expression) { unordered_map<string, int> m; return helper(expression, m); } int helper(string str, unordered_map<string, int> m) { if (str[0] == '-' || (str[0] >= '0' && str[0] <= '9')) return stoi(str); else if (str[0] != '(') return m[str]; string s = str.substr(1, str.size() - 2); int cur = 0; string cmd = parse(s, cur); if (cmd == "let") { while (true) { string var = parse(s, cur); if (cur > s.size()) return helper(var, m); string t = parse(s, cur); m[var] = helper(t, m); } } else if (cmd == "add") { return helper(parse(s, cur), m) + helper(parse(s, cur), m); } else if (cmd == "mult") { return helper(parse(s, cur), m) * helper(parse(s, cur), m); } } string parse(string& s, int& cur) { int end = cur + 1, t = cur, cnt = 1; if (s[cur] == '(') { while (cnt != 0) { if (s[end] == '(') ++cnt; else if (s[end] == ')') --cnt; ++end; } } else { while (end < s.size() && s[end] != ' ') ++end; } cur = end + 1; return s.substr(t, end - t); } };
類似題目:
參考資料: