224. Basic Calculator
題目 / Problem
中文
給你一個字串 s,代表一個合法的數學運算式。請實作一個基本計算機來計算它的結果,並回傳這個結果。
注意:不能使用任何會把字串當成數學式來計算的內建函式(例如 eval())。
運算式中只會出現:數字、+、-、(、) 和空白。其中 + 一定是「二元運算子」(不會出現像 +1 這種寫法),但 - 可以是「一元負號」(像 -1 或 -(2 + 3) 都是合法的)。輸入保證合法,且不會出現連續兩個運算子。
English
Given a string s representing a valid mathematical expression, implement a basic calculator to evaluate it and return the result. You may not use eval() or any function that evaluates strings as math.
The expression contains only digits, +, -, (, ), and spaces. + is always a binary operator (no +1), but - may be a unary minus (-1 and -(2 + 3) are valid). The input is guaranteed valid with no two consecutive operators.
Constraints
- 1 <= s.length <= 3 * 10^5
- s consists of digits, +, -, (, ), and ' '.
- s represents a valid expression.
- Every number and running calculation fits in a signed 32-bit integer.
Worked example
Input: s = "(1+(4+5+2)-3)+(6+8)"
Output: 23
Because (1 + (4+5+2) - 3) + (6+8) = (1 + 11 - 3) + 14 = 9 + 14 = 23.
名詞解釋 / Glossary
- 括號 / parentheses — 成對的
(和)。括號內的運算要先算完,再把結果交給外層。 Parentheses group a sub-expression that must be evaluated first, then handed back to the surrounding expression. - 堆疊 / stack — 一種「後進先出」(LIFO) 的資料結構,只能從頂端推入 (push) 或彈出 (pop)。我們用它來「記住」遇到
(之前外層累積的結果與符號,等)出現時再取回。 A LIFO structure where you push/pop only at the top; we use it to remember the outer running total and sign before entering a(, and restore them at the). - 一元負號 / unary minus — 出現在數字或括號前面、表示「負」的
-,例如-5或-(2+3)。它和「減法」共用同一個字元,需要靠上下文判斷。 A-that means "negative of" rather than subtraction; same character, distinguished by context. - 符號 / sign (+1 or -1) — 我們用一個整數變數記住「下一個數字要加還是要減」,
+1代表加、-1代表減。 An integer that records whether the next number should be added (+1) or subtracted (-1). - 進位累積數字 / building a multi-digit number — 字串裡
123是三個字元,要靠num = num * 10 + digit一位一位拼成整數 123。 Digits arrive one char at a time;num = num*10 + dassembles them into the full integer. - 原地掃描 / single pass — 從左到右把字串看一遍就得到答案,不需要先轉成其他結構。 Reading the string left-to-right exactly once to produce the answer.
思路
中文:最直覺的「暴力法」是先把字串解析成一棵運算式樹,或先處理括號再算加減——但對 hard 題來說這既麻煩又容易出錯。關鍵觀察是:這題只有加減和括號,沒有乘除,所以不需要考慮運算子優先級,純粹是「由左到右一路加減」。唯一會打斷這個流程的就是括號:遇到 ( 時,我們要先把括號內整段算完,再把它當成一個數字併回外層。
怎麼用一次掃描就做到?我們維護三個東西:result(目前累積的總和)、sign(下一個數字的正負,+1 或 -1)、以及一個 stack。掃描時:碰到數字就用 num = num*10 + d 把完整數字拼出來,遇到非數字時就把 sign * num 加進 result。碰到 + 把 sign 設成 +1,碰到 - 設成 -1(注意 - 同時涵蓋「減法」和「一元負號」,因為兩者效果一樣:把後面那個數字變負再加進去)。
括號是精髓。遇到 ( 時,外層的 result 和它前面的 sign 還沒用完,但我們要先進去算括號裡的東西。於是我們把目前的 result 和 sign 推入堆疊保存,然後把 result 歸零、sign 重設為 +1,彷彿在一張新白紙上算括號內的子運算式。當遇到對應的 ) 時,括號內的結果已經算在 result 裡,我們從堆疊彈出剛才存的 sign(這是括號整體的正負)和舊的 result(外層累積),用 result = oldResult + poppedSign * result 把括號結果併回外層。因為堆疊是後進先出,巢狀括號會自動正確配對。整個過程只掃一遍,時間與字串長度成正比。
English: The brute-force instinct is to build an expression tree or resolve parentheses recursively, but that is heavy and bug-prone for a hard problem. The key insight: this expression has only +, -, and parentheses — no * or / — so there is no operator precedence to worry about. It is just a left-to-right running sum of signed numbers. The only thing that interrupts that flow is a parenthesis: at ( we must finish the inner sub-expression first, then fold its value back as a single number.
To do it in one pass, keep three things: result (running total), sign (+1/-1 for the next number), and a stack. While scanning: digits build up via num = num*10 + d; when a non-digit is hit, add sign * num into result. A + sets sign = +1; a - sets sign = -1 (the same code handles both subtraction and unary minus, because both simply mean "make the following value negative before adding").
Parentheses are the heart of it. At (, the outer result and the sign sitting in front of the ( are not finished, but we must compute the inside first. So we push the current result and sign onto the stack, reset result = 0 and sign = +1, and evaluate the inner sub-expression on a fresh slate. At the matching ), the inner total is in result; we pop the saved sign (the sign of the whole parenthesized group) and the saved outer result, and fold back with result = outerResult + poppedSign * result. Because a stack is LIFO, nested parentheses pair up automatically. One pass, time linear in the string length.
逐步走查 / Walkthrough
我們追蹤 s = "(1+(4+5+2)-3)+(6+8)",但為了讓表格不過長且清楚,先用第三個範例的較短前段思路示範一遍完整機制。逐字元追蹤如下(num 為正在拼的數字,sign 為下一數字符號,res 為當前層累積,stk 為堆疊;空白略過):
Tracing s = "(1+(4+5+2)-3)+(6+8)". Columns: c = current char, then state after processing it.
| step | c | num | sign | res | stk (top→bottom) | 說明 / note |
|---|---|---|---|---|---|---|
| 0 | start | 0 | +1 | 0 | [] | 初始 / initial |
| 1 | ( |
0 | +1 | 0 | [0, +1] | push res=0, sign=+1;新白紙 / fresh slate |
| 2 | 1 |
1 | +1 | 0 | [0,+1] | 拼數字 / build num |
| 3 | + |
0 | +1 | 1 | [0,+1] | res += sign*num = 1;sign=+1 |
| 4 | ( |
0 | +1 | 0 | [1,+1, 0,+1] | push res=1,sign=+1;再開新層 / nest |
| 5 | 4 |
4 | +1 | 0 | … | build |
| 6 | + |
0 | +1 | 4 | … | res=4 |
| 7 | 5 |
5 | +1 | 4 | … | build |
| 8 | + |
0 | +1 | 9 | … | res=9 |
| 9 | 2 |
2 | +1 | 9 | … | build |
| 10 | ) |
0 | +1 | 11 | [1,+1] | 先 res+=signnum=11;再 pop:res = 1 + (+1)11 = 12 |
等等——步驟 10 要小心順序。先把尚未加入的 num 結算,再做 pop。修正後:
Careful at ): first settle the pending num, then pop. Corrected continuation:
| step | c | num | sign | res | stk | 說明 / note |
|---|---|---|---|---|---|---|
| 10 | ) |
0 | +1 | 12 | [0,+1] | settle: 9+2=11;pop sign=+1,old=1 → res=1+1*11=12 |
| 11 | - |
0 | −1 | 12 | [0,+1] | sign=−1 |
| 12 | 3 |
3 | −1 | 12 | [0,+1] | build |
| 13 | ) |
0 | +1 | 9 | [] | settle:12+(−1)3=9;pop sign=+1,old=0 → res=0+19=9 |
| 14 | + |
0 | +1 | 9 | [] | sign=+1 |
| 15 | ( |
0 | +1 | 0 | [9,+1] | push res=9,sign=+1;新層 |
| 16 | 6 |
6 | +1 | 0 | [9,+1] | build |
| 17 | + |
0 | +1 | 6 | [9,+1] | res=6 |
| 18 | 8 |
8 | +1 | 6 | [9,+1] | build |
| 19 | ) |
0 | +1 | 14 | [] | settle:6+8=14;pop → res=9+1*14=23 |
| end | — | 0 | +1 | 23 | [] | 結束時 num=0,無待結算 / final answer |
最終 res = 23,正確。Final res = 23. ✅
Solution — C
// 演算法 / Algorithm:
// 由左到右掃描一次。用 sign(+1/-1) 記下一個數字正負,res 累積當前層總和。
// 遇到 '(' 把外層 res 與 sign 推入堆疊並重置;遇到 ')' 先結算待加數字,
// 再彈出折回外層。只有加減與括號,無需運算子優先級。
// Single left-to-right pass; stack saves the outer (res, sign) at '(' and
// restores/folds them at ')'. No precedence needed (only + - and parens).
#include <stdlib.h> // 提供 malloc / free(手動配置堆疊記憶體)/ for malloc & free
int calculate(char* s) {
int n = 0; // 算字串長度 / measure length of s
while (s[n] != '