FAANG Interview Prep
Foundations, Memory & Complexity
Big-O notation, time/space analysis, memory layoutRecursion Complete Guide
Base cases, call stack, tail recursion, memoizationArrays & Array ADT
Static/dynamic arrays, operations, amortized analysisStrings
Pattern matching, string algorithms, encoding, manipulationMatrices
2D arrays, sparse matrices, matrix operations, traversalsLinked Lists
Singly, doubly, circular lists, pointer manipulationStack
LIFO, push/pop, expression evaluation, backtrackingQueue
FIFO, circular queue, deque, priority queueTrees
Binary trees, traversals, expression trees, threaded treesBST & Balanced Trees
Search, insert, delete, AVL, red-black, B-treesHeaps, Sorting & Hashing
Min/max heaps, heapsort, hash tables, collision handlingGraphs, DP, Greedy & Backtracking
BFS, DFS, shortest paths, dynamic programming, optimizationHeap Fundamentals
A Heap is a complete binary tree that satisfies the heap property. It's the underlying data structure for priority queues and is crucial for many interview problems.
Mental Model: The Heap
Problem it solves: Repeatedly extract the minimum (or maximum) element efficiently. Heaps give \(O(\log n)\) insert and \(O(\log n)\) extract-min — far faster than \(O(n)\) linear scan, with \(O(1)\) peek.
When NOT to use: When you need arbitrary element access or full sorted order — a heap only guarantees fast access to the root. Use a sorted array or BST for full ordering.
Common misconception: "A heap is sorted." It is NOT. Heap order only guarantees parent ≤ children (min-heap) — siblings have no ordering relationship. Heap order ≠ sorted order.
Interview lens: The pattern is: "find or maintain K elements" = heap. Top-K largest, K closest points, K-th smallest, sliding window median, merge K sorted lists — all are heap problems.
Production lens: OS schedulers use priority queues (nice values). Dijkstra's and A* pathfinding are heap-powered. Event-driven simulations process the next-soonest event by always pulling from a min-heap.
Heap Structural Properties
- Max Heap: Parent ≥ Children (root is maximum)
- Min Heap: Parent ≤ Children (root is minimum)
- Complete Binary Tree: All levels filled except possibly last, filled left to right
- Array Representation: For index i: parent = (i-1)//2, left = 2i+1, right = 2i+2
Heap Implementation from Scratch
Heap Operations
flowchart TD
A(["Extract root (min)"]) --> B["Move last element to root\nheap.size -= 1"]
B --> C{"Has children?"}
C -->|"No"| G["Heap property restored ✓"]
C -->|"Yes"| D["Find smaller child"]
D --> E{"Current node >\nsmaller child?"}
E -->|"No"| G
E -->|"Yes"| F["Swap current with smaller child"]
F --> C
Build Heap (Heapify)
Building a heap from an unsorted array seems like it should take \(O(n \log n)\) — insert each element one at a time. But the bottom-up heapify approach is faster: start from the last non-leaf node and sift-down each node. Why \(O(n)\) instead of \(O(n \log n)\)? Most nodes are near the bottom and sift-down very little. Mathematically: half the nodes are leaves (sift 0), a quarter sift 1 level, an eighth sift 2 levels... the sum converges to \(O(n)\).
Analogy: Imagine organizing a company hierarchy from the bottom up — managers at the lowest level check only one report, mid-level managers check two levels, and only the CEO checks the full depth. The total work is dominated by the many small checks at the bottom, not the few expensive ones at the top.
Extract-Min + Heapify-Down — Step-Through
Root = 1 is the minimum. Click Next to step through the extract-min + heapify-down process.
Time Complexity Analysis
| Operation | Time | Notes |
|---|---|---|
| Insert | \(O(\log n)\) | Heapify up |
| Extract Min/Max | \(O(\log n)\) | Heapify down |
| Peek | \(O(1)\) | Return root |
| Build Heap | \(O(n)\) | Not \(O(n \log n)\)! |
| Heap Sort | \(O(n \log n)\) | Build + n extractions |
Python heapq Module
Python's built-in heapq module provides a min-heap backed by a regular list. There is no max-heap variant — to simulate one, negate values on insert and negate again on extraction. The module offers \(O(\log n)\) push/pop and a powerful \(O(n)\) heapify() that converts any list into a valid heap in-place. For interview coding, mastering heapq lets you solve priority queue problems in 3-5 lines instead of implementing from scratch.
Key heapq Functions
heapq.heappush(heap, item)— push item maintaining heap invariant, \(O(\log n)\)heapq.heappop(heap)— pop and return smallest, \(O(\log n)\)heapq.heapify(list)— transform list into heap in-place, \(O(n)\)heapq.nlargest(k, iterable)— return k largest elements, \(O(n \log k)\)heapq.heappushpop(heap, item)— push then pop in one operation (faster than separate calls)
import heapq
# heapify - Convert list to heap in-place O(n)
arr = [5, 7, 9, 1, 3]
heapq.heapify(arr)
print("Heapified:", arr) # [1, 3, 9, 7, 5]
# nlargest and nsmallest - O(n log k)
nums = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3]
print("3 largest:", heapq.nlargest(3, nums)) # [9, 6, 5]
print("3 smallest:", heapq.nsmallest(3, nums)) # [1, 1, 2]
# With key function
people = [('Alice', 30), ('Bob', 25), ('Charlie', 35)]
print("Oldest:", heapq.nlargest(1, people, key=lambda x: x[1]))
import heapq
# Max Heap using negative values
max_heap = []
values = [3, 1, 4, 1, 5, 9]
for val in values:
heapq.heappush(max_heap, -val) # Negate to simulate max heap
print("Max heap (negated):", max_heap)
# Extract max
max_val = -heapq.heappop(max_heap)
print("Maximum:", max_val) # 9
import heapq
# Priority Queue with tuples (priority, item)
# Lower priority value = higher priority
pq = []
heapq.heappush(pq, (2, "task B"))
heapq.heappush(pq, (1, "task A"))
heapq.heappush(pq, (3, "task C"))
print("Processing order:")
while pq:
priority, task = heapq.heappop(pq)
print(f" {task} (priority {priority})")
Heap Applications
Heaps shine in problems that repeatedly ask for the extreme element (smallest/largest) from a changing collection. The universal interview pattern: "find K-th / top-K / K closest" — maintain a heap of size K, and every element beyond K either enters (bumping one out) or is discarded. This gives \(O(n \log k)\) instead of \(O(n \log n)\) sorting.
Kth Largest Element
Maintain a min-heap of size K. As you scan the array, push each element; if the heap exceeds size K, pop the smallest (it can't be in the top-K). After processing all elements, the heap root is the K-th largest. This is \(O(n \log k)\) time, \(O(k)\) space — much better than \(O(n \log n)\) full sort when \(k \ll n\). The alternative Quick-Select gives \(O(n)\) average but \(O(n^2)\) worst case.
Top K Frequent Elements
A two-phase problem: first count frequencies using a hash map (\(O(n)\)), then find the K most frequent. A min-heap of size K holding (frequency, element) pairs does this in \(O(n \log k)\). Alternative: bucket sort by frequency — create buckets indexed 1..n where bucket[f] contains all elements with frequency f, then collect from highest bucket down. This gives \(O(n)\) but uses more space.
Merge K Sorted Lists
Given K sorted linked lists, merge them into one sorted list. The naive approach compares all K heads each step (\(O(nk)\) total). A min-heap of size K holding the current head of each list reduces each extraction to \(O(\log k)\), giving \(O(n \log k)\) total where n is the total number of nodes. This is a fundamental pattern in external sorting — when data doesn't fit in memory, sort chunks individually, then K-way merge the sorted chunks using a heap.
Sorting Algorithms Overview
Sorting Algorithms Comparison
| Algorithm | Best | Average | Worst | Space | Stable |
|---|---|---|---|---|---|
| Merge Sort | \(O(n \log n)\) | \(O(n \log n)\) | \(O(n \log n)\) | \(O(n)\) | Yes |
| Quick Sort | \(O(n \log n)\) | \(O(n \log n)\) | \(O(n^{2})\) | \(O(\log n)\) | No |
| Heap Sort | \(O(n \log n)\) | \(O(n \log n)\) | \(O(n \log n)\) | \(O(1)\) | No |
| Counting Sort | O(n+k) | O(n+k) | O(n+k) | \(O(k)\) | Yes |
| Radix Sort | O(nk) | O(nk) | O(nk) | O(n+k) | Yes |
| Bubble Sort | \(O(n)\) | \(O(n^{2})\) | \(O(n^{2})\) | \(O(1)\) | Yes |
| Insertion Sort | \(O(n)\) | \(O(n^{2})\) | \(O(n^{2})\) | \(O(1)\) | Yes |
Sorting Complexity — Visual Comparison
Relative “cost” per algorithm at n = 10,000 (normalised to Merge Sort = 1.0). Lower is better for time; hover for space and stability.
Merge Sort
Merge Sort is the quintessential divide-and-conquer algorithm. It splits the array in half, recursively sorts each half, then merges the two sorted halves in linear time. The genius is that merging two sorted arrays is trivial — just compare the front elements and take the smaller. This guarantees \(O(n \log n)\) in all cases (best, average, worst) — no degenerate inputs exist.
Merge Sort: Tradeoffs
Strengths: Guaranteed \(O(n \log n)\), stable (preserves relative order of equal elements), parallelizable (both halves can be sorted independently), and the go-to for linked list sorting (no random access needed).
Weaknesses: \(O(n)\) extra space for the auxiliary array. For in-memory sorting of arrays, Quick Sort is often faster in practice due to better cache locality.
Real-world usage: Python's sorted() and Java's Arrays.sort() for objects use Timsort — a hybrid of Merge Sort and Insertion Sort that exploits existing order ("runs") in real data.
Quick Sort
Quick Sort selects a pivot element, then partitions the array: all elements smaller than the pivot go left, all larger go right. Then recursively sort each partition. The key operation is the partition step — done in-place in \(O(n)\) time with a two-pointer technique. Unlike Merge Sort which does work when merging, Quick Sort does work when dividing.
Why is Quick Sort usually faster than Merge Sort? Despite having \(O(n^2)\) worst case, Quick Sort has better cache locality (operates on contiguous memory, no auxiliary array), smaller constant factors, and the worst case is avoidable with randomized pivot selection. In practice, Quick Sort is 2-3× faster than Merge Sort for in-memory arrays.
Avoiding \(O(n^2)\) Worst Case
The worst case occurs when the pivot is always the smallest or largest element (already sorted arrays with naive first/last pivot). Solutions: (1) Random pivot selection, (2) Median-of-three (pick median of first, middle, last), (3) Introsort (switch to Heap Sort if recursion depth exceeds \(2 \log n\) — used by C++ STL). Always use randomized pivots in interviews.
Heap Sort
Heap Sort combines the heap's extraction property with in-place sorting. The algorithm has two phases: (1) Build a max-heap from the unsorted array in \(O(n)\) — now the maximum is at index 0. (2) Repeatedly swap the root (maximum) with the last unsorted element, shrink the heap by one, and sift-down to restore the heap property. After \(n-1\) extractions, the array is sorted.
When to choose Heap Sort: It guarantees \(O(n \log n)\) worst case with \(O(1)\) extra space — making it ideal when memory is constrained and worst-case guarantees matter (embedded systems, real-time systems). The tradeoff: it's not stable and has poor cache locality (parent-child jumps in the array are not sequential), making it ~2× slower than Quick Sort in practice for random data.
Hash Tables
A Hash Table (HashMap/Dictionary) is arguably the most important data structure in software engineering. It provides \(O(1)\) average-case lookup, insert, and delete by using a hash function to map keys directly to array indices. Think of it like a library's card catalog: instead of searching every shelf, you compute exactly which shelf a book belongs on.
How it works: (1) Compute hash(key) % array_size to get a bucket index. (2) Store the key-value pair at that index. (3) On lookup, compute the same hash to find the bucket directly. The magic: going from \(O(n)\) linear search to \(O(1)\) constant-time access by trading space for speed.
Real-world ubiquity: Python dict, JavaScript objects, Java HashMap, C++ unordered_map, database indexes, caching layers (Redis/Memcached), DNS resolution, compiler symbol tables — all powered by hashing. In interviews, if you need to check "have I seen this before?" or "count occurrences" — the answer is almost always a hash map.
Hash Table Concepts
- Hash Function: Maps keys to array indices
- Collision: When two keys hash to same index
- Load Factor: n/m (items/buckets) - typically keep < 0.75
- Rehashing: Resize and rehash when load factor exceeds threshold
Collision Handling
A collision occurs when two different keys hash to the same index. Since the hash function maps an infinite key space to a finite array, collisions are inevitable (Pigeonhole Principle). The quality of a hash table depends entirely on how it resolves collisions. Two families of strategies exist:
Separate Chaining (Open Hashing): Each bucket stores a linked list (or dynamic array) of all entries that hash there. Simple, never fills up, but degrades to \(O(n/m)\) per operation when the load factor rises. Java's HashMap uses chaining (upgrading to red-black trees when a bucket exceeds 8 entries).
Open Addressing (Closed Hashing): All entries live directly in the array. On collision, probe for the next available slot using a probing sequence (linear, quadratic, or double hashing). Better cache locality than chaining but suffers from clustering — occupied runs grow and slow down subsequent operations. Python's dict uses open addressing with a custom probing scheme.
Open Addressing (Linear Probing)
graph TD
C["Hash Collision
Resolution"]
C --> OA["Open Addressing
Closed Hashing"]
C --> SC["Separate Chaining
Open Hashing"]
OA --> LP["Linear Probing
h k + i"]
OA --> QP["Quadratic Probing
h k + i squared"]
OA --> DH["Double Hashing
h1 k + i * h2 k"]
SC --> LL["Linked Lists"]
SC --> BST["Balanced BSTs
Java 8+ HashMap"]
style C fill:#132440,stroke:#132440,color:#fff
style OA fill:#16476A,stroke:#132440,color:#fff
style SC fill:#3B9797,stroke:#132440,color:#fff
Load Factor vs Collision Probability (Interactive)
As load factor (α = n/capacity) increases, average probe length and collision probability grow rapidly. Most implementations rehash at α = 0.7–0.75.
Common Hash Table Problems
Hash tables are the Swiss Army knife of interview problems. The three most common patterns: (1) Two-pointer substitute — use a hash map to remember "what have I seen so far?" (Two Sum, Subarray Sum Equals K). (2) Frequency counting — count occurrences, then query (Group Anagrams, Top K Frequent). (3) Set membership — track visited states (detect duplicates, cycle detection, graph visited). Below are the must-know problems that appear in >50% of coding interviews.
Decision Lens: Choosing a Sorting Algorithm
| Scenario | Best Choice | Reason |
|---|---|---|
| General purpose, stability needed | Merge Sort / Timsort | \(O(n \log n)\) guaranteed; stable; Python’s sort() uses Timsort |
| In-place, average performance | Quick Sort (randomized) | \(O(n \log n)\) average, \(O(1)\) extra space; cache-friendly |
| Small arrays (< 32 elements) | Insertion Sort | \(O(n^{2})\) worst but tiny constants; optimal for nearly-sorted small input |
| Integer keys, bounded range | Counting / Radix Sort | O(n+k) — beats comparison lower bound when k is small |
| Extract top-K only | Partial Heap Sort | O(n log K) — no need to fully sort 10M records to find top 100 |
| External sort (disk data) | External Merge Sort | Minimizes disk I/O by sorting in chunks and merging |
LeetCode Practice Problems
Essential Problems
| # | Problem | Difficulty | Key Concept |
|---|---|---|---|
| 215 | Kth Largest Element in an Array | Medium | Min heap of size k |
| 347 | Top K Frequent Elements | Medium | Hash map + heap |
| 23 | Merge k Sorted Lists | Hard | Min heap for k-way merge |
| 295 | Find Median from Data Stream | Hard | Two heaps (max + min) |
| 973 | K Closest Points to Origin | Medium | Max heap of size k |
| 1 | Two Sum | Easy | Hash map lookup |
| 49 | Group Anagrams | Medium | Hash map grouping |
| 128 | Longest Consecutive Sequence | Medium | Hash set for \(O(n)\) |
| 912 | Sort an Array | Medium | Merge/Quick/Heap sort |
| 148 | Sort List | Medium | Merge sort on linked list |
Complete DSA Series
FAANG Interview Preparation
- Stack & Applications
- Queue & Variants
- Trees & Traversals
- BST & Balanced Trees
- Heaps, Sorting & Hashing (You are here)
- Graphs, DP & Greedy
Quick Check — Test Yourself
- Why is
build_heap\(O(n)\) and not \(O(n \log n)\)? Prove it by summing the work per level. - Given a stream of integers, find the median after each insertion using two heaps.
- A hash table has load factor 0.9. Approximately how many probes does a lookup need on average (open addressing)?
Common Bugs
- Python’s heapq is a min-heap only: For max-heap, negate values: push
-val, pop and negate result. Forgetting the negation is a very common interview mistake. - Hash collisions with mutable keys: In Python, mutable objects (lists, dicts) cannot be dict keys — they’re not hashable. Use tuples for composite keys.
- Quicksort worst case on sorted input: Always use randomised pivot selection or 3-way partition to avoid \(O(n^{2})\) on already-sorted data.
Interview Lens
The two key heap patterns: (1) Top-K: maintain a min-heap of size K — if new element > heap.min, pop and push. O(n log K). (2) Two-heap median: left max-heap + right min-heap, balance sizes to within 1. Both appear in every senior-level interview loop.
Production Lens
Python’s dict uses open addressing with a compact table — benchmarks show it outperforms Java’s HashMap (chaining) at low load factors due to cache locality. Java’s HashMap converts chains to Red-Black trees at bucket size 8 (Java 8+) to prevent \(O(n)\) worst case from hash flooding attacks.
Next in the Series
In Part 12: Graphs, DP, Greedy & Backtracking, we'll bring together everything from the series — graphs unify trees, queues, and heaps; DP and greedy build on recursion and arrays; backtracking uses the stack patterns from Part 7.