06 — Algorithms and Correctness
A structured guide to algorithm specifications, searching and sorting methods, iteration and recursion, complexity, termination, and formal correctness proofs.
Specifications: What an Must Promise
An is a finite, precise procedure for solving a problem. Its specification describes what result must be produced, while its implementation describes how the result is produced. Separating these ideas allows different implementations to satisfy the same behavioral promise.
A useful specification identifies:
Inputs: the data supplied to the procedure.
Outputs: the result that must be returned.
: facts that must hold before execution.
: facts guaranteed after successful execution.
Termination condition: the reason the process eventually stops.
Resource requirements: how running time and memory use grow.
For a search procedure, a postcondition might say that the result is exactly when the target is absent; otherwise, the returned index identifies an element equal to the target. This is more precise than merely saying that the procedure “looks for” a value.
Takeaway: A correctness claim is meaningful only when the inputs, assumptions, expected result, and stopping behavior are specified clearly.
Searching: Linear and Binary Methods
Searching locates a target in a collection, but the best method depends on whether the collection is ordered.
checks elements from left to right until it finds the target or exhausts the collection. It works without sorting. For the list and target , it checks , then , and then returns index . With elements, its worst-case running time is .
requires a sorted collection. It compares the target with the middle element, then keeps only the half that might still contain the target. Each unsuccessful comparison approximately halves the remaining interval, producing running time . For example, when searching for in , a comparison with eliminates the left half, and a later comparison finds the target.
is not automatically faster overall if the data must first be sorted. It is most attractive when the collection is already sorted or will be searched repeatedly.
Takeaway: trades simplicity and no ordering requirement for worst-case time; trades a sorted-input requirement for search time.
Sorting: Simple and Divide-and-Conquer Strategies
Sorting rearranges values according to a chosen order, such as increasing numerical order or alphabetical order. A sorting specification should also state whether the original collection changes, how duplicates are handled, whether equal records retain their relative order, and how much additional memory is used.
maintains a sorted prefix. It removes the next unsorted value and shifts larger prefix values one position to the right until the value can be inserted. Starting with , the sorted prefixes become , , , and finally . It uses extra space and has worst-case time , but it can perform well on small or nearly sorted inputs.
uses divide and conquer. It divides the collection, recursively sorts each half, and merges the sorted halves by repeatedly choosing the smallest remaining value. There are approximately levels of division, and each level processes values during merging, so the running time is . Its usual implementation uses additional space.
Takeaway: Choose a sorting method by considering input size, existing order, memory limits, duplicate handling, and whether stability matters.
Iteration and Loop-Invariant Proofs
Iteration repeats instructions with a loop. A correct loop must make progress toward a stopping condition. For example, a list-summing procedure can initialize a total to and an index to , add the element at the current index, and increase the index by on each pass. Once the index reaches the list length, the loop stops.
A makes the loop’s changing state easier to reason about. For , a suitable invariant is: before iteration , the subarray from index through index is sorted and contains exactly the values that originally occupied those positions.
The proof has three stages:
Initialization: Before the first iteration, the prefix contains one value and is therefore sorted.
Maintenance: Assuming the prefix is sorted, inserting the next value while shifting larger values preserves the invariant for the next iteration.
Termination: When the loop ends, the prefix is the entire collection, so the whole collection is sorted.
This reasoning proves the general behavior of the procedure rather than only confirming a few examples.
Takeaway: A connects local updates to a global postcondition through initialization, maintenance, and termination.
, Induction, and Iterative Alternatives
solves a problem by calling the same procedure on a smaller instance. A correct recursive procedure needs a base case, a recursive case that reduces the problem, progress toward the base case, and a way to combine the smaller result.
Factorial illustrates the pattern:
For input , the calls reduce from to , then , and finally . The base case returns , after which the pending multiplications produce .
Many recursive procedures have iterative equivalents. can express trees, nested structures, and divide-and-conquer methods naturally, while iteration often uses less call-stack memory for repetitive processes.
Recursive correctness is commonly established by mathematical induction: prove the base case, assume correctness for a smaller input, and show that the recursive step uses that result to solve the current input.
Takeaway: is correct only when every call moves toward a directly solvable base case and the returned smaller solution is combined correctly.
Termination, , and Review
Correctness has two separate parts. means that if an terminates, its output satisfies the specification. Termination means that the eventually stops. Together, they establish .
A common termination argument uses a : a nonnegative quantity that strictly decreases on every loop iteration or recursive call. Examples include the number of unprocessed elements, the size of a binary-search interval, the current value of in factorial, or the number of unsorted positions.
For , the interval between the lower and upper bounds becomes smaller after every unsuccessful comparison. A finite interval cannot shrink forever while remaining nonempty, so the procedure terminates. For factorial, the argument decreases by on every recursive call until it reaches .
A practical review should ask:
Are inputs, outputs, , and precise?
Are empty collections, duplicates, and boundary cases handled?
Does every loop or recursive call make progress?
What invariant or induction hypothesis supports correctness?
What are the running-time and memory requirements?
Do tests include ordinary, boundary, and error-revealing cases?
Testing can expose bugs, but it cannot prove correctness for every possible input. Specifications, invariants, induction, and termination arguments provide stronger guarantees.
Takeaway: Reliable design combines a precise specification, a correctness argument, a termination argument, and an efficiency analysis.