Stacks, Queues & Monotonic Stacks
LIFO, FIFO, and a clever variant that solves the "next greater element" family of problems.
Stacks (LIFO)
A stack is a Last-In, First-Out collection. The two operations are push (add to top) and pop (remove from top). Both are O(1). Real-world examples: the call stack of a running program, browser back history, undo buffers.
In Python, just use a list and treat `append` and `pop` as push/pop. In JS, same with `push` and `pop` on an Array.
Classic problem: balanced parentheses
def is_balanced(s):
pairs = {")": "(", "]": "[", "}": "{"}
stack = []
for ch in s:
if ch in "([{":
stack.append(ch)
elif ch in pairs:
# close: top of stack must match
if not stack or stack.pop() != pairs[ch]:
return False
return not stack
print(is_balanced("()[]{}")) # True
print(is_balanced("([)]")) # FalseStacks naturally model nesting. If your problem involves matching pairs, undoing the most recent action, or tracking depth — try a stack first.
Queues (FIFO)
A queue is First-In, First-Out: enqueue at the back, dequeue from the front. Both should be O(1). In Python, DON'T use a list as a queue — `pop(0)` is O(n). Use `collections.deque` instead.
from collections import deque
q = deque()
q.append("a") # enqueue
q.append("b")
q.append("c")
print(q.popleft()) # \"a\" — dequeue, O(1)Queues are the natural data structure for breadth-first search (BFS), level-order tree traversal, and producer/consumer pipelines.
Monotonic stack
A monotonic stack is a stack whose elements are kept in sorted order (e.g. always increasing). Whenever you push something that would break the invariant, you pop the offenders first. This solves an entire family of problems where you need the "next greater" or "previous smaller" element for every position.
// For each element, find the index of the next greater element to the right.
// Returns -1 if none.
function nextGreater(nums) {
const result = new Array(nums.length).fill(-1);
const stack = []; // holds INDICES, with values that are decreasing
for (let i = 0; i < nums.length; i++) {
while (stack.length && nums[stack[stack.length - 1]] < nums[i]) {
const j = stack.pop();
result[j] = i;
}
stack.push(i);
}
return result;
}
console.log(nextGreater([2, 1, 5, 3, 6, 4]));
// [2, 2, 4, 4, -1, -1]Each element is pushed and popped at most once, so the entire algorithm is O(n) — a huge win over the naive O(n²) for-each-pair approach.