Stacks & Queues
Stacks and queues answer one question: which unfinished item should be handled next? Choose the order, state what each stored entry means, then write the update rule.
Fast review: focus on matching, reduction, and expiration. Practice Custom Brackets, Compress Array, Min Stack, and Viewer Counter; the other exercises are optional variations.
1. Choose the Access Order
| Trigger | Structure | Meaning of the next item |
|---|---|---|
| Nested pairs, undo, depth-first work | Stack · LIFO | Most recent unfinished item |
| Ordered arrivals, breadth-first work | Queue · FIFO | Oldest waiting item |
| Expire at the front and update at the back | Deque | Both ends have useful roles |
Implementation trap: use Python's deque.popleft() for FIFO removal; repeated list.pop(0) can turn a scan into O(n²). A dynamic-array stack has amortized O(1) push/pop; deque end operations are O(1). In Go, a queue with a head index needs to reclaim consumed storage if it is long-lived.
2. Recipe: Validate Nested Brackets
Use when: pairs can nest, and a closing bracket must match the most recent opening bracket that has not been closed yet. The goal is to validate both bracket types and nesting order.
For example, ([]) is valid. In ([)], after reading ([, the [ must close first. The next character is ), so the string is invalid—even though the total counts match. This is why we check only the stack top, rather than search for any matching opener.
Recipe: push opening brackets. For a closing bracket, reject an empty stack or a mismatched top; otherwise pop the matched opener. At the end, the stack must be empty. The stack always contains the unmatched opening brackets, in their original order.
def valid_brackets(s):
opening_for = {')': '(', ']': '[', '}': '{'}
openings = set(opening_for.values())
stack = []
for char in s:
if char in openings:
stack.append(char)
elif char in opening_for:
if not stack or stack[-1] != opening_for[char]:
return False
stack.pop() # This pair is now complete.
# Ignore non-bracket characters.
return not stack # Any remaining opener is unmatched.
Each bracket is pushed and popped at most once: O(n) time, O(n) space. Custom Brackets applies this recipe to a supplied set of pairs. If the task asks which brackets to remove, store opening indices so unmatched positions can be identified.
When a counter is enough: with only ( and ), track balance = opens − closes. It must never be negative and must end at zero. Multiple bracket types require the stack's ordering information; a counter loses it. For a balanced input, cutting at every return to zero gives the maximum number of balanced pieces: a cut at positive balance would leave an opener unmatched. See Balanced Partition.
3. Common Applications
Chain reactions
In Compress Array, repeatedly merge the leftmost adjacent equal pair into its sum. Scan left to right, keeping the processed prefix fully reduced. Hold the new value separately: while it equals the top, pop and add that top to it; push the value only after the chain stops. This requires while, not if.
For [8, 4, 2] followed by 2, the merges are 2+2 → 4+4 → 8+8 → 16. Each input is pushed once and each merge removes an entry, so the scan is O(n) time and O(n) space despite the nested loop. The full implementation is on the problem page.
Reverse Polish Notation is another reduction: pop operands and push the result. Pop the right operand before the left.
Expiring events
In Viewer Counter, keep arrivals in a queue: append on arrival, evict expired entries before answering, then read the remaining count. Timestamps across arrivals and queries must be nondecreasing, so expired entries form a prefix of the queue. The invariant after eviction is that the queue contains exactly the events in the current window.
For the inclusive window [t − window, t], evict while oldest < t - window. At t=12 with window=10, timestamp 2 stays; at t=13 it expires. This counts arrival events, not unique users. Each event enters and leaves once: amortized O(1) per operation, though one query may evict many. Space is O(m) for m retained events; expire on arrivals too if queries may be rare. Out-of-order timestamps break FIFO expiration. Continue to Sliding Windows for maintaining window aggregates.
Reusable idea — carry extra information. Choose the entry's fields from the information needed by the next update:
| Need | Stored state | Update rule |
|---|---|---|
| Merge k equal items | (value, count) | Start a new run at count 1; increment an equal top; at k, remove it and process value × k again |
| Group arrivals at one timestamp | (timestamp, count) plus a total | Extend the newest group if timestamps match, otherwise append a group; update the total on arrival and expiration |
| Retrieve the minimum after a pop | (value, minimum_so_far) | Store min(value, previous minimum), or value when empty; popping reveals the previous summary |
Grouping changes the representation: number of groups is not number of events. Prefix summaries retain history: pushing 2 twice into Min Stack and popping once must still leave minimum 2. If using a separate minima stack instead of pairs, record tied minima as well.
4. Key Takeaways
- Use a stack for nested brackets; use a queue for expiring events. A closing bracket must match the most recent unmatched opener. With time-ordered arrivals, the oldest events expire first.
- Store extra information to avoid scanning again. In Min Stack, store each value together with the minimum so far. Popping removes both and reveals the previous minimum, so finding the minimum stays O(1).
- A nested loop can still take O(n) total time. In Compress Array, one input may trigger several merges, but the entire scan performs n pushes and at most n pops. Count those total operations instead of multiplying the two loop bounds.
Where to Apply These Ideas
| Situation | Reusable move | Continue with |
|---|---|---|
| DFS or nested execution | Store unfinished work in a stack | Recursion, Trees |
| BFS / fewest edges in an unweighted graph | Process a FIFO frontier; mark visited on enqueue | Graphs |
| Back / forward history | Move entries between two stacks; a new visit clears forward history | Browser history variation |
| Queue built from stacks | Transfer only when the output stack is empty | Queue using Stacks |
For two-stack implementations, count how often an item switches stacks. In a queue built from two stacks, each item is pushed into the input stack, transferred once to the output stack, and removed once: O(1) amortized per operation. Browser back/forward can move the same page repeatedly, so its cost depends on the total actual moves.
Practice Problems
Core Practice
Compress Array
Merge the leftmost equal neighbors until no more chain reactions remain.
Viewer Counter Class
Count arrivals of each viewer type within the last window seconds, including both endpoints.
Custom Brackets
Validate nesting for arbitrary bracket pairs, ignoring non-bracket characters.
Min Stack
Maintain the current minimum, including duplicate minima, alongside a stack.
Optional Variations (7)
Choose a variation when you want to explore a specific pattern. These are not required before moving to the next topic.
Compress Array by K
Track value/count runs and resolve cascading merges of k equal neighbors.
Current URL
Model browser back navigation while preserving the first page.
Current URL with Forward
Move pages between two stacks and invalidate forward history on a new visit.
Balanced Partition
Maximize balanced pieces by cutting at every return to height zero.
Longest Balanced Subsequence
Remove unmatched parentheses by tracking positions, preserving a longest subsequence.
Evaluate Reverse Polish Notation
Evaluate postfix expressions with the correct operand order and integer division.
Implement Queue using Stacks
Reverse incoming values only when the output stack is empty to preserve FIFO.