Skip to content
Informatics

Algorithms: flowcharts, structured programming and complexity

Properties of an algorithm, flowcharts and pseudocode with Euclid's algorithm, the Böhm-Jacopini theorem, big-O notation and binary search.

An algorithm is a finite sequence of unambiguous instructions that solves a problem. The word comes from the name of the Persian mathematician al-Khwarizmi, and the first algorithm we know of, Euclid’s for the greatest common divisor, is more than two thousand years old. Before writing code it pays to design the algorithm: that is where you decide whether the program will be correct and fast. In this lesson we see how an algorithm is described, the basic structures of structured programming and how cost, that is complexity, is measured.

Properties of an algorithm

  • Finiteness: it ends after a finite number of steps.
  • Unambiguity: each step has a single meaning.
  • Input and output: it receives data and produces a result.
  • Effectiveness: each step can really be carried out with the available operations.
  • Generality: it solves a class of problems, not a single case.

How to describe it: flowcharts and pseudocode

A flowchart represents the algorithm with blocks joined by arrows: ovals for start and end, parallelograms for input and output, rectangles for operations, diamonds for decisions.

Flowchart of Euclid's algorithm for the greatest common divisor with its execution table
Euclid’s algorithm: while b is not zero, replace the pair (a, b) with (b, a mod b). Right, the run with a = 48 and b = 18: after three steps b is 0 and the result is a = 6.

Pseudocode describes the same algorithm in a language halfway between English and a programming language, more compact than the flowchart:

read a, b
while b ≠ 0:
    r ← a mod b
    a ← b
    b ← r
write a

Why it works. Every common divisor of a and b also divides r = a - qb, and vice versa: so \text{GCD}(a, b) = \text{GCD}(b, r). Since r \lt b, the second number shrinks at each step and reaches 0 in finitely many steps, and \text{GCD}(a, 0) = a.

Structured programming

The Böhm-Jacopini theorem (1966, two Italian computer scientists) proves that any algorithm can be written using only three structures:

  1. sequence: one instruction after another;
  2. selection: if a condition is true do one thing, otherwise another (if);
  3. iteration: repeat while a condition is true (while).

Arbitrary jumps (goto), which make code hard to follow, are not needed: this is the basis of structured programming, and of the C we will see from the next lesson.

Complexity

Two correct algorithms for the same problem can take very different times. Computational complexity measures how the number of operations grows as the size n of the data grows. The O notation is used: an algorithm is O(f(n)) if for large n it performs at most a number of operations proportional to f(n).

ComplexityExamplen = 1000n = 10^6
O(1)accessing an array element11
O(\log n)binary search in a sorted array1020
O(n)finding the maximum of an array10^310^6
O(n\log n)merge sort, FFT10^42\times 10^7
O(n^2)bubble sort, direct DFT10^610^{12}
O(2^n)trying every subset10^{301}impossible

At a billion operations per second, 10^{12} operations take a quarter of an hour; 2\times 10^7 take two hundredths of a second. Improving the algorithm matters far more than buying a faster computer.

Example: how many steps Euclid takes. Every two steps the first number at least halves (it is easy to prove), so the number of steps is at most about 2\log_2 of the smaller number: O(\log n). For 300-digit numbers, like those of RSA cryptography, a few thousand steps are enough, a fraction of a millisecond. Trying every divisor would be impossible.

Linear search and binary search

Searching for a value in an array of n unsorted elements means looking at them one by one: O(n). If the array is sorted you can do much better: look at the middle element, and depending on whether it is larger or smaller than the value sought, discard half the array. After k steps n/2^k elements remain, so \log_2 n steps are enough: 20 comparisons for a million elements, 30 for a billion.

/* returns the index of x in the sorted array v of n elements, or -1 */
int binary_search(const int v[], int n, int x)
{
    int lo = 0, hi = n - 1;
    while (lo <= hi) {
        int mid = lo + (hi - lo) / 2;   /* avoids computing lo + hi, which could overflow */
        if (v[mid] == x) return mid;
        if (v[mid] < x) lo = mid + 1;
        else            hi = mid - 1;
    }
    return -1;
}

Computing mid as lo + (hi - lo) / 2 instead of (lo + hi) / 2 is not pedantry: with indices close to the largest value of an int, the sum would overflow. It is a bug that stayed for years in the standard libraries of several languages.

Subscribe
Notify of
guest
0 Comments
Oldest
Newest Most Voted
0
Would love your thoughts, please comment.x
()
x