258 lines
11 KiB
YAML
258 lines
11 KiB
YAML
title: Basic Calculator
|
|
slug: basic-calculator
|
|
difficulty: hard
|
|
leetcode_id: 224
|
|
leetcode_url: https://leetcode.com/problems/basic-calculator/
|
|
categories:
|
|
- strings
|
|
- stack
|
|
- math
|
|
patterns:
|
|
- slug: monotonic-stack
|
|
is_optimal: true
|
|
|
|
function_signature: "def calculate(s: str) -> int:"
|
|
|
|
test_cases:
|
|
visible:
|
|
- input: { s: "1 + 1" }
|
|
expected: 2
|
|
- input: { s: " 2-1 + 2 " }
|
|
expected: 3
|
|
- input: { s: "(1+(4+5+2)-3)+(6+8)" }
|
|
expected: 23
|
|
hidden:
|
|
- input: { s: "1" }
|
|
expected: 1
|
|
- input: { s: "-1" }
|
|
expected: -1
|
|
- input: { s: "-(3+2)" }
|
|
expected: -5
|
|
- input: { s: "1-(5)" }
|
|
expected: -4
|
|
- input: { s: "2147483647" }
|
|
expected: 2147483647
|
|
- input: { s: "1-(-2)" }
|
|
expected: 3
|
|
|
|
description: |
|
|
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()`.
|
|
|
|
constraints: |
|
|
- `1 <= s.length <= 3 * 10^5`
|
|
- `s` consists of digits, `'+'`, `'-'`, `'('`, `')'`, and `' '`
|
|
- `s` represents 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
|
|
|
|
examples:
|
|
- input: 's = "1 + 1"'
|
|
output: "2"
|
|
explanation: "Simple addition of two numbers."
|
|
- input: 's = " 2-1 + 2 "'
|
|
output: "3"
|
|
explanation: "2 - 1 = 1, then 1 + 2 = 3. Spaces are ignored."
|
|
- input: 's = "(1+(4+5+2)-3)+(6+8)"'
|
|
output: "23"
|
|
explanation: "Inner parentheses: (4+5+2) = 11, so (1+11-3) = 9. Then (6+8) = 14. Finally 9 + 14 = 23."
|
|
|
|
explanation:
|
|
intuition: |
|
|
Imagine you're evaluating a mathematical expression by hand. When you encounter parentheses, you mentally "pause" what you were doing, compute the inner expression first, and then come back to where you left off.
|
|
|
|
This is exactly what a **stack** enables programmatically. Think of the stack as a "memory" for the outer context. When you see an opening parenthesis `(`, you save your current progress (the running result and the sign before the parenthesis) onto the stack. Then you start fresh to compute the inner expression. When you hit a closing parenthesis `)`, you pop the saved context and combine it with the inner result.
|
|
|
|
The key insight is that addition and subtraction are **left-associative** and have the **same precedence**, so we can evaluate the expression in a single left-to-right pass. The only complication is parentheses, which create nested scopes that we handle with a stack.
|
|
|
|
For the sign, we track whether the next number should be added or subtracted. A `+` means add (sign = 1), and a `-` means subtract (sign = -1). Unary minus (like `-5` or `-(3+2)`) is handled naturally by setting the sign before parsing the number or entering the parentheses.
|
|
|
|
approach: |
|
|
We solve this using a **Stack-Based Single Pass** approach:
|
|
|
|
**Step 1: Initialise variables**
|
|
|
|
- `result`: Set to `0` to accumulate the running total
|
|
- `sign`: Set to `1` (positive) representing whether to add or subtract the next value
|
|
- `stack`: Empty list to save context when entering parentheses
|
|
- `i`: Index to iterate through the string
|
|
|
|
|
|
|
|
**Step 2: Iterate through each character**
|
|
|
|
- **Digit**: Build the full number (could be multi-digit), then add `sign * number` to result
|
|
- **`+`**: Set sign to `1` (next value will be added)
|
|
- **`-`**: Set sign to `-1` (next value will be subtracted)
|
|
- **`(`**: Push current `result` and `sign` onto stack, then reset `result = 0` and `sign = 1` to start evaluating the sub-expression
|
|
- **`)`**: Pop the saved sign and previous result from the stack. Compute `popped_result + popped_sign * result` to combine inner result with outer context
|
|
- **Space**: Skip whitespace characters
|
|
|
|
|
|
|
|
**Step 3: Return the result**
|
|
|
|
- After processing all characters, `result` contains the final evaluated value
|
|
|
|
|
|
|
|
The stack stores pairs of `(previous_result, sign_before_parenthesis)`. This lets us "freeze" the outer computation, evaluate the inner expression independently, and then seamlessly merge them when we close the parenthesis.
|
|
|
|
common_pitfalls:
|
|
- title: Not Handling Multi-Digit Numbers
|
|
description: |
|
|
A common mistake is treating each digit character as a separate number. For example, `"123"` should be parsed as the number `123`, not `1`, `2`, `3`.
|
|
|
|
You need a loop that continues reading digits while the current character is a digit, building the number: `num = num * 10 + int(char)`.
|
|
wrong_approach: "Treating each digit as a separate number"
|
|
correct_approach: "Build multi-digit numbers in a loop"
|
|
|
|
- title: Forgetting Unary Minus
|
|
description: |
|
|
The problem states that `-` can be used as a unary operator, like `-1` or `-(3+2)`. If you only handle binary subtraction, expressions like `"-(1+2)"` will fail.
|
|
|
|
The sign variable naturally handles this: when we see `-` followed by `(`, we push the current state with sign `-1`, so the entire inner result gets negated.
|
|
wrong_approach: "Only handling binary subtraction"
|
|
correct_approach: "Track sign separately; it applies to both numbers and sub-expressions"
|
|
|
|
- title: Incorrect Stack Order
|
|
description: |
|
|
When pushing to and popping from the stack, order matters. You need to push `result` first, then `sign`, and pop in reverse order (sign first, then result).
|
|
|
|
If you get this backwards, you'll apply the wrong sign or add to the wrong accumulated value.
|
|
wrong_approach: "Inconsistent push/pop order"
|
|
correct_approach: "Push (result, sign) in order; pop sign first, then result"
|
|
|
|
- title: Not Resetting After Opening Parenthesis
|
|
description: |
|
|
After pushing the current context onto the stack, you must reset `result = 0` and `sign = 1` to start evaluating the inner expression from scratch.
|
|
|
|
Forgetting to reset means you'll incorrectly mix the outer and inner computations.
|
|
wrong_approach: "Continuing with old result/sign after '('"
|
|
correct_approach: "Reset result = 0 and sign = 1 after pushing to stack"
|
|
|
|
key_takeaways:
|
|
- "**Stack for nested structures**: Stacks are ideal for problems involving parentheses or any nested scope. Push context on open, pop on close."
|
|
- "**Sign tracking**: Instead of handling `+` and `-` as operators between terms, track a sign multiplier that applies to the next value."
|
|
- "**Single pass efficiency**: Despite the apparent complexity, expression evaluation with `+`, `-`, and parentheses can be done in O(n) time."
|
|
- "**Foundation for harder calculators**: This pattern extends to Basic Calculator II (with `*` and `/`) and III (with all operators and parentheses)."
|
|
|
|
time_complexity: "O(n). We iterate through each character in the string exactly once."
|
|
space_complexity: "O(n). In the worst case with deeply nested parentheses like `(((...)))`, the stack stores O(n) pairs."
|
|
|
|
solutions:
|
|
- approach_name: Stack-Based Single Pass
|
|
is_optimal: true
|
|
code: |
|
|
def calculate(s: str) -> int:
|
|
result = 0 # Running total for current scope
|
|
sign = 1 # 1 for positive, -1 for negative
|
|
stack = [] # Stores (result, sign) when entering parentheses
|
|
i = 0
|
|
n = len(s)
|
|
|
|
while i < n:
|
|
char = s[i]
|
|
|
|
if char.isdigit():
|
|
# Build the full number (could be multi-digit)
|
|
num = 0
|
|
while i < n and s[i].isdigit():
|
|
num = num * 10 + int(s[i])
|
|
i += 1
|
|
# Add (or subtract) this number to our result
|
|
result += sign * num
|
|
continue # i already advanced past the number
|
|
|
|
elif char == '+':
|
|
# Next number should be added
|
|
sign = 1
|
|
|
|
elif char == '-':
|
|
# Next number should be subtracted
|
|
sign = -1
|
|
|
|
elif char == '(':
|
|
# Save current context and start fresh
|
|
stack.append(result)
|
|
stack.append(sign)
|
|
result = 0
|
|
sign = 1
|
|
|
|
elif char == ')':
|
|
# Combine inner result with saved outer context
|
|
prev_sign = stack.pop()
|
|
prev_result = stack.pop()
|
|
result = prev_result + prev_sign * result
|
|
|
|
# Skip spaces (char == ' ')
|
|
i += 1
|
|
|
|
return result
|
|
explanation: |
|
|
**Time Complexity:** O(n) — Single pass through the string.
|
|
|
|
**Space Complexity:** O(n) — Stack depth proportional to nesting level, worst case O(n).
|
|
|
|
We process each character once. Digits form multi-digit numbers, operators update the sign, and parentheses push/pop context from the stack. The sign variable elegantly handles both binary subtraction and unary minus.
|
|
|
|
- approach_name: Recursive Descent
|
|
is_optimal: false
|
|
code: |
|
|
def calculate(s: str) -> int:
|
|
# Remove all spaces for easier parsing
|
|
s = s.replace(' ', '')
|
|
index = [0] # Use list for mutable reference in nested function
|
|
|
|
def parse_expression() -> int:
|
|
result = 0
|
|
sign = 1
|
|
|
|
while index[0] < len(s):
|
|
char = s[index[0]]
|
|
|
|
if char.isdigit():
|
|
# Parse multi-digit number
|
|
num = 0
|
|
while index[0] < len(s) and s[index[0]].isdigit():
|
|
num = num * 10 + int(s[index[0]])
|
|
index[0] += 1
|
|
result += sign * num
|
|
continue
|
|
|
|
elif char == '+':
|
|
sign = 1
|
|
index[0] += 1
|
|
|
|
elif char == '-':
|
|
sign = -1
|
|
index[0] += 1
|
|
|
|
elif char == '(':
|
|
index[0] += 1 # Skip '('
|
|
# Recursively evaluate sub-expression
|
|
inner = parse_expression()
|
|
result += sign * inner
|
|
|
|
elif char == ')':
|
|
index[0] += 1 # Skip ')'
|
|
return result # Return to caller
|
|
|
|
else:
|
|
index[0] += 1 # Skip unexpected characters
|
|
|
|
return result
|
|
|
|
return parse_expression()
|
|
explanation: |
|
|
**Time Complexity:** O(n) — Each character is visited once.
|
|
|
|
**Space Complexity:** O(n) — Recursion depth proportional to nesting level.
|
|
|
|
This approach uses recursion to handle parentheses. When we see `(`, we recursively call `parse_expression()` to evaluate the inner content. When we see `)`, we return the inner result to the caller. The recursion stack implicitly does what the explicit stack does in the iterative solution.
|
|
|
|
While equally efficient, this approach may hit Python's recursion limit for very deeply nested expressions, making the iterative stack solution preferred.
|