[Medium] 224. Basic Calculator
[Medium] 224. Basic Calculator
Given a string s representing a valid expression, implement a basic calculator to evaluate it, and return the result of the evaluation.
Note: You are not allowed to use any built-in function which evaluates strings as mathematical expressions, such as eval().
Examples
Example 1:
Input: s = "1 + 1"
Output: 2
Example 2:
Input: s = " 2-1 + 2 "
Output: 3
Example 3:
Input: s = "(1+(4+5+2)-3)+(6+8)"
Output: 23
Constraints
1 <= s.length <= 3 * 10^5sconsists of digits,'+','-','(',')', and' '.srepresents a valid expression.'+'is not used as a unary operation (i.e.,"+1"and"+(2 + 3)"is invalid).'-'could be used as a unary operation (i.e.,"-1"and"-(2 + 3)"is valid).- There will be no two consecutive operators in the input.
- Every number and running calculation will fit in a signed 32-bit integer.
Clarification Questions
Before diving into the solution, here are 5 important clarifications and assumptions to discuss during an interview:
-
Expression format: What operations are supported? (Assumption: Addition ‘+’, subtraction ‘-‘, parentheses ‘()’ - no multiplication/division)
-
Unary operators: How should we handle unary operators? (Assumption: ‘-‘ can be unary (negative numbers), ‘+’ cannot be unary)
-
Parentheses: How should parentheses be handled? (Assumption: Standard precedence - evaluate inner parentheses first)
-
Return value: What should we return? (Assumption: Integer result of evaluating the expression)
-
Whitespace: Should we ignore whitespace? (Assumption: Yes - spaces can be ignored)
Interview Deduction Process (20 minutes)
Step 1: Brute-Force Approach (5 minutes)
Initial Thought: “I need to evaluate expression. Let me parse and compute manually.”
Naive Solution: Parse expression manually, handle parentheses by recursive evaluation, compute step by step.
Complexity: O(n) time, O(n) space
Issues:
- Complex parsing logic
- Hard to handle nested parentheses
- Error-prone
- Doesn’t leverage stack structure
Step 2: Semi-Optimized Approach (7 minutes)
Insight: “I can use stack to handle parentheses and operator precedence.”
Improved Solution: Use stack to track signs and numbers. When encountering ‘(‘, push current result and sign. When ‘)’, pop and combine.
Complexity: O(n) time, O(n) space
Improvements:
- Stack naturally handles parentheses
- Cleaner parsing logic
- Handles nested parentheses correctly
- O(n) time is optimal
Step 3: Optimized Solution (8 minutes)
Final Optimization: “Stack-based approach is optimal. Track sign and number separately.”
Best Solution: Stack-based approach is optimal. Use stack to track signs for parentheses. Process numbers and operators, handle unary minus. Stack enables correct evaluation order.
Complexity: O(n) time, O(n) space
Key Realizations:
- Stack is perfect for nested structures
- Sign tracking handles unary operations
- O(n) time is optimal - single pass
- O(n) space for stack is necessary
Solution 1: Stack-Based Approach
Time Complexity: O(n)
Space Complexity: O(n) - Stack for parentheses
Use a stack to handle parentheses. When encountering (, push current result and sign onto stack. When encountering ), pop sign and previous result, then combine.
Note: Using switch instead of multiple if-else statements is cleaner and more efficient when handling multiple character cases, as the compiler can optimize switch statements better. The switch executes for all characters, but digits are handled before the switch, and non-operator characters (like spaces) simply fall through without matching any case.
class Solution {
public:
int calculate(string s) {
const int len = s.length();
if(len == 0) return 0;
int rtn = 0, last = 0, curr = 0, sign = 1;
stack<int> stk;
for(int i = 0; i < len; i++) {
char ch = s[i];
if(isdigit(ch)) {
curr = (10 * curr) + (ch - '0');
}
switch (ch) {
case '+':
rtn += sign * curr;
sign = 1;
curr = 0;
break;
case '-':
rtn += sign * curr;
sign = -1;
curr = 0;
break;
case '(':
stk.push(rtn);
stk.push(sign);
sign = 1;
rtn = 0;
break;
case ')':
rtn += sign * curr;
rtn *= stk.top();
stk.pop();
rtn += stk.top();
stk.pop();
curr = 0;
break;
}
}
return rtn + (sign * curr);
}
};
Solution 2: Optimized Recursive Approach
Time Complexity: O(n)
Space Complexity: O(n) - Recursion stack
Use recursion to naturally handle nested parentheses. This approach is cleaner and more intuitive.
class Solution {
private:
int parseExpr(const string& s, int& idx) {
int result = 0;
int sign = 1;
int num = 0;
while(idx < s.length()) {
char c = s[idx];
if(isdigit(c)) {
num = num * 10 + (c - '0');
} else if(c == '+') {
result += sign * num;
sign = 1;
num = 0;
} else if(c == '-') {
result += sign * num;
sign = -1;
num = 0;
} else if(c == '(') {
idx++; // Skip '('
num = parseExpr(s, idx); // Recursive call
} else if(c == ')') {
result += sign * num;
return result; // Return to parent
}
idx++;
}
return result + sign * num;
}
public:
int calculate(string s) {
int idx = 0;
return parseExpr(s, idx);
}
};
Solution 3: Simplified Iterative (No Stack for Numbers)
Time Complexity: O(n)
Space Complexity: O(n) - Only for signs
A cleaner iterative approach that only uses stack for signs, not numbers.
class Solution {
public:
int calculate(string s) {
stack<int> signs;
int sign = 1;
int result = 0;
int num = 0;
signs.push(1); // Initial sign
for(char c : s) {
if(isdigit(c)) {
num = num * 10 + (c - '0');
} else if(c == '+' || c == '-') {
result += signs.top() * sign * num;
num = 0;
sign = (c == '+') ? 1 : -1;
} else if(c == '(') {
signs.push(signs.top() * sign);
sign = 1;
} else if(c == ')') {
result += signs.top() * sign * num;
num = 0;
signs.pop();
sign = 1;
}
}
result += signs.top() * sign * num;
return result;
}
};
How the Algorithms Work
Key Insight: Sign Propagation
Parentheses can flip the sign of the entire expression inside. We need to:
- Stack approach: Save result and sign before
(, restore after) - Recursive approach: Naturally handle nesting through recursion
- Sign stack: Track cumulative sign changes through parentheses
Solution 1: Stack-Based Step-by-Step
Example: s = "(1+(4+5+2)-3)+(6+8)"
Step 0: rtn=0, sign=1, curr=0, stk=[]
Step 1: '(' → push rtn(0) and sign(1)
stk=[0, 1], rtn=0, sign=1, curr=0
Step 2: '1' → curr=1
Step 3: '+' → rtn += sign*curr = 0 + 1*1 = 1
rtn=1, sign=1, curr=0
Step 4: '(' → push rtn(1) and sign(1)
stk=[0, 1, 1, 1], rtn=0, sign=1, curr=0
Step 5-6: '4' → curr=4
Step 7: '+' → rtn += 1*4 = 4
rtn=4, sign=1, curr=0
Step 8-9: '5' → curr=5
Step 10: '+' → rtn += 1*5 = 9
rtn=9, sign=1, curr=0
Step 11-12: '2' → curr=2
Step 13: ')' → rtn += 1*2 = 11
rtn *= stk.top() (1) = 11
rtn += stk.top() (1) = 12
stk=[0, 1], rtn=12, curr=0
Step 14: '-' → rtn += 1*0 = 12
sign=-1, curr=0
Step 15-16: '3' → curr=3
Step 17: ')' → rtn += (-1)*3 = 9
rtn *= stk.top() (1) = 9
rtn += stk.top() (0) = 9
stk=[], rtn=9, curr=0
Step 18: '+' → rtn += 1*0 = 9
sign=1, curr=0
Step 19: '(' → push rtn(9) and sign(1)
stk=[9, 1], rtn=0, sign=1, curr=0
Step 20-21: '6' → curr=6
Step 22: '+' → rtn += 1*6 = 6
rtn=6, sign=1, curr=0
Step 23-24: '8' → curr=8
Step 25: ')' → rtn += 1*8 = 14
rtn *= stk.top() (1) = 14
rtn += stk.top() (9) = 23
stk=[], rtn=23
Final: return 23 + 1*0 = 23 ✓
Solution 3: Sign Stack Step-by-Step
Example: s = "1 + (2 - 3)"
Step 0: signs=[1], sign=1, result=0, num=0
Step 1: '1' → num=1
Step 2: '+' → result += 1*1*1 = 1
sign=1, num=0
Step 3: '(' → signs.push(1*1) = [1, 1]
sign=1
Step 4: '2' → num=2
Step 5: '-' → result += 1*1*(-1)*2 = 1 + (-2) = -1
sign=-1, num=0
Step 6: '3' → num=3
Step 7: ')' → result += 1*(-1)*3 = -1 + (-3) = -4
signs.pop() → [1]
sign=1, num=0
Wait, let me recalculate more carefully:
Actually, the sign stack approach works differently. Let me trace correctly:
s = "1 + (2 - 3)"
signs = [1] initially
i=0: '1' → num=1
i=1: ' ' → skip
i=2: '+' → result += signs.top()*sign*num = 1*1*1 = 1
sign=1, num=0
i=3: ' ' → skip
i=4: '(' → signs.push(signs.top()*sign) = signs.push(1*1) = [1, 1]
sign=1
i=5: '2' → num=2
i=6: ' ' → skip
i=7: '-' → result += signs.top()*sign*num = 1*1*2 = 1+2 = 3
sign=-1, num=0
i=8: ' ' → skip
i=9: '3' → num=3
i=10: ')' → result += signs.top()*sign*num = 1*(-1)*3 = 3+(-3) = 0
signs.pop() → [1]
sign=1, num=0
Final: result += signs.top()*sign*num = 0 + 1*1*0 = 0
But the answer should be 0, which is correct! (1 + (2-3) = 1 + (-1) = 0)
Key Insights
- Stack for Parentheses: Save state (result and sign) before
(, restore after) - Sign Handling: Track current sign (1 or -1) for each number
- Number Building: Accumulate multi-digit numbers
- Final Number: Don’t forget to add the last number at the end
- Recursion Alternative: Natural way to handle nested parentheses
Algorithm Breakdown
Solution 1: Stack-Based
1. Process Digits
if(isdigit(ch)) {
curr = (10 * curr) + (ch - '0');
}
- Build multi-digit numbers incrementally
2. Process Operators
switch(ch) {
case '+':
rtn += sign * curr;
sign = 1;
curr = 0;
break;
case '-':
rtn += sign * curr;
sign = -1;
curr = 0;
break;
// ...
}
- Use
switchfor cleaner code when handling multiple character cases - Add current number with sign to result
- Reset sign and current number
3. Handle Opening Parenthesis
case '(':
stk.push(rtn);
stk.push(sign);
sign = 1;
rtn = 0;
curr = 0;
break;
- Save current result and sign
- Reset for inner expression
4. Handle Closing Parenthesis
case ')':
rtn += sign * curr;
rtn *= stk.top(); // Apply saved sign
stk.pop();
rtn += stk.top(); // Add saved result
stk.pop();
curr = 0;
break;
- Complete inner expression
- Apply saved sign
- Add to saved result
5. Final Addition
return rtn + (sign * curr);
- Add the last number if any
Solution 2: Recursive
1. Recursive Parsing
int parseExpr(const string& s, int& idx) {
int result = 0;
int sign = 1;
int num = 0;
while(idx < s.length()) {
// Process characters...
if(c == '(') {
idx++; // Skip '('
num = parseExpr(s, idx); // Recursive call
} else if(c == ')') {
result += sign * num;
return result; // Return to parent
}
idx++;
}
return result + sign * num;
}
Solution 3: Sign Stack
1. Sign Propagation
stack<int> signs;
signs.push(1); // Initial sign
if(c == '(') {
signs.push(signs.top() * sign); // Cumulative sign
sign = 1;
}
2. Apply Cumulative Sign
result += signs.top() * sign * num;
signs.top(): Cumulative sign from all outer parenthesessign: Current operator signnum: Current number
Complexity Analysis
| Solution | Time | Space | Notes |
|---|---|---|---|
| Stack-Based | O(n) | O(n) | Explicit state management |
| Recursive | O(n) | O(n) | Natural for nesting |
| Sign Stack | O(n) | O(n) | Only stores signs |
Edge Cases
- Single number:
"42"→42 - Negative start:
"-1"→-1(unary minus) - Nested parentheses:
"((1+2))"→3 - Only spaces:
" "→0 - Multiple spaces:
"1 + 2"→3 - Empty parentheses:
"()"→0(handled by current number being 0)
Common Mistakes
- Forgetting final number: Not adding
sign * currat the end - Wrong stack order: Pushing/popping in wrong order
- Sign handling: Not applying saved sign correctly
- Number building: Not resetting
currafter processing - Unary minus: The problem allows
-as unary, but the solution handles it naturally
Detailed Example Walkthrough
Example: s = "1 + (2 - 3)"
Solution 1 (Stack):
Step 0: rtn=0, sign=1, curr=0, stk=[]
Step 1: '1' → curr=1
Step 2: '+' → rtn += 1*1 = 1
rtn=1, sign=1, curr=0
Step 3: '(' → push rtn(1) and sign(1)
stk=[1, 1], rtn=0, sign=1, curr=0
Step 4: '2' → curr=2
Step 5: '-' → rtn += 1*2 = 2
rtn=2, sign=-1, curr=0
Step 6: '3' → curr=3
Step 7: ')' → rtn += (-1)*3 = -1
rtn *= stk.top() (1) = -1
rtn += stk.top() (1) = 0
stk=[], rtn=0, curr=0
Final: return 0 + 1*0 = 0 ✓
Why Solution 1 Works
Stack State Management
When we see (, we save:
- Current result (
rtn): What we’ve calculated so far - Current sign (
sign): The sign that will apply to the inner expression
When we see ), we:
- Complete the inner expression:
rtn += sign * curr - Apply the saved sign:
rtn *= saved_sign - Add to saved result:
rtn += saved_result
Example: "1 + (2 - 3)"
Before '(': rtn=1, sign=1
Save: stk=[1, 1]
Reset: rtn=0, sign=1
Inside '()': Calculate 2-3 = -1
rtn = -1
After ')':
rtn *= 1 (saved sign) = -1
rtn += 1 (saved result) = 0
Final: 0 ✓
Related Problems
- 227. Basic Calculator II - Adds
*and/ - 772. Basic Calculator III - All operators + parentheses
- 150. Evaluate Reverse Polish Notation - Postfix notation
- 394. Decode String - Nested structure evaluation
Pattern Recognition
This problem demonstrates the Expression Evaluation with Parentheses pattern:
- Use stack or recursion to handle nested structures
- Track signs and results separately
- Save state before entering parentheses
- Restore state after exiting parentheses
Key Insight:
- Parentheses create nested evaluation contexts
- Stack naturally handles the LIFO nature of parentheses
- Recursion provides a natural way to handle nesting
Optimization Tips
Recursive vs Iterative
- Recursive: More intuitive, natural for nested structures
- Iterative: Avoids recursion overhead, more control
Sign Stack Optimization
Solution 3 only stores signs, not results, which can be more memory-efficient in some cases.
Code Quality Notes
- Readability: Recursive approach is most intuitive
- Efficiency: All approaches are O(n) time and space
- Correctness: All handle parentheses and signs correctly
- Maintainability: Stack approach is most explicit about state
This problem is the foundation for more complex calculator problems. Understanding how to handle parentheses with a stack is crucial for expression evaluation.