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 −1-1 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 [8,3,11,4][8, 3, 11, 4] and target 1111, it checks 88, then 33, and then returns index 22. With nn elements, its worst-case running time is O(n)\mathrm{O}(n).

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 O(log⁡n)\mathrm{O}(\log n). For example, when searching for 1717 in [2,5,8,12,17,21,30][2,5,8,12,17,21,30], a comparison with 1212 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 O(n)\mathrm{O}(n) worst-case time; trades a sorted-input requirement for O(log⁡n)\mathrm{O}(\log n) 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 [5,2,4,6][5,2,4,6], the sorted prefixes become [5][5], [2,5][2,5], [2,4,5][2,4,5], and finally [2,4,5,6][2,4,5,6]. It uses O(1)\mathrm{O}(1) extra space and has worst-case time O(n2)\mathrm{O}(n^2), 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 log⁡n\log n levels of division, and each level processes nn values during merging, so the running time is O(nlog⁡n)\mathrm{O}(n\log n). Its usual implementation uses O(n)\mathrm{O}(n) 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 00 and an index to 00, add the element at the current index, and increase the index by 11 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 ii, the subarray from index 00 through index i−1i-1 is sorted and contains exactly the values that originally occupied those positions.

The proof has three stages:

  1. Initialization: Before the first iteration, the prefix contains one value and is therefore sorted.

  2. Maintenance: Assuming the prefix is sorted, inserting the next value while shifting larger values preserves the invariant for the next iteration.

  3. 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:

0!=10! = 1
n!=ntimes(n−1)!quadtextforn>0n! = n \\times (n-1)! \\quad \\text{for } n > 0

For input 33, the calls reduce from 33 to 22, then 11, and finally 00. The base case returns 11, after which the pending multiplications produce 66.

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 nn 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 11 on every recursive call until it reaches 00.

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.