Heap & Stack
Key idea — Both structures share a governing principle: the top is the most important thing. Find the logic with respect to the top element. Combined with a hashmap, heap and stack are extremely useful in system design (LRU cache, median stream, rate limiter).
Heap (priority queue) — always gives you the min (or max) in O(log n). Use for top-K, streaming medians, or any "I need the extreme value at every step" problem.
Stack — gives you the most recently seen element in O(1). The monotonic stack maintains an increasing or decreasing order, enabling O(n) solutions to "next greater element" style problems.
When to use
- Heap: top-K frequent elements, merge K sorted lists, sliding window maximum, task scheduling.
- Stack: next greater / smaller element, largest rectangle in histogram, valid parentheses, expression evaluation.
Pattern / template
go
import "container/heap"
// Go has no built-in heap — implement container/heap interface (5 methods).
type MinHeap []int
func (h MinHeap) Len() int { return len(h) }
func (h MinHeap) Less(i, j int) bool { return h[i] < h[j] }
func (h MinHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
func (h *MinHeap) Push(x any) { *h = append(*h, x.(int)) }
func (h *MinHeap) Pop() any {
old := *h; n := len(old); x := old[n-1]; *h = old[:n-1]; return x
}
// Top-K smallest — keep a max-heap of size k (negate to invert order)
func topKSmallest(nums []int, k int) {
h := &MinHeap{}
heap.Init(h)
for _, x := range nums {
heap.Push(h, -x) // negate → max-heap behaviour
if h.Len() > k {
heap.Pop(h) // evict the largest of the k smallest
}
}
// h now holds (negated) K smallest elements
}
// Stack — a plain slice; push = append, pop = s[:len-1]
// No separate type needed.
// Monotonic stack — next greater element
func nextGreater(nums []int) []int {
result := make([]int, len(nums))
for i := range result { result[i] = -1 }
stack := []int{} // stores indices; top = most recent unresolved index
for i, x := range nums {
for len(stack) > 0 && nums[stack[len(stack)-1]] < x {
top := stack[len(stack)-1]
stack = stack[:len(stack)-1]
result[top] = x // x is the next greater for top
}
stack = append(stack, i)
}
return result
}Complexity
- Heap push/pop: O(log n); peek: O(1).
- Monotonic stack: O(n) — each element is pushed and popped at most once.
- Space: O(n) for both.
Pitfalls
- Go has no built-in heap — use
container/heapwith the 5-method interface (Len, Less, Swap, Push, Pop). The boilerplate is unavoidable but small. - For a max-heap, either flip the
Lessmethod or negate values when pushing/popping. - Stack is just a slice:
s = append(s, x)to push;x, s = s[len(s)-1], s[:len(s)-1]to pop. - Monotonic stack direction matters: decreasing stack → next greater element; increasing stack → next smaller.
- Heap does not sort the full slice — only guarantees the top is the min/max.
Common follow-ups
- How do you maintain a running median using two heaps?
- When is a monotonic stack increasing vs decreasing, and how does the direction determine what you're computing?
- What is the time complexity of building a heap from an unsorted array, and why?
- How would you implement a max-heap in Go given that
container/heapis a min-heap by default?
Practice (LeetCode)
- LC 215 — Kth Largest Element in an Array
- LC 347 — Top K Frequent Elements
- LC 23 — Merge k Sorted Lists
- LC 20 — Valid Parentheses
- LC 739 — Daily Temperatures
- LC 84 — Largest Rectangle in Histogram