Algorithm Design
An algorithm is a finite, unambiguous sequence of steps that transforms defined inputs into desired outputs. Recipes, assembly instructions, and emergency checklists are algorithms for humans; code is algorithms for machines. Algorithm design is the pillar where the previous three pay off: decomposition gave you tractable pieces, pattern recognition matched them to known shapes, abstraction fixed the vocabulary — now you write the steps.
From steps to algorithms
A to-do list is not yet an algorithm. Four properties make the difference:
- Precision — every step is executable without judgment calls. “Season to taste” is fine for cooks, fatal for machines.
- Determinacy of flow — at every point it is clear what happens next, including for weird inputs. This is where
if, loops, and error paths come from. - Finiteness — it provably stops. Every loop needs a reason it terminates.
- Generality — it handles the class of inputs, not just Tuesday’s example.
The standard building blocks are famously few: sequence (do A then B), selection (if condition, do A, else B), iteration (repeat while condition), plus subroutines (name a chunk and reuse it — decomposition, again). Everything from spell-checkers to orbital mechanics compiles down to these.
Pseudocode: thinking without syntax
Pseudocode is deliberately informal notation for algorithms — precise enough to reason about, loose enough that you are not fighting a compiler. Here is a complete example solving a real problem: merge two sorted lists of appointments into one sorted list.
ALGORITHM merge_sorted(A, B)
INPUT: A, B - lists sorted by start time
OUTPUT: M - all items of A and B, sorted
M <- empty list
i <- 1 # position in A
j <- 1 # position in B
WHILE i <= length(A) AND j <= length(B):
IF A[i].start <= B[j].start:
append A[i] to M
i <- i + 1
ELSE:
append B[j] to M
j <- j + 1
# one list is exhausted; copy the remainder of the other
append A[i..end] to M
append B[j..end] to M
RETURN MCheck the four properties: every step is mechanical (precision); the IF covers both cases and the trailing appends cover exhaustion (determinacy); each loop pass advances i or j, so the loop must end (finiteness); nothing assumes list lengths or contents (generality). This ten-line shape — two pointers walking two sorted sequences — is a pattern worth naming and keeping: it reappears in merge sort, log-file merging, and calendar conflict detection.
Correctness: does it actually work?
An algorithm is correct when it produces the specified output for every valid input — not the inputs you happened to imagine. Three levels of assurance, in increasing rigor:
- Testing — run it on chosen inputs, especially edge cases: empty lists, one element, all duplicates, already-merged input. Testing finds bugs but can never prove their absence.
- Invariant reasoning — state something that is true on every loop iteration and check the steps preserve it. For
merge_sorted: “M always contains, in sorted order, exactly the items consumed so far from A and B.” If the invariant holds each pass and at exit, correctness follows. This habit — even done informally in comments — catches whole classes of off-by-one and boundary bugs. - Specification first — you cannot be correct against a spec that does not exist. “Sorted by start time; ties broken by… ?” Deciding tie-breaking before coding is algorithm design; discovering it in production is incident response.
Complexity: how does it scale?
Correct is not enough; algorithms meet reality at scale. Big-O notation answers one question: when the input grows, how fast does the work grow? You need no math beyond noticing the shape:
| Growth | Name | Feel at n = 1,000,000 |
|---|---|---|
| O(1) | constant | instant, regardless of size |
| O(log n) | logarithmic | ~20 steps — halving is magic |
| O(n) | linear | touch everything once — fine |
| O(n log n) | linearithmic | good sorting lives here |
| O(n²) | quadratic | a trillion steps — coffee break or outage |
| O(2ⁿ) | exponential | do not wait |
Conceptual rules of thumb: a loop over the input is O(n); a loop inside a loop over the same input is O(n²); repeatedly halving the search space is O(log n); “sort first, then scan” is O(n log n). Our merge_sorted touches each appointment exactly once — O(n) in the combined length — which is why calendars merge instantly while a naive “compare every pair of appointments” O(n²) approach would crawl.
The design trade-off is real: the O(n²) version is often simpler to write and perfectly fine for n = 50. Choosing the simple-but-slower algorithm knowingly, with a note about the size at which it breaks, is good engineering. Choosing it unknowingly is a time bomb.
Designing, in practice
A compact workflow that scales from whiteboard to production:
- Specify inputs, outputs, and edge behavior in one paragraph.
- Match patterns — is this a search, a sort, a merge, a graph walk, a state machine? Reuse beats invention roughly always.
- Write pseudocode and argue its invariant in one sentence.
- Eyeball the complexity and compare against realistic input sizes.
- Trace it by hand on a tiny input and one nasty edge case.
- Only then, implement — or hand the pseudocode to an LLM, which will translate faithfully including your design mistakes. Generation is cheap now; steps 1–5 are the part that still requires you, as Enduring Skills argues in depth.
Verification of the result belongs to the next pillar: Evaluation & Debugging.