Proving a recursion right: induction
Assume the smaller calls are already right, show the step is right, and check that the input really is getting smaller.
A recursive procedure has no loop to hang an invariant on. It calls itself, and the argument that it is right has a different shape: assume the calls it makes are already correct, then show that what it does with their answers is correct. That sounds circular and is not, provided the calls are on genuinely smaller inputs.
Here is a procedure that adds up a list by taking the first value and asking itself for the sum of the rest. The printing shows the calls going down and the answers coming back up.
Add a fifth value to the list and run it again. Count how many lines are printed before the first answer comes back.
Read the output bottom upwards and the structure is visible. The deepest call answers without asking anyone. Every other call takes one answer from below, adds one number to it, and passes it up. Nothing in that chain checks the whole list; each link only has to be right about its own single addition.
What induction is
Mathematical induction proves a statement for every whole number from 0 upwards, using two pieces. First a base case: show directly. Then an inductive step: show that if is true then is true, for every greater than 0. The assumption you are allowed to make inside the step is called the induction hypothesis.
Together they reach every number. is proved outright. The step turns into , then into , and so on, so no number is left out. Strong induction is the same thing with a more generous hypothesis: in proving you may assume for every smaller than , not only for . You need it whenever a procedure calls itself on something more than one step smaller.
For a recursive procedure, the statement being proved is normally the procedure's own contract, and the number being inducted on is the size of the input that call was given. Here is the whole argument for the sum above.
Let be the number of positions left, that is where is the length of the list. The claim is: total_from(a, i) returns the sum of the values at positions to , and returns it after finitely many calls.
- Base case, $m = 0$. Then equals , the test
i == len(a)succeeds, and the procedure returns 0 with no further calls. The sum of no values is 0, so the claim holds. - Inductive step, $m > 0$. Then , so the test fails and the procedure calls itself with , which leaves positions. By the induction hypothesis , that call returns the sum of positions to , after finitely many calls. Name that value . The procedure returns
a[i] + S. The sum of positions to is the value at position plus the sum of positions to , which is exactlya[i] + S. And it made one call more than a finite number, which is finite. So holds. - Conclusion. holds for every , so in particular for with , which is the call the caller makes.
The third condition is the one that bites
The size the recursion shrinks is called the measure. It has to strictly decrease at every call and it has to be caught by a base case before it can run past the bottom. Decreasing on its own is not enough, and the next snippet shows why. It counts its own calls and gives up once it has made a dozen, so that you can see the failure rather than wait for it.
Change the 7 to 8 and run it again. Then keep 7 and change the base case to if n <= 0. Both fixes work, for different reasons.
Starting from 7 the argument goes 7, 5, 3, 1, -1, -3, and never lands on 0. The measure decreases by 2 every time, which satisfies the letter of the rule, and the base case only catches one exact value, so the recursion steps straight over it. Without the call counter this program would run until the machine ran out of room to remember where each call came from.
When one call is not enough to assume
Some recursions make more than one call, or call themselves on an input more than one step smaller. Then the plain hypothesis is not enough and you use strong induction. The Fibonacci numbers are the standard case: , , and for .
Change range(12) to range(31) and run it again. It still answers, but notice how much longer it takes. That cost is the subject of a later section.
The proof needs two base cases, because there are two calls that answer without recursing. : the procedure returns 0, which is . : it returns 1, which is . For the step, take and assume for every with . Both and are in that range, so the two calls return and , and their sum is by the definition. The measure is itself: both calls lower it, and the base cases catch 0 and 1, so nothing steps past the floor.
| Obligation | In a loop | In a recursion |
|---|---|---|
| The start is right | Initialisation: the invariant holds before the first pass | Base cases: the smallest inputs are answered correctly outright |
| Each move preserves it | Maintenance: one pass turns a true invariant into a true invariant | Inductive step: the recursive case is right if the calls are |
| It gets to an end | Termination: a quantity moves towards the loop's exit | A measure strictly decreases and a base case catches it |
A recursive procedure calls itself on an input of size m - 1 and again on an input of size m - 1, and its base case is m = 0. Is ordinary induction enough to prove it correct?
Yes. Both calls are on size m - 1, so the hypothesis P(m-1) covers both of them; you may use one assumption twice. Strong induction is needed when the calls land on sizes you cannot name in advance, such as m - 2, or half of m, or anything the data decides. Using strong induction when ordinary induction would do costs nothing and is never wrong, so when in doubt assume the smaller cases and get on with the step.