questions B (backspace - burst-balloons)
This commit is contained in:
232
backend/data/questions/basic-calculator.yaml
Normal file
232
backend/data/questions/basic-calculator.yaml
Normal file
@@ -0,0 +1,232 @@
|
||||
title: Basic Calculator
|
||||
slug: basic-calculator
|
||||
difficulty: hard
|
||||
leetcode_id: 224
|
||||
leetcode_url: https://leetcode.com/problems/basic-calculator/
|
||||
categories:
|
||||
- strings
|
||||
- stack
|
||||
- math
|
||||
patterns:
|
||||
- monotonic-stack
|
||||
|
||||
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.
|
||||
Reference in New Issue
Block a user