Standish Sol Manual Ece223

You might also like

Download as pdf or txt
Download as pdf or txt
You are on page 1of 258

Table of Contents

Solutions for Chapter 1 Preparing for the Journey


Problems and Exercises for Chapter 1, p. 1
Solutions for Chapter 2 Linked Data Representations
Section 2.3 p. 2
Section 2.4 p. 3
Section 2.5 p. 4
Section 2.6 p. 7
Solutions for Chapter 3 Introduction to Recursion
Section 3.2 p. 8
Section 3.3 p. 13
Section 3.4 p. 14
Solutions for Chapter 4 Modularity and Data Abstraction
Section 4.2 p. 15
Section 4.3 p. 15
Section 4.4 p. 16
Section 4.5 p. 18
Section 4.6 p. 19
Solutions for Chapter
Concepts
Section 5.2 p.
Section 5.3 p.
Section 5.4 p.
Section 5.5 p.
Section 5.6 p.
Section 5.7 p.
Section 5.8 p.

5 Introduction to Software Engineering


21
26
29
30
32
32
33

Solutions for Chapter 6 Introduction to Analysis of Algorithms


Section 6.2 p. 36
Section 6.3 p. 37
Section 6.4 p. 38
Section 6.5 p. 40
Section 6.6 p. 46
Solutions for Chapter
Queues
Section 7.2 p.
Section 7.3 p.
Section 7.4 p.

7 Linear Data Structures Stacks and


47
48
49

Table of Contents continued


Section
Section
Section
Section
Section

7.5
7.6
7.7
7.8
7.9

p.
p.
p.
p.
p.

50
53
56
56
59
i

Solutions for Chapter


Allocation
Section 8.2 p.
Section 8.3 p.
Section 8.4 p.
Section 8.5 p.
Section 8.6 p.

8 Lists, Strings, and Dynamic Memory


60
67
68
69
71

Solutions for Chapter 9 Trees


Section 9.2 p. 74
Section 9.3 p. 74
Section 9.4 p. 75
Section 9.5 p. 75
Section 9.6 p. 78
Section 9.7 p. 83
Section 9.8 p. 91
Section 9.9 p. 105
Section 9.10 p. 107
Section 9.11 p. 107
Solutions for Chapter 10 Graphs
Section 10.2 p. 112
Section 10.3 p. 112
Section 10.4 p. 117
Section 10.5 p. 122
Section 10.6 p. 128
Section 10.7 p. 134
Section 10.8 p. 142
Solutions for Chapter 11 Hashing and the Table ADT
Section 11.2 p. 145
Section 11.3 p. 146
Section 11.4 p. 150
Section 11.5 p. 152
Section 11.6 p. 157
Section 11.7 p. 159
Solutions for Chapter 12 External Collections of Data
Section 12.2 p. 160

Table of Contents continued


Section 12.3 p. 161
Section 12.4 p. 162
Section 12.5 p. 163
Solutions for Chapter 13 Sorting
Section 13.2 p. 164
Section 13.3 p. 164
Section 13.4 p. 166
Section 13.5 p. 169
Section 13.6 p. 170
Section 13.7 p. 179
Section 13.8 p. 182
ii
Solutions for Chapter 14 Advanced Recursion
Section 14.2 p. 183
Section 14.3 p. 184
Section 14.4 p. 190
Section 14.5 p. 197
Solutions for Chapter 15 Object-Oriented Programming
Section 15.2 p. 202
Section 15.3 p. 211
Section 15.4 p. 213
Solutions for
Concepts
Section
Section
Section

Chapter 16 Advanced Software Engineering


16.2 p. 215
16.3 p. 217
16.4 p. 218

Solutions for the Math


Section A.1 p.
Section A.2 p.
Section A.3 p.
Section A.4 p.
Section A.5 p.
Section A.6 p.
Section A.7 p.
Section A.8 p.

Reference and Tutorial Appendix


220
220
221
221
222
223
224
226

Table of Contents continued

iii

Table of Contents continued

Solutions to Problems and Exercises in Chapter 1


1. If the array A contains no negative integers, A [ M a x I n d e x ] will
eventually be evaluated in the while-condition of the while-loop. If
array index bounds checking is turned on, the erroneous array
access A[MaxIndex] , in which MaxIndex is out of bounds of the index
range 0:MaxIndex1 of A , will be detected. The error can be fixed by
replacing line 5 with
5

while ( (i < MaxIndex) && (A[i] >= 0) ) {

This works because in the short-circuit and expression A && B, the


subexpression A is evaluated before evaluating the subexpression
B , and if A s value is false (i.e., 0), then the value of the entire
expression A && B is taken to be false without evaluating B . Hence,
when is value is increased so that i == MaxIndex, the subexpression (i
< MaxIndex) evaluates to false and the erroneous out-of-range
access of A[MaxIndex] in (A[i] >= 0) is never performed.
2. The statement of the problem on page 16 states that, In order to
avoid doing useless work, the solution should exit as soon as the
first negative integer is discovered. The proposed solution does
not exit as soon as the first negative integer is discovered.
Rather, it examines all array items in descending order and saves
the index of the most recent negative integer encountered in
descending order. The conditions of the problem statement are
therefore not satisfied.
3. Hardware tends to come and go rather rapidly. Vendors release
new models of computers, usually with increased performance or
lower cost, at frequent intervals in order to keep up with the
competition. Programming languages and software tend to ride
out the rapid shifts in underlying computers by being rehosted on
new platforms although software versions go through rapid
evolution, too, in response to competitive market forces. The
fundamental laws of computing seem to have the greatest
longevity. For example, the discovery of the n log n barrier for
comparison-based sorting has endured since its discovery.
Various human-computer interfaces undergo evolution as the
decades pass. The early paper-tape and punched card inputs and
electric typewriter or teletype outputs gave way to cathode ray
tube screens with lines of characters, and these, in turn, gave way
to windows, mice, and the desktop metaphor. If versatile,

Table of Contents continued


efficient voice input were to evolve, it might change the nature of
the human-computer interface even further.

Table of Contents continued

Answers to Review Questions 2.3


1. When we dereference a pointer , we follow the pointer to the unit
of storage for which the pointer value is the address (in some
addressable storage medium in which units of storage have unique
addresses that identify them). If A is a pointer variable in C, the
notation *A dereferences the pointer value in A .
2. We can say that: B links to A , or that A is B s referent , or that B
references A .
3. typedef int *IntegerPointer;
4. Execute the assignment A = (T*) malloc(sizeof(T)), after which A contains
a pointer to a new block of storage of type T .
5. *A = 19 assigns 1 9 to be the value of A s referent. The expression
2*(*A) has a value equal to twice the value of A s referent.
6. It is a type mismatch, since A must take pointer values, and since 5
is not a pointer value, but rather is an integer value.
7. Two different expressions that reference the same unit of storage
are called aliases.
8. You execute the function call free(P) , where P contains a pointer to
the storage to be recycled.
9. Dynamically allocated storage becomes inaccessible when there
are no pointers to it that can be reached either directly as values
of pointer variables, or indirectly by following links in data
structures along a path that can reach it.
10. Garbage is inaccessible dynamically allocated storage that is no
longer needed during the execution of a program.
11. A dangling pointer is a pointer to a unit of storage that has been
returned to the pool of unallocated dynamic storage.
12. The scope of a variable in C relates to the lifetime of its existence
and the places it is visible . During the time a variable exists and is
visible (by virtue of its name not being hidden by another variable
of the same name that has a more local scope), the value stored in
the storage location associated with the variable can be both
assigned and accessed, using the variables name. The scope of a
unit of dynamic storage allocated in C can be thought of as global
to the scope of named variables in a C program. This means that
the lifetime of a unit of dynamically allocated storage lasts from
the moment the unit is allocated (using malloc ) until the moment the
units storage is reclaimed (using free(P) ). So long as a pointer to a
unit of storage exists, and so long as that unit of storage has not

Table of Contents continued


been reclaimed, the value in the unit can be accessed and a new
value can be assigned to it.
13. Since units of dynamically allocated storage do not have names in
a C program, they are sometimes called anonymous variables ,
meaning variables with no names. Names for variables are created
in the text of a C program, but, since dynamically allocated
storage is created at the time a program is executed, rather than
when the program is written, there is no way to give textual names
to units of dynamically allocated storage. However, the pointer to
a unit of dynamically allocated storage acts in place of the name of
an ordinary variable, since, using the pointer, the value stored in
the storage unit can be both assigned and accessed.

Solutions to Exercises 2.3


1. It prints

7.

2. It prints

5.

3. Answer depends on the behavior of your C system and is


determined by experiment.
4. Answer depends on the behavior of your C system and is
determined by experiment.

Answers to Review Questions 2.4


1. The null address is a special address that is not the address of any
node, and which, by convention, is used to indicate the end of
linked lists.
2. By a dot ( ) or by a dot connected to an arrow pointing to the
value NULL .
3. The value NULL represents the null address in C.
4. By a solid dot ( ) in a link field.
5. An empty linked list is a list L having no nodes on it. By convention,
it is indicated by the value NULL .
6. Explicit pointer variable notation consists of a box containing the
tail of an arrow representing the pointer value, where the box is
labeled on the left with the name of the pointer variable followed
by a colon. Implicit pointer variable notation consists of an oval
containing the pointer variable name in which an arrow connects
the boundary of the oval to the referent of the pointer value. The
two notations are equivalent. The implicit pointer variable
notation is used in most diagrams in the book.

Table of Contents continued


7. By a question mark (?), or, in the case of a pointer to an unknown
location, by an arrow pointing to a circle containing a question
mark.

Solutions to Exercises 2.4


1. (a) NULL , (b) GCM , (c) MEX , (d) NULL .
2. (a) L>Link, (b) L>Link>Link, (c) L, (d) *L.
3. N>Link = L>Link>Link; L>Link>Link = N;

4.
Airport

N
Airport

Link

ORD

Link

GCM
Airport

Link

MIA

NULL

Airport

Link

MEX

5. strcpy(L>Link>Airport, "JFK");
6. N>Link = L>Link>Link; free(L>Link); L>Link = N;

Answers to Review Questions 2.5


1. In top-down programming using stepwise refinement, one starts
with an outline of a program which leaves out specific details, and
one progressively fills in more details by a process known as
stepwise refinement . In stepwise refinement , at each step more
specific detail is filled in, until finally, a specific executable
program has been created, written in an actual programming
language.
2. We can define a struct for a NodeType that is tagged with the name
N o d e T a g and we can use this tag to define a Link member of the
struct whose type is a pointer to the NodeType struct being defined,
as shown in the type definition struct given at the beginning of
Exercises 2.5 on page 53.
3. The value NULL belongs to every pointer type in C.

Solutions to Exercises 2.5


1.

Table of Contents continued


void InsertNewFirstNode(AirportCode A, NodeType **L)
{
NodeType *N;
/* Allocate a new node and let the pointer variable N point to it */
N = (NodeType *) malloc(sizeof(NodeType));
/* Set the Airport field of Ns referent to A */
strcpy(N> Airport, A);
/* Change the Link field of Ns referent to point to the first node of list L */
N> Link = *L ;
/* Change L to point to the node that N points to */
*L = N;
}

2.
void DeleteFirst(NodeType **L)
{
NodeType *N;
if (*L != NULL) {
N = *L;
*L = (*L)>Link;
free(N);
}
}

3.
void InsertBefore(NodeType *N, NodeType *M)
{
AirportCode A;
/* insert node M after node N on list L */
M>Link = N>Link;
N>Link = M;
/* swap airport codes in N and M */
strcpy(A, N>Airport);
strcpy(N>Airport, M>Airport);
strcpy(M>Airport, A);
}

4.
NodeType *Copy(NodeType *L)
{
NodeType *N, *M, *L2;
if (L == NULL) {
return NULL;
} else {
/* initialization and copying of first node */
M = (NodeType *) malloc(sizeof(NodeType));
L2 = M;
N = L;
strcpy(M>Airport, N>Airport);
/* L2 points to the copy of the list L being constructed */
/* N is a pointer that steps along the nodes of L to copy */
/* M is a pointer that steps along the nodes of L2 that are copies */

Table of Contents continued


/* of the corresponding nodes in L that N points to */
while (N>Link != NULL) {
M>Link = (NodeType *) malloc(sizeof(NodeType));
N = N>Link;
/* create new last node on copy list */
M = M>Link;
/* advance pointers on both lists */
strcpy(M>Airport, N>Airport);
/* copy airport code */
}
/* mark last node of copy as the end of the list L2 */
M>Link = NULL;
/* return pointer to first node of L2 as the function result */
return L2;
}
}

5.
void Reverse(NodeType **L)
{
NodeType *R, *N;
R = NULL;
while (*L != NULL) {
N = *L;
*L = (*L)>Link;
N>Link = R;
R = N;
}
*L = R;

/* initialize R, the reversed list, to be the empty list */


/* let N point to Ls first node */
/* now, let L point to the remainder of L */
/* link N to the rest of R */
/* and make R point to its new first node N */
/* finally, replace L by a pointer to the reversed list R */

6. If L is the null list (meaning L = N U L L ), then, when the condition in


the while-loop is evaluated, the attempt to find the value of
(strcmp(L>Airport, A) != 0 ) will try to dereference the null pointer, NULL .
This error can be fixed by reversing the order of the operands of
the short-circuit & & operator in the while-condition on line 4
giving: while ( (L != NULL) && (strcmp(L>Airport, A) != 0) ) {
7.
NodeType *Concat(NodeType *L1, NodeType *L2)
{
NodeType *N;
if (L1 == NULL) {
return L2;
} else {
N = L1;
/* let N point to the first node of L1 */
while (N>Link != NULL) N = N>Link;
/* find the last node of L1 */
N>Link = L2;
/* set the link of the last node of L1 to L2 */
return L1;
/* return the pointer to the concatenated lists */
}
}

Table of Contents continued

8. If L points to a list consisting of just one node, then the function


call L a s t N o d e ( L ) results in executing a statement that tries to
dereference N U L L . This is another example of a bug found by
checking a boundary case.
9. Suppose L is a list containing only one node. That is, suppose L
contains a pointer to a node whose link is N U L L . Then the special
case on lines 9:12 of Program 2.15 applies, and we free the node * L
points to, after which we need to store a NULL pointer in the
variable L external to the function call. To do this, we need to use
L s external address, &L , as an actual parameter when the function
is called and we need to use *L = NULL inside the function in order to
store NULL in the location to which the actual parameter &L points.
If the function prototype had been void DeleteLastNode(NodeType *L) ,
there would be no way to set the contents of the external variable
L to N U L L in the case of a list consisting of only one node to be
deleted, since the actual parameter passed for the value of L at
the time of the function call would be the address of the first node
on the list, leaving no way to change the contents of the external
variable L after the deletion from a pointer to the first node of the
list to a NULL pointer.

Answers to Review Questions 2.6


1. Nodes having two separate pointer fields can be linked into twoway lists, binary trees, and two-way rings.
2. In symmetrically linked lists, nodes point both to their
predecessors and successors in the list except for the first
node in the list whose predecessor is NULL and the last node of the
list whose successor is NULL .
Left
Link

Airport

Right
Link

Left
Link

BRU

Airport

Right
Link

Left
Link

x2
ORD

Solution to Exercise 2.6


1.
void Delete(NodeType *L)
{
/* Make Ls predecessor point to its successor */
if (L>LeftLink != NULL) {
L>LeftLink>RightLink= L>RightLink;
}

Airport
SAN

Right
Link

Table of Contents continued


/* Make Ls successor point to its predecessor */
if (L>RightLink != NULL) {
L>RightLink>LeftLink= L>LeftLink;
}
/* Dispose of the storage for node L */
free(L);
}

Table of Contents continued

Answers to Review Questions 3.2


1. The base case occurs when a recursive program gives a direct
solution to a subproblem without using any recursive calls to
compute it.
2. Four decomposition methods: (i) First & Rest, where First = m and
Rest = m +1: n ; (ii) Last & All but Last , where Last = n and All but
Last = m : n 1, (iii) Split in Halves , where LeftHalf = m : m i d d l e ,
RightHalf = middle+1: n , and middle = (m + n ) / 2; and (iv) Edges &
Center , where Edges = (m & n ) and Center = m +1: n 1.
3. A call tree is a tree
descendants of each
call at that node.
descendants, and are

with a recursive call at its root, in which the


node show the recursive calls made by the
Calls resulting in base cases have no
the so-called leaves of the call tree.

4. A trace of a recursive function call is a sequence of successive


lines on which the calling expressions for successive recursive
calls are given amidst the values waiting to be combined with the
results returned by those recursive calls. When the base cases are
reached and values are returned directly from them, the trace
shows how the values combine to produce the value returned by
the original function call.
5. The number of different combinations of n things taken k at a time
n
n!
=
.
is given by the formula: k

k!(nk)!
6. Decompose a non-empty list into its Head (which is its first node)
and its Tail (consisting of the rest of the nodes on the list after
the first one).

Solutions to Selected Exercises in Exercises 3.2


1.
double Power(double x, int n)
{
if (n == 0) {
return 1.0;
} else {
return x * Power(x, n 1);
}
}

/* base case, x^0 == 1.0 */


/* recursion step */

2.
double Power(double x, int n)
{
double p;
if (n == 0) {
return 1.0;

/* base case, x**0 = 1.0 */

Table of Contents continued


} else {
p = Power(x, n / 2);
if ( (n % 2) == 0 ) {
return p * p;
} else {
return x * p * p;
}

/* let p == x to the half(n) power */


/* if n was even, return Square(p) */
/* if n was odd, return x*Square(p) */

}
}

3.
int Mult(int m, int n)
{
if (m == 1) {
return n;
} else {
return n + Mult(m 1, n);
}
}

4. Recursive Euclids Algorithm:


int gcd(int m, int n)
{
if (n == 0) {
return m;
/* base case, if remainder is 0, then result is m */
} else {
return gcd(n, m % n) ;
/* recursion step */
}
}

5. First, to prove that gcd(m , n ) terminates, we know that when m is


divided by the divisor n to obtain a quotient q and a remainder r =
m % n , the quantities obtained obey the relation: m = q * n + r ,
where 0 r < n . So, the remainder r is either 0 or a positive
integer less than the divisor n . Thus, there can only be a finite
number (less than n ) of successive non-zero remainders, r , which
decrease by at least 1 before a zero remainder is produced. Once a
zero remainder is obtained, the base case of the recursion (on line
4 of gcd(m , n )) is reached, and the function call terminates. To
prove that gcd( m , n ) returns the greatest common divisor, we note
that on successive calls,
if n = 0, then gcd( m , n ) = m , and
otherwise, gcd( m , n ) = gcd(n , r ). In the latter case, r is related to m
and n by the equation r = m q * n , so any common divisor of the
pair (m , n ) is also a common divisor of the pair (n , r ) including the
greatest common divisor. Consequently, the gcd of successive
remainder pairs is preserved. Finally, when n = 0, gcd(m , n ) = m , so

Table of Contents continued

m is the last non-zero remainder in the sequence of remainder


pairs, all of which were divisible by the gcd. This implies that m
itself is the gcd.
6.
int Product(int m, int n)
{
if (m < n) {
return m * Product(m+1,n);
} else {
return n;
}
}

7. Despite what some conceive to be the elegance and simplicity of


the R e v e r s e function given in the solution below, the overall
solution is a poor one on two counts: (1) The overall running time is
quadratic instead of linear because of repeated scanning and
character copying of the successively smaller tails that are
reversed, and (2) The scratch storage used to accommodate
concatentations and tails is taken from the dynamic storage pool
using malloc , and is never freed after use.
/****
*
* Solution to Exercise 3.2.7 in Chapter 3, page 81.
*
****/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char *EmptyString = "";
/* ------------------------------------------------------------------------------ */
int Empty(char *S)
{
return (S[0] == '\0');
}
/* -------------------------------------------------------------- */
char *Head(char *S)
{
char *t;
t = (char *) malloc(2*sizeof(char));
t[0] = S[0];
t[1] = '\0';
return t;
}
/* -------------------------------------------------------------- */

Table of Contents continued


char *Tail(char *S)
{
char *t, *temp;
int n;
n = strlen(S);
if (n <= 1) {
return EmptyString;
}else {
t = (char *) malloc(n*sizeof(char));
temp = t;
S++;
while ((*t++ = *S++) != '\0')
;
}
return temp;
}
/* -------------------------------------------------------------- */
char *Concat(char *S, char *T)
{
char *P;
char *temp;

/* see Program 8.17 on page 315 */

P = (char *) malloc((1+strlen(S)+strlen(T))*sizeof(char));
temp = P;
while ((*P++ = *S++) != '\0')
;
P--;
while ((*P++ = *T++) != '\0')
;
return temp;
}
/* -------------------------------------------------------------- */
char *Reverse(char *S)
{
char *temp;

/* to Reverse a String S */

if (Empty(S)) {
/* the empty string is returned */
return EmptyString;
/* for the base case */
} else {
return Concat(Reverse(Tail(S)), Head(S));
}
}
/* ------------------------------------------------------------------------------ */
int main(void)
{
char *S = "abcdefg", *S1;

Table of Contents continued


printf("the string S == %s\n",S);
S1 = Reverse(S);
printf("the reverse of S == %s\n",S1);
}
/* ------------------------------------------------------------------------------ */

8. The answer to exercise 8 is not given.


9.
int Length(NodeType *L)
{
if (L == NULL) {
return 0;
} else {
return 1 + Length(L>Link);
}
}

/* to compute the number of */


/* nodes in a linked list L */
/* since the empty list has no nodes in it */
/* length = 1 + length of Ls tail */

10.
int Min2(int A[ ], int m, int n)
{
int MinOfRest;

/* first define an auxiliary function Min2 */

if (m == n) {
return A[m];
} else {
MinOfRest = Min2(A,m+1,n);
if (A[m] < MinOfRest) {
return A[m];
} else {
return MinOfRest;
}
}
}
int Min(int A[ ])
/* to find the smallest integer in an integer array A[0:n1] */
{
return Min2(A,0,n1);
}

11.

int Ack(int m, int n)


/* assume m 0 and n 0 */
{
if (m == 0) {
return (n+1);
} else if (n == 0) {
return Ack(m1,1);
} else {
return Ack(m1,Ack(m,n1));
}
}

12. The answer to exercise 12 depends on the behavior of your C


system.

Table of Contents continued


13. The procedure, P( n ), writes the digits of the non-negative integer
n.
14. The procedure, R( n ), writes the digits of the
order.

integer n in reverse

15. Using the auxiliary function NewtonSqrt(x,epsilon,a) , call Sqrt(x,epsilon) in


what follows (making sure to #include <math.h> beforehand):
double NewtonSqrt(double x, double epsilon, double a)
{
if (fabs(a*a x) <= epsilon) {
return a;
} else {
return NewtonSqrt(x, epsilon, (a + x/a)/2.0);
}
}
double Sqrt(double x, double epsilon)
{
return NewtonSqrt(x,epsilon,x/2.0);
}

16.
int C(int n, int k)
/* where n and k are non-negative integers */
{
if ((k == 0) | | (n == k)) {
return 1;
}else {
return C(n1,k) + C(n1,k1);
}
}

17. The answer to exercise 17 is not given.

Answers to Review Questions 3.3


1. An infinite regress occurs when a recursive program calls itself
endlessly.
2. Two programming errors that can cause infinite regresses are: (1)
a recursive program with no base case, or (2) a recursive program
with a base case that never gets called.
3. An infinite regress causes an unending sequence of recursive calls.
To evaluate each such call, the C run-time system allocates a new
call frame which it allocates in a run-time call-frame storage
region. Thus, the call-frame region will become exhausted when
the run-time system attempts endlessly to allocate new call
frames.
4. If single precision integers were used as the integer
representation of the parameter, n , in Program 3.12, then the call
Factorial(0) would result in the recursive calls Factorial(0),

Table of Contents continued


Factorial(1), Factorial(2), and so on, being made. If, for example,
single precision integers are represented by 16-bit, twos
complement integers, then after descending to the value 32,768,
the values cycle back to 32,767 and descend towards 0 again,
eventually reaching the value 1. When Factorial(1) is called, the
function terminates with the value 1. Then, all the values in the
range 32768:32768 are multiplied together, giving the value 0.
This scenario can happen only if there is enough memory space to
hold all 65536 calls in the call-frame storage region. But many
contemporary computers will have enough memory to do this.

Solution to Exercise 3.3


1. The function call F(2) causes an infinite regress.

Answers to Review Questions 3.4


1. The complexity class that characterizes the recursive solution of
the Towers of Hanoi puzzle is called the exponential complexity
class.
2. The principal disadvantage of the exponential complexity class is
that the running time of the algorithms that fall in this class
require very large running times for all but very small arguments.
Such running times are so large that we cannot expect an answer
from these algorithms given a reasonably sized input for a long,
long time. Thus, we generally try to avoid using such exponential
algorithms.

Solution to Exercise 3.4


1. Since there are 3.1536*107 seconds in one year, and the length of
the instruction sequence is L( n ) = 2n 1, we need to find the
largest n such that
2n 1

3.1536*107.

The largest such n is 24 (which can be determined by finding the


largest n such that n log2 (3.1536*10 7 + 1), where log2 (3.1536*10 7
+ 1) = 24.91049639). Hence, a tower of at most 24 disks can be
moved in a years time.

Table of Contents continued

Answers to Review Questions 4.2


1. A C module M is a program consisting of two separate coordinated
text files, M . h , its interface file (or header file) and M . c , its
implementation file (or source file), such that M provides a
collection of related entities all of which work together to offer a
set of capabilities or to provide a set of services or components
that can be used to help solve some class of problems.
2. The interface file M.h of a module M is a text which declares
entities that are visible to (and hence usable by) external users.
These entities can include declarations of constants, typedefs,
variables, and functions. Only the prototypes of functions are
given (and in them, only the argument types, and not the argument
names, are given).
3. The implementation file M.c of a module M is a program text which
declares local program entities that are private to the module
(and cannot be seen or used by external users), and which also
gives the full declarations of functions whose prototypes are
given in the interface header file, M . h , and which are visible to
external users.
4. To use the services provided by a module M , you place an include
directive which includes the header file M . h at the beginning of
your program, using the syntax #include "M.h" . The effect is as if the
declarations inside M . h had been substituted in your program at
the place the include directive is given. The module M is usually
compiled separately. The extern declarations in M . h tell the linker
how to link in externally defined functions from other modules
compiled separately.

Answers to Review Questions 4.3


1. A priority queue is a collection of prioritized items in which items
can be inserted in any order of ranking of their priorities, but are
removed in highest-to-lowest order of their priorities.
2. A priority queues items could be stored in arrays (or linked lists,
or trees, or many other kinds of data structures acting as
containers). Considering only arrays as containers, the items could
be stored either in sorted or unsorted order (where sorting is
done with respect to the order implied by the items priorities). If
an unsorted array is used, you add a new item at the end of the
array, and to remove an item, you scan the array to locate the

Table of Contents continued


item of highest priority, remove it, and then move the last item
into the hole created by the removal of the highest priority item.
For sorted arrays, to insert an item, you move all items of priority
greater than the item one space to the right, and insert the item
into the hole created. To remove an item you simply remove the
last item in the array.
3. Given the sketch in the answer to the previous question, the
unsorted array representation has a more efficient insertion
operation while the sorted array has a more efficient removal
operation. The reason is that these two efficient operations
operate on only one item, whereas insertion into a sorted array
and removal from an unsorted array potentially require all items
in the array either to be scanned or to be moved.

Answers to Exercises 4.3


1.
80

85

90

|
|
|
|
|
|
|
|
|
|
|
|
|
|

PQItem Remove(PriorityQueue *PQ)


{
PQItem temp;
PQListNode *NodeToFree;
if ( ! Empty(PQ)) {
/* result undefined if PQ empty */
NodeToFree = PQ>ItemList;
/* otherwise, remove the */
temp = NodeToFree>NodeItem;
/* highest priority item */
PQ>ItemList = NodeToFree>Link; /* from the front of the list */
PQ>Count ;
/* decrease the item count */
free(NodeToFree);
/* and free the space for the */
return (temp);
/* node that was removed */
}
}

2. The items in the ItemArray are stored in positions 0:Count 1. Given an


I t e m A r r a y that currently contains Count items, its last item will
therefore be found in the Count 1 position. Thus, to remove the
last item, we first decrement the Count member of the PriorityQueue
struct, using PQ>Count , and then we move the last item at P Q
> I t e m A r r a y [ P Q > C o u n t ] into the hole opened up by removal of the
maximum priority item at the position PQ>ItemArray[MaxIndex] .

Answers to Review Questions 4.4


1. A program shell is a top-level program with holes that invokes
and uses plug-in modules and organizes the operations and
services these modules provide to define the topmost level of
operation of the overall program.

Table of Contents continued


2. Using modules can help organize the work in a software project by
providing a clean way to break the work of the project into
subprojects associated with implementation of the separate
modules.
3. Using modules can help structure a software system design during
the design phase, by first defining only the interfaces (or header
files) of the modules (and not their detailed implementations) so
that the modules fit together cleanly in the overall design. The
overall design will be clean, in general, if it is composed from a
few modules having simple, well-defined interactions.

Answers to Exercises 4.4


1. Tic-Tac-Toe Program Shell:

10

15

20

25

30

35

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

/* the following is the text of a TicTacToe Program Shell */


#include <stdio.h>
#include "TicTacToeUserInterface.h"
#include "MoveCalculationModuleInterface.h"
Move theMove;
Board theBoard;

/* theMove contains current player's move*/


/* theBoard gives the configuration of the game */

int main (void)


{
InitializeAndDisplayTicTacToeBoard( );
do {
GetAndProcessOneEvent( ); /* Gets and displays user's move */
/* if user clicks in a square */
/* on the board. */
/* Otherwise, lets user select X or O */
/* to play, and let's user choose who */
/* plays first the user or the machine */
if (ItIsMachinesTurnToMove( ) {
theMove = CalculateMove(theBoard);
/* done by */
/* MoveCalculationModule*/
Display(theMove);
/* done by */
/* TicTacToeUserInterfaceModule */
}
if (GameIsOver( ) ) {
DisplayResults( );
/* X wins, O wins, or Draw */
AskIfUserWantsToPlayAgain( );
}
} while ( ! UserWantsToQuit( ) );
Shutdown( );
}

Table of Contents continued

Interface file of a module to handle the Tic-Tac-Toe user


interface:

10

15

20

| /* the text of the file "TicTacToeUserInterface.h" */


|
| #include <stdio.h>
|
| typedef
enum { false, true } Boolean ;
| typedef
enum {X, O, Blank} Token;
| typedef
Token Board[3][3];
| typedef
struct {
|
int row;
|
int column;
|
} Move;
|
| extern Boolean ItIsMachinesTurnToMove(void);
| extern Boolean UserWantsToQuit(void);
| extern void InitializeAndDisplayTicTacToeBoard(void);
| extern void GetAndProcessOneEvent(void);
| extern void Display(Move);
| extern Boolean GameIsOver(void);
/* true if there was a win or draw */
| extern void DisplayResults(void);
| extern void AskIfUserWantsToPlayAgain(void);
| extern void Shutdown(void);
|
| /* end of file "TicTacToeUserInterface.h" */

Interface file of a module to calculate the machines move:

| /* the text for the file "MoveCalculationModuleInterface.h" */


|
| #include <stdio.h>
| #include "TicTacToeUserInterface.h" /* included in order to import the */
|
/* typedefs for Move and Board */
|
/* needed below */
| extern Move CalculateMove(Board);
|
| /* end of file "MoveCalculationModuleInterface.h" */

Answers to Review Questions 4.5


1. An available space list is a linked list of unused list nodes that is
organized as a pool of available storage to allocate during the
operation of a list-processing program. It is generally linked
together during the initialization of the program. Freshly allocated
nodes are removed from it, and freed nodes are returned to it
during the execution of the program. It thus provides the basis for
a storage allocation and management policy for user-defined
linked lists.
2. Information hiding occurs when program entities are declared and
used locally within a function or module, preventing them from
being seen and/or used outside the function or module.

Table of Contents continued


3. Information hiding can promote ease of program modification by
confining the region of program text in which changes have to be
made to a local region of the total program text. When entities are
defined locally within a function, or privately inside a module in its
implementation file, then changes in the local or private entities
can be made without interfering with entities outside.
4. A representation-independent notation is a notation which does
not have to be changed when its underlying data representation is
changed. For example, GetLink(N) is a representation-independent
notation for getting the Link of node N in a linked-list, whereas the
three expressions
N > L i n k , L i s t M e m o r y [ N ] . L i n k , and L i n k [ N ] are
representation-dependent notations (from which, by looking at
the notations, one can discover what the underlying data
representations are).
5. If one does not use a representation-independent notation, then
the users of the module will have to use notations that depend on
the data representations chosen, in which case the data
representation isnt hidden properly (since it can be discovered
by studying the notation of use outside the module in which the
representation is supposed to be hidden).
6. Efficiency trades off against generality whenever making a
program more general makes it more costly to execute, or
whenever making it more efficient entails making it less general.
Using a representation-dependent notation makes a program
more efficient but less general than using a representationindependent notation that hides a specific representationdependent data access notation behind a general function call
notation. The latter, though more general, is less efficient because
it incurs the expense of making extra function calls (i.e., those
providing the representation-independent wrapper, as it is
sometimes called, which hides the representation-dependent
notation inside).
7. The representation-independent notation for operating on nodes
of a linked list is less efficient than the particular kinds of
representation-dependent notations because using it incurs the
extra cost of transmitting procedure parameters and making
procedure calls and returns which are extra expenses not
incurred by using the representation-dependent notations.

Answers to Selected Exercises in Exercises 4.5


6. Nothing is wrong with the program. It will work as it is. However,
note that it redeclares the variable, Avail , to be a string variable
local to the function main( ), when an identically named variable is

Table of Contents continued


used in the implementation section of the module, ParallelArrayLists .
This is acceptable, since the variable, A v a i l , used inside the
implementation file " L i s t I m p l e m e n t a t i o n . c " for the Parallel Array
Representation module of Program 4.23 does not interfere with
any identically named locally defined variables inside the main( )
procedure.

Answers to Review Questions 4.6


1. Modules are also called u n i t s or p a c k a g e s in other modern
languages such as Pascal and Ada.
2. Procedural abstraction occurs when one creates a procedure, P( a 1 ,
a 2 , ..., a n ), as a named unit of action. Then, one can use the action
represented by P, knowing only w h a t P does and not h o w P is
implemented. Later, one can change how P is implemented without
changing every instance of Ps use (which could not have happened
if the instructions giving Ps action had been repeated every time
P was used). The use of procedural abstraction therefore promotes
ease of use and change.
3. Data abstraction occurs when one hides the representation of a
data structure and the implementation details of the operations
on it using a representation-independent notation. This enables
the abstract data to be used knowing only what it does, not how its
details are implemented. As with procedural abstraction, so also
with data abstraction, by separating the what from the h o w , both
ease of use and ease of modification are promoted.
4. E n c a p s u l a t i o n consists of hiding program entities (or certain
features of externally visible program entities) inside a
protective wall, called the capsule boundary, so that they can
neither be seen nor used outside the capsule.
5. In summary, using C modules properly can provide for: (a) separate
compilation (which can reduce compilation times since the whole
program doesnt need to be compiled each time), (b) ease of
modification, (c) ease of use of the services provided by the
module, (d) help with software system design, and (e) help in
organizing the work of a software project.

Table of Contents continued

Table of Contents continued

Answers to Review Questions 5.2


1. The method of starting with a top-level design in outline form
and progressively filling in details until an actual program is
developed is called top-down programming. The method of
building general-purpose, reusable software components, and
later on, using these components in system construction
activities, is called bottom-up programming.
2. Suppose that a system we are constructing uses many layers of
data structures and algorithms including top-level abstract data
types (ADTs). Suppose the mechanics of the higher layers are
implemented entirely in terms of ADTs in a representationindependent notation, free from the details of lower-level
representational choices. Further, suppose we do not choose a
representation for the abstract data structures and algorithms in
the higher layers until the very last stage of implementation.
Before choosing, we might even compare several options for
alternative, substitutable data representations based on
efficiency considerations and on the constraints of the problem we
are trying to solve. If we choose to program in this fashion, we say
we have postponed the choice of data representations and
algorithms , since we have delayed choosing them until the last
possible moment.
3. When implementing a program strategy, the goals in the program
strategy can be achieved by supplying more specific programming
details. The process of filling-in more specific details in several
successive steps is called stepwise refinement . The abstract
higher-level, less-detailed program strategies are r e f i n e d by
transforming them into lower-level, more-specific programs.

Solutions to Selected Exercises in Exercise 5.2


1. A complete C program that refines Program Strategies 5.4 5.8,
using a linked row of structs to represent the table is given as
follows:

|
|
|
|
|
|
|
|
|

#include <stdio.h>
#include <stdlib.h>
/* type definition section */
typedef struct RowTag{
int Amount;
int Repetitions;

/* defines types given in Ex. 5.2.1 */


/* on page 153 */

Table of Contents continued


10

15

20

25

30

35

40

45

50

55

60

65

70

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

struct RowTag*Link;
} RowStruct;
typedef RowStruct *RowStructPointer;
typedef int InputArray[6];
/* declarations of variables */
RowStructPointer Table = NULL;
InputArray A;
/* Program 5.5 */
void GetSixInputs(InputArray A)
{
int i;

/* Get six dollar amounts */


/* and put them in InputArray A */

printf("Give six dollar amounts separated by spaces: ");


for (i = 0; i < 6; ++i) {
scanf("%d",&A[i]);

/* read in six dollar amouts */

}
}
/* Refinement of Program Strategy 5.7 */
void InsertAmount(int AmountToinsert)
{
RowStructPointer RowPointer;
int UpdateCompleted;
RowPointer = Table;
UpdateCompleted = 0;

/* UpdateCompleted is a flag variable */

while ((RowPointer != NULL) && ( ! UpdateCompleted)) {


if (RowPointer>Amount != AmountToinsert) {
RowPointer = RowPointer>Link;
} else {
(RowPointer>Repetitions)++;
UpdateCompleted = 1; /* set UpdateCompleted to true */
}
}
if ( ! UpdateCompleted) {
RowPointer = (RowStruct *) malloc(sizeof(RowStruct));
RowPointer>Amount = AmountToinsert;
RowPointer>Repetitions = 1;
RowPointer>Link = Table;
Table = RowPointer;
}
}
/* Refinement of Program Strategy 5.6 */
void MakeTable(int A[])
{

Table of Contents continued

75

80

85

90

95

100

105

110

115

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

int i;
for (i = 0; i < 6; ++i) {
InsertAmount(A[i]);
}
}
/* Refinement of Program Strategy 5.8 */
void FindAndPrintWinner(void)
{
int AmountWon;
RowStructPointer RowPointer;
AmountWon = 0;
RowPointer = Table;
while (RowPointer != NULL) {
if ((RowPointer >Repetitions >= 3) &&
(RowPointer >Amount > AmountWon)) {
AmountWon = RowPointer >Amount;
} else {
RowPointer = RowPointer >Link;
}
}
printf("You Won $% d\n",AmountWon);
}
/* Refinement of Program Strategy 5.4 */
void FindWinningAmount(void)
{
GetSixInputs(A);
MakeTable(A);
FindAndPrintWinner();
}
int main(void)
{
FindWinningAmount( );
}
/* end of main */

2. Data collected in a study by Sackman, Erikson, and Grant showed


there exists a large variance between programmers (of factors up
to twenty) in various measures of programmer performance such
as the time needed to write a program, the time needed to debug
a program, the time and space required for the program to run, and
so forth. For given programmers, however, these measures tended
to cluster together, in the sense that if a programmer took more
time to write a program, s/he tended to take more time to debug
it, it tended to contain more errors, and it tended to run less

Table of Contents continued


efficiently. And, to the contrary, programmers who wrote concise
programs, tended to write and debug them quickly, and the
programs tended both to contain fewer errors and to run more
efficiently. Thus, van Snipes position may have merit when applied
to differences in program quality among small program modules.
However, in large programming projects, the advantages of
separating the system design into representational layers
( i ncl u d i ng su b s titu t a b ility of re p res enta t ions , eas e o f
understanding and maintenance, etc.) can be lost if one makes a
premature representational choice and tries to compress portions
of the system into a shortened form to attain cleverness of
expression. Moreover, if such concise forms rely on clever tricks
that are difficult to understand, it can impose costs on
maintenance programmers who must later understand how the
system works in order to upgrade the system to do something
new and different. Nonetheless, there are clearly situations in
which concise, clever solutions are efficient, relatively errorfree, and easy to understand if properly documented. Careful use
of such solutions can contribute to a systems performance
advantages without being a burden for maintenance programmers.
3. Assuming the amount X is of higher priority than the amount Y if X
Y in the implementation of priority queues by the
PQImplementation.c , we can write:

10

15

20

25

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

/*
* Priority Queue Lottery Solution to Exercise 5.2.3
*/
#include <stdio.h>
#include <stdlib.h>
#include "PQInterface.h"

/* see interface in Program 4.3 */

/* type definitions */
typedef PQItem InputArray[6];
/* declarations of variables */
InputArray A;
PriorityQueue PQ;
/* Program 5.5 */
void GetSixInputs(InputArray A)
{

/* Get six dollar amounts */


/* and put them in array A */

int i;
printf("Give six dollar amounts separated by spaces: ");

Table of Contents continued


30

35

40

45

50

55

60

65

70

75

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

for (i = 0; i < 6; ++i) {


scanf("%d",&A[i]);

/* read in six dollar amouts */

}
}
/* Exercise 5.2.2, pp. 153154 van Snipe's Short Lottery Solution */
/* assume the global InputAray A has been declared */
/* and is used in these routines */
int ThreeOrMore(int Amt)
{
int j;
int k = 0;
for (j = 0; j < 6; ++j) if (A[j] == Amt) k++;
return( k >= 3 );
}

/* Program for Exercise 5.2.3 on page 154 PQ Lottery Solution */


void PQLotterySolution(void)
{
int i, AmountWon;
Initialize(&PQ);

/* Initialize the priority queue, PQ, */


/* to make it empty */
/*Get six inputs from the user */

GetSixInputs(A);
for (i=0; i<6; ++i) {
if ( ThreeOrMore(A[i]) ) Insert(A[i], &PQ);
}
AmountWon = ( Empty(&PQ) ? 0 : Remove(&PQ));
printf("You win $ %d\n",AmountWon);
}
/* main program to test results */
int main(void)
{
/* test priority queue lottery solution */
PQLotterySolution( );
}
/* end of main */

4. The if-condition (( A[i] == A[i+1]) && (A[i+2] == A[i+2] )) can be replaced by


the simpler if-condition ( A[i] == A[i+2] ) in this case because we are
testing for the presence of a run of three consecutive identical
values in a sorted array whose values are given in increasing
order. Here, if the first value in a run of three identical values is
equal to the third (i.e., if ( A[i] == A[i+2] )), then the second member of
the run must be identical to the first and the third. To prove this

Table of Contents continued


mathematically, consider three consecutive array items in
increasing sorted order, A[i] A[i+1] A[i+2]. Now if A[i] == A[i+2], then it is
impossible for either of the strict inequalities A[i] < A[i+1] or A[i+1] <
A[i+2] to hold also. Otherwise, by the transitivity of inequality, we
would have A[i] < A[i+2], which contradicts the known truth of A[i] ==
A[i+2].

Answers to Review Questions 5.3


For all the answers, let us assume that we are trying to prove program
P correct.
1. Assertions are logical statements that specify: (a) the conditions
that hold before a program is executed, (b) the net effect
achieved after executing a program, or (c) the state of affairs
achieved inside a program at various intermediate points during
its execution.
2. The precondition describes the conditions that hold true before P
is executed.
3. The postcondition describes the conditions that hold true after P
is executed.
4. A proof of
deductions
assuming
assertions
performing
execution.
properly to

correctness of program P is a sequence of logical


that establishes the truth of the postcondition,
the truth of the precondition initially, and using
internal to the program which capture effects of
individual actions inside the program during its
Logically rigorous rules of inference must be used
achieve a valid correctness proof.
5. A loop-invariant is an assertion inside a loop that it is always true,
no matter how many times the loop has been executed.
6. Loop-invariants capture the essence of what a loop is supposed to
do and what it maintains always to be true throughout its
execution. Also, loop-invariants along with preconditions and
postconditions may aid us in proving loop termination after a
finite number of steps.

Solutions to Selected Exercises in Exercises 5.3


1. There exists K and there exists n 0 such that if K is greater than 0
and n 0 is greater than 0 then it is true that for all n if n is greater
than n 0 then| g ( n )| < K | f ( n )|. [ N o t e : This condition is used in the
definition of O-notation. cf p. 221. ]

Table of Contents continued


2. There exists k such that 1 k n and for all i, if 1 i n , then A[ k ]
A [ i ] . [ N o t e : this describes A [ k ] as the maximum element in the
array A[1: n ]].
3. [( > 0) [( > 0) [ (|x c|< ) (|f(x) f(c)| < )] ] ].
4. k [((1 k ) (k n )) i [((1 i ) (i n )) (A[k ] A[i] ) ] ]
5. ~ K [ n0 [((K > 0 ) (n0 > 0 )) n[(n > n0 ) (|g(n)| < K|f(n)|)] ] ]
K [ n 0 ~ [~((K > 0 ) (n 0 > 0 )) n [(n > n 0 ) (|g (n )| < K |f(n )|)] ] ]

K [ n0 [~~((K > 0 ) (n0 > 0 )) n [~(n > n 0 ) (|g (n )| < K |f(n )|)] ] ]

K [ n 0 [((K > 0 ) (n 0 > 0 )) n [~~( n > n 0 ) ~(|g (n )| < K |f(n )|)] ] ]

K [ n0 [ (K > 0 ) (n0 > 0 ) n[(n > n0 ) (|g(n)| K|f(n)|)] ] ].

Translating the later into English yields, For any K >0 and any n 0 >0
there exists an integer n greater than n 0 such that |g ( n )| K | f ( n )|. In
essence, this says that if f ( n ) and g ( n ) are positive functions, then
g ( n ) grows so fast as n increases that it is never bounded above by
some constant multiple of f ( n ) for sufficiently large n . This would
be a true statement, for instance, if g ( n ) = n 2 and f ( n ) = n . For
example, for any K > 0 and any n0 > 0, choose n > max (K , n0). Then n >
n 0 and n >K , and multiplying both sides of n > K by n , we get n 2 > K n
which is the same as |g ( n )| > K |f ( n )|, from which it follows that |g ( n )|
K|f(n)|.
6. Loop invariant :

[((0 i ) (i < n )) ( S == (0

j i)

A [ j] ) ]

Precondition : i == 0, S == 0.
Postcondition : the value returned by the function S um == (0 i n
1) A[i ]
The following version of the original program is annotated with
assertions:

10

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

int Sum(InputArray A, int n)


{
int i = 0, S = 0;
{ ( S == 0 ) ( i == 0 ) ( n > 0 ) }
while (i < n) {
{ ( 0 i < n ) ( S == (0 j < i) A[j] ) }
S += A[i];
{ ( 0 i < n ) ( S == (0 j i) A[j] ) }
i ++;
}
{ ( i == n ) ( S == (0 j n1) A[j] ) }
return S
{ ( Sum == (0 j n1) A[j] ) }
}

/* assume n > 0 */

/* precondition */

/* loop invariant */

/* postcondition */

Table of Contents continued

Without loss of generality, we can assume (n > 0) as part of the


precondition. Otherwise, if n 0, the while-loop on lines 6:11 never
executes, and the sum becomes zero, which is the correct value of
the sum of the integers in the array A[ 0: n 1 ] when the range 0:n 1
is empty. (The range 0: n 1 is empty whenever n 0, but if n > 0,
the range 0:n 1 is non-empty and contains at least the item A [ 0 ] .)
We proceed to prove correctness by the method of induction:

Base Case : Before entering the while-loop for the first time, S
== 0 and i == 0. In particular, S == (0 j < i ) A [ j ] == (0 j < 0) A [ j ]
= 0, because there are no integers j in the range 0 j < 0. Hence,
the assertion on line 7 is true on the first trip through the whileloop. Then on line 8, the statement S += A[i] is executed, with the
previous values S == 0 and i == 0. So A[ 0 ] is added to S and S ==
A [ 0 ] == (0 j i) A [ j ] . From this, the truth of the loop invariant on
line 9 follows on the first trip through the loop.
Induction Step : Assume that the invariant holds when we are in
the i th iteration of the loop. Then the invariant on line 9 holds,
namely:
{ ( 0 i < n ) ( S == (0 j i) A[j] ) }

/* the loop invariant */

When the statement i++ on line 10 is executed, the value of i is


increased by 1. At this moment, the assertion ( S == (1 j i) A[j] ) i s
not true for the newly increased value of i , but rather it is true
only for the previous value of i which was (i 1). So to maintain the
truth of this assertion after incrementing i by 1, we need a new
version in which we substitute (i 1) for i , giving:
{ ( S == (0 j i-1) A[j] ) }

Because i and j are integers, this simplifies immediately to:


{ ( S == (0 j < i) A[j] ) }

We now return to the beginning of the while-loop on line 6 to


test the while-condition. If the newly increased value of i is still
less than or equal to n , i.e., provided (i < n ), we enter the whileloop for the (i + 1)st iteration, knowing that the assertion ( i < n ) is
true (because the while-condition was true in order to enter the
while-loop) and that ( S == (0 j < i) A[j] ) which held true for the
incremented value of i at the end of the last trip through the
while-loop. These two facts, together with the fact that (0 i ) ,
since i started at 1 and was possibly incremented on previous

Table of Contents continued


iterations of the loop, establish the truth of the assertion on line
7. Executing the statement S += A[i] on line 8 now adds a new last
term A[i] to the value (S == (0 j < i) A[j] ), yielding the value ( S == (0 j
i) A[j] ), which establishes the truth of the loop invariant on line 9.
When we fall out of the while-loop on the last trip, the value of i
has been incremented to be n , and the value of S on the previous
trip through the loop will have been ( S == (0 j < n) A[j] == (0 j n1) A[j]
), which establishes the truth of the assertion on line 12. Finally,
the value of S is returned as the value of the function S u m on line
13, establishing the validity of Sum == (0 j n1) A[j] on line 14. The
latter can be converted to the exact original postcondition by a
change of dummy variables inside the summation from j to i.
This proves that the program correctly computes the sum of all
elements in the array A[ 0: n 1 ] .
Finally, we need to establish is that the loop terminates. This is
easy to prove because the loop variable i starts at 0 and
thereafter is always incremented. Since n is finite and positive,
the last in a continually increasing sequence of values of i will
eventually reach it, causing termination of the loop in a finite
number of iterations.

7. Let us prove this by induction as well. Here is the outline.

Base case : Fact(0) == 1, which the program computes correctly,


since the input n = = 0 causes the if-condition (n > 0) on line 7 to
evaluate to false, after which the statement in the else-part on
line 13 is executed, returning the result 1.
Induction step : Let us assume that Fact computes the factorial
for all integers which are less than n .
i [ (0 i < n ) (Fact(i) == 1*2*3*...*i ) ]

We now attempt to prove that factorial of n


computed by Fact.

is correctly

Taking i == (n 1) in the inductive assumption, we get


Fact( n 1) == 1*2*3*...*(n 1 )
Substituting this value for Fact(n 1) on the right side of the
assignment statement Fact == n * Fact(n 1) on line 9 of the program,
and executing this assignment statement yields the value
n *(1*2*3*...*( n 1))== 1*2*3*...*( n 1)* n for Fact(n ).
Finally, we know that since n is finite and non-negative, Fact will
terminate after a finite number of recursions, because each
successive recursion diminishes the value of n by 1 until n reaches

Table of Contents continued


the value 0 causing the test (n > 0) to fail and causing the function
to terminate execution.

Answers to Review Questions 5.4


1. Program transformations can be thought of as exchange laws that
permit replacement of one form of a program with an equivalent
one.
2. Yes.
3. One transformation is tail recursion elimination . Another is
replacing a function call by an appropriately substituted function
body.

Solutions to Selected Exercises in Exercise 5.4


1.

|
|
|
|
|
|
|
|
|

void ReverseString(char *S, int m, int n)


{
char c;

/* to reverse the characters */


/* S[m:n] in string S */

while (m < n) {
/* while the center has characters to reverse */
c = S[m]; S[m] = S[n]; S[n] = c;
/* swap the edges, then move */
m++; n ;
/* the edge indices toward the center */
}
}

In one measurement experiment the iterative string reversal


procedure shown in the answer above was compared with the
recursive string reversal procedure given in the problem
statement in Exercise 5.4.1 on page 172. Both procedures were
timed when performing 10,000 reversals of a string S containing
the 26 letters of the alphabet. The iterative reversal procedure
took an average of 0.0102 ticks per reversal, and the recursive
reversal procedure took an average of 0.0137 ticks per reversal.
Consequently, the recursive version took 34.3% more time per
reversal than the iterative version, on the average. [ Note: a tick is
a 60th of a second, so 0.0102 ticks = 0.17 milliseconds and 0.0137
ticks = 0.228 milliseconds. ]

Answers to Review Questions 5.5


1. Unit testing consists of testing individual functions in isolation to
see if they work on all expected classes of inputs. I n t e g r a t i o n
testing tests the coordinated execution of functions, acting
together in the context of the conditions expected for actual
system operations.

Table of Contents continued


2. Acceptance testing is a practice, sometimes specified in software
acquisition contracts, in which a completed software system must
pass a specified test in order to be accepted for delivery by the
client. R e g r e s s i o n
t e s t i n g is a practice, often used in the
maintenance phase of the software lifecycle, in which, after
changing a system, you retest it thoroughly to make sure that
everything that used to work still works in the new version.
3. In top-down testing , the top-levels of a system are tested before
testing the components that they use. In bottom-up testing , the
components of the system are tested before testing other parts
of the system that use them.
4. A stub is a subroutine that stands in place of another subroutine
(that will be written later), and fakes its output on certain test
cases. A test driver for a subroutine S, is a function that
enumerates test cases for S, calls S for each test case, checks the
outputs afterward to see that they are correct, and notes any
discrepancies in a printed test result report.
5. A formatted debugging aid is a routine that prints a data
structure. Early in the history of software engineering, it was
discovered that debugging times for a software project could
often be cut in half if formatted debugging aids were implemented
first and then were used later when implementing and testing the
rest of the system.

Table of Contents continued

Solutions to Selected Exercises in Exercise 5.5


1.

CalculatorProgramShell:
CalculatorProgramShell

S2

CalculatorModule:
InitializeAndDisplayCalculator

GetAndProcessOneEvent

UserSubmittedAnExpression

Display

UserWantsToQuit

Shutdown

S1

YourCalculationModule:
Evaluate

S1

+ functions of YourCalculatorUnit

2.

PriorityQueueSorting program:
PriorityQueueSort

S2

PQImplementation.c:
Initialize
Full

Empty
Insert

Remove

S1

PQTypes.h:
PQItem

PQListNode

PriorityQueue

Answers to Review Questions 5.6

S1

Table of Contents continued


1. It is frequently hard to anticipate where the inefficiencies in a
program lie. Consequently, it is important first to measure where
a system is spending its time, and then to concentrate on tuning
(i.e., changing so as to improve) the parts that account for most of
the execution time.
2. An execution profile tool (or profiler) measures where a program
is spending its time during execution. A profiler develops data
that tells you what percentage of the sample execution time is
spent in various parts of the code. Its results are sometimes
presented graphically in the form of a histogram.
3. Experience shows that most programs consume running time in a
fashion that is unevenly distributed throughout the code.
4. Tuning is a cyclical process of rewriting portions of a program to
attempt improvement and then measuring the results to see if the
expected improvements were achieved.
5. Small differences, such as declaring frequently used variables of
loops as double precision integers or using floating point division
of two integers instead of integer division (on a machine with
simulated floating point instructions) can increase the time
consumed by a program.

Solutions to Selected Exercises in Exercise 5.6


1. On line 8 of the program the floor call is used after the division
operator /. The division operator / operator with floating
point denominator 2.0 uses floating point division and returns a
floating point result after first converting its integer- valued
numerator (m + n) to a floating point number. The result is then
truncated using the floor function to eliminate the fractional part of
the positive floating point result, and is then converted back to
integer form using the type cast with (int) . Machines which simulate
floating point operations use on the order of 30 to 50 non-floating
point instructions to simulate each floating point instruction.
Hence, on a machine with simulated floating point, it would be
better to use integer division (by using an integer denominator 2
in Middle = (m + n) / 2. This would eliminate the costly simulated
floating point operations.
2. [ Note: This exercise can be used as the basis for an informative
laboratory experiment in a CS2 Lab section. ]

Answers to Review Questions 5.7

Table of Contents continued


1. The case for advocating the building of software from reusable
software components is based on its economic advantage. The
cost of building software is an exponential function of its size.
Since software size is the biggest cost driver in a software
project, if you can reduce the size of what you have to build you
can best reduce the cost. Hence, if you can build your software
from reusable parts, you can often reduce the overall cost of
system construction substantially.
2. An equation estimating the effort required to build a software
system in the most familiar class of software systems in Boehms
C OCOMO model gives the number of person months (PM) required to
build a system in terms of the size of the system, as follows:
PM = 2.4*(KDSI)1.05
where the quantity KDSI refers to the Kilo-Delivered Source
Instructions in the system.
3. In bottom-up programming the strategy is to implement generalpurpose software components first, and then to assemble them
together into higher-level systems. Under some economic
conditions associated with developing proprietary reusable
software component libraries bottom-up programming may be
useful because it yields proprietary advantage useful for building
other systems and cultivates in-house expertise.

Solutions to Selected Exercises in Exercise 5.7


1. It is cheaper to implement VisiPhysics by buying and using the two
libraries. The economics can be worked out using Boehms
equation:
Cost of creating VisiPhysics from scratch =
((2.4*(56) 1.05 /12)*$100,000) = $1,369,706
Cost of creating VisiPhysics using libraries =
((2.4*(32) 1.05 /12)*$100,000)+$300,000 =

$1,061,093.

Thus, the savings realized through software reuse are $308,613 in


this instance.

Answers to Review Questions 5.8


1. It is better to have few global variables and to have good
communication between subroutines to minimize the chances that
separate routines will accidentally clobber global variables used
as communication devices between other subroutines. Without
global variables, data flows cleanly between subroutines and their
callers. This helps make subroutines fit together nicely in the

Table of Contents continued


system design and reduces the risk that use of global variables as
communication devices poses with respect to accidental
clobbering of communicated values through unintended
interference.
2. Use of named constants is sensible because it enables easy change
of constant values in just one location. Moreover, all uses of the
constant value are changed accordingly. This minimizes the effort
needed to locate and change each of a number of separate
constant values spread throughout the program text and it
minimizes the risk that some of the separately expressed
constant values spread throughout the program text would be
given incorrectly.
3. Two reasons are:
(a) If everyone programs in the same style, each programmer
will be able
more easily to read code
written by the others.
(b) Error detection during code reviews will be easier, since
mistakes will
be easier to spot in a
single uniform style than in many separate styles.
4. There are a lot of people who will use, read, or modify a big
software system with a long service lifetime. Some may use it
extensively, exploring every bit of power that the software can
give them in their work. These people will require detailed
documentation of the softwares user features. Others may use
only minimal simple features of the software, i.e., only the parts
needed in their work. These people will require an easy-to-read
tutorial or a simple, easy-to-comprehend version of the users
manual. Others will be maintenance programmers who need
detailed explanations of how the code works. Yet others will be
managers who need good overviews of how the systems parts fit
together, but who would get lost in the details if details were all
that were available. A single form of documentation cannot
simultaneously satisfy all of these needs.

Solutions to Selected Exercises in 5.8


1. This program computes the greatest common divisor ( gcd ) of m
and n . Hence, we can redefine the function and write it as follows:

|
|
|
|
|

int gcd(int m, int m)


{
if (n == 0) {
return m;
} else {

/* where gcd stands for the */


/* greatest common divisor */
/* the gcd is m, when n == 0 */
/* otherwise, compute the gcd of n and the */

Table of Contents continued


|
|
|

return gcd (n, m % n)

/* remainder of m after division by n */

}
}

2. This program writes out the successive digits of its non-negative


integer input. Hence, we can call it PrintDigits :

10

|
|
|
|
|
|
|
|
|
|
|
|
|
|

void PrintADigit(int digit)


/*prints the character corresponding */
{
/* to an integer value == digit */
printf("%c", (char)((int)('0') + digit));
/*not followed by a newline */
}
void PrintDigits(int n)
{
if (n < 10) {
PrintADigit(n);
} else {
PrintDigits(n / 10);
PrintADigit(n % 10);
}
}

/*let n be a non-negative integer */


/*if there is just one digit in n, then */
/*print it */
/*otherwise, first print the digits other */
/*than the last, and then */
/*print the last digit */

3. Use the input sequence: 0.3, 0.4, 1.1, 2.3, 99999, 99999. This sequence
forces the program to add 99999 to the sum giving a wrong answer,
25000.20.
4. Use a transformation that eliminates the duplicated assignment
statement,
|

PlayersMove = GetMoveFromUser( );

and places it first inside a while-loop that uses a break s t a t e m e n t


to exit. Here is the transformed program:

|
|
|
|
|
|
|
|

while (1) {
PlayersMove = GetMoveFromUser( );
if (PlayerMove == QuitSignal) {
break;
}
DisplayPlayersMoveOnTheBoard( );
MakeMachineMove( );
}

Table of Contents continued

Answers to Review Questions 6.2


1. When we pose the question, What do we use for a yardstick? we
mean how can we measure and compare algorithms meaningfully
when the same algorithm runs at different speeds and will require
different amounts of space when run on different computers or
when implemented in different programming languages?
2. When an algorithm is written in different programming languages
and implemented on different machines, both the execution time
and the space consumed may differ. However, when implementing
an algorithm on different machines using different software, what
does not change is the family of growth curves which plot resource
consumption as a function of the size of the algorithms inputs. The
basic shape of these curves will remain the same within the family
of resource consumption curves.
3. The part of the curves equation that dominates the value of the
function (i.e., accounts for most of the functions value) when the
input size is large is called the dominant term of the function. For
example, suppose a n 2 + b n + c is the equation of a curve that fits
some data. In this equation an 2 is the dominant term. Supposing for
a moment that a = b = c = 1, the term a n 2 accounts for more
than 98% of the value of a n 2 + b n + c whenever n 50, and the
decision to ignore the contribution of the lesser terms, b n + c ,
would introduce an error of less than 2%. For n 100, the error is
less than 1%.
4. This approach is perhaps justified because, for large input values,
the dominant term determines almost all of the value of the
function, and because the lesser terms contribute negligible
amounts that do not influence the functions value appreciably
i.e., if the contributions of the lesser terms were ignored for large
input sizes, only minor errors would result. Thus the approach
could be viewed as purchasing simplicity and ease of
comprehension in exchange for the admission of negligible errors
of net effect.

Answer to Exercise 6.2


1. The data in Table 5.36 for the recursive SelectionSort Program 5.29
are fit by the equation:

f( n ) = 0.00024148*n 2 + 0.00136*n + 0.001

Table of Contents continued


The data in Table 5.36 for the iterative SelectionSort Program 5.35 are
fit by the equation:

f( n ) = 0.000221*n 2 + 0.0002*n + 0.015


Thus, the data are closely matched by curves that are instances of
the basic running-time equation, f ( n ) = an 2 + bn + c , given in Table
6.5, for various values of the coefficients a , b , and c . Hence, we can
conclude that the basic resource consumption curves for various
versions of SelectionSort are the same. The curves fitting the data in
Tables 5.36, 6.3, and 6.5 are independent of S e l e c t i o n S o r t s
implementation features i.e., whether or not recursion or
iteration was used, or whether while-statements, for-statements,
or if-statements were used in C. They are also independent of the
programming language used and the particular brand of computer
on which SelectionSort was run.

Answers to Review Questions 6.3


1. Constant
Logarithmic
Linear

O(1)
O(log n)
O(n)

n log n
O(n log n)
Quadratic
O(n2)
Cubic
O(n3)
where c > 0 is a finite constant.
Exponential
O(cn)
2. No, the complexity class tends to give useful comparative
information only when the input size of the algorithm is
comparatively large.
3. If we are using small data sets (in which the input size is small)
then the complexity class of the algorithm does not matter very
much. For small n , an O(n 2 ) and an O(n log n ) algorithm have almost
the same running time. On the other hand, for large sorting
problems, an O( n log n ) sorting algorithm may have dramatic
advantages in running time over an O( n 2 ) sorting algorithm (as
illustrated by the answer to Ex. 6.3.2 below). HeapSort , MergeSort , and
QuickSort are O( n log n ) algorithms. SelectionSort and InsertionSort a r e
O( n 2 ) sorting algorithms.
4. Using algorithms that take exponential space or exponential
running time is usually impractical for all but small input values.
However, for small inputs occasionally it makes sense to use an
exponential algorithm. For example, the input sizes for which an

Table of Contents continued


exponential algorithm running in time 2n will perform better than a
cubic algorithm running in time n 3 , can be computed by solving the
inequality: 2 n < n 3 for n . (This inequality holds for 2 n 9 but not
for n 10).

Solutions to Selected Exercises in Exercises 6.3


1. If the computation requires 2n steps at one microsecond per step,
the largest problem that can be solved before the sun burns out is
of size 77. Since there are 3.1536*10 7 seconds per year, 10 6
microseconds per second, and 5*10 9 years before the sun burns
out, there are (3.1536*10 7 ) * ( 1 0 6 ) * ( 5 * 1 0 9 ) = 1.5768 * 10 2 3
microseconds left before the sun burns out. Thus, we need to find
the largest value of n such that 2n is less than 1.5768 * 1023 . This
is gotten by finding the largest n for which the following inequality
holds true:
2 n < 1.5768 * 1023 ,

which holds if and only if (iff)

ln (2n ) < ln(1.5768 * 1023 ), which, in turn, holds iff


n * ln 2 < ln(1.5768 * 1023 ),
which, in turn, holds iff
n < ln(1.5768 * 1023 ) /(ln 2),
which, in turn, holds iff

n < 77.0613459
2. S e l e c t i o n S o r t will take (262144*262144)/(10 6 *60) = 1145.3246
minutes (or a little over 19 hours) and Q u i c k S o r t will take
262144*log 2 (262244)/(10 6 *60) = 0.0786432 minutes. Applying a
cost of $100 per minute, SelectionSort costs $114,532.46 and QuickSort
costs $7.86. QuickSort seems a tad cheaper under these particular
economic circumstances.
3. Many practical parsing algorithms used in contemporary compilers
run in time O(n ), even though theoretically it is not known how to
perform completely general parsing in time less than O( n 2.8 ).
4. There are substring searching algorithms that run O( n ) time. There
are even some that run in sublinear time, meaning that they run in
fewer than n steps for a text being searched that is n characters
long. There are also obvious, but inefficient, text searching
algorithms that run in time proportional to m * n , where m is the
length of the search pattern and n is the length of the text being
searched.

Answers to Review Questions 6.4

Table of Contents continued


5. As can be seen from Equation A.22 on page 708 of the M a t h
Reference appendix, changing the base of a logarithm from logb x
to logc x involves multiplying logb x by a constant whose value is
log c b . Hence, the logarithms of x using various different bases
differ from one another only by constant multiples. The O-notation
for O( f ( x ))is insensitive to constant multiples. Therefore, the Onotation for O(log n ) is conventionally given without a base.
6. Once the constant of proportionality is removed from a constant
function, all that is left is 1. Hence, we get O(1).

Solutions to Selected Exercises in Exercises 6.4


1. f(n ) is O(n log n ).
2. f( n ) is O(n 3 ).
3. From the definition of the O-notation, we need to find K and n 0 such
that:
5n3+3n2+4n+8 K n3, n n0.
Letting n 0 = 1, n n 0 is the same as n 1, which is the same as 1
n . Now, multiplying both sides of 1 n by n gives n n 2 and
multiplying both sides by n 2 gives n 2 n 3 . And putting 1 n
together with n n 2 gives 1 n 2 , and putting 1 n 2 together with n 2
n 3 gives 1 n 3 . Similarly, multiplying both sides of 1 n 2 by n
gives n n 3 . So if n 1, we have four consequences: (a) n 2 n 3 , (b) n
n 3 , (c) 1 n 3 , and (d) n 3 n 3 . Now,
multiplying inequality (a) by 3 gives

3 n 2 3n 3 ,

multiplying inequality (b) by 4 gives

4 n 4n 3 ,

multiplying inequality (c) by 8 gives

multiplying inequality (d) by 5 gives

5 n 3 5n 3 .

Then adding up these inequalities gives:

8n 3 ,

and

5n3+3n2+4n+8 20 n3.

Hence, if we choose K = 20, then for all n 1 (i.e., n n 0 ) we have


proven that 5n 3 +3 n 2 +4 n +8 K n 3 , which formally establishes that
5 n 3 +3 n 2 +4 n +8 is O(n 3 ).
4. From the definition of the O-notation, we need to find K and n 0 such
that
7*2 n +9* n 3 K 2 n , n n0.
This is the same as asserting that 7+9*n 3 /2 n K , n n0, which,
in turn, is the same as asserting that n 3 /2 n (K 7)/9, n n0.

Table of Contents continued


If we choose K = 16, then (K 7)/9 = (16 7)/9 = 9/9 = 1. So,we
can then ask if we can find some n 0 such that n n 0 , n 3 /2 n 1.
Choosing n 0 = 10 is sufficient because 10 3 /2 1 0 = 1000/1024 =
0.9765625 1. Now we can work backwards to put the final result
together, as follows.
We know that for n 0 = 10, any n n 0 , implies n 3 2n , which is the
same as n 3 1*2 n , which is the same as, n 3 (16 7)/9*2n , which,
in turn, is the same as: 9*n 3 16*2n 7*2n , which is the same as,
7 * 2 n + 9*n 3 16*2n . This last inequality is equivalent to 7*2 n +
9* n 3 K *2 n , which holds n n 0 . Thus, we have proven formally that
7*2 n +9* n 3 is O(2n ).
5. From the definition of the O-notation, we need to find K and n 0 such
that
log( n +1) K *log n , n > n0.
First, choose n 0 = 2 and then choose K = 2. Thus, for any n n 0 , we
have 2 n 2n n 2 (n + n ) n 2 (n + 1) n 2 . Now taking base b
logarithms of both sides of the latter inequality (for any arbitrary
base b > 1) gives, logb ( n +1) logb ( n 2 ). But since logb ( n 2 ) = 2*logb
n , the latter inequality becomes logb ( n +1) 2*logb ( n ), for all n n 0 .
Hence log(n +1) is O(log n ).

Answers to Review Questions 6.5


1. O(log n )
O(n)
O( n log n )

Binary Search
Sequential Searching
MergeSort

O(n2)

SelectionSort

O(2n)

Solution to the Towers of Hanoi problem

2. One way of solving the recurrence is to use unrolling and summing.


First, because the general recurrence exchanges T( n ) for an
expression involving T( n /2), it is useful to assume n is of the
general form n = 2k , for k 0, because this is the only form n can
take which allows it to be evenly divided by 2 repeatedly. Then we
can rewrite T(n ) = 3n + 2*T(n /2) in the form, T(2k ) = 3*2k + 2*T(2k
1 ). If we unroll T(2k ) a number of times, say i times, and then
simplify, we notice that it takes the form, T(2 k ) = i * ( 3 * 2 k ) +
2 i *T(2 k i ). Then, when i = k , we get T(2k ) = 3*k *2 k + 2k *T(2 k k ) .
But since, T(2k k ) = T(20 ) = T(1) = 2, the latter can be rewritten as
T(2 k ) = 3*k *2 k + 2*2k . Now solving n = 2k for k (which yields k = lg

Table of Contents continued

n ) and then substituting lg n for k and n for 2k in the last form of


the equation for T(2 k ) yields the final solution, T( n ) = 3 n lg n+
2 n.
Another way to solve the recurrence is to use the general
solution table (given as Table 6.21 on page 241 and also on the
bottom of the last panel of the Math Survival Guide ). After
matching the equation for T( n ) to the general form given in the
solution table, we find that a = 2, b = 3, c = 0, d = 2, and p = 2.
Then, since, d = p , we select the general solution form that
applies under the condition d = p , substitute the values of a , b , c ,
d , and p , and simplify to obtain, T(n ) = 3 n lg n+ 2 n .
Yet a third way of solving the recurrence relations is to use the
method of summing factors explained on pages 725 to 726 of the
Math Reference appendix.
3. Recurrence relations give a relationship between the value of a
general term for n , T( n ), and the values of T( k ) for various
arguments k less than n . In the method of unrolling and summing,
we expand the general term for T( n ) repeatedly, substituting
expressions for T( n ) given by the right side of the general
recurrence relation for T( n ), until the expansion stops by reaching
base cases. This process of expansion is called unrolling .Then we
substitute the values for the base cases and sum up the unrolled
expression into a closed form, which is then simplified to get the
general solution.

Solutions to Selected Exercises in Exercises 6.5


2. Starting with Recurrence Relations 6.6 (on p. 236) which are,
T(1) = a
T( n ) = b n + c + d T(n 1)
and unrolling, we get:
T( n ) = b n + c + d T(n 1)
= b n + c + d [b(n1) + c + d T(n 1)]
= b n + c + d b(n1) + d c + d2T(n 2)] .

Table of Contents continued


Reexpressing the last line with powers of d i used as
multipliers, gives:
= d 0 b (n ) + d 0c +

d 1 b (n 1) + d 1 c + d2T(n 2) .
Then, unrolling some more, we get:
= d 0 b (n ) + d 0c +
d 1 b (n 1) + d 1 c +
d 2 b (n 2) + d 2 c +
:
:
d i1 b ( n (i 1)) + d i1 c
+
d i T( n i)
:
:
So letting i = n 1, the unrolling stops on the line reaching
the base case:
dn2b(n (n 2)) + dn2c + dn1 T(n (n 1)) .
But, T(n (n 1)) = T(1) = a . So the last line can be rewritten
as:
dn2b(n (n 2)) + dn2c + adn1 .
Then, summing all the columns in the unrolled equation gives:
n2

T(n) = b

d i( n i) + c

i= 0

n2

di

+ adn1 .

i= 0

And this can be slightly rearranged to yield Equation A, as


follows:
n2

T(n) =

adn1 + (bn + c)

di

n2

i= 0

i di .

i= 0

(A)

[ Note : We can also solve recurrence relations 6.6 using the method
of summing factors. ] Each of the following lines is multiplied by
progressively higher powers of d . That is, when we see the
notation: d i *{T(n i ) d T(n i 1) = b ( n i ) + c }, it signifies
multiplying all terms on both sides of the equation by d i:

T(n) dT(n1)

d *{T(n 1) d T(n 2)
d2*{T(n2) dT(n3)
:
:
:
:
d n 2*{T(2) d T(1)

=b n

+c

= b (n 1)+ c }
= b (n 2)+ c }
:
:
= b (2)

+c }

Table of Contents continued


When we sum the above equations, we get a telescoping sum on
the left in which all but the first and last terms cancel each other:
n2

T(n) dn1T(1) = b

n2

d i .

d i( n i) + c

i= 0

i= 0

The latter can again be rearranged to give Equation A, again:


n2

T(n) =

adn1 + (bn + c)

n2

di b

i= 0

i di .

i= 0

(A)

Now that we have gotten Equation A by two separate methods,


lets derive the two special cases given in Table 6.18 (on page
236). Namely, the cases for d = 1 and d 1.
First, lets solve the last Equation A for the case that d = 1.
Noting that, if d = 1 then d i = 1 for any i , we can rewrite Equation
A as:
n2

T(n) = a +

(b n + c )

n2

i= 0

which

i= 0

simplifies first to,


T( n ) = a +

(bn + c )( n 1)

( n 2 )( n 1 )
2

A fter more simplification, the last line yields the final form given

in Table 6.18:
T( n ) = (b /2 ) n 2 + (b /2 + c ) n + (a b c ),

provided d = 1.

Now we consider the case that d 1. Returning to Equation A


above, we see that some terms are geometric progressions. For
example,
n2

d i is a geometric progression with a sum equal to

i= 0

dn1 1
d 1

.
Also, one of the terms in Equation A is of the form ( 1 i n ) i * d i .
Using the hint that ( 1 i n ) i * d i = [ d /( d 1) 2 ] * [ ( n d n 1) d n + 1 ] , and
taking this sum with an upper limit of (n 2 ) instead of n , yields:
n2

id i

= b * [ d /( d 1) 2 ] * [ (( n 2) d ( n 2)1) d n 2 +1 ] .

i= 0

Now, substituting these results for d i and id i in Equation A


gives:

Table of Contents continued


T( n ) = ad n 1 + (bn + c )

dn1 1
b * [ d /( d 1) 2 ] * [ (( n 2) d ( n 2 )
d 1

1)dn2+1 ] .
The last equation is in closed form. After (lots of) simplification,
this equation can be rearranged to take the final form in Table
6.18:

T( n ) = \B
b
n +
1

(\F(a b c, d ) + \F(b + c,d 1 ) + \F(b, ( d 1 ) 2 )) d n


c ( b + c ) d

, for d 1.

2
(
d

1
)

3. We proceed to set up and solve some recurrence relations for the


running time. Let T(n ) be the time required to sort an array A[0:n1]
of n items. In the abstract strategy for recursive SelectionSort,
given in Exercise 6.5.3, if n == 1 and we call SelectionSort(A,1) to sort
the array with one item, A[0:0] , the function returns without doing
any sorting, after performing the test on line 3. It takes a constant
amount of time, say a , to call and return from the function and to
perform the test on line 3. Hence, we can write a base case for our
recurrence relations, T(1) = a . Proceeding now to characterize the
general recurrence T( n ), for selection sorting an array A[0:n1] o f
size n , we annotate the abstract strategy with costs, as follows:

|
|
|
|
|
|
|
|

void SelectionSort(SortingArray A, int n)


/* To sort A[0:n1] */
{
if ( n > 1) {
/* if the array A[0:n1] has more than one item */
Cost = bn + c1; /* Find position, p, of smallest item in A[0:n1] */
Cost = c2;
/* Exchange A[p] and A[n1] */
Cost = T(n 1); /* Sort the rest of the array A[0:n2] recursively */
}
}

We comment on these annotations as follows. First, on line 4, the


cost of scanning all n items in the array A [ 0 : n 1 ] to locate the
position p of the smallest item takes linear time of the form b n +
c 1 , (where b and c 1 are constants). It then takes a constant amount
of time, c 2 , on line 5 to exchange the items in A[p] and A[n1] . Finally,
on line 6, the SelectionSort procedure is called recursively using
the procedure call SelectionSort(A, n 1). This takes time T(n 1).
Letting c = c 1 + c 2 , and summing the costs on lines 4:6 yields:
T(n) = b n + c + T(n 1) .
Putting this together with T(1) = a , we get the following
recurrence relations:
T(1) = a
T(n) = b n + c + T(n 1) .

Table of Contents continued


Consulting Table 6.18 on page 236 (or the solution on the top of
the last panel of the Math Survival Guide) and noting that d = 1, we
can read the solution to the running time equation as follows:
b
b

T( n ) = 2 n 2 + 2 + c n + (a b c).
Thus, the running time for recursive SelectionSort is O(n 2 ).

4. The recurrence relations (from Eqs. (6.9) on p. 240) are:


T(1) = a
T( n ) = b n + c + d T( n / p ),

where p > 1, d > 0

If we assume n is evenly divisible repeatedly by powers of p , then


n takes the form n = p k , which implies k = l o g p n . Using this
assumption, then unrolling and summing gives Equation (6.10) on
page 240, which is rewritten as Equation (B) below:
T( n ) =

d 0 b n /p 0 + d 0 c +
+ d1 c +
+ d2 c +
+ d3 c +

d 1 b n /p 1
d 2 b n /p 2
d 3 b n /p 3
...
d ( k 1) b n / p ( k

1)

+ d (k

k1

T(n) =

bn ( d j/ p j )

1)

c + d k T( n / p k )

k1

j= 0

d j + d k a

(B)

j= 0

We now derive the four special cases of the latter equation


given in the table of solutions (in Table 6.21, on p. 241).

First Case:
Assume d = p . Then d k = p k = n , k = logp n , and (d j/ p j) = 1. So,
Equation B simplifies to:
T( n ) =

d k 1

bnk + c

d 1

+ adk .

And substituting n for d k and logp n , for k in the latter gives:


c ( n 1)
T( n ) =
bn log p n +
+ an
d 1
which can be rearranged into the final form given in Table 6.21:

Table of Contents continued


T( n ) =

c
c
bn logp n + a +
n

d 1
d 1

Second Case: d = 1
Assume d = 1. Then d k = 1, p k = n , k = logp n , and (d j/ p j) = (1/p j) .
So, Equation B simplifies to:
T( n ) =

1 / p k 1

bn

1/p 1

1/n 1

bn

1 / p 1

(n 1)/n

bn

( p 1 ) / p

+ ck + a
+ c logp n + a
+ c logp n + a

bp
(n 1) + c logp n + a
(p 1 )
which simplifies to the final form:
=

T( n ) =

bp

bp
+ c logp n + a
p

.
1

Third Case: d = 1, b = 0
Assume d = 1 and b = 0. Then d k = 1, p k = n , and k = logp n . So,
Equation B simplifies to:
T( n ) = ck + a
and substituting logp n for k yields the final form:
T( n ) = c logp n + a .
Fourth Case: d 1, d p
Assume d 1 and d p . Then p k = n and k = logp n . So, Equation B
simplifies to:
T( n ) =

d k / p k 1

bn

d
/
p

( d k n )/ n

bn

(
d

p
)/
p

d k 1

+ c

dk

+ c

bp
c
+
+ a d k
p
d 1

+ adk

c
1

+ adk

bp

n c

d 1

p
d

To get the final form of the solution, we need to reexpress d k in


the form n log p d . To see why this holds, we note the following series
of equivalences:
1 = 1 logp n / l o g p n = log p d / l o g p d
l o g p d * l o g p n = logp n * l o g p d logp n l o g p d = logp d l o g p n
n log p d = d log p n n log p d = d k . Hence, we can write the final form:

Table of Contents continued


T( n ) =

\B(a +

\ F ( b p , d p ) + \F(c , d 1 )) n l o g p d

b p
.
n

p
d 1

5. (a)

O( n log n )

(b)

O(n)

(c)

O(log n )

(d)

O( n logp d ) or O(n ) depending on which one is greater.

6. Equations 6.7 on page 237 match the conditions for the first line of
Table 6.21 on page 241, if we set d = p = 2. The solution resulting
from substituting 2 for both d and p in the first line of Table 6.2.1
is:
T( n ) = b n log2 n + (a + c ) n c
This equation gives the running time of MergeSort and is identical
Equation 6.8 on page 239.
7. The proof that log 2 ( n + 1) < log2 ( n ) + 1, for all n > 1, is given on
pages 709 to 710 of the Math Reference Appendix . (A brief
summary of the steps in this proof is: (n > 1) (1 < n ) ( n + 1) <
2 n lg(n + 1) < lg(2 n ) lg(n +1) < lg 2 + lg n lg( n +1) < lg n + 1).
Now lets take the running time equation for binary search given
by Equation 6.14 on page 245:
C( n ) =

2 log2 (n + 1) 3 +

2 log2 (n + 1)/n

Applying the inequality log2 ( n +1) < log2 n + 1 to this, we get,


C( n ) < 2 log2 n + 2 3 + 2 (log2 n )/n + 2/n .
Noting that both of the terms 2(log2 n )/ n and 2/n are less than 1
for n > 5, we can simplify the last line above to,
C(n) < 2 log2 n + 1 .
Because the dominant term on the right side of the latter
inequality is 2 log 2 n , we can conclude that the average number of
comparisons used in binary searching is O(log n ).

Answers to Review Questions 6.6


1. It is often, though not always, inappropriate to apply the
conclusions normally derived from O-notation when small problem
sizes are being considered.

Table of Contents continued


2. A good idea is to try measurement and tuning to optimize the
performance of the algorithm for small-sized problems.

Solutions to Selected Exercises in Exercises 6.6


1. [ Note: This exercise makes a good laboratory assignment for a CS2
course. ]

Table of Contents continued

Table of Contents continued

Answers to Review Questions 7.2


1. Stacks are useful for processing nested structures because we can
use a stack to interrupt processing of a given level of a nested
structure in order to process an immediately deeper level. When
we have finished processing the immediately deeper level, we can
then resume the interrupted processing of the given level. In
order for this to work, we need to push a record on the stack which
provides enough information to resume processing of the original
interrupted level. If the immediately deeper level itself contains
additional nested levels of the structure, all records for
processing nested substructures will have been popped from the
stack by the time we return to the original interrupted level to
resume processing. Thus, stacks can accommodate processing of
nested structures with an arbitrary number of levels of nested
substructures.
2. Some examples of nested structures: subroutine calls within
subroutines, subexpressions inside algebraic expressions,
outlines of numbered items containing suboutlines of numbered
items, and finally, blocks within blocks and loops inside loops in a
program written in a programming language.
3. Push-down automata can recognize sentences in context-free
languages according to the theory of formal languages. Stacks are
used as memory-structures in push-down automata, which play a
key role in formal language theory. As a consequence, syntax
analyzers for programming languages often employ push-down
stacks to accomplish recognition of the structure of computer
programs during compilation.
4. Yes, stacks can be used to process function calls. Stacks are often
used in various algorithms to hold information about postponed
obligations for further processing. A stack of activation records
can be used to keep track of a sequence of function calls during
program execution. Each time a function call is made, an activation
record for the call is pushed onto the stack. This activation record
contains information about how to resume execution of its caller
after its own execution is finished. A stack is a good data
structure for this purpose, since function calls are dynamically
nested (in time, during the running of a program).

Solutions to Selected Exercises in Exercises 7.2

Table of Contents continued


1. Whenever a tray of weight L is added to the stack, the spring is
displaced downward by distance d = L , which is exactly equal to
the thickness d of the tray. Therefore, the top of the stack
remains at the same height.
2. Let them grow toward each other, starting at opposite ends of the
array. E.g., let S 1 grow from A [ 0 ] upward so that S1 s second
element will be placed in A[ 1 ] , and so forth. S2 should grow from
A [ n 1] downward so that S2 s second element will be placed in
A [ n 2 ] , and so forth. The positions of the tops of the two stacks
should be maintained in order to determine when space in A [ 0: n 1 ]
is exhausted.

Table of Contents continued

Answers to Review Questions 7.3


1. Two different structuring methods used in C are: the formation of
structs and arrays. In addition, using pointers to components and
embedding pointers inside other composite data structures, such
as structs or arrays, enables us to create a class of linked data
structures in C.
2. We can define a C module consisting of a separately compiled
source file in C that specifies the underlying representation of an
ADT and that implements the required abstract ADT operations.
The functions exported by this module can be specified in a header
file for the module giving e x t e r n declarations for the function
prototypes being exported in the module interface. The header
file can be included, using an #include directive at the beginning of
another C source program using the ADT. The ADT module itself
may import an ItemType specifying the type of item that can be used
inside the ADT from another header file giving the typedefs of data
structures used in the data representation of the ADT.
3. You can be sure that you have used proper modular programming
practices when defining and using an ADT if you do not have to
make changes in the ADTs interface design when changing the
underlying ADT representation. If the ADTs data representation
has to be changed, and you have followed proper modular
programming practices, then you will not have to make global
changes external to the program text defining the ADT. Rather, the
changes will be local to the ADTs defining program text. Modular
programming will also ensure that the details of the
implementation are hidden and the ADT will be used only via
features defined and made externally accessible in its interface.
4. No. By looking at the interface files in Programs 7.3 and 7.4 you
cannot tell whether the underlying data representations for stacks
and queues are linked or sequential. In fact, Programs 7.8 and 7.9
given later in Chapter 7 specify sequential and linked stack
representations sharing the same Stack ADT interface given in
Program 7.3, and Programs 7.19 and 7.20 specify sequential and
linked queue representations sharing the same Queue ADT
interface given in Program 7.4.

Solutions to Selected Exercises in Exercises 7.3


1. Here is the representation of an ordered sequence <a , b , c > as a set
<a, b, c> = { {2,b}, {1,a}, {3,c} }.

Table of Contents continued


This gives us method for representing an ordered sequence of real
numbers. The integer 2 defines the position at which the real
number b has to be placed in the ordered sequence of real
numbers. No matter where in an unordered set {2,b } appears (such
as before {1, a } in the above example), its position in the ordered
sequence is unambiguously defined.
2. The representation specified in the answer to Exercise 7.3.1 would
not be efficient to use in order to insert or delete a new last item
because all unordered pairs of the form {i , x i } would have to be
scanned either to determine the number n of the last item to
delete or to determine the number n + 1 to use in a new pair {n + 1 ,
x n + 1 } to be added. Likewise, it would be inefficient to use if you
wanted to delete the first item { 1 , x 1 } because, after deleting the
first item, all other items { i , x i } would have to be renumbered as { i
1, x i}, forcing you to scan and change the entire remainder of the
set in order to delete the first item. Consequently, the suggested
representation would be suitable neither for stacks nor for
queues. However, if you bundled the set of unordered pairs as a
member in a struct having an additional integer MaxCount member,
you could always remember the number of the largest numbered
item, making it efficient to add and delete last items, and
producing a suitable stack representation. A similar consideration
could bundle a set of unordered pairs as a member inside a struct
having other integer members to remember the current numbers
of the Front and Rear items of a queue, leading to an efficient queue
representation. Finally, instead of using structs with integer
members, you could save the MaxCount of the stack and the Front and
Rear item numbers of a queue inside specially configured ordered
pairs, such as {1, MaxCount } or {1, Front } and {2, Rear }, leading to a
way to represent stacks and queues efficiently using only
unordered sets. (Here, it is assumed that you can efficently find,
delete, replace, and insert an unordered pair { i , x i } in a set of
unordered pairs S , given only i as a search key. The hash table
repesentations studied in Chapter 11 provide one possible basis
for representing such operations efficiently.)

Answers to Review Questions 7.4


1. The last-in, first-out property is crucial for the stack
implementation of the parenthesis matcher because when we scan
a sequence of properly nested pairs of parentheses, brackets, and
braces, and push left parentheses (, left brackets [ and left
braces { onto a stack, the last-in left-item X must match the
next non-nested right-item in the input and therefore X will be
first-out. No, a queue with a first-in, first-out property would

Table of Contents continued


not work, because it would cause the parenthesis matcher to
recognize improperly nested parenthesis sequences, such as
( [ { ) ] } in which the sequence of left-items ( [ { gets put
into the queue, and then is removed, one-by-one, in the order: (
to match ) in the input, followed by [ to match ] in the input,
followed finally by { to match } in the input.
2. Not only does Program 7.5 use a Stack ADT, it also uses only the
stack operations defined in the Stack ADTs interface without
assuming anything about the stacks implementation. The stack
implementation, in turn, is completely hidden inside the Stack ADT
implementation module. Thus we can switch Stack ADT modules
sharing the same abstract stack operations in their interfaces, but
having different hidden stack representations one based on a
linked stack representation and the other based on a sequential
stack representation without affecting Program 7.5s validity. In
this sense, the two stack representations can be used
interchangeably, and we have achieved the property of
substitutability of representations.

Solutions to Selected Exercises in Exercises 7.4


2. Replace lines 41:45 with the following lines:

45

50

|
|
|
|
|
|
|
|
|
|
|
|
|
|

if ( d == '('

| | d == '[' | | d == '{' ) {

if ( Full(&ParenStack) ) {
printf("Results inconclusive. Stack overflow during processing.\n");
return;
} else {
Push(d, &ParenStack);
}
} else if ( d == ')' | |

d == ']' | | d == '}' ) {

3. This method will not work because it will accept as valid the
improperly nested sequence of parentheses given in the following
counterexample: } ( ) {. For this sequence, BraceCount will go to 1
when the first brace is encountered and will later return to 0 when
the last brace is encountered. This leaves BraceCount at 0, but the
parentheses are not matched. Another counterexample is
( [ { ) ] } which will leave all counts zero at the end, but which
is not well-formed

Table of Contents continued

Answers to Review Question 7.5


a b * c 5 / 1 3 / ^.
2. The infix translation is: ((x (y + z )) / (a * b ))^2.
3. Parentheses in infix notation are needed to specify the extent of
the operands of an operator in cases where the assumed
precedence of the operators would give an incorrect result in the
absence of parentheses. For example, to specify that the sum (a +
b ) is the left operand of the multiplication operator in (a + b ) * c ,
we need to use parentheses, because in the absence of
parentheses, as in writing, a + b * c , the left operand of the
multiplication operator would be assumed only to be b , since the
precedence of * is assumed to be higher than the precedence of +
when interpreting ordinary infix notation. In the latter case, the
absence of parentheses causes the multiplication of b and c to be
performed before the addition of a and b * c , whereas in the former
case, the addition of a and b is performed before the
multiplication of ( a + b ) and c . The order of operations (i.e.,
multiplication before addition v e r s u s
addition before
multiplication) is therefore determined by the presence or
absence of parentheses. This proves that parentheses are needed
in infix notation to determine the order of performance of
operations.
By contrast, when using postfix notation, the order of
operations is entirely determined by the left-to-right order of
the operators in a postfix expression. Each operator performs an
operation on the most recently determined operands when
evaluating the postfix expression from left-to-right. So no
parentheses are needed to determine the order of operations
when postfix is used.
4. The topmost positions of a stack contain the most recently
determined operands when evaluating a postfix expression from
left-to-right. When an operator is applied to its operands, the
most recently evaluated operands are popped from the stack, and
the result of applying the operator to the operands is pushed back
onto the stack. Thus, the last-in, first-out property of the stack is
ideally suited to the evaluation of postfix expressions. A queue
would not work properly in this application, because the alreadyevaluated operands of an operator would be removed from the
queue in least-recent to most-recent order, which is the wrong
order to use when attempting to apply an operator to the mostrecently evaluated operands.
1. The postfix translation is:

Table of Contents continued

Solutions to Selected Exercises in Exercises 7.5


1. The program will print out 2 as the answer. To verify that this is
so, the corresponding infix-expression can be determined and
then can be evaluated as a cross-check on the correct answer:
4 2

(((6*7)2)/5)^(1/3) =

2
1/3 =
^(1/3) = 8
5

8 = 2

2. Program 7.7 can be modified to handle the errors: (1) stack


overflow, (2) division by zero, (3) malformed postfix expression
with too many operators or operands, and (4) illegal character in
postfix expression, as shown in the following program which is an
extension of Program 7.7:
/*
* Exercise 7.5.2 Interpreting a Postfix String with Error Checking p. 270
*/
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <ctype.h>
#include <string.h>
#include "StackInterface.h"

/* contains atof */
/* contains log and exp */
/* contains isdigit(d) */

/* Assume that the types and operations defining a stack ADT are defined */
/* in "StackInterface.h". The source file "StackImplementation.c" */
/* should be compiled independently and then linked with this program */
Stack EvalStack;
char *PostfixString;

/* where ItemType = float */

void InterpretPostfix(void)
{
float LeftOperand, RightOperand, Result;
int i;

/* the index of the ith character in the PostfixString */

char c;

/* c = ith character of the input string */

char *s = "x";

/* s will hold a null-terminated string in which */


/* s[0] will hold c, for use in an atof conversion */

InitializeStack(&EvalStack);
for (i = 0; i < strlen(PostfixString); ++i) {
s[0] = c = PostfixString[i];
if (isdigit(c)) {

/* s[0] = c = ith character of input string */


/* if c is a digit, push c's value onto stack */

if ( Full(&EvalStack) ) {
printf("Stack full. Postfix Evaluation could not be completed.\n");

Table of Contents continued


return;
} else {
Push((float)atof(s),&EvalStack);
}
} else if (c=='+' || c=='' || c=='*' || c=='/' || c=='^') {
if ( Empty(&EvalStack) ) {
printf("Malformed postfix input string. Too many operators\n");
printf("and too few operands.\n");
return;
} else {
Pop(&EvalStack,&RightOperand);
if ( Empty(&EvalStack) ) {
printf("Malformed postfix input string. Too many operators\n");
printf("and too few operands.\n");
return;
} else {
Pop(&EvalStack,&LeftOperand);
switch (c) {
/* perform the operation */
case '+': Push(LeftOperand+RightOperand,&EvalStack);
break;
case '': Push(LeftOperand-RightOperand,&EvalStack);
break;
case '*': Push(LeftOperand*RightOperand,&EvalStack);
break;
case '/': if (RightOperand != 0.0) {
Push(LeftOperand/RightOperand,&EvalStack);
} else {
printf("Attempt to divide by zero.\n");
return;
}
break;
case '^': Push(exp(log(LeftOperand)*RightOperand),&EvalStack);
break;
default:
break;
}
}
}
} else {
printf("Illegal character '%c' in postfix expression.\n",c);
return;
}
} /* end for */
Pop(&EvalStack,&Result);

/* remove final result from stack */

if ( Empty(&EvalStack) ) {
printf("Value of postfix expression = %f\n", Result);
} else {
printf("Malformed postfix string.\n");
printf("Too many operands and not enough operators.\n");
}
}
int main(void)

/* and print it */

Table of Contents continued


{
PostfixString = (char*) malloc(20);
while (1) {
printf("Please input a postfix expression or 'q' to quit: ");
scanf("%s",PostfixString);
if (PostfixString[0] == 'q') break;
InterpretPostfix();
putchar('\n');
}
}

Answers to Review Questions 7.6


1. If the (hidden) data representation given for an ADT can be
switched to another data representation while the program text of
the external users of the ADT does not have to be changed, then
the ADT has a substitutable representation.
2. This means that the Stack ADTs notation for external users does
not depend on its implementation in any way. If external users of a
Stack ADT use only a representation-independent notation, then
stack representations can be substituted for one another without
having to change the program text of the external stack users.
3. Programs 7.8 and 7.9 give sequential and linked stack
implementations, respectively. They both export stack operations
that match the abstract operations in the Stack ADT interface
given in Figure 7.3 on p. 262. Program 7.5, which checks for
balanced parentheses, and Program 7.7, which interprets postfix
expressions, are programs that use the abstract Stack ADT
operations of Figure 7.3. Consequently, it is possible to substitute
either of the sequential or linked stack implementations given in
Programs 7.8 and 7.9 into the initial declaration parts of Programs
7.5 and 7.7 and to have both of the latter programs work without
change. This illustrates both the substitutability of data
representations and good modular programming practices.

Solutions to Selected Exercises in Exercises 7.6


4. The following program implements the mirror image language
recognizer:
/*
* Exercise 7.6.4 Recognizing Strings in the Mirror Image Language. p. 275
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "StackInterface.h"

/* Note: ItemType == char */

/* Assume that the types and operations defining a stack ADT are defined */

Table of Contents continued


/* in "StackInterface.h". The source file "StackImplementation.c" */
/* should be compiled independently and then linked with this program */
char *InputString;
Boolean Recognize(char *A)
{

/* returns true iff the string A */


/* is in the language */

char c;
int i,L;
Stack S;
Boolean Match;
L = strlen(A);
/* check first to see that the string A is spelled only with the allowable */
/* letters 'a', 'b', 'c', and 'm' and that its length is at least 3 */
Match = true;
for (i = 0; i < L; ++i) {
if (! (A[i] == 'a' | | A[i] == 'b' | | A[i] == 'c' | | A[i] == 'm') ) {
Match = false;
}
}
if (L < 3) Match = false;
if ( Match ) {
c = A[0]; i = 0;
InitializeStack(&S);
while ( (c != 'm') && (i < L) ) {
Push(c,&S);
i++;
if (i < L) c = A[i];
}

/* initialize S to be the empty stack */


/* push the character c onto the stack */

if (c != 'm') {
Match = false;
} else {
while ( (i < L1 /* not at end of string A */) && (Match /* == true */) ) {
if ( Empty(&S) ) {
Match = false;
} else {
Pop(&S,&c);
i++;
if (c != A[i]) {
/* match the remaining */
Match = false; /* characters with those from the stack */
}
}
}
if (Match && ! Empty(&S)) Match = false;
}
/* end if (c != M) */
}
/* end if (Match) */
return (Match);
}
/* end Recognize */

Table of Contents continued


int main(void)
{
Boolean result;
InputString = (char*) malloc(20);
while (1) {
printf("Please input a sentence to recognize or 'q' to quit: ");
scanf("%s",InputString);
if (InputString[0] == 'q') break;
result = Recognize(InputString);
printf( (result ? "true" : "false" ) );
putchar('\n');
}
}

Answers to Review Questions 7.7


1. The stack space in a run-time C system is limited to a finite
portion of memory. Each time a function is called during the
execution of a C program, a (non-empty) call-frame is placed on
top of Cs run-time execution stack. When a non-terminating
recursive function calls itself, the C run-time system attempts to
allocate an unending sequence of call-frames on top of its runtime stack. This process causes all of the stack space to be used
up.

Solutions to Selected Exercises in Exercises 7.7


1. In a stack frame, the place reserved for returning the value of a
function call is at the bottom of the stack frame. This stack frame
sits just above the previous stack-top. When a stack frame for a
function call is popped off the stack, the stack pointer can be set
to point to the functions return value sitting in bottom-most
position in the stack frame. Likewise, when evaluating an
expression, intermediate values used in the calculation of the
expressions value can be pushed and popped on top of the stack.
When the final value of the expression is calculated, it will sit on
top of the stack, awaiting its consumption by an operation of some
sort. Thus, when an operand value appears on the stack, it could
have come either from evaluating an expression or from
evaluating a function call. In either case, it sits immediately above
the previous stack top as if it had been pushed onto the stack.

Answers to Review Questions 7.8

Table of Contents continued


1. The disadvantage of having just one pointer to the Front item of a
linked queue representation is that to insert a new item on the
Rear of the queue, we have to start at the Front item and follow links
until we reach the last item in order to link on a new Rear item. This
takes time O( n ) if there are n items in the queue. If we save a
pointer to the current Rear of the queue, we can insert a new rear
item in time O(1), instead.
2. It is possible to have such a queue representation by linking the
queue items in the one-way linked list in reverse order such that
the R e a r item links to the next-to-last item, which links to the
next-to-next-to-last item, etc., until, finally, the F r o n t item is
given last in the order of linkage. Even though all items in the
queue are accessible in this reverse linked representation, the
operation of removing the first item in the queue takes time O(n )
for a queue of n items, and is therefore not as efficient as it is in
more efficient queue representations in which removal takes time
O(1).

Solutions to Selected Exercises in Exercises 7.8


1. The InitializeQueue and Insert procedure are the only ones that need to
be changed. In the InitializeQueue procedure, in Program 7.19 on page
283, we need to delete line 31, which sets Q>Rear = 0. (Also delete
the declaration of the Rear member of the Queue struct on line 10 of
Program 7.19 on page 282.) In the Insert procedure, we need only
substitute the expression for the value of R e a r given by the
relationship stated in the problem, Rear = (Front+Count) %
MAXQUEUESIZE.
50

55

|
|
|
|
|
|
|
|
|

void Insert(ItemType R, Queue *Q);


{
if (Q>Count == MAXQUEUESIZE ) {
SystemError("attempt to insert item into full Queue");
} else {
Q>Items[(Q>Front + Q>Count) % MAXQUEUESIZE ] = R;
++(Q>Count);
}
}

These changes constitute a tradeoff of time for space, because we


reduced the space in each Queue record by one integer field at the
expense of spending more time to compute the location of the
rear of the queue, each time we needed to insert a new item.

Table of Contents continued


2. The Hint given in the problem statement gives the solution away.
Some of the changes are as follows. In the Remove procedure in
Program 7.19 on page 283, replace lines 65:66 as follows:
65

|
|

if (Q>Front == Q>Rear) {
SystemError("attempt to remove item from empty Queue");

Then in the Insert procedure, replace lines 52:53 as follows:


|
|

if (Q>Front == ((Q>Rear+1) % MAXQUEUESIZE ) {


SystemError("attempt to insert item into a full Queue");

Then delete the Count field from the declaration of the Queue record
on line 8 and remove all operations on the Count field in the queue
functions. The definitions of E m p t y ( & Q ) and F u l l ( & Q ) have to be
modified in a fashion similar to that given immediately above for
lines 52:53 and 65:66.
3. Just remove the items from the front of the queue one-by-one and
push them onto the stack until the queue becomes empty. Then
pop the items off the stack one-by-one and insert them into the
queue until the stack becomes empty, as follows:

10

15

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

void ReverseQueue(Queue *Q)


{
Stack *S;
ItemType F;
InitializeStack(&S);
while ( ! Empty(&Q) ) {
Remove(&Q, &F);
Push(F, &S);
}
while ( ! Empty(&S) ) {
Pop(&S, &F);
Insert(F, &Q);
}
}

4. The queue is defined to be empty if and only if Rear == NULL . If Rear


is not NULL , then the queue is non-empty and, by convention, we let
the queue node struct for the rear node link to the node for the
front item of the queue, rather than having a N U L L link field in the
rear queue node, as is normal in the one-way-linked queue
representation. To insert an item into the rear of the queue, we
insert a new queue node after the current rear item and before the
front item. To remove an item from a non-empty queue, we will
have to consider two cases: (1) if there is only one item in the
queue, we remove the only item and set Rear = NULL ; (2) If there is
more than one item, we follow links around the circular list until

Table of Contents continued


finding the predecessor node P for the rear node, i.e., the node
whose Link field points to Rear and then we set Rear = P, remove the
former Rear node, and set the link of P to point to the Front node that
the link in the Rear node used to point to.
Here is a program that will remove an item node from the queue:

10

15

20

25

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

void Remove(Queue *Q, ItemType *F)


{
QueueNode *Prev;
Prev = Q>Rear;

/* Prev will eventually be set to */


/* the predecessor of Q>Rear */

if ( Prev == NULL ) {
/* if the Queue is empty, report error */
SystemError("attempt to remove item from empty queue");
} else if (Prev>Link == Prev) {
/* if there is only one node in Q */
/* free it and set Q>Rear to NULL */
*F = Prev>Item;
Q>Rear = NULL;
free(Prev);
} else {
/* otherwise */
while (Prev>Link != Q> Rear) {
/* find the predecessor */
Prev = Prev>Link;
/* of Q>Rear */
}
*F = Q>Rear> Item;
Prev>Link = Q> Rear>Link;
/* link Prev to front of queue */
free(Q>Rear);
/* free old Q>Rear */
Q>Rear = Prev;
/* and set new Q>Rear to Prev */
}
}

5. Answer not given.


6. This representation requires that the whole queue be shifted down
whenever we do a R e m o v e operation. This increases the time
complexity of the R e m o v e operation from O(1) to O( n ) and is
therefore inefficient.

Answers to Review Questions 7.9


1. Queues are useful for regulating the flow of tasks within a system,
especially when tasks must be processed by several kinds of
servers. Queues are commonly used in operating systems for this
purpose. Additionally, queues are used to synchronize the
exchange of data between two concurrent processes running at
different speeds. Print buffers and printer spoolers offer two
common examples of such synchronization. Queues are also useful
in setting up simulations and models of systems in which clients
arrive and are given service.

Table of Contents continued


2. When we use a queuing system simulation to model an actual
system we wish to study, we try to model each queue in the realworld system with a queue in the simulation model. We try to
model the arrival rate of clients entering queues to await service
at various servers. We try to model accurately the service times
taken by clients when being served by servers and we try to
measure how well the overall system performs. For example, we
may be interested in measuring: (a) the average waiting time to
obtain service, (b) the variance in the waiting times, (c) the
average length of the queues and (d) the throughput of the system.
All these output measures can be determined and may be
descriptively accurate if the system is modeled correctly.

Solution to Selected Exercises 7.9


1. Answer not given.

Table of Contents continued

Answers to Review Questions 8.2


1. A list is a sequence of items. Some operations usually permitted
on lists are selecting the i t h item; insertions, deletions and
replacements of items in arbitrary places; finding the length of a
list; and determining whether or not a list is empty.
2. If a packed array is used to represent a list then when an item
needs to be added to the list or deleted from it, all the items
beyond the point of insertion or deletion must be shifted in order
to maintain the lists contiguous sequential representation.
Another potential difficulty relates to the fact that the array size
must be determined in advance of storing list items. If lists are
used in circumstances in which we need to handle unpredictable
growth in the amount of data, it is inconvenient to have to define
the array size before knowing how big the array might need to be
to store potential future list items. On the other hand, if we
initially define arrays that are very large and use only a portion of
them, we risk wasting a considerable amount of space. Another
potential inefficiency relates to the need to pack and unpack list
items if several list items are stored in packed form in a single
array item A[i].
3. It would be advantageous to use a sequential array to represent a
list if the application requires frequent access to the ith item of
the list as compared to insertions or deletions and if a limit on the
list size is known in advance and is never violated during the
running of the program.
4. A circular linked list has the advantage that any node on it is
accessible from any other node. Hence, it could be used when each
node has to be reached from some other node in the list. For
example, in circular lists containing two or more items, it is
always possible to delete the node before a given node N starting
only with a pointer to node N. (However, a note of caution is in
order here: If an external pointer is being maintained that points
to one of the nodes on the circular list, it is important to be able
to update this external pointer when it points to the node being
deleted.)

Solutions to Selected Exercises in Exercises 8.2


1. Consider an arbitrary item, x i . We have to shift left all items after
item x i up until the end of the array, n . Hence, the number of left
shifts to delete the i th item is S(i ) = (n i ). We now compute the

Table of Contents continued


total number of left shifts, assuming that we delete items x i in all
possible positions i of the array.
( 1 i n ) S(i) =

= ( 1 i n ) (n i )
= ( 1 i n ) n ( 1 i n ) i
= n 2 n( n +1)/2
= n ( n 1)/2
Now, to average over all n items of the array, we divide the latter
total by n , getting:
(1/ n )* n ( n 1)/2 = (n 1)/2
Therefore, the average time for deletion in such an array is O(n ).
2. By an analysis similar to that for Exercise 1, the average time
required for insertion is O(n ). The analysis goes as follows: If we
insert a new item after the last, or n th , item, we have to shift 0
items to the right. If we insert a new item after the (n 1)st item,
we have to shift 1 item to the right. In general, if we have to insert
a new item after the i th item, we have to shift (n i ) items to the
right. Finally, if we have to insert a new item before the first item,
we have to shift n items to the right. The total time to shift items
for insertions in all (n + 1) possible positions (from before the
first to after the last) is a sum of the form 0 + 1 + 2 + ... + n =
n ( n +1)/2. Dividing by n + 1 yields n /2 shifts on the average. Hence,
the average number of shifts is O( n ). Since the shifting time
dominates the time required to do insertion, the average insertion
operation takes time O(n ).
3. Some arguments supporting the results in Table 8.5 are as follows:
(a) Finding the length of L

Sequential Rep .: O(1). Since we explicitly store the count of the


number of
items in the list all we have to do is access the
value of the count to get the
list length.
Linked Rep .: O(n ). We have to traverse the entire list to count
the number of
linked nodes in it.
(b) Inserting a new first item

Sequential Rep .: O(n ). To insert a new first item we have to shift


all other
elements in the array one space to the right.
Linked Rep .: O(1). All we have to do is link the new first item to
the previous
first item and then change the pointer to
the old first list item to point to the
new first item.
(c) Deleting the last item

Table of Contents continued

Sequential Rep.: O(1). We need only decrement the counter which


tells us the
number of items in the list.
Linked Rep .: O(n ). We have to traverse the entire list to find the
predecessor of
the last item. Then we have
to delete the last item and set its predecessors
link
to
NULL. The dominant time is the traversal time O( n ).
(d) Replacing the i th item
Sequential Rep.: O(1). Replace the ith item directly with A[i] = R.
Linked Rep. : O( n ). Access the i t h node, starting from the
beginning of the list
and then replace its item. The
average must be taken over all i from 1 to n .
The
total
number of pointers to follow to access all i items, starting with a
pointer to the first node, is 1 + 2 + 3 + ... + n = n *( n + 1)/2. So
the average is
(n +1)/2 which is O(n ).
(e) Deleting the ith item
Sequential Rep. : O(n ). You have to shift (n 1) items leftward
after deleting
the first item. You have to
shift (n i ) items leftward after deleting the i th
item.
Consequently, the total number of left shifts after deleting each of
the
n items is (n 1) + (n 2) + ... + 1 + 0 = n *( n 1)/2. So the
average number
of shifts is n *( n 1)/2 which
is O(n).
Linked Rep.: O(n ). To access the predecessor of the ith node and
link it to the
successor of the i t h node requires
following i pointers, starting with a
pointer to the first
node of the linked list. The average of i for i in the
range 1 i n is O(n).
4. Each node in the linked list requires p bytes for the pointer and q
bytes for the item. Hence the space requirement for a linked list
of n nodes is n *( p + q ). An array of size MaxSize requires q bytes
for each item and there are up to MaxSize number of items. Hence
the space preallocated for the array is q *MaxSize. Now we set the
space requirement for the linked list of length n equal to the
space requirement for the array and solve for n to get the result
given by Equation (8.1) on p. 302, as follows:
n *( p + q ) = q * M a x S i z e
q
n =
* MaxSize
(p + q )
5. Assuming that queue nodes containing the queue items are linked
to one another in the order of front queue items to rear queue
items, the Queue ADT would be represented best by Daisee
Chaynes design. The reason for this choice is that the Front of the

Table of Contents continued


queue can be easily accessed from the R e a r of the queue by
referring to the item pointed to by the Link field of the node that
the Rear pointer references. Alf Witts design is not efficient
because the process of insertion would require traversing the
entire length of the queue before coming to the R e a r pointer to
perform the insertion. You might think that you could make Witts
design work equally as well as Chaynes by linking the nodes for
the queue items in the order of rear queue items to front queue
items, but such is not the case. With such reverse linking, you can
easily insert a new rear item, but removing the front item
requires traversing nodes on the circular list in the order of rearto-front items to find the predecessor of the front node, in order
to link it to the rear node. Hence, Chaynes representation is
superior no matter which linking direction is used.
6. The implementations of the List ADT operations on the one-way
circular linked-list representation are given in the following
complete C program:
/***
*
***/

Exercise 8.2.6, page 305 Circular List Operations

#include <stdio.h>
#include <stdlib.h>
typedef int ItemType;
typedef struct NodeTag {
ItemType
struct NodeTag
} ListNode;
void Initialize (ListNode **L)
{
*L = NULL;
}

Item;
*Link;

/* Initialize L to be the empty list. */

Boolean Empty(ListNode *L)


{
return (L == NULL);
}
int ListLength(ListNode *L)
{
int i;
ListNode *Temp;
if (L == NULL) {
return 0;
} else {

/* if the list is empty */


/* its length is zero */
/* otherwise, traverse list until */

Table of Contents continued


i = 1;
Temp = L;
while (Temp>Link != L) {
i++;
Temp = Temp>Link;
}
return i;

/* the last node is reached */

/* return the length of the list */

}
}
ListNode *Select(int i,ListNode *L)
{
int j;
ListNode *Temp;
if (L == NULL) {
return NULL;
} else {
j = 1;
Temp = L;
while ((i != j) && (Temp>Link != L)) {
/* find node */
Temp = Temp>Link;
/* you are looking for */
j++;
/* by advancing i nodes in the list */
}
if (i == j) {
return Temp;
} else {
return NULL;
}
}
}
void Replace (int i, ItemType Y, ListNode *L)
{
ListNode *Temp;
Temp = Select(i,L);
if (Temp != NULL) Temp>Item = Y;
}
void DeleteFirst (ListNode **L)
{
ListNode *First, *Last;
First = *L;
if (First != NULL) {
/* if list is nonempty */
if (First>Link == First) {
/* if list has only one node */
free(First);
/* recycle storage for the node */
*L = NULL;
/* and set the list pointer in L to NULL */
} else {
/* list had more than one node */
Last = First;
while (Last>Link != First) {
/* locate last node */
Last = Last>Link;
}
*L = Last>Link = First>Link; /* link around first and reset *L */
free(First);
/* recycle storage for deleted node */
}
}

Table of Contents continued


}
void Delete(int i, ListNode **L)
{
ListNode *Predecessor, *N;
if (i == 1) {
DeleteFirst(L);
} else {
Predecessor = Select(i1,*L); /* find predecessor of node N to delete */
if ( (Predecessor != NULL) && ( (N = Predecessor>Link) != *L) ) {
Predecessor>Link = N>Link;
/* link around node to delete */
free(N);
/* recycle storage for deleted node */
}
}
}

void InsertFirst(ItemType Y, ListNode **L)


{
ListNode *NewNode,*Last;
NewNode= (ListNode *) malloc (sizeof(ListNode));
NewNode>Item = Y;
if (*L == NULL) {
*L = NewNode;
NewNode>Link = NewNode;
} else {
Last = *L;
while (Last>Link != *L) {
Last = Last>Link;
}
Last>Link = NewNode;
NewNode>Link = *L;
*L = NewNode;
}
}
void Insert(int i,ItemType Y, ListNode **L)
{
ListNode *N, *Temp;
if (i == 1) {
InsertFirst(Y,L);
} else {
N = Select(i1,*L);
if (N != NULL) {
Temp = (ListNode *) malloc(sizeof(ListNode));
Temp>Item = Y;
Temp>Link = N>Link;
N>Link = Temp;
}
}
}
void PrintList(ListNode *L)
{

Table of Contents continued


ListNode *Temp;
printf("(");
if (L != NULL) {
Temp = L;
do {
printf("%d",Temp>Item);
if (Temp>Link != L) printf(","); /* print commas between list items */
Temp = Temp>Link;
} while (Temp != L);
}
printf(")\n");
}
int main(void)
{
int i,k;
ListNode *L, *N;
/* test initialization */
Initialize(&L);
/* test empty function */
if (Empty(L)) {
printf("it was empty\n");
} else {
printf("It was nonempty\n");
}
/* test length function */
k = ListLength(L);
PrintList(L);
printf("list length was == %d\n",k);
for (i=5; i>=1; i) {
InsertFirst(i,&L);
/*
k = ListLength(L);
PrintList(L);
printf("list length was == %d\n",k);
*/
}
PrintList(L);
/* test replacement */
for (i=0; i<=6; i++) {
Replace(i,11*i,L);
}
PrintList(L);
/* test selection */
for (i=1; i<=5; i++){
N = Select(i,L);
if (N != NULL) printf(" the %dth item was == %d\n",i,N>Item);
}
/* test deletion */
Delete(5,&L);
PrintList(L);

Table of Contents continued


Delete(1,&L);
PrintList(L);
Delete(2,&L);
PrintList(L);
Delete(2,&L);
PrintList(L);
Delete(1,&L);
PrintList(L);
/* test insertion */
for (i=1; i<6; i++) {
Insert(i,i,&L);
}
PrintList(L);
for (i=0; i<7; i++) {
Insert(2*i+1,11*i,&L);
}
PrintList(L);
}
/* end main */

7. Move the contents of the Item field of N s successor into N s I t e m


field. Save a pointer P to N s successor. Set N s Link field to contain
the pointer in P s Link field. Finally, free the storage for the node
pointed to by P . A C code fragment to perform these actions is as
follows:

|
|
|
|
|
|

ListNode *P;
N>Item = N>Link>Item;
P = N>Link;
N>Link = P>Link;
free(P);

Answers to Review Question 8.3


1. A generalized list is a list in which the individual list items are
permitted to be sublists, unlike simple linear lists in which
individual items are not lists.
2. We can use C u n i o n s to define the structure of generalized list
nodes. Here is an example :
#define
#define

TRUE 1
FALSE 0

typedef

int ItemType;

typedef

struct GenListTag {
struct GenListTag *Link;
short Atom;
union SubNodeTag {
ItemType
Item;
struct GenListTag *SubList;

Table of Contents continued


} SubNode;
} GenListNode;

Then, if the Atom field of a node contains the constant value T R U E ,


the nodes Item field contains an ordinary list item. But if the A t o m
field contains the constant value FALSE , then the nodes SubList field
contains a pointer to the first node of a generalized sublist.
3. If some pointers in a generalized list G formed a cycle (i.e., a path
composed of pointers which begins at some node and travels back
to itself), then Program 8.12 would get into an endless loop.

Solutions to Exercises 8.3


No solutions are given in this section.

Answers to Review Questions 8.4


1. In a typeless language, variables can take values of any type, such
as integers, floating point numbers, characters, strings or lists.
This is not possible in a hard-typed language where the types of
variables have to be declared before the variables can be used and
where variables may be assigned values only if the value types to
be assigned agree with (or may be implicitly converted to or
typecast to) the types of the variables being assigned.
2. Typeless languages permit the construction of generalized lists
containing many different types of atomic items. This provides a
highly flexible kind of data structure which can meet the
requirements
of applications having highly unpredictable
demands for supporting representations. A hard-typed language
might waste space to provide the same kind of flexibility and
generality and its programs might require extensive case anaylsis
based on type switching.
3. The price paid is that interpreted type-switching code for
typeless languages runs much slower than compiled code for hardtyped languages because type switching must be done at run-time
for typeless languages, whereas no type switching code need be
executed at run time for programs compiled from a program in a
hard-typed language.
4. In symbolic algebraic manipulation systems, certain derivations
often involve long chains of intermediate steps using expressions
that can swell to unpredictably large sizes. This leads to a

Table of Contents continued


phenomenon called intermediate expression swell in algebraic
manipulation systems.

Solutions to Exercises 8.4


No solutions are given for this section.

Answers to Review Questions 8.5


1. The character constant '\0' represents the character with the value
zero and is sometimes called the null byte. A C string is a
character array consisting of a sequence of characterrs ending
with the null byte. In C, a null string is represented by a character
array containing a single null byte whose value is zero. The empty
string in C programs is denoted by two consecutive double quotes,
" ".
2. In C, character strings are represented by contiguous sequences
of characters having a special null character which terminates the
sequence. In C, the problem of string overflow can be handled by
allocating new blocks of characters big enough to accommodate
the strings growth to any new size, provided the underlying
memory allocation system can find space. Pascal strings are of
predetermined, bounded length, so overflows cannot be
accommodated conveniently. General Pascal strings consume 256
bytes of space for strings of between 0 and 255 characters in
length. Unused space inside the packed character arrays holding
these Pascal string representations is wasted. Because C strings
are not of bounded length, no space is wasted. However, to find the
length of a C string, we must scan the characters in the string until
reaching the terminating null byte at the end of the string. This is
an O(n ) operation for a string of n characters, whereas Pascals
length operation is O(1), since it looks up the length byte in the
zeroth position of the packed character array representing the
string.
3. Text files can be represented on external storage media as linked
blocks in which each block contains a packed array of characters
holding a portion of the text. Each of these linked blocks can be
stamped with: (1) a unique file identifier, (2) the date and time of
the text files creation, and (3) a block sequence number. This
information can be used to recreate the text file from the raw
undamaged text blocks in the event that the disks directory is

Table of Contents continued


damaged or destroyed, or in the event of an unintended, accidental
deletion of the text file from the directory.
4. Text files can be represented in main memory by coalescing their
characters into a single large block containing the sequence of
characters in the text. A separate array of line starts can be
created which is useful for rapid display and scrolling of the text
by a word processor. Sequences of control character codes, which
are invisible to the word processor user because they are not
printed on the screen, can be interspersed in the texts character
code sequence, and can designate format features of the
underlying text (such as page breaks, special character fonts or
sizes, tab alignments, and so forth).

Solutions to Selected Exercises in Exercises 8.5


1. The strlen function:
/* strlen is a standard C function usually explained in C texts such as */
/* e.g., The C Programming Language, Second Edition, by Kernighan */
/* and Ritchie (known as K & R), Prentice-Hall, 1988, page 39 */

10

|
|
|
|
|
|
|
|
|
|
|
|
|

int strlen(char S[ ])
{
int i;
i = 0;
while (S[i] != '\0' ) {
++i;
}
return i;
}

2. The strstr(S, T) function (return a pointer to the first occurrence of


string S in string T , or NULL if no occurrence is present):

10

|
|
|
|
|
|
|
|
|
|
|
|
|
|

char *strstr(char *S, char *T) /* return pointer to first occurrence of S in T */


{
char *p, *q, *r;
p = S;
r = T;
while (1) {
q = r;
while ( (*p != '\0') && ((*r != '\0') && (*p++ == *r++)) ) {
;
}

Table of Contents continued


15

20

25

|
|
|
|
|
|
|
|
|
|
|
|

if (*p == '\0') {
return q;
} else if (*r == '\0') {
return NULL;
} else {
p = S;
r++;
}
}
}

3. The strcpy function (from K&R, p. 105.):


|
|
|
|
|

void strcpy(char *S, char *T )


{
while ( ( *S++ = *T++) != '\0')
;
}

4. The strspn(S, T) function (return length of prefix of S with characters


in T):

10

15

20

25

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

/* first define the auxiliary function Member(c,T) which returns */


/* true iff c is a non-null character that found in string T */
Boolean Member(char c, char *T)
{
if (c == '\0') {
return 0;
} else {
while ((*T != '\0') && (*T != c)) {
T++;
}
return ( *T != '\0' );
}
}
int strspn(char *S, char *T) /* return length of prefix of S with chars in T */
{
int i;
i = 0;
while (Member(S[i],T)) {
i++;
}

/* Member returns false if S[i] == '\0' */

return i;
}

Answers to Review Questions 8.6


1. Static storage allocation takes place before a program is
executed and involves setting up storage areas whose size and

Table of Contents continued


arrangement can be calculated from declarations in the text of the
program before the program is run. Dynamic storage allocation
involves allocating portions of memory in various sizes and
shapes during the execution of the program. In dynamic allocation
we may be able to calculate the sizes and shapes needed only
while the program is being run and not beforehand. Three examples
of dynamic allocation are: (1) Stack-based dynamic allocation used
to support subroutine calls and expression evaluation, (2)
Organizing memory into an available space list in order to perform
dynamic allocation of list nodes on demand at run-time, and (3)
Organizing memory into a heap which can be used to allocate
blocks of differing sizes on demand during program execution.
2. There are three phases in the marking and gathering method for
garbage collection. In the initialization phase all list nodes are
marked as free . In the marking phase all list nodes in current use
are marked as reserved . In the gathering phase all free list nodes
are linked into a new available space list .
3. A heap is a zone of memory organized to support dynamic memory
allocation of blocks of storage of different sizes. The heap is
organized into a collection of reserved blocks that are in use by
the running program, and a collection of free blocks available for
allocation to satisfy future allocation requests. The free blocks
may be linked into lists, rings, trees, or other forms of data
organization, in order to promote the efficiency of the process of
searching for a free block to satisfy an allocation request.
4. Two methods for allocating a block of size n in the heap are the
first-fit method and the b e s t - f i t method. The first-fit method
scans a two-way linked ring of free blocks and allocates the first
block big enough to satisfy the request. The best-fit method scans
the entire ring of free blocks to find the block that is the tightest
possible fit satisfying the request, where the tightest possible fit
is the one having least excess size beyond the size requested.
5. Fragmentation occurs during the use of the first-fit and best-fit
policies when free blocks are split into smaller blocks. Coalescing
is a method used to counteract the tendency towards
fragmentation. When a block is liberated and can be returned to
the ring of free blocks, it is possible to coalesce it with
neighboring blocks that are also free, in order to form larger free
blocks.
6. Compacting is a strategy that can be used for storage reclamation
when allocation failure occurs in a heap. When a heap is compacted,
all reserved blocks are moved to one end of the heap, allowing all
free blocks to be moved to the opposite end and to be coalesced

Table of Contents continued


into one large free block. Handles are used to make heap
compacting easier. A handle is a pointer to a pointer. Each
reserved block in a heap is referenced by a master pointer in a
special master pointer region. A handle to a block is a pointer to
the blocks master pointer. When a block is moved during heap
compacting, its master pointer is updated to point to the blocks
new location. References to the block using double dereferencing
of the blocks handle, are unaffected by the act of moving a block
and updating its master pointer.
7. In a reference count technique, each node contains a number,
called its reference count , which equals the number of external
pointers which point to the node. Each time an external pointer to
a node is destroyed, the nodes reference count is decreased by
one. When the nodes reference count becomes zero, its storage
can be returned to the pool of available space. Since this can be
done incrementally, long pauses for the execution of multipass
storage reclamation algorithms can be avoided.

Solutions to Exercises 8.6


1. To make the coalescing policy work efficiently, we need to be able
to inspect the neighboring blocks above and below a given block
that is being freed so that it may be joined together with either or
both neighboring blocks if either or both is free. (Here, when we
use the words top and bottom we are referring to memory
address order, where up is the direction of decreasing memory
addresses, and down is the direction of increasing memory
addresses.) Suppose we are trying to free a given block, B. If we
were to store every blocks size and mark bit only at its top and
not at its bottom, then when we are trying to look at the block
immediately above B to see if it is free and available for
coalescing, we would have no information to use to determine its
top neighbors size and status (free or reserved). By storing the
size and mark bit of a block at its bottom boundary as well as at
its top boundary, we can look at the address immediately before
Bs starting address (which is the location of Bs top neighbors
bottom boundary) to find the size and mark bit of Bs top neighbor.
This permits us to coalesce B with its top neighbor in the event
that Bs top neighbor is found to be free.
2. Consider any collection of nodes having links that form a cycle
(i.e., a path formed from pointers that starts at some node and
returns to that node). The reference counts in each node in a cycle
can never become zero, even though there are no external

Table of Contents continued


pointers pointing to any node in the collection. Consequently, a
reference-count based incremental storage reclamation technique
can never return any of the nodes in such a cycle to free storage.
Islands of nodes linked by pointer cycles can thus form and will
never be garbage collected using a reference-count technique.

Table of Contents continued

Answers to Review Questions 9.2


1. (a) R, T
(b) X
(c) X, Y, Z
(d) T
2. The root node in a tree is the only node having no ancestors.
3. A leaf.
4. There is one and only one path from the root node R in a tree to
any given node N that is a descendant of R.

Solutions to Selected Exercises in Exercises 9.2


1. In a tree, the number of nodes n is always 1 greater than the
number of edges e . Thus, n = e + 1. This relationship must hold true
in all trees because each node, except the root, sits at the bottom
of exactly one edge. Thus the number of edges equals the the
number of non-root nodes. Adding the root node, the number of
nodes equals one plus the number of edges.
2. A proof can be constructed using the idea that the number of edge
tops equals the number of edge bottoms. Thus, 2*I = # edge tops =
# edge bottoms = L + I 1. This implies 2*I = L + I 1, from which,
by subtracting I from both sides and adding one to both sides, we
get, L = I + 1.

Answers to Review Questions 9.3


1. A binary tree is either the empty tree or else is a node that has
left and right subtrees that are binary trees.
2. An extended binary tree is a binary tree having its empty binary
subtrees explicitly represented by square symbols.
3. A complete binary tree is a binary tree with leaves either on a
single level or on two adjacent levels such that the leaves on the
bottommost level are placed as far left as possible.
4. Yes, a single node with empty left and right subtrees is a binary
tree whose single node is both a root and a leaf.

Table of Contents continued

Solutions to Selected Exercises in Exercises 9.3


1. The number of empty binary trees (squares) in an extended binary
tree is one greater than the number of internal (non-empty) nodes
(circles) because such a tree is a full binary tree (see the solution
to Ex. 9.2.2).
2. At each level l, there are 2l nodes, so the total number of nodes is:

(0 l n )

2l = 20 + 21 + 22 +...+ 2n = 2n +1 1 .

[ See the solutions to Exercises A.2.1 and A.2.2 in the Math


Reference Appendix, or apply Eq. A.8 in the Appendix to the sum
above to determine the solution. ]

Answers to Review Question 9.4


1. If we number the nodes of the complete binary tree in level order
as shown in Figure 9.5 on p. 344, we can represent the tree nodes
in an array A in which the i th node in level order is stored in array
position A [ i ] . Table 9.7 on p. 345 defines the mathematical
properties of this representation, which is called the contiguous
sequential representation of the complete binary tree.
2. In the contiguous sequential representation, the parent of A [ i ] is
A [ i / 2] . The root, A[ 1 ] , has no parent.
3. In the contiguous sequential representation, the left child of A[ i ] is
A [ 2* i ] . All nodes with 2 i > n have no left children, and each such
node is a leaf.
4. In the contiguous sequential representation, the right child of A [ i ]
is A[ 2 *i+ 1 ] . Any node with 2*i+ 1 > n has no right child, but it may
have a left child (when 2*i = n ).

Solutions to Selected Exercises in Exercises 9.4


1. If A[ i ] is a leaf, it has no left child A[ 2* i ] . But if 2*i n then the left
child A[ 2* i ] would have existed. Consequently we must have 2*i >
n . On the other hand, suppose 2*i > n is given. Then it is also true
that 2*i+ 1 > n . Hence both the left child A[ 2* i ] and the right child
A [ 2 *i+ 1 ] of node A[ i] do not exist. Because A[ i] has no children it is
a leaf.
2. The level number l of the level containing the n t h node in a
complete binary tree is l = lg n . [ Strictly speaking, the number
of levels in a complete binary tree T is one greater than the level
number l of the bottom row of T because the levels are numbered
starting at 0 for the topmost level containing the root. Thus the
number of levels is 1 + lg n .]

Table of Contents continued


3. A [ i ] resides on level l = lg i .

Answers to Review Question 9.5


1. A h e a p is a complete binary tree with values stored in its nodes
such that no child has a value greater than the value of its parent.
2. A heap can be used to represent a priority queue by implementing
appropriate removal and insertion operations. The r e m o v a l
operation removes the value from the root of the heap (which is
the largest value in the heap and thus the highest priority value
in the priority queue) after which the tree with the missing root
value must be reheapified by placing the value of the last leaf
node in level order into the root and repeatedly exchanging it
downward along a path from the root toward the leaves with the
larger of any children node values exceeding its own value. The
insertion operation adds a new value by inserting it in a new last
leaf in level order and repeatedly exchanging the new value with
any smaller parent value upward along a path from the new leaf
toward the root.
3. Starting with the last internal node in level order, which is node
A [ n /2 ] , and proceeding to process all internal nodes from
A [ n /2 ] backwards toward the root node A [ 1 ] in reverse level
order, take the value of each such internal node and repeatedly
exchange it downward along a path toward the leaves with the
larger of any child values exceeding its own value. The entire
process takes time O(n ) for a heap of n nodes.
4. Removing and inserting heap items are operations each taking
O(log n) for a heap of n nodes.

Solutions to Selected Exercises in Exercises 9.5


1. Suppose there exists a heap H with root R having a node N R
containing a value larger than the value in any other node of H.
Then let P be the parent node of node N. (Node N must have a
parent because the root of a tree is the only node with no parent,
and N is not the root). Then Ns value is larger than Ps value which
violates the heap property that no child can have a value larger
than the value of its parent. Consequently, there exists no heap
with a non-root node having a value larger than the value of any
other node. Therefore, the largest value in a heap always resides
at the root.
2. n/ 2 nodes are internal nodes and n/ 2 nodes are leaf nodes.
3. The solution is given by the Heapify function in the following
source program:

Table of Contents continued


/*
* Solution to Ex 9.5.3 also does the example in the book
*
by converting Fig. 9.17 into Fig. 9.8
*/
#include <stdio.h>
#define MAXCOUNT 10
typedef int HeapItemType;
typedef int HeapNodeType;
typedef

struct {
int
HeapItemType
} Heap;

/* for now */

Count;
ItemArray[MAXCOUNT+1];

/* ---------------------------------------------------------------------- */
void BuildInitialNonHeap(Heap *H)
{
int i;
HeapItemType InitializerArray[ ] = {0,2,4,5,7,3,10,8,1,9,6};
H>Count = MAXCOUNT;
for (i=1; i<=MAXCOUNT; i++) H>ItemArray[i] = InitializerArray[i];
}
/* ---------------------------------------------------------------------- */
void Reheapify(Heap *H, HeapNodeType N)
{
HeapNodeType M;
HeapItemType V1,V2, temp;
Boolean Finished;
/* Let V1 refer to N's value */
V1 = H>ItemArray[N];
Finished = (2*N > MAXCOUNT);

/* Finished = true iff */


/* node N has no children */

while (!Finished) {
/* let M be the child of node N having the larger value V2 */
M = 2*N;
/* let M be the left child of N */
if (M < MAXCOUNT) {
/* if a right child of node N exists, then */
if (H>ItemArray[M+1] > H>ItemArray[M]) M++;
/* if the */
}
/* right childs value is larger than the left child's value */
V2 = H>ItemArray[M];
/* then let M be the right child of N */
if (V1 >= V2) {
Finished = true;
} else {
/* exchange the values in nodes N and M */
temp = H>ItemArray[N];
H>ItemArray[N] = H>ItemArray[M];
H>ItemArray[M]=temp;
/* Let N refer to node M and let V1 refer to N's value */
N = M;
V1 = H>ItemArray[N];
Finished = (2*N > MAXCOUNT);
/* true iff node */
/* N has no children */

Table of Contents continued


}
}
}
/* ---------------------------------------------------------------------- */
void Heapify(Heap *H)
{
int i;
for (i = H>Count/2; i>=1; i) {
Reheapify(H,i);
}

/* reheapify H starting at node i */

}
/* ---------------------------------------------------------------------- */
void PrintHeap(Heap *H)
{
int i;
for (i=1; i<=H>Count; ++i) {
printf("%2d%2d; ",i,H>ItemArray[i]);
}
printf("\n");
}
/* ---------------------------------------------------------------------- */
int main(void)
{
Heap H;
BuildInitialNonHeap(&H);
PrintHeap(&H);
Heapify(&H);
PrintHeap(&H);
}
/* main program */

4. This exercise is solved in the Math Reference appendix on pages


718-720, yielding Eq. A.28, from which it can be shown that (0 i l
1 ) i / 2 i = 2 ( l + 1 ) / 2 l 1 , from which it follows that ( 0 i l 1 ) i / 2 i < 2
for any l 0.

Answers to Review Questions 9.6


1. PreOrder, InOrder, and PostOrder.
2. The three traversal orders are defined

recursively as follows:

PreOrder : Visit the root. Traverse the left subtree in PreOrder.


Traverse the
right subtree in PreOrder.

Table of Contents continued

I n O r d e r : Traverse the left subtree in InOrder. Visit the root.


Traverse the right
subtree in InOrder.
PostOrder : Traverse the left subtree in PostOrder. Traverse the
right subtree in
PostOrder. Visit the root.
3. No. LevelOrder visits all siblings and cousins of a node N on the
same level before visiting any children of N, whereas PreOrder,
InOrder, and PostOrder visit all descendants of N before visiting
any siblings or cousins of N on the same level as N.
4. Stacks and queues can hold postponed obligations to visit
subtrees of a tree by storing pointers to the subtrees as stack or
queue items. Suppose we visit a node N, then insert pointers to the
left and right subtrees of N on the rear of a queue, and then
remove a pointer from the front of the queue to obtain a pointer
to the next node to visit. The result will be a LevelOrder traversal
of the tree rooted at N. If we visit a node followed by pushing
pointers to the right and left subtrees of N on a stack, we can
implement a PreOrder traversal non-recursively.

Solutions to Selected Exercises in Exercises 9.6


1. PreOrder Traversal
InOrder Traversal

:RSXYZTUVW
:XSYZ RUTWV

PostOrder Traversal

:XZYSUWVTR

LevelOrder Traversal

:RSTXYUVZW

2. The tree is:


/

^
b

*
2

3. The solutions to Ex. 9.6.3 and Ex. 9.6.4 are given in the following
program:
|
|

/*
*

Solutions to Ex 9.6.3 and 9.6.4

Table of Contents continued

10

15

20

25

30

35

40

45

50

55

60

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct NodeTag {
char
struct NodeTag
struct NodeTag
} Node;

Symbol;
*LLink;
*RLink;

/* Assume typedef enum { false, true} Boolean; has been given */


extern void PrintParens3(Node *T);
extern void PrintParens2(Node *T, char op);
/* ---------------------------------------------------------------------- */
Boolean IsOperator(char c)
{
switch (c) {
case '+':
case '':
case '*':
case '/':
case '^':
return true;
default:
return false;
}
}
/* ---------------------------------------------------------------------- */
Node *PrefixStringToTree(char **S)
{
char c;
Node *N;
c = *(*S)++;

/* extract next character from string S */


/* and postincrement char pointer (*S) */
N = (Node *) malloc(sizeof(Node));
/* create a new node, N */
N>Symbol = c;
if (IsOperator(c)) {
N>LLink = PrefixStringToTree(S);
N>RLink = PrefixStringToTree(S);
} else {
N>LLink = NULL;
N>RLink = NULL;
}
return N;
}
/* ---------------------------------------------------------------------- */
void PrintTree1(Node *T)

Table of Contents continued


65

| {
|
if (T != NULL) {
|
if (IsOperator(T>Symbol) ) {
|
putchar('(');
|
PrintTree1(T>LLink);
70
|
printf(" %c ",T>Symbol);
|
PrintTree1(T>RLink);
|
putchar(')');
|
} else {
|
putchar(T>Symbol);
75
|
}
|
}
| }
|
| /* ---------------------------------------------------------------------- */
80
|
| void PrintTree(Node *T)
| {
|
PrintTree1(T);
|
putchar('\n');
85
| }
|
| /* ---------------------------------------------------------------------- */
|
| int precedence(char c)
/* define precedence of operators & atoms */
90
| {
|
switch (c) {
|
case '^':
|
return 3;
|
case '*':
95
|
case '/':
|
return 2;
|
case '+':
|
case '':
|
return 1;
100
|
default :
|
return 4;
|
}
| }
|
105
| /* ---------------------------------------------------------------------- */
|
| void WriteP(char c)
/* write spaces around '+' and '' */
| {
/* but don't write spaces around other operators */
|
if (precedence(c) == 1) {
110
|
printf(" %c ",c);
|
} else {
|
putchar(c);
|
}
| }
115
|
| /* ---------------------------------------------------------------------- */
|
| void PrintParens1(Node *T, char op)
/* op is the operator above and */
| {
/* to the right of subtree T, where T is not atomic */
120
|
if (T == NULL) {
|
;
/* print nothing */
|
} else if ( !IsOperator(T>Symbol) ) {
|
putchar(T>Symbol);
|
} else if (precedence(T>Symbol) < precedence(op) ) {
125
|
putchar('(');

Table of Contents continued

130

135

140

145

150

155

160

165

170

175

180

185

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

PrintParens1(T>LLink,T>Symbol);
WriteP(T>Symbol);
PrintParens2(T>RLink,T>Symbol);
putchar(')');
} else {
PrintParens1(T>LLink,T>Symbol);
WriteP(T>Symbol);
PrintParens2(T>RLink,T>Symbol);
}
}
/* ---------------------------------------------------------------------- */
void PrintParens2(Node *T, char op)
{
/* op is the operator above and to the left of subtree T, */
/* where T is not atomic */
if (T == NULL) {
;
/* print nothing */
} else if ( !IsOperator(T>Symbol) ) {
putchar(T>Symbol);
} else if ( (precedence(T>Symbol) < precedence(op)) | |
( (precedence(T>Symbol) == precedence(op)) &&
( !((T>Symbol == op) && ((op == '+') | | (op == '*')) )) )) {
putchar('(');
PrintParens1(T>LLink,T>Symbol);
WriteP(T>Symbol);
PrintParens2(T>RLink,T>Symbol);
putchar(')');
} else {
PrintParens1(T>LLink,T>Symbol);
WriteP(T>Symbol);
PrintParens2(T>RLink,T>Symbol);
}
}
/* ---------------------------------------------------------------------- */
void PrintParens3(Node *T)
{
if (T != NULL) {
if ( IsOperator(T>Symbol) ) {
PrintParens1(T>LLink,T>Symbol);
WriteP(T>Symbol);
PrintParens2(T>RLink,T>Symbol);
} else {
putchar(T>Symbol);
}
}
/* ---------------------------------------------------------------------- */
void PrintParens(Node *T)
{
PrintParens3(T);
putchar('\n');
}
/* ---------------------------------------------------------------------- */

Table of Contents continued

190

195

200

205

210

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

int main(void)
{
char *S = ".....................",
*S1 = "/^b2**4ac*2a",
*S2 = "**+ag+bc*++cd+de++efg",
*S3 = "/^2+a*bcefg",
*S4 = "/^2*a^bcehg",
*S5 = "/^2*a^bc//xy/hg";

/* empty string */
/* first test case */
/* second test case */
/* third test case */
/* fourth test case */
/* fifth test case */

Node *T;
int i;
strcpy(S,S1);
/* substitute Si for S1 for other test cases */
printf("translating %s:\n",S);
T = PrefixStringToTree(&S);
PrintTree(T);
PrintParens(T);

/* print fully parenthesized tree to verify */


/* tree construction. Ex 9.6.3 */
/* print parentheses only when required */
/* by operator precedence. Ex 9.6.4 */

}
/* main program */

Answers to Review Questions 9.7


1. A binary search tree is a binary tree with keys stored at its nodes
such that for any node N with key K, the keys in the nodes of the
left subtree of the node rooted at N are less than K, and the keys
in the nodes of the right subtree of the node rooted at N are
greater than K.
2. To search for a key K in a binary search tree T, if T is N U L L the
search fails. If K equals the key Kr stored in the root node of T, the
search succeeds. Otherwise, if K < K r , further binary search is
conducted on the left subtree of the root node of T, and if K > K r ,
further binary search is conducted on the right subtree of the root
node of T.
3. The internal path length I, is the sum of all the path lengths from
the root node to each internal node of tree T. The external path
length E, is the sum of the path lengths from the root to each
external node of an extended binary tree, T. In a binary tree with n
internal nodes, we always have E = I + 2 n .
4. O-notations for various cases of successful search in a binary
search tree n nodes:
(a) best case:

O(log n )

(b) worst case:

O(n)

(c) average case:

O(log n )

Table of Contents continued

Solutions to Exercises 9.7


1. Leff Wrights program is correct.
2. A recursive function to search for an airport code
binary search tree T is given as follows:

10

15

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

key A in a

TreeNode *BinaryTreeSearch (AirportCode A, TreeNode *T)


{
int result;
if (T == NULL) {
return NULL;
} else if ( (result = strcmp(A,T>Airport)) == 0) {
return (T);
/* T points to the node containing A */
} else if (result < 0) {
return BinaryTreeSearch(A,T>LeftLink);
/* search in */
/* the left subtree */
} else {
return BinaryTreeSearch(A,T>RightLink);
/* search in */
/* the right subtree */
}
}

3. A TreeInsert function that inserts a new airport code A in tree T is


given below. It uses the auxiliary function MakeNode .

10

15

20

25

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

/* First we give the auxiliary function MakeNode(A) */


TreeNode *MakeNode(AirportCode A)
{
TreeNode *temp;
temp = (TreeNode *) malloc(sizeof(TreeNode));
temp>LeftLink = NULL;
temp>RightLink = NULL;
strcpy(temp>Airport, A);
return temp;
}
/* The function TreeInsert(A,T) uses the auxiliary function MakeNode */
TreeNode *TreeInsert(AirportCode A, TreeNode *T)
{
if (T == NULL) {
return MakeNode(A);
} else if ( strcmp(A,T>Airport) < 0 ) {
T>LeftLink = TreeInsert(A,T>LeftLink);
} else {
T>RightLink = TreeInsert(A,T>RightLink);
}
return T;
}

4. The tree printing function T r e e P r i n t ( T ) given below is helpful for


verifying that the binary search tree of Fig. 9.31 can be
constructed by inserting the nodes of Table 9.32 into an initially

Table of Contents continued


empty binary search tree using the solution to Ex. 9.7.3 given
above. A section of a main program which verifies that the solution
works is also given below.

10

15

20

25

30

35

40

45

50

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

/* -------------------------------------------------------------- */
void AuxTreePrint(TreeNode *T)
{
if (T != NULL) {
putchar('(');
AuxTreePrint(T->LeftLink);
printf(" %s ",T->Airport);
AuxTreePrint(T->RightLink);
putchar(')');
}
}
/* -------------------------------------------------------------- */
void TreePrint(TreeNode *T)
{
if (T == NULL) {
printf("( )\n");
} else {
AuxTreePrint(T);
putchar('\n');
}
}
/* -------------------------------------------------------------- */
int main(void)
{
TreeNode *T;
/* First insert the airport codes from Table 9.3.2 in the order given */
T = NULL;
T = TreeInsert("ORY",T);
T = TreeInsert("JFK",T);
T = TreeInsert("BRU",T);
T = TreeInsert("DUS",T);
T = TreeInsert("ZRH",T);
T = TreeInsert("MEX",T);
T = TreeInsert("ORD",T);
T = TreeInsert("NRT",T);
T = TreeInsert("ARN",T);
T = TreeInsert("GLA",T);
T = TreeInsert("GCM",T);
/* Then print the tree T as a parenthesized expression */
TreePrint(T);
}
/* end main */

5. We can prove that E = I + 2n by induction, where n is the number of


internal nodes. Starting with the base case, let n = 1. Then I = 0 (in

Table of Contents continued


) and E = 2 (in
). So, E = 2 = 0 + 2*1 = I + 2n . Proceeding
with the induction step, suppose E old = Iold + 2n for a tree with n
internal nodes. Choose a leaf at distance r from the root (where r
1). Substitute a new internal node ( ) for the leaf and add two new
leaves ( ) as children of the new internal node, using the
transformation:

The number of internal nodes increased from n to n + 1. A leaf at


distance r was subtracted and two leaves at distance r + 1 were
added. So,
E new = Eold r + 2(r + 1).
An internal node at distance r was added. So, Inew = Iold + r . Now
substituting in Eold = Iold + 2n ,

E new + r 2(r + 1) = Inew r + 2n


Enew + 2 r 2r 2 = Inew + 2n

E new = Inew + 2n + 2 = Inew + 2(n + 1).

Therefore, E new = In e w + 2(n + 1), which is what we wanted. Any


tree of n 1 internal nodes can be built up from a tree of one
internal node by repeatedly substituting new internal nodes for its
leaves.
6. When n takes the special form n = 2r 1, where r is the number of
rows in the tree, we can simplify the formula for L n , given in the
Math Reference Appendix, as follows. First, if n = 2r 1, then r =
lg( n +1), and because r is an integer, we also have r = lg(n +1) =
lg( n +1) . Equation A.31 in the Appendix gives a formula for Ln a s
follows:
n

Ln =

l g i = (n +1)q 2q+1 + 2, where q = lg(n +1) .

i = 1

Because q = r and because n = 2r 1 implies that 2q + 1 = 2(2r ), we


can simplify the formula for Ln above to get Ln = (n +1) r 2(2r ) + 2.
Now, we can simplify the exact formula for the best case, Cn =
(2L n + n )/ n , as follows:
( 2 [ ( n +1) r 2(2r ) + 2] + n ) /
(2L n + n )/ n =
n
=

2
[(n +1) r 2(2r1) ] + 1
n

Table of Contents continued


=

2
[(n +1) r 2 n ] + 1
n

2r+2r/n4+1

( 2r 3 ) +

7. The functions below


9.7.8:

10

15

20

25

30

35

40

45

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

2 r
.
n

calculate the formulas for Ex. 9.7.7 and Ex.

#include <math.h>
/* -------------------------------------------------------------- */
double Hn(int n)
{
double Temp; int i;

/* computes n th Harmonic number Hn */

Temp = 0.0;
for (i = 1; i <= n; ++i) Temp += 1.0/i;
return Temp;
}
/* -------------------------------------------------------------- */
double Lg(int n) /* Base 2 Logarithm */
{
return log(n)/log(2);
/* log(n) is C's natural log function */
}
/* -------------------------------------------------------------- */
double Power(int n, int m)
{
return exp(m*log(n));
}

/* n^m */

/* -------------------------------------------------------------- */
double L(int n)
{
int q;

/* L = (1in) Floor(lg(i)) */

q = (int) floor(Lg(n+1));
return (n + 1)*q Power(2,(q+1)) + 2;
}
/* -------------------------------------------------------------- */
double ExactCn(int n)
{
return 4*(1 + 1.0/n)*Hn(n) 7;
}
/* -------------------------------------------------------------- */
double ApproxCn(int n)

Table of Contents continued

50

55

60

65

70

75

80

85

90

95

100

105

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

{
return 2.77*Lg(n) 4.7;
}
/* -------------------------------------------------------------- */
int TestFormulas(void)
{
int n;
double X, Y, Cn;
/* Exercise 9.7.7 -- Compare Exact and Approximate Average */
/* Case Formulas */
printf("Exercise 9.7.7\n");
n = 510;
printf("Exact Average Cn for n = %d: Cn = %3.7f\n", n, ExactCn(n));
printf("Apprx Average Cn for n = %d: Cn = %3.7f\n", n, ApproxCn(n));
n = 511;
printf("Exact Average Cn for n = %d: Cn = %3.7f\n", n, ExactCn(n));
printf("Apprx Average Cn for n = %d: Cn = %3.7f\n", n, ApproxCn(n));
n = 1023;
printf("Exact Average Cn for n = %d: Cn = %3.7f\n", n, ExactCn(n));
printf("Apprx Average Cn for n = %d: Cn = %3.7f\n", n, ApproxCn(n));
putchar('\n');
n = 1;
while (fabs(ExactCn(n) ApproxCn(n))/ExactCn(n) > 0.02) {
n++;
}
printf("The n for which ApproxAverageCn and ");
printf("ExactAverageCn differ by 2%% = %d\n",n);
putchar('\n');
printf("Exact Cn for n = 90:
printf("Apprx Cn for n = 90:
printf("Exact Cn for n = 91:
printf("Apprx Cn for n = 91:

Cn = %3.7f\n", ExactCn(90));
Cn = %3.7f\n", ApproxCn(90));
Cn = %3.7f\n", ExactCn(91));
Cn = %3.7f\n", ApproxCn(91));

printf("\n\n");
/* Exercise 9.7.8 -- Compare Exact and Approximate Best Case Formulas */
printf("Exercise 9.7.8\n");
n = 0;
do {
n++;
X = ((2*L(n) + n)/n);
Y = (2*Lg(n) 3);
} while ( (fabs(X Y)/X) > 0.02 );

/* exact best case for n */


/* approximate best case for n */

printf("The least n for which ApproxBestCn and ");


printf("ExactBestCn differ by 2%% = %d\n",n);

Table of Contents continued


110

115

|
|
|
|
|
|
|
|
|
|
|

n = 115;
printf("Exact Best Cn for n = %d: Cn = %3.7f\n", n, ((2*L(n) + n)/n));
printf("Apprx Best Cn for n = %d: Cn = %3.7f\n", n, (2*Lg(n) 3));
n = 116;
printf("Exact Best Cn for n = %d: Cn = %3.7f\n", n, ((2*L(n) + n)/n));
printf("Apprx Best Cn for n = %d: Cn = %3.7f\n", n, (2*Lg(n) 3));
}
/* end TestFormulas */

When the above program is executed, it determines that the least


n for which the Approximate Average Cn and the Exact Average Cn
differ by 2% is n = 91. It also prints the following values of Cn
requested in Ex. 9.7.7:
for n = 511
Exact Average Cn
Approx Average
Cn

for n = 512 for n = 1023

20.312

20.319

23.062

20.222

20.230

22.996

8. See the program above in the solution to Ex. 9.7.7. When this
program is executed, it determines that the least n for which the
Approximate Best C n and the Exact Best Cn differ by 2% is n =
116. Hence, it is verified that these values also differ by less than
2% for any n 197.
node with airport code A , call the function
in what follows, where T contains a pointer to the
root of the binary search tree:

9. To

delete

DeleteNode(A,&T)

10

15

20

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

/*
*
*/

Ex. 9.7.9 -- Routines to delete a node in a binary search tree

/* -------------------------------------------------------------- */
void DeleteSmallest(AirportCode *A, TreeNode **T)
{
TreeNode *N;
if ( (*T)>LeftLink == NULL ) {
strcpy(*A,(*T)>Airport);
N = (*T)>RightLink;
free(*T);
*T = N;
} else {
DeleteSmallest(A,&(*T)>LeftLink);
}
}

Table of Contents continued

25

30

35

40

45

50

55

60

65

70

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

/* -------------------------------------------------------------- */
void DeleteLargest(AirportCode *A, TreeNode **T)
{
TreeNode *N;
if ( (*T)>RightLink == NULL ) {
strcpy(*A,(*T)>Airport);
N = (*T)>LeftLink;
free(*T);
*T = N;
} else {
DeleteLargest(A,&(*T)>RightLink);
}
}
/* -------------------------------------------------------------- */
void DeleteRoot(TreeNode **T)
{
AirportCode temp;
if ( (*T)>LeftLink == NULL ) {
if ( (*T)>RightLink == NULL ) {
free(*T);
*T = NULL;
} else {
DeleteSmallest(&temp, &(*T)>RightLink);
strcpy( (*T)>Airport, temp);
}
} else {
DeleteLargest(&temp, &(*T)>LeftLink);
strcpy( (*T)>Airport, temp);
}
}
/* -------------------------------------------------------------- */
void DeleteNode(AirportCode A, TreeNode **T)
{
int result;
if ( (*T) == NULL ) {
; /* the tree is empty and does not contain the airport code A */
} else if ( ( result = strcmp(A, (*T)>Airport)) == 0 ) {
DeleteRoot(T);
} else if (result < 0) {
DeleteNode(A, &(*T)>LeftLink);
} else {
DeleteNode(A, &(*T)>RightLink);
}
}
/* -------------------------------------------------------------- */

Answers to Review Questions 9.8

Table of Contents continued


1. The height of a binary tree is defined to be the length of the
longest path from the root to one of its leaves. As a special case
the height of the empty tree is defined to be 1.
2. Node N in tree T has the AVL property if the heights of Ns left and
right subtrees differ by at most 1. An AVL tree is a binary search
tree in which each node has the AVL property.
3. See Fig. 9.44 on p. 379 of the text.
4. O(log n ).

Solutions to Selected Exercises in Exercise 9.8


1. The basic idea is to add a field to each node in an AVL tree
containing 1 + the number of nodes in the left subtree of the node.
Let us call such a field the Index of node N . If N contains a pointer to
an AVL tree node, the Index of node N is given by N>Index .
Then to get the length of a tree representing a list, we use:

|
|
|
|
|
|
|
|

int Length(TreeNode *T)


{
if (T == NULL) {
return (0);
} else {
return (T>Index + Length(T>RightLink));
}
}

To find a pointer to the node containing the i th item of T , we can


use:
|
|
|
|
|
|
|
|
|
|
|
|
|
|

TreeNode *Find(int i, TreeNode *T)


{
if (T != NULL) {
if (i == T>Index) {
return (T);
} else if (i < T>Index) {
return (Find(i, T>LeftLink);
} else {
return(Find(i T>Index, T>RightLink);
}
} else {
return NULL;
/* return NULL if the ith item doesnt exist */
}
}

2. Yes, the InOrder traversal of a binary search tree reads the keys
in the nodes in increasing order of the keys. It helps to use an AVL
tree to keep the total insertion time bounded by O(n log n ) where
the sum of the O(log i ) insertion times for the n keys Ki, (1 i n ),
starting with an empty tree, is O(n log n ).
3. The solutions to Exs. 9.8.3 and 9.8.4 are given in the following
program which provides a complete set of AVL-tree algorithms

Table of Contents continued


for insertion, deletion, searching, and tree printing (with balance
factors shown). To insert an AirportCode A into an AVL tree T
(where T is a variable containing a T r eeNode pointer to the root of
AVL tree T ), you make the function call, InsertNode(A, &T, &Heavier) ,
using the function InsertNode defined on lines 167:230. To delete a
node containing the AirportCode A from the tree whose root is
referenced by the TreeNode pointer in the variable T , you make the
function call, DeleteNode(A, &T, &Lighter) , using the function DeleteNode
defined on lines 487:511.

10

15

20

25

30

35

40

45

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

/*
* Complete AVL-tree programs including solutions to Exs. 9.8.3 and 9.8.4
*/
#include <stdio.h>
#include <string.h>
typedef char AirportCode[4];
typedef enum {LeftHeavy, Balanced, RightHeavy} BalanceCode;
typedef struct TreeNodeTag {
BalanceCode
AirportCode
struct TreeNodeTag
struct TreeNodeTag
} TreeNode;

BalanceFactor;
Airport;
*LeftLink;
*RightLink;

/* some variables */
TreeNode *T, *N;
Boolean Heavier, Lighter;
/* -------------------------------------------------------------- */
TreeNode *CreateNode(AirportCode A)
{
TreeNode *N;
N = (TreeNode *)malloc(sizeof(TreeNode)); /* create a new blank */
/* tree node which N references */
N>LeftLink = NULL;
/* set its left and right links to be NULL */
N>RightLink = NULL;
strcpy(N>Airport,A);
/* let its airport code be A */
N>BalanceFactor = Balanced;
return N;
/* and let N be the value of the function */
}
/* -------------------------------------------------------------- */
void RotateLeft(TreeNode **T)
/* Assume *T points to a non-empty */
{
/* tree having a non-empty right subtree */
TreeNode *N;
/* let N be a tree node pointer */
N = (*T)>RightLink;

Table of Contents continued

50

55

60

65

70

75

80

85

90

95

100

105

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

(*T)>RightLink = N>LeftLink;
N>LeftLink = (*T);
(*T) = N;
}
/* -------------------------------------------------------------- */
void RotateRight(TreeNode **T)
/* Assume *T points to a non-empty */
{
/* tree having a non-empty left subtree */
TreeNode *N;
/* let N be a tree node pointer */
N = (*T)>LeftLink;
(*T)>LeftLink = N>RightLink;
N>RightLink = (*T);
(*T) = N;
}
/* -------------------------------------------------------------- */
void RightRebalance(TreeNode **T, Boolean *Heavier)
{
TreeNode *N, *P;
N = (*T)>RightLink;
switch (N>BalanceFactor) {
case LeftHeavy:

/* perform a double left rotation */

P = N>LeftLink;
switch (P>BalanceFactor) {
case LeftHeavy:
(*T)>BalanceFactor = Balanced;
N>BalanceFactor = RightHeavy;
break;
case Balanced:
(*T)>BalanceFactor = Balanced;
N>BalanceFactor = Balanced;
break;
case RightHeavy:
(*T)>BalanceFactor = LeftHeavy;
N>BalanceFactor = Balanced;
break;
}
P>BalanceFactor = Balanced;
RotateRight(&N);
(*T)>RightLink = N;
RotateLeft(T);
*Heavier = false;
break;
case Balanced:
/* do nothing -- this case cannot occur */
break;
case RightHeavy:

/* perform a single left rotation */

(*T)>BalanceFactor = Balanced;
N>BalanceFactor = Balanced;
RotateLeft(T);
*Heavier = false;
break;

Table of Contents continued


110

115

120

125

130

135

140

145

150

155

160

165

170

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

}
}
/* -------------------------------------------------------------- */
void LeftRebalance(TreeNode **T, Boolean *Heavier)
{
TreeNode *N, *P;
N = (*T)>LeftLink;
switch (N>BalanceFactor) {
case LeftHeavy:
/* perform a single right rotation */
(*T)>BalanceFactor = Balanced;
N>BalanceFactor = Balanced;
RotateRight(T);
*Heavier = false;
break;
case Balanced:
/* do nothing -- this case cannot occur */
break;
case RightHeavy:

/* perform a double right rotation */

P = N>RightLink;
switch (P>BalanceFactor) {
case LeftHeavy:
(*T)>BalanceFactor = RightHeavy;
N>BalanceFactor = Balanced;
break;
case Balanced:
(*T)>BalanceFactor = Balanced;
N>BalanceFactor = Balanced;
break;
case RightHeavy:
(*T)>BalanceFactor = Balanced;
N>BalanceFactor = LeftHeavy;
break;
}
P>BalanceFactor = Balanced;
RotateLeft(&N);
(*T)>LeftLink = N;
RotateRight(T);
*Heavier = false;
break;
}
}
/* -------------------------------------------------------------- */
void InsertNode(AirportCode A, TreeNode **T, Boolean *Heavier)
{
TreeNode *P, *N;
Boolean HeavierSubtree;

Table of Contents continued

175

180

185

190

195

200

205

210

215

220

225

230

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

/* If T was the empty tree, then change it to be a pointer to */


/* a new node with airport code A, and return */
if ((*T) == NULL) {
*T = CreateNode(A);
*Heavier = true;
} else {
if ( strcmp(A,(*T)>Airport) < 0 ) {
InsertNode(A,&(*T)>LeftLink,&HeavierSubtree);
if (HeavierSubtree) {
switch ((*T)>BalanceFactor) {
case LeftHeavy:
LeftRebalance(T,Heavier);
break;
case Balanced:
(*T)>BalanceFactor = LeftHeavy;
*Heavier = true;
break;
case RightHeavy:
(*T)>BalanceFactor = Balanced;
*Heavier = false;
break;
}
} else {
*Heavier = false;
}
} else {

/* we must have had A > (*T)>Airport */

InsertNode(A, &(*T)>RightLink, &HeavierSubtree);


if (HeavierSubtree) {
switch ( (*T)>BalanceFactor) {
case LeftHeavy:
(*T)>BalanceFactor = Balanced;
*Heavier = false;
break;
case Balanced:
(*T)>BalanceFactor = RightHeavy;
*Heavier = true;
break;
case RightHeavy:
RightRebalance(T,Heavier);
break;
}
} else {
*Heavier = false;
}
} /* end if */
} /* end if */
} /* end InsertNode */
/* -------------------------------------------------------------- */

Table of Contents continued


235

240

245

250

255

260

265

270

275

280

285

290

300

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

void RebalanceToLeft(TreeNode **T, Boolean *Lighter)


{
/* Rebalance after T's left subtree became lighter */
TreeNode *N, *P;
switch ( (*T)>BalanceFactor ) {
case LeftHeavy:
(*T)>BalanceFactor = Balanced;
*Lighter = true;
break;
case Balanced:
(*T)>BalanceFactor = RightHeavy;
*Lighter = false;
break;
case RightHeavy:
N = (*T)>RightLink;
switch (N>BalanceFactor) {
case LeftHeavy:
P = N>LeftLink;
switch (P>BalanceFactor) {
case LeftHeavy:
(*T)>BalanceFactor = Balanced;
N>BalanceFactor = RightHeavy;
break;
case Balanced:
(*T)>BalanceFactor = Balanced;
N>BalanceFactor = Balanced;
break;
case RightHeavy:
(*T)>BalanceFactor = LeftHeavy;
N>BalanceFactor = Balanced;
break;
}
RotateRight(&N); /* double left rotation rooted at T */
(*T)>RightLink = N;
RotateLeft(T);
*Lighter = true;
break;
case Balanced:
(*T)>BalanceFactor = RightHeavy;
N>BalanceFactor = LeftHeavy;
RotateLeft(T);
*Lighter = false;
break;
case RightHeavy:
(*T)>BalanceFactor = Balanced;
N>BalanceFactor = Balanced;
RotateLeft(T);
*Lighter = true;
break;

Table of Contents continued

305

310

315

320

325

330

335

340

345

350

355

360

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

} /* end switch */
break;
} /* end switch */
} /* RebalanceToLeft */
/* -------------------------------------------------------------- */
void RebalanceToRight(TreeNode **T, Boolean *Lighter)
{
/* Rebalance after T's right subtree became lighter */
TreeNode *N, *P;
switch ((*T)>BalanceFactor) {
case LeftHeavy:
N = (*T)>LeftLink;
switch (N>BalanceFactor) {
case LeftHeavy:
(*T)>BalanceFactor = Balanced;
N>BalanceFactor = Balanced;
RotateRight(T);
*Lighter = true;
break;
case Balanced:
(*T)>BalanceFactor = LeftHeavy;
N>BalanceFactor = RightHeavy;
RotateRight(T);
*Lighter = false;
break;
case RightHeavy:
P = N>RightLink;
switch (P>BalanceFactor) {
case LeftHeavy:
(*T)>BalanceFactor = RightHeavy;
N>BalanceFactor = Balanced;
break;
case Balanced:
(*T)>BalanceFactor = Balanced;
N>BalanceFactor = Balanced;
break;
case RightHeavy:
(*T)>BalanceFactor = Balanced;
N>BalanceFactor = LeftHeavy;
break;
}
RotateLeft(&N); /* double right rotation rooted at T */
(*T)>LeftLink = N;
RotateRight(T);
*Lighter = true;

Table of Contents continued

365

370

375

380

385

390

395

400

405

410

415

420

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

break;
} /* end switch */
break;
case Balanced:
(*T)>BalanceFactor = LeftHeavy;
*Lighter = false;
break;
case RightHeavy:
(*T)>BalanceFactor = Balanced;
*Lighter = true;
break;
} /* end switch */
} /* RebalanceToRight */
/* -------------------------------------------------------------- */
void DeleteSmallest(AirportCode *A,TreeNode **T, Boolean *Lighter)
{
TreeNode *temp;
Boolean SubtreeLighter;
if ( (*T)>LeftLink == NULL) {
if ( (*T)>RightLink == NULL) {
strcpy(*A,(*T)>Airport);
free(*T);
(*T) = NULL;
*Lighter = true;
} else {
strcpy(*A,(*T)>Airport);
temp = (*T);
(*T) = (*T)>RightLink;
free(temp);
*Lighter = true;
}
} else {
/* neither (*T)>LeftLink nor (*T)>RightLink is NULL */
DeleteSmallest(A,&(*T)>LeftLink,&SubtreeLighter);
if (SubtreeLighter) RebalanceToLeft(T,Lighter);
}
}
/* -------------------------------------------------------------- */
void DeleteLargest(AirportCode *A, TreeNode **T, Boolean *Lighter)
{
TreeNode *temp;
Boolean SubtreeLighter;
if ( (*T)>RightLink == NULL ) {
if ( (*T)>LeftLink == NULL ) {
strcpy(*A,(*T)>Airport);
free(*T);
(*T) = NULL;
*Lighter = true;
} else {
strcpy(*A,(*T)>Airport);
temp = (*T);
(*T) = (*T)>LeftLink;

Table of Contents continued


425

430

435

440

445

450

455

460

465

470

475

480

485

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

free(temp);
*Lighter = true;
}
} else {
/* neither (*T)>LeftLink nor (*T)>RightLink is NULL */
DeleteLargest(A,&(*T)>RightLink,&SubtreeLighter);
if (SubtreeLighter) RebalanceToRight(T,Lighter);
}
}
/* -------------------------------------------------------------- */
void DeleteRoot(TreeNode **T, Boolean *Lighter)
{
AirportCode A;
Boolean SubtreeLighter;
if ( ((*T)>RightLink == NULL) && ((*T)>LeftLink == NULL) ) {
free(*T);
(*T) = NULL;
*Lighter = true;
} else {
switch ((*T)>BalanceFactor) {
case LeftHeavy:
DeleteLargest(&A,&(*T)>LeftLink,&SubtreeLighter);
strcpy((*T)>Airport,A);
if (SubtreeLighter) {
(*T)>BalanceFactor = Balanced;
*Lighter = true;
} else {
*Lighter = false;
}
break;
case Balanced:
DeleteSmallest(&A,&(*T)>RightLink,&SubtreeLighter);
strcpy((*T)>Airport,A);
if (SubtreeLighter) (*T)>BalanceFactor = LeftHeavy;
*Lighter = false;
break;
case RightHeavy:
DeleteSmallest(&A,&(*T)>RightLink,&SubtreeLighter);
strcpy((*T)>Airport,A);
if (SubtreeLighter) {
(*T)>BalanceFactor = Balanced;
*Lighter = true;
} else {
*Lighter = false;
}
break;
} /* end switch */
} /* end if */
} /* DeleteRoot */
/* -------------------------------------------------------------- */

Table of Contents continued

490

495

500

505

510

515

520

525

530

535

540

545

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

void DeleteNode(AirportCode A,TreeNode **T, Boolean *Lighter)


{
Boolean SubtreeLighter;
int result;
if ((*T) == NULL) {
/* AirportCode was not in tree T */
*Lighter = false;
} else if ( (result = strcmp(A,(*T)>Airport)) == 0){
DeleteRoot(T,Lighter);
} else if (result < 0 ) {
DeleteNode(A,&(*T)>LeftLink,&SubtreeLighter);
if (SubtreeLighter) {
RebalanceToLeft(T,Lighter);
} else {
/* left subtree did not get lighter after deletion */
*Lighter = false;
}
} else {
/* A > T>Airport */
DeleteNode(A,&(*T)>RightLink,&SubtreeLighter);
if (SubtreeLighter) {
RebalanceToRight(T,Lighter);
} else {
/* right subtree did not get lighter after deletion */
*Lighter = false;
}
}
} /* DeleteNode */
/* -------------------------------------------------------------- */
TreeNode *TreeSearch(AirportCode A, TreeNode *T)
{
TreeNode *N;
int result;
/* Start with N pointing to the root of the tree T */
N = T;
/* Advance the pointer N down the tree until finding the node with A */
while (N != NULL) {
if ( (result = strcmp(A,N>Airport)) == 0) {
return N;
} else if (result < 0) {
N = N>LeftLink;
} else {
N = N>RightLink;
}
}
/* If no node in tree T contains A and N becomes NULL, */
/* we return NULL */
return N;
} /* TreeSearch */
/* -------------------------------------------------------------- */
void PrintBalanceFactor(TreeNode *T)

Table of Contents continued


550

555

560

565

570

575

580

585

590

595

600

605

610

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

{
if (T != NULL) {
switch (T>BalanceFactor) {
case LeftHeavy:
putchar('');break;
case RightHeavy:
putchar('+');break;
case Balanced:
putchar('=');break;
}
}
}
/* -------------------------------------------------------------- */
void PrintTree(TreeNode *T)
{
if (T != NULL) {
if (T>LeftLink != NULL) {
putchar('(');
PrintTree(T>LeftLink);
putchar(')');
}
printf(" %s",T>Airport);
PrintBalanceFactor(T);
putchar(' ');
if (T>RightLink != NULL) {
putchar('(');
PrintTree(T>RightLink);
putchar(')');
}
} else {
printf("( )");
}

/* the null tree prints as ( ) */

}
/* -------------------------------------------------------------- */
void Delete(AirportCode A)
{
printf("\nAbout to delete %s: ",A);
DeleteNode(A,&T,&Lighter);
PrintTree(T);
putchar('\n');
}
/* -------------------------------------------------------------- */
int main(void)
{
/* Construct the AVL tree of Fig. 9.46 on p. 382 */
T = NULL;
InsertNode("ORY",&T,&Heavier);

Table of Contents continued

615

620

625

630

635

640

645

650

655

660

665

670

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

InsertNode("JFK",&T,&Heavier);
InsertNode("BRU",&T,&Heavier);
/* Print the Tree */
PrintTree(T);
putchar('\n');
InsertNode("DUS",&T,&Heavier);
InsertNode("ZRH",&T,&Heavier);
InsertNode("MEX",&T,&Heavier);
InsertNode("ORD",&T,&Heavier);
InsertNode("NRT",&T,&Heavier);
/* Print the Tree */
PrintTree(T);
putchar('\n');
InsertNode("ARN",&T,&Heavier);
InsertNode("GLA",&T,&Heavier);
InsertNode("GCM",&T,&Heavier);
/* Print the Tree */
PrintTree(T);
putchar('\n');
/* Create a Binary Search Tree T pointing to 15 airports */
/*
T = NULL;
InsertNode("MEX",&T,&Heavier);
InsertNode("GCM",&T,&Heavier);
InsertNode("ORY",&T,&Heavier);
InsertNode("BRU",&T,&Heavier);
InsertNode("HKG",&T,&Heavier);
InsertNode("NRT",&T,&Heavier);
InsertNode("YYZ",&T,&Heavier);
*/
/*
InsertNode("ARN",&T,&Heavier);
InsertNode("DUS",&T,&Heavier);
InsertNode("GLA",&T,&Heavier);
InsertNode("JFK",&T,&Heavier);
InsertNode("MIA",&T,&Heavier);
InsertNode("ORD",&T,&Heavier);
InsertNode("SAN",&T,&Heavier);
InsertNode("ZRH",&T,&Heavier);
*/
/* Print the Tree */
/*
PrintTree(T);
putchar('\n');
Delete("NRT"); /* does DeleteNode("NRT",&T,&Lighter); and prints tree */
Delete("ORY"); /* does DeleteNode("ORY",&T,&Lighter); and prints tree */
Delete("YYZ"); /* does DeleteNode("YYZ",&T,&Lighter); and prints tree */
Delete("GCM");/* does DeleteNode("GCM",&T,&Lighter); and prints tree */
Delete("HKG"); /* does DeleteNode("HKG",&T,&Lighter); and prints tree */
Delete("MEX"); /* does DeleteNode("MEX",&T,&Lighter); and prints tree */
Delete("BRU"); /* does DeleteNode("BRU",&T,&Lighter); and prints tree */
putchar('\n');
*/

Table of Contents continued

675

680

685

690

695

700

705

710

715

720

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

/* Create a Binary Search Tree T pointing to 15 airports */


/*
T = NULL;
InsertNode("MEX",&T,&Heavier);
InsertNode("GCM",&T,&Heavier);
InsertNode("ORY",&T,&Heavier);
InsertNode("BRU",&T,&Heavier);
InsertNode("HKG",&T,&Heavier);
InsertNode("NRT",&T,&Heavier);
InsertNode("YYZ",&T,&Heavier);
InsertNode("ARN",&T,&Heavier);
InsertNode("DUS",&T,&Heavier);
InsertNode("GLA",&T,&Heavier);
InsertNode("JFK",&T,&Heavier);
InsertNode("MIA",&T,&Heavier);
InsertNode("ORD",&T,&Heavier);
InsertNode("SAN",&T,&Heavier);
InsertNode("ZRH",&T,&Heavier);
*/
/* Print the Tree */
/*
PrintTree(T);
putchar('\n');
*/
/* Do Tree Searches */
/*
N = TreeSearch("MEX",T);
N = TreeSearch("GCM",T);
N = TreeSearch("ORY",T);
N = TreeSearch("BRU",T);
N = TreeSearch("HKG",T);
N = TreeSearch("NRT",T);
N = TreeSearch("YYZ",T);
N = TreeSearch("ARN",T);
N = TreeSearch("DUS",T);
N = TreeSearch("GLA",T);
N = TreeSearch("JFK",T);
N = TreeSearch("MIA",T);
N = TreeSearch("ORD",T);
N = TreeSearch("SAN",T);
N = TreeSearch("ZRH",T);
*/
}
/* end main */

4. The answer to Ex. 9.8.4 is given in the program above in the


solution to Ex. 9.8.3.
5. Yes, AVL trees can be used to organize the free blocks in a
dynamic memory allocation algorithm so that block allocation
takes O(log n ) time. The details are in the hint in Ex. 9.8.5 on p. 388.

Answer to Review Question 9.9

Table of Contents continued


1. A 23 tree is a tree such that each non-empty node contains either
2 or 3 keys, such that all empty trees lie on the same bottom
level, and such that the InOrder traversal of the tree generates
the keys in increasing order.
2. The disadvantage of attempting to maintain perfect balance is
that, when we attempt to insert or delete nodes, costly
rebalancing may be required in which the relative position in the
tree of every node has to be rearranged. A 23 tree allows all
subtrees to be perfectly balanced with respect to heights, but
compromises on perfect balance by allowing nodes to have either
two or three subtrees as descendants. This compromise in perfect
balance allows for efficient insertion and deletion.
3. The branching factor of a node N in a tree T is the number of
descendants of node N in tree T. If all internal nodes have the same
number of descendants b , we say that b is the branching factor of
tree T.
4. The larger the branching factor the smaller the number of levels
needed to store a given number of keys n in a completely balanced
tree. It is advantageous to use trees with high branching factors
when attempting to search trees containing large numbers of
nodes stored in external memory devices for which the cost of
accessing nodes is relatively high, because the cost of search is
proportional to the number of levels that need to be searched.

Solutions to Selected Exercises in Exercise 9.9


1. The tree is drawn below.
L
H

P
J

Table of Contents continued


2. The deepest B-tree of order m containing n nodes can be
constructed by taking the minimum number of children allowed for
each node according to the definition of B-trees. Thus, the root
will have two children, and each internal node on levels below the
root will have m /2 children. So the number of nodes on levels 1, 2,
3, ..., follows the geometric progression: m /2 , m /2 2 , m /2 3 , ... . If
all leaves appear on the level having level number k , the number of
leaves is a term in this progression of the form 2 m /2 k 1 . But the
number of leaves is just one more than the number of keys (as can
be seen by enumerating the leaves and keys of a B-tree in
generalized InOrder, which yields a sequence in which leaves and
keys alternate, and in which leaves are at both ends of the
sequence). Hence, n + 1 = #Keys + 1 = #leaves. Therefore, for the
deepest tree, k is the largest integer for which,

n + 1 2m /2k1.
Solving for k yields,
k

n+1
1 + log m /2
.
2

So,

n+1
k = 1 + log m /2
.
2

Answers to Review Questions 9.10


1. The word trie is pronounced so as to rhyme with pie or try.
2. A trie is a tree organized to use the i t h character c of a key to
select the c t h subtree on level i of a node at level i 1. For
example, suppose keys are spelled from letters of the alphabet.
For a given key K, the first letter K [ 0 ] of K is used to select a
subtree of the root of the trie (where each of the subtrees of the
root corresponds to a different first letter of the key K). The
second letter of K, which is K [ 1 ] (if there is one) selects the
grandchild subtree of the root, and so on. When the characters
associated with the edges of the path from the root to a node N
spell a unique prefix p of a key K (i.e., if K is the only key having a
prefix p among all keys stored in the trie) then K is stored in node
N.
3. The shape of a trie is insensitive to the order of insertion of keys.
Tries have remarkably good logarithmic search times. However,
the efficiency of tries can be reduced if there is clustering in the
prefixes of the keys (i.e., if many groups of keys share long
prefixes).

Table of Contents continued

Solutions to Selected Exercises in Exercise 9.10


1. Tries for searching 3-letter airport codes could be represented
by linked lists to save space, or by a 3-level trie with nodes each
consisting of pointer arrays of length 26 indexed by the letters of
the alphabet (to save look-up time). In the case of a dictionary of
65,000 words with a largest word length of 33, the nodes could be
represented by pointer arrays indexed by letters if the nodes
were full (say x % occupied by non-null subtree pointers), or (to
save space) by a sequence of pairs (l , p ) where l is a letter and p
is a subtree pointer, arranged in ascending order of l , so that
binary searching could be used to locate a particular pair (l , p ) ,
given l as a search key.

Answers to Review Questions 9.11


1. A Huffman Code is a variable-length binary code (formed from
strings over the alphabet {1, 0}) in which shorter bit strings are
used to represent frequently used letters and longer bit strings
are used to represent less frequently used letters in such a way as
to minimize encoded message lengths for messages which use
letters with the same relative frequencies as those used to
construct the Huffman Code.
2. A Huffman Coding Tree is a binary tree having 0s and 1s labelling
its branches and having letters on its leaves. The successive 0s
and 1s labelling the branches along a path from the root to a leaf
containing the letter L form the Huffman code for the letter L.
Given a bit string equal to the Huffman code for letter L, its
successive 0s and 1s can be used to select the successive
branches of the Huffman Coding Tree starting with the root,
identifying a path leading to the leaf containing L.
3. A Huffman Code minimizes the length of an encoded message
having the same relative letter frequencies as those used to
construct the Huffman code, thus achieving a shorter encoded
message length than a fixed length code.

Solutions to Selected Exercises in Exercise 9.11


1. STINT is encoded as 101111101010011 and SINE is encoded as
101110101000.
2. 100101010001011 decodes as NINES and 101010011010011
decodes as INTENT.
3. The solution to Ex. 9.11.3 is given in the following program:
|

/*

Table of Contents continued

10

15

20

25

30

35

40

45

50

55

60

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

* HuffmanCodes -- solution to Ex. 9.11.3


*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MaxCount 26
typedef struct ItemTag {
char
int
char
struct ItemTag
struct ItemTag
} ItemStruct;

Letter;
Frequency;
*Code;
*LLink;
*RLink;

ItemStruct *ItemArray[MaxCount];
/* ---------------------------------------------------------------------- */
void InitItemArray(void)
{
int i;
char *Letters = "ETAONISRHLDCUPFMWYBGVKQXJZ";
ItemStruct *N;
for (i = 0; i< MaxCount; ++i) {
N = (ItemStruct *) malloc(sizeof(ItemStruct));
N>Letter = Letters[i];
N>LLink = NULL;
N>RLink = NULL;
ItemArray[i] = N;
}
/* input letter frequencies from Table 9.52, followed by */
/* Letter Huffman Code assigned by the algorithm given below */
ItemArray[ 0]>Frequency = 1231;
ItemArray[ 1]>Frequency = 959;
ItemArray[ 2]>Frequency = 805;
ItemArray[ 3]>Frequency = 794;
ItemArray[ 4]>Frequency = 719;
ItemArray[ 5]>Frequency = 718;
ItemArray[ 6]>Frequency = 659;
ItemArray[ 7]>Frequency = 603;
ItemArray[ 8]>Frequency = 514;
ItemArray[ 9]>Frequency = 403;
ItemArray[10]>Frequency = 365;
ItemArray[11]>Frequency = 320;
ItemArray[12]>Frequency = 310;
ItemArray[13]>Frequency = 229;
ItemArray[14]>Frequency = 228;
ItemArray[15]>Frequency = 225;
ItemArray[16]>Frequency = 203;
ItemArray[17]>Frequency = 188;
ItemArray[18]>Frequency = 162;
ItemArray[19]>Frequency = 161;
ItemArray[20]>Frequency = 93;
ItemArray[21]>Frequency = 52;

/* E - 100 */
/* T - 110 */
/* A - 0000 */
/* O - 0001 */
/* N - 0011 */
/* I - 0100 */
/* S - 0110 */
/* R - 1010 */
/* H - 1011 */
/* L - 00100 */
/* D - 01010 */
/* C - 01110 */
/* U - 01111 */
/* P - 11100 */
/* F - 11101 */
/* M - 11110 */
/* W - 001010 */
/* Y - 001011 */
/* B - 010110 */
/* G - 010111 */
/* V - 111111 */
/* K - 1111101 */

Table of Contents continued


65

70

75

80

85

90

95

100

105

110

115

120

125

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

ItemArray[22]>Frequency = 20;
ItemArray[23]>Frequency = 20;
ItemArray[24]>Frequency = 10;
ItemArray[25]>Frequency = 9;

/* Q - 11111001 */
/* X - 111110000 */
/* J - 1111100010 */
/* Z - 1111100011 */

/* if the ItemArray is not sorted in descending order of */


/* frequencies, sort it here */
} /* InitItemArray */
/* ---------------------------------------------------------------------- */
void ConstructHuffmanTree(void)
{
int i,j;
ItemStruct *N;
Boolean NotFinished;
for (i = MaxCount1; i>0; i) {
N = (ItemStruct *) malloc(sizeof(ItemStruct));
N>Frequency
= ItemArray[i]>Frequency+ItemArray[i1]>Frequency;
N>RLink = ItemArray[i];
N>LLink = ItemArray[i1];
/* insert the new node in proper order of its frequency in */
/* the Item Array */
j = i2;
NotFinished = (j>=0);
while (NotFinished) {
if (ItemArray[j]>Frequency < N>Frequency) {
ItemArray[j+1] = ItemArray[j];
/* shift one space */
/* right to make a hole */
j;
NotFinished = (j >= 0);
} else {
NotFinished = false;
}
}
ItemArray[j+1] = N;

/* final placement of the new node N */

} /* end for */
} /* ConstructHuffmanTree */
/* ---------------------------------------------------------------------- */
char *Concat(char *S, char c)
{
char *T,*temp;
T = (char *)malloc(2+strlen(S));
temp = T;

/* allocate space for string */

while ((*T++ = *S++) != '\0') {


/* transfer chars of S into T */
;
}
*(T) = c;
/* append new character c to end of string T */

Table of Contents continued

130

135

140

145

150

155

160

165

170

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

*(++T) = '\0';

/* terminate T properly with a null character '\0' */

return temp;
}
/* ---------------------------------------------------------------------- */
void AssignHuffmanCodes(ItemStruct *N, char *CodeToAssign)
{
ItemStruct *L, *R;
/* Assume N is non-null */
N>Code = CodeToAssign;
L = N>LLink;
R = N>RLink;
if (L != NULL) AssignHuffmanCodes(L,Concat(CodeToAssign,'0'));
if (R != NULL) AssignHuffmanCodes(R,Concat(CodeToAssign,'1'));
} /* AssignHuffmanCodes */
/* ---------------------------------------------------------------------- */
void PrintHuffmanCodes(ItemStruct *N)
{
if (N != NULL) {
if ((N>RLink == NULL) && (N>LLink == NULL)) {
printf("%c %s\n", N>Letter, N>Code);
}
PrintHuffmanCodes(N>LLink);
PrintHuffmanCodes(N>RLink);
}
} /* PrintHuffmanCodes */
/* ---------------------------------------------------------------------- */
int main(void)
{
char *Test = "", *V;
InitItemArray();
ConstructHuffmanTree();
AssignHuffmanCodes(ItemArray[0],"");
PrintHuffmanCodes(ItemArray[0]);
}
/* main program */

Table of Contents continued

Answers to Review Questions 10.2


1. A graph G = (V,E) is a set of distinct vertices V together with a set
of edges E consisting of pairs of distinct vertices of V. In an
undirected graph , the edges {v i , v j } are unordered pairs (or sets)
of distinct vertices v i and v j . In a directed graph each edge (v i , v j )
is an ordered pair of vertices in which the direction of travel along
the edge is from v i to v j .
2. In an undirected graph there is no direction of travel along the
edges, whereas in a directed graph, each edge e = (v i , v j )
prescribes a direction of travel starting at the origin of the edge
v i and ending at the terminus of the edge v j.
3. A path in a graph G = (V, E) is a sequence of vertices v 1 , v 2 , ... , v n
such that there is an edge (v i , v i + 1 ) in E for each i in (1 i < n ). In
other words, a path is a sequence of vertices each of which is
adjacent to the next vertex in the sequence. A cycle is a path p =
v 1 , v 2 , ... , v n such that v 1 = v n , meaning that a cycle is a path that
forms a loop by starting and ending at the same vertex.
4. Two vertices v i and v j in a graph G are connected if there is a path
in G starting at v i and ending at v j.
5. A connected component of a graph G = (V, E) is a subset S of the
vertices V such that any two distinct vertices v i and v j in S are
connected.
6. An adjacency set V x for a vertex x in a graph G = (V, E) is the set
of all vertices y such that (x , y ) is an edge in E. In symbols, Vx = { y
| y V and (x, y) E}
7. If x is a vertex in a directed graph G = (V, E) the in-degree of x
the number of distinct vertices y V such that (y , x ) is an edge
E, and the out-degree of x is the number of distinct vertices y
such that (x , y ) is an edge in E. Put differently, the in-degree of
is the number of predecessors of x in G, and the out-degree of x
the number of successors of x in G.

is
in
V
x
is

Solutions to Selected Exercises in Exercises 10.2


1. Vertex 2 is the only vertex of degree 3 in the graph of Figure 10.2.
2. V 1 = { 2,3}

V 5 = {4, 6}

V 2 = {1, 3, 4}

V 6 = {4, 5, 7, 8}

V 3 = {1, 2}

V 7 = {4, 6}

Table of Contents continued

V 4 = {2, 5, 6, 7}

V 8 = {6}

Answers to Review Questions 10.3


1. An adjacency matrix for a graph G = (V, E) having n vertices v 1 , v 2 ,
... , v n is an n n array T such that T[ i , j ] = 1 if and only if (v i, v j) is
an edge in E and such that T[ i , j ] = 0 if and only if (v i , v j ) is not an
edge in E. The adjacency matrix for an undirected graph G is always
a symmetric matrix with a zero diagonal such that T[ i , j ] = T[ j , i ] for
all 1 i , j n and T[ i , i ] = 0 for all 1 i n . Here T is symmetric
because if v i is adjacent to v j in an undirected graph, then v j is
also adjacent to v i . In a directed graph, however, the adjacency
matrix need not be symmetric because it is possible to have a
directed edge traveling from v i to v j without there being a
directed edge in the reverse direction from v j to v i . Here, T[ i , j ] =
1 but T[ j, i] = 0.
2. If we look at row R i , corresponding to the vertex v i in the
adjacency matrix T for a graph G = (V, E), row Ri has a one in each
column j for which an edge e = ( v i, v j) exists in E, and Ri has a zero
in each column j for which no such edge ( v i , v j ) exists. We can
represent the information in row R i as a bit-vector in C by using a
sequence of n bits packed together, Ri = b1 b2 ... bn , such that bj =
T [ i , j ] . In C we can support this representation by a b i t - v e c t o r
representation of sets as follows. Let S e t S i z e be the number of
elements in the set, and assume that the set elements are
represented by the non-negative integers in the range 0:SetSize 1.
Let ByteSize represent the number of bits in a byte. We represent
the set by a contiguous array of bytes large enough to hold one bit
for each separate element in the set. If SetSize is an exact multiple
of ByteSize , the number of bytes we need is SetSize / ByteSize , but if
SetSize is not an exact multiple of ByteSize (i.e., if SetSize % ByteSize != 0)
then we need to use a number of bytes equal to 1 + SetSize / ByteSize.
Thus, the number of bytes to use is given by ByteLength = (SetSize %
ByteSize != 0) ? 1 : 0) + SetSize / ByteSize. We can then give a type definition
for a Set , as follows:
typedef char Set[ByteLength];

The following function Member(i,S) determines (by extraction and


shifting) the value of the i t h bit in the bit-vector representation
of the set S .
|
|
|
|

int Member (int i, Set S)


/* to return the value (0 or 1) of the ith bit */
{
/* in the bit-vector */
return ( (S[i/ByteSize] >> (i% ByteSize) ) & 1 );
/* representing set S */
}

Table of Contents continued


To represent a graph G = (V, E), one constructs an adjacency
matrix T for G using rows R i represented by bit-vectors
corresponding to the rows.
3. The adjacency sets Vx can be used in a graph representation of a
graph V = (G, E) to represent the set of edges (x , y ) in E having x
as one end-point and having another distinct vertex y as the other
end-point.
4. In a directed graph the in-degree of a vertex x is the number of
distinct edges terminating on vertex x , whereas the out-degree is
the number of distinct edges originating at x .

Solutions to Selected Exercises in Exercises 10.3


1. To compute the list of predecessors, Pred( x ), for a vertex x
starting with an adjacency matrix T for a graph G, you can scan
column x of T and make a list of the vertices y for which T[ y , x ] = 1.
For example, letting L be an initially empty list, you could use the
following program strategy:

|
|
|
|
|
|
|

/* initialize L to be the empty list */


for (y = 0; y < MaxVertexNumber; ++ y) {
if (T[y][x] == 1) {
Insert(y, &L);
/* insert new member y on list L */
}
}

2. The following is a complete test program for converting the array


of linked-lists graph representation into the array of bit-vectors
graph representation. It is executed on the graph of Figure 10.5.

10

15

20

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

/*
*
* Solution to Ex. 10.3.2 -- converting a Graph representation from
*
-- array of linked lists of successor vertices to an
*
-- array of bit vectors representing adjacency sets
*
*/
#include <stdio.h>
#include<stdlib.h>
#define NumberOfVertices 5
#define ByteSize 8
#define ByteLength (NumberOfVertices/ByteSize +
(((NumberOfVertices % ByteSize) != 0)?1:0))
typedef struct NodeTag {
int
Item;
struct NodeTag *Link;
} NodeType;
typedef NodeType *ArrayOfLinkedLists[NumberOfVertices];

Table of Contents continued

25

30

35

40

45

50

55

60

65

70

75

80

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

typedef char Set[ByteLength];


typedef Set ArrayOfSets[NumberOfVertices];
void PrintList(NodeType *L)
{
int A;
printf("(");
while (L != NULL) {
A = L>Item;
printf("%d",A);
L = L>Link;
if (L != NULL) printf(", ");
}
printf(")\n");
}
void InsertNewFirstNode (int A, NodeType **L)
{
NodeType *N;
N = (NodeType *)malloc(sizeof(NodeType));
N>Item = A;
N>Link = *L;
*L = N;
}
void Insert(int A, int B, ArrayOfLinkedLists F)
{
InsertNewFirstNode(A,&F[B1]);
}

/* insert A into set B */


/* in Graph F */

void PrintLinkedListGraph(ArrayOfLinkedLists F)
{
int i;
for (i=0; i<NumberOfVertices; ++i){
printf("%2d ",(i+1));
PrintList(F[i]);
}
}
int Member(int i, Set S)
{
i ;
/* membership is shifted from 1:n ==> 0:n1 */
return( ( S[i/ByteSize ] >> (i % ByteSize) ) & 1);
}
void InitializeSet(Set S)
{
int i;
for (i=0; i<ByteLength; ++i) S[i]='\0';
}
void InsertIntoSet(int i, Set S)
{
i;
/* insertion is shifted from the number range 1:n ==> 0:n1 */

Table of Contents continued


85

90

95

100

105

110

115

120

125

130

135

140

145

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

S[i/ByteSize] |= 1 << (i % ByteSize);


}
void ConvertSet(NodeType *R, Set S)
{
NodeType *N;
int
K;
N = R;
while (N != NULL) {
K = N>Item;
N = N>Link;
InsertIntoSet(K,S);
}
}
void ConvertGraphRep(ArrayOfLinkedLists F, ArrayOfSets G)
{
int i;
for (i=0; i<NumberOfVertices; ++i) {
InitializeSet(G[i]);
ConvertSet(F[i],G[i]);
}
}
void PrintSet(Set S)
{
int i;
for (i=1; i<=NumberOfVertices; ++i){
putchar((Member(i,S)?'1':'0'));
putchar(' ');
}
putchar('\n');
}
void PrintBitVectorGraph(ArrayOfSets G)
{
int i;
printf(" n ");
for (i=0; i<NumberOfVertices; ++i) printf("%2d",(i+1));
putchar('\n');
for (i=0; i<NumberOfVertices; ++i) {
printf("%2d ",(i+1)); /* adjust vertex numbers from 0:n1 ==> 1:n */
PrintSet(G[i]);
}
}
int main(void)
{
int i;
ArrayOfLinkedLists F;
/* F is a linked list graph representation */
ArrayOfSets G;
/* G is an array of bit-vectors for adjacency sets */
/* initialize the array of linked lists representation F */
for (i=0; i<NumberOfVertices; ++i) F[i] = NULL;
/* then load the array of linked lists representation F */
/* with graph of Fig. 10.5 */

Table of Contents continued

150

155

160

165

170

175

180

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

Insert(3, 1, F);
Insert(2, 1, F);
Insert(5, 2, F);
Insert(4, 2, F);
Insert(3, 2, F);
Insert(4, 3, F);
Insert(1, 5, F);

/* Insert 2 as a new member of the list in F[1] */

/* Insert 4 as a new member of the list in F[3] */

printf("print linked-list representation of the graph of Fig. 10.5, p.413:\n");


PrintLinkedListGraph(F);
putchar('\n');
ConvertGraphRep(F,G);
printf("print bit-vector representation of the graph of Fig. 10.5, p.413:\n");
PrintBitVectorGraph(G);
}
/* end main */
/* the results printed by the program above are as follows:
print linked-list representation of the graph of Fig. 10.5, p.413:
1 - (2, 3)
2 - (3, 4, 5)
3 - (4)
4 - ()
5 - (1)
print bit-vector representation of the graph of Fig. 10.5, p.413:
n - 1 2 3 4 5
1 - 0 1 1 0 0
2 - 0 0 1 1 1
3 - 0 0 0 1 0
4 - 0 0 0 0 0
5 - 1 0 0 0 0

*/

Answers to Review Questions 10.4


1. In depth-first searching in a tree we visit all children of a vertex
before any right brothers and sisters, whereas in breadth-first
searching in a tree, we visit all right brothers and sisters before
visiting any children. In depth-first searching in a graph, we visit
all previously unvisited neighbors of each neighbor of a vertex x
before visiting x s other neighbors, whereas in breadth-first
searching in a graph, we visit x s unvisited neighbors before
visiting any unvisited neighbors of those neighbors.
2. Using stacks to hold unvisited neighbors of a vertex in graph
searching yields depth-first searching because, as neighbors of x
are popped from the stack and unvisited neighbors of these
neighbors are pushed on the stack, we guarantee that neighbors of
the neighbors will be processed before other original neighbors of

Table of Contents continued

x lower on the stack. Using queues to hold unvisited neighbors in


graph searching results in breadth-first search because all
unvisited neighbors of x are processed in arrival order on the
queue before any neighbors of those neighbors which are inserted
on the end of the queue after all of the original neighbors.
3. We must ensure that we visit at least the list of those vertices
from which all others in the graph are accessible via some path in
the graph. In an undirected graph, for instance, we must ensure
that we initially visit at least on vertex in each separate
connected component.

Solutions to Selected Exercises in Exercises 10.4


1. Contents of Queue C
stage
(1)

List of vertices previously visited at each

(2, 3, 4)

(3, 4, 5, 6)

1, 2

(4, 5, 6)

1, 2, 3

(5, 6, 7, 8)

1, 2, 3, 4

(6, 7, 8)

1, 2, 3, 4, 5

(7, 8)

1, 2, 3, 4, 5, 6

(8)

1, 2, 3, 4, 5, 6, 7

( )

1, 2, 3, 4, 5, 6, 7, 8

2. For the solutions to Exs. 10.4.2 and 10.4.3, see the program that
follows:

10

15

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

/*
* Solutions to Exs. 10.4.2 and 10.4.3:
*
* Ex. 10.4.2 -- Refinement of Program Strategy 10.7 for Graph Searching
* Ex. 10.4.3 -- Recursive version of Program Strategy 10.7
*/
#include <stdio.h>
#include<stdlib.h>
#include "QueueInterface.h"

/* we use Queues as containers */


/* taken from Program 7.19 -- */
/* Circular Queue Representation, on pp. 282-283 */

#define MaxVertexNumber 9

/* actually one greater than */


/* max vertex number */
/* because we use vertices in 1:n instead of 0:n-1 */

typedef struct NodeTag {

Table of Contents continued


20

25

30

35

40

45

50

55

60

65

70

75

80

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

int
VertexNumber;
struct NodeTag *Link;
}Node;
typedef struct {
Boolean
Node
}Vertex;

Visited;
*AdjacencyList;

typedef Vertex ArrayOfVertices[MaxVertexNumber];


/* ------------------------------------------------------------------ */
void InsertNewFirstNode (int A, Node **L)
{
Node *N;
N = (Node *)malloc(sizeof(Node));
N>VertexNumber = A;
N>Link = *L;
*L = N;
}
/* ------------------------------------------------------------------ */
void ListInsert(int A, int B, ArrayOfVertices F)
{
InsertNewFirstNode(A,&F[B].AdjacencyList);
}
/* ------------------------------------------------------------------ */
void InitGraph(ArrayOfVertices G)
{
int i;
/* init G to the graph of Figure 10.9 on p. 417 */
for (i=1; i<MaxVertexNumber; ++i) {
G[i].Visited = false;
G[i].AdjacencyList = NULL;
}
/* G[1] = [2,3,4]; */
ListInsert(4,1,G);
ListInsert(3,1,G);
ListInsert(2,1,G);
/* G[2] = [5,6]; */
ListInsert(6,2,G);
ListInsert(5,2,G);
/* G[3] = []; */
/* G[4] = [7,8]; */
ListInsert(8,4,G);
ListInsert(7,4,G);
/* G[5] = [ ]; */
/* G[6] = [5]; */
ListInsert(5,6,G);
/* G[7] = [1,3,6]; */
ListInsert(6,7,G);
ListInsert(3,7,G);
ListInsert(1,7,G);

/* insert A into */
/* adjacency list */
/* of vertex B */
/* in Graph F */

Table of Contents continued

85

90

100

105

110

115

120

125

130

135

140

145

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

/* G[8] = [6]; */
ListInsert(6,8,G);
}
/* ------------------------------------------------------------------ */
void Visit(int x)
{
printf("Visit Vertex Number: %2d.\n",x);
}
/* ------------------------------------------------------------------ */
void GraphSearch(ArrayOfVertices G, int v)
/* Ex. 10.4.2 -- */
{
/* refinement of Program Strategy 10.7, p. 416 */
int i,x;
Queue C;
Node *L;
/* Mark each vertex as being unvisited */
for (i = 1; i<MaxVertexNumber; ++i) {
G[i].Visited = false;
}
InitializeQueue(&C);
Insert(v,&C);

/* initialize C to be empty */
/* let the search begin at vertex v */

while (!Empty(&C)) {
Remove(&C,&x); /* remove a vertex from the container C */
if (! G[x].Visited) {
Visit(x);
/* mark x as having been visited */
G[x].Visited = true;
/* Enter all unvisited vertices of Vx into C */
L = G[x].AdjacencyList;
while (L != NULL) {
if (! G[L>VertexNumber].Visited) {
Insert(L>VertexNumber, &C);
}
L = L>Link;
}
}
}
}
/* ------------------------------------------------------------------ */
/* the following recursive procedure is the solution to Ex. 10.4.3 */
void RecursiveGraphSearch(ArrayOfVertices A, int v)
{
Node *L;
/* for the recursive function, assume each vertex of */
/* the graph has already been marked unvisited */
if (!A[v].Visited) {
Visit(v);
A[v].Visited = true;
L = A[v].AdjacencyList;

Table of Contents continued

150

155

160

165

170

175

180

185

190

195

200

205

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

while (L != NULL) {
if (! A[L>VertexNumber].Visited) {
RecursiveGraphSearch(A, L>VertexNumber);
}
L = L>Link;
}
}
}
/* ------------------------------------------------------------------ */
void PrintLinkedListGraph(ArrayOfVertices A)
{
int i,j;
Node *L;
for (i = 1; i<MaxVertexNumber; ++i) {
L = A[i].AdjacencyList;
printf("G[%2d] = [",i);
while (L != NULL) {
printf("%2d ",L>VertexNumber);
L = L>Link;
}
printf("]\n");
}
putchar('\n');
}
/* ------------------------------------------------------------------ */
int main(void)
{
int i;
ArrayOfVertices G;

/* G is an array of vertices */
/* each vertex of which has */
/* a linked adjacency list */

InitGraph(G);
printf("print the graph of Figure 10.9 on p. 417:\n");
PrintLinkedListGraph(G);
printf("Search graph and print list of vertices visited.\n");
GraphSearch(G,1);
/* GraphSearch uses queue containers */
/* to test breadth-first search */
/* comment GraphSearch(G,1) above, and then uncomment */
/* RecursiveGraphSearch(G,1) below */
/* to test RecursiveGraphSearch */
/* RecursiveGraphSearch(G,1); */
}
/* end main */
/* The following is the printout from the program above:
print the graph of Figure 10.9 on p. 417:
G[ 1] = [ 2 3 4 ]
G[ 2] = [ 5 6 ]
G[ 3] = [ ]
G[ 4] = [ 7 8 ]

Table of Contents continued


210

215

220

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

G[ 5] = [ ]
G[ 6] = [ 5 ]
G[ 7] = [ 1 3 6 ]
G[ 8] = [ 6 ]
Search graph and print list of vertices visited.
Visit Vertex Number: 1.
Visit Vertex Number: 2.
Visit Vertex Number: 5.
Visit Vertex Number: 6.
Visit Vertex Number: 3.
Visit Vertex Number: 4.
Visit Vertex Number: 7.
Visit Vertex Number: 8.
*/

3. See lines 133:154 of the program given above in Ex. 10.4.2 for the
solution to Ex. 10.4.3.
4. All you have to do is to remove the initialization code (on lines
8:10) from Program Strategy 10.7 and make the removed lines into
a separate procedure, called, say, MarkVerticesUnvisited(G) , following
which, you insert a call on MarkVerticesUnvisited(G) on line 4 of Program
Strategy 10.10, and then change line 6 of Program Strategy 10.10
to be: if (! v.Visited) GraphSearch(G,v);

Answers to Review Questions 10.5


1. A DAG (directed acyclic graph) is a directed graph having no
cycles. [ Recall that a cycle in a directed graph is a path with two or
more distinct vertices that forms a loop. ]
2. Let G be a DAG. A topological order for the vertices of G is a list L
of Gs vertices such that if there is a path from vertex v to vertex
w in G, then v comes before w in the vertex list L. Another way of
saying this is that L is a vertex list for G in which, for each vertex
v , all predecessors of v are listed in L before v itself is listed.
3. First compute the in-degrees of each vertex v and store them,
say, in an array D[ v ] indexed by vertices of the graph G. Then, using
an initially empty queue Q, scan the array D and insert in Q all
vertices having in-degrees of 0. These are the vertices with no
predecessors in G which can be listed first in topological order.
Now let L be an empty list. Process the vertices v in the queue Q
one at a time until Q becomes empty. For each vertex v removed
from the front of Q, add v to the end of list L, decrease the indegree of each successor w of v by one, and if the in-degree of w
drops to zero, insert w on the rear of Q. When Q becomes empty, L
contains a list of vertices of G in topological order.

Solutions to Selected Exercises in Exercises 10.5

Table of Contents continued


1. See the function TopologicalOrder(G, L) on lines 82:138 of the following
program:

10

15

20

25

30

35

40

45

50

55

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

/*
* Solution to Exs. 10.5.1 and 10.5.3
*
* Ex. 10.5.1 -- Implement Program Strategy 10.12 for TopologicalOrder
* Ex. 10.5.3 -- Recursive version of Topological Order
*/
#include <stdio.h>
#include<stdlib.h>
#include "QueueInterface.h"

/* we use Queues as containers */


/* taken from Program 7.19 -- */
/* Circular Queue Representation, on pp. 282-283 */

#define MaxVertexNumber 6
/* actually one greater than max vertex */
/* number because we use vertices in 1:n instead of 0:n-1 */
typedef struct NodeTag {
int
struct NodeTag
}Node;
typedef struct {
int
Node
}Vertex;

VertexNumber;
*Link;

InDegree;
*AdjacencyList;

typedef Vertex ArrayOfVertices[MaxVertexNumber];


Queue TopoOrderList;

/* the output list is represented */


/* by a queue for simplicity */

/* ------------------------------------------------------------------ */
void InsertNewFirstNode (int A, Node **L)
{
Node *N;
N = (Node *)malloc(sizeof(Node));
N>VertexNumber = A;
N>Link = *L;
*L = N;
}
/* ------------------------------------------------------------------ */
void ListInsert(int A, int B, ArrayOfVertices F)
/* insert A into */
{
/* adjacency list of vertex B */
InsertNewFirstNode(A,&F[B].AdjacencyList);
/* in Graph F */
}
/* ------------------------------------------------------------------ */
void InitGraph(ArrayOfVertices G)
{
int i;
/* init G to the graph of Figure 10.11 on p. 420 */

Table of Contents continued


60

65

70

75

80

85

90

95

100

105

110

115

120

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

for (i=1; i<MaxVertexNumber; ++i) {


G[i].InDegree = 0;
G[i].AdjacencyList = NULL;
}
/* G[1] = [2,3,4,5]; */
ListInsert(5,1,G);
ListInsert(4,1,G);
ListInsert(3,1,G);
ListInsert(2,1,G);
/* G[2] = [4]; */
ListInsert(4,2,G);
/* G[3] = [2,5]; */
ListInsert(5,3,G);
ListInsert(2,3,G);
/* G[4] = [5]; */
ListInsert(5,4,G);
}
/* ------------------------------------------------------------------ */
/* the following procedure is the solution to Ex. 10.5.1 */
void TopologicalOrder(ArrayOfVertices G, Queue *TopoOrderList)
{
int x,w;
/* vertices are item types for use in queues */
Queue Q;
Node *L;
/* Compute the in-degrees A[x].InDegree of the */
/* vertices x in the graph G */
/* initialize InDegrees to zero */
for (x=1; x<MaxVertexNumber; ++x) G[x].InDegree = 0;
for (x=1; x<MaxVertexNumber; ++x) {
L = G[x].AdjacencyList;
while (L != NULL) {
w = L>VertexNumber;
G[w].InDegree++;
L = L>Link;
}
}
/* Initialize the queue Q to contain all vertices */
/* having zero in-degrees */
InitializeQueue(&Q); /* initialize Q to be the empty queue */
for(x=1; x<MaxVertexNumber; ++x) {
if (G[x].InDegree == 0) {
Insert(x,&Q);
/* insert x on the rear of Q*/
}
/* if x's InDegree == 0 */
}
/* Initialize the list TopoOrderList to be the empty list */
InitializeQueue(TopoOrderList);
/* for simplicity, */
/* TopoOrderList is */
/* represented by a queue. */
/* Process vertices in the queue Q until the queue becomes empty */
while (!Empty(&Q)) {
Remove(&Q,&x);

/* Remove vertex x from the front of Q */

Table of Contents continued

125

130

135

140

145

150

155

160

165

170

175

180

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

Insert(x,TopoOrderList);

/* Insert x on the rear of */


/* list TopoOrderList */

/* for each successor w of x, where w in Vx do begin */


L = G[x].AdjacencyList;
while (L != NULL) {
w = L>VertexNumber;
G[w].InDegree ;
/* decrease predecessor */
/* count of w */
if (G[w].InDegree == 0) Insert(w,&Q);
/* insert w */
/* on the rear of Q */
L = L>Link;
}
}
}
/* TopologicalOrder */
/* ------------------------------------------------------------------ */
void ComputeInDegrees(ArrayOfVertices G)
{
int x,w;
/* vertices are item types for use in queues */
Node *L;
/* Compute the in-degrees A[x].InDegree of the */
/* vertices x in the graph G */
/* initialize InDegrees to zero */
for (x=1; x<MaxVertexNumber; ++x) G[x].InDegree = 0;
for (x=1; x<MaxVertexNumber; ++x) {
L = G[x].AdjacencyList;
while (L != NULL) {
w = L>VertexNumber;
G[w].InDegree++;
L = L>Link;
}
}
}
/* ------------------------------------------------------------------ */
/* the following recursive procedure is the solution to Ex. 10.5.3 */
void RecursiveTopoOrder(ArrayOfVertices G, int v)
{
Node *L;
if (G[v].InDegree == 0) Insert(v, &TopoOrderList);
L = G[v].AdjacencyList;
while (L != NULL) {
G[L>VertexNumber].InDegree ;
RecursiveTopoOrder(G,L>VertexNumber);
L = L>Link;
}
}

Table of Contents continued

185

190

195

200

205

210

215

220

225

230

235

240

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

/* ------------------------------------------------------------------ */
void PrintLinkedListGraph(ArrayOfVertices A)
{
int i,j;
Node *L;
for (i = 1; i<MaxVertexNumber; ++i) {
L = A[i].AdjacencyList;
printf("G[%2d] = [",i);
while (L != NULL) {
printf("%2d ",L>VertexNumber);
L = L>Link;
}
printf("]\n");
}
putchar('\n');
}
/* ------------------------------------------------------------------ */
void PrintQueue(Queue *Q)
{
ItemType x;
putchar('(');
while (!Empty(Q)) {
Remove(Q,&x);
printf("%2d ",x);
}
printf(")\n");
}
/* ------------------------------------------------------------------ */
int main(void)
{
int i;
ArrayOfVertices G;

/* G is a linked list graph representation */

InitGraph(G);
printf("print the graph of Figure 10.11 on p. 420:\n");
PrintLinkedListGraph(G);
/* make a list of the vertices of G in topological order */
InitializeQueue(&TopoOrderList);
TopologicalOrder(G, &TopoOrderList);
/* comment out function calls above and comment in these three */
/* InitializeQueue(&TopoOrderList); */
/* ComputeInDegrees(G); */
/* Compute the*/
/* InDegrees of the Graph G */
/* RecursiveTopoOrder(G,1); */
/* This function */
/* assumes InDegrees are already computed */
/* and that the TopoOrderList is initialized to empty */
printf("The list of vertices in topological order is = \n");
PrintQueue(&TopoOrderList);
/* Print the list of vertices */
/* in topological order */
}

Table of Contents continued


245

250

255

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

/* end main */
/* The print out of the above program is as follows:
print the graph of Figure 10.11 on p. 420:
G[ 1] = [ 2 3 4 5 ]
G[ 2] = [ 4 ]
G[ 3] = [ 2 5 ]
G[ 4] = [ 5 ]
G[ 5] = [ ]
The list of vertices in topological order is =
(1 3 2 4 5)
*/

2. If, after the procedure of Program Strategy 10.12 terminates,


there are any vertices in the graph whose in-degree has not
dropped to zero and which are not listed in topological order, then
such vertices lie on an illegal cycle in the graph.
3. See the function RecursiveTopoOrder( ) on lines 167:181 of the program
given in Ex. 10.5.1 above.
4. Let G = (V,E) be a graph of n vertices and e edges. To compute indegrees takes time O( e + n ) because n vertices are scanned to
have their in-degrees initialized to zero, followed by scanning
each of the e edges in E to increment the in-degrees of their
termini. Then the n vertices are scanned to put on queue Q all
vertices with in-degree 0. Finally, each of the e edges in G is again
scanned while finding successors of each vertex whose in-degree
has dropped to zero and has been processed in the queue Q. The
total time required is thus O(e + n ).

Answers to Review Questions 10.6


1. A weighted graph is a directed graph in which positive numerical
quantities called w e i g h t s are attached to the edges. A s h o r t e s t
path between two vertices v and w in a weighted graph G is a path
from v to w in G, the sum of whose edge weights is less than or
equal to the sum of the edge weights on any other distinct path
from v to w .
2. Dijkstras shortest path algorithm gives a way to compute the
shortest distance from a single chosen vertex v to every other
vertex w in a weighted directed graph G. The basic idea is to start
with a subset W = {v } of the vertices V of graph G, and to enlarge it
in steps, adding at each step a new vertex w V W which is at a
minimum distance from v among all the vertices in V W that have
not yet been added to W. Eventually W is enlarged to contain all

Table of Contents continued


vertices of V and the shortest distance from v to every other
vertex in V then becomes known.

Solutions to Selected Exercises in Exercises 10.6


1. The function ShortestPath(void) given on lines 171:230 below provides
a refinement of Program Strategy 10.16.

10

15

20

25

30

35

40

45

50

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

/*
*
*/

Solution to Ex. 10.6.1 -- ShortestPaths

#include <stdio.h>
#include<stdlib.h>
#define MaxVertex 6
#define Infinity 32000 /* it can't be INT_MAX, since adding small edge */
/* lengths to INT_MAX causes it to wrap to a */
/* big negative number. */
#define NumberOfVertices 7

/* one more than MaxVertex */


/* because we start at 1 not 0 */

#define ByteSize 8
#define ByteLength (NumberOfVertices/ByteSize +
(((NumberOfVertices % ByteSize) != 0)?1:0))
typedef int Weight;
typedef int Vertex;
typedef Weight AdjacencyMatrix[NumberOfVertices][NumberOfVertices];
typedef char Set[ByteLength];
AdjacencyMatrix T;
Set W;
Weight ShortestDistance[NumberOfVertices];
/* -------------------------------------------------------------- */
int Member(int i, Set S)
{
i ;
/* membership is shifted from 1:n ==> 0:n1 */
return ( ( S[i/ByteSize ] >> (i % ByteSize) ) & 1);
}
/* -------------------------------------------------------------- */
void InitializeSet(Set S)
{
int i;
for (i=0; i<ByteLength; ++i) S[i]='\0';
}
/* -------------------------------------------------------------- */

Table of Contents continued

55

60

65

70

75

80

85

90

95

100

105

110

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

void InsertIntoSet(int i, Set S)


{
i ;
/* insertion is shifted from the number */
/* range 1:n to the range 0:n1 */
S[i/ByteSize] |= 1 << (i % ByteSize);
}
/* -------------------------------------------------------------- */
void PrintSet(Set S)
{
int i;
for (i=1; i<=MaxVertex; ++i){
putchar((Member(i,S)?'1':'0'));
putchar(' ');
}
putchar('\n');
}
/* -------------------------------------------------------------- */
/* Boolean Full(Set S) returns true iff set S == the whole vertex set V. */
/* Note: this function is inefficient and should later be replaced by a*/
/* test against a "full" bit-vector. For example, "return (S[0] == '\077');" */
/* suffices in the case of the current one-byte bit-vector example. */
Boolean Full(Set S)
{
int i;
Boolean result;
result = true;
for (i=1; i<=MaxVertex; ++i){
if (!Member(i,W)) {
result = false;
break;
}
}
return result;
}
/* -------------------------------------------------------------- */
void InitializeAdjacencyMatrix(AdjacencyMatrix T)
{
int i,j;
for (i=1; i<=MaxVertex; ++i) {
for (j=1; j<=MaxVertex; ++j) {
T[i][j] = Infinity;
}
}
for (i=1; i<=MaxVertex; ++i) T[i][i] = 0;
T[1][2] = 3; T[1][6] = 5;
T[2][3] = 7; T[2][6] = 10;
T[3][4] = 5; T[3][5] = 1;

Table of Contents continued


115

120

125

130

135

140

145

150

155

160

165

170

175

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

T[4][5] = 6;
T[5][6] = 7;
T[6][4] = 2; T[6][3] = 8;
}
/* -------------------------------------------------------------- */
void PrintAdjacencyMatrix(AdjacencyMatrix T)
{
int i,j;
printf("---");
for (i=1; i<=MaxVertex; ++i) printf("%3d",i);
putchar('\n');
for (i=1; i<=MaxVertex; ++i) {
printf("%3d",i);
for (j=1; j<=MaxVertex; ++j) {
if (T[i][j] == Infinity) {
printf(" ");
} else {
printf("%3d",T[i][j]);
}
}
putchar('\n');
}
putchar('\n');
putchar('\n');
}
/* -------------------------------------------------------------- */
void PrintDistances(void)
{
int i;
printf("Shortest Distances to Vertices in Graph G\n");
for (i=1; i<=MaxVertex; ++i) {
printf("vertex %2d: %5d\n",i, ShortestDistance[i]);
}
}
/* -------------------------------------------------------------- */
Weight Minimum(Weight x, Weight y)
{
if (x < y) {
return x;
} else {
return y;
}
}
/* -------------------------------------------------------------- */
void ShortestPath(void)
{
/* Let MinDistance be a variable that contains edge weights */
/* as values and let Minimum(x,y) be a function whose value */

Table of Contents continued

180

185

190

195

200

205

210

215

220
*/

225

230

235

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

/* is the lesser of x and y */


Vertex v1,u1,w1,v2;
Weight MinDistance;
/* Let v1 in V be the origin vertex at which the shortest path starts */
/* Initialize W and ShortestDistance[u] as follows */
/* in the comments that follow V is the entire vertex set */
v1 = 1;

/* set up the origin vertex */

/* W = [v1]; */
InitializeSet(W);
InsertIntoSet(v1,W);
ShortestDistance[v1] = 0;
for (v2 = 1; v2<=MaxVertex; ++v2) {
/* for each v2 in V [v1] */
if (!Member(v2,W)) ShortestDistance[v2] = T[v1][v2];
}
/* PrintDistances(); */
/* Now repeatedly enlarge W until W includes all vertices in V */
while (!Full(W)) {
/* while (W != V) { */
/* find the vertex v2 in V W of minimum distance from v1 */
MinDistance = Infinity;
for (w1=1; w1<=MaxVertex; ++w1) { /* for each w1 in V W */
if (!Member(w1,W)) {
/* if w1 in (V W) */
if (ShortestDistance[w1] < MinDistance) {
MinDistance = ShortestDistance[w1];
v2 = w1;
}
}
}
/* add v2 to W by doing W = W + [v2]; */
InsertIntoSet(v2,W);
printf("new vertex to add to W = %2d\n",v2);
/* update the shortest distances to vertices in V W */
for (u1=1; u1<=MaxVertex; ++u1) {/* for each u1 in V W
if (!Member(u1,W)) {
ShortestDistance[u1] =
Minimum(ShortestDistance[u1],
ShortestDistance[v2] + T[v2][u1]);
}
}
} /* end while */
}
/* end ShortestPath */
/* -------------------------------------------------------------- */
int main(void)
{

Table of Contents continued

240

245

250

255

260

265

270

275

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

/* Initialization */
InitializeAdjacencyMatrix(T);
printf("The adjacency matrix for graph G is:\n");
PrintAdjacencyMatrix(T);
/* Find Shortest Path */
ShortestPath();
/* Print the ShortestDistances */
putchar('\n');
PrintDistances();
PrintSet(W);
}
/* end main program */
/* the printout obtained from running the above program is:
The adjacency matrix for graph G is:
--- 1 2 3 4 5 6
1 0 3 5
2 0 7 10
3 0 5 1
4 0 6
5 0 7
6 8 2 0

new
new
new
new
new

vertex
vertex
vertex
vertex
vertex

to
to
to
to
to

add
add
add
add
add

to
to
to
to
to

W
W
W
W
W

=
=
=
=
=

2
6
4
3
5

Shortest Distances to Vertices in Graph G


vertex 1:
0
vertex 2:
3
vertex 3:
10
vertex 4:
7
vertex 5:
11
vertex 6:
5

*/

2. We have initially, that S = ( 2 i n ) ( 1 j n + 1 i ) C, where C is a


n ( n 1)
constant and we want to show that S = C *
, so that S is O(n 2 ).
2
First, recall that the number of integers in the range a : b = { j | a j
b } is b a + 1 = top bottom + 1, so we have ( 1 j n + 1 i ) C =
C*(n + 1 i).
Hence,
C * (2 i n ) [(n + 1) i]

S =
C*

=
=

(1in) [(n + 1) i] n

C*

[ n *(n + 1)

n (n +1)
n]
2

Table of Contents continued


=
=
=
=

n (n +1)
2n

]
2
2
n 2 + n 2n
C * [
]
2
n2 n
C *[
]
2
n (n 1 )
C * [
] ,
2

C*

which is what we

wanted to show.

Answers to Review Questions 10.7


1. A task network is a directed acyclic graph (DAG) whose nodes
represent tasks and have task durations attached. A PERT Chart is
a representation of a task network showing, for each task, its
predecessors, its successors, and its duration. A milestone chart
is a view (or representation) of a task network showing for each
task its earliest start time, latest finishing time, and slack time
(if any). Certain key times on the critical path are presented as
milestones.
2. A critical path in a task network G is a path from the start node of
G to the finish node of G such that if any task on the critical path
has its completion time delayed by an amount of time T, then the
completion time for the entire project is delayed by T .
3. Slack time is the time duration available for delaying a tasks
individual completion time that can occur without delaying the
entire project completion time. Only tasks that are not on the
critical path can have such slack time.
4. Tasks on the critical path have no slack time because their
completion times cannot be delayed without delaying the project
completion time.

Solutions to Selected Exercises in Exercises 10.7


1. Complete implementations of Program Strategies 10.24, 10.25,
10.27 and 10.28 are given in the following complete C program.
However, the arrays used in this program are not indexed by
indices in the range 0:n 1 as called for in Ex. 10.7.1. In contrast,
indices in the range 0:n are used where n is the maximum vertex
number of a vertex in the example of Figure 10.19. This sidesteps
an index range translation that would have rendered the program
below less legible.
|

/*

Table of Contents continued

10

15

20

25

30

35

40

45

50

55

60

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

*
*/

Solution to Ex. 10.7.1 -- Critical Path Algorithms

#include <stdio.h>
#include <stdlib.h>
#include "QueueInterface.h"

/* use Queue ADT, see p. 262 and */


/* pp 282283 */

#define MaxVertex 12
/* Actually MaxVertex == 1 + maximum vertex number used, in order */
/* to accommodate arrays indexed by 1:n instead of 0:n1 */

typedef int Duration;


typedef int VertexNumber;
typedef Duration DurationArray[MaxVertex];
typedef int AdjacencyMatrix[MaxVertex][MaxVertex];
AdjacencyMatrix T;
DurationArray D,EFT,EST,LFT,LST;
Duration PFT;
VertexNumber TopoOrderArray[MaxVertex];
/* -------------------------------------------------------------- */
void InitializeDurationArray(DurationArray D)
{
D[1] = 0;
D[2] = 5;
D[3] = 9;
D[4] = 20;
D[5] = 2;
D[6] = 3;
D[7] = 10;
D[8] = 15;
D[9] = 2;
D[10] = 10;
D[11] = 0;
}
/* -------------------------------------------------------------- */
void InitializeAdjacencyMatrix(AdjacencyMatrix T)
{
int i,j;
for (i = 1; i<MaxVertex; ++i) {
for (j = 1; j<MaxVertex; ++j) {
T[i][j] = 0;
}
}
T[1][2] = 1; T[1][4] = 1;
T[2][3] = 1;

Table of Contents continued


65

70

75

80

85

90

95

100

105

110

115

120

125

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

T[3][5] = 1;
T[4][5] = 1;
T[5][6] = 1; T[5][7] = 1; T[5][8] = 1;
T[6][9] = 1;
T[7][9] = 1;
T[8][9] = 1;
T[9][10] = 1;
T[10][11] = 1;
}
/* -------------------------------------------------------------- */
void PrintAdjacencyMatrix(AdjacencyMatrix T)
{
int i,j;
printf("The Adjacency Matrix is:\n\n");
printf("---");
for (i = 1; i<MaxVertex; ++i) printf("%3d",i);
putchar('\n');
for (i = 1; i<MaxVertex; ++i) {
printf("%3d",i);
for (j = 1; j<MaxVertex; ++j) {
printf("%3d",T[i][j]);
}
putchar('\n');
}
putchar('\n');
putchar('\n');
}
/* -------------------------------------------------------------- */
void PrintDurationArray(DurationArray D)
{
int i;
printf(" i = ");
for (i = 1; i<MaxVertex; ++i) printf("%3d",i);
putchar('\n');
printf(" A[i] = ");
for (i = 1; i<MaxVertex; ++i) printf("%3d",D[i]);
putchar('\n');
}
/* -------------------------------------------------------------- */
void PrintTopoOrderArray(void)
{
int i;
printf("
i = ");
for (i = 1; i<MaxVertex; ++i) printf("%3d",i);
putchar('\n');
printf("TopoOrder = ");
for (i = 1; i<MaxVertex; ++i) printf("%3d",TopoOrderArray[i]);
putchar('\n');
}

Table of Contents continued

130

135

140

145

150

155

160

165

170

175

180

185

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

/* -------------------------------------------------------------- */
void PrintQueue(Queue *Q)
{
ItemType x;
putchar('(');
while (!Empty(Q)) {
Remove(Q,&x);
printf("%2d ",x);
}
printf(")\n");
}
/* -------------------------------------------------------------- */
void TopologicalOrder(void)
{
DurationArray InDegree;
ItemType x,w,i,j;
/* vertices are item types for use in queues */
Queue Q,L;
/* Compute the in-degrees InDegree[x] of the vertices x in G */
for (x=1; x<MaxVertex; ++x) InDegree[x] = 0;
/* initialize */
/* InDegree[x] to zero */
for (x=1; x<MaxVertex; ++x) {
for (w=1; w<MaxVertex; ++w) InDegree[x] += T[w][x];
}
/* Initialize the queue Q to contain all vertices */
/* having zero in-degrees */
InitializeQueue(&Q); /* initialize Q to be the empty queue */
for (x=1; x<MaxVertex; ++x) {
if (InDegree[x] == 0) Insert(x,&Q);
/* insert x */
/* on the rear of Q if InDegree[x] = 0 */
}
/* Initialize the list L to be the empty list */
InitializeQueue(&L);
/* for simplicity, we let the list L */
/* be represented by a queue, also. */
/* Process vertices in the queue Q until the */
/* queue becomes empty */
while (!Empty(&Q)) {
Remove(&Q,&x);
/* Remove vertex x from front of Q */
Insert(x,&L);
/* Insert x on rear of list L */
for (w=1; w<MaxVertex; ++w) { /* for each successor w */
/* of x, where w in Vx */
if (T[x][w] == 1) {
/* i.e., if w is a successor of x */
InDegree[w] ;
/* decrease predecessor */
/* count of w */
if (InDegree[w] == 0) {
Insert(w,&Q);
/* insert w */
}
/* on the rear of Q */
}
}
}
/* The list L now contains the vertices of G in topological order */
/* Remove them one by one and put them into TopoOrderArray. */
for (i=1; i<MaxVertex; ++i) {

Table of Contents continued

190

195

200

205

210

215

220

225

230

235

240

245

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

Remove(&L,&x);
TopoOrderArray[i] = x;
}
}
/* end TopologicalOrder */
/* -------------------------------------------------------------- */
Boolean EmptyPred(VertexNumber v)
{
VertexNumber w;
Boolean result;
result = true;
for (w=1; w<MaxVertex; ++w) {
if (T[w][v] == 1) {
result = false;
break;
}
}

/* w is a predecessor of v */

return result;
}
/* -------------------------------------------------------------- */
void ComputeEarliestFinishTimes(void)
{
VertexNumber i,v,w;
for (i=1; i<MaxVertex; ++i) {
v = TopoOrderArray[i];

/* let v be the ith vertex */


/* in topological order */
/* v has no predecessors */

if (EmptyPred(v)) {
EFT[v] = D[v];
} else {
EFT[v] = 0;
for (w=1; w<MaxVertex; ++w) {
/* w in Pred(v) */
if (T[w][v] == 1) {
/* if w is a predecessor of v */
if (EFT[w] + D[v] > EFT[v]) {
EFT[v] = EFT[w] + D[v];
}
}
}
}
}
}
/* -------------------------------------------------------------- */
void ComputeProjectFinishTime(Duration *PFT)
{
VertexNumber v;
/* The earliest Project Finish Time, PFT, is the latest of the */
/* earliest finishing times for any of the vertices in the graph. */
*PFT = 0;
for (v=1; v<MaxVertex; ++v) {

Table of Contents continued


250

255

260

265

270

275

280

285

290

295

300

305

310

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

if (EFT[v] > *PFT) *PFT = EFT[v];


}
}
/* -------------------------------------------------------------- */
Boolean EmptySucc(VertexNumber v)
{
VertexNumber w;
Boolean result;
result = true;
for (w=1; w<MaxVertex; ++w) {
if (T[v][w] == 1) {
result = false;
break;
}
}
return result;

/* w is a successor of v */

}
/* -------------------------------------------------------------- */
void ComputeLatestFinishTimes(void)
{
VertexNumber i,v,w;
for (i=MaxVertex; i>=1; i) {
v = TopoOrderArray[i];

/* let v be the ith vertex */


/* in reverse topological order */
/* if v has no successors */

if (EmptySucc(v)) {
LFT[v] = PFT;
} else {
LFT[v] = PFT;
for (w=1; w<MaxVertex; ++w) {
/* w in Succ(v) */
if (T[v][w] == 1) {
/* w is a successor of v */
if (LFT[w] D[w] < LFT[v]) {
LFT[v] = LFT[w] D[w];
}
}
}
}
}
}
/* ComputeLatestFinishTimes */
/* -------------------------------------------------------------- */
void ComputeLatestStartTimes(void)
{
VertexNumber v;
for (v=1; v<MaxVertex; ++v) LST[v] = LFT[v] D[v];
}
/* -------------------------------------------------------------- */
void ComputeEarliestStartTimes(void)
{

Table of Contents continued

315

320

325

330

335

340

345

350

355

360

365

370

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

VertexNumber v;
for (v=1; v<MaxVertex; ++v) EST[v] = EFT[v] D[v];
}
/* -------------------------------------------------------------- */
void PrintVerticesOnCriticalPath(void)
{
VertexNumber v;
printf("Vertices on Critical Path = \n");
for (v=1; v<MaxVertex; ++v) {
if (EST[v] == LST[v]) printf(" %3d",v);
}
}
/* -------------------------------------------------------------- */
int main(void) /* MainProgram */
{
/* Note: the following are declared globally:
AdjacencyMatrix T;
DurationArray D,EFT,EST,LFT,LST;
Duration PFT;
VertexNumber TopoOrderArray[MaxVertex];
*/
/* Initialization */
InitializeAdjacencyMatrix(T);
/* PrintAdjacencyMatrix(T); */
InitializeDurationArray(D);
/* printf("Duration Array:\n"); */
/* PrintDurationArray(D); putchar('\n'); */
/* Compute Topological Order */
TopologicalOrder();
PrintTopoOrderArray(); putchar('\n');
/* Compute Critical Path & Other Times in Activity Network */
ComputeEarliestFinishTimes();
ComputeProjectFinishTime(&PFT);
ComputeLatestFinishTimes();
ComputeLatestStartTimes();
ComputeEarliestStartTimes();
/* Print Results */
printf("Earliest Finish Times:\n");
PrintDurationArray(EFT); putchar('\n');
printf("Project Finish Time, PFT = %d\n",PFT);
printf("Latest Finish Times:\n");
PrintDurationArray(LFT); putchar('\n');
printf("Latest Start Times:\n");
PrintDurationArray(LST); putchar('\n');
printf("Earliest Start Times:\n");
PrintDurationArray(EST); putchar('\n');
/* Print vertices on Critical Path */
PrintVerticesOnCriticalPath();

Table of Contents continued


375

380

385

390

395

400

405

410

415

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

}
/* main program */
/* the printout obtained from running the above program is:
The Adjacency Matrix is:
--1
2
3
4
5
6
7
8
9
10
11

1
0
0
0
0
0
0
0
0
0
0
0

2
1
0
0
0
0
0
0
0
0
0
0

3
0
1
0
0
0
0
0
0
0
0
0

4
1
0
0
0
0
0
0
0
0
0
0

6
0
0
0
0
1
0
0
0
0
0
0

7
0
0
0
0
1
0
0
0
0
0
0

8
0
0
0
0
1
0
0
0
0
0
0

Duration Array:
i =
1 2
A[i] =
0 5

3 4
9 20

5
2

6 7 8
3 10 15

9 10 11
2 10 0

i =
TopoOrder =

2
2

4
3

5
5

8
8

1
1

5
0
0
1
1
0
0
0
0
0
0
0

3
4

9 10 11
0 0 0
0 0 0
0 0 0
0 0 0
0 0 0
1 0 0
1 0 0
1 0 0
0 1 0
0 0 1
0 0 0

6
6

7
7

9 10 11
9 10 11

Earliest Finish Times:


i =
1 2 3 4 5 6 7 8 9 10 11
A[i] =
0 5 14 20 22 25 32 37 39 49 49
Project Finish Time, PFT = 49
Latest Finish Times:
i =
1 2 3 4 5 6 7 8 9 10 11
A[i] =
0 11 20 20 22 37 37 37 39 49 49
Latest Start Times:
i =
1 2 3 4 5 6 7 8 9 10 11
A[i] =
0 6 11 0 20 34 27 22 37 39 49
Earliest Start Times:
i =
1 2 3 4 5 6 7 8 9 10 11
A[i] =
0 0 5 0 20 22 22 22 37 39 49
Vertices on Critical Path =
1
4
5
8
9
10

11

*/

2. Two noncritical tasks share the same slack time if they are in
series (i.e., if one task either precedes or follows the other along
a path), whereas two noncritical tasks are independent if they are
in parallel (i.e., if they are on separate paths or path segments
such that neither precedes or follows the other). Independent
noncritical tasks have independent slack times that do not need to
be shared. A slack time t shared between two tasks t 1 and t 2
means that if t 1 uses a portion u of t to delay its completion
time, then t 2 cannot delay its completion time by more than t u
without delaying the project completion time. That is, shared slack

Table of Contents continued


time u used by one task diminishes the slack time available to
the other task by an identical amount u .

Answers to Review Questions 10.8


1. If G is a weighted, connected, undirected graph, and T is a subgraph
of G containing all vertices of G and some of the edges of G such
that T is a tree, then T is said to be a spanning tree for G. If the
cost of a spanning tree is defined to be the sum of the edge
weights for edges in T, then a minimal spanning tree for G is a
spanning tree of least cost among all spanning trees of G.
2. A flow network is a connected graph having flow capacities
attached to its edges (as edge weights) and having two distinct
vertices designated as the source and the destination (or the sink )
respectively. A maximum flow from the source to the destination
can be found by determining a minimum cut. A cut is a set of edges
which, when removed from the flow network, separates the
network into two distinct connected components, one of which
contains the source and the other of which contains the
destination. A cut is a minimum cut if the sum of the flow
capacities of its edges is less than or equal to that of any other
cut.
3. The four-color problem is the problem of assigning one of four
distinct colors to each vertex in a planar graph so that no two
adjacent vertices have the same assigned color. It is equivalent to
coloring the countries on a map using four colors so that no two
countries sharing a common border have the same color. The fourcolor problem was solved in 1977 by Appel and Haken.
4. NP stands for the class of problems that can be solved in
polynomial time on a nondeterministic computer. To say that a
problem is in NP means that you can give an algorithm for solving
it that runs in polynomial time on a non-deterministic computer. A
problem is NP-complete if it is in NP and if any other problem in
NP can be translated into it in polynomial time. The question P =
NP? asks whether or not the class of problems NP is the same as
the class of problems P, where P is the class of problems that can
be solved in polynomial time on a deterministic machine.
5. O( c n ). The best known time for solving any NP-complete problem
on a deterministic machine is exponential.
6. If someone were to prove that P = NP then it would be possible to
solve problems in NP on deterministic machines in polynomial
time instead of exponential time.

Table of Contents continued


7. A Hamiltonian circuit in a graph G is a cycle that travels through
each vertex of G exactly once.
8. If G is a weighted, directed graph, the traveling salesperson
problem (TSP) is that of determining a cycle of least cost in G that
visits every vertex of G exactly once (where the cost of a cycle is
the sum of its edge weights).

Solutions to Selected Exercises in Exercises 10.8


1. A 4-color solution is shown in the following diagram:

2. The maximum flow on the graph of Figure 10.31 is 23.

Table of Contents continued

Answers to Review Questions 11.2


1. A Table ADT consists of a table , which is an abstract storage
device containing entries of the form (K, I), where K is a key and I
is some information associated with K, together with some
abstract table operations for inserting , deleting , retrieving , and
updating table entries, for enumerating the entries in a table in
increasing order of their keys, for initializing a table to be the
empty table , and for determining if a table is full .
2. The operations we can perform on table T include: initializing T to
be the empty table , determining whether or not T is f u l l ,
e n u m e r a t i n g the table entries in T in increasing order of their
keys, and r e t r i e v i n g , u p d a t i n g , i n s e r t i n g , and d e l e t i n g table
entries in T .
3. Lists of structs having the members Key K and Info I; arrays of
such structs; AVL-trees or B-trees containing such structs at the
tree nodes; a hash table containing such structs; or parallel
arrays with keys K in one array and associated information I in a
parallel array, are all feasible representations of a Table ADT.

Solutions to Selected Exercises in Exercises 11.2


1. Assuming table T is represented by an array of table entry
structs stored in ascending order of their keys: (1) Table T is full
if the array index of the last table entry is one less than the
maximum number of table entries that can be stored in the array;
(2) To insert a new entry (K, I) in Table T, you can use binary
searching to locate where to insert a new struct of the form {Key
K, Info I}, following which you move all table entries with keys
greater than K up one space and then insert the struct for (K, I)
into the hole so created; (3) To delete a table entry (K, I), you can
locate the table entry to delete using binary searching and then
you can move all structs with keys greater than K down one space
in the array, overwriting the space for the struct that used to
contain (K, I); (4) To retrieve or update the record containing key
K, you can use binary searching to locate the record with key K
and then either retrieve the associated information or store the
new update information; and (5) to enumerate the table entries, a
sequential scan in the order of increasing array indices can be
used.
2. In the case of an AVL-tree table representation, a new member is
added to the struct representing each node of the AVL tree to
contain the information associated with the key at the node.
Binary tree searching is used to locate nodes containing a key K

Table of Contents continued


for purposes of retrieval and updating . The AVL-tree insertion
and deletion algorithms given in the solution to Exs. 9.8.3 and
9.8.4 can be used to insert and delete new table entries. The table
can be enumerated in increasing order of the keys by using an
InOrder tree traversal of the AVL tree. A table is full when there
is no more memory to allocate to form new AVL-tree nodes.

Answers to Review Questions 11.3


1. In linear probing , after a first probe is made at location h ( K ) ,
subsequent probes are tried at locations h ( K ) 1, h ( K ) 2, ... , h ( K )
i , ... , 2, 1, 0. After the probes fall off the bottom of the table, the
probe sequence wraps around to the top of the table and,
starting at the topmost table address, moves downward
sequentially in steps of one. In double hashing , after a first probe
is made at location h ( K ), subsequent probes are tried at [ h ( K )
i * p ( K ) ] mod M, where i is the number of the i th probe, M is the
table size, and p ( K ) is a second hash function of the key K such
that p (K ) 1.
2. In collision resolution by separate chaining all keys colliding at a
given hash address h ( K ) are placed on a linked list whose first
node is referenced by a pointer stored in location h ( K ) in the
table. This linked list is searched to find a node containing key K .
3. In hashing with buckets a large hash table is divided into a
number of smaller subtables each of which is called a bucket. The
hash function h ( K ) sends a key K onto a bucket number b . Key K is
then stored sequentially in sorted order of keys inside the
bucket (or subtable) with bucket number h ( K ) = b .

Solutions to Selected Exercises in Exercises 11.3


1. The table resulting from the order of insertion X2 4 , W2 3 , J1 0 , B2 ,
N 14 and S19 using linear probing is as follows:
0
1
2
3
4
5
6

B2
J10
W23
X24
S19
N14

2. The table resulting from the order of insertion X2 4 , W2 3 , J1 0 , B2 ,


N 14 and S19 using double hashing is as follows:

Table of Contents continued


0
1
2
3
4
5
6

B2
J10
W23
X24
N14
S19

3. A complete C program that solves Ex. 11.3.3 is given as follows:

10

15

20

25

30

35

40

45

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

/*
*
*
*
*/

Ex. 11.3.3 -- Hash insertion and searching using chaining

#include <stdio.h>
#include <stdlib.h>
#define MaxIndex 6
#define TableSize 7
typedef char KeyType;
typedef struct NodeTag{
KeyType
Key;
struct NodeTag *Link;
}Node;
typedef Node * HashTable[TableSize];
/* -------------------------------------------------------------- */
int h(KeyType K)
{
return (1+ (int)(K) (int)('A')) % TableSize;
}
/* -------------------------------------------------------------- */
void InitializeTable(HashTable T)
{
int i;
for (i=0; i<TableSize; ++i) T[i] = NULL;
}
/* -------------------------------------------------------------- */
void Insert(KeyType K, HashTable T)
{
Node *L, *N;
L = T[h(K)];
/* obtain pointer to list on which to insert K */
N = (Node *)malloc(sizeof(Node)); /* create a new list node, N */

Table of Contents continued

50

55

60

65

70

75

80

85

90

95

100

105

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

N>Key = K;
N>Link = L;
T[h(K)] = N;

/* let its Key be K */


/* let it link to the remainder of the list */
/* update T[h(K)] to point to new list */

}
/* -------------------------------------------------------------- */
Node *Find(KeyType K, HashTable T)
{
Node *L;
L = T[h(K)];

/* let L point to the list potentially containing K */

while (L != NULL) {
if (L>Key == K) {
return L;
}
L = L>Link;
}
return L;

/* Find returns NULL if K was not on list */

}
/* -------------------------------------------------------------- */
void PrintTable(HashTable T)
{
int i; Node *L;
for (i=0; i<TableSize; ++i) {
L = T[i];
printf("T[%2d] = (",i);
while (L != NULL) {
putchar(L>Key);
if (L>Link != NULL) putchar(',');
L= L>Link;
}
printf(")\n");
}
putchar('\n');
}
/* -------------------------------------------------------------- */
void SearchAndPrint(KeyType K, HashTable T)
{
Node *N;
printf("Searching for Key '%c' in Table T\n",K);
N = Find(K,T);
if (N != NULL) {
printf(" Found %c\n",N>Key);
} else {
printf(" Key '%c' not Found in Table T\n",K);
}
}
/* -------------------------------------------------------------- */
int main(void)

Table of Contents continued


110

115

120

125

130

135

140

145

150

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

{
HashTable T;
InitializeTable(T);
Insert('X',T);
Insert('W',T);
Insert('J',T);
Insert('B',T);
Insert('N',T);
Insert('S',T);
PrintTable(T);

/* prints the Table in Fig. 11.11 on p. 460 */

printf("Table printed above\n\n");


SearchAndPrint('W',T);
SearchAndPrint('N',T);
SearchAndPrint('A',T);
}
/* end main program */
/* the printout resulting from running the program above is:
T[ 0] = (N)
T[ 1] = ( )
T[ 2] = (B,W)
T[ 3] = (J,X)
T[ 4] = ( )
T[ 5] = (S)
T[ 6] = ( )
Table printed above
Searching for Key 'W' in Table T
Found W
Searching for Key 'N' in Table T
Found N
Searching for Key 'A' in Table T
Key 'A' not Found in Table T
*/

4. You could redesign the insertion algorithm to put keys on their


chains in alphabetical order (or in key order, whatever it happens
to be). Then, when searching for a key K 1 , if you scan a chain and
arrive at a node containing a key K 2 which occurs after key K 1 in
alphabetical order (or key order), you know K 1 is not on the chain
hence the search is unsuccessful. The time for unsuccessful
searching is reduced because, on the average, you need search
only half of the keys on a chain to determine that the search key
is not on the chain.

Answers to Review Questions 11.4

Table of Contents continued


1. Two keys K 1 and K2 (where K 1 K 2 ,) are said to collide in a hash
table T if they have identical hash addresses h ( K 1 ) = h ( K 2 ).
2. A collision resolution policy is a method for finding an empty
table entry in which to store a key K 2 if, after trying to store K 2
in table T, we find that the table location given by the hash
address h ( K 2 ) is already occupied by another previously inserted
key K 1 .
3. If a hash table T of 365 entries has 23 or more occupied entries,
then there is a greater than 50% chance that a new key K will
collide at address h ( K ) with a previously inserted key, assuming
the hash function h is uniform and random. It might seem
counterintuitive that this could happen in a table that is only 6.3%
full, but such is the case.
4. The load factor of a hash table T is the ratio of the number of
occupied table entries N to the total number of table entries M.
Thus, = N/M. Generally speaking, the higher the load factor of a
hash table, the worse the performance of a hashing method
becomes.
5. Primary clustering occurs in hashing by open addressing with
linear probing when contiguous clusters of keys form in the hash
table. Each such cluster tends to grow at its low address end at a
rate proportional to its cluster size, and adjacent clusters tend
to join to form even larger clusters which grow even faster(
because they form larger targets for collision with new keys
being inserted).

Solutions to Selected Exercises in Exercises 11.4


1. A program to compute P( n ) is given below. The value of n for
which P(n ) 0.5 and P(n 1) < 0.5 is n = 23, as can be seen from the
printout that the program produces.

10

15

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

/*
*
*/

Ex. 11.4.1 -- Computing von Mises probabilities

#include <stdio.h>
#include <stdlib.h>
double P(int n)
{
int i;
double temp;
temp = 1.0;
for (i=2; i<=n; ++i) {
temp = temp * (365.0 i + 1) / 365.0;
}

Table of Contents continued

20

25

30

35

40

45

50

55

60

65

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

return 1.0 temp;


}
int main(void)
{
int i,TopValue;
printf("Give top value for n = ");
scanf("%d", &TopValue);
printf("\n n:

P(n)\n");

for (i=1; i<=TopValue; ++i) {


if ( ((i % 5) == 0) || (i == 22) || (i == 23)) {
printf("%4d:%11.8f\n", i, P(i));
}
}
}
/* The printout produced by this program for the input 100 is:
n:
5:
10:
15:
20:
22:
23:
25:
30:
35:
40:
45:
50:
55:
60:
65:
70:
75:
80:
85:
90:
95:
100:

P(n)
0.02713557
0.11694818
0.25290132
0.41143838
0.47569531
0.50729723
0.56869970
0.70631624
0.81438324
0.89123181
0.94097590
0.97037358
0.98626229
0.99412266
0.99768311
0.99915958
0.99971988
0.99991433
0.99997600
0.99999385
0.99999856
0.99999969

*/

2. If the Barber of Seville shaves himself, then he is one of those


whom the Barber of Seville does not shave (according to his
shaving policy). Hence he does not shave himself. On the other
hand, if he does not shave himself, then he is one of those whom
he does shave (by his shaving policy). So if he does he doesnt
and if he doesnt he does a contradiction in logic.

Answers to Review Questions 11.5

Table of Contents continued


1. Given a key K you first compute i = h (K), the hash address of K ,
and a probe decrement pd = p ( K ). (In linear probing p ( K ) 1 for all
keys, but in double hashing, for two distinct keys K 1 and K 2 , the
values of p ( K 1 ) and p ( K 2 ) will usually differ, even if K 1 and K 2
collide at the same hash address h ( K 1 ) = h ( K 2 ).) Next you search
for an empty table entry, starting first at location i in the table T,
and then at locations i pd , i 2*pd , and so on. Thus, the sequence
of table locations examined is T[ i ] , T[ i pd ] , T[ i 2*pd ] , etc. If, at
any time, you fall off the bottom of the table, you wrap around to
the top of the table and start searching downwards again. Thus,
the locations examined in the probe sequence are of the form ( i
k * pd ) % M for k = 0, 1, 2, ... , M1, where M is the table size. (A full
table is defined to be a table with exactly one empty entry, so
you are always guaranteed to find an empty entry in every probe
sequence of a table that is not full, provided you know that the
probe sequence enumerates all possible table addresses.) When
you locate the first empty table entry in the probe sequence, you
can insert the key K into that entry.
2. When searching a table T for a key K which is not in T using open
addressing with linear probing, Program 11.17 starts searching at
location h ( K ), and continues to probe locations in the sequence
( h ( K ) i) % M, for i = 0, 1, 2, ... , M 1. This implies that the probe
sequence starts at location h ( K ) and moves linearly downward
toward the bottom of table T. If search falls off the bottom, it
wraps around and starts downward linearly from the top of the
table. The search terminates when the first empty entry in table
T is found (and it is guaranteed that such an empty entry exists,
because even a full table has one empty entry by definition).
Thus, Program 11.17 will terminate by returning 1 on line 25
after having found the E m p t y K e y at some probe location in the
probe sequence while searching for the key K which was not in the
table.
3. A probe sequence for a key K in a hash table T with table
addresses in the range 0:M1, is a permutation (or ordered
rearrangement) of the addresses in 0:M1 starting at the hash
address h ( K ). In general, for open addressing, the probe sequence
is of the form (h ( K ) i * p ( K )) % M for i = 0, 1, 2, ... , M1, where
p ( K ) 1 for linear probing and 1 p ( K ) < M for double hashing. In
double hashing, for two distinct keys K 1 K 2 , we usually have
p ( K 1 ) p ( K 2 ). Thus, in linear probing, two distinct keys K 1 and K 2
colliding at h ( K 1 ) = h ( K 2 ) trace out identical probe sequences,
starting at h ( K 1 ) = h ( K 2 ) and moving downward in steps of 1 in T,
whereas, in double hashing, two distinct colliding keys K 1 and K 2
will trace out different probe sequences whenever p ( K 1 ) p ( K 2 ).

Table of Contents continued


4. The probe sequence is (h ( K ) i * p ( K )) % M for i = 0, 1, 2, ..., M1.
This probe sequence is guaranteed to cover all addresses in the
range 0:M1 in table T whenever p ( K ) and M are relatively prime .
This will happen whenever: (1) M is a prime number and p ( K ) is an
integer in the range 1 p ( K ) < M, or (2) M is a power of 2 and p ( K )
is an odd integer in the range 1 p (K ) < M.
5. With = 0.9, successful search theoretically takes the following
number of probes:
C N 1 + (1 / 2 )

(a) for separate chaining

1.45
1

(b) for open addressing with linear probingCN 2 1 +

5.50

(c) for open addressing with double hashingC N l n


=

1
Hence, theoretically, separate chaining takes the least number of
probes.

2.56

Solutions to Selected Exercises in Exercises 11.5


1. The solution is not given.
2. The solution is not given.
3. Let us first prove that the triangle numbers m o d M, starting at 0,
cover the addresses 0:M1 in a table T of size M, whenever M is of
the form M = 2n .

Proof :
Suppose there exist i, j 1:2n for which i j and
i( i 1 )
j( j 1 )
n
mod
2
=
mod 2n.
2
2
Then,
But
=
=
=

i( i 1 )
2

i( i 1 )
i2

j( j 1 )

must be evenly divisible by 2n .

j( j 1 )

j2

+ j

( i2 j2 ) ( i j)

( i + j) ( i j) ( i j)
2

( i + j 1 ) ( i j)

Thus,

( i + j 1 ) ( i j)
2

must be evenly divisible by 2 n .

Therefore, ( i + j 1 ) ( i j )
2 n + 1. Now, we consider two cases:

must be evenly divisible by

Table of Contents continued

Case 1: i and j are both even or both odd. Then (i + j 1) is odd,


so 2n + 1 must divide evenly into (i j ). But with i , j 1:2n ,
all of the differences of the form i j are less than 2 n ,
and hence none are divisible by 2n + 1 .
Case 2: i and j are of opposite parity. I.e., i is even and j is odd,
or i is odd and j is even. Then (i j) is odd, so 2n + 1 must
divide evenly into ( i + j 1). But the largest that i + j 1
can be with i, j 1:2n and i j is 2n + 1 2, which is again
not divisible by 2n + 1 .
i( i 1 )
Consequently, the numbers
m o d 2n are distinct for all
2
i 1:2n.
N o t e : It is important to choose i 1:2 n , whence the triangle
numbers start at 0. If you start the triangle numbers at 1, with i =
i( i 1 )
2 (2 1 )
=
= 1, then you get a duplicate for i = 2n
2 in
2
2
and j = 2n + 1, where i j and i, j 2:2n +1.
In the proof above when i and j are of opposite parity (Case 2),
2 n + 1 must divide (i + j 1) evenly. But if i = 2n and j = 2n + 1, (i + j
1) = (2n +2n + 1 1) = 2n + 1 , which is evenly divisible by 2n + 1 .

For example : Let n = 3, so 2n = 8, and look at the results in the


following table:

i
1
2
3
4
5
6
7
8
9
10

i( i 1 ) i( i 1 )
2

0
1
3
6
10
15
21
28
36
45

0
1
3
6
2
7
5
4
4
5

mod 8

Note the duplicates in the third


column for i = 8 and i = 9.

As i increases, the remainders m o d 8 for i 9:16 are the reverse


of the sequence of remainders for i in 1:8.
The following is a complete C program that illustrates triangle
number hashing.

Table of Contents continued

10

15

20

25

30

35

40

45

50

55

60

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

/*
*
*/

Ex. 11.5.3 -- Triangle Number Hashing, pp. 484-486

#include <stdio.h>
#define TABLESIZE 8
#define EmptyKey '\0'
typedef char KeyType;
typedef int InfoType;
typedef struct {
KeyType Key;
InfoType Info;
} TableEntry;
typedef TableEntry Table[TABLESIZE];
Table T;

/* declare T to be a Table of TableEntries */


/* indexed by 0:TABLESIZE1 */

/* ------------------------------------------------------ */
int h(KeyType L)
{
return (1 + (int)(L) (int)('A')) % TABLESIZE;
}
/* ------------------------------------------------------ */
void InitializeTable(Table T)
{
int i;
for (i=0; i<TABLESIZE; ++i) {
T[i].Key = EmptyKey;
T[i].Info = 0;
}
}
/* ------------------------------------------------------ */
void PrintTable(Table T)
{
int i;
for (i=0; i<TABLESIZE; ++i) {
if (T[i].Key != EmptyKey) {
printf("%2d: %c%d\n",i,T[i].Key,1 + (int)(T[i].Key) (int)('A'));
} else {
printf("%2d: %c\n",i,T[i].Key);
}
}
putchar('\n');
}
/* ------------------------------------------------------ */
void HashInsert (KeyType K, InfoType I)

Table of Contents continued

65

70

75

80

85

90

95

100

105

110

115

120

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

{
int i, j;
KeyType ProbeKey;
/* Initializations */
i = h(K);
/* first hash */
j = 0;
/* initialize counter for triangle number hashing */
ProbeKey = T[i].Key;
/* find first empty slot */
while (ProbeKey != EmptyKey) {
++j;
/* increment triangle number hashing counter */
i = j;
/* compute next probe location */
if (i < 0) {
i += TABLESIZE;
/* wrap around if needed */
}
ProbeKey = T[i].Key;
}
/* Insert new key and info into table */
T[i].Key = K;
T[i].Info = I;
}
/* ------------------------------------------------------ */
int HashSearch (KeyType K)
{
int i,j;
KeyType ProbeKey;
/*Initializations */
i = h(K);
/* first hash */
j = 0;
/* intialize counter for triangle number hashing */
ProbeKey = T[i].Key;
/* Find either an entry with key K, or the first empty entry */
while (ProbeKey != K && ProbeKey != EmptyKey) {
++j;
/* increment triangle number hashing counter */
i = j;
/* decrement probe location by amount j */
if (i < 0) {
/* wrap around if needed */
i += TABLESIZE;
}
ProbeKey = T[i].Key;
}
/* return the position of key K in table, or return -1 if K not in T */
if (ProbeKey == EmptyKey) {
return 1;
/* return 1 to signify K was not found */
} else {
return i;
/* return location, i, of key K in table T */
}
}
/* ------------------------------------------------------ */
void SearchAndReportLocation(KeyType K)
{
int result;
result = HashSearch(K);
printf("Searched for Key %c, ",K);

Table of Contents continued


125

130

135

140

145

150

155

160

165

170

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

printf("Found it at Table Location == %d\n",HashSearch(K));


}
/* ------------------------------------------------------ */
int main(void)
{
InitializeTable(T);
HashInsert('B',0);
HashInsert('J',0);
HashInsert('S',0);
HashInsert('N',0);
HashInsert('X',0);
HashInsert('W',0);
HashInsert('A',0);
PrintTable(T);
SearchAndReportLocation('N');
SearchAndReportLocation('B');
SearchAndReportLocation('J');
SearchAndReportLocation('X');
SearchAndReportLocation('S');
SearchAndReportLocation('W');
SearchAndReportLocation('A');
SearchAndReportLocation('C');
}
/* end of main */
/* The printout obtained from running the program above is:
0:
1:
2:
3:
4:
5:
6:
7:

X-24
J-10
B-2
S-19
A-1
N-14
W-23

Searched
Searched
Searched
Searched
Searched
Searched
Searched
Searched

for
for
for
for
for
for
for
for

Key
Key
Key
Key
Key
Key
Key
Key

N,
B,
J,
X,
S,
W,
A,
C,

Found
Found
Found
Found
Found
Found
Found
Found

it
it
it
it
it
it
it
it

at
at
at
at
at
at
at
at

Table
Table
Table
Table
Table
Table
Table
Table

Location
Location
Location
Location
Location
Location
Location
Location

== 6
== 2
== 1
== 0
== 3
== 7
== 4
== 1

*/

Answers to Review Questions 11.6


1. A hash function h ( K ) performs well if the hash addresses it
computes for keys K are distributed evenly in a hash tables
address space. A good hash function disperses clusters of keys in
the key space by mapping them into dispersed hash table
addresses. A hash function performs poorly if it does not

Table of Contents continued


distribute hash addresses evenly in a hash table or if it fails to
disperse clusters of keys.
2. In the division method of hashing, an integer representation of a
key K is divided by the hash table size M, yielding a quotient Q
and a remainder R. The remainder R is used as the value of the
hash function h ( K ), and the quotient Q, reduced modulo the table
size M, and replaced by 1 if (Q m o d M) was zero, is used as the
probe decrement p ( K ). In symbols, p ( K ) = max (1, Q mod M). In this
case, the probe decrement p ( K ) is a second hash function, which
usually yields different values of p ( K ) for different keys. The
first hash function h ( K ) = (K % M), determines the first probe
address. The term double hashing refers to the fact that two
separate hash functions are used to determine the remainder of
the probe sequence after the first probe.
3. One example of a pitfall when choosing a hash function is to use
the division method and to use a table size which is a power of 2,
such as M = 2 1 2 = 4096. In this case h ( K ) selects the least
significant 12 bits of the binary representation of keys K, which
preserves clusters of keys and tends to mirror the uneven
distribution patterns of keys in the set of keys to be hashed.

Solutions to Selected Exercises in Exercises 11.6


1. Witt chose a poor hash function which failed to avoid the pitfall
explained in the answer to Review Question 11.6.3 above. For
example, Witts hash function will take clusters of keys, such as
X1, X2, and X3 or P1, P2, and P3 in the key space, and will map
them onto contiguous clusters of hash addresses in the hash table
address space.
2. A short program illustrating multiplicative hashing is:

10

15

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

/*
*
*/

Ex. 11.6.2 -- Multiplicative hashing

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
long A, Result, W, i;
A = 20071;
W = 32768;
for (i=0; i<16; ++i) {
Result = ((A*(i+1)) % W) / 2048;
printf("%3ld",i);printf(": %3ld\n",Result);
}

Table of Contents continued


20

25

30

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

}
/* end main */
/* The printout for this program is:
0: 9
1: 3
2: 13
3: 7
4: 1
5: 10
6: 4
7: 14

8: 8
9: 2
10: 11
11: 5
12: 15
13: 9
14: 3
15: 12

*/

3. The solution is not given.

Answers to Review Questions 11.7


1. A Table ADT could be represented by: (a) a sorted array of
structs, (b) an AVL-tree of node structs, or (c) a hash table.
2. In the case of a hash table representation of a Table ADT,
insertion, retrieval, and update follow the normal procedures for
hash table insertion and search, with the understanding that after
successful search, the update operation modifies the information
members of a retrieved struct for a table entry. Enumeration
requires sorting the hash table entries before it is possible to
enumerate them in the order of their keys. In some hash table
representations, such as those for open addressing, deletion can
be accomplished by marking table entry structs as deleted even
though they are still physically present in the hash table. Such a
policy is necessitated by the fact that actual physical removal of
deleted table entry structs leaves empty entries which may
interrupt and abort successful search for other keys present in
the table (because, during search or insertion, when an empty
entry is encountered during the probe sequence, it signals either
unsuccessful search or the successful location of an address in
which to insert a new table entry).

Solutions to Selected Exercises in Exercises 11.7


1. A hash table representation of a Table ADT will be superior under
circumstances when deletions and table enumerations are rare,
and when the table is not only not fully saturated, but also rarely
overflows, necessitating rehashing into a table of larger size.
2. An AVL-tree representation of a Table ADT could be superior
under circumstances in which deletions, insertions, and
enumerations are frequent and numerous.

Table of Contents continued


3. The solution is not given.

Table of Contents continued

Answers to Review Questions 12.2


1. The latency time is the rotational delay until the proper sector
moves beneath the read/write head.
2. The seek time is the time to move the read/write heads to the
proper track (or cylinder).
3. The transmission time is the rotational delay while the portion of
the track containing the record to be read or written passes under
the read/write head.

Solutions to Selected Exercises in Exercises 12.2


1. The average latency time is the time for one half a rotation. If
there are 60 seconds in a minute and the disk rotates 3600 times
in one minute, then the seconds per rotation are 60/3600, and the
seconds for one half a rotation are:
60
2*3600

1
2*60

1
120

0.008333,

or 8 1 / 3

milliseconds per rotation.


2. Prove that the average of |i j| for i, j chosen randomly 1:n is
2
A=

n +1

3
n2

where

n +1

(n +1)( n )( n 1)
.
6

| i j |

j= 1
.
n2
Consider an n n matrix T with rows i and columns j such that T[ i, j]
= |i j|. The matrix is symmetric and has a zero diagonal, since T[ i,
j ] = |i j | = |j i |= T[ j , i ] and T[ i , i ] = |i i | = 0. So if S is the sum of
the elements above the diagonal, the average is given by A =
2*S/ n 2 . The n elements of T on the diagonal are zero. The (n 1 )
elements above the diagonal are 1. The ( n 2) elements two above
the diagonal are 2. And so on, until the one element in the upper
right corner is (n 1). Hence, the sum S of the elements of T above
n
the diagonal is, S = i * ( n i ) .
i= 0

We need to compute A =

i= 1

For example, for n = 5, the portion of the matrix T on and above


the diagonal is:
1 2 3 4 5

Table of Contents continued


1 0 1 2 3 4
2

0 1 2 3

0 1 2
Here, we have S =
4*1.

0 1

i* ( n i) =

Hence, S =

i= 0

i= 0

i= 0

0*5 + 1*4 + 2*3 + 3*2 +

i* n i 2 .

Using Formula A.29 from the Math Reference appendix for the
sum of the squares ( 1 i n ) i2 = n ( n +1)(2 n +1)/6, we get,
n

i = 0

S= n

n (n +1)(2n +1)
6

n ( n +1)

= n

n (n +1)(2n +1)
n (n +1)
=
[3n 2n 1]
6
6

n +1
(n +1)( n )(n 1)
= 3 .

But A = 2 S/n 2 , so
n +1

3
n2

2
A =

, which is what we needed.

3. We can use the result of the previous exercise to prove that the
average of |i j| for i, j chosen randomly from 1:n is n / 3 .
n +1

3
n2

2
A =

2( n +1)( n )(n 1)
.
6n2

Simplifying A, we get

n3
n
n
1

.
2
2
3n
3n
3
3n
Hence, the difference between n / 3 and A is n / 3 A = 1 / 3 n . The
quantity 1 / 3 n . becomes small as n becomes large. For example, for
n = 100,
A

( n 2 1) n
3n2

1 0 1

3
1002

33.333333... .

2*166650
10000

= 33.33,

and

3 =

100
3

Table of Contents continued


The difference between A and n / 3 , is 1 / 300 = 0.0033333... , which is
about 1 / 1 0 0 t h of a percent of As value. In fact, for n 11, n / 3
differs from A by less than one percent of As value.

Answers to Review Questions 12.3


1. Four such techniques are: binary searching in a sorted contiguous
array, searching in an AVL tree representation of a Table ADT,
sorting an unsorted contiguous array by comparison/exchange
sorting, and searching for a record containing key K in a hash table
using open addressing and double hashing.
2. Even though AVL trees are well-balanced, on the average, the
branching factors at their internal nodes are at most two, which
implies that AVL trees containing large numbers of nodes will
require many nodes to be searched in an average case, even
though the number of nodes searched is O(log n ). For example, in
an AVL tree with 106 nodes, it takes about 21.4 nodes accesses in
the average case and 29 node accesses in the worst case to find a
node containing a search key K . At one disk access per node, the
AVL-tree representation is too inefficient for practical use.

Solutions to Selected Exercises in Exercises 12.3


1. On the average, in an AVL tree of n nodes, it takes lg n + 1.5 disk
accesses at one disk access per node to find a node containing a
search key K . Hence, for n = 16.777 million nodes, it takes
approximately 25.5 disk accesses for an average successful
search.
2. Regardless of the table size M, when the load factor of the table is
, it takes CN = ( 1 / ) ln( 1 / 1 ) hash probes to find a key K in a
hash table using open addressing and double hashing. Thus for =
0.95, CN 3.15 disk accesses at one disk access per hash probe.

Answers to Review Questions 12.4


1. The B-tree of order 200 works well and the 2-3 tree works poorly
because, for a given number of records n , it takes a tree of many
fewer levels to store n records when the node branching factor is
high than when the node branching factor is low, and the fewer the
number of tree levels, the fewer the number of disk accesses
required during searching. In particular, in a B-tree of order m that
indexes n keys, the worst case number of nodes l that we have to
search is log m / 2 (( n +1)/2). As a result, if a B-tree of order
approximately m = 200, is used to represent a Table ADT
containing no more than n = 1,999,998 keys, then the number of

Table of Contents continued


disk accesses is 3 or less. This is substantially better than AVLtrees which need 22.4 accesses on the average to access a key in
an AVL-tree containing 1,999,998 keys.
2. Merge sorting works better than selection sorting because
selection sorting requires a large number of accesses to records
on disk to implement comparisons and exchanges, whereas merge
sorting reads and writes contiguous blocks of records efficiently
from/to disk and rearranges such blocks into sorted order
efficiently in internal (primary) memory.
3. Hashing using buckets of size 50 works better than double hashing
with open addressing, because the latter technique traces out
probe sequences that hop all over the disk at one disk access per
probe, whereas the former almost always leads to a bucket
containing the search key in one disk access, following which the
entire bucket is read into primary memory and is subsequently
searched efficiently using binary searching.

Solutions to Selected Exercises in Exercises 12.4


1. For n = 4 million, CN lg n + 1.5 node accesses on the average in an
AVL-tree, so CN = 23.43 disk accesses at one disk access per node.
2. If we use 7 of the 10 tracks per cylinder as prime tracks and 120
of the 128 cylinders as prime cylinders, we can accommodate:
1 2 0

7 t r a c k s

c y l i n d e r s
* 6 4 s e c t o r s * 2 0 4 8 b y t e s =
*

disk
track
s e c t o r

cylinder

110,100,480 bytes

in the prime storage area for records on a single disk, and, at 16


bytes/record, we can store 6,881,280 records. Hence, our 4 million
record ISAM file will fit onto one disk of the kind whose
characteristics are given in Table 12.5 (on page 503). In such an
ISAM file, assuming a master index is stored in primary memory,
we can access a record using at most 3 disk accesses if no
overflows occur.

Answers to Review Questions 12.5


1. A fully inverted file F is one for which each value v of each
attribute a in each record R of F is in an index such that v can be
looked up in the index and will lead to a list of all records having
the value v for some attribute.
2. A query language is a language in which information retrieval
queries can be expressed, such as queries: to find a record
containing a key K ; to find all records whose A-attribute either has
the value v or lies in some range R 1 v R 2 ; to find the sum,

Table of Contents continued


maximum, minimum, or average of the A-attribute of all records
satisfying some search criterion; to find Boolean combinations of
simpler queries having truth values (true , false ) as results; and so
forth.
3. A database is
organization or
together with
procedures that

a collection of files concerned with a particular


enterprise. A database system is a database
the set of machines, people, policies, and
govern its use in the context of the enterprise.

Solutions to Selected Exercises in Exercises 12.5


1. (and 2.) The answers are dependent on the details of the readers
institution and are not given.

Table of Contents continued

Answers to Review Questions 13.2


1. A comparison-based sorting method is one that compares values
of pairs of keys and rearranges their order depending on the
outcomes of the comparisons.
2. A comparison tree is a binary tree in which each internal node
represents a comparison between a specific pair of keys and in
which the internal nodes two descendants represent the yes and
no outcomes of the comparison. A leaf in a comparison tree
represents a particular sorted ordering of the keys implied by the
outcomes of the comparisons along the path from the root node to
the leaf.
3. n log n.
4. The full binary trees having minimum external path length are
those with leaves on at most two adjacent levels..
5. The average number of comparisons needed to sort n keys using
the comparisons in a comparison tree T is the external path length
of T divided by the number of external nodes in T (where the
number of external nodes is the number of different sorted
arrangements of the n keys, and the external path length is the
sum of the path lengths from the root to each external node).
Hence, this average is at a minimum when the external path length
of T is at a minimum.

Solutions to Selected Exercises in Exercises 13.2


1. In a full binary tree with k leaves on at most two adjacent levels,
let r be the smallest level number of a level containing at least
one leaf. If all k leaves are on level r , there are exactly k = 2r
leaves and r = lg k = lg k , so a path from the root to each leaf
contains exactly r edges. On the other hand, suppose at least one
leaf is on level r and some other leaves are on level r + 1. In this
case, level r + 1 can contain up to 2r + 1 2 leaves, while at least one
leaf remains on level r . The maximum number of leaves that can
occur on two adjacent levels with at least one leaf on level r is
therefore k = 2r + 1 1. Here, we again have r = l g k , because
k = 2 r + 1 1 (implies) 2 r k < 2r + 1 r lg k < r + 1 lg k = r .
Again, a path from the root to any leaf in such a tree must contain
at least r edges (because all such paths from the root to various
leaves contain either r or r + 1 edges).
2. Applying formula A.22 of the Math Reference appendix to change
logarithm bases, we have lg (n !) = ln(n !)/ ln 2. Hence, lg(n !) ln(n !)/

Table of Contents continued


ln 2 + O(n ), which still holds if we replace the exact equivalence
with approximations such as Stirlings approximation.
3. O(n log n).

Answers to Review Questions 13.3


1. In abstract priority queue sorting, the keys to be sorted are
inserted into a priority queue PQ in any order. Then they are
removed, one-by-one, in order of highest to lowest priority and
are inserted in arrival order on the rear end of an initially empty
queue Q. The order of arrival in Q is thus the sorted order of
highest-to-lowest priority of the keys.
2. In S e l e c t i o n S o r t using an array A, the array is partitioned into an
unsorted subarray A [ 0: i ] , representing a priority queue, and a
sorted subarray A [ i +1: n 1 ] representing an output queue. To find
the highest priority (largest) key in the priority queue, you scan
A [ 0: i ] to find the position j of the largest key and remove A[ j ] as
the highest priority key. Hence, the unsorted array
representation of a priority queue is used in SelectionSort .
3. In H e a p S o r t , the priority queue is represented by A[ 1: i ] which is
initially rearranged into a heap whose highest priority element
resides in A[ 1 ] . The subarray A[ i+1: n ] represents an output queue.
(An array of size n +1 is used with A [ 0 ] unoccupied and A [ 1: n ]
occupied by keys to sort.)

Solutions to Selected Exercises in Exercises 13.3


1. Demonstrate that S = ( 2 i n ) ( lg i +2) is O(n log n ). Using formula

A.31 (which is

1 i n

lg i

= (n + 1 ) q 2( q + 1 ) + 2, where q =

lg( n +1) ), we have

S =

2 i n

1 i n

lg i + 2 ) =

2 i n

lg i +

2 i n

lg i + 2(n 1) = (n +1) q 2( q + 1) + 2n , where q =

lg( n +1)

(n + 1) lg( n +1) 2(q + 1) + 2n

<

(n + 1) lg(n +1) (n +1) + 2n

n lg(n +1) + lg(n +1) + n 1.


To deduce the second line above from the line above it, we used
two facts: (a) that lg( n +1) lg(n +1), and (b) that 2( q + 1) <
=

Table of Contents continued


( n +1). The second of these facts follows from the following
argument. For any real number x , we have x < x + 1, from which 2x
< 2( x + 1 ) and 2( x + 1 ) < 2x . Now letting x = lg(n +1) the latter
becomes 2 ( lg(n +1) +1) < 2lg(n +1) , which implies 2(q + 1) < (n +1).
Now we need to use the fact that lg(n + 1) < lg n + 1 for n > 1, which
is proved on p. 709 of the Math Reference appendix. Applying
this to the expression on the last line above, we get

n lg(n +1) + lg(n +1) + n 1


< n (lg n + 1) + lg(n +1) + n 1
= n lg n + lg(n +1) + 2 n 1
which is O(n log n ).
2. SiftUp(A,i,n) is applied to the nodes in the range 2 to n /2 in reverse
order because: (1) n /2 computes n /2 , since in C, if n is an integer,
n /2 signifies the quotient of n divided by 2 with the remainder
thrown away. Now, by Table 9.7 (on p. 345), node i is a leaf node iff
2* i > n , which is the same as saying that the nodes i in the range
n /2 < i n are leaves, from which it follows that the nodes i in the
range 1 i n /2 are internal nodes. S i f t U p is applied only to
internal nodes since it is useless to apply it to leaves (which are
single nodes, and hence are heaps already); (2) SiftUp is not applied
to the root node i = 1 when heapifying, because the tree rooted at
the root node gets heapified at the beginning of the second phase
of He a p S o rt ; (3) The internal nodes, except for the root node, are
heapified in the reverse of level order to guarantee that the left
and right subtrees of each internal node have already been
heapified. Hence, the order of enumeration is from n /2 downwards
to 2 in steps of 1.

Answers to Review Questions 13.4


1. In divide-and-conquer methods for sorting, an initially unsorted
array of keys is d i v i d e d into two or more subarrays that are
conquered (sorted separately), after which the sorted subarrays
are combined in some fashion to yield the entire sorted array that
solves the original sorting problem.
2. In the M e r g e S o r t method, an unsorted array is divided into two
unsorted arrays of (roughly) equal size which are merge sorted to
yield two sorted subarrays. These sorted subarrays are then
merged by imagining that they are queues, repeatedly removing
the larger key from the front of either of the two queues, and
inserting it on the rear of an initially empty output queue.
3. QuickSort is an instance of a divide-and-conquer method because it
divides an initially unsorted array A into two adjacent unsorted

Table of Contents continued


arrays L and R that contain keys that are respectively less than or
equal to some initially chosen pivot key p and greater than or
equal to the chosen pivot key p . These unsorted subarrays L and R
are then recursively sorted in place using Q u i c k S o r t , after which
there is no need to combine them explicitly because they are
already in sorted order as subarrays of A.

Solutions to Selected Exercises in Exercises 13.4


1. When the pivot key is always the smallest (or largest) key in the
subarray being partitioned, the partition sizes are as lopsided as
possible, causing QuickSort to run in time O(n 2 ). For example, if the
pivot key is chosen as the middle key in A[ i : j ] with pivot = A[ ( i + j )/2 ] ,
an initial unsorted array such as A = [ 2, 7, 4, 1, 3, 5, 6] will cause
the QuickSort algorithm of Program 13.14 to run in worst case time.
The sequence of right partitions, in this case, is:

array index
i=
0 1 2 3
initial array A = [ 2, 7, 4, 1,
== 1
R = A[1:6 ] in
A = [ 1, 7, 4, 2, 3, 5,
R = A[2:6 ] in
A = [ 1, 2, 4, 7, 3, 5,
R = A[3:6 ] in
A = [ 1, 2, 3, 7, 4, 5,
R = A[4:6 ] in
A = [ 1, 2, 3, 4, 7, 5,
R = A[5:6 ] in
A = [ 1, 2, 3, 4, 5, 7,
R = A[6:6 ] in
A = [ 1, 2, 3, 4, 5, 6,
A complete C program which uses

10

|
|
|
|
|
|
|
|
|
|
|
|

4 5 6
3, 5, 6 ] , swap A[ 0 ] == 2 & A[ 3 ]

6 ] , swap A[ 1 ] == 7 & A[ 3 ] == 2
6 ] , swap A[ 2 ] == 4 & A[ 4 ] == 3
6 ] , swap A[ 3 ] == 7 & A[ 4 ] == 4
6 ] , swap A[ 4 ] == 7 & A[ 5 ] == 5
6 ] , swap A[ 5 ] == 7 & A[ 6 ] == 6
7 ].
this example is as follows:

/*
* Ex. 13.4.1 -- Worst Case QuickSort
*/
#include <stdio.h>
#include <stdlib.h>
#define MaxIndex 7
typedef int KeyType;

Table of Contents continued

15

20

25

30

35

40

45

50

55

60

65

70

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

typedef KeyType SortingArray[MaxIndex];


/* ----------------------------------------------------- */
void PrintArray(SortingArray A)
{
int i;
printf("i = ");
for (i=0; i<MaxIndex; ++i) printf("%2d ",i);
putchar('\n');
printf("A = ");
for (i=0; i<MaxIndex; ++i) printf("%2d ",A[i]);
putchar('\n');
}
/* ----------------------------------------------------- */
void Partition (SortingArray A, int *i, int *j)
{
KeyType Pivot, Temp;
Pivot = A[(*i + *j) / 2];
do {
while (A[*i] < Pivot) (*i)++;
while (A[*j] > Pivot) (*j) ;
if (*i <= *j) {
Temp = A[*i]; A[*i] = A[*j]; A[*j] = Temp;
(*i)++;
(*j) ;
}
} while (*i <= *j);
}
/* ----------------------------------------------------- */
void QuickSort (SortingArray A, int m, int n)
{
/* to sort the subarray A[m:n] into ascending order */
int i,j;
if (m < n) {
i = m;
j = n;
Partition(A, &i, &j);
printf("After partition, j = %2d, i = %2d, and Array A = \n",j,i);
PrintArray(A);
QuickSort(A, m, j);
QuickSort(A, i, n);
}
}
/* ----------------------------------------------------- */
int main(void)
{
SortingArray A = {2,7,4,1,3,5,6};
PrintArray(A);
QuickSort(A, 0, MaxIndex1);
PrintArray(A);

/* Initialize A to [2,7,4,1,3,5,6] */

Table of Contents continued


75

80

85

90

95

100

105

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

printf("-- end of program\n");


}
/* end main program */
/*
When executed, the program above prints:
i = 0
1
2
A = 2
7
4
After partition, j
i = 0
1
2
A = 1
7
4
After partition, j
i = 0
1
2
A = 1
2
4
After partition, j
i = 0
1
2
A = 1
2
3
After partition, j
i = 0
1
2
A = 1
2
3
After partition, j
i = 0
1
2
A = 1
2
3
After partition, j
i = 0
1
2
A = 1
2
3
i = 0
1
2
A = 1
2
3
-- end of program

3
1
=

0,
3
2

1,
3
7

2,
3
7

3,
3
4

4,
3
4

5,
3
4
3
4

4
3
i
4
3
i
4
3
i
4
4
i
4
7
i
4
5
i
4
5
4
5

5
5
=

1,
5
5

2,
5
5

3,
5
5

4,
5
5

5,
5
7

6,
5
6
5
6

6
6
and
6
6
and
6
6
and
6
6
and
6
6
and
6
6
and
6
7
6
7

Array A =

Array A =

Array A =

Array A =

Array A =

Array A =

*/

2. In some versions of QuickSort , the pivot is chosen to be A[ 0 ] , the


first item in the array. These versions of QuickSort do not work well
when the array A is already in sorted order, or the reverse of
sorted order, or nearly so. In such cases, it is advantageous to use
the median of three method. For the version of QuickSort given in
Program 13.14, arrays of the sort illustrated in the solution to Ex.
13.4.1 above cause the partitions to be as lopsided as possible.
Using the median of three method helps eliminate the lopsided
partitions in these cases also.

Answers to Review Questions 13.5


1. We split the array A into two subarrays S and U, where S = A[ 0: i1 ]
is a sorted subarray and U = A[ i: n 1 ] is an unsorted array. For each
i in the range 1:n 1 we choose K = A[ i ] , the leftmost key in U, as a
key to insert, and we insert it into S in sorted order, which
enlarges S by one more item and shrinks U by one item. The way
we insert K into S is to move every key in S that is bigger than K
one space to the right, which leaves a hole in which to insert K ,
after which we insert K into this hole.

Table of Contents continued


2. In T r e e S o r t we insert the keys to sort one-by-one into an initially
empty binary search tree (or AVL-tree) T after which we read
them off in sorted order using an InOrder traversal of T.
3. InsertionSort runs in time O(n 2 ) and TreeSort runs in time O(n log n ).

Solutions to Selected Exercises in Exercises 13.5


1. Suppose A is the array A = [2, 3, 4, 5, 1] and we make the initial function
call InsertionSort(A,0,4) in which m == 0 and n == 4. This causes a recursive
call on InsertionSort(A,0,3) after which C is set to A[4] , which is 1 , and i is
set to 4 , in a situation in which m == 0 and n == 4. Subsequently, the
items in A[0:3] are shifted right into A[1:4] , after which m == 0, i == 0, and
C == 1 . At this moment, the while-condition on line 14 of the
function is again evaluated. This time A[i1] tries to access A [ 1 ]
which is outside the range of the array A[0:4] , and if array range
checking has been turned on, an array bounds error will be
detected and reported. To fix the bug, simply replace line 14 by
while ( (i > m) && (A[i1] > C) ) {. In this case, the test (i > m) will fail when i
== 0 and m == 0, and the short-circuit and operator && will
prevent (A[i1] > C) from being evaluated. Then the while-loop on
lines 14:17 will terminate properly, and C will be placed in the hole
A [ i] on line 20 in a proper fashion.
2. In InsertionSort , for each i in 1:n 1 half the keys in A[ 0: i1 ] are moved
(i.e., i / 2 of the i keys in A[ 0: i 1 ] are moved), on the average, after
which A[ i] is moved into the hole opened up for insertion. Summing
up the total number of these moves gives the sum S, where

n1

S=

i= 1

i
2

+ 1 =

1
2

n1


i = 1

i + (n 1)

1
2

1
4

[ n ( n 1 ) + 4 ( n 1 )]

S =

n ( n 1 )
+ (n 1)
2

(n + 4)(n 1 )
4

moves of keys.

In SelectionSort , one pair of keys is exchanged for each i in the range


1: n 1, meaning that exactly (n 1) pairwise exchanges of keys take
place. If each pairwise exchange takes 3 moves, the total number
of key moves is 3n 3 in SelectionSort . Because 3n 3 is less than
( n + 4 ) ( n 1)/4 for all n 9, S e l e c t i o n S o r t moves fewer keys than
InsertionSort
on the average whenever the array to be sorted
contains nine or fewer keys.

Table of Contents continued


[ On the other hand, I n s e r t i o n S o r t performs half as many key
comparisons on the average as S e l e c t i o n S o r t , with I n s e r t i o n S o r t
performing n ( n 1)/4 comparisons, and SelectionSort performing n ( n
1)/2 comparisons. ]

Answers to Review Questions 13.6


1. ProxmapSort scans through an array to be sorted and precomputes
the locations that small subarrays of keys will occupy in final
sorted order before moving any keys. It also computes a mapping,
called a proximity map , or proxmap for short, that sends each key
onto the beginning of its reserved subarray in final sorted order.
In a final pass, keys are moved to their final reserved subarrays
where they are insertion-sorted into position.
2. RadixSort makes k linear passes through keys containing k digits.
The first pass sorts the keys on the least significant digit, the
second pass sorts the output of the first pass on the second least
significant digit, and the i t h pass sorts the output of the (i 1 ) s t
pass on the i th least significant digit. The process ends when the
keys have been sorted on the most significant digit.
3. RadixSort runs in time O(n ) because, for any n keys of k -digits each,
it makes k linear passes, thus using time proportional to k * n ,
which is O(n ).

Solutions to Selected Exercises in Exercises 13.6


1. See the function RadixSort in the program that follows:

10

15

20

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

/*
* Ex. 13.6.1 -- Radix Sorting Example Using 3-Letter Airport Codes
*/
/* This program uses a parallel array representation of linked-list nodes. */
/* The SortingArray, A, contains the keys, which are three-letter Airport */
/* Codes. In parallel to the SortingArray is a Link array holding links to */
/* successor nodes on the linked list of nodes holding AirportCodes and */
/* links. There is also an array of 26 "Bins" -- one for each letter of the */
/* alphabet. Each bin has a Front pointer and a Rear pointer which point to */
/* the respective front and rear of a linked list of nodes that fall into that bin */
/* during Radix Sorting. */
#include <stdio.h>
#include <stdlib.h>
#define MaxIndex 32
#define MaxLetter 'Z'+1
#define null
0
typedef int LinkAddress;

Table of Contents continued

25

30

35

40

45

50

55

60

65

70

75

80

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

typedef char AirportCode[4];


typedef AirportCode SortingArray[MaxIndex];
typedef LinkAddress LinkArray[MaxLetter];
LinkAddress Link[MaxIndex];
LinkArray Front,Rear;
SortingArray A = { "AAA", "DUS", "MEX", "ORD", "RAL",
"ABQ", "ONT", "NRT", "YYZ", "GLA",
"ZRH", "BUF", "DFW", "DEN", "ARN",
"JAX", "BRU", "CRQ", "MIA", "SEA",
"STL", "HOU", "SAN", "GCM", "HKG",
"PIT", "MKC", "SFO", "JFK", "ORY",
"LGA", "ORL"
};
LinkAddress ListHead;
/* ------------------------------------------------------------------------------- */
void Insert(LinkAddress L, int k)
{
LinkAddress N;
int C;
/* inserts (Airport,Link) pair on rear of list in Bin(A[L][k]) */
C = A[L][k]; /* extract kth-radix digit used to index bins */
if (Front[C] == null) {
Front[C] = L;
Rear[C] = L;
} else {
Link[Rear[C]] = L;
Rear[C] = L;
}
}
/* ------------------------------------------------------------------------------- */
LinkAddress SweepBinsAndLink(void)
{
LinkAddress CurrentListHead;
int C;
CurrentListHead = null;
for (C = 'Z'; C>='A'; C) {
if (Front[C] != null) {
Link[Rear[C]] = CurrentListHead;
CurrentListHead = Front[C];
}
}
return CurrentListHead;
}
/* ------------------------------------------------------------------------------- */
void InitializeLinkedList(LinkAddress *ListHead)
{
int i;

Table of Contents continued


85

90

95

100

105

110

115

120

125

130

135

140

145

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

Link[MaxIndex] = null;
for (i = (MaxIndex - 1); i>0; i) {
Link[i] = i+1;
}
*ListHead = 1;
}
/* ------------------------------------------------------------------------------- */
void InitializeBins(void)
{
int C;
for (C = 'A'; C <= 'Z'; ++C) {
Front[C] = null;
Rear[C] = null;
}
}
/* ------------------------------------------------------------------------------- */
void PrintList(SortingArray A, LinkAddress L)
{
int counter = 0;
while (L != null) {
printf("%s ",A[L]);
if (((++counter)%11) == 0) {
putchar('\n');
/* print a return every 11 airport codes */
}
L = Link[L];
}
printf("\n\n");
}
/* ------------------------------------------------------------------------------- */
LinkAddress RadixSort(SortingArray A)
{
int k;
LinkAddress ListHead, CurrentNode;
/* initialize the linked list and let ListHead point to first node */
InitializeLinkedList(&ListHead);
/* make three passes from least to most significant radix digit k */
for (k = 2; k>=0; k) {
/* initialize the bins to point to empty lists */
InitializeBins();
/* scan the list and insert key-records into lists in the bins */
while (ListHead != null) {
CurrentNode = ListHead;
ListHead = Link[ListHead];
Insert(CurrentNode,k);
}
/* sweep the bins and link them into a linked list */

Table of Contents continued

150

155

160

165

170

175

180

185

190

195

200

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

ListHead = SweepBinsAndLink();
printf("pass number = %1d\n",3-k);
PrintList(A,ListHead);
}
return ListHead;

/* the procedure returns a pointer */


/* to the head of the sorted list */

}
/* ------------------------------------------------------------------------------- */
int main(void)
{
/* Initialize the array A and print it */
InitializeLinkedList(&ListHead);
printf("Original Unsorted Array =\n");
PrintList(A,ListHead);
/* Then RadixSort the array A and print it */
ListHead = RadixSort(A);
printf("Final Sorted Array =\n");
PrintList(A,ListHead);
}
/* main program */
/* -------------------------------------------------------------- */
/* when executed, the above program prints:
Original Unsorted Array =
DUS MEX ORD RAL ABQ ONT NRT YYZ GLA ZRH BUF
DFW DEN ARN JAX BRU CRQ MIA SEA STL HOU SAN
GCM HKG PIT MKC SFO JFK ORY LGA ORL
pass number = 1
GLA MIA SEA LGA MKC ORD BUF HKG ZRH JFK RAL
STL ORL GCM DEN ARN SAN SFO ABQ CRQ DUS ONT
NRT PIT BRU HOU DFW MEX JAX ORY YYZ
pass number = 2
RAL SAN JAX ABQ GCM SEA DEN MEX JFK SFO DFW
LGA MIA PIT MKC HKG GLA ONT HOU ORD ZRH ORL
ARN CRQ NRT BRU ORY STL BUF DUS YYZ
pass number = 3
ABQ ARN BRU BUF CRQ DEN DFW DUS GCM GLA HKG
HOU JAX JFK LGA MEX MIA MKC NRT ONT ORD ORL
ORY PIT RAL SAN SEA SFO STL YYZ ZRH
Final Sorted Array =
ABQ ARN BRU BUF CRQ DEN DFW DUS GCM GLA HKG
HOU JAX JFK LGA MEX MIA MKC NRT ONT ORD ORL
ORY PIT RAL SAN SEA SFO STL YYZ ZRH
*/
/* -------------------------------------------------------------- */

2. The results below are printed by executing the program for Ex


13.6.2 that follows the solution:
the original array A was:
i = 0
1
2

10

11

12

Table of Contents continued


A[i]=[ 3.5, 12.3,

4.2,

1.5,

5.7, 12.6,

4.7,

7.2, 12.1,

2.9,

0.7,

8.1,

9.3]

hit counts:
i = 0
H[i] = 1

1
1

2
1

3
1

4
2

5
1

6
0

7
1

8
1

9
1

10
0

11
0

12
3

prox map:
P[i] = 0

10

insertion locations:
L[i] = 3
10
4
i = 0
1
2

1
3

6
4

10
5

4
6

7
7

10
8

2
9

0
10

8
11

9
12

10

15

20

25

30

35

40

45

50

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

/*
* Ex. 13.6.2 -- Computing Hit Counts, Proxmap, and Insertion Locations
*/
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#define MaxIndex 13
typedef long int
typedef double

LongInt;
KeyType;

/* keys to be sorted are */


/* double precision floating point */

typedef enum {Empty, NotYetMoved, Moved} OccupantStatus;


/* typedef enum {false, true} Boolean; */
typedef struct {
OccupantStatus
int
KeyType
}Slot;

Status;
Proxmap, InsertionLoc;
Key;

typedef Slot SortingArray[MaxIndex];


/* ------------------------------------------------------------------------------- */
int MapKey(KeyType K)
{
return (int) floor(K);
}

/* Maps key K into 0:MaxIndex 1 */

/* -------------------------------------------------------------- */
void PrintArray1(SortingArray A)
{
int i,K;
putchar('[');
for (i = 0; i < MaxIndex; ++i) {
K = MapKey(A[i].Key);
if ((A[i].Status == NotYetMoved) || (A[i].Status == Empty) ) {
printf("-----");
} else {
printf("%5.1f",A[i].Key);
}
if (i<MaxIndex-1) putchar(',');
}
printf("]");
}

Table of Contents continued


|
|
|
55

60

65

70

75

80

85

90

95

100

105

110

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

/* -------------------------------------------------------------- */

void ProxmapSort(SortingArray A)
{
int
i, j, RunningTotal, TempInt;
KeyType
KeyToInsert, TempKey;
Boolean
NotInserted;
char
c;
/* Initialize Status and Proxmap */
for (i = 0; i < MaxIndex; ++i) {
A[i].Proxmap = 0;
/* init Proxmap to all zeroes */
A[i].Status = NotYetMoved;
}
/* Count hits when keys are mapped into insertion locations */
for (i = 0; i < MaxIndex; ++i) {
j = MapKey(A[i].Key);
A[i].InsertionLoc = j;
A[j].Proxmap++;
}
/* Print hit counts */
printf("\nhit counts:\n");
for (i = 0; i < MaxIndex; ++i) printf("%6d",i);
putchar('\n');
for (i = 0; i < MaxIndex; ++i) printf("%6d",A[i].Proxmap);
putchar('\n');
/* Convert hit counts to a Proxmap */
RunningTotal = 0;
for (i = 0; i < MaxIndex; ++i) {
if (A[i].Proxmap > 0) {
TempInt = A[i].Proxmap;
A[i].Proxmap = RunningTotal;
RunningTotal += TempInt;
}
}
/* Print Proxmap and check it */
printf("\nprox map:\n");
for (i = 0; i < MaxIndex; ++i) printf("%6d",A[i].Proxmap);
putchar('\n');
/* Compute InsertionLocs */
for (i = 0; i < MaxIndex; ++i) {
A[i].InsertionLoc = A[A[i].InsertionLoc].Proxmap;
}
/* Print insertion locations */
printf("\ninsertion locations:\n");
for (i = 0; i < MaxIndex; ++i) printf("%6d",A[i].InsertionLoc);
putchar('\n');
for (i = 0; i < MaxIndex; ++i) printf("%6d",i);
putchar('\n');
PrintArray1(A);
putchar('\n');
/* Now, A[i].InsertionLoc gives insertion location for A[i].Key */

Table of Contents continued

115

120

125

130

135

140

145

150

155

160

165

170

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

/* and A[i].Status is NotYetMoved for all i in the address range */


/* Rearrange A[i] in situ in A */
for (i = 0; i < MaxIndex; ++i) {
/* Find next key in ascending order of i that is NotYetMoved */
if (A[i].Status == NotYetMoved) {
j = A[i].InsertionLoc;
KeyToInsert = A[i].Key;
A[i].Status = Empty;
NotInserted = true;
while (NotInserted) {
if (A[j].Status == NotYetMoved) {
TempKey = A[j].Key;
A[j].Key = KeyToInsert;
KeyToInsert = TempKey;
A[j].Status = Moved;
j = A[j].InsertionLoc;
} else if (A[j].Status == Moved) {
if (KeyToInsert < A[j].Key) {

/* If KeyToInsert < */
/* A[j].Key */
TempKey = A[j].Key;
/* swap KeyToInsert */
A[j].Key = KeyToInsert;
/* and A[j] */
KeyToInsert = TempKey;

}
j++;

/* move to next key at A[j+1] */

} else {
A[j].Key = KeyToInsert;
A[j].Status = Moved;
NotInserted = false;
}
PrintArray1(A);
c = getchar();
}
/* end while */
}
/* end if */
}
/*end for */
}
/* end ProxmapSort */
/* -------------------------------------------------------------- */
void PrintArray(SortingArray A)
{
int i,K;
for (i=0; i<MaxIndex; ++i) printf("%6d",i);
printf("\n[");
for (i=0; i<MaxIndex; ++i) {
K = MapKey(A[i].Key);
printf("%5.1f",A[i].Key);
if (i < MaxIndex1) putchar(',');

/* A[j].Status = Empty */

Table of Contents continued


175

180

185

190

195

200

205

210

215

220

225

230

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

}
printf("]\n");
}
/* -------------------------------------------------------------- */
void InitArray(SortingArray A)
{
int i;
KeyType B[ ] = { 3.5, 12.3, 4.2, 1.5, 5.7, 12.6,
4.7, 7.2, 12.1, 2.9, 0.7, 8.1, 9.3
};
for (i=0; i<MaxIndex; ++i) A[i].Key = B[i];
}
/* -------------------------------------------------------------- */
int main(void)
{
SortingArray A;
/* Initialization */
InitArray(A);
PrintArray(A);
/* sort the array A */
ProxmapSort(A);
PrintArray(A);
}
/* end main */
/* -------------------------------------------------------------- */
/* the printout was:

0
1
3.5, 12.3,

2
4.2,

3
1.5,

4
5
5.7, 12.6,

6
4.7,

7
8
7.2, 12.1,

9
2.9,

10
0.7,

11
8.1,

12
9.3]

1
1

2
1

3
1

4
2

5
1

6
0

7
1

8
1

9
1

10
0

11
0

12
3

10

10

10

hit counts:
0
1

prox map:
0

insertion locations:
3

10

progressive stages of in situ rearrangement in ProxmapSort:


0
1
2
3
4
5
6
7
8
9
10
11
12
[-----,-----,-----,-----,-----,-----,-----,-----,-----,-----,-----,-----,-----]
[-----,-----,-----, 3.5,-----,-----,-----,-----,-----,-----,-----,-----,-----]
[-----, 1.5,-----, 3.5,-----,-----,-----,-----,-----,-----,-----,-----,-----]
[-----, 1.5,-----, 3.5,-----,-----,-----,-----,-----,-----, 12.3,-----,-----]
[ 0.7, 1.5,-----, 3.5,-----,-----,-----,-----,-----,-----, 12.3,-----,-----]
[ 0.7, 1.5,-----, 3.5, 4.2,-----,-----,-----,-----,-----, 12.3,-----,-----]
[ 0.7, 1.5,-----, 3.5, 4.2,-----, 5.7,-----,-----,-----, 12.3,-----,-----]
[ 0.7, 1.5,-----, 3.5, 4.2,-----, 5.7,-----,-----,-----, 12.3,-----,-----]
[ 0.7, 1.5,-----, 3.5, 4.2, 4.7, 5.7,-----,-----,-----, 12.3,-----,-----]
[ 0.7, 1.5,-----, 3.5, 4.2, 4.7, 5.7,-----,-----,-----, 12.3,-----,-----]
[ 0.7, 1.5,-----, 3.5, 4.2, 4.7, 5.7,-----,-----,-----, 12.3, 12.6,-----]

Table of Contents continued


235

240

245

|
|
|
|
|
|
|
|
|
|
|
|

[
[
[
[
[
[
[
[

0.7,
0.7,
0.7,
0.7,
0.7,
0.7,
0.7,
0
0.7,

1.5,-----,
1.5,-----,
1.5,-----,
1.5,-----,
1.5,-----,
1.5, 2.9,
1.5, 2.9,
1
2
1.5, 2.9,

3.5,
3.5,
3.5,
3.5,
3.5,
3.5,
3.5,
3
3.5,

4.2,
4.2,
4.2,
4.2,
4.2,
4.2,
4.2,
4
4.2,

4.7,
4.7,
4.7,
4.7,
4.7,
4.7,
4.7,
5
4.7,

5.7,-----,
5.7,-----,
5.7,-----,
5.7,-----,
5.7,-----,
5.7,-----,
5.7, 7.2,
6
7
5.7, 7.2,

8.1,-----,
8.1,-----,
8.1,-----,
8.1,-----,
8.1, 9.3,
8.1, 9.3,
8.1, 9.3,
8
9
8.1, 9.3,

12.3,
12.1,
12.1,
12.1,
12.1,
12.1,
12.1,
10
12.1,

12.6,-----]
12.6,-----]
12.3,-----]
12.3, 12.6]
12.3, 12.6]
12.3, 12.6]
12.3, 12.6]
11
12
12.3, 12.6]

*/
/* -------------------------------------------------------------- */

Answers to Review Questions 13.7


1. ShellSort applies insertion sorting to subsequences of keys spaced
delta spaces apart, for a sequence of deltas diminishing eventually to
1, at which time ordinary insertion sorting is applied. For example,
if d e l t a == 3, then S h e l l S o r t insertion sorts the subsequences
consisting of every third key starting at A [ 0 ] , every third key
starting at A [ 1 ] , and every third key starting at A [ 2 ] . A possible
sequence of deltas to use to sort A[0:n1] is to start with delta = 1 + n/3 ,
and to diminish d e l t a , using delta = 1 + n / 3 ; on each pass until
eventually delta == 1, at which time ShellSort terminates. ShellSort is
called a diminishing increment method because the deltas used as
increment intervals, diminish systematically to 1.
2. BubbleSort makes repeated passes through the array of keys to be
sorted, by scanning (A[i], A[i+1]) for i in 0:MaxIndex2 . It transposes (i.e.,
exchanges) any pair that is out of sorted order (i.e., for which A[i] >
A[i+1] ). It terminates after making the first pass during which no
out-of-order pairs were detected. It is called a t r a n s p o s i t i o n
sorting method because it works by transposing adjacent out-oforder pairs of keys.
3. BubbleSort runs in time O(n 2 ).

Solutions to Selected Exercises in Exercises 13.7


1. See the function FasterBubbleSort(A) in the program below.
2. See the function CocktailShakerSort(A) in the program below.

10

|
|
|
|
|
|
|
|
|
|
|

/*
* Ex. 13.7.1 & 13.7.2 -- Three Versions of BubbleSort including
*
FasterBubbleSort & CocktailShakerSort
*/
#include <stdio.h>
#include <stdlib.h>
/* -------------------------------------------------- */
void BubbleSort(SortingArray A)

Table of Contents continued

15

20

25

30

35

40

45

50

55

60

65

70

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

{
int
KeyType
Boolean

i;
Temp;
NotFinished;

do {
NotFinished = false;
for (i = 0; i < MaxIndex 1; ++i) {
if (A[i] > A[i+1]) {
/* exchange A[i] and A[i+1] */
Temp = A[i]; A[i] = A[i+1]; A[i+1]=Temp;
/* if you made an exchange you are not finished and */
/* you need another pass */
NotFinished = true;
}
}
} while (NotFinished);
/* NotFinished = false iff you made a */
/* pass and no pair of keys was out of order */
}
/* -------------------------------------------------- */
void FasterBubbleSort(SortingArray A)
{
int
i, UpperIndex;
KeyType Temp;
Boolean NotFinished;
UpperIndex = MaxIndex 2;
do {
NotFinished = false;
for (i = 0; i <= UpperIndex; ++i) {
if (A[i] > A[i+1]) {
/* exchange A[i] and A[i+1] */
Temp = A[i]; A[i] = A[i+1]; A[i+1]=Temp;
/* if you made an exchange you are not finished and */
/* you need another pass */
NotFinished = true;
}
}
UpperIndex ;
} while (NotFinished);
/* NotFinished = false iff you made a */
/* pass and no pair of keys was out of order */
}
/* -------------------------------------------------- */
void CocktailShakerSort(SortingArray A)
{
int
i, LowerIndex, UpperIndex;
KeyType Temp;
Boolean NotFinished;
LowerIndex = 0;
UpperIndex = MaxIndex 2;
do {
NotFinished = false;
for (i = LowerIndex; i <= UpperIndex; ++i) {
if (A[i] > A[i+1]) {
/* exchange A[i] and A[i+1] */

Table of Contents continued


75

80

85

90

95

100

105

110

115

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

Temp = A[i]; A[i] = A[i+1]; A[i+1]=Temp;


/* if you made an exchange you are not finished and */
/* you need another pass */
NotFinished = true;
}
}
UpperIndex ;
for (i = UpperIndex; i >=LowerIndex; i) {
if (A[i] > A[i+1]) {
/* exchange A[i] and A[i+1] */
Temp = A[i]; A[i] = A[i+1]; A[i+1]=Temp;
/* if you made an exchange you are not finished and */
/* you need another pass */
NotFinished = true;
}
}
LowerIndex++;
} while (NotFinished);
/* NotFinished = false iff you made a */
/* pass and no pair of keys was out of order */
}
/* -------------------------------------------------- */
/*
The Table below give the results in Ticks for an average
running time of five runs of each variant of BubbleSort on
the array sizes given at the left of the table.
Array
Normal
Faster
Cocktail
Size
BubbleSort
BubbleSort
ShakerSort
----------------------------------------------------64
7.2
4.6
3.8
128
33.0
19.2
16.4
256
131.8
76.0
60.6
512
543.2
305.4
242.4
1024
2234.2
1229.6
972.4
-----------------------------------------------------

It is seen that CocktailShakerSort is 26% faster than


the faster BubbleSort, and 230% faster than normal
BubbleSort, for arrays of size 1024.
*/
/* -------------------------------------------------- */

Answers to Review Questions 13.8


1. InsertionSort is the best of the O(n 2 ) sorting methods compared in
Table 13.47.
2. QuickSort is the best of the O(n log n ) sorting methods compared in
Table 13.47.
3. Sometimes special cases arise in which a direct efficient method
of sorting can be applied, such as when we are given an array A[0: n
1 ] containing a permutation of the numbers in the range

Table of Contents continued


100:100+ n 1. In this case, we need only store A[ i] in location A[ A [ i]
100] (for 0 i n 1) to sort the array.

Solutions to Selected Exercises in Exercises 13.8


1. Solution not given.
2. B u b b l e S o r t because its first pass is its last pass and it does not
move any keys.
3. Solution not given.

Table of Contents continued

Answers to Review Questions 14.2


1. A context-free grammar (CFG) is a 4-tuple (S, VN , VT , P), where VN
and VT are finite sets of distinct symbols (such that VN VT = )
called the non-terminal symbols (VN ) and the terminal symbols
( V T ) respectively, where S V N is a distinguished symbol of V N
called the start symbol , and where P is a set of productions of the
form x , such that x VN and such that is a string of symbols in
V T VN.
2. Let x be a production in a CFG. We say x is d i r e c t l y
recursive if the string contains x , and we say x is indirectly
recursive if contains a non-terminal symbol, say y , such that y
can derive to a string containing x using other productions of the
CFG.
3. A rightmost derivation of a sentence with respect to a contextfree grammar G = (S, V N , VT , P) is a derivation of starting with S,
in which only the r i g h t m o s t non-terminal symbol of each
sentential form is derived at each step of the derivation.
4. The language L(G) generated by the context-free grammar G is the
set of all sentences derived in G starting with the start symbol S
of G.

Solutions to Selected Exercises in Exercises 14.2


1. Starting with

to rewrite it as

E+T

E + T*F
E + T*P
E + T*a
E + F*a
E + P*a
E + a*a
T + a*a
F + a*a
F P + a*a
F a + a*a
P a + a*a
( E ) a + a*a
(E T) a + a*a
(E F) a + a*a
(E P) a + a*a
(E a) a + a*a
(T a) a + a*a
(F a) a + a*a
(P a) a + a*a

apply production
EE+T
TF
FP
Pa
TF
FP
Pa
ET
TF
FF P
Pa
FP
P(E)
EET
TF
FP
Pa
ET
TF
FP
Pa

Table of Contents continued


(a a) a + a*a

2. A new extended grammar which generates expressions having a


unary minus is as follows:

EE+T | ET | F
TT*F | T/F | F
FFU | U
UP |+P | P
P(E) | a

where a <letter> | <digit>

3. Define a struct type for binary tree nodes as follows:


typedef struct TreeNodeTag {
KeyType
Key;
struct TreeNodeTag *LeftLink;
struct TreeNodeTag *RightLink;
} TreeNode;

Then you can declare a variable N suitable for containing a pointer


to a TreeNode as follows:
TreeNode *N;

Then you can store a pointer to a T r e e N o d e in N , which, in turn,


contains TreeNode pointers (or N U L L pointers) as its LeftLink and
RightLink members. Non-null LeftLink and RightLink TreeNode pointers can,
in turn, point to other T r e e N o d e s having subtrees of finite, but
unbounded, shape. Thus, the struct typedef given above
recursively defines an unbounded set of linked binary tree data
structures using a finite definition.

Answers to Review Question 14.3


1. The process of parsing a sentence is the process of discovering
its grammatical structure with respect to a grammar G, by
producing a derivation of the sentence in G. This can be done by
progressively collapsing the sentence into the start symbol of G,
using repeated replacement of various substrings of the
sentence (or of partially collapsed sentential forms) with nonterminals N, where N is an appropriate production of the
grammar G.
2. The parser of Program 14.6 uses a set of four functions that can
call one another in a cyclic fashion, and hence which can make
recursive calls on themselves indirectly, using a chain of calls on
the others. Such a set of functions is said to be mutually recursive .
In particular, in Program 14.6, ParseP calls ParseE , which calls ParseT ,

Table of Contents continued


which calls ParseF , which calls ParseP in a cyclic fashion. In symbols,
we could write: ParseP ParseE ParseT ParseF ParseP , where the
symbol means calls.
3. Rightmost derivations and leftmost parses are inverses in the
sense that when a rightmost derivation (in which rightmost nonterminals are replaced by right sides of productions) is read
backwards, it gives a plan for replacing a leftmost subexpression,
which is equal to the right side of some production, with the nonterminal on the left side of that production yielding an instance
of a leftmost parse.

Solutions to Selected Exercises in Exercises 14.3


1. A C module ParserUtilities.c , which implements the low-level utility
calls in Program 14.6 is given as follows:

10

15

20

25

30

35

40

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

/*
* Ex. 14.3.1 the "ParserUtilities.c" module
*
* This module exports parser utility services. It uses the same sequential
* stack representation as is used for Program 7.5, the parenthesis matching
* program in Chapter 7.
*/
#include "ParserUtilities.h"
/******
*
* The ParserUtilities.h file contains the following external declarations:
*
*****/
/*
#include
#include
#include
#include
#include

<stdio.h>
<stdlib.h>
<string.h>
<ctype.h>
"SeqStackInterface.h"

/+ The last line above includes the following from +/


/+ the "SeqStackInterface.h" file: +/
typedef StackType Stack;

/+ the StackType will depend on the +/


/+ representation +/
extern void InitializeStack (Stack *S);
/+ Initialize the stack S to be the empty stack +/
extern int Empty (Stack *S);
/+ Returns TRUE if and only if the stack S is empty +/
extern int Full (Stack *S);
/+ Returns TRUE if and only if the stack S is full +/
extern void Push (ItemType X, Stack *S);
/+ If S is not full push a new item X onto the top of stack S +/

Table of Contents continued

45

50

55

60

65

70

75

80

85

90

95

100

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

extern void Pop (Stack *S, ItemType *X);


/+ If S is non-empty, pop an item off the top of the stack S +/
/+ and put it in X +/
/+ -------------------< a typedef exported by the module >----------------------- +/
extern typedef enum {false, true} Boolean;
/+ ---------------< the only variable exported by the module >------------------- +/
extern char Next;

/+ the next character in the input string +/

/+ --------------< the four procedures exported by the module >----------------- +/


extern void GetInputStringFromUser(void);
extern void Reduce(int n, char S);
extern void Shift(char c);

/+ gets the Input +/


/+ string from the user +/

/+ pops n items off Stack, +/


/+ then pushes S on Stack +/
/+ reads c from Input and +/
/+ pushes c on Stack.+/

extern Boolean UserWantsToContinue(void);


/+ returns "true" iff +/
/+ user wants to continue.+/
/+ ------------------------------------------------------------------------------- +/
*/
/* ----<< the implementation part of the module >>---- */
/* --------------------< variables private to the module>------------------------- */
char

Next;

/* the next character in the input string */

char
*Answer = "blank",
/* a reply from the user */
*InputString
/* the input string */
= "a blank input string long enough for an expression";
Stack

InputStack, ParseStack;

/* -------------------< functions defined in the module >------------------------ */


void GetInputStringFromUser(void)
{
int i;
InitializeStack(&ParseStack);
InitializeStack(&InputStack);
Push('#',&InputStack);
printf("give input string: ");
gets(InputString);

/* gets an Input */
/* string from user to parse */
/* initialize the ParseStack */
/ to the empty stack */
/* initialize the InputStack */
/* to the empty stack */
/* put a 'pad' character */
/* at bottom of InputStack */
/* scanf("%s",InputString); */

for (i = strlen(InputString) 1; i >= 0; i) {


Push(InputString[i],&InputStack);
}
Next = '#';

Table of Contents continued

105

110

115

120

125

130

135

140

145

150

155

160

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

Shift('#');

/* get things started */

}
/* ------------------------------------------------------------------------------- */
char FetchNext(void)
{
char d, f;
d = Next;

/* returns next non-blank */


/* character in the Input */

/* find the next non-space character in the Input */

do {

/* find the next non-space character in the Input */


Pop(&InputStack, &f);
} while (f == ' ');
Next = f;

/* let Next be this next non-space */


/* character in the Input */

return d;

/* return d as the value of the function */

}
/* ------------------------------------------------------------------------------- */
void PrintErrorMessageIfNecessary(char c, char d)
{
/* if d isn't equal to the expected character c, write an error message. */
if (c == 'a') {

/* recall that 'a' stands for any <letter> or <digit> */

if (! (islower(d) | | isdigit(d)) ) {
printf("Syntax Error: expected <letter> or <digit> but got %c\n", d);
}
} else if (c != d) {
printf("Syntax Error: expected %c but got %c\n", c, d);
}
}
/* ------------------------------------------------------------------------------- */
void Shift(char c)
{
char d;
/* d holds the character Shifted from the Input string */
d = FetchNext( );

/* read the next character d from */


/* the Input string */
Push(d,&ParseStack);
/* push d on top of ParseStack */
PrintErrorMessageIfNecessary(c,d);
/* if c != d then */
/* print Shift error message */
}
/* ------------------------------------------------------------------------------- */
void Reduce(int n, char S)
{
int i; char *Output;

/* pop n items from Stack, push S */

/* write the reduction: top n items of stack > S, and */


/* pop n chars off stack. */

Table of Contents continued


165

170

175

180

185

190

195

200

205

210

215

220

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

Output = "

";

Output[2*n] = '\0';
for (i = n 1; i >= 0; i) {
Pop(&ParseStack,&Output[2*i]);
Output[2*i+1] = ' ';
}
if (n == 1) {
if (S == 'P') {
/* PFstring = Concat(PFString,Output); */
printf(" %c >

{ where a = %s}\n", S, Output);


/* later for use in Program 14.8 */
/* add," %s", . . . , PFString) */

} else {
printf(" %c >

%s\n",S,Output);

}
} else {
printf(" %c > %s\n", S, Output);
/* later for use in Program 14.8, add:
if ((n == 3) && (S != 'P') ) {
PFstring = Concat(PFstring, Output[3],' ');
printf("

%s\n", PFString);

} else {
putchar('\n');
}
*/
}
/* end if */
/* push S on stack */
Push(S,&ParseStack);
}
/* end Reduce */
/* ------------------------------------------------------------------------------- */
int UserWantsToContinue(void)
{
printf("Do you want to give another string? (y/n) ");
gets(Answer);
/* scanf("%s",Answer); */
return (Answer[0] == 'y');
}
/* -----------------------------< end of ParserUtilities.c >----------------------------- */

Table of Contents continued


2. The productions for G 1 are {E 0 E 0, E 1 E 1, E 2}. The
corresponding railroad diagram for E is:

A function for a recursive-descent Parser for E is:

10

15

20

25

30

35

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

/*
* Solution to Ex.14.3.2 -- Marked Palindrome Parser
*/
#include "ParserUtilities.h"
extern char Next;

/* the next character in the input string */

/* ------------------------------------------------------------------------------- */
void ParseE(void)
/* parse a marked palindrome */
{
if (Next == '2') {
Shift('2');
Reduce(1,'E');
} else if (Next == '0') {
Shift('0');
ParseE( );
Shift('0');
Reduce(3,'E');
} else if (Next == '1') {
Shift('1');
ParseE( );
Shift('1');
Reduce(3,'E');
}
}
/* ------------------------------------------------------------------------------- */
int main(void)
{
do {
GetInputStringFromUser( );
ParseE( );
/* call the parser to parse the input string. */
} while ( UserWantsToContinue( ) );
}

Table of Contents continued


3. It is not possible to write a left-to-right, shift-reduce, recursive
descent parser with a finite lookahead that parses unmarked
palindromes in the language generated by the grammar G2 .
4

G3 = { E 1 1 0, E 1 1 E 0 }.

5. The grammar cannot generate any sentences, which are sentential


forms containing only terminal symbols, because every sentential
form generated by the grammar contains exactly one non-terminal
symbol. Hence L(G) = .

Answers to Selected Review Questions 14.4


1. The infix-to-postfix translator given in Program 14.8 is a modified
left-to-right, shift-reduce parser for infix expressions that has
been extended to produce postfix output. There is an initially
empty postfix output string stored as the value of the variable
PostfixString and there is a procedure AppendToOutput(x) , which concatenates the string x onto the right end of the PostfixString . Each of the
original procedures P a r s e P , P a r s e F , P a r s e T , and P a r s e E , is a selfcontained unit of action which parses respectively, an instance of a
P, F, T, or E, in the input string, and appends its respective postfix
translation P , F , T , or E , to the output stored as the value of
PostfixString . In so doing, each parsing routine may make calls on the
others and may depend on their output as units of action used to
accomplish its own task. For example, ParseT may try to parse a T*F
as a T and may call P a r s e F first to parse the initial T which is
grammatically forced to become an F in T*F. and may then call
P a r s e F again to parse the rightmost F in T*F. However, before
calling P a r s e F the second time, the multiplication operator (*) is
saved as the value of the local variable Operator in ParseT . Then after
P a r s e F has been called twice, the multiplication operator is
appended to the end of PostfixString, using AppendToOutput(Operator) . The
result is to append T F * onto the end of the PostfixString as the
postfix translation of the infix T*F (where T is the postfix
translation of T obtained by calling ParseF and reducing its parser
output F to a T, and where F is the postfix translation of F obtained
by calling Parse F).
2. The main theme used in the infix-to-postfix translator is: To
translate an expression containing a binary infix operator into
postfix, first parse the infix subexpression for the left operand
and append its postfix translation to the end of the output string,
save the binary operator, parse the infix subexpression for the
right operand and append its postfix translation to the end of the
output string, and finally, append the operator to the end of the
output string. The translations of the left and right

Table of Contents continued


subexpressions are performed by making appropriate calls on
functions to parse and translate the expected types of operand
subexpressions.
3. Yes. For instance, ParseE calls Parse T, which calls ParseF , which calls
ParseP , which calls ParseE in a cycle. Hence a call to any of ParseE ,
ParseT , ParseF , or ParseP can generate an indirect recursive call to
itself via a chain of intermediate calls to the others.

Solutions to Selected Exercises in Exercises 14.4


1. The solutions to Exs. 14.4.1 and 14.4.2 are contained in the
following program:

10

15

20

25

30

35

40

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

/****
*
* Exs 14.4.1 & 14.4.2 -- Augmented Infix-to-Postfix translator & evaluator
*
-- which handles unary minus
*
***/
/****
*
* To complete the module "YourCalculationModule" of Ex. 14.4.2 add a
* headerfile "YourCalculationModule.h" to the current ".c" file containing
* the extern declaration:
*
*
extern char *Evaluate(char *);
*
* as shown in Program 4.12 on p. 109
*
***/
/****
*
* This program augments the Recursive Descent Parser of Program 14.6
* to handle unary minus operators according to the grammar of Ex 14.2.2
* shown immediately below, translates infix to postfix, evaluates the
* postfix, and prints the string representing the floating point value.
*
*
E --> E + T | E T | T
*
T --> T * F | T / F | F
*
F --> F ^ U | U
*
U --> P | + P | P
*
P --> ( E ) | a
*
* Here, we let 'a' stand for any letter or digit:
*
*
a --> <letter> | <digit>
*
* The grammar above uses E, T, F, and P to stand for the following
* "standard" abbreviations in the literature:
*
* E = Expression, T = Term, F = Factor, and P = Primary.
*
* In addition, it uses U to stand for a Unary operand.
*
****/

Table of Contents continued


45

50

55

60

65

70

75

80

85

90

95

100

105

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

#include "ParserUtilities.h"
#include "EvalStackInterface.h"
/* -< The ParserUtilities.h file contains the following external declarations >- */
/*
#include
#include
#include
#include
#include

<stdio.h>
<stdlib.h>
<math.h>
<ctype.h>
<string.h>

/+ ---------------< the four procedures exported by the module >---------------- +/


extern void GetInputStringFromUser(void);
extern void Reduce(int n, char *S);
extern void Shift(char *c);

/+ gets the Input string +/


/+ from the user
+/
/+ pops n items off Stack, +/
/+ then pushes S on Stack +/
/+ reads c from Input and +/
/+ pushes c on Stack
+/

extern Boolean UserWantsToContinue(void); /+ returns "true" iff user +/


/+ wants to continue
+/
extern void AppendToOutput(char X);

/+ appends X to postfix
/+ output string

+/
+/

/+ ----------------------------------------------------------------------------- +/
*/
/* ------< three external variables imported from ParserUtilities.c >--------- */
extern char Next;

/* the next character in the input string */

extern char *PostfixString;


extern char *InputString;

/* the postfix output string */


/* the user-supplied input string */

/* ---< an external variable imported from EvalStackImplementation.c >--- */


extern Stack EvalStack;

/* where ItemType = float */

/* --------------------< a global variable for this module >--------------------- */


char *ResultString

= "00000000000000000000";
/* the string */
/* for the value of the output */

/* -------------< the internal functions of the module begin here >--------------- */


extern void ParseE(void);

/* extern because function */


/* called before defined */

/* ------------------------------------------------------------------------------- */

Table of Contents continued

110

115

120

125

130

135

140

145

150

155

160

165

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

void ParseP(void)
{
char Operand;

/* parse a Primary */

if (Next == '(') {
Shift('(');
ParseE( );
Shift(')');
Reduce(3,'P');
} else {
Operand = Next;
Shift('a');
/* 'a' stands for any <letter> or <digit> */
Reduce(1,'P');
AppendToOutput(Operand);
}
}
/* ------------------------------------------------------------------------------- */
void ParseU(void)
{
char Operator;
if ( (Next == '+') || (Next == '') ) {
Operator = Next;
Shift(Next);
ParseP( );
Reduce(2,'U');
if (Operator == '') {
AppendToOutput('~');
}
} else {
ParseP( );
Reduce(1,'U');
}

/* parse a Unary */

/* appends unary minus */


/* operator */

}
/* ------------------------------------------------------------------------------- */
void ParseF(void)
{
char Operator;

/* parse a Factor */

ParseU( );
Reduce(1,'F');
while (Next == '^') {
Operator = Next;
Shift(Next);
ParseU( );
Reduce(3,'F');
AppendToOutput(Operator);
}
}
/* ------------------------------------------------------------------------------- */
void ParseT(void)
{
char Operator;
ParseF( );
Reduce(1,'T');

/* parse a Term */

Table of Contents continued


170

175

180

185

190

195

200

205

210

215

220

225

230

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

while ( (Next == '*') || (Next == '/') ) {


Operator = Next;
Shift(Next);
ParseF( );
Reduce(3,'T');
AppendToOutput(Operator);
}
}
/* ------------------------------------------------------------------------------- */
void ParseE(void)
{
char Operator;

/* parse an Expression */

ParseT( );
Reduce(1,'E');
while ( (Next == '+') || (Next == '') ) {
Operator = Next;
Shift(Next);
ParseT( );
Reduce(3,'E');
AppendToOutput(Operator);
}
}
/* ------------------------------------------------------------------------------- */
void InterpretPostfix(void)
{
float LeftOperand, RightOperand, Result;
int i;

/* the index of the ith character in the PostfixString */

char c;

/* c = ith character of the input string */

char *s = "x";

/* s will hold a null-terminated string in which */


/* s[0] will hold c, for use in an atof conversion */

InitializeStack(&EvalStack);
for (i = 0; i < strlen(PostfixString); ++i) {
s[0] = c = PostfixString[i];
if (isdigit(c)) {

/* s[0] = c = ith character of */


/* input string */
/* if c is a digit, push c's value onto stack */

if ( Full(&EvalStack) ) {
printf("Stack full. Postfix Evaluation could not be completed.\n");
return;
} else {
Push((float)atof(s),&EvalStack);
}
} else if (c=='~') {

/* handle case of unary minus operator */

if ( Empty(&EvalStack) ) {
printf("Malformed postfix input string. Too many operators\n");
printf("and too few operands.\n");
return;
} else {
Pop(&EvalStack,&RightOperand);

Table of Contents continued


|
Push( RightOperand, &EvalStack);
|
}
|
|
235
|
} else if (c=='+' || c=='' || c=='*' || c=='/' || c=='^') {
|
|
if ( Empty(&EvalStack) ) {
|
printf("Malformed postfix input string. Too many operators\n");
|
printf("and too few operands.\n");
240
|
return;
|
} else {
|
Pop(&EvalStack,&RightOperand);
|
if ( Empty(&EvalStack) ) {
|
printf("Malformed postfix input string. Too many\n");
245
|
printf("operators and too few operands.\n");
|
return;
|
} else {
|
Pop(&EvalStack, &LeftOperand);
|
250
|
switch (c) {
/* perform the operation */
|
|
case '+': Push(LeftOperand+RightOperand,&EvalStack);
|
break;
|
case '': Push(LeftOperandRightOperand,&EvalStack);
255
|
break;
|
case '*': Push(LeftOperand*RightOperand,&EvalStack);
|
break;
|
case '/': if (RightOperand != 0.0) {
|
Push(LeftOperand/RightOperand,&EvalStack);
260
|
} else {
|
printf("Attempt to divide by zero.\n");
|
return;
|
}
|
break;
265
|
case '^': Push(exp(log(LeftOperand)*RightOperand),
|
&EvalStack);
|
break;
|
default: break;
|
}
270
|
}
|
}
|
} else {
|
printf("Illegal character '%c' in postfix expression.\n",c);
|
return;
275
|
}
|
} /* end for */
|
|
|
Pop(&EvalStack,&Result);
/* remove final result from stack */
280
|
|
if ( Empty(&EvalStack) ) {
|
sprintf(ResultString,"%f",Result);
/*convert float result */
|
} else {
/* to string output */
|
printf("Malformed postfix string.\n");
285
|
printf("Too many operands and not enough operators.\n");
|
}
| }
|
| /* -----< the following function is exported by YourCalculationModule >----- */
290
|
|
char *Evaluate(char *S)

Table of Contents continued

295

300

305

310

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

{
strcpy(InputString,S);
ParseE();
/* call the parser to parse the input string. */
InterpretPostfix();
return ResultString;
}
/* --< the main function tests the operation of YourCalculationModule >-- */
int main(void)
{
do {
PostfixString[0] = '\0';
/* initialize PostfixString to empty string */
GetInputStringFromUser( );
ResultString = Evaluate(InputString);
printf("output = %s\n",ResultString);
putchar('\n');
} while ( UserWantsToContinue( ) );
}
/* end main program */

Answers to Review Questions 14.5


1. The first is : To prove P(n ) is true for all non-negative integers n
0, (1) First, prove P(0) is true, and then, (2) Assuming P( n ) is true
for n , prove P(n +1) is true.
The second is: To prove P(n ) is true for all integers n 0, (1) First,
prove P(0) is true, and then, (2) Assuming P(k ) is true for 0 k < n ,
prove P(n ) is true.
2. Recursion induction is a method that can be used to prove that
recursive programs correctly produce their intended results by
first proving that base cases are correct, and then, on the
assumption that recursive calls correctly solve smaller
subproblems and eventually result in calls to base cases, by
proving that the recursive programs themselves correctly
produce their intended results.
3. First, for the m base cases of sizes n 1 , n 2 , ... , n m , prove F(t 1 ), F(t 2 ),
... , F(t m ) satisfy P(n 1 ), P(n 2 ), ... , P(n m ) respectively. Then, on the
assumption that F( t ) calls itself recursively with calls F( t j ) on
problems of smaller size n j (eventually leading to base cases),
show that F(t ) produces results that enable you to prove that P(n )
is true.

Solutions to Selected Exercises in Exercises 14.5


1. When you attempt to apply recursion induction to
f ( n ) = 1, you encounter the difficulty that recursive
within f sometimes make calls on subproblems of larger
the cases where n is odd when f ( n ) calls f ( 3 * n +1).

prove that
calls on f
size, as in
Thus, the

Table of Contents continued


assumption that all calls on subproblems of smaller size yield
correct results is of no use in constructing the proof in cases
where the subproblems in the recursive calls are of larger size
because, in such cases, this assumption doesnt apply.
2. The Partition algorithm given as Program 13.15 does not always
meet the requirement that i == j+1 after the call to Partition(A, &i, &j) .
Hence, a redesigned partition algorithm, such as that given below,
must be used, which guarantees that A[m:n] is partitioned into A[m:j]
and A[i:n] where i == j+1 and m j < n after returning from the call
Partition(A, m, n) and after setting i and j appropriately. On that
assumption, we turn to the proof that the Find function given below
correctly moves the k th item of A[m:n] into the k th position, where 1
k nm+1 initially.
For the base case , suppose k == 1 and m == n. Then the call
does nothing to rearrange A (because the condition m < n
on line 28 is false) and the k th (i.e., the first) item of A[m:n] = A[m].
Find(A,k,m,n)

Now suppose A[m:n] contains more than one item, so that m < n, and
suppose, by recursion induction, that Find(A,k,m,n) works correctly
for all arrays with fewer than nm+1 items.
The partition algorithm is called with Partition(A, m, n) to partition
A[m:n] into a left partition A[m:j] and a right partition A[i:n] , where i ==
j+1 and m j < n. Lines 30:36 of the Find function below set i and j
appropriately to ensure that these conditions are met. The
elements in the left partition will be less than or equal to the
smallest element in the right partition after returning from the
partition function call. If now, k (jm+1), it means that there are at
least k elements in the left partition (recalling that the number of
elements in A [ m : j ] is j m + 1 according to Eq. A.5 of the Math
Reference appendix). Since all the items in the left partition are
smaller than any of the items in the right partition and there are k
or more items in the left partition, the k th smallest item in A[m:n] is
found by taking the k th smallest item in the left partition, which, by
recursion induction, is correctly found by recursively calling
F i n d ( A , k , m , j ) on line 39 of the F i n d function given below. (It is
guaranteed that A[m:j] is a smaller array than A[m:n] because j < n after
the call to Partition(A,m,n) and after the computations to set i and j on
lines 30:36 of Find below. Hence, the recursion induction condition
applies in this case.)
Now suppose that, after returning from the call Partition(A,m,n) and
after the computations to set i and j on lines 30:36 of Find below, it
is false that k (jm+1) , meaning that there are fewer than k
elements in A[m:j] . Then because all jm+1 items in A[m:j] are less than
or equal to each of the items in A[i:n] , the k th smallest item in A[m:n] is

Table of Contents continued


not to be found among the items in the left partition but instead is
to be found among the items in the right partition. Because (jm+1)
of the smallest items of the original A[m:n] are already in the left
partition, the k th smallest item of A[m:n] is obtained by finding the k
( j m + 1 ) th item of the right partition A[i:n] where i == j+1 . Again by
recursion induction, since A[i:n] contains fewer items than A[m:n] , the
call Find(A,k(jm+1),i,n) on line 41 correctly finds the k(jm+1) th item of
A[i:n] which is the same as the k th smallest item of A[m:n] .

10

15

20

25

30

35

40

45

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

/*--------------------------------------------------------------------------*/
int Partition(ItemArray A, int i, int j)
/* assume i < j */
{
ItemType Pivot, Temp;
/* the value returned by the Partition */
int k, middle, p;
/* function is the location of the Pivot */
/* after partitioning */
middle = (i+j)/2;
Pivot = A[middle]; A[middle] = A[i]; A[i] = Pivot;
p = i;
for (k= i+1; k <= j; ++k) {
if (A[k] < Pivot) {
Temp = A[++p]; A[p] = A[k]; A[k] = Temp;
}
}
Temp = A[i]; A[i] = A[p]; A[p] = Temp;
return p;
}
/*--------------------------------------------------------------------------*/
void Find(ItemArray A, int k, int m, int n)
/* to move the kth smallest */
{
/* item in A[m:n] into the kth position */
int i, j, p;
if (m < n) {

/* if there is more than one item in A to partition */

p = Partition(A, m, n); /* p == location of the Pivot after partition */


if (p == n) {
j = p1; i = p;
} else {
j = p; i = p+1;
}

/* ensure that the partition is of the form */


/* A[m:j] and A[i:n], where i == j+1 */
/* and ensure that mj<n */

if ( k <= (j m + 1) ) {
/* if A[m:j] has at least k items, then */
Find(A, k, m, j);
/* Find kth in A[m:j] */
} else {
/* otherwise, */
Find(A, k(jm+1), i, n);
/* Find k(jm+1)th in A[i:n] */
}
}
}
/*--------------------------------------------------------------------------*/

Table of Contents continued


3. Assume A [ 0 : n 1 ] is an array of n distinct objects, and let the
recursion induction hypothesis for calling P e r m ( A , m , n ) be the
following:
prints all distinct permutations of A[0:n1] with
fixed and A[0:m1] varying.

Perm(A,m,n)
A[m:n1]

For the base case , m == 0, we need to show that Perm(A,0,n) prints all
permutations of A[0:n1] with A[0:n1] fixed and A[0:1] varying. The
range 0:1 is empty (because by definition i:j == { k | i k j}, so 0:1 == { k
| 0 k 1} == ). Hence there is only one permutation of A[0:n1] with
A[0:n1] fixed and A[0:1] == varying, and it is obtained by printing
A[0:n1] . But, when m == 0, Perm(A,0,n) calls PrintPerm(A,n) which simply
prints the items in A [ 0 : n 1 ] (see lines 13:21 and 38:39 of the
program below). Hence, for m == 0, Perm(A,m,n) prints A[0:n1] which is
what we needed to show.
Now assume that Perm(A,k,n) prints all the permutations with A[k:n
fixed and A[0:k1] varying for all k < n. In Perm(A,m,n) , when m == n, for
each i in 0:n1 Perm(A,n,n) exchanges A[i] with A[n1] , then calls Perm(A,n
1,n) to print all permutations of A[0:n1] with A[n1] fixed and A[0:n2]
varying, and exchanges A[n1] and A[i] back again. The effect of this
is to choose each of the distinct objects in A [ 0 : n 1 ] as the fixed
object A [ n 1 ] while printing all permutations with the remaining
A [ 0 : n 2 ] objects varying (which must occur correctly by the
recursion induction hypothesis using the recursive call Perm(A,n1,n)
on a problem of size k == n1 where k < n).
1]

Therefore, calling Perm(A,n,n) prints all distinct permutations of


A[0:n1].

10

15

20

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

/*------------------------------------------------------------*/
void Exchange(ItemArray A, int i, int j)
{
ItemType temp;
temp = A[i]; A[i] = A[j]; A[j] = temp;
}
/*------------------------------------------------------------*/
void PrintPerm(ItemArray A, int n)
{
int i;
for (i = 0; i < n; ++i) {
printf("%2d, ",A[i]);
}
putchar('\n');
}
/*------------------------------------------------------------*/

Table of Contents continued


25

30

35

40

45

50

55

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

void InitPermArray(ItemArray A, int n)


{
int i;
for (i=0; i<n; ++i) A[i]=i+1;
}
/*------------------------------------------------------------*/
void Perm(ItemArray A, int m, int n)
{
int i;
if (m == 0) {
PrintPerm(A, n);
} else {
for (i = 0; i < m; ++i) {
Exchange(A, i, m1);
Perm(A, m1, n);
Exchange(A, m1, i);
}
}
}
/*------------------------------------------------------------*/
void Permute(ItemArray A, int n)
{
InitPermArray(A,n);
Perm(A,n,n);
}
/*------------------------------------------------------------*/

Table of Contents continued

Answers to Review Questions 14.2


1. A context-free grammar (CFG) is a 4-tuple (S, VN , VT , P), where VN
and VT are finite sets of distinct symbols (such that VN VT = )
called the non-terminal symbols (VN ) and the terminal symbols
( V T ) respectively, where S V N is a distinguished symbol of V N
called the start symbol , and where P is a set of productions of the
form x , such that x VN and such that is a string of symbols in
V T VN.
2. Let x be a production in a CFG. We say x is d i r e c t l y
recursive if the string contains x , and we say x is indirectly
recursive if contains a non-terminal symbol, say y , such that y
can derive to a string containing x using other productions of the
CFG.
3. A rightmost derivation of a sentence with respect to a contextfree grammar G = (S, V N , VT , P) is a derivation of starting with S,
in which only the r i g h t m o s t non-terminal symbol of each
sentential form is derived at each step of the derivation.
4. The language L(G) generated by the context-free grammar G is the
set of all sentences derived in G starting with the start symbol S
of G.

Solutions to Selected Exercises in Exercises 14.2


1. Starting with

to rewrite it as

E+T

E + T*F
E + T*P
E + T*a
E + F*a
E + P*a
E + a*a
T + a*a
F + a*a
F P + a*a
F a + a*a
P a + a*a
( E ) a + a*a
(E T) a + a*a
(E F) a + a*a
(E P) a + a*a
(E a) a + a*a
(T a) a + a*a
(F a) a + a*a
(P a) a + a*a

apply production
EE+T
TF
FP
Pa
TF
FP
Pa
ET
TF
FF P
Pa
FP
P(E)
EET
TF
FP
Pa
ET
TF
FP
Pa

Table of Contents continued


(a a) a + a*a

2. A new extended grammar which generates expressions having a


unary minus is as follows:

EE+T | ET | F
TT*F | T/F | F
FFU | U
UP |+P | P
P(E) | a

where a <letter> | <digit>

3. Define a struct type for binary tree nodes as follows:


typedef struct TreeNodeTag {
KeyType
Key;
struct TreeNodeTag *LeftLink;
struct TreeNodeTag *RightLink;
} TreeNode;

Then you can declare a variable N suitable for containing a pointer


to a TreeNode as follows:
TreeNode *N;

Then you can store a pointer to a T r e e N o d e in N , which, in turn,


contains TreeNode pointers (or N U L L pointers) as its LeftLink and
RightLink members. Non-null LeftLink and RightLink TreeNode pointers can,
in turn, point to other T r e e N o d e s having subtrees of finite, but
unbounded, shape. Thus, the struct typedef given above
recursively defines an unbounded set of linked binary tree data
structures using a finite definition.

Answers to Review Question 14.3


1. The process of parsing a sentence is the process of discovering
its grammatical structure with respect to a grammar G, by
producing a derivation of the sentence in G. This can be done by
progressively collapsing the sentence into the start symbol of G,
using repeated replacement of various substrings of the
sentence (or of partially collapsed sentential forms) with nonterminals N, where N is an appropriate production of the
grammar G.
2. The parser of Program 14.6 uses a set of four functions that can
call one another in a cyclic fashion, and hence which can make
recursive calls on themselves indirectly, using a chain of calls on
the others. Such a set of functions is said to be mutually recursive .
In particular, in Program 14.6, ParseP calls ParseE , which calls ParseT ,

Table of Contents continued


which calls ParseF , which calls ParseP in a cyclic fashion. In symbols,
we could write: ParseP ParseE ParseT ParseF ParseP , where the
symbol means calls.
3. Rightmost derivations and leftmost parses are inverses in the
sense that when a rightmost derivation (in which rightmost nonterminals are replaced by right sides of productions) is read
backwards, it gives a plan for replacing a leftmost subexpression,
which is equal to the right side of some production, with the nonterminal on the left side of that production yielding an instance
of a leftmost parse.

Solutions to Selected Exercises in Exercises 14.3


1. A C module ParserUtilities.c , which implements the low-level utility
calls in Program 14.6 is given as follows:

10

15

20

25

30

35

40

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

/*
* Ex. 14.3.1 the "ParserUtilities.c" module
*
* This module exports parser utility services. It uses the same sequential
* stack representation as is used for Program 7.5, the parenthesis matching
* program in Chapter 7.
*/
#include "ParserUtilities.h"
/******
*
* The ParserUtilities.h file contains the following external declarations:
*
*****/
/*
#include
#include
#include
#include
#include

<stdio.h>
<stdlib.h>
<string.h>
<ctype.h>
"SeqStackInterface.h"

/+ The last line above includes the following from +/


/+ the "SeqStackInterface.h" file: +/
typedef StackType Stack;

/+ the StackType will depend on the +/


/+ representation +/
extern void InitializeStack (Stack *S);
/+ Initialize the stack S to be the empty stack +/
extern int Empty (Stack *S);
/+ Returns TRUE if and only if the stack S is empty +/
extern int Full (Stack *S);
/+ Returns TRUE if and only if the stack S is full +/
extern void Push (ItemType X, Stack *S);
/+ If S is not full push a new item X onto the top of stack S +/

Table of Contents continued

45

50

55

60

65

70

75

80

85

90

95

100

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

extern void Pop (Stack *S, ItemType *X);


/+ If S is non-empty, pop an item off the top of the stack S +/
/+ and put it in X +/
/+ -------------------< a typedef exported by the module >----------------------- +/
extern typedef enum {false, true} Boolean;
/+ ---------------< the only variable exported by the module >------------------- +/
extern char Next;

/+ the next character in the input string +/

/+ --------------< the four procedures exported by the module >----------------- +/


extern void GetInputStringFromUser(void);
extern void Reduce(int n, char S);
extern void Shift(char c);

/+ gets the Input +/


/+ string from the user +/

/+ pops n items off Stack, +/


/+ then pushes S on Stack +/
/+ reads c from Input and +/
/+ pushes c on Stack.+/

extern Boolean UserWantsToContinue(void);


/+ returns "true" iff +/
/+ user wants to continue.+/
/+ ------------------------------------------------------------------------------- +/
*/
/* ----<< the implementation part of the module >>---- */
/* --------------------< variables private to the module>------------------------- */
char

Next;

/* the next character in the input string */

char
*Answer = "blank",
/* a reply from the user */
*InputString
/* the input string */
= "a blank input string long enough for an expression";
Stack

InputStack, ParseStack;

/* -------------------< functions defined in the module >------------------------ */


void GetInputStringFromUser(void)
{
int i;
InitializeStack(&ParseStack);
InitializeStack(&InputStack);
Push('#',&InputStack);
printf("give input string: ");
gets(InputString);

/* gets an Input */
/* string from user to parse */
/* initialize the ParseStack */
/ to the empty stack */
/* initialize the InputStack */
/* to the empty stack */
/* put a 'pad' character */
/* at bottom of InputStack */
/* scanf("%s",InputString); */

for (i = strlen(InputString) 1; i >= 0; i) {


Push(InputString[i],&InputStack);
}
Next = '#';

Table of Contents continued

105

110

115

120

125

130

135

140

145

150

155

160

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

Shift('#');

/* get things started */

}
/* ------------------------------------------------------------------------------- */
char FetchNext(void)
{
char d, f;
d = Next;

/* returns next non-blank */


/* character in the Input */

/* find the next non-space character in the Input */

do {

/* find the next non-space character in the Input */


Pop(&InputStack, &f);
} while (f == ' ');
Next = f;

/* let Next be this next non-space */


/* character in the Input */

return d;

/* return d as the value of the function */

}
/* ------------------------------------------------------------------------------- */
void PrintErrorMessageIfNecessary(char c, char d)
{
/* if d isn't equal to the expected character c, write an error message. */
if (c == 'a') {

/* recall that 'a' stands for any <letter> or <digit> */

if (! (islower(d) | | isdigit(d)) ) {
printf("Syntax Error: expected <letter> or <digit> but got %c\n", d);
}
} else if (c != d) {
printf("Syntax Error: expected %c but got %c\n", c, d);
}
}
/* ------------------------------------------------------------------------------- */
void Shift(char c)
{
char d;
/* d holds the character Shifted from the Input string */
d = FetchNext( );

/* read the next character d from */


/* the Input string */
Push(d,&ParseStack);
/* push d on top of ParseStack */
PrintErrorMessageIfNecessary(c,d);
/* if c != d then */
/* print Shift error message */
}
/* ------------------------------------------------------------------------------- */
void Reduce(int n, char S)
{
int i; char *Output;

/* pop n items from Stack, push S */

/* write the reduction: top n items of stack > S, and */


/* pop n chars off stack. */

Table of Contents continued


165

170

175

180

185

190

195

200

205

210

215

220

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

Output = "

";

Output[2*n] = '\0';
for (i = n 1; i >= 0; i) {
Pop(&ParseStack,&Output[2*i]);
Output[2*i+1] = ' ';
}
if (n == 1) {
if (S == 'P') {
/* PFstring = Concat(PFString,Output); */
printf(" %c >

{ where a = %s}\n", S, Output);


/* later for use in Program 14.8 */
/* add," %s", . . . , PFString) */

} else {
printf(" %c >

%s\n",S,Output);

}
} else {
printf(" %c > %s\n", S, Output);
/* later for use in Program 14.8, add:
if ((n == 3) && (S != 'P') ) {
PFstring = Concat(PFstring, Output[3],' ');
printf("

%s\n", PFString);

} else {
putchar('\n');
}
*/
}
/* end if */
/* push S on stack */
Push(S,&ParseStack);
}
/* end Reduce */
/* ------------------------------------------------------------------------------- */
int UserWantsToContinue(void)
{
printf("Do you want to give another string? (y/n) ");
gets(Answer);
/* scanf("%s",Answer); */
return (Answer[0] == 'y');
}
/* -----------------------------< end of ParserUtilities.c >----------------------------- */

Table of Contents continued


2. The productions for G 1 are {E 0 E 0, E 1 E 1, E 2}. The
corresponding railroad diagram for E is:

A function for a recursive-descent Parser for E is:

10

15

20

25

30

35

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

/*
* Solution to Ex.14.3.2 -- Marked Palindrome Parser
*/
#include "ParserUtilities.h"
extern char Next;

/* the next character in the input string */

/* ------------------------------------------------------------------------------- */
void ParseE(void)
/* parse a marked palindrome */
{
if (Next == '2') {
Shift('2');
Reduce(1,'E');
} else if (Next == '0') {
Shift('0');
ParseE( );
Shift('0');
Reduce(3,'E');
} else if (Next == '1') {
Shift('1');
ParseE( );
Shift('1');
Reduce(3,'E');
}
}
/* ------------------------------------------------------------------------------- */
int main(void)
{
do {
GetInputStringFromUser( );
ParseE( );
/* call the parser to parse the input string. */
} while ( UserWantsToContinue( ) );
}

Table of Contents continued


3. It is not possible to write a left-to-right, shift-reduce, recursive
descent parser with a finite lookahead that parses unmarked
palindromes in the language generated by the grammar G2 .
4

G3 = { E 1 1 0, E 1 1 E 0 }.

5. The grammar cannot generate any sentences, which are sentential


forms containing only terminal symbols, because every sentential
form generated by the grammar contains exactly one non-terminal
symbol. Hence L(G) = .

Answers to Selected Review Questions 14.4


1. The infix-to-postfix translator given in Program 14.8 is a modified
left-to-right, shift-reduce parser for infix expressions that has
been extended to produce postfix output. There is an initially
empty postfix output string stored as the value of the variable
PostfixString and there is a procedure AppendToOutput(x) , which concatenates the string x onto the right end of the PostfixString . Each of the
original procedures P a r s e P , P a r s e F , P a r s e T , and P a r s e E , is a selfcontained unit of action which parses respectively, an instance of a
P, F, T, or E, in the input string, and appends its respective postfix
translation P , F , T , or E , to the output stored as the value of
PostfixString . In so doing, each parsing routine may make calls on the
others and may depend on their output as units of action used to
accomplish its own task. For example, ParseT may try to parse a T*F
as a T and may call P a r s e F first to parse the initial T which is
grammatically forced to become an F in T*F. and may then call
P a r s e F again to parse the rightmost F in T*F. However, before
calling P a r s e F the second time, the multiplication operator (*) is
saved as the value of the local variable Operator in ParseT . Then after
P a r s e F has been called twice, the multiplication operator is
appended to the end of PostfixString, using AppendToOutput(Operator) . The
result is to append T F * onto the end of the PostfixString as the
postfix translation of the infix T*F (where T is the postfix
translation of T obtained by calling ParseF and reducing its parser
output F to a T, and where F is the postfix translation of F obtained
by calling Parse F).
2. The main theme used in the infix-to-postfix translator is: To
translate an expression containing a binary infix operator into
postfix, first parse the infix subexpression for the left operand
and append its postfix translation to the end of the output string,
save the binary operator, parse the infix subexpression for the
right operand and append its postfix translation to the end of the
output string, and finally, append the operator to the end of the
output string. The translations of the left and right

Table of Contents continued


subexpressions are performed by making appropriate calls on
functions to parse and translate the expected types of operand
subexpressions.
3. Yes. For instance, ParseE calls Parse T, which calls ParseF , which calls
ParseP , which calls ParseE in a cycle. Hence a call to any of ParseE ,
ParseT , ParseF , or ParseP can generate an indirect recursive call to
itself via a chain of intermediate calls to the others.

Solutions to Selected Exercises in Exercises 14.4


1. The solutions to Exs. 14.4.1 and 14.4.2 are contained in the
following program:

10

15

20

25

30

35

40

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

/****
*
* Exs 14.4.1 & 14.4.2 -- Augmented Infix-to-Postfix translator & evaluator
*
-- which handles unary minus
*
***/
/****
*
* To complete the module "YourCalculationModule" of Ex. 14.4.2 add a
* headerfile "YourCalculationModule.h" to the current ".c" file containing
* the extern declaration:
*
*
extern char *Evaluate(char *);
*
* as shown in Program 4.12 on p. 109
*
***/
/****
*
* This program augments the Recursive Descent Parser of Program 14.6
* to handle unary minus operators according to the grammar of Ex 14.2.2
* shown immediately below, translates infix to postfix, evaluates the
* postfix, and prints the string representing the floating point value.
*
*
E --> E + T | E T | T
*
T --> T * F | T / F | F
*
F --> F ^ U | U
*
U --> P | + P | P
*
P --> ( E ) | a
*
* Here, we let 'a' stand for any letter or digit:
*
*
a --> <letter> | <digit>
*
* The grammar above uses E, T, F, and P to stand for the following
* "standard" abbreviations in the literature:
*
* E = Expression, T = Term, F = Factor, and P = Primary.
*
* In addition, it uses U to stand for a Unary operand.
*
****/

Table of Contents continued


45

50

55

60

65

70

75

80

85

90

95

100

105

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

#include "ParserUtilities.h"
#include "EvalStackInterface.h"
/* -< The ParserUtilities.h file contains the following external declarations >- */
/*
#include
#include
#include
#include
#include

<stdio.h>
<stdlib.h>
<math.h>
<ctype.h>
<string.h>

/+ ---------------< the four procedures exported by the module >---------------- +/


extern void GetInputStringFromUser(void);
extern void Reduce(int n, char *S);
extern void Shift(char *c);

/+ gets the Input string +/


/+ from the user
+/
/+ pops n items off Stack, +/
/+ then pushes S on Stack +/
/+ reads c from Input and +/
/+ pushes c on Stack
+/

extern Boolean UserWantsToContinue(void); /+ returns "true" iff user +/


/+ wants to continue
+/
extern void AppendToOutput(char X);

/+ appends X to postfix
/+ output string

+/
+/

/+ ----------------------------------------------------------------------------- +/
*/
/* ------< three external variables imported from ParserUtilities.c >--------- */
extern char Next;

/* the next character in the input string */

extern char *PostfixString;


extern char *InputString;

/* the postfix output string */


/* the user-supplied input string */

/* ---< an external variable imported from EvalStackImplementation.c >--- */


extern Stack EvalStack;

/* where ItemType = float */

/* --------------------< a global variable for this module >--------------------- */


char *ResultString

= "00000000000000000000";
/* the string */
/* for the value of the output */

/* -------------< the internal functions of the module begin here >--------------- */


extern void ParseE(void);

/* extern because function */


/* called before defined */

/* ------------------------------------------------------------------------------- */

Table of Contents continued

110

115

120

125

130

135

140

145

150

155

160

165

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

void ParseP(void)
{
char Operand;

/* parse a Primary */

if (Next == '(') {
Shift('(');
ParseE( );
Shift(')');
Reduce(3,'P');
} else {
Operand = Next;
Shift('a');
/* 'a' stands for any <letter> or <digit> */
Reduce(1,'P');
AppendToOutput(Operand);
}
}
/* ------------------------------------------------------------------------------- */
void ParseU(void)
{
char Operator;
if ( (Next == '+') || (Next == '') ) {
Operator = Next;
Shift(Next);
ParseP( );
Reduce(2,'U');
if (Operator == '') {
AppendToOutput('~');
}
} else {
ParseP( );
Reduce(1,'U');
}

/* parse a Unary */

/* appends unary minus */


/* operator */

}
/* ------------------------------------------------------------------------------- */
void ParseF(void)
{
char Operator;

/* parse a Factor */

ParseU( );
Reduce(1,'F');
while (Next == '^') {
Operator = Next;
Shift(Next);
ParseU( );
Reduce(3,'F');
AppendToOutput(Operator);
}
}
/* ------------------------------------------------------------------------------- */
void ParseT(void)
{
char Operator;
ParseF( );
Reduce(1,'T');

/* parse a Term */

Table of Contents continued


170

175

180

185

190

195

200

205

210

215

220

225

230

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

while ( (Next == '*') || (Next == '/') ) {


Operator = Next;
Shift(Next);
ParseF( );
Reduce(3,'T');
AppendToOutput(Operator);
}
}
/* ------------------------------------------------------------------------------- */
void ParseE(void)
{
char Operator;

/* parse an Expression */

ParseT( );
Reduce(1,'E');
while ( (Next == '+') || (Next == '') ) {
Operator = Next;
Shift(Next);
ParseT( );
Reduce(3,'E');
AppendToOutput(Operator);
}
}
/* ------------------------------------------------------------------------------- */
void InterpretPostfix(void)
{
float LeftOperand, RightOperand, Result;
int i;

/* the index of the ith character in the PostfixString */

char c;

/* c = ith character of the input string */

char *s = "x";

/* s will hold a null-terminated string in which */


/* s[0] will hold c, for use in an atof conversion */

InitializeStack(&EvalStack);
for (i = 0; i < strlen(PostfixString); ++i) {
s[0] = c = PostfixString[i];
if (isdigit(c)) {

/* s[0] = c = ith character of */


/* input string */
/* if c is a digit, push c's value onto stack */

if ( Full(&EvalStack) ) {
printf("Stack full. Postfix Evaluation could not be completed.\n");
return;
} else {
Push((float)atof(s),&EvalStack);
}
} else if (c=='~') {

/* handle case of unary minus operator */

if ( Empty(&EvalStack) ) {
printf("Malformed postfix input string. Too many operators\n");
printf("and too few operands.\n");
return;
} else {
Pop(&EvalStack,&RightOperand);

Table of Contents continued


|
Push( RightOperand, &EvalStack);
|
}
|
|
235
|
} else if (c=='+' || c=='' || c=='*' || c=='/' || c=='^') {
|
|
if ( Empty(&EvalStack) ) {
|
printf("Malformed postfix input string. Too many operators\n");
|
printf("and too few operands.\n");
240
|
return;
|
} else {
|
Pop(&EvalStack,&RightOperand);
|
if ( Empty(&EvalStack) ) {
|
printf("Malformed postfix input string. Too many\n");
245
|
printf("operators and too few operands.\n");
|
return;
|
} else {
|
Pop(&EvalStack, &LeftOperand);
|
250
|
switch (c) {
/* perform the operation */
|
|
case '+': Push(LeftOperand+RightOperand,&EvalStack);
|
break;
|
case '': Push(LeftOperandRightOperand,&EvalStack);
255
|
break;
|
case '*': Push(LeftOperand*RightOperand,&EvalStack);
|
break;
|
case '/': if (RightOperand != 0.0) {
|
Push(LeftOperand/RightOperand,&EvalStack);
260
|
} else {
|
printf("Attempt to divide by zero.\n");
|
return;
|
}
|
break;
265
|
case '^': Push(exp(log(LeftOperand)*RightOperand),
|
&EvalStack);
|
break;
|
default: break;
|
}
270
|
}
|
}
|
} else {
|
printf("Illegal character '%c' in postfix expression.\n",c);
|
return;
275
|
}
|
} /* end for */
|
|
|
Pop(&EvalStack,&Result);
/* remove final result from stack */
280
|
|
if ( Empty(&EvalStack) ) {
|
sprintf(ResultString,"%f",Result);
/*convert float result */
|
} else {
/* to string output */
|
printf("Malformed postfix string.\n");
285
|
printf("Too many operands and not enough operators.\n");
|
}
| }
|
| /* -----< the following function is exported by YourCalculationModule >----- */
290
|
|
char *Evaluate(char *S)

Table of Contents continued

295

300

305

310

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

{
strcpy(InputString,S);
ParseE();
/* call the parser to parse the input string. */
InterpretPostfix();
return ResultString;
}
/* --< the main function tests the operation of YourCalculationModule >-- */
int main(void)
{
do {
PostfixString[0] = '\0';
/* initialize PostfixString to empty string */
GetInputStringFromUser( );
ResultString = Evaluate(InputString);
printf("output = %s\n",ResultString);
putchar('\n');
} while ( UserWantsToContinue( ) );
}
/* end main program */

Answers to Review Questions 14.5


1. The first is : To prove P(n ) is true for all non-negative integers n
0, (1) First, prove P(0) is true, and then, (2) Assuming P( n ) is true
for n , prove P(n +1) is true.
The second is: To prove P(n ) is true for all integers n 0, (1) First,
prove P(0) is true, and then, (2) Assuming P(k ) is true for 0 k < n ,
prove P(n ) is true.
2. Recursion induction is a method that can be used to prove that
recursive programs correctly produce their intended results by
first proving that base cases are correct, and then, on the
assumption that recursive calls correctly solve smaller
subproblems and eventually result in calls to base cases, by
proving that the recursive programs themselves correctly
produce their intended results.
3. First, for the m base cases of sizes n 1 , n 2 , ... , n m , prove F(t 1 ), F(t 2 ),
... , F(t m ) satisfy P(n 1 ), P(n 2 ), ... , P(n m ) respectively. Then, on the
assumption that F( t ) calls itself recursively with calls F( t j ) on
problems of smaller size n j (eventually leading to base cases),
show that F(t ) produces results that enable you to prove that P(n )
is true.

Solutions to Selected Exercises in Exercises 14.5


1. When you attempt to apply recursion induction to
f ( n ) = 1, you encounter the difficulty that recursive
within f sometimes make calls on subproblems of larger
the cases where n is odd when f ( n ) calls f ( 3 * n +1).

prove that
calls on f
size, as in
Thus, the

Table of Contents continued


assumption that all calls on subproblems of smaller size yield
correct results is of no use in constructing the proof in cases
where the subproblems in the recursive calls are of larger size
because, in such cases, this assumption doesnt apply.
2. The Partition algorithm given as Program 13.15 does not always
meet the requirement that i == j+1 after the call to Partition(A, &i, &j) .
Hence, a redesigned partition algorithm, such as that given below,
must be used, which guarantees that A[m:n] is partitioned into A[m:j]
and A[i:n] where i == j+1 and m j < n after returning from the call
Partition(A, m, n) and after setting i and j appropriately. On that
assumption, we turn to the proof that the Find function given below
correctly moves the k th item of A[m:n] into the k th position, where 1
k nm+1 initially.
For the base case , suppose k == 1 and m == n. Then the call
does nothing to rearrange A (because the condition m < n
on line 28 is false) and the k th (i.e., the first) item of A[m:n] = A[m].
Find(A,k,m,n)

Now suppose A[m:n] contains more than one item, so that m < n, and
suppose, by recursion induction, that Find(A,k,m,n) works correctly
for all arrays with fewer than nm+1 items.
The partition algorithm is called with Partition(A, m, n) to partition
A[m:n] into a left partition A[m:j] and a right partition A[i:n] , where i ==
j+1 and m j < n. Lines 30:36 of the Find function below set i and j
appropriately to ensure that these conditions are met. The
elements in the left partition will be less than or equal to the
smallest element in the right partition after returning from the
partition function call. If now, k (jm+1), it means that there are at
least k elements in the left partition (recalling that the number of
elements in A [ m : j ] is j m + 1 according to Eq. A.5 of the Math
Reference appendix). Since all the items in the left partition are
smaller than any of the items in the right partition and there are k
or more items in the left partition, the k th smallest item in A[m:n] is
found by taking the k th smallest item in the left partition, which, by
recursion induction, is correctly found by recursively calling
F i n d ( A , k , m , j ) on line 39 of the F i n d function given below. (It is
guaranteed that A[m:j] is a smaller array than A[m:n] because j < n after
the call to Partition(A,m,n) and after the computations to set i and j on
lines 30:36 of Find below. Hence, the recursion induction condition
applies in this case.)
Now suppose that, after returning from the call Partition(A,m,n) and
after the computations to set i and j on lines 30:36 of Find below, it
is false that k (jm+1) , meaning that there are fewer than k
elements in A[m:j] . Then because all jm+1 items in A[m:j] are less than
or equal to each of the items in A[i:n] , the k th smallest item in A[m:n] is

Table of Contents continued


not to be found among the items in the left partition but instead is
to be found among the items in the right partition. Because (jm+1)
of the smallest items of the original A[m:n] are already in the left
partition, the k th smallest item of A[m:n] is obtained by finding the k
( j m + 1 ) th item of the right partition A[i:n] where i == j+1 . Again by
recursion induction, since A[i:n] contains fewer items than A[m:n] , the
call Find(A,k(jm+1),i,n) on line 41 correctly finds the k(jm+1) th item of
A[i:n] which is the same as the k th smallest item of A[m:n] .

10

15

20

25

30

35

40

45

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

/*--------------------------------------------------------------------------*/
int Partition(ItemArray A, int i, int j)
/* assume i < j */
{
ItemType Pivot, Temp;
/* the value returned by the Partition */
int k, middle, p;
/* function is the location of the Pivot */
/* after partitioning */
middle = (i+j)/2;
Pivot = A[middle]; A[middle] = A[i]; A[i] = Pivot;
p = i;
for (k= i+1; k <= j; ++k) {
if (A[k] < Pivot) {
Temp = A[++p]; A[p] = A[k]; A[k] = Temp;
}
}
Temp = A[i]; A[i] = A[p]; A[p] = Temp;
return p;
}
/*--------------------------------------------------------------------------*/
void Find(ItemArray A, int k, int m, int n)
/* to move the kth smallest */
{
/* item in A[m:n] into the kth position */
int i, j, p;
if (m < n) {

/* if there is more than one item in A to partition */

p = Partition(A, m, n); /* p == location of the Pivot after partition */


if (p == n) {
j = p1; i = p;
} else {
j = p; i = p+1;
}

/* ensure that the partition is of the form */


/* A[m:j] and A[i:n], where i == j+1 */
/* and ensure that mj<n */

if ( k <= (j m + 1) ) {
/* if A[m:j] has at least k items, then */
Find(A, k, m, j);
/* Find kth in A[m:j] */
} else {
/* otherwise, */
Find(A, k(jm+1), i, n);
/* Find k(jm+1)th in A[i:n] */
}
}
}
/*--------------------------------------------------------------------------*/

Table of Contents continued


3. Assume A [ 0 : n 1 ] is an array of n distinct objects, and let the
recursion induction hypothesis for calling P e r m ( A , m , n ) be the
following:
prints all distinct permutations of A[0:n1] with
fixed and A[0:m1] varying.

Perm(A,m,n)
A[m:n1]

For the base case , m == 0, we need to show that Perm(A,0,n) prints all
permutations of A[0:n1] with A[0:n1] fixed and A[0:1] varying. The
range 0:1 is empty (because by definition i:j == { k | i k j}, so 0:1 == { k
| 0 k 1} == ). Hence there is only one permutation of A[0:n1] with
A[0:n1] fixed and A[0:1] == varying, and it is obtained by printing
A[0:n1] . But, when m == 0, Perm(A,0,n) calls PrintPerm(A,n) which simply
prints the items in A [ 0 : n 1 ] (see lines 13:21 and 38:39 of the
program below). Hence, for m == 0, Perm(A,m,n) prints A[0:n1] which is
what we needed to show.
Now assume that Perm(A,k,n) prints all the permutations with A[k:n
fixed and A[0:k1] varying for all k < n. In Perm(A,m,n) , when m == n, for
each i in 0:n1 Perm(A,n,n) exchanges A[i] with A[n1] , then calls Perm(A,n
1,n) to print all permutations of A[0:n1] with A[n1] fixed and A[0:n2]
varying, and exchanges A[n1] and A[i] back again. The effect of this
is to choose each of the distinct objects in A [ 0 : n 1 ] as the fixed
object A [ n 1 ] while printing all permutations with the remaining
A [ 0 : n 2 ] objects varying (which must occur correctly by the
recursion induction hypothesis using the recursive call Perm(A,n1,n)
on a problem of size k == n1 where k < n).
1]

Therefore, calling Perm(A,n,n) prints all distinct permutations of


A[0:n1].

10

15

20

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

/*------------------------------------------------------------*/
void Exchange(ItemArray A, int i, int j)
{
ItemType temp;
temp = A[i]; A[i] = A[j]; A[j] = temp;
}
/*------------------------------------------------------------*/
void PrintPerm(ItemArray A, int n)
{
int i;
for (i = 0; i < n; ++i) {
printf("%2d, ",A[i]);
}
putchar('\n');
}
/*------------------------------------------------------------*/

Table of Contents continued


25

30

35

40

45

50

55

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

void InitPermArray(ItemArray A, int n)


{
int i;
for (i=0; i<n; ++i) A[i]=i+1;
}
/*------------------------------------------------------------*/
void Perm(ItemArray A, int m, int n)
{
int i;
if (m == 0) {
PrintPerm(A, n);
} else {
for (i = 0; i < m; ++i) {
Exchange(A, i, m1);
Perm(A, m1, n);
Exchange(A, m1, i);
}
}
}
/*------------------------------------------------------------*/
void Permute(ItemArray A, int n)
{
InitPermArray(A,n);
Perm(A,n,n);
}
/*------------------------------------------------------------*/

Table of Contents continued

Answers to Review Questions 15.2


1. If theTObject is a variable which can contain a pointer to an instance
of an object of class TClass , we perform the assignment:
theTObject = new TClass;

to create an instance of an object of type TClass and to assign it to


be the value of the variable theTObject . Recall that new X allocates an
instance of a new object of class X in the heap and returns a
pointer to that object instance.
2. An object instance is analogous to a struct in C which has two kinds
of members: data members and function members. The data
members are called the objects instance variables and contain
data values local to the object instance. The function members,
which are called the objects m e t h o d s , are local operators that
apply to the object instance and that can manipulate values of its
instance variables.
3. In C++ it is possible to define a class C 2 as a derived class of a
base class C1 . When you do this, the object instances of class C 2
inherit the instance variables and methods of the base class C 1 .
Inheritance means that every instance of an object of class C 2 has
all the instance variables and methods defined for objects that are
instances of the base class C1 . When you customize an object class
C 2 , you define some features local to C 2 that differ from the
features inherited from the base class C 1 . In effect, this allows you
to program using differences by saying that the object instances
in the derived class C 2 are exactly like those in C 1 except that
they have some local differences (i.e., local instance variables and
local methods) defined by customization. Sometimes a base class
is set up as an abstract class that will never have any actual object
instances. New derived classes are formed as subclasses of this
abstract base class by customization. For example, a class T S h a p e
could be defined as an abstract base class having a virtual Draw( )
method. New classes, such as TRectangle , TSquare , TCircle and TOval
could be defined as derived classes of the class TShape each having
its own actual distinct Draw( ) method. Nobody will ever create an
actual instance of the abstract class T S h a p e , although individual
instances of each of the derived classes, TRectangle , TSquare , TCircle
and T O v a l , will be created and will be drawn. The virtual method
Draw( ) defined for the class T S h a p e functions, so-to-speak, as a
placeholder method that gives a common method name to
customize, and thus to share, among the identically-named Draw( )
methods of derived classes having particular shapes that can
actually be drawn. Supplying actual executable Draw( ) methods for

Table of Contents continued


each derived class customizes the virtual Draw( ) m e t h o d of the
abstract class TShape . A variable S of type TShape can have, as its
value, an object of any of the derived classes of actual shapes,
TRectangle , TSquare , TCircle and TOval . When the Draw( ) message is sent
to the object that is the value of S , the individual customized D r a w (
) method of the object that is the current value of S is executed
and draws the individual shape in a fashion appropriately defined
for its particular derived class. Thus, for example, sending the
Draw( ) message to each object on a list of individual shapes
derived from the abstract class TShape causes the list of individual
object instances to be drawn each according to its own customized
Draw( ) method.
4. In C++ the keyword this is a variable whose value points to the
object to which the current method is being applied.
5. In C++, executing X = new TObject; allocates space in dynamic memory
for an object instance of type T O b j e c t , returns a pointer to the
storage for this object instance, and stores the pointer as the
value of the variable X . Executing the statement delete X; deletes
the space for the object instance pointed to by the pointer value
of the variable X and returns the space to the pool of available
dynamic memory in the heap.

Solutions to Selected Exercises in Exercises 15.2


1. The solution is given by the function UserDesignatesShape on lines
22:69 of Program 15.21 (on pages 635:636).
2. The solution is given by the following program which designates
moveable lists of hollow or filled shapes in which the filled shapes
can be filled by LtGray , CrossHatch or HorizontalLines patterns:

10

15

20

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

/////
//
/ / Ex. 15.2.2 -- Extension of Third Stage Drawing Program to use
//
three distinct types of fill patterns for filled shapes
//
/////
#include
#include
#include
#include
#include
#include

<stdio.h>
<string.h>
<oops.h>
"windowUtils.h"
"Shapes.h"
"ShapeList.h"

// see Shapes.h and Shapes.c below


// see ShapeList.h and ShapeList.c below

// Global variable that contains the shape list


TList *theShapeList;
// Prompts user to give (deltaX, deltaY) move increments and

Table of Contents continued

25

30

35

40

45

50

55

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

// calls Move method for theShapeList to move the entire list


// of shapes.
void MoveShapes( )
{
short deltaX, deltaY;
if (theShapeList>IsNonEmpty( )) {
fflush(stdin);
printf("deltaX deltaY: ");
scanf("%hd%hd", &deltaX,&deltaY);
fflush(stdin);
theShapeList>Move(deltaX,deltaY);
theShapeList>Draw( );
}
}
//////
//
// UserDesignatesShape prompts the user for a shape type,
// creates a pointer to a shape instance of that type,
// and returns the shape pointer.
//
//////
static Boolean UserDesignatesShape (TShape **theShape)
{
char
*reply = "
";
int
ReplyLength;
while (1) {
printf("Designate one of: Q = Quit; M = Move shapes, HO = Hollow Oval,\n");
printf("FO = Filled Oval, HR = Hollow Rectangle, FR = Filled Rectangle,\n");
printf("HRR = Hollow Rounded Rectangle, FRR = Filled Rounded Rectangle:

");

60

65

70

75

80

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

fflush(stdin);
gets(reply);
ReplyLength = strlen(reply);
if ((reply[0] == 'Q') || (reply[0] == 'q')) {
return false;
} else if ((reply[0] == 'M') || (reply[0] == 'm')) {
MoveShapes( );
*theShape = NULL;
return true;

// stay in the loop

} else {
switch(reply[1]) {
case 'O':
case 'o':
if ((reply[0] == 'H') || (reply[0] == 'h')) {
*theShape = new TOval;
} else {
*theShape = new TFilledOval;
(*theShape)>GetItsPattern( );
}
return true;
// stay in the loop

Table of Contents continued


|
|
case 'R':
|
case 'r':
if (ReplyLength < 3) {
85
|
if ((reply[0] == 'H') || (reply[0] == 'h')) {
|
*theShape = new TRectangle;
|
} else {
|
*theShape = new TFilledRectangle;
|
(*theShape)>GetItsPattern( );
90
|
}
|
} else {
|
if ((reply[0] == 'H') || (reply[0] == 'h')) {
|
*theShape = new TRoundedRectangle;
|
} else {
95
|
*theShape
=
new
TFilledRoundedRectangle;
|
(*theShape)>GetItsPattern( );
|
}
|
}
|
return true;
// stay in the loop
100
|
default:
*theShape = NULL;
|
return true;
// stay in the loop
|
} // end switch
|
|
} // end if
105
|
|
} // end while
|
| } // end UserDesignatesShape
|
110
| //////
| //
| // The main( ) function initializes the system, prompts the user
| // for instructions to create, draw, and move shapes and shapelists,
| // and terminates the session when the user designates the "quit"
115
| // instruction code.
| //
| //////
|
| int main( void)
120
| {
|
TShape
*theShape;
|
short
x1, y1, x2, y2;
|
|
SetUpWindows( );
125
|
DrawCoordinateSystem( );
|
|
theShapeList = new TList;
|
theShapeList>InitShapeList( );
|
130
|
while (UserDesignatesShape(&theShape)) {
|
|
if (theShape != NULL) {
|
|
printf("left top right bottom: ");
135
|
scanf("%hd%hd%hd%hd", &x1,&y1,&x2,&y2);
|
fflush(stdin);
|
|
theShape>SetEnclosingRect(x1, y1, x2, y2);
|
theShape>Draw( );
140
|
theShapeList>Append(theShape);
|
}
|
}

Table of Contents continued

145

|
|
|
|

return(0);
}

The main program above for Ex 15.2.2 uses the Shapes and ShapeList
modules defined below. We first give the S h a p e s . h header file
followed by the Shapes.c source file.
| //////
| //
| // The Shapes.h header file defines the Shape class hierarchy
| //
5
| //////
|
|
| class TShape {
// the abstract class of which all
|
// actual Shape classes are derivatives
10
|
public:
|
|
// The SetEnclosingRect method sets (left, top, right, bottom)
|
// values for the EnclosingRectangle.
|
15
|
void SetEnclosingRect (short x1, short y1, short x2, short y2);
|
|
void Move(int deltaX, int deltaY);
|
|
void GetItsPattern(void);
20
|
|
// The Draw( ) method draws the shape. All derived classes
|
// override it. At this abstract class level it is virtual,
|
// in order to behave as a place-holder that expects to be
|
// overridden by local customized methods in each derivative
25
|
// subclass.
|
|
virtual void Draw( );
|
|
protected:
30
|
|
Rect
EnclosingRectangle;
// The rectangle that encloses
|
// the shape
|
PatPtr itsPattern;
// The fill pattern for the shape
|
// (if applicable)
35
|
| };
|
| class TRectangle : public TShape {
|
40
|
public:
|
void Draw( );
| };
|
| class TOval : public TShape {
45
|
|
public:
|
void Draw( );
| };
|
50
| class TRoundedRectangle : public TShape {

Table of Contents continued

55

60

65

70

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

public:
void Draw( );
};
class TFilledRectangle : public TRectangle {
public:
void Draw( );
};
class TFilledOval : public TOval {
public:
void Draw( );
};
class TFilledRoundedRectangle : public TRoundedRectangle {
public:
void Draw( );
};

The source file Shapes.c follows:

10

15

20

25

30

35

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

//////
//
// Shapes.c contains the implementations of TShape methods
//
//////
#include
#include
#include
#include
#include

<oops.h>
<stdio.h>
<string.h>
"windowUtils.h"
"Shapes.h"

extern WindowPtr DrawingWindow;


Pattern HorizontalLines
Pattern CrossHatch

// The drawing window is


// defined in windowUtils.c

= {0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00};


= {0x88, 0x11, 0x22, 0x44, 0x88, 0x11, 0x22, 0x44};

void TShape : : SetEnclosingRect (short x1, short y1, short x2, short y2)
{
EnclosingRectangle.top
= 100 y1;
EnclosingRectangle.left
= 150 + x1;
EnclosingRectangle.bottom = 100 y2;
EnclosingRectangle.right
= 150 + x2;
}
void TShape : : Move(int deltaX, int deltaY)
{
EnclosingRectangle.top
= deltaY;
EnclosingRectangle.left
+= deltaX;
EnclosingRectangle.bottom = deltaY;
EnclosingRectangle.right
+= deltaX;
}
void TShape : : GetItsPattern(void)

Table of Contents continued

40

45

50

55

60

65

70

75

80

85

90

95

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

{
char
int

*reply = "
";
ReplyLength;

printf("Designate one of: G = LtGray, C = CrossHatch, \n");


printf("
H = Horizontal Lines : ");
fflush(stdin);
gets(reply);
ReplyLength = strlen(reply);
switch (reply[0]) {
case 'G':
itsPattern = &ltGray;
break;
case 'C':
itsPattern = &CrossHatch;
break;
case 'H':
itsPattern = &HorizontalLines;
break;
default:
itsPattern = &ltGray;
break;
}
}
// end of GetItsPattern
// The Draw( ) method is a virtual method that should be
// overridden by individual customized Draw( ) methods in
// each derivative Shape subclass
void TShape : : Draw ( )
{
SetPort(DrawingWindow);
}
// The TRectangle : : Draw( ) method calls its parental Draw( ) function
// to set the port to the drawing window and then draws a wire frame
// shape for a rectangle.
void TRectangle : : Draw ( )
{
TShape : : Draw( );
FrameRect(&EnclosingRectangle);
}
// TOval : : Draw
void TOval : : Draw ( )
{
TShape : : Draw( );
FrameOval(&EnclosingRectangle);
}
// TRoundedRectangle : : Draw
void TRoundedRectangle : : Draw ( )
{
TShape : : Draw( );
FrameRoundRect(&EnclosingRectangle,20,20);
}

Table of Contents continued


100

105

110

115

120

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

// TFilledRectangle : : Draw
void TFilledRectangle : : Draw ( )
{
FillRect(&EnclosingRectangle,*itsPattern);
TRectangle : : Draw( );
}
// TFilledOval : : Draw
void TFilledOval : : Draw ( )
{
FillOval(&EnclosingRectangle,*itsPattern);
TOval : : Draw( );
}
// TFilledRoundedRectangle : : Draw
void TFilledRoundedRectangle : : Draw ( )
{
FillRoundRect(&EnclosingRectangle,20,20,*itsPattern);
TRoundedRectangle : : Draw( );
}

The main program above for Ex 15.2.2 uses the ShapeList module
defined below. The header file ShapeList.h is given first, followed by
the source file ShapeList.c .

10

15

20

25

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

//////
//
// ShapeList.h
//
//////
// define the Shape List base class
class TList
{
public:
void InitShapeList( );
void Append(TShape *theShape);
void Move(int deltaX, int deltaY);
void Draw( );
Boolean IsNonEmpty( );
private:
TShape
TList

*Shape;
*Link;

};

The source file ShapeList.c follows:


|

//////

Table of Contents continued

10

15

20

25

30

35

40

45

50

55

60

| //
| // ShapeList.c
| //
| //////
|
| #include <oops.h>
| #include "Shapes.h"
| #include "ShapeList.h"
|
| extern WindowPtr DrawingWindow;
// The drawing window is
|
// defined in windowUtils.c
|
| void TList : : InitShapeList( )
| {
|
Shape = NULL;
// Convention: A list node with NULL shape is
|
Link = NULL;
// an empty node awaiting a non-null
|
//Shape to be assigned
| }
|
| void TList : : Append (TShape *theShape)
| {
|
TList *LastNode, *NewNode;
|
|
if (Shape == NULL) {
//overwrite an "empty" shape with
|
// theShape to fill in the empty slot
|
Shape = theShape;
|
|
} else {
|
|
NewNode = new TList;
|
NewNode>Shape = theShape;
|
NewNode>Link = NULL;
|
LastNode = this;
|
while (LastNode>Link != NULL) {
|
LastNode = LastNode>Link;
|
}
|
LastNode>Link = NewNode;
|
}
| }
|
| void TList : : Move(int deltaX, int deltaY)
//calls Move(deltaX, DeltaY)
| {
// method on each Shape in the list
|
TList
*TempNode;
|
TShape *theShape;
|
|
TempNode = this;
|
|
while (TempNode != NULL) {
|
|
theShape = TempNode>Shape;
|
theShape>Move(deltaX, deltaY);
//call theShape's move
|
// method to move it by (deltaX,deltaY)
|
TempNode = TempNode>Link;
//advance to next
|
// node on list
|
}
| }
|
| void TList : : Draw( )
| {

Table of Contents continued

65

70

75

|
|
|
|
|
|
|
//
|
|
|
|
|
|
|
|
//
|
|

TList *TempNode;
TempNode = this;
while (TempNode != NULL) {
TempNode>Shape>Draw( );

//send the Draw message

to the shape in the node's Shape instance variable


TempNode = TempNode>Link;
//advance to next node on list
}
}
Boolean TList : : IsNonEmpty( )
{
return (Shape != NULL);

// a NULL Shape in the first list node

signifies an empty list, by convention


}

Answers to Review Questions 15.3


1. In an object-oriented graphics drawing system, one kind of
objects can be a grouped object consisting of a pointer to a
linked list of subobjects that are grouped together, having an
enclosing rectangle that is the smallest rectangle containing all of
the enclosing rectangles of the individual subobjects on the
subobject list. Such a grouped object can be drawn or moved by
sending a draw or move message to each object on its
subobject list.
2. A user interface to a system is that part of the system which
interacts with users by accepting user inputs (e.g., from
keyboards, pointing devices, microphones, video cameras, etc.)
and giving outputs to the user (e.g., on computer display screens,
or with speakers in the case of sounds, etc.)
3. Object-oriented programming techniques can be used to
advantage to create file and printing subsystems for a software
application by allowing programmers to customize generalpurpose, prefabricated subsystem objects. For example, a printing
subsystem may have an abstract virtual method for drawing a
page image which functions like a blank method for the
programmer to fill in by supplying a specific draw page image
method. This is the only missing piece needed to complete the
printing subsystem. Similarly, the programmer can fill in the
blanks in a file subsystem object by supplying converter methods
that translate byte streams in a file into and out of internal object
format. Thus, the programmer need only specify the least amount
of application-specific detail to complete a prefabricated
subsystem design a design which may have been very carefully

Table of Contents continued


conceived to handle all exception conditions and device types
required.
4. Object-oriented systems may provide building-block objects with
which to construct user interfaces. Using a graphics-oriented
editor, it may be possible to design a control panel with
buttons, sliders, and other input devices to perform button-press
actions, such as starting, stopping, and resetting, or to perform
quantity-specifying actions, such as that achieved by adjusting a
slider indicator on a slider scale. Further, it may be possible to
connect the models outputs to the inputs of display objects that
can display the models outputs using graphics, text, or sound.

Solutions to Selected Exercises in Exercises 15.3


1. Let L be a list of objects (O1 , O2 , ... , On ), having respective
enclosing rectangles R1 , R2 , ... , Rn , where each rectangle Ri is a
struct having four members of the form R i = {lefti , top i , right i ,
b o t t o m i }. The smallest rectangle enclosing the R i (1 i n ) is
obtained by computing the rectangle R = {left, top, right, bottom}
whose left and bottom are the respective minima of the lefti and
bottom i , and whose top and right are the respective m a x i m a of
the topi and righti of the Ri for (1 i n ).
2. To implement a method to ungroup a grouped object G, one need
merely append the subobjects on Gs own list of subobjects to
the end of theShapeList , the global list of currently visible shapes in
the drawing application, after which the space for the grouped
object G can be reclaimed (by calling delete G).
3. Let L = (O1 , O2 , ... , On ) be a list of shape objects having respective
enclosing rectangles R 1 , R2 , ... , Rn , where Ri is of the form Ri =
{left i , top i , right i , bottom i }, and let R = {left, t o p , right, b o t t o m }
be the smallest rectangle enclosing all the Ri (1 i n ) computed
according to the method given in Ex. 15.3.1 above. Now define the
center of a rectangle R = {left, top, right, bottom} to be the point
C = {HorizontalCenter, Vertical Center} where HorizontalCenter =
(left+right)/2 and VerticalCenter = (top+bottom)/2. To align the
objects on list L horizontally , so that their bottoms, centers, or
tops align, you can move them so that the enclosing rectangle R i
of object Oi has its respective bottom, center, or top lying on the
respective bottom, center, or top of the enclosing rectangle R .
Similarly, to align the objects on list L vertically , so that their
lefts, centers, or rights align, you can move them so that the
enclosing rectangle R i of object Oi has its respective left, center,
or right lying on the respective left, center, or right of the
enclosing rectangle R . The align message to send to an object is

Table of Contents continued


just a variant of the Move(deltaX, deltaY) message defined on lines
51:57 of Program 15.20 (on p. 634), where the distances to move
an object deltaX and deltaY to produce the desired alignment are
obtained by differencing the left, top, right, bottom, or center of
the enclosing rectangle R i of the object to move with the
appropriate left, top, right, bottom, or center of the smallest
rectangle R enclosing all of the objects Oi (1 i n )

Answers to Review Questions 15.4


1. Some advantages are: (a) Provides a framework in which reusable
software components can be expressed and used and in a form in
which objects are customizable to fit differing circumstances of
component use; (b) Provides for ease of maintenance and
modification of software permitting programming to occur only by
expressing differences; (c) Provides clear modular structure for
software at a level of organization finer than the C source module;
(d) Provides for fast easy definitions of prototypes and user
interfaces; and (e) Enables prefabricated software subsystems,
such as those for printing and filing, to be converted into finished
subsystems merely by filling in the blanks with applicationspecific components.
2. If objects are allocated storage in heaps and are referenced by
handles (pointers to pointers) in order to make heap compacting
efficient, there may be run-time penalties associated with
storage allocation and double dereferencing of handles. If dynamic
binding of method names to actual method implementations is
required at run time, there may be penalties associated with
performing searches to determine these run-time bindings as in
the case when a message for method M sent to object Ob1 does not
bind to a local method for O b 1 , but instead binds to a method
defined for a remote ancestor of Ob1 in the class inheritance
hierarchy.
3. Class browsers help perform convenient, on-line searches during
program editing and composition to locate textual definitions of
classes and methods, and they portray class hierarchies
graphically, allowing menu-driven navigation of and access to the
definitions in the class hierarchy. This is much more efficient than
attempting to search in cross-reference listings, since the latter
tend not to be organized advantageously to support searches for
class definitions and methods, especially those sharing identical
method names.

Solutions to Exercises 15.4

Table of Contents continued


1. In order to draw the class hierarchy, you have to build a tree (or a
forest) at compile time which encodes the subclass relations
between class definitions. Such a forest is shown in Fig. 15.32,
using a horizontal graphical forest representation in which the
roots of the trees in the forest lie to the left. You also have to
save the object class names to use as tree node names. To
display a menu of method names, such as that shown in Fig. 15.33,
you have to attach to each tree node representing a class
definition, the names of each of the methods defined for that
class definition. Moreover, you have to save a textual reference
for each such method or for each such class declaration giving
both the text file path name and also the starting and ending line
numbers of the definitions text in this text file, in order to be
able to display the text of the respective method or class
declaration.

Table of Contents continued

Answers to the Review Questions 16.2


1. The software lifecycle is the span of time that starts with the
initial conception of a software system and ends with its
retirement from use, following its useful service lifetime. It has
been found useful to break the software lifecycle into various
phases consisting of activities such as requirements analysis ,
s p e c i f i c a t i o n , d e s i g n , coding and debugging , testing and
integration , and operations and maintenance.
In requirements analysis , we try to formulate a high-quality
statement of the true capabilities and properties of the system
that are needed by its users and operators. The result of the
requirements analysis process is usually a requirements document
that enumerates the requirements the system will have to meet.
In general, in a good requirements analysis, were trying to
conceive of everything that is relevant and nothing more. We
attempt to generate a set of requirements statements with at
least the following properties: (a) completeness , (b) consistency ,
(c) unambiguity , (d) correctness , (e) comprehensibility .

S p e c i f i c a t i o n is the enumeration of specific, quantitative


behavioral constraints a system must satisfy in order to meet its
requirements. Good specifications are: (1) c o m p l e t e , (2)
unambiguous , (3) minimal , (4) comprehensible , and (5) sufficiently
specific and well-quantified to be testable whether or not they
are satisfied. Specification is a key activity in the software
lifecycle because, in the absence of specific measurable
properties a system must exhibit to meet its requirements, it
may not be testable whether a system satisfies the requirements.
A d e s i g n is a representation of an artifact or a system. A
software design is a representation of a software system suitable
for programmers to use to implement it. D e s i g n i n g is the art of
constructing and evaluating designs to meet constraints and
satisfy purposes. In the software lifecycle, we try to produce
designs: (a) To satisfy the specifications and requirements, (b) To
help prescribe further implementation activities, and (c) To
exhibit certain useful properties, namely: (1) c o m p l e t e n e s s , (2)
c o n s i s t e n c y , (3) c o m p r e h e n s i b i l i t y , (4) technical feasibility , (5)
unambiguity , and (6) susceptibility to analysis and evaluation.
During the coding and debugging phase of the software lifecycle
(sometimes called the implementation phase ) you construct and
debug a working executable representation of the system design.
During implementation, we try not only to produce a concrete
design realization in a suitable programming language that

Table of Contents continued


satisfies the specifications and meets the requirements, we also
attempt to produce an artifact that enhances the ease of
subsequent maintenance and modification.
During module testing and integration, we attempt to ensure
that modules have correct behavior, that they satisfy
performance specifications, and that they cooperate correctly
together to achieve overall system performance. Testing and
integration is a key phase of the software lifecycle because it
ensures that software quality requirements are met prior to
system release.
During the maintenance and upgrade phase of the lifecycle, we
attempt to alter the system either to remedy defective
properties revealed during system usage, or to meet new
behavioral requirements. The activities involved during
maintenance may recapitulate any and all of the previous lifecycle
activities.
2. In the software lifecycle, good designs: (a) Satisfy the
specifications and requirements, (b) Help prescribe further
implementation activities, and (c) Exhibit certain useful
properties, namely: (1) c o m p l e t e n e s s , (2) c o n s i s t e n c y , (3)
comprehensibility , (4) technical feasibility , (5) unambiguity , and
(6) susceptibility to analysis and evaluation .
3. After finishing a design, a useful practice is to conduct a d e s i g n
walkthrough . In a design walkthrough, the designers try to explain
the design to a critical (but friendly) team of design reviewers
(who should be distinct from the actual designers, themselves).
The idea is to go over the design with a fine-toothed comb, soto-speak, to attempt to catch errors and problems early in the
lifecycle before investing in costly implementations that will be
thrown away or that will need costly rework.
4. The maintenance and upgrade phase of the lifecycle often
comprises 70% to 90% of the total lifecycle cost of a large, longlived software system. In other words, maintenance is usually
expensive. This implies that spending extra time and effort in the
prerelease phases of the software lifecycle, in order to decrease
downstream maintenance costs, can yield significant costavoidance. For example, by one estimate, only 17% of the time
spent in the maintenance phase is attributable to debugging, but
over 50% of the time is spent by programmers trying to
understand the system they need to repair or extend. If software
u n d e r s t a n d i n g constitutes 50% or more of the maintenance
activities, then a key prerelease, cost-avoidance activity is
producing clear documentation (or other types of system

Table of Contents continued


understanding aids, such as videotapes of key designers and
implementers explaining what was going through their minds when
they built the system). Implementing the design in a clean,
modular fashion yields implementations that are susceptible to
modification and upgrade at less expense than less modular
implementations. Also, maintenance costs can be reduced (or
avoided) by ensuring that tried-and-proven techniques are used
for trouble report handling, installing new system releases, and
updating both documentation and system configurations.

Solutions to Selected Exercises in Exercises 16.2


1. Some sensible general requirements are that the system should
be affordable , reliable , efficient , correct , and user-friendly . (This
list is not exhaustive.) By affordable , you might mean that the cost
of system development and operation should live within the
systems development budget and the systems operation budget
agreed upon at the time the development contract is signed by
the client. (Such budgets reflect the amount the client is willing to
pay and expects to pay to purchase and operate the system.) By
reliable , you might mean that the system should be continuously
available and should function correctly after it is released for
service and put into operational use. By efficient , you might mean
that the system as implemented does not waste resources and
uses the minimum resources needed to satisfy all the other
system requirements. By correct , you might mean that the system
operates free of error. By user-friendly , you might mean that the
systems inputs and outputs are comprehensible, that users find it
easy to learn how to use the system, and that the system is
responsive.
2. Taking the r e s p o n s i v e n e s s subrequirement of the u s e r friendliness requirement: (a) For on-line transactions at teller
windows in branch banks, the system shall be deemed responsive
if, after submission of a command at a teller terminal, the system
responds within 1/2 second for 95% of the transactions, within 2
seconds for 99.8% of the transactions, and in no more than 5
seconds for all transactions (where the time of submission of
the command is defined as the time the teller presses the enter
key on the keyboard); (b) For printing and mailing of monthly
statements to customers, the system shall be deemed responsive
if it never delays scheduled printing and mailing of a batch of
statements more than two days after the scheduled mailing time.
Taking the c o r r e c t n e s s requirement: (a) The system shall not
make data recording errors by entering a datum into its database
that differs from that in an executed data entry command on the

Table of Contents continued


screen of any bank data entry terminal; (b) The system shall not
lose or corrupt data in its database; (c) The system shall make no
calculation errors, nor shall it fail to make calculations that are
precise to the nearest 1/1000th of a cent.

Answers to the Review Questions 16.3


1. In general, productivity is a measure of the output achieved from
spending a units worth of input effort. Software productivity can
be measured by dividing the number of lines of delivered source
instructions (DSI) by the number of person-months (PMs) needed
to build a system prior to its release into service.
2. The software learning curve refers to an observed phenomenon
that when a team of system programmers implements a system
(or a family of substantially similar systems) repeatedly under
slightly changing circumstances, the resources consumed for
system development declines dramatically on the second and
succeeding attempts as trial-and-error behavior on the first pass
gives way to the use of tried-and-true techniques on succeeding
passes. Not only does the teams performance improve, but its
ability to estimate its future resource consumption accurately
improves as well.
3. To conduct a software productivity audit you first rate your
organizations productivity attributes using C O C O M O rating sheets.
Then, among those attributes with low productivity ratings, you
identify those that can be addressed as manipulable cost-drivers
in which you could make investments to improve productivity.
Next, you do a cost-benefit analysis which estimates the return on
investment (ROI) in terms of quantified productivity
improvements attainable by making various productivityimproving investments. Finally, you identify those investments, if
any, that yield a payoff as potential candidates for performanceenhancing, net-cost reducing improvements to make.
4. The effort and schedule equations favor the use of reusable
software components when the sum of the costs of: (a) component
acquisition, (b) learning how to use the components, and (c)
implementing the glue code to assemble the components into a
system, is less than the cost of implementing the system from
scratch starting with a clean sheet of paper.

Solutions to Selected Exercises in Exercises 16.3


1. Using Eq. 16.1, with KDSI = 100, m 7 = 0.93, and m 14 = 0.83, yields
the number of person months (PMs) to develop the system:

Table of Contents continued

m 7 * m 14 * 2.4 * (KDSI)1.05
= 0.93 * 0.83 * 2.4 * 1001.05
= 233.2 person months
Then, substituting this value of PM in Eq. 16.2 to get Td yields,
PM

Td

2.5*

15.4 months

233.2

Hence, the nominal development time is 15.4 months.


2. It should be possible to compress the nominal schedule to 75% of
its value at an extra 23% cost. Taking 75% of 15.4 months yields
11.54 months for the compressed schedule. It should cost an
additional 53.6 person months beyond the original 233.2 PMs to
achieve this schedule compression. (At $100K per fully burdened
person year, the added expense to meet the compressed schedule
should be $447K.)

Answers to the Review Questions 16.4


1. A software process model is a scheme for organizing the
activities that take place during the software lifecycle which
defines the different activities and stages of the software
development process and which specifies the criteria for
transitioning from one stage to the next.
2. In the Code-and-Fix model, you repeatedly write code and debug
it until enough substantially effor-free code exists to constitute a
system implementation. In the Waterfall Model , you transition
through distinct stages consisting of requirements analysis,
specification, design, coding and debugging, testing and
integration, and maintenance. In the Evolutionary Development
model, you expand an initial partial working solution in increments
using directions for expansion determined by experience with the
partially working system. In the Spiral Model , you perform a
number of iterations or passes with the activities in each pass
determined by a risk analysis of the alternative courses of action
needed to meet the objectives. Risk analysis includes risk item
identification, prioritization, and resolution.
3. Risk analysis in the Spiral Model follows the determination of
o b j e c t i v e s to be attained, a l t e r n a t i v e s for implementation of a
portion of the system, and the c o n s t r a i n t s imposed when the
alternatives are pursued. Risk analysis consists of evaluating the
a l t e r n a t i v e s with respect to the o b j e c t i v e s and c o n s t r a i n t s to
identify uncertainties that are significant sources of risk to the
project. Risk item identification is the process of determining and

Table of Contents continued


enumerating a list of these uncertainties that impose risk. R i s k
item prioritization consists of arranging the risk items in highestto-lowest order of priority for subsequent attention and action.
Risk item resolution consists of devising and following costeffective strategies for eliminating the high-priority risk items.
4. The prototyping or simulation activities used in the Evolutionary
Development model can be considered as risk resolution
strategies because they are activities that buy information to
reduce risk. For example, building and using a prototype user
interface with actual users can develop information that reduces
the risk that the user interface design will be found to be
ineffective for these actual users. (This is useful information to
buy when confronting the design of ill-understood parts of a
system, such as user interfaces.) Simulating system performance
can reduce the risk that the proposed hardware configuration is
either insufficient to meet the project performance constraints or
so excessive as to be wasteful of resources.

Solutions to Selected Exercises in Exercises 16.4


1. Designing a user interface in a software project in a new
application area or using a novel interface technology (such as a
3D virtual reality helmet to navigate a database) presents the risk
that designers cannot easily foresee how actual future users will
react to the user interface, and that the interface design will not
be well-received by future users. In the Spiral Model this risk of
possible design failure can be addressed by constructing a rapid
prototype of the new user interface and by evaluating it in trial
use with real users to see if it works as expected (or to learn how
to adjust the design in case it doesnt work effectively). In the
Waterfall Model, the user-interface design gets implemented,
debugged, and tested against the specifications before it gets put
into actual use with real users in the post-release period. Thus, a
looming disaster, such as a poorly-designed user interface, may
go undetected until after the system is released into service with
actual users. Only then do the actual users generate information
that the user-interface design is ineffective.
2. The development of ARTS III did not follow the Waterfall Model
because actual end-users (i.e., terminal area radar air traffic
controllers) got to use and evaluate early, partially-completed
versions of the system, and the system design was revised and
improved in response to constructive suggestions these users
made. In the Waterfall Model, end-users wouldnt have been able to
use the system until after it was finished and was released for

Table of Contents continued


service. Instead, the development of ARTS III followed the
Evolutionary Development model because end-users reacted to
aspects of an evolving design as it was progressing toward
completion.

Table of Contents continued

Answers to Review Questions A.1


1. An arithmetic progression is an ordered sequence of terms in
which any two consecutive terms t i and t i + 1 differ by a constant
amount d = t i+ 1 t i .
2. n ( n + 1)/2.
3. S = n * ( a + l)/2.
4. Write down two copies of S = a + (a + d ) + (a + 2d ) + . . . + (l 2d ) +
( l d ) + l , with the terms of the second copy of S written in
reverse order. Then add the two copies of S together, getting 2* S ,
and solve for S .
5. top bottom + 1.

Solutions to Exercises A.1


1. S = 1 + 2 + 3 + . . . + 99 = 99 * (99 + 1) / 2 = 50 * 99 = 4950 ccs.
2. S = (n /2)[ 2 l (n 1)d ].
3. S = (l + a )( l a + d )/(2 d ).
4. S = (3/2)(n + 2)(n 3).
5. The formula solving Exercise 3 above gives the sum S in terms of
the first term a , the last term l , and the constant difference d . In
the case of the current exercise, a = 2, l = 2 n , and d = 2.
Substituting these values in the formula of Ex. 3 and simplifying
gives S = (2 n + 2)(2 n 2 + 2)/(2* 2) = n ( n + 1 ).
6. S = (n 2)(n + 4).
7. S = 202*n .

Answers to Review Questions A.2


1. A geometric progression is an ordered sequence of terms having a
constant ratio r between pairs of consecutive terms. Thus, r = t i /
t i+ 1 for each pair of consecutive terms t i and t i+ 1 .
2. Multiply the sum S = a + ar +. . . + ar n by r , getting a formula for rS ,
subtract S from rS , and solve for S .
3. The sum S of 1 /2 + 1 /4 + . . . + 1 /2 n is given by S = 1 1 /2 n . Therefore
as n increases, 1 / 2 n gets smaller and smaller, and S gets closer and
closer to 1 but S never exceeds 1. Hence, 1 is an upper limit for
S.
4. If T is a tree in which all internal nodes have b children, the
branching factor of T is b .

Table of Contents continued

Solutions to Exercises A.2


1. Applying formula A.8, in which S = (r n + 1 1)/(r 1) to S = 1 + 21 +
2 2 + . . . + 2n , where r = 2, gives S = 2n + 1 1.
2. The total number of nodes in a complete binary tree with a full
bottom row on level l is 2l + 1 1, as can be seen by substituting l
for n in the answer to Ex. 1 above.
1 /2 n .
4. 1 / 2 j 1 1 / 2 k .

3.

1 /4

5. 2 (k +2) / 2 n
6. $1656.03 (which is the sum of 100*(1.09) 1 + 100*(1.09) 2 + . . . +
100*(1.09) 10 ).
7. S = a * [ (r n k + 2 1) / (r 1) ].

Answers to Review Questions A.3


1. If x is a real number, the floor of x is the largest integer y , such
that y x , and the ceiling of x is the smallest integer y , such that y
x.
2. Minus the floor of x is the ceiling of minus x . In symbols, x =
x .
3. Whenever x is an integer x = x = x .
4. Whenever x is a negative real number that is not an integer, then
the floor of x is not equal to x with its fractional part discarded.
For example, 6.3 6. Instead, 6.3 = 7.

Solutions to Exercises A.3


1. 3, 2, 3, and 1.
2. The conjecture is false . As a counterexample, let x = 3.7 and y =
3.2. Then, even though 3.7 3.2 is true, it is not true that 3.7
3.2.
3. The assertion is true. Because x y is always an integer, say a ,
and because the ceiling of an integer a is the same integer a, the
result follows from substituting a = x y in a = a .

Answers to Review Questions A.4

Table of Contents continued


1. The base-2 logarithm of a positive real number x , denoted lg x , is
the power to which the base 2 must be raised to be equal to x . In
symbols, 2 lg x = x .
2. Apply formula A.22 for changing bases of logarithms: log b x = logc
x / logc b.
3. log b ( x y) = logb x + logb y.

Solutions to Exercises A.4


1. We have the facts that b > 2 and n > k . Starting with k < n , derive
the result using the following steps:

Step
k<n
k /n < 1
1 + k /n < 2
fact, listed above)
1 + k /n < b
n + k < n* b
both sides
log b (n + k ) < logb (n * b )
1.
log b (n + k ) < logb n + 1.

to get next Step


divide both sides by n
add 1 to both sides
combine with 2 < b (a starting
multiply both sides by n
take

base- b

logarithms of

expand log b (n * b ) and replace logb b by

[ N o t e : The latter result can be used to show that log b ( n + k ) is


O(log n ).]

2. lg 4x 2 = lg (2x ) 2 = 2 lg(2x ) = 2 [ lg 2 + lg x ] = 2 (lg x + 1).


3. Apply formula A.24 to show that ln((8x )/ y ) / ln 2 = lg ((8x )/ y ). Then
expand lg ((8 x )/ y ) to lg 8 + lg x lg y which equals 3 + lg x y .

Answers to Review Questions A.5


1. If a and b are any two real numbers, then if b 0, we define a mod b
= a b a /b . Otherwise, if b = 0, we define a mod b = a .
2. a b (modulo m) means (a mod m) = (b mod m).
3. The modulus is m .
4. You can cancel c whenever c and m are relatively prime.

Table of Contents continued

Solutions to Exercises A.5


1. 3, 4, and 0.18.
2. The following table demonstrates the result.

i = 0
i * 5 = 0
( i *5) m o d 11 = 0

10 15 20 25 30 35 40 45 50

10 4

5
3

6
8

7
2

8
7

9
1

10
6

Answers to Review Questions A.6


1. The general term is F ( i) and the index variable is i.
2. An index variable is sometimes called a dummy variable because
it acts as a place holder for a range of values, analogous to the
role of a dummy corporation which is set up to act in place of
another corporation as a place holder to represent the other
corporations interests.
3. The n th Harmonic number, Hn , is the sum of the reciprocals of the
integers ranging from 1 to n . Thus, Hn = (1 i n ) 1 / i = 1 + 1 / 2 + 1 / 3 +
. . . + 1/n.
4. You can replace the index variable in a sum with an expression
involving another index variable provided the substitution yields
a permutation of the range.

Solutions to Exercises A.6


d

[ (nd n 1) d n + 1] yields,
1. Putting d = 1 / 2 in (1 i n ) i d i =

2
(
d
1)

1 /2

( 1 / ) 2 [
2

n+2 1
2

2n

+ 1] , which simplifies first to,

1 n+2
2 [ 1 2 n ] , and finally simplifies to,
2

n+2
2
.
n
2

Equation A.28 follows from the latter by replacing n with k .


2. From Ex. A.1.3, when we know a , l, and d , but not n in an arithmetic
progression, the sum is given by S = (l + a )( l a + d )/(2 d ) .
Substituting a = 1, l = 2n 1, and d = 2 in the latter yields S = (
2 n 1 + 1)(2n 1 1 + 2)/(2*2) which simplifies to S = n 2 .

Table of Contents continued


3. Writing S = 2 + 4 + 6 + . . . + 2n as a sum with an index variable i
yields S = ( 1 i n ) 2*i . Because the range (1 i n ) contains
exactly n numbers, there are n terms in the sum S . Therefore,
applying formula A.1 (or Memory Jog A.1), the sum is the number
of terms n times the average of the first and last terms
( 2 + 2 n )/2. Hence, S = n *(2 + 2n )/2 = n *( n +1). [ N o t e : In this
solution, we used summation notation to deduce the number of
terms, n . Compare this solution with the solution to Ex. A.1.5, which
solves the same problem a different way without deducing the
number of terms.]
4. Solving the simultaneous equations,
1 =

+ D

8 A + 4B + 2C

+ D

1 0 = 27 A + 9 B + 3 C

+ D

2 0 = 64 A + 16 B + 4 C

+ D

4 =

A +

B +

yields A = 1 / 6 , B = 1 / 2 , C = 1 / 3 , and D = 0. Hence, S = n 3 / 6 + n 2 / 2 +


n / 3 , which simplifies to S = n ( n +1)( n +2)/6.
5. Yes, S = ( m i n ) 1 / 2 i = 2*(1 / 2 m ) (1 / 2 n ) which is twice the first
minus the last, as can be seen by replacing j and k with m and n
respectively in the solution to Ex. A.2.4, giving S = 1 / 2 m 1 1 / 2 n ,
after which 1 / 2 m 1 can be replaced by 2*(1 / 2 m ).
6. After replacing i with (j 1), the sum (1 i n 1) 2i + 1 becomes
(0 j n ) 2j which has the value 2n + 1 1 (from formula A.8).

Answers to Review Questions A.7


1

1. The base case is T(0) = a . The general case is T(k ) = 2 + 2 T(k /2).
2. A summing factor is a multiplier used to multiply several
instances of recurrence relations so that when they are added
together they form a telescoping sum .
3. A telescoping sum is a sum of terms such that the terms in the
middle cancel one another in adjacent pairs.
4. Substitute the general formula for the solution into the original
recurrence relations and verify that the relations hold.

Solutions to Exercises A.7


1. First, putting n = 1 in T(n ) = b n + (a b ), we get T(1) = b *1 + (a b )
= b + a b so that T(1) = a holds in Eqs. A.35. For the general case,
T( n 1) = b ( n 1) + a b . Substituting this for T(n 1) in b + c T(n 1),
with c = 1 gives:

Table of Contents continued


= b + [ b (n1) + a b ]
= b+[bn+a2b]
= bn+(ab)
T(n).

Hence, the general case in recurrence relations A.35 is also


satisfied by the solution T(n ) = b n + (a b ).
2. If T(0) = a and T(n ) = a + r T( n 1), then by expansion,
T(0) = a
T(1) = a + a r
T(2) = a + a r + a r2
...
T( n ) = a + a r + a r2 + . . . + a r n.
Using Eq. A.9, T(n ) has the general form T(n ) = a [(r n + 1 1)/(r 1)].

3. Let T(1) = a and T(n ) = n d + (a d ) + T(n 1). Rearranging T(n ) to


be
T( n ) = a + (n 1)d + T(n 1) and expanding gives,
T(1) = a
T(2) = (a + d) + T(2 1)

= (a + d) + a

T(3) = (a + 2 d ) + T(2)

= (a + 2 d ) + ( a + d) + a

...
T( n ) =
+ ( a + d) + a

(a + (n 1) d ) + (a + (n 2) d ) + . . . + (a + 2 d )

Hence T( n ) is the sum of an arithmetic progression of n terms with


first term a and last term l = a + (n 1) d . The general formula for
n
the sum can be found by applying Eq. A.3 to yield T(n ) = 2 [ 2 a +
(n1)d].
4. Letting a = 3, b = 1, and c = 2 in the general solution for T(n ) in
Table A.12 and simplifying gives, T(n ) = 2n + 1 1.
5. Rewrite Recurrence Relations A.35 as,
T(1)

T( n )

c T(n1)

a
=

Then write,
=

T(1)
1 /c T(2)

c / c T(1) =

a
b /c

{multiply by 1/ c }

Table of Contents continued


1 /c 2 T(3)

c / c 2 T(2) =

b /c 2

{multiply by 1/ c 2 }

b / c n 1

{multiply

. . .

1/ c n 1 T( n ) c/ c n 1 T(n1) =
1/ c n 1 }

{and

by

summing

the

rows

above}
T( n )/ c n 1

a + b /c + b /c 2 + . . . + b /c n 1
In the sum immediately above, if c = 1 then T(n ) = a + (n 1) b = b
n + (a b ), which yields the solution given in the first case of Table
A.12. Now suppose c 1. Then the sum above is,
T( n )/ c n 1 = a + b /c [ 1 + 1/c + 1/c 2 + . . . + 1/c n 2 ]
= a + b / c [ (1 1/c n 1 ) / (1 1/c ) ]
= a + b [ (1 1/c n 1) / (c 1)]
Multiplying both sides by c n 1 gives,
= a *c n 1 + b [ (c n 1 1) / (c 1)]
T( n )
= a * c n 1 + ( b*c n 1 )/( c 1) b /( c 1)
= [ a /c + b /(c ( c 1))] c n b /(c 1)
After some final simplification, this yields the solution for the
case c 1.
=

T( n ) =

b
c

b n
b
c
.

c 1
c 1

Answers to Review Questions A.8


1. Subexpressions having the logical values 0 (representing false )
and 1 (representing true ) can be represented by propositional
variables.
2. First, replace distinct logical subexpressions with distinct
propositional variables and replace the Cs logical operators && , | |,
and ! with , , and respectively. Second, use the laws in Tables
A.13 and A.14 to simplify the latter logical expressions. Finally,
reverse the substitutions of the first step to obtain simplified C
expressions.

Solutions to Exercises A.8


1. The manager has the correct solution. The programmer has
misapplied the law a t r u e t r u e of Table A.13 to conclude
incorrectly that (!A) | | true simplified to (!A) instead of to true.
2. The input expression simplifies to (x == y).

Table of Contents continued


3. Use the first Ground Resolution law in Table A.14 to simplify the
input expression to (x>top <= 9).
4. In C it is possible to use short circuit logical operators to guard
against erroneous evaluation of subexpressions, as in (x == 0.0) | | (y/x
> 15) . Here, x == 0.0 is true , the value of the entire subexpression
becomes true and the subexpressions (y/x > 15) is never evaluated. On
the other hand, if (y/x > 15) | | (x == 0.0) is evaluated with x == 0.0, a n
error occurs when evaluating y/x upon the attempt to divide y by
zero. Thus, there exist circumstances in C in which the short circuit
operators & & and | | do not obey the commutative laws of logic.
Caution should be exercised not to apply these commutative laws
when attempting to simplify such logical subexpressions in C.

You might also like