From Theory to Mastery
[!NOTE] This module explores the core principles of Stacks & Queues Practice Problems, deriving solutions from first principles and hardware constraints to build world-class, production-ready expertise.
You know push and pop. Now let’s solve problems that appear in Google, Amazon, and Meta interviews.
1. Daily Temperatures
Problem: Given [73, 74, 75, 71, 69, 72, 76, 73]. Return [1, 1, 4, 2, 1, 1, 0, 0].
(How many days until a warmer temperature?)
Constraints:
1 <= temperatures.length <= 10^530 <= temperatures[i] <= 100
Edge Cases & Clarifying Questions
- Q: What if there is no future day with a warmer temperature?
A: Return
0for that day. - Q: What if all temperatures are the same or strictly decreasing?
A: Return
0for all days.
Intuition
Brute Force: The simplest approach is for every day, scan all subsequent days to find the first day that is warmer. The time complexity for this is O(N^2), which is too slow given the constraint of N = 10^5.
Optimized: Monotonic Stack: We want the Next Greater Element. Instead of rescanning future days, we can keep track of days waiting for a warmer temperature. Use a Decreasing Monotonic Stack storing indices.
- If
current > top, we found a warmer day fortop. Pop and calculate distance. - If
current <= top, push thecurrentindex onto the stack.
Common Pitfalls:
- Storing temperature values instead of indices in the stack. We need the indices to calculate the distance.
- Forgetting to process remaining elements (they naturally default to 0 if the result array is initialized with 0s).
Interactive Analysis
Watch how the monotonic stack resolves the next warmer temperature.
Temperatures
Stack (Indices)
Solutions
public int[] dailyTemperatures(int[] temperatures) {
int n = temperatures.length;
int[] res = new int[n];
Deque<Integer> stack = new ArrayDeque<>(); // Indices
for (int i = 0; i < n; i++) {
while (!stack.isEmpty() && temperatures[i] > temperatures[stack.peek()]) {
int prevIndex = stack.pop();
res[prevIndex] = i - prevIndex; // Distance
}
stack.push(i);
}
return res;
}
func dailyTemperatures(temperatures []int) []int {
n := len(temperatures)
res := make([]int, n)
stack := []int{} // Indices
for i, t := range temperatures {
for len(stack) > 0 && t > temperatures[stack[len(stack)-1]] {
idx := stack[len(stack)-1]
stack = stack[:len(stack)-1]
res[idx] = i - idx
}
stack = append(stack, i)
}
return res;
}
Complexity
| Strategy | Time | Space | Notes |
|---|---|---|---|
| Brute Force | O(N^2) | O(1) | Too slow. Time Limit Exceeded. |
| Monotonic Stack | O(N) | O(N) | Each index is pushed and popped at most once. |
2. Largest Rectangle in Histogram
Problem: Given heights [2, 1, 5, 6, 2, 3]. Find the area of the largest rectangle.
Constraints:
1 <= heights.length <= 10^50 <= heights[i] <= 10^4
Edge Cases & Clarifying Questions
- Q: What if all heights are 0? A: The area is 0.
- Q: What if all bars have the same height? A: The area is the height multiplied by the number of bars.
Intuition
Brute Force:
For every bar, find the left and right boundaries where the height is greater than or equal to the current bar’s height. The area with the current bar as the minimum height is current_height * (right - left + 1). Time complexity is O(N^2), which is too slow.
Optimized: Monotonic Stack: This is the Next Smaller Element (and Previous Smaller Element) problem! We can do this in one pass with a Monotonic Increasing Stack.
- Pop Condition: When current height is less than stack top. The popped bar is taller than current.
- Area:
h * (i - new_top - 1).
Common Pitfalls:
- Forgetting to process the remaining elements in the stack after the loop ends. A common trick is to use a dummy
-1index. - Calculating width incorrectly. It should be
current_index - stack.peek() - 1after popping.
Interactive Analysis
Watch how the stack calculates area.
- Push index if height is increasing.
- Pop if height decreases. Calculate area for popped bar.
Histogram [2, 1, 5, 6, 2, 3]
Stack (Indices)
Solutions
public int largestRectangleArea(int[] heights) {
Deque<Integer> stack = new ArrayDeque<>();
stack.push(-1); // Sentinel for left boundary
int maxArea = 0;
for (int i = 0; i < heights.length; i++) {
while (stack.peek() != -1 && heights[i] <= heights[stack.peek()]) {
int h = heights[stack.pop()];
int w = i - stack.peek() - 1; // Right - Left - 1
maxArea = Math.max(maxArea, h * w);
}
stack.push(i);
}
// Clean up remaining stack
while (stack.peek() != -1) {
int h = heights[stack.pop()];
int w = heights.length - stack.peek() - 1;
maxArea = Math.max(maxArea, h * w);
}
return maxArea;
}
func largestRectangleArea(heights []int) int {
stack := []int{-1} // Sentinel
maxArea := 0
for i, h := range heights {
for len(stack) > 1 && h <= heights[stack[len(stack)-1]] {
height := heights[stack[len(stack)-1]]
stack = stack[:len(stack)-1]
width := i - stack[len(stack)-1] - 1
if area := height * width; area > maxArea {
maxArea = area
}
}
stack = append(stack, i)
}
// Remaining
for len(stack) > 1 {
height := heights[stack[len(stack)-1]]
stack = stack[:len(stack)-1]
width := len(heights) - stack[len(stack)-1] - 1
if area := height * width; area > maxArea {
maxArea = area
}
}
return maxArea;
}
Complexity
| Strategy | Time | Space | Notes |
|---|---|---|---|
| Brute Force | O(N^2) | O(1) | Checking all left/right pairs. |
| Monotonic Stack | O(N) | O(N) | One pass. |
3. Implement Queue using Stacks
Problem: Implement a FIFO Queue using only two LIFO Stacks.
Constraints:
1 <= x <= 9- At most
100calls topush,pop,peek, andempty. - All calls to
popandpeekare valid.
Edge Cases & Clarifying Questions
- Q: What happens if we call
popon an empty queue? A: The problem guarantees valid calls, but normally we would return a sentinel or throw an exception.
Intuition
Brute Force:
For every push operation, we can push onto s1. For every pop operation, we can transfer all elements from s1 to s2, pop the top of s2, and transfer everything back to s1. This makes pop O(N) and push O(1).
Optimized: Amortized O(1):
Instead of transferring elements back and forth, keep elements in s2!
- Push (
s1): Incoming elements always go tos1. - Pop / Peek (
s2): Outgoing elements come froms2. Ifs2is empty, pour all elements froms1intos2. This reverses order (LIFO + LIFO = FIFO).
Common Pitfalls:
- Transferring elements back to
s1after a pop. This defeats the optimization. - Not transferring all elements from
s1when pouring.
Interactive Analysis
Watch how pouring elements from Stack 1 to Stack 2 reverses their order.
Stack 1 (In)
Stack 2 (Out)
Solutions
class MyQueue {
Deque<Integer> in = new ArrayDeque<>();
Deque<Integer> out = new ArrayDeque<>();
public void push(int x) {
in.push(x);
}
public int pop() {
peek(); // Ensure out has elements
return out.pop();
}
public int peek() {
if (out.isEmpty()) {
while (!in.isEmpty()) {
out.push(in.pop());
}
}
return out.peek();
}
}
type MyQueue struct {
in, out []int
}
func (this *MyQueue) Push(x int) {
this.in = append(this.in, x)
}
func (this *MyQueue) Pop() int {
this.Peek()
val := this.out[len(this.out)-1]
this.out = this.out[:len(this.out)-1]
return val
}
func (this *MyQueue) Peek() int {
if len(this.out) == 0 {
for len(this.in) > 0 {
val := this.in[len(this.in)-1]
this.in = this.in[:len(this.in)-1]
this.out = append(this.out, val)
}
}
return this.out[len(this.out)-1]
}
Complexity
| Strategy | Time | Space | Notes |
|---|---|---|---|
| Brute Force | O(N) per pop/push | O(N) | Transferring back and forth. |
| Amortized | O(1) Amortized | O(N) | Each element is transferred at most once. |