An Introduction To Computing
An Introduction To Computing
August 2, 2003
I Models of computation 5
1 Introduction 7
2 Mathematical preliminaries 9
2.0.1 Sets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
2.0.2 Relations and Functions . . . . . . . . . . . . . . . . . . . . . . . . . 11
2.0.3 Principle of Mathematical Induction . . . . . . . . . . . . . . . . . . 11
3
4 CONTENTS
Models of computation
5
Chapter 1
Introduction
This course is about computing. The notion of computing is much more fundamental than
the notion of a computer, because computing can be done even without one. In fact, we have
been computing ever since we entered primary school, mainly using pencil and paper. Since
then, we have been adding, subtracting, multiplying, dividing, computing lengths, areas,
volumes and many many other things. In all these computations we follow some definite,
unambiguous set of rules. This course is about studying these rules for a variety of problems
and writing them down explicitly.
When we explicitly write down the rules (or instructions) for solving a given computing
problem, we call it an algorithm. Thus algorithms are primarily vehicles for communication;
for specifying solutions to computational problems, unambiguously, so that others (or even
computers) can understand the solutions. When an algorithm is written according to a
particular syntax of a language which can be interpreted by a digital computer, we call it
a program. This last step is necessary when we wish to carry out our computations using a
computer.
While writing down algorithms, it is important to choose an underlying model of com-
putation, i.e., to choose appropriate primitives to describe an algorithm. This choice deter-
mines the kind of computations that can be carried out in the model. For example, if our
computational model consists of only ruler and compass constructions, then we can write
down explicit rules (algorithms) for bisecting a line segment, bisecting an angle, construct-
ing lengths equal to given irrational numbers and a plethora of other things. We cannot,
however, trisect an angle. For trisecting an angle, we require additional primitives like, for
example, a protractor. For arithmetic computations we can use various computing models
like calculators, slide rules or even an abacus (as we believe our ancestors have been using).
With each of these models of computing, the rules for specifying a solution (algorithms) are
different, and the precision of the solution also differs. Thus, in our study of algorithms and
programs, it becomes important to first choose a reasonable model of computation. Soon
in these notes we will describe our choice of computational models which are widely used
in modern computing using digital computers.
Once a computational model is available, and we can specify an algorithm (or a program)
7
8 CHAPTER 1. INTRODUCTION
to solve a given problem, we have to ensure that the algorithm is correct. We would also
wish that our algorithms are efficient. Correctness and efficiency of algorithm design are
central issues in this course. In what follows, we will endeavour to develop methodologies
for the design of correct algorithms.
Thus, the major vehicles for problem solving using computers are:
In the two subsequent chapters we will establish two widely used models of computation
and develop methodologies for algorithm design and proving correctness of algorithms in
these models. The two models that we will introduce are the functional and the imperative
models of computation. We will use the programming languages ML and Java to write
programs in these two models respectively. We will devote the remaining part of this
chapter to discuss some mathematical preliminaries necessary for programming.
Chapter 2
Mathematical preliminaries
We will discuss sets, relations and functions, Boolean logic and Principle of Mathematical
Induction. Most of these topics are covered in the high school curriculum and a confident
reader may wish to skip this section. However, we urge the reader to definitely read the
material on Mathematical Induction which forms the basis for programming.
2.0.1 Sets
A set is a collection of distinct objects. The class of CS120 is a set. So is the group of all
first year students at the IITD. We will use the notation {a, b, c} to denote the collection of
the objects a, b and c. The elements in a set are not ordered in any fashion. Thus the set
{a, b, c} is the same as the set {b, a, c}. Two sets are equal if they contain exactly the same
elements.
We can describe a set either by enumerating all the elements of the set or by stating
the properties that uniquely characterize the elements of the set. Thus, the set of all
even positive integers not larger than 10 can be described either as S = {2, 4, 6, 8, 10} or,
equivalently, as S = {x | x is an even positive integer not larger than 10}
A set can have another set as one of its elements. For example, the set A = {{a, b, c}, d}
contains two elements {a, b, c} and d; and the first element is itself a set.
We will use the notation x ∈ S to denote that x is an element of (“belongs to”) the set
S.
A set A is a subset of another set B, denoted as A ⊆ B, if x ∈ B whenever x ∈ A.
An empty set is one which contains no elements and we will denote it with the symbol
φ. For example, let S be the set of all students who fail the course CS120. S might turn
out to be empty (hopefully; if everybody studies hard). By definition, the empty set φ is
a subset of all sets. We will also assume an Universe of discourse U, and every set that we
will consider is a subset of U. Thus we have
9
10 CHAPTER 2. MATHEMATICAL PRELIMINARIES
The union of two sets A and B, denoted A ∪ B is the set whose elements are exactly the
elements of either A or B (or both). The intersection of two sets A and B, denoted A ∩ B
is the set whose elements are exactly the elements of both A and B. Thus, we have
1. S = A ∪ B = {x | (x ∈ A) or (x ∈ B)}
2. S = A ∩ B = {x | (x ∈ A) and (x ∈ B)}
1. A ∪ φ = A
2. A ∪ U = U
3. A ∩ φ = φ
4. A ∩ U = A
The Cartesian product of two sets A and B, denoted by A × B, is the set of all ordered
pairs (a, b) such that a ∈ A and b ∈ B. Thus,
An is the set of all ordered n-tuples (a1 , a2 , . . . , an ) such that ai ∈ A for all i. i.e.,
An = A
| ×A× {z· · · × A}
n times
4. The binary relations =, 6=, <, ≤, >, ≥ are also functions of the type f : N × N → B
where B = {f alse, true}.
Example 2.1 The factorial function on natural numbers (of the type f : N → N) is defined
as follows
Basis. 0! = 1
Example 2.2 The nth power (where n is a natural number) of a positive number x is often
defined as
Basis. x0 = 1
Example 2.3 The set of n-tuples of natural numbers can be defined in terms of Cartesian
products as
Basis. N1 = N
Example 2.4 For binary relations R and S on A we define their composition (denoted
R ◦ S) as follows.
Basis. R1 = R
Similarly the principle of mathematical induction is the means by which we have often
proved (as opposed to defining) properties about numbers, or statements involving the
natural numbers. The principle may be stated as follows.
The underlined portion, called the Induction hypothesis, is an assumption that is nec-
essary for the conclusion to be proved. Intuitively, the principle captures the fact that in
order to prove any statement involving natural numbers, it suffices to do it in two steps.
The first step is the basis which needs to be proved. The proof of the induction step es-
sentially tells us that the reasoning involved in proving the statement involving the other
natural numbers is the same once the basis has been proved. Hence instead of an infinitary
proof (one for each natural number) we have a compact finitary proof which exploits the
similarity of the proofs for all the naturals except the basis.
13
Example 2.5 We prove that all natural numbers of the form n3 + 2n are divisible by 3.
Proof:
(n + 1)3 + 2(n + 1)
= (n3 + 3n2 + 3n + 1) + (2n + 2)
= (n3 + 2n) + 3(n2 + n + 1)
which is divisible by 3.
Several versions of this principle exist. We state some of the most important ones. In each
case, the underlined portion is the induction hypothesis. For example it is not necessary
to consider 0 (or even 1) as the basis step. Any integer k could be considered the basis, as
long as the property is to be proved for all n ≥ k.
Such a version seems very useful when the property to be proved is not true or is undefined
for 0 or 1. The following example illustrates this.
Example 2.6 Suppose we have stamps of two different denominations, 3 paise and 5 paise.
We want to show that it is possible to make up exactly any postage of 8 paise or more using
stamps of these two denominations [Liu]. Thus we want to show that every positive integer
n ≥ 8 is expressible as n = 3i + 5j where i, j ≥ 0.
Proof:
2
14 CHAPTER 2. MATHEMATICAL PRELIMINARIES
Induction step: If P holds for all m, 0 < m ≤ n for an arbitrary n ≥ 0, then P also holds
for n + 1.
Example 2.7 Let F0 = 0, F1 = 1, F2 = √1, . . . be the Fibonacci sequence where for all
n ≥ 2, Fn = Fn−1 + Fn−2 . Let φ = (1 + 5)/2. We now show that Fn ≤ φn−1 for all
positive n.
Proof:
Induction step.
Fn+1 = Fn + Fn−1
≤ φn−1 + φn−2 (by the induction hypothesis)
= φn−2 (φ + 1)
= φn (since φ2 = φ + 1)
Versions 1 and 2 of PMI rely on the fact that starting from 0 (or k) every integer larger
than 0 (or k) may be obtained by successively adding 1 to the previous one, whereas version
3 is obtained by considering the natural numbers as being totally ordered by the < relation.
Since the natural numbers are themselves defined as the smallest set N such that 0 ∈ N
and whenever n ∈ N, n + 1 also belongs to N. Therefore we may state yet another version
of PMI from which the other versions previously stated may be derived. The intuition
behind this version is that a property P may also be considered as defining a set S = {x |
x satisfies P}. Therefore if a property P is true for all natural numbers the set defined by
the property is the set of natural numbers. This gives us the last version of PMI.
Problems
1. GCD of two integers a, b > 0 is defined as max{x : x is an integer, x > 0, x | a, x | b},
where the notation x | a means x divides a. Consider the following algorithm for
computing GCD using pencil and paper:
a if a = b
gcd(a, b) = gcd(a − b, b) if a > b
gcd(a, b − a) if b > a
Convince yourself that the above algorithmic specification (rule) is correct for com-
puting GCD. Carry out the pencil and paper computation using the above algorithm
for the special case of a = 18 and b = 12.
(a) Explain how you can compare two integer lengths to determine which is larger.
(b) Explain how you can subtract one integer from the other.
(c) Give a set of rules (algorithm) for computing the GCD of two integers using ruler
and compass.
4. Note that
1 1
1+ = 2−
2 2
1 1 1
1+ + = 2−
2 4 4
1 1 1 1
1+ + + = 2−
2 4 8 8
Guess the general law suggested and prove it by using PMI.
5. Prove the following statement using PMI: If a line of unit length is given, then a line
√
of length n can be constructed using ruler and compass for every positive integer n.
6. Prove by PMI, that every integer n > 1 is either a prime or a product of primes.
8. Find the fallacy in the following proof by PMI. Rectify it and again prove using PMI.
Theorem
1 1 1 3 1
+ + ··· + = −
1×2 2×3 (n − 1) × n 2 n
Proof: For n = 1 the LHS is 1/2 and so is the RHS. Assume that the theorem is
true for an n > 1. We then prove the induction step.
1 1 1 1
LHS = + + ··· + +
1×2 2×3 (n − 1) × n n × (n + 1)
3 1 1
= − +
2 n n × (n + 1)
3 1 1 1
= − + −
2 n n n+1
3 1
= −
2 n+1
2
In this chapter we will introduce the basics of a functional model of computation. The func-
tional model is very close to mathematics; hence functional algorithms are easy to analyze
in terms of correctness and efficiency. We will use the ML interactive environment to write
and execute functional programs. This will provide an interactive mode of development and
testing of our early algorithms. In the later chapters we will see how a functional algorithm
can serve as a specification for development of algorithms in other models of computation.
In the functional model of computation every problem is viewed as an evaluation of a
function. The solution to a given problem is specified by a complete and unambiguous
functional description. Every reasonable model of computation must have the following
facilities:
Primitive expressions which represent the simplest objects with which the model is con-
cerned.
Methods of combination which specify how the primitive expressions can be combined
with one another to obtain compound expressions.
Methods of abstraction which specify how compound objects can be named and ma-
nipulated as units.
We will introduce the more advanced concepts in our functional model later in these notes.
17
18 CHAPTER 3. A FUNCTIONAL MODEL OF COMPUTATION
If we now type x; at the ML prompt the interpreter returns val it = 5 : int. The word
it is an ML keyword indicating the the value of the last expression that it has evaluated.
Further, note that ML also informs you of the type of the value that it has returned. The
word int is used to denote an integer.
- x;
val it = 5 : int
Similarly we can bind the variable y to the boolean constant true (true) as follows:
- val y = true;
val y = true : bool
-
- y;
val it = true : bool
-
In the above example the user types in the expressions after the ML prompt - and the ML
interpreter prints the result in the next line. Note that ML distinguishes between the binary
operation of subtraction - from the unary minus ~ used for negative numbers. Further notice
that ML follows standard mathematical convention for the div and mod operators, viz. that
the quotient and remainder on division must satisfy the condition that the remainder is
non-negative.
hExamplei≡ is not a part of the ML code. It is our convention for stating that hExamplei
is the name we will use to denote the following Program codes. The actual ML code follows
in the type-writer font.
We can directly specify the function square, which is of the type square : N → N in
terms of the standard multiplication function ∗ : N × N → N as
square(n) = n ∗ n
Here, we are assuming that we can substitute one function for another provided they both
return an item of the same type. To evaluate, say, square(5), we have to thus evaluate 5 ∗ 5.
An ML program for this function can be described as
hSquarei≡
fun square(n):int = n * n;
fun is a special word in ML (called a keyword) that is used for defining new functions. In
our example we use it to define square n to be n * n. An invocation of the ML function
with square(5) returns 25.
Thus, we can build more complex functions from simpler ones. As an example, let us
define a function to compute x2 + y 2 .
The function sum squares is thus defined in terms of the functions + and square. The
corresponding ML program can be written as
hSum of squaresi≡
fun sum_squares (x, y) = square (x) + square (y);
3.2. SUBSTITUTION OF FUNCTIONS 21
Example 3.3
Let us define a function f : N → N as follows
In ML we can define it as
hThe function f i≡
fun f (n) = sum_squares (n+1, n+2);
Invocation of the function with f(5) results in evaluation of sum squares (5+1, 5+2)
which, in turn, results in the evaluation of square (6) + square (7) yielding the final
answer + (6 * 6) + (7 * 7) which is 85.
a = 1 + xy
b=1−y
2
f (x, y) = xa + yb + ab
Thus we can avoid multiple computations of 1 + xy and 1 − y by using local variables a and
b.
In ML this can be achieved using the primitive let as follows
husing leti≡
fun f (x, y) =
let
val a = 1 + x * y;
val b = 1 - y
in x*a*a + y*b + a*b
end;
22 CHAPTER 3. A FUNCTIONAL MODEL OF COMPUTATION
While defining the function max, we have assumed that we can compare two natural num-
bers, with the ≥ function and determine which is larger. The basic primitive used in this
case is if-then-else. Thus if a ≥ b, the function returns a as the output, else it returns b.
Note that for every pair of natural numbers as its input, max returns a unique number as
the output and hence it adheres to the definition of a function given in Section 2.0.2. In ML
the above definition looks as follows.
hmax i≡
fun max (a, b):int =
if a >= b then a
else b;
or, alternatively as
habs (second alternative)i≡
fun abs (x) =
if x < 0 then ~x
else x;
24 CHAPTER 3. A FUNCTIONAL MODEL OF COMPUTATION
However not all mathematically valid specifications of functions are algorithms. For exam-
ple, ½
m if m ∗ m = n
sqrt(n) =
0 if 6 ∃m : m ∗ m = n
is mathematically a perfectly valid description of a function of the type sqrt : N → N.
However the mathematical description does not tell us how to evaluate the function, and
hence it is not an algorithm. An algorithmic description of the function would have to start
with m = 1 and check if m ∗ m = n for all subsequent increments of m by 1 till either
such an m is found or m ∗ m > n. We will soon see how to describe such functions as
computational processes or algorithms such that the computational processes terminate in
finite time.
As another example of a mathematically valid specification of a function which is not an
algorithm, consider the following functional description of f : N → N
Now this indeed defines a valid algorithm for computing f (n). Mathematically the spec-
ifications for f (n) and g(n) are equivalent in that they both define the same function.
However, the specification for g(n) constitutes a valid algorithm whereas that for f (n) does
not. For successive values of n, g(n) can be computed as
g(0) = 0
g(1) = g(0) + 1 = 1
g(2) = g(1) + 1 = g(0) + 1 + 1 = 2
..
.
f (n) = f (n)
3. It is inductively defined and the validity of its description can be established through
the Principle of Mathematical Induction.
4. It is obtained through any finite number of combinations of the above three steps
using substitutions.
Here f actorial(n) is the function “name” and the description after the = sign is the “body”
of the function. A ML program for the above algorithm looks as follows:
hFactorial i≡
fun factorial (n) =
if n = 0 then 1
else n * factorial (n-1);
1
The factorial function was first defined by Euclid in his Elements during the course of his proof of the
existence of infinitely many prime numbers. This was written around 300 B.C.
3.5. RECURSIVE PROCESSES 27
f actorial(5)
= (5 × f actorial(4))
= (5 × (4 × f actorial(3)))
= (5 × (4 × (3 × f actorial(2))))
= (5 × (4 × (3 × (2 × f actorial(1)))))
= (5 × (4 × (3 × (2 × (1 × f actorial(0))))))
= (5 × (4 × (3 × (2 × (1 × 1)))))
= (5 × (4 × (3 × (2 × 1))))
= (5 × (4 × (3 × 2)))
= (5 × (4 × 6))
= (5 × 24)
= 120
Exercise 3.1 Consider the following example of a function f : N → Z defined just like
factorial except that multiplication is replaced by subtraction which is not associative.
½
1 if n = 0
f (n) =
n − f (n − 1) otherwise
1. Unfold the computation, as in the example of f actorial(5) above, to show that f (5) =
2.
28 CHAPTER 3. A FUNCTIONAL MODEL OF COMPUTATION
2. What properties will you use as a human computer in order to avoid deferred com-
putations?
3.6.1 Correctness
The correctness of the above functional algorithm can be established by using the Principle
of Mathematical Induction. The algorithm adheres to an inductive definition and, conse-
quently, can be proved correct by using PMI. Even though the proof of correctness may
seem obvious in this instance, we give the proof to emphasize and clarify the distinction
between a mathematical specification and an algorithm that implements it.
To show that: For all n ∈ N, f actorial(n) = n! (i.e., the function f actorial implements
the factorial function defined in Example 2.1).
Proof: By PMI on n.
f actorial(n) = n × f actorial(n − 1)
= n × (n − 1)! by the induction hypothesis
= n! by the definition of n!
3.6.2 Efficiency
The other important aspect in the analysis of an algorithm is the issue of efficiency - both in
terms of space and time. The efficiency of an algorithm is usually measured in terms of the
space and time required in the execution of the algorithm (the space and time complexities).
These are functions of the input size n.
A careful look at the above computational process makes it obvious that in order to
compute f actorial(n), the n integers will have to be remembered (or stacked up) before the
actual multiplications can begin. Clearly, this leads to a space requirement of about n. We
will call this the space complexity.
The time required to execute the above algorithm is directly proportional (at least as a
first approximation) to the number of multiplications that have to be carried out and the
number of function calls required. We can evaluate this in the following way. Let T (n) be
3.6. ANALYSIS OF CORRECTNESS AND EFFICIENCY 29
the number of multiplications required for a problem of size n (when the input is n). Then,
from the definition of the function f actorial we get
½
0 if n = 0
T (n) = (3.1)
1 + T (n − 1) otherwise
T (0) is obviously 0, because no multiplications are required to output f actorial(0) = 1 as
the result. For n > 0, the number of multiplications required is one more than that required
for a problem of size n − 1. This is a direct consequence of the recursive specification of the
solution. We can solve Equation 3.1 by telescoping, i.e.,
T (n) = T (n − 1) + 1 (3.2)
= T (n − 2) + 2
..
.
= T (0) + n
= n
Thus n is the number of multiplications required to compute f actorial(n) and this is the
time complexity of the problem.
To estimate the space complexity, we have to estimate the number of deferred operations
which is about the same as the number of times the function f actorial is invoked.
Exercise 3.2 Show, in a similar way, that the number of function calls required to evaluate
f actorial(n) is n + 1.
Equation 3.1 is called a recurrence equation and we will use such equations to analyze the
time complexities of various algorithms in these notes. Note that the measure of space and
time given above are independent of how fast a computing machine is. Rather, it is given
in terms of the amount of space required and the number of multiplications and function
calls that are required. The measures are thus independent of any computing machine.
Suppose you have implemented this algorithm on your laptop to run in 10−4 × 2n seconds
when confronted with any n × n matrix (it will actually be worse than this!). You can solve
an instance of size 10 in 10−4 × 210 seconds, i.e., about a tenth of a second. If you double
the problem size, you need about a thousand times as long, or, nearly 2 minutes. Not too
bad. But to solve an instance of size 30 (not at all an unreasonable size in practice), you
require a thousand times as long again, i.e. even running your laptop the whole day isn’t
sufficient (the battery would run out long before that!). Looking at it another way, if you
ran your algorithm on your laptop for a whole year (!) without interruption, you would still
only be able to compute the detrminant of a 38 × 38 matrix!
Well, let’s buy new hardware! Let’s go for a machine that’s a hundred times as fast –
now this is getting almost into supercomputing range and will cost you quite a fortune!
What does it buy you in computing power? The same algorithm now solves the problem
in 10−6 × 2n seconds. If you run it for a whole year non–stop (let’s not even think of
the electricity bill!), you can’t even compute a 45 × 45 determinant! In practice, we will
routinely encounter much larger matrices. What a waste!
Exercise 3.3 In general, show that if you were previously able to compute n × n determi-
nants in some given time (say a year) on your laptop, the fancy new supercomputer will
only solve instances of size n + log 100 or about n + 7 in the same time.
Suppose that you’ve taken this course and invest in algorithms instead. You discover the
method of Gaussian elimination (we will study it later in these notes) which, let us assume,
can compute a n × n determinant in time 10−2 n3 on your laptop. To compute a 10 × 10
determinant now takes 10 seconds, and a 20 × 20 determinant now requires between one
and two minutes. But patience! It begins to pay off later: a 30 × 30 determinant takes only
four and a half minutes and in a day you can handle 200 × 200 determinants. In a years’s
computation, you can do monster 1500 × 1500 determinants.
respectively. We see, that according to our definition of order of growth, each of these are
O(n). Thus, we can say that the space complexity and the time complexity of the algorithm
are both O(n). In the example of determinant computation, regardless of the particular
machine and the corresponding constants, the algorithm based on Gaussian elimination has
time complexity O(n3 ).
Order of growth is only a crude measure of the resources required. A process which
requires n steps and another which requires 1000n steps have both the same order of growth
O(n). However, On the other hand, the O(·) notation has the following advantages:
• It gives fairly precise indication of how the algorithm scales as we increase the size of
the input. For example, if an algorithm has an order of growth O(n), then doubling
the size of the input will very nearly double the amount of resources required, whereas
with a O(n2 ) algorithm will square the amount of resources required.
• It tells us which of two competing algorithm will win out eventually in the long
run: for example, however large the constant K may be, it is always possible to find
a break point above which Kn will always be smaller than n2 or 2n giving us an
indication of when an algorithm with the former complexity will start working better
than algorithms with the latter complexities.
• Finally the very fact that it is a crude analysis means that it is frequently much easier
to perform than an exact analysis! And we get all the advantages listed above.
In what follows we will give examples of algorithms which have different orders of growth.
Exercise 3.4 What does O(1) mean? Are O(1) and O(n) different?
In Figure 3.1 we show the relative scaling of some order functions with respect to n. In
Figure 3.2 we plot the O(n2 ) and the O(2n ) curves with an increased y-axis range. Clearly
any algorithm with a time complexity of O(2n ) is computationally infeasible. In order to
solve a problem of size 100 roughly 2100 ≈ 1030 steps will be required.
Exercise 3.5 Assuming that a single step may be executed in, say, 10−9 seconds, obtain
a rough estimate to solve a problem of size 100 using an algorithm with a time complexity
of O(2n ).
100
90 O(n)
O(log n)
80 O(n*log n)
70 O(n^2)
O(2^n)
60
50
40
30
20
10
10 20 30 40 50 60 70 80 90 100
n
Figure 3.1: A comparison of various orders of growth.
We seek a function of the type power : P × N → N. Let us develop this algorithm using
PMI- version 1 according to Example 2.2. Clearly, the base case specification can be
given as power(x, n) = 1 if n = 0. If we assume, as the induction hypothesis, that we
can compute power(x, n − 1) = xn−1 for an n ≥ 1, then the induction step to compute
power(x, n) = xn would be x ∗ power(x, n − 1). Thus, an obvious algorithmic specification
for this problem is
½
1 if n = 0
power(x, n) =
x ∗ power(x, n − 1) otherwise
The correctness of the algorithm can be established by the PMI. See Example 2.2.
Exercise 3.6 Show that the space and time complexities of the above algorithm are both
O(n).
10000
9000 O(n^2)
O(2^n)
8000 O(n*log n)
7000
6000
5000
4000
3000
2000
1000
0
10 20 30 40 50 60 70 80 90 100
n
Figure 3.2: A comparison of O(n2 ), O(n logn) and O(2n ).
Correctness
To show that: f ast power(x, n) = xn for all x ∈ P, n ∈ N.
Proof: By induction on n using PMI – version 3.
Efficiency
To see that the successive squaring method is more efficient than our previous method,
let us compute the number of multiplications required by the method of recurrence. For
simplicity, we assume that n is a power of 2 (n = 2m ). The recurrence is given by
½
1 if n = 1
T (n) =
T (n/2) + 1 for n > 1
T (n) = T (2m−1) + 1
= T (2m−2) + 2
..
.
= T (20) + m
= m+1
= log2 n + 1
Thus, instead of O(n) multiplications, the new algorithm requires only O(log2 n) multi-
plications. (we will write this as O(lg n)) multiplications. To see how significant this
improvement is, we compare n and lg n in the following table.
3.7. MORE EXAMPLES OF RECURSIVE ALGORITHMS 35
n 2 4 8 16 32 64 ...
lg n 1 2 3 4 5 6 ...
1, 1, 2, 3, 5, 8, 13, . . .
Each number beyond the first two is derived from the sum of its two nearest predecessors.
We can give a straightforward functional description for computing the nth Fibonacci
number. It is a function of the type f ib : P → P
1 if n = 1
f ib(n) = 1 if n = 2
f ib(n − 1) + f ib(n − 2) otherwise
The correctness of the algorithm is obvious from the inductive definition. We can write an
ML function for the above as
hFibonaccii≡
fun fib (n) =
if (n=0) orelse (n=1) then 1
else fib (n-1) + fib (n-2);
3
Note that the most recent version of ML (version 110.0.3) assumes by default that all arithmetic variables
and operations like +, \* are integer operations unless specified explicitly as real
36 CHAPTER 3. A FUNCTIONAL MODEL OF COMPUTATION
fib(5)
fib(4) fib(3)
fib(2) fib(1)
Exercise 3.8 Show that the number of times f ib(1) or f ib(2) will have to be computed by
the above algorithm while computing f ib(n) is equal to f ib(n) itself.
n n
√ √
Exercise 3.9 Verify,
√ by induction, that f ib(n) = (φ 4− ψ )/ 5, where φ = (1 + 5)/2 =
1.618 and ψ = (1 − 5)/2. φ is called the golden ratio
From the above exercises it is obvious that the time complexity of the above algorithm is
clearly O(φn ). Thus, the number of steps required to compute f ib(n) grows exponentially
with n, and the computation is intractable for large n. φ100 is of the order of 1020 , and,
consequently, the evaluation of f ib(n) using the above algorithm will require of the order
of 1020 function calls. This is a very large number indeed, and may take several years
of computation even on the fastest of computers. In the Section 3.9 we will see how the
computation of f ib(n) can be speeded up by designing an iterative process.
4
Many of the ancient Greek monuments (including the Parthenon) had an elevation where the ratio of
the base of the monument to its height was a close approximation of φ. It was considered the most majestic
proportion for temples. Can you give a ruler and compass construction of the golden ratio?
3.7. MORE EXAMPLES OF RECURSIVE ALGORITHMS 37
Example 3.11 Counting the number of primes between integers a and b (both inclusive).
We will assume the availability of a function prime(n) which returns true if n is a prime
and returns false otherwise. The function we are seeking is of the type count primes :
N × N → N. We can give an inductive definition of this function as
0 if a > b
count primes(a, b) = count primes(a, b − 1) + 1 if prime(b)
count primes(a, b − 1) otherwise
Correctness
To show that: The function count primes(a, b) returns the count of the number of primes
between a and b assuming the function prime(n) to be correct.
Proof: By PMI – Version 2 on (b − a + 1).
Induction hypothesis. count primes(a, b − 1) returns the count of the number of primes
between a and b − 1 for a, b such that (b − a + 1) ≥ 0.
Induction step. If b is a prime then count primes(a, b) returns count primes(a, b−1)+1.
Otherwise, it returns count primes(a, b − 1).
Exercise 3.10 Show that the number of additions required and number of function calls to
prime(n) required are both O(n) where n = b − a. Note that it is not possible to determine
the time and space complexities of this algorithm without the knowledge of the complexities
of the function prime(n).
Pb
Example 3.12 Computing n=a f (n).
38 CHAPTER 3. A FUNCTIONAL MODEL OF COMPUTATION
We will assume that the function f (n) is available. We can then define the function
sum : N × N → N, inductively, as
½
0 if a > b
sum(a, b) =
f (b) + sum(a, b − 1) otherwise
2. Show that both the time and the space complexities of the algorithm are O(n) where
n = b − a. Assume that the function f (n) can be computed using O(1) time and
space.
where the function addf actors : P → N computes the sum of the proper factors of n. We
can define add-factors as
Note that the n used in the definition of f (i) is the same as in the function perf ect?.
5
The smallest perfect numbers 6 and 28 were known to the Hindus as well as the Hebrews. Some
commentators of the bible regard 6 and 28 as the basic numbers of the Supreme Architect. They point to
the 6 days of creation and the 28 days of the lunar cycle. Others go so far as to explain the imperfection of
the second creation by the fact that eight souls, not six, were rescued in Noah’s ark. Said St. Augustine:
“Six is a number perfect in itself, and not because God created all things in six days; rather the converse is
true; God created all things in six days because this number is perfect, and it would have been perfect even
if the work of six days did not exist.”
40 CHAPTER 3. A FUNCTIONAL MODEL OF COMPUTATION
Exercise 3.13 Using the property that if i is a divisor of n then (n div i) is also a divisor
of n, give an improved version of the above algorithm and thus improve the complexity from
√
O(n) to O( n). What happens if n is a perfect square? Write a ML program to implement
your improved algorithm.
The above is a typical example of program development through top down design and
step-wise refinement. We strongly recommend this method of program development and
will adhere to this method for most examples in these notes.
It is instructive to note the nesting of the various ML functions declared above. The
function add-factors is local to the function perfect. Hence it cannot be directly accessed
from the level from which perfect can be invoked. The accessibility of various variables
and functions from different parts of the ML code is guided by the Scope rules in functional
programming. In what follows in the next section we formalize the notion.
It contains as free the names z, f and g. The other names x, u and y are bound. The scopes
of the bound variables are shown below.
Z z Z y Z y
f (x)dx + g(u)du dy
0 0 0
| {z } | {z }
x u
| {z }
y
42 CHAPTER 3. A FUNCTIONAL MODEL OF COMPUTATION
where the two uses of x in the two different integrals are meant to denote different variables.
Further we may note that though y is a bound variable of the complete expression, when
we consider only the sub-expressions
Z y Z y
f (x)dx and f (x)dx
0 0
Example 3.16 Now consider the complete ML code of Example 3.13 (perfect numbers).
• The name perfect is bound and has a scope which extends beyond the definition.
This implies that in some later program in the same file or ML session one could use
this name to mean exactly what we have defined it to be.
• The name add-factors is bound and has a scope which begins with its definition and
extends right up to the end of the definition of perfect (n) but no further. Hence if
after defining perfect (n) as given one types in, say, add-factors (12) in the same
session then one would get an error. This is because add-factors has no meaning
outside the scope of the definition of perfect (n).
• Similarly the name f is bound and has a scope that extends up to the end of the
definition of add-factors and no further. The name sum also has a scope similar to
that of f.
• The variables a and b are bound and have scopes beginning at their first occurrence
in the definition of sum and ending with the same definition.
• Similarly i in (define (f i) ..) has a scope that extends over the definition of f
and no further.
• The name n in the definition of the function f has a scope that begins with its first
occurrence in the definition of the function add-factors (n) and extends only up to
the end of this definition of add-factors and no further. Thus within the scope of
the function f the variable n is free. The variable n in the definition perfect (n) has
a scope that extends up to the end of that definition. It is important to note that
the variable n in the definition perfect (n) and the variable n in the definition of
add-factors are actually different. We could, for example, replace all occurrences of
n in the scope of add-factors with m without affecting the program in any way.
3.9. TAIL-RECURSION AND ITERATIVE PROCESSES 43
• There are a few other names used in the program like div and mod. At the initiation of
the ML session these functions are automatically loaded by the ML interactive system
and therefore they occur as bound names whose scopes extend right up to the end
of the session. It is however possible to create a large “hole” in the scopes of these
definitions by writing our own definition of div and mod
f act iter(m, f, c)
½
f if c = m
=
f act iter(m, f ∗ (c + 1), c + 1) otherwise
Note that the invariant condition (which is a boolean function of the state of the system
described in terms of the variables f and c) holds true every time the function f act iter is
invoked. We can write an ML program for this iterative version as follows
hIterative factorial i≡
fun factorial (n) =
let hCode for fact iteri
in fact_iter (n, 1, 0)
end;
The function description of f act iter is called tail-recursive because the “otherwise”
clause in its description is a simple recursive call to the function itself. Contrast this with
the “otherwise” clause of the recursive factorial (described in Example 3.8) which is given
as n ∗ f actorial(n − 1) and involves the recursive call with the multiplication operation. A
tail-recursive definition such as this leads to a computational process different from that of
the recursive version for the same problem. The underlying computational process for the
special case of f actorial(5) looks as follows
f actorial(5)
= f act iter(5, 1, 0)
= f act iter(5, 1, 1)
= f act iter(5, 2, 2)
= f act iter(5, 6, 3)
= f act iter(5, 24, 4)
= f act iter(5, 120, 5)
= 120
Contrast this with the recursive process for computing f actorial(n) in Example 3.8. The
recursive process is characterized by a growing and shrinking due to deferred computations,
where, in the growing process, the multiplicative constants 5,4,3,2 and 1 are stacked up
before the results of f actorial(0), f actorial(1), f actorial(2), f actorial(3) and f actorial(4)
become available. In the shrinking process the actual multiplications n ∗ f actorial(n − 1)
are carried out to obtain f actorial(n) successively. In contrast, there is no growing process
in the iterative version. The results of the successive stages are captured in the value of f
where the stage itself is indicated by the value of c. The values of these two variables, at
any instant, give the state of the computation.
The time complexity of the iterative algorithm is clearly O(n) which is same as that of the
recursive one, whereas the space complexity in this case reduces to O(1). This is because,
at any stage, the instantaneous values of only three variables are required to be stored.
Basis. (m − c) = 0 or (m = c).
m
Y
f act iter(m, f, c) = f = f ∗ i=f ∗1
i=c+1
2
Then we can prove the correctness of the function f actorial(n) as follows:
Proof:
n
Y
f actorial(n) = f act iter(n, 1, 0) = 1 ∗ i = n!
i=1
2
On the other hand, the invariant condition
c
Y m
Y m
Y
(c0 ≤ c ≤ m) ∧ (f = f0 ∗ i) ∧ (f0 ∗ =f∗ i)
i=c0 +1 i=c0 +1 i=c+1
encodes the above proof of correctness through a description of state changes. At the initial
stage, when c = c0 , the invariant condition gives us thatQf = f0 . At the final stage when
c = m, the invariant condition gives us that f = f0 ∗ m i=c0 +1 i which is the final value
that the function returns. According to the initial invocation of f act iter from the function
f actorial,
Qm the initial values are f0 = 1, c0 = 0 and m = n. Thus the final value of f is
f = i=1 i = n!.
Since iterative algorithms are described through state changes, for correct design of an
iterative algorithm, it is helpful to first design the invariant condition such that the desired
result can be obtained from the final state of the variables. The invariant condition can
then act as a specification for the design of the algorithm. In what follows, we give some
more examples of iterative processes.
As before, we assume that the function f (n) is available. We can describe the iterative
process in terms of the auxiliary variables s and c. WePcan initialize the process with c = c0
and s = s0 = 0, keep computing the partial sum s = c−1 i=c0 f (i), and continue the iterative
process till c reaches the final value cf + 1 . An invariant capturing the above idea can be
written as
c−1 cf cf
X X X
IN V = (c0 ≤ c ≤ cf + 1) ∧ (s = f (i)) ∧ (s + f (i) = f (i))
i=c0 i=c i=c0
P
In order to compute ba f (n) using the computational process described by the above
invariant, we have to initialize the process with c0 = a, cf = b and s = 0. We can describe
the iterative algorithm for sum : N × N → N as
sum iter(c, cf , s)
½
s if c = cf + 1
=
sum iter(c + 1, cf , s + f (c)) otherwise
1. Establish the correctness, independently, using both PMI and the invariant property.
2. Estimate the time and space complexities assuming that f (n) can be computed in
O(1) time using O(1) space.
Note that in the above code f occurs free in the definition. Hence it is necessary to have
already the function f previously in the ML session before using these definitions.
Induction hypothesis. For all b ≤ k such that 0 ≤ b, for all a > 0, Euclid gcd(a, b) =
gcd(a, b).
2
If a0 and b0 are the initial values of a and b respectively, an invariant condition for the
above is
IN V = (gcd(a, b) = gcd(a0 , b0 )) ∧ (a > 0) ∧ (b ≥ 0)
Exercise 3.15 Verify that the invariant condition is satisfied both at the initial and the
final stage of the algorithm.
6
So called because it appears in Euclid’s Elements. This book was written around 300 B.C. According
to Knuth (1969) it may be considered the oldest known non-trivial algorithm. The Egyptian method of
multiplication (Problem 5) is definitely older, but, as Knuth explains, Euclid’s algorithm is the oldest known
to have been presented as a general algorithm, rather than as a set of illustrative examples.
3.10. MORE EXAMPLES OF ITERATIVE PROCESSES 49
Efficiency
To analyze the efficiency of the Euclid’s algorithm for gcd we need the following result.
Lamé’s Theorem: If Euclid’s algorithm requires k steps to compute the gcd of some pair,
then the smaller number in the pair must be greater than or equal to the k th Fibonacci
number.
We can use this theorem to analyze the time complexity of the Euclid’s algorithm. Let n
be the smaller of the two inputs to the function. If the process takes k steps, then we must
have n ≥ f ib(k) ≈ φk . Thus, the number of steps k is O(log n).
The ML function that implements the algorithm can be written as
hgcd i≡
fun Euclid_gcd (a, b) =
if b=0 then a
else Euclid_gcd (b, a mod b);
50 CHAPTER 3. A FUNCTIONAL MODEL OF COMPUTATION
Then, when count = n, the process may terminate and we may obtain the value a + b =
f ib(count − 2) + f ib(count − 1) = f ib(n) as the final answer. An algorithm based on this
invariant condition can be described as
1 if n = 1
f ib(n) = 1 if n = 2
f ib iter(n, 1, 1, 3) otherwise
The function f ib iter(n, a, b, count) is invoked only if n ≥ 3, and every time this function
is invoked, the invariant condition holds. The above process obviously requires only n − 2
additions to compute f ib(n) for n ≥ 3. Thus, the iterative algorithm requires O(n) time
and O(1) space for computing the nth Fibonacci number. This is a significant improvement
over the purely recursive version we considered earlier.
Problems
For each of the problems given below, identify the types of the functions you need to define,
establish their correctness using PMI and invariants (if the algorithm is iterative) and
determine the space and time complexities. Finally, translate your functional algorithms
into ML programs and execute them.
(a) Finding the number of digits (in base 10) in a given positive integer assuming
there are no leading zeroes.
(b) Reversing the digits of a positive integer in base 10.
2. Suppose we rewrite the fast powering algorithm described in Example 3.9 as follows
1 if n = 0
power(x, n) = x ∗ power(x, (n div 2)) ∗ power(x, (n div 2)) if odd(n)
power(x, (n div 2)) ∗ power(x, (n div 2)) otherwise
4. Design an iterative process that uses successive squaring to compute xn and works in
O(lg n) time.
6. If a ≥ 0 and b > 0 are two integers, then there exists q ≥ 0 and 0 ≤ r < b such that
a = q∗b+r. The div and mod functions are defined as div(a, b) = q and mod(a, b) = r.
Develop iterative algorithms for div and mod using addition and subtraction.
7. Amicable numbers are pairs of numbers each of whose proper divisors add up to
the other (1 is included as a divisor but the numbers are not included as their own
7
This algorithm which is sometimes known as the ‘Russian peasant method” of multiplication, is very
old. Examples of its use are found in the Rhind Papyrus, one of the two oldest mathematical documents in
existence, written about 1700 B.C. (and copied from an even older document) by an Egyptian scribe named
A’h-mose.
52 CHAPTER 3. A FUNCTIONAL MODEL OF COMPUTATION
divisors). The smallest pair of amicable numbers are 220 and 284 8 . Develop a
functional algorithm to determine whether a given pair of numbers are amicable or
not.
9. Develop an algorithm to compute the sum of the first n Fibonacci numbers. The
algorithm should work in O(n) time and O(1) space.
10. Given that tn is the nth term in the expansion of sin(x), write a function to determine
the n + 1th term.
11. Using your solution for the above, write a function to evaluate the value of sin(x) up
to n terms.
12. Suppose you have an infinite supply of coins of denomination 50p, 25p, 10p, 5p and 1p.
In how many ways can you generate change for a given amount, say Rs. 1/- = 100p.
Given that a function d(n) is available which gives the denomination of the nth type
of coin, develop a recursive algorithm to count the number of ways to generate change
for a given amount. What can you say about the number of steps required for the
computation?
13. Write an iterative function for the coin exchange problem. Show that a suitably
defined iterative function will work faster for the given problem.
8
Down through their quaint history, amicable numbers have been important in magic and astrology, and
in casting of horoscopes, making talismans, and concocting love potions. The philosopher Iamblichus of
Chalcis (A.D 250 - A.D 330) ascribed a knowledge of the pair 220 and 284 to the Pythagoreans. He wrote:
“They [the Pythagoreans] call certain numbers amicable numbers, adopting virtues and social qualities to
numbers, as 220 and 284; for the parts of each have the power to generate the other”. See Elementary
number theory by D. M. Burton for details.
Chapter 4
In the last chapter we studied the basics of a functional model of computation. Though
the functional model is attractive from the point of view of ease of algorithm design and
correctness analysis, imperative models of computation are more commonly used in practice
mainly because of reasons of efficiency. In particular, imperative programming languages
like Fortran and C have been thoroughly optimized through years of research, and pro-
grams compiled in these languages, in general, work faster. Hence, in this chapter, we will
introduce the basics of an imperative model of computation. We will use the programming
language Java as a representative language for writing programs for imperative algorithms.
In the next chapter we will relate the two models of computation and show how an initial
functional design of an algorithm helps in the development of an imperative algorithm using
step-wise refinement.
In the imperative model of computation an algorithm is a specification of what to do
in order to solve a given problem in terms of a sequence of instructions which have to be
executed in the given order. In what follows we describe the primitives for the imperative
model.
53
54 CHAPTER 4. THE IMPERATIVE MODEL OF COMPUTATION
int a,b;
creates and reserves two locations (or boxes) with the names a and b in which integer values
can be stored.
a b
Initially, immediately after the declaration, the contents of the boxes with names a and
b are undefined. The following assignment instruction stores the integer 2 in the location
named a
a = 2;
The above is read as “a assigned 2”. The content of the location b is still undefined and the
state of the variables is
a 2 b
b = a;
a 2 b 2
Note that the content of a gets copied to b, and a is unchanged. A further assignment
instruction
4.1. THE PRIMITIVES FOR THE IMPERATIVE MODEL 55
a = a + b;
a 4 b 2
The contents of a gets replaced by the sum of the present contents of a and b, whereas the
contents of b remains unchanged.
On the left of the = operator must be a single variable whose state is being updated, and
on the right must be an expression comprising operators, variables and values. The right
side must evaluate to the same type as the variable specified in the left.
As a result of this, in the imperative style, state changes can be performed only one at
a time. Consequently, simultaneous changes in the values of several variables have to be
performed in some orderly fashion, one-at-a time, so as to ensure that the desired final state
is obtained through several one-step changes.
The state of the computation at any instant is a snapshot of the contents of all the
variables used in the algorithm.
As an example of an imperative style algorithm using the assignment instruction, let us
consider the following problem of swapping the contents of two variables.
The ‘pre-condition’ and the ‘post-condition’ are logical properties of the initial and the
desired final states of the computation respectively. Together they form the specification for
the algorithm. Thus the objective of the algorithm is to change the state of the computation,
through a sequence of steps, so that finally the post-condition is true given that the pre-
condition is true to start with. We can achieve the transformation by using a variable
temp for temporary storage. We can first copy the contents of a to temp, then replace the
contents of a with the contents of b, and finally replace the content of b with that of temp.
We describe the complete imperative algorithm as
In the above example, the three assignment instructions have to be executed in the given
order to achieve the exchange. An algorithm in the imperative model is a sequence of
instructions which have to be executed in a step by step manner to carry out a desired
computation. The instructions are separated by the symbol ‘;’. The sequence of three
instructions may be regarded as a single compound instruction. To enable us to regard
certain sequences of instructions as a single instruction we use a bracketing mechanism,
where the left bracket is the symbol { and the closing bracket is the symbol }. Hence the
above algorithm could be rewritten using brackets as follows
/* assert A : (a = a0) ∧ (b = b0) */
{
temp = a;
a = b;
b = temp;
}
/* assert B : (a = b0) ∧ (b = a0) */
As we will see the {...} brackets are very useful to avoid possible confusion.
4.1.2 Assertions
The reader may have noticed that in the algorithm stated above there are statements labeled
“assert” about the state of the computation. These are not instructions to be executed but
are essential documentation necessary for correct design of imperative algorithms. These
are true statements about the state of a computation. Such statements are called assertions
or logical propositions. Throughout these notes we will make such assertions about the state
of a computation in the imperative style description of algorithms. The pre-condition and
the post-condition are assertions about the initial and the final states of the algorithm
respectively. Very often they do not completely describe the state of each variable in the
computation, but instead give us an abstract property of the state which should be true.
The invariant properties described in the last chapter are also examples of such assertions.
In special cases an assertion may completely describe the state as in the algorithm below
in which between any two instructions there is an assertion.
/* assert A0 : (a = a0 ) ∧ (b = b0 ) */
{
temp = a;
/* assert A1 : (a = a0 = temp) ∧ (b = b0 ) */
a = b;
/* assert A2 : (a0 = temp) ∧ (a = b = b0 ) */
b = temp;
}
/* assert A3 : (a0 = temp = b) ∧ (a = b0 ) */
4.1. THE PRIMITIVES FOR THE IMPERATIVE MODEL 57
The assertions A1 , A2 and A3 completely describe the state of the computation at those
points. In contrast, the post-condition, /* assert B: (a = b0 ) ∧ (b = a0 ) */, used earlier,
does not completely describe the state and it is merely a true statement about the final
state. It is obvious that from A3 one may deduce B and hence if A3 is true then so is B.
In general, a complete description of the state may not be interesting, relevant or even
known to us at any stage of a computation. Hence assertions, which are true statements
about the state, are used to capture the essential properties of the state. Throughout these
notes we will use suitable assertions as algorithm/program documentations wherever there
is a scope of ambiguity and definitely as the input/output specifications for the various
modules which we will design. This will enable us to develop our algorithms in a modu-
lar fashion with the input-output specifications of the individual modules serving as the
interfaces between the different modules.
Exercise 4.1 Which of the following algorithms achieve the same result? Which of them
achieves the same result as the swap algorithm defined above?
1. (a = b0 ) ∧ (b = c0 ) ∧ (c = a0 )
2. (a = c0 ) ∧ (b = a0 ) ∧ (c = b0 )
Exercise 4.3 Given the pre-condition (a = a0 ) ∧ (b = b0 ) where a and b are integers, indi-
cate the state changes (using suitable assertions) that take place in the following algorithm.
What is the final state?
{
a = a + b;
b = a - b;
a = a - b;
}
In what follows we introduce the if then else and the while do instructions which provide
the basic mechanisms for the flow of control in an imperative style algorithm.
58 CHAPTER 4. THE IMPERATIVE MODEL OF COMPUTATION
/* assert A : . . . */
if (C)
/* assert A ∧ C */
s1
else
/* assert A ∧ ¬C */
s2
Note that if C is true in the state before the if then else instruction is performed then we
may assert that both A and C are true. However if C is false, then we assert that both A
and ¬C are true (as given after the “else”).
A special case of the if then else is the if then instruction
if (C)
s1
In this case s1 is executed if the condition C is true, else nothing is done. The following
two examples illustrate the use of such an instruction.
Example 4.2 Swap the values of variables a and b (which are of the same type) if a > b.
Let temp be a variable of the same type as a and b. The algorithm can be given as
/* assert A ∧ (a > b) */
{
temp = a;
a = b;
b = temp;
{
/* assert B : ((a0 <= b0) ∧ (a = a0) ∧ (b = b0)) ∨ ((a = b0) ∧ (b = a0)) */
Note the use of {...} brackets to ensure that the entire sequence is to be regarded as a single
(compound) instruction if the condition a > b is true. On the other hand if the brackets
were not used then the instructions a= b and b = temp are executed even if a ≤ b and the
instruction temp = a is executed only when a > b.
Write assertions after each step of the above sequence of instructions starting from the
pre-condition.
Example 4.3 The following if then instruction ensures that x is always non-negative.
/* assert A : (x < 0) ∨ (x ≥ 0) */
if (x < 0)
/* assert A ∧ (x < 0) */
x = -x;
/* assert B : (x ≥ 0) */
In what follows we give a few more examples of case analysis using the if then else.
Example 4.4 Finding the roots of a quadratic equation of the form ax2 + bx + c = 0.
Here a, b and c are real valued coefficients. We assume that a 6= 0. The variables r1 and i1
should contain the real and the imaginary parts of the first root, and the variables r2 and
i2 should contain the real and the imaginary parts of the second root.
We describe the algorithm, in terms of the variables a, b, c, d and e of the type real as
follows
/* assert A ∧ (d ≥ 0) */
{
e = sqrt(d); r1 = (-b+e)/(2*a); r2 = (-b-e)/(2*a);
i1 = 0; i2 = 0;
}
else
/* assert A ∧ (d < 0) */
{
e = sqrt(-d); r1 = -b/(2*a); r2 = r1;
i1 = e/(2*a); i2 = -e/(2*a);
}
2
V
/* assert B : [(a = a0) ∧ (b = b0) ∧ (c =
√ c0) ∧ (d = (b − 4ac)]
[ ((d ≥ 0) ∧ (r1√= (−b + d)/2a)∧
W
(r2 = (−b − d)/2a) ∧ (c1 = c2 = 0))p
((d < 0) ∧
p(r1 = r2 = −b/2a) ∧ (c1 = | d |/2a)∧
(c2 = − | d |/2a))
]
*/
Example 4.5 Determining whether a given month and day represent a valid date.
Given that m and d are integer type variables and the pre-condition (m = m0) ∧ (d = d0),
establish whether m and d together give a valid day of an year (m gives the month and d
gives the day). Set an integer variable valid to 1 if they represent a valid day and set it to
0 otherwise.
We describe the algorithm as follows.
if (d < 31)
/* assert: m ∈ {4, 6, 9, 11} ∧ (1 ≤ d ≤ 30) */
valid = 1;
else
/* assert: m ∈ {4, 6, 9, 11} ∧ (d = 31) */
valid = 0;
else
/* assert: (m = 2) ∧ (1 ≤ d ≤ 31)}
if (d < 30)
/* assert: (m = 2) ∧ (1 ≤ d ≤ 29) */
valid = 1;
else
/* assert: (m = 2) ∧ (30 ≤ d ≤ 31) */
valid = 0; W
/* assert: [(valid = 1) ∧ B] [(valid = 0) ∧ ¬B] */
where,
B = ((m ∈ {1, 3, 5, 7, 8, 10, 12}) ∧ (1 ≤ d ≤ 31)) ∨
((m ∈ {4, 6, 9, 11}) ∧ (1 ≤ d ≤ 30)) ∨
((m = 2) ∧ (1 ≤ d ≤ 29))
Exercise 4.5 Given that y, m and d are integer type variables and the pre-condition
(y = y0) ∧ (m = m0) ∧ (d = d0), establish whether y, m and d together give a valid date.
Set an integer variable valid to 1 if they represent a valid date and set it to 0 otherwise.
In our examples using the if then else instruction, we have made assertions at every stage
of the decision making. However, in future, for the sake of brevity, we will make suitable
assertions only at places where there is some scope of ambiguity.
f = 1;
c = 0;
if (c <> n) then
begin
f = f * (c+1);
c = c + 1;
end
Thus we will have to put at least n such if then else instructions in a sequence. Since n
is not known in general, we require a finite and compact representation of such repetitive
operations. The primary iterative construct in this model which does this is the while do.
The instruction
while (C)
s1
repeats s1 while the boolean condition C is true. Thus the instruction s1 is executed if the
condition C is true to start with. After each execution of the instruction s1 the condition
C is evaluated again to determine whether it is true or not. The process is repeated if C
is true; otherwise the while do instruction is terminated. As in the case of if then else, s1
may either be a simple instruction or a compound instruction. Each of these instructions,
in turn, can be one of the three types - assignment, if then else or while do.
Since the purpose of the while do instruction is to represent iterative processes, we must
associate an invariant condition with every while do loop. This invariant condition must
hold true every time the condition C is evaluated. We may rewrite the while do instruction
with its associated assertions as
/* assert: I */
while (C)
/* assert I ∧ C */
{
s1;
/* assert I */
}
/* assert: B = I ∧ ¬C */
Thus, the invariant assertion, I, must be true the first time the while do instruction is
encountered. This has to be ensured though a proper initialization process. If the condition
C is true, then the assertion I ∧ C must be true before the state changes proposed in s1
are carried out every time during the iterative process. The invariant condition I along
with the condition C may thus be looked upon as the specification for the design of while
4.1. THE PRIMITIVES FOR THE IMPERATIVE MODEL 63
do instruction. Finally, when the loop is terminated, the condition I ∧ ¬C gives the final
desired state.
To see an example of algorithm design using the while do instruction, let us consider an
iterative version of the factorial computation in the imperative style.
we can then translate the above algorithm in the imperative style using the while do
instruction as
/* assert A : n ≥ 0}
count = 0;
fact = 1;
/* assert I : (0 ≤ count ≤ n) ∧ (f act = count !) */
while (count != n)
/* assert I ∧ (count 6= n) */
{
count = count + 1;
fact = fact * count;
/* assert I */
}
/* assert I ∧ (count = n) */
/* assert B : f act = n! */
An imperative style function for the factorial computation using the recursive algorithm
of Example 3.8 can be written using Java syntax as
hFactorial (recursive)i≡
int factorial(int n)
/* assert: (n >= 0) */
{
if (n = 0)
return 1;
else
return (n * factorial(n-1));
};
4.1. THE PRIMITIVES FOR THE IMPERATIVE MODEL 65
The function declaration int factorial(int n) indicates that the function factorial
can be invoked with an integer input and that it returns an integer value.
Note, however, that the imperative style function defined above is fundamentally different
from its corresponding functional definition given in the last chapter, though they use the
same algorithm. Here n is an imperative style variable which stores a value. An invocation
of the function from the calling environment with
a = factorial(5)
assigns the value 5 to n. The function returns an integer value through its name factorial.
Alternatively we can write an imperative style function using the iterative algorithm of
Example 3.17 as
hFactorial (iterative)i≡
int factorial(int n)
/* assert: (n >= 0) */
int f,c;
{
f = 1; c = 0;
{INV: (0 <= c <= n) and (f = c!) and (n! = f * (c+1)*(c+2)...*n)}
while (c != n) {
c := c + 1;
f := f * c;
}
/* assert: (f = n!) */
return f;
}
This function can also be invoked in the same way. This function uses its own local
variables f and c which are not visible from the calling environment.
In some cases it may not be necessary to return a value through the function name.
Rather we may wish to define a generic algorithm which effects some state changes. In such
cases we may use a procedure.
A procedure for swapping the values of two global variables a and b can be defined as
follows
hSwapi≡
void swap();
int t;
{
/* assert: (a = a0) and (b = b0) */
t := a;
a := b;
b := t;
/* assert: (a = b0) and (b = a0) */
}
Note that the procedure swap defined above does not have any formal input passed from
the calling program as an argument. Neither does it return any output. It merely affects
some state changes of the globally defined variables a and b
m := 3; n := 4;
swap();
/* assert: (m = 4) and (n = 3) */
The input parameters to a procedure or a function are specified within brackets, along
with their types, in the declaration. These parameters are called the formal parameters
of a procedure or a function. For example, if a function or procedure is invoked from the
calling environment with an instruction like a = factorial(b), the formal parameter n
is initialized by copying the value of the variable b. The effect is similar to that of an
assignment like n = b. Thus, in this case, n and b are two different variables, and even if
n is modified in the function the variable b in the calling environment remains unaffected.
The variable n is local to the function and is not accessible from the point of invocation in
the calling environment.
In what follows we describe a complete Java program for computing f actorial(n) using
the iterative method.
A complete Java program for computing factorial using the iterative method is given as
follows:
hComplete programi≡
import java.io.*;
import javagently.*;
In the above code we define a Java class for computing factorial. The declaration
public implies that this code can be accessed by anybody. The code imports two packages
called java.io and javagently to facilitate input and output. Text.open and Text.readInt
are methods or functions defined in the public class Text in the javagently package. The
instruction BufferedReader in = Text.open(System.in); sets up the Java system for
input from the keyboard. System.out.println is a method for generating output on the
screen. It prints the input string. In this case the input string is a concatenation of two
strings "f = " and the string corresponding to the inetger f which is automatically gener-
ated by the concatenation operator +.
We will study more about Java classes and methods later in these notes while discussing
Object oriented programming.
Problems
1. Develop Java programs/functions for each of the problems in the last Chapter.
(a) Given three points (x1 , y1 ), (x2 , y2 ), (x3 , y3 ) to determine whether they are collinear,
and if so which point lies between which two.
(b) Given the coordinates of the centers of two circles and their radii, find whether
they intersect, and if so find the points of their intersection.
(c) Given the coordinates of four points on a plane, determine whether they form
a quadrilateral, and if so classify it if possible as a square, rectangle, rhombus,
parallelogram etc.
4. Develop a Java function which reads n integers from the input and returns the max-
imum.
5. Given the coordinates (x0 , y0 ) of the center of a circle and a point (x1 , y1 ) lying on its
circumference, develop a Java program that outputs the coordinates (x1 , y1 ), . . . , (x6 , y6 )
of the vertices of a regular hexagon inscribed in the circle.
It outputs the balance the customer should be paid indicating how many notes of
each denomination (Rs. 100, Rs. 50, Rs. 20, Rs. 10, Rs. 5, Rs. 2, Rs. 1) should
be paid so that the minimum number of notes are given to the customer. Develop an
iterative Java program to solve this problem. Assume that all costs and notes are
integral multiple of the Rupee.
P
8. Let p(x) = ni=0 ai xi be a polynomial. Assuming that the input is received in the
order
x, n, a0 , a1 , . . . , an
develop a Java program to do the following tasks.
Now that we have studied the essentials of the functional and the imperative models of
computation, we are ready to develop more complex programs. In this chapter we will study
the methodology of developing complex programs using step-wise refinement, procedural
abstractions and higher order functions.
71
72 CHAPTER 5. STEP-WISE REFINEMENT AND PROCEDURAL ABSTRACTION
into many sub-problems; and separate functional algorithms were then written for each of
those. The main idea behind the decomposition strategy is not merely to divide a program
into parts, but to ensure that each sub-problem accomplishes a clearly identifiable task and
is written as a separate function or procedure which can then be used as a module to define
other functions. For example, the function perf ect was defined in terms of the function
addf actors, and while describing the function perf ect we were able to regard addf actors as
a black-box. Thus the actual design of the function addf actors could be postponed till later
and we were only concerned with what it computes and not how it computes. Thus, as far
as the function perf ect is concerned, addf actors is not really a procedure but a procedural
abstraction. Procedural abstraction is an integral part of step-wise refinement and it allows
program development in a modular and systematic fashion.
The correctness of the above algorithm can be established from the invariant condition.
Alternatively, the correctness of the iterative algorithm can also be established using PMI.
Exercise 5.1 For the above functional algorithm
1. Verify that the iterative procedure satisfies the invariant condition given above. In
particular verify that the invariant condition holds initially, and the desired final result
can also be obtained from the invariant condition.
2. Establish the correctness of the above algorithm using PMI.
√
Since the algorithm tests for divisors between 1 and n the time complexity of the
√
algorithm is clearly O( n). The space complexity of the iterative process in O(1).
Program development: The algorithm developed above can now be treated as a spec-
ification for the imperative Java program. We can first translate the above functional
description into a ML program which can then be tested before designing the corresponding
Java program.
hPrime-test 1 (ML)i≡
fun prime(n) =
let
hCode for smallest divisori
fun odd(n) = (n mod 2 = 1)
in
(n=2) orelse (odd(n) andalso (n = smallest_divisor(n)))
end;
hCode for smallest divisori≡
fun smallest_divisor(n) =
let
hCode for find divisori
in
find_divisor(n,3)
end;
hCode for find divisori≡
fun find_divisor(n,test_divisor) =
let
fun square(x:int) = x*x
in
if square(test_divisor) > n then
n
else if (n mod test_divisor) = 0 then
test_divisor
else
find_divisor(n, test_divisor + 2)
end
5.1. STEP-WISE REFINEMENT 75
The ML program given above can be thoroughly tested. In fact, if the ML program is
written by defining every component function at the top-level (i.e., by not defining a function
within the scope of another function as suggested above), then each component function
can also be tested individually. Thus any mistake, due to oversight at the algorithm design
phase, can be removed by testing the functional code.
Experiments can also be conducted to verify the expected time complexity of the algo-
rithm. One can use the special timer function available in the ML basis library to obtain the
evaluation time of a function along with the overheads for garbage collection 1 . The overall
execution time minus the the garbage collection time and the system time (this is the time
spent in input/output) gives a reasonable estimate of the run-time of the program. Note
that the run-time thus obtained is not a very accurate measure because of other system
overheads but suffices for rough profiling of functional programs.
Exercise 5.2 Since the algorithm for primality testing described above has a time com-
√
plexity of O( n), the time taken to test the primality of a number ≈ 10m should be about
three times the time taken to test the primality of m. Experiment to find out whether this
is indeed true. Explain any discrepancy that you may observe. The experiment may be
conducted on the numbers 21893 and 218947, both of which are primes.
int n;
BufferedReader in = Text.open(System.in);
Text.prompt("Input n: ");
n = Text.readInt(in);
if (prime(n))
System.out.println(n + " is a prime");
else
System.out.println(n + " is not a prime");
}
1
Garbage collection is a part of the memory management of the ML run-time system, which identifies and
recycles memory space which is not required any more. Hence different runs of the same program with the
same inputs could give you different run-times, depending upon how often the garbage collector is called in
the interactive mode.
76 CHAPTER 5. STEP-WISE REFINEMENT AND PROCEDURAL ABSTRACTION
int test_divisor;
test_divisor = 3;
/* assert: INV */
while ((test_divisor*test_divisor <= n) & ((n % test_divisor) != 0))
test_divisor = test_divisor + 2;
/* assert: INV and
((Math.sqr(test_divisor) > n) OR ((n mod test_divisor) = 0))
*/
if ((n % test_divisor) == 0)
return test_divisor;
else
return n;
}
}
Note that in the Java implementation we have replaced the tail-recursive algorithm for
computing the smallest divisor with a while loop and we have used the same invariant
condition for the tail-recursive function and the while loop. The condition of the while
loop is the same as the otherwise clause of the tail-recursive procedure. The extra variable
test divisor required to define the state space is now a local variable of the function
smallest divisor and it has been initialized before the while loop. As we have men-
tioned before, a tail-recursive function can always be directly translated into an imperative
procedure described by a while loop.
Example 5.2 Determining whether a given positive integer n is a prime (Method 2).
5.1. STEP-WISE REFINEMENT 77
Outline of the solution: Our new method of primality testing is based on a result in
number theory known as the Fermat’s little theorem.
Fermat’s little theorem: If n is a prime number and a is any positive integer less than
n, then a raised to the nth power is congruent to a modulo n.
Two numbers are said to be congruent modulo n if they both have the same remainder
when divided by n. The remainder of a number a when divided by n is also referred to as
a modulo n.
If n is not a prime, then, in general, most of the numbers a < n will not satisfy the above
relation. Thus, given a number n, we can pick a random number a < n and the compute
the remainder an modulo n. If this is not equal to a, n is certainly not a prime. Otherwise,
chances are good that n is a prime. We can assume that the probability that n is a prime
is 0.5. We can keep repeating the above experiment and stop if either
2. we find that the probability that n is not a prime has decreased to an acceptable
level. Note that with the successive experiments, the probability that n is not a prime
decreases as 0.5, 0.25, 0.125 . . ..
Unfortunately, there are numbers which fool Fermat’s test. These numbers are called
Carmichael numbers. Little is known about these numbers except that they are extremely
rare. There are 16 Carmichael numbers below 100,000. The smallest few are 561, 1105,
1729, 2821 and 6601. Thus the chances are little that an arbitrary number will fool the
Fermat’s test and the above strategy is quite reliable for deciding whether a number is prime
or not 2 .
Algorithm design and analysis: We can now develop a functional algorithm for primality
testing based on the above computational theory. We start by defining an iterative function
prime(n, q) of the type prime : P × N → {true, f alse}, where n is the number whose
primality is to be tested and q is the maximum number of times the Fermat’s test is to be
applied. ½
true if n = 2
prime(n, q) =
prime test(n, q, f alse) otherwise
where the iterative function prime test : P × N × {true, f alse} → {true, f alse} is defined
using the invariant
where, the function F ermat test(n) applies the Fermat’s test on the number n once. Note
that prime test returns f alse if the Fermat’s test fails even once. We can define the function
F ermat test : P → {true, f alse} as
where a is a random number between 2 and n−1 and expmod(b, e, m) computes be modulo m.
We can, to start with, define the function expmod : P × P × P → P in terms of the fast
exponentiation function power(b, e) of Example 3.9 as
Since the time complexity of the function power(b, e) is O(lg e), we can expect to compute
expmod also with O(lg e) multiplications. Note, however, that the unit cost here is a
multiplication. However, multiplying, say, 423223 with 378127 is far more complex than
multiplying, say, 23 with 35. Thus, the cost of multiplication itself depends on the number
of digits of the multiplicands. In order to compute be (or, an ) with the above algorithm, for
large n, we have to multiply large numbers, thereby increasing the cost of the multiplications.
Hence, even though the time complexity of the algorithm is O(lg n) multiplications, the cost
of each multiplication cannot be treated as O(1).
We can speed up the process by observing that
Thus, if we take the mod operation inside the scope of square in the fast exponentiation
algorithm of Example 3.9, then we can get by without ever having to multiply numbers
larger than m. This gives us the following algorithm for expmod:
1 if e = 1
expmod(b, e, m) = 2
(expmod(b, (e div 2), m)) mod m if even(e)
(b ∗ expmod(b, (e − 1), m)) mod m otherwise
Exercise 5.3 Establish the correctness of the expmod function defined above by the fol-
lowing steps:
It is important to note that though we can establish the correctness of F ermat test,
the correctness of the overall algorithm cannot be established because the algorithm is not
correct for primality testing owing to the existence of the Carmichael numbers. Theoretically
this remains an inexact method for computing primality.
Exercise 5.4 Show that the time complexity of the expmod function defined as above is
O(lg e).
Consequently, the Fermat’s test can be conducted once in O(lg n) time. Thus the time com-
plexity of the overall algorithm is O(q lg n) where q is maximum number of times Fermat’s
test has to be applied.
Exercise 5.5 Given that ² is the acceptable probability of error in declaring a number n
as a prime, find out q, the number of times Fermat’s test must be executed, in terms of
². Assume that if the Fermat’s test succeeds once, the chances are even that n is a prime.
Ignore the existence of numbers which may fool Fermat’s test.
Actually, the probability that a number n is prime if it passes the Fermat’s test once is
more than even, and, consequently, only a few tests will suffice. Even if the probability is
0.5, the expected number of times the test has to be conducted to determine whether n is
composite is only 2. Thus q can be taken as a constant and the average time complexity of
the overall algorithm is only O(lg n).
Program development: We will first develop a ML program for the above algorithm. We
will use an ML function Rand.randint(m), which generates a random number between 0
and m − 1, to generate the random numbers necessary for the Fermat’s test.3
hPrime-test 2 (ML)i≡
fun prime(n,q) =
let
hCode for prime testi
in
if (n = 2) then
true
else
prime_test(n,q,false)
end;
3
The ML module Rand can be downloaded from the CS120 home page. The module will have to be loaded
in to the ML interactive environment before the ML function prime can be invoked. It will also be necessary to
initialize the random number generator with the command Rand.initialize(x) where x can be an arbitrary
real number in the range from 0.0 to 2147483647.0. Note that Rand.randint is not a function in the strict
sense because it returns a different output for the same input for different invocations. It is only packaged
as a function.
80 CHAPTER 5. STEP-WISE REFINEMENT AND PROCEDURAL ABSTRACTION
Exercise 5.7 Implement the primality test, separately, using each of the above definitions
of the function expmod. Experimentally determine in which case you get a behaviour closer
to that suggested by the O(lg n) time complexity. You can measure the times required to
test the primality of 218947 and 21893, compute the ratios of the the two times and check
if they are close to (lg 218947/ lg 21893). Explain any discrepancy that you may observe.
The Java code for the function prime(n,q) can be now written as follows.
hPrime-test 2 (Java)i≡
import java.io.*;
import javagently.*;
long n,q;
BufferedReader in = Text.open(System.in);
Text.prompt("Input n");
n = Text.readInt(in);
Text.prompt("Input q");
q = Text.readInt(in);
if (prime(n,q))
System.out.println(n + " is a prime");
else
System.out.println(+n + " is not a prime");
}
boolean failed;
failed = false;
while (q != 0 & !failed) {
failed = !fermat_test(n);
q = q-1;
}
if (q == 0)
return true;
else
82 CHAPTER 5. STEP-WISE REFINEMENT AND PROCEDURAL ABSTRACTION
return false;
}
long a;
a = randint(n-2) + 2;
return (expmod(a,n,n) == a);
}
if (e == 0)
return 1;
else if (e%2 == 0)
return square(expmod(b,e/2,m)) % m;
else
return ((b%m) * expmod(b,e-1,m)) % m;
}
The imperative function randint is used to generate a random number in the range
0 . . . (n−1) from the standard Java function Math.random() which generates a real random
number in the interval (0, 1).
Exercise 5.8 Try to test the primality of a large prime number (say 218947, or even larger)
with your ML and Java programs. What is the largest prime for which you obtain a correct
result?
The reason that the above ML and Java programs will not work correctly for large numbers
has nothing to do with the correctness of the algorithm. The largest integer that any ML or
Java implementations can represent is limited. In order to compute an modulo n using the
function expmod, we need to compute products of numbers which may be as large as n. For
large n, this product may exceed largest integers that the programming languages ML and
Java can represent causing integer overflow. The ML interpreter can detect the overflow
and give a suitable error message. The Java interpreter will simply crash in case of an
integer overflow. We will see later how such errors can be detected and handled in both ML
and Java .
Algorithm design and analysis: We will design an algorithm for computing the function
sqrt(x, ²) which returns the square root of a real number x > 0 within a relative error of ².
The function we are seeking is of the type sqrt : R × R → R.
We can determine the initialization and the termination conditions from the following
analysis. We have, for n > 0
√ 1 √ 2
gn − x= (gn−1 − x)
2gn−1
and
√ 1 √ 2
gn + x= (gn−1 + x)
2gn−1
Thus, we have
√ µ √ ¶2 µ √ ¶2n
gn − x gn−1 − x g0 − x
√ = √ = √ ≥0
gn + x gn−1 + x g0 + x
√ √
Hence gn ≥ x for all n > 0 even if g0 < x. We also have that
n
√ √ q2
gn − x = 2 x
1 − q 2n
where √
g0 − x
q= √
g0 + x
Thus, to obtain √
lim gn = x
n→∞
where q is as above. We start with g0 = 1. At any stage stop if the error 2(gn − gn+1 ) falls
below a desired level of accuracy ².
The complete algorithm can then be given as
where
gnew = update(gn , x)
Note that we have given a top-level description of the algorithm in terms of the procedural
abstractions acceptable and update. We may now design algorithms for these two procedures
after deriving their exact specifications from the above description.
The function acceptable : R × R × R × R → B accepts gn , gnew, x and ² as input and
determines whether the termination condition is satisfied. The function update : R×R → R
updates the guess gn according to Newton’s iteration formula. We may define these functions
as
acceptable(gn , gnew, x, ²) = (4(gn − gnew)2 < x ∗ ²2 )
and
update(g, x) = (x/g + g)/2
86 CHAPTER 5. STEP-WISE REFINEMENT AND PROCEDURAL ABSTRACTION
Correctness
It can easily be verified that the initialization satisfies the invariant. Also the choice g0 = 1
ensures that | q |< 1. Thus, according to the invariant the successive guesses generate a
monotonically decreasing sequence, i.e.,
√
g1 ≥ g2 · · · ≥ gn−1 ≥ gn ≥ · · · ≥ x
and the convergence of the process is guaranteed. The termination condition ensures that
we have the solution to the desired level of accuracy.
Efficiency
Suppose we wish to have an accuracy up to the k th decimal digit, i.e.,
√
gn − x
√ ≤ ² = 10−k
x
Then, the number of iterations, n, required can be estimated from the invariant condition
as follows. We require that
√ n
gn − x q2
√ =2 ≤ 10−k
x 1 − q 2n
n
where | q |< 1 is a constant. In the asymptotic analysis the denominator term, 1 − q 2 , can
n
be bounded by a constant, i.e., for some n > n0 we must have 1 − q 2 > c for some constant
c such that 0 < c < 1. Hence, for n > n0 , we require that
n n
q2 q2
2 n < 2 ≤ 10−k
1 − q2 c
Hence, the number of iterations required is
n = O(lg k)
Program development: Now we are ready to translate the above algorithm description
into programming syntax. The complete ML program can be given as follows.
hSqrti≡
fun sqrt(x,epsilon) =
let
hCode for sqrt iteri
in
sqrt_iter(x,epsilon,1.0,0)
end;
5.1. STEP-WISE REFINEMENT 87
Exercise 5.9 Test each of the ML functions developed above by actual execution.
Note that in the above algorithm we have not used the variable n explicitly. Its only role is
to establish the correctness.
Exercise 5.10 Modify the above code to return the pair (gn, n) ∈ R × N and verify that
the number of steps required (given by n) to compute the square root of a number agrees
with the theoretically derived time complexity.
Once we understand abstract role of the variable n in the above program (and complete the
actual testing of the program), we may drop it from the actual program.
Exercise 5.11 Rewrite the functional algorithm and the ML program without using the
iteration counter n explicitly.
Exercise 5.12 Develop a Java function for square root computation by translating the
above ML program.
88 CHAPTER 5. STEP-WISE REFINEMENT AND PROCEDURAL ABSTRACTION
2. Computing the sum of squares of all integers from a to b. A functional algorithm for
the problem can be given in terms of the function sum squares : N × N → N as
½
0 if (a > b)
sum squares(a, b) =
square(a) + sum squares(a + 1, b) otherwise
3. Computing the sum of a sequence of terms in the following series which converges to
π/8:5
1 1 1
+ + + ...
1 · 3 5 · 7 9 · 11
A functional algorithm for the problem can be given in terms of the function pi sum :
N → R as
½
0 if (a > b)
pi sum(n) =
(1/(a ∗ (a + 2))) + pi sum(a + 4, b) otherwise
Clearly, the three algorithms share a lot in common; so much so that they warrant the
design of a common function which combines the common characteristics of the three dif-
ferent different functions. This can be achieved by defining a generic summation whose
functionality is given by
Xb
f (x)
x=a,succ(x)
5 π 1 1 1
This series, usually written as 4
=1− 3
+ 5
− 7
+ . . ., is due to Leibniz
5.2. PROCEDURAL ABSTRACTION USING HIGHER-ORDER FUNCTIONS 89
i.e., the summation of an arbitrary function f (x) in the range [a, b] in steps defined by
the successor function succ(x). In order to write a functional algorithm for such a generic
summation one needs to be able to accept two functions f : N → α and succ : N → N as
input in addition to the parameters a and b which belong to the set N. Here α may be
any generic type on which the operation + is defined (e.g. N or R). Hence, the generic
summation must be a higher-order function whose type is
summation : N × N × (N → α) × (N → N) → α
The advantage of defining such a higher-order function independent of any particular prob-
lem is that the analysis of correctness and efficiency of the algorithm can be carried out in
a general setting.
Exercise 5.13 For the function summation
1. Establish the correctness assuming the correctness of the functions f and succ. Note
that f and succ are just procedural abstractions of two unknown functions.
2. Determine the time and the space complexity in terms of number of calls to the
functions f and succ.
In ML the higher-order summation function can be written as follows:
hsummationi≡
fun summation(a:int,b,f,succ) =
if (a > b) then
0
else
f(a) + summation(succ(a),b,f,succ);
> val summation = fn : int * int * (int -> int) * (int -> int) -> int
Then the summation functions sum and sum squares of the type N × N → N can be
defined in terms of the higher-order summation function as follows:
hsumi≡
fun sum(a,b) =
let
fun term(x) = x : int;
fun next(x) = x + 1
in
summation(a,b,term,next)
end;
> val sum = fn : int * int -> int
90 CHAPTER 5. STEP-WISE REFINEMENT AND PROCEDURAL ABSTRACTION
hsum squaresi≡
fun sum_squares(a,b) =
let
fun term(x) = x*x : int;
fun next(x) = x + 1
in
summation(a,b,term,next)
end;
> val sum_squares = fn : int * int -> int
The functions sum and sum squares are of the type N×N → N. In contrast, the function
pi sum is of the type N × N → R. In order to satisfy the strict type checking in ML , we
have to re-define the function summation so that it returns a value of the type R.
hreal summationi≡
fun real_summation(a : int,b,f,succ) =
if (a > b) then
0.0
else
f(a) + summation(succ(a),b,f,succ);
> val real_summation = fn : int * int * (int -> real) * (int -> int) -> real
Note that the only change in code that is required is the replacement of the identity
element of summation. We can now define pi sum as
hpi sumi≡
fun pi_sum(n) =
let
fun term(x) = 1.0/real(x*(x+2));
fun next(x) = x + 4
in
summation(1,n,term,next)*8.0
end;
> val pi_sum = fn : int * int -> real
Thus we see that the same form of the abstract function summation can be used to
compute sums of various different kinds. It may appear that the same effect may be achieved
by defining the summation function directly (not as a higher-order function) as
hsummation (incorrect)i≡
fun summation(a,b) =
if (a > b) then
0
else
f(a)+summation(succ(a),b);
> Error: unbound variable or constructor: succ, f
5.2. PROCEDURAL ABSTRACTION USING HIGHER-ORDER FUNCTIONS 91
There is still something unsatisfactory about our higher order summation. We have had
to define two versions of the same function to account for the type checking in ML . Since the
two versions are essentially the same, it should be possible to write a single type independent
version. One option would be to pass the identity element of summation as a formal
parameter to the higher order function. But, in such a case, we will need to pass the operator
(‘+’ in this case) also as a formal parameter because the identity element depends on the
operator. If we change both the identity element and the operator, it will be inappropriate to
call the resulting higher
Q order function summation. Then, with the same function, we will
be able to compute bi=a f (i) as well by setting the operator to ‘*’ and the identity element
to 1. In view of this we will call the modified function accumulator. The type of the higher
order function will be accumulator : N × N × (N → α) × (N → N) × (α × β → β) × β → β.
Here α and β represent sets of generic type which can be substituted with any specific
instance. Such functions of generic types are called polymorphic. Such a function can be
written in ML as follows:
haccumulator i≡
fun accumulator(a:int,b,f,succ,oper,iden) =
if (a > b) then
iden
else
oper(f(a),accumulator(succ(a),b,f,succ,oper,iden));
>val accumulator = fn
: int * int * (int -> ’a) * (int -> int) * (’a * ’b -> ’b) * ’b -> ’b
We can now use the polymorphic higher order function to compute sum defined above
in the following way:
hsum (modified)i≡
fun sum(a,b) =
let
fun term(x) = x : int;
fun next(x) = x + 1
in
accumulator(a,b,term,next,op+,0)
end;
> val sum = fn : int * int -> int
5.2. PROCEDURAL ABSTRACTION USING HIGHER-ORDER FUNCTIONS 93
Here op+ is the ML syntax for converting the infix operator + to the corresponding binary
function. The function pi sum can be defined in terms of accumulator as
hpi sum (modified)i≡
fun pi_sum(n) =
let
fun term(x) = 1.0/real(x*(x+2));
fun next(x) = x + 4
in
accumulator(1,n,term,next,op+,0.0)
end;
> val pi_sum = fn : int -> real
The higher order accumulator can even be used to compute f actorial(n) as follows:
hfactorial (modified)i≡
fun factorial(n) =
let
fun term(x : int) = x;
fun next(x) = x + 1
in
accumulator(1,n,term,next,op*,1)
end;
> val factorial = fn : int -> int
In fact, we can go one step further and re-define accumulator so that the input parameters
a and b are also polymorphic. In such a case we will also have to pass a comparison function
in order to make the > operator of a generic type. We give the definition as follows:
haccumulator (modified)i≡
fun accumulator(a:’a,b,comp,f,succ,oper,iden) =
if comp(a,b) then
iden
else
oper(f(a),accumulator(succ(a),b,comp,f,succ,oper,iden));
> val accumulator = fn
: ’a * ’b * (’a * ’b -> bool) * (’a -> ’c) * (’a -> ’a) * (’c * ’d -> ’d)
* ’d
-> ’d
94 CHAPTER 5. STEP-WISE REFINEMENT AND PROCEDURAL ABSTRACTION
We can then use the higher order accumulator to compute summations on the real line
as well.
integral : (R → R) × R × R × R → R
add dx(x) = x + dx
The advantage of writing such generic higher order functions is that the correctness and
efficiency analysis of the algorithmic process needs to be carried out only once. The higher
order function can then be applied to solve a whole class of similar problems instead of a
single one.
λx[x + 4]
is used to denote “a function which accepts x as input and returns x + 4 as the output”.
The corresponding definition in ML is given as:
hfn examplei≡
fn x => x + 4;
> val it = fn : int -> int
The general form of a λ-expression in mathematical notation is
λhformal parametersi[hbodyi]
and in ML it is
hfni≡
fn (hformal parametersi) => (hbodyi);
The scope of the formal parameters is limited to the body of the λ-expression. Thus fn
is used to create functions in the same way as fun except that a function defined by fn has
no name. For example we can use λ to define the function pi sum described in the previous
section without using any auxiliary function as follows:
mult : N → (N → N)
The function mult accepts a number x ∈ N as its input and returns a function of the type
(N → N) as its output. The corresponding definition in ML can be given as
hmulti≡
val mult = fn x:int => fn y => x*y;
> val mult = fn : int -> int -> int
A completely equivqlent way of writing this in ML is as follows:
hmult (equivalent)i≡
fun mult x y = x*y : int;
> val mult = fn : int -> int -> int
Thus typing, say, (mult 2) at the prompt of the ML interpretor returns a function λy(2 ∗
y) of the type N → N.
hinteractivei≡
(mult 2);
>val it = fn : int -> int
which indicates that it is a function of y. One can now apply the function returned by
(mult 2) to, say, 3 and obtain 6 as the answer.
hinteractivei+≡
(mult 2) 3;
> val it = 6 : int
5.2. PROCEDURAL ABSTRACTION USING HIGHER-ORDER FUNCTIONS 97
By defining mult as above, the binary function ∗ has been represented in terms of two
unary functions. The advantage of defining mult as above is that it becomes a higher-order
function and one can define other functions based on multiplication in terms of mult. For
example, consider the ML definitions
hdoublei≡
val double = (mult 2);
> val double = fn : int -> int
and
hhundred timesi≡
val hundred_times = (mult 100);
> val hundred_times = fn : int -> int
The resulting functions double : N → N and hundred times : N → N are both derived
from a higher-order function mult. These functions can now be applied to multiply a
number by 2 or 100.
hinteractivei+≡
double(5);
> val it = 10 : int
hundred_times(5);
> val it = 500 : int
The above strategy of representing an n-ary function in terms of n unary functions by using
λ-expressions to return functions as parameters is called Currying6 . We will see several
more examples of Currying in the following examples.
or, equivalently, as
hcompose (equivalent)i≡
fun compose f g x = f(g(x));
> val compose = fn : (’a -> ’b) -> (’c -> ’a) -> ’c -> ’b
The function compose can then be used to compose two functions, say sin(x) and cos(x)
as follows:
hsin of cosi≡
val sin_of_cos = compose(Math.sin) Math.cos;
> val sin_of_cos = fn : real -> real
The function sin of cos can then be invoked on any input, say π, to compute sin(cos(π))
as
hinvocationi≡
sin_of_cos(Math.pi);
> val it = ~0.841470984808 : real
or directly as
hinvocationi+≡
(compose(Math.sin) Math.cos) Math.pi;
> val it = ~0.841470984808 : real
1. (compose Math.sin)
Exercise 5.16 Give a ML function for composing f ◦ g where g(x, n) = xn and and f (x) =
x + 1 where the input x and n are integers. Indicate the type of composed function.
Exercise 5.17 Consider the following alternate description of compose where it is of the
type compose : (α → β) × (γ → α) → (γ → β) and is defined as
1. Give the equivalent definition in ML and show how two functions, say sin and cos,
can be composed.
2. Why is our original definition a more preferred option? Give examples to show that
the original definition is more general.
7
The solution of f (x) = x is called the fixed-point of the function f .
100 CHAPTER 5. STEP-WISE REFINEMENT AND PROCEDURAL ABSTRACTION
Consider the statement “the derivative of sin(x) is cos(x)”. What this really means is
that the derivative of a function whose value at x is sin(x) is another function whose value
at x is cos(x). Thus derivative may be regarded as a function which given a function f as
input, returns another function Df as the output. Thus, if f is a function and dx is some
small number, then the derivative Df of f is that function whose value at any number x is
given by (in the limit of small dx)
f (x + dx) − f (x)
Df (x) =
dx
Thus derivative may be thought of as a higher-order function of the type
deriv : R → (R → R) → (R → R)
which takes dx as input and returns a function which takes f as its input and returns the
function Df as the output. It can be defined using a λ-expression as
Exercise 5.19 Use the higher-order function deriv to approximate the value of the deriva-
tive of x3 at 5.
We can even use the higher-order derivative function to compute the partial derivative of a
multi-variate function. For example, if f (x, y) = x3 y then the partial derivative of f with
respect to x is 3x2 y. The partial derivative evaluated at say x = 3, y = 4 is 108.
In order to compute the partial derivative of f with respect to x we need to write the
function f in the curried form as
f = λy[λx[x3 ∗ y]]
Exercise 5.20 Use the function deriv to compute the partial derivative of x3 y 2 with
respect to y at x = 3, y = 4. Indicate the type of any function that you may need to define.
We can also combine the higher order functions repeat and deriv to define a function to
compute the nth derivative of a given function.
nderiv(dx, n) = (repeat(derivdx)n)
It takes the parameters dx and n as input, and returns a function which takes f as input
and returns a function to compute the nth derivative of f as the output. It can be written
in ML as
hnth derivativei≡
fun nderiv(dx,n) = (repeat (deriv dx) n);
> val nderiv = fn : real * int -> (real -> real) -> real -> real
We can now define a sequence of functions D0, D1, D2, . . . to compute the zeroth, first,
second, ... derivatives of a given function.
hhigher derivativesi≡
val D0 = nderiv(0.001,0);
> val D0 = fn : (real -> real) -> real -> real
val D1 = nderiv(0.001,1);
> val D1 = fn : (real -> real) -> real -> real
val D2 = nderiv(0.001,2);
> val D2 = fn : (real -> real) -> real -> real
Exercise 5.21 Use the functions D0, D1, D2, . . . to compute the higher derivatives of
sin(x) and cos(x) at a given value of x.
A root of a function f (x) is a value r such that f (r) = 0. Newton’s method for computing
the roots of a differentiable function can be described as follows. If f is a function and y is
an approximation to the root of f , then a better approximation to the root can be obtained
by
f (y)
y−
Df (y)
This generalizes the formula we used for computing the square root of a number in Example
5.3.
Exercise 5.22 Consider f (y) = y 2 − x. Clearly, the root of f (y) gives the square root of
x. Show that the formula for iterative improvement used in Example 5.3 is a special case
of Newton’s method.
Newton’s method does not always converge to an answer, but it can be shown that in cases
where it does converge every iteration of Newton’s method doubles the number of digits of
accuracy of the approximation to the root. In the special case of computing square roots
we have already established that the Newton’s method is guaranteed to converge.
Newton’s method for computing a root of an arbitrary function f can be written as a
higher-order function. It should accept a function f , a parameter guess and an accuracy
factor ² as input and return the root as the output. Hence it has the following type.
newton : (R → R) × R × R → R
It can be defined as
½
guess if acceptable?(guess, f, ²)
newton(f, guess, ²) =
newton(f, update(guess, f ), ²) otherwise
1. Compute the square root of a number by Newton’s method. Compare the execution
time with the program developed in Example 5.3.
2. Compute the fixed point of the equation x = cos(x). Start with an initial value of
guess = 1.
The idea of using functions as input parameters and returned values may take some
getting used to, or it may appear to be little more than a mathematical trick. However the
increased flexibility in expressing programming ideas is enormous and using higher-order
functions it becomes possible to abstract out the essence of a general idea or algorithm
without having to bother about the specific details. Thus, programming in a language that
supports higher-order functions is really convenient.
Problems
1. Use the following timer function to profile the run-times of both our methods of
primality testing.
104 CHAPTER 5. STEP-WISE REFINEMENT AND PROCEDURAL ABSTRACTION
The following function timer measures the execution speed of any arbitrary ML ex-
pression. The arguments f is the function to be timed and x is its input. n is the
number of times the function should be invoked. The function doesn’t take in to ac-
count the time spent in the control loop of the timer function itself, and, consequently,
the timing is somewhat inaccurate. Further, different runs of the same program may
however give slightly different times because of system overheads.
htimer i≡
fun timer f x n =
let fun ntimes(f,x,n) =
let fun ntimes_iter(f,x,n,i) =
if (i=n) then
()
else
(f(x);ntimes_iter(f,x,n,i+1))
in
ntimes_iter(f,x,n,0)
end
in
let val dummy =
Timer.startCPUTimer ()
in ntimes(f,x,n); Timer.checkCPUTimer(dummy)
end
end;
> val timer = fn
: (’a -> ’b) -> ’a -> int -> {gc:Time.time, sys:Time.time, usr:Time.time}
The timer function can be invoked from the ML interpreter to evaluate, say, prime(79)
hundred times as follows:
hinvocationi≡
(timer prime 79 1000);
> val it =
{gc=TIME {sec=0,usec=0},sys=TIME {sec=0,usec=0},usr=TIME {sec=0,usec=10000}}
: {gc:Time.time, sys:Time.time, usr:Time.time}
5.2. PROCEDURAL ABSTRACTION USING HIGHER-ORDER FUNCTIONS 105
Note that the timer function outputs the garbage collection time, the system time
and the user time separately.
2. Two prime numbers p and q are said to be twin primes if q = p + 2. Develop a Java
program, using step-wise refinement, to output the first twin primes after n where n
is an input parameter.
3. Assuming that a function f (x) is given, develop Java functions, using step-wise re-
finement, for
(a) finding a root of the function between two limits by bisection method (see Section
0.7 of Kreyszig).
(b) finding the definite integral of the function between two limits a and b, by
i. the trapezoidal rule (see Section 0.8 of Kreyszig)
ii. Simpson’s rule (see Section 0.8 of Kreyszig)
4. Obtain an algorithm for finding the k th root of a number x and perform an analysis
similar to that in Example 5.3.
5. The reciprocal 1/x of a number x > 0 can be computed as follows. Start with an
initial guess y0 and iteratively update the guess as
yn+1 = yn ∗ (2 − x ∗ yn )
and terminate the process when | 1−x∗yn |< ² for a given ². Use step-wise refinement
to
(a) determine a suitable value of y0 .
(b) develop a complete Java program for the problem.
(c) carry out an analysis of the number of steps required.
6. Define a higher-order double summation function to compute
b X
X d
f (i, j)
i=a j=c
10. Newton’s method is an example of a still more general computational strategy known
as iterative improvement. An iterative improvement says that, to compute something,
we start with an initial guess for the answer, test if the guess is good enough, and
otherwise improve the guess and continue the process using the improved guess as
the new guess. Write a higher-order function called iterative improve that takes two
procedures as arguments: a method for telling whether the guess is good enough and
a method for improving the guess. iterative improve should return as its value a
function that takes a guess as argument and keeps improving the guess until it is
good enough. Express the following using iterative improve:
(a) The algorithm for computing the square root of Example 5.3
(b) The algorithm developed in Problem 5 for computing the reciprocal of a number.
(c) A fixed-point iteration for computing the solution of the equation x = cos(x).
(d) Newton’s method.
11. Can you use the higher-order function iterative improve to compute the factorial
of a given number n iteratively? Iteration itself can be regarded as a higher-order
function. What would be required to write a general purpose higher-order function
that can represent any iteration?
12. In a language like ML which can manipulate functions, we can get by without numbers
(at least insofar as non-negative integers are concerned) by implementing 0 and the
operation of adding 1 as
This representation is known as Church numerals, after the logician Alonzo Church
who invented λ-calculus.
Data-directed programming
107
Chapter 6
So far we have been computing only with integer, boolean and real data types. These
basic data-types, along with the character data type, are supported by most programming
languages. However, for more complex computations we need to deal with more structured
forms of data. Examples of such structured data-types are rational and complex numbers,
lists and sequences, polynomials, vectors and matrices, trees, stacks, queues, files etc. In
this chapter we will see how structured data-types like rational and complex numbers, lists
and sequences can be constructed out of the basic data-types. Our ultimate objective is to
create a hierarchy of data-types, as shown in Figure 6.1, with more complex ones defined
on top of simpler ones, so that the programs which use a particular data-type become
independent of how the data-type is implemented. Complex computations which need to
manipulate objects of these types can then be designed at a higher conceptual level in terms
of these data types.
Any data-type which is not supported natively by a programming language is called an
abstract data type. It includes a representation for items of the data type and associated
operations on the items. An implementation of an abstract data type is called a data struc-
ture. In what follows we will illustrate the design of some abstract data types in both ML
and Java .
RAT = {(p, q) | p, q ∈ Z, q 6= 0}
where Z is the integer data-type. Rational numbers are interpreted as the ratio p/q. Our
design objective is to construct functions like plus : RAT × RAT → RAT , minus :
RAT × RAT → RAT , mult : RAT × RAT → RAT , div : RAT × RAT → RAT ,
equalto : RAT × RAT → B which adds, subtracts, multiplies, divides and checks the
equality of rational numbers. Any user program that uses rational arithmetic may then
109
110 CHAPTER 6. ABSTRACT DATA TYPES
expressions, polynomials
6
lists, sequences
6
rational, complex
6
pairs
6
directly use these functions and treat RAT as a data-type in its own right. The major
advantage of such a scheme is increased modularity. Even if we decide, at a later point
of time, to change our implementation of the data-type RAT , there should be no need to
modify any higher level computation that uses rational numbers.
Let us assume, for the time being, that we can construct a rational number given a
numerator and a denominator using the function
i.e., make rat(n, d) returns the rational number n/d. Let us also assume the we have the
functions
numer : RAT → Z
denom : RAT → {Z − {0}}
which given a rational number as the input, return the numerator and the denominator
respectively.
We can then add, subtract, multiply, divide and check equality of rational numbers using
6.1. BUILDING THE RATIONAL DATA-TYPE (PAIRS) 111
Thus, as long as two integers can be glued together using the function make rat and
un-glued using the functions numer and denom, we have an effective way of implementing
the data-type RAT and its associated functions. We have to, of course, decide how to
implement the functions make rat, numer and denom.
112 CHAPTER 6. ABSTRACT DATA TYPES
define numer and denom to be functions that accept compound items created with the
constructor ratify as input and return the components a and b respectively as output.
The mechanism for extracting the components a and b from the compound item is called
pattern matching. The notation ratify(a, ) means that we do not care about the second
argument in the pattern matching.
Armed with the above, we can now define an ML module called RAT1 which is our first
implementation of the abstract data type Rational.
hModule RAT1 i≡
structure RAT1 : Rational =
struct
datatype number = ratify of int*int;
exception DenomIsZero;
fun gcd(a,b) =
if b = 0 then
a
else gcd(b,a mod b);
fun numer(ratify(x,_)) = x;
fun denom(ratify(_,y)) = y;
fun plus(a,b) =
let
val x = numer(a)*denom(b) + numer(b)*denom(a);
val y = denom(a)*denom(b);
114 CHAPTER 6. ABSTRACT DATA TYPES
in
makerat(x,y)
end;
Exercise 6.1 Complete the functions minus, mult, div and equalto in the above module
definition.
The above ML module RAT1 is a particular implementation (a data structure) of the abstract
data type Rational. The declaration
hstructure declarationi≡
structure RAT1 : Rational =
struct
...
end;
specifies that the module RAT1 adheres to the signature Rational defined earlier. We
can use the module RAT1 from the ML prompt in the following way.
hML usage examplesi≡
- val x = RAT1.makerat(2,4);
val x = ratify (1,2) : RAT1.number
- val y = RAT1.makerat(3,~6);
val y = ratify (~1,2) : RAT1.number
- RAT1.plus(x,y);
val it = ratify (0,1) : RAT1.number
In the above example DenomIsZero is declared to be an exception which is a standard
data type in ML . The DenomIsZero exception is raised when the denominator is zero in
makerat. A typical ML usage example is
hML exceptioni≡
- RAT1.makerat(2,0);
uncaught exception DenomIsZero
raised at: rat.ml:23.13-23.24
6.2. RATIONAL DATA-TYPE IN ML 115
We will illustrate later how to write code to catch and exception to facilitate a graceful
exit.
Note that the ML module RAT1 uses the internal function gcd to reduce a rational number
to its canonical form. It also uses functions numer and denom for the internal representa-
tion of the data type. However, since these functions are not a part of the signature
Rational, they are not visible outside the module RAT1, and, consequently, an usage like
RAT1.gcd(4,6) is illegal. Only those components of a module which are explicitly specified
in the signature of the abstract data type are available for use from outside.
We have thus implemented the data-type for rational numbers using several abstraction
barriers. At the lowest level is the pairing of integers using the primitive datatype and
the constructor facility in ML . At the next higher level are our functions makerat, numer
and denom. Using these functions we have defined, at a still higher level, our functions
for manipulating rational numbers. Any user program at a higher level may now simply
define variables of the type rational (RAT1.number), construct rational numbers using the
function makerat and use the functions for carrying out arithmetic on rationals directly.
The overall hierarchy is illustrated in Figure 6.2. Thus, at the interface to the highest
level of any program using rational numbers, only the functions RAT1.makerat, RAT1.plus,
RAT1.minus, RAT1.mult, RAT1.div and RAT1.equalto need to be available. The rest of
the detail of the implementation may be completely hidden from an user program. This is
achieved through the signature declaration.
The above idea of hiding the details of the implementation of an abstract data type is
key to modular programming. After all, in order to drive a car we need to know only the
116 CHAPTER 6. ABSTRACT DATA TYPES
methods available - the use of the steering, the brake, the indicators 1 , the clutch and gear
changing. We definitely do not want to know about the details of the transmission system,
the viscocity of the brake fluid etc. These issues are relevant when we are designing a
car, exactly in the same way the internal representation of the data type is relevant to the
designer of the data-type module. This clear cut separations of concerns is key to succesful
development of large programs.
We may even change the underlying representation of rational numbers without affecting
any computation at the higher levels. For example, we may consider a totally different,
though somewhat inefficient, implementation of our functions makerat, numer and denom.
If we consider only positive rational numbers, we may define the function makerat as
The function makerat then returns an integer as defined above. Using the unique prime
factorization theorem which states that any positive integer can be uniquely expressed as a
product of primes, we can define the functions numer and denom by simply counting the
numbers or 2’s and 3’s, respectively, in the prime factorization of any integer returned by
makerat. We may thus define these functions as
½
0 if r mod 2 6= 0
numer(r) =
numer(r div 2) + 1 otherwise
and ½
0 if r mod 3 6= 0
denom(r) =
denom(r div 3) + 1 otherwise
We can then implement an entirely different module (a different data structure) for the
same abstract data-type Rational in the following way.
hModule RAT2 i≡
structure RAT2 : Rational =
struct
type number = int;
exception DenomIsZero;
fun gcd(a,b) =
if b = 0 then
a
else gcd(b,a mod b);
1
Not in Delhi!
6.2. RATIONAL DATA-TYPE IN ML 117
fun plus(a,b) =
let
val x = numer(a)*denom(b) + numer(b)*denom(a);
val y = denom(a)*denom(b);
in
makerat(x,y)
end;
Exercise 6.2 Verify that if we use the above definitions of the functions makerat, numer
and denom, we do not need to modify any of the functions plus, minus, mult, div and
equalto. Also verify that any top level program that was defined using the module RAT1
can be used unchanged with the module RAT2.
6.3. RATIONAL DATA-TYPE IN JAVA 119
The interface Number specifies that any realization of this data-type must support the
methods show, which converts the object under consideration to a String which can be
displayed; add, sub, mult and div which return the results of arithmetic operations with
other objects; and lessthan, equal and lteq which return the results of comparison with
other objects. Note that the operation div can throw an exception to indicate a possible
division by zero.
A class in Java is a particular realization of an abstract data type, pretty much similar
to a module in ML . A class implements an interface if it implements all the methods of an
interface. A class typically is a specification of the data fields or instance variables that an
object of this class contains as well as the methods or operations that an object can execute.
An object is a specific instance of a class which is created with a constructor. When an object
is created, memory is allocated for its data fields which are initialized to specific beginning
values.
In what follows, we give a class corresponding to the abstract data type Rational.
hRational classi≡
package myutils;
public class Rational implements Number {
private int numer, denom; /* data fields or instance variables */
}
return pd;
}
The above class description associates the instance variables numer and denom with every
object. A new object of the type Rational can be created with the constructor called
Rational. The name of the constructor for a class and the name of the class must be the
same. The instruction new Rational (n, d), used at several places in the above code,
creates a new Rational object. New memory is allocated for the instance variables numer
and denom and these are initialized to the values n and d respectively, as specified by the
constructor. Apart from the instance variables the object also has the associated methods -
add, sub etc.
Note the use of keywords public and private in the above code. The keyword public
specifies that the corresponding instance variable or the method can be accessed from any
other class, whereas private specifies that the corresponding items are hidden from other
classes and can be accessed only from within. The instance variables numer and denom
and the method gcd are details of the internal representation of a rational number in this
particular class definition and there is no need to make these visible outside the class
definition. This principle of making only the necessary items visible outside a class definition
is called data hiding.
In what follows we give an example of the usage of the Rational data type from a
top level program. Note that the specification package myutils at the top of the above
interface and class definitions make the above codes a part of a package called myutils. In
the following program we include the package myutils accordingly.
hTesting the Rational classi≡
import java.io.*;
import cs120.*;
import myutils.*;
int n1,d1,n2,d2;
BufferedReader in = Text.open(System.in);
Text.prompt("Input n for first number");
n1 = Text.readInt(in);
Text.prompt("Input d for first number");
d1 = Text.readInt(in);
/* show p and q */
System.out.println(p.show());
System.out.println(q.show());
/* show r */
System.out.println(r.show());
}
}
In the above definition of the class Rational, the methods show, add, sub etc. are all
object methods. Note that they are invoked from above test program as p.show(), p.add(q)
etc., where p is the name of an object. We could also choose to make them class methods
by using the Java keyword static in the following way.
hclass methodsi≡
public static Number add(Number a, Number b) {
Rational r = (Rational) a;
Rational s = (Rational) b;
int n = r.numer*s.denom + r.denom*s.numer;
int d = r.denom*s.denom;
return new Rational (n, d);
}
We would also need to make the corresponding changes in the interface definition. The
keyword static specifies that the corresponding instance variable or method is shared by
all objects of this class and they become class variables or class methods. The class method
add could be invoked from the test program as
hclass method invocationi≡
Rational r = (Rational) Rational.add(p,q);
6.3. RATIONAL DATA-TYPE IN JAVA 125
Note that the method add is then prefixed by the class name and not by the object name.
We will discuss the situations in which a class method may be preferred over an object
method in the later chapters.
Exercise 6.3 Change the above Java programs so that all the methods are class methods
instead of object methods.
Exercise 6.4 Implement a Java class for Rational using the same interface given above
using the second scheme described in the previous section using uniqueness of prime fac-
torization. Show that the test program can be used unchanged.
Problems
Implement the following in both ML and Java .
1. Develop an abstract data-type for carrying out arithmetic with complex numbers.
3. Suppose that you have to compute resistances in electrical circuits where the resistance
of each resistor is known only up to some tolerance. For example, a resistor labeled
“6.8 ohms with 10% tolerance” has a resistance between 6.12 ohms and 7.48 ohms.
Using the abstract data-type called interval developed in the previous exercise, develop
functions for computing the resistances of
List is among the simplest of all data structures yet one of the most important, because a
very large variety of application programs are based on them. In this chapter we will study
the basics of programming with lists in both functional and imperative settings.
7.1 Lists
Let α be an arbirary data-type. The abstract data-type α−LIST , which a list of elements
of α, is defined recursively as follows. For any arbitrary data-type α
2. α−LIST = α × α−LIST
In other words a member of α−LIST may be empty or may contain an arbitrarily long
sequence of the elements of the set α. We will denote the data-type of non-empty lists as
∞
[
α−LIST + = α+ = αn
n=1
Following the methodology of implementing a new data-type illustrated in the last chap-
ter, we will develop the α−LIST data-type using abstraction barriers. In addition to deciding
on a representation for lists in the ML and Java programming languages, we will also pro-
vide certain functions available at the interface of the next higher level for manipulating
lists. In particular we will provide the following list functions:
127
128 CHAPTER 7. PROGRAMMING WITH LISTS
1. attach : α × α−LIST → α−LIST , which given an element from the set α and a list
(which may be empty) attaches the element to the front of the list. For example, if
ls = [1, 2, 3], then attach(0, ls) should return the list ls = [0, 1, 2, 3], and attach(0, [])
should return the list [0].
2. empty : α−LIST → {true, f alse}, which given an input list determines whether it is
empty or not.
3. head : α−LIST + → α, which given a non-empty list as its input returns the first
element of the type α.
4. tail : α−LIST + → α−LIST which given a non-empty list as its input returns the
sub-list without the first element. It returns the empty list if the input list has only
one element.
Assuming, for the time being, that we can implement the abstract data-type α−LIST
with the above associated methods in both ML and Java programming languages, we can
illustrate its use by developing some algorithms for manipulating lists.
Example 7.1 Determining whether a given list is a singleton list (contains only one ele-
ment).
We can define the function singleton : α−LIST → {true, f alse} as
½
f alse if empty(ls)
singleton(ls) =
empty(tail(ls)) otherwise
M AXM : α−LIST + → α
2. it is the largest element of the list (note that there may be more than one occurrence
of the largest value).
7.1. LISTS 129
Base case. (n = 1) or singleton?(ls). If the list has only one element then M AXM (ls) =
head(ls) which is an element of the list and is trivially the largest.
Induction hypothesis. M AXM (ls) returns the largest value in the list if the size of the
list is (n − 1).
Induction step. Consider a list ls such that the size is (n > 1). Note that tail(ls) is a list
of size (n − 1). Now,
where a = head(ls) is an element of the list and b = M AXM (tail(ls)) is the largest
element in the sub-list tail(ls) by the induction hypothesis. By the definition of the
binary function max, whose correctness is trivially established, M AXM thus returns
the largest element in the list ls.
2
The time complexity of the above algorithm is obviously O(n).
Altenatively, we can construct an iterative version of the above function using the invariant
condition
where ½
len if empty(ls)
length iter(ls, len) =
length iter(tail(ls), len + 1) otherwise
Exercise 7.2 Establish the correctness of the above algorithms for computing the length
of a given list and estimate the time complexity.
130 CHAPTER 7. PROGRAMMING WITH LISTS
Exercise 7.3 Show that the time complexity of append is O(n) where n is the size of l1.
What is the space complexity?
Exercise 7.4 Establish the correctness of map and f ilter using PMI and estimate the
time complexities.
Hence we see that the abstract data-type α−LIST along with its associated functions attach,
empty, head and tail is quite powerful and we can indeed develop complex functions for
list manipulation using these. We have to, of course, address the issues of implementing the
abstract data-type in both ML and Java programming languages. In what follows, we will
first develop the list data-type in ML and then follow it up with a Java implementation.
We may define the basic methods attach, empty, head and tail of the data-type α−LIST
in ML as follows
hList data-type in ML i≡
structure LIST =
struct
exception emptylist;
Thus, the abstract data-type α−LIST can easily be realized in ML using the basic con-
structors [] and :: and using pattern matching.
However, since ML already provides the basic constructors for list programming, we will
continue to use the ML primitives directly in our subsequent programs instead of using
struct LIST. We will do so only for convenience with the understanding that both are
equivalent.
We can now translate the functional descriptions of Examples 7.1, 7.2, 7.3, 7.4 into ML
programs.
hsingleton? i≡
fun singleton([]) = false
| singleton(x::[]) = true
| singleton(ls) = false;
hMAXM i≡
fun max([]) = raise empty
| max(x::[]) = x
| max(x::ls) =
if x > max(ls) then
x
else
max(ls);
hlengthi≡
fun length([]) = 0
| length(x::ls) = length(ls) + 1;
hlength (iterative)i≡
fun length(ls) =
let
fun length_iter([],len) = len
| length_iter(x::ls,len) = length_iter(ls,len+1)
in
length_iter(ls,0)
end;
happend i≡
fun append([],l2) = l2
| append(x::ls,l2) = x::append(ls,l2);
7.2. THE α−LIST DATA-TYPE IN ML 133
hmapi≡
fun map f [] = []
| map f (x::ls) = f(x)::(map f ls);
hfilter i≡
fun filter pred [] = []
| filter pred (x::xs) =
if pred(x) then
x::(filter pred xs)
else
(filter pred xs);
Exercise 7.5 Use the ML interpreter to execute each of the above functions.
Now that we have implemented the data-type α−LIST in ML , we can consider a few
more algorithms for lists.
Exercise 7.6 Establish the correctness of the above function using PMI.
Exercise 7.7 Establish the correctness of the above function using PMI.
Note, however, that the above function reverse has an unacceptably high time complexity.
To see this we may write a recurrence relation describing the time complexity of the above
function. Let T (n) be the number of operations required to reverse a list of size n using
the above algorithm. Note that in order to reverse a list of size n, it is required to reverse
a list of size n − 1 and append it to a single element list. Also note that (append x y)
requires O(m) steps when the list x is of size m and a :: operation is required to form a
single element list. A recurrence for T (n) may be given as
T (0) = 0
T (n) = T (n − 1) + n
T (n) = T (n − 1) + n
= T (n − 2) + (n − 1) + n
..
.
= T (0) + 1 + 2 + . . . + n
= n(n + 1)/2
Hence the overall time complexity of the function reverse given above is O(n2 ).
Alternatively, we may define an iterative version of the above function using the invariant
hreverse (iterativei≡
fun rev(ls) =
let
hCode for rev iteri
in
reviter(ls,[])
end;
Exercise 7.9 Establish the correctness of the above iterative algorithm for reversing a
given list and show that the time complexity is O(n). Compare the space complexities of
the two algorithms.
Exercise 7.10 Execute the ML programs for both the algorithms for reversing random
lists of size 2,4,8,32,64,128,256,512 and 1024 and compare the execution times of the two
algorithms. Verify that each time the problem size is doubled, the execution time of the
first algorithm increases by roughly four times whereas the execution time of the second
algorithm doubles.
Exercise 7.11 Establish the correctness of the above algorithm using PMI and show that
the worst case time complexity (measured in terms of number of :: operations) of the
above algorithm for inserting an element into a list of size n is n + 1. Under what condition
of the input list does the worst case situation occur?
Exercise 7.14 Establish the correctness of the insort function using PMI.
We can analyze the time complexity of the above algorithm as follows. Let T (n) be the
number of operations required to sort a list of size n using the above algorithm. Note that
in order to solve a sorting problem of size n, it is required to solve a sorting problem of size
n − 1 in addition to inserting an element, x , into a list of size n − 1. A recurrence for T (n)
may be given as
T (0) = 0
T (n) = T (n − 1) + n
Exercise 7.16 Establish the correctness of the iterative algorithm and compare the space
complexities of the recursive and the iterative algorithms for insertion sort.
We can develop the function split : α−LIST → α−LIST × α−LIST using the following
invariant. Let the original list be ls0 = [a1 , a2 , . . . , an ].
IN V = (1 ≤ i ≤ n + 1) ∧ (ls = [ai , . . . , an ])
∧
((either i is odd ∧ l2 = [a1 , a3 , .., ai−2 ] ∧ l1 = [a2 , a4 , ..., ai−1 ])
(or i is even ∧ l2 = [a1 , a3 , .., ai−1 ] ∧ l1 = [a2 , a4 , ..., ai−2 ]))
Exercise 7.17 Establish the correctness of the functions split and msort.
We can calculate the number of steps required for msort as follows. Since the number
of steps required for both split and merge is n, we can write a recurrence for the overall
procedure as
T (1) = 1 (we do not require any step for the singleton input)
T (n) = 2T (n/2) + 2n
Assuming, for the time being, that n = 2m (a perfect power of two), we can solve the above
recurrence by telescoping, i.e.,
Hence, the worst case complexity of msort is better than that of insort. We will see later
that this is the best we can do for sorting.
Exercise 7.18 Show that the above result holds even if n is not a perfect power of two.
and then define a function for quick sort, in terms of comp and filter as
hqsorti≡
fun qsort([]) = []
| qsort(x::xs) =
let
hCode for compi
in
append(qsort(filter (comp op<= x) xs),x::qsort(filter (comp op> x) xs))
end;
7.2. THE α−LIST DATA-TYPE IN ML 141
Exercise 7.20 Generalize qsort by converting it to a higher order function which takes a
suitable comparison function as it’s input and show how can one use the same higher order
function to sort an arbitrary list in either ascending or descending order.
Exercise 7.21 We have developed the ML function for insertion sort for sorting lists only
in the ascending order. Modify the functions insort and insert into higher order functions
like in the previous exercise.
The run time analysis of qsort is instructive. The split operation always takes n steps.
Since the value of the first element, x, is arbitrary, the two partitions we obtain are of sizes
i and n − 1 − i, where i is any value between 0 and n − 1 with equal probability. That is,
if the partitioning element happens to be the smallest element in the list then i is 0, and if
it happens to be the largest element in the list then i is n − 1. For any other choice i has
a value in between. Now, in the extreme cases, if at every recursive stage the partitioning
element is either the smallest or the largest, then, to solve a problem of size n we have to
solve two sub-problems of sizes 0 and n − 1. This would obviously happen if the input list
is either in increasing or in decreasing order. Note that the problem of size 0 has zero cost.
In addition, we have to append the two sublists after attaching the partitioning element to
the front of the second list. The cost of the append operation is proportional to the size of
the first list. Thus, if the list is arranged in increasing order, the first partition is of size 0
and the second partition is of size n − 1 and append has no cost. The only cost involved is
attaching the partitioning element to the front of the second list and the cost of split. We
can write a recurrence for the number of steps in quick-sort as
T (0) = 0
T (n) = T (0) + T (n − 1) + n + 1 = T (n − 1) + n + 1
T (0) = 0
T (n) = T (n − 1) + T (0) + (n − 1) + 1 + n = T (n − 1) + 2n
Thus we see that in case the input list is already in ascending or descending order, the
number of steps required for qsort is identical to that for the worst case for insort. This
is the worst case behaviour for qsort.
We are yet to analyze the in between cases. It may so happen, if we are lucky, that every
time the partitioning element falls in the middle of the list and the two sublists generated
out of partitioning are of roughly equal sizes. In such a case we have to solve two sub-
problems of sizes roughly equal to n/2 and the append operation also requires n/2 steps.
The recurrence is given as
T (0) = 0
T (n) = 2T (n/2) + n + n/2
We have obtained a divide-and conquer recurrence and it is easy to verify that this recurrence
solves to T (n) = O(n log2 n) and the behavior is similar to msort.
In most cases, however, god is neither so cruel nor so benevolent, and, we have an in
between situation. We obtain splits of sizes i and n − i − 1 with i ranging from 0 to n − 1
with equal probability. Thus, the average cost of the two sub-problems that we have to
solve can be written as
n−1 n−1
1X 2X
(T (i) + T (n − i − 1)) = T (i)
n n
i=0 i=0
Writing the cost of split and append as cn for some constant c, we have the average case
recurrence for qsort as
T (0) = 0
n−1
2X
T (n) = T (i) + cn
n
i=0
Subtracting the second equation from the first, to be rid of the summation, we obtain
The constant −c is insignificant for the analysis and can be dropped. We obtain, after
rearranging
nT (n) = (n + 1)T (n − 1) + 2cn
Dividing the above by n(n + 1) we obtain
T (n) T (n − 1) 2c
= +
n+1 n n+1
Telescoping, we obtain,
T (n−1) T (n−2) 2c
n = n−1 + n
T (n−2) T (n−3) 2c
n−1 = n−2 + n−1
..
.
T (2) T (2) 2c
3 = 1 + 3
Adding all the above yields
X1 n+1
T (n) T (1)
= + 2c
n+1 2 i
i=3
T (1) R n+1 1
Now 2 is a constant and the summation to the right is bounded above by 0 x dx =
O(ln n) . Hence, clearly, T (n) = O(n ln n). Thus, the average case behaviour of qsort
is closer to it’s best case behaviour. In fact, on random data, qsort is one of the fastest
sorting algorithms.
ls.start
null
1 2 3 4
Building the list data-type in an imperative language like Java is more complicated than
in ML because the storage management has to be explicitly handled. We will represent an
object of the type α−LIST using the popular linked-lists scheme common in imperative
languages. Conceptually the representation can be viewed as in Figure 7.1. A linked-lists
is a linear ordering of nodes. Each node is a compound object which stores one reference to
an element (an integer object, for example) and another reference to the next node in the
representation. The last node may be terminated by assigning the Java constant null to
the next-node reference. Note that every variable in an imperative language like Java can
be thought of as a reference to an object or a memory location. Thus the references, both to
integer objects and to next nodes can be represented by simple Java variables. The class
for a node can be defined in Java as
hCode for Node classi≡
class Node {
Object data;
Node link;
Node(Object d,Node n) {
data = d; link = n;
}
}
7.3. THE α−LIST DATA-TYPE IN JAVA 145
The class defines two instance variables, data and link, of types Object and Node. These
are references to a data element and the next node, respectively. Object is a key-word in
Java used to indicate a polymorphic data-type (analogous to α). Note that the definition is
circular, because the type of the instance variable link is Node itself. However, conceptually
there is nothing wrong in this circular definition and it is allowed in Java . The constructor
Node takes two references, one to a data element and another to the next node, as input
and creates and initializes the instance variables.
We now want to define a Java class for the abstract data type α−LIST described at the
beginning of this chapter. Apart from the internal representation, the class definition must
also support a constructor, and the methods empty, attach, head and tail. Each object of
the class alphaList needs to have only one instance variable, start, which is a reference
to the first node in the list. Thus, we may define the Java class alphaList as follows.
hclass alphaListi≡
public class alphaList {
public alphaList() {
start = null;
}
The instance variable start is declared private because we want to hide our internal
representation from external classes which may use alphaList. It is for the same reason
that we have defined the class Node within the class alphaList. Node is an inner class of
alphaList and is consequently hidden from all external classes.
From our experience with list programming in ML we know that we often require to
create empty lists, or assign the value of one list to another. Accordingly, we have designed
the basic constructor for the class alphaList so that it returns an empty list indicated by
start == null. To be able to assign the value of one list to another we have defined the
function public static alphaList newList(alphaList ls). Note, from the definition,
that newList creates a new list object, with it’s own instance variable, which is then set
to the instance variable of the old list. The two list objects then have different references
(names), but the share the same set of nodes. We will discuss the consequences of sharing of
nodes by two lists a little later Section 7.3.1. We also provide a function private static
alphaList makeList(Node start) for creating new list starting from an arbitrary start
node reference. The new list created by makeList will also share it’s nodes with some other
lists. The function is declared private because it directly uses the internal representation.
Note that both newList and makeList are class methods because of the use of the key-word
static. They cannot be object methods because their sole purpose is to create new list
objects.
We can now develop the code for list methods as follows.
For the function empty, we have to merely check whether start == null.
hCode for emptyi≡
public boolean empty() {
return (start ==null);
}
For attaching an Object x to an list object we have to first create a new instance of
Node (an object of type Node) and set it’s data element to x. Then we have to set the link
field of the new node so that it points to the original list and set the start field of the list
object to the new node.
hCode for attachi≡
public void attach(Object x) {
if (empty())
start = new Node(x,null);
else {
Node T = new Node(x,start);
start = T;
}
}
7.3. THE α−LIST DATA-TYPE IN JAVA 147
head returns the reference of the data object of the node pointed at by the start of the
list
hCode for headi≡
public Object head() {
return start.data;
}
To return the tail of a list we create a new list whose start is start.link of the input
list
hCode for taili≡
public alphaList tail() {
return makeList(start.link);
}
148 CHAPTER 7. PROGRAMMING WITH LISTS
class Node {
Object data;
Node link;
Node(Object d,Node n) {
data = d; link = n;
}
}
public alphaList() {
start = null;
}
We can now define a class called ListFunctions in which we can translate most of our
ML programs.
hclass ListFunctionsi≡
package myutils;
}
}
}
l1 a1 a2 a3 an nil
l2 b1 b2 b3 bn nil
append(l1,l2) a1 a2 a3 an
Figure 7.2: Appending two lists in Java using the function ListFunctions.append
It is instructive to consider the actual process of appending two lists with the function
ListFunctions.append. The process is illustrated in Figure 7.2. The input lists l1 and
l2 are not modified by the function append described above. A new copy of the list l1 is
made using successive calls to the function attach and the new list is attached to the front
of l2. The combined list is returned by the function append(l1,l2). Note that the lists
l1 and l2 are left intact.
Similarly, the effect of inserting the element 4 in the list [1,2,3,5] using ListFunctions.insert
is illustrated in Figure 7.3. Note that, like the function ListFunctions.append, the func-
tion ListFunctions.insert also generates a new list.
7.3. THE α−LIST DATA-TYPE IN JAVA 153
ls 1 2 3 5 nil
insert(4,ls) 1 2 3 4
Figure 7.3: Inserting an element into a sorted lists in Java using the function
ListFunctions.insert
Programs that use the alphaList data-type and the methods in ListFunctions
We have designed the Java classes alphaList and ListFunctions using several ab-
straction barriers. The abstraction barriers in the implementation are illustrated in Figure
7.4. We can now define programs at a higher level, using the above functions, without any
explicit reference to the details of the implementation.
In what follows we will give an example of top-level list programming in Java using the
classes alphaList and ListFunctions. Java does not allow us to use the basic data-type
int interchangeably with the polymorphic type Object. In view of this, we will need to
define a new Java class, Int, to test our list programs. In what follows, we only describe
the methods of Int. It implements interface Sortable which we have already described
The details of the implementation are given in the appendix.
hclass Inti≡
package myutils;
public class Int implements Sortable {
public Int(int a); /* the constructor */
public int toint(); /* object mehod to convert to int */
public boolean lessthan(Sortable x); /* comparison method */
154 CHAPTER 7. PROGRAMMING WITH LISTS
System.out.println("input a:");
alphaList la = Int.readlist();
System.out.println("input b:");
alphaList lb = Int.readlist();
System.out.print("a = ");
Int.printlist(la);
System.out.print("b = ");
Int.printlist(lb);
alphaList lc = ListFunctions.reverse(la);
System.out.print("a = ");
Int.printlist(la);
System.out.print("c = ");
Int.printlist(lc);
lc = ListFunctions.append(la,lb);
System.out.print("c = ");
Int.printlist(lc);
alphaList ld = ListFunctions.insort(lc);
System.out.print("d = ");
Int.printlist(ld);
}
}
7.3. THE α−LIST DATA-TYPE IN JAVA 155
Exercise 7.23 Add the functions merge sort and quick sort to the class ListFunctions
Problems
Implement the following in both ML and Java .
1. Develop an abstract data-type called set. Represent a set of integers as a list (there
should be no duplications) and develop functions for
2. Represent a set as an ordered list of integers and develop functions for each of the
above operations. The time complexity of each of the above functions should be linear.
(a) Develop algorithms/functions for adding and multiplying two polynomials. Es-
timate the time complexities of your algorithms.
(b) Develop Java function/procedure for reading and printing a polynomial.
import java.io.*;
import java.util.*;
import cs120.*;
}
return ListFunctions.reverse(ls);
}
public static void printlist(alphaList ls) {
while (!ls.empty()) {
Int x = (Int) ls.head();
System.out.print(x.val+" ");
ls = ls.tail();
}
System.out.println();
}
public static void readarray(Int a[],int n) throws IOException {
StringTokenizer T;
int i,ntokens,c;
String s;
Int x;
BufferedReader in = Text.open(System.in);
s = in.readLine();
T = new StringTokenizer(s);
ntokens = T.countTokens();
if (ntokens >= n) {
i = 0;
while (i < n) {
c = Integer.parseInt(T.nextToken());
x = new Int(c);
a[i] = x;
i++;
}
}
}
public static void printarray(Int a[],int n) {
for(int i=0; i < n;i++) System.out.print(a[i].val + " ");
System.out.println();
}
}