Stacks Can Do Math
A month ago, I was on a video call with a few friends and we tried to crack the following problem. We flailed our brains around for 40 minutes before giving up. So let’s revisit this tough cookie!!!
Basic Calculator
Implement a basic calculator to evaluate a simple expression string.
The expression string may contain open (
and closing parentheses )
, the plus +
or minus sign -
, non-negative integers and empty spaces ``.
Example 1:
Input: "1 + 1"
Output: 2
Example 2:
Input: " 2-1 + 2 "
Output: 3
Example 3:
Input: "(1+(4+5+2)-3)+(6+8)"
Output: 23
Note:
- You may assume that the given expression is always valid.
-
Do not use the
eval
built-in library function.
What are the input(s)?
- A string representing a mathematical expression consisting of non-negative integers being added or subtracted
What are the output(s)?
- An integer that is the result of evaluating the expression
What data structures can model this problem?
- stacks
Why Stacks?
Stacks process data in a Last In First Out (LIFO) order…Huh?
Think of a stack of plates. When you wash the dishes, you’ll remove the top plate. To add a plate, you’ll add the plate to the top of the stack. The first plate in the stack is washed last and the last plate is washed first. Last in, first out.
This LIFO property is useful for situations where we want to process data and save intermediate results for later use.
If you’re still not sure why we’re using a stack, we’ll walk through a concrete example right now!
Input: (12 - ( 4 + 2 ))
Expected output: 6
Our grade-school training prompts us to evaluate the innermost parenthetical expression (4 + 2) and then subtract from 12.
Notice the Last In, First Out order in which we performed those steps:
-
(4 + 2)
is the last part of the string;(4 + 2)
was evaluated first -
12 -
is the first part of the string;12 -
was evaluated last
Now that we’ve settled on using a stack, let’s see how we can use this stack to do math.
Use a stack for expression evaluation
We read our string from left to right, character by character.
- we see
12 - (
-
1
, so our current number is 1 -
2
, so our current number is 1 * 10 + 2 = 12 -
-
sign, so we’re done building our current number - open
(
indicates the start of a new expression; we’ll need to revisit12 -
later - store
12
and-
on the stack
-
- we see
4 + 2 )
-
4
, so our current number is 4 -
+
sign, so we’re about to see a new number; our current result is 4 -
2
, so our current number is 2 - we’ve seen
4 + 2
so far, so our current result is 6 - closing
)
indicates that we’ve reached the end of our current expression - we need to combine our previous result
12 -
with our current result- previous result = 12
- current result = 6
- 12 - 6 = 6 (desired output)
-
From this example, we can tease out how the stack responds to each element.
The elements of our string are: (
, )
, +
, -
, and non-negative integers.
- A open
(
indicates the start of a new expression- push the current result and the operator (+/-) onto the stack
- A closing
)
indicates the end of our current expression- pop the previous operator and previous result from the stack
- combine (+/-) the previous result with the current result
- An integer indicates we’re building our current number (multiplying by 10 and adding the integer)
- A
+
/-
indicates the operation to perform between our current number and the current result-
+
: add the current number to our current result -
-
: subtract the current number from our current result - +/- : indicate we’re about to build a new number
-
Phew! We’re done planning our solution. Onto the code!
Building our solution piece by piece
Instantiate our current result, current number, sign (+/-), and stack.
res, num, sign, st = 0,0,1,[] # current result, current number, (+/-), stack
The integer case:
Consecutive digits like “1” and “2” become 12.
# process string character by character
for char in s:
# build number
if char.isdigit():
num = 10*num + int(char)
The (+/-) case:
a - b is the same as a + (-b)
With input 12 - ( 4 + 2 )
, we’ll eventually store (-1) in our stack.
-
-
operator sets sign = -1 -
+
operator sets sign = +1
# operation is about to happen
elif char in "+-":
res += sign * num # add our current result to our current number
num = 0 # reset num to build upcoming number
sign = [-1,1][char =="+"] # a - b = a + (-b)
The open ( case:
Remember to reset our current result and sign back to their defaults of 0 and 1.
# a new expression is about to happen
elif char == "(":
st.append(res) # store result up to this point
st.append(sign) # store the sign of the current expression
res = 0 # reset result for upcoming expression
sign = 1 # reset sign for upcoming expression
The closing ) case:
Finish up evaluating the current expression.
With input 12 - ( 4 + 2 )
, we get a current result of 6.
Our stack has: | 12 | -1 |
We pop (-1) and apply it to 6, getting (-6)
We pop 12 and combine with (-6) to get an output of 6
# end current expression (expr) and combine with previous result
elif char == ")":
res += sign * num # result of current expression
res *= st.pop() # apply sign; -(expr) subtracts, +(expr) adds
res += st.pop() # add to previous result
num = 0 # reset num to build upcoming number
Solution
def calculate(self, s: str) -> int:
res, num, sign, st = 0,0,1,[]
for char in s:
if char.isdigit():
num = 10*num + int(char)
elif char in "+-":
res += sign * num
num = 0
sign = [-1,1][char =="+"]
elif char == "(":
st.append(res)
st.append(sign)
res = 0
sign = 1
elif char == ")":
res += sign * num
res *= st.pop()
res += st.pop()
num = 0
return res + sign*num # handles cases that don't end in ")", like 1 + 1
The time and space complexity are O(n)
, as we process each character once and store intermediate results in the stack.
Now that you’ve learned how to use stacks for expression evaluation, try Decode String. You got this!