LeetCode: MinStack
Yet another solution to a very simple exercise.
Problem
One of the exercises proposed in the third chapter of The Algorithm Design Manual1 is this one:
3-4. Design a stack S that supports S.push(x), S.pop(), and S.findmin(), which returns the minimum element of S. All operations should run in constant time.
Right now it seems that the website of the book doesn’t provide a solution (most likely because it is a pretty simple one).
Anyway, this exercise is also provided by LeetCode.
Some solutions
An obvious path to follow to solve this problem is to create a stack and to use an external variable to handle the minimum value, taking care to update it after every push(x)
and most of all after every pop(x)
. Yet, this leaves us with a new issue: if the pop(x)
interests that minimum value, how do we track the previous minimum value, so that we can serve it when getMin()
is called?
I realised most people do like this: they create a second stack; when pushing a new value they insert whatever is smallest between the one they are inserting and the last minimum value recorded.
For instance this is the code that this brilliant youtube channel proposes:
|
|
Oh yes, it works and it does so even in time complexity $\mathcal{O}(1)$, as requested in the assignment!
Other similar approaches exist in the Discussion section of LeetCode2.
Issues with these approaches
The above method carries a burden: the size of the second stack grows linearly with the length of the main one, doubling the memory usage.
Another way
We still create the two stacks.
At the start, both of them are empty: during the first push(x)
we add the same value to both stacks.
During the following push(x)
calls we will be more careful: we always add the value to self.stack
, nonetheless in the stack tracking the minimum values (in my code that’s self.minS
) we save only the val
if it is smaller or equal to the last element contained in self.minS
.
When we pop()
3 a value from the main stack (self.stack
) we delete it from self.minS
only if the val
popped is the same as the last value saved in self.minS
.
By operating in this way we are guaranteed that self.minS
saves the minimum values in decrescent mode, being always certain to work in $\mathcal{O}(1)$ without duplicating (in the average case and when the stack grows) our memory footprint.
This is the naïve implementation:
|
|
Comparing the two approaches
In order to see how the different approaches behave with larger values of n, the following set of instructions have been fed through the cProfile
python module.
|
|
To be noted that:
- as a random int is inserted during the loop, results will vary slightly;
min_len
andstack_len
are incremented beforepop()
on purpose;- we want the size of the stack to grow (
pop()
must happen less frequently thanpush(x)
), otherwise it’s granted that both stacks will always have the same length.
Before
The first solution explored above generates the following result:
|
|
Of course with the first method the two stacks have always the same length. The whole function took 5.59 seconds.
When submitted to LeetCode it returned:
After
Let’s do the same for our script and explore the size of the stacks as we did before:
|
|
I’m not that impressed by the improvement, albeit minor, in speed (a marginal 4.74 seconds), but by the reduction (two orders of magnitude4) in the average length of self.minS
!
At submission, even LeetCode noticed the improvement5.
Last version: 8365627B0DCFEDF1B17B77EB4F33EB6C99ABB1BD26BF4FA7CDB57F9FAB1E6BA6.
-
See The Algorithm Design Manual by Steven Skiena (Springer, 2020). ↩︎
-
To be noted: the interface of the
MinStack
class, as defined by LeetCode, doesn’t require us to return the popped value. ↩︎ -
For these values of n. ↩︎
-
At the moment of writing of this post. ↩︎