Recursion with natural numbers

Like lists, natural numbers have a recursive structure of which we can take advantage when we write direct-recursion procedures. A natural number is either (a) zero, or (b) the successor of a smaller natural number, which we can obtain by subtracting 1.

Standard Scheme provides the predicate zero? to distinguish between the (a) and (b) cases, so we can again use an if-expression to ensure that only the expression for the appropriate case is evaluated. So we can write a procedure that applies to any natural number if we know (a) what value it should return when the argument is 0 and (b) how to convert the value that the procedure would return for the next smaller natural number into the appropriate return value for a given non-zero natural number.

For instance, here is a procedure that computes the termial of any natural number number, that is, the result of adding together all of the natural numbers up to and including number:

;;; termial: compute the sum of natural numbers not greater than a given
;;; natural number 

;;; Given:
;;;   NUMBER, a natural number.

;;; Result:
;;;   SUM, a natural number.

;;; Preconditions:
;;;   None.

;;; Postcondition:
;;;   SUM is the sum of the natural numbers not greater than NUMBER.

(define termial
  (lambda (number)
    (if (zero? number)
        0
        (+ number (termial (- number 1))))))

Whereas in a list recursion, we called the cdr procedure to reduce the length of the list in making the recursive call, the operation that we apply in recursion with natural numbers is reducing the number by 1. Here's a summary of what actually happens during the evaluation of a call to the termial procedure -- say, (termial 5):

    (termial 5)
--> (+ 5 (termial 4))
--> (+ 5 (+ 4 (termial 3)))
--> (+ 5 (+ 4 (+ 3 (termial 2))))
--> (+ 5 (+ 4 (+ 3 (+ 2 (termial 1)))))
--> (+ 5 (+ 4 (+ 3 (+ 2 (+ 1 (termial 0))))))
--> (+ 5 (+ 4 (+ 3 (+ 2 (+ 1 0)))))
--> (+ 5 (+ 4 (+ 3 (+ 2 1))))
--> (+ 5 (+ 4 (+ 3 3)))
--> (+ 5 (+ 4 6))
--> (+ 5 10)
--> 15

The restriction that termial takes only natural numbers as arguments is an important one: If we gave it a negative number or a non-integer, we'd have a runaway recursion, because we cannot get to zero by subtracting 1 repeatedly from a negative number or from a non-integer, and so the base case would never be reached. If we gave the termial procedure an approximation rather than an exact number, we might or might not be able to reach zero, depending on how accurate the approximation is and how much of that accuracy is preserved by the subtraction procedure.


Exercise 1

Using natural-number recursion, define and test a Scheme procedure named power-of-two that takes a natural number as its argument and returns the result of raising 2 to the power of that number. (For instance, the value of (power-of-two 3) should be 23, or 8.)

It's possible to define this procedure non-recursively, using Scheme's primitive expt procedure, but the point of the exercise is to use recursion.


Exercise 2

Define and test a Scheme procedure named fill-list that takes two arguments, the second of which is a natural number, and returns a list consisting of the specified number of repetitions of the first argument:

> (fill-list 'sample 5)
(sample sample sample sample sample)
> (fill-list (list 'left 'right) 3)
((left right) (left right) (left right))
> (fill-list null 1)
(())

Exercise 3

Define and test a Scheme procedure named count-down that takes a natural number as argument and returns a list of all the natural numbers less than or equal to that number, in descending order:

> (count-down 5)
(5 4 3 2 1 0)

The important part of getting recursion to work is making sure that the base case is inevitably reached by performing the simplification operation enough times. For instance, we can use direct recursion on exact positive integers with 1, rather than 0, as the base case.

;;; factorial: compute the product of positive integers not
;;; greater than a given positive integer

;;; Given:
;;;   NUMBER, an integer.

;;; Result:
;;;   PRODUCT, an integer.

;;; Precondition:
;;;   NUMBER is positive and exact.

;;; Postcondition:
;;;   PRODUCT is the product of the positive integers not
;;;   greater than NUMBER.

(define factorial
  (lambda (number)
    (if (= number 1)
        1
        (* number (factorial (- number 1))))))

We require the invoker of this factorial procedure to provide an exact, strictly positive integer. (Zero won't work in this case, because we can't reach the base case, 1, by repeated subtractions if we start from 0.)

Similarly, we can use direct recursion to approach the base case from below by repeated additions of 1, if we know that our starting point is less than or equal to that base case. Here's an example:

;;; count-from: given two natural numbers, construct a list of the
;;; natural numbers from the first to the second, inclusive, in ascending
;;; order

;;; Given:
;;;   LOWER and UPPER, both natural numbers.

;;; Result:
;;;   LS, a list.

;;; Precondition:
;;;   LOWER is less than or equal to UPPER.

;;; Postconditions:
;;;   (1) The length of LS is UPPER - LOWER + 1.
;;;   (2) For every natural number k less than or equal to the length of
;;;       LS, the element in position k of LS is LOWER + k.

(define count-from
  (lambda (lower upper)
    (if (= lower upper)
        (list upper)
        (cons lower (count-from (+ lower 1) upper)))))

Exercise 4

What is the value of the call (count-from 15 25)? Write down what you think that it should be, then copy the definition of count-from into DrScheme and use it to find out what the call actually returns.


Exercise 5

Using count-from, define and test a Scheme procedure that takes a natural number as argument and returns a list of all the natural numbers that are strictly less than the argument, in ascending order. (The traditional name for this procedure is iota -- another Greek.letter.)


Exercise 6

Here is a procedure that computes the product of all of the odd natural numbers up to and including number:

(define odd-factorial
  (lambda (number)
    (if (= number 1)
        1
        (* number (odd-factorial (- number 2))))))

What precondition does odd-factorial impose on its argument? What will happen if this precondition is not met?


Exercise 7

Here is the definition of a procedure that computes the number of digits in the decimal representation of number:

(define number-of-decimal-digits
  (lambda (number)
    (if (< number 10)
        1
        (+ (number-of-decimal-digits (quotient number 10)) 1))))

Test this procedure.

The definition of number-of-decimal-digits uses direct recursion. Describe the base case of this recursion. Identify and describe the way in which a simpler instance of the problem is created for the recursive call. Explain how the procedure correctly determines that the decimal numeral for the number 2000 contains four digits.

What preconditions does number-of-decimal-digits impose on its argument?


Exercise 8 (optional)

For this exercise, you'll need to know about two predefined Scheme procedures that operate on strings:

The string-length procedure takes a string as argument and returns a natural number indicating how many characters are in that string:

> (string-length "Grinnell")
8
> (string-length "Hello, world!")
13
> (string-length " ")
1
> (string-length "")
0

The string-append procedure takes any number of strings as arguments and returns a string containing all the characters from its arguments, in order:

> (string-append "alpha" "beta")
"alphabeta"
> (string-append "a1" "b2" "c3" "d4" "e5")
"a1b2c3d4e5"
> (string-append "alif")
"alif"
> (string-append "" "" "" "" "")
""

Define and test a Scheme procedure named duplicate-to-fit that takes two arguments, a string and a maximum length, and returns a string consisting of as many repetitions of the given string as possible, provided that the specified maximum length is not exceeded.

> (duplicate-to-fit "alf" 11)
"alfalfalf"
; Four copies would exceed the maximum length, but three will fit.

> (duplicate-to-fit "alf" 12)
"alfalfalfalf"
; An exact fit is okay.  Four copies of "alf" make a string of 12 characters.

> (duplicate-to-fit "*" 7)
"*******"
; A one-character string always fits exactly; just duplicate it.

> (duplicate-to-fit "*-" 9)
"*-*-*-*-"

> (duplicate-to-fit "invisible" 3)
""
; If the maximum length is less than the length of the string, return
; the null string (zero copies of the given string).

Exercise 9 (optional)

Define and test a procedure named runs that takes a natural number n as argument and returns a list of Boolean values consisting of n runs, where a run consists of zero or more occurrences of #f followed by one occurrence of #t. The first of the n runs should be of length 1, the second of length 2, and so on.

> (runs 5)
(#t #f #t #f #f #t #f #f #f #t #f #f #f #f #t)

This document is available on the World Wide Web as

http://www.cs.grinnell.edu/~stone/courses/scheme/recursion-with-natural-numbers.xhtml

created February 3, 2000
last revised September 15, 2000

John David Stone (stone@cs.grinnell.edu)