8.2. Algorithm Discovery


Common sense

Your most important tool for discovering algorithms is common sense. Think about how you would solve a problem by hand. Try examples. Then work out how to express your hand algorithm so that a computer can carry it out.


A sequence of steps

Some problems break naturally down into a sequence of a fixed number of steps. The steps themselves can be big ones, each involving an algorithm of its own, but the big picture is a sequence of steps.

As an example, consider the problem of writing a nonnegative integer i using at least n characters by adding spaces as required. But instead of padding with spaces on the left, we will add spaces to the right end. For example,

  writePadded(25,4);
  writePadded(725,4);
  writePadded(9,4);
  printf(".");
writes
25  725 9   .
using 4 total characters for each number, but with the added spaces to the right of the number. (If i uses more than n characters, then writePadded(i, n) just writes i, without any added spaces.)

(It would be possible to use printf to do this job. A simple definition of writePadded is as follows.

  void writePadded(int x, int n)
  {
    printf("%-*i", n, x);
  }
but our purpose here is not so much about writing a definition of writePadded, but instead of using it to illustrate algorithm design, so we avoid using printf.)

The steps to do writePadded(i,n) are as follows.

  1. Write the integer i (without any padding), counting the number of characters used. Suppose that a total of k characters are used in this step.
  2. If n > k then write nk spaces.

The individual steps still need to be solved, but you don't need to worry about that right away. Breaking the problem down into steps has yielded some simpler problems.


Top-down design

At this point in writing a definition of writePadded, you might hope to find a function in the library that carries out step 1 and another that carries out step 2. But you do not always find one. In that case, the principle of top-down design suggests that you should imagine that suitable functions are already available, and use them. Then work out how to solve them; write them yourself. Of course, in order to do this, you need to know exactly what each of the imagined functions is supposed to do. So you need a contract and heading for each one. It is the function bodies that you fill in later.

For writePadded(i, n), here are some obvious tools, one for doing step 1 and the other for doing step 2.

  1. writeInteger(i) writes nonnegative integer i and returns the number of characters written.

  2. writeSpaces(n) writes n spaces.

Using those tools yields a simple definition of writePadded.

  void writePadded(int i, int n)
  {
    int k = writeInteger(i);
    if(n > k)
    {
      writeSpaces(n - k);
    }
  }
Notice that it is clearly two steps, one after the other.


Loops

If a problem involves doing a variable number of steps, a loop is an obvious tool to use. For example, how would you write n spaces? It suffices to write one space n times. That is a simple repetition.

  void writeSpaces(int n)
  {
    for(int i = 1; i <= n; i++)
    {
      putchar(' ');
    }
  }
We will encounter other algorithms that are naturally expressed as loops.


Cases

Some problems break down into two or more cases. For example, to find the larger of x and y, you know that

  1. If x > y, then x is the larger one.
  2. If x <= y, then y is the larger one.
The great thing about breaking a problem down into cases is that you only need to think about how to solve one case at a time. Then, putting all of the cases together yields a solution to the whole problem, as in the following definition of larger.
  int larger(int x, int y)
  {
    if(x > y)
    {
      return x;
    }
    else
    {
      return y;
    }
  }