What Is an Algorithm?
An algorithm is a finite, mechanical recipe that transforms inputs into outputs. We define what counts as an algorithm, how we write one down, and the three things we always ask of it: is it correct, is it fast, and can we prove it.
╌╌╌╌
You have already met dozens of algorithms without being told that's what they were. Merge sort, quicksort, binary search, linear search, breadth-first search; even the rote procedures for multiplying () or adding () two integers by hand; even tidying a room by a fixed set of rules. A simple working definition unites them:
def fib(n: int) -> int:
if n <= 1:
return n
return fib(n-1) + fib(n-2)
def fib2(n: int) -> int:
M = [0, 1]
for i in range(1, n):
M.append(M[-1] + M[-2])
return M[-1]
fib :: Int -> Int
fib 0 = 0
fib 1 = 1
fib n = fib (n - 1) + fib (n - 2)
fib2 :: Int -> Int
fib2 n
| n <= 1 = n
| otherwise = fib2 (n - 1) + fib2 (n - 2)
That definition is deliberately blunt. Erickson sharpens it the same way: an
algorithm is a procedure a rock could follow, where every step is so
mechanical that no intelligence, intuition, or luck is required to carry it
out.1 CLRS frames it operationally: a well-defined computational procedure
that takes a value (or set of values) as input and produces a value
as output.2 Skiena adds the engineer's caveat: it must work correctly on
every instance, not merely on the examples we happen to test.3
Pull those framings apart and you get a checklist. A procedure earns the name algorithm only if it is:
- Finite. It halts after finitely many steps on every valid input.
Repeat until it looks sorted
is not a step count you can bound. - Definite. Every step is unambiguous: two executors starting from the same
state and reading the same step do the same thing.
Pick a good pivot
fails this test until you say which element. - Effective. Each step is basic enough to actually carry out: compare two
numbers, copy a cell, add.
Factor the integer
is a legal step only if you already hold an algorithm for factoring. - Anchored to a problem. It consumes specified inputs and produces specified outputs. A procedure that produces nothing computes nothing.
The multiplication procedure you learned as a child passes all four; season to taste
fails definiteness, and a program that loops forever on one unlucky
input fails finiteness. Three demands run through everything that does pass,
and through this entire course:
- Correctness. The algorithm produces the right output on every valid input. One counterexample is enough to sink it.
- Efficiency. It uses few resources (time, space) as the input grows.
- Provability. We can argue, rather than assert, that the first two hold.
Communicating an algorithm
Having an algorithm in your head is not enough; you must convey it so that someone else can run it, trust it, and predict its cost. In this course every algorithm comes with four deliverables:
- High-level idea. One or two sentences of plain English.
- Pseudocode. The steps, precise enough to analyze.
- Proof of correctness. An argument that it always returns the right answer.
- Complexity analysis. How its running time (and sometimes space) grows with the input.
These four form a pipeline. The problem spec fixes what counts as a correct answer; the idea and pseudocode are two resolutions of the same method; the proof certifies the pseudocode against the spec; and the analysis measures that same pseudocode. Each deliverable feeds the next.
We will build a complete example of all four below. First, two preliminaries: what exactly the algorithm is for, and how we write it down.
Specifying the problem
Before writing an algorithm we must agree on the problem it solves: the set of legal inputs and, for each, the required output. A problem is a relation between inputs and outputs; an instance is one particular input. For sorting:
Notice what the specification hides: it says nothing about how to reorder the numbers, only what the result must satisfy. Algorithm design rests on that separation of what from how.
Our running example will be a smaller, more concrete problem:
It is simple enough that you can see the whole algorithm at once, yet rich enough to demand all four deliverables, including a proof that is more subtle than it first looks.
Deliverable 1 — the high-level idea
Before any pseudocode, say what you intend to do in plain words. For :
That sentence is the whole algorithm. Everything that follows makes it precise and proves it works.
Deliverable 2 — the pseudocode
We describe algorithms in pseudocode: precise enough to analyze, free of the syntactic noise of any real language. The high-level idea translates directly:
- 1largest value seen so far
- 2for to do
- 3if then
- 4
- 5return
Two small but real design choices deserve attention. We seed with
rather than with or . This is why the specification insisted
, so that exists and the maximum
is well defined. The loop
starts at , since has already been accounted for by the seed.
As a second specimen of pseudocode, here is the classic insertion sort, which sorts in place by growing a sorted prefix one element at a time. We will return to it when we study sorting; for now it shows what nested loops and an in-place rearrangement look like on the page.
- 1for to do
- 2
- 3insert into sorted prefix
- 4while and do
- 5
- 6
- 7
- 8return
The outer loop walks a marker from left to right; everything before is already sorted. Each pass lifts out as , slides the larger sorted-prefix elements one slot right, and drops into the gap that opens up — exactly how you would tidy a hand of playing cards. The trace below shows the sorted prefix (shaded) absorbing one new element per row.
A picture of the idea
Here is mid-sweep on . The cursor has just reached ; everything to its left has been scanned, and holds the largest value among . Since , the if fires and is updated to .
The shaded cell is the element under the cursor; the region to its left (positions through ) is the part already summarized by .
Deliverable 3 — proof of correctness
How do we know works? Erickson's a rock could run it
intuition tells us the steps are mechanical, but it does not tell us the
answer is right. Correctness needs an argument.
It is tempting to argue by contradiction (suppose is the true maximum but returns something else), but the clean way is to name what the loop preserves and induct on it: is only ever overwritten by a larger value, so never decreases and never holds anything that wasn't actually in the array. Made precise, that is a loop invariant: a statement true before and after every iteration.
We verify it with the three-part rubric that will recur throughout the course:
- Initialization. Before the first iteration , so we must check . The seed line set , and the maximum of a one-element set is that element. ✓
- Maintenance. Assume the invariant holds entering iteration , i.e. . The body sets (it overwrites exactly when , and leaves it otherwise). Hence after the body , which is precisely the invariant for the next cursor value . ✓
- Termination. The loop ends once the cursor would exceed , i.e. with the invariant established for . So , and returns exactly the value the specification demands. ✓
Initialization, maintenance, termination: that triple drives correctness proofs, and we will use it constantly.
The rubric on a harder loop: insertion sort
's invariant fit in one clause. Insertion sort (Algorithm 2) needs two, and the second is the one beginners drop:
The phrase "exactly the elements originally in :q is doing real work.The prefix is sorted:q alone would not pin the algorithm down: a procedure could make the prefix sorted by destroying it (we will meet such asorter" shortly), so the invariant must also record that elements are only ever rearranged, never invented or lost. CLRS states the insertion-sort invariant in exactly this two-clause form.4
Before the proof, look at what one pass actually does to the array. Lifting out of its cell (line 2) leaves a hole; each pass of the while loop (lines 4–6) slides one prefix element right into the hole, moving the hole one step left; line 7 fills the final hole with .
Now the three parts, argued against the line numbers of Algorithm 2.
- Initialization. Before the first iteration, , so the prefix is the single cell . Nothing has executed yet, so it still holds its original element, and a one-element array is trivially sorted. ✓
- Maintenance. Assume the invariant entering the iteration for some : the prefix is a sorted arrangement of the original first elements. Line 2 copies into , so we may treat cell as a hole. Each pass of the while loop (lines 4–6) fires only when ; line 5 copies that element one slot right into the hole, and line 6 makes cell the new hole. Ignoring the hole, every shift leaves the prefix's elements intact and in the same relative order, so the region right of the hole (through cell ) stays sorted and consists entirely of elements . The loop exits in one of two ways: , so every prefix element was and the hole is cell ; or , so the hole sits just right of the rightmost element . Either way, everything left of the hole is and sorted, everything right of it through cell is and sorted, and line 7 drops into the hole. The result is sorted, holding exactly the original elements of . That is the invariant with in place of . ✓
- Termination. The for loop exits when reaches . Plugging into the invariant: consists of exactly the elements originally in , in increasing order. That is word for word the sorting specification (a sorted permutation of the input), and line 8 returns it. ✓
The maintenance step above quietly ran a second induction: the claim about holes and shifted elements is itself an invariant of the inner while loop, checked once per shift. For nested loops that layering is the normal shape of a correctness proof — an outer invariant whose maintenance step leans on an inner one. Here the inner argument is short enough to inline; when it is not, state the inner invariant explicitly and give it the same three-part treatment.
Aside: soundness and completeness
For algorithms that answer yes/no rather than compute a value, the same rigor takes a slightly different shape. Consider , which reports whether key occurs in . Its correctness has two independent halves, and they carry standard names worth adopting now — they recur across the whole course, in search, decision procedures, verifiers, and the reductions of intractability:
- Soundness — every
foundanswer is true. The procedure never lies in the affirmative: when it saysfound, genuinely . Soundness rules out false positives. - Completeness — every true case is caught. The procedure never misses: when
, it really does say
found. Completeness rules out false negatives.
The two are genuinely separate. An algorithm that always answered not found
would be vacuously sound — it never makes a false claim of membership — yet
hopelessly incomplete; one that always answered found would be complete but
unsound. Correctness requires both guarantees at once.
Both tools at once: binary search
Invariants are not only about prefixes growing left to right. When the array is sorted, we can search it by repeatedly halving a window, and the invariant describes where the answer can still hide rather than what has been built so far.
- 1
- 2
- 3while do
- 4
- 5if then return found
- 6else if thendiscard left half
- 7elsediscard right half
- 8return not found
The invariant is deliberately conditional. It does not claim is in the window — may not be in the array at all — only that the cells outside the window have been legitimately ruled out.
- Initialization. Lines 1–2 set and , so the window is the whole array and the claim is vacuous: if is in , it is in . ✓
- Maintenance. Suppose the invariant holds entering an iteration and line 5 does not return, so . If (line 6), sortedness gives , so no cell at index can hold . If is in the array at all, the invariant places it in , and we just excluded , so it lies in — exactly the new window after . The case (line 7) is symmetric. ✓
- Termination. Two duties here, and the first is easy to forget: the loop
must actually end, and the exit state must imply the postcondition. For
progress: inside the loop forces , so line 6
raises by at least one and line 7 lowers by at least one; the
window length strictly shrinks every iteration and the loop
runs at most times. For the exit itself there are two
doors. Through line 5, the algorithm just witnessed , so
foundis sound. Through line 3 failing, and the window is empty; the invariant says that if were in the array it would be in that empty window, which is absurd, so andnot foundis correct — the algorithm is complete. ✓
The same vocabulary as , but here completeness is not a one-line observation: it rests entirely on the invariant. Every discarded cell was ruled out for a reason, and the invariant records those reasons.
How invariant proofs go wrong
An invariant proof has exactly three joints, and each one has a characteristic failure. All three failures look like proofs until you press on the right spot.5
False at initialization. The seed in
looks fussier than the neutral
seed , and the neutral seed is a
genuine bug. With , the invariant is
already false entering whenever ; and on an all-negative
array the if never fires, so the algorithm returns — a value that is
not even in the array. The failed initialization check is not pedantry; it
points at a real input that breaks the program.
Too weak to imply the postcondition. Take insertion sort and drop the permutation clause, keeping only " is sorted." Now consider this impostor, which replaces lines 2–7 of Algorithm 2 with a single assignment:
- 1for to do
- 2
- 3return
The weak invariant sails through all three checks: a one-cell prefix is sorted
(initialization); appending a copy of the last element keeps a sorted prefix
sorted (maintenance); at the whole array is sorted (termination).
Every step is airtight, and the program is garbage — on
it sorts
to . Nothing in the proof was wrong; the
invariant proved a true statement that fails to imply the specification,
which demanded a sorted permutation of the input. When the termination step
ends with anything short of the postcondition, word for word, the invariant
needs strengthening.
Off-by-one at the exit. The termination step must use the exact negation
of the loop guard, evaluated at the actual exit value of the counter.
's loop ends with , not ; plugging the
wrong value into the invariant proves
only , which leaves unaccounted for. The same slip in
is a live bug rather than a weak conclusion: change
line 3 to while and, on a one-element array with , the
loop body never runs and line 8 answers not found. The invariant itself
survives untouched — what breaks is the exit analysis, because
failing gives , a window that may still hold one unexamined cell,
and the window is empty
step of the termination argument is simply false.
Deliverable 4 — complexity, in brief
The fourth deliverable asks how many steps the algorithm takes as a function of the input size . For , the seed and the final return cost a constant; the loop runs times, doing a comparison and at most one assignment each pass. If each line costs some machine-dependent constant , the total is
a linear running time: double the array and you roughly double the work. The point of the is that it throws away the machine-specific constants and keeps only the growth rate.
That insertion sort, by contrast, is not always so cheap is why fast
needs care. On an array already in reverse order, every new element sifts past
all its predecessors, costing
comparisons, which is quadratic in . On an already-sorted array it does
only comparisons, which is linear: the same algorithm, wildly different
costs.
Defining , , and precisely, and measuring this growth independently of the machine, is the subject of asymptotic analysis in the next lesson.
Further frontiers
The four deliverables are a working discipline, but each has grown into a field
of its own. The precise, mechanical procedure
we relied on informally has an
exact meaning: a function is computable if some Turing machine computes it, and
the Church–Turing thesis holds that every reasonable model of computation —
Turing machines, the lambda calculus, the RAM of the next lesson — computes
exactly the same class of functions. Turing's 1936 construction also produced the
first problem that no algorithm can solve, the halting problem: there is no
procedure that decides, for an arbitrary program and input, whether the program
eventually stops.6 So write an algorithm for it
is not always a request
that can be met, a boundary worth knowing before spending a week on a problem that
is provably undecidable.
The proof-of-correctness deliverable, done here by hand, can be machine-checked.
Interactive proof assistants such as Coq and Isabelle let one state an algorithm's
specification and its invariants formally and have the computer verify every step;
the CompCert C compiler and the seL4 operating-system kernel are large
systems proved correct this way, insertion sort's loop invariant scaled up to
tens of thousands of lines.7 And the high-level-idea
deliverable is where the standard design paradigms live — divide-and-conquer,
greedy, dynamic programming, and the rest — each a reusable pattern for the idea
step, and each the subject of a later module. Skiena frames the whole design
manual around recognizing which paradigm a new problem fits.8
Takeaways
- An algorithm is a precise recipe of steps for solving a computational problem: a finite, mechanical, input-to-output procedure that must be correct on every instance.
- Every algorithm comes with four deliverables: high-level idea, pseudocode, proof of correctness, and complexity analysis. shows all four at full size.
- Specify the problem (legal inputs → required outputs) before the method. That is also what tells you to seed and require .
- Pseudocode is for humans to reason about; a loop invariant (
only ever grows, to a value actually in the array
) turnsit looks right
into a proof by initialization, maintenance, termination. State which moment your invariant describes. - Invariants must be strong enough to imply the postcondition: insertion
sort's invariant needs the permutation clause, not just
the prefix is sorted
— satisfies the weak version while destroying the input. - The termination step has two duties: show the loop ends (a quantity that strictly shrinks), then combine the invariant with the exact negation of the guard at the counter's real exit value. Off-by-ones live here.
- For yes/no algorithms, prove each direction separately, and use the
contrapositive to
pick the easier statement. needs both:
foundis witnessed directly;not foundis forced by the invariant. - Correctness and efficiency are separate questions: an algorithm can win one and lose the other.
Footnotes
- Erickson, Algorithms, Ch. 0 — Introduction: an algorithm as a procedure mechanical enough that
a rock could follow it.
↩ - CLRS, Ch. 1 — The Role of Algorithms (§1.1): the operational
well-defined computational procedure
framing. ↩ - Skiena, The Algorithm Design Manual, §1.1–1.3 — Introduction to Algorithm Design: an algorithm must be correct on every instance. ↩
- CLRS, §2.1 — Insertion sort: the loop invariant is stated with both clauses, "the elements originally in :q andin sorted order." ↩
- Skiena, §1.3 — Reasoning about Correctness: on how plausible-looking correctness arguments fail, and why counterexamples and induction are the tools that expose them. ↩
- Turing, A. M. (1936).
On computable numbers, with an application to the Entscheidungsproblem.
Proc. London Mathematical Society s2-42 — Turing machines, the Church–Turing thesis, and the undecidability of the halting problem. ↩ - Leroy, X. (2009).
Formal verification of a realistic compiler.
Communications of the ACM 52(7) — the CompCert verified C compiler; Klein, G. et al. (2009).seL4: formal verification of an OS kernel.
Proc. SOSP. ↩ - Skiena, The Algorithm Design Manual, §1.1 and Part I — organizing algorithm design around a small set of reusable paradigms (divide-and-conquer, greedy, dynamic programming, and others). ↩
╌╌ END ╌╌