Brian WilcoxZero → HeroThe Recipe: Algorithms
An interactive field guide

Algorithms,
from zero to fluent.

An algorithm is a recipe a computer can follow: a finite list of unambiguous steps that turns an input into the right output. This guide takes you from that one sentence to the methods professionals reach for daily, and every widget runs the real algorithm and animates each step it takes.

17 chapters 18 live algorithms 0 prerequisites read time ~3 hrs
Examine: comparing, probing, the frontier Move: swapping, relaxing, the pivot Settled: sorted, visited, finalized The answer: found target, optimal path

Every widget in this guide uses those four colors and only those four. The three cool hues mark work in progress; amber is reserved for the result. Learn them once here and you will read every animation at a glance.

Part I
Cost & Correctness
01

What an Algorithm Is

A recipe that says "stir until done" works fine for a cook who can tell when it's done, but a computer needs a rule it can check mechanically, not a feeling. An algorithm is that rule made precise: a finite sequence of unambiguous steps that transforms an input into a desired output, and is guaranteed to stop. Every algorithm in this guide is a precise procedure you could carry out with pencil and paper, only faster.

Algorithm
A finite, well-defined, deterministic procedure that takes an input and produces the correct output in a finite number of steps. "Well-defined" means each step is unambiguous; "finite" means it always terminates.

Two questions decide whether an algorithm is any good. Is it correct? Does it produce the right answer on every allowed input, not just the one you tried. And what does it cost? How does the amount of work grow as the input gets bigger. Correctness without cost is a daydream (a correct method that takes a billion years is useless); cost without correctness is a fast wrong answer. The whole field lives in the tension between the two.

We measure cost by counting basic operations, not seconds on a stopwatch. Seconds depend on your laptop, the language, and what else is running. The count of comparisons or additions an algorithm performs is a property of the algorithm itself. The widget below runs the simplest useful algorithm there is, "find the largest number in a list," and counts every comparison it makes as it scans. One pass, left to right, holding the best value seen so far.

RUNS: FIND-MAXCounting the Work in a Single Scan
Array (bar height = value)
Position scanned-
Value here-
Best so far-
Comparisons0
Press Play to scan the array once, left to right.

The outlined bar is the running best; the blue bar is where we are now. The winner turns amber. Find-max on n items always costs exactly n−1 comparisons.

Key idea

Judge an algorithm by two things: is it correct on every input, and how does its operation count grow with input size. Count operations, not seconds.

Takeaways
  • An algorithm is a finite, unambiguous procedure that always terminates.
  • Cost is measured by counting basic operations, which is machine-independent.
  • Find-max is n−1 comparisons: the count is a fact about the method.
02

Big-O, Θ, and Ω

Counting exact operations is precise but fussy. What we really care about is how the count grows as the input n gets large. An algorithm that does 3n + 7 operations and one that does 100n both grow linearly: double the input and the work roughly doubles. The constants and low-order terms wash out at scale. Big-O notation throws them away on purpose and keeps only the shape of the growth.

Big-O, Big-Ω, Big-Θ
O(f) is an upper bound: the algorithm grows no faster than f. Ω(f) is a lower bound: no slower than f. Θ(f) is both at once, a tight bound. When people say an algorithm "is O(n log n)," they usually mean Θ: that is genuinely its growth rate.

The reason this abstraction earns its keep is that the differences between growth classes dwarf every constant factor. A quadratic algorithm on a fast computer loses to a linearithmic one on a slow computer, as soon as the input is big enough. The widget below plots the common growth classes and lets you push n up. Watch the order flip: for tiny n the curves are tangled, but by the right edge they have sorted themselves into a strict hierarchy that never reverses again.

COMPUTES: GROWTHHow the Classes Separate
Operations at n = 32 (bar width is log-scaled)

Each bar is the exact operation count for that growth class at the current n, drawn on a log scale so the whole range fits. Slide n and watch 2ⁿ outrun everything, then , while log n barely moves.

Key idea

Big-O keeps the growth class and discards constants. A better growth class beats any constant-factor advantage once the input is large enough, which is exactly the regime we build for.

Takeaways
  • O is an upper bound, Ω a lower bound, Θ a tight bound (both).
  • Constants and low-order terms are dropped: only the dominant term survives.
  • The ladder 1 < log n < n < n log n < n² < 2ⁿ is the whole vocabulary.
03

Cases and Invariants

One algorithm can have three different costs. The best case is the luckiest input, the worst case the unluckiest, and the average case what you expect over random inputs. Engineers usually quote the worst case, because it is a promise: the algorithm will never do worse, no matter how adversarial the data. Average case matters too, but only if you know the input distribution.

Linear search makes this concrete. To find a target by scanning left to right, the best case is one comparison (it is first), the worst is n (it is last or absent), and the average over random positions is about n/2. Same code, three costs. Choose where the target sits and run it.

RUNS: LINEAR SEARCHBest, Worst, and Average
Scanning for the target, one cell at a time
Case-
Comparisons0
Worst case (n)12
Pick a position, then press Play.

Correctness needs its own tool. A loop invariant is a statement that is true before the loop starts and stays true after every iteration. If the invariant holds at the end, and the end condition plus the invariant together imply the answer, the loop is correct. For linear search the invariant is: "the target is not in the part already scanned." When the scan ends, either we found it, or the scanned part is the whole array and it is truly absent. That is a proof, not a hope.

Key idea

Report the worst case as a guarantee. Prove correctness with a loop invariant: something true before the loop and preserved by every step, which forces the right answer at the end.

Takeaways
  • Best, worst, and average case can all differ for the same algorithm.
  • The worst case is the honest promise; average case needs a distribution.
  • A loop invariant turns "it seems to work" into a correctness proof.
Part II
Sorting
04

Bubble & Insertion Sort

Sorting is the model problem of the field: everyone understands the goal, and the algorithms range from naive to brilliant. We start with the two simplest, both Θ(n²) in the worst case, because seeing why they are slow motivates everything faster.

Bubble sort repeatedly walks the array, swapping any two neighbors that are out of order. Each full pass floats the largest remaining value to its final place, like a bubble rising. Insertion sort instead grows a sorted region on the left: it takes the next item and slides it back until it lands in the right spot, the way you order a hand of cards. Both are real below, animated with live comparison and swap counts. Flip between them on the same array.

RUNS: O(n²) SORTSBubble and Insertion, Step by Step
Bubble sort
Comparisons0
Swaps / shifts0
Total operations0
Press Play to sort.

Blue = being compared, violet = being moved, green = settled in final position.

Insertion sort has a saving grace: on a nearly sorted array it is close to Θ(n), because each item barely moves. That is why it is used inside faster sorts to clean up small chunks. But in the worst case (a reversed array) both are quadratic, and quadratic is a wall.

Key idea

Bubble and insertion sort are Θ(n²) in the worst case because they move data one adjacent swap at a time. Insertion sort is Θ(n) on nearly-sorted input, which keeps it useful.

Takeaways
  • Both do work proportional to the number of out-of-order pairs (inversions).
  • Worst case is a reversed array: about n²/2 comparisons.
  • Insertion sort is fast on almost-sorted data and stable; keep it in the toolbox.
05

Merge Sort

The way past the quadratic wall is divide and conquer: split the problem into halves, solve each half, then combine. Merge sort splits the array down to single elements (which are trivially sorted), then merges sorted runs back together. Merging two sorted lists is cheap: walk both with a finger, always taking the smaller front element. Each merge of total length m costs about m comparisons.

There are about log₂ n levels of splitting, and each level does n work to merge everything at that level, so the total is Θ(n log n). The widget runs real merge sort and animates the write-backs as runs combine. Watch the array reassemble in sorted runs of length 1, then 2, then 4.

RUNS: MERGE SORTDivide, Sort, Merge
Merging sorted runs back together
Comparisons0
Writes0
n log₂ n (for scale)0
Press Play to sort.

The blue cell is where the merge is writing the next-smallest value. Merge sort always does Θ(n log n) work, best case and worst case alike.

Key idea

Splitting in half gives log n levels; merging costs n per level; the product is n log n. Merge sort guarantees Θ(n log n) on every input, at the cost of extra memory for the merge.

Takeaways
  • Divide and conquer: solve halves, then combine in linear time.
  • Θ(n log n) worst case, no bad inputs, and stable.
  • The trade-off is O(n) auxiliary space for merging.
06

Quicksort

Quicksort is the other great divide-and-conquer sort, and the one most standard libraries use. Picture sorting a stack of exam papers by picking one paper's score as a splitter: every lower score goes to a left pile, every higher score to a right pile, and that splitter paper is now sitting exactly where it belongs. It picks a pivot, then partitions the array the same way, so everything smaller sits left of the pivot and everything larger sits right. The pivot is now in its final place. Recurse on each pile. No merge step is needed: the partition does the sorting in place.

When pivots split the array roughly evenly, quicksort is Θ(n log n), and its inner loop is so tight it usually beats merge sort in practice. Its dark secret: if pivots are chosen badly (say, always the largest element on an already-sorted array), the split is lopsided and it degrades to Θ(n²). The widget runs the real Lomuto partition and animates the pivot and the scanning pointer.

RUNS: QUICKSORTPartition Around a Pivot
Partitioning: pivot outlined, scanner in blue
Comparisons0
Swaps0
Total operations0
Press Play to sort.

The outlined violet bar is the pivot. When the scan finishes, the pivot swaps into place (green) and each side recurses.

Key idea

Quicksort sorts in place by partitioning around a pivot. Balanced pivots give Θ(n log n); adversarial pivots give Θ(n²). Randomizing or median-of-three pivots makes the bad case vanishingly unlikely.

Takeaways
  • Partition places the pivot for good; recursion handles the two sides.
  • Average Θ(n log n), in place, cache-friendly, usually the fastest in practice.
  • Worst case Θ(n²) on bad pivots; randomization defends against it.
07

The Comparison Lower Bound

Merge sort and quicksort both land at n log n. That is not a coincidence, it is a wall. Think of Twenty Questions: every yes/no question can at best cut the remaining possibilities in half, so pinning down one answer among N options takes roughly log₂(N) questions. A comparison sort is playing that same game against the n! possible orderings of the input. Any sort that works only by comparing pairs of elements must make at least Ω(n log n) comparisons in the worst case: each comparison has two outcomes and so at best halves the remaining orderings, and you need enough comparisons to single out the one correct ordering, which takes log₂(n!) ≈ n log₂ n comparisons. No comparison sort can beat it.

Put all four sorts on one starting array and the wall is visible: bubble and insertion pile up quadratic operations while merge and quicksort stay near n log n. Re-roll the array to see the counts hold up across inputs.

RUNS: ALL FOURThe Sorting Race
Total operations to sort the same 28-element array (log-scaled)

Every count is measured by running the real sort and tallying its comparisons and moves. Try "reversed input" to trigger quicksort's bad case.

Escaping the wall: counting sort

The lower bound only binds comparison sorts. If the keys are small integers, you can sort without ever comparing two of them. Counting sort tallies how many times each value appears, turns those tallies into positions, and drops each element straight into its slot. It runs in Θ(n + k) for values in 0..k, which is linear when k is not much bigger than n. It buys this speed by knowing something extra about the data.

RUNS: COUNTING SORTSorting Without Comparing
Phase 1: tally each value
Phasetally
Operations (n + k)0
Press Play. No two elements are ever compared.
Key idea

Every comparison sort needs Ω(n log n) comparisons, because it must distinguish n! orderings. Counting and radix sorts beat that by not comparing at all, trading generality for a linear runtime on small-integer keys.

Takeaways
  • n log n is a proven floor for comparison-based sorting.
  • The proof: n! orderings, each comparison halves the candidates, so log₂(n!) comparisons are needed.
  • Counting and radix sort sidestep the floor by using the keys directly.
Part III
Searching
08

Linear vs Binary Search

To find a value in an unsorted list you have no choice but to look at everything: linear search, Θ(n). But if the list is sorted, one comparison tells you a great deal. Check the middle element. If your target is smaller, it can only be in the left half; if larger, only the right. Either way, one comparison throws away half the array. That is binary search, and it runs in Θ(log n).

Log growth is staggering. A million sorted items take at most 20 comparisons; a billion take 30. The widget runs real binary search, animating the lo and hi bounds closing in on the target. The greyed cells are the ones proven irrelevant and never examined again.

RUNS: BINARY SEARCHHalving the Interval
Sorted array. Mid checked; discarded half greyed
Interval [lo, hi]-
Comparisons (binary)0
Linear would need-
Pick a value and press Play.

Invariant: if the target is present, it is always inside [lo, hi]. When the interval closes, the target is found (amber) or proven absent.

Key idea

Sorting is a one-time investment that turns every future lookup from Θ(n) into Θ(log n). Binary search works because each comparison, against a sorted array, eliminates half of what remains.

Takeaways
  • Linear search is Θ(n) and needs no structure; binary search is Θ(log n) and needs sorted input.
  • The loop invariant "target is within [lo, hi]" proves correctness.
  • log n is so flat that a billion items resolve in about 30 steps.
Part IV
Recursion & Divide-and-Conquer
09

Recursion & the Stack

Ask a friend a question they can't answer outright, and they turn around and ask a second friend a smaller version of the same question, who asks a third, and so on, until someone can answer directly, then the answers travel back up the chain. That line of friends waiting on a reply is the call stack. A recursive function solves a problem by calling itself on smaller inputs, until it hits a base case small enough to answer directly. Merge sort and quicksort are recursive; so is walking a tree. The machine tracks the half-finished calls on the call stack: each call pushes a frame, each return pops one. The stack's depth is the recursion's depth.

The widget traces a real recursive routine (summing an array by repeatedly splitting it in half) and shows the call stack growing as it descends to the base cases, then unwinding as results bubble back up. The tree it draws is the recursion, level by level.

RUNS: RECURSIONThe Call Tree and the Stack
Call stack (top = deepest)
Calls made0
Max stack depth0
Running result-
Press Play to descend into the recursion.
Key idea

Recursion is self-reference with a base case that stops it. The call stack stores the pending work; its maximum depth is the recursion depth, which is where deep recursions run out of memory.

Takeaways
  • Every recursion needs a base case, or it never terminates.
  • The call stack holds one frame per pending call; depth equals recursion depth.
  • Divide-and-conquer recursions have depth about log n, which is shallow and safe.
10

The Master Theorem

Picture a family tree, or an org chart: the root is the original problem, it spawns a children each holding a piece of size n/b, and each of those spawns its own children, branching downward until the pieces are trivially small. Two costs compete for the total bill: how many leaves pile up at the bottom, and how expensive combining results is at each level on the way back up. Divide-and-conquer algorithms share that exact shape: T(n) = a·T(n/b) + f(n). Solve a subproblems, each of size n/b, and spend f(n) to split and combine. Merge sort is 2·T(n/2) + n. The master theorem reads off the answer by comparing those same two forces: the work that multiplies as you recurse (the leaves, growing like n^(log_b a)) against the work at the top (the root, f(n)).

Whichever force dominates sets the total. If the leaves win, the answer is leaf-heavy. If the root wins, it is root-heavy. If they tie, you pay the tie cost times an extra log n factor (that is where merge sort's log comes from). Slide the parameters and watch which side of the balance tips.

COMPUTES: T(n)Leaves vs Root
Work per recursion level (root at top)
log_b(a)1.00
d1
T(n) =Θ(n log n)

Merge sort is a=2, b=2, d=1: a perfect tie, so Θ(n log n). Binary search is a=1, b=2, d=0: Θ(log n).

Key idea

Compare log_b(a) with d. Leaves dominate (log_b a > d): Θ(n^log_b a). Root dominates (log_b a < d): Θ(n^d). Tie (equal): Θ(n^d log n). One comparison reads off the runtime.

Takeaways
  • Most divide-and-conquer costs fit T(n)=a T(n/b)+f(n).
  • The answer is whichever grows faster: the leaf work or the root work.
  • A tie costs an extra factor of log n, the source of the ubiquitous n log n.
11

Memoizing Fibonacci

Recursion can be a trap. The naive Fibonacci, fib(n) = fib(n-1) + fib(n-2), looks elegant and is catastrophically slow: it recomputes the same subproblems over and over. fib(5) calls fib(3) twice, fib(2) three times, and the call tree explodes exponentially, about 2ⁿ calls.

The fix is memoization: remember each answer the first time you compute it, and on any later request return the cached value instead of recursing again. The exponential tree collapses to a thin one, because each distinct subproblem is solved exactly once: n+1 of them, so Θ(n). Toggle the cache below and watch the tree shrink and the call count fall off a cliff.

RUNS: FIBONACCIExponential, Then Linear
fib(n) value8
Function calls25
Distinct subproblems7

Dashed grey nodes are cache hits: subproblems already solved, returned instantly. Memoization is the doorway to dynamic programming.

Key idea

Naive Fibonacci is exponential because it re-solves overlapping subproblems. Caching each result once turns 2ⁿ calls into n, the single biggest speedup in this guide, and the core trick of dynamic programming.

Takeaways
  • Overlapping subproblems make naive recursion exponential.
  • Memoization stores each answer once: exponential becomes linear.
  • This "solve each subproblem once" idea is exactly dynamic programming.
Part V
Dynamic Programming
12

Optimal Substructure

Picture a street grid where you can only walk right or down. The number of ways to reach any corner is simply the ways to reach the corner above it plus the ways to reach the corner to its left: dp[i][j] = dp[i-1][j] + dp[i][j-1]. Edges have one path. Fill the grid corner by corner and the far corner holds the answer.

That is dynamic programming (DP): memoization made deliberate. It applies when a problem has two properties. Optimal substructure: the best solution is built from best solutions to subproblems, exactly as each grid cell is built from the cells above and to its left. Overlapping subproblems: the same subproblems recur, so caching pays off. When both hold, you build a table of subproblem answers from the bottom up, each cell using cells already filled.

RUNS: GRID DPCounting Paths, Cell by Cell
Each cell = paths from the top-left, moving only right or down
Cell being filled-
up + left-
Total paths (corner)-
Press Play to fill the table.

The violet cell is being computed; its two blue sources are the cell above and the cell to the left. Every cell is solved exactly once.

Key idea

Dynamic programming needs optimal substructure (best-of-whole from best-of-parts) and overlapping subproblems (so caching pays). Then a table filled in dependency order solves each subproblem once.

Takeaways
  • DP = recursion + a table, filled bottom-up in dependency order.
  • Optimal substructure lets you assemble the answer from sub-answers.
  • The recurrence is the algorithm; the table is just where you store it.
13

Filling the Table: Edit Distance

The flagship DP is edit distance (Levenshtein): the fewest single-character edits (insert, delete, or substitute) to turn one string into another. It powers spell-checkers, DNA alignment, and diff tools. Turning "kitten" into "sitting" takes 3 edits: substitute k for s, substitute e for i, and insert g; edit distance is the systematic way to find that minimum for any pair of strings, and the widget below finds it for exactly this pair. The subproblem dp[i][j] is the distance between the first i letters of the source and the first j of the target.

The recurrence weighs three moves. If the two current letters match, the cost is whatever dp[i-1][j-1] was, free. Otherwise take 1 + the cheapest of the three neighbors: delete (dp[i-1][j]), insert (dp[i][j-1]), or substitute (dp[i-1][j-1]). The widget fills the real table cell by cell, highlighting the three candidates, then traces the amber path of edits back from the corner.

RUNS: EDIT DISTANCE"kitten" → "sitting"
Filling dp[i][j] from three neighbors
Cell-
Letters match?-
Edit distance-
Press Play to fill, then trace the optimal edits.

Three blue neighbors feed each violet cell. The amber path from the bottom-right corner is the cheapest sequence of edits.

Key idea

Edit distance fills an (m+1)×(n+1) table in Θ(mn), each cell the min of three neighbors plus a match test. Tracing back from the corner recovers not just the cost but the actual sequence of edits.

Takeaways
  • A 2D table turns an exponential edit search into Θ(mn).
  • Each cell is 1 + min of delete, insert, substitute (or a free match).
  • Backtracking through the choices recovers the optimal solution itself.
Part VI
Graph Algorithms
14

BFS & DFS

A graph is nodes joined by edges: road maps, social networks, web links, dependency chains. The two foundational traversals differ only in the order they explore, and that difference comes down to one data structure. Breadth-first search (BFS) uses a queue and explores in rings of increasing distance from the start, so it finds shortest paths in unweighted graphs. Depth-first search (DFS) uses a stack and plunges as deep as it can before backtracking.

The widget runs both on the same graph. Switch modes and watch the frontier: BFS spreads outward evenly, DFS dives down one branch at a time. The numbers on the nodes are the visitation order. Same graph, same start, two very different shapes.

RUNS: BFS / DFSQueue vs Stack
Queue (front → back)
Visited0
Order-
Press Play to traverse from node A.
Key idea

BFS and DFS are the same code with one swap: a queue makes it breadth-first (shortest paths, ring by ring), a stack makes it depth-first (dive and backtrack). Both visit every node and edge once: Θ(V + E).

Takeaways
  • Queue → BFS → explores by distance → unweighted shortest paths.
  • Stack → DFS → explores by depth → cycle detection, topological order.
  • Both run in Θ(V + E), touching each node and edge a constant number of times.
15

Dijkstra's Shortest Path

BFS finds shortest paths only when every edge costs the same. Real maps have weights: distances, times, tolls. Dijkstra's algorithm finds the cheapest path from a start to every other node when weights are non-negative. It keeps a tentative distance to each node, always settles the closest unsettled node next (pulled from a priority queue), and relaxes its neighbors: if going through this node is cheaper than their current best, update them.

The genius is the greedy guarantee: once a node is settled, its distance is final and can never improve, because every other route would have to pass through a farther node first. The widget runs real Dijkstra, animating each relaxation and growing the shortest-path tree. The final route to the goal lights up amber.

RUNS: DIJKSTRARelax and Settle
Priority queue (nearest first)
Settling-
Nodes settled0
Distance to T
Press Play. Shortest path from S to T.

Node labels show the current best distance. Violet edges are being relaxed; the final amber path is the shortest route.

Key idea

Dijkstra settles nodes in order of distance, and each settled distance is final. With a binary-heap priority queue it runs in Θ((V + E) log V). Non-negative weights are required; the greedy proof breaks with negative edges.

Takeaways
  • Relaxation: improve a neighbor's distance if this route is cheaper.
  • Always settle the nearest unsettled node; its distance is then locked in.
  • Needs non-negative weights; use Bellman-Ford when edges can be negative.
16

Greedy & the Minimum Spanning Tree

A greedy algorithm makes the locally best choice at every step and never reconsiders. Greed usually fails to find the global optimum, but for some problems it is provably perfect. The minimum spanning tree (MST) is the classic win: connect every node with the least total edge weight and no cycles, the cheapest possible network that leaves nobody isolated.

Prim's algorithm grows the tree from one node, each step adding the cheapest edge that reaches a node not yet connected. That greedy rule is guaranteed optimal by the cut property: the lightest edge crossing any division of the nodes must belong to some MST. The widget runs real Prim's and highlights each chosen edge in amber as the tree spans the graph.

RUNS: PRIM'S MSTCheapest Edge Across the Cut
Candidate edges crossing the cut
Edge added-
Nodes in tree1
Total weight0
Press Play to grow the tree from node S.

At each step Prim's takes the lightest candidate edge that reaches a new node. Chosen tree edges turn amber.

Key idea

Greedy is fast but usually wrong. For the MST it is provably optimal thanks to the cut property: the lightest edge crossing any cut is safe to add. Knowing when greed is correct is the real skill.

Takeaways
  • Greedy takes the best local step and never backtracks.
  • Prim's grows an MST by always adding the cheapest edge to a new node.
  • The cut property proves this greed optimal; most problems are not so lucky.
Part VII
Capstone
17

P vs NP: Why Some Problems Are Hard

Every algorithm so far has been efficient: its cost grows as a polynomial in the input size (n, n log n, ). Call that class P, the problems solvable in polynomial time. But a vast number of important problems have no known polynomial algorithm. The best we have is essentially brute force, and brute force is exponential.

The catch is a strange asymmetry. For many hard problems, checking a proposed answer is easy even though finding one is not. Given a filled-in Sudoku you can verify it in seconds; solving a blank one is another matter. That class, "answers you can verify quickly," is NP. The trillion-dollar question, P vs NP, asks whether fast-to-check always means fast-to-solve. Almost everyone believes no, but nobody has proved it.

Subset sum shows the wall. Given a set of numbers, is there a subset that adds up to a target? Verifying a candidate subset is a single linear pass. Finding one, in general, means checking up to 2ⁿ subsets. Push n and watch the search space detonate while the verifier stays trivial.

RUNS: SUBSET SUMEasy to Check, Hard to Solve
Brute-force search: subsets examined
Subsets to check (2ⁿ)-
Brute-force time-
Verify one answer (n)-

"Solve" really enumerates subsets until it finds one hitting the target. At a billion checks per second, watch when brute force stops finishing before lunch.

Faced with an NP-hard problem in practice, you do not give up, you change the question. Accept a near-optimal answer from an approximation algorithm, use a heuristic that is usually good, or exploit structure special to your inputs. Recognizing that a problem is hard is itself a valuable result: it tells you to stop hunting for a perfect fast algorithm that almost certainly does not exist.

Key idea

P is "solvable fast," NP is "checkable fast." Whether they are equal is the deepest open question in computing. When a problem is NP-hard, the professional move is to approximate, use heuristics, or restrict the input, not to brute-force forever.

Takeaways
  • Polynomial = tractable; exponential = intractable at scale.
  • NP problems are easy to verify but (as far as anyone knows) hard to solve.
  • For hard problems, approximate or use heuristics instead of exact brute force.

Where to Go Next

You now have the working vocabulary of algorithms: how to measure cost, the sorting and searching workhorses, recursion and divide-and-conquer, dynamic programming, the graph traversals, and the boundary of what is tractable. The next moves depend on where you want to point it.

Depth
Read CLRS (Introduction to Algorithms) for the rigorous proofs behind every method here, and Skiena's Algorithm Design Manual for the practitioner's view of when to use what.
Practice
Solve problems on LeetCode or Codeforces. Pattern recognition (this is a DP, this is a BFS) is a muscle built only by reps.
Data structures
Algorithms and the structures they run on are two halves of one subject. Heaps, hash tables, and balanced trees are what make these methods fast.
Beyond exact
For hard problems, study approximation algorithms, linear programming, and randomized methods, the toolkit for when the perfect answer is out of reach.
In real code
The same classics, implemented and annotated in Go, live in my older algorithms-in-go repository, a predecessor to this guide.

The mental shift worth keeping: given any task, ask what the input size is, what growth class you can afford, and which known pattern it matches. That question, asked early, is most of the job.

More from Zero → Hero

Browse all 15 guides →