Crack the Coding Interview in 100 Hours
The full roadmap, start to finish: 18 patterns that cover the vast majority of FAANG interview questions — each taught with the signal that tells you to reach for it, a reusable template, and signature problems solved in JavaScript and Go. Every solution here was run through node and go before publishing.
- 00Foundations — the plan, the language, the mindset
- 01Arrays & Hashing
- 02Two Pointers
- 03Sliding Window
- 04Stacks
- 05Binary Search
- 06Linked Lists
- 07Trees
- 08Tries (Prefix Trees)
- 09Heap / Priority Queue
- 10Backtracking
- 11Graphs
- 12Advanced Graphs (Dijkstra)
- 131-D Dynamic Programming
- 142-D Dynamic Programming
- 15Greedy
- 16Intervals
- 17Bit Manipulation
- 18Math & Geometry
Foundations: the plan, the language, the mindset
Before a single pattern: how to spend 100 hours, the interview stdlib for JavaScript and Go side by side, Big-O without the hand-waving, and how to actually behave in the room.
The 100-hour plan
A hundred hours is enough to walk into a FAANG loop prepared — but only if it's structured. Grinding random problems plateaus fast. This is the schedule this page is built around: learn a pattern, drill its signature problems in both languages, move on. Roughly two focused hours a day gets you through in seven weeks.
| Block | Hours | What you drill |
|---|---|---|
| 0 · Foundations | 8h | Big-O, your language's interview stdlib (arrays, maps, sets, heaps, sorting), and how to talk while you code. |
| 1 · Arrays & Hashing | 8h | The bread and butter. Hash maps turn O(n²) scans into O(n). Two Sum, Group Anagrams, Top K. |
| 2 · Two Pointers + Sliding Window | 10h | Linear scans over sorted arrays and substrings. 3Sum, Container With Most Water, Longest Substring. |
| 3 · Stacks + Binary Search | 10h | Monotonic stacks and searching on a sorted range — or on the answer itself. Valid Parentheses, Koko. |
| 4 · Linked Lists + Trees | 14h | Pointers and recursion. Reverse a list, detect a cycle, BFS/DFS a tree, level-order traversal. |
| 5 · Tries + Heaps + Backtracking | 14h | Prefix trees, priority queues (the one Go gives you and JS doesn't), and exhaustive search with pruning. |
| 6 · Graphs + Advanced Graphs | 16h | Islands, topological sort / cycle detection, Dijkstra. The topic that separates offers from rejections. |
| 7 · Dynamic Programming (1-D & 2-D) | 16h | The scary one, demystified: recognize the subproblem, memoize, then flatten to a table. |
| 8 · Greedy, Intervals, Bit, Math | 10h | The long tail that still shows up: Jump Game, Merge Intervals, Single Number, Rotate Image. |
| 9 · Mock interviews | 14h | Timed, out-loud, whiteboard-style. This is where preparation becomes performance. Do ~20 mocks. |
Pick a language: JavaScript or Go?
Both are excellent interview languages and this page teaches every solution in both. The honest trade-off:
- JavaScript — ubiquitous, terse, and interviewers always know it. The catches: no built-in heap, sort() defaults to string order, and shift() on an array is O(n).
- Go — fast, explicit, and its zero-value maps make counting elegant. It ships container/heap and sort.Slice. The catch: more ceremony (heap boilerplate, verbose error handling you can mostly skip in interviews).
Pick the one you'll be fastest and calmest in. If you know both, default to JavaScript for speed and reach for Go when the problem needs a heap or heavy integer math.
The interview stdlib, side by side
Ninety percent of problems need only these four tools. Learn them cold in your language so you never burn interview minutes on syntax.
Arrays & slices
// Arrays are dynamic; these are your workhorses.
const a = [3, 1, 2];
a.push(4); // add to end -> [3,1,2,4]
a.pop(); // remove from end -> 4
a.length; // size
// SORTING GOTCHA: default sort is lexicographic (string order)!
[10, 2, 1].sort(); // -> [1, 10, 2] ❌
[10, 2, 1].sort((x, y) => x - y); // -> [1, 2, 10] ✅
// Iterating
for (const x of a) { /* value */ }
a.forEach((x, i) => {});
const doubled = a.map((x) => x * 2);
// Queue: shift() is O(n)! Use an index pointer for BFS instead:
let head = 0;
const q = [1, 2, 3];
while (head < q.length) { const x = q[head++]; }// Slices are dynamic views over an array.
a := []int{3, 1, 2}
a = append(a, 4) // add to end -> [3 1 2 4]
a = a[:len(a)-1] // remove from end
n := len(a) // size
// Sorting — no gotcha, but you pick the right helper:
sort.Ints(a) // ascending ints
sort.Slice(a, func(i, j int) bool { // custom comparator
return a[i] < a[j]
})
// Iterating
for _, x := range a { _ = x } // value
for i, x := range a { _, _ = i, x } // index + value
// Queue for BFS: slice + index, same idea as JS
q := []int{1, 2, 3}
for head := 0; head < len(q); head++ {
x := q[head]; _ = x
}🏋️ Practice first — try these before moving on
Warm-ups — write each from a blank editor:
- Reverse an array in place — no second array, just two pointers.
- Find the maximum value AND its index in a single pass.
- Remove duplicates from a sorted array in place, returning the new length.
- Rotate an array to the right by k positions.
- Move all zeros to the end while keeping the other elements' order.
Hash maps & sets
// Map preserves insertion order and takes any key type. Prefer it over {}.
const count = new Map();
count.set("a", (count.get("a") ?? 0) + 1); // increment with default
count.has("a"); // true
count.get("a"); // 1
count.delete("a");
for (const [k, v] of count) {} // iterate entries
// Set — membership in O(1)
const seen = new Set();
seen.add(5);
seen.has(5); // true
// Frequency map idiom
const freq = new Map();
for (const c of "aab") freq.set(c, (freq.get(c) ?? 0) + 1);// Maps take comparable keys. The zero value of an int map entry is 0,
// so you can increment without checking existence — a big convenience.
count := map[string]int{}
count["a"]++ // no need to init: missing key reads as 0
_, ok := count["a"] // ok == true if present
delete(count, "a")
for k, v := range count {} // iterate (order is RANDOM — never rely on it)
// Set — Go has none, so use a map to empty struct (zero bytes):
seen := map[int]struct{}{}
seen[5] = struct{}{}
_, exists := seen[5] // true
// Frequency map idiom
freq := map[rune]int{}
for _, c := range "aab" { freq[c]++ }🏋️ Practice first — try these before moving on
Warm-ups — write each from a blank editor:
- Count the frequency of each character in a string and return the map.
- Given two arrays, return their intersection (unique common elements).
- Find the first non-repeating character in a string.
- Detect whether an array contains any duplicate, in one pass.
- Given an array, return true if any two numbers sum to a target (Two Sum, from memory).
Heaps / priority queues
This is the biggest divergence between the two languages, so it's worth memorizing your side now — heaps power "top K", Dijkstra, and median problems later.
// JS has NO built-in heap. In an interview, either implement a tiny one
// or keep an array sorted. This min-heap is enough for most problems:
class MinHeap {
heap = [];
size() { return this.heap.length; }
peek() { return this.heap[0]; }
push(v) {
this.heap.push(v);
let i = this.heap.length - 1;
while (i > 0) {
const p = (i - 1) >> 1;
if (this.heap[p] <= this.heap[i]) break;
[this.heap[p], this.heap[i]] = [this.heap[i], this.heap[p]];
i = p;
}
}
pop() {
const top = this.heap[0], last = this.heap.pop();
if (this.heap.length) {
this.heap[0] = last;
let i = 0, n = this.heap.length;
while (true) {
let s = i, l = 2 * i + 1, r = 2 * i + 2;
if (l < n && this.heap[l] < this.heap[s]) s = l;
if (r < n && this.heap[r] < this.heap[s]) s = r;
if (s === i) break;
[this.heap[s], this.heap[i]] = [this.heap[i], this.heap[s]];
i = s;
}
}
return top;
}
}// Go gives you container/heap — you implement 5 methods on a slice type.
import "container/heap"
type MinHeap []int
func (h MinHeap) Len() int { return len(h) }
func (h MinHeap) Less(i, j int) bool { return h[i] < h[j] } // > for max-heap
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)
v := old[n-1]
*h = old[:n-1]
return v
}
// Usage
h := &MinHeap{}
heap.Init(h)
heap.Push(h, 5)
top := (*h)[0] // peek
min := heap.Pop(h).(int)🏋️ Practice first — try these before moving on
Warm-ups — write each from a blank editor:
- Implement the min-heap above from a blank editor — push and pop, no peeking.
- Use your heap to print a stream of numbers' running minimum after each insert.
- Turn your min-heap into a max-heap by changing exactly one comparison.
- Given an array, use a size-k heap to keep only its k smallest elements.
Big-O in ninety seconds
Big-O is just: how does the work grow as the input grows? You only need to recognize a handful, and to say the right one out loud when asked.
| Complexity | Name | You'll see it in |
|---|---|---|
| O(1) | constant | hash lookup, array index, math |
| O(log n) | logarithmic | binary search, balanced-tree ops |
| O(n) | linear | one scan, two pointers, sliding window |
| O(n log n) | linearithmic | sorting, heap of n items |
| O(n²) | quadratic | nested loops, naive pair-checking |
| O(2ⁿ) / O(n!) | exponential / factorial | backtracking, subsets, permutations |
Here's that exact trade — the twelve most important lines on this page. Two Sum brute-forces as a double loop; a hash map of values you've already seen collapses it to a single pass:
function twoSum(nums, target) {
const seen = new Map(); // value -> index
for (let i = 0; i < nums.length; i++) {
const need = target - nums[i];
if (seen.has(need)) return [seen.get(need), i];
seen.set(nums[i], i);
}
return [];
}
// twoSum([2,7,11,15], 9) -> [0, 1]func twoSum(nums []int, target int) []int {
seen := map[int]int{} // value -> index
for i, x := range nums {
if j, ok := seen[target-x]; ok {
return []int{j, i}
}
seen[x] = i
}
return nil
}
// twoSum([]int{2,7,11,15}, 9) -> [0 1]How to behave in the room
Solving isn't enough — FAANG hires for how you think out loud. The roadmap's interview tips, made concrete:
- Clarify before you code. Restate the problem, ask about input size, empty inputs, duplicates, negatives. Two minutes here saves ten later and shows seniority.
- State the brute force first, give its Big-O, then say "but we can do better" and improve. Interviewers want the journey, not a memorized answer teleported onto the board.
- Pattern first, code second. Name the technique ("this is a sliding window") before typing. It anchors you and signals you've seen the shape.
- Narrate as you type, and walk a tiny example through your code by hand to catch bugs before running.
- Struggle a little before asking for help — but ask. Silence for ten minutes reads as stuck; "I'm weighing a heap versus sorting, here's the trade-off" reads as strong. Ask for hints, not solutions.
- Always debrief. After solving, state the complexity, name one edge case you'd test, and one thing you'd improve with more time.
# 1. Read the problem. Restate it out loud.
# 2. Brute force + its Big-O (say it, don't skip it).
# 3. Improve to the target complexity. Name the pattern.
# 4. Code it in your primary language.
# 5. Walk a small example through by hand.
# 6. Re-code it in your second language for reinforcement.
# 7. THREE DAYS LATER: re-solve from a blank file. This is the rep that counts.Arrays & Hashing
The foundation of half of all interview problems. The core insight: a hash map (or set) turns 'have I seen this before?' from an O(n) scan into an O(1) lookup — collapsing quadratic solutions to linear.
🎯 Recognize it when…
You already saw the archetype in Foundations — Two Sum. The pattern generalizes: build a map in one pass, then answer questions against it in O(1). Two more signature problems.
Group words that are anagrams of each other. The trick is finding a canonical key that's identical for all anagrams: the sorted letters. "eat", "tea" and "ate" all sort to "aet". Map that key to the list of words that produced it. (n = number of words, k = word length.)
function groupAnagrams(strs) {
const groups = new Map(); // sortedKey -> list of words
for (const s of strs) {
// Two words are anagrams iff their sorted letters match.
const key = [...s].sort().join("");
if (!groups.has(key)) groups.set(key, []);
groups.get(key).push(s);
}
return [...groups.values()];
}
// groupAnagrams(["eat","tea","tan","ate","nat","bat"])
// -> [["eat","tea","ate"], ["tan","nat"], ["bat"]]func groupAnagrams(strs []string) [][]string {
groups := map[string][]string{} // sortedKey -> list of words
for _, s := range strs {
b := []byte(s)
sort.Slice(b, func(i, j int) bool { return b[i] < b[j] })
key := string(b)
groups[key] = append(groups[key], s)
}
out := [][]string{}
for _, g := range groups {
out = append(out, g)
}
return out
}Return the k most frequent numbers. The obvious solution counts, then sorts by frequency — O(n log n). But we can do O(n) with bucket sort: a number can appear at most n times, so index an array by frequency and drop each number into its bucket. Walk buckets high-to-low and take the first k.
// Bucket sort by frequency: no sorting needed, O(n) overall.
function topKFrequent(nums, k) {
const freq = new Map();
for (const n of nums) freq.set(n, (freq.get(n) ?? 0) + 1);
// buckets[f] = numbers that appear exactly f times. Max f is nums.length.
const buckets = Array.from({ length: nums.length + 1 }, () => []);
for (const [n, f] of freq) buckets[f].push(n);
const out = [];
for (let f = buckets.length - 1; f >= 0 && out.length < k; f--) {
for (const n of buckets[f]) {
out.push(n);
if (out.length === k) break;
}
}
return out;
}
// topKFrequent([1,1,1,2,2,3], 2) -> [1, 2]func topKFrequent(nums []int, k int) []int {
freq := map[int]int{}
for _, n := range nums {
freq[n]++
}
// buckets[f] = numbers appearing exactly f times.
buckets := make([][]int, len(nums)+1)
for n, f := range freq {
buckets[f] = append(buckets[f], n)
}
out := []int{}
for f := len(buckets) - 1; f >= 0 && len(out) < k; f-- {
for _, n := range buckets[f] {
out = append(out, n)
if len(out) == k {
break
}
}
}
return out
}🏋️ Practice first — try these before moving on
You've met Two Sum, Group Anagrams and Top K. Now cement the pattern:
- Contains DuplicateEasy
- Valid AnagramEasy
- Product of Array Except SelfMedium
- Valid SudokuMedium
- Longest Consecutive SequenceMedium
Two Pointers
Two indices walking a (usually sorted) array — from the ends inward, or both from the start. It replaces an O(n²) nested loop with a single O(n) sweep, using the sorted order to decide which pointer to move.
🎯 Recognize it when…
Two pointers from the ends, walking inward. Skip non-alphanumeric characters, compare case-insensitively. The O(1) space matters: the naive answer builds a cleaned string and reverses it, but pointers need no extra memory.
function isPalindrome(s) {
let l = 0, r = s.length - 1;
const ok = (c) => /[a-z0-9]/i.test(c); // alphanumeric only
while (l < r) {
while (l < r && !ok(s[l])) l++; // skip junk from the left
while (l < r && !ok(s[r])) r--; // skip junk from the right
if (s[l].toLowerCase() !== s[r].toLowerCase()) return false;
l++; r--;
}
return true;
}
// isPalindrome("A man, a plan, a canal: Panama") -> truefunc isPalindrome(s string) bool {
ok := func(c byte) bool {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9')
}
lower := func(c byte) byte {
if c >= 'A' && c <= 'Z' {
return c + 32 // ASCII: 'a' is 'A' + 32 (bit 5!)
}
return c
}
l, r := 0, len(s)-1
for l < r {
for l < r && !ok(s[l]) {
l++
}
for l < r && !ok(s[r]) {
r--
}
if lower(s[l]) != lower(s[r]) {
return false
}
l++
r--
}
return true
}The pattern-defining problem. Find all unique triples summing to zero. Sort first, then fix one number and two-pointer the rest for the remaining pair — that's O(n) per anchor, O(n²) total, beating the O(n³) brute force. The fiddly part is skipping duplicates at both the anchor and the pointers so triples aren't repeated.
function threeSum(nums) {
nums.sort((a, b) => a - b); // sort enables the two-pointer sweep
const res = [];
for (let i = 0; i < nums.length - 2; i++) {
if (nums[i] > 0) break; // all positive from here
if (i > 0 && nums[i] === nums[i - 1]) continue; // skip dup anchor
let l = i + 1, r = nums.length - 1;
while (l < r) {
const sum = nums[i] + nums[l] + nums[r];
if (sum < 0) l++;
else if (sum > 0) r--;
else {
res.push([nums[i], nums[l], nums[r]]);
l++; r--;
while (l < r && nums[l] === nums[l - 1]) l++; // skip dup
while (l < r && nums[r] === nums[r + 1]) r--; // skip dup
}
}
}
return res;
}
// threeSum([-1,0,1,2,-1,-4]) -> [[-1,-1,2], [-1,0,1]]func threeSum(nums []int) [][]int {
sort.Ints(nums)
res := [][]int{}
for i := 0; i < len(nums)-2; i++ {
if nums[i] > 0 {
break
}
if i > 0 && nums[i] == nums[i-1] {
continue
}
l, r := i+1, len(nums)-1
for l < r {
sum := nums[i] + nums[l] + nums[r]
switch {
case sum < 0:
l++
case sum > 0:
r--
default:
res = append(res, []int{nums[i], nums[l], nums[r]})
l++
r--
for l < r && nums[l] == nums[l-1] {
l++
}
for l < r && nums[r] == nums[r+1] {
r--
}
}
}
}
return res
}🏋️ Practice first — try these before moving on
Templates learned — now build recall. Solve each from a blank editor, then re-solve cold in three days:
- Two Sum II (Sorted)Medium
- Container With Most WaterMedium
- 3Sum ClosestMedium
- Trapping Rain WaterHard
Sliding Window
A window [left, right] over a contiguous run of an array or string. You expand the right edge to include, and contract the left edge when a constraint breaks — turning 'check every substring' (O(n²)) into one O(n) pass.
🎯 Recognize it when…
The gentlest window: track the cheapest price seen so far (the "buy" edge) and the best profit if you sold today. One pass, constant space. It's a window where the left edge only ever moves to a new minimum.
function maxProfit(prices) {
let minSoFar = prices[0], best = 0;
for (const p of prices) {
minSoFar = Math.min(minSoFar, p); // cheapest buy day seen
best = Math.max(best, p - minSoFar); // best sell-today profit
}
return best;
}
// maxProfit([7,1,5,3,6,4]) -> 5 (buy at 1, sell at 6)func maxProfit(prices []int) int {
minSoFar, best := prices[0], 0
for _, p := range prices {
if p < minSoFar {
minSoFar = p
}
if p-minSoFar > best {
best = p - minSoFar
}
}
return best
}The canonical variable-size window. Expand right; when the new character was already in the window, jump left to just past its previous position. A map of last-seen indices makes the jump O(1) instead of shrinking one step at a time.
function lengthOfLongestSubstring(s) {
const lastSeen = new Map(); // char -> last index
let left = 0, best = 0;
for (let right = 0; right < s.length; right++) {
const c = s[right];
// If we've seen c inside the current window, jump left past it.
if (lastSeen.has(c) && lastSeen.get(c) >= left) {
left = lastSeen.get(c) + 1;
}
lastSeen.set(c, right);
best = Math.max(best, right - left + 1);
}
return best;
}
// lengthOfLongestSubstring("abcabcbb") -> 3 ("abc")func lengthOfLongestSubstring(s string) int {
lastSeen := map[byte]int{} // char -> last index
left, best := 0, 0
for right := 0; right < len(s); right++ {
c := s[right]
if idx, ok := lastSeen[c]; ok && idx >= left {
left = idx + 1
}
lastSeen[c] = right
if right-left+1 > best {
best = right - left + 1
}
}
return best
}🏋️ Practice first — try these before moving on
Templates learned — now build recall. Solve each from a blank editor, then re-solve cold in three days:
Stacks
Last-in-first-out. Perfect for matching pairs, and for the 'monotonic stack' — keeping a stack in sorted order to answer 'next greater/smaller element' problems in a single O(n) pass.
🎯 Recognize it when…
Push openers; on a closer, the top of the stack must be its matching opener. Valid iff every closer matched and the stack ends empty. The textbook use of a stack for nesting.
function isValid(s) {
const stack = [];
const pairs = { ")": "(", "]": "[", "}": "{" };
for (const c of s) {
if (c in pairs) {
// Closing bracket must match the most recent opener.
if (stack.pop() !== pairs[c]) return false;
} else {
stack.push(c); // opener
}
}
return stack.length === 0; // nothing left unclosed
}
// isValid("([]{})") -> true ; isValid("(]") -> falsefunc isValid(s string) bool {
stack := []byte{}
pairs := map[byte]byte{')': '(', ']': '[', '}': '{'}
for i := 0; i < len(s); i++ {
c := s[i]
if open, isClose := pairs[c]; isClose {
if len(stack) == 0 || stack[len(stack)-1] != open {
return false
}
stack = stack[:len(stack)-1] // pop
} else {
stack = append(stack, c) // push opener
}
}
return len(stack) == 0
}For each day, how many days until a warmer one? The monotonic stack: keep a stack of day-indices whose temperatures are decreasing. When today is warmer than the top, we've just resolved that day's wait — pop and record the distance. Each index is pushed and popped once, so it's O(n) despite the nested loop.
function dailyTemperatures(temps) {
const res = new Array(temps.length).fill(0);
const stack = []; // indices of days awaiting a warmer day (decreasing temps)
for (let i = 0; i < temps.length; i++) {
// While today is warmer than the day on top, we've found its answer.
while (stack.length && temps[i] > temps[stack[stack.length - 1]]) {
const j = stack.pop();
res[j] = i - j;
}
stack.push(i);
}
return res;
}
// dailyTemperatures([73,74,75,71,69,72,76,73]) -> [1,1,4,2,1,1,0,0]func dailyTemperatures(temps []int) []int {
res := make([]int, len(temps))
stack := []int{} // indices awaiting a warmer day
for i, t := range temps {
for len(stack) > 0 && t > temps[stack[len(stack)-1]] {
j := stack[len(stack)-1]
stack = stack[:len(stack)-1]
res[j] = i - j
}
stack = append(stack, i)
}
return res
}🏋️ Practice first — try these before moving on
Templates learned — now build recall. Solve each from a blank editor, then re-solve cold in three days:
- Min StackMedium
- Generate ParenthesesMedium
- Car FleetMedium
- Largest Rectangle in HistogramHard
Binary Search
Halve the search space each step: O(log n). The leap that unlocks the hard problems is realizing you can binary-search not just a sorted array, but the ANSWER itself — any value where 'does this work?' flips from no to yes exactly once.
🎯 Recognize it when…
The template to memorize cold. Two things people get wrong: compute mid as lo + (hi - lo) / 2 (not (lo + hi) / 2, which can overflow in languages with fixed-width ints), and be consistent with your loop bounds — here lo <= hi with an inclusive hi.
function search(nums, target) {
let lo = 0, hi = nums.length - 1;
while (lo <= hi) {
const mid = lo + ((hi - lo) >> 1); // avoids overflow; >>1 is /2
if (nums[mid] === target) return mid;
if (nums[mid] < target) lo = mid + 1;
else hi = mid - 1;
}
return -1;
}
// search([-1,0,3,5,9,12], 9) -> 4func search(nums []int, target int) int {
lo, hi := 0, len(nums)-1
for lo <= hi {
mid := lo + (hi-lo)/2 // avoids overflow
switch {
case nums[mid] == target:
return mid
case nums[mid] < target:
lo = mid + 1
default:
hi = mid - 1
}
}
return -1
}There's no sorted array here — the insight is that the answer space is sorted. Koko's eating speed is somewhere in [1, max pile], and "can she finish in h hours at speed k?" is monotonic: true for large k, false for small k. Binary-search for the smallest k where it flips to true. (m = max pile size.)
function minEatingSpeed(piles, h) {
// Feasibility is monotonic: if speed k works, so does any speed > k.
// So binary-search the SMALLEST k in [1, max(piles)] that fits in h hours.
const hoursAt = (k) =>
piles.reduce((sum, p) => sum + Math.ceil(p / k), 0);
let lo = 1, hi = Math.max(...piles);
while (lo < hi) {
const mid = lo + ((hi - lo) >> 1);
if (hoursAt(mid) <= h) hi = mid; // fast enough -> try slower
else lo = mid + 1; // too slow -> must go faster
}
return lo;
}
// minEatingSpeed([3,6,7,11], 8) -> 4func minEatingSpeed(piles []int, h int) int {
hoursAt := func(k int) int {
total := 0
for _, p := range piles {
total += (p + k - 1) / k // ceil(p/k) with integer math
}
return total
}
lo, hi := 1, 0
for _, p := range piles {
if p > hi {
hi = p
}
}
for lo < hi {
mid := lo + (hi-lo)/2
if hoursAt(mid) <= h {
hi = mid // fast enough -> try slower
} else {
lo = mid + 1 // too slow -> go faster
}
}
return lo
}🏋️ Practice first — try these before moving on
Templates learned — now build recall. Solve each from a blank editor, then re-solve cold in three days:
Linked Lists
Nodes chained by pointers. The whole topic is really one skill — manipulating three pointers (prev, curr, next) without losing the rest of the list — plus the fast/slow trick that solves cycles, midpoints, and nth-from-end in one pass.
🎯 Recognize it when…
The most important linked-list problem — its three-pointer dance shows up as a subroutine inside dozens of harder ones (reverse in k-groups, reorder list, palindrome list). Memorize the four lines until they're muscle memory.
// ListNode = { val, next }
function reverseList(head) {
let prev = null, curr = head;
while (curr) {
const next = curr.next; // save the rest of the list
curr.next = prev; // flip the pointer
prev = curr; // advance prev
curr = next; // advance curr
}
return prev; // new head
}
// 1->2->3->null becomes 3->2->1->nulltype ListNode struct {
Val int
Next *ListNode
}
func reverseList(head *ListNode) *ListNode {
var prev *ListNode
curr := head
for curr != nil {
next := curr.Next // save the rest
curr.Next = prev // flip
prev = curr
curr = next
}
return prev
}Floyd's tortoise and hare. A slow pointer takes one step, a fast pointer takes two. If there's a cycle, the fast one eventually laps the slow one and they collide; if not, fast runs off the end. O(1) space — no hash set of visited nodes needed.
// Floyd's tortoise & hare: slow moves 1 step, fast moves 2.
// If there's a loop, fast eventually laps slow and they meet.
function hasCycle(head) {
let slow = head, fast = head;
while (fast && fast.next) {
slow = slow.next;
fast = fast.next.next;
if (slow === fast) return true;
}
return false; // fast fell off the end -> no cycle
}func hasCycle(head *ListNode) bool {
slow, fast := head, head
for fast != nil && fast.Next != nil {
slow = slow.Next
fast = fast.Next.Next
if slow == fast {
return true
}
}
return false
}🏋️ Practice first — try these before moving on
Templates learned — now build recall. Solve each from a blank editor, then re-solve cold in three days:
- Merge Two Sorted ListsEasy
- Reorder ListMedium
- Remove Nth Node From End of ListMedium
- Add Two NumbersMedium
- Merge k Sorted ListsHard
Trees
Recursion's home turf. Almost every tree problem is 'do something at this node, then recurse left and right, then combine.' Master that shape plus level-order BFS and you've covered the vast majority of tree questions.
🎯 Recognize it when…
The template in its purest form — internalize this shape and most "Easy" tree problems fall out of it. Space is O(h), the recursion-stack height: O(log n) for a balanced tree, O(n) worst case for a degenerate one.
// TreeNode = { val, left, right }
function maxDepth(root) {
if (!root) return 0; // base case: empty subtree
return 1 + Math.max(maxDepth(root.left), maxDepth(root.right));
}
// The whole shape of tree recursion: base case, recurse both sides, combine.type TreeNode struct {
Val int
Left *TreeNode
Right *TreeNode
}
func maxDepth(root *TreeNode) int {
if root == nil {
return 0
}
l, r := maxDepth(root.Left), maxDepth(root.Right)
if l > r {
return l + 1
}
return r + 1
}When the answer is grouped by level, recursion is awkward — use BFS with a queue. The key move: capture the queue's size at the start of each level (or swap in a fresh next array, as here) so you process exactly one level before moving on.
function levelOrder(root) {
const res = [];
if (!root) return res;
let queue = [root]; // BFS, one level at a time
while (queue.length) {
const level = [], next = [];
for (const node of queue) {
level.push(node.val);
if (node.left) next.push(node.left);
if (node.right) next.push(node.right);
}
res.push(level);
queue = next; // whole next level at once
}
return res;
}
// A tree [3,9,20,null,null,15,7] -> [[3], [9,20], [15,7]]func levelOrder(root *TreeNode) [][]int {
res := [][]int{}
if root == nil {
return res
}
queue := []*TreeNode{root}
for len(queue) > 0 {
level := []int{}
next := []*TreeNode{}
for _, node := range queue {
level = append(level, node.Val)
if node.Left != nil {
next = append(next, node.Left)
}
if node.Right != nil {
next = append(next, node.Right)
}
}
res = append(res, level)
queue = next
}
return res
}🏋️ Practice first — try these before moving on
Templates learned — now build recall. Solve each from a blank editor, then re-solve cold in three days:
Tries (Prefix Trees)
A tree where each edge is a character and each root-to-node path spells a prefix. It makes 'is this a known word?' and 'does any word start with this?' both O(word length), independent of how many words you've stored.
🎯 Recognize it when…
Build the data structure itself: insert, search (full word), and startsWith (prefix). Each node holds a map (or fixed 26-slot array) of child characters and an isEnd flag. The only difference between search and startsWith is whether you require that end flag. L = word length.
class TrieNode {
children = {}; // char -> TrieNode
isEnd = false; // does a word end here?
}
class Trie {
root = new TrieNode();
insert(word) {
let node = this.root;
for (const c of word) {
node.children[c] ??= new TrieNode(); // create branch if missing
node = node.children[c];
}
node.isEnd = true;
}
search(word) {
const node = this._find(word);
return node !== null && node.isEnd; // must be a full word
}
startsWith(prefix) {
return this._find(prefix) !== null; // any node is enough
}
_find(str) {
let node = this.root;
for (const c of str) {
if (!node.children[c]) return null;
node = node.children[c];
}
return node;
}
}type Trie struct {
children [26]*Trie // fixed array for 'a'-'z' — faster than a map
isEnd bool
}
func Constructor() Trie { return Trie{} }
func (t *Trie) Insert(word string) {
node := t
for _, c := range word {
i := c - 'a'
if node.children[i] == nil {
node.children[i] = &Trie{}
}
node = node.children[i]
}
node.isEnd = true
}
func (t *Trie) find(s string) *Trie {
node := t
for _, c := range s {
i := c - 'a'
if node.children[i] == nil {
return nil
}
node = node.children[i]
}
return node
}
func (t *Trie) Search(word string) bool {
n := t.find(word)
return n != nil && n.isEnd
}
func (t *Trie) StartsWith(prefix string) bool {
return t.find(prefix) != nil
}🏋️ Practice first — try these before moving on
Templates learned — now build recall. Solve each from a blank editor, then re-solve cold in three days:
Heap / Priority Queue
A heap always hands you the smallest (or largest) element in O(log n), without keeping everything sorted. It's the right tool for 'top K', 'kth largest', 'merge k lists', and streaming medians — and it's the one structure Go gives you but JavaScript doesn't.
🎯 Recognize it when…
The counter-intuitive trick: to find the kth largest, keep a min-heap of size k. Push everything; whenever it exceeds k, evict the smallest. What survives is the k largest elements, and the smallest of those — the heap's root — is your answer.
// Keep a MIN-heap of the k largest seen so far. Its root is the answer.
// (Reusing the MinHeap from Foundations.)
function findKthLargest(nums, k) {
const heap = new MinHeap();
for (const n of nums) {
heap.push(n);
if (heap.size() > k) heap.pop(); // evict the smallest -> keep top k
}
return heap.peek(); // kth largest = smallest of the top k
}
// findKthLargest([3,2,1,5,6,4], 2) -> 5import "container/heap"
// MinHeap from Foundations: Len/Less/Swap/Push/Pop on []int.
func findKthLargest(nums []int, k int) int {
h := &MinHeap{}
heap.Init(h)
for _, n := range nums {
heap.Push(h, n)
if h.Len() > k {
heap.Pop(h) // drop the smallest, keep top k
}
}
return (*h)[0] // root of a size-k min-heap
}The elegant two-heap classic. Split the numbers into a lower half (a max-heap, so its largest is at the root) and an upper half (a min-heap). Keep the sizes within one of each other, and the median is either the larger heap's root or the average of the two roots — both O(1) to read, O(log n) to insert.
// Two heaps: a MAX-heap for the lower half, a MIN-heap for the upper half.
// Keep them balanced; the median is a root (or the average of both roots).
class MedianFinder {
low = new MaxHeap(); // lower half (max at root)
high = new MinHeap(); // upper half (min at root)
addNum(num) {
this.low.push(num);
this.high.push(this.low.pop()); // funnel largest-of-low into high
if (this.high.size() > this.low.size())
this.low.push(this.high.pop()); // rebalance: low keeps the extra
}
findMedian() {
if (this.low.size() > this.high.size()) return this.low.peek();
return (this.low.peek() + this.high.peek()) / 2;
}
}// low = max-heap (lower half), high = min-heap (upper half).
type MedianFinder struct {
low *MaxHeap // Less uses >
high *MinHeap // Less uses <
}
func (mf *MedianFinder) AddNum(num int) {
heap.Push(mf.low, num)
heap.Push(mf.high, heap.Pop(mf.low)) // largest-of-low -> high
if mf.high.Len() > mf.low.Len() {
heap.Push(mf.low, heap.Pop(mf.high)) // rebalance
}
}
func (mf *MedianFinder) FindMedian() float64 {
if mf.low.Len() > mf.high.Len() {
return float64((*mf.low)[0])
}
return float64((*mf.low)[0]+(*mf.high)[0]) / 2
}🏋️ Practice first — try these before moving on
Templates learned — now build recall. Solve each from a blank editor, then re-solve cold in three days:
- Last Stone WeightEasy
- K Closest Points to OriginMedium
- Task SchedulerMedium
- Design TwitterMedium
Backtracking
Systematic exhaustive search: choose, explore, un-choose. You build a candidate step by step, recurse, then undo the last choice to try the next — walking the whole decision tree while pruning branches that can't lead anywhere.
🎯 Recognize it when…
The cleanest introduction: here every node of the decision tree is a valid subset, so you record path at the top of every call. Passing i + 1 as the next start prevents reusing an element and prevents [2,1] after [1,2] — no duplicate sets.
function subsets(nums) {
const res = [], path = [];
const backtrack = (start) => {
res.push([...path]); // every node is a valid subset
for (let i = start; i < nums.length; i++) {
path.push(nums[i]); // choose
backtrack(i + 1); // explore (i+1: no reuse, no dup order)
path.pop(); // un-choose
}
};
backtrack(0);
return res;
}
// subsets([1,2,3]) -> [[],[1],[1,2],[1,2,3],[1,3],[2],[2,3],[3]]func subsets(nums []int) [][]int {
res := [][]int{}
path := []int{}
var backtrack func(start int)
backtrack = func(start int) {
cp := make([]int, len(path)) // copy: path is reused
copy(cp, path)
res = append(res, cp)
for i := start; i < len(nums); i++ {
path = append(path, nums[i]) // choose
backtrack(i + 1) // explore
path = path[:len(path)-1] // un-choose
}
}
backtrack(0)
return res
}Same skeleton, two tweaks. The base case is remaining === 0 (we hit the target), and we prune when remaining < 0 (overshot). The single most important line: recurse with i, not i + 1, because a number may be reused — that's the whole difference from Subsets.
function combinationSum(candidates, target) {
const res = [], path = [];
const backtrack = (start, remaining) => {
if (remaining === 0) { res.push([...path]); return; }
if (remaining < 0) return; // overshot: prune
for (let i = start; i < candidates.length; i++) {
path.push(candidates[i]);
backtrack(i, remaining - candidates[i]); // i, not i+1: reuse allowed
path.pop();
}
};
backtrack(0, target);
return res;
}
// combinationSum([2,3,6,7], 7) -> [[2,2,3], [7]]func combinationSum(candidates []int, target int) [][]int {
res := [][]int{}
path := []int{}
var backtrack func(start, remaining int)
backtrack = func(start, remaining int) {
if remaining == 0 {
cp := make([]int, len(path))
copy(cp, path)
res = append(res, cp)
return
}
if remaining < 0 {
return
}
for i := start; i < len(candidates); i++ {
path = append(path, candidates[i])
backtrack(i, remaining-candidates[i]) // i: reuse allowed
path = path[:len(path)-1]
}
}
backtrack(0, target)
return res
}🏋️ Practice first — try these before moving on
Templates learned — now build recall. Solve each from a blank editor, then re-solve cold in three days:
- PermutationsMedium
- Subsets IIMedium
- Word SearchMedium
- Palindrome PartitioningMedium
- N-QueensHard
Graphs
Nodes and edges — and the topic that most often decides a FAANG loop. Almost everything reduces to two traversals: BFS (a queue, shortest path in unweighted graphs) and DFS (recursion, connectivity and cycles). Grids are just graphs where neighbours are the four adjacent cells.
🎯 Recognize it when…
The archetypal grid problem. Scan every cell; each time you hit unseen land, that's a new island — increment the count and flood-fill (DFS) every connected land cell to "0" so you never count it again. Marking visited in place saves a separate visited set.
function numIslands(grid) {
const rows = grid.length, cols = grid[0].length;
let count = 0;
const sink = (r, c) => { // DFS flood-fill
if (r < 0 || c < 0 || r >= rows || c >= cols || grid[r][c] !== "1") return;
grid[r][c] = "0"; // mark visited (sink the land)
sink(r + 1, c); sink(r - 1, c);
sink(r, c + 1); sink(r, c - 1);
};
for (let r = 0; r < rows; r++)
for (let c = 0; c < cols; c++)
if (grid[r][c] === "1") { count++; sink(r, c); } // new island
return count;
}func numIslands(grid [][]byte) int {
rows, cols := len(grid), len(grid[0])
count := 0
var sink func(r, c int)
sink = func(r, c int) {
if r < 0 || c < 0 || r >= rows || c >= cols || grid[r][c] != '1' {
return
}
grid[r][c] = '0' // mark visited
sink(r+1, c)
sink(r-1, c)
sink(r, c+1)
sink(r, c-1)
}
for r := 0; r < rows; r++ {
for c := 0; c < cols; c++ {
if grid[r][c] == '1' {
count++
sink(r, c)
}
}
}
return count
}"Can you finish all courses given prerequisites?" is really "does this directed graph have a cycle?" Kahn's topological sort: track each node's in-degree (unmet prereqs), repeatedly take any node with in-degree 0, and decrement its neighbours. If you can take all of them, there's no cycle.
function canFinish(numCourses, prerequisites) {
const graph = Array.from({ length: numCourses }, () => []);
const indeg = new Array(numCourses).fill(0);
for (const [course, pre] of prerequisites) {
graph[pre].push(course); // pre -> course
indeg[course]++;
}
// Kahn's algorithm: repeatedly take a course with no remaining prereqs.
const queue = [];
for (let i = 0; i < numCourses; i++) if (indeg[i] === 0) queue.push(i);
let taken = 0, head = 0;
while (head < queue.length) {
const c = queue[head++];
taken++;
for (const next of graph[c]) {
if (--indeg[next] === 0) queue.push(next);
}
}
return taken === numCourses; // all taken => no cycle
}
// canFinish(2, [[1,0]]) -> true ; canFinish(2, [[1,0],[0,1]]) -> falsefunc canFinish(numCourses int, prerequisites [][]int) bool {
graph := make([][]int, numCourses)
indeg := make([]int, numCourses)
for _, p := range prerequisites {
course, pre := p[0], p[1]
graph[pre] = append(graph[pre], course)
indeg[course]++
}
queue := []int{}
for i := 0; i < numCourses; i++ {
if indeg[i] == 0 {
queue = append(queue, i)
}
}
taken := 0
for head := 0; head < len(queue); head++ {
c := queue[head]
taken++
for _, next := range graph[c] {
indeg[next]--
if indeg[next] == 0 {
queue = append(queue, next)
}
}
}
return taken == numCourses
}🏋️ Practice first — try these before moving on
Templates learned — now build recall. Solve each from a blank editor, then re-solve cold in three days:
- Clone GraphMedium
- Rotting OrangesMedium
- Pacific Atlantic Water FlowMedium
- Course Schedule IIMedium
- Word LadderHard
Advanced Graphs
Weighted graphs, where edges have costs. The one algorithm to know cold is Dijkstra — shortest path from a source when edge weights are non-negative — implemented with a min-heap that always expands the closest unsettled node next.
🎯 Recognize it when…
Classic single-source shortest path. A signal starts at node k; how long until it reaches everyone? Dijkstra: a min-heap keyed by distance always pops the closest unsettled node, relaxes its edges, and pushes improved distances. The answer is the farthest of all final distances (or −1 if anyone is unreachable).
// Dijkstra with a min-heap of [dist, node]. Returns the time for the
// signal to reach ALL nodes, or -1 if some are unreachable.
function networkDelayTime(times, n, k) {
const graph = Array.from({ length: n + 1 }, () => []);
for (const [u, v, w] of times) graph[u].push([v, w]);
const dist = new Array(n + 1).fill(Infinity);
dist[k] = 0;
const heap = [[0, k]]; // [distance, node], min by distance
while (heap.length) {
heap.sort((a, b) => a[0] - b[0]); // simplest correct PQ (small inputs)
const [d, u] = heap.shift();
if (d > dist[u]) continue; // stale entry
for (const [v, w] of graph[u]) {
if (d + w < dist[v]) {
dist[v] = d + w;
heap.push([dist[v], v]);
}
}
}
let ans = 0;
for (let i = 1; i <= n; i++) ans = Math.max(ans, dist[i]);
return ans === Infinity ? -1 : ans;
}
// networkDelayTime([[2,1,1],[2,3,1],[3,4,1]], 4, 2) -> 2import "container/heap"
type Edge struct{ dist, node int }
type PQ []Edge
func (p PQ) Len() int { return len(p) }
func (p PQ) Less(i, j int) bool { return p[i].dist < p[j].dist }
func (p PQ) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
func (p *PQ) Push(x any) { *p = append(*p, x.(Edge)) }
func (p *PQ) Pop() any {
old := *p; n := len(old); e := old[n-1]; *p = old[:n-1]; return e
}
func networkDelayTime(times [][]int, n, k int) int {
graph := make([][][2]int, n+1)
for _, t := range times {
graph[t[0]] = append(graph[t[0]], [2]int{t[1], t[2]}) // {node, weight}
}
dist := make([]int, n+1)
for i := range dist {
dist[i] = 1 << 30
}
dist[k] = 0
pq := &PQ{{0, k}}
for pq.Len() > 0 {
e := heap.Pop(pq).(Edge)
if e.dist > dist[e.node] {
continue
}
for _, nb := range graph[e.node] {
if e.dist+nb[1] < dist[nb[0]] {
dist[nb[0]] = e.dist + nb[1]
heap.Push(pq, Edge{dist[nb[0]], nb[0]})
}
}
}
ans := 0
for i := 1; i <= n; i++ {
if dist[i] > ans {
ans = dist[i]
}
}
if ans == 1<<30 {
return -1
}
return ans
}🏋️ Practice first — try these before moving on
Templates learned — now build recall. Solve each from a blank editor, then re-solve cold in three days:
1-D Dynamic Programming
The topic people fear needlessly. DP is just recursion where you cache repeated subproblems. The recipe: find the recurrence ('the answer here depends on the answer one/two steps back'), then either memoize the recursion or fill a 1-D table bottom-up.
🎯 Recognize it when…
The "hello world" of DP. Ways to reach step i = ways to i-1 (take one step) + ways to i-2 (take two) — it's Fibonacci. Since each state needs only the previous two, two variables replace the whole table: O(1) space.
function climbStairs(n) {
// Ways to reach step i = ways to i-1 (one step) + ways to i-2 (two steps).
// That's Fibonacci. Keep only the last two values -> O(1) space.
let oneBack = 1, twoBack = 1;
for (let i = 2; i <= n; i++) {
[twoBack, oneBack] = [oneBack, oneBack + twoBack];
}
return oneBack;
}
// climbStairs(5) -> 8func climbStairs(n int) int {
oneBack, twoBack := 1, 1
for i := 2; i <= n; i++ {
oneBack, twoBack = oneBack+twoBack, oneBack
}
return oneBack
}Can't rob two adjacent houses. At each house the choice is binary: skip it (carry the best-so-far) or rob it (best from two houses back, plus this one). dp[i] = max(dp[i-1], dp[i-2] + nums[i]). A greedy "always take the biggest" fails here — which is exactly why it's DP.
function rob(nums) {
// At each house: skip it (keep prev best) or rob it (best before last + this).
let prev = 0, prev2 = 0;
for (const n of nums) {
const take = prev2 + n;
[prev2, prev] = [prev, Math.max(prev, take)];
}
return prev;
}
// rob([2,7,9,3,1]) -> 12 (rob houses 2 + 9 + 1)func rob(nums []int) int {
prev, prev2 := 0, 0
for _, n := range nums {
take := prev2 + n
best := prev
if take > best {
best = take
}
prev2, prev = prev, best
}
return prev
}🏋️ Practice first — try these before moving on
Templates learned — now build recall. Solve each from a blank editor, then re-solve cold in three days:
- House Robber IIMedium
- Coin ChangeMedium
- Longest Increasing SubsequenceMedium
- Word BreakMedium
- Maximum Product SubarrayMedium
2-D Dynamic Programming
Same idea as 1-D DP, but the state needs two indices — usually 'the answer for the first i of one thing and first j of another.' A grid, or two strings compared position by position. Fill a 2-D table; often you can then roll it down to one row.
🎯 Recognize it when…
A robot moves only right or down across an m×n grid — how many paths to the corner? Paths to a cell = paths from the cell above + paths from the left. That's a 2-D table, but since each row depends only on the row above, a single rolling row collapses it to O(n) space.
function uniquePaths(m, n) {
// Paths to a cell = paths from above + paths from the left.
// One row is enough: row[j] += row[j-1] rolls the 2-D table into 1-D.
const row = new Array(n).fill(1);
for (let i = 1; i < m; i++)
for (let j = 1; j < n; j++)
row[j] += row[j - 1];
return row[n - 1];
}
// uniquePaths(3, 7) -> 28func uniquePaths(m, n int) int {
row := make([]int, n)
for j := range row {
row[j] = 1
}
for i := 1; i < m; i++ {
for j := 1; j < n; j++ {
row[j] += row[j-1]
}
}
return row[n-1]
}The template for all two-string DP. dp[i][j] = LCS length of the suffixes starting at a[i] and b[j]. If the characters match, add one and move both diagonally; otherwise take the best of advancing one string or the other. The padding row/column of zeros is the base case that makes the recurrence uniform.
function longestCommonSubsequence(a, b) {
const m = a.length, n = b.length;
// dp[i][j] = LCS length of a[i:] and b[j:]. Extra row/col of zeros = base.
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
for (let i = m - 1; i >= 0; i--) {
for (let j = n - 1; j >= 0; j--) {
if (a[i] === b[j]) dp[i][j] = 1 + dp[i + 1][j + 1]; // match
else dp[i][j] = Math.max(dp[i + 1][j], dp[i][j + 1]); // skip one
}
}
return dp[0][0];
}
// longestCommonSubsequence("abcde", "ace") -> 3 ("ace")func longestCommonSubsequence(a, b string) int {
m, n := len(a), len(b)
dp := make([][]int, m+1)
for i := range dp {
dp[i] = make([]int, n+1)
}
for i := m - 1; i >= 0; i-- {
for j := n - 1; j >= 0; j-- {
if a[i] == b[j] {
dp[i][j] = 1 + dp[i+1][j+1]
} else if dp[i+1][j] > dp[i][j+1] {
dp[i][j] = dp[i+1][j]
} else {
dp[i][j] = dp[i][j+1]
}
}
}
return dp[0][0]
}🏋️ Practice first — try these before moving on
Templates learned — now build recall. Solve each from a blank editor, then re-solve cold in three days:
- Coin Change IIMedium
- Edit DistanceMedium
- Longest Palindromic SubstringMedium
- Buy and Sell Stock with CooldownMedium
Greedy
Make the locally optimal choice at each step and never look back. It's O(n) and beautiful when it works — but it only works when a local optimum is provably global. The skill is spotting which problems allow it (and which are DP in disguise).
🎯 Recognize it when…
The greedy classic. Walk the array keeping a running sum; at each step make one greedy decision — extend the current subarray or restart from the current element (whichever is larger). The moment your running sum would drag the next element down, drop it. Track the best sum seen.
function maxSubArray(nums) {
// At each element, either extend the running sum or restart from here.
let best = nums[0], curr = nums[0];
for (let i = 1; i < nums.length; i++) {
curr = Math.max(nums[i], curr + nums[i]); // restart vs extend
best = Math.max(best, curr);
}
return best;
}
// maxSubArray([-2,1,-3,4,-1,2,1,-5,4]) -> 6 (subarray [4,-1,2,1])func maxSubArray(nums []int) int {
best, curr := nums[0], nums[0]
for i := 1; i < len(nums); i++ {
if curr+nums[i] > nums[i] {
curr += nums[i]
} else {
curr = nums[i] // restart: the prefix was dragging us down
}
if curr > best {
best = curr
}
}
return best
}Can you reach the last index? The greedy insight: track the farthest index reachable so far. Sweep left to right; if you ever stand on an index beyond that frontier, you're stuck. Otherwise extend the frontier. No DP table needed — one number.
function canJump(nums) {
let reach = 0; // farthest index reachable so far
for (let i = 0; i < nums.length; i++) {
if (i > reach) return false; // stuck before this index
reach = Math.max(reach, i + nums[i]);
}
return true;
}
// canJump([2,3,1,1,4]) -> true ; canJump([3,2,1,0,4]) -> falsefunc canJump(nums []int) bool {
reach := 0
for i := 0; i < len(nums); i++ {
if i > reach {
return false
}
if i+nums[i] > reach {
reach = i + nums[i]
}
}
return true
}🏋️ Practice first — try these before moving on
Templates learned — now build recall. Solve each from a blank editor, then re-solve cold in three days:
- Jump Game IIMedium
- Gas StationMedium
- Hand of StraightsMedium
- Partition LabelsMedium
Intervals
Problems about ranges — [start, end] pairs — that overlap, merge, or need scheduling. The universal first move is to sort by start time (occasionally by end time). After sorting, a single linear sweep resolves almost everything.
🎯 Recognize it when…
Sort by start. Then sweep: if the current interval starts at or before the last kept interval's end, they overlap — extend the end; otherwise there's a gap, so start a new interval. The sort is the O(n log n) cost; the sweep is linear.
function merge(intervals) {
intervals.sort((a, b) => a[0] - b[0]); // sort by start — the whole trick
const res = [intervals[0]];
for (let i = 1; i < intervals.length; i++) {
const [start, end] = intervals[i];
const last = res[res.length - 1];
if (start <= last[1]) last[1] = Math.max(last[1], end); // overlap: extend
else res.push([start, end]); // gap: new interval
}
return res;
}
// merge([[1,3],[2,6],[8,10],[15,18]]) -> [[1,6],[8,10],[15,18]]func merge(intervals [][]int) [][]int {
sort.Slice(intervals, func(i, j int) bool {
return intervals[i][0] < intervals[j][0]
})
res := [][]int{intervals[0]}
for i := 1; i < len(intervals); i++ {
last := res[len(res)-1]
if intervals[i][0] <= last[1] {
if intervals[i][1] > last[1] {
last[1] = intervals[i][1] // extend the overlap
}
} else {
res = append(res, intervals[i])
}
}
return res
}Here the list is already sorted, so you skip the sort and get O(n). Three phases: copy all intervals that end before the new one starts, merge everything that overlaps into one widened interval, then copy the rest. Clean, and a favourite because the phases must be exactly right.
function insert(intervals, newInterval) {
const res = [];
let [start, end] = newInterval, i = 0;
const n = intervals.length;
while (i < n && intervals[i][1] < start) res.push(intervals[i++]); // before
while (i < n && intervals[i][0] <= end) { // overlap
start = Math.min(start, intervals[i][0]);
end = Math.max(end, intervals[i][1]);
i++;
}
res.push([start, end]);
while (i < n) res.push(intervals[i++]); // after
return res;
}
// insert([[1,3],[6,9]], [2,5]) -> [[1,5],[6,9]]func insert(intervals [][]int, newInterval []int) [][]int {
res := [][]int{}
start, end := newInterval[0], newInterval[1]
i, n := 0, len(intervals)
for i < n && intervals[i][1] < start {
res = append(res, intervals[i]) // ends before new starts
i++
}
for i < n && intervals[i][0] <= end {
if intervals[i][0] < start {
start = intervals[i][0]
}
if intervals[i][1] > end {
end = intervals[i][1]
}
i++
}
res = append(res, []int{start, end})
for i < n {
res = append(res, intervals[i])
i++
}
return res
}🏋️ Practice first — try these before moving on
Templates learned — now build recall. Solve each from a blank editor, then re-solve cold in three days:
Bit Manipulation
Operating on the binary representation directly. A small toolkit — XOR to cancel pairs, AND/shift to inspect bits, n & (n-1) to drop the lowest set bit — solves a recurring set of problems in O(1) space that look impossible otherwise.
🎯 Recognize it when…
Every number appears twice except one — find it in O(1) space. XOR the whole array: identical values cancel to zero (a ^ a = 0), and zero XOR the lonely number leaves it standing. A hash set works too, but XOR needs no memory and delights interviewers.
function singleNumber(nums) {
// XOR: a^a = 0 and a^0 = a. Pairs cancel; the lonely number survives.
let x = 0;
for (const n of nums) x ^= n;
return x;
}
// singleNumber([4,1,2,1,2]) -> 4func singleNumber(nums []int) int {
x := 0
for _, n := range nums {
x ^= n
}
return x
}Count set bits for every number from 0 to n. The naive way counts each number independently; the elegant way is DP on bits: i has all the bits of i >> 1 (i with its last bit shifted off) plus its own last bit i & 1. One pass, each entry O(1).
function countBits(n) {
// dp[i] = dp[i >> 1] + (i & 1): i has the bits of i/2, plus its last bit.
const dp = new Array(n + 1).fill(0);
for (let i = 1; i <= n; i++) dp[i] = dp[i >> 1] + (i & 1);
return dp;
}
// countBits(5) -> [0,1,1,2,1,2]func countBits(n int) []int {
dp := make([]int, n+1)
for i := 1; i <= n; i++ {
dp[i] = dp[i>>1] + (i & 1)
}
return dp
}🏋️ Practice first — try these before moving on
Templates learned — now build recall. Solve each from a blank editor, then re-solve cold in three days:
- Number of 1 BitsEasy
- Reverse BitsEasy
- Missing NumberEasy
- Reverse IntegerMedium
- Sum of Two IntegersMedium
Math & Geometry
Grid mechanics and number tricks. No deep theory — the challenge is careful index bookkeeping: rotating a matrix in place, spiralling through it with four moving boundaries, or handling overflow. Precision under pressure, not cleverness.
🎯 Recognize it when…
Rotate an n×n matrix 90° clockwise in place. The elegant decomposition: transpose (swap across the diagonal), then reverse each row. Two simple passes beat trying to cycle four cells at a time — and it's far easier to get right under pressure.
function rotate(matrix) {
const n = matrix.length;
// Rotate 90° clockwise = transpose, then reverse each row. In place.
for (let i = 0; i < n; i++)
for (let j = i + 1; j < n; j++)
[matrix[i][j], matrix[j][i]] = [matrix[j][i], matrix[i][j]]; // transpose
for (const row of matrix) row.reverse(); // mirror
return matrix;
}
// rotate([[1,2,3],[4,5,6],[7,8,9]]) -> [[7,4,1],[8,5,2],[9,6,3]]func rotate(matrix [][]int) {
n := len(matrix)
for i := 0; i < n; i++ { // transpose
for j := i + 1; j < n; j++ {
matrix[i][j], matrix[j][i] = matrix[j][i], matrix[i][j]
}
}
for i := 0; i < n; i++ { // reverse each row
for l, r := 0, n-1; l < r; l, r = l+1, r-1 {
matrix[i][l], matrix[i][r] = matrix[i][r], matrix[i][l]
}
}
}Read the matrix in spiral order. Keep four boundaries — top, bottom, left, right — and walk one edge at a time (right, down, left, up), shrinking the matching boundary after each edge. The two if guards prevent re-reading a row or column when the remaining region is a single line.
function spiralOrder(matrix) {
const res = [];
let top = 0, bottom = matrix.length - 1;
let left = 0, right = matrix[0].length - 1;
while (top <= bottom && left <= right) {
for (let c = left; c <= right; c++) res.push(matrix[top][c]); // →
top++;
for (let r = top; r <= bottom; r++) res.push(matrix[r][right]); // ↓
right--;
if (top <= bottom)
for (let c = right; c >= left; c--) res.push(matrix[bottom][c]); // ←
bottom--;
if (left <= right)
for (let r = bottom; r >= top; r--) res.push(matrix[r][left]); // ↑
left++;
}
return res;
}
// spiralOrder([[1,2,3],[4,5,6],[7,8,9]]) -> [1,2,3,6,9,8,7,4,5]func spiralOrder(matrix [][]int) []int {
res := []int{}
top, bottom := 0, len(matrix)-1
left, right := 0, len(matrix[0])-1
for top <= bottom && left <= right {
for c := left; c <= right; c++ {
res = append(res, matrix[top][c])
}
top++
for r := top; r <= bottom; r++ {
res = append(res, matrix[r][right])
}
right--
if top <= bottom {
for c := right; c >= left; c-- {
res = append(res, matrix[bottom][c])
}
bottom--
}
if left <= right {
for r := bottom; r >= top; r-- {
res = append(res, matrix[r][left])
}
left++
}
}
return res
}🏋️ Practice first — try these before moving on
Templates learned — now build recall. Solve each from a blank editor, then re-solve cold in three days:
- Set Matrix ZeroesMedium
- Happy NumberEasy
- Plus OneEasy
- Pow(x, n)Medium
- Multiply StringsMedium


