LearnThatStack Ace your next interview

Sorting, Searching & Recursion

Code in JavaScript unless a row names the language Checked Oct 2026 10 code snippets

The model

  • Binary search checks the middle and drops half the range at each step, so a million items take about 20 checks. The data must be sorted by the key you search.
  • No comparison sort beats O(n log n). Quicksort is usually fastest but can hit O(n^2). Merge sort is stable and always O(n log n), but needs O(n) extra memory.
  • Every recursion needs a base case and a step that shrinks the problem. Each call waiting on a deeper one holds a stack frame, so deep recursion overflows the stack.
  • The shape of the call tree sets the cost. One call on half the input gives log n. Two calls on halves plus a linear merge give n log n. Two calls on n-1 give 2^n.
  • Backtracking tries a choice, explores it, then undoes it, and prunes paths that can't work. Greedy takes the best choice now and never goes back, so it needs a proof.
nn/2n/2n/4n/4n/4n/4
binary searchEach call keeps one half. The faded calls never happen, so n items take about log n calls.
nn/2n/2n/4n/4n/4n/4
merge sortTwo calls on halves, then a linear merge. log n levels of n work make n log n.
nn-1n-1n-2n-2n-2n-2
backtrackingEach item is taken or skipped, so each call makes two calls on n-1. That makes 2^n leaves.
Each tree shows one algorithm's calls, marked with its cost from the key. Binary search never makes the faded calls.

How the main sorts work

quicksort, merge sort, heapsort and what each costs

Sort costs at a glance

O(n log n) average, O(n^2) worst.Sort with quicksort
O(n log n) always, O(n) extra.Sort with merge sort
O(n log n) always, O(1) extra.Sort with heapsort
O(n^2), near O(n) if nearly sorted.Sort with insertion sort
O(n + k) for keys in a range of k.Sort small integers with counting sort
O(d * (n + b)) for d digits, base b.Sort fixed-width keys with radix sort
yesno

TipStart with the built-in sort. Switch only when a constraint rules it out.

Quicksort's worst case

n0n-10n-20n-3
bad pivotsA last-item pivot on sorted input leaves one side empty. n levels of n work make n^2.
Spot what makes quicksort O(n^2)The first or last item as pivot, on sorted input.
Sort input full of equal keysThree-way partition. Equal keys get a middle group.
Pick a pivot no fixed input can beatChoose it at random. Expected time stays O(n log n).
Handle sorted input cheaplyUse the median of the first, middle and last items.

TrapMedian of three fixes sorted input, but a crafted input can still force O(n^2).

Heapsort

Sort with a heapBuild a max-heap. Swap the root to the end, sift down.
Explain why libraries rarely pick itParent and child sit far apart, so it misses the cache.

TipIts real job is a fallback. Introsort switches to it when quicksort recurses too deep.

Quicksort or merge sort

Sort an array fast with little memoryQuicksort. It sorts in place with a tight inner loop.
Keep ties in order or cap the worst caseMerge sort. It's stable and never worse than O(n log n).
Explain why quicksort usually winsMerge sort copies to a second buffer and misses the cache.
Rank the n log n sorts on real hardwareQuicksort first, heapsort last. Cache misses decide it.

TrapQuicksort isn't faster in Big-O. Both average O(n log n), and quicksort wins on cache use and constant factors.

Stable & library sorts

keep ties in order, and know what your language runs

Stable sorts

Say what a stable sort doesItems with equal keys keep their original order.
Sort by last name, then first nameSort by first name, then stable-sort by last name.
Name the stable sortsMerge, insertion and counting. Not quicksort or heapsort.
Explain what stability costsMemory or speed. Merge sort pays an O(n) buffer.
Make an unstable sort stableBreak ties on the original index, at O(n) memory.

TrapAn unstable sort can reorder equal keys, so sorting in two passes stops working.

What your language's sort runs

Sort in Pythonsorted(xs)Powersort since 3.11, a stable Timsort variant.
Sort in Javalist.sort(cmp)Timsort for objects, quicksort for primitives.
Sort in C++std::sort(v.begin(), v.end())Introsort, unstable. std::stable_sort keeps ties in order.
Sort numbers in JavaScriptnums.sort((a, b) => a - b)Without a comparator, numbers sort as strings.

TipJavaScript's sort has had to be stable since ES2019.

Faster than n log n

the limit on comparison sorts, and how to get under it

Why comparison sorts can't beat n log n

Prove the n log n limitTelling n! orders apart takes log2(n!) comparisons, about n log n.
Name the sorts the limit doesn't coverCounting and radix sort. They never compare keys.

TipDraw a decision tree with n! leaves. Its height is the worst-case comparison count.

Counting sort and radix sort

Sort with counting sortCount each value, then place each item in its final slot.
Decide when counting sort fitsInteger keys in a small, dense range, like 0 to 255.
Sort with radix sortA stable counting sort per digit, lowest digit first.

TipKeys spread over billions of values make counting sort's array too big.

TrapEach digit's pass must be stable, or it scrambles the earlier digits.

Find the k-th smallest without sorting

Find the k-th smallest or the medianQuickselect, O(n) on average. Recurse only into k's side.
Answer many different kSort once in O(n log n), then read each answer in O(1).
Guarantee linear-time selectionMedian of medians, O(n) at worst, or introselect.

TrapQuickselect is O(n^2) with bad pivots, and it reorders the array.

Sorting at scale

data bigger than memory, many machines, many threads

Sort more data than fits in memory

Make sorted runsSort memory-sized chunks and write each out as a run.
Merge the runsK-way merge, with a heap picking the next item.
Tune the merge widthWider merges cut passes but make reads smaller.

TipDisk reads set the cost, not comparisons. Databases sort big ORDER BY results this way.

Sort across many machines

Pick each machine's key rangeSample the data to pick balanced split points.
Shuffle and sortSend records to their range's machine, then sort locally.

TipNo final merge is needed. The network shuffle usually takes most of the time.

TrapA poor sample skews the ranges, and one machine gets most of the data.

Sort on many threads

Parallelize quicksort or merge sortEach recursive call is a task on its own range. No locks.
Find the serial bottleneckQuicksort's first partition, and merge sort's last merge.
Stop spawning tasksBelow a few thousand items, run sequentially.
Find the limit on speedupThe serial merge, by Amdahl's law. Memory bandwidth too.

TipA work-stealing pool lets idle threads take waiting subtasks.

TrapFalse sharing. Threads writing slices on one cache line slow each other.

Sort when comparisons are expensive

Compute an expensive sort key once per itemsorted(items, key=score)Python calls score n times, not n log n.
Pick the sort with the fewest comparisonsMerge sort, near the minimum. Heapsort compares more.

TipCache each pair's result if the same pair can come up again.

Recursion & the call stack

base cases, stack frames and stack overflows

Write a recursive function

Name the two parts every recursion needsA base case, and a step that shrinks toward it.
Leave out the base caseThe calls never stop, and the stack overflows.
Choose recursion over a loopWhen the data branches, like trees or nested JSON.

TrapA base case the calls never reach fails the same way.

The call stack

Say what each call pushesA frame with its arguments, locals and return address.
Say which call finishes firstThe deepest. Its frame pops and the caller resumes.
Know how much stack you getOften 1 to 8 MB. Ten thousand levels can overflow it.

TipEach nested call gets its own copy of every local variable.

TrapDepth crashes the stack, not total work. A million-node list needs a million frames.

Cost of a recursion

recurrences, the Master Theorem and stack space

Work out a recursion's running time

Write the recurrenceOne call's work plus the cost of the calls it makes.
Count the branching factorTwo calls per level is often O(2^n), as in naive Fibonacci.
Solve an uneven recurrenceDraw the recursion tree and add up the work on each level.
Solve T(n) = aT(n/b) + f(n)The bigger of f(n) and n^(log_b a) wins. A tie adds log n.

TipMerge sort is a tie, with a = 2, b = 2 and f(n) = n. That gives O(n log n).

TrapThe Master Theorem fits only even splits, not T(n-1) recursions.

Work out a recursion's space

Count the stack spaceMax depth times frame size. Only live calls count.
Keep quicksort's stack smallRecurse on the smaller side first. Depth stays O(log n).
Count the rest of the memoryAdd memory each frame holds, plus any memo cache.

TipFast code can still crash from stack depth. Time analysis won't show it.

Backtracking

try a choice, explore it, undo it

The backtracking template

function solve(state) { if (isComplete(state)) { record(state); return; } for (const choice of options(state)) { apply(choice, state);choose solve(state);explore undo(choice, state);unchoose }}
Tell backtracking from brute forceBacktracking checks as it builds, not at the end.
Say why the undo step mattersIt resets shared state, so the next branch starts clean.

TrapThe template alone explores the whole tree and times out. Prune inside the loop.

Why backtracking is exponential

Estimate the search tree's sizeUp to b^d leaves for b choices over d steps.
Prune a branchCut it once it can't succeed, like a queen under attack.
Make pruning pay offNever drop a real answer, and keep the check cheap.

TrapPruning only helps when branches die early. Loose constraints mean brute force again.

Permutations, combinations and duplicates

Generate permutationsTry every unused item at each level. O(n!).
Generate combinations or subsetsfor (let i = start; i < nums.length; i++)Look forward only. Recurse with i + 1. O(2^n).
Skip duplicate answersif (i > start && nums[i] === nums[i - 1]) continue;Sort first, then skip equal siblings.

TipWithout the start index, combinations come out in every order.

TrapA hash set of results still builds every duplicate branch. Skip at the branch.

Pruning big searches

what expert interviews add to backtracking

Fail fast with ordering and propagation

Choose the next variableThe most constrained one, with the fewest legal values.
Choose the next valueThe one that rules out the fewest neighbor options.
Find a dead end before recursingForward checking. An empty neighbor domain ends it now.
Skip branches that mirror each otherFix a canonical order, or skip seen canonical keys. Only cut what you've proved symmetric.

TrapOrdering and propagation cost work at every node. On loosely constrained problems they can cost more than they save, so measure.

Greedy algorithms

take the best choice now, and know when that's safe

When greedy works

Say what a greedy algorithm doesTake the best-looking option each step and never go back.
Name the two properties greedy needsGreedy choice and optimal substructure.
Prove a greedy algorithm rightExchange argument. Swapping in the greedy pick is no worse.
Fit the most meetings into one roomSort by finish time, then take each one that fits.

TipUS coins work greedily. Take the biggest coin that fits, 25, then 10, 5 and 1.

When greedy fails

Make 6 from coins of 1, 3 and 4Greedy takes 4 + 1 + 1. The best is 3 + 3.
Test a greedy ideaHunt for a small counterexample. One loss rules it out.
Accept greedy on a hard problemWhen it has a proven ratio, like set cover's ln n.

TrapAn unproven greedy fails silently. If you can't write the swap step, it's a heuristic.

Found this useful? Pass it on.
Pro · $10/mo

This sheet stays free. With Pro, you can:

  • Every answer on this sheet. Read the full answer to all 61 questions it links, not just the 10 free ones.
  • My sheet. The cards you star and the ones you miss in Quiz me, from every sheet, on one page to quiz and print.
  • PDFs. Download this sheet and My sheet, laid out for A4, with every question a link to its answer.
  • The rest of Pro. Every answer in the question library, and 1,000 AI credits a month.