Sunteți pe pagina 1din 409

Programming with

Ada 95:
a structured approach

Robert A. Pilgrim
William F. Lyle
Brenda Wilson
Robot Crafters, LLC
Computer Science & Engineering Series

Programming with Ada 95: a structured approach


authors Robert A. Pilgrim, William F. Lyle, Brenda Wilson

edited by Virginia Kelley

1304 Chestnut Street, P.O.Box 1445, Murray KY 42071


Copyright  Robot Crafters, LLC 2006, All rights reserved

ISBN:0-9778095-0-1
ISBN13 978-0-9778095-0-9

www.robotcrafters.com
Programming with Ada 95:
a structured approach

Robot Crafters, LLC, Publisher


Computer Science and Engineering Series
Copyright  2006

All rights reserved. No part of this publication may be reproduced, stored in a retrieval system, or
transmitted, in any form or by any means, electronic, mechanical, photocopying, recording, or
otherwise, without the prior permission of the authors. Printed in the United States of America.
Contents
Preface v

PART ONE

0 Getting Started 1
0.1 History of Computing 2
0.2 The Evolution of Software Development 8
0.3 The Ada Programming Language 11
1 Structure of An Ada Program 15
1.1 Definition of a Computer Program 16
1.2 Program Components 21
1.3 From Source Code to Executable Programs 22
1.4 Things That Can Go Wrong 24
1.5 Software Development Lifecycle 27
1.6 Comments on with and use 29
1.7 Syntax Notation 29
2 Data Representations in a Computer 33
2.1 Numeric Data Types 34
2.2 Character Types 39
2.3 String Types 39
2.4 Boolean Types 41
2.5 Assignment Statements 44
2.6 Type Conversions 46
2.7 Internal Number Representations 47
2.8 Applications 55
3 Using Conditionals 63
3.1 The if...then... Construct 64
3.2 Using the ...else... and ...elsif... 65
3.3 The case Statement 68
3.4 Nested Conditionals 70
3.5 Exception Handling 71
36 Applications 73
4 Using Loops 81
4.1 The generic loop...exit when... 82
4.2 The for...loop... 84
4.3 The while...loop... 85
4.4 Nested loops 85
4.5 Applications 86
5 Text Files and Structured Data Types 93
5.1 Text Files 94
5.2 Arrays 97
5.3 Records 101
5.4 Arrays of Records 101
5.5 Applications 102

i
6 Subprograms 117
6.1 Functions 118
6.2 Procedures 121
6.3 Parameter Passing 121
6.4 Identifier Scope and Extent 122
6.5 Applications 125
7 Graphics 135
7.1 The Importance of Visualization 136
7.2 Basics of Two-Dimensional Graphics 136
7.3 Ada Graphics Packages 138
7.4 Modeling and Transformations 140
7.5 Applications 146
8 Using and Creating Packages 155
8.1 Adagraph Package 156
8.2 NT_Console Package 161
8.3 Math Package 163
8.4 Creating Packages 164
8.5 Generics 168
8.6 Applications 170
9 Special Topics in Programming 179
9.1 Case Study: Pseudorandom Number Generation 180
9.2 Case Study: Plotting Graphs in Three Dimensions 188
9.3 Case Study: Scripting POV-Ray 192

ii
PART TWO

10 Dynamic Memory Allocation and Recursion 199


10.1 Static vs. Dynamic Memory 200
10.2 Iteration vs. Recursion 207
10.3 Algorithm Analysis 211
10.4 Applications 216
11 Introduction to Abstract Data Types (ADTs) 225
11.1 An Example ADT 226
11.2 Lists 231
11.3 Stacks 248
11.4 Queues 254
11.5 Applications 257
12 Trees 291
12.1 Preliminary Definitions 272
12.2 Tree Data Representations 275
12.3 Tree Traversals 278
12.4 Applications 283
13 Searching and Sorting Algorithms 291
13.1 Linear Search (unordered lists) 292
13.2 Binary Search (ordered lists) 294
13.3 Brute-Force Sorting Routines 295
13.4 Optimal Sorting (by key comparison) 300
13.5 Hashing 311
14 Graphs 319
14.1 Graph Definitions 320
14.2 State Machines 322
14.3 Graph Data Representations 324
14.4 Graph Traversals 326
14.5 Implementing Graph Traversals 331
15 Graph Algorithms 339
15.1 What are Graph Algorithms? 340
15.2 Example Problems for Graph Algorithms 340
15.3 Algorithm Design and Implementation 345
15.4 Applications 345
16 Numerical Algorithms 359
16.1 Introduction 360
16.2 Drawing the Graph of a Function – Graph_It 361
16.3 Finding a Zero of a Function -- Bisection 364
16.4 Integrating a Function – Trapezoidal Rule and Simpson’s Rule 366

Glossary 375

Index 391

iii
iv
Preface

This textbook is intended to be used in the first two programming courses of an undergraduate
major in computer science. It covers the basics of structured programming while introducing the
principles of computing, software development, and algorithm design and analysis. The text is
divided into two parts. Part One covers the computational elements of high-level, procedural
programming languages. It is appropriate for both computer science majors and non-majors with
a mathematical background in college-level algebra and trigonometry. Part Two covers abstract
data types and encapsulation with an introduction to discrete and numerical algorithms. This part
of the text introduces the dynamic memory model and recursion.

Ada is the language of choice for this textbook, but our primary purpose is not to teach Ada.
Instead, we are using Ada to provide the student with a solid foundation in the principles of
computer science without letting the language get in the way. Ada is ideal for learning structured
programming and for studying classic data structures, abstract data types, and computer
algorithms. This language is based on a strictly enforced standard, which makes it one of the
most portable and stable high-level languages available.

For the student, this means that once a program written in Ada compiles successfully, it is more
likely to run as intended. The straightforward syntax and simple IDE provided by GNAT make it
possible for a novice programmer to begin creating and running programs using a small subset of
the Ada language. Early success is the best motivation for learning computer programming, and
Ada offers a unique combination of ease of use, expressiveness, and computational power that is
not available in more popular "industrial strength" programming languages.

rap

v
vi
Chapter 0 - Getting Started

0.1 History of Computing


Devices for Computing and Record-Keeping
Digits are Fingers
Place-Value Number Systems
Mechanical Calculators
The General-Purpose Computer
From Light to Logic
The Von Neumann Architecture

0.2 The Evolution of Software Development


In the Beginning...
Machine Code
Assembly Language
High-Level Language (HLL)
Imperative (Procedural) Languages
Functional and Declarative Languages
Object-Oriented Programming
Computational Constructs

0.3 The Ada Programming Language

1
Chapter 0 - Getting Started
In this chapter, we briefly review the history of mechanical computing devices and the
programming languages that they implement. Do not be concerned if some of the terminology of
this chapter is not familiar to you. The important terms will be defined later in the text. Also, you
may refer to the glossary if you prefer.

0.1 History of Computers

Devices for Computing and Record-Keeping

For thousands of years people have used mechanical devices and symbolic manipulation
methods to do arithmetic calculations and to keep records of important events. Even before there
were names for numbers shepherds would carry a pouch full of pebbles, one pebble for each
sheep in the field. We have evidence that as long as 20,000 years ago people were making
marks on bones and other artifacts for purposes of counting. A bone dated to 8500 BC has been
discovered in Africa with marks in groups of 11, 13, 17 and 19, leading some to suggest that
1
primitive humans placed significance on prime numbers.

a. Ishango Bones - Prime Tables? b. tally sticks


Figure 0-1: Primitive Devices to Make Mathematics Simpler

In ancient times and throughout the middle ages, simple devices have been used to keep records
of business transactions. A merchant would cut notches into a tally stick indicating the number of
items taken or received, then the stick would be split lengthwise and one half given to the
2
customer as a record. The halves of the tally stick could be recombined later to verify and settle
the account.

In primary school, we learned methods for addition, subtraction, multiplication and division of
numbers by manipulating the symbols for the digits 0 through 9. Any step-by-step method for
calculating a value or solving a problem is called an algorithm.

202 11001010 11001010


x 13 x 1101 x 1101
606 11001010 1101
202_ 00000000 1101
2626 11001010 1101
11001010___ 1101_
101001000010 101001000010
a. base-10 b. base-2 c. another base-2 algorithm

Figure 0-2: Example Algorithms for Multiplication

When you learned how to multiply large base-10 numbers, you probably didn't know you were
learning an algorithm. The development of machines to perform operations like multiplication is a
natural result of the desire to reduce human labor and to increase productivity. We program

2
machines to perform similar operations in the base-2 (or binary) number system. The reason for
using base-2 will become clear later. Compare the multiplications of base-10 and base-2
numbers above. When we multiply 202 by 13 in base ten we multiply 202 by the individual digits
1 and 3. Since the 1 in 13 represents 10 we align the two products so that we are actually adding
606 and 2020. There are only two digits in the base-2 number system, namely 0 and 1. For this
reason, base-2 multiplication becomes a process of shifting and adding one of the multiplicands
to itself as indicated by the positions of the 1's in the other multiplicand. As shown in Figure 0-2b
the top multiplicand is shifted to the left 0 places, 2 places, and 3 places, then summed to obtain
the product. Figure 0-2c demonstrates that there is more than one algorithm for base-2
multiplication.

Digits are Fingers

The digits of our base-10 number system were originally the fingers of our ancestors. If people
had three fingers and a thumb on each hand our number system would probably be base-8.
Fingers were the computing tools of our ancestors and they have been used in a variety of ways
over the centuries. The ancient Egyptians used their thumbs to point at the joints of their fingers.
They devised a base-12 number system using the three joints of each finger as counters.
10
We can use our fingers as binary digits to count to 2 or 1024 on our hands. Let your right
thumb be the least significant bit of a base-2 number and start counting by raising and lowering
each finger to represent a one or a zero respectively. When counting in binary the least
significant digit changes every time, the next digit changes every other time, the next every 4th
time and so on. The value 1001111001 or 633 decimal is shown in the hand-binary number
system below.
27=128
28=256 26=64 22=4
23=8 21=2
29=512
24=16

25=32
20=1

1 0 0 1 1 1 1 0 0 1 2 = 63310

Figure 0-3: Counting in Base-2 On Our Hands

Place-Value Number Systems (1900 BC) - In a place-value number system the value of a digit
depends on both its value and its position or place in a sequence of digits representing a number.
For example the base-10 number 302 represents 2 ones, zero tens and three hundreds.

The first known place-value number system was invented by the Babylonians around 1900 BC.
This system used a base of 60 which meant that Babylonians needed unique symbols for 60
different digits (0 through 59). This may seem strange but consider that 60 is evenly divisible by
2, 3, 4, 5, 6, 10, 12, 15, 20, and 30. In our system, 10 is evenly divisible by only 2 and 5.
Originally, the Babylonians did not have a digit zero. Instead they just left a blank space as a
place-holder. For over 1000 years, this was the source of frequent errors in data recording and
calculations. Around 300 BC someone finally got the bright idea to use a symbol as a place-
3
holder for the absent digits. It would be another 900 years before the zero was accepted as an
actual value.

3
Egyptian Greek Hindu-Arabic
Maya
Babylonian Hindu
Figure 0-4: Nothing to See Here - Symbols for the Numeral Zero

If you find this interesting you might want to look into the confusion caused by the concept of
4
negative numbers, which confounded mathematicians well into the 19th century.

Negative numbers are ‘absurd’ (Chuquet, 15th Century)


Negative numbers are ‘absurd’ (Stifel, 16th Century)
Negative numbers are ‘impossible’, and ‘fictitious’ (Caradan, 16th Century)
Negative numbers should be ‘discarded entirely’ (Vieta, 16th/17th Century)
Negative numbers are ‘false’ (Descartes, 17th Century)
Negative numbers are ‘utter nonsense’ (Pascal, 17th Century)
Negative numbers are ‘larger than infinity’ and ‘less than zero’ (Wallis, 17th Century)
Negative numbers are ‘greater than infinity’ (Euler, 18th Century)
Negative numbers should be ‘rejected’ (Maseres, 18th/19th Century)
Their use will lead to ‘erroneous conclusions’ (Carnot, 18th/19th Century)
Negative numbers are evidence of ‘inconsistency’ or ‘absurdity’ (De Morgan, 19th Century).

List of quotes compiled by the New Zealand NZMaths Web Site http://www.nzmaths.co.nz/Number/

Mechanical Calculators

A wide variety of devices have been developed to help with arithmetic calculations. One of the
5 6
most widely used is the abacus, a forerunner of the mechanical calculator. This device uses a
combination of base 2 and base 5 to aid the user in performing addition, subtraction as well as
some multiplication and division. An expert abacus user can outperform the average person
using a modern electronic hand calculator.

Around 1600 AD a number of mathematicians and inventors began constructing mechanical


calculators. These devices usually consisted of a series of gears or toothed wheels with ten
possible positions (one for each digit). These wheels were connected in such a manner that one
revolution of a wheel (through its ten positions) would advance the next wheel by one position. In
this way a sequence of numbers could be added or subtracted mechanically by dialing the digits
7
on the wheels. The carry and borrow operations would be accounted for automatically by the
interactions between the wheels.

Other devices based on the use of logarithms were constructed that permitted mechanical
8
multiplication, division and root finding such as the slide rule and Leibniz's stepped reckoner.
Also between 1600 and 1800 some progress was made in the development of machines that
solved problems in logic. The reader who is interested in learning more about these machines,
their inventors, and the work leading to their creation, should refer to the exercises at the end of
this chapter as a guide.

The General-Purpose Computer

By the early 1800's elaborate mechanisms were the rage. Inventors and craftsmen were busy
creating an astonishing variety of exquisite music boxes, mechanical automatons that could draw
and write messages and even robotic ducks that could eat, quack, and simulate other bodily
functions. These mechanisms inspired Charles Babbage to devise the difference engine and
later to design the first general-purpose computer which he named the analytical engine.
Augusta Ada King, the Countess of Lovelace and Lord Byron's daughter worked closely with

4
Babbage on the design of the analytical engine. She was the only person who understand the full
potential of the machine and, in fact, made a number of innovative suggestions including the use
of subroutines, loops and using defining variables in a subscripted array.
9
At the same time that Babbage was working on his analytical engine , the British mathematician
George Boole was developing the algebra of logical connectives or Boolean algebra. His primary
contribution was to demonstrate that logical processes could be expressed as algebraic symbols.
In his book "An Investigation into the Laws of Thought", Boole makes two major arguments: (1)
the operations of the mind are subject to general laws, and (2) these laws are mathematical in
nature. While his efforts to reduce all human thought to algebraic expressions may seem to be
somewhat naive and misguided, his work has formed the basis of digital logic and has been
incorporated into the design of the modern digital computer.

a. Abacus b. Pascaline

c. Chinese Calculating Rods

d. Stepped Reckoner
e. Analytical Engine
Figure 0-5: Early Mechanical Computing Devices

From Light to Logic - By the end of the 19th century experiments with electricity were beginning to
produce some illuminating results. In 1883, Thomas Edison invented the first commercially viable
electric light. During his extensive testing of electric conductors for filaments, Edison noticed that
electrons stream from an incandescent filament to a grounded metal plate in a vacuum (the
Edison Effect).

In 1906 Lee De Forest, expanded on Edison's idea by inserting a metal grid as a third conductor
between the filament (electron emitter) and the metal plate (electron collector) and discovered
that a small current on the grid could be used to control a much larger current between the
filament and plate. His tube, called a triode made it possible to amplify signals and to switch
circuits on and off electronically. In his master's thesis, Claude Shannon noted the importance of
Boolean logic and its relation to electronic switching circuits. The earliest electronic computers
used vacuum tubes as logic switches (one or two vacuum tubes per binary bit).

5
10
One of the hundreds of racks from an IBM 701 is shown in Figure 0-6 below. This is the
circuitry required to hold one byte (8 bits) of information. The IBM 701 was the first commercial
computer designed and sold specifically for scientific computing purposes. From the late 1940's
to the present, the primary changes in the digital computer have been size, speed and memory
capacity. While these changes have been impressive, the underlying architecture of the
computer has not varied significantly. For the past fifty years we have witnessed a relatively
modest refinement of the Von Neumann Architecture.

Figure 0-6: Vacuum Tubes used as Electronic Switches in an Early Computer

The Von Neumann Architecture

John Von Neumann was a man in the right place at the right time. He had a long-time interest in
the development of an electronic computer for solving mathematical and scientific problems. He
was responsible for writing a draft report on the collaborative efforts of himself, J. Presper Eckert,
John Mauchly, and Arthur Burks, among others who were working on an improved, stored-
program version of the ENIAC (Electronic Numerical Integrator and Computer). For some reason
the draft report was released with Von Neumann's name as the sole author, hence this design is
known as the Von Neumann architecture. Later Von Neumann was part of the team that built the
EDVAC (Electronic Discrete Variable Automatic Computer) based on this design. Ironically,
Maurice Wilkes of the Cambridge University Mathematical Laboratory independently designed
and built the EDSAC (Electronic Delay Storage Automatic Computer) making it the first working
Von Neumann architecture computer.

In "The First Draft Report on the EDVAC" written by Von Neumann under contract to the U.S.
Army Ordinance Department the essential elements of a stored-program computer was
described. This report was widely distributed by the Army representative creating conflict
between Von Neumann and the other members of the research team as well as eliminating the
possibility of anyone obtaining a patent on the design. William Aspray recounts the controversial
events:

"Credit for the intellectual content of this document has been disputed for forty years.
The Draft Report, as its name indicated, was intended as a preliminary document.
However, Herman Goldstine, the Army's project administrator and a close associate
of von Neumann, distributed it throughout the United States and Britain without von
Neumann's knowledge. Because von Neumann's name appeared alone on the
report, he received all the credit for the stored-program concept. Although there is no
question that he was the sole author, several members of the Moore School staff
claim that the ideas described in the report were group results and should not be
credited to von Neumann alone. J. Presper Eckert and John W Mauchly, the senior
members of the Moore School computer team, have contended further that the
stored-program concept was fundamentally theirs.

The Moore School staff had begun to consider the prospects of storing instructions in
the machine at least eight months before von Neumann's involvement with the
project, and the ideas that went into the report were discussed in great detail by
6
Eckert, Mauchly, Arthur Burks, Goldstine, and von Neumann before the writing of the
report. However, it is also clear that von Neumann was an important catalyst and
major contributor to these discussions and that the description of these ideas in a
theoretical form, stripped of all engineering details, was his contribution alone. Much
of the controversy that ensued unquestionably stems from the differing perspectives
of practicing scientists and engineers and from the values they attach to theoretical
constructs, technical feasibility, engineering design, and construction.

The Draft Report was the source of an additional friction. Eckert and Mauchly were
interested in the commercial prospects of the computer and expected to obtain the
patent rights to both the ENIAC and EDVAC. They were concerned that the Draft
Report would jeopardize these rights, for it disclosed the technology before they had
the opportunity to flle patent. As they had feared, a federal court ruled many years
later that the distribution of the Draft Report in 1945 rendered it a public disclosure,
thereby invalidating their patent claim filed in 1947."

from Papers of John von Neumann on Computing and Computer Theory, by William Aspray,
11
Charles Babbage Institute Reprint Series, MlT Press, 1987.

So what is the Von Neumann architecture B us


anyway? Although the concept of a stored
program was part of the design of the Input/O utput
Analytical Engine and well understood by U nit
Charles Babbage and Augusta Ada King, this
M em ory
concept was temporarily lost or forgotten by the U nit
middle of the 20th century. Early electronic
computers such as the ENIAC had to be C ontrol
U nit
rewired in order to change their programs. The
"innovation" of the Von Neumann architecture
was that the computer's instructions would be R egisters

loaded into the same memory unit as the data :


being manipulated by those instructions. The
interpretation of a word in memory as either an
instruction or data would be determined by its
A rithm etic
location in the memory unit rather than its Logic
pattern of ones and zeros. For example, the bit U nit
pattern,

01000000110010010000111111011010 Figure 0-7: Von Neumann Architecture


Block Diagram
can represent the integer 1,086,918,618 or an instruction to JUMP to another location (address)
in memory or it could represent an approximation for the numeric value of π (see Section 2.7
IEEE Single-Precision Floating Point Format).

A computer program is a list of simple instructions placed into the Memory Unit along with any
data needed to run the program. These instructions are fetched by the Control Unit and executed
one-by-one. The control unit contains electronic circuitry that drives the Arithmetic Logic Unit
(ALU) by setting voltages on control lines to choose which part of the computing circuitry will be
activated for each instruction. Some instructions perform additions, multiplications or other
arithmetic operations, while some perform logical operations such as comparing the magnitudes
of two values in memory.

Other instructions do not involve the ALU at all. Instead they are used to move data between the
registers and the memory unit. Registers are special memory locations that hold instructions, and
values to be used or output by the ALU, and information about the state of a program during
execution. Taken together, the control unit, the ALU, and the registers are referred to as the
Central Processing Unit (or CPU). From main-frame to mini-computer to desktop to the hand-
held computer, the design of the CPU has remained essentially the same.

7
0.2 The Evolution of Software Development

In the beginning there were bits. The bits were all and the programmer knew that the bits were
good.

As mentioned earlier, the very first computers were "programmed" by rewiring them. Obviously,
this was not a practical method for general-purpose programming and was quickly abandoned.
With the advent of the stored-program computer, instructions were loaded electronically through
various input devices. Before the invention of the hard drive, floppy drive, and the Disk Operating
System (DOS), computers had to have their operating instructions input by hand in order to be
able to read and run application programs.

This process involved loading a few instructions manually by setting toggle switches for the bit
pattern for the instruction being loaded, setting other toggle switches for the address at which the
instruction was to be placed, and then pressing the LOAD button. This process was repeated for
each instruction entered. After a dozen or so instructions were loaded in this way, the computer
was capable of reading from an electro-mechanical input device such as a paper tape reader.
The first tape to be read contained a few hundred instructions which made up the operating
system of the computer. This tape was a few feet long and became known as the bootstrap.
Hence the term bootstrapping the computer came to mean getting the computer ready to read
and run application programs.

1
Figure 0-8: The Altair 8800 showing Front Panel and Paper Tape Reader

Machine Code

We call the programs stored and executed on a computer the software. The first generation of
software was written in machine code, which is also called binary code. The binary code is
directly readable by the central processing unit. Other than converting to base 8 (octal) or base
16 hexadecimal, there was not much that could be done to reduce the amount of labor involved
with writing and entering machine code programs into a computer. Shown below is a short
sequence of binary instructions in machine language with their equivalent representations in octal
and hexadecimal (HEX). In HEX, the letters A through F are used to represent the digits 10
through 15.
1100101000110101 145065 CA35
0010001110100010 021642 23A2
0001111001010101 017125 1E55
0101011100110011 053463 5733
0011001100001111 031417 330F
Figure 0-9: Binary Machine Code Translated into Octal and Hexadecimal
To the untrained eye, machine code is nothing more than a random collection of ones and zeros.
For the computer there is a direct correspondence between these patterns and the operations
that can be performed and locations in memory for data storage and retrieval.

Some portion of a machine code instruction is interpreted as the operations code (also called the
op-code) while the rest of the instruction is interpreted as a data value or an address in memory.
8
Each instruction is fetched from memory and loaded into the instruction register. Based on the bit
pattern of the portion of the instruction, which is the op-code, a particular operation is performed
by the computer hardware. Machine code is difficult to write and even more difficult to read. It is
particularly troublesome when the person responsible for correcting the errors is a different
person than the one who wrote it. The next advance in the evolution of software was the
development of assembly language.

Assembly Language

The second generation of software was developed to make machine code more readable. There
is a one-to-one correspondence between assembly language and machine code as shown below.

start;
MOV A,[0] 3e 00 00 0011 1110 0000 0000 0000 0000
SUB A,#4 58 04 0101 1000 0000 0100
JMP NC,endloop fb 0e 1111 1011 0000 1110
MOV A,[0] 3e 00 00 0011 1110 0000 0000 0000 0000
SUB A,#1 58 01 0101 1000 0000 0000
MOV [0],A 4e 00 00 0100 1110 0000 0000 0000 0000
ADD A,[1] 31 00 01 0011 0001 0000 0000 0000 0000
MOV [1],A 4e 00 01 0100 1110 0000 0000 0000 0001
JMP start fc e9 1111 1100 1110 1001
endloop;

Figure 0-10: An Assembly Language Program Segment and the Equivalent Machine Code

This example demonstrates the value of assembly language. Although you may not be familiar
with this language, you can understand operations such as MOVing a value from a register A to
an address 0 in memory (line one of the code segment above), or ADDing the value in register A
to the value at address 1 (line 7 above). The commands for MOVing data, ADDing, SUBtracting
and JuMPing to a new location in the program are readily understood by the human programmer
and are operations that can be performed by the computer hardware.

High-Level Language (HLL)

Machine code and assembly language are called low-level languages because they are "near" to
computer hardware. Unfortunately, programs written in low-level languages are not easily
understood by humans. By the mid 1950s, computer programs had become much more
sophisticated. IBM had demonstrated programs for business applications, scientific research,
and even language translation. However, more and more time was being spent encoding and
debugging (correcting) larger programs. The larger the programs became, the more time was
required per line of code to produce a working program. It was clear that a more efficient means
of programming was required.

FORTRAN was the first commercially successful high-level language. It was developed by John
Backus at IBM. FORTRAN stands for FORmula TRANslation and was designed primarily for
scientific research applications. The primary difference between a high-level language such as
FORTRAN and assembly language is that a few lines of a HLL can represent many lines of
assembly. In other words a single statement of an HLL may require several separate operations
for the computer. For example, setting a variable Z to the sum of two other values X and Y is a
single high-level language statement. This statement translates into three or four machine code
instructions as shown below.
Z=X+Y MOV X,A
MOV Y,B
ADD A,B
MOV A,Z
high-level language assembly language

9
Since a high-level language does not have a one-to-one translation into machine code, we need
another program to generate the machine code from the high-level language. This is
accomplished by a language interpreter or a compiler. More details on these translators will be
provided in the next chapter. For now, we will just point out that an interpreter performs a line-by-
line translation of a high-level language into machine code during program execution. A compiler
builds a complete machine code version of the high-level program before execution begins.

A few of the most popular high-level languages created on the past 50 years are listed in the
timeline below:

FORTRAN Algol-68 Miranda


LISP Pascal Java
Algol-60 Smalltalk
COBOL C C++
Simula Python
PL/I
Prolog
Basic Ada

1950 1960 1970 1980 1990 2000

Figure 0-11: Timeline for the Development of Some Popular High-Level Languages

Imperative Languages - Languages such as FORTRAN, C, and Ada are called imperative or
procedural languages because they support the development of programs that are a sequence of
commands or operations to be executed in the order provided. This style of programming is best
suited for scientific applications.

Functional and Declarative Languages - Other languages such as LISP and Miranda are called
functional since they support the evaluation of expressions. As a functional program executes,
newly scanned functions become part of the active system state. These functions are available
for processing as they are referenced by the program. A closely related type of programming
language is called declarative. Prolog is an example of a declarative language in which the
system state is changed by the entry of facts and functional relations. Once a set of facts and
relations has been entered, the environment of this running program can be asked questions
(queries). The facts and relations are scanned and compared as the system attempts to
determine an answer.

Object-Oriented Programming - More recently in the evolution of programming languages the


concept of objects has become a popular programming method. Object-oriented languages such
as Smalltalk, C++ and Java support large-scale programming by permitting the programmer to
extend the functionality of an existing program through the constructs of inheritance and
polymorphism. Simply stated these constructs support code reuse through the modification of
existing executable program elements. The main difference between procedural languages and
object-oriented languages is that object-oriented languages use encapsulation to place more
emphasis on the relationship between data and the operations performed on that data.

Computational Constructs - Although there are an enormous number of programming languages


there are only a few programming (or computational) constructs. A construct is a mechanism for
controlling the operation of the central processing unit or associated computer hardware in order
to perform some computation. There are six computational constructs: (1) sequence, (2)
alternation, (3) repetition, (4) hierarchy, (5) recursion, and (6) concurrency. The names and
specific methods of implementation of these constructs vary somewhat among languages, but the
underlying constructs remain the same.

10
sequence - enforces a particular order of execution of statements
alternation - chooses which sequence of statements to execute based on a conditional value
repetition - repeats a sequence of statements as in a loop
hierarchy - permits subprogram calls and returns
recursion - a special form of hierarchy in which subprograms can call themselves
concurrency - independent execution of two or more sequences of statements in any order

We describe the Ada implementation of these constructs in the following chapters.

Chapter 2 - data representations and assignment statements (sequence)


Chapter 3 - if...then and case statements (alternation)
Chapter 4 - loops (repetition)
Chapter 6 - subprograms (hierarchy)
Chapter 10 - recursive functions and procedures (recursion)

0.3 The Ada Programming Language

The programming language Ada was originally developed for the U.S. Department of Defense to
support the development of large real-time embedded systems. Released in 1981 the language
has been used extensively for its original purpose by both the DoD and private commercial
industry. Ada has also gained popularity in Europe and the U.K. A significant portion of the
software used to control the trains of the Chunnel is written in Ada. Ada is used in subway
systems in Hong Kong, Paris, London, and New York. It is also a popular language for
manufacturing Boeing 777 aircrafts as well as Volvos in Sweden.

The most recent release of Ada (Ada 95) supports both structured programming and object-
oriented programming. In fact, Ada 95 was the first internationally standardized object-oriented
language. Ada also supports concurrency through program components called tasks. There are
bindings to support the development of GUI applications, OpenGL graphics animations and real-
time control systems. Ada is one of the most diverse and versatile languages available today.

Most importantly for us, the Ada programming language is ideal for learning programming and
practicing with classic data structures and algorithms. It is based on a strictly enforced standard
which makes it the most portable high-level language ever devised. This means that an Ada
program written to run on one platform (CPU/operating system combination) can be run on
another platform with little or no modifications. We can begin using a small subset of the Ada
language to build simple structured programs with a minimal amount of effort. Consider the
following program to compute the area of a circle given its radius.

with ada.text_io, ada.float_text_io;


use ada.text_io, ada.float_text_io;

procedure circ_area is
pi : constant float :=3.1415926;
radius : float;
area : float;
begin
put("Enter the radius... ");
get(radius);
area:=pi*radius**2;
put("The area is........ ");
put(area);
end circ_area;

Even though you may not be familiar with Ada you should be able to understand much of this
sample program. The value of pi is provided and made into a constant to prevent its value from
being accidentally changed later in the program. The identifiers radius, and area are declared so

11
that there will be space in memory for them during program execution. The program begins just
below the reserved word begin. It first displays a request (on the default output device, i.e.
monitor) asking the user to enter the value of the radius. It then gets the value entered by the
user from the keyboard (the default input device) and places this value in the memory location
2
referenced by the identifier radius. Then the program computes the area as πr and assigns this
value to the memory location referenced by the identifier area. Finally the program displays the
statement "The area is..." followed by the computed value for the area.

12
Exercises

0.1 Conduct an online search for each of the following, and write a short synopsis of the topic.

a. Leonardo da Vinci's Mechanical Calculator


b. Ramon Lull's Ars Magna
c. Napier's Bones
d. Chinese Calculating Rods
e. Gaspard Schott - Organum Mathematicum
f. William Oughtred and the Invention of the Slide Rule
g. Wilhelm Schickard's Mechanical Calculator
h. Blaise Pascal and the Pascaline
i. Charles Stanhope and the Stanhope Demonstrator
j. Thomas' Arithmometer
k. Leibniz's Stepped Reckoner
l. Jevons' Logical Piano
m. Konrad Zuse - Z1, Z2, Z3, etc...
n. Howard H. Aiken - MARK I
o. Charles Babbage's Difference Engine and Analytical Engine
p. Augusta Ada King and her contribution to the design and development of the Analytical
Engine

0.2 Create a timeline for the history of the development of the modern computer. Include the
topics and inventors listed in Exercise 0.1.

0.3 After reading this chapter and completing Exercise 0.1, give a list of the five most important
inventions and/or theoretical breakthroughs leading to the development of the modern digital
computer.

0.4 Complete the history of the computer by giving a brief review of the invention of the transistor,
and the integrated circuit. List the inventors and the importance of their contributions.

0.5 How do you think the development of the computer would have been different if Eckert and
Mauchly would have been permitted to file for a patent for the stored-program computer?

0.6 Conduct an online search for the meaning of each of the following terms.

a. bootstrapping g. Gate's Law


b. hacker h. honeypot
c. boat anchor i. magic smoke
d. core dump j. TCP/IP
e. face time k. vaporware
f. GIGO l. wumpus

13
Chapter 0 - References
1
http://www.math.buffalo.edu/mad/Ancient-Africa/ishango.html
2
Origins of Mathematics, Donald Allen, http://personal.ashland.edu/~dwick/courses/history/origins.pdf
3
History of Mathematical Symbols, Douglas Weaver, http://www.roma.unisa.edu.au/07305/symbols.htm
4
List of quotes compiled by the New Zealand NZMaths Web Site http://www.nzmaths.co.nz/Number/
5
The Abacus, Jim Loy, http://www.jimloy.com/arith/abacus.htm
6
Chinese Counting Rods, http://auction-team.de/new_highlights/2002_11/office/033.htm
7
Pascaline - http://perso.chello.fr/users/l/ludovic1/Mes%20machines.htm
8
Liebniz' Stepped Reckoner, http://www.computer-museum.org/
9
A History of Information Technology and Systems, http://www.tcf.ua.edu/AZ/ITHistoryOutline.htm
10
The Personal Computer, A Revolution from the Ground Up, Antonio Carolina, http://www.aedo-
to.com/eng/inspiration/future/realizzati/biblioteca.html
11
The First Draft Report on the EDVAC, from Papers of John von Neumann on Computing and Computer
Theory, by William Aspray, Charles Babbage Institute Reprint Series, MlT Press, 1987.

14
Chapter 1 - Structure of an Ada Program

1.1 Definition of a Computer Program


Parts of a Program Hidden from the Programmer
Main Procedure Declarations
Executable Block
Comment Blocks

1.2 Program Components


Program Layout
Identifiers - Constants and Variables
Ada Reserved Words

1.3 From Source Code to Executable Programs


Text Editor
Interpreter
Compiler
Linking Object Code
Building an Executable Binary File

1.4 Things That Can Go Wrong


Syntax Errors
Warnings
Propagation of Error Messages
Run Time Errors
Application Errors

1.5 Software Development Lifecycle


Define the Problem
Design the Program
Encode the Source Code
Debug the Source Code
Verify the Design
Validate the Program
Document Your Work

1.6 Comments on with and use

1.7 Syntax Notation

15
Chapter 1 - Structure of an Ada Program
The elements of Ada emphasized in this text are common to most popular programming
languages. Since this book uses Ada 95 to teach introductory computer programming, we will
become familiar with the structure of the Ada language.

1.1 Definition of a Computer Program


A computer program is the implementation of an algorithm, which means that it is a description of
a sequence of operations that can be performed by a computer. Computers can read and
execute only instructions and data written in sequences of 0's and 1's called binary files. Binary
files are difficult for humans to read, write, or understand. Usually a computer programmer writes
a program in a language that is more readable than machine language but must be converted to
a machine language program before execution. Consider the simple Ada program below that
adds two values stored in the memory locations named X and Y, and places the result into the
memory location named Z. The description of a program such as the example shown below is
referred to as the program source code (sometimes called just the code or the source).

procedure sample_adder is
X : integer := 3;
Y : integer := 6;
Z : integer;
begin
Z := X + Y;
end sample_adder;

In Ada we use the term procedure to refer to the main program. The portion of a program source
code between the begin and end is called the main executable block (or just the executable
block). This is where the operations of the program are described. The area of the source code
between procedure and begin is the main declaration block (or just the declaration block) of the
program and contains a list of the variable and constant identifiers being used in the program.
Sometimes the main declaration block holds other procedures and functions (called subroutines
or subprograms) used by the main procedure. We will learn about the creation and use of
subprograms in Chapter 6.
We have discussed the functional blocks of a computer and how they are controlled using
machine language. Ada is a kind of programming language that is much more readable by
humans than machine language. In order to better appreciate languages such as Ada, let's
convert the program above into the equivalent machine language program for a simple computer.
As a first step we will convert the Ada program into an assembly program. Recall that an
assembly language program (also called assembly code) is a bridge between a high-level
language such as Ada and the executable machine language program. An assembly language
has a line-for-line correspondence with machine language but uses understandable names
(called mnemonics) for the data and instructions of the machine language program.
0 Load X 00000100 00000100
1 Add Y 01000101 01000101
2 Store Z 00100110 00100110
3 Halt 11111111 11111111
4 X 3 00000011 00000011
5 Y 6 00000110 00000110
6 Z 0 00000000 00001001
Assembly Code Binary Executable Binary Executable
Before Execution After Execution

We convert each line of assembly code into its corresponding binary executable instruction or
value. In the example above the machine language program is shown before and after execution
by the computer. The sequence of commands is executed in order from the top to the bottom of
the list. Notice that lines 4, 5, and 6 are used to hold data are referenced in the assembly
16
language program by the identifiers X, Y, and Z. Z is initialized to 0 and is used to hold the sum
of X and Y. At the end of the program, Z contains the value 9.

First, the value of the variable X (at address 00100 = 4) is loaded into the ALU input register.
Next, the value of the variable Y (at address 00101 = 5) is added to the value of X and the
result is placed in the Accumulator (output register of the ALU). Then this value is stored in the
variable location for Z (at address 00110). The conversion of source code to machine code is
the task of the compiler discussed in greater detail in Section 1.3.

High-level languages (HLLs) such as Ada make the work of writing computer programs much
easier. When we write programs using a high-level language they are the same for any platform
(computer hardware and operating system). The machine language version of a program
generated for an Intel or Athlon processor is different than that generated for a Motorola, DEC
Alpha, or mainframe computer. Being able to compile the same program on many different types
of computers without having to modify the code is called portability.
Ada is arguably the most portable computer language. This is because portability was a major
design goal for Ada from the beginning and there is a strictly enforced standard for the
implementation of the language on any computer hardware.
Portability and a language standard make Ada one of the most maintainable computer languages
in use today. Maintainability means that your Ada programs can be used without modification on
new computer hardware and new versions of a computer operating system. Essentially the same
version of any Ada program can be compiled on any computer system running almost any
operating system including Windows 98/NT/2000, any flavor of UNIX, GNU/Linux, and even Mac
OS.

Parts of a Program Hidden from the Programmer


The example program sample_adder doesn't do very much. In fact, if we were to compile and
run it on our computer we wouldn't see anything happen at all. This is because the program does
not tell the computer to show us the value of Z. Ada provides us with a set of input and output
(I/O) functions and procedures collected in a separate library or package called ada.text_io or
sometimes just text_io. There are also special versions or instances of the text_io package for
integer and floating-point (real number) I/O routines, called ada.integer_text_io and
ada.float_text_io. These will be explained in greater detail later, but for now we will learn how to
use them in our programs.
In most other computer languages I/O commands are included as a part of the language itself.
The manner in which I/O devices are interfaced with the CPU differs from one type of computer
system to another. In order to help make Ada portable, different versions of the text_io packages
are designed for each computer system.
We can modify sample_adder to display the result of the calculation using a put( ) statement.

with ada.integer_text_io;
procedure sample_adder is
X : integer := 3;
Y : integer := 6;
Z : integer;
begin
Z := X + Y;
ada.integer_text_io.put(Z);
end sample_adder;

In order to use the put( ) procedure we must first indicate that we intend to use a procedure
contained in the ada.integer_text_io package. We do this by including the statement

with ada.integer_text_io;

as the first line of our program.

17
When we reference a put( ) procedure we need to indicate where it is coming from by including
the package name as part of the put( ) statement,

ada.integer_text_io.put(Z);

The packages for ada.text_io are standard packages containing many useful functions and
procedures, which we will use in writing our programs. Some of the most frequently used
subroutines in these packages are listed below.

create( ) get( )
open( ) get_line( )
new_line put( )
end_of_line( ) put_line( )
end_of_file( ) close( )

Later we will learn how to use these routines to read and write integers, floating point numbers,
and strings of characters from the keyboard and to the monitor as well as the hard drive.

We can make our sample program a little more useful by letting the user enter the values for X
and Y rather than setting the values in the program. This can be done by using the get( )
procedure, which is also included in the ada.integer_text_io package.

The get( ) statement will cause the program to stop and wait for a value to be entered from the
keyboard. To give the user some idea of why the program is waiting we will put a line of text on
the screen asking the user to enter an integer value. The put( ) statement for plain text is in the
package ada.text_io so we must include this package in the with statement.

with ada.text_io, ada.integer_text_io;


procedure sample_adder is
X : integer;
Y : integer;
Z : integer;
begin

ada.text_io.put("Please enter an integer for X... ");


ada.integer_text_io.get(X);
ada.text_io.put("Now enter an integer for Y... ");
ada.integer_text_io.get(Y);
Z := X + Y;
ada.text_io.put("The value of Z is... ");
ada.integer_text_io.put(Z);

end sample_adder;

The program segment inside the begin...end block lists the statements in the order they are
executed by the computer. So the program will first display the text line

Please enter an integer for X... __

and then wait for the user to type in a value. Let's assume that we have entered the value 3.
When the user presses enter, the program will assign this value to the variable X and then display
the next line of text,

Now enter an integer for Y... __

We will assume that we have entered the value 6. This second value is assigned to the variable
Y. The next line of the program computes the sum of X and Y and assigns the result to the
variable Z. This operation does not display anything on the screen since the computation and

18
assignment are performed in the internal memory. The next two program lines are put( )
statements. Together, they display the line

The value of Z is...9

It is important to understand that the lines of text are included for the benefit of the user and are
not required for the proper operation of the program. However they make the program much
easier to use.

Notice that we have to include the package name before each call to a procedure defined in the
package. In order to improve the readability of the source code and to reduce the amount of
typing required, we can include a use command following the with command.

with ada.text_io, ada.integer_text_io;


use ada.text_io, ada.integer_text_io;

The use command permits us to call functions and procedures from the packages without having
to include the package name. Assuming that the correct procedure or function can be
determined at compile time we can omit the explicit reference to the package. Even though the
put( ) procedure for strings has the same name as the put( ) procedure for integers, the compiler
can determine which put( ) to use by testing the defined type of data value being passed to the
procedure. The value placed inside the parentheses is called the parameter. There is a put( )
procedure for integers, another for strings, another for characters, and yet another for floating-
point values. Including the use command allows us to rewrite our sample program as,

with ada.text_io, ada.integer_text_io;


use ada.text_io, ada.integer_text_io;

procedure sample_adder is

X : integer;
Y : integer;
Z : integer;

begin
put("Please enter an integer for X... ");
get(X);
put("Now enter an integer for Y... ");
get(Y);
Z := X + Y;
put("The value of Z is... ");
put(Z);
end sample_adder;

Main Procedure Declarations

As previously mentioned, an Ada program is divided into two main parts. The first part is called
the declaration block. This is where the constant and variable identifiers are listed and defined.
Identifiers are named memory locations to hold a specified kind (or type) of information. A
declaration of a variable identifier includes the name of the variable and a description of the type
of value it will contain.

19
Executable Block

The second part of an Ada program is the executable block. This is the part of the Ada program
that appears between the words begin and end, as shown below, and describes what the
program does. In a procedural language such as Ada, the statements in the executable block are
executed in order from the top to the bottom. Computational constructs such as conditionals and
loops can redirect the order of execution, which will be discussed in later chapters.

with ..........
use ..........
procedure sample is

Declaration Block

begin

Executable Block

end sample;

The executable block defines the operation of your program but it is not just a list of statements.
There are a number of program designs or control models used in designing the executable block
of computer programs. It is important to choose the correct control model before beginning to
implement an algorithm. Some popular control models are listed below:

Input-Process-Output Model - This is the classic control model for engineering code. The
necessary data are obtained from the user or from a file (input); the results are computed
(process) and then displayed to the user or transferred to a file (output).

Menu Driven Model - A menu of choices is displayed to the user who makes selections invoking
program actions. Control is always returned to the user through display of the menu. This model
is more versatile than the input-process-output model since the user can select from multiple
paths of execution.

Event Driven Model - In this control model, execution of the program is driven by computed
(internal) or sensed (external) data values. This model is used to implement computer
simulations as well as some hardware automations (e.g. manufacturing or flight control systems).
This is also the primary design model for programs with graphical user interfaces.

Hybrid Model - Many programs use a model that combines user and event driven control.
Examples of the hybrid model include computer games and the Anti-skid Braking System (ABS)
of your car.

Comment Blocks

Every high-level language gives programmers a means to include comments in their source code.
Comments are text descriptions embedded in the source that are ignored by the compiler.
Typically comments are preceded or enclosed in special symbols. Ada uses a double dash
preceding the line or portion of the line of the comment. Comments can be placed on a line by
themselves or at the right end of a line of source code.

20
-- This is a header comment block typically used to display
-- the programmers name, program name, copyright information
-- and/or instructions for the proper use of the source code
with ada.text_io;
use ada.text_io;
procedure comment_demo is
zero : constant integer := 0; -- comments can be appended to
an_int : integer; -- the ends of lines of code
-- comments can be inserted between lines of code
begin -- main program
-- this is null (avoid unnecessary commenting)
null;
end comment_demo;
-- comments can be placed after the source code
-- but this is less common

1.2 Program Components

We will now investigate the major components of an Ada program in more detail.
Program Layout

The sample program illustrates the typical layout of a simple procedural program. Most of the
programs we build have three components: Input, Process, and Output.

(1) Input - Get the required information from the user or from an input file.
(2) Process - Perform the indicated calculations.
(3) Output - Put the results onto the display or into an output file.

It is important to remember that a program will do exactly what we tell it to do whether or not this
is what we want it to do. In our sample program, the input, process, and output are well defined

Identifiers - Constants and Variables

The declaration block of a program contains descriptions of the variable and constant identifiers
to be used in the executable block of the program. An identifier is a name given to the place in
memory used to hold a value. For example,

X : integer;

declares X to be an integer variable. The X is the name (or the identifier) for the integer value.
The type of the value associated with the name X is integer. The declaration,

Y : float;

declares Y as the name or identifier of a memory location to hold a value of type float. We need
to tell the computer whether we are using integers or floating-point numbers because these two
types of values are stored differently in the computer's memory. (see Sections 2.1 and 2.7)

We can assign the memory location an initial value by including the assignment after the variable
type,
X : integer := 3;

The symbol := is used to indicate assignment in Ada. We can also specify an identifier to be a
constant. For example,

Pi : constant float := 3.1415926;

declares the identifier Pi to be a floating-point value equal to 3.1415926. The word constant
prevents the value of Pi from being changed during the running of the program. When we don't
21
specify an identifier as a constant, it is a variable, which means that its value can be changed by
an assignment statement or input procedure such as,

X:= X + 1;
or
get(X);

We will cover standard identifiers in detail in the next chapter.

There are specific rules for the names of identifiers:

1. Identifiers must consist of a sequence of one or more characters up to 200 (as set by the
Ada standard).
2. The first character must be a letter A..Z (Ada does not distinguish between upper and
lower case letters).
3. The remaining characters may be letters, digits or underscores.
4. No spaces or any other characters are permitted.

Ada Reserved Words

When learning a new computer language it is important to become familiar with the words that
are pre-defined and used to specify operations in the language. These are called the reserved
words of the language, which means that they cannot be used as identifiers or names of
procedures or functions by the programmer. The reserved words of Ada 95 are listed below:

abort abs abstract accept access aliased


all and array at begin body
case constant declare delay delta digits
do else elsif end entry exception
exit for function is goto if
in is limited loop mod new
not null of or others out
package pragma private procedure protected raise
range record rem renames requeue return
reverse select separate subtype tagged task
terminate then type until use when
while with xor
Ada does not recognize letter case. Which means that the names WIDGET, Widget, wIDGet, etc.
are all interpreted as the same identifier. Even though there are no restrictions on letter case, it is
good programming practice to be consistent in your use of upper and lower case letters. We will
use all lowercase for identifiers and reserved words of the Ada language. In the text we bold the
reserved words for emphasis. The GNAT Ada_GIDE text editor can be configured to color-code
reserved words in blue, as well as maintain a consistent letter case and indentation.

1.3 From Source Code to Executable Programs

In this section, we review the steps performed in the development of a working executable
program. Along the way, we will learn that the development of a computer program involves
much more than typing the source code into the computer.

Text Editor

A program's source code is a document that can be written using any text editor. However, most
computer language development packages provide a text editor that supports the building of

22
1
executable programs using special built-in commands. The GNAT (GNU-NYU Ada Translator)
Ada Integrated Development Environment AdaGIDE is shown below.

Figure 1-1: GNAT Ada Graphical Integrated Development Environment (GIDE)

The editor is divided into two main sections. The upper section is the text editor in which the
program source code is written. The lower section cannot be altered by the programmer but
contains messages from the compiler about the status of the program. If your program contains
syntax errors they will be described in this message box.
The task bar at the top of the Ada_GIDE includes buttons for compiling, building, and running the
programs. It also supports editing functions such as find, cut, copy, and paste.
In addition to the editing and compiling functions, the Ada_GIDE reformats program source code
based on the typing case and indentation rules set by the programmer. It also colors reserved
words in blue and literal strings (statements between quote marks) in green.

Interpreter

There are two common methods for converting source code into executable machine code. In
the first method, the source code is converted into machine instructions and executed one line at
a time. In this case, the program must be run inside another program called the interpreter. The
interpreter reads a line of source code and then executes it by converting it to machine code,
which is a machine readable pattern of bits. Interpreters are relatively slow since the lines of
source code must be converted to machine code each time they are encountered.

1
For more information on the GNU Project and GNAT, see Appendix C.
23
execution

translation

translation
source machine
source
code code
code
machine code

execution

interpreter compiler
operating system operating system

Figure 1-2: Operations of Interpreters and Compilers


Compiler

The other method commonly used to convert source code into machine code is to scan the entire
program source code and to convert it to machine code before execution. The program that
converts program source code into machine code is called the compiler. The compiler first scans
the source code looking for reserved words and identifiers so that it can allocate memory and
report misspelled words or improper program structure. The compiler scans the source code a
second time making sure that the functions and procedures used in the main executable are
actually provided in the specified packages or have been defined in the declaration block of the
program being compiled.

The scanned source code is converted into an intermediate form of machine code called object
code. This object code is a binary file with some addresses and code segments left unspecified
until additional information is available from the packages for associating with functions and
procedures contained in these packages. This process of association is called linking.

The compiled machine code can be executed from within the Ada_GIDE or independent of it as a
free-standing program. In either case the execution of the program is separate from the
compilation process.

Linking Object Code

Once the source code has been scanned and converted into object code, the functions and
procedures in the specified packages are compiled and appended to the main program object
code. The addresses to these functions and procedures are filled in at the correct calling points
in the object code. This process is called linking.

Building an Executable Binary File

Once the package functions and procedures have been linked to the main program object code
all remaining addresses are specified and any special initialization machine instructions are
added to the binary executable file. Once the program is built it can be run independently from
the Ada_GIDE. These executable binary files run much faster than interpreted programs on the
same system since lines of code do not have to be converted each time they are executed.

1.4 Things That Can Go Wrong

You may not be surprised to learn that programming is difficult. One reason for the difficulty is
that computers and compilers are very simplistic. This means that the instructions we give the
computer must be precise. In this section, we will review the types of things that can go wrong
with a computer program.
Syntax Errors
When reserved words of the language are misspelled or the grammar rules are broken, the
source code will not compile. A compilation failure will display a syntax error (also called a

24
compile-time error). Compilers provide the programmer with a list of error messages describing
the particular problem and usually point to the incorrect line of code. The error messages being
displayed were created by compiler designers who are computer experts. For this reason, the
messages are almost impossible to understand.

In order to help you get the most from the syntax error messages, we list below a few of the most
common and what they mean:

samp_program.adb:3:03: "put" is undefined - indicates that the procedure put is not


recognized by the compiler. This probably means that you forgot to include the with ada.text_io
package at the top of the source code, or you misspelled the package name. Common
misspelling errors include ada_text_io, ada.test_io, ada.text.io, ada.text_io. Error detected on line
3, column 3.

sample_prog.adb:4:03: "put" is not visible - the procedure put is recognized by


the compiler but is not visible (usable). This almost always means that you forgot to include a
use statement to match the with statement or you misspelled the package name in the use
statement. Error detected on line 4, column 3.

sample_prog.adb:8:03: invalid parameter list in call - a get or put procedure


has been called but contains invalid identifier(s). This can mean that you have omitted or
misspelled the name of the package for a type of identifier that you are attempting to get or put.
For example, you may be attempting to get an integer but you have not with(ed) or use(d) the
ada.integer_text_io package. (The same error message will be displayed for floating point). You
may also have called a procedure with the wrong number or type(s) of parameter(s). Error
detected on line 8, column 3.

sample_prog.adb:1:19: compilation unit expected - error in the structure of your


program source code. Many different syntax errors can produce this message. A common
reason for this error is that you have prematurely terminated a statement by using a semicolon (;)
rather than a comma (,) to separate items in a list. For example,

with ada.text_io; ada.integer_text_io; --WRONG!


use ada.text_io; ada.integer_text_io; --WRONG!
procedure sample_prog is

In this code segment, the two package names should be separated by a comma, as shown below

with ada.text_io, ada.integer_text_io;


use ada.text_io, ada.integer_text_io;
procedure sample_prog is

sample_prog.adb:4:03: declaration expected - the compiler has encountered an


executable statement too soon. This could mean that you have omitted the reserved word begin
from the start of the executable block, or you could have placed an executable expression inside
the declaration block.

sample_prog.adb:4:03: statement not allowed in declarative part - similar to


the previous error. In this case you have placed an executable statement in the portion of the
program reserved for declaring constants and variables. A common cause for this error is the
use of := rather than : in a declaration.

X := float; --WRONG!
n : integer; --OK
Y : float; --OK
Z : float := 3.0; --OK

sample_prog.adb:6:07: "n" is undefined - The identifier n is being referenced in the


execution block but has not been declared in the declaration block of the source code.
25
sample_prog.adb:4:15: ":" should be ":=" - This is an instance of an error message
that actually means what it says. You receive this message when you attempt to assign a value
during identifier declarations but use a colon rather than a colon-equals for the assignment.

n : integer : 3; --WRONG!

sample_prog.adb:6:03: declarations must come before "begin" - a declaration


has appeared in the execution block of the program. Actually, this is usually the result of omitting
the equals sign from an assignment statement.

n : 3; --WRONG!
n := 3; --OK

sample_prog.adb:11:02: statement expected - general error in the structure of the


source code. This error message is displayed whenever the compiler gets lost in the syntax.
Many different errors can produce the message, but they are usually easy to spot. Have the
compiler point to the incorrect line of code and begin proof-reading down to the indicated position.
A common error associated with this message is the reversal of an assignment statement.

3 := n; --WRONG!
n := 3; --OK

Warnings

A warning message indicates that there is a potential problem in your source code but the
compiler can still build an executable program (assuming there are no other syntax errors). A
common warning message is:

sample_prog.adb:4:03: warning: "n" is never assigned value

In this case, an identifier has been declared and is being referred to in the execution block but is
never given a value. This is not a problem when you are testing a partially completed program in
which you have declared identifiers that are not yet being used.

On the other hand, if your program is performing computations with variables that have never
been assigned a value, it may produce unpredictable results. This is because every identifier that
is declared is associated with a word of memory that contains some value left from the last time
that memory location was used. It is good programming practice to initialize all variable identifiers
with some default value.

Propagation of Error Messages

Sometimes a single syntax error will generate several error messages called propagation errors.
For example, omitting or misspelling a package name will produce a separate error message for
each time your program calls a function or procedure that is defined in that package.

When debugging source code it is important to only change the lines of code you know contain
mistakes. If you are uncertain about the cause of some of the messages, concentrate on
correcting the error that is generating the first error message. Once you have found the first
syntax error, recompile to make sure that other error messages are not propagation errors
caused by an earlier syntax error.

Run Time Errors

In addition to syntax errors, your program can contain errors that appear when the program is
running. This means that some incorrect source code will compile successfully. It is likely that
every large program that exists contains errors. Consider the operating system of your favorite

26
desktop/laptop computer. Has it ever locked-up for no apparent reason? This is probably due to
a run-time error in the operating system or program you are using.

When writing your own programs, runtime errors are more difficult to correct than syntax errors
because there is less support provided by the compiler for locating them. Sometimes a runtime
error will not occur while you are testing your program. They can remain hidden until a particular
data condition or system state occurs exposing the error and crashing the program. It is
practically impossible to eliminate all run-time errors from large programs. However, we can
minimize their occurrence by improving our programming skills and by employing good
programming practices. (see Section 1.5)

Application Errors

Even when all the syntax and run-time errors have been eliminated, a program can still have
errors.
with ada.text_io, ada.integer_text_io;
use ada.text_io, ada.integer_text_io;

procedure application_error_demo is
p,q,r : integer;
begin
--input section
put("Enter an integer........ ");
get(p);
put("Enter another integer... ");
get(q);
new_line;
-- process section
r:=p*q; -- what's wrong with this picture?
-- output section
put("The sum of these two numbers is... ");
put(r);
end application_error_demo;

This program compiles and executes normally. It even gives the correct answer for some values
of p and q. Otherwise this program outputs an incorrect answer. This type of error is called an
application error or logic error. Application errors are the origin of the computer term "garbage in,
garbage out" (GIGO).

1.5 Software Development Lifecycle

As a novice programmer, the programs that you write will be relatively short. As you gain
experience in designing and implementing software, your programs will be more involved. For
this reason, it is important to develop good programming habits now. There is a branch of
computer science called Software Engineering, which is concerned with achieving program
correctness through strict methodologies to manage the software development lifecycle.

For now, we will not worry about the details of formal software engineering methods. However,
we can reduce the number of programming errors and improve the readability and reusability of
our source code by learning and implementing good programming practices, borrowed from the
software engineering community.

The most important lesson to be learned from software engineering is, good programs are
designed before the source code is written. In other words, we should have a good idea of the
problem we are solving, the data we will need and the algorithm we will implement, before we
begin slapping the keyboard. Figure 1-3 gives the steps in the software development lifecycle.

27
Define

Design

Encode

Debug

Verify

Validate

Document

Figure 1-3: The Software Development Lifecycle

As shown above, the development of software is not a linear process. As we progress with the
software development process, we may find ourselves returning to the source code to correct
syntax errors, returning to the program design to correct runtime errors, or even returning to the
problem definition to correct application errors. We discuss these steps in greater detail below.

Define the Problem - The first step in software development is to clearly define the problem being
solved. Many errors and costly delays are caused by proceeding to the design step without a
clear understanding of, or agreement on, the specifics of the problem being solved. A good way
to clarify the details of the problem definition is to define a set of tests with expected results that
will be used to validate program correctness. This is especially important when the
programmer(s) and the end-user(s) or customer(s) are different persons.

Design the Program - Once everyone has agreed on the problem being solved, the programmers
can begin designing the software solution. An important part of this step is to decide the type of
programming model being used. We will discuss programming models in detail in Chapter 9. For
now, we can say that the design step includes determining the data structure (i.e. the identifiers
for input, output, and internal process and their data types), and the algorithms being
implemented. Many times the algorithms can be written in a list of statements describing the
operations to be performed. This is called pseudocode because it is similar to source code but is
more general and not a real programming language. Pseudocode should be written with
sufficient detail so that an experienced programmer can convert it into a real computer language
program. During the design step the programmer(s) should develop a set of tests and expected
results that will be used to verify the correct execution of the program and/or its components.

Encode the Source Code - Encoding is the process of converting the algorithms and pseudocode
into actual source code. This source code is typed into a text editor and saved for conversion into
a binary file by the language compiler.

Debug the Source Code - When there are errors in the source code (which is nearly every time a
source code is written) the programmer corrects them. The process of correcting program errors
is called debugging the software. As discussed above, syntax errors are quickly detected and
relatively easily corrected. Runtime errors are more difficult to correct because they usually
require a correction of the program implementation. The most difficult types of errors to correct
are application errors since they require a change in the program design or even a redefinition of
the problem being solved.

Verify the Design - Verification is the process of making sure that the software correctly
implements the specified design. We are concerned here, that the design has been correctly
28
implemented. In commercial software development, the results of code verification are usually
limited to the programming team assigned to the project. If code components are not verified,
they are returned to the designer/encoders for modification.

Validate the Program - Validation is the process of making sure that the program is solving the
problem we originally intended. We are concerned here, that we have, in fact, correctly
interpreted the problem statement. In large, commercial software development projects, program
validation involves the end-user and can determine if the program is considered deliverable (i.e.
must be paid for). This is another reason why it is a good idea for the programmer(s) and the
end-user(s)/customer(s) to agree on a validation test set as part of the problem definition step. It
is common that the customer will wish to make changes during the software development
lifecycle and having a written agreement on what constitutes the finished product will reduce the
chances of misunderstandings.

Document Your Work - Documentation is an essential part of the software development lifecycle.
There are many different forms of documentation, including formal descriptions of each line of
source code (sometimes called Equations In Order of Solution, EIOS), data descriptions, input-
output requirements lists, control flow diagrams, verification and validation test sets, user's
manuals, software libraries, and comments in the source code itself.

In addition to technical documentation, large commercial software projects need legal


documentation. As discussed above, it is important to include a detailed description of the
required functionality of the program. This is best accomplished by providing a set of test data,
settings, or interface events and the expected results or operations of the program. Sometimes it
becomes clear that modifications are necessary in the program design. When the customer(s)
and the programmer(s) agree to modify a program, these modifications should be defined, in
writing.

Throughout the text, we will borrow from these software engineering principles, as appropriate, to
help to improve our software designs and programming style.

1.6 Comments on with and use


Throughout this text we will make use of the use statement to simplify our sample program code.
Later we will see that in some cases the use statement can create an ambiguous reference to a
function or procedure. To understand how this can happen you need to understand that Ada
permits overloading. Overloading is the use of the same procedure or function name to perform
different but related operations.

For example, the put( ) procedure used to display text strings on the monitor is a different
procedure from the put( ) procedure used to display the integer Z. Even though both put( )
procedures have the same name, we can still employ both the ada.text_io and
ada.integer_text_io packages in the use statement without causing confusion because the two
put( ) procedures have different types of parameters (string versus integer).

In very large programs, the likelihood that two different programming teams would create
packages containing functions or procedures with the same name and parameter lists is high.
When this occurs the compiler/linker will not be able to determine which function or procedure to
use in a particular instance. This problem is corrected by specifying the proper package for the
offending function or procedure.

1.7 Syntax Notation


Throughout this text we will use syntax notation to describe the form of elements of the Ada
language. This notation is based on a formal grammar called the Backus-Naur Form (BNF). The
rules of syntax notation are summarized below:

Lower case italic words will be used to denote syntactic categories, also called non-terminals. A
syntactic category is a general term referring to a category of language elements.

29
For example,

assignment_statement
sequence_of_statements
condition
expression

Boldface words will be used to denote reserved words, such as

procedure demo is
declaration_block
begin
executable_block
end demo;

Square brackets are used to enclose optional language elements, while curly brackets enclose
elements that may be repeated zero or more times. For example, the syntax notation for the
if..then construct is,

if_statement ::=
if condition then
sequence_of_statements
{elsif condition then
sequence_of_statements}
[else
sequence_of_statements]
end if;

Note that the italicized terms do not refer to a specific item but to a category of items. Thus the
condition in the first line is not necessarily the same as the condition in the third line. The elsif
construct may be omitted or repeated as many times as desired. The else construct is optional
and can be included at most once. The ::= is part of the Backus-Naur grammar and indicates
that what follows is a definition of the form of an if_statement.

A vertical line separating two items means that either one (but not both) of these items will be
placed in this position. For example,

return_statement ::= return [expression];


return_statement ::= return; | return expression;

are equivalent.

The indentation and line breaks demonstrated in syntax notation are recommended for the
constructs being described. Otherwise the preferred places for line breaks are after semicolons.

Semicolons in Ada are statement terminators. The rules for placement of semicolons will be
discussed in detail for each language construct as they are introduced. Generally, we will
terminate each statement (including those appearing in a compound statement) with a semicolon.

if X=0 then
Y:=1;
Z:=2;
else
Y:=1/X;
Z:=1;
end if;

30
The if...then...else...end if statement is terminated with a semicolon as are each of the
assignment statements for X, Y and Z. However the then and else lines do not include
semicolons because these items are not complete statements themselves but rather are parts of
the if...then...else...end if construct.

31
32
Chapter 2 - Data Representations in a Computer

2.1 Numeric Data Types


Integers
Floats
Attribute Functions
Enumeration Types

2.2 Character Types

2.3 String Types

2.4 Boolean Types


Relational Operators
The in range operator
Boolean Operators
The and then and or else Operators

2.5 Assignment Statements


Arithmetic Operators
Operator Overloading

2.6 Type Conversions

2.7 Internal Number Representations


Unsigned Integers
Signed-Magnitude
Two's Complement
Excess Notation
IEEE Single-Precision Floating Point Format

2.8 Applications
Personal Screen Message
Pepperoni Pizza

33
Chapter 2 - Data Representations in a Computer
In this chapter, we review the standard data types defined in the Ada programming language, and
we discuss their machine representations. Studying the standard data types and their use will
help improve our understanding of Ada and programming languages in general. As we review
the sample programs in this chapter, we will become familiar with the reserved words of the Ada
language.
2.1 Numeric Data Types
We use integers or whole numbers for counting discrete quantities such as the number of cards
in a deck or the number of beans in a jar. We use real numbers or floating-point values to
measure continuous quantities such as gallons of gasoline or miles traveled. We are so
accustomed to their proper use that we usually don't concern ourselves with the differences
between integers and real numbers. Since the integers are a subset of the real numbers, we are
not concerned with their differences in everyday use. However, we need to be more precise
when we use a computer to represent and manipulate numeric values. This is because the
internal (machine) representations of these two number types are different.
In order to use numbers properly in a computer program, we must learn a little about how they
are represented in the computer's memory. The most important things to remember concerning
the representation of numbers in a computer is that these representations are not exact and the
mathematical operations performed by the computer are only simulations of actual mathematical
operations.
Integers
As we have seen, all data in a computer are represented using binary (base-2) values. Integers
in Ada running on a typical desk-top computer are represented using 32-bits of memory. Both
positive and negative integers must be represented using these 32 bits, which means that one of
the bits must be used to indicate the sign of the value being stored. This leaves 31 bits to
represent the magnitude. But since only a finite number of values can be represented using a
finite length binary value, there is a largest and smallest integer that can be contained using a 32
bit data value or word.
The binary version of the value +27 is shown below:

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

These 32 bits can represent a very large but finite integer. The largest positive integer that can
be represented using 32 bits is 0111111111111111111111111111111. This is equal to
2,147,483,647 in base-10. With Ada, we can discover the largest and smallest integer values that
can be represented on our computer by using the attribute functions 'first and 'last. (We will
encounter a more comprehensive discussion of attribute functions later in this section.)
with ada.integer_text_io;
use ada.integer_text_io;

procedure first_last_integer is
begin
put(integer'first);
put(integer'last);
end first_last_integer;

This program displays the first (most negative) and the last (most positive) integer values which
can be represented in 32 bits. Note the tick-mark separating the data type integer and the
attribute functions 'first and 'last. The output values obtained from this program are,
-2,147,483,648 and 2,147,483,647
The put( ) procedure provided in Ada permits us to control the number of spaces occupied by an
integer being displayed. As shown in the example program below, including a WIDTH parameter
in the put( ) statement sets the width of the space into which the integer will be placed. That is,

34
put(N,WIDTH=>6);

forces the value of the integer N to be displayed right-justified in 6 spaces. This is an example of
the use of a named parameter. In this example, the named parameter WIDTH is set to 6. Ada
also permits positional notation for formatting. The put( ) statement above is equivalent to,

put(N,6);

We will use positional notation for formatting in the remainder of the text.

with ada.text_io, ada.integer_text_io;


use ada.text_io, ada.integer_text_io;

procedure integer_format_demo is

N : integer := 123;
M : integer := 12345678;

begin
put(N);
new_line;
put(N,WIDTH=>6);
new_line;
put(N,6);
new_line;
put(N,2);
new_line;
put("The value of N is ");
put(N,0);
put(" and M is ");
put(M,0);
new_line;
end integer_format_demo;

123 "leading blanks are shown as "


123
123
123
The value of N is 123 and M is 12345678
Figure 2-1: Sample Output of integer_format_demo.adb

Note that the value is right-justified so additional blank spaces, if any, will be on the left of the
displayed value. Also, note that the complete integer is displayed, even when the specified width
is less than number of spaces needed. A width of 0 can be used to ensure that the integer
occupies the minimum amount of space on the display.

Floats

Real numbers are values that can include fractional parts such as 0.0005, 1234.5678, 3000.0 or
3.1415926. Real numbers are referred to, in Ada, as float types. Standard (single-precision)
floating-point values are also maintained in a 32-bit word, in a format similar to scientific notation.
Part of these 32 bits is used to define the magnitude of the number while another part is used to
indicate the power of the base of the number. One bit is used to represent the sign of the
number. We can use the attribute functions 'first and 'last to find the limiting floating point
numbers that can be represented in Ada.

with ada.float_text_io;
use ada.float_text_io;
35
procedure first_last_float is
begin
put(float'first);
put(float'last);
end first_last_float;

This program outputs the two values -3.40282E+38 and +3.4028E+38. Just as with integers the
computer can represent only a finite range of real values. It is important to realize that this range
can be different on different platforms. The notation used by Ada to display floats is a little
cryptic. For example, the E in the values shown above represents the exponent of a base-10
number, so we have,
38 38
-3.4028E+38 = -3.4028 x 10 and 3.4028E+38 = 3.4028 x 10 .

Ada supports a higher precision (and larger range) floating point data type called long_float.
Long floats can be important when we are writing some scientific applications. The range and
precision of long floats can be measured using 'first and 'last.

with ada.long_float_text_io;
use ada.long_float_text_io;

procedure long_first_last_float is
begin
put(long_float'first);
put(long_float'last);
end long_first_last_float;

The output of this program is -1.7976931486232E+308 and +1.7976931486232E+308. In some


other languages extended precision floating point values are called doubles. Believe it or not,
Ada supports an even higher precision floating point data type. You can probably guess the
name of this data type. That's right, they are called long_long_float types. The range of
long_long_floats is,

-1.18973149535723177E+4932 to +1.18973149535723177E+4932

No, long_long_long_float is not a predefined data type.

Just as with integers, we can control the format of displayed floats, but in this case we have three
formatting parameters to specify,

FORE=> sets the number of spaces for digits before the decimal point
AFT=> sets the number of spaces for digits after the decimal point
EXP=> sets the number of spaces for digits in the exponent (if scientific notation)

The whole number part of the displayed float is right-justified in the space provided by the FORE
parameter. The decimal point occupies the next space to the right. The fractional part of the
displayed float begins just to the right of the decimal point and continues for the number of digits
specified by the AFT parameter. Compare the program float_format_demo.adb shown below
with its sample output given in Figure 2-2.

with ada.text_io, ada.float_text_io;


use ada.text_io, ada.float_text_io;

procedure float_format_demo is

X : float:= 12.3456;

begin

36
put(X);
new_line;
put(X,FORE=>5,AFT=>6,EXP=>2);
new_line;
put(X,5,6,2);
new_line;
put(X,5,6,0);
new_line;
put(X,1,1,0);
new_line;
put("The value of X is ");
put(X,0,4,0);
new_line;
put("The value of X is approximately ");
put(X,0,3,0);
new_line;
end float_format_demo;

1.23456E+01
1.234560E+1
1.234560E+1
12.345600
12.3
The value of X is 12.3456
The value of X is approximately 12.346
Figure 2-2: Sample Output of float_format_demo.adb

Attribute Functions
As we have seen, simple data types have attributes that are recognized by Ada. We can use the
attribute functions provided in Ada to obtain information about the data type in general or about
specific values of a data type. Example attribute functions are listed below:
integer'first - returns the first (most negative) value of type integer
float'last - returns the largest (most positive) value of type float
float'digits - returns the number of digits precision of the type float
color'width - returns the length (number of character spaces) needed to display values (or
names) of the type color (a user-defined enumerated type). This attribute function also works
with integer types.
color'image(col) - returns the string version of the color type identifier col
color'value(a_color_string) - returns the value of the enumerated type color
corresponding to the string a_color_string.
integer'pred(a_num) - returns the integer that is the predecessor of a_num
integer'succ(a_num) - returns the integer that is the successor of a _num
float'min(X,Y) - returns the smaller value X or Y
float'max(X,Y) - returns the larger value X or Y
character'pos(char) - returns the ASCII value (an integer) of the character char
character'val(num) -returns the character whose ASCII value is the integer num

For a complete discussion of attributes, please refer to the Ada Language Reference Manual
1
(LRM).

1
An electronic version of the LRM is accessible from within Ada_GIDE.

37
Enumeration Types

Ada supports a user-defined type called an enumeration type in which a finite list of items can be
associated with labels of names rather than integer values. With an enumerated type, names for
a finite set of values can be used to provide meaning for what is essentially a sub range of
integers. For example, we can declare an enumeration type for colors as

type color is (RED,ORANGE,YELLOW,GREEN,BLUE,VIOLET);


col : color;

The items RED, ORANGE, etc are not variables but values of the type color. Any identifier of
type color can be assigned one of these values. It is important to understand that the values of
an enumeration type are not just a sequence of characters. Therefore we can assign the value
ORANGE to col by,

col := ORANGE; --OK

With a little effort we can display enumerated types in Ada. Consider the following.

with ada.text_io, ada.integer_text_io;


use ada.text_io, ada.integer_text_io;
procedure enumeration_demo is
type color is (RED,ORANGE,YELLOW,GREEN,BLUE,VIOLET);
package my_color_io is new ada.text_io.enumeration_io(color);
use my_color_io;
col : color;
begin
col:=YELLOW;
put(col);
new_line;
put(color'pred(col));
new_line;
put(color'succ(col));
new_line;
put(color'first);
new_line;
put(color'last);
new_line;
put(color'pos(BLUE),3); -- position numbering starts at zero
new_line; -- so BLUE is in position 4
end enumeration_demo;

The sample output for enumeration_demo.adb is provided in Figure 2-3 below. In order to
display the values of the enumeration type we had to instantiate the package enumeration_io.
Please do not be concerned about the details of instantiation that appear in the declaration block
of this program. We will study packages and instantiation in Chapter 8. For now let's look at the
results of the sequence of put( ) statements. YELLOW is the value of the identifier col, the value
ORANGE is the predecessor of YELLOW in the enumeration list, and GREEN is the successor of
YELLOW. RED is the first element and VIOLET is the last element of the enumeration list. The
pos (position) attribute is an integer indicating the position of an item in the list. This count starts
at 0 rather than 1, so BLUE occupies position 4 in the enumeration list.
YELLOW
ORANGE
GREEN
RED
VIOLET
4
Figure 2-3: Sample Output of enumeration_demo.adb
38
2.2 Character Types

Each key press on the keyboard produces a character. Characters can be letters, digits,
punctuation marks or other symbols. Just like numeric types, characters are internally
represented in the computer as binary values. The most popular coding standard for characters
is called the ASCII (American Standard Code for Information Interchange) code.

In the ASCII code each character is represented using one byte. The table below gives the
decimal number corresponding to the binary code for the printable characters. In addition to
printable characters, the ASCII code defines display commands called control characters, such
as line-feed, tab and backspace.

Table Showing Printable Characters and their ASCII Values in Decimal


32 33 ! 34 " 35 # 36 $ 37 % 38 & 39 '
40 ( 41 ) 42 * 43 + 44 , 45 - 46 . 47 /
48 0 49 1 50 2 51 3 52 4 53 5 54 6 55 7
56 8 57 9 58 : 59 ; 60 < 61 = 62 > 63 ?
64 @ 65 A 66 B 67 C 68 D 69 E 70 F 71 G
72 H 73 I 74 J 75 K 76 L 77 M 78 N 79 O
80 P 81 Q 82 R 83 S 84 T 85 U 86 V 87 W
88 X 89 Y 90 Z 91 [ 92 \ 93 ] 94 ^ 95 _
96 ` 97 a 98 b 99 c 100 d 101 e 102 f 103 g
104 h 105 i 106 j 107 k 108 l 109 m 110 n 111 o
112 p 113 q 114 r 115 s 116 t 117 u 118 v 119 w
120 x 121 y 122 z 123 { 124 | 125 } 126 ~
160 161 ¡ 162 ¢ 163 £ 164 ¤ 165 ¥ 166 ¦ 167 §
168 ¨ 169 © 170 ª 171 « 172 ¬ 173 - 174 ® 175 ¯
176 ° 177 ± 178 ² 179 ³ 180 ´ 181 µ 182 ¶ 183 ·
184 ¸ 185 ¹ 186 º 187 » 188 ¼ 189 ½ 190 ¾ 191 ¿
192 À 193 Á 194 Â 195 Ã 196 Ä 197 Å 198 Æ 199 Ç
200 È 201 É 202 Ê 203 Ë 204 Ì 205 Í 206 Î 207 Ï
208 Ð 209 Ñ 210 Ò 211 Ó 212 Ô 213 Õ 214 Ö 215 ×
216 Ø 217 Ù 218 Ú 219 Û 220 Ü 221 Ý 222 Þ 223 ß
224 à 225 á 226 â 227 ã 228 ä 229 å 230 æ 231 ç
232 è 233 é 234 ê 235 ë 236 ì 237 í 238 î 239 ï
240 ð 241 ñ 242 ò 243 ó 244 ô 245 õ 246 ö 247 ÷
248 ø 249 ù 250 ú 251 û 252 ü 253 ý 254 þ 255 ÿ

The characters '0' through '9' have ASCII values 48 through 57 respectively. The uppercase
letters are listed in alphabetical order and have ASCII values 65 through 90. The lowercase
letters have ASCII values 97 through 122 and are each 32 above their uppercase equivalents.
The blank space ' ' is a character with ASCII value equal to 32.

As with the other data types we have studied, we do not have to deal with the internal
representation of characters in order to use them. We can declare a variable or constant
identifier as type character and assign it a character value as,

kyle : constant character := 'Z';


eric : character := '3';
stan, kenny : character;

As shown above, literal characters are placed between single quotes in Ada.

2.3 String Types

A string type contains a sequence of characters of a fixed length such as,

word : string(1..28);

39
which declares the identifier word as a string type to hold exactly 28 characters. Strings can also
be declared implicitly using the type string without specifying the length. Instead, we can assign a
literal string to a string identifier and let the Ada compiler determine the length. For example,

a_word : string := "establishment";

In this case, the identifier a_word is constrained to a string of 13 characters since the initializing
value "establishment" is 13 characters long. The identifier can be assigned a value during
program execution, but the number of characters in the string literal being assigned to an
identifier must match the original length as specified during the type declaration. For example,
the identifier word can be assigned as,

word := "antidisestablishmentarianism";

since this string is 28 characters in length.

Substrings within strings can be accessed using range parameters. We can extract the substring
"establishment" (the 8th through 20th characters) from the string word and place it in the first 13
positions of the string piece_of_word in the following manner:

piece_of_word(1..13):=word(8..20);

There is an automatic type conversion between character data types and the individual
characters in a string data type. For example we can extract a character from the string word by,

char := word(24);

In this example the letter 'a' was assigned to the character identifier char. This process is
reversible, meaning that a character can be embedded into any position of a string by,

word(24):=char;

where char is a character data type and word(24) is the 24th position in the string identifier word.

The ada.text_io package includes input and output procedures for strings. The put( ) procedure
is used to display strings such as,

put(" Hello there! ");


put("Enter the value for X... ");
put(a_string);

where a_string is an identifier of type string. We can also use the procedure put_line( ) to display
a string and skip to a new line of the output without using the new_line procedure.

put_line(" Main Menu");


put_line(" 1. Load Data");
put_line(" 2. Save Data");
put_line(" 3. Quit");
put_line("Enter your choice... ");

This example code segment displays the following.

Main Menu
1. Load Data
2. Save Data
3. Quit
Enter your choice... _

Permitting the user to input strings from the keyboard is a bit more involved. When we use a
get( ) procedure to accept user input of a string we must make sure that the user enters the exact
40
number of characters specified in the declaration of the string variable. Assuming that the string
variable a_string is declared to hold 10 characters,

a_string : string(1..10);

We must require that 10 characters are entered.

put("Enter your name... ");


get(a_string);

If less than 10 characters are typed before the <Enter> key is pressed, the get( ) procedure will
wait for the additional characters. On the other hand, if more than 10 characters are entered
before the <Enter> key is pressed, the get( ) procedure will assign the first 10 characters to the
string identifier a_string, leaving the remaining characters in the keyboard input buffer. The
program will attempt to use these leftover characters the next time a get( ) procedure is called.
This can cause great confusion when attempting to debug a program that accepts string input.

Ada provides another procedure called get_line( ) for inputting strings. The get_line( ) procedure
uses two parameters. The first is a string variable to hold the string being input and the second is
an integer used to indicate the number of characters entered. The following code segment
illustrates how this procedure is used.

put("Enter your name... ");


get_line(a_string,leng); -- a_string is declared as shown above
put("Hello, ");
put(a_string(1..leng));
put(". Welcome to Ada!");

Assuming that the name Doug Wells is entered, the get_line( ) procedure will assign these 10
characters (including the blank space) to the first 10 character positions in the string a_string.
The get_line( ) procedure will also set the value of the integer leng to 10. This example code
segment generates the following output.

Enter your name... Doug Wells


Hello, Doug Wells. Welcome to Ada!

2.4 Boolean Types

A Boolean type has a value of TRUE or FALSE. Variable and constant Boolean identifiers are
declared as,

say_it_aint_so : constant boolean := false;


whynot : boolean := true;
huh : boolean;

Relational Operators

Since they cannot be directly displayed, the use of Boolean values is not as clear to the novice
programmer as other data types like integers or floats. Saving and transferring the results of
numeric comparisons is an important application for Booleans. The result of the expression 2+2
is the value 4, however, the result of the expression 2+2 > 3 has the value TRUE.

We can compare the magnitudes of numeric data types using the relational operators.
Ada defines the following relational operators.

< less than 3<4


<= less than or equal to 3<=3
41
= equal to 3=3
>= greater than or equal to 4>=4
> greater than 4>3
/= not equal to 4/=3

Boolean variables can be assigned the results of numeric comparisons as shown in this sample
program.

with ada.text_io;
use ada.text_io;
procedure bool_test is
bool_1,bool_2 : boolean;
X : integer := 3;
Y : integer := 4;
begin
bool_1 := X<Y;
bool_2 := X=Y;
if bool_1 then
put("X is less than Y");
end if;
if bool_2 then
put("X is equal to Y");
end if;
end bool_test;

Since 3 is less than 4, this program displays

X is less than Y.

We can also compare characters and strings using relational operators. For example, 'A' < 'B' is
TRUE while 'a'<'A' is FALSE. When comparing characters using relational operators, we are
actually comparing their ASCII values. Since the lowercase letters have larger ASCII values than
the uppercase letters, lowercase letters are considered to be "greater than" (>) uppercase letters.
This is an important fact to remember when we attempt to sort characters and strings
alphabetically. For instance,

"ape" < "apple" is TRUE but "ape" < "APPLE" is FALSE

These examples demonstrate the use of Boolean data types but are not very useful otherwise.
We will make better use of Boolean data types when we combine them with other programming
constructs.

The in range operator

In Ada we can test if a particular value is in a specified range of values of the same type. The
Boolean variable bool_val is assigned TRUE in the following examples,

bool_val := 3 in 2..4;
bool_val := 1.234 in 1.0..2.0;
bool_val := 'M' in 'A'..'Z';
and assigned FALSE in these examples,
bool_val := 1 in 2..4;
bool_val := 2.234 in 1.0..2.0;
bool_val := 'm' in 'A'..'Z';

42
Boolean Operators

So far we have seen how numeric, character, and string types can be compared to produce
Boolean values. We can also compare and combine Booleans to generate Boolean values. The
Boolean operators and, or, xor, and not can be used to build logical statements. Assume that A,
B, C, and D are Boolean data types in the following examples of logical assignment statements.

C := A or B; C is TRUE if either A or B or both are TRUE


D := A and (B or C); D is TRUE if A is TRUE and either B or C it TRUE
A := not B; A is TRUE only if B is FALSE.
B := C xor D; B is TRUE if C or D is TRUE but not both.

In order to know the truth value of statements such as these, you would have to know the truth
value of each of the Boolean identifiers. The Boolean operators used in these statements are
defined in terms of their effects on all possible truth values of their operands. The truth table
below shows the resulting truth value for each Boolean operator (F=FALSE, T=TRUE).

A B A or B A and B A xor B not A


F F F F F T
F T T F T T
T F T F T F
T T T T F F

Although it is good programming style to use parentheses to indicate the intended order of logical
operations, there is a predefined order of precedence. The not operator is a unary operator (only
one operand needed) and has the highest precedence. The and operator has the next highest
precedence. The or and xor operators are equal in precedence and therefore are evaluated in a
left to right order.

The and then and or else Operators

Ada provides two more Boolean operators not offered by most other high-level languages. The
and then and the or else operators are used to implement a feature called the short circuit. The
examples below illustrate the use of these operators.

if Y/=0.0 and then Z>X/Y then


put("yadda yadda yadda");
else
put("blah blah blah");
end if;

Notice that if Y=0.0 the division X/Y would not be a valid mathematical expression (either infinite
or undefined). The and then operator allows the second operand to be evaluated only if the first
operand is TRUE. Otherwise, evaluation of the second operand is skipped avoiding a run-time
error.

if A or else B then
put("one of these is true");
else
put("neither of these is true");
end if;

In this example, the or else tests the truth value of B only if A is FALSE. These two special-
purpose Boolean operators generate the same logical result as AND and OR but implement the
short circuit feature to prevent errors and/or reduce the number of comparisons needed.

43
2.5 Assignment Statements

We have been using the assignment operator := to store values into variables in the declaration
block and in assignment statements in the executable block of sample programs. We now
formally introduce the assignment operator.

The expression
X := Y + Z;

assigns the result of the expression Y+Z on the right side of the assignment operator to the
variable identifier X on the left side of the assignment operator.

It is important to understand that assignment is not equality. The right side of an assignment
statement is evaluated first and then the resulting value is assigned to the identifier on the left-
hand side. This permits assignments such as,

X := X + 1;

In this example the value of X is incremented by 1.

Even though it appears in a single statement in a high-level language, implementing an


assignment statement is an involved process. Data values must be retrieved from their memory
locations, any operations appearing of the right-hand side of the assignment must be performed
and the result is stored back into memory at the location associated with the identifier appearing
on the left-hand side of the assignment operator. The figure below illustrates this process in
detail.

1 X 42

X 42 42 + 1

43

3 X 43

Figure 2-4: Example Operations Performed in the Assignment X:=X+1

The assignment occurs in three steps:


(1) the value in the memory location labeled X is accessed;
(2) the value of X is added to 1;
(3) the result is placed into the memory location labeled X.

Arithmetic Operators

High-level languages support arithmetic operations on integer and floating point data types. The
operations of addition, subtraction, multiplication and division are common to all high-level
languages. Ada supports exponentiation (raising a value to a power) but this operation is not
supported by some other languages such as C++ or Java.

Addition X + Y the sum of X and Y


Subtraction X - Y the subtrahend X minus Y

44
Multiplication X * Y the product X times Y
Division X / Y the quotient X divided by Y
Exponentiation X ** Y X raised to the power Y
2
Modulo X mod Y X modulo Y
Remainder X rem Y the remainder after dividing X by Y
Absolute abs X the absolute value of X
Negation -X the negation of X

The addition (+) multiplication (*) and subtraction (-) operations are performed by a computer in
an equivalent manner as we perform in hand calculations. With the exception of the finite limits of
numeric types, which we have already discussed, the results should be as you would expect.
The division (/) operation is different for integers and floats. The result of integer division (i.e. the
division of one integer by another) is truncated to its whole number value. More details
concerning arithmetic operations on integers and float is provided in Section 2.6 below. Also, you
may wish to experiment with these arithmetic operations in your own Ada programs.

The order in which arithmetic operations will be executed in an assignment statement can be
forced using parentheses, but there is a built-in order of precedence for operators. The order of
precedence, from highest to lowest, is provided below. Higher precedence operations are
performed before lower precedence operations when the order is not forced through the use of
parentheses.

X**Y abs X <- highest precedence


X*Y X/Y N mod M N rem M :
+X -X :
X+Y X-Y <- lowest precedence

Using the order of precedence rules above, the assignment statements

W := X + Y*Z - X/Y + Z;
A := -B + C - D;
P := Q/R/S/T;

will result in the precedence indicated by the fully parenthesized versions of these statements
below:

W := (((X + (Y*Z)) - (X/Y)) + Z);


A := (((-B) + C) - D);
P := (((Q/R)/S)/T);

When mixing Boolean and arithmetic operators, the arithmetic operators will have the higher
precedence. For example, if we are testing x>3 and Y<2 we do not need to enclose the
arithmetic comparisons in parentheses. Other high-level languages do not use the same
precedence. Omitting the parentheses may result in some languages attempting to evaluate 3
and Y first, creating a syntax error. When in doubt, parenthesize.

bool_val := X > 3 and Y < 2; -- OK in Ada


bool_val := (X > 3) and (Y < 2); -- OK in any HLL.

Operator Overloading

The arithmetic operators ( +, -, *, / ) used in an assignment statement are automatically matched


to the types of the associated operands. When two floats are added together for example, the
floating point version of addition is used. This may seem obvious but it is important to remember

2
For a complete definition of the mod and rem operators, refer to the Glossary
45
that the computer only simulates mathematical operations. Since the internal representations of
integers and floats are different, there are different implementations of the arithmetic operators for
each numeric data type. In Ada you are not permitted to perform arithmetic operations on
identifiers of mixed types. For example, if we set N to type integer and X and Y to type float we
would not be permitted to make the following assignment,

Y := N * X;

In the next section we learn how to deal with mixed types in arithmetic expressions.

2.6 Type Conversions

Some high-level languages allow mixing of numeric types in an assignment statement such as,

area = 4 * pi * radius**2

where 4 is an integer, pi is a float (3.1415926...) and radius may be an integer or a float. Not
having to worry about converting integer and floating point types makes writing arithmetic
expressions easier but it makes the control of precision more difficult. Computer languages that
allow mixing of numeric types are said to be weakly typed.

Ada is a strongly typed language, which means that two operands must be of the same type
before any arithmetic operations can be performed on them. Sometimes we want to compute the
value of an expression containing a combination of integers and floats. In cases such as these
we can convert one of the data values into the type of the other value using the type conversion
functions float( ) or integer( ). Whether we convert the integer value to a float or the float to an
integer value depends on the requirements of the problem being solved.

In the following examples let M and N be integers and let X and Y be floats.

Y := X/float(N);
M := integer(X/Y);
N := integer(X)/integer(Y);

In the first example, the integer N is converted to a floating point value before it is divided into the
floating point value of X. The result is assigned to the float Y. Since both operands are float
types, the division results in a floating-point value.

In the second example the result of dividing the float X by the float Y is converted to an integer
and assigned to the integer M. In this case the division is floating point since it occurs before the
type conversion.

In the third example the floating-point values X and Y are both converted to integers before the
integer division. The integer result is assigned to the integer N. Attempting to assign a float
value to an integer identifier or vice versa will also result in a type conflict.

Integer division truncates the fraction while the type conversion function integer( ) rounds to the
nearest integer. For example,

N := 3/4 assigns N=0


N := integer(3.0/4.0) assigns N=1

Generally we don't worry about loss of precision when we convert an integer value to a float.
However, when converting large integers to floats we need to remember that the number of
significant digits in the float value is less than in the integer value. In our discussion of number
representations, we saw that 31 bits are used to hold the significant digits of an integer while only
24 bits are used to hold the significant bits of a single-precision float. For example, when the
integer N is assigned a value and then converted to float and back to integer as shown,

46
N := 123456789;
put(N,0);
new_line;
put(integer(float(N)),0);
we lose accuracy on the least significant digits. The two put( ) statements display the values of
the original and modified values of N.
123456789
123456792
Be careful to use the type conversion that best suits the problem you are solving. When dealing
with other languages, take time to learn the rules of truncation and/or rounding used when
converting floating point values into integers.

2.7 Internal Number Representations

This section is optional and therefore is not necessary for an understanding of most of the other
topics presented in this text. It covers advanced concepts of number representation and should
be included only in a comprehensive coverage of the foundations of computer science.

Even though we usually do not have to deal with the underlying data representations on a
computer when we are using high-level languages such as Ada, C++, or Java, it is important to
understand them so we can recognize problems when they occur.

Unsigned Integers

We can represent positive integers as unsigned or simple magnitude-only binary values. The
magnitude, M of an n-bit binary value bn-1 bn-2 . . . b2 b1 b0 is given by
n −1
M= bi 2 i
i =0

where bi = the value (0 or 1) of the ith most significant bit. One byte (8 bits) unsigned integer
binary values are used to represent many useful quantities, such as sound file samples and
indexed color values.

Signed-Magnitude

A common method of representing both positive and negative integers is the signed-magnitude
approach. In signed-magnitude representations the left-most bit of the binary string is used to
indicate the sign of the number: zero (0) for positive and one (1) for negative. The next 31 bits
are used to express the magnitude of the number.

Current desktop computer systems offer 32 bit word addressing and support 32 bit integer values.
Since one of the bits is used to maintain sign, the maximum magnitude of a 32-bit sign-magnitude
31 0
integer is 2 -1. Remember that the least significant (rightmost) bit represents a base value 2 =1.
Some example 32 bit sign-magnitude values are,

0000000000000000000000000000000 = +0
0000000000000000000000000000001 = +1
0000000000000000000000000000010 = +2
:
0111111111111111111111111111111 = +2,147,483,647
1000000000000000000000000000000 = -0
1000000000000000000000000000001 = -1
1000000000000000000000000000010 = -2
:
1111111111111111111111111111111 = -2,147,483,647
47
The magnitude, M of an n-bit binary number is given by,

n−2
M = (−1) n −1 bi 2 i
i =0

where bi = the value of the ith most significant bit. In base 2, each successive position represents
0
a value that is 2 times the previous position. The rightmost value is 2 or 1, which is the least
significant bit (LSB). The exponent increases by one for each position we move to the left. The
leftmost digit is called the most significant bit (MSB).

27 26 25 24 23 22 21 2 0

128 64 32 16 8 4 2 1
To convert a binary value to its decimal equivalent we need to add the base values for those
positions containing a 1 in the corresponding binary number. Just as in the base 10 number
system, the zeros are place holders and do not contribute directly to the number's magnitude.

The binary value 00010101 has a decimal value of


4 3 2 1 0
(1)2 + (0)2 + (1)2 + (0)2 + (1)2 = 16 + 0 + 4 + 0 + 1 = 21

The binary value 00111110 has a decimal value of

(1)64 + (1)32 + (1)16 + (1)8 + (1)4 + (1)2 + (0)1 = 126

If we assume that the left-most bit is the sign bit, we can represent 8-bit negative numbers as well
as 32-bit numbers. For example, the 8 bit binary (signed-magnitude) value 10011101 is

(-1)[ 16 + 8 + 4 + 1] = -29

You may have noticed that in the signed-magnitude representation we have separate
representations for +0 and -0. Positive and negative zero have no meaning in mathematics. This
is an instance in which the computer representation fails us. Having two representations for zero
can create problems when we attempt to test for equality between two integers.

Two's Complement

Let's look at another popular method of representing integers in binary called two's complement.
In this method, the value of the non-negative numbers appear the same as the signed-magnitude
or unsigned integers. If the most significant bit (MSB) is a zero (0) the value is positive and if the
most significant bit is a one (1) the value is negative, but we don't have to treat the most
significant bit in a special way during arithmetic operations. When the value is positive, a two's
complement binary value is generated in the normal manner. When the value is negative, we
have to perform the following operations to represent the value as a two's complement binary
number:

1. Generate the magnitude of the value in binary


2. Invert each bit of the binary number (0 becomes 1 and 1 becomes 0), called 1's complement
3. Add one (1) to the one's complement to produce the two's complement. (Ignore any overflow.)

The addition in Step 3 is binary. There are only two digits (0 and 1) in a binary number so when
the sum of two digits in a column is two (102) we place the zero (0) in this column and carry the
one (1) to the next column.

0 0 1 1 00110111
+0 +1 +0 +1 + 1
0 1 1 10 00111000

48
Now let's convert a base-10 number to its equivalent two's complement base-2 value. The
decimal value -34 can be converted into its equivalent binary magnitude by successive division by
2. When the result of the division has no remainder, the corresponding bit is 0, when there is a
remainder the corresponding bit is 1.

34/2 = 17 remainder 0 _ _ _ _ _ _ _ 0
17/2 = 8 remainder 1 _ _ _ _ _ _ 1 0
8/2 = 4 remainder 0 _ _ _ _ _ 0 1 0
4/2 = 2 remainder 0 _ _ _ _ 0 0 1 0
2/2 = 1 remainder 0 _ _ _ 0 0 0 1 0
1/2 = 0 remainder 1 _ _ 1 0 0 0 1 0
0/2 = 0 remainder 0 _ 0 1 0 0 0 1 0
0/2 = 0 remainder 0 0 0 1 0 0 0 1 0

Now to generate the two's complement representation by

0 0 1 0 0 0 1 0 <- magnitude of -34


1 1 0 1 1 1 0 1 <- one's complement
+ 1
1 1 0 1 1 1 1 0 <- two's complement

Notice that we must maintain the same number of bits in the 2's complement number. When
adding 1 to the one's complement we may find that there is a carry bit out of the MSB column.
This value is called an overflow and should be ignored when dealing with two's complement.

The largest positive decimal value that can be represented as an 8-bit two's complement binary
number is 127=01111111. This is because two's complement numbers with a 1 MSB are
interpreted as negative values.

Now we convert a two's complement value 11110100 into its base-10 value. First of all, we can
see that this value is negative since the MSB is a 1. Since we are converting to base-10 we need
to obtain the magnitude of this value by separating it from its negative sign. To accomplish this
we must repeat the process we use to convert base-10 values into two's complement.

11110100 original two's complement number is negative


00001011 first we take the one's complement

00001011 and now we add one to obtain the magnitude


+ 1
00001100 two's complement is positive and has the same magnitude

Taking the two's complement of a negative two's complement gives the positive magnitude of the
original value. Now we can compute the base-10 magnitude of the value.
8 + 4 = 12
Since the original number had its MSB=1, we know that we need to change the sign of this base-
10 value to obtain -12. So the value 11110100 is the two's complement representation of -12.
One of the advantages of two's complement is that there is only one representation of zero.

00000000 positive zero in two's complement notation


11111111
+ 1
00000000 negative zero in two's complement notation

The two's complement number representation is useful since it simplifies the hardware
implementation of arithmetic operations. For example, if the value of X-Y is to be computed, we
can convert Y to its two's complement form (i.e. -Y) and simply add X and Y ignoring any
overflow. If the result happens to be negative the MSB of the sum will be a 1.
49
Let X=5 and Y=3 in the three simple examples below.

X+Y X-Y Y-X


X=5 = 00000101 00000101 00000101 00000011
Y=3 = 00000011 +00000011 +11111101 +11111011
00001000 100000010 11111110
-X 11111011 ^
-Y 11111101

In the first case, the sum X+Y results in a positive value (8), which is indicated by the fact that the
MSB is a zero (0). In the second case, the result of X-Y is obtained by adding X to the two's
complement of Y. The result is again positive (2) because the MSB is a zero (0). Note that the
overflow bit (i.e. the ninth bit) is simply ignored. In the third case, the result of Y-X is negative (-2)
since the MSB is a one (1). In order to determine the base-10 value we need to extract the
positive magnitude from the two's complement representation.

11111110 original value is negative


00000001 one's complement
+00000001
00000010 two's complement gives positive magnitude

As stated in Section 2.1, there are limiting values that can be represented with a finite number of
bits. So what happens when we exceed these values? In Ada we receive a constraint error when
we exceed the valid range of integers. This range is [-2,147,483,648...2,147,483,647] on
computers in which a 32-bit word is used to store integers. In some other languages, adding one
to the maximum integer can return the most negative value. In a sense, the integer values are
arranged in a ring as shown in Figure 2-5 below. Exceeding the range limit is called integer wrap-
around.

-2 -1 0 1
-3 2
3
positive values
negative values

X
first
last

+Y
Figure 2-5: The Range of Integers in a Finite Representation (Two's Complement)

Crossing the boundary from the last (largest positive) integer to the first (largest negative) integer
corresponds to an overflow of the data word representing this integer.

01111111111111111111111111111111 last integer


+ 1
10000000000000000000000000000000 first integer

50
When the sum of two integers X + Y exceeds the maximum integer that can be represented on
the computer, the resulting value will be the value corresponding to the position on the ring that is
Y positions beyond the position of X. So the sum of two positive integers can result in a negative
integer in a computer. Ada prevents this by throwing a range error. Most other high-level
languages allow wrap-around of the integer value, and don't provide any runtime error messages
or warnings. This can cause unpredictable behavior by your program.

Excess Notation

Excess notation is another important method for representing ranges of integers that include
positive and negative values. In excess notation we shift the meaning of the binary values by half
the range of possible values that can be represented. Let's use 8-bit binary values as our
example.

An 8-bit binary integer has a range [00000000..11111111]. Normally this would represent a
decimal range of [0..255]. If we subtract 255/2 = 127 from each value then the range of
values represented would be [-127..127]. In order to convert a decimal value between -127
and 127 into a binary number we only need to add 127 to the value and then convert it to binary
in the normal manner. Numbers created using this procedure are called excess 127 notation
binary numbers.

Another limitation in the representation of floating point numbers is less obvious. The
mathematical concept of real numbers is that they are continuous. Between two real numbers
X and Y, if X<Y there exists another real number W for which X<W<Y. Actually we know that
there is an infinite number of real numbers between any two distinct real numbers. The number
of digits in the magnitude of a real number is a measure of its precision. As shown in the
program output above, floats in Ada have approximately six digits of precision. This is typical of
single precision floats in most high-level languages.

There is a minimum difference between any two floating point values that can be represented on
a computer. When we attempt to change a floating point value by an amount that is less than the
LSB of the larger number its magnitude is not affected.

IEEE Single Precision Floating Point

The IEEE (Institute of Electrical and Electronics Engineers) has defined a standard for the
representation of single-precision floating point numbers. The IEEE standard uses a 32-bit word,
with 23 bits to hold the significant digits of the number (called the mantissa), 8 bits to hold the
power of 2 to be multiplied times the mantissa, and a sign bit.

0 1 0 0 0 0 0 1 0 1 0 0 1 1 0 1 1 0 1 1 1 0 0 0 1 0 0 1 1 0 1 1
sign exponent mantissa
bit

The sign bit is set to zero (0) for positive values and one (1) for negative values.

The exponent is an integer in excess-127 notation that gives the power of 2 to which the mantissa
is to be raised.

The mantissa is a binary fraction whose magnitude is >=0.5 and <1.0, but with the leading 1
omitted. This is possible since binary fractions in the range [0.5,1.0) all have a 1 as their first bit.

We will convert the value 3.1415926 into IEEE floating point. First, the sign bit is zero (0) since
the value is positive. Now we convert the whole number portion into binary

310 = 112

51
Next we convert the fractional part to binary. This can be done by successively multiplying the
value by 2 and placing a zero (0) when the resulting value is less than 1.0 or placing a one (1) as
the next bit when the resulting value is greater than or equal to 1.0. After each doubling, omit the
whole number portion of the resulting value.

0.1415926 0.5309632
x 2 x 2
0.2831852 11.0 1.0619264 11.001001
x 2
0.5663704 11.00 0.0619264
x 2 x 2
1.1372408 11.001 0.1238528 11.0010010
x 2
0.1372408 0.2477056 11.00100100
x 2 x 2
0.2654816 11.0010 0.4954112 11.001001000
x 2 x 2
0.5309632 11.00100 0.9908224 11.0010010000

This process continues until we have 24 bits of precision including the whole number portion.
11.0010010000111111011010
Now we shift the radix point to the left of the leading 1 bit and account for this shift in the
exponent. We can do this since the base of the exponent is 2.
0.110010010000111111011010 x 22
Since this shift always forces the leading MSB to be a one (1) we don't have to include it in the
IEEE representation. We will remember to replace it when we convert the value back into a
decimal number.

Since the exponent needs to be able to represent both positive and negative powers the IEEE
standard uses excess 127 notation. This means that we need to add 127 (01111111) to the
value of the exponent.
01111111
+00000010
10000001
Putting it all together we have,

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

which is the computer's format for storing the numbers sign bit, exponent, and mantissa. Don't
forget that there are a few elements that are not shown in this notation such as the base of the
exponent and the leading bit of the mantissa.

sign
bit
0 exponent
mantissa
+ .1 1 0 0 1 0 0 1 0 0 0 0 1 1 1 1 1 1 0 1 1 0 1 0
(
x 2
1 0 0 0 0 0 0 1 - 01111111
)

52
When working in a high-level language we don't have to deal with converting real numbers from
decimal to IEEE formats. So why do we bother learning the computer's internal representation of
floating point data types? The short answer is: we need to understand the way floats are stored
in the computer so we can avoid errors caused by the limitations of this representation. Some
examples will help to illustrate this.

When we try to represent rational numbers such as 1/3 in decimal form we discover that an exact
decimal form requires an infinite number of digits.

1/3 = 0.33333333333333333333333333333....

Any finite decimal representation of 1/3 is actually less than 1/3 since we lose the value of the
omitted digits. The decimal form of 2/3 is rounded to the nearest value which happens to be
greater than 2/3.

2/3 = 0.666666666666666666666666666666667

Some rational numbers that have exact decimal representations in base 10 become repeating
fractions in base 2. We convert 8/10 = 0.8 to base 2 to illustrate this,

0.8 0.2
x 2 x 2
1.6 0.1 0.4 0.110
0.6 x 2
x 2 0.8 0.1100
1.2 0.11 x 2
0.2 1.6 0.11001

We started with the value 0.8 and after generating the binary fraction 0.1100 we obtain the
decimal value 0.8 again. Therefore the exact representation of 0.8 in base 2 is a repeating binary
fraction, which must be truncated to be stored in the computer's memory.

0.8 = 0.11001100110011001100...

Truncation changes the value being stored. This can cause problems when we attempt to
compare two floats. Let's take a look at the results of the following program.

with ada.text_io, ada.float_text_io;


use ada.text_io, ada.float_text_io;

procedure truncate_test is
X,Y : float;
begin

X:=0.8;
Y:=0.8;
Y:=Y+1.0;
Y:=Y-1.0;
put(X);
put(Y);
new_line;

if X=Y then
put("They're equal!");
else
put("What's goin on here?");
end if;

end truncate_test;
53
Although you have not learned much about the Ada programming language so far, you should be
able to follow the operations in this source code. X and Y are both given the value 0.8. Next 1.0
is added to and then subtracted from Y. This should result in the same value of Y= 0.8.

We display the values of X and Y to verify that they are the same value which gives,

8.00000E-01 8.00000E-01

These values are the computer's method of displaying scientific notation in which the E
represents (x10) and the value -01 is the power of 10. 8.00000E-01 = 8.0x10-1.

Next we compare the values of X and Y in the if X=Y then... statement but in this case we
obtain the message

What's goin on here?

which tells us that the comparison X=Y was false. The problem we have created here is caused
by the truncation of the repeating binary fraction for 0.8.

1 1 0 0 1 1 0 0 1 1 0 0 1 1 0 0 1 1 0 0 1 1 0 0 1 0.8

1 1 1 0 0 1 1 0 0 1 1 0 0 1 1 0 0 1 1 0 0 1 1 0 0 1.8

1 1 0 0 1 1 0 0 1 1 0 0 1 1 0 0 1 1 0 0 1 1 0 0 0 0.8

When X and Y were originally assigned the value 0.8, their internal representations were both
truncated at the 24th bit. Then we added 1.0 to the value of Y. The internal representation of 1.8
pushed the LSB off the right end of the mantissa. When we subtract 1.0 from Y the LSB is set to
zero (0).
You may be wondering why both X and Y are displayed as 8.00000E-01 even though their LSBs
-24
are different. The magnitude of the LSB is 2 = 0.000000059, which is 500 times smaller than
can be represented in the six decimal digits used to display single-precision floating point
numbers.

We can force the put( ) statements to display more digits by including the formatting parameter
aft. We can use aft=>8 to display 8 digits after the decimal point.

put(X,aft=>8);
new_line;
put(Y,aft=>8);
new_line;

which displays,

8.00000012E-01
7.99999952E-01

Mystery solved. The statement if X=Y then... correctly determines that X and Y are not
equal. The important issue illustrated here is that computers only approximate mathematical
operations. We need to keep this in mind when we are creating programs to solve mathematical
problems.

54
2.8 Applications

In this section we will practice using the programming constructs introduced in this chapter. The
example problems will be solved by writing and running Ada programs, but the problem solving
techniques used here can be applied to any high-level language.
Personal Screen Message
2.1: Write an Ada program to display your name, major, and email address centered on the text
screen.

We will build this program in stages, testing each step along the way to minimize the number and
complexity of syntax errors. We will use the basic design steps of the software development
lifecycle as discussed in Section 1.5 whenever it makes sense to do so.

As stated in the problem statement you are to display your name, major, and email address
centered on the text screen. At first, this may seem to be a clear statement of a relatively simple
problem. However, when we begin to consider implementation we discover that a lot of
information has been omitted. For example,

How many lines will you use to display the information?


Do you wish to center the text vertically as well as horizontally?
Should the user enter the string data, or should it be hardwired?
Should the program compute the starting column for each line of text?
What size is the text screen (i.e. display window)?

Since we cannot obtain a more detailed problem statement in this case, we will have to make
some decisions about the problem and program design.

We will display the information in three lines.


We will center the text horizontally and will start approximately 10 lines from the top of the
page. (This choice is totally arbitrary and is based on aesthetics alone. In other words, I like
the look of a 10-line header.)
We will hardwire the strings into the program.
We will have the computer determine the starting column for each line. The reason for this is
that we will then be able to use the same program to display other data sets by changing only
the three lines of text.
We will assume that the text display is 80 columns wide by 24 rows in height.

We now have enough information to begin program design. In this case we can describe the
program using pseudocode.

-- Define the strings for Name, Major, and email address.


-- Skip down 10 lines from the top of the text display
For each line of text...
-- Determine the number of characters in this line of text
-- Compute the starting column for this line of text
-- Move output cursor to this column
-- Put this line of text
-- Skip to the next line of the text display
Next line of text

Implement a Working Program

We can use some built-in procedures provided in the ada.text_io package to help us convert the
pseudocode above into a working program. The set_line( ) and set_col( ) procedures give us an
easy way to move the output cursor to a particular position in the text display screen.

55
set_line(n) - sets the next line to display text to line number n
set_col(m) - sets the next column to display text to column number m

These procedures as well as many other functions and procedures provided in the ada.text_io
package can be reviewed in the Ada Language Reference Manual (LRM). An electronic copy of
the Ada LRM is provided as part of the GNAT Ada_GIDE.

A good rule of programming style is to design source code as if you planned on reusing it. This
means that you should consider the labor involved in modifying the source code in some
predictable way to solve a similar problem. In this example, it is reasonable to anticipate that we
would want to write screen message programs for other students.

It would be nice to be able to avoid the manual calculation of starting columns for different length
strings. We would like to find a way to make the computer count the lengths of the strings
automatically. As you may recall, there is an attribute function called 'length that returns the
number of elements in any composite type. Attribute functions are implemented by appending
them to a type or an identifier. If we want to display the number of characters in the string word,
we can use the 'length function as shown.

put(word'length);

The identifier word is a string type but word'length is an integer. We have used 'length to help us
compute the proper starting column for centered text strings in the screen message program.

starting_column := center_column - string_length/2

For example, if the string Name has 11 characters, and we wish to center this string on an
80-column line, we compute,

starting_column := 40 - Name'length/2;
so
starting_column := 40 - 11/2; -- i.e. 35.

The starting_column can be omitted by placing the computation of the starting column directly
into the parameter list of the set_col( ) procedure call. The program below is a working program
that implements our pseudocode program design.

with ada.text_io;
use ada.text_io;

procedure screen_message is
name : string := "Robin Banks";
major : string := "Computer Science";
email : string := "robin.banks@murraystate.edu";
begin
set_line(10);
set_col(40 - name'length/2);
put_line(name);
set_col(40 - major'length/2);
put_line(major);
set_col(40 - email'length/2);
put_line(email);
end screen_message;

56
Robin Banks
Computer Science
robin.banks@myschool.ed

So why is it better to let the program compute the set_col( ) values rather than simply hardwiring
these values? This question is best answered by considering what we must do to modify this
program for another student. Which lines of code would you need to modify to build a screen
message program for another student? For example, we want the program to display,

Amanda B. Reckonwith
Math
rudedog@nowhere.who

To modify the program for Amanda, we need to replace the three lines of text to display her
name, major, and email address. Since the program computes the starting column for each line
of text, we do not need to modify any other part of the code. If we had hardwired the starting
columns, then those lines would also need to be modified.

Pepperoni Pizza

2.2: Three pizza parlors in town make equally good pepperoni pizzas. They each offer their best
deals on their largest pizzas. The sizes and prices are given in the table below:
Business Size Shape Price_
Mac A's Pizza 17" circular $11.00
Mac B's Pizza 18" circular $12.00
Mac C's Pizza 20" square $20.00

Write an Ada program to compute the best deal in town.

The program used to solve this problem is based on one of the most common programming
models called input-process-output. We can either hardwire the program (include the specific
values in the source code) to compute the cost per square inch of each of these particular pizzas
or we can make the program interactive (prompt the user to enter the pizza data) so that we can
continue to use the program when the prices change. An interactive program is more versatile
than a hardwired program but requires more effort from the user. Let's build the hardwired
version first.

The pseudocode below follows the input-process-output programming model.


-- declare/define all input data and constants
For each pizza...
-- compute total area
-- compute unit cost
-- display pizza name and unit cost
Next pizza
57
Before leaping onto the keyboard we need to develop the underlying algorithm. For this problem,
we want to compare the costs of the pizzas in a way that compensates for different sizes and
shapes. The costs of an equal amount of pizza from the three pizza parlors should be
determined. We will compute the cost per square inch of each pizza. The best deal will be the
pizza that has the lowest cost per square inch. First, we need to compute the area of each pizza.

The area Acircle of a circle given the diameter D is,


2
D
Acircle =π
2

and the area Asquare of a square given the length of one side L is,

Asquare = L2

This version of the program will not require external input since we will include the pizza sizes
and prices in the declaration block. The program still needs an output so we will declare
variables to hold the costs per square inch of each pizza, unit_cost_A, unit_cost_B, and
unit_cost_C.

pi : constant float := 3.1415926;


diameter_A : float := 17.0;
diameter_B : float := 18.0;
length_C : float := 20.0;
price_A : float := 11.0;
price_B : float := 12.0;
price_C : float := 20.0;
unit_cost_A : float;
unit_cost_B : float;
unit_cost_C : float;

Since the declaration block already contains the program input, the execution block will contain
only the process (calculations) and output of the program. We can compute the unit costs using
the expressions for area defined above.

unit_cost_A := price_A/(pi*(diameter_A/2.0)**2);
unit_cost_B := price_B/(pi*(diameter_B/2.0)**2);
unit_cost_C := price_C/(length_C**2);

Now that we have computed the unit costs we need to display them. Just because a variable is
assigned a value, it is not automatically displayed by the program (i.e. there is no magic.) We will
also want to include some text explaining the values being displayed.

put("Cost per square inch of Mac A's pizza = $");


put(unit_cost_A);
new_line;
put("Cost per square inch of Mac B's pizza = $");
put(unit_cost_B);
new_line;
put("Cost per square inch of Mac C's pizza = $");
put(unit_cost_C);
new_line;

58
Putting it all together, we have,

with ada.text_io, ada.float_text_io;


use ada.text_io, ada.float_text_io;

procedure best_deal is

pi : constant float := 3.1415926;


diameter_a : float := 17.0;
diameter_b : float := 18.0;
length_c : float := 20.0;
price_a : float := 11.0;
price_b : float := 12.0;
price_c : float := 20.0;
unit_cost_a : float;
unit_cost_b : float;
unit_cost_c : float;

begin
unit_cost_a := price_a/(pi*(diameter_a/2.0)**2);
unit_cost_b := price_b/(pi*(diameter_b/2.0)**2);
unit_cost_c := price_c/(length_c**2);

put("Cost per square inch of Mac A's pizza = $");


put(unit_cost_a);
new_line;

put("Cost per square inch of Mac B's pizza = $");


put(unit_cost_b);
new_line;

put("Cost per square inch of Mac C's pizza = $");


put(unit_cost_c);
new_line;
end best_deal;

We can build an interactive version of this program that prompts the user for size, price and
shape of each pizza.

put("Enter size (diameter or width) of pizza... ");


get(size);
put("Enter price of pizza... ");
get(price);
put("Enter pizza shape (0=round, 1=square)... ");
get(shape);

Now we need a way to compute the area of the pizza based on the input shape. Later we will be
able use an if...then statement but we don't "know" about such conditional construct (i.e.
alternation) yet.

if shape=0 then
area := pi*(size/2.0)**2;
else
area := size**2;
end if;

Another way to simplify this program is to use a loop, which we will learn about in Chapter 4. This
is an example of the repetition construct. Take a sneak preview of the tweaked-out version of this

59
program. Comparing this version with the previous one gives us motivation to learn more about
the capabilities of Ada.

with ada.text_io, ada.integer_text_io, ada.float_text_io;


use ada.text_io, ada.integer_text_io, ada.float_text_io;

procedure tweaked_best_deal is
pi : constant float := 3.1415926;
shape : integer;
size : float;
price : float;
area : float;
cost : float;

begin
loop
put("Enter size (dia or width) of pizza (0 to quit)... ");
get(size);
exit when size<=0.0;

put("Enter price of pizza... ");


get(price);

put("Enter pizza shape (0=round, 1=square)... ");


get(shape);

if shape=0 then
area := pi*(size/2.0)**2
else
area := size**2;
end if;

cost := price/area;
put("Cost per square inch of this pizza = $");
put(cost);
new_line;
end loop;
end tweaked_best_deal;

In this program, we can generate the price per square inch of round or square pizzas. Also, we
can enter data for as many pizzas as we want. The program ends when we enter a pizza size
that is less than or equal to zero. In this example, setting the value of size alerts the program that
we are finished (see sentinel value in the Glossary).

Notice that even though the two versions of this program are very different in style and
appearance, they both are faithful implementations of the pseudocode. The same pseudocode
could be used to implement the best_buy program in any high-level language. This demonstrates
the difference between a program design and a program implementation. It also illustrates the
value of pseudocode in the program design process.

This source code could be refined even more using Ada specific constructs. For now, we will
concentrate on learning basic programming methods common to most high-level languages. An
important lesson to be learned from this example is there is more than one correct way to write a
program. The program tweaked_best_buy is not necessarily better than the original version of
the program. In fact the hardwired best_buy is preferred if you want a program that generates a
table of results for a specific situation without the need for user interaction.

60
Exercises

2.1 Write the following integer values as signed-magnitude binary numbers (8 bits).

7 8 23 66 125
-7 -8 -23 -66 -125

2.2 Write the values above in twos-complement (8 bits).

2.3 Convert the following decimal fractions to their binary equivalents

0.75 0.1 0.9 0.999

2.4 Convert the following binary fractions to their decimal equivalents

0.110011 0.11111 0.00000001

2.5 Given the string

samp:="THEQUICKBROWNFOXJUMPEDOVERTHELAZYDOGS"

determine the values of the following:

samp'length samp(31)
samp(9) samp'first
samp(22..26) samp(10..10)
samp(10..20)'length put(samp(1..3) & samp(14..16))

2.6 Given the string of Exercise 2.5 determine the values of the following:

samp(1..10)'length
samp(3..4) & samp(1..10)
samp'last
samp(character'pos(samp(31))-50)

2.7 Find out how to represent zero (0.0) in IEEE single-precision format. (This is definitely a
tricky question.) While you are researching this question you might also look into the
representations of positive and negative infinity, and not-a-number (NaN).

2.8 What other representations for floating-point numbers can you find? Use the terms below as
a guide.
a. ANSI/IEEE Std 754-1985
b. Cray, VAX and Burroughs B 5500 representations
c. denormalized IEEE formats
d. IEEE double-precision

2.9 Write an Ada program to compute gas mileage given the odometer readings before and after
a fill-up and the number of gallons required to fill the tank.
a. List all the variables to be used in this program
b. Write a detailed description of how to perform this calculation by hand.
c. Write the input, process, and output steps for this program as separate computational
blocks.

2.10 Write an Ada program to convert degrees Fahrenheit to degrees Celsius. Include the same
work specified in parts a, b, and c in Exercise 2.9.

61
2.11 Write an Ada program to convert miles per hour into feet per second.

2.12 Write an Ada program to compute the distance between two points in a Cartesian coordinate
3
system. How would you modify this program to compute the distance between two points in R
n
(3-dimensional space)?...R (n-dimensional space)?

2.13 Write an Ada program to compute the area of a triangle given the lengths of the three sides.
Your program should prompt the user to enter the lengths. (Use the semi-perimeter formula to
compute the area.) What test(s) might need to be performed before the area is computed?

62
Chapter 3 - Using Conditionals

3.1 The if...then... Construct

3.2 Using the ...else... and ...elsif...

3.3 The case Statement


Using Enumeration Types with case Statements

3.4 Nested Conditionals

3.5 Exception Handling


Common Predefined Exceptions
Programmer-Defined Exceptions

3.6 Applications
Area of a Triangle
Net Pay

63
Chapter 3 - Using Conditional Control Statements
Conditional control statements change the flow of program execution based on the truth value of
a Boolean expression. In this chapter you will study the if...then and case constructs and their
applications in problem solving.

3.1 The if...then...end if Construct

Every high-level language offers the programmer some form of the if...then construct. The Ada
construct if...then...end if is a compound statement composed of a Boolean conditional and a
block containing one or more program statements. If the Boolean conditional expression is TRUE
then the optional statements are executed, otherwise they are skipped and program control is
passed to the next statement after the if...then...end if construct.

if conditional expression then

optional statement(s)

end if;

Figure 3-1: A Graphical Illustration of the if...then...end if Construct

The optional statements between the if...then and end if will be executed only if the conditional
expression is TRUE. Although it is not required by the compiler, the code in the if...then
statement is indented for emphasis.

An example of the use of the if...then...end if construct is shown below. In this code segment, a
Boolean variable diag controls diagnostic outputs (values that are used to help with debugging) of
the program. The values of intermediate computations and debugging messages will be
displayed only if the value of diag is TRUE.

distAB:=sqrt((Ax-Bx)**2 + (Ay-By)**2);
if diag then
put("distance A->B = ");
put(distAB,0,2,0);
new_line;
end if;

distBC:=sqrt((Bx-Cx)**2 + (By-Cy)**2);
if diag then
put("distance B->C = ");
put(distBC,0,2,0);
new_line;
end if;

When diagnostic output is not needed, the value of diag is set to FALSE causing the put( )
statements to be skipped.

Another common use of the if...then...end if construct is to check and possibly correct range
errors or other runtime problems.

put("Enter amount of withdrawal... ");


get(withdrawal);
64
if balance<withdrawal then
put("Sorry you only have $");
put(balance,0,2,0);
put(" available);
new_line;
withdrawal:=0.0;
end if;

balance:=balance-withdrawal;
pay_out(withdrawal); -- tells ATM machine to dispense CASH

In the example above, the user is advised that there are not sufficient funds available if the
amount requested exceeds the balance. Notice that the if...then block also resets withdrawal to
0.0 before it is subtracted from the balance. In case you're wondering, Ada does not have a
predefined procedure named pay_out( ), but we could write one. This procedure could display a
message on the teller's computer monitor or it could be used to direct an ATM machine to
dispense the amount of cash indicated by the parameter withdrawal.

In the previous example, the use of the if..then construct is a bit awkward. It would be better if we
did not have to subtract withdrawal even when it is zero. (Review the discussion of the problems
with the internal representation of floating point values in Chapter 2). The if...then...else construct
solves this problem for us.

3.2 Using the ...else... and ...elsif...

The if...then...else...end if is a compound statement composed of a Boolean conditional and two


executable blocks. If the Boolean conditional expression is TRUE then the first block of program
statements is executed, otherwise the second block is executed.

if conditional expression then

optional statement(s)
executed if conditional
expression is TRUE

else

optional statement(s)
executed if conditional
expression is FALSE

end if;

Figure 3-2: A Graphical Illustration of the if...then...else...end if Construct

Only one of the two blocks will be executed when the if...then...else construct is encountered.
Inclusion of the else block allows us to improve our code in the previous example.

put("Enter amount of withdrawal... ");


get(withdrawal);

if balance<withdrawal then
put("Sorry you only have $");
65
put(balance,0,2,0);
put(" available);
new_line;
else
balance:=balance-withdrawal;
pay_out(withdrawal); -- this is not a predefined Ada procedure
put("Please take your cash...");
new_line;
end if;

In this version of the code a new balance is computed only if a withdrawal is made. We can now
display a message reminding customers to take their cash. Also, we have moved the pay_out( )
procedure inside the else block to eliminate the need for the ATM hardware to deal with requests
to pay out $0.00.

The if...then...else construct should be used whenever there are two alternative actions exactly
one of which must be performed. DO NOT USE the if...then...else when two if...then constructs
are appropriate. Sometimes we just need to make a couple of tests that are independent.

Consider the example in which a certain value X must be between 0.0 and 1.0. We won't worry
about how X gets its initial value we will just make sure that upon leaving the code segment
below, it is in the specified range. We will also change the value of X as little as possible to keep
it in the range [0.0,1.0].

if X<0.0 then
X:=0.0;
end if;

if x> 1.0 then


X:=1.0;
end if;

Sometimes we need to make a sequence of tests that are related. Consider the example of
assigning letter grades for a given average. We could use a sequence of if...then statements as
above,

if average>=90.0 then
grade:='A';
end if;

if average<90.0 and average>=80.0 then


grade:='B';
end if;

if average<80.0 and average>=70.0 then


grade:='C';
end if;

if average<70.0 and average>=60.0 then


grade:='D';
end if;

if average<60.0 then
grade:='E';
end if;

This code segment assigns a letter to the character identifier grade that corresponds to the
average based on a 10-point grading scale. However, we are forced to refer to each of the
intermediate thresholds twice, increasing the potential for runtime errors. For example, omitting
an equal sign on one of the >= comparisons could leave the grade unassigned for a particular
66
average. Besides, the code is rather verbose and inelegant. Ada offers us a better construct for
conditional sequences such as these.

the ...elsif...

Ada includes a variation of the if...then construct called elsif (pronounced "else if"). The ...elsif...
extends the sequence of alternative program blocks indefinitely.

if condition_1 then

statement(s) executed
if condition_1 is TRUE

elsif condition_2 then

statement(s) executed
if condition_2 is TRUE

:
:

elsif condition_n then

statement(s) executed
if condition_n is TRUE

else

statement(s) executed
if no condition is TRUE

this block may be omitted


end if;
Figure 3-3: A Graphical Illustration of the if...then...elsif...else...end if Construct

Using syntax notation, we define the if...then construct as,

if condition then
sequence_of_statements
{elsif condition then
sequence_of_statements}
[else
sequence_of_statements]
end if;

Recall from our discussion on Backus-Naur Form (BNF), that curly brackets enclose items that
may be repeated zero or more times, while square brackets enclose optional items (may be
omitted or included once). This syntax notation defines all versions of the if...then construct. The
item condition is a non-terminal referring to a Boolean expression such as X>Y or (A or B) and
not(C or D). Each time a non-terminal appears in a syntax expression it is assumed to be a
separate instance of that type of item. So, each appearance of the term condition in the syntax
notation above can represent a different Boolean expression.

67
Compare the code segment below with the previous version to compute letter grades.

if avg >= 90.0 then


grade:='A';
elsif avg >= 80.0 then
grade:='B';
elsif avg >= 70.0 then
grade:='C';
elsif avg >= 60.0 then
grade:='D';
else
grade:='E';
end if;

In the if...then...elsif construct the first TRUE condition encountered causes the associated code
block to be computed and the remainder of the construct to be ignored. For example, if avg=94.0
the grade is set equal to 'A'. Even though 94.0 is also greater than 80.0, 70.0, etc. grade is not
reset to 'B' , 'C', etc. because at most one of the code blocks will be executed.

3.3 The case Statement

The case statement selects a sequence of statements for execution based on the value of a
discrete expression. (Discrete types include integers, characters, or Boolean, but not floats.) The
syntax notation below shows the format of this conditional control statement. Each of the when
alternatives lists one or more discrete values of the same type as the discrete_expression. If the
value is a member of this list, the corresponding sequence of statements is executed.
case discrete_expression is
{when discrete_choice_list => sequence_of_statements}
[when others => sequence_of_statements]
end case;
Each discrete choice list must contain a unique set of values. That is, no possible value of the
discrete_expression can appear in more than one discrete_choice_list. The reserved word
others refers to all values of the discrete_expression type that are not included in one of the
previous discrete_choice_lists. When every possible value of the discrete type being tested is not
explicitly referenced in a when alternative, the when others alternative must be included. The
item sequence_of_statements refers to one or more executable statements.

case discrete_expression is

sequence of
when discrete_choice_list =>
statements
sequence of
when discrete_choice_list =>
statements
: :

sequence of
when others =>
statements

end case;

Figure 3-4: The Control Flow Diagram for the Ada case Construct
68
The control flow diagram illustrates the order of computation in the typical case statement.
Sometimes action is not needed for every value of the discrete identifier type. For such cases,
the when others alternative can be associated with the null operator.

when others => null;

A simple example will help. In the code segment below, the character char is tested to determine
if it is a letter, a digit, or some other symbol.

put("Enter a character... ");


get(char);
case char is
when '0'..'9' => put("This is a digit");
when 'A'..'Z' | 'a'..'z' => put("This is a letter");
when '+' => put("This is a plus sign";
when others => put("This is another symbol");
end case;

Exactly one of the alternative code blocks (sequence_of_statements) will be selected each time a
case statement is executed. The discrete choice list '0'..'9' refers to the characters 0
through 9 inclusive. The vertical strike ( | ) is used to represent the logical OR, so the discrete
choice list in the second alternative refers to uppercase or lowercase letters. The third alternative
contains a single item (plus sign) as its discrete choice list. Finally, the others refers to every
other value of type character not already listed.

Using Enumeration Types with case statements

Since enumeration types are discrete, we can use a case statement to select a sequence of
statements for execution based on the value of the enumeration type. Also, since the list of
possible values for the enumeration type is finite, we do not have to include the when others
alternative as a catch all in the case statement. An enumeration type consisting of n items is
equivalent to a sub range of integers 0,1,...,n-1 with each value in the range given a name.
Enumeration types are used to provide meaningful context to a computer program.

type color is (VIOLET,RED,ORANGE,YELLOW,GREEN,BLUE);


col : color;

The case statement below prints a statement indicating the selected color, where col is a variable
identifier of type color as defined above.

col:=ORANGE;
case col is
when VIOLET => put("this is violet");
when RED => put("this is red");
when ORANGE => put("this is orange")
when YELLOW => put("this is yellow");
when GREEN => put("this is green");
when BLUE => put("this is blue");
end case;

As with all other components of the Ada programming language, enumeration types are not case
sensitive. We have chosen to display enumeration type values in uppercase to distinguish them
from reserved words and other elements of the language.

69
3.4 Nested Conditionals

Conditional control statements can be used in combination to provide even more programming
power. The sequence_of_statements referred to in the syntax notation, represent any executable
Ada statements including other conditional control statements. This hierarchical feature of the
Ada syntax permits the nesting of conditionals as shown below.

W:=1;
X:=2;
Y:=3;
Z:=4;
if X>Y then
if X<Z and X>W then
X:=X+1;
end if;
else
if X<Z and X<W then
X:=X-1;
end if;
end if;
put(X);

See if you can determine the value of X that will be displayed. Note the use of indentation to
improve the readability of the nested if..then statements. As previously stated, the Ada compiler
ignores indentation, but it is an important part of good programming style.

In this example, the conditional control statement if X>Y then...else...end if contains two
executable code blocks. The first block is executed if X is greater than Y. This block contains the
conditional control statement if X<Z and X>W then X:=X+1 end if. The other code block (which
will be executed only if X is less than or equal to Y) is if X<Z and X<W then X:=X-1 end if.

Proper nesting of compound statements (i.e. statements that themselves contain sequences of
statements) is important. For example, when nesting if...then conditionals the first if..then to be
terminated (with an end if ) will be the one most recently initiated.

if conditional_1 then -- start of if..then #1


sequence_of_statements
if conditional_2 then -- start of if..then #2
sequence_of_statements
else -- goes with if..then #2
sequence_of_statements
if conditional_3 then -- start of if..then #3
sequence_of_statements
end if; -- end of if..then #3
end if; -- end of if..then #2
else -- goes with if..then #1
if conditional_4 then -- start of if..then #4
sequence_of_statements
end if; -- end of if..then #4
sequence_of_statements
end if; -- end of if..then #1

We can combine case conditionals with if...then conditionals. Consider the example below which
computes the value of Z for some value of X and Y.

Z:=0;
case X is
when 0 => if Y<=0 then
Z:=-Y*Z;
else

70
Z:=Y*Z;
end if;
when 1 => if Y<=0 then
Z:=Z-Y;
else
Z:=Z+Y;
end if;
when 2,3,4 => Z:= Y-1;
when 5..9999 => Z:=X;
when others => null;
end case;

This sample code segment also illustrates a potential problem with combined conditionals. They
can become very complicated. Since the logic of a combined conditional can be hard to
understand it is important to employ these constructs only when necessary and to provide
explanatory comments in the source code.

3.5 Exception Handling

When a program accepts user input or involves the use of data files, we cannot always predict
the effects of this external input on the program in execution. For example, the code segment
below asks the user to enter an integer. Let's say that instead of an integer the user types in
"GRINIXAL".
put("Enter an integer... ");
get(n);
put("Thank you. Have a nice day.");

Rather than displaying the Thank you message the program crashes because the input string
GRINIXAL does not match the expected data type of the get( ) procedure. This raises an
exception, which stops the program and displays the error message,

raised ADA.IO_EXCEPTIONS.DATA_ERROR

It would be better if we could rewrite the program to catch the mistake and ask the user to reenter
the value rather than permitting a run-time error. But to do this we would have to accept the input
as a string, test it to see if it could be interpreted as an integer, and finally, convert to an integer
manually and assign the value to the variable n. This would increase the complexity of the
program significantly. Ada provides a method for trapping errors called exception handling. The
exception construct is similar to the case construct.

begin
sequence_of_statements;
exception
{when exception_name {| exception_name} => sequence_of_statements}
[when others => sequence_of_statements]
end;

The term exception_name refers to the name of any run-time error conditions you wish to handle.
The reserved word others is used here to trap any exception not specifically listed. The non-
terminal sequence_of_statements refers to the code segment that will handle the exception.

The scope of an exception is limited by the begin..end block that contains it. This can be the
main program itself or any smaller code segment. Consider the modification of our simple
example code segment for requesting an integer from the user.

begin --exception demo


begin -- start of exception block
put("Enter an integer... ");
get(n);

71
exception
when DATA_ERROR =>
put_line("You should have entered an integer");
n:=0;
put_line("A value of 0 has been entered for you");
end; -- end of exception block
put("Thank you, have a nice day.");
end exception_demo;

We are trapping on the exception raised by a user entering the incorrect data type. If the user's
input cannot be interpreted as an integer by the get( ) procedure the exception code segment will
be executed.

Since the predefined exception named DATA_ERROR is a choice, the exception construct will
execute an alternative sequence of statements, which displays a warning message and gives n a
default value of 0. If the user's input is an integer, the exception construct will be skipped and the
next line to be executed will be the put( "Thank you..."); procedure.

Common Predefined Exceptions

DATA_ERROR an integer, float or enumeration type is being read but the


input cannot be interpreted as that type
DEVICE_ERROR indicates a hardware error in an I/O device
END_ERROR program is attempting to read past the end of a file
LAYOUT_ERROR attempt to exceed display limits (current_row, or current_col
for example)
MODE_ERROR attempt to read from an output file or write to an input file
NAME_ERROR attempt to open a file that does not exist
STATUS_ERROR attempt to read or write to a closed file or attempt to open an
open file
USE_ERROR improper use of a file, such as attempting to create a file that
already exists and is a read-only file.

Programmer-Defined Exceptions

In addition to the predefined exceptions listed above, we can create our own exception names
and raise them as desired in our applications. Exception names can be declared like any other
type of identifier in the declaration block. Consider the following example.

with ada.text_io, ada.integer_text_io;


use ada.text_io, ada.integer_text_io;

procedure exceptions_demo_2 is
TWEEDLE_DEE, TWEEDLE_DUM : exception;
x : integer;
begin
put("Enter an integer... ");
get(x);
if x=0 then
raise TWEEDLE_DEE;
elsif x=1 then
raise TWEEDLE_DUM;
end if;
put("Thank you, have a nice day.");
new_line;
exception
72
when DATA_ERROR =>
put("apparently you didn't enter an integer");
when TWEEDLE_DEE =>
put("apparently x is equal to zero");
when TWEEDLE_DUM =>
put("apparently x is equal to one");
end exceptions_demo_2;

The exceptions TWEEDLE_DEE and TWEEDLE_DUM are declared and used to test the value of
the input integer x. If x is 0, the if...then conditional raises the TWEEDLE_DEE exception. If x is
1 then TWEEDLE_DUM is raised. If the user's input is not an integer, the exception handler traps
on a DATA_ERROR and reports the invalid input.

3.6 Applications

Now we will practice applying the conditional constructs introduced in this chapter.

Area of a Triangle

3.1: Write an Ada program that computes the area of any triangle using the semi-perimeter
formula.

As shown in the pseudocode below, this program is an example of the input-process-output


programming model. We will use this model to implement a solution to the problem.

-- Declare the input, output, and internal computation identifiers


-- Input:
-- Obtain the necessary data from the user
-- Process:
-- Compute the value of S
-- Compute the area of the triangle
-- Output:
-- Display the area or an appropriate message

First, we need to find the semi-perimeter formula. Any good math handbook will provide a
definition of the semi-perimeter formula as,

Area triangle = S ( S − a )( S − b)( S − c)


S = (a + b + c ) 2

where a, b, and c are the lengths of the three sides of the triangle. The semi-perimeter S is just
half the sum of these three lengths. We can compare the semi-perimeter formula with another
formula for computing areas of triangles.

5 Area = 1/2 base x height


4 = 1/2 3 4
= 6 square units

3
Figure 3-5: An Alternative Method for Computing the Area of a Triangle

73
Letting a=3, b=4, and c=5, we obtain S=(3+4+5)/2=6 as the semi-perimeter. The area as
computed by the semi-perimeter formula is,

Area = 6(6 − 3)(6 − 4)(6 − 5) = 36 = 6 square units

The advantage of the semi-perimeter formula is that we can compute the area of a triangle
directly from the lengths of its sides. When the triangle is not a right triangle we need to perform
additional calculations to determine its vertical height.

Figure 3-6: Heights of Example "Non-Right" Triangles

The numeric computation can be written in Ada as,

put("Enter the lengths of the three sides of the triangle.. ");


get(a);
get(b);
get(c);
S:=(a+b+c)/2.0;
area:=sqrt(S*(S-a)*(S-b)*(S-c));
put("The area of the triangle is ");
put(area,0,4,0);
put(" square units");

This code segment performs the basic operations of input, process and output, but there is a
problem. What happens if the length of one of the sides is greater than the sum of the lengths of
the other two sides?

For example, let a=10, b=2, c=4. S=16/2=8, which gives an area of

Area = 8(8 − 10)(8 − 2)(8 − 4) = 8 ⋅ (−2) ⋅ 6 ⋅ 4 = − 384

which is not a real number. Negative roots in this formula occur when the three lengths a,b, and
c cannot form a triangle. Our program needs to check for this condition to avoid attempting to
take the square root of a negative value.

Figure 3-7: An Example of Three Sides that Do Not Form a Triangle

74
There are many ways to address this problem. We will consider three approaches here.

Version 1: We could test to see if a+b>c but there is no assurance that the user will enter the
largest value as c. We could request that the values be entered in order of increasing length but
this would add an unnecessary complication to the program interface.

A better solution would be to make sure that none of the three values is greater than the sum of
the other two. That is, make sure that all three of the following conditions are TRUE:

a + b > c
a + c > b
b + c > a

These values could be combined into a single Boolean expression as,

a+b>c and a+c>b and b+c>a

If this expression is true then we know that the semi-perimeter area formula will produce a real
value (i.e. non-negative root). We can use the if..then..else conditional to test the lengths of the
sides before the computations

if a+b>c and a+c>b and b+c>a then


do the computations
else
report the problem
end if

The source code for the complete program is provided below.

with ada.text_io, ada.float_text_io, ada.numerics.elementary_functions;


use ada.text_io, ada.float_text_io, ada.numerics.elementary_functions;

procedure semi_perimeter_area is
a,b,c,s,area : float;
begin

put("Enter the lengths of the three sides of the triangle.. ");


get(a);
get(b);
get(c);

if a+b>c and a+c>b and b+c>a then

s:=(a+b+c)/2.0;
area:=sqrt(s*(s-a)*(s-b)*(s-c));
put("The area of the triangle is ");
put(area,0,4,0);
put(" square units");

else

put("Sorry but these sides cannot make a triangle.");

end if;

end semi_perimeter_area;

75
We have withed the package ada.numerics.elementary_functions in order to gain access to the
sqrt( ) function. Note that the Boolean expression must be true for the computations to be
performed.

Version 2: An equally valid approach would be to ensure that one of the sides is not greater than
the sum of the other two sides using the Boolean expression,

a+b<=c or a+c<=b or b+c<=a

In this case, the program would be written to perform the computations only if this expression is
false.

if a+b<=c or a+c<=b or b+c<=a then


report the problem
else
do the computations
end if

In this version, only one of the terms needs to be TRUE to block the computations. This version
of the Boolean expression is the logical negation of the previous version.

a+b<=c or a+c<=b or b+c<=a = not(a+b>c and a+c>b and b+c>a)

We notice that a+b<c is true whenever a+b>c is false. If we let a+b<=c = P, a+c<=b = Q, and
b+c<=a = R we can rewrite the expression above as,

P or Q or R = ~( ~P and ~Q and ~R )

where the ~ is the symbol for the unary logical operator not. We can use a truth table to verify
the equivalence.

P Q R PorQ (PorQ)orR ~P ~Q ~R ~Pand~Q (~Pand~Q)and~R ~[*]


F F F F F T T T T T F
F F T F T T T F T F T
F T F T T T F T F F T
F T T T T T F F F F T
T F F T T F T T F F T
T F T T T F T F F F T
T T F T T F F T F F T
T T T T T F F F F F T

3
Since there are three Boolean variables P, Q and R there are 2 = 8 possible permutations of
truth values for these variables. The asterisk (*) in the rightmost column ~[*] represents the
expression (~Pand~Q)and~R. The columns corresponding to the left side and the right of the
Boolean expression being tested are identical. From this exhaustive test we find that the
expression,

P or Q or R = ~( ~P and ~Q and ~R )

is true for all truth values of P, Q and R (called a tautology). Depending upon the application, it is
sometimes better to use the negated form of a Boolean expression in a conditional. In this case,
either version is adequate.

Version 3: Let's take one more look at this problem. If we compute the value under the radical
first we can simply test its value and compute the area only if this term is non-negative. For
example, we can rewrite the code segment,

76
put("Enter the lengths of the three sides of the triangle.. ");
get(a);
get(b);
get(c);
s:=(a+b+c)/2.0;
area_squared:=s*(s-a)*(s-b)*(s-c)
if area_squared>=0.0 then
area:=sqrt(area_squared);
put("The area of the triangle is ");
put(area,0,4,0);
put(" square units");
else
put("Sorry but these sides cannot make a triangle.");
end if;

In this version, we only test to make sure that the term under the radical is not negative, which
prevents an error (specifically ADA.NUMERICS.ARGUMENT.ERROR), but does it prevent the
computation of invalid areas in all cases?

Consider that the area_squared term will be negative if one of its multiplicative terms (s-a), (s-b)
or (s-c) is negative. But if two of these terms are negative the resulting product will be positive
and the computation will be performed. Is this possible? Is this OK? More specifically we need to
prove that

s(s-a)(s-b)(s-c)>0 if and only if [(a+b)>c and (a+c)>b and (b+c)>a]

In this case, using one of the Boolean expressions to test the conditional of interest is better than
testing the value under the radical.

3.2: Write an Ada program to compute net pay, given hourly wage, number of hours worked and
a tax rate of 18%. Assume time-and-a-half (i.e. 1.5 x regular pay) for all hours over 40 per week.

Input - We will ask the user to enter the hourly wage and number of hours worked. Since only
one tax rate is specified we will include the tax rate as a constant in our program.

Process - We will compute the gross pay, then compute the tax and finally compute the net pay
as the gross minus the tax. The first 40 hours of work will earn regular pay. Hours over 40 (if any)
will earn 1.5 times the hourly wage.

-- Declare Identifiers for Input, Output and Internal Computations


-- Declare Tax Rate (0.18)
-- Declare Number Hours Regular Time (40)
-- Declare Overtime Rate (1.5)
-- Input:
-- Get Hourly Wage
-- Get Hours Worked
-- Process:
-- Compute Gross Pay
-- Compute Tax Paid
-- Compute Net Pay
-- Output:
-- Put Gross Pay
-- Put Tax Paid
-- Put Net Pay

We could accumulate the gross pay using the following computations,

if hours_worked>40.0 then
77
gross_pay:=40*hourly_wage + (hours_worked - 40.0)*1.5*hourly_wage;
else
gross_pay:=hours_worked*hourly_wage;
end if;

The next part of the computation process is the computation of the tax. As stated, we will declare
the 18 percent tax rate as a constant

tax_rate : constant float := 0.18;

The tax is computed as the tax_rate times the gross_pay,

tax:=tax_rate*gross_pay;

and the net pay is the gross_pay minus the tax,

net_pay:=gross_pay - tax;

Output - Our output could be as simple as the value of the net pay, but it is a nice touch to include
helpful text and formatting. For example,

new_line;
put("Gross pay = ");
set_col(20);
put("$ ");
put(gross_pay,4,2,0);
new_line;
put("Tax = ");
set_col(20);
put("$ ");
put(tax,4,2,0);
new_line;
put("Net pay = ");
set_col(20);
put("$ ");
put(net_pay,4,2,0);
new_line;

Putting it all together we have,

with ada.text_io, ada.integer_text_io, ada.float_text_io;


use ada.text_io, ada.integer_text_io, ada.float_text_io;

procedure net_pay_demo is

tax_rate : constant float := 0.18;


regular_pay,overtime_pay,gross_pay,net_pay : float;
hours_worked,hourly_wage, tax : float;

begin

-- Input Section
put("Enter hourly wage... ");
get(hourly_wage);
put("Enter number of hours worked... ");
get(hours_worked);

-- Process Section
if hours_worked>40.0 then
regular_pay:=40.0 * hourly_wage;

78
overtime_pay:=1.5 * hourly_wage * (hours_worked - 40.0);
else
regular_pay:=hours_worked * hourly_wage;
overtime_pay:=0.0;
end if;

gross_pay:=regular_pay + overtime_pay;
tax:=tax_rate*gross_pay;
net_pay:=gross_pay - tax;

-- Output Section
new_line;
put("Gross pay = ");
set_col(20);
put("$ ");
put(gross_pay,4,2,0);
new_line;
put("Tax = ");
set_col(20);
put("$ ");
put(tax,4,2,0);
new_line;
put("Net pay = ");
set_col(20);
put("$ ");
put(net_pay,4,2,0);
new_line;
end net_pay_demo;

We have used the set_col( ) procedure in our output section. It is used here to align the
displayed values of gross pay, tax, and net pay. Here is an example of the output of our
net_pay_demo program:

Enter hourly wage... 12.50


Enter number of hours worked... 47

Gross pay = $ 631.25


Tax = $ 113.63
Net pay = $ 517.63

79
Exercises

3.1 Write an Ada program to compute the real root(s) of the quadratic formula (if any). This
program should accept the values of the coefficients a, b, and c and report the number of real
roots found along with their values.
− b ± b 2 − 4ac
x=
2a
3.2 In Exercise 3.1 you need to verify that the discriminant (the part of the expression under the
radical) is greater than or equal to 0.0. Discuss the possible errors that could occur when
attempting to determine that the discriminant is equal to zero. (refer to Section 2.7)

3.3 What is the significance of the sign and magnitude of the discriminant to Exercise 3.1?

3.4 Write an Ada program to display the letter grade associated with an average score input by
the user. Use the 7-point scale (i.e. (>=93 and <=100) = A, (>=86 and <93), etc.

3.5 Redo the program of Exercise 3.4 for a 10-point grading scale.

3.6 Modify the program of Exercise 3.5 to include commentary for letter grades that are close the
boundaries. For example,

Average Score Letter Commentary


Grade
100 A "Great Job"
95 A
90 A "Whew, that was close!"
89 B "Tough Break"
84 B
80 B "To B or not to B"
60 D "On the Edge"
59 E "You made the highest possible failing grade."

3.7 Extend the program from Exercise 3.5 to display an appropriate comment based on the letter
grade. For example A -> "Excellent", B=> "Very Good", C=> "OK", D=>"Please Try Harder",
E="Sorry About That".

3.8 Modify the program exception_demo discussed in this chapter so that it gives the user
another chance to enter the correct input.

3.9 Write a version of the program described in Exercise 2.9 that checks for proper input.
Specifically allow for the possibility the odometer readings have been entered in the wrong order
and/or the odometer has rolled over (started over). You may assume that the maximum number
of miles displayed by the odometer is 99,999.9 miles.

80
Chapter 4 - Using Loops

4.1 The generic loop...exit when...


Placement of the Conditional

4.2 The for...loop...

4.3 The while...loop...

4.4 Nested loops

4.5 Applications
Computing the Average, Minimum and Maximum
Displaying the Multiplication Tables

81
Chapter 4 - Using Loops
In this chapter, we will study the repetition construct, also called looping. This is one of the most
important computational constructs provided by a programming language.

4.1 The general loop...exit when...

We will begin our study of loops with the general loop...end loop construct. This construct is used
to repeat a segment of code until the program is interrupted. Each repetition of the body of the
loop (sequence_of_statements) is called an iteration.

loop

sequence_of_statements

end loop;

Let's build a simple example program using the generic infinite loop.

with ada.text_io;
use ada.text_io;
procedure hello_hello is
begin
loop
put("Hello There ");
end loop;
end hello_hello;

This code segment will continue displaying the words "Hello There " until the program is
terminated by some external command.

Hello There Hello There Hello There Hello There Hell


o There Hello There Hello There Hello There Hello Th
ere Hello There Hello There Hello There Hello There
Hello There Hello There Hello There Hello There Hell
o There Hello There Hello There Hello There Hello Th
scrolling text

ere Hello There Hello There Hello There Hello There


Hello There Hello There Hello There Hello There Hell
o There Hello There Hello There Hello There Hello Th
ere Hello There Hello There Hello There Hello There
Hello There Hello There Hello There Hello There Hell
o There Hello There Hello There Hello There Hello Th
ere Hello There Hello There Hello There Hello There_

Figure 4-1: Example Output of hello_hello.adb

The general loop construct can use the exit when conditional to leave the loop and to
continue normal program execution. In the following example the code segment computes and
displays the sum of the first 100 positive integers.

sum:=0; -- used as an accumulator for the sum


count :=0; -- used as the loop counter
loop
count:=count+1; -- counter is incremented here
sum:=sum+count; -- the sum is accumulated here
82
exit when count>=100;
end loop;
put("Sum of first 100 integers is... ");
put(sum,0);

When the conditional in the exit when statement is true, program control passes to the first
statement after the end loop statement. The exit when statement can be placed anywhere inside
the loop construct so long as the identifier(s) involved in its conditional expression has been given
a value before the exit when is encountered. Also, it is important to verify that the variables
making up the conditional are being updated by the code segment in the loop.

loop
sequence_of_statements;
returns to top exit when conditional_expression;
of loop
sequence_of_statements; if true then go to here
end loop;
sequence_of_statements;

Upon entering the loop..end loop construct, the sequence of statements will be executed in order
(top to bottom). If the conditional expression in the exit when statement is true, then program
control jumps to the sequence of statements outside the loop. Otherwise, the remaining
statements in the loop are executed in order. At the end of the loop, program control jumps back
up to the top of the loop code block as illustrated by the arrows in the example above.

Placement of the Conditional

Consider the pseudocode description below, which accumulates a sum of grades and computes
their average. The user is asked to enter some negative value when data entry is complete. Any
out-of-range value that is used to indicate the end of data entry is called a sentinel value.

-- initialize the counter for the loop


-- initialize the accumulator for the sum
-- Input:
-- data entry loop
-- get the next grade (called this_grade) from user
-- if this_grade is not valid then exit this loop
-- add 1 to counter
-- add this_grade to sum
-- end of the data entry loop
-- Process:
-- compute average avg = sum/counter
-- Output:
-- Display avg

When we implement source code that includes loop constructs, it is easy to make errors
concerning the manner in which program control leaves or exits the loop. A common
misunderstanding is the exact position in the source code where the loop is exited. Look at our
implementation of the grade averaging program below:

sum:=0.0; -- this code segment contains an error!


count:=0;
loop
put("Enter next value (or a negative value to stop)... ");
get(x);
sum:=sum+x;
count:=count+1;
exit when x<0.0;
end loop;

83
avg:=sum/float(count);
put("The average is = ");
put(avg,0,4,0);

Can you see the problem? Since we want to compute the average of some unknown number of
values, we need to accumulate the sum of the values entered by the user and we need to count
how many values have been entered. With these two quantities we can compute the average
with the assignment

avg := sum/count;

In the previous code segment, we stay in the data entry loop as long as the value of x (the
identifier we are using to enter grades) is not negative. When a negative value of x is entered,
program control is passed out of the loop to the avg computation by the exit when statement.
Unfortunately, the value of x that was used as a sentinel value has already been added to sum
and the count has already been incremented. To fix this problem, we can move the exit when
statement to come after get(x) statement and before the sum:=sum+x statement.

sum:=0.0; -- the fix is in


count:=0;
loop
put("Enter next value (or a negative value to stop)... ");
get(x);
exit when x<0.0; -- proper location for the exit statement
sum:=sum+x;
count:=count+1;
end loop;
avg:=sum/float(count);
put("The average is = ");
put(avg,0,4,0);

4.2 The for...loop...

One of the oldest versions of a repetition programming construct is the for...loop. An


implementation of this construct was part of the first high-level language (FORTRAN) introduced
nearly 50 years ago. In its simplest implementation, the for...loop has a built-in index counter that
controls the number of repetitions of the loop code block.

for index in start_val..finish_val loop

sequence_of_statements

end loop;

In words, this construct says, "give the identifier index an initial value of start_val and execute the
body of the loop, then let index:=index+1. If index is not greater than finish_val then execute the
body of the loop again. Repeat for each value in the range until index>finish_val".

The for...loop is most closely related to the mathematical expression for finite series. For
example, consider the formula for the sum S of the first 100 integers.
100
S= i
i =1

84
In Ada we can compute the sum of the first 100 integers using the for...loop as,

S:=0; -- initialize the accumulator to zero


for i in 1..100 loop -- i:=i+1 from 1 to 100
S := S + i; -- S accumulates the sum of i values
end loop;

Compare this code segment with the previous version of integer summation using the generic
loop. Notice that the incrementing of i is hidden, resulting in a more compact source code. The
for...loop is hardwired to repeat the code segments it contains, a fixed number of times. Note that
Ada does not permit the value of the loop counter (i in this case) to be assigned a value inside the
loop. Other languages permit manipulation of the counter value, which has different effects
depending on the language being used. The details of the effects of altering the loop counter are
a subject for a comparative language course. Rules of good programming practice prohibit the
manipulation of the loop counter.

We will return to the for...loop when we introduce array variable types.

4.3 The while...loop...

The while...loop uses a built-in conditional at the top of the loop to control when the program
enters and exits the loop. The loop is repeated as long as the conditional is true.

while conditional loop

sequence_of_statements

end loop;

A requirement of the while...loop is that any identifiers in the conditional must be defined before
program control reaches the loop. For example, we need to initialize the value of the counter and
the value of the accumulator S before entering the while...loop. Otherwise, the conditional would
raise a warning. If S is not initialized then it will contain an arbitrary value (garbage) that will
result in unpredictable behavior in your program.
count:=1; -- this loop computes the sum of the first
S:=0; -- 100 integers
while count<=100 loop
S:=S+count;
count:=count+1;
end loop;
The code segment above shows the while...loop implementation of the same integer sum
formula.

4.4 Nested loops

The rules for nested loops are similar to those for nested conditionals. Any end loop statements
correspond to the loop that was most recently encountered that is still open.

for i in 1..n loop -- start of loop i


for j in 1..m loop -- start of loop j
for k in 1..p loop -- start of loop k
sequence_of_statements;
end loop; -- end of loop k
end loop; -- end of loop j
end loop; -- end of loop i
85
How many times is the sequence of statements executed in the sample code above? The body
of the i-loop is repeated n times. The body of the j-loop is repeated m times each time it is
entered from inside the i-loop. This means that the body of the j-loop is repeated n ∗ m times.
The body of the k-loop is repeated p times, each time the k-loop is called inside the j-loop. So the
total number of times the sequence of statements is executed is n ∗ m ∗ p . Later, we will cover
this issue in much greater detail as part of algorithm analysis in Part II of this text.

4.5 Applications

4.1: Write an Ada program that determines the average, minimum, and maximum of a list of
integer values input by the user. You may use -999 as the sentinel value.

As usual, we will begin with a pseudocode description of the program.

-- declare input, output, and internal computation identifiers


-- minimum, maximum, average
-- declare an accumulator for sum
-- declare a counter for the num_entries
-- determine initial values for minimum, maximum

-- input:
-- Ask the user to enter values or -999 to stop
-- loop
-- get the next value X
-- if X is not valid (i.e. X=-999) then exit loop
-- add the value of X to sum (the accumulator)
-- add 1 to the value of num_entries (the counter)
-- if X < current value of minimum then minimum:=X
-- if X > current value of maximum then maximum:=X
-- end loop

-- process:
-- compute average

-- output:
-- display average
-- display minimum
-- display maximum

The computation of the average is essentially the same as in the example given in Section 4.1.
In this case, however, we need to keep up with the minimum and maximum value that has been
entered by the user. We can do this by comparing the current minimum and maximum to the
next value of x and making the appropriate assignments.

if x<minimum then
minimum:=x;
end if;

if x>maximum then
maximum:=x;
end if;

This works, except for one problem. We need to give initial values to minimum and maximum.
We want to make sure that both of these values get replaced by one or more of the x values in
the list. In other words, we don't want to allow a situation in which values of minimum and
maximum are output that were not entered by the user. We can use the attribute functions 'first
and 'last to give extreme values to minimum and maximum that would be certain to be replaced.

86
minimum:=integer'last;
maximum:=integer'first;

Notice that we give minimum the last (largest possible) value and maximum the first (lowest
possible) value so that any value of x will replace these extreme values. This ensures that the
final values of minimum and maximum will be values in the list. These initial values are assigned
before entering the loop.

minimum:=integer'last;
maximum:=integer'first;
sum:=0;
count:=0;
loop
put("Enter the next value or enter -999 to quit... ");
get(x);
exit when x=-999;
count:=count+1;
sum:=sum+x;
if x<minimum then
minimum:=x;
end if;
if x>maximum then
maximum:=x;
end if;
end loop;
avg:=float(sum)/float(count);
new_line;
put("Count = "); set_col(20); put(count,8); new_line;
put("Average = "); set_col(20); put(avg,4,4,0); new_line;
put("Minimum = "); set_col(20); put(minimum,8); new_line;
put("Maximum = "); set_col(20); put(maximum,8); new_line;

Not all high-level languages provide a simple means to determine the first and last values of a
data type, so the solution presented above is not generally applicable. There is a way to set the
initial values of minimum and maximum without using attribute functions. When the user enters
the first value of x we can set both the minimum and maximum to this value. We can replace the
conditional in the dashed box above with the compound conditional statement shown below to
implement this method,

if count=1 then

minimum:=x;
maximum:=x;

else

if x<minimum then
minimum:=x;
end if;

if x>maximum then
maximum:=x;
end if;

end if;

There are still a number of issues that have been ignored in this example. Some of these we will
address in later chapters. For example, consider the case in which the user does not enter any
values. How do we ensure that the output is appropriate? When the count=0 we should provide

87
an alternative output message. Also, the computation of avg must be avoided when no data has
been entered. For now we will assume that the user will make reasonable choices for input.

4.2: Write an Ada program to display the multiplication tables up to 10 x 10.

We can use a nested for...loop to display the results of multiplying every pair of integers between
1 and 10.

for i in 1..10 loop


for j in 1..10 loop
put(i*j, 5);
end loop;
new_line;
end loop;

The inner loop displays the value of i*j for all values of j (1..10) and for a particular i value.
The new_line procedure is placed after the inner loop and inside the outer loop. This starts the
display of a new line of values for each value of i. Thus the 100 values are displayed in a 10 by
10 array.
1 2 3 4 5 6 7 8 9 10
2 4 6 8 10 12 14 16 18 20
3 6 9 12 15 18 21 24 27 30
4 8 12 16 20 24 28 32 36 40
5 10 15 20 25 30 35 40 45 50
6 12 18 24 30 36 42 48 54 60
7 14 21 28 35 42 49 56 63 70
8 16 24 32 40 48 56 64 72 80
9 18 27 36 45 54 63 72 81 90
10 20 30 40 50 60 70 80 90 100

We can make the output fancier by including row and column headers showing the multiplicands
(the values of i and j being multiplied).

column header
row header

body of multiplication table

Figure 4-2: Layout for the Fancy Multiplication Table Program

Since text outputs are generated row-by-row, the column header must be generated before
beginning the multiplication table. On the other hand, each entry of the row header must be
generated just before the corresponding row of the multiplication table is output.

88
set_col(6);
for j in 1..10 loop
put(j,5); -- displays column header
end loop;
new_line;
for i in 1..10 loop
put(i,5); -- displays row header
for j in 1..10 loop
put(i*j, 5);
end loop;
new_line;
end loop;

The output of this code segment is shown below.

1 2 3 4 5 6 7 8 9 10
1 1 2 3 4 5 6 7 8 9 10
2 2 4 6 8 10 12 14 16 18 20
3 3 6 9 12 15 18 21 24 27 30
4 4 8 12 16 20 24 28 32 36 40
5 5 10 15 20 25 30 35 40 45 50
6 6 12 18 24 30 36 42 48 54 60
7 7 14 21 28 35 42 49 56 63 70
8 8 16 24 32 40 48 56 64 72 80
9 9 18 27 36 45 54 63 72 81 90
10 10 20 30 40 50 60 70 80 90 100

The underscore "_" and vertical bar "|" could be used to improve readability even more.
Multiplication Tables
1 2 3 4 5 6 7 8 9 10
____________________________________________________
1 | 1 2 3 4 5 6 7 8 9 10
|
2 | 2 4 6 8 10 12 14 16 18 20
|
3 | 3 6 9 12 15 18 21 24 27 30
|
4 | 4 8 12 16 20 24 28 32 36 40
|
5 | 5 10 15 20 25 30 35 40 45 50
|
6 | 6 12 18 24 30 36 42 48 54 60
|
7 | 7 14 21 28 35 42 49 56 63 70
|
8 | 8 16 24 32 40 48 56 64 72 80
|
9 | 9 18 27 36 45 54 63 72 81 90
|
10 | 10 20 30 40 50 60 70 80 90 100
|

89
Exercises

4.1 Write an Ada program to display a rectangular array of stars (asterisks) with n rows and m
columns where n and m are input by the user.

a. solid rectangles
* * * * *
* * * * * * * *
* * * * * * * * * * * * * * * *
* * * * * * * * * * * * * * * *
* * * * * * * * * * * * * * * *

b. open rectangles
* * * * *
* * * * *
* * * * * * * * * * * *
* * * * * *
* * * * * * * * * * * * * * * *

Make sure your program works for limiting cases such as n=1 or m=0.

4.2 Write an Ada program to display a triangular array of stars (asterisks) where the height and
base (you may let height=base) of the triangle are input by the user.

a. left justified right triangles with base at bottom


b. right justified right triangles with base at top
c. isosceles triangle with base at bottom

* * * * * * * *
* * * * * * * * *
* * * * * * * * * *
* * * * * * * * * * *
* * * * * * * * * * * *
*
4.3 The code segment below displays a solid NxN array of stars (asterisks). Develop a
conditional statement to replace the put("* ") procedure so that the code segment displays an
X pattern of stars. No other modifications of the program are allowed.

for i in 1..n loop * * * * * * * *


for j in 1..n loop * * * * * * * *
put("* "); * * * * * * * *
end loop; * * * * * * * *
new_line; * * * * * * * *
end loop; * * * * * * * *

Verify that your version of the code segment executes properly for both even and odd values of n.
Also test for limiting conditions such as n=0, 1 and 2.

4.4 In Gödel, Escher, and Bach: An Eternal Golden Braid, Douglas Hofstadter describes a
mathematical exercise called the Hailstone Problem. In this exercise you start with some positive
integer n and determine the number of iterations required to reach n=1 under the following
conditions:
90
if n is even, let n = n/2
if n is odd, let n=3n+1
repeat until n=1.

Write an Ada program that accepts a positive integer n from the user and displays the list of
values of n generated with a running count of the number of iterations until n=1.

Will this program terminate for every starting value of n? In other words, will the condition n=1
eventually be reached regardless of initial value of n? Alternatively, does there exist a starting
value of n for which n=1 will never be reached?

Can you develop a formula that computes the count without having to perform the iterations?

Why do you think this exercise is referred to as the Hailstone Problem?

4.5 Write an Ada program to play the Hi-Lo Game in which the computer guesses a player's
number between 1 and 100. After each guess the human player should respond higher, lower or
correct. Your program should be able to guess the player's number in no more than 7 tries.

How many tries would your program need to guess a number between 1 and 1000 in the worst
case? ...a number between 1 and 1,000,000?

4.6 Write Ada code to compute the following series:

n n
a. X= i2 b. X= (−1) i i
i =1 i =1

n n
i −1
c. X = 2i + 1 d. X=
i =0 i =1 i +1
n n
i +1
e. X = ∏i f. X =∏
i =1 i =1 i

4.7 Write an Ada program to compute the number of permutations of m out of n items (order is
important), where n and m are entered by the user and n>=m.

4.8 Write an Ada program to compute the number of combinations of m out of n items (order is
not important), where n and m are entered by the user and n>=m.

4.9 Write an Ada program to generate a table showing the number of acres of land per person on
the Earth for a population of 2, 4, 6, 8, and 10 billion (column header), and a percentage of land
that is usable equal to 10, 20, . . . , 100 percent. You may assume that 30% of the surface of the
Earth is land. You will need to determine the number of acres on the surface of the Earth. You
should find the values you need in a table of physical constants.

91
92
Chapter 5 - Text Files and Structured Data Types

5.1 Text Files


Operations on Text Files
A Simple Example
Handling Physical Storage Media

5.2 Arrays
One Dimensional Arrays
Higher Dimensional Arrays

5.3 Records

5.4 Arrays of Records

5.5 Applications
Loading Employee Data into an Array of Records
Working with Arrays of Records
Creating a Text File
Sorting Numeric Data
Printing Payroll Checks

93
Chapter 5 - Text Files and Structured Data Types
In this chapter, we learn how to create and use structured data types. We also briefly review a
special type of sequential data file called a text file.

5.1 Text Files

A file is an organized collection of information written on some form of data-storage medium, such
as a hard drive, floppy drive, CD, magnetic tape, or memory card. Files are used to hold
information for retrieval, editing, or distribution. The medium used to hold files is usually referred
to as permanent or non-volatile. This means that it does not require external power to hold its
contents. This is different from random access memory (RAM), which is the internal memory of
the computer. Information stored in RAM is lost when the computer loses power.
A text file is a sequential file comprised of the ASCII characters with values between 0 and 127.
Sequential access means that the contents of the file must be accessed in the same order it was
created. File access is an important subject of computer science. For now, we will limit our study
to the file operations that permit us to create, open, access, and edit sequential text files.

Operations on Text Files


create - refers to the writing of new files to the storage device
open - preparation of an existing file for reading or rewriting
reset - moves the read/write point back to the beginning of the file
close - completes the recording of an output file or removes an input file from access

Ada provides functions and procedures to support file access and creation. Ada uses the open( )
procedure to make an existing file accessible for reading or writing.
open(logical_filename,file_mode,physical_filename);
The logical_filename is the name used in the program code to refer to the file. The
physical_filename is the actual name of the file (either a string variable or a string literal) as saved
in secondary storage (e.g. the hard drive, floppy disk, or CD). Note: This file name must comply
with the naming conventions of the operating system of the platform in use. For Windows OS, file
names may include blank spaces and letter case does not matter. However, in a UNIX/Linux OS
letter case is significant. The mode parameter file_mode indicates whether the file is being
opened for reading or writing by the program. If file_mode is set to in_file, the program will be
reading from the file. If file_mode is set to out_file then the program can write to the file. Ada
uses the procedure get( ) to read from the file. This procedure is used in the same manner as it
is used when getting input from the keyboard except that the first parameter is the
logical_filename. For example,
get(datin,num);
Other procedures are defined for file input such as get_line( ) and skip_line( ) which permit the
program to read an entire line of text at once or to skip the rest of the current line:
get_line(datin,str,leng);
skip_line(datin);
In the example above, the get_line( ) reads the current line of text from the file datin and places it
in the string variable str. The number of characters found in the line is stored in the integer
variable leng (see Section 2.3). Ada uses the create( ) procedure to prepare a new file to receive
data. The parameters of the create( ) procedure are the logical_filename, the file_mode (out_file
only for text files) of the file, and the physical_filename.
create(logical_filename,out_file,physical_filename);
The logical filename is the name used in the program to redirect input and output to the file from
the default devices (e.g. keyboard and monitor). The mode parameter out_file specifies that the
file is an output file and will be written to. The physical filename is a string containing the actual
name of the file, which appears in the operating system directory list of filenames.
94
As stated, the physical filename can be a string literal or a string identifier. Examples of the use
of the create( ) procedure are,
create(datin,out_file,"sampledata.dat");
create(weasel_data,out_file,"c:\weasels\otter.txt");
create(my_data,out_file,fname(1..leng));
In the first example, the logical filename datin is associated with the physical file sampledata.dat.
In the second example, the logical filename weasel_data is associated with the physical file
otter.txt located in the sub directory weasels. In the last example the logical filename my_data is
associated with a physical file whose name is stored in the string identifier fname(1..leng).

So why do we need both a logical filename and a physical filename? One reason is that we want
to be able to write programs that deal with more than one specific file. We can use the get_line( )
procedure to obtain a physical filename from the user,

fname : string(1..30);
leng : integer;
begin
put("Enter filename... ");
get_line(fname,leng);
create(datin,out_file,fname(1..leng));

The identifier fname must be declared as a string of sufficient size to hold any filename the user
might enter. The number of characters in the input string is leng, which is used to specify the
length of the physical filename as a substring of the string fname. Using this approach the code
can refer to any file by the logical filename datin.

When reading or writing a file, the default location of the file is the same as the directory
containing the executable program accessing the file. If the location of the file is different from
that of the running program, the path to the file must be specified as part of the
physical_filename.

We can use special versions of procedures such as put( ), put_line( ), and new_line( ) to redirect
their outputs to the file datin by including this filename as the first parameter.
put(datout,num,5);
put_line(datout,"Hello There");
new_line(datout);
When the program has finished writing to the file we can close it using the close( ) procedure.
close(datout);
Closing an output file is particularly important because the close( ) procedure places an
end_of_file marker on the end of the file. It also ensures that any data left in the output buffer has
been transferred to the file before closing. The details of I/O buffering are too involved to address
here, but it is important to understand that high-level languages do not have direct access to I/O
operations on secondary storage devices. Instead, programs communicate with the OS kernel
that passes data into and out of memory buffers for indirect communication with the storage
device.

Actually, text files include two non-printable control characters called the end_of_line and
end_of_file markers.

The end_of_line marker indicates the end of the current line of text. The end_of_file marker
indicates the end of the file. Most DOS and file access software will not attempt to read past an
end_of_file marker. The Ada language provides two Boolean functions that can test for these
markers. These functions use the logical file name as their argument.

95
end_of_line(logical_filename) - returns TRUE if the next character to be read
in the file named logical_filename is the
end_of_line marker.
end_of_file(logical_filename) - returns TRUE is the next character to be read
in the file named logical_filename is the
end_of_file marker.

A Simple Example

Any text editor (such as NotePad) can be used to generate text files. MSWord can generate plain
ASCII files by choosing the Text Only (*.txt) option under the Save As command. AdaGIDE
generates plain ASCII files by default.

We can write a program to read integers from a simple text file called sampfile.dat.

with ada.text_io, ada.integer_text_io; sampfile.dat


use ada.text_io, ada.integer_text_io;
procedure file_demo is 9
num : integer; 2
datin : file_type; 4
begin 1
open(datin,in_file,"sampfile.dat"); 8
for i in 1..5 loop 3
get(datin,num); 2
put(num,5);
new_line;
end loop;
close(datin);
end file_demo;

This program opens the file sampfile.dat for input. It reads the first 5 values from the file and then
closes the file. While the file is open it is referred to as datin in the program even though the file
has the physical_filename sampfile.dat.

The extension .dat is arbitrary. We could have used .txt or .duh or no extension at all. Keep in
mind that the file may not appear in the directory listing displayed when the Open command is
invoked in some text editors. This is because the default extension expected can be limited to
.doc, .adb, or .txt. You can force the display of an extension such as .dat by entering *.dat in the
File Name textbox or you can enter *.* to see all files in the directory.

Handling Physical Data Storage Media

Magnetic media such as floppy disks or magnetic tapes can be expected to hold their data for up
to 10 years if properly maintained. The life of a floppy disk that is overwritten frequently is much
shorter. Floppy disks should not be relied upon to hold important information. Always
maintain multiple copies of essential data at multiple locations when possible.

Optical media such as data CDs have a much longer life expectancy when properly cared for
than do magnetic storage media. It is estimated that professionally recorded CDs have a shelf
life of well over 100 years, which is much longer than there is likely to be devices to read them.
CDs written using consumer-grade CD-RW drives have a shorter lifespan but are still more
reliable than floppy disks. CDs are written to and read from the bottom-side of the disk but the
recording material itself is usually applied to the top of the CD. Be careful not to scratch either
surface of the CD. Some carrying cases designed for professionally produced CDs will stick to
and peel off the recording surface of consumer grade CDs. OUCH!

96
5.2 Arrays
Arrays are structured data types in which multiple values can be referenced with a single identifier
name. The particular values in an array are accessed using an index placed in parentheses after
the array name. For example, we can refer to A as a list of 100 integers by declaring A to be an
array (1..100) of integer. Then we can refer to the ith value in A as A(i).
One Dimensional Arrays
The concept of a list is implemented in Ada (and other high-level languages) as a one-
dimensional array. Array types must be listed explicitly in the program declaration. The array
declaration specifies the number and type of the entries as well as the range of the index values.
type my_list_type is array(1..1000) of float;
S,R : my_list_type;

In this example, the arrays S and R can each index S R


hold up to 1000 floating point values. The valid
range for the index is between 1 and 1000 1 S(1) R(1)
inclusive. These two arrays are of the same
2 S(2) R(2)
type so they can be assigned to each other
directly. Assuming S has had its 1000 values 3 S(3) R(3)
defined we can assign all values of S to the : : :
corresponding values of R by,
?
R:=S; =
20 S(20) R(20)
Individual values of S and R can be assigned 21 S(21) :
or compared. For example, :
:
R(47):=S(21); 47 R(47)
:
if R(20)=S(20) then : :
put("They're the same!"); 999 S(999) R(999)
else
put("Sorry"); 1000 S(1000) R(1000)
end if;
Figure 5-1: Example One-Dimensional Array

Also, equal sized sub ranges of values can be assigned,


R(1..20):=S(300..319);
Other arrays of 1000 floats could be specified by,

type your_list_type is array (1..1000) of float;


type their_list_type is array (1..1000) of float;
T : your_list_type;
U : their_list_type;

Notice that both T and U are arrays holding 1000 floats, but each is of its own named type.
These array types are different and therefore cannot be equated. Only explicit sub-ranges of
values can be assigned,

T(10):=U(10); -- OK
U(42):=T(300); -- OK
T(20..30):=U(400..410); -- OK
T(10..100):=U(100..200); -- not permitted, wrong size sub-range
U:=T; -- not permitted, different named types

The value of arrays is realized when combined with the for..loop. The loop counter of the
for..loop can be used as the index in an array. Suppose that you wanted to get a number of
values from the user that you want to save for access at a later time. Since we want to write a
97
program that will accept and store an arbitrary number of values, we can't define separate
variable identifiers ahead of time. Instead we can use an array identifier,

with ada.text_io, ada.integer_text_io;


use ada.text_io, ada.integer_text_io;

procedure user_array_demo is

type my_list_type is array(1..1000) of integer;


S : my_list_type;
n : integer;

begin
put("How many values will you enter? (max 1000).. ");
get(n);

if n>1000 then -- makes sure n is not > 1000


n:=1000;
put_line("Your value was too large and was reset to 1000.");
end if;

for i in 1..n loop -- accepts input from user


put("Enter value ");
put(i,0);
put(".. ");
get(S(i));
end loop;

for i in 1..n loop


put(S(i),0);
new_line;
end loop;

end user_array_demo;

Notice that the value of n can be less than the maximum index of the array S. As long as we
keep a record of the value of n, we can work with the first n values in the array. We can declare
the array to hold the largest number of values that we would expect to be entered by a user. In
this case we have assumed that the array will never have to hold more than 1000 values.
The values in the array S are individually accessible as long as the program is running. We could
perform any type of mathematical operation on them we wanted. For example, we could
compute the average using,

sum:=0;
for i in 1..n loop
sum:=sum+S(i);
end loop;
avg:=float(sum)/float(n);

The values in S are not altered by the computation of the average, so they can be used for other
calculations. The program can be expanded to compute the standard deviation (an important
property used in statistics) of the values by,

n
(avg − S (i))2
i =1
std _ dev =
n −1

98
When implemented in Ada we have,

std_num:=0.0;
for i in 1..n loop
std_num:=std_num+(avg-float(S(i))**2;
end loop;
std_dev:=sqrt(std_num/float(n-1));

The use of the array S greatly simplifies this calculation since we need to compute the average
before we compute the standard deviation. The array gives us a way to use the values entered
by the user more than once. How would you compute the standard deviation if the entered
values could not be stored for later access?
Higher Dimensional Arrays
Many applications require that we refer to a set of values by two or more indices. For example,
each pixel in a digital image is located by its row number and column number in the image. In
mathematics we use two dimensional matrices to store and manipulate the coefficients of a
system of equations, as well as many other applications in linear algebra and matrix theory.
Ada supports the use of multi-dimensional arrays using the following type declaration,

type type_name is array([index_range]{,index_range})of data_type;

Examples of multi-dimensional type declarations are shown below,

type matrix_type is array(1..10,1..20)of float;


type rubic_cube_type is array(1..3,1..3,1..3)of color_type;
type tensor_type is array(1..10,1..5,1..5,1..10)of float;

These declarations do not allocate space in memory for data. Instead they provide names for
user-defined structured data types that can be used in variable declarations such as,

mat : matrix_type;
cube : rubic_cube_type;
tensor : tensor_type;

These are the declarations that allocate memory to the variables, mat, cube, and tensor. You
may wonder why we make these declarations in two steps. It is true that we could allocate
memory to these data types directly by,

mat : array(1..10,1..20)of float;


cube : array(1..3,1..3,1..3)of color_type;
tensor : array(1..10,1..5,1..5,1..10)of float;

But if we used this approach we would not be able to compare the contents of two or more
structured variables of the same size and dimension. Recall that, for two variables to be
recognized as the same type they must be declared as the same named type. For example, the
arrays A and B in the example below are not recognized as being the same structured type even
though they are declared to be the same size integer matrices.

A : array(1..10,1..10)of integer;
B : array(1..10,1..10)of integer;

We can not perform the assignment A:=B Also, we will not be able to pass A or B through the
parameter list of a subprogram (more on this later). For A and B to be considered the same
structured data types we must declare them using a named data type such as matype below.
type matype is array(1..10,1..10)of integer;
A,B : matype;
Now A and B are recognized by the Ada programming language as the same named type.
99
Elements of structured data types are accessed by specifying the index values in parentheses
following the identifier name. When the structured data type is two-dimensional or higher we use
multiple indices separated with commas. Let's say we wanted to define the elements of the two-
dimensional matrix A, declared above to be the product of its indices (just an example).

for i in 1..10 loop


for j in 1..10 loop
A(i,j):=i*j;
end loop;
end loop;

Now each element A(i,j) of the matrix A contains a value equal to i * j. To retrieve a value from A,
we need to reference the specific element by its indices. For example, the put( ) statement,

put(A(3,5))

displays the value 15.

Let's look at another example. An n x n matrix A is said to be symmetric if

Ai , j = A j ,i for all i = 1..n, j = 1..n

We can build an Ada code segment that tests the matrix A to see if it is symmetric.

is_sym:=TRUE;
for i in 1..10 loop
for j in 1..10 loop
if A(i,j)/=A(j,i) then
is_sym:=FALSE;
end if;
end loop;
end loop;

put("The matrix A is ");


if is_sym then
put("symmetric");
else
put("not symmetric.");
end if;

In this example, values in the matrix A are compared in the conditional expression of the if..then
statement. In this case, A(i,j) must equal A(j,i) for every i and j so we preset the value of the
boolean is_sym to TRUE and change the value to FALSE if any A(i,j) is found to be different from
A(j,i).

You may be wondering if there is a limit to the number of dimensions allowed in an Ada array
declaration. The answer is yes but it is not an issue of practical importance. A bit of explanation
is called for here. In theory we can specify arrays of more than 100 dimensions, but we are
25
limited in the total amount of data held in a single structured data type to around 2 = elements.
If each dimension has a range of only two,

type fat_array is array(1..2,1..2,1..2,. . .


,1..2) of data_type;

we will run out of allowed memory space when we exceed 25 dimensions. This limit may depend
on the specific compiler, OS or computer being used. There are no known practical applications
requiring such high dimensional arrays, so whether we are limited to 25 or 100 dimensional
arrays is not of any practical importance.

100
5.3 Records

A record is another kind of structured data type. A record is a collection of data elements, which
may or may not be the same type, all associated by a single record type name. A common
example of a record is found in an employee data base. Each employee's name, age, hourly
wage, years employed, and identification number make up the employee's record. We can
declare a record data type to hold all this information in the following manner:

subtype wordtype is string(1..20);

type emptype is record


last_name : wordtype;
first_name : wordtype;
age : integer;
wage : float;
yemp : integer;
ID : wordtype;
end record;

an_emp : emptype;

The subtype wordtype is used to define ASCII strings of 20 characters. As with arrays, the record
declaration simply names the type emptype but does not reserve any space in memory for the
record. The declaration an_emp : emptype reserves memory space for the fields, last_name,
first_name, age, wage, yemp and ID, each with the appropriate data type. This record can be
visualized as shown in the figure below:

an_emp

last_name
first_name
age
wage
yemp
ID

Figure 5-2: Illustration of the Data Structure for the Record emp_type

We refer to the entire record as an_emp. In order to assign or access individual fields of the
record, we use dot notation. For example, if we wished to assign a value of $25.50 for the hourly
wage we could use the statement,
an_emp.wage := 25.50;
and to display the first_name and last_name of the employee whose data are held in the record
an_emp we could use the following code segment:

put(an_emp.first_name);
put(' ');
put(an_emp.last_name);
new_line;
As you may have noticed by now, record data types, by themselves, are not very useful. In the
next section we will learn how to combine record types with array types to increase their value to
the programmer.

5.4 Arrays of Records

As discussed in Section 5.2 we can create arrays of any type. This means that we can define a
record type and then declare arrays of those records. Continuing with our employee data
example above we can define an array of emptype as,

101
type emplisttype is array(1..1000)of emptype;
emplist : emplisttype;

In this example, the variable emplist represents a list of up to 1000 employees. Each element of
emplist is a record that can contain the first and last name, age, wage, years employed, and ID
number for an employee. We access a particular record with the index value in parentheses
following the variable name emplist. So,
emplist(42)
refers to the 42nd record in emplist. We access a particular field of a particular record using dot
notation combined with an index value. For example,
emplist(42).last_name:="Dorfmeister ";
assigns the string "Dorfmeister" as the last_name of the 42nd employee. Such an array of
records can be thought of as a large table (also called a table in relational database theory).
emplist

index last_name first_name age wage yemp ID


1
2
:
:
1000

Figure 5-3: Illustration of the Array of Records emplist


Let's talk about databases a little before we move on. There is an important property that must
exist in an array of records before it can be considered a relational database. Each record must
be distinguishable from all the others. This means that no two records can have all fields
containing the same values. Any field or set of fields that can be used to distinguish every record
in a database is called a key. In our example, two or more employees can have the same
last_name, first_name, age, wage, or years employed. Therefore, these fields would not help us
establish a key of our database. The one field which can be the key field is the employee ID.
Certainly we would want to be able to distinguish employees in some way, so we give each one a
unique ID number.

5.5 Applications

5.1: Write an Ada program that reads a text file containing employee data and loads it into a
corresponding array of records.

First, we have to carefully design the text file containing the employee data. Since we will be
reading different data types from the same text file, we need a detailed understanding of how the
various get( ) procedures provided in the I/O packages operate. Since we will be reading three
20 character strings, two integers, and a floating point value from the text file, we need to know
how the various get procedures handle blanks and end_of_line markers.

We can test the string get( ) first by implementing a simple program that processes a text file.
Let's assume all strings will be 20 characters and then make sure that we provide the correct
number of characters for each field.

Create and save a text file containing the letters A through Z as shown below. Make sure there
are no blanks following the last Z in each line.

ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZ
ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZ
ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZ
ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZ
ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZ
102
Name this text file readtest.dat. Now write an Ada program to read and display ten 20-character
strings from this text file.

with ada.text_io;
use ada.text_io;
procedure readtest is
subtype wordtype is string(1..20);
a : wordtype;
datin : file_type;
begin
open(datin,in_file,"readtest.dat");
for i in 1..10 loop
get(datin,a);
put_line(a);
end loop;
close(datin);
end readtest;

The variable a is a 20-character string so each time we call the procedure get(datin,a) the next 20
characters are read from the file and displayed on the screen with the put_line(a) procedure. The
read point for the next get( ) is the next character in the current line. When there are less than 20
characters left in a line the get( ) procedure for strings simply jumps to the next line and continues
reading characters until a total of 20 characters are read.

The output of this program looks like,

ABCDEFGHIJKLMNOPQRST
UVWXYZABCDEFGHIJKLMN
OPQRSTUVWXYZABCDEFGH
IJKLMNOPQRSTUVWXYZAB
CDEFGHIJKLMNOPQRSTUV
WXYZABCDEFGHIJKLMNOP
QRSTUVWXYZABCDEFGHIJ
KLMNOPQRSTUVWXYZABCD
EFGHIJKLMNOPQRSTUVWX
YZABCDEFGHIJKLMNOPQR

The unread characters in the text file are ignored when the text file is closed. We can read 20
characters from the file readtest.dat a maximum of 13 times (20x13=260) since there are
26x10=260 characters in the ten copies of the alphabet in this text file. Attempting to read farther
raises an END_ERROR exception.

raised ADA.IO_EXCEPTIONS.END_ERROR

We now know how the get( ) procedure for string data types operates. When getting a string
whose maximum length is n characters, the get( ) procedure takes the next n characters.
Therefore, we will want to make sure that the first_name, last_name and ID fields occupy exactly
20 characters in our text file.

WARNING! Some text editors delete trailing blanks in a text file when it is saved.
Others omit or create blank lines at the end of text files. You cannot see these
changes but they can cause confusion when attempting to use a computer to read
string data from text files.

Now let's test the integer and float get( ) procedures combined with the get( ) procedure for
strings. Create and save a text file to include the employee data as shown on the next page.

103
6
BANKS ROBIN 30 20.00 8 ABC0001003 .
WELLS DOUG 25 12.50 4 ABC0001000 .
BOGGS WADE 45 28.25 18 ABC0000033 .
CASE JUSTIN 19 8.50 2 ABC0001100 .
SHAW RICK 22 10.00 4 ABC0001099 .
BREAK ANITA 22 9.00 3 ABC0001086 .

The integer on the first line should give the number of records (i.e. number of lines) to follow. The
string fields for last_name and first_name need to have exactly 16 characters since this is the
size of the wordtype string we will be using to hold them. There should be at least one blank
space between each numeric field. There can be more than one blank space between numeric
fields since the get( ) procedures for integers and floats skip extra blanks while searching for
numeric characters. There should be exactly one blank character between the yemp (integer)
field and the ID (string field) since the get( ) procedures for numeric types will skip one blank
space after reading their values.

Note that even though ID contains 10 characters, we have provided at least 16 characters for this
field. Also, we have placed a period (beyond the 16th character position) at the end of each line
to prevent the editor from removing the trailing blanks following the ID. We can force the program
to ignore the extra spaces and the period by placing a skip_line( ) procedure call following the
get( ) statements which read a line of data from the text file.

with ada.text_io, ada.integer_text_io, ada.float_text_io;


use ada.text_io, ada.integer_text_io, ada.float_text_io;

procedure readtest is

subtype wordtype is string(1..16);

type emptype is record


last_name : wordtype;
first_name : wordtype;
age : integer;
wage : float;
yemp : integer;
ID : wordtype;
end record;

type emplistype is array(1..1000)of emptype;


emplist : emplistype;
num_emp : integer;
datin : file_type;

begin
open(datin,in_file,"readtest.dat");
get(datin,num_emp);
for i in 1..num_emp loop
get(datin,emplist(i).last_name);
get(datin,emplist(i).first_name);
get(datin,emplist(i).age);
get(datin,emplist(i).wage);
get(datin,emplist(i).yemp);
get(datin,emplist(i).ID);
skip_line(datin);
put_line(emplist(i).last_name);
end loop;
close(datin);
end readtest;
104
We have included a put_line( ) to display the last name for each employee as we read each line
from the text file. This will show us if our program is staying properly aligned as we are reading
the text file.

5.2: Extend the program written in 5.1 above to display a list of all employees who have been
employed at least 4 years and are at most 25 years old. Display the ID, first name, and last name
of each employee whose data meet both these conditions.

Searching a database and extracting a subset of its records is called a projection into the
database. The statement of the condition(s) that a record must meet to be accepted is called the
query. Projections and queries are important operations provided in a database management
system (DBMS). Our query can be written as a Boolean conditional expression,

emplist(i).age <= 25 and emplist(i).yemp >= 4

We wish to display ID, first_name, and then last_name for each employee for which this
conditional is TRUE.

Since the data has been loaded into the array emplist which is in active memory we can search
the fields of this array after the text file has been closed. Therefore, we can place the following
code segment just after the close(datin) procedure in our program.

for i in 1..num_emp loop


if emplist(i).age<=25 and emplist(i).yemp>=4 then
put(emplist(i).ID(1..12));
put(emplist(i).first_name);
put(emplist(i).last_name);
new_line;
end if;
end loop;

Running the program with these modifications gives.

ABC0001000 DOUG WELLS


ABC0001099 RICK SHAW

These are the two employees who are not older than 25 and who have been working for at least
4 years. Later we will see how to deal with trailing blanks in strings so that they do not have to be
displayed. For now we can patch this code to skip the trailing blanks when we display the
first_name field.

If we decide not to display extra blanks between the employees' first and last names, we can use
a while...loop to skip the trailing blanks in the first name string field. In the example code
segment below, we display the non-blank characters in the string emplist(i).first_name field, one
character at a time. When we encounter a blank we exit the while...loop, display a single blank
and then display the last name which is in the string field emplist(i).last_name.

for i in 1..num_emp loop


if emplist(i).age<=25 and emplist(i).yemp>=4 then
put(emplist(i).ID(1..12));
letnum:=1;
while emplist(i).first_name(letnum)/=' ' loop
put(emplist(i).first_name(letnum)); -- displays one character
letnum:=letnum+1;
end loop;
put(' ');
put(emplist(i).last_name);
new_line;
end if;
end loop;
105
This modified code segment produces the more readable result,

ABC0001000 DOUG WELLS


ABC0001099 RICK SHAW

Of course, the variable letnum must be declared as an integer in the program declaration block.
In this example, we display the trailing blanks of each employee's last name since this is the end
of the displayed lines anyway.

5.3: Create a text file containing 1000 integers randomly distributed between 0 and 99 inclusive.

Most high-level languages include a method of generating random numbers. In Ada there are
generic packages (called generics) with random number generating functions for floats and
discrete types such as integers and enumerated types. In order to use this package, we need to
tell the compiler the type of the random numbers and the range of values that are going to be
generated. We will discuss the details of generics later. For now, it is enough for us to learn how
to make use of them.

Since we are to generate a range of integers, we will use the package


ada.numerics.discrete_random. If we were generating floats we would use the package
ada.numerics.float_random. As mentioned, this is a special kind of package called a generic in
which some information has been left unspecified (or generic). In this case, the type of the
random numbers being generated must be indicated in the declaration block of our program
before we can use the random number generator. This is called instantiation. We are asked to
generate random integers in the range 0..99 so we need to make up a name for this subtype of
integers that helps us remember its purpose such as randtype.

subtype randtype is integer range 0..99;

package rand is new ada.numerics.discrete_random (randtype);


use rand;

We also have to make up a name for the package instantiation. We will use the name rand.
Once we have instantiated the rand package we can use the function random( ) to generate
integer values in the range of randtype. The problem asks us to create a text file containing 1000
integers, so we will need to open a file for output. This is accomplished using the create
statement,

create(datout,out_file,"rand.dat");

This statement associates the logical filename datout with the physical file rand.dat and prepares
this file for receiving information. The random( ) function can be embedded directly into a put
statement as,

put(datout,random(g),5);

The parameter g is of type generator. The type generator is defined in the


ada.numerics.discrete_random package and is used as a seed for the random number
generation. A mathematical calculation is performed to generate the next random number. This
calculation uses the value of g in the computation of the random number. With each random
number generated, the value of g is changed as well. This way, the value returned by random( )
changes each time the function is called. We will discuss random number generation in greater
detail in Chapter 9.

All this may seem a bit strange, but for now, we can use the method shown in the proceding
sample program to generate random numbers in our own programs.

with ada.text_io, ada.integer_text_io, ada.numerics.discrete_random;


use ada.text_io, ada.integer_text_io;
106
procedure random_number_generator is

subtype randtype is integer range 0..99;


package rand is new ada.numerics.discrete_random (randtype);
use rand;

datout : file_type;
g : generator;

begin
create(datout,out_file,"rand.dat");
for i in 1..1000 loop
put(datout,random(g),5);
new_line(datout);
end loop;
close(datout);
end random_number_generator;

The generation of random numbers is an important part of computer science applications such as
computer gaming, simulation, and scientific analysis. We will study the theory and limitations of
random number generators in Chapter 9.

5.4: Write an Ada program to sort the random values generated in Application 5.3 into ascending
order.

Sorting is another important operation performed by computers. A common sorting technique


called the exchange sort. For each position in a list of values, the program searches for the next
smallest value and places it in it proper position by exchanging the value currently in the position
with the next correct value for that position. Let's try to execute the exchange sort algorithm
manually before we attempt to build a sort program. Initially none of the values are sorted so we
would search the entire list to find the smallest value. When the smallest value is located we
exchange the value currently in the first position with this smallest value.
sorted

7 0 swap 0 0 0 0

3 3 3 1 1 1
2 2
4 4 4 4
9 9 9 9 9 3
swap
4
2 2 2 2 4
unsorted

. . . 5
0 7 7 7 7
6
1 1 1 3 3
5 5 5 5 5 7

8 8 8 8 8 8
9
6 6 6 6 6
unsorted sorted
list list

Figure 5-4: Example of the Exchange Sort Operating on an Array of Integers


Once this value is placed we never look at this position in the list again. We know that the
smallest value in the entire list is in this position, so we search the remainder of the list to find the
next smallest value. When it is located, we place it in the second position and put the value that
was in the second position in the location where we found this next smallest value. This process
is repeated until every value is in its proper position in the list. We will assume that the following
declarations have been performed for the list S and the associated identifiers:

maxnum : constant integer := 1000; -- maximum array size


type listype is array(1..maxnum) of integer;
S : listype;
n : integer;
tmp : integer;
107
The code segment shown below uses a for...loop to search a list S of n values and to place the
smallest value in the first position in the list S(1).

for i in 1..n loop


if S(i)<S(1) then
tmp:=S(i);
S(i):=S(1);
S(1):=tmp;
end if;
end loop;

We use the variable tmp to temporarily hold the value in S(i) while we copy the value of S(1) into
its location in the list S. If we wanted to place the second smallest value into the second position
S(2) of the list we could use another loop such as,

for i in 2..n loop


if S(i)<S(2) then
tmp:=S(i);
S(i):=S(2);
S(2):=tmp;
end if;
end loop;

If we were to sort all n values in this manner we would need n loops. However we can use a pair
of nested loops to place each value in S into its proper position.

for i in 1..n-1 loop -- outer loop chooses next position

for j in i+1..n loop -- S(i) will hold the ith smallest value
if S(i)>S(j) then
tmp:=S(i);
S(i):=S(j);
S(j):=tmp;
end if;
end loop;

end loop;

The range of the outer loop is i = 1..n-1 since we do not need to compare the last item S(n) to
itself. The inner loop has a range from j = i+1..n, since we do not want to disturb any values
already placed in the first i positions of the list S.

In the code sample above, we see that the ith value is replaced each time we find a smaller value
in the remainder of the list S. Actually, we don't need to place a value at S(i) until we find the
smallest value in S(i+1)..S(n).

We can save a few operations by searching the remainder of the list before swapping a value into
S(i). In order to reduce the number of swaps we will need to define the variable identifier i_min to
hold the index (position in S) of the candidate smallest value. We can use the variable identifier
tmp to hold the value itself.

for i in 1..n-1 loop

tmp:=S(i); -- gives tmp an initial value


i_min:=i; -- index in S of the current value in tmp

for j in i+1..n loop -- searches list from S(i+1) to S(n)

108
if S(j)<tmp then
tmp:=S(j); -- replaces tmp with a smaller value and
i_min:=j; -- updates the index
end if;

end loop;

S(i_min):=S(i); -- moves the value in S(i) to S(i_min)


S(i):=tmp; -- places ith smallest value in S at S(i)

end loop;

There are many different methods for ordering values in a list. One of the most extensively
researched topics of computer science is the design and analysis of algorithms that search and
sort lists of data.

Chapter 13 provides a detailed study of searching and sorting methods. For now, we will look at
one other sorting method called the insertion sort. This example will give some idea of the wide
variety of approaches possible.

Starting with an unordered list S, we choose an element (we will start with the second element,
since a list of a single element is already sorted) and place it into a temporary location, tmp. The
inner loop searches for a location to insert this value so that locations 1 through i are kept in
sorted order.

for i in 2..n loop

tmp := s(i);

for j in reverse 1..i-1 loop


here := j;
exit when tmp >= s(j);
s(j+1) := s(j);
end loop;

s(here) := tmp;

end loop;

Note that the inner loop uses the Ada reserved word reverse. This has the effect of starting the
loop index j at i-1 and decrementing it by 1 until it reaches the value 1. The operation of the inner
loop is to move the numbers in S down one-by-one until tmp>=S(j) which leaves the index named
here at the correct location for inserting tmp back into S. A complete listing for an Ada program to
demonstrate this sort algorithm is given below:

with ada.text_io, ada.integer_text_io, ada.float_text_io,


ada.numerics.float_random;
use ada.text_io, ada.integer_text_io, ada.float_text_io,
ada.numerics.float_random;

procedure insertion_sort_demo is

n : constant integer := 10;


type listype is array(1..n) of float;
s : listype;
tmp : float;
seed : generator; --seed for random number package
here : integer;

109
begin
for i in 1..n loop
s(i):=random( seed );
put(i, 3);
put(s(i),5,4,0);
new_line;
end loop;
new_line;

for i in 2..n loop


tmp := s(i);
for j in reverse 1..i-1 loop
here := j;
exit when tmp >= s(j);
s(j+1) := s(j);
end loop;
s(here) := tmp;
end loop;

for i in 1..n loop


put(i, 3);
put(s(i),5,4,0);
new_line;
end loop;
end insertion_sort_demo;

In the example above, the inner loop is configured as a for...loop. Since the index of the loop, j is
defined only inside the loop; we have to use a separate identifier here to hold the value of j when
we exit this loop. Instead, we could use a while...loop as shown below.

for i in 2..n loop


tmp := s(i);
j:=i-1;
while j>=1 and then tmp<s(j) loop -- and then avoids
s(j+1) := s(j); -- zero index in s(j)
j:=j-1;
end loop;
s(j+1):=tmp; -- use j+1 since j was decremented
end loop;

5.5 Modify the program net_pay_demo.adb of Application 3.2 to read a text file containing
employee names, hourly wages and hours worked. Extend the program to print "payroll checks"
for each employee.

The processing operations for this program are similar to those of Application 3.2 in which we
computed the net pay for individual employees. In this case, however, we will read the employee
name, hourly wage, and hours worked from a text file rather than asking the user to enter the
data by hand. For each employee listed in the text file, the program will compute the net pay and
print (display on the console) the payroll check.

The modified pseudocode for this problem is shown below:

-- Declare Identifers for Input, Output and Internal Computations


-- Declare Tax Rate (0.18)
-- Declare Number Hours Regular Time (40)
-- Declare Overtime Rate (1.5)

-- Open text file for input

110
-- For each employee
-- Input: Get Name, Hourly Wage and Hours Worked
-- Process: Compute Gross pay, Tax Paid, and Net Pay
-- Output: Print Employee Check
-- Next employee

-- Close text file

Notice that the input-process-output steps have been placed inside a loop that repeats these
operations for each employee. We will use the same tax rate (18%), the same number of hours
to be considered regular pay (40), and the same overtime rate (1.5) for hours worked over the
regular pay hours, if any. The information specific to each employee, needed for the
computations, will be stored in a text file called payroll.dat.

10
Robin Banks 12.45 40.0
Doug Wells 25.00 50.0
Wade Boggs 10.50 60.0
Justin Case 15.00 47.0
Amanda Reckonwith 30.00 24.0
Willie Maykett 20.10 45.0
Ilene Dover 15.50 55.0
Ben Dover 8.50 42.0
Philly Buster 29.00 30.0
Sarah Bellum 40.00 50.0

Figure 5-5: Employee Name, hourly wage, and hours worked in text file payroll.dat

The first line of this text file is an integer indicating the number of employees in the file. Each of
the remaining lines contains an employee name (in the first 20 spaces), the hourly wage (as a
float), and the hours worked (also as a float). Recall that the string version of the get( ) procedure
reads the number of characters matching the length of the string identifier used, so

get(datin,name);

will read 20 characters (including blank spaces) since name is a 20-character string. The get( )
procedures for float types skips any leading blanks, reads the first number it encounters (with or
without a decimal point), and reads one blank following the number. If you create the payroll.dat
text file make sure that you do not leave any trailing blanks to the right of the rightmost value in a
line. (Can you discover why this is important?)

The program batch_net_pay_demo.adb opens the text file, reads the first value indicating the
number of employees, and then executes a for...loop that reads each employee's payroll data,
computes the gross pay, tax, and net pay, and then prints a payroll check for the employee. The
complete program is shown below:

with ada.text_io, ada.integer_text_io, ada.float_text_io;


use ada.text_io, ada.integer_text_io, ada.float_text_io;

procedure batch_net_pay_demo is

-- Declare Identifers for Input, Output and Internal Computations


tax_rate : constant float := 0.18;
subtype wordtype is string(1..20);

num_emp : integer;
name : wordtype;
regular_pay : float;
overtime_pay : float;
111
gross_pay,net_pay : float;
hours_worked : float;
hourly_wage : float;
tax : float;
datin : file_type;

begin

open(datin, in_file,"payroll.dat"); -- open payroll data file


get(datin,num_emp);

for i in 1..num_emp loop

-- Input: Get Name, Hourly Wage and Hours Worked


get(datin,name);
get(datin,hourly_wage);
get(datin,hours_worked);

-- Process: Compute Gross pay, Tax Paid, and Net Pay


if hours_worked>40.0 then
regular_pay:=40.0 * hourly_wage;
overtime_pay:=1.5 * hourly_wage * (hours_worked - 40.0);
else
regular_pay:=hours_worked * hourly_wage;
overtime_pay:=0.0;
end if;
gross_pay:=regular_pay + overtime_pay;
tax:=tax_rate*gross_pay;
net_pay:=gross_pay - tax;

-- Output: Print Employee Check


new_line;
put_line("* * * * * * * * * * * * * * * * * * * * * * * * * *");
put_line("* *");
put_line("* The A B C Company *");
put_line("* 123 Main Street *");
put_line("* Any Town, USA, 00000 *");
put_line("* *");
put_line("* Payee Amount *");
put_line("* *");
put('*');
set_col(5);
put(name);
set_col(38);
put(net_pay,4,2,0);
set_col(51);
put_line("*");
put_line("* *");
put_line("* Signed _________________ *");
put_line("* I. M. DeBoss *");
put_line("* *");
put_line("* NOT VALID IF OVER $5000.00 *");
put_line("* * * * * * * * * * * * * * * * * * * * * * * * * *");
new_line;
put_line("----------------------- FOLD ----------------------");
new_line(5);
put(" Hours worked.........................");
put(hours_worked,6,2,0);
new_line;
112
put(" Gross Pay............................");
put(gross_pay,6,2,0);
new_line;
put(" Tax Withheld.........................");
put(tax,6,2,0);
new_line;
put(" Net Pay..............................");
put(net_pay,6,2,0);
new_line(6);
put("----------------------- CUT -----------------------");
new_line;
end loop;
close(datin); -- close payroll data text file
end batch_net_pay_demo;

Note that the text file payroll.dat is opened for input and the value for num_emp is read before
entering the for...loop. In this way, we can set the number of repetitions of the loop ahead of
time. For each employee, the program reads a line of the text file (name, hourly wage, hours
worked) and computes the values of gross pay, tax, and net pay. These data are then used to
"print" (in this case we just display the output in the console window) the next payroll check.

The output section of this program could save the payroll data to another text file rather than
printing checks. In any case the program runs without human interaction. A sample of the output
is shown in Figure 5-6 on the next page.

113
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* The A B C Company *
* 123 Main Street *
* Any Town, USA, 00000 *
* *
* Payee Amount *
* *
* Amanda Reckonwith 590.40 *
* *
* Signed _________________ *
* I. M. DeBoss *
* *
* NOT VALID IF OVER $5000.00 *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
----------------------------- FOLD ----------------------------

Hours worked..................................... 24.00


Gross Pay........................................ 720.00
Tax Withheld..................................... 129.60
Net Pay.......................................... 590.40

----------------------------- CUT -----------------------------


* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* The A B C Company *
* 123 Main Street *
* Any Town, USA, 00000 *
* *
* Payee Amount *
* *
* Willie Maykett 782.90 *
* *
* Signed _________________ *
* I. M. DeBoss *
* *
* NOT VALID IF OVER $5000.00 *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
----------------------------- FOLD ----------------------------

Hours worked..................................... 45.00


Gross Pay........................................ 954.75
Tax Withheld..................................... 171.86
Net Pay.......................................... 782.90

----------------------------- CUT -----------------------------

Figure 5-6: Sample Output from batch_net_pay_demo.adb

114
Exercises `

5.1 Write an Ada program that creates a text file containing the multiplication tables through
12x12. The generated text file should include row and column headers and a title. See
Application 4.2.

5.2 Write an Ada program to read a text file containing two 3x3 matrices and to compute the
product of these two matrices. Matrix multiplication is defined by,

n
C (i, j ) = A(i, k ) × B (k , j )
k =1

where C(i,j) is a term the 3x3 product matrix C and A(i,k) and B(k,j) are terms in the matrices
being multiplied together. By convention, the first index indicates the row of the matrix element
and the second index indicates its column position. For example, the product of A and B below,

1 0 2 2 1 2
A= 0 3 2 , B= 3 1 5
4 3 1 1 0 0
gives,
1× 2 + 3 × 0 + 2 ×1 1×1 + 0 ×1 + 2 × 0 1× 2 + 0 × 5 + 2 × 0 4 1 2
C = A × B = 0 × 2 + 3 × 3 + 2 × 1 0 × 1 + 3 × 1 + 2 × 0 0 × 2 + 3 × 5 + 2 × 0 = 18 3 15
4 × 2 + 3 × 3 + 1×1 4 ×1 + 3 ×1 + 1× 0 4 × 2 + 3 × 5 + 1× 0 18 7 23

5.3 In Application 5.3 a random number generator is used to generate a text file containing 1000
random values between 0 and 99 inclusive.

a. Implement this program, and generate the sequence of random numbers with seed initially set
to 0.

b. A problem with computer generated sequences of random numbers is that they may not be
completely independent from one value to the next. This means that each time a number is
generated, the next number is more likely to occur within a sub range of the set of all possible
values. When two values are related in this way they are said to be correlated. Design and
implement a program that reads the text file and computes the probability of obtaining a particular
value following each of the values 0 through 9. You output should be a two dimensional array of
size 100x100 giving the number of occurrences of each value 0 - 9 that appears following each
value 0 - 9.
0 1 2 3 4 5 . . . 9
0 0 2 0 0 1 1 0
1 1 0 1 0 0 0 1
2 0 0 0 2 0 1 0
: :
:
9 0 1 4 0 0 0 0

This sample table shows that 9 is followed by a 2 four times.

c. Based on the results of your program, do you feel that this is a good random number generator
program? Explain your answer.

5.4 Use the data in the text file generated in Application 5.3 in this exercise.

a. Write an Ada program that counts the number of comparisons (if..then tests) that must be
made to sort the n values in the text file using the exchange sort.
115
b. Modify your program to count the number of comparisons that must be made to sort the n
values using the insertion sort.

c. Which of the sorting algorithms is more efficient? Does the original order of the values make a
difference in the number of comparisons required for either sorting algorithm?

5.5 Write an Ada program to compute the mean and standard deviation of the data in the text file
created in Application 5.3.

5.6 A histogram is a bar graph or plot used to show the number of occurrences of data values or
entities with values falling within a fixed number of partitions. The histogram below shows the
number of occurrences of each of the letters of the alphabet in this sentence.

A B C D E F G H I J K L M N O P Q R S T U V W X Y Z

a. Write an Ada program to count the number of occurrences of each of the values 0 through 99
in the text file created in Application 5.3.

b. Modify your program to compute the number of values in each of 10 partitions

0..9, 10..19, . . . 90..99.

c. Modify your program to generate a histogram using asterisks, as shown below.

0 - 9 |***********
10 - 19 |******************
20 - 29 |********
30 - 39 |***************
40 - 49 |**********************
50 - 59 |************
60 - 69 |************************
70 - 79 |***************
80 - 89 |****************
90 - 99 |*******************

Since the number of stars in some columns exceeds 80, you should divert the output of your
program to a text file for viewing.

d. How might you modify the program of part c. so that the histogram can be displayed on the
computer screen?

116
Chapter 6 - Subprograms

6.1 Functions
The Format of a Function
Defining Subprograms in a Main Procedure

6.2 Procedures

6.3 Parameter Passing

6.4 Identifier Scope and Extent

6.5 Applications
Recognizing Palindromes
The Magic Square and Tic-Tac-Toe
Playing Tic-Tac-Toe

117
Chapter 6 - Subprograms
In this chapter, we study special program units called subprograms. There are two types of
subprograms common in structured languages. In Ada (and some other languages) they are
called functions and procedures. Subprograms permit the programmer to encapsulate
computational constructs as separate compilation units that can be implemented and called by
other programs.

6.1 Functions

The first type of subprogram we will study is called a function. Based on the concept of a
mathematical function, these subprogram units can have input parameters passed to them and
can then compute and return a value of any type. The result of a function computation is returned
to the calling program as a value that replaces the function reference in an expression.
The ada.numerics.elementary_functions package contains a number of predefined functions
including,
sin(X) returns trigonometric sine of X given in radians
cos(X) returns trigonometric cosine of X given in radians
arctan(X) returns the angle in radians whose tangent is X
log(X) returns the base-10 logarithm of X
sqrt(X) returns the square root of X
exp(X) returns the value of eX

These functions accept a parameter X (of type float) and return values of type float to the calling
program. The term returns needs some explanation. Functions can be used as elements of
expressions such as,

dist := sqrt((X1-X2)**2 + (Y1-Y2)**2);


X:=r*cos(theta);
and
Y:=r*sin(theta);
As shown in these examples, the value returned by the function replaces the function reference in
the expression. A function can be thought of as a special type of variable that carries its
computation around with it. We can make up our own functions as well.

The Format of a Function

A function is declared using the following format,

function function_name [(argument_declarations)] return data_type is

[identifier_declarations]

begin

[sequence of statements]

return (expression|variable_identifier);
end function_name;

The first line of a function declaration gives the function name, a list of input arguments (if any)
and the type of the value being returned by the function. If the function uses any internal variable
identifiers they are defined in a declaration block between the function name line and the
reserved word begin in the same manner used when declaring identifiers in a main program. The

118
executable portion of the function lies between its begin and end statements. The last line of the
execution block is a special statement called the return statement.

The return statement is made up of the reserved word return followed by an expression or an
identifier indicating the value being returned by the function. The type of the value computed by
this expression or the identifier must match the return type specified in the first line of the function
declaration. Let's create a function that returns the minimum of three integers passed to the
function as arguments.

function min_of_three_ints(a,b,c : integer) return integer is


minval : integer;
begin
minval:=a;
if b<minval then
minval:=b;
end if;
if c<minval then
minval:=c;
end if;
return minval;
end min_of_three_ints;

The three integers a, b, and c are passed into the function through the argument list. The internal
variable identifier minval is assigned the smallest of these three values and is returned to the
calling program. This function can be called from a main program by using its name appropriately
in an expression as shown below,

put("Enter three integers... ");


get(x); get(y); get(z);
put("The smallest value you entered was ");
put(min_of_three_ints(x,y,z),0);
new_line;

Notice that the function min_of_three_ints( ) is used in the same place that an identifier of type
integer could be used. Alternatively, we could make the function part of a mathematical
expression such as,

smallest_offer:= items_per_case * min_of_three_ints(p,q,r);

where smallest_offer is the smallest number of items in p, q, or r cases of items. Note that the
names of the identifiers passed to the function do not have to match the names of the arguments
used in the function. The values of the identifiers are passed to the arguments in the same
relative position in the argument list of the function. That is, the value of the first identifier is given
the first argument, the value of the second identifier to the second argument and so on. This is
called positional notation. As we will see in the next section the parameter passing method for
functions is called pass by value.

Defining Subprograms in a Main Procedure

Later, we will learn how to declare sets of functions in packages which we can include in our
programs using the with and use statements. While we are learning how to build and use
functions we will define them as part of the declaration block of the main program. The complete
function (and procedure) descriptions can be placed in the declaration block of a program (or
another subprogram) as shown on the next page.

119
with [package_list];
use [package_list];
program main_program_name is
declaration block
[identifier_declarations]

[function_and_procedure_declarations]

begin
execution block
[sequence_of_statements]

end main_program_name;

In the following example, the function dist( ) computes the distance between two points in a
Cartesian coordinate system. The function dist( ) is defined as part of the main program
declaration block.

with ada.text_io, ada.float_text_io,


ada.numerics.elementary_functions;
use ada.text_io, ada.float_text_io,
ada.numerics.elementary_functions;

procedure calc_dist is
x1,y1,x2,y2 : float;

function dist(x1,y1,x2,y2 : float) return float is


begin
return sqrt((x1-x2)**2 + (y1-y2)**2);
end dist;

begin
put("Enter the x,y position of point one... ");
get(x1); get(y1);
put("Enter the x,y position of point two... ");
get(x2); get(y2);
put("The distance between these two points is ");
put(dist(x1,y1,x2,y2),0,8,0);
new_line;
end calc_dist;

The function dist( ) is called in the main program in the last put( ) statement (see underlined
code). Since the return type of the function is float, the compiler knows to choose the floating-
point version of the put procedure. This program is shown to illustrate the form for defining
functions in a main program declaration block. In this case, the function could have been omitted
and the return expression itself placed in the put( ) statement.

put(sqrt((x1-x2)**2+(y1-y2)**2),0,8,0);

The advantage of using functions will be apparent when the function computation is more
involved and/or the function is called more than once in a program.

120
6.2 Procedures

Another type of subprogram is called a procedure. At first, this may be a bit confusing since main
programs in Ada are also called procedures. Procedures are more general types of subprograms
that can be called upon to perform any kind of action. All the input and output actions we use in
Ada are predefined procedures such as put( ), get( ), create( ), open( ), close( ), new_line,
skip_line, etc.

Just as with functions we can define our own procedures. The program below is a modification of
the program tweaked_screen_message written in Section 2.8.

with ada.text_io;
use ada.text_io;
procedure proc_demo_1 is

name : string := "Robin Banks";


major : string := "Computer Science";
email : string := "robin.banks@murraystate.edu";

procedure center_text(str : string) is


begin
set_col(40 - str'length/2);
put_line(str);
end center_text;

begin

set_line(10);
center_text(name);
center_text(major);
center_text(email);
end proc_demo_1;

In this version, we have moved the text-centering operation into a procedure called center_text( )
which accepts an unconstrained string str. This procedure sets the starting column needed to
center the string str and then puts the string to the display. This procedure is called in the main
program three times. Notice that procedures are called as separate lines in the program rather
than as part of an expression.

6.3 Parameter Passing

Functions have only input parameters. This means that values can be passed into functions
through their argument lists, but values cannot be passed back to the calling program from the
function through the parameter list. Procedures, on the other hand, can have both input and
output parameters. The type of parameter being passed by a procedure is specified as part of
the parameter list declaration using the reserved words in and out. For example,

procedure foob(x,y : in float);


procedure feeb(a,b : in integer; c : out float);
procedure flub(p : in out integer);

In the procedure foob( ), x and y are passed as in parameters. This means that the value of the
parameters in the procedure call are assigned to the identifiers x and y just before execution of
foob( ) begins. When control is returned to the calling program the values of x and y are not
passed back to their corresponding parameters in the calling program.

IMPORTANT: The parameters designated as in parameters are treated like constants in the
procedures containing them. It is illegal to attempt to modify their values. Attempting to do so will
generate a compile-time error.

121
The parameters a and b in feeb( ) are in parameters while c is an out parameter. Out parameters
pass their values back to the corresponding identifier (using positional notation) in the procedure
call.

put("Enter values for s and t... ");


get(s); get(t);
feeb(s,t,u);
put("The value of u is... ");
put(u,0,4,0);

In this example, the values of s and t must be specified before feeb( ) is called, but the value of u
can be left undefined. When program control is returned to this code segment from feeb( ), the
variable identifier u is given the value held by c in feeb( ) at the end of execution of this
procedure.

In the third example p is defined as an in out parameter. This means that the value of the
parameter in the call to flub( ) is passed to p at the beginning of the execution of flub( ) and the
value of p is passed back to the parameter in the calling program at the end of the execution of
flub( ).

In Ada, simple or elementary data types are passed by value. Some structured types are passed
by reference. In a call by reference the corresponding identifier name in the subprogram is
associated with the same memory space of the identifier in the calling program. This means that
any changes in the value of the subprogram parameter are actually changing the value stored in
the corresponding parameter of the main program.

main program subprogram main program subprogram

x p x p

begin begin
: :
end end

call by value call by reference

Figure 6-1: Parameter Passing in Call-by-Value and Call-by-Reference

The advantage of call by reference is that there is only one copy of the data being held in
memory. For simple data types, this does not mean much, but consider the situation in which you
are passing a very large array to a subprogram. We can conserve memory by permitting the
subprogram to manipulate the original array rather than a copy of it. The Ada Reference Manual
(ARM) specifies the type of parameter passing for some data types but leaves others unspecified.
For example, structured types such as arrays and records are not specified as call by reference
or call by value. Therefore, you should not rely on the way a particular compiler/OS passes
structured parameters when you port your source code to another computer. This will become
more important when we work with access types (dynamic memory) and aliasing in Part Two of
this text.

6.4 Identifier Scope and Extent

As you may have noticed, parameter names in calls to subprograms do not have to match the
names in the subprogram parameter list. It is important to understand when (the extent) and
where (the scope) the identifiers in subprograms have meaning.

The extent of an identifier defined in a subprogram is limited to the time the subprogram has
control of the computer's execution. The variables and constants declared in a subprogram come
into existence when the subprogram is called and they cease to exist when control is returned to
122
the main (calling) program. When a subprogram ends, all the memory allocated to the
subprogram is returned to the available memory space. Also, any values taken on by internal
variables in a subprogram are lost at the end of execution and are therefore not available when
the subprogram is called again. Consider the following simple example.

procedure sample(x : in out integer) is


y : integer;
begin
put(y,3); -- bad programming practice
y:=x;
put(y,3);
end sample;

The value of y is undefined until it is assigned the value of x. This will be true regardless of the
number of times the procedure sample( ) is called. The first put(y,3) statement prints some value
that happens to be in the memory location allocated to y which may not be the same memory
location previously allocated. The scope of an identifier is limited to the most local declaration of
that identifier.

procedure scope_demo is
w,x,y : float;

procedure sample(y : in out float) is


z : float;
begin
z:=y+x;
y:=z;
end sample;

begin
w:=3.0;
x:=2.0;
y:=1.0;
sample(w);
put(w);
put(y);
end scope_demo;

In the program above, the identifiers w, x and y are defined in the main program scope_demo.
These variables are assigned the values 3.0, 2.0, and 1.0 respectively in the executable body of
the main program.

Another identifier named y is declared in the parameter list of the procedure sample( ). Finally,
an identifier named z is declared as an internal variable inside sample( ). Let's walk through the
execution of this program as we study the scope and extent of each of the identifiers.

123
scope_demo sample

3
procedure scope_demo is w ? y
w,x,y : float;
2
procedure sample(y : in out float) is x z
?
z : float;
1
begin y
z:=y+x;
2 y:=z; 1
in
end sample; w 3 3 y
begin
w:=3.0; x:=2.0; y:=1.0; x 2 ? z
sample(w); 3
put(w); put(y); y 1
end scope_demo;

w 3 3 y
1. Identifiers w, x and y are assigned values
x 2 + 5 z
2. Subprogram sample( ) is called and the value of w is
passed to the parameter y. The identifier y in the sample( ) y 1
is not the same variable as the identifier y in the main program. 4

3. The identifier z in sample( ) is assigned the sum of the locally w 3 5 y


defined y and the value of x defined in the main program.
x 2 5 z
4. The identifier y in sample( ) is assigned the value of z. This
does not affect the identifier named y in the main program. 5 y 1

5. The value of y in sample( ) is passed back to the main and


out
assigned to the associated identifier w. w 5 5 y

x 2 5 z

y 1

Figure 6-2: Example of the Scope of Identifiers Between Main Program and Sub-Procedure

When attempting to determine the scope of an identifier, start with the location of the identifier
and find its most local declaration. Consider the assignment z:=y+x in the procedure sample( )
above. The identifier z is declared as an internal variable in sample( ). The identifier y is declared
in the main program and in the parameter list of the procedure. Since the parameter list gives a
more local declaration for y, we use the value of y in the procedure for the sum y+x. Finally, the
most local declaration of x is in the main program scope_demo, so we use its value in the
assignment z:=y+x. We say that the scope of the main program identifier y is hidden by the
declaration of an identifier named y in the subprogram.

When the scope is not hidden by local declarations, subprograms can access identifiers declared
at a higher level. The reverse is not true. That is, outer programs do not have access to
internally declared identifiers in subprograms contained within them. For example, the identifier z
is not accessible from the main executable block of the program scope_demo.

Important: It is not good programming practice to permit a subprogram to access or alter the
values of identifiers declared outside the subprogram (this is called a side effect). Generally, all
communication between a subprogram and its calling program should be through the parameter
list.

We will see a few applications in which permitting procedures to produce side effects is
acceptable. For example, if an application program is performing operations on a large data
structure such as an array and changes to the data structure are always global (that means that
all parts of the program are using the same data structure), then it is more efficient to permit the
124
subroutines to access the data structure directly and for procedures to alter the data structure
through side effects. However, it is never acceptable for functions to produce side effects.

6.5 Applications

6.1: Write an Ada program that determines if an input string entered by the user is a palindrome.

A palindrome is a string that contains the same sequence of characters when read from either
end. Examples of palindromes are,

eve
madamimadam
radar
ablewasiereisawelba
amanaplanacanalpanama

Typically, we ignore blanks, punctuation and letter case so that a sentence is considered a
palindrome if the sequence of letters is the same read forward and backward regardless of how
they are grouped into words. We could represent the palindromes above in a more readable form
as,
Eve
Madam I'm Adam.
RADAR
Able was I ere I saw Elba.
A man, a plan, a canal, Panama.
The following is pseudocode for our main program. At the top level, we see the structure of the
program. However, at this level, the work of determining if the input string is a palindrome is
contained in the third line.
-- Prompt the user to enter the candidate string
-- Get the candidate string and the length of the string
-- if the candidate string is a palindrome then
display the statement "This is a palindrome"
else
display the statement "This is not a palindrome"
We will place the code to evaluate the string into a Boolean function called is_palindrome( ). In
the first version of our function we will require palindromes to be an exact match in character
sequence, letter case, and other symbols as illustrated in the first group of examples. This will
make the program easier to design. Once we have a working program, we will modify it to
recognize palindromes like those in the second group.

n = length of string

str m a d a m i m a d a m

str(i) str(n+1-i)
Figure 6-3: Algorithm Testing a Candidate String for the Palindrome Property

125
We will define a string called str to hold strings of characters input by the user. Since we cannot
predict the length of these strings we will have to be able to determine their length during run
time.

To test to see if the string is a palindrome we will compare the first character in the input string to
the last, the second to the next to last, and so on. If any such pair of characters is not the same
we will say that this string is not a palindrome. Otherwise, we will say that it is a palindrome.

In general, we are comparing the ith character from the left to the ith character from the right. For
a string of length n, this means that we wish to compare str(i) with str(n+1-i). This can
be accomplished using a for..loop as shown below.

result:=true;
for i in 1..n/2 loop
if a_str(i)/=a_str(n+1-i) then
result:=false;
end if;
end loop;

Since a single mismatch means that a string is not a palindrome, we will initially assign result to
TRUE. We will search the string for mismatches and set bool_val to FALSE if we find any
mismatched pair. We are comparing opposite ends of the string to each other so the range of the
for loop can be 1..n/2. When the number of characters in the string is even, every pair of
characters will be tested. When the number of characters in the string is odd, the middle
character is not tested. This is not a problem, since it doesn't have to be compared with anything
other than itself anyway.

The Boolean test described above is a good candidate for a function. We can create a function
that accepts the string and returns TRUE if the string is a palindrome and FALSE otherwise.

function is_palindrome(a_str : string) return boolean is


result : boolean :=true;
n : integer;
begin
n:=a_str'length;
for i in 1..n/2 loop
if a_str(i)/=a_str(n+1-i) then
result:=false;
end if;
end loop;
return result;
end is_palindrome;

We are using an unconstrained string a_str as the parameter holding the candidate palindrome in
the function. To determine the length of the string we use the attribute function 'length. We can
use an unconstrained string in this function even though we need to specify a maximum length
string in the main program. Since the user can input any length string (up to a declared maximum
number of characters) we will use the get_line( ) procedure in the main program to capture the
input string, str, and its length, len, as shown,

procedure palindrome is
str : string(1..80);
len : integer;

-- function declaration goes here

begin
put("Enter your string... ");
get_line(str,len);
if is_palindrome(str(1..len)) then
126
put("This is a palindrome");
else
put("This is not a palindrome");
end if;
end palindrome;

The get_line( ) procedure places the user's input string into the string identifier str and the length
of the string into the integer identifier len. The maximum length of str is set to 80 characters since
this is the number of characters in one line of text displayed on the console. The identifier len
holds the actual number of characters entered by the user, so we can use this value to specify
the length of the string being passed to the function is_palindrome( ).

is_palindrome(str(1..len))

Each time is_palindrome( ) is called, the number of characters may be different. We use the
attribute function 'length to determine the length of the candidate palindrome string a_str inside
the function. This value is used to set the upper limit of the for..loop.

This program works but the user is forced to type only the letters in the candidate palindrome
omitting blanks, punctuation, and letter case. We can modify this program by including additional
subprograms to perform these housekeeping tasks for the user.

Let's write a procedure that converts all uppercase letters to lower case. We need to test each
character in the string to see if it is an uppercase letter. If so, then we convert it to lowercase as
illustrated in the code segment below.

n:=a_str'length;
for i in 1..n loop
if is_uppercase(a_str(i)) then
a_str(i):=make_lowercase(a_str(i));
end if;
end loop;

We have used a Boolean function is_uppercase( ) and another function make_lowercase( ) to


test for and change the case of the characters in the string a_str. These are not predefined
functions in Ada or any of its standard I/O packages. The reason we refer to such non-existent
functions is to simplify the code design process. The operations of these two functions are well
understood and are distinct from the search process itself. Compare this code segment with the
functionally equivalent code segment in which these operations are explicitly included,

n:=a_str'length;
for i in 1..n loop
if a_str(i)>='A' and a_str(i)<='Z' then
a_str(i):=character'val(character'pos(a_str(i))+32);
end if;
end loop;

As you can see, the operations in the code segment are not as easily understood in this version.
The functions is_uppercase( ) and make_lowercase( ) are simple one-line functions that deal with
single characters.

function is_uppercase(ch : character) return boolean is


begin
return ch>='A' and ch<='Z';
end is_uppercase;

function make_lowercase(ch : character) return character is


begin
return character'val(character'pos(ch)+32);
end make_lowercase;
127
These two functions can be placed in the declaration block of the procedure lowercase( ) which is
itself a subprogram of the main program palindrome. A complete listing of this version of the
program is shown below.

with ada.text_io;
use ada.text_io;

procedure palindrome is

str : string(1..80);
len : integer;

function is_palindrome(a_str : string) return boolean is


bool_val : boolean :=true;
n : integer;
begin
n:=a_str'length;
for i in 1..n/2 loop
if a_str(i)/=a_str(n+1-i) then
bool_val:=false;
end if;
end loop;
return bool_val;
end is_palindrome;

procedure lower_case(a_str : in out string) is


n : integer;

function is_uppercase(ch : character) return boolean is


begin
return ch>='A' and ch<='Z';
end is_uppercase;

function make_lowercase(ch : character) return character is


begin
return character'val(character'pos(ch)+32);
end make_lowercase;

begin
n:=a_str'length;
for i in 1..n loop
if is_uppercase(a_str(i)) then
a_str(i):=make_lowercase(a_str(i));
end if;
end loop;
end lower_case;

begin
put("Enter your string... ");
get_line(str,len);
lower_case(str(1..len));
if is_palindrome(str(1..len)) then
put("This is a palindrome");
else
put("This is not a palindrome");
end if;
end palindrome;

128
Next, we want to recognize palindromes that include arbitrarily placed blanks and punctuation
marks. That is, we want to be able to ignore non-alphabetic characters. This can be done in two
ways. Either we can modify the input string to remove all non-alphabetics, or we can modify the
is_palindrome( ) function to ignore non-alphabetics when comparing letter pairs.

Let's try the second approach. We will replace the for...loop with a general loop and set the
upper and lower indices to 1 and a_str'length respectively.

i:=1;
j:=a_str'length;
loop
exit when i>=j;
-- increment i until a_str(i) is a letter
-- decrement j until a_str(j) is a letter
-- compare a_str(i) with a_str(j) return FALSE if they don't match
i:=i+1;
j:=j-1;
end loop;

We will use i for the lower index and j for the upper index. We will move i and j toward each other
(to the next letter) in a_str until i passes j. At each letter pair, we compare a_str(i) with a_str(j) as
in our original version of is_palindrome( ).

m a d a m i ‘ m a d a m .

j
i

Figure 6-4: Position of Indices at First Comparison


The way we make sure that a_str(i) is a letter is to increment i until the character a_str(i) is an
alphabetic character. The way we make sure that a_str(j) is a letter is to decrement j until the
character a_str(j) is an alphabetic character.

while not_a_letter(a_str(i)) and i<j loop


i:=i+1;
end loop;

while not_a_letter(a_str(j)) and i<j loop


j:=j-1;
end loop;

We also check to make sure that the indices i and j have not passed each other. When i>=j we
have reached the end of the comparisons and we exit the loop. There are a number of details
omitted from this example, which may be addressed by the reader or in a laboratory assignment.

6.2: Given the following data structure shown for a tic-tac-toe game, write an Ada boolean
function that returns TRUE if a player has won the game (i.e. has three in a row). The function
should accept a copy of the board (the 3x3 matrix) and the player's symbol (a character type).
Unused positions in the array contain blanks.

type boardtype is array(1..3,1..3) of character;


b : boardtype;
p : character;

Since there are eight ways a player can win the game, we can check for the player's symbol in all
three positions for each of the eight possible winning combinations.

129
x x x x x x x x
x x x x x x x x
x x x x x x x x

Figure 6-5: Possible Winning positions for Player 'X' in Tic-Tac-Toe

function wins(b : boardtype; p : character) return boolean is


begin
return (b(1,1)=p and b(1,2)=p and b(1,3)=p) or
(b(2,1)=p and b(2,2)=p and b(2,3)=p) or
(b(3,1)=p and b(3,2)=p and b(3,3)=p) or
(b(1,1)=p and b(2,1)=p and b(3,1)=p) or
(b(1,2)=p and b(2,2)=p and b(3,2)=p) or
(b(1,3)=p and b(2,3)=p and b(3,3)=p) or
(b(1,1)=p and b(2,2)=p and b(3,3)=p) or
(b(3,1)=p and b(2,2)=p and b(1,3)=p);
end wins;

This function is cumbersome but functional. Can you think of a better way to solve this problem?
Consider the arrangement of the digits 1 through 9 in the magic square below.

8 1 6
3 5 7
4 9 2

Figure 6-6: The Magic Square in which Every 3 Values in a Line Sum to 15
Notice that the sum of three digits in a straight line in any direction is 15. Can you think of a way
to use this relationship to simplify the boolean function wins( )?

6.3: Use the array of digits above to define a data structure and functions or procedures to find
winning and blocking moves in tic-tac-toe.

We can associate the positions selected by each player in a game of tic-tac-toe with the
corresponding values in the array of digits. For each pair of digits we can determine the value
required for a total of 15. This third digit represents a winning or blocking move if the
corresponding position is available. For example, if a player has positions corresponding to 1, 3,
and 6 then this player has potential winning moves at,

1 + 3 + ___ = 15, missing value is 11 (not valid),


1 + 6 + ___ = 15, missing value is 8 (valid move), and
3 + 6 + ___ = 15, missing value is 6 (not available)

So this player has one winning move at position 8. The opponent's positions can be tested in a
similar way to check for positions to block a winning move. The body of a procedure to list all
winning moves is,

for i in 1..9 loop


for j in 1..9 loop
if s(i)=p and s(j)=p and is_available(v(i)+v(j)) then
put("Winning move at ");
put(k(15-v(i)+v(j)));
new_line;
end if;
end loop;
end loop;
130
where s(i) is a character array holding each of the player's symbols in the locations corresponding
to their moves in the tic-tac-toe board, v(i) is an array holding the positional values used to test
for three-in-a-row (sum to 15), and k( ) is an array indicating the positional value (index)
corresponding to each of the values in v( ).

S X O O O X X
8 1 6 1 2 3 X O
3 5 7 4 5 6 O O X
V 8 1 6 3 5 7 4 9 2
4 9 2 7 8 9 X
board board
“15 values”
positions symbols K 2 9 4 7 5 3 6 1 8

Figure 6-7: Data Structure for Tic-Tac-Toe Using the Magic Square

The array k( ) tells us the position in the 15 array of any particular value 1..9. For example, player
"O" has positions 3 and 5 whose "15 values" are 6 and 5. This sums to 11 and 15-11=4.
Therefore, we use k(4) to discover that player "O" can win by choosing board position 7. The K
array maps the "15 values" back into board positions.

Although the use of the "15 values" may seem to add unnecessary complexity to the problem,
consider the alternative. Sixteen pairs of positions must be tested while searching for winning
and blocking moves. Using the original data structure, all of these would have to be hardwired
into the subprograms. This application demonstrates the importance of designing a good data
structure for your program.

131
Exercises .

6.1 Write a function to return the whole number part of its floating point argument as an integer.

6.2 Write a function to return the fractional part of its floating point argument.

6.3 Write a function to return the value of n factorial (n!) given n as in integer argument. Beware
of integer overflow.

6.4 Write a function that uses the factorial function of Exercise 6.3 and returns the number of
ways to choose m out of n items C(m,n) (order is not important).

n n!
C = C (n, m) =
m m!(n − m )!

This function computes the number of combinations of n items chosen m at a time. These values
are also known as the binomial coefficients.

6.5 Write a function that returns the number of ways to choose m out of n items (order is not
important). Design this function to compute the largest possible values of C(n,m) that can be
represented as a single precision integer. Warning: Computing the factorials individually will limit
the maximum size of C(n,m). For example, C(100,1)=100 but 100! exceeds the range of
integers.

6.6 Write a function that returns the number of ways to arrange m out of n items (order is
important). Design the function to compute the largest possible value of P(n,m) that can be
represented as a single precision integer.

n n!
P = P(n, m) =
m (n − m )!
This function computes the number of possible permutations (or arrangements) of m items
chosen from a collection of n items.

6.7 Write a function that returns the least common multiple of its two integer arguments.

6.8 Write a function that returns the greatest common divisor of its two integer arguments.

6.9 Write a procedure that changes its in out parameters to represent the fraction expressed in its
lowest terms, given that the first parameter is the numerator and the second is the denominator of
a proper fraction.

procedure reduce(N,D : in out integer) is

6.10 Write a procedure that converts a terminating decimal value X (between 0.0 and 1.0) into a
proper fraction. This procedure should include two integer out parameters representing the
numerator and denominator of the derived fraction.

procedure fraction(X : in float; N,D : out integer) is

6.11 Write a Boolean function to return TRUE if two of its three integer arguments are equal.

6.12 Write a Boolean function to return TRUE if its three floating point arguments could represent
the lengths of the sides of a triangle.

132
6.13 Write a Boolean function to return TRUE if its integer argument is even.

6.14 Write a procedure that adds two time durations together and displays their sum in proper
form. For example, 3 hours 20 minutes plus 2 hours 55 minutes would give 6 hours 15 minutes.

133
134
Chapter 7 - Graphics

7.1 The Importance of Visualization

7.2 Basics of Two-Dimensional Graphics

7.3 Ada Graphics Packages


Adagraph and Adagraph 2000

7.4 Modeling and Transformations


Modeling Graphical Objects
Shifting
Scaling
Rotation

7.5 Applications
Drawing an Array of Boxes
Shifting and Scaling the X-Box
The Bouncing Ball

135
Chapter 7 - Graphics
In this chapter, we learn about the importance of visualization in the understanding and
presentation of abstract concepts. We will use Adagraph-2000 to generate simple images in the
Ada language. Although this graphics package has limited features, the principles and
programming techniques used here are applicable to all computer graphics applications.

7.1 The Importance of Visualization

Anyone familiar with consumer electronics is aware of the tremendous advances in speed and
image quality of graphics in computer games and in computer generated video. Computer
generated images (CGI) and animations have attained a level of refinement that makes them
virtually indistinguishable from reality. While it is true that the entertainment industry is driving
this technology, it is not the only group that is reaping the benefits.

Many scientists and technicians in basic scientific research, training, diagnostics, and engineering
use graphical representations of complex data to improve understanding and to discover subtle
but detectable patterns within the data. These are not just pretty pictures for marketing blurbs.
The human mind/brain is much better adapted to spatial reasoning than to interpreting large
tables of numbers. Experienced researchers can assimilate information at a much faster rate
when represented graphically. Anyone interested in learning more about this field is encouraged
to review the large number of Web sites available on this subject. The NASA Ames Research
Center maintains an online bibliography of Scientific Visualization Sites at

http://www.nas.nasa.gov/Groups/VisTech/visWeblets.html

This bibliography lists hundreds of university, government, commercial, and military scientific
visualization centers around the world. Each one of these sites represents a community of
researchers and businesses using computer generated imagery as a tool to gain insight and to
advance the state of scientific knowledge in their respective fields.

7.2 Basics of Two-Dimensional Graphics

We will begin our study of computer graphics with simple two-dimensional (2-D) drawings
composed of the primitive elements of point and line. We can describe a position in a plane using
a pair of numbers representing a location relative to some reference system. We call this pair of
numbers the coordinates of the point (i.e. its position) in the 2-D plane. The reference system
depicted on the left below is called a Cartesian reference system.

second first second


quadrant first
quadrant quadrant quadrant

X
R
Y
θ

origin

third fourth fourth


third
quadrant quadrant quadrant
quadrant

Rectangular or
Cartesian Coordinates Polar Coordinates

Figure 7-1: The Cartesian and Polar Coordinate Reference Systems in Two-Dimensions

In the Cartesian system the coordinates of a point indicate the horizontal (X) and vertical (Y)
location of the point relative to the origin. The origin has coordinates (0,0). The reference system
on the right is called the Polar coordinate system. In this system, the position of a point is defined

136
by the magnitude of its distance from the origin (R) and the angular direction of the point from the
horizontal axis (θ). A point in the Polar coordinate system can be mapped to the equivalent
location in the Cartesian coordinate system using the trigonometric relationships,

x = R cos(θ )
y = R sin(θ )

Therefore, the point at (R,θ) in Polar coordinates is at position (x,y) in Cartesian coordinates.
Similarly, we can map points in Cartesian coordinates to Polar coordinates by,

R = x2 + y2
y
θ = arctan for points in first quadrant only
x
θ = arctan( x, y )

It is important to remember that when mapping points from Cartesian to Polar coordinates you
o
should use a version of the inverse tangent arctan( ) that supports full four-quadrant angles (0 to
o
360 ). A four-quadrant arctan( ) function will require that you enter y and x as separate values.
This is because the argument y/x (first quadrant) will return the same angle as -y/-x (third
quadrant) and the argument -y/x (fourth quadrant) will return the same angle as y/-x (second
quadrant).

In mathematics, a line segment is defined as the shortest distance between two points. On a
computer display every line segment is actually made up of a set of points in a rectangular grid
whose positions are as close as possible to the ideal straight line connecting the end points.

(X1,Y1)
a
pixel at
maximum
resolution

(X2,Y2)
a low
resolution
pixel

Figure 7-2: Points (Pixels) and Lines in a Digital Image

This is because digital images displayed on computer monitors or when printed are made up of a
rectangular array of colored spots called pixels. A small drop of water on your computer screen
makes a convenient magnifying lens for viewing these pixels. A pixel on your computer screen is
actually a set of three (or more) dots of phosphor in three primary colors (red, green, and blue).
The phosphor emits light when energized by a beam of electrons (see also the Edison effect in
the glossary). Different screens use different arrangements and shapes for these dots attempting
to increase the perceived quality (sharpness) of the images. The space between the dots is dark
and is sometimes masked off to increase image contrast (the difference between the darkest and
the brightest part of an image).

A measure of the resolution of a monitor is the maximum number of pixels that can be displayed
on the screen. Your computer operating system lets you choose from a number of screen display
resolutions. Typical spatial resolutions are 640x480 pixels, 800x600, 960x720, 1024x768 and
1280x1024. When you set your computer to display less than the maximum resolution possible
for your monitor, a pixel will be made up of more than three dots of phosphor, as shown in Figure
7-2 above. At lower screen resolutions, a single pixel may be comprised of several of each color
of the dots.
137
In addition to controlling the spatial resolution, you can set the number of colors displayed by your
monitor. A minimal standard for digital color images is 256 colors. In the 256 color mode a list of
the 256 most frequently used colors in an image is stored in a color lookup table (CLUT).
Associated with each of these indexed colors are numbers indicating the amount of red, green,
and blue to be used for this color. Higher color resolutions are available with up to 24 or more
bits per pixel. This mode may be called true color mode on your computer.

In true color mode each of the three primary color dots can be set to any of 256 brightness levels
(8 bits per dot or 24 bits per pixel). Since the human visual system can discern at most a few
thousand variations in color (called JNDs or just noticeable differences) most of these millions of
color variations are invisible to us anyway. It is not necessarily a good idea to set your display to
the maximum spatial or color resolutions since this can slow down your computer's response time
significantly.

A note on Computer Hardware: When you are wanting a computer that can display high
resolution images at a high-speed (high frame rate), you are much better off spending your
money on a good graphics accelerator card than on the fastest and newest CPU. The difference
in processor speed between the hottest new processor and a much less expensive one is
typically 20% or less. This 20% can cost you several hundred dollars. Compare this with
spending a couple hundred dollars more on a really good graphics accelerator to gain a factor of
5 or more in frame rate. That is a 500 percent faster frame rate and improved image quality as
well.

7.3 Ada Graphics Packages

Graphics support is not part of most computer languages but is usually available as a language
extension. A simple 2-D graphics package available for GNAT Ada is called AdaGraph (see
Chapter 8 for more information about the AdaGraph). AdaGraph implements an easy to use
graphics system for the GNAT and ObjectAda compilers for Windows 98/2000/NT/XP. AdaGraph
was designed as an educational tool by Jerry Van Dijk and is available on his Web page at,
http://home.trouwweb.nl/Jerry/adagraph.html
An improved version of Adagraph called Adagraph-2000 was developed by Dr. Martin Carlisle
and his colleagues at the Air Force Academy. The primary difference between these two
packages is that the original AdaGraph supports only 16 colors while Adagraph-2000 supports
over 240 colors (see Chapter 8). The Adagraph-2000 package includes a limited set of drawing
primitives including,
Open_Graph_Window - creates a graphics window of the user specified size.
Clear_Window - erases the contents of the graphics window with a specified color.
Set_Window_Title - displays a specified title in the Graphics Window Title Bar
Close_Graph_Window - closes the graphics window
Closest Color - returns the closest index color for a given set of RGB values
Get_Pixel - returns the color of the indicated pixel
Put_Pixel - sets the indicated point in the graphics window to the specified color
Draw_Line - draws a line from a specified point to a specified point of a given color
Where_X - returns the current X position of the drawing point in the graphics window
Where_Y - returns the current Y position of the drawing point in the graphics window
Goto_XY - moves the drawing point to the specified location in the graphics window
Draw_Box - draws a rectangle with a specified upper left and lower right corner, line color,
and fill/no fill factor
Draw_Ellipse - draws an ellipse contained in a box with a specified upper left and lower
right corner and sets the line color and fill factor
Flood_Fill - fills a closed region defined by all pixels of the same color as the pixel at a
specified starting location and that are adjacent (up,down,left,right) to a member pixel
Display_Text - displays a line of text starting at a given position and in a specified color
138
A sample GNAT Ada program demonstrating some of the features of AdaGraph is shown below.

with ada.text_io, adagraph;


use ada.text_io, adagraph;

procedure adagraph_demo is
x_max : constant integer :=640; -- Max horizontal coordinate
y_max : constant integer :=480; -- Max vertical coordinate
-- Text string to display
hello_string : constant string := "Hello, world!";
string_length : constant integer := hello_string'length;
key : character; -- user input character

begin
open_graph_window (x_max, y_max);
set_window_title ("AdaGraph 'Hello World' example");
clear_window (light_blue);

-- Demonstrate drawing primitives


draw_line(10,10,400,100,black);
draw_box(100,20,180,70,yellow,fill);
draw_circle(60,300,50,light_magenta,fill);
draw_ellipse(50,280,90,310,blue,no_fill);
flood_fill(75,325,light_gray);

for i in reverse 1..241 loop


draw_circle(400,240,i,extended_color_type'val(i),fill);
end loop;

-- Display the string in red and black


display_text (50, 450, hello_string, black);
display_text (49, 449, hello_string, light_red);
display_text (48, 448, hello_string, light_red);

-- Wait until the user presses any key


wait_for_key;

-- Close the AdaGraph window


close_graph_window;

end adagraph_demo;

When compiled, this program opens a graphics window and generates an image like the one
shown on the next page.

139
Figure 7-3: Graphics Window Generated by adagraph_demo.adb

This sample shows a number of the primitive drawing procedures in AdaGraph, including
Draw_Line, Draw_Box, Draw_Ellipse, Draw_Circle, and Display_Text. This graphics package
also supports direct keyboard control with the wait_for_key procedure. See how these two
subprograms are used in the previous sample code to force the program to wait for the user to
press a key before closing the graphics window.

7.4 Modeling and Transformations

Modeling Graphical Objects

In order to create an image with a computer, we need to describe the parts of the image in a
manner that can be interpreted by the computer program. We will typically use a geometric
model, which is a numerical representation of the shape, size, position, and other attributes of the
image elements.

We will create a geometric model of the simple drawing of a house. We could model this house
with a list of line segments, but the circular window would create a problem for use. We would
need to include many short line segments to represent a circle, but since we haven't determined
a scale for our drawing, we don't know how many segments would be needed to make sure that
the circle appears smooth.

A better approach is to define a set of drawing primitives that include circles as elemental forms.
Then we can describe a circle by giving the location of its center and its radius. Our model could
include other primitives such as squares, rectangles and general polygons.

140
geometric model of house
origin of circle(0,6,1)
coordinate system square(-4,3,-2,1)
square(2,3,4,1)
rectangle(-1,3,1,-2)
polygon(-5,-2,-5,4,0,9,5,4,5,-2,-5,-2)

circle(xcen,ycen,radius) - center position and radius


square(x1,y1,x2,y2) - upper left and lower right corners
rectangle(x1,y1,x2,y2) - upper left and lower right corners
polygon(x1,y1,x2,y2, . . . ,xn,yn,x1,y1) - sequence of points defining polygon shape

Figure 7-4: Geometric Model of a Simple 2-D Drawing (House).

The geometric model of the house is shown in a Cartesian coordinate system. The position of
the house relative to the origin of the coordinate system is not important in this description. The
points described as x,y pairs give the positions of the points relative to each other with the origin
of the points as shown in Figure 7-4 above. Using shifting and scaling operations, we can move
and resize the house to fit any graphics window.

Shifting

The shift operation allows us to translate an object or collection of objects horizontally and/or
vertically without rotation or change in shape or size. For example, to move a set of objects to
the right 3 units, we simply add 3 to each of the x components of every point in the object set.

A common problem in computer graphics is finding the amount of shift necessary to place an
object inside a particular region of the graphics window. The amount of shift needed in the x or y
direction is the difference between the edge (minimum x or y) of the object set and the
corresponding edge of the target region of the graphics window.

x_shift := X_regionmin - X_setmin;


y_shift := y_regionmin - y_setmin;

Most geometric models use the convention that positive y is up, while many computer graphics
systems set the origin in the upper left corner of the graphics window and define positive y as
down. In order to rectify the image in such a graphics system (put the top of the image toward
the top of the graphics window) we would need to perform the following transformations when we
plot the image in the graphics window.

x_plot := x(i) + x_shift;


y_plot := y_regionmax - (y(i) + y_shift);

Note that the original Adagraph used the inverted (positive y down) system with the window origin
in the upper left corner of the graphics window. Adagraph-2000 places the window origin in the
lower left corner and makes the positive y direction upwards.

141
Scaling

We use the scale operation to change the size of an object being displayed in the graphics
window. The scale is just the relative size of the object before and after the scale change. If we
wish to fit an object whose left and right x limits are x_setmin and x_setmax into a graphics region
whose left and right x limits are x_regionmin and x_regionmax, we compute x_scale as,

x_scale := float(x_regionmax-x_regionmin)/float(x_setmax-x_setmin);

and apply this scale factor by using the assignment,

x_plot := integer(x_scale*(float(x(i))));

Notice that even though the points in the graphics window are integer types we set the scale to a
floating point type (more on this later). Similarly, we can compute and apply a scale factor for the
y direction,

y_scale := float(y_regionmax-y_regionmin)/float(y_setmax-y_setmin);
y_plot :=integer(y_scale*(float(y(i))));

We make x_scale and y_scale float types because we need to maintain the precision of these
values. If we were to use integers, then any attempt to make the image smaller (scale < 1.0)
would result in a scale factor of zero.

Let's say we wish to draw an image whose object set has limits (-3,5,7,12)
(x_setmin,y_setmin,x_setmax,y_setmax) into the graphics region whose limits are (20,30,45,50)
(x_regionmin,y_regionmin,x_regionmax,y_regionmax).

target region
(20,30,45,50)

object set
(-3,5,7,12)

Figure 7-5: Coordinates for a Sample Shift and Scale Operation in Two Dimensions

We will compute the shift and scale factors needed to transform the object set into the target
region. We will compute the scale factors first to make the image fit inside the target region. We
compute the scale factors first since they are needed to determine the shift factors.

x_scale := float(x_regionmax-x_regionmin)/float(x_setmax-x_setmin);
y_scale := float(y_regionmax-y_regionmin)/float(y_setmax-y_setmin);

Now we compute the shift factors (we must make sure to use the newly scaled x,y values),

x_shift := x_regionmin-integer(x_scale*float(x_setmin);
y_shift := y_regionmin-integer(y_scale*float(y_setmin);

142
We use the shift and scale factors to transform each point in the object set into the viewing
coordinates. For example, the ith point in the object set called object(i) is used to compute the
corresponding plotting point in the viewing window (x_plot,y_plot) by,

x_plot := integer(x_scale*float(object(i).x))+x_shift;
y_plot := integer(y_scale*float(object(i).y))+y_shift;

In the code segment below, we are drawing a polygon defined in the point set called object( ).

for i in 1..n loop


x_plot:=integer(x_scale*float(object(i).x))+x_shift;
y_plot:=integer(y_scale*float(object(i).y))+y_shift;
if i=1 then
goto_xy(x_plot,y_plot);
else
draw_to(x_plot,y_plot,black);
end if;
end loop;

Let's run some numbers to see if our transformation process makes sense. The center of the
"x-box" defined in the region (-3,5,7,12) is at ([7+(-3)]/2,[12+5]/2) or (2,8.5), where the y
coordinate would be truncated from 8.5 to 8 (integer arithmetic) if we were to actually plot this
point. However, we are using our object set as the geometrical model of an image, so we will not
throw away precision until we are ready to plot the point. The center of the transformed object set
should be the center of the target region (20,30,45,50) or (32,40). We can compute the scale and
shift factors and then test them to see if the point (2,8.5) actually gets transformed to the point
(32,40).

x_scale = (x_regionmax-x_regionmin)/(x_setmax-x_setmin) = (45-20)/(7-(-3)) = 25/10 = 5/2


y_scale = (y_regionmax-y_regionmin)/(y_setmax-y_setmin) = (50-30)/(12-5) = 20/7
.
x_shift = x_regionmin-int[x_scale y_setmin] = 20 - int[(5/2)(-3)] =20 - (-7) = 27
.
y_shift = y_regionmin-int[y_scale y_setmin] = 30 - int[(20/7)(5)] = 30 - 14 = 16
.
x_plot = int[x_scale x_obj] + x_shift = int[(5/2)(2)] + 27 = 32
.
y_plot = int[y_scale y_obj] + y_shift = int[(20/7)(8.5)] + 16 = 24 + 16 = 40

which matches our predicted position for the transformed point.

Rotation

Another important type of image transformation is called rotation. We use rotation to change the
orientation or angle of the image. By convention, counter-clockwise (top of image to the left and
bottom to the right) rotations are positive while clockwise rotations are negative. This follows the
convention used in polar coordinate systems. Given a point (x,y) we can rotate it about the origin
through an angle φ using the trigonometric relationships,
xr = x cos(φ) - y sin(φ)
yr = x sin(φ) - y cos(φ)

When the transformation includes a simple rotation (i.e. object centered) you should rotate the
points before scaling or shifting. This is because angular rotations are performed around the
origin. Therefore, if the object is moved away from the origin before it is rotated, the rotation will
move the object around the coordinate system origin like a rock being spun on the end of a string.

In the following code segment, we are reading a list of points from the text file datin and rotating
them through an angle ang, and then applying scale and shift operations on each point before
they are displayed.

143
get(datin,npts);
for i in 1..npts loop
get(datin,x);
get(datin,y);
x_rot:=x*cos(ang)-y*sin(ang);
y_rot:=x*sin(ang)+y*cos(ang);
pointlist(i).x:=integer(scale*x_rot+x_shift);
pointlist(i).y:=integer(scale*y_rot+y_shift);
end loop;

Let's create a version of the geometric model of the simple house that includes only circles and
polygons. The basic shape of the house can be defined by the data set,

c 0 6 1
p 5 -4 3 -2 3 -2 1 -4 1 -4 3
p 5 2 3 4 3 4 1 2 1 2 3
p 4 -1 -2 -1 3 1 3 1 -2
p 6 -5 -2 -5 4 0 9 5 4 5 -2 -5 -2
x

where a 'c' indicates a circle and is followed by the circle center and radius. A 'p' indicates a
polygon and is followed by the number of points and then that number of (x,y) point pairs. We
have used an 'x' to indicate the end of the file. Using this geometric model, we can write an Ada
program to read each line, perform the rotation, scaling, and shifting operations, and display the
house object. We will use a case statement to choose the correct code segment for reading the
remainder of a line based on the character read from the first position of the line of text.

-- open the geometric model file


-- while we are not at the end of the file...
-- get the first character from the next line, call it shape

-- case shape is
-- when 'c' read the center point (x_cen,y_cen) and the radius
-- when 'p' read the number of points npts for the polygon
-- for each point in the polygon
-- get the next point (x,y)
-- transform the point
-- save the point in a point list
-- next point
-- plot the points in the point list
-- when 'x' we are done
-- end case

-- close file

The executable body of an Ada program based on the pseudocode shown above is,

open(datin,in_file,"house.dat");
while not(done) loop
get(datin,shape);
case shape is
when 'c' => get(datin,x);
get(datin,y);
get(datin,rad);
x_rot:=x*cos(ang)-y*sin(ang); -- rotation ang about the origin
y_rot:=x*sin(ang)+y*cos(ang);
rad:=float(rad)*scale;
x:=scale*float(x_rot)+x_shift;
y:=scale*float(y_rot)+y_shift;
draw_circle(integer(x),integer(y),integer(rad),black,no_fill);
when 'p' => get(datin,npts);
144
for i in 1..npts loop
get(datin,x);
get(datin,y);
x_rot:=x*cos(ang)-y*sin(ang); -- rotation ang about the origin
y_rot:=x*sin(ang)+y*cos(ang);
pointlist(i).x:=integer(scale*x_rot+x_shift);
pointlist(i).y:=integer(scale*y_rot+y_shift);
end loop;
goto_xy(pointlist(1).x,pointlist(1).y);
for i in 2..npts loop
draw_to(pointlist(i).x,pointlist(i).y,black);
end loop;
when others => done:=true;
end case;
end loop;
close(datin);

In this code segment the variable shape is of type character and is the first symbol in each line of
the geometric object text file datin (with physical file name house.dat). The primitive shapes are
transformed and displayed in the graphics window. A sample of random transformations of this
geometric model is shown in Figure 7-6 below.

Figure 7-6: Example Output of a Program to Rotate, Scale, and Shift the House Object

You may be curious why the model data is treated as floating points until just before they are
displayed. Compare the code segment below with the corresponding code above

get(datin,npts);
for i in 1..npts loop
get(datin,x);
get(datin,y);
x_rot:=integer(float(x)*cos(ang)-float(y)*sin(ang));
y_rot:=integer(float(x)*sin(ang)+float(y)*cos(ang));
pointlist(i).x:=integer(scale*float(x_rot)+x_shift);
pointlist(i).y:=integer(scale*float(y_rot)+y_shift);
end loop;
goto_xy(pointlist(1).x,pointlist(1).y);
for i in 2..npts loop
draw_to(pointlist(i).x,pointlist(i).y,black);
end loop;

In this sample, the identifiers for x,y, x_rot and y_rot are declared as integers, therefore we must
convert them to floats before multiplying by sin( ) and cos( ). When we compute x_rot and y_rot
we convert them back to integers. This creates significant truncation errors which can affect the
precision of the display as shown in the next examples,

145
ang = 0.0 ang = p/2 ang = p/4 ang=0.3
Figure 7-7: Examples of Loss of Precision in a Poorly Written Graphics Transformation Program

7.5 Applications

7.1: Write an AdaGraph program that displays a rectangular array of square boxes (black lines)
against a white background.

We will assume that the program should accept user input for the number of rows and columns of
boxes and that the boxes should be separated by a distance that is 1/10 the size (length or width)
of the boxes.

In order to make the program more user-friendly, we will require that it determine the limiting
dimension (height or width) of the graphics window and choose a boxsize to fit the array of boxes
in the window with a border (on at least three sides) equal to the box separation. This requires
that we know the graphics window size. In particular, we need to know the aspect ratio of the
graphics window. This is the ratio of the height to width. An aspect ratio of 2 means that the
graphics window is twice as high as it is wide. A knowledge of the aspect ratio tells us whether
we need to set the box size according to the number of rows or number of columns of boxes.

The user will input the number of rows nrow and number of columns ncol of boxes to be drawn
and the program will have the size of the graphics windows in number of pixels x_max and
y_max. These values can be used to compute a candidate size for a single box for the x direction
and y direction. The smaller of these two candidate values will be used as the boxsize.

delbox = boxsize/10
boxsize

nrow = # of rows of boxes


ncol = # of cols of boxes
boxsize
x_max = # pixels in x direction
y_max = # pixels in y direction
y_max

delbox
x_max

Figure 7-8: Parameters for the Array-of-Boxes Graphics Program

Let's walk through the candidate box size calculations carefully. We will compute the candidate
box size for the x direction first. We want to use all of the graphics window that we can do draw
the array of boxes. This means that,
. .
x_max = ncol boxsize + (ncol+1) boxsize/10

146
since there are ncol boxes and ncol+1 spaces between boxes which are 1/10 the size of a box.
We can rewrite this expression to give boxsize in terms of ncol and x_max.

boxsize = x_max/(ncol + (ncol + 1)/10)

We can compute the candidate boxsize for the y direction in an equivalent manner

boxsize = y_max/(nrow + (nrow +1)/10)

These expressions can be converted to Ada assignment statements

x_box:=integer(float(x_max)/(float(ncol)+(float(ncol)+1.0)/10.0));
y_box:=integer(float(y_max)/(float(nrow)+(float(nrow)+1.0)/10.0));

We must take care to deal with the data types in these calculations. All the variables are
integers, but our internal computations will use floating point to reduce truncation errors. (More on
this later.) We will choose the smaller value and set the separation between boxes to 1/10
boxsize by,

if x_box<y_box then
boxsize:=x_box;
else
boxsize:=y_box;
end if;
delbox:=boxsize/10;

Now all we need to do is to draw the array of boxes. Keep in mind that we want a space of size
delbox for the border and for the separation between boxes. The procedure Draw_Box( ) needs
the upper left and lower right corners of the box as its input parameters. A box in the ith row and
the jth column of the array of boxes will be placed at,

upper_left(x,y) = (j*delbox+(j-1)*boxsize, i*delbox+(i-1)*boxsize)


lower_right(x,y) = (j*(delbox+boxsize), i*(delbox+boxsize))

Notice that there will always be one more space (delbox) than boxes in each row and column in
the array. A complete code listed for this application is provided below.

with ada.text_io, ada.integer_text_io, adagraph;


use ada.text_io, ada.integer_text_io, adagraph;

procedure draw_box_array is
x_max, y_max : constant integer := 450;
nrow, ncol, x_box, y_box, boxsize, delbox : integer;
begin
put("Enter number of rows and number of columns... ");
get(nrow); get(ncol);
open_graph_window (x_max, y_max);
set_window_title ("Draw Box Array");
clear_window (white);
x_box:=integer(float(x_max)/(float(ncol)+(float(ncol)+1.0)/10.0));
y_box:=integer(float(y_max)/(float(nrow)+(float(nrow)+1.0)/10.0));
if x_box<y_box then
boxsize:=x_box;
else
boxsize:=y_box;
end if;
delbox:=boxsize/10;
for i in 1..nrow loop
for j in 1..ncol loop
draw_box(j*delbox+(j-1)*boxsize,i*delbox+(i-1)*boxsize,
147
j*(delbox+boxsize),i*(delbox+boxsize),black,no_fill);
end loop;
end loop;
wait_for_key;
close_graph_window;
end draw_box_array;

7.2: Write a demonstration program that transforms and draws the simple "x-box" graphical
object below, defined in the region (-3,5,7,12) to a randomly generated set of regions in the
graphics window.

We will declare a point record type to hold the x,y pairs defining the endpoints of lines used to
draw the x-box object as a polygon.

type point is record (-3,5)


x : integer;
y : integer;
end record;

type polytype is array(1..8) of point; (7,12)


object : polytype :=
((-3,5),(7,5),(7,12),(-3,12),(-3,5),(7,12),(7,5),(-3,12));
n_pts : integer := 8;

Notice that we have defined a sequence of points that can be used to draw the x-box object with
one continuous line. This means that the right-hand edge of the x-box object is drawn twice. At
first this may seem to be a wasted step but it requires fewer lines of code than specifying that the
drawing "pen" be lifted and moved to draw the second diagonal line in the x-box object.

Since we are defining the original x-box using our user-defined polytype object, we will be able to
draw other x-box objects by shifting and scaling these values. To be able to accomplish this
shifting and scaling we need to know the graphical limits of the x-box object (i.e. its minimum and
maximum horizontal and vertical extent). We will write a subprogram to compute the values of
the minimum and maximum limits of the drawing region named x_setmin, x_setmax, y_setmin,
and y_setmax for any object( ) of type polytype.

procedure find_object_minmax is
begin

x_setmin:=object(1).x;
y_setmin:=object(1).y;
x_setmax:=object(1).x;
y_setmax:=object(1).y;

for i in 2..n loop


if x_setmin>object(i).x then
x_setmin:=object(i).x;
end if;
if x_setmax<object(i).x then
x_setmax:=object(i).x;
end if;
if y_setmin>object(i).y then
y_setmin:=object(i).y;
end if;
if y_setmax<object(i).y then
y_setmax:=object(i).y;
end if;
end loop;

end find_object_minmax;

148
When this procedure is called, no parameters are passed. Likewise, no identifiers are declared in
this procedure. Instead, it directly accesses the main program variables. This is OK here for
several reasons. First of all, we are moving this code segment into a subprogram strictly for the
purpose of improving the readability of the main program. Second, this is such a specific
computational task that a more generic form of this procedure is not likely to be of any value in
another program. Finally, creating internal versions of the parameters x_setmin, y_setmin, etc,
and the object array would diminish the readability and efficiency of the program in general.

We are asked to make the program generate random graphics regions in a graphics window with
plotting limits (0,0,x_max,y_max). When generating these regions we need to maintain the
following conditions,

x_regionmin > 0
y_regionmin > 0
x_regionmax < x_max
y_regionmax < y_max
x_regionmin < x_regionmax
y_regionmin < y_regionmax

We also will limit the size of the target regions for aesthetic reasons. The package random
includes a function rnd(maxval) that returns an integer value between 0 and maxval-1. This
function can be used as part of an expression to set the edges of the target region.

procedure make_region_minmax is
begin
x_regionmin:=10+rnd(x_max-101);
y_regionmin:=10+rnd(y_max-101);
x_regionmax:=x_regionmin + 5 + rnd((x_max - x_regionmin)/2-10);
y_regionmax:=y_regionmin + 5 + rnd((y_max - y_regionmin)/2-10);
end make_region_minmax;

Each time this procedure is called a different sized target region is defined. We have arbitrarily
restricted the maximum size of the target regions and their possible placements in the graphics
windows. We can now compute the shift and scale parameters using the object set limits and the
target region limits.

x_scale := float(x_regionmax-x_regionmin)/float(x_setmax-x_setmin);
y_scale := float(y_regionmax-y_regionmin)/float(y_setmax-y_setmin);
x_shift := x_regionmin - integer(x_scale*float(x_setmin));
y_shift := y_regionmin - integer(y_scale*float(y_setmin));

The points being plotted can be transformed by,

x_plot:=integer(x_scale*float(object(i).x))+x_shift;
y_plot:=integer(y_scale*float(object(i).y))+y_shift;
draw_to(x_plot,y_plot,black);

The code segment below plots the x-box into 10 randomly generated target regions,

find_object_minmax;

for k in 1..10 loop

make_region_minmax;

x_scale := float(x_regionmax-x_regionmin)/float(x_setmax-x_setmin);
y_scale := float(y_regionmax-y_regionmin)/float(y_setmax-y_setmin);
x_shift := x_regionmin - integer(x_scale*float(x_setmin));
149
y_shift := y_regionmin - integer(y_scale*float(y_setmin));

for i in 1..n loop


x_plot:=integer(x_scale*float(object(i).x))+x_shift;
y_plot:=integer(y_scale*float(object(i).y))+y_shift;
if i=1 then
goto_xy(x_plot,y_plot);
else
draw_to(x_plot,y_plot,black);
end if;
end loop;

end loop;

A sample run of a program based on this design is shown in Figure 7-9.

Figure 7-9: Random Shift and Scale Operations Applied to the X-Box Object

7.3: Write an Ada/Adagraph program that displays an animation of a ball bouncing inside the
graphics window.

In this application, we will assume that the ball is moving in a straight line at a constant speed
until it reaches the edge of the graphics window.

In an animation, the computer displays a sequence of still images, each one with the moving
objects shown in a slightly different position. First, we will make the ball move in a straight line as
described in the pseudocode below. We define the speed in the x-direction vx (horizontal) and
the y-direction vy (vertical) in units of pixels per image. To give the impression of a moving ball,
we erase the current ball image and replace it with another ball image shifted by vx pixels
horizontally and vy pixels vertically.

-- set the horizontal and vertical velocity vx,vy


-- set initial position of ball x,y
-- draw_ball(x,y,ball_color)
-- loop until done
-- compute new position of ball
-- xnew = x + vx
-- ynew = y + vy
-- erase ball
-- draw_ball(x,y,back_color)
-- update x and y
150
-- x = xnew
-- y = ynew
-- end loop

Rather than erasing the entire image we have chosen to erase just the ball by drawing over the
current ball image with the background color back_color. We will implement this code to test our
animation and moving algorithm. For now, we will stop the program when the ball reaches the
edge of the graphics window.

with adagraph;
use adagraph;
procedure bouncing_ball is
xmax : constant integer := 600;
ymax : constant integer := 500;
vx : integer := 3;
vy : integer := -2;
x : integer := 20;
y : integer :=400;
xnew,ynew : integer;
r : integer :=20;
begin
open_graph_window(xmax,ymax);
-- the graphics window is colored white
clear_window(white);
-- a black ball of radius r is drawn at position (x,y)
draw_circle(x,y,r,black,fill);
while x < xmax-r and y>r loop
xnew := x + vx;
ynew := y + vy;
delay(0.02);
-- the ball in position (x,y) is erased
draw_circle(x,y,r,white,fill);
-- a black ball is drawn at position (xnew,ynew)
draw_circle(xnew,ynew,r,black,fill);
-- the values of x and y are updated to the new position
x := xnew;
y := ynew;
end loop;
close_graph_window;
end bouncing_ball;

We have called the delay( ) procedure to slow down the animation. The value 0.02 represents 20
milliseconds. The placement of the delay( ) procedure just before the line that erases the ball is
important. In this position, there will be a ball displayed most of the time during the animation. If
the delay( ) procedure was placed between the erase and the redraw then there would be no ball
displayed most of the time.

Now we need to make the ball bounce off the edges of the graphics window. This can best be
accomplished by changing the velocity values. When the ball of radius r reaches an edge its
center will be r pixels away from the edge. If the ball reaches the left or right edge of the graphics
window we will change the sign of the value of vx. If the ball reaches the top or bottom edge of
the graphics window we will change the sign of the value of vy.

if x >= xmax-r or x <= r then


vx := -vx;
end if;

if y >= ymax-r or y <= r then


vy := -vy;
end if;
151
At first you may be surprised that we can handle the boundary tests and bounce operations for
two edges with a single if...then statement. However, we see that when the ball is moving toward
the minimum valued edge the corresponding velocity parameter is negative. Reversing its sign
will cause future values of the position to increase. Alternatively, when the ball is moving toward
the maximum valued edge the corresponding velocity parameter is positive. Reversing its sign
will cause future values of the position to decrease.

The boundary tests and bounce operations code segment above can be placed anywhere inside
the animation loop. This is because the values of x and y always represent the current position of
the ball in the graphics window. As soon as x or y or both come within the ball radius of a
boundary the boundary test will reverse the sign of the corresponding velocity parameters.
Therefore, the next time xnew and ynew are calculated they will use the updated velocity values.
Depending on where the boundary tests are placed there will be a slight difference in the
animation but this does not affect the running of the program.

In the example run in Figure 7-10 below, we have modified the code slightly so that an outline of
the ball is left behind when the ball is erased. This was accomplished by reducing the radius in
the call to draw_circle( ) that erases the black ball.

draw_circle(x,y,r-1,black,fill);

Figure 7-10: Example Run of the Adagraph Program bouncing_ball.adb

Changing the values of vx and vy, the initial position x,y, and the radius of the ball will change the
pattern of the bouncing.

152
Exercises .

7.1 Write an Ada/AdaGraph program to draw the following shapes. Write your program so that
the scales and positions of the objects can be set by the user. The lengths of the sides of these
objects are all the same.

7.2 Modify the program from Exercise 7.1 to arrange the three objects into a "cube" and to flood-
fill adjacent regions with red, green and blue. Write a procedure called draw_cube( ) that accepts
parameters specifying the lower left-hand corner of the cube (x,y) and the length of the one side
of the cube (length) that can be called from the main program.

7.3 Use the draw_cube( ) procedure from Exercise 7.2 in Ada/AdaGraph procedures to draw the
figures below, called draw_cross( ) and draw_cubestack( ). These procedures should accept
parameters specifying a reference point for the drawing object's position (x,y) and a size
parameter (size).

7.4 Write an Ada/AdaGraph program to simulate a bouncing ball as in Application 7.3 but with the
inclusion of gravity. Hint: Adding a small negative value to the vertical velocity inside the
animation loop can simulate gravity. You may also consider including some friction loss when the
ball hits the bottom boundary of the graphics window. This can be simulated by multiplying the
vertical velocity by some value less than 1.0 inside the boundary check if..then statement.

with friction without friction


7.5 Write an Ada/AdaGraph program to draw a plot of the data in a one-dimensional array. The X
axis corresponds to the array index, and the height (Y axis) of the curve is proportional to the
value in the array at the corresponding X index.

153
7.6 Write an Ada/AdaGraph program to draw an orthographic plot of the data in a two-
dimensional array in which the row and column index determine the X and Y, and the height of
the curve (Z axis) is proportional to the value in the table at the corresponding row, column index.
Hint: Extend the program written in Exercise 7.4 by shifting each plot down and to the left by an
amount proportional to the row index.

column index

row index
orthographic plot

7.6 How might the program described in Exercise 7.5 be modified to mask hidden lines from view.
Hint: A solution can be implemented using the flood_fill( ) procedure provided in AdaGraph.

154
Chapter 8 - Using and Creating Packages

8.1 Adagraph Package


Color Type
Mouse Event Handler
Special Keys - Extended Character Codes
Graphics Window Management
Drawing and Display Operations

8.2 NT_Console Package

8.3 Math Package

8.4 Creating Packages


Package Specification
Package Body
Private Type
LImited Private Type

8.5 Generics
Creating a Generic
Instantiation

8.6 Applications
Displaying the Logistic Map
Building a Package for Complex Numbers
Displaying the Mandelbrot Set

155
Chapter 8 - Using and Creating Packages
In this chapter, we will review the contents of a number of predefined packages used in Ada. We
will learn how to create and use our own packages. We will learn about generics, which are
general-purpose packages that can be implemented for use on multiple data types.

A package is made of functions and procedures with a related purpose that support each other
and are associated under a collective name. Packages are used to extend the functionality of a
language. Packages are defined with a specification and a body. The specification source code
is made available to the programmer and contains information necessary for the proper use of the
elements of the package. The package specification is a list of the first lines of each function and
procedure showing the input/output parameters and return types. The package body is not
generally available since it contains the complete function and procedure descriptions, which are
usually considered proprietary to the package creator.

In order to use a package, you will need compiled versions of the specification and the body. We
will review a number of packages provided through the GNU Software License Agreement (see
GNU Copyleft).

8.1 Adagraph Package

In the previous chapter, we used the AdaGraph package for our 2-D graphics applications.
Adagraph was written by Jerry Van Dijk and is available free for non-commercial use. The
complete source code for the Adagraph specification is included in Appendix B. We will review
some of the main features of Adagraph in this section.
Color_Type
The AdaGraph package declares an enumerated type giving names to the 16 colors used in the
display functions and procedures. These colors correspond to the numbers 0, 1, . . ., 15. These
literal values can be used wherever a color_type identifier is specified.
These values are not arbitrary. When viewed as binary numbers, the numeric values for the
colors indicate which of the three primary colors are used with the MSB (most significant bit)
indicating whether these colors are displayed at full or half brightness.

# Level Red Green Blue Name .


0 0 0 0 0 Black
1 0 0 0 1 Blue
2 0 0 1 0 Green
3 0 0 1 1 Cyan
4 0 1 0 0 Red
5 0 1 0 1 Magenta
6 0 1 1 0 Brown (dim yellow)
7 0 1 1 1 Light_Gray
8 1 0 0 0 Dark_Gray
9 1 0 0 1 Light_Blue
10 1 0 1 0 Light_Green
11 1 0 1 1 Light_Cyan
12 1 1 0 0 Light_Red
13 1 1 0 1 Light_Magenta
14 1 1 1 0 Yellow
15 1 1 1 1 White

Figure 8-1: Relationship Between Color_Type value and Name of Colors in Adagraph

156
type Color_Type is (Black, Blue, Green, Cyan, Red, Magenta,
Brown, Light_Gray, Dark_Gray, Light_Blue,
Light_Green, Light_Cyan, Light_Red,
Light_Magenta, Yellow, White);

Another enumerated type used in AdaGraph is the shape fill parameter.

type Fill_Type is (No_Fill, Fill);

The Fill_Type is used in draw_circle( ), draw_box( ), and draw_ellipse( ) to indicate whether the
shape should be drawn as a filled or outline shape.

Mouse Event Handler

A useful user interactive feature provided in AdaGraph is the mouse event handler. Mouse
events are handled with a Boolean function Mouse_event, which returns TRUE if a new mouse
event has occurred, and a procedure Get_Mouse( ) that loads a record with the event_type and
the mouse position at the time of the mouse event. The type declarations for event_type and
mouse_type are shown below.

type Event_Type is (None, Moved, Left_Up,


Left_Down, Right_Up, Right_Down);
type Mouse_Type is
record
Event : Event_Type;
X_Pos : Integer;
Y_Pos : Integer;
end record;

The code segment below is a partially completed sample of how a mouse event handler can be
implemented.

--main event loop


while not(done) loop
if Mouse_Event then
Mouse := Get_Mouse;
-- code before mouse event is handled goes here
case Mouse.Event is
when None => null;
when Moved => X := Mouse.X_Pos;
Y := Mouse.Y_Pos;
when Left_Up => -- mouse event code segment goes here
when Right_Up => -- mouse event code segment goes here
when Left_Down => -- mouse event code segment goes here
when Right_Down => -- mouse event code segment goes here
end case;
-- code after mouse event is handled goes here
end if;
end loop;

This if..then construct must be placed in an active loop so that it will be tested regularly. Since
the case statement is dealing with an enumerated type it does not need a when others option. If
a mouse event has occurred then the case statement checks to see what type of event it was. If
the mouse has been moved then its position is updated. The rate at which the mouse movement
is updated depends on the speed of the main event loop. For this reason, you will want to
minimize the complexity of the operations being executed in this loop.

157
Special Keys - Extended Character Codes
AdaGraph defines the extended character codes for those keystrokes that generate ASCII values
greater than 127. These keystrokes are given special names as listed below.
Vk_Nul : constant Character := Character'Val (16#00#);
Vk_Prior : constant Character := Character'Val (16#21#);
Vk_Next : constant Character := Character'Val (16#22#);
Vk_End : constant Character := Character'Val (16#23#);
Vk_Home : constant Character := Character'Val (16#24#);
Vk_Left : constant Character := Character'Val (16#25#);
Vk_Up : constant Character := Character'Val (16#26#);
Vk_Right : constant Character := Character'Val (16#27#);
Vk_Down : constant Character := Character'Val (16#28#);
Vk_Insert : constant Character := Character'Val (16#2D#);
Vk_Delete : constant Character := Character'Val (16#2E#);
Extended character codes should be referred to by these names in your Ada programs. For
example, if you wanted the user to be able to change the x,y position of a symbol on the graphics
screen you could test the arrow keys.

done:=false;
while not(done) loop
case key is
when vk_up => y:=y-1;
when vk_down => y:=y+1;
when vk_left => x:=x-1;
when vk_right => x:=x+1;
when 'x'|'X' => done:=true;
when others => null;
end case;
if x<x_min then x:=x_min; end if;
if x>x_max then x:=x_max; end if;
if y<y_min then y:=y_min; end if;
if y>y_max then y:=y_max; end if;
update_display(x,y);
end loop;

The sequence of if..then statements makes sure that the position stays inside some display
region (x_min,y_min,x_max,y_max).

Graphics Window Management

AdaGraph displays graphics in a separate window from the console (e.g. MS DOS console
window) that is opened at program start. To open a graphics window, the program must call one
of the following procedures.

procedure Create_Graph_Window (X_Max, Y_Max : out Integer;


X_Char, Y_Char : out Integer);

procedure Ext_Create_Graph_Window (X_Max, Y_Max : out Integer;


X_Char, Y_Char : out Integer);

procedure Create_Sized_Graph_Window (X_Size, Y_Size : in Integer;


X_Max, Y_Max : out Integer;
X_Char, Y_Char : out Integer);

In the first two procedures, the size of the window is set by the procedure itself. Notice that the
parameters are out parameters. After the graphics window is created, the value of x_max and
y_max can be tested to determine its size. The third procedure, create_sized_graph_window( ,)
allows the programmer to specify the size of the window. The parameters x_char and y_char
indicate the width and height of characters drawn using display_text( ).
158
AdaGraph includes a Boolean function is_open that returns TRUE if a graphics window is
currently open.

function Is_Open return Boolean;

Using the procedure get_max_size( ) you can test the configuration of the computer monitor to
determine the maximum size of a graphics window to fit the display. This can be done before the
window is created and it can be sized using create_sized_graph_window( ).

procedure Get_Max_Size (X_Size, Y_Size : out Integer);

This is a very useful feature in programs that are anticipated to be used on a variety of computer
systems. The values of x_size and y_size can also be used to set the scale for the graphics
being displayed.

Using set_window_title( ), the program can label the graphics window. This title appears in the
blue bar at the top of the window frame, not inside the drawing region itself.

procedure Set_Window_Title (Title : in String);

When the program is finished with the currently open graphics window it can remove it with the
destroy_graph_window procedure.

procedure Destroy_Graph_Window;

When a graphics window is open the user can switch the focus between the text console and
graphics window by clicking on the desired widow frame. The programmer can use this feature to
display messages and request data from the user in the text console while the graphics window is
open.

Drawing and Display Operations

The following functions and procedures are provided for graphics operations that affect the
drawing region of the graphics window. The procedure clear_window( ) replaces the current
graphical display with a blank region of a specified color.

procedure Clear_Window (Hue : in Color_Type := Black);

The simplest graphics object is a single pixel. The function get_pixel(x,y) return the color of the
specified pixel at point (x,y) in the graphics window. The return type is color_type. The
procedure put_pixel(x,y,hue) sets the specified pixel to the color hue.

function Get_Pixel (X, Y : in Integer) return Color_Type;

procedure Put_Pixel (X, Y : in Integer; Hue : in Color_Type:=White);

The most common drawing primitive is the line segment. The procedure draw_line( ) draws a line
of the specified color between two points (x1,y1) and (x2,y2) in the graphics window. The default
color (i.e. when no color is given is white).

procedure Draw_Line (X1, Y1, X2, Y2 : in Integer;


Hue : in Color_Type := White);

The procedure draw_box( ) draws a rectangular box (oriented with its sides parallel to the edges
of the graphics window) with its upper left corner at point (x1,y1) and its lower right corner at
(x2,y2). The color of the line can be set by the program (default is white) and the box can be
specified as fill (solid color) or no_fill (outline only). The default when fill_type is not given is
no_fill.

159
procedure Draw_Box (X1, Y1, X2, Y2 : in Integer;
Hue : in Color_Type := White;
Filled : in Fill_Type := No_Fill);

Circles can be drawn using the draw_circle( ) procedure. The position (x,y) of the center of the
circle and its radius are input through the parameter list along with the color_type and fill_type.
These parameters have the same default values as those for draw_box( ) (i.e. white and no_fill).

procedure Draw_Circle (X, Y, Radius : in Integer;


Hue : in Color_Type := White;
Filled : in Fill_Type := No_Fill);

The parameter list of the draw_ellipse( ) procedure is the same as that of draw_box( ). This
procedure draws an ellipse inscribed in the rectangle defined by its upper left (x1,y1) and lower
right (x2,y2) corners. Again the color_type and fill_type have default values white and no_fill.

procedure Draw_Ellipse (X1, Y1, X2, Y2 : in Integer;


Hue : in Color_Type := White;
Filled : in Fill_Type := No_Fill);

The procedure flood_fill( ) is used to fill any closed region with a specified color. The starting
position (x,y) for the flood_fill and the color to be used are input parameters. The flood_fill( )
procedure operates in the following manner.

procedure Flood_Fill (X, Y : in Integer; Hue : in Color_Type:= White);

flood fill starting position fill color

Figure 8-2: Example of the Operation of the Adagraph Flood_Fill( ) Procedure

The color of the starting pixel is tested. The starting pixel is the first pixel whose color is changed
to the fill color. Every pixel of the same (original) color that is sharing an edge with a pixel that
has had its color changed to the flood fill color is also changed. In the example above notice that
pixels at the upper left and upper right boundaries of the region being flooded touch the corners
of pixels of the same color outside the boundary. But since they do not share an edge with the
pixels inside the boundary the flood fill does not pass through. However, the flood fill does
continue through the one pixel wide gap in the center of the region since those pixels share an
edge. The island of pixels in the right half of the region are not affected since their color is
different from the original color of the starting pixel.

Text strings can be drawn in a graphics window using the display_text( ) procedure. The starting
position (x,y) of the lower left corner of the string is specified followed by the string to be
displayed. The color of the characters can also be given (default color is white). The width and
height of each character are given by x_char and y_char, which are out parameters of the
create_graph_window( ) procedures (see prior code). These values are useful in computing the
proper position of the text string in the graphics window.

160
procedure Display_Text (X, Y : in Integer;
Text : in String;
Hue : in Color_Type := White);

When drawing operations are executed the drawing point remains at the position it was at the end
of the last operation. This position can be determined using the functions where_x and where_y.
These functions return the horizontal (x) and vertical (y) position of the drawing point as integers.

function Where_X return Integer;


function Where_Y return Integer;

The (x,y) drawing point can be set using the procedure goto_xy(x,y). This procedure moves the
drawing point to the specified location without drawing any lines or changing the color of any
pixels. The procedure draw_to( ) draws a line segment from the current drawing point to a
specified (x,y) point of a specified color.

procedure Goto_XY (X, Y : in Integer);


procedure Draw_To (X, Y : in Integer; Hue : in Color_Type := White);

These two procedures are used together to support polydraw operations. We used these
procedures to draw the geometric model of the house in the previous chapter.

8.2 NT_Console Package

Before there was a Graphical User Interface (GUI) such as Windows_98, Windows_2000, or the
Macintosh OS, all computer applications were text based. This did not mean that program
displays were limited to white text scrolling up from the bottom of a black screen. The
programmer could choose from a variety of display (background) colors and character
(foreground) colors and could control the placement of text on the screen. There is a standard for
such text-based interfaces called the ANSI Textual Interface. Even now, many business
applications use the ANSI Textual Interface.

The NT_console package written by Jerry Van Dijk provides functions and procedures to
implement the ANSI Textual Interface. These tools support cursor position detection and
placement, foreground and background color control, and a mechanism for detecting and
decoding standard ALT, CTRL, and CTRL-ALT keystrokes.

The ANSI Textual Interface offers 80 characters per line and 25 lines on the display. The x_pos
and y_pos have ranges of 0..79 and 0..24 respectively.

subtype X_Pos is Natural range 0 .. 79;


subtype Y_Pos is Natural range 0 .. 24;

The color_type in NT_Console is missing the light_gray color provided in the AdaGraph package.
All other colors are available and have the same name as those defined in AdaGraph.

type Color_Type is (Black, Blue, Green, Cyan, Red, Magenta, Brown,


Gray, Light_Blue, Light_Green, Light_Cyan,
Light_Red, Light_Magenta, Yellow, White);

The NT_Console package defines a much wider range of extended PC keystrokes than those
provided in AdaGraph. Essentially, every keystroke with ALT, CTRL, and CTRL-ALT are
accounted for. See Appendix C for a complete listing of the NT_Console package including a list
of the extended PC keystrokes handled by this package.

This package also supports cursor controls to test and set the screen position for the next text
character being displayed. The functions where_x and where_y return x_pos and y_pos which
are the column and line of the next character to be printed.

161
function Where_X return X_Pos;
function Where_Y return Y_Pos;

The goto_xy( ) procedure permits us to set the position of the cursor on the display.

procedure Goto_XY (X : in X_Pos := X_Pos'First;


Y : in Y_Pos := Y_Pos'First);

Each character (80 columns by 25 lines) position can have its own background and foreground
colors. The background must be one of the first eight defined in the color_type declaration. The
background is the space around the character. The foreground color (i.e. the color of the
character itself) can be any color defined in the color_type. The background and foreground color
of the portion of the screen defined by the current cursor position can be obtained using the
get_foreground and get_background functions.

function Get_Foreground return Color_Type;


function Get_Background return Color_Type;

The procedures set_foreground( ) and set_background( ) are used to set the foreground and
background colors of the current cursor position. The default foreground and background colors
are gray and black respectively.

procedure Set_Foreground (Color : in Color_Type := Gray);


procedure Set_Background (Color : in Color_Type := Black);

The screen can be erased to a specified background color using the clear_screen( ) procedure.
The default color for clear_screen( ) is black. Note that this does not alter the foreground or
background color set for any character position. So the next time a character is displayed at a
position, the foreground and background colors will be the same as the last time a character was
displayed in this position.

procedure Clear_Screen (Color : in Color_Type := Black);

The NT_Console package includes a legacy from the times when computer interfaces used
teletype machines (these are special kinds of automatic typewrites remotely operated over
telephone lines). In those days, a bell was used to alert the operator that a message was coming
in. When teletypes began to be used as computer interfaces, programmers naturally made use
of this bell. The internal beep on your desktop computer can be invoked using the same special
ASCII character defined for the teletype bell decades ago.

The NT_Console package includes a procedure called bleep that beeps the computer speaker.

procedure Bleep;

There is, included, a function called get_key that is similar to the one found in the adagraph
package. It gets the next character in the input buffer. If the buffer is empty, get_key forces the
computer to wait for the user to press a key before continuing.
function Get_Key return Character;

The NT-Console also provides a boolean function called key_available to test is a key has been
pressed.

function Key_Available return Boolean;

This function can be used to permit a program event-loop to continue to execute until the user
interrupts with a key press.

loop
if key_available then
key:=get_key;
162
case key is
-- case statement handles key press events
end case;
end if;
-- other event loop stuff
end loop;

The NT-console package provides the programmer with a reasonable alternative to a graphical
interface for some applications.

8.3 Math Package

The GNAT Ada compiler includes a math package called ada.numerics_elementary.functions.


This package is an instantiation for single precision floating numbers. The functions included in
this package are listed below:

sqrt(x)- returns the square root of the argument x


log(x) - returns the natural logarithm of the argument x
log(base,x) - returns the logarithm of the argument x in the specified base
x
exp(x) - returns the (base e) exponent of the argument x (i.e. returns e ).
x**y - returns the value of x raised to the y power (both x and y are float types)
sin(x) - returns the sine of the argument x where x is in units of radians
sin(x,cycle) - returns the sine of the argument x where the value cycle represents the
units of one full cycle of the argument angle (if you want the argument to be in units of
degrees, set cycle=360).
cos(x) - returns the cosine of the argument x where x is in units of radians
cos(x,cycle) - returns the cosine of the argument x where the value cycle represents
the units of one full cycle of the argument angle (if you want the argument to be in units
of degrees, set cycle=360).
tan(x) - returns the tangent of the argument x where x is in units of radians
tan(x,cycle) - returns the tangent of the argument x where the value cycle represents
the units of one full cycle of the argument angle (if you want the argument to be in units
of degrees, set cycle=360).
cot(x) - returns the cotangent of the argument x where x is in units of radians
cot(x,cycle) - returns the cotangent of the argument x where the value cycle
represents the units of one full cycle of the argument angle (if you want the argument to
be in units of degrees, set cycle=360).
arcsin(x) - returns the angle (in radians) whose sine is x. The range is –π/2.0 to π/2.0
arcsin(x,cycle) - returns the angle whose sine is x. The range is –Cycle/4.0 to
Cycle/4.0.
arccos(x) - returns the angle (in radians) whose cosine is x. The range is 0 to π
arccos(x,cycle) - returns the angle whose cosine is x. The range is 0 to Cycle/2.0.
arctan(y[,x]) - returns the angle whose tangent is y/x or y/1.0 when x is omitted. The
range is -π to p
arctan(y[,x],cycle) - returns the angle whose tangent is y/x or y/1.0 when x is
omitted. The range is -cycle/2.0 to cycle/2.0.
arccot(x[,y]) - returns the angle whose cotangent is x/y or x/1.0 when y is omitted.
The range is –π to π
arccot(x[,y],cycle) - returns the angle whose cotangent is x/y or x/1.0 when y is
omitted. The range is –Cycle/2.0 to Cycle/2.0.
sinh(x) - returns the hyperbolic sine of the argument x
cosh(x) - returns the hyperbolic cosine of the argument x
163
tanh(x) - returns the hyperbolic tangent of the argument x
coth(x) - returns the hyperbolic cotangent of the argument x
arcsinh(x) - returns the inverse hyperbolic sine of the argument x
arccosh(x) - returns the inverse hyperbolic cosine of the argument x
arctanh(x) - returns the inverse hyperbolic tangent of the argument x
arccoth(x) - returns the inverse hyperbolic cotangent of the argument x

These functions match their mathematical counterparts as closely as possible. However, there
are a number of differences forced on us by the finite precision of the representation of single
precision floating-point numbers in the computer. The Ada Language Reference Manual (look at
Language RM under the Help menu of the Ada_GIDE) includes the following comments about the
elementary_functions package:

The computed results of the mathematically multi-valued functions are rendered single-valued by
the following conventions, which are meant to imply the principal branch:

• The results of the Sqrt and Arccosh functions and that of the exponentiation operator are
nonnegative.

• The result of the Arcsin function is in the quadrant containing the point (1.0, x), where x is the
value of the parameter X. This quadrant is I or IV; thus, the range of the Arcsin function is
approximately –π/2.0 to π/2.0 (–Cycle/4.0 to Cycle/4.0, if the parameter Cycle is specified).

• The result of the Arccos function is in the quadrant containing the point (x, 1.0), where x is the
value of the parameter X. This quadrant is I or II; thus, the Arccos function ranges from 0.0 to
approximately π (Cycle/2.0, if the parameter Cycle is specified).

• The results of the Arctan and Arccot functions are in the quadrant containing the point (x, y),
where x and y are the values of the parameters X and Y, respectively. This may be any
quadrant (I through IV) when the parameter X (resp., Y) of Arctan (resp., Arccot) is specified,
but it is restricted to quadrants I and IV (resp., I and II) when that parameter is omitted. Thus,
the range when that parameter is specified is approximately –π to π (–Cycle/2.0 to Cycle/2.0,
if the parameter Cycle is specified); when omitted, the range of Arctan (resp., Arccot) is that
of Arcsin (resp., Arccos), as given above. When the point (x, y) lies on the negative x-axis,
the result approximates:

π (resp., –π) when the sign of the parameter Y is positive (resp., negative), if
Float_Type’Signed_Zeros is True;

π, if Float_Type’Signed_Zeros is False.

(In the case of the inverse trigonometric functions, in which a result lying on or near one of the
axes may not be exactly present, the approximation inherent in computing the result may place it
1
in an adjacent quadrant, close to but on the wrong side of the axis.)

8.4 Creating Packages

The packages we have studied up to now are part of the functionality of the GNAT_Ada compiler
and supporting tools. We can build new packages that can be used by listing them in the with
and use commands in our programs. In this section, we will learn how to create our own
packages and use them to extend the functionality of the Ada language.

Packages are collections of subprograms (functions and procedures) that are compiled
separately from our main program. A package has two parts: (1) The package specification,
1
from: Ada LRM, A.5.1 Elementary Functions, paragraphs 10 through 18.

164
which is provided to show how to access the functions and procedures; (2) The package body in
which the operations of the functions and procedures are defined.

Package Specification

A simple package specification is shown below. It describes how to access and use the
subprograms by providing the first line of each function and procedure. Everything you need to
know to access a function or procedure is given in this line.

package triangle_stuff is
function is_a_triangle(a,b,c : float) return boolean;
function triangle_area(a,b,c : float) return float;
procedure get_sides(a,b,c : out float);
procedure order_sides(a,b,c : in out float);
end triangle_stuff;

This package specification is saved under the file name triangle_stuff.ads. We use the .ads
extension to indicate that this is a specification rather than a program or package body. Typically,
specifications will include a comment block explaining the purpose for each function and
procedure. The package specification is compiled before the package body. When a
specification is compiled without errors using GNAT, a message like the following in generated.

Compiling...
No code generated for file triangle_stuff.ads (package spec)
Completed.

Note that in the specification the reserved is at the end of each line is replaced with a semicolon.
This is because we are not providing the body of the function or procedure here, just the
interface.

When creating your own package you will probably Save_As your specification file as a package
body and then modify it with the function and procedure declarations as shown below.

Package Body

The package body for the specification above would be named triangle_stuff.adb and would
appear similar to the following code:

with ada.text_io,ada.float_text_io,ada.numerics.elementary_functions;
use ada.text_io,ada.float_text_io,ada.numerics.elementary_functions;

package body triangle_stuff is

function is_a_triangle(a,b,c : float) return boolean is


begin
return (a+b>c and a+c>b and b+c>a);
end is_a_triangle;

function triangle_area(a,b,c : float) return float is


s : float;
begin
s:=(a+b+c)/2.0;
return sqrt(s*(s-a)*(s-b)*(s-c));
end triangle_area;

procedure get_sides(a,b,c : out float) is


begin
new_line;
put("Enter side a... "); get(a);
new_line;
165
put("Enter side b... "); get(b);
new_line;
put("Enter side c... "); get(c);
new_line;
end get_sides;

procedure order_sides(a,b,c : in out float) is


procedure swap(x,y: in out float) is
tmp : float;
begin
tmp:=x;
x:=y;
y:=tmp;
end swap;
begin
if a>b then swap(a,b); end if;
if a>c then swap(a,c); end if;
if b>c then swap(b,c); end if;
end order_sides;

end triangle_stuff;

Notice that the body of each function and procedure listed in the specification appears in the
package body and in the same order as they appear in the specification. The semicolon has
been replaced by an is, and the complete description of the subprogram is provided.

Since some of the subprograms needs functions and/or procedure in other packages, they are
included in the with and use statements. The get( ) for floating point values and the put( ) for
strings are used in the get_sides( ) procedure. The sqrt( ) function from the math package
ada.numerics.elementary_functions is used in the triangle_area( ) function.

These subprograms can include their own subprograms that are not listed in the specification.
Internal subprograms such as the swap( ) procedure included in order_sides( ) are not available
to the package user. When the package body is compiled without errors the following message is
generated.

Compiling...
Completed.

So far we have generated a lot of code but none of it can be executed in the package. To make
use of the subprograms, we need to write a main program that accesses the package. For

with ada.text_io, ada.float_text_io, triangle_stuff;


use ada.text_io, ada.float_text_io, triangle_stuff;
procedure package_demo is
a,b,c : float;
begin
get_sides(a,b,c);
if is_a_triangle(a,b,c) then
put("The area of this triangle is... ");
put(triangle_area(a,b,c),0,4,0);
new_line;
else
order_sides(a,b,c);
put("These sides can't make a triangle because ");
put(a,0,2,0); put(" + "); put(b,0,2,0);
put(" is less than "); put(c,0,2,0);
new_line;
end if;
end package_demo;

166
In this sample program, we have called the packages ada.text_io, ada.float_text_io, and
triangle_stuff. We have used the first two packages in other programs. They contain functions
and procedures for input and output of text strings and floating point numbers. Our new package,
triangle_stuff, contains the functions and procedures for dealing with triangles.

The get_sides( ) procedure is called to prompt the user to enter the three sides of a triangle, then
these sides are tested with is_a_triangle( ) to verify that a triangle can be formed using sides of
these lengths. If it is possible to make a triangle with sides of these lengths, the triangle's area is
computed using the triangle_area( ) function. If it is not possible, a message is displayed
explaining the problem. Note that the order_sides( ) procedure is used to arrange the values of
the triangle into increasing lengths so that a message can be displayed stating that the sum of the
two shorter sides is less than the longest side.

Why Packages?

Let's summarize the situation before we move on to the next topic. There are many ways to
implement a program to solve a particular problem. For this problem, we could have written a
single main program to produce the same result that did not use any subprograms. Alternatively,
we could have placed the subprograms in the declaration block of our program rather than in a
separate package. So why go to the trouble of building subprograms and putting them into
separate compilation units called packages? For the new programmer, this is a question that
needs to be answered.

Except for the learning experience, the extra work of building a package for a small sample
problem is not justified. The value of packages is realized when we begin to reuse our code.
Consider the ada.text_io, ada.float_text_io, and ada.integer_text_io packages for example. We
use one or more of these packages in nearly every program we write. It would be tedious and
time consuming to cut-and-paste the functions and procedures from these packages into the
declaration blocks of each program we write. Instead we just make reference to them using the
with and use commands.

Private Type

We can restrict access to structured data types by using the private type construct. This means
that the package user (i.e. a programmer that is using our package) can declare variables of this
type, but they cannot manipulate the internal components of the type directly. At first, this may
seem odd but we must remember that we may be building a package for use by another
programmer as part of the software development team. We may want to permit access to our
structured data types only through the functions and procedures we write. This can reduce the
chance that our data type is used incorrectly.

package sample is
type grinixal is private;
function some_function( whatever : grinixal) return boolean;
procedure some_procedure (a,b : in float; c,d : out grinixal);
procedure get(a : out grinixal);
procedure put(a: in grinixal);
private
type grinixal is record
x : float;
y : float;
n : integer;
c : character;
end record;
end sample;

In the sample package specification above, a data type called grinixal is designated as private.
This data type is fully specified following the specifications for the functions and procedures of the

167
package. The components of grinixal are defined in this private section and can be accessed
only by the functions and procedures in this package. The user of this package,
(1) may declare variables or include parameters in subprograms of a private type,
(2) may test two variables of the private type for equality or inequality,
(3) may use the functions and procedures of this package to get and put the private data type
(4) may not directly access the components of the private type

Limited Private Type

Sometimes we want to create and use a private data type but not give access to this type by the
package user in any way. For example, we may want to store information that can be viewed or
changed only by our own functions and procedures. Ada gives us a way to do this with the
limited private type. We will make use of the limited private type when we begin building abstract
data types in Part II of this text.

8.5 Generics

The main reason we build packages is to reduce the amount of work required to reuse our source
code. When we can make use of the same subprograms in more than one main program, we
can implement them in a separate compilation unit called a package. This works well when the
subprograms can be reused without modification, but when any changes are necessary we have
to modify and then recompile the package specification and body. Sometimes we can predict
how a package might need to be modified for use in another program. For example, consider the
procedure swap( ) used to sort items in a list. (see Section 5.4)

procedure swap(a,b : in out integer) is


tmp : integer;
begin
tmp:=a;
a:=b;
b:=tmp;
end swap;

This procedure is used in a nested loop to exchange or "swap" two values in a list of integers that
are out of order. If we wanted to sort a list S of n integers into ascending order we could use the
following code segment,

for i in 1..n-1 loop


for j in i+1..n loop
if S(i)>S(j) then
swap(S(i),S(j));
end if;
end loop;
end loop;

Now, if we wanted to sort a list T( ) of floating point values instead of integers, we would need to
create a different swap( ) procedure

procedure swap(a,b : in out float) is


tmp : float;
begin
tmp:=a;
a:=b;
b:=tmp;
end swap;

The only difference between these two swap procedures is the type of parameters a and b and
the type of the internal variable tmp. Ada gives us a way to create a general form of the swap( )

168
procedure in which the variable types are left unspecified until we are ready to use the procedure.
This is done using a compilation unit called a generic.

generic
type data_type is private;
procedure swap(a,b : in out data_type);

This unit should be saved as swap.ads (corresponding to a package specification) since it does
not include the body of the procedure swap( ). Next, we create the generic swap( ) procedure
using the private type data_type for a, b, and tmp.

procedure swap(a,b : in out data_type) is


tmp : data_type;
begin
tmp:=a;
a:=b;
b:=tmp;
end swap;

Instantiation

It is important to notice that this is not just an ordinary procedure. It is a procedure template since
the type of the parameters being passed into and out of the procedure have not been specified.
Now we can utilize this generic swap( ) procedure by calling it in another program such as,

with ada.text_io, ada.integer_text_io, swap;


use ada.text_io, ada.integer_text_io;

procedure demo_generic_swap is
procedure swap_int is new swap(integer); -- instantiation of the
-- swap( ) procedure
type listype is array(1..10) of integer; -- for integers
S : listype;
n : integer := 0;
val : integer;
begin
loop
exit when n>=10;
put("Enter next integer or (-999) to quit... ");
get(val);
exit when val=-999;
n:=n+1;
S(n):=val;
end loop;
new_line;
for i in 1..n-1 loop -- The Sort using the swap_int( )
for j in i+1..n loop
if S(i)>S(j) then
swap_int(S(i),S(j));
end if;
end loop;
end loop;
for i in 1..n loop
put(S(i),5);
new_line;
end loop;
end demo_generic_swap;

Note the use of is new to define or instantiate a swap( ) procedure for integers. The generic unit
swap is withed but is not used since we must specify an instantiation of swap for the type of
169
values being swapped. We could create a swap_float( ) or a swap_char( ) procedure using the
same generic unit. Obviously, the use of a generic unit can save a lot of recoding when similar
programs are used in multiple applications.

This is an example of the advantage of object-oriented programming (OOP) over simple


structured programming, since OOP supports the concepts of templates and generics as an
integral part of its implementation.

8.6 Applications

8.1: Use adagraph to display the output of the iterated function called the Logistic Map.

When you iterate a function, you calculate a series of function values in the following way. Start
with some acceptable initial input value for the function. Compute the function's value, and then
use this computed value as the input value for the next calculation. Repeat this process using the
function's previous output as the input for the next calculation.

A simple example: you can use a hand calculator to iterate the cosine function. Set the calculator
to units of radians and enter any initial value. Repeatedly, take the cosine of the result and note
that, regardless of the initial value, you will eventually reach a value that is approximately
0.73908. This is called the fixed point of the iteration.

Sometimes an iterated function does not approach a fixed point but can generate a periodic
sequence (i.e. a list of repeating values). There would be little interest in this topic if fixed point
and periodic sequences were the only behaviors for iterated functions. There is another
possibility. Under certain conditions an iterated function can generate an apparently random
sequence of values that does not approach a fixed point and is not periodic. This so-called
chaotic behavior is what makes iterated systems interesting and is the basis for two areas of
experimental mathematics called chaos theory and fractal geometry.

Consider the function below called the Logistic Map.

f ( x) = µ x(1 − x)

where µ is a fixed value between 0 and 4. The variable x is initially set to some value in between
0.0 and 1.0. The computed value of f(x) is used as the value of x in the next calculation. This
process is repeated and the behavior of the iterated function is observed.

For example, let µ=3.0 and set the initial value of x to 0.5.

fo(x) = f(0.5) = 3.0 (0.5)(1 - 0.5) = 0.75


f1(x) = f(0.75) = 3.0 (0.75)(1 - 0.75) = 0.5625
f2(x) = f(0.5625) = 3.0 (0.5625)(1 - 0.5625) = 0.73828125
n
The superscript notation f (x) represents function iteration rather than raising the function f(x) to
the power n. When there is a possibility of confusion the superscript representing iteration is
[n]
placed in square brackets f (x).

The pseudocode below outlines the method for iterating any function and will be used as a model
for our logistic map program.

-- initialize parameters
-- set initial value of x
-- loop
-- calculate f(x)
-- display results
-- assign x the value of f(x)
-- end loop
170
We will let the value of µ be constant in our program and change its value by modifying the code.
Since the domain and range of the logistic map is (0.0..1.0) we will use a scale factor to multiply
by f(x) when plotting the values in the graphics window. The graphics window will be set to
500x200 and the logistic map will be plotted for the first 480 iterations. This should be sufficient
to demonstrate the behavior of the function for various settings of the parameters µ and x.
Finally, we will draw the axes of the graph of f(x) before entering the iteration loop.

with ada.text_io, ada.float_text_io, adagraph;


use ada.text_io, ada.float_text_io, adagraph;

procedure logistic_map is
m : constant float := 3.999;
scale : constant float := 180.0;
x,fx : float;
begin
put("Enter initial value... ");
get(x);
open_graph_window(500,200);
clear_window(white);
draw_line(10,10,10,390,black);
draw_line(5,10,490,10,black);
for i in 1..480 loop
fx:=m*x*(1.0-x);
x:=fx;
if i=1 then
goto_xy(i+10,10+integer(fx*scale));
else
draw_to(i+10,10+integer(fx*scale),black);
end if;
end loop;
wait_for_key;
close_graph_window;
end logistic_map;

µ = 2.6

µ = 3.6

µ = 3.2 µ = 3.99

Figure 8-3: Charts Showing the Possible Behaviors of the Logistic Map

171
8.2: Build a package to support arithmetic operations on complex numbers. This package should
include the operations of addition, subtraction, multiplication, and division as well as a mechanism
to get( ) and put( ) complex numbers.

As you may recall, a complex number has a real part and an imaginary part. The real part
corresponds to the real number line or the horizontal axis of the complex plane. The imaginary
part corresponds to the vertical axis of the complex plane.

imaginary

c = a+ib
b

e
tud
gn
ma real
a

Complex Plane

Figure 8-4: Complex Number a+ib Shown in the Complex Plane

Complex numbers can be represented as data pairs or as the sum of the real part and the
imaginary part such as a+ib where a is the real part and ib is the imaginary part. In this
representation i is the square root of negative one. Please keep in mind that the real and
2
imaginary parts are never combined and that i = -1. Therefore, if p and q are complex numbers
with real and imaginary components as shown below,

p = a + ib and q = c + id

then the arithmetic operations of +, -, * and / are given by,

p + q = (a + c ) + i (b + d )
p − q = (a − c ) + i (b − d )
p ∗ q = (a ∗ c − b ∗ d ) + i (a ∗ d + c ∗ b )
a∗c +b∗d b∗c − a∗d
p/q = +i
c∗c + d ∗d c∗c + d ∗d

We can write functions that compute the real and imaginary components and return them as
complex data types. We will first create the package specification using a private type for the
complex number.

In addition to the required components we will include a function called make_complex( ) to


convert a pair of floating-point numbers into a complex number and a function, called
magnitude( ) to return the magnitude (a floating point number) of it complex argument.

172
package complex is
type complex is private;
procedure get(c: out complex);
procedure put(c: in complex);
function "+"(a,b: complex) return complex;
function "-"(a,b: complex) return complex;
function "*"(a,b: complex) return complex;
function "/"(a,b: complex) return complex;
function make_complex(r,i : float) return complex;
function real_part(c: complex) return float;
function imag_part(c: complex) return float;
function magnitude(c:complex) return float;
private
type complex is record
real : float;
imag : float;
end record;
end adt_complex;

Now we have to create a package body that implements these functions and procedures. We
need to decide how we will get( ) and put( ) the complex numbers. We will require that the user
enter the complex number as a data pair and we will display complex numbers using the
real_part + i imagninary_part notation. This decision was completely arbitrary.

procedure get(c: out complex) is


begin
get(c.real);
get(c.imag);
end get;
procedure put(c: in complex) is
begin
put(c.real,0,4,0);
if c.imag<0.0 then
put("-i");
put(-c.imag,0,4,0);
else
put("+i");
put(c.imag,0,4,0);
end if;
end put;

Notice that these procedures have access to the fields of the complex number, but the package
user cannot directly access these fields since complex is specified as a private type. For
example, the package user cannot perform the following assignments,

:
C : complex; -- this declaration is OK
begin
get(C.real); get(C.imag); -- this assignment is not permitted
:

The complete package body for adt_complex is provided here.

with ada.text_io, ada.float_text_io, ada.numerics.elementary_functions;


use ada.text_io, ada.float_text_io, ada.numerics.elementary_functions;
package body adt_complex is

procedure get(c:out complex) is


begin

173
get(c.real);
get(c.imag);
end get;

procedure put(c:in complex)is


begin
put(c.real,0,4,0);
if c.imag<0.0 then
put("-i");
put(-c.imag,0,4,0);
else
put("+i");
put(c.imag,0,4,0);
end if;
end put;

function "+"(a,b: complex) return complex is


c : complex;
begin
c.real:=a.real+b.real;
c.imag:=a.imag+b.imag;
return c;
end "+";

function "-"(a,b: complex) return complex is


c : complex;
begin
c.real:=a.real-b.real;
c.imag:=a.imag-b.imag;
return c;
end "-";

function "*"(a,b: complex) return complex is


c : complex;
begin
c.real:=a.real*b.real-a.imag*b.imag;
c.imag:=a.real*b.imag+b.real*a.imag;
return c;
end "*";

function "/"(a,b: complex) return complex is


c : complex;
begin
c.real:=(a.real*b.real+a.imag*b.imag)/
(b.real*b.real+b.imag*b.imag);
c.imag:=(a.imag*b.real-a.real*b.imag)/
(b.real*b.real+b.imag*b.imag);
return c;
end "/";

function make_complex(r,i : float) return complex is


c : complex;
begin
c.real:=r;
c.imag:=i;
return c;
end make_complex;

function real_part(c: complex) return float is


begin
return c.real;
174
end real_part;

function imag_part(c: complex) return float is


begin
return c.imag;
end imag_part;

function magnitude(c:complex) return float is


begin
return sqrt(c.real*c.real+c.imag*c.imag);
end magnitude;

end adt_complex;

8.3 Write a program that uses the package adt_complex, created in Application 8.1, to generate
images of the Mandelbrot Set. This program should permit the user to enter the limits of the
region of the complex plane to be displayed.

First of all, we need to review some details of the Mandelbrot Set. In the mid 1970's, Benoit
Mandelbrot popularized the study of fractal geometry by extending the work of Gaston Julia in
iterated functions. Mandelbrot used a computer to iterate functions on complex numbers. He
then used the computer to generate a graphical representation of the behavior of the iterated
functions. Called fractals or fractal images, the Mandelbrot Set is the most famous example of a
data set generated by an iterated function and displayed graphically.

The function,
2
f(Z) = Z + C

is iterated by repeatedly computing f(Z) and setting Z=f(Z) for the next iteration. The values Z
and C are complex numbers and therefore represent positions in the complex plane (see Figure
8-4 above).

The magnitude of a complex number |C| is the length of the line in the complex plane connecting
the origin and the point representing C in the complex plane. The Mandelbrot Set is the set of all
complex points C in the expression above for which the magnitude of Z converges to a fixed
value when iterated.

The parameter Z is a complex number initially set to zero (Z=0+i0). To iterate this expression we
set Z=0+i0 and C to some number in the complex plane. For each C in the complex plane we
compute a new value for Z and check its magnitude. We always use the current value of Z to
compute the new Z value. Eventually, we find that the magnitude of Z either converges to a fixed
point or it diverges (continually grows to a larger and larger value).

We can create a graphical image in which each pixel corresponds to a point C in the complex
plane. If we color the pixels corresponding to the points C black when the iterated function
converges and white when it diverges we obtain an image similar to the one shown in Figure 8-5.
The black region represents the Mandelbrot Set of points in the complex plane.

175
Figure 8-5 Graphical Representation of the Mandelbrot Set

It turns out that all the points in the Mandelbrot Set are within a radius of magnitude 2.0 from the
origin of the complex plane. This means that we can terminate the iteration when the magnitude
of the parameter Z exceeds 2.0. We can also terminate the iteration when we have reached a
fixed value for Z.

We now need to address how we establish a relationship between the values in the complex
plane with those in a graphics window. Let's say that we wished to display the Mandelbrot Set in
the range (-2.0..2.0, -2.0..2.0) in a graphics window of size (0..500, 0..500). For each point in
graphics window we need to compute the corresponding value C in the complex plane. As
shown in Figure 8-6 this is done through a combination of scaling and shifting of values.

Figure 8-6 - Coordinate Transformation between the Complex Plane and the Graphics Window

Asking the user to enter the limits of the complex plane for which the Mandelbrot Set will be
graphed will provide the necessary information for the coordinate transformation. This also
allows the user to "zoom in" on some interesting feature of the Mandelbrot Set.

Use the get( ) procedure defined in the complex package built in Application 8.2 to obtain the
complex numbers for the upper-left corner, ul, and the lower-right corner, lr, of the region of the
complex plane to be graphed. Then use the real_part( ) and imag_part( ) functions in the

176
package to compute a value for scalex (the scale change for the x direction) and scaley (the scale
change for the y direction) as shown here,

scalex:=(real_part(lr)-real_part(ul))/float(xmax);
scaley:=(imag_part(ul)-imag_part(lr))/float(ymax);

where xmax and ymax are the size of the graphics window in the x and y directions respectively.
Using these values, we can compute the value of C being used to compute the iterated function
for the Mandelbrot Set.

C:=make_complex(float(px)*scalex+real_part(ul),
float(py)*scaley+imag_part(lr));

Note that we are starting with the plotting point (px,py) in the graphics window and converting this
data pair into the corresponding point in the complex plane. Once we know the color to plot, we
already know where to plot it.

for px in 1..xmax loop


for py in 1..ymax loop
C:=make_complex(float(px)*scalex+real_part(ul),
float(py)*scaley+imag_part(lr));
put_pixel(px,py,mandel_color(C));
end loop;
end loop;

In this example, the function mandel_color( ) returns the color to plot based of the result of the
iterated function.

function mandel_color(C : complex) return extended_color_type is


maxcount : constant integer := 240;
count : integer := 0;
Z : complex;
begin
Z:=make_complex(0.0,0.0);
loop
exit when magnitude(Z)>2.0 or count>maxcount;
Z:=Z*Z+C;
count:=count+1;
end loop;
if magnitude(Z)>2.0 then
return white;
else
return black;
end if;
end mandel_color;

Note that there are two ways to terminate the iteration loop. We exit when the magnitude of Z is
greater than 2.0 since we know that this point cannot be a member of the Mandelbrot Set (at least
this is what the experts tell us :-). We also exit when the loop count exceeds some maximum
value. This is because we want to avoid an infinite loop which may occur when the iterated value
of Z asymptotically approaches a value less than 2.0.

To add some interest (and color) to our images we can choose colors for the points outside the
Mandelbrot Set other than white. This can be done by choosing a color that corresponds to the
number of iterations needed to cause the magnitude of Z to exceed 2.0. Since we are counting
the iterations anyway, this should not add much complexity to our program.

color:=extended_color_type'val( count mod 214 +1);

177
This is the simplest way to color our images; however, we can rearrange the order of the colors to
generate a more pleasing display. Color order can be done by building color lookup table as an
array of extended_color_type.

As mentioned earlier, we can choose different limits to display more details of some part of the
Mandelbrot Set. Try to pick limits that display small regions of the complex plane at the edge of
the set. As shown in the example images of Figure 8-7

Figure 8-7: Sample Images of the Mandelbrot Set and Its Neighborhood

178
Chapter 9 - Special Topics in Programming

9.1 Case Study: Pseudorandom Number Generation


Theory and Limitations
Uniform Distributions
Normal Distributions

9.2 Case Study: Plotting Graphs in Three Dimensions


Storage and Retrieval of Multi-Variable Data
Scaling the Image
Orthographic Projections

9.3 Case Study: Scripting POV-Ray


Introduction to POV-Ray
Using Ada to Create POV-Ray Files

179
Chapter 9 - Special Topics in Programming
In this chapter, we demonstrate some new techniques in applied computing through Case
Studies. The topics in this chapter are provided as special interests and can be omitted from a
study of programming without loss of continuity.

These case studies introduce some important issues in the foundations of computer science.
While not essential to the understanding the material in the remainder of this text, they may
answer some questions that have been forming in the mind of the curious student about how
computer programs are used to perform certain tasks.

Pseudorandom Number Generation - The first case study introduces the concept of randomness
and the generation of random number sequences using an algorithm. This case study explains
how apparent randomness is achieved with a completely deterministic algorithm.

The second introduces the issue of data visualization in which we go beyond text file I/O using
graphical methods to simplify the presentation of large amounts of multidimensional data to the
user.

The third case study demonstrates the value of scripting in which a program generates an output
that is to be read by another computer program rather than a human user.

9.1 Case Study: Pseudorandom Number Generation

Theory and Limitations

The generation of random numbers using a computer is one of the most important computational
methods you will study. As you may have heard, digital computers are completely deterministic.
This means that there is no uncertainty or randomness in the execution of a program on a digital
computer. Since the computer, and therefore any algorithm running on the computer, is
deterministic it will generate the same output for the same input each time it is executed.

So how do we generate random numbers using a computer? The short answer is, we don't.
Actually, we can generate sequences of numbers that only appear to be random. In other words,
we can write computer programs to generate sequences of pseudorandom numbers. A
pseudorandom number generating program will generate a sequence of numbers that appear to
have no predictable pattern. That is, given a portion of this sequence we would not be able to
guess the next value.

Compare the sequences below and see if you can determine the next number in the sequence,

1) 10 13 16 19 22 ___

2) 3 8 15 24 35 ___

3) 7 5 1 5 13 ___

4) 6 4 2 8 9 ___

In the first sequence each number is 3 greater than the previous number, so the next number
would be 25.
10 13 16 19 22

+3 +3 +3 +3 +3

180
We can write a closed-form expression for this sequence in terms of the position, n, of the
numbers in the sequence. The nth number in the sequence S1 is given by,

S1(n) = 3n + 7

In the second sequence, the successive values are increasing more rapidly. Using the same
method of successive differences shown above we have,

3 8 15 24 35

+5 +7 +9 +11 +13

+2 +2 +2 +2

where the last row is the successive differences of the successive differences and is a constant
+2. The formula for this sequence is,
2
S2(n) = n - 1

It is more than a coincidence that the second level of successive differences of this sequence is
constant and the expression for this sequence is a second-order polynomial.

Although the third sequence may not appear to have any pattern, you may detect one if you
compare it with the previous two sequences. If you don't see it you can look at the footnote at the
1
bottom of this page for the answer.

The last sequence was generated using a random number generator set to return values in the
range [0..9]. Is this sequence random? Actually there is no absolute test for randomness for any
finite sequence; however there are some practical definitions for randomness that are generally
accepted.

Even though computer generated sequences of random numbers are not really random, they can
pass many of the tests for randomness, and, therefore, are useful for many applications needing
sequences of random numbers. A few of these applications include computer games, scientific
simulations, and many types of large-scale modeling tasks in science and economics.

We will study a method of generating pseudorandom number sequences in which we start with a
seed value and use it to generate a random number and a new seed value for generating the
next number in the sequence. Each seed value will generate a specific random number so that if
we started with the same seed, we would generate the same sequence of random numbers.

This brings up another problem associated with the computer as a generator of random
sequences. Computers are finite state machines. This means that there are only a finite number
of states or data values that a computer can exhibit for a given data type. For our purposes we
can think of each seed value as corresponding to a particular state of the computer.

If we start generating pseudorandom numbers (and hence new seed values) we will eventually
encounter a seed value that is the same as some previous seed value. This must be so since
there are only a finite number of possible seed values (also called machine states) from which to
choose. Since a computer algorithm is completely deterministic the sequence of random
numbers will begin to repeat from this point forward.

1
S3(n) = |S1(n) - S2(n)|
181
We will encounter what is called a limit cycle.

seed
values

limit cycle

Figure 9-1: Example of State Transitions in a Pseudorandom Number Generator

A limit cycle is a sequence of states generated by an algorithm or its implementation on a


computer that will repeat indefinitely as the algorithm is executed. As shown in Figure 9-1, there
can be more than one limit cycle for a particular random number generator program, and many
different seeds can lead to the same limit cycle. Every random number generator running on a
digital computer has limit cycles that set the maximum length of sequences of values that can be
considered random. Once a limit cycle is reached some subset of possible values is repeated
over and over.

At first this may not seem like a big deal, and for some applications the length of the limit cycle
(and therefore the quality of the randomness of the sequence of numbers) doesn't matter that
much. However, repeating patterns of values assumed to be random could create structure or
artifacts in the results of analysis that can be misinterpreted as significant features. Literally
millions of dollars can be wasted conducting studies and/or building hardware whose design is
based on fictitious data generated by a bad or improperly used random number generator.
DON'T LET THIS HAPPEN TO YOU!

In order to avoid embarrassing and costly mistakes, it is important to understand how computers
are used to generate pseudorandom number sequences and what the limitations of these
programs are.

A popular method for generating random numbers using a computer is called the congruence
method. If we let a sequence of pseudorandom numbers be given by the set {Xn}, where n=1,2,...
Then the congruence method is expressed by the formula,

Xn+1 = (a Xn + b) mod T

where b and T are relatively prime. Given an initial value X1 as a seed, all future values of Xn+1
are computed from the previous value Xn. The parameters a, b, and T are constants for a
particular random number generator. We can't just pick any values for these parameters. They
must be chosen based on the length of the sequence of pseudorandom values we intend to
generate and on the details of the design of the computer on which the pseudorandom number
generator program will be running.

182
While the mathematical theory underlying the congruence method is quite involved we can use a
2
number of rules to help us make good choices for the parameters a, b, and T.

1. Let T be one larger than the limit of the range of values to be generated 0..(T-1).
2. Make b relatively prime to T
3. Let a = 1 (mod p) (i.e. a mod p = 1) where p is a prime factor of T or
a= 1(mod 4) if 4 is a factor of T.

Following these rules will ensure that the pseudorandom sequence of numbers generated will
cover the range of value 0..T-1 and that the sequence will pass the standard tests for
randomness.

As an illustration of the effects of good and bad choices for a and b, we will let T=8. If we choose
a and b wisely, our range of values will be 0..7 and our limit cycle will be of length 8. According to
the rules, b must be relatively prime to T, so we choose b=3. Rule 3 states that given p is a prime
factor of T, a mod p must equal 1. The only prime factor of 8 is 2 which means that a would have
to be 1. However, there is a caveat for Rule 3 that states that if 4 is a factor of T then a mod 4
must be equal to 1. We choose a=5 to satisfy Rule 3. In summary, we show our choices below.

T : constant long_integer := 8;
a : constant long_integer := 5;
b : constant long_integer := 3;

Setting the initial value of seed=0 we execute the assignment statement,

seed := (a*seed + b) mod T;

repeatedly to generate the following sequence of digits in the range 0..7.

0325476103254761...

Using any value 0 through 7 as an initial value results in the continuation of this sequence starting
with the next number and repeating for as long as we call the assignment statement to update the
seed. Choosing values for a and b that violate the rules stated above would result in repeating
sequences of numbers that can be much shorter in length than T. For example, if we let b=2 we
obtain the sequence,
1753175317...
Uniform Distributions

Returning to our original problem we want to generate a sequence of 1000 random numbers
between 0 and 99 inclusive. But we are limiting our value of T to be some power of 2. We could
7
set T to 2 =128 and omit any value greater than 99 but this would still leave us with the problem
of how to generate 1000 numbers using a random number generator that will begin repeating its
sequence after 128 values.

Since T determines how many values we can generate without repeating, we will want to set T to
some power of 2 whose magnitude is greater than 1000. Actually we can make T very large,
allow the seed value to be in the range 0 through T-1 and simply scale the random numbers
generated to fall in the range 0 through 99 as shown in the following source code:

with ada.text_io, ada.long_integer_text_io;


use ada.text_io, ada.long_integer_text_io;

procedure random_generator is
T : constant long_integer := 16_384;

2
For more information on random number generation see Handbook of Mathematical Functions
with Formulas, Graphs, and Mathematical Tables, by Milton Abramowitz and Irene A. Stegun,
National Bureau of Standards, Applied Mathematics Series 55, June 1964.
183
a : constant long_integer := 5;
b : constant long_integer := 1_023;
seed, max_val, rand : long_integer;
begin
put("Enter an initial seed (integer) for the generator... ");
get(seed);
max_val:=100;
for i in 1..100 loop
seed := (a*seed + b) mod T;
rand:=seed mod max_val;
put(rand,3);
end loop;
end random_generator;

The value of seed is between 0 and 16,384 but since rand is seed mod max_val we can set
max_val=100 to keep rand between 0 and 99 inclusive. Now we only need to modify this
program to create a text file to hold the generated random values.

Sometimes we want to create uniformly distributed random sequences of floating-point values


rather than integers. In this case, we can set the range of T to the maximum integer and then
divide by this value to produce floating-point values between 0.0 and 1.0. Then we can multiply
these values by the scale-factor needed to produce the desired range of floating-point values.

function ranu return float is


a : constant long_integer := 16_807;
m : constant long_integer := 2_147_483_647;
q : constant long_integer := 127_773;
r : constant long_integer := 2_836;
begin
seed :=a*(seed mod q) - r*(seed/q);
if seed <=0 then
seed:=seed+m;
end if;
return float(seed)/float(maxi);
end ranu;

Note that the function, ranu has no argument since the range of values will be in the range
[0.0..1.0] each time it is called. Consider the example in which we wish to generate random
floating-point values in the range [-5.0..10.0]. This range has magnitude 15.0 and a minimum
value of -5.0. To produce this range of values from ranu, we simply multiply ranu by 15.0 and
then add -5.0. In general, uniformly distributed random values in the range [xmin..xmax] can be
generated using ranu by,

scale_factor := xmax - xmin;


r := ranu*scale_factor + xmin;

You can test this transformation by computing the values at the limits of the range. For example,
let xmin = -5.0 and xmax = 10.0. The value of scale_factor is,

scale_factor = 10.0 - (-5.0) = 15.0

Now when ranu = 0.0 we have r = 15.0 x 0.0 + (-5.0) = -5.0

and when ranu = 1.0 we have r = 15.0 x 1.0 + (-5.0) = 10.0

which demonstrates the desired range of values. Since ranu is uniformly distributed over the
range [0.0..1.0] we can assume that r will be distributed over the range [-5.0..10.0].

184
There are other random number distributions that occur in nature for which some values are more
likely to occur than others. The most commonly occurring, and therefore the most important to
computer simulations of real-world systems, is called the normal distribution.

Normal Distributions

Imagine that you are throwing darts at the bullseye of the dartboard. The distribution of the darts
on the board will be distributed around the bullseye with (hopefully) more darts closer to the
center. This is an example of a normal distribution (also called a Gaussian distribution). The
bullseye is the mean of the distribution. If you are an expert dart player the spread of the darts,
also called the standard deviation (see Section 5.2), will be small. If you are a beginner the
spread in the darts will be larger.

σ σ

µ µ
Figure 9-2: Two Sample Distributions of Darts on a Dartboard with the
Same Means but Different Standard Deviations

While this is a two-dimensional example, we can consider the simpler case of a one-dimensional
or single-variable normal distribution with mean µ and standard deviation σ. The curves shown
below the dartboards in Figure 9-2 above represent the probability that a particular dart will land
within a specified distance from the center (or mean). We can express the probability density p( )
of values mathematically as,
−( x − µ )2
1
p( x) = e 2σ 2

σ 2π
If we let µ=0 and σ=1 then we have the standard normal form for the probability density function,

x2
1 −
p( x) = e 2

We can compute the probability that a dart will fall between horizontal positions x1 and x2 by
integrating the probability density function between these limits.

185
x2
1 x2 −
P ( x1 ..x 2 ) = e 2
dx
2π x1

x1 µ x2
Figure 9-3: Integral of the Probability Density
Function Between x1 and x2

In this example, we have used the standard form of the probability density function, which means
that we are assuming that the mean is zero and the standard deviation is unity. It is easy to
convert the standard form of the probability density to a probability density function with mean µ
and standard deviation σ by multiplying the standard form by σ and then adding µ.

Unfortunately, this integral is not solvable analytically but can be approximated numerically using
a computer program. This integral is also called the error function. You can refer to any
introductory probability and statistics textbook for more information on the error function and
computing normal random probabilities.

For now we will limit ourselves to the task of generating pseudorandom number sequences with a
normal distribution. Again, according to Abramowitz and Stegan, we can produce normally
distributed random values from uniformly distributed random values Normal using the following
formula.
Normal = − 2 log( R1 ) × cos(2 π R2 )

where R1 and R2 are two independent, uniformly distributed, random values in the range[0.0..1.0],
log( ) is the base-10 logarithm, and cos( ) is the trigonometric cosine. Even though a proof of the
validity of this relationship is beyond the scope of this text we can implement this method in an
Ada function.

function normal return float is


pi : constant float := 3.1415926;
r1,r2 : float;
begin
r1:=ranu;
r2:=ranu;
return float(sqrt(-2.0*log(r1))*cos(2.0*pi*r2));
end normal;

Each time the function normal is called, a normally distributed random value with a mean of 0.0
and a unit (1.0) standard deviation is returned. As with the uniformly distributed values, we can
convert these values into normally distributed random values with any mean and any standard
deviation by,
Gaussian = σ × normal + µ

where Gaussian is a normally distributed random value with mean µ and standard deviation σ.

Write a program to count the number of occurrences of values falling in each of the following
listed intervals when 1000 normally distributed random values with a mean of 50.0 and a
standard deviation of 2.5 are generated.

186
bin 0 ............... >40.0 and <= 41.0
bin 1 ............... >41.0 and <= 42.0
: :
bin 18............... >58.0 and <= 59.0
bin 19............... >59.0 and <= 60.0

We will create an array of integers called count( ) to keep a count of the number of occurrences in
each interval.

numbins : constant integer := 20;


type countlistype is array(0..numbins-1) of integer;
count : countlistype;

Since the sizes of the intervals are all the same and they are contiguous (i.e. there are no gaps
between them) we can convert each generated random value directly into an index into the array
count( ) by,

norm := sigma*normal + mean;


nbin := integer((norm-xmin)-0.5);

The value of xmin is set to 40.0 because this is the lower limit of the range of values of interest.
Since the conversion to integer type rounds up to the next larger integer for values with fractional
parts >0.5 we must subtract 0.5 from norm. Let's test this conversion on the values 43.7 and 43.2
to ensure that the proper bin index is calculated.

nbin = integer(43.7 - 40.0 - 0.5) = integer(3.2) = 3


nbin = integer(43.2 - 40.0 - 0.5) = integer(2.7) = 3

In our test case, the value of nbin is 3 so we would increment the value of count( ) by,

if nbin >= 0 and nbin <= numbins - 1 then


count(nbin) := count(nbin) + 1;
end if;

The purpose of the if..then statement is to make sure that the value of nbin is within the valid
index range of count( ). Putting it all together, we have,

with ada.text_io, ada.integer_text_io, ada.float_text_io, random;


use ada.text_io, ada.integer_text_io, ada.float_text_io, random;

procedure normal_test is

numbins : constant integer := 20;


type countlistype is array(0..numbins-1) of integer;

count : countlistype;
n : integer := 1000;
mean : float := 50.0;
xmin : float := 40.0;
sigma : float := 2.5;
nbin : integer;
norm : float;

begin

-- preset all the counts to zero


for i in 0..numbins-1 loop
count(i) := 0;
end loop;

187
-- generate normally distributed random values and increment counts
for i in 1..n loop
norm := sigma*normal + mean;
nbin := integer((norm-xmin)-0.5);
if nbin>=0 and nbin<=numbins-1 then
count(nbin) := count(nbin) + 1;
end if;
end loop;

--display results
for i in 0..numbins-1 loop
put(integer(xmin)+i,0);
put(" to ");
put(integer(xmin)+i+1,0);
put(" ... ");
put(count(i),0);
new_line;
end loop;
end normal_test;

The following is a sample of the output of this program for two runs. Note that the values are
different for each run but that the general characteristics of the distributions are similar.

40 to 41 ... 0 40 to 41 ... 0
41 to 42 ... 0 41 to 42 ... 1
42 to 43 ... 0 42 to 43 ... 0
43 to 44 ... 3 43 to 44 ... 3
44 to 45 ... 13 44 to 45 ... 16
45 to 46 ... 26 45 to 46 ... 32
46 to 47 ... 56 46 to 47 ... 71
47 to 48 ... 98 47 to 48 ... 106
48 to 49 ... 145 48 to 49 ... 126
49 to 50 ... 149 49 to 50 ... 155
50 to 51 ... 166 50 to 51 ... 161
51 to 52 ... 123 51 to 52 ... 128
52 to 53 ... 85 52 to 53 ... 89
53 to 54 ... 69 53 to 54 ... 60
54 to 55 ... 38 54 to 55 ... 26
55 to 56 ... 22 55 to 56 ... 17
56 to 57 ... 3 56 to 57 ... 7
57 to 58 ... 4 57 to 58 ... 2
58 to 59 ... 0 58 to 59 ... 0
59 to 60 ... 0 59 to 60 ... 0

Figure 9-4: Output from Two Runs of normal_test.adb

9.2 Case Study: Plotting Graphs in Three Dimensions

In this case study, we will display three-dimensional data in an orthographic view.


Storage and Retrieval of Multi-Variable Data
The first task in plotting multi-variable data is to establish a means for the storage and retrieval of
the data being displayed. In this case study, we will use a text file, as shown on the next page, to
hold the values and some of the information needed for its display.

188
Number of Rows NRow and Number of Columns NCol
mat(1,1) mat(1,2) mat(1,3) . . . mat(1,NCol)
mat(2,1) mat(2,2) mat(2,3) . . . mat(2,NCol)
mat(3,1) mat(3,2) mat(3,3) . . . mat(3,NCol)
:
mat(NRow,1) mat(NRow,2) . . . mat(NRow,NCol)

Figure 9-5: Format of Text File for Holding Multi-Variable Data

The load_data( ) procedure below has been designed to read data from a text file with the format
as shown in Figure 9-5.

procedure load_data(fname:out wordtype; fnum: out integer) is


datin : file_type;
begin

put("Enter filename... ");


get_line(fname,fnum);
open(datin,in_file,fname(1..fnum));
get(datin,nrow);
get(datin,ncol);

for i in 1..nrow loop


for j in 1..ncol loop
get(datin,mat(i,j));
end loop;
end loop;

close(datin);

end load_data;
In this program, the array mat( ) is left as a global parameter. This eliminates the need to create
a separate copy of the file to be passed back to the main procedure.
Scaling the Image

We will display the data in an orthographic view . In order to set the proper scale factors we need
to scan the data to determine the minimum and maximum values.

procedure get_minmax(min,max : in out float) is


begin
min:=mat(1,1);
max:=mat(1,1);
for i in 1..nrow loop
for j in 1..ncol loop
if min>mat(i,j) then min:=mat(i,j); end if;
if max<mat(i,j) then max:=mat(i,j); end if;
end loop;
end loop;
end get_minmax;

Orthographic Projections

An orthographic projection is one in which there is no perspective or reduction in the size of


objects as they appear farther from the observer.

189
orthographic perspective
Figure 9-6: Orthographic and Perspective Projections

Since we are using an orthographic projection we can display the data in array mat( ) row-by-row,
shifting our origin by a constant amount for each row. Figure 9-7 shows an example plot for a
50x50 array.

Figure 9-7: Orthographic Projection of a 50x50 Array of Data Values

The source code for this projection is given in the procedure draw_orthoview( ) below.

procedure draw_orthoview is
dy : integer:=3;
dx : integer:=2;
scale : float:=1.0;
xstart: integer;
x : integer;
xold: integer;
yold: integer;
ybase: integer;
min : float;
max : float;
col : extended_color_type;
begin
get_minmax(min,max);
scale:=float(ym)/5.0/float(max-min);
for i in reverse 1..nrow loop
for j in 1..ncol loop
x:=100+ncol*dx+size*j-i*dx;
if j=1 then
xstart:=x;
end if;
ybase:=integer(scale*min)+50+dy*i;

190
if palette=0 then
col:= extended_color_type'val(integer(14.0*
(mat(i,j)-min)/(max-min))+1);
else
col:=extended_color_type'val(palette+6);
end if;
if j=1 then
goto_xy(x,integer(scale*min)+50+dy*i);
xold:=x;
yold:=integer(scale*min)+50+dy*i;
else
if hidden_line then
draw_box(x-dx/2,integer(scale*mat(i,j))+99+dy*i,
x+2*dx,integer(scale*min)+50+dy*i,white,fill);
goto_xy(xold,yold);
end if;
yold:=integer(scale*mat(i,j))+100+dy*i;
draw_to(x,yold,col);
xold:=x;
end if;
end loop;
if not(hidden_line) then
draw_to(x,ybase,col);
else
if i=1 then
draw_to(x,ybase,col);
draw_to(xstart,ybase,col);
end if;
end if;
end loop;
end draw_orthoview;

The Boolean hidden_line should be set to true if the plot is to have a solid appearance as shown
in Figure 9-8 below.

Figure 9-8: Orthographic Projection with Hidden Lines

Depending on the application, hidden lines may be preferred or they may obscure some
important feature in the data. As shown in the source code for draw_orthoview( ), hidden lines
add significant complexity to the program.

191
9.3 Case Study: Scripting POV-Ray

Introduction to POV-Ray

When the output of a computer program is to be read as input by another computer program, the
shared file is called a script, and the process of writing the program to generate a script is called
TM
scripting. In this section, we will use the Persistence of Vision Ray-Tracing (POV-Ray) program
as our target for Ada scripts. POV-Ray creates three-dimensional, photo-realistic images using a
rendering technique called ray tracing. The POV-Ray rendering program reads a text file (file
extension is .pov) containing information describing the objects and lighting in a scene and
generates an image of that scene from the viewpoint of an observer in the three-dimensional
virtual world.

POV-Ray was developed from DKBTrace 2.12 (written by David K. Buck and Aaron A. Collins) by
TM
a group called the POV-Team . You can download a free copy of the POV-Ray programs for
Windows, Mac OS, or Linux at the Persistence of Vision Home Page (http://www.povray.org).
Details of how to install and run POV-Ray are included in the extensive read-me and help files
provided.

Using Ada to Create POV-Ray Files

The POV-Ray files such as those discussed in the previous section are text files just like the Ada
source code we have been writing. As we learned in Chapter 5, Ada can be used to create text
files. In this section, we will use Ada to automatically generate .pov files in a process called
scripting.

As stated, scripting is the automatic generation of files that are intended to be read by another
program. Interactive Web pages generated with JavaScript, Cold Fusion, Perl, or any CGI
(common gateway interface) object codes are all examples of scripting. Responses to database
queries are other examples of scripted files. For example, whenever you perform an Internet
search using an online search engine, the client side component of the search engine scripts your
query and sends it to the server where middleware scripts a Web page containing the matching
links and sends it back to your browser.

In its simplest form, scripting is just writing strings of characters into a text file. Outputting string
literals was one of the first things we learned how to do. Consider the Ada program below which
scripts a simple POV-Ray file.

with ada.text_io;
use ada.text_io;
procedure script_1 is
dout : file_type;
begin
create(dout,out_file,"script_1.pov");
put_line(dout,"camera {location <0,0,-80> look_at <0,0,0> }");
put_line(dout,"background { color blue 1 green .5 red .5 }");
put_line(dout,"light_source {<30,100,-30> color red 1 green 1
blue 1}");
put_line(dout,"light_source {<-30,100,30> color red 1 green 1
blue 1}");
put(dout,"sphere{ <0, 0, -50 >, 10 texture{ pigment {color ");
put_line(dout,"rgbf<0.8, 0.1, 0.1, 0.8>} finish {phong 1}}}");
close(dout);
end script_1;

Note: The sphere description was broken into a put( ) and put_line( ) procedure to fit the printed
page.

192
The previous program scripts a POV-Ray file that draws a red semi-transparent sphere against a
light blue background. The output of this program is a file named script_1.pov shown below. This
file is read by the POV-Ray renderer to create the image.

// POV-Ray File Scripted using Ada

camera {location <0,0,-80> look_at <0,0,0> }


background { color blue 1 green .5 red .5 }
light_source {<30,100,-30> color red 1 green 1 blue 1}
light_source {<-30,100,30> color red 1 green 1 blue 1}
sphere { <0, 0, -50 >, 10 texture{ pigment {
color rgbf<0.8, 0.1, 0.1, 0.8>} finish {phong 1}}}

Figure 9-9: Balloon Image Scripted in .pov by an Ada Program


So why would anyone want to go to the trouble of writing an Ada program to script POV-Ray
files? Obviously there is no advantage in writing scripting programs when the source code is
longer than the file being scripted. Instead of one sphere (balloon), let us assume that we need
to create an image containing 100 randomly placed balloons in a variety of colors.

In this case we will want to replace the hardwired put and put_line statements for the sphere with
a subprogram that can script sphere descriptions with random positions and colors.

The sphere description needs to be broken down into components that permit the position and
color to be specified using variable identifiers. We will let the position be given by <x,y,z> and the
color/transparency by the texture function rgbf<r,g,b,t> where r,g,b, and t are floating point
values in the range [0.0..1.0]. The first three components of the rgb are the relative levels of the
colors red, green, and blue. the fourth parameter is the transparency with increasing value
representing increasing transparency. In the sample listing below, we have used the random
number generator function rnd(maxint) for random integers between 0 and maxint-1 and ranu for
random floats between 0.0 and 1.0.

with ada.text_io, ada.float_text_io, ada.integer_text_io, random;


use ada.text_io, ada.float_text_io, ada.integer_text_io, random;

procedure povballoons is

dout : file_type;

procedure make_balloon(x0,y0,z0,rad0,del : in integer) is


x,y,z,rad : integer;
begin
x:=x0+(rnd(2*del)-del);
y:=y0+(rnd(2*del)-del);
z:=z0+(rnd(2*del)-del);
rad:=rad0;
put(dout,"sphere { <");
put(dout,x,0); put(dout,", ");
put(dout,y,0); put(dout,", ");
put(dout,z,0); put(dout," >, ");
put(dout,rad,0);
put(dout," texture{ pigment {color rgbf<");
put(dout,ranu,0,4,0); put(dout,", ");
193
put(dout,ranu,0,4,0); put(dout,", ");
put(dout,ranu,0,4,0);
put_line(dout," , 0.6>} finish {phong 1}}}");
end make_balloon;

begin

create(dout,out_file,"balloons.pov");
put_line(dout,"camera{location <0,50,-100> look_at <0,0,0> }");
put_line(dout,"background{ color blue 1 green .5 red .5 }");
put_line(dout,"light_source{<30,50,-30> color red 1 green 1 blue 1}");
put_line(dout,"light_source{<-30,50,30> color red 1 green 1 blue 1}");

for i in 1..100 loop


make_balloon(0,0,0,10,50);
end loop;
close(dout);

end povballoons;

Each of the output files scripted with this program contain descriptions for 100 spheres. It is
much simpler to script large files such as these using an Ada program. Sample images rendered
from these scripted balloons.pov files are shown below. The files were generated by making
minor modifications in the cameral position and varying the del parameter of the subprogram
make_balloon( ).

Figure 9-10: POV-Ray Images Generated by the povballoons.adb Scripting Program

These balloons images are generated randomly but we can also script POV-Ray source that is
more controlled.

194
In our next example, we will write an Ada program to script a POV-Ray file to generate an image
of a three-dimensional rectangular framework of cylindrical rods. As shown,

cylinder {<i,j,k>, <i+1,j,k>,rad. . . }


cylinder {<i,j,k>, <i,j+1,k>,rad. . . }
cylinder {<i,j,k>, <i,j,k+1>,rad. . . }

Figure 9-11: Layout for the grid_sticks.adb Scripting Program

We can use a three-level nested loop to address each row, column, and layer of this structure.

for i in 1..nmax loop


for j in 1..nmax loop
for k in 1..nmax loop
-- code for scripting block(i,j,k) goes here
end loop;
end loop;
end loop;

At first look, the basic unit of this structure appears to be a cube. However, these cubes share
sides with each other, so we do not need to script twelve cylinders for each block location. In
fact, we can script only three cylinders as shown above. Also, we need to make sure that both
ends of the cylinders are within the limits of the grid structure. In particular, we don't want to
script a cylinder with index i>nmax, j>nmax, or k>nmax. We can place each cylinder script inside
an if...then block,

for i in 1..nmax loop


for j in 1..nmax loop
for k in 1..nmax loop
if i<nmax then
-- script for cylinder{<i,j,k>,<i+1,j,k>,rad. . . }
end if;
if j<nmax then
-- script for cylinder{<i,j,k>,<i,j+1,k>,rad. . . }
end if;
if k<nmax then
-- script for cylinder{<i,j,k>,<i,j,k+1>,rad. . . }
end if;
end loop;
end loop;
end loop;

195
It remains for us to build a procedure for scripting the cylinders. As we have seen, the format for
cylinders in POV-Ray is

cylinder {
<x1, y1, z1>, // Center of one end
<x2, y2, z2>, // Center of other end
rad // Radius
texture { . . . }
}

We need to pass the positions of the endpoints and the radius into the cylinder scripting
procedure. We can hardwire the procedure with the texture settings for this problem.

procedure make_stick(x1,y1,z1,x2,y2,z2 : in integer; rad: in integer)


is
begin
put(dout,"cylinder { <");
put(dout,x1,0);
put(dout,", ");
put(dout,y1,0);
put(dout,", ");
put(dout,z1,0);
put(dout," >, ");
put(dout," < ");
put(dout,x2,0);
put(dout,", ");
put(dout,y2,0);
put(dout,", ");
put(dout,z2,0);
put(dout," >, ");
put(dout,rad,0);
put_line(dout,
", 1 texture { pigment { color red 2 green 1 blue 0 } finish {phong
1}}}");
end make_stick;

Finally, we will blob the structure to smooth the interfaces between cylinders. The source code
below scripts a POV-Ray file with 144 cylinders arranged in a three-dimensional grid as shown in
the images following the code. The camera location was manipulated by hand to obtain the view
from inside the grid.

with ada.text_io, ada.integer_text_io;


use ada.text_io, ada.integer_text_io;
procedure grid_sticks is
dout : file_type;
nmax : integer := 4;

procedure make_stick(x1,y1,z1,x2,y2,z2 : in integer; rad: in integer)


is
begin
put(dout,"cylinder { <");
put(dout,x1,0); put(dout,", ");
put(dout,y1,0); put(dout,", ");
put(dout,z1,0); put(dout," >, ");
put(dout," < ");
put(dout,x2,0); put(dout,", ");
put(dout,y2,0); put(dout,", ");
put(dout,z2,0); put(dout," >, ");
put(dout,rad,0);
put_line(dout,

196
", 1 texture { pigment { color red 2 green 1 blue 0 } finish {phong
1}}}");
end make_stick;

begin
create(dout,out_file,"grid_sticks.pov");
put_line(dout,"camera { location <0,30,-80> look_at <30,60,0> }");
put_line(dout,"background{ color blue 1 green .5 red .5 }");
put_line(dout,"light_source { <30,50,-100> color green 1 blue 2 }");
put_line(dout,"light_source { <-30,50, 0> color red 2 green 1 }");
put_line(dout,"blob { threshold 0.2");

for i in 1..nmax loop


for j in 1..nmax loop
for k in 1..nmax loop
if i<nmax then
make_stick(i*10,j*10,k*10,(i+1)*10,j*10,k*10,1);
end if;
if j<nmax then
make_stick(i*10,j*10,k*10,i*10,(j+1)*10,k*10,1);
end if;
if k<nmax then
make_stick(i*10,j*10,k*10,i*10,j*10,(k+1)*10,1);
end if;
end loop;
end loop;
end loop;
put_line(dout," }");
close(dout);
end grid_sticks;

Figure 9-12: Example POV-Ray Images for Two Different Views of the grid_sticks.adb Script

Scripting can be accomplished using any computer language and the functions and procedures
included in the ada.text_io package provide reasonable support for scripting. There are a number
of languages designed specifically for scripting such as Perl, JavaScript, and Tcl. Scripting has
many other applications besides computer graphics including dynamic Web page generation,
compiler creation, and database reformatting.

197
Exercises .

9.1 Use the successive differences method of Section 9.1 to determine the next number in each
sequence below:

a. 2 7 12 17 ____

b. 10 4 2 8 ____

c. 1 2 3 ____

d. 1 2 3 5 ____

9.2 Based on the sequences c and d of the previous exercise make an argument to support the
statement "Given any finite sequence of integers S, there is an infinite number of sequences T
with |T|>|S| which include S as the initial subsequence.”

9.3 Implement the function below and use it to generate a random sequence of digits in the range
[0..9]. Then verify that the distribution of digits generated using this function is uniform by
counting the number of occurrences of each digit in 10,000 trials.

function ranu return float is


a : constant long_integer := 16_807;
m : constant long_integer := 2_147_483_647;
q : constant long_integer := 127_773;
r : constant long_integer := 2_836;
begin
seed :=a*(seed mod q) - r*(seed/q);
if seed <=0 then
seed:=seed+m;
end if;
return float(seed)/float(maxi);
end ranu;

9.4 Write an Ada program to script a POV-Ray file for a


fictitious cityscape. Your program can be as simple as a
collection of randomly sized and colored boxes
(skyscrapers) restricted to a series of city blocks.

9.5 Modify the POV-Ray script created in Exercise 9.4 to


change the camera position (viewpoint) to show:

a. a ground-level view looking level


b. a ground-level view looking up
c. a flying view (see image on right)
d. a spiderman view (building tops looking down)

9.6 Modify the povballons.adb scripting program to include a blob function around the balloons.
Experiment with the blobbing threshold and balloon spacing to create a variety of images.

198
Chapter 10 - Dynamic Memory Allocation and Recursion

10.1 Static vs. Dynamic Memory


What is dynamic memory?
Why is dynamic memory important?
Elements of Dynamic Memory Management
A Program that Builds a Linked-List
Traversing a Linked-List

10.2 Iteration vs. Recursion


Recurrence Relations

10.3 Algorithm Analysis


Methods of Analysis
Orders of Run Time

10.4 Applications
Graphical Demonstration of a Linked List0
Estimating Runtime Performance

199
Chapter 10 - Dynamic Memory Allocation and Recursion
In this chapter, we introduce the concept of dynamic memory, which is a type of memory
allocation that occurs during program execution. Dynamic memory is accessed and controlled
through the use of access types, also called pointers and pointer records. We also introduce a
special type of subroutine call in which a procedure or function calls itself during its execution.
This is a powerful programming construct called recursion. Dynamic memory management and
recursion will be used extensively in the remainder of this text. In this chapter, we also begin a
more formal study of algorithm performance analysis.

10.1 Static vs. Dynamic Memory

Up to now, we have written programs in which all the computer memory needed to hold
constants, variables, and instructions was declared in the declaration block of our program and
allocated to the program during its compilation. This method of memory allocation is called static
memory management. Sometimes this requires us to reserve more space than our program will
use in order to be sure to have enough memory to solve a particular problem. Static memory
allocation forces us to decide the maximum sizes for arrays and other data structures that will be
used to hold the problem and its solution before we build the program. If we later decide to apply
our program to a larger problem, we must modify the source code to permit larger data files (e.g.
increasing array bounds) and then recompile the program. Static memory is called static
because, once the program is created, its memory size is fixed.

What is dynamic memory?

Sometimes we are not sure of the amount of memory needed by our program because the
required array sizes depend on the values of data entered or computed during program
execution. Ada (as well as most other programming languages) provides us with the capability to
write programs that access more memory from the available pool of memory, as it is needed. This
technique is called dynamic memory management. Dynamic memory can also be returned to the
pool of available memory (called the heap) when the running program no longer needs it.

Why is dynamic memory important?

Using dynamic memory management permits more effective use of computer memory. Memory
is a resource shared by many programs at any given time. When you run more than one
application at a time these programs are sharing the computer memory. In addition to your
application programs, the operating system can be running programs called kernel processes that
all share the available heap. Often applications need additional memory for only a short period of
time during their execution. This changing need for memory can be for internal calculations or for
temporary buffers to hold data during data manipulation or input/output tasks. If all programs had
to be allocated the maximum amount of memory they would ever need, fewer programs could be
running at the same time.

Elements of Dynamic Memory Management

The data structure used for dynamic memory management uses two components. These are the
pointer and the pointer_record. A pointer is a single unit of memory that gives the address of
some location in computer memory. A pointer_record is a block of memory locations whose first
element is at the address held in the pointer that is "pointing to" this pointer-record.

A pointer_record is declared in the same way that normal records are declared with the additional
step of stating what type of pointer has access to (or points to) this record. In the example
declaration on the next page, we see that the pointer named a_pointer accesses the
pointer_record called my_stuff.

200
type my_stuff; -- partial declaration of record

type a_pointer is access my_stuff; -- declaration of pointer to the


-- record named my_stuff

type my_stuff is record -- now the record declaration is


this : integer; -- completed
that : float;
the_other : array_type;
next : a_pointer; -- next will point to records of
end record; -- the same type (my_stuff)

The partial declaration of my_stuff indicates that a_pointer is an access type to something called
my_stuff. The reserved word access is used to declare pointers in the Ada programming
language.

Notice that one of the fields in my_stuff is an identifier called next with the type a_pointer. This
element is a pointer used to point to the next pointer record in a list (if one is created) of these
records. This is why we must specify the type a_pointer before the pointer record is declared.

In the code above, we have only defined types for a_pointer and my_stuff. We have not allocated
any memory. If we were declaring static memory, we would assign names to instances of the
record type my_stuff as part of the declaration. However, allocation of memory for these records
will occur during program execution; so the code for this memory allocation will be placed in the
executable block. The only memory allocations needed in the declaration block are pointers that
will be used to provide access to the pointer records being allocated during execution.

We will write a program that creates a list of records, my_stuff, built from data that is entered by
the user. The length of this list will depend on the number of values entered. In our example
program, we will declare a pointer called head_node that will give us access to the first record in
the list of records and a pointer called temp_node that will be used to temporarily access records
as the program builds the list. In the declaration block we write,

head_node : a_pointer;
temp_node : a_pointer;

These pointers can be used to access records of type my_stuff. In a program using dynamic
memory, new records are allocated by requesting the type of record needed. In Ada, this
dynamic memory allocation is accomplished using the reserved word new. For example, an
executing program could create a new node using the new function by making the following call.

head_node:= new my_stuff; head_node


this

We can make a sketch of the associated that


memory allocation as shown in Figure 10-1. In the_other
this graphical representation, the pointer field next
of the record is indicated with an arrow. Since
that pointer has not been assigned, it points
to an unknown (probably invalid) address. Figure 10-1: Pointer and Pointer Record

The head _node is shown as an arrow pointing to the block representing the record. Remember
that the pointer is just a variable holding the memory address of the first element of a record of
the type my_stuff. It is very helpful to make such sketches when debugging dynamic memory
applications.

Next, we will create another record pointed to by temp_node and then connect these records to
each other.

temp_node:= new my_stuff;

201
Now we have two records, one pointed to by head_node and one pointed to by temp_node.
head_node temp_node

this this
that that
the_other the_other
next next

Figure 10-2: Two Pointer Records of the Same Type

We can set the next pointer in the first record to point to the second record using the assignment,

head_node.next:=temp_node;

As shown in Figure 10-3, this assignment causes the pointer called next in the first record to be
pointed to the second record. This assignment does not change the address held by temp_node,
so we show it continuing to point to the second record as well. Since it is not good programming
practice to leave any pointers dangling (i.e. pointing to some arbitrary location), we can assign the
next pointer in the second record to point to null, which is a special address reserved for
unassigned pointers.

head_node.next.next:=null;

Following the execution of these previous two statements we have a data structure as
represented in Figure 10-3.

head_node temp_node

this this
that that
the_other the_other
next next null

Figure 10-3: The pointer field of the first record now points to the second record
while the pointer field of the second record points to null.

which is an example of a linked-list. In this example, we can access the values (i.e. the fields) of
the pointer records using the pointers and dot notation. For example, the value contained in the
field named this of the first record is accessed with the variable identifier, head_node.this. The
field labeled the_other in the second record can be accessed by temp_node.the_other. Typically,
a linked list has only one pointer.

The pointer named temp_node would be a temporary handle for records as they are appended to
the linked-list, and the pointer named head_node would be the only access to the records in the
linked-list. Consider the linked-list below as you determine how you would access the fields in
the second and third records.

Figure 10-4: A Linked-List with Three Records

202
In the example linked-list shown in Figure 10-4, the only access to the fields of the records is
through the pointer head_node, which is our handle to the list. Dot-notation can still be used to
access all these fields. For example, the field named that in the rightmost record can be
accessed using the identifier, head_node.next.next.that.

A Program that Builds a Linked-List

Let's look at a complete program that builds a linked-list of pointer records from user input and
then scans the list to display its contents.

with ada.text_io, ada.integer_text_io;


use ada.text_io, ada.integer_text_io;
procedure linked_list_demo is

type node; -- incomplete type definition of record node


type link is access node; -- link is a pointer to the record node
type node is record -- node is a record to hold an integer and
data : integer; -- a pointer to another node record
next : link;
end record;
start,curnode : link; -- pointers to node records
val : integer; -- temporary variable to hold input data
begin
start:=null;
loop
put("Enter next value (or -999 to end data entry)... ");
get(val);
exit when val=-999;
if start=null then -- if list is empty then start is to point to
start:=new node; -- the first record which is created with the
start.data:=val; -- new statement
start.next:=null;
else
curnode:=new node; -- if list is not empty then the new record
curnode.data:=val; -- is pointed to by curnode, this access
curnode.next:=start; -- pointer is used to point to the current
start:=curnode; -- record each time another value is entered
end if;
end loop;

curnode:=start; -- now curnode is used to traverse the list


while curnode/=null loop -- of integers created above, when curnode
new_line; -- points to null we have reached the end
put(curnode.data); -- of the list
curnode:=curnode.next;
end loop;

end linked_list_demo;

In this program, the pointer record named node contains an integer field called data and a pointer
field called next. The name for the access type in this program is link. The only memory
allocations that have been made at the beginning of execution are two pointer variables named
curnode and start (addresses to memory locations) and an integer variable named val.

In order to better understand the use of dynamic memory, we will sketch a graphical
representation of the dynamic memory structures created during the execution of this program.
As we walk through the execution of this program we will assume that the values 4, 6, 5, 8, and
finally -999 are entered by the user in this order. Before any data are entered, the program
initializes the pointer start with a special value called null. This is a value that is recognized by
Ada as an invalid address. We say the pointer start "points to" null. Graphically we show,

start
null

203
The program then displays the message asking for the user to enter a value. And, as stated, we
will enter the value 4.

Enter next value or (-999) to stop... 4

Initially, the value 4 is held as the integer variable val. The pointer variable, start points to null so
the if..then conditional is true, and the program creates a new node (i.e. a pointer record). This
new memory allocation is performed by the assignment statement,

start := new node;

Following the execution of this statement, the pointer start will point to a record with an integer
field named data and a pointer field named next, as shown graphically below,

We use question marks to indicate that these fields have not been assigned values. It is very
important that we distinguish between an unspecified pointer and one that is pointing to null. The
question mark means that we do not know which location in memory the pointer is pointing. In
contrast, pointing a pointer to null ensures that it is not pointing to any valid memory location.
The next two lines of code make the assignments to the fields of the record.

start.data:=val;
start.next:=null;

The value 4 is now placed in the data field while the link field is assigned the value null.

When assigning values to pointers (access types) we use the term "points to". Reading these
two lines of code, we would say "start dot data is assigned val," for the first and "start dot next
points to null" for the second.

We can show the result as,

start

4 null

We have reached the end of the loop so the program requests another value to be entered. The
next value entered by the user is a 6, which temporarily is placed in the integer variable val. This
time the pointer start does not point to null, so the if..then conditional is false, causing the else
portion of the if..then construct to be executed. First a new record is created but this time it is
pointed to by the pointer named curnode,

curnode:=new node;

We copy the value in val into the data field of this new record.

curnode.data:=val;

204
In this pass the program connects the next field of the new record to our linked list. As you will
see, the graphical representation of the data structure is very important to understanding program
operation. This program is creating a linked list with the most recently entered value listed first,
so the new record will be placed in the front of the list on each pass.

A general rule for creating a linked list is to make new connections before breaking old
connections. For example, if we were to point start at the newly created record we would lose our
access to the record containing the value 4. Instead, we first point the next field of the new
record at the record pointed to by start.

curnode.next:=start;

In this configuration, we can access the record containing the value 4 from the record containing
the value 6. However, we cannot access the record containing the value 6 from the record
containing the value 4. As the execution of the program continues, the pointer named curnode
will be used to point to other, newly created records so we need a permanent "handle" to access
all the records in our linked list of records.

The next line of code solves this problem by assigning the pointer start so that it points to the
record currently pointed to by curnode.

start:=curnode;

This graphic image can be simplified by omitting the pointer curnode (which will be reused on the
next iteration anyway) and redrawing the sketch with the linked records aligned. When redrawing
the sketch we are careful not to change the structure of the linked list we have, thus far, created.

We note that the list is being formed in reverse order. That is, if we begin scanning the list at the
pointer start, we encounter the values in the opposite order in which they were entered by the
user. Later we will see how to create linked lists in which the order of entry is preserved.

As you can see, the value of the graphical representation is that it helps us to gain a detailed
understanding of the operation of a program and to uncover programming errors. The reader
205
should use graphical representations to verify that, after two more iterations, the linked list data
structure is equivalent to the following.
start
8 5 6 4 null

Traversing a Linked-List

As mentioned earlier, the pointer named start is our only access to the records in this list. For
example, the value 4 is in the list above in the integer field named start.next.next.next.dat.

It is not practical to use dot-notation in this manner to gain access to all the fields of a long linked
list. Instead we can use a temporary pointer that is moved along the list to gain access to the
data. For example, we can use the pointer curnode to traverse this list. We first set curnnode to
point to the same record pointed to by start using the assignment,

curnode:=start;

In a while...loop we display the current value held in the record pointed to by curnode and then
we point curnode at the next record in the list. This continues until we have reached the end of
the list (that is, when curnode points to null).

while curnode/=null loop


new_line;
put(curnode.data);
curnode:=curnode.next;
end loop;

Figure 10-5: Graphical Display of the Execution of Read Loop in Linked_List_Demo

This code segment prints the list of four values in the order 8 5 6 4. When curnode points to null
the conditional of the while..loop is false and the program ends.
206
Through the remainder of this textbook we will be making frequent use of dynamic memory in the
form of linked-lists and other data structures. You can refer to the example applications in
Section 10.4.

10.2 Iteration vs. Recursion


One of the most important operations used in computer programming is looping, also called
repetition. We have learned a number of looping constructs in Ada such as the for..loop and the
while..loop. In this section, we will learn about an alternative to looping that can be used to solve
search-and-traversal problems in a much more efficient and powerful manner than would be
possible using other methods.
Consider the following function in which the value of the sum of the first n integers is computed
and returned to the calling program.
function sum_of_ints(n: integer) return integer is
sum : integer :=0;
begin
for i in 1..n loop
sum:=sum+i;
end loop;
return sum;
end sum_of_ints;

This program uses a for..loop to compute the value of sum, which is then returned to the calling
routine. This function could be called from a main program as illustrated in the following code
segment.

put("Enter the value for n... );


get(n);
put(sum_of_ints(n));

In this simple example, program control is passed to the function sum_of_ints( ) from within the
put( ) statement; then, the value of the sum is passed back to the calling program where the value
is output.

Now consider the alternative recursive version of this function for sum_of_ints( ) shown below.

function sum_of_ints(n : integer) return integer is


begin
if n <= 0 then
return 0
else
return sum_of_ints(n-1)+n;
end if;
end sum_of_ints;

If the value of n is 0 then this function returns a value of 0. So far, so good. But what happens
when a value of n > 0 is passed into this function? The else portion of the if..then..else construct
is executed. In this case, the function must call itself to compute the value of sum_of_ints(n-1)
before it can be added to the value n. But what is the value of sum_of_ints(n-1)? At the time of
the execution of this line of code the value is unknown, so the current copy of the function
sum_of_ints( ) is saved and another copy of sum_of_ints( ) is generated in order to determine the
value needed to compute the expression

sum_of_ints(n-1) + n

Each time the function is called with a value of the argument n that is greater than 0, the else
portion of the if..then..else construct calls another copy of the function. When the value of n is
finally less than 1, the conditional is true and this copy of the function returns a value of 0. Once

207
this value is returned to the calling function, it is added to 1 and returned to the next copy of the
function. This copy of the function, in turn, adds the 1 to 2 and returns a 2 to its calling routine.
This sequence of returns and summing calculations is repeated until the final value is returned to
the original call statement in the main program. For example, if the original call is sum_of_ints(3),
we would have,

sum_of_ints(3) ->
sum_of_ints(2) + 3 ->
(sum_of_ints(1) + 2) + 3 ->
((sum_of_int(0) + 1) + 2) + 3 ->
(0 + 1) + 2) + 3 <-
(1 + 2) + 3 <-
3 + 3 <-
6 <-

where the right arrow (->) indicates a call and the left arrow (<-) indicates a return. In this
example, none of the return values can be computed until the if..then..else conditional is true. For
this reason, up to n copies of the function sum_of_ints( ) will be saved at some time during the
execution of this program.

Recurrence Relations

At this point you may be wondering why anyone would bother with writing a recursive program
when a loop construct seems to work as well and is much easier to understand. Many important
solutions to problems in applied mathematics are described as recurrence relations, which can be
directly implemented as recursive programs. For example, the recurrence relation for our
sum_of_ints( ) function is given as,

S (n − 1) + n for n > 0
S ( n) =
0 for n ≤ 0

The curly bracket on the right side of this relation indicates that we have two choices for the
formula for S(n). This mathematical notation is equivalent to an if..then..else construct in a
computer program. If n is greater than or equal to 0, then S(n) = 0. Otherwise (else), n is greater
than 0, and S(n) is S(n-1) + n. In this case, we must compute S(n-1) before we can proceed.
Substituting n-1 into the recurrence relation for n gives,

S (n − 1) = S (n − 2) + (n − 1)

which means that we have to determine a value for S(n-2) before we can compute a value for
S(n-1). This sequence of deferred computations ends when we reach n=0. Then the values for
S(1), S(2), . . . ,S(n-1) are computed and passed back to the calling expression until S(n) is
reached. We can implement such recurrence relations as recursive programs. Let's look at
another example.

The calculation of the product of the first n natural numbers, called n factorial (n!), can be
expressed as a recurrence relation.

n × fact (n − 1) for n > 1


fact (n) =
1 for n ≤ 1
208
In the function below, either the termination condition is met (i.e. n<=1) and the function directly
returns a value or it is not met (n>1) and the function calls a copy of itself with the value of its
argument reduced by one.

function fact(n : integer) return integer is


begin
if n<=1 then
return 1;
else
return n*fact(n-1);
end if;
end fact;

The common features of recursive programs are

(1) they have a termination condition


and
(2) the recursive call moves us closer to the termination condition.

It is important to understand that each time the function fact( ) is called, a different copy of the
function is created and held in memory until its computation is completed. When program control
reaches the end of a particular copy of fact( ), that copy of the function is removed from memory
and the code segment that called this copy is reloaded and becomes the active copy. The point
of execution for this reloaded copy is set to the point in the program where the last recursive call
was made. When fact(n) is called by a program, there will be at most n copies of fact( ) in active
memory during the computation. When the value of fact(n) is returned to the original calling
program, all the copies of fact( ) will have been executed and eliminated from active memory (i.e.
the memory space they occupied will be available for other uses by the computer. Figure 10-6
shows a graphical representation of the sequence of recursive calls made by the function fact( ).

Figure 10-6: Demonstration of the Execution of the Recursive Function fact( )

The memory required to hold the copies of a recursive function can be large. For this reason we
should not use recursion when a simpler and more efficient construct such as repetition can be
used effectively. For example, consider the following iterative version of the factorial function.

function fact(n : integer) return integer is


f : integer:=1;
begin
for i in 1..n loop
f:=f*i;
end loop;
return f;
end fact;
209
Just as with the sum_of_ints( ) function there is a version of the factorial function that uses
repetition. This version is faster than the recursive version of the factorial function since it does
not invoke n function calls. It is also more efficient since it requires memory space for only one
copy of itself.

We will look at one more example of the implementation of a recurrence relation in a recursive
function. The recurrence relation for the combination function C(n,k) is shown below:

C (n − 1, k − 1) + C (n − 1, k ) for n > k > 0


C (n, k ) =
1 for (n = k ) or (k = 0)

This function turns up in a wide range of applications, such as computing the values of the
coefficients for a binomial expansion
n n 0 n-1 1 n-2 2 1 n-1 0 n
(X+Y) = C(n,0)X Y + C(n,1)X Y + C(n,2)X Y + . . . +C(n,n-1)X Y + C(n,n)X Y

or the number of ways to choose k items out of a set of n items. For example, the number of
ways to choose 2 items from a set of 4 items (where the order of the chosen items does not
matter) is,

C(4,2) = C(3,1) + C(3,2)


C(4,2) = C(2,0) + C(2,1) + C(2,1) + C(2,2)
C(4,2) = 1 + C(1,0) + C(1,1) + C(1,0) + C(1,1) + 1
C(4,2) = 1 + 1 + 1 + 1 + 1 +1 = 6

You may recognize C(n,k) as the generating function for Pascal's Triangle in which each internal
value is calculated as the sum of the adjacent values from the previous row.
k=0

k=1
n=0 1

k=2
n=1 1 1

k=3
n=2 1 2 1

k=4
n=3 1 3 3 1

k=5
n=4 1 4 6 4 1

k=6
n=5 1 5 10 10 5 1

n=6 1 6 15 20 15 6 1
Figure 10-7: Pascal's Triangle

210
We can write a recursive function to compute the value of C(n,k),

function comb(n,k : integer) return integer is


begin
if n=k or k=0 then
return 1;
else
return comb(n-1,k-1) + comb(n-1,k);
end if;
end comb;

Note that the structure of the recursive function closely follows the form of the recurrence relation.
In this example, the design of the recursive version of this function is straightforward, while the
implementation of the recurrence relation as an iterative function is more difficult.

These examples have been chosen because they are simple implementations of recursive
functions not because they are necessarily the best methods of solving the associated problems.
There are simpler and more efficient solutions possible using iterative methods. Later we will
make effective use of recursion for building and traversing complex data structures in which
iteration would be cumbersome.

10.3 Algorithm Analysis

Most of the programs we have studied so far are relatively simple with short run times. We are
now beginning to implement programs that can require significant run times. As a part of the
program design process, we will need to estimate the run times for our programs to be sure that
they will complete in a reasonable amount of time.

Since a program's speed of execution in terms of operations per second depends on the CPU
clock rate, which depends on the particular machine on which it is running, we do not normally
analyze program run times in terms of seconds or hours. Instead, we count the total number of
operations required to complete execution of the program as a function of the size of the problem
being solved. A couple of examples are useful in illustrating how this is done.

Example 1: Determine the worst case (maximum number of key comparisons) run time for a
program to find a particular value in an unordered list S( ) of n values.

In this example, we do not know the position of the target value in the list S( ) or even if the list
contains the target value. In the worst case, we will need to check every value in the list so the
run time for an algorithm solving this problem is n key comparisons.

Example 2: Determine the worst case run time for a program that finds a particular value in an
ordered list T( ) of n values.

In this case, we can check the middle value in the list T(n/2). If the target value is larger than the
value at this location, we know that, it is not one of the values between T(1) and T(n/2). On the
other hand, if the target value is smaller than T(n/2) then we know that the it is not one of the
values between T(n/2) and T(n). In either case a single key comparison we will eliminate 1/2 of
the values in the list. With each successive comparison we will eliminate half the remaining
values. For example, consider the number of key comparisons required to find a value in an
order list of 100 values. For each comparison we reduce the number of values by a factor of two.

100 -> 50 -> 25 -> 13 -> 7 -> 4 -> 2 -> 1

Certainly we could find the target value quicker, but, in the worst case, it will take us 7 key
comparisons to find the target value in a list of 100 values or show that it is not in the list.

211
Methods of Analysis

The run time of a program is proportional to the number of operations performed in the
completion of its execution. Since the number of operations is related to the size of the problem
being solved, we can express the run time in terms of the size of the problem (e.g. the number of
elements in the input data set). Let's looks at a few code segments and their corresponding run
times expressed in numbers of computations.

Sequence - Lists of program statements that require a fixed amount of processing require a
constant run time.

x:=1;
y:=2;
put(x+y);

Since this code is executed only once we say that it contributes a constant or fixed amount of
time to the total run time of the program. This means that the time required to perform this
sequence of statements is not affected by the size of the data set used elsewhere in the program.

Alternation - Any statement that directs program flow based on the value of a conditional requires
a run time that is the weighted average of the run times for each of the alternative computations.

if conditional then
alternative_1
else
alternative_2
end if

For example, if the conditional above selected the first alternative with probability t and the
second alternative with probability 1-t then the run time for this segment would be given by,

avg_runtime = (t)x (runtime for alternative 1) + (1-t)x(runtime for alternative 2)

Repetition - Program loops are represented by powers of the loop index since this indicates the
number of times the body of the loop will be executed.

for i in 1..n loop


code block
end loop;

The run time for this code segment would be (n) times the runtime for the code block contained in
the loop. This code block can, itself, contain a loop as shown below.

for i in 1..n loop


some code
for j in 1..m loop
other code
end loop;
end loop;
The runtime for this nested loop is of order (n)x(m).

Recursion - Recursive programs are analyzed by solving their associated recurrence relations.
Complexity analysis of recurrence relations is beyond the scope of this text, but we will use an
intuitive graphical approach to estimate the runtime for recursive algorithms. For example,
consider the factorial function introduced in the previous section.

212
Each time fact( ) is called with a value of its argument n>1, another call is made to fact( ). Since
a single recursive call is made at each level, the recursion is linear (i.e. the run-time graph is a
single line of calls as shown in Figure 10-8.

n-1

function fact(n : integer) return integer is .


begin . depth = n
if n<=1 then .
return 1;
else
return n*fact(n-1);
2
end fact;

Figure 10-8: Run Time of fact( )

Each box in the figure represents one copy of the function fact( ) with the value in the box
representing the value of the function argument. Since the depth of the recursion is n and there
is one call to fact( ) at each level, we can see that the run time for this function is proportional to
n.

Now consider the recursive function foob(n) shown below, which calls itself twice for each non-
terminal call. This is not a practical or useful function. It is just a simple example of a recursive
function with a run time that is greater than n.

function foob(n : integer) return integer is


begin
if n<=1 then
return 1;
else
return foob(n-1)+foob(n-1);
end if;
end foob;

When the argument n of foob(n) is greater than 1, the function makes two calls to foob(n-1). This
causes a doubling of the number of foob( ) calls at each level. Figure 10-9 on the next page
illustrates the calls to foob( ) for n=4.

213
4

3 3

2 2 2 2

1 1 1 1 1 1 1 1

Figure 10-9: Graphical Representation of the Recursive Function foob( )

Since the argument is diminished by 1 at each level, the height of the figure will be n. However,
the number of copies of the function doubles at each level so the total number of calls to foob(n)
n
is 2 -1 for a initial call to foob(n). Given that there is a constant amount of work done in each call,
n
the order of the run time for foob(n) is directly proportional to 2 -1. For example, if each copy of
foob( ) represents p operations, the total number of operations required to complete the execution
n n
of foob(n) would be p(2 -1) = p2 -p. We will see that the part of this expression that is the most
n
important to us is the growth rate as a function of n, which is 2 .

We will look at one more example of run time analysis for a recursive function. Let's modify
foob( ) so that the argument is reduced by a factor of 2 at each level. We'll call this function
feeb( ).

function feeb(n : integer) return integer is


begin
if n<=1 then
return 1;
else
return feeb(n/2)+feeb(n/2);
end if;
end feeb;

In feeb( ), we see that the argument n approaches the termination condition faster. Rather than
decrementing the argument by 1 at each level, it is divided by 2. How many calls are required to
reach the termination condition? This is the same as asking: How many times can we divide n
by two before we reach a value of 1?

n n n
n, , , , , 2, 1
2 4 2i

n n
The first term in the sequence above is 0
, the second term is 1 , and so on. We need to
2 2
n
determine for what value of k does = 1 ? We can determine this by,
2k
n
=1 n = 2 k so k = log 2 (n)
2k
This means that the depth of the recursion for feeb(n) is log2(n).
214
n

n/2 n/2

depth n/4 n/4 n/4 n/4

log2(n)

. .
. .
. .

2 2 2 2

...
1 1 1 1 1 1 1 1

Figure 10-10: Graphical Representation of the Recursive Function feeb( )


n
Previously, we noted that since the depth of recursion was n, the number of calls would be 2 -1.
k
Using the same method, we know that, in this case, the total number of calls will be 2 -1, which is
2 log 2 ( n ) − 1 or just n-1.
Orders of Run Time

The growth rate of a function of n is an indication of its eventual behavior with increasing problem
size n. The chart in Figure 10-11 compares the growth rates of a number of common functions.
These various curves show the number of operations (y-axis) as a function of the problem size n
(x-axis).

It is important to understand that we are most interested in the run time as the problem size
continues to grow. We can see that the run time for smaller problem sizes can be different than
the run time behavior for larger problem sizes. For example the run times represented by the
2 3 n
polynomial functions n and n exceed the run times represented by the exponential function 2
n
for small values of n. However the exponential function 2 eventually passes every polynomial
k
function n for sufficiently large values of n and any fixed value k. It is the eventual behavior of
these functions for increasing n that is important in algorithm analysis.

For small problem sizes (such as the region shown inside the rectangle) there is no real
advantage of a polynomial-time algorithm over an algorithm running in exponential time.
However, exponential run times severely limit the sizes of problems that can be solved (see
Application 10.2)

215
Number of
Operation
s f(n)

Problem Size, n

Figure 10-11: Comparison of Common Runtimes

Beyond some problem size N the value of an exponential function will exceed any polynomial
function. The problem is not just that it exceeds the polynomial, it’s also the rate at which it
exceeds it.

10.4 Applications

10.1: Graphical Demonstration of a Linked List - Build an Ada program that generates a
graphical demonstration of the linked list program given in Section 10.1. This program should
show the creation of the list as the user enters values to be added to the list. When the special
value to end data entry (the sentinel value) is entered the program should show a graphical
demonstration of the list traversal and recovery of the data in the linked-list.

In order to build a graphical demonstration we will start with the working program and make the
necessary additions and modifications to display the programs operation. The following listing is a
copy of the program linked_list_demo from Section 10.1 with the comments removed.

with ada.text_io, ada.integer_text_io;


use ada.text_io, ada.integer_text_io;
procedure linked_list_demo is

216
type node;
type link is access node;
type node is record
data : integer;
next : link;
end record;
start,curnode : link;
val : integer;
begin
start:=null;
loop
put("Enter next value (or -999 to end data entry)... ");
get(val);
exit when val=-999;
if start=null then
start:=new node;
start.data:=val;
start.next:=null;
else
curnode:=new node;
curnode.data:=val;
curnode.next:=start;
start:=curnode;
end if;
end loop;

curnode:=start;
while curnode/=null loop
new_line;
put(curnode.data);
curnode:=curnode.next;
end loop;

end linked_list_demo;

Each record will be displayed as a rectangular box divided into two cells, one cell to show the
data value and the other to represent the link (pointer) field. In this program, the data values are
type integer. When we consider how we might display these using the tools available in
Adagraph, it is clear that converting integers into text strings for the display_text( ) procedure will
be an unnecessary complication. We decide to change the data field into a character type for the
graphical demonstration. This will also simplify data entry since characters can be read using the
get_key function rather than forcing the user to switch to the console window for data entry.
type node;
type link is access node;
type node is record
char : character;
next : link;
end record;
When developing a graphical demonstration it is important to avoid hardwiring the drawing points
as literal values. It is much better to define a small number of constants for the sizes of drawing
objects and then to make all the drawing points relative to these values. In this case, we will
define the dimensions of the graphics window as px_max and py_max, the dimensions of the
pointer record display rectangle as node_x and node_y, and the separation between nodes in the
linked list display as sep.
px_max : constant integer := 750;
py_max : constant integer := 120;
node_x : constant integer := 28;
node_y : constant integer := 16;
sep : constant integer := 10;
217
We will start with the main program. Since this is a graphics demonstration, we will need to open
a graphics window and set it to the background color.

open_graph_window(px_max,py_max);
clear_window(black);

Since we are now using the get_key function for data entry we need to modify the data entry code
segment.

char:=get_key;

The loop exit condition also needs to be changed as well as the sentinel value. We will use the
character X (either upper- or lowercase) as the new sentinel value. Also the number of nodes in
the linked list will need to be limited to the number of nodes that can be displayed in the graphics
window. We will count the number of nodes entered (node_count) and exit the data-entry loop
when some maximum node_count is reached.

node_count:=node_count+1;
exit when char='x' or char='X' or node_count>max_nodes;

We will determine a value for the maximum node_count a little later. For now, we need to make a
few decisions about the details of the graphical demonstration. For each value entered (i.e. each
key pressed), we will redisplay the linked list with the new record added to the start of the list.
The display window is cleared to the background color just before the linked list is redisplayed.

start:=null;
loop
clear_window(black);
draw_list;
char:=get_key;
node_count:=node_count+1;
exit when char='x' or char='X' or node_count>max_nodes;
if start=null then
start:=new node'(char,null);
else
curnode:=new node'(char,start);
start:=curnode;
end if;
end loop;

Now we can turn our attention to the draw_list procedure. We need to decide where in the
graphics window the list will be drawn. We can define a drawing point (px,py) that is updated as
the list is drawn. We will use our graphics plot point constants to define the initial drawing point
as shown below.

px := sep;
py := node_y/2+sep;

The head node start will be displayed simply as the string "start" in the graphics window using a
display_text( ) procedure.

display_text(px,py,"start",white);

Since this string occupies a fixed number of pixels, we can update the value of px to move the
plot point to the right of this string in the display. A little experimentation shows that 50 pixels is a
reasonable shift for px.

px := px + 50;

218
From this point, we will draw the link itself as a light_green line and the null address will be
displayed as a forward slash.

draw_line(px-node_x/8,py+node_y/2,px+sep,py+node_y/2,light_green);
:
display_text(px,py+node_y/2-8,"/",light_green);

For each record in the list we will display a rectangular box with dimensions node_x and node_y
with a separation of sep between boxes. The record box will be separated into a data field
showing the character of the corresponding record and a link field showing a link to the next
record or to the null address. During the drawing process, the linked-list is traversed using a
while..loop.

curnode:=start;
while curnode/=null loop
draw_node(px,py,curnode.char,white);
curnode:=curnode.next;
end loop;

Since the graphical representation of a node (record) is a bit involved we have decided to put the
details into a separated procedure called draw_node( ). This procedure is passed the current plot
point px,py, the contents of the data field of the current record curnode.char, and the color of the
lines (white in this case). The reason for passing the line color as a parameter is so that we might
draw nodes with other colors for animation effects or with the background color to erase nodes.

We are careful to make effective use of the graphics constants defined earlier. For example, the
drawing points of the record box are defined in terms of the current drawing point px,py and the
box dimensions.

draw_box(px,py,px+node_x,py+node_y,col,no_fill);

The line that partitions the record box into two fields is placed 2/3 of the way along the box width.
The character in the data field is displayed using the display_text( ) procedure. As shown below,
this Adagraph procedure requires a string data type.

str(1):=ch;
draw_line(px+2*node_x/3,py,px+2*node_x/3,py+node_y,col);
display_text(px+2,py+2,str(1..1),yellow);

The link to the next record is shown as a light_green line from the link field of the current record
box to a point a distance of sep beyond the right-hand edge of this box. Finally, the value of the
plot point px is updated to this new position and passed back to the calling routine.

procedure draw_node(px, py : in out integer;


ch : character; col : in extended_color_type) is
str : string(1..1);
begin
str(1):=ch;
draw_box(px,py,px+node_x,py+node_y,col,no_fill);

219
draw_line(px+2*node_x/3,py,px+2*node_x/3,py+node_y,col);
display_text(px+2,py+2,str(1..1),yellow);
px:=px+node_x;
draw_line(px-node_x/8,py+node_y/2,px+sep,py+node_y/2,light_green);
px:=px+sep;
end draw_node;

For a list of three records the draw_list procedure would call draw_node( ) three times to produce
an image similar to the one shown below.

We need a way to prevent the demonstration program from accepting more nodes than can be
displayed in the graphics window. Each record requires node_x + sep pixels so we can use this
value and the declared graphics window width px_max to compute a maximum node_count for
the linked list. Since this calculation uses only values that are already declared we can place the
expression to compute max_nodes into the program declaration block.

max_nodes : constant integer := (px_max-50)/(node_x+sep)-1;

In the original program, the finished list is traversed as the data values are recovered and
displayed. We wish to show this process in our graphical demonstration. We will replace the list
traversal while..loop of the main program with a call to a procedure called traverse_list. This
procedure will traverse the list as it generates a graphical animation of the process. The
draw_node( ) procedure can be reused to show which node is being accessed by redrawing it in
light_red. We will display the pointer curnode as the string "curnode" with a line pointing to the
associated record. We use the Ada delay( ) procedure to control the speed of the animation.

curnode:=start;
while curnode/=null loop
display_text(px,py+2*node_y,"curnode",white);
draw_line(px+node_x/3,py+2*node_y-3,
px+node_x/3,py+node_y,light_green);
str(1):=curnode.char;
delay(0.5);
draw_line(px+5,py+node_y+1,dpx,dpy-3,black);
display_text(px,py+2*node_y,"curnode",black);
draw_line(px+node_x/3,py+2*node_y-3,
px+node_x/3,py+node_y,black);
draw_node(px,py,curnode.char,light_red);
curnode:=curnode.next;
end loop;

We will display the data values being recovered from the linked-list using the display_text( )
procedure. In order to clearly indicate from which record each value is being obtained, we will
momentarily display a line (light_blue) between the data value and its associated record.

220
A few other details are added to improve the animation, such as showing each new record before
it is added to the linked-list. The completed program is listed below.

with ada.text_io, adagraph;


use ada.text_io, adagraph;
procedure link_demo is
px_max : constant integer := 750;
py_max : constant integer := 120;
node_x : constant integer := 28;
node_y : constant integer := 16;
sep : constant integer := 10;
max_nodes : constant integer := (px_max-50)/(node_x+sep)-1;
type node;
type link is access node;
type node is record
char : character;
next : link;
end record;
start,curnode : link;
char : character;
node_count : integer := 0;
npx : integer;
npy : integer := 2*(node_y/2+sep);

procedure draw_node(px, py : in out integer;


ch : character; col : in extended_color_type) is
str : string(1..1);
begin
str(1):=ch;
draw_box(px,py,px+node_x,py+node_y,col,no_fill);
draw_line(px+2*node_x/3,py,px+2*node_x/3,py+node_y,col);
display_text(px+2,py+2,str(1..1),yellow);
px:=px+node_x;
draw_line(px-node_x/8,py+node_y/2,px+sep,py+node_y/2,light_green);
px:=px+sep;
end draw_node;

procedure draw_list is
curnode : link;
px,py : integer;
begin
px := sep;
py := node_y/2+sep;
display_text(px,py,"start",white);
px := px + 50;
curnode:=start;
draw_line(px-node_x/8,py+node_y/2,px+sep,py+node_y/2,light_green);
px:=px+sep;
while curnode/=null loop
draw_node(px,py,curnode.char,white);
221
curnode:=curnode.next;
end loop;
display_text(px,py+node_y/2-8,"/",light_green);
end draw_list;

procedure traverse_list is
curnode : link;
px,py : integer;
dpx,dpy : integer;
str : string(1..1);
begin
dpx:= sep;
dpy:=py_max-15;
px := sep;
py := node_y/2+sep;
display_text(px,py,"start",white);
px := px + 50;
curnode:=start;
draw_line(px-node_x/8,py+node_y/2,px+sep,py+node_y/2,light_green);
px:=px+sep;
while curnode/=null loop
display_text(px,py+2*node_y,"curnode",white);
draw_line(px+node_x/3,py+2*node_y-3,
px+node_x/3,py+node_y,light_green);
str(1):=curnode.char;
delay(0.5);
draw_line(px+5,py+node_y+1,dpx,dpy-3,light_blue);
display_text(dpx,dpy,str(1..1),yellow);
delay(0.5);
draw_line(px+5,py+node_y+1,dpx,dpy-3,black);
dpx:=dpx+20;
display_text(px,py+2*node_y,"curnode",black);
draw_line(px+node_x/3,py+2*node_y-3,
px+node_x/3,py+node_y,black);
draw_node(px,py,curnode.char,light_red);
curnode:=curnode.next;
end loop;
display_text(px,py+node_y/2-8,"/",light_green);
end traverse_list;

begin
open_graph_window(px_max,py_max);
clear_window(black);
start:=null;
loop
draw_list;
while not(key_hit) loop
null;
end loop;
char:=get_key;
node_count:=node_count+1;
exit when char='x' or char='X' or node_count>max_nodes;
if start=null then
start:=new node;
start.char:=char;
start.next:=null;
else
curnode:=new node;
curnode.char:=char;
curnode.next:=start;
start:=curnode;
222
npx := sep;
draw_node(npx,npy,char,white);
delay(0.5);
end if;
clear_window(black);
end loop;
curnode:=start;
traverse_list;
display_text(px_max/2-60,py_max/2,"PRESS ANY KEY", yellow);
while not(key_hit) loop
null;
end loop;
close_graph_window;
end link_demo;

10.2: Estimating Runtime Performance - Assume that you are given an algorithm that exhibits an
n
exponential (i.e. order 2 ) runtime. This means that the runtime of the problem doubles every
time you add one item to the problem size. You perform benchmark tests and discover that, on
the current computer system, a problem of size n=500 can be completed in 1 hour (this is the
maximum time your application is permitted for solving the problem). You know that it may be
necessary to solve problems larger than size n=500 on occasion but the maximum problem size
cannot be determined at this time.

To ensure that future problems larger than size n=500 can be solved, you recommend the
purchase of a new computer system with 1000 times the processing speed of the current system.
This means that, on the new system, the problem of size n=500 can be solved in 1/1000 hours or
3.6 seconds. Given that a problem still must be solved within 1 hour, how large a problem can be
solved using this new computer system?

Since the runtime doubles with each addition of 1 in the size of the problem, the runtime required
to complete a problem of size m = n + k is given by,

2 m = 2 ( n+ k ) = 2 n 2 k
n
We already know that 2 (for n=500) is 1/1000 hr or 3.6 seconds, so we are looking for the
k
smallest value of k which results in 2 begin greater than 1000. This is k=10. Therefore our new
computer system fails to complete the task in time for problems of size 510 or larger. Just 10
more items added to the 500 exceed the hardware performance limits. This example illustrates
why we can't just buy our way around problems that require exponential runtime algorithms for
their solution.

223
Exercises

10.1 Sketch the dynamic memory created by the following code segment. Assume that
headnode, newnode, and curnode are pointers to the pointer record defined.

type node; headnode:= new node;


type pointer is access node; headnode.data:=1;
type node is record headnode.next:=null;
data : integer; curnode:=headnode;
next : pointer; for i in 2..5 loop
end record; newnode:=new node;
headnode : pointer; newnode.data:=i;
newnode : pointer; newnode.next:=curnode.next;
curnode : pointer; curnode.next:=newnode;
end loop;

10.2 Write a recursive function for computing the value of the Fibonacci numbers for a given
value of n. Each Fibonacci number is the sum of the two previous Fibonacci numbers. A
generator for Fibonacci numbers is defined by the following recurrence relation:

fibo(n − 1) + fibo(n − 2) for n > 1


fibo(n) =
n for n = 0,1

10.3 Write an iterative version of the Fibonacci number generating function in Exercise 10.2.

10.4 Determine the order of the run time of the following code segments in terms of the problem
size.

a. b.

for i in 1..n loop for i in 1..n loop


some code here
for j in 1..m loop
end loop;
some code here
end loop; for j in 1..n loop
end loop; some code here
end loop;

c. d.
function feeble(n)is function foible(n) is
begin begin
if n<=1 then if n<=1 then
return 1; return 1;
else else
return n+feeble(n/2) return foible(n)
end if; end if;
end feeble; end foible;

224
Chapter 11 - Introduction to Abstract Data Types (ADTs)

11.1 An Example ADT


An Abstract Data Type for Complex Numbers
Arithmetic Operations on Complex Numbers
Designing an ADT for Complex Numbers
The adt_complex Package Specification
The adt_complex Package Body
Testing the adt_complex Package

11.2 Lists
Array Implementation, the adt_bounded_list
Linked List Implementation, the adt_list
Implementation of adt_list

11.3 Stacks
Implementation of adt_stack using adt_list
Array Implementation - adt_bounded_stack

11.4 Queues
Direct Linked List Implementation of a Queue
Implementation of adt_queue using adt_list

11.5 Applications
The Mandelbrot Set Generator
RPN Arithmetic Expression Evaluator
Infix to Postfix Expression Converter
Drawing the Centipede

225
Chapter 11 - Introduction to Abstract Data Types (ADTs)
In this chapter, we formalize the concept of an abstract data type (ADT) as a data structure and
the functions and procedures that give the programmer access to it. We will build ADTs for a
number of common structured data types including the list, stack, and queue.

11.1 An Example ADT

We are already familiar with the predefined data types such as float, integer, character, and
string. We have also studied programmer-defined arrays and records of these literal types.
When working with such data types in Ada, we use functions and procedures provided in
packages that have been created and included as part of the integrated development
environment. For example, the packages ada.text_io, ada.integer_text_io, and ada.float_text_io
give us routines to access and manipulate the predefined data types mentioned earlier. When we
create our own data types we must provide the functions and procedures needed to make use of
these new data types.

In Chapter 6, we created record data types, array types, and arrays of records. Since these were
not predefined data types, we had to build our own routines to read, write, and use the
components of these data types. The source code for this collection of supporting functions and
procedures was integrated with the main program. In Chapter 8, we learned that we could place
functions and procedures into a separate compilation unit, such as a package, to simplify our
application programs. We also learned that once the routines in a package were debugged and
compiled they could be reused in other application programs without the need for further work.
We will now formalize this technique of hierarchical software development.

An abstract data type (ADT) is a data entity with its associated set of mathematical and logical
operations (called the interfaces) on this entity in which the details of implementation are hidden
from the application programmer (i.e. the user of the ADT). At first, it may seem odd that we
would want to hide the details of the implementation from the user; however, we will see that this
hiding or abstraction is a powerful and important tool in programming.

An Abstract Data Type for Complex Numbers

As a first example, we will build an ADT for complex numbers. Before designing the adt_complex
package, we need to review the properties of complex numbers. Let p = a+bi represent a
complex number, where i = − 1 . The number a is the real part and b is the imaginary part of p.
Graphically, a complex number is represented as a point in the complex plane.

Figure 11-1: Definition of a Complex Number

226
Arithmetic Operations on Complex Numbers
The basic arithmetic operations of addition, subtraction, multiplication, and division on complex
numbers are given by,

where p and q are complex numbers. Notice that for each operation there is a real part and an
imaginary part that remains separated. The + sign between the real and imaginary parts is not a
mathematical operation but rather a notational separator.

Designing an ADT for Complex Numbers

The ADT for complex numbers should permit the user (application programmer) to create, read,
and display complex numbers as well as compute the results of +, -, *, and /. The user also
needs to extract the real and imaginary components as separate entities (real values). However,
the user should not be given direct access to the components of a complex number. All these
functions should be provided as part of the ADT interface.

Choosing a Data Representation - We first need to establish an internal representation for


complex number types for our ADT. In the package adt_complex, we can define a record that
holds the real and imaginary components of a complex number as fields of type float.

type complex is record


real : float;
imag : float;
end record;

As you may recall from Chapter 8, we can declare parts of a package as private. This is done
when we want to restrict the application programmer (i.e. the user of the package) from certain
details of the package specification. We will make the complex type private so that the user of
the adt_complex package will not be able to manipulate directly the real and imaginary
components of a complex number.

Reading and Writing Complex Numbers - As part of the adt_complex interface we include get( )
and put( ) procedures for reading and writing complex numbers. These procedures can be
implemented in a number of ways. In our version of this ADT, the complex get( ) procedure will
read a pair of values separated by space representing the real and imaginary components of a
complex number. The complex put( ) procedure will display the complex number as,

real_part + i imaginary_part

The specifications for the get( ) and put( ) procedures do not show these components. Instead,
we simply pass a complex value as shown below.

procedure get(c:out complex);


procedure put(c:in complex);

It is not clear, from the specification, that the user must enter two values when the complex get( )
is called. Rather, it shows that a complex number is expected. We need to include some
additional information in the package specification in the form of comments to guide the

227
application programmer in the proper use of our abstract data type. This can be as simple as a
comment clarifying the usage of the procedure.

-- when using the get( ) procedure to accept a complex value


-- enter the real and imaginary coefficients
-- separated by a space
procedure get(c:out complex);

Arithmetic Operations – If we write functions for adding, subtracting, multiplying, and dividing
complex numbers in the normal way, these functions would look something like the following,

function add(a,b: complex) return complex;


function sub(a,b: complex) return complex;
function mul(a,b: complex) return complex;
function div(a,b: complex) return complex;

Consider the following example in which p, q, and r are complex types.

r = (p-q)/(p+q)*p

When this expression is written as an Ada assignment statement using the arithmetic functions
defined above, it becomes,

r := mul(div(sub(p,q),add(p,q)),p);

which is difficult to read and therefore likely to cause errors. Instead of using standard functions
for the interface as given above, Ada provides us with an alternative. The basic arithmetic
operations can be written as special types of functions, called in-fix functions, in order to simplify
expressions using complex values. We change the names of these functions into their
corresponding arithmetic symbols.

function "+"(a,b: complex) return complex;


function "-"(a,b: complex) return complex;
function "*"(a,b: complex) return complex;
function "/"(a,b: complex) return complex;

Placing the symbols in quotes permits us to overload the arithmetic operators for our complex
data types. To add two complex numbers p and q we simply type p+q rather than add(p,q). The
compiler knows which type of add to invoke based on the data types being added. The complex
arithmetic expression of our previous example becomes,

r := (p-q)/(p+q)*p;

which is more readable and therefore less likely to cause implementation errors. The ability to
write functions as in-fix operators and the ability to use the same arithmetic operator symbols for
complex values that are used for floating-point and integer values are powerful features of the
Ada programming language.

Composing and Decomposing Complex Numbers - The application programmer may want to
create complex numbers within a program or to extract the values of the real and imaginary
components of a complex number. This capability can be implemented by including in our
adt_complex package functions such as make_complex( ), get_real( ), and get_imag( ).

function make_complex(r,i : float) return complex;

Using make_complex(real,imag), the programmer can create a complex number by passing the
real and imaginary parts into the function as a pair of floating-point values. The interface should
also permit the programmer to extract the real and imaginary parts from a complex number.

228
function get_real(c: complex) return float;
function get_imag(c: complex) return float;

These functions return the components of a complex number as floating-point values.

Computing the Magnitude - Another common operation performed on a complex number p is the
calculation of |p|, the absolute value or magnitude of p. The absolute value of a complex number
is a real value representing the distance of the complex number from the origin in the complex
plane (see Figure 11-1). This value is computed by,

p = a + ib
p = a2 + b2

where a and b are the real and imaginary components of the complex number p. We will define
an interface function to compute the absolute value of p, or abs(p), and return it as a floating-point
value.

function "abs"(c:complex) return float;

If we enclose the function name abs in quotes we can convert the function into a unary operator
(i.e. an operator with a single parameter). The difference between a unary operator and a
function is that the operand does not need to be placed in parentheses. For example,
x := abs p;
where x is a floating-point value and p is a complex number.

The adt_complex Package Specification

We will use a standard package compilation unit to build an ADT for complex numbers; therefore,
we will need to create a package specification and a package body. We designate the complex
record type as private in the package specification. A complete listing of the adt_complex
specification is shown below.
with ada.float_text_io;
use ada.float_text_io;
package adt_complex is

type complex is private;

-- when using the get( ) procedure to accept a complex value


-- enter a pair of float values separated by a space
procedure get(c:out complex);
procedure put(c:in complex);
function "+"(a,b: complex) return complex;
function "-"(a,b: complex) return complex;
function "*"(a,b: complex) return complex;
function "/"(a,b: complex) return complex;
function make_complex(r,i : float) return complex;
function get_real(c: complex) return float;
function get_imag(c: complex) return float;
function "abs"(c:complex) return float;

private
type complex is record
real : float;
imag : float;
end record;

end adt_complex;
229
This specification is saved with the name adt_complex.ads and is compiled as a separate
compilation unit. Note that this specification does not show any of the details of implementation
for the interface functions and procedures, but it does provide the application programmer with all
the information needed to use the adt_complex package.

The adt_complex Package Body

The package body for adt_complex contains complete implementations of the functions and
procedures listed in the specification. The body uses the data types defined in the specification
so they are not re-declared. The package body must have the same name as the specification
but with an .adb file extension (i.e. adt_complex.adb). We include in the partial listing below
some of the arithmetic operations for the adt_complex interface. The adt_complex interface
makes use of other packages such as ada.text_io and ada.float_text_io so they must be included
as part of the package body. The square root sqrt( ) is used in the abs( ) function so we include
the mathematics package ada.numerics.elementary_functions as well.

with ada.text_io,ada.float_text_io,ada.numerics.elementary_functions;
use ada.text_io,ada.float_text_io,ada.numerics.elementary_functions;
package body adt_complex is

function "+"(a,b: complex) return complex is


c : complex;
begin
c.real:=a.real+b.real;
c.imag:=a.imag+b.imag;
return c;
end "+";

function "*"(a,b: complex) return complex is


c : complex;
begin
c.real:=a.real*b.real-a.imag*b.imag;
c.imag:=a.real*b.imag+b.real*a.imag;
return c;
end "*";

-- other functions and procedures go here

function "abs"(c:complex) return float is


begin
return sqrt(c.real*c.real+c.imag*c.imag);
end "abs";

end adt_complex;

Even though the complex data type is private, the functions and procedures in the package body
can access the individual fields of the complex type using dot-notation. Making the complex type
private prevents the use of dot-notation to access the real or imag fields of the record in a
program that is using the package, but it permits the application programmer to declare variables
as type complex.

The completion of the adt_complex package listing shown above has been left as an exercise for
the reader. Using the listing above as a guide, the student should complete the adt_complex
package body, then compile and test this ADT, using a test_complex program similar to the one
provided in the next subsection.

230
Testing the adt_complex Package

When building an ADT, we need to make sure that it is free of errors. This is particularly
important when the application programmer will not have access to the package body. We can
write a simple program to test the adt_complex package. The listing below asks the user to enter
two complex numbers p and q. Then these numbers are added together, subtracted, multiplied,
and divided with the results displayed. Also the real and imaginary components of p are
extracted and the magnitude of p is computed and displayed.

with ada.text_io, adt_complex, ada.float_text_io;


use ada.text_io, adt_complex, ada.float_text_io;
procedure test_complex is
p,q : complex;
begin
put("Enter p..... ");
get(p);
put("Enter q..... ");
get(q);
new_line;
put("p+q = ");
put(p+q);
new_line;
put("p-q = ");
put(p-q);
new_line;
put("p*q = ");
put(p*q);
new_line;
put("p/q = ");
put(p/q);
new_line;
put("abs p = ");
put(abs p);
new_line;
put("real part of p = ");
put(get_real(p));
new_line;
put("imag part of p = ");
put(get_imag(p));
new_line;
end test_complex;
We make a number of runs of the test program using different complex values for p and q. Can
you think of a set of values for p and q that would thoroughly test the adt_complex package?

11.2 Lists

Many important ADTs for structured data types such as stacks, queues, graphs, and trees can be
represented as special instances of a common ADT called the list. A list is an ordered sequence
of items of any elemental type, special names (or pointers) are used to designate the first item,
the last item, and the current item in the list.

first last

current
Figure 11-2: Components of List
231
An ADT for a list provides a container (e.g. an array) for the list items, tags, or pointers to special
items as shown in Figure 11-2, and a set of interfaces to access and manipulate the list ADT.
The interface will include functions and procedures, such as, a procedure for adding items to the
list, one for removing them, a function for counting the number of items in the list, and one for
finding a particular item. Table 11.1 below is a set of the most common operations on lists.

is_empty(A) boolean function returns true if the list A is empty


end_of_list(A) boolean function returns true if current location is at the end of A
add_item(x,A) inserts an item x into list A at the current location
remove_item(A) removes an item from list A at the current location
remove_last(x,A) removes the last item x from the list A and outputs it
reset_location(A) resets the current location in list A to point to the first item
next_location(A) advances the current location one position in A
prev_location(A) decrements the current location one position in A
find_item_location(x,A) sets the current location to the item x in the list A
get_item(A) returns the value of the item at the current location in A
size(A) returns the number of items in list A
is_in_list(x,A) boolean function returns true if the item x is in list A

Table 11.1: Common Operations Provided in a List Abstract Data Type

Operations such as the ones shown above can be used in an application program that does not
need to deal with the implementation details of the list. It is important to understand that the
operations on a list as shown above are common to many different applications and, once
implemented, can be used without knowing any details about implementation of the underlying
data structure. Much of the value of ADTs is in their reuse, since their construction usually
requires a greater effort than building a single application from the basic language constructs.

When creating an ADT we get to decide what type of interface operations we will provide. The
operations included in an ADT should be basic operations common to a wide variety of
applications using the associated data type. The application programmer can then combine
these common operations to build the functionality needed for a particular application.

As the ADT designer, we need to choose a data structure to be used to hold the values in the list.
Two common container types are a fixed-size array (also called a bounded list) and a linked-list.
We compare these two implementations below.

Array Implementation, the adt_bounded_list

We can implement a list as a fixed-size data structure using the predefined type array in Ada. We
will name this version of the list abstract data type adt_bounded_list.

The operations in adt_bounded_list must be compatible with the array data structure. Using an
indexed array will make the implementation of some of the interface operations simple. However,
this decision will force us to make some compromises with respect to our goal of hiding the
details of the implementation from the user.

Using an array to hold the list values will require that the application programmer specify the
maximum size of a list when it is created. In our idealized list ADT there was no limit to the size
of the list. Actually, there will always be a maximum list size due to the finite memory capacity of
any real computer, but using an indexed array implementation forces the application programmer
to choose a value for the maximum list size before the program is compiled.

232
We would like to set the array size as large as the largest list we will need for any application, but
we may not be able to predict this value for some applications. Alternatively, we could require
that the application programmer modify and recompile the package adt_list for each application,
but this largely defeats the purpose of making an ADT in the first place. Instead, we will make a
generic form of the adt_list for which the application programmer can specify the data_type for
the list items and give a maximum list size for each list declared. Recall from Section 8.5 that we
can leave certain details of a compilation unit unspecified until it is instantiated in the program
using this compilation unit. In this case, we will use a generic construct to permit our
adt_bounded_list package to operate on lists of any data_type and with lists of any specified size.
The package specification adt_bounded_list.adt is shown below.

generic
type data_type is private;

package adt_bounded_list is

type data_type_array is array(positive range <>) of data_type;


type bounded_list(max_size : positive) is private;

list_empty, list_full, no_prev_index, no_next_index : exception;

function is_empty(L : bounded_list) return boolean;


function end_of_list(L : bounded_list) return boolean;
procedure add_item(x: in data_type; L : in out bounded_list);
procedure remove_item(L : in out bounded_list);
procedure reset_location(L : in out bounded_list);
procedure next_location(L : in out bounded_list);
procedure prev_location(L : in out bounded_list);
procedure find_item_location(x : in data_type;
L : in out bounded_list);
function get_item(L : bounded_list) return data_type;
function is_in_list(x : data_type; L : bounded_list) return boolean;
function is_full(L : bounded_list)return boolean;

private

type bounded_list(max_size : positive) is record


current_index : integer:= 0;
first_index : integer:= 0;
last_index : integer:= 0;
list: data_type_array(1..max_size);
end record;

end adt_bounded_list;

This specification gives the user the essential information for using adt_bounded_list in an
application. Notice that are a few modifications of the interface from that described in Table 11.1.
For example, there is no create_list( ) procedure included. Instead, the user will create lists by
declaring them in the declaration block of the application program. In addition, we have provided
a Boolean function, is_full( ), to indicate if the list is full.

Let's take a closer look at the types declared in this specification. First of all, data_type_array is
an array type without a specific index range. Instead, the box <> operator is used. This permits
us to give a value for the index range at a later time. Specifying this range as positive sets the
lower limit of the index range to 1 for a range of [1..<>].

type data_type_array is array(positive range <>) of data_type;

233
We provide the missing upper limit of the index range in the private part of adt_bounded_list. The
parameter max_size is given a literal value when a list is declared in the application program. For
example, a list A of up to 10 integers and a list B of 100 integers are declared by,

package bounded_int_list is new adt_bounded_list (integer);

A : bounded_int_list.bounded_list(10);
B : bounded_int_list.bounded_list(100);

In this code segment, the generic adt_bounded_list is instantiated as bounded_int_list for integer
types, and then two bounded integer lists are declared; list A has max_size=10 and B has
max_size=100. Note that we can declare different sized lists from the same instantiation.

Figure 11-3 below gives a graphical representation of a newly declared list capable of holding a
maximum of 10 values. Since the list is empty, the tags to L.first_index, L.last_index, and
L.current_index are set to zero. In the adt_bounded_list implementation, these tags are just
integer values that correspond to indices in the array L.list.

L.first_index L.last_index L.list

0 0 1 2 3 4 5 6 7 8 9 10
0

L.current_index
Figure 11-3: Graphical Representation of the Bounded List Abstract Data Type

We will now consider how the procedure add_item( ) is implemented for a bounded list. If the list
is not already full then the value x will be added to the list as the next item past the current_index.
If the list is full then the procedure should raise the exception list_full. The user can choose to
trap on this exception to take actions appropriate for the specific application.

procedure add_item(x: in data_type; L : in out bounded_list) is


begin
if L.last_index < L.list'length then
for i in reverse L.current_index+1..L.last_index loop
L.list(i+1):=l.list(i);
end loop;
L.current_index:=L.current_index+1;
if L.current_index=1 then
L.first_index:=1;
end if;
L.list(L.current_index):=x;
L.last_index:=L.last_index+1;
else
raise list_full;
end if;
end add_item;

In the source code above, the attribute function L.list'length compares the maximum size of the
list to the L.last_index, which is the location of the end of the list of values entered so far. If the
current_index is already set to the last_index then the beginning range of the for..loop is greater
than the ending range, which prevents the for..loop from executing. In this case, the value x is
added to the end of the list. Figure 11-4 shows the state of the list before and after add_item( ) is
called. In this example we list x=21.

234
L.first_index L.last_index

L.list 42 17 30 5 66

1 2 3 4 5 6 7 8 9 10

L.current_index

L.first_index L.last_index

L.list 42 17 30 5 66 21

1 2 3 4 5 6 7 8 9 10

L.current_index
Figure 11-4: A Sample Execution of the Procedure add_item( )

If current_index < last_index then all the values beyond current_index are shifted one space
toward last_index; then the new value x is placed at position current_index+1, and the values of
current_index and last_index are increased by one.

Now we will look at the operation of the procedure remove_item( ). The source code for this
procedure is given below.

procedure remove_item(L : in out bounded_list) is


begin
if not(is_empty(L)) then
for i in L.current_index..L.last_index-1 loop
L.list(i):=L.list(i+1);
end loop;
L.last_index:=L.last_index-1;
else
raise list_empty;
end if;
end remove_item;

As shown in Figure 11-5 on the next page, the value at L.list(L.current_index) is removed by
being overwritten. The values at locations greater than current_index are moved toward the
beginning of the list by one space. In this example, the value 30 is being removed. The value 5
is written over the value 30 as the values beyond current_index are moved one space toward the
beginning of the list.

235
L.first_index L.last_index

L.list 42 17 30 5 66

1 2 3 4 5 6 7 8 9 10

L.current_index

L.first_index L.last_index

L.list 42 17 30 5 66

1 2 3 4 5 6 7 8 9 10

L.current_index

L.first_index L.last_index

L.list 42 17 5 66 66

1 2 3 4 5 6 7 8 9 10

L.current_index
Figure 11-5: Demonstration of remove_item( ) Removing an Internal Value

Notice that when an internal value is removed, an additional copy of the last value in the list (the
value 66 in this case) is left in a position beyond the end of the list. Since the tag L.last_index is
moved back one space, this repeated value is ignored and will be written over if another value is
added to the list. The operation of the remaining functions and procedure of adt_bounded_list
are more easily understood so we provide the source code for the complete package body for
adt_bounded_list.adb below.

package body adt_bounded_list is

function is_empty(L : bounded_list)return boolean is


begin
return L.last_index=0;
end is_empty;

function end_of_list(L : bounded_list) return boolean is


begin
return L.current_index = L.last_index;
end end_of_list;

procedure add_item(x: in data_type; L : in out bounded_list) is


begin
if L.last_index < L.list'last then
for i in reverse L.current_index+1..L.last_index loop
L.list(i+1):=l.list(i);
end loop;
L.current_index:=L.current_index+1;
if L.current_index=1 then
L.first_index:=1;
236
end if;
L.list(L.current_index):=x;
L.last_index:=L.last_index+1;
else
raise list_full;
end if;
end add_item;

procedure remove_item(L : in out bounded_list) is


begin
if not(is_empty(L)) then
for i in L.current_index..L.last_index-1 loop
L.list(i):=L.list(i+1);
end loop;
L.last_index:=L.last_index-1;
else
raise list_empty;
end if;
end remove_item;

procedure reset_location(L : in out bounded_list) is


begin
if not(is_empty(L)) then
L.current_index:=1;
end if;
end reset_location;

procedure next_location(L : in out bounded_list) is


begin
if L.current_index < L.last_index then
L.current_index:=L.current_index+1;
else
raise no_next_index;
end if;
end next_location;

procedure prev_location(L : in out bounded_list) is


begin
if L.current_index>L.first_index then
L.current_index:=L.current_index-1;
else
raise no_prev_index;
end if;
end prev_location;

procedure find_item_location(x : in data_type;


L : in out bounded_list) is
begin
L.current_index:=0;
for i in 1..L.last_index loop
if L.list(i)=x then
L.current_index:=i;
end if;
end loop;
end find_item_location;

function get_item( L : in bounded_list) return data_type is


begin
if not(is_empty(L)) then
return L.list(L.current_index);
else
237
raise list_empty;
end if;
end get_item;

function is_in_list(x : data_type;


L : bounded_list) return boolean is
boolval : boolean := false;
begin
for i in 1..L.last_index loop
if L.list(i)=x then
boolval:=true;
end if;
end loop;
return boolval;
end is_in_list;

function is_full(L : bounded_list)return boolean is


begin
return L.last_index=L.list'last;
end is_full;

end adt_bounded_list;

Now we need an application program to test the proper operation of adt_bounded_list. Testing is
an important part of the software development process. Creating a complete validated set of
tests to verify that adt_bounded_list is error free would be an extensive task. For now, we will
simply demonstrate some of the functions and procedures with the test program below.

with ada.text_io, ada.integer_text_io, adt_bounded_list;


use ada.text_io, ada.integer_text_io;
procedure bounded_list_test is
package bounded_int_list is new adt_bounded_list (integer);
use bounded_int_list;
A : bounded_int_list.bounded_list(10);
val : integer;

procedure display_list(A : in out bounded_list) is


begin
reset_location(A);
if not(is_empty(A)) then
new_line;
loop
put(get_item(A),0);
new_line;
exit when end_of_list(A);
next_location(A);
end loop;
end if;
end display_list;

begin

loop
put("Enter next value or -999 to quit... ");
get(val);
exit when val=-999;
add_item(val,A);
end loop;

new_line;

238
put("You entered ");
put(size(A),0);
put_line(" items.");
display_list(A);
put("Enter a value to remove... ");
get(val);
if is_in_list(val,A) then
find_item_location(val,A);
put("The value of this item is... ");
put(get_item(A),0);
remove_item(A);
display_list(A);
end if;

exception
when list_full =>
put_line("This list is full");
when list_empty =>
put_line("This list is empty");
when no_next_index =>
put_line("Already at the end of the list");
when no_prev_index =>
put_line("Already at the beginning of the list");

end bounded_list_test;

In this test program, we ask the user to enter integer values into a bounded integer list with a
maximum capacity of ten values. If the user attempts to enter more than ten values, the
add_item( ) procedure will raise the exception list_full. The test program traps on this exception
to display the message "This list is full" and the program terminates. The user can end the data
entry phase of the program by entering the sentinel value -999. The program then displays the
list and asks the user for a value to find. If this value is in the list, the program removes it and re-
displays the remaining values in the list. If the value is not in the list, the program terminates
without re-displaying the list.

We will now implement an ADT for a list using dynamic memory, which will eliminate the need to
specify the size of the list at the time of creation.

Linked List Implementation, the adt_list

In order to use the adt_bounded_list package the programmer must specify the maximum size of
the list. For some applications, the maximum size of the list cannot be predicted. The
programmer would be forced to specify a maximum size as large as could be conceived for the
application. The problem of wasting memory by having to establish a large array that may not be
used can be avoided by using a list ADT that holds the data in a linked-list. As we have seen,
linked-lists can grow with the needs of the application. Also, we will see that a linked-list can
return memory to the computer system to be made available to other running processes when it
is no longer needed by the application program.

We need to decide what type of linked list we will use in this unbounded list ADT. Figure 11-6, on
the next page, gives a graphical representation of a singly linked and a doubly linked list.

239
singly-linked list
head

null

current

doubly-linked list
head

null null

current

Figure 11-6: Graphical Representations of a Singly Linked and a Doubly Linked List

Consider a singly linked list implementation for the list ADT. The current item can now be
designated with a pointer to the appropriate list element, rather than an index into an array.
Creation and insertion of new values can be accomplished using techniques of dynamic memory
manipulation. We could implement the ADT container list as a singly linked list, similar to the
ones we worked with in Chapter 10. However, a singly linked list does not give us direct access
to the node that is previous to the current node, making it difficult to move the current pointer
toward the beginning of the list.

In contrast, the doubly linked list implementation will give us access to nodes on either side of the
current node. This version of the linked-list permits us to move the current pointer in either
direction with a simple assignment statement. A pointer record for a doubly linked list is defined
with a data structure similar to,

type node;
type pointer is access node;
type node is record
data : any_data_type;
next : pointer;
prev : pointer;
end record;

This record provides a pointer to the next node and to the prev (previous) node in the list. An
interface function to insert a record before the current position could be implemented with a
sequence of assignments as shown below. Assume that current and newnode are pointers to the
type of pointer records defined above for building doubly linked lists.

newnode := new node;


newnode.data := val;
newnode.next := current;
newnode.prev := current.prev;
current.prev := newnode;
newnode.prev.next := newnode;

The pointer current is pointing to some record in the list. The data value val is placed into a newly
created record, pointed to by the pointer newnode. The sequence of assignment statements
given above connects the newnode record to the current record and the record pointed to by
current.prev. After the new connections have been made from the newnode record, the old
connections between the current record and the current.prev record are broken and redirected to
the newnode.

240
head
current

null null

val

newnode

head
current

null null

val

newnode

head
current

null val null

newnode

Figure 11-7: Insertion of a new record into a doubly linked list


We will make the data type generic so that the adt_list package can be used to hold any type of
values or structured types. We will define a list as a pointer to a first item, a pointer to a last item
and a pointer to a current item. The data container type will be a pointer record with a field of
data_type (to be instantiated in the application program), a pointer to the next record, and a
pointer to the previous record.
L (list_type)

L.last

L.first L.current

(node)
1 2 3 4 5

null
null
prev next
Figure 11-8: The Data Structure for a Linked-List Implementation of adt.list

As shown in Figure 11-8 above, the container for the data is a doubly linked list (of nodes), which
is accessed by any of three pointers from the list L. The next pointers in the doubly linked list
move toward the end of the list; the prev pointers move toward the beginning of the list.

Due to the use of dynamic memory methods, the implementation of the interfaces for adt_list will
be more complicated than for the bounded list. We will review the design of the add_item_after( )
and remove_item( ) procedures to demonstrate how the doubly linked list is used.

241
add_item_after(x,L) - This procedure will add a new item x to the list L in a location that is
past the item being pointed to by L.current. We have to consider whether the list is empty and if
L.current is pointing to the first or the last node in the container (i.e. the doubly-linked) list.

Case 1 - The list is empty

In this case, a new node is created to hold the item x, its prev and next pointers are pointed to
null, and all three list pointers are pointed to this new node.
L

L.first := new node'(item,null,null);

L.last := L.first;

L.current := L.first; x

null
null
Figure 11-9: Adding an Item
to an Empty List

Case 2 - The list is not empty

In this case a new node is created to hold the item x, its prev pointer is pointed to L.current.next,
and its prev pointer is pointed to L.current.

newnode := new node'(item,L.current.next,L.current);

Now that the next and prev pointers of newnode are connected to the container list, we need to
redirect the pointers in the container list back to newnode. The operations to accomplish this
depend on whether the newnode is the last node in the container list. Figure 11-10 illustrates the
two possible situations.
L (list_type) L (list_type)
L.last L.last
L.current L.current
L.first L.first

p q r s p q r s
null null null null

newnode newnode

x x

a. newnode is an internal node b. newnode is the last node


Figure 11-10: Possible locations for the new node in add_item_after( )
In Figure 11-10a, the pointers connecting q and r need to be redirected to newnode. In Figure
11-10b, the next pointer from s needs to be redirected to newnode. In either case, we will move
L.current to point to newnode. The following code segment is one way to accomplish these
operations.

if newnode.next/=null then
newnode.next.prev:=newnode;
end if;
L.current.next:=newnode;

242
if L.current=L.last then
L.last:=newnode;
end if;
L.current:=newnode;

remove_item(L) - This procedure removes the node in the container list L that is pointed to by
L.current. When removing a node, we do not simply break the links to it. We want to release it
back to the heap (i.e. to the available memory). Breaking links to dynamic memory without
releasing it to the heap causes a loss of available memory called a memory leak. The generic
procedure ada.unchecked_deallocation permits us to instantiate a procedure to remove any type
of pointer record and release its memory for use by another running process. This instantiation
requires two parameters; the first is the pointer record type and the second is the access type
corresponding to this pointer record. For example,

procedure release_node is new ada.unchecked_deallocation(node,pointer);

In order to remove a node from the container list, we must verify that the container list of L is not
empty. If the list is empty then remove_item( ) will raise the exception list_empty. If the container
list is not empty then the procedure attaches a temporary pointer, dumpthis, to the node being
removed. This pointer will be used to release the node after the links to it have been redirected.

dumpthis := L.current;

In order to properly redirect the links around the node being removed, we need to consider four
cases as illustrated in Figure 11-11 below:

L (list_type) L (list_type)
L.last
L.last

L.first L.current L.first L.current

q p q r

null null null null


a. Case 1 - there is a single node b. Case 2 - first node is being removed

L (list_type) L (list_type)

L.last L.last

L.first L.current L.first L.current

p q r p q r

null null null


null
c. Case 3 - last node is being removed d. Case 4 - internal node is being removed
Figure 11-11: Four Cases to Consider for the remove_item( ) Procedure

The dynamic memory management operation for each of these cases is provided on the next
page.

243
Case 1: The list contains A Single Node

If the node being removed is the only node in the container list then L will be empty following the
removal.

L.current:=null;
L.first:=null;
L.last:=null;

Case 2: The Node Being Removed is the First Node

If the node being removed is the first node in the container list then the pointer L.first and
L.current will be moved to the next node.

L.current:=L.first.next;
L.first:=L.current;

Case 3: The Node Being Removed is the Last Node

If the node being removed is the last node in the container list then the pointers L.last and
L.current will be moved to the previous node.

L.current:=L.last.prev;
L.last:=L.current;

Case 4: The Node Being Removed is an Internal Node

If the node being removed is an internal node (i.e. not the first or the last node) in the container
list then the links to this node must be redirected around this node, and the pointer L.current must
be moved to either the previous or the next node. We choose to move L.current to the next node
in the container list.

L.current.next.prev:=L.current.prev;
L.current.prev.next:=L.current.next;
L.current:=L.current.next;

When the redirections of the links as described above are complete the node pointed to by
dumpthis can be released to available memory.

release_node(dumpthis);

Implementation of adt_list

The complete source code for adt_list.ads the generic package specification for the unbounded
list ADT is provided here:

generic

type data_type is private;

package adt_list is

type node is private;


type pointer is private;
type list is private;

list_empty, no_prev, no_next : exception;

function is_empty(L : list) return boolean;


procedure add_item_after(item : data_type; L : in out list);
244
procedure add_item_before(item : data_type;L : in out list);
procedure remove_item(L : in out list);
procedure remove_last(x : out data_type; L : in out list);
function is_last(L : list) return boolean;
procedure reset(L : in out list);
function get_item(L : list) return data_type;
procedure next(L : in out list);
procedure prev(L : in out list);
function size(L : list) return integer;

private

type pointer is access node;


type node is record
data : data_type;
next : pointer;
prev : pointer;
end record;

type list is record


first : pointer;
last : pointer;
current : pointer;
end record;

end adt_list;

This specification includes functions and procedures for all the interfaces described in Table 11.1,
which do not involve issues with a bounded list. For example, we have omitted the boolean
function is_full( ) because we no longer need to concern ourselves with the maximum list length
being exceeded.

The following source code is a complete implementation of the package body adt_list.adb for
these interfaces. It is important to understand that a particular implementation of this unbounded
list can be used by incorporating the package into an application program.

with ada.unchecked_deallocation;
package body adt_list is

procedure release_node is new


ada.unchecked_deallocation(node,pointer);

function is_empty(L : list) return boolean is


begin
return L.first=null;
end is_empty;

procedure add_item_after(item: data_type; L: in out list) is


newnode : pointer;
begin
if is_empty(L) then
L.first := new node'(item,null,null);
L.last := L.first;
L.current := L.first;
else
newnode := new node'(item,L.current.next,L.current);
if newnode.next/=null then
newnode.next.prev:=newnode;
end if;
L.current.next:=newnode;

245
if L.current=L.last then
L.last:=newnode;
end if;
L.current:=newnode;
end if;
end add_item_after;

procedure add_item_before(item:data_type; L: in out list) is


newnode : pointer;
begin
if is_empty(L) then
L.first := new node'(item,null,null);
L.last := L.first;
L.current := L.first;
else
newnode := new node'(item,L.current,L.current.prev);
if newnode.prev/=null then
newnode.prev.next:=newnode;
end if;
L.current.prev:=newnode;
if L.current=L.first then
L.first:=newnode;
end if;
L.current:=newnode;
end if;
end add_item_before;

procedure remove_item(L : in out list) is


dumpthis : pointer;
begin
if not(is_empty(L)) then
dumpthis:=L.current;
if L.first=L.last then
L.current:=null;
L.first:=null;
L.last:=null;
elsif L.current=L.first then
L.current.prev:=null;
L.current:=L.first.next;
L.first:=L.current;
elsif L.current=L.last then
L.current.next:=null;
L.current:=L.last.prev;
L.last:=L.current;
else
L.current.next.prev:=L.current.prev;
L.current.prev.next:=L.current.next;
L.current:=L.current.next;
end if;
release_node(dumpthis);
else
raise list_empty;
end if;
end remove_item;

procedure remove_last(x : out data_type; L : in out list) is


dumpthis : pointer;
begin
if not(is_empty(L)) then
x:=L.last.data;
dumpthis := L.last;
246
if L.first=L.last then
L.first:=null;
L.last:=null;
L.current:=null;
elsif L.current=L.last then
L.last:=L.last.prev;
L.current:=L.last;
L.last.next:=null;
else
L.last:=L.last.prev;
L.last.next:=null;
end if;
else
raise list_empty;
end if;
release_node(dumpthis);
end remove_last;

function is_last(L : list) return boolean is


begin
if not(is_empty(L)) then
return L.current=L.last;
else
raise list_empty;
end if;
end is_last;

procedure reset(L : in out list) is


begin
L.current:=L.first;
end reset;

function get_item(L : list) return data_type is


begin
if not(is_empty(L)) then
return L.current.data;
else
raise list_empty;
end if;
end get_item;

procedure next(L : in out list) is


begin
if L.current/=L.last then
L.current:=L.current.next;
else
raise no_next;
end if;
end next;

procedure prev(L : in out list) is


begin
if L.current/=L.first then
L.current:=L.current.prev;
else
raise no_prev;
end if;
end prev;

247
function size(L : list) return integer is
count : integer := 0;
temp : pointer;
begin
if not(is_empty(L)) then
temp:=L.first;
count:=1;
while temp.next/=null loop
temp:=temp.next;
count:=count+1;
end loop;
end if;
return count;
end size;

end adt_list;

A test program to verify the correctness of the adt_list package will be developed as one of the
applications at the end of this chapter.

There are a number of special types of lists that we use frequently in computer science. The
most common are stacks and queues, which we will review in the following sections. Stacks and
queues are sufficiently popular in software development so that standard libraries for these
abstract data types are included with most programming languages. This means that we will be
able to more easily port application programs using these ADTs to other systems and languages.

11.3 Stacks

The stack is an ADT that can be used to save data elements in a last-in first-out (LIFO) order.
The only item in a stack that can be accessed is the last item that was placed onto the stack.
This data structure is analogous to a stack of trays in a cafeteria. As you put trays onto the stack
the rest of the stack is pushed down onto a spring loaded platform. As you take trays off the top
of the stack the platform pops up to give access to the next tray on the stack as shown in
Figure 11-12 below.

push and pop


17
stack

Figure 11-12: Illustration of a Stack as Cafeteria Trays and a Deck of Cards

In computer science, a stack is a special form of a list in which only the top item is accessible. At
first it may seem odd that we should want to limit our access to the contents of a list in this
manner, but we will show that the use of a stack can simplify an otherwise cumbersome
algorithm.

248
We can build an ADT for a stack that support the following operations:

push(x,S) adds a new item x to the top of stack S


pop(x,S) removes the item x from the top of the stack S
pop(S) removes an item from the top of the stack S without returning its value
top(S) returns the value of the item on the top of the stack S without removing it
is_empty(S) returns true is S is empty

Table 11.2: Common Operations Provided in a Stack Abstract Data Type

As you may have noticed, this is a much simpler interface than the interface for a generalized list
ADT. Since we can only access the top of the stack, we don’t have to choose a value from the
list.

Implementation of adt_stack using adt_list

We can implement a stack by using the adt_list package we developed in the previous section.
The adt_list package provides all the functionality needed to implement the interfaces given in
Table 11.2. The source code of the specification for adt_stack is provided below.

with adt_list;
generic
type data_type is private;

package adt_stack is

package stack_list is new adt_list(data_type);


use stack_list;

type stack is limited private;

stack_empty : exception;

function is_empty(S : stack) return boolean;


procedure push(x : in data_type; S : in out stack);
procedure pop(x : out data_type; S : in out stack);
procedure pop(S : in out stack);
function top(S : stack) return data_type;

private
type stack is record
a_list : list;
end record;

end adt_stack;

Note that data_type remains generic in this specification and is passed to adt_list through the
instantiation of stack_list. This technique gives us access to type list from adt_list and permits the
application programmer to instantiate adt_stack with a literal data type in the application program.

The package body for adt_stack is provided here.

package body adt_stack is

function is_empty(S : stack) return boolean is


begin
return is_empty(S.a_list);

249
end is_empty;

procedure push(x : in data_type; S : in out stack) is


begin
add_item_before(x,S.a_list);
end push;

procedure pop(x : out data_type; S : in out stack) is


begin
if not(is_empty(S)) then
x:=get_item(S.a_list);
remove_item(S.a_list);
else
raise stack_empty;
end if;
end pop;

procedure pop(S : in out stack) is


begin
if not(is_empty(S)) then
remove_item(S.a_list);
else
raise stack_empty;
end if;
end pop;

function top(S : stack) return data_type is


begin
if not(is_empty(S)) then
return get_item(S.a_list);
else
raise stack_empty;
end if;
end top;

end adt_stack;

As you can see, the implementation of these functions and procedures using adt_list is much
simpler than the implementation of the interfaces for adt_list itself. This reduction in complexity is
a result of the layers of abstraction provided by the ADTs.

You may wonder what happens when the adt_list interfaces raise an exception that is not defined
in adt_stack. The fact is, adt_stack has its own exception for stack_empty, which will get raised
before the exceptions in adt_list are reached.

The use of adt_stack can be demonstrated with a simple test program as follows.

with ada.text_io, ada.integer_text_io, adt_stack;


use ada.text_io, ada.integer_text_io;
procedure adt_stack_test is

package int_stack is new adt_stack(integer);


use int_stack;

S : stack;
x : integer;

begin

loop

250
put("Enter next value to push or -999 to quit... ");
get(x);
exit when x=-999;
push(x,S);
end loop;

new_line;

while not(is_empty(S)) loop


put(top(S),0);
put(" ");
pop(x,S);
put(x,0);
new_line;
end loop;

end adt_stack_test;

The package int_stack is instantiated with data_type defined as integer. The integer type is
passed on to adt_list as instantiated in adt_stack. The test program asks the user to enter a
sequence of integers that are pushed onto the integer stack S. When the user is finished with
data entry, the test program displays the top value on the stack and then pops this value off the
stack for redisplay. This operation is repeated until the stack is empty.

The size of the stack that can be created using adt_stack is practically unlimited. Occasionally,
we will want to limit the maximum number of items in a stack. For these occasions, we provide
below an array implementation of a stack called adt_bounded_stack.

Array Implementation - adt_bounded_stack

As with the previous ADTs, we will make the container type generic. In addition to the interfaces
listed in Table 11.2, we will need to include a Boolean function is_full(S) to check for a full stack.
When making use of the adt_bounded_stack package, we want the user to be able to specify the
maximum size of the list. Again, we will use the box <> operator to leave the size of the list
unspecified until adt_bounded_stack is instantiated.

The following source code is a complete listing of adt_bounded_stack.ads, which is the package
specification for the bounded stack.

generic
type data_type is private;

package adt_bounded_stack is
type data_type_array is array(positive range <>) of data_type;
type bounded_stack(max_size : positive) is private;
stack_empty, stack_full : exception;

procedure push(x:data_type; s : in out bounded_stack);


procedure pop(x: out data_type; s: in out bounded_stack);
procedure pop( s: in out bounded_stack);
function top( s : in bounded_stack ) return data_type;
function is_empty(s:bounded_stack)return boolean;
function is_full(s:bounded_stack)return boolean;
private
type bounded_stack(max_size : positive) is record
top : integer := 0;
list: data_type_array(1..max_size);
end record;
end adt_bounded_stack;

251
The following is the complete source code for adt_bounded_stack.adb, the package body for the
bounded stack package. The push function raises a stack_full exception if an item is pushed
onto a full stack. The pop( ) procedure raises a stack_empty exception if it is called when the
stack is empty. The top( ) function also raises the stack_empty exception when there are no
values to return.

package body adt_bounded_stack is

procedure push(x:data_type; s : in out bounded_stack) is


begin
if s.top< s.list'last then
s.top:=s.top+1;
s.list(s.top):=x;
else
raise stack_full;
end if;
end push;

procedure pop(x: out data_type; s: in out bounded_stack) is


begin
if s.top>0 then
x:=s.list(s.top);
s.top:=s.top-1;
else
raise stack_empty;
end if;
end pop;

procedure pop( s: in out bounded_stack) is


begin
if s.top>0 then
s.top:=s.top-1;
else
raise stack_empty;
end if;
end pop;
function top( s: in bounded_stack) return data_type is
begin
if s.top>0 then
return s.list( s.top );
else
raise stack_empty;
end if;
end top;

function is_empty(s:bounded_stack)return boolean is


begin
return s.top<=0;
end is_empty;

function is_full(s:bounded_stack)return boolean is


begin
return s.top>=s.list'last;
end is_full;

end adt_bounded_stack;

252
The following is the source code for a program to test the bounded stack package. In the
declaration block of this program, a stack for integers and a stack for characters are instantiated
separately. The two different stacks are declared to hold 10 integers and 5 integers respectively.

with ada.text_io, ada.integer_text_io, adt_bounded_stack;


use ada.text_io, ada.integer_text_io;

procedure bounded_stack_test is
package bounded_int_stack is new adt_bounded_stack(integer);
package bounded_char_stack is new adt_bounded_stack(character);
use bounded_int_stack, bounded_char_stack;

c_stack : bounded_char_stack.bounded_stack(10);
stack10 : bounded_int_stack.bounded_stack(10);
stack05 : bounded_int_stack.bounded_stack(5);

--Package name needed above since bounded_stack type is ambiguous;


--Not needed if only ONE instantiation of adt_bounded_stack.

c : character;
x : integer;

begin
if is_empty(c_stack) then
put_line("Character stack is empty");
end if;
if is_empty(stack10) then
put_line("Integer stack (10) is empty");
end if;
if is_empty(stack05) then
put_line("Integer stack (5) is empty");
end if;

for x in 65..74 loop


put("Pushing ");
put(character'val(x));
put(" and ");
put(x,1 );
put_line(" into the stacks.");
push(character'val(x), c_stack );
push( x, stack10);
end loop;

if is_full(c_stack) then
put_line("Character stack is full");
end if;
if is_full(stack10) then
put_line("Integer stack (10) is full");
end if;
while(not is_empty(c_stack)) loop
pop(c,c_stack);
put("Popping ");
put(c);
put(", ");
pop(c, c_stack);
put(c);
put(", and ");
x := top(stack10);
pop(stack10);
put(x,1);

253
put(" from stacks, and pushing ");
put(x, 1);
put_line(" onto integer stack (5).");
push(x, stack05);
end loop;

put("Popping from stack(5): ");


for i in 1..6 loop
x := top(stack05);
pop(stack05);
put(x,3);
end loop;
put_line("Should NOT see this!");

exception --Qualification needed here too to eliminate ambiguity


when bounded_int_stack.stack_full|bounded_char_stack.stack_full =>
new_line;
put_line("Attempted to PUSH onto a FULL stack");
when bounded_int_stack.stack_empty|bounded_char_stack.stack_empty=>
new_line;
put_line("Attempted to retrieve from an EMPTY stack");
end bounded_stack_test;

The test program tests the interfaces by pushing values onto the stacks and popping them off
while checking for is_full( ) and is_empty( ). In this test program, the exceptions that are raised
generate messages to the user and then end execution.

11.4 Queues

A queue is another type of list data structure to which the user has restricted access. New items
are placed at the back of a queue and the next available item is taken from the front of the queue.
This is called first-in, first-out (FIFO) order. An idealized queue is an infinite capacity list with
special tags for the entries front and back.
queue

back front
Figure 11-13: A Graphical Representation of a Queue

We define the interface for a queue ADT in Table 11.3, below.

enqueue(x,Q) append the value x to the back of the queue Q


dequeue(x,Q) removes the item at the front of the queue Q and outputs its value as x
front(Q) a function that returns the value at the front of the queue Q
is_empty(Q) a Boolean function that returns true if the queue Q is empty
Table 11.3: Operations of a Queue ADT

Direct Linked List Implementation of a Queue

We can create a queue ADT directly with a linked list. While we are rebuilding the adt_queue we
will also make it generic. The sample code on the next page is a generic version of a queue ADT
that uses a linked list data structure for the container list.

254
generic
type data_type is private;
package adt_queue is
type qpointer is private;
type qnode is private;
type qtype is private;
queue_empty : exception;

function is_empty(Q : qtype) return boolean;


procedure enqueue(Q: in out qtype; x : in data_type);
procedure dequeue(Q: in out qtype; x : out data_type);
function front(Q : qtype) return data_type;

private
type qpointer is access qnode;
type qnode is record
data : data_type;
next : qpointer;
end record;
type qtype is record
front : qpointer := null;
back : qpointer := null;
end record;
end adt_queue;

The package body for a direct implementation of a queue using dynamic memory requires that
the interfaces access the internal details of the linked list records. The source code shown below
is a partial implementation of the package body corresponding to the specification above. Two of
the four interfaces, namely enqueue( ) and dequeue( ), are provided.

with ada.text_io, ada.unchecked_deallocation;


use ada.text_io;
package body adt_queue is

procedure remove_qnode
is new ada.unchecked_deallocation(qnode,qpointer);

-- the source for the function is_empty goes here

procedure enqueue(Q: in out qtype; x : in data_type) is


newQ : qpointer;
begin
newQ := new qnode;
newQ.data := x;
newQ.next := null;
if Q.front=null then
Q.front:=newQ;
Q.back:=newQ;
else
Q.back.next:=newQ;
Q.back:=newQ;
end if;
end enqueue;

procedure dequeue(Q: in out qtype; x : out data_type) is


dumpme : qpointer;
begin
if not Q.front=null then
x:=Q.front.data;
dumpme:=Q.front;

255
Q.front:=Q.front.next;
remove_qnode(dumpme);
else
raise queue_empty;
end if;
end dequeue;

-- the source for the function front( ) goes here

end adt_queue;

Implementation of adt_queue using adt_list

There is a better way to build a queue ADT. We can use the adt_list generic package to create a
generic package adt_queue. We will compare the complexity of this implementation with the
previous direct implementation. As with the adt_stack, we will see that the additional level of
abstraction provided with adt_list has the advantage of simplifying the implementation of the
adt_queue package body.

The complete source code of the specification and body of adt_queue is provided below:

with adt_list;
generic
type data_type is private;

package adt_queue is
package queue_list is new adt_list(data_type);
use queue_list;

type queue is limited private;

queue_empty : exception;

function is_empty(Q : queue) return boolean;


procedure enqueue(x : in data_type; Q : in out queue);
procedure dequeue(x : out data_type; Q : in out queue);
procedure dequeue(Q : in out queue);
function top(Q : queue) return data_type;

private
type queue is record
a_list : list;
end record;

end adt_queue;

package body adt_queue is

function is_empty(Q : queue) return boolean is


begin
return is_empty(Q.a_list);
end is_empty;

procedure enqueue(x : in data_type; Q : in out queue) is


begin
add_item_before(x,Q.a_list);
end enqueue;

procedure dequeue(x : out data_type; Q : in out queue) is


begin

256
if not(is_empty(Q)) then
remove_last(x:Q.a_list);
else
raise queue_empty;
end if;
end dequeue;

function front(Q : queue) return data_type is


begin
if not(is_empty(Q)) then
return get_item(Q.a_list);
else
raise queue_empty;
end if;
end front;

end adt_queue;

Other than a slightly simpler private type, there is not much difference between this specification
and the previous version. The primary advantage in reducing complexity is realized in the
package body. For example, the procedure enqueue( ) is reduced to a single call to
add_item_before( ), and dequeue( ) is reduced to a single call to remove_last( ) wrapped in an
if..then statement that checks to make sure that the queue is not empty.

11.5 Applications

The Mandelbrot Set Generator

11.1: Write an Ada program that determines if a particular complex value is a member of the
Mandelbrot Set.

The Mandelbrot Set is defined on the complex plane. Every complex number has a
corresponding point in the complex plane. By convention the real component specifies the
position on the horizontal axis and the imaginary component specifies the position on the
imaginary axis. The Mandelbrot set is the set of all complex points C for which the magnitude of
Z in the function,
Z := Z*Z + C
converges to a fixed value when iterated. The parameter Z is a complex number initially set to
zero (Z=0+i0). To iterate this expression we set Z=0+i0 and C to the chosen number in the
complex plane. We compute a new value for Z and check its magnitude.

We then use the current value of Z to compute the next Z value. Eventually, we find that the
magnitude of Z either converges to a fixed value or it diverges (continually grows to a larger and
larger value).

If we color the points, C, that converge to zero in the complex plane black and color the points
that diverge white, we obtain an image similar to the one shown on the next page, in which the
black region represents points in the Mandelbrot Set.

257
Figure 11-14: Mandelbrot Set

Our task in this exercise is to create a boolean function that returns true if its complex argument is
in the Mandelbrot Set and false otherwise. All the points in the Mandelbrot Set are within a radius
of magnitude 2.0 from the origin of the complex plane. This means that we can terminate the
iteration when the magnitude of the iterated value of Z exceeds 2.0. We can also terminate the
iteration when we have reached a fixed value for Z. This may not be so easy to determine, as we
will see. We can attempt to detect when the iterated value of Z has reached a fixed point
(constant value) using a method similar to the following,

put("Enter a complex number C = ");


get(C);
Z:=make_complex(0.0,0.0);
Zmag_old:=float'last;
loop
Z := Z*Z + C;
Zmag := abs Z;
exit when Zmag >= 2.0 or Zmag = Zmag_old;
Zmag_old:=Zmag;
end loop;
put("The value C ");
if abs Z > 2.0 then
put("is not a member of the Mandelbrot Set");
else
put("is a member of the Mandelbrot Set");
end if;

The problem with this method is that we are testing to see if two floating point values, namely
Zmag and Zmag_old, are equal. As we have seen, this can create problems due to the way
floating point values are internally represented in the computer. We need a better way to test for
convergence. We could try to test for the difference between the magnitudes of two successive
-6
iterations of Z being less than some small amount, say 1x10 .

exit when abs Z >= 2.0 or abs(Zmag - Zmag_old) < 1.0E-6;

258
We will use this approach to implement our Mandelbrot Set Boolean function.

function is_mandel(C : complex) return boolean is


Z : complex;
begin
Z:= C;
Zmag_old:=float'last;
loop
Z:=Z*Z + C;
exit when abs Z >= 2.0 or abs(abs Z - zmag_old) < 1.0E-6;
Zmag_old:=abs Z;
end loop;

if abs Z > 2.0 then


return false;
else
return true;
end if;
end is_mandel;

When we test this function we find that certain complex values cause our program to "hang", or
get caught in an infinite loop. The fact is, some values of C that are in (or very near) the
Mandelbrot Set are strange attractors. This means that the iterated values of Z stay inside a
confined region of the complex plane but do not converge to a fixed point. Since they do not
diverge they are not excluded from the Mandelbrot set, but since they do not converge to zero or
any other fixed point they cannot be determined to be in the Mandelbrot Set. Therefore, we have
three conditions, divergence, non-divergence, and convergence.

For this program, we will arbitrarily choose a fixed number of iterations Nmax and claim that the
point C is a member of the Mandelbrot Set if the magnitude of the iterated value of Z does not
exceed 2.0 after Nmax iterations. We can get a more accurate estimate of which points in the
boundary are members of the set by increasing the value of Nmax.

RPN Arithmetic Expression Evaluator

11.2: Write an Ada program that evaluates Reverse Polish Notation (RPN) arithmetic
expressions. You may assume that operands are single characters A..Z and that the expressions
will involve only +, -, *, and /.

An RPN Expression is also sometimes called a postfix expression. RPN expression are evaluated
by scanning them in a left-to-right order. The operands are placed to the left of the operation
being performed on them. So the infix expression

X + Y
would become
XY+
and the expression
(X+Y)/Z – (X-Y)*Z
would become
XY+Z/XY-Z*-.

The first example is straightforward. However, the procedure for converting general arithmetic
infix expressions into their postfix equivalent expressions is not obvious. This will be discussed in
detail in the next application. For now, we will concern ourselves with how to implement an RPN
expression evaluator. (see Exercise 11.5)

259
We can use a stack data structure to hold the intermediate values as we evaluate an RPN
expression. The rules for RPN evaluation are:
(1) Scan the expression one character (or token) at a time from the left to the right.
(2) When an operand is encountered, push the corresponding value onto the stack.
(3) When an operator is encountered, pop the top two values off the stack, perform the
indicated operation, and push the result back onto the stack.
(4) Repeat steps (1)..(3) above until expression is completely scanned. The value on the
top of the stack is the result.
Before we attempt to build our program, let's try out this procedure by hand, using the previous
example postfix expression.

The problem statement says that we can assume that the operands (variables) will be
represented by single letters A through Z. This will simplify our expression scanning procedure.
Let’s assume that we have values associated with our operands as we practice.

Assume that X=1, Y=2, and Z= -1 as you evaluate the RPN expression, XY+Z/XY-Z*-.

XY+Z/XY-Z*- push the value of X onto the stack


_1_
XY+Z/XY-Z*- push the value of Y onto the stack
2
_1_
XY+Z/XY-Z*- pop the top two values off the stack and push the sum (1+2)
_3_
XY+Z/XY-Z*- push the value of Z onto the stack
-1
_3_
XY+Z/XY-Z*- pop the top two values and push the ratio (3/[-1])
-3_
XY+Z/XY-Z*- push the value of X onto the stack
1
-3_
XY+Z/XY-Z*- push the value of Y onto the stack
2
1
-3_
XY+Z/XY-Z*- pop the top two values and push the difference (1-2)
-1
-3_
XY+Z/XY-Z*- push the value of Z onto the stack
-1
-1
-3_
XY+Z/XY-Z*- pop the top two values and push the product ([-1]*[-1])
1
-3_
XY+Z/XY-Z*- pop the top two value and push the difference ([-3]-1)
-4_

Table 11.4: Using a Stack to Evaluate a Post-Fix Expression

260
Therefore, the result of this expression is the value remaining on the top of the stack (i.e. -4).
Besides performing these operations properly, we may want to include error checking in our
program. For example, there always must be at least one more operand encountered than
operators as we scan an RPN expression from left to right (why?).

In general, an RPN expression can be evaluated using a stack that is not empty. The value at
the top of the stack following the evaluation of a properly formatted RPN expression will be the
same. Actually, this situation is quite common.

In-Fix to Post-Fix Expression Converter

11.3*: Write an Ada program that generates a postfix expression given, as input, a string
representing the corresponding infix arithmetic expression. As with the previous example, you
may assume that all operands will be represented as single ASCII characters A..Z.

Given a fully parenthesized infix expression we can convert it to postfix (RPN) notation by hand
without much difficulty, by using the following rules.

(1) Starting with the innermost (first to be computed) operations, rearrange the symbols
or tokens (i.e. sets of symbols being treated as atomic) such that the operator is just to
the right of the operand (or token) pair.

(2) Repeat Step (1) until all operators have been moved to their postfix locations

We can demonstrate this approach with the following example. Convert the following infix
expression into postfix notation:

((X+Y)*(X-Z))/((X+Z)*(X-Y))

The parentheses tell us which operations must be performed first (these are highlighted),

((X+Y)*(X-Z))/((X+Z)*(X-Y))

For each of these operations we move the operator to the right of the operands. From this point,
we must consider these postfix terms as single elements (i.e. they are atomic). We will place
these terms in square brackets to indicate that they must be treated as single elements.

([XY+]*[XZ-])/([XZ+]*[XY-])

The next operations to be considered in this example are the multiplications.

([XY+]*[XZ-])/([XZ+]*[XY-])

These operators are moved to the right of their operands.

[XY+XZ-*]/[XZ+XY-*]

Finally the division is performed, so we move this operator to the right of its operands to obtain
the final result.
XY+XZ-*XZ+XY-*/

This expression is the postfix equivalent to the original infix expression. The postfix expression is
easier for a computer to evaluate since there is not operator precedence (e.g. multiplication and
division over addition and subtraction), no parentheses to consider and no need to look ahead to
find the next operation to perform. The computer can scan the postfix expression from left to right
one token at a time to compute the resulting value.

We can check to see if our postfix expression is valid (i.e. matches the infix expression from
which it was derived) by evaluating it just as we did the numeric postfix expression before, but
this time we will push the algebraic terms onto the stack rather than their computed values.
261
XY+XZ-*XZ+XY-*/ _X_ push the operand X onto the stack
XY+XZ-*XZ+XY-*/ Y push the operand Y onto the stack
_X_
XY+XZ-*XZ+XY-*/ _X+Y_ pop the last 2 operands and push the addition
XY+XZ-*XZ+XY-*/ X push X
_X+Y_
XY+XZ-*XZ+XY-*/ Z push Z
X
_X+Y_
XY+XZ-*XZ+XY-*/ X-Z pop the operands and push the resulting operation
_X+Y_
XY+XZ-*XZ+XY-*/ (X+Y)*(X-Z) pop the operands (infix terms) and push the product
XY+XZ-*XZ+XY-*/ X push X
(X+Y)*(X-Z)
XY+XZ-*XZ+XY-*/ Z push Z
X
(X+Y)*(X-Z)
XY+XZ-*XZ+XY-*/ X+Z pop the operands and push the resulting operation
(X+Y)*(X-Z)
XY+XZ-*XZ+XY-*/ X push X
X+Z
(X+Y)*(X-Z)
XY+XZ-*XZ+XY-*/ Y push Y
X
X+Z
(X+Y)*(X-Z)
XY+XZ-*XZ+XY-*/ X-Y pop the operands and push the resulting operation
X+Z
(X+Y)*(X-Z)
XY+XZ-*XZ+XY-*/ (X+Z)*(X-Y) pop the operands and push the resulting operation
(X+Y)*(X-Z)
XY+XZ-*XZ+XY-*/ ((X+Y)*(X-Z))/((X+Z)*(X-Y)) completed infix expression

Table 11.5: Using a Stack to Construct an In-Fix Expression From a Post-Fix Expression

We have learned how to evaluate a numerical postfix expression and how to generate postfix
expressions from infix expressions. We have also learned that postfix expressions are much
easier for a computer to evaluate than infix expressions. High-level languages such as Ada allow
us to write our arithmetic expressions as assignments in infix notation. The language compiler
then converts them to postfix as the machine language executable is being built.

Our task here is to write an Ada program that automatically converts an infix expression into its
equivalent postfix form, but the hand-method we have learned for generating postfix expressions
is not easily implemented in a computer program. We will want to find another approach. A

262
popular computer algorithm for converting infix expressions into postfix expressions uses two
stack ADTs.

Computer Generation of Postfix Expressions (Optional Material)

The algorithm provided below as pseudo code is to be executed on an input string representing a
valid infix expression. Assume you have established a stack for OPERATORs and an ANSWER
string. Scan the input string from the left to the right as you exercise the algorithm.

while tokens remain in the input string loop

get the next input_token from the input string

if the input_token is an operand then

output(ANSWER,input_token)

elsif the input_token is an operator then

while OPERATOR stack not empty and top(OPERATOR) /= '(' and


top(OPERATOR) precedence is >= input_token precedence loop

operator_token=pop(OPERATOR)

if operator_token /= '(' then


output(ANSWER,operator_token)
end if

end loop

push(OPERATOR,input_token)

elsif input_token = '(' then

push(OPERATOR,input_token)

else -- input token must be ')'

while top(OPERATOR) /= '(' loop


operator_token=pop(OPERATOR)
output(ANSWER,operator_token)
end loop

operator_token=pop(OPERATOR) -- this token must be a '('

end if

end loop

while OPERATOR stack not empty loop --flush the OPERATOR stack
output(ANSWER,pop(OPERATOR))
end loop

This algorithm relies on the establishment of an order of precedence for each operator. This
precedence follows our notion of operator precedence from basic arithmetic. That is,
multiplication and division have higher precedence than addition and subtraction. We can test
our algorithm on the same example infix expression we used in the previous exercise.

263
Infix Expression OPERATOR stack ANSWER postfix expression
((X+Y)*(X-Z))/((X+Z)*(X-Y)) (
__(___
((X+Y)*(X-Z))/((X+Z)*(X-Y)) (
__(___ X
((X+Y)*(X-Z))/((X+Z)*(X-Y)) +
(
__(___ XY

((X+Y)*(X-Z))/((X+Z)*(X-Y)) __(___ XY+


((X+Y)*(X-Z))/((X+Z)*(X-Y)) *
__(___ XY+
((X+Y)*(X-Z))/((X+Z)*(X-Y)) (
*
__(___ XY+
((X+Y)*(X-Z))/((X+Z)*(X-Y)) ______ XY+X
-
((X+Y)*(X-Z))/((X+Z)*(X-Y)) (
*
__(___ XY+XZ
((X+Y)*(X-Z))/((X+Z)*(X-Y)) *
__(___ XY+XZ-

((X+Y)*(X-Z))/((X+Z)*(X-Y)) __ ___ XY+XZ-*

(
((X+Y)*(X-Z))/((X+Z)*(X-Y)) (
__/___ XY+XZ-*X

+
((X+Y)*(X-Z))/((X+Z)*(X-Y)) (
(
__/___ XY+XZ-*XZ

(
((X+Y)*(X-Z))/((X+Z)*(X-Y)) *
(
__/___ XY+XZ-*XZ+X

-
((X+Y)*(X-Z))/((X+Z)*(X-Y)) (
*
(
__/___ XY+XZ-*XZ+XY

((X+Y)*(X-Z))/((X+Z)*(X-Y)) __/___ XY+XZ-*XZ+XY-*

((X+Y)*(X-Z))/((X+Z)*(X-Y)) __ ___ XY+XZ-*XZ+XY-*/

Table 11.6: Using a Stack to Construct a Post-Fix Expression from an In-Fix Expression

264
The implementation of this algorithm is challenging for the beginning programmer. It requires a
substantial program design effort before sitting down at the keyboard. An effective design
approach is to use a top-down problem decomposition.

As we scan the pseudo code we find a list of operations that should be written as separate
functions or procedures.

is_empty(input_string) - a boolean function to tell us when we have read all the tokens from the
input string.

get_next_token(input_token) - a procedure to get the next input_token (for our example we can
assume that all tokens are single ASCII characters)

is_operand(input_token) - a boolean function to recognize if an input_token is an operand

is_operator(input_token) - a boolean function to recognize if an input_token is an operator

precedence(input_token) - a function to return the precedence of an operator (we will need to


establish a number precedence for +, -, * and / operators (e.g. +=1, -=1, *=2, /=2).

output(ANSWER,token) - a procedure to append the next token to the ANSWER string

When testing for '(' and ')' we can simply compare the token to these ASCII characters directly.
Finally, we can use the interface provided in the adt_stack for the pop( ) and push( ) operations in
our algorithm implementation.

This application gives you a rough sketch of the effort required to implement the infix to postfix
expression converter. This program is a component of the Spreadsheet Class Project in
Appendix B.

Drawing the Centipede

11.4: Build a graphical program to display a centipede (segmented worm) moving across the
screen. The user should be able to specify the number and size of the segments and the
flexibility of the centipede (i.e. how quickly the centipede can change directions).

The segments can be drawn as circles of radius r (specified by the user). They need to be
separated by an amount equal to their diameters, so we can compute the position of the next
segment by converting the segment center position from Polar to Cartesian coordinates.

centipede body segments


...

current direction
del_ang

px = px _ old + 2 * r cos( ang )


py = py _ old + 2 * r sin( ang )
new segment

Figure 11-15: Segment Detail for the Centipede Graphical Animation


The maximum change in direction between segments del_ang (specified by the user) can be
used to generate a random angle ang for the coordinate conversions. Choosing 2*r for the
magnitude of the change in position ensures that the segments will be contiguous.
ang:=ang+(ranu-0.5)*del_ang;
px:=integer(float(px_old)+float(size)*cos(ang));
py:=integer(float(py_old)+float(size)*sin(ang));
265
As usual, we need to be careful about type casting. In this case, we convert our position px_old
and py_old to floating point so that we can multiply by the cosine and sine of the angle ang.

The problem statement does not mention what we should do when our centipede moves off the
edge of the display window. As the centipede leaves one side of the window we will begin
displaying it on the opposite side. This window wrapping operation can be implemented with a
set of if..then statements.

if px>=xm-size then
px:=size;
elsif px<=size then
px:=xm-size;
end if;

if py>=ym-size then
py:=size;
elsif py<=size then
py:=ym-size;
end if;

Next, we need to decide how we will use the adt_queue to help us implement the centipede
program. In general, we will use the queue to hold the locations of the individual segments so
that they can be erased later. As we generate new segments we will place their x,y positions in
the graph window into the back of the queue as we draw them in the display. When we are ready
to erase a segment, we dequeue the x,y position (of the oldest segment) and redraw that
segment using the background color, which erases it. We note here that this may also erase part
of another segment if the centipede has looped over top of itself, however, this should not detract
from the visual effect.

For this application, our generic data type will be a record containing the px, py plotting positions
of the segments. We declare a record type called segment type, which we will use as the
container type when we instantiate our queue ADT.

type segmentype is record


px : integer;
py : integer;
end record;

package my_queue is new adt_queue(segmentype);


use my_queue;

cent : qtype;

The package my_queue provides a private data type called qtype that is used to hold a list of type
segmentype. We declare cent to be a queue of this type. Since the data type segmentype is
available to us, we can create segments and pass them into our queue, cent, with the interface
enqueue(cent,curseg).

In the main program, we will ask the user to enter the number and size of the segments and the
flexibility of the centipede. The parameter will be defined as the maximum change in angle
between consecutive segments.

put("Enter number of segments.... ");


get(nseg);
new_line;

put("Enter segment size.......... ");


get(size);
new_line;
put("Enter max delta angle....... ");
266
get(delang);
new_line;
put("Amount of delay............. ");
get(lag);

Before opening a graph window and running the animation, we will create a centipede with the
number of segments as specified by the user and store this initial centipede in the container list of
the queue

with ada.text_io, ada.integer_text_io, ada.float_text_io,


adagraph, random, ada.numerics.elementary_functions, adt_queue;
use ada.text_io, ada.integer_text_io, ada.float_text_io,
adagraph, random, ada.numerics.elementary_functions;
procedure a_centipede is

type segmentype is record


px : integer;
py : integer;
end record;

package my_queue is new adt_queue(segmentype);


use my_queue;

cent : qtype;
curseg : segmentype;
oldseg : segmentype;
nseg : integer;
size : integer;
col : extended_color_type;
delang : float;
ang : float;
char : character;
xm : integer :=500;
ym : integer :=500;
lag : float;

begin
put("Enter number of segments.... ");
get(nseg);
new_line;
put("Enter segment size.......... ");
get(size);
new_line;
put("Enter max delta angle....... ");
get(delang);
new_line;
put("Amount of delay............. ");
get(lag);

for i in 1..nseg loop


curseg.px:=320;
curseg.py:=240;
enqueue(cent,curseg);
end loop;
ang:=6.28*ranu;

open_graph_window(xm,ym);
clear_window(white);
set_window_title("Centipede Demo Program");
if key_hit then

267
char:=get_key;
end if;
while not key_hit loop
delay(duration(lag));
dequeue(cent,oldseg);
draw_circle(oldseg.px,oldseg.py,size/2,white,fill);
ang:=ang+(ranu-0.5)*delang;
curseg.px:=integer(float(curseg.px)+float(size)*cos(ang));
curseg.py:=integer(float(curseg.py)+float(size)*sin(ang));
if curseg.px>=xm-size then
curseg.px:=size;
elsif curseg.px<=size then
curseg.px:=xm-size;
end if;
if curseg.py>=ym-size then
curseg.py:=size;
elsif curseg.py<=size then
curseg.py:=ym-size;
end if;
col:=color_type'val(rnd(13)+1);
enqueue(cent,curseg);
draw_circle(curseg.px,curseg.py,size/2,col,fill);
end loop;
close_graph_window;
end a_centipede;

Figure 11-16: Example Outputs of the Centipede Program

268
Exercises

11.1 Indicate which of the following are valid postfix expressions.

a. XYZ++ b. XY+Z+ c. XY++Z d. X+YZ+

e. XY+XZ-/ f. XXYZ+-/ g. YZ+XX-/ h. XXXX***

11.2 Convert each of the in-fix expressions below into the corresponding postfix expression.

a. (X+Y)/(X-Y) b. (XY-Z)*(Z-X) c. 1/(1+(1/(1+X))) d. X*Y*Z-Z*Z

11.3 Use a stack to evaluate each of the post-fix expressions below assuming X=1, Y=2, and
Z=3. Show the contents of the stack following each arithmetic operation.

a. XY+ZX-+ b. XYZX+-* c. ZXY+/ d. XYZ-+ZYX+-*

11.4 Starting with an empty queue, Q, show the contents of the Q following the indicated
operations.
a. enqueue(5,Q); b. enqueue(10,Q); c. enqueue(1,Q); d. enqueue(1,Q);
enqueue(25,Q); dequeue(X,Q); enqueue(2,Q); enqueue(front(Q)+1,Q);
dequeue(X,Q); enqueue(X*X,Q); enqueue(front(Q),Q); dequeue(X,Q);
enqueue(125,Q); enqueue(1000,Q); enqueue(3,Q); dequeue(X,Q);

11.5 Write a code segment that pops integers off a stack P and pushes them onto stack E if they
are even and onto stack O if they are odd. You can assume that all the stacks are defined and
that P is initially loaded with integers and that stacks E and O are initially odd.

11.6 Write a code segment that simulates the shuffling of a deck of cards. Initially you have two
queues, L and R (for left-hand and right-hand), containing 26 cards each and a queue P (for pile)
that is initially empty. Your code segment should alternate between L and R dequeuing a card
and enqueuing it into P.

11.7 Modify the code segment of Exercise 11.6 so that the queue L or R from which to dequeue
the next card is selected randomly with approximately .5 probability. Be sure to account for the
situation in which one of the queues L or R is empty.

11.8 Use the adt_list package in an application program to perform an insertion sort of a list of
integers as entered from the keyboard. An insertion sort traverses a sorted list to find the proper
location to place the next item to maintain an ordered list. When the user has finished entering
values the program should display the list in order.

11.9 Modify the program of Exercise 11.1 to find a particular value in the list or to report that the
value is not in the list.

11.10 Use the adt_list package in an application to build a list of employees. The employee data
is held in a record that includes fields for last_name, first_name, years_emp, age, and wage.
Your program should read the employee data from a text file.

11.11 Modify the program of Exercise 11.3 to search for all employees in the list that have been
employed for at least 5 years and make an hourly wage of less than 10.0 dollars.

11.12* Write an Ada program that uses adt_stack to build a stack of floats to evaluate simple
post-fix expressions. Refer to the following features as you develop your program:

1. The expressions are read as strings from keyboard entry.


2. All expressions use single-character identifiers A, B, C, ....

269
3. The number of identifiers is given by the user at runtime.
4. The program asks the user to enter a value for each identifier.
5. The program asks the user to type in a post-fix expression involving +, -, *, and / only.
6. The program then scans the string (left to right) as it uses a floating point stack to hold
the intermediate values of the computation. (see Application 11.2).

11.13 Modify the program of Exercise 11.5 to include the symbol ^ for exponentiation. The
exponent should be a single-digit integer in the range (1..9).

11.14 Write an abstract data type (generic package) for a new type of list called a deque. The
deque is similar to a standard queue except that values can be added or removed from either
end, however, no internal values are accessible. Your adt_deque should provide all the controls
for manipulating the data structure through the interface (i.e. a collection of functions and
procedures).

11.15 Write an Ada program that extends the centipede program of Application 11.4 to generate
multiple centipedes. The number of centipedes should be input by the user.

11.16 Write an Ada program that uses a stack to hold the positions of the end points of randomly
drawn lines. The maximum number of segments, N, and the maximum distance moved between
segments, D, are specified by the user. The animation draws a random line of N segments and
then undraws the line in the reverse order.

270
Chapter 12 - Trees

12.1 Preliminary Definitions


tree path root internal node leaf node
height degree diameter breadth siblings
sub tree cycle sparse tree binary tree child node
left-child right-child full tree complete tree permutation tree
weighted tree

12.2 Tree Data Representations


Array Representation of a Binary Tree
Edge-List Representation of a General Tree
Representation of a Binary Tree Using Dynamic Memory
Representation of a General Tree Using Dynamic Memory
General Tree Implemented with Binary Tree Nodes

12.3 Tree Traversals


Implementing a Depth-First Traversal
Implementing a Breadth-First Traversal

12.4 Applications
Depth-First Tree Traversal Demonstration
Permutations
Sum of Subsets

271
Chapter 12 - Trees
In this chapter, we give a formal introduction to trees as abstract data types. We investigate the
various machine representations for trees and review a number of sample applications using
these representations.

12.1 Preliminary Definitions

Trees are the first non-linear data structure we will study. They are an important category of a
more general class of data structures called graphs. We will review graphs, in detail, in Chapter
14.

A tree T is defined as a set of nodes V (also called vertices) with one node designated as the root
node and a list of edges E connecting the nodes such that there is exactly one path between
every pair of nodes,

T = {V,E}
V = {v1,v2,v3, . . . ,vn},
E = {(v1,vi), (vj,vk), . . . , (vm,vn)}

A path P from node vs to vt is a sequence of nodes P={vs, . . . ,vt} with zero or more intermediate
nodes and every consecutive pair of nodes in P corresponding to an edge in E. In Figure 12-1
below, the path Pkm between node k and node m is Pkm = { k,f,b,a,d,i,m}.

a root
a child of
node 'a'
b c d

subtrees

e f g h i j

k l leaf m
nodes

Figure 12-1: Graphical Representation of a Tree

The root of a tree is the first or top node in the tree. Once the root node has been designated
many of the properties of the other nodes are defined in terms of their relation to the root. When
two nodes are connected by an edge, the node closer to the root node is called the parent node
and the other node is called a child node. The root node is the only node that has no parent.

An internal node in a tree is any node that has at least one child and is not the root node. There
are four internal nodes in the tree shown in Figure 12-1. Can you find them?

A leaf node in a tree is any node that has no child node. The leaf nodes of the tree in Figure 12-1
are c, e, g, h, h, k, l, and m.

The level of a node in a tree is the distance that node is from the root. By this definition, the root
of a tree is at level one. In Figure 12-1, the nodes b,c, and d are level-two nodes, while nodes k,
l, and m are level-four nodes. Sometimes the root is said to be at level-zero, but we choose to
define the root at level-one so we can refer to an empty-tree as level-zero.

The height of a tree is the level of the most distant leaf node. The tree in Figure 12-1 is height 4.

272
The degree of a node is the number of edges incident with it. For example, node d in Figure 12-1
is degree 5 (i.e. it has four child nodes and one parent node).

The diameter of a tree is the length of the longest path in the tree. The diameter of the tree
shown in Figure 12-1 is 6. Can you find the pair of nodes that define the diameter of this tree?

We will define the breadth of a tree as the maximum number of nodes at any level. The breadth
of the tree in Figure 12-1 is 6 and is determined by the number of nodes in level 3. Note: this
parameter is different than the width of a graph.

Siblings are two or more nodes in a tree with the same parent. Any node can have a sibling
except the root node.

A sub tree is any connected subset of nodes in a tree that does not include the root. Sub trees
are defined by the node in the set of nodes that is closest to the root. For example, the three sub
trees in the shaded areas of Figure 12-1 are labeled sub trees b, c, and d. These nodes become
the "root" nodes of their respective sub trees.

There are three equivalent definitions of a tree in graph theory:


(1) a graph with exactly one path between every pair of nodes
(2) a connected graph with exactly one more node than edges
(3) a connected graph with no cycles
where a cycle is a closed-loop path from a node through one or more other nodes and back to the
original node. Each of these three definitions of a tree is important to the development of graph
theory. For now, it is enough to know the terminology and understand the definitions.

When a tree has a small number of nodes compared to its height we refer to the tree as sparse.
There are no clearly defined conditions that make a tree a sparse tree. Rather, we call a tree
sparse when it has fewer nodes than other trees of similar heights for a particular application.

A binary tree is a particular type of tree that is important to computer science. A binary tree is a
tree in which each node has, at most, two children. The children are named the left-child and the
right child (if they exist). Figure 12-2a shows a typical binary tree.

a a a

b c b c b c

d e f d e f g d e f g

g h i j k h i j k l m n o h i j k l
a. binary tree b. full binary tree c. complete binary tree
Figure 12-2: Types of Binary Trees

A full binary tree is a binary tree in which each level that exists has a maximum number of nodes.
k
The number of nodes in a k-level full binary tree is 2 -1. Figure 12-2b shows a full binary tree of
height = 4.

A complete binary tree is a binary tree, which has been completely expanded, in a level-by-level
left-to-right order. Note that in the complete binary tree of Figure 12-2c, node g cannot have a
child node until node f has a right-child node.

273
1
order of
expansion

2 3

4 5 6 7

8 9 10 11 12

Figure 12-3: Expansion of a Complete Binary Tree

As shown in Figure 12-3, a complete binary tree is formed by starting with the root node and
moving down through the levels creating nodes in a left-to-right order. No node in a complete
binary tree can exist until all its left-hand siblings exist, and no node can have a child node until
every other node exists at the same level.

Another important type of tree is called the permutation tree. This type of tree is used to manage
the possible permutations of a set of elements. When building a permutation tree, we let the root
node represent the starting condition for which no elements of the permutation have yet been
selected. The children of the root node are all elements that can be chosen as the first element.
Once an element has been selected it is no longer available. Therefore at each level the number
of choices is reduced by one until the last element has been selected.

A B C D

B C D A C D A B D A B C

C D BD BC CD A D AC B D AD AB B C A C A B

D C D B C B D C D A C A D B D A B A C B C A B A

Figure 12-4: Example Permutation Tree for Four Elements (A,B,C,D)

The possible permutations of a set of four elements (A, B, C, D) are represented in the
permutation tree of Figure 12-4 above. Each path from the root node to a leaf node in the
permutation tree represents one of the possible permutations (arrangements) of the elements. In
this example, we see that there are a total of 4! = 24 arrangements of the 4 elements.
A weighted tree is a tree with data values associated with each edge. The meaning of the
weights depends on the application being modeled with the tree. For example, the weights can
represent the information bandwidth between two nodes of the Internet, or they can represent the
distances between cities in a transportation problem.

274
12.2 Representations of Tree Data Structures
Many problems applicable to computer solution can be represented using a tree data structure.
For example, the solution to the maze shown in Figure 12-5 below can be obtained by a traversal
of the embedded tree data structure. Internal nodes in this tree represent decision points in the
maze while leaf nodes represent the start, the finish, and the dead-ends in the maze.

Figure 12-5: Application of a Tree Data Structure to a Simple Maze


In this section, we will review various methods of representing tree data structures in a
computer's memory. The best choice of data representation will depend on the particular
application of the problem and the operation of the algorithm being implemented to solve the
problem.
Array Representation of a Binary Tree
A binary tree can be embedded into a one-dimensional array data structure as shown in Figure
12-6 below. The root of the tree is in the first position of the array. The children of the root are in
positions 2 and 3. The children of the ith node in the tree are at positions 2i and 2i+1. The
relationships between a node, its parent and its children are shown below.

left_child of node i is 2*i


right_child of node i is 2*i+1
parent of node i is i/2 (integer division)

Figure 12-6: An Array Representation of a Binary Tree


These three rules can be used to compute the indices of the children and the parent of each
node. In the array representation of a tree of N nodes, all entries in the array beyond position N/2
represent leaf nodes. Can you see why this is true? The array representation for a binary tree
will be used in Chapter 13 to create an efficient algorithm for sorting values into ascending order
(called heapsort). However, there are limitations to this array representation. For example, we
can only represent binary trees in an array data structure and when the tree is not full, there can
be many blank positions in the array.

Edge List Representation of a General Tree

We can store the data for an N-node tree in an N-element array called the node list, shown in
Figure 12-7 on the next page. The relationships between parent nodes and child nodes can be
represented in an edge list, which is N-1 pairs of indices into the node list. The first value in the
edge list is the index of the parent node and the second value is the index of the corresponding
child node.
275
node list edge list root
1 A 1 2 A
2 B 1 9
3 E 1 10
4 F 2 3
5 K 2 4
B C D
6 L 2 7
7 G 2 8
8 H 4 5
9 C 4 6
E F G H I J
10 D 10 11
11 I 10 13
12 M 11 12
13 J 13 14 K L M N
14 N

Figure 12-7: Edge List Representation of a General Tree

Representation of a Binary Tree Using Dynamic Memory

Now we will look at ways to use dynamic memory management to define and associate tree
nodes in a computer's memory. Just as we created dynamic records for a linear linked list, we
can create records to represent the nodes of a binary tree.

type node;
type pointer is access node; data
type node is record
data : data_type; left-child right-child
left_child : pointer; pointer pointer
right_child : pointer;
end record; Figure 12-8: Dynamic Memory
Record for a Binary Tree

These records can be linked together to construct a binary tree using the same programming
constructs used to create linked-lists in the stack and queue ADTs. The advantage of this
implementation is that we allocate only as much memory as we need to hold the active data
values. In the example tree shown in Figure 12-9 on the next page, the dynamic memory
implementation requires 12 records as defined in Figure 12-8.

The same tree represented in a static memory model would require an array of size at least 46
entries. If we assume that we will need to hold other nodes at the same level as the node
containing the value L, we would have to declare an array with a total of 63 elements.

276
root

B C

D E F

G H I J

Figure 12-9: A Dynamic Memory Representation of a Binary Tree

Representation of a General Tree Using Dynamic Memory

When we wish to represent a tree with more than two children in a computer we can use a more
general dynamic memory record. In the data structure shown in Figure 12-10, we provide a link
to a dynamic list of child nodes. Each of these nodes points to a tree node record.
type treenode;
type childnode;
type childpointer;
type treepointer is access treenode; root
type childpointer is access childnode;
type treenode is record
data
data : data_type;
clink : childpointer;
end record; null

type childnode is record


clink : childpointer;
child : treepointer;
end record;

data data data data

null null
null null

Figure 12-10: Dynamic Memory Representation of a General Tree Node

General Tree Implemented with Binary Tree Nodes

A general tree can also be represented using the same dynamic memory nodes used to
represent a binary tree. In the example on the next page, we see that the left-child pointer of a
binary tree node is used to point to the left-most child node and the right child pointer points to the
next sibling. The right-child pointer of the last sibling points to null. Using this representation, a
general tree node can have an arbitrary number of children.
277
root
A
A

B C D
B C D

E F G H I J
E F G H I J

K L M N K L M N

Figure 12-11: Example of a Dynamic Memory Representation for a General Tree

In Figure 12-11, we see that the root node A has three children, B, C, and D. Since node C is a
leaf node, its left-child node points to null. In this diagram, all pointers without arrows are
assumed to point to null.

12.3 Tree Traversals

In applications using tree data structures, we will need to visit the nodes of the tree. In this
section, we will study a number of methods of visiting the nodes of a tree called tree traversals.
In a typical application, the only access we have to a tree is through its root, so we will limit our
study of traversal methods to those starting at the root.

The two most common methods of tree traversal are depth-first and breadth-first. Before we look
at the details of their implementation we need to gain a general understanding of their operation.

A depth-first traversal takes a direct path from the root to a leaf node, moving from level to level in
the tree. In the example shown in Figure 12-12, we see a depth-first traversal from the root node
1 to the leaf node 45. At each level a decision is made as to which edge to choose to move to
the next lower level.

Once a leaf node is reached, a depth-first traversal backtracks to the parent node to choose
another direction, if one exists. In this example, the parent of node 45 is 32, but there is no other
child node for this node, so we backtrack to node 23. The depth-first traversal then chooses either
node 33 or node 34 as the next node to visit.

A breadth-first traversal visits all the nodes at a given level before visiting any nodes at the next
lower level in the tree. In the example shown in Figure 12-12, the shaded nodes have been
visited and the next node to be visited will be node 11 or node 12. As with depth-first, there is no
strict requirement for the selection of the next node from the list of choices but it would need to be
one of the nodes at the current level.

278
1

2 3 4 5

6 7 8 9 10 11 12

13 14 15 16 17 18 19 20 21

22 23 24 25 26 27 28

29 30 31 32 33 34 35 36 37 38 39 40

41 42 43 44 45 46 47 48 49 50 51 52

Figure 12-12: Depth-First and Breadth-First Traversal in a Tree


When no other constraints apply, we will use the convention of choosing the nodes in an alpha-
numeric order from the set of valid nodes. By this convention, depth-first and breadth-first
traversals of the tree above would visit the nodes in the following order:

depth-first {1,2,6,7,13,14,22,29,41,42,43,44,30,31,15,8,16,23,32,45,33,34,46, 3, . . . , 40,51,52}

breadth-first {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23, . . . , 50,51,52}

Starting from the root node 1, the depth-first can choose from nodes 2, 3, 4, or 5. Using the
alpha-numeric convention we choose 2. Now from node 2 the valid nodes are 6, 7, and 8. We
choose 6. From 6 there are no valid nodes so we backtrack to node 2 and choose the next node
3 to continue our traversal.

Since the nodes in this example tree were labeled in a breadth-first manner their order in a
breadth-first traversal is more straightforward. The details of implementation of algorithms for
these traversal methods are presented in the following subsection.

Implementing a Depth-First Traversal

Depth-first traversal is inherently recursive in its implementation. It uses an implied stack data
structure to hold pending copies of the recursive calls of the procedure as it moves from node to
node in the tree. The following pseudocode gives an overview of this method.
procedure DFT(this_node : in treepointer) is
begin
-- do stuff with this_node
for each child(i) of this_node loop
if child(i) available then
DFT(child(i));
end if
end loop
end DFT;
279
Each time DFT( ) is called, the current copy of DFT( ) is pushed onto a process stack along with
the current state of its parameters (and CPU register values, etc.). When we encounter a leaf
node (or any node which has no available child nodes) no recursive calls will be made. This copy
of DFT( ) will then terminate, and the copy of DFT( ) that is on the top of the process stack will be
popped back into active execution.

It is important to understand that the first available child node encountered causes the current
copy of DFT( ) to be interrupted and pushed onto the process stack. Even though several child
nodes may have been available at the time DFT( ) was interrupted, some or all of them may be
visited by the time this copy of DFT( ) is reactivated.

Let's look at a depth-first traversal in a binary tree. For a binary traversal there will be, at most,
two children, so we can replace the for..loop with two if..then constructs as shown below.

procedure DFT_bin(this_node : in treepointer) is


begin
-- do stuff with this_node
if left_child(this_node) is available then
DFT_bin(left_child(this_node));
end if;
if right_child(this_node) is available then
DFT_bin(right_child(this_node));
end if;
end DFT_bin;

We traverse a tree to accomplish some task. Since the nodes are the data containers for the
tree, the task we perform will usually involve the contents of the nodes in some way. We could
be searching for a particular value, arranging the values into a particular order, or keeping track of
the sequence of nodes encountered in a path in the tree. The comment line in the previous
examples "--do stuff with this_node" represents some unspecified task to be performed on each
node.

You may have noticed that the stuff being done with a node is happening before the recursive
calls in the example code shown above. This is not necessarily the best way to implement some
applications using tree data structures. We could place the task being performed on the node
after one or both of the recursive calls. For example, consider the following pseudocode,

DFT_NLR(this_node) DFT_LNR(this_node) DFT_LRN(this_node)


begin begin begin
-- do stuff with this_node DFT_LNR(this_node.lchild); DFT_LRN(this_node.lchild);
DFT_NLR(this_node.lchild); -- do stuff with this_node DFT_LRN(this_node.rchild);
DFT_NLR(this_node.rchild); DFT_LNR(this_node.rchild); -- do stuff with this_node
end DFT_NLR; end DFT_LNR; end DFT_LRN;

Figure 12-13: Three Versions of Depth-First Traversal in a Binary Tree

We have designated these three possible versions of depth-first traversal as NLR, LNR, and LRN
where N refers to work on the current Node, L refers to a recursive call to the Left child node, and
R refers to a recursive call to the Right child node.

When the operations on the contents of the current node are performed before either child node
is visited we call the tree traversal a pre-order traversal. When the operations on the contents of
the current node are performed after visiting the left child node but before visiting the right child
node, we call the tree traversal an in-order traversal. Finally, when the operations on the
contents of the current node are performed after visiting both child nodes we call the tree
traversal a post-order traversal.

You may have noticed that in all tree traversals we visit the left child before the right child. In
large part this is just a convention that has developed, but as we will see in Chapter 13 there are

280
some applications in which the choice of left child (or right child) first is an essential part of an
algorithm implementation.

As we have seen, the implementation of depth-first tree traversals uses a recursive function.
What is less obvious is that this use of recursion also involves a stack data structure. Each time
a function or procedure is called, the current state of the calling routine is pushed onto a stack by
the executing program. This stack holds all the information necessary to reinstate the calling
routine when the called function or procedure finishes and returns control.

: root 1

procedure LNR(node) is;


if not(node=null) then
register
put(node.data);
values
2 9
(a) LNR(node.left);
(b) LNR(node.right);
CPU
end if;
end LNR;
3 6 10 13

:
4 5 7 8 11 12 14 15

4a 4b 5a 5b 7a 7b 8a 8b 11a
3a 3a 3a 3b 3b 3b 6a 6a 6a 6b 6b 6b 10a 10a ...
2a 2a 2a 2a 2a 2a 2a 2b 2b 2b 2b 2b 2b 2b 9a 9a 9a
1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1b 1b 1b 1b

Figure 12-14: Contents of the Process Stack During Execution of a Recursive Procedure

Let's look at this process in greater detail. A recursive procedure NLR(root) is called to visit the
nodes in the tree shown in Figure 12-14, above. Since root is not null, the content of the root is
displayed. Then the left child of this node is entered by a recursive call to NLR(node.left). At this
time, the current copies of the CPU registers (and some other information defining the current
state of this procedure) are pushed onto a stack (called the process stack). We refer to this
information as 1a in the figure above since a new copy of NLR( ) (for node 2) was called at line a
in the current copy of NLR( ). When the copy of NLR( ) corresponding to node 2 is finished, the
state of the original copy of NLR( ) will be reinstated and line b will be executed. Since the tree
being traversed is of depth four, a maximum of four copies of NLR( ) will be on the process stack
at any time. When the node passed to a copy of NLR( ) is null, no recursive calls are made and
the procedure immediately returns control to the copy of NLR( ) at the top of the stack. When a
copy of NLR( ) is reinstated following its second recursive call (line b) there is nothing remaining
for it to do; so it leaves the if..then structure and returns control to the next copy of NLR( ) at the
top of the process stack. When the stack is empty, the routine that made the original call to
NLR( ) will be back in control.

Note that the programmer does not need to instantiate a stack ADT or reserve memory for the
process stack. Its creation and maintenance are controlled by the operating system and the
executing program containing the recursive function or procedure, and they are transparent to the
application programmer. However, it is important to understand how recursive calls are handled
in order to avoid exceeding system resources and to make good decisions as to when recursion
should be used in implementing a computer solution to a problem.

281
Implementing a Breadth-First Traversal

In some applications, we need to search a tree data structure in a breadth-first manner.


As stated earlier in this Chapter, a breadth-first traversal visits all the children of a node before
visiting any of the nodes at a lower level. A breadth-first traversal is an iterative rather than a
recursive process. Also, it makes use of a queue data structure rather than a stack. The queue
holds the nodes in the order they will be visited. When a node is taken from the front of a queue
(dequeued) all its child nodes are placed at the back of the queue (enqueued). Each node is
visited in the order it is encountered in the queue.

Before we try to write Ada code for this process, let's exercise a breadth-first traversal of a
permutation tree.

A B C D

2 3 4 5

B C D A C D A B D A B C

6 7 8 9 10 11 12 13 14 15 16 17

C D B D B C C D A D A C B D A D A B B C A C A B

18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41

D C D B C B D C D A C A D B D A B A C B C A B A

42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65

Figure 12-15: Sample Permutation Tree

Each path in the tree shown in Figure 12-15 represents one of the 24 possible permutations of
the elements ABCD. Each node represents some step toward one of these permutations. At
node 1, we have not chosen any items. Nodes 2 through 5 represent the choice of the first
character, nodes 6 through 17 represent the choice of the first two characters, nodes 18 through
41 represent the choice of the first three characters, and nodes 42 through 65 represent the 24
complete four-character sequences.

A breadth-first traversal of this tree begins by placing node 1 into the queue. With this single
node as a beginning we will perform the following operations until the queue is empty.

1. Dequeue the node at the front the queue.

2. Add its value to the output list.

3. Place all the children of this node into the back of the queue.

4. If the queue is not empty return to Step 1, otherwise end.

282
For our example, the contents of the queue and the output list will take on the following values.

queue 1 output list


5432 1
876543 12
11 10 9 8 7 6 5 4 123
14 13 12 11 10 9 8 7 6 5 1234
17 16 15 14 13 12 11 10 9 8 7 6 12345
19 18 17 16 15 14 13 12 11 10 9 8 7 123456
back
21 20 19 18 17 16 15 14 13 12 11 10 9 8 1234567

23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 front 12345678...

Figure 12-16: Queue and Output List for a Breadth-First Traversal

The state of the breadth-first traversal shown in Figure 12-16 corresponds to the shaded nodes in
Figure 12-15. The dark gray nodes represent those that have been placed on the output list and
have had their children placed at the back of the queue. The light gray nodes are those that have
been placed in the queue but have not yet been expanded (i.e. they have not yet had their
children placed in the queue).

Eventually, the nodes remaining in the queue will be leaf nodes so no additional nodes will be
placed into the queue as these nodes are removed and added to the output list. When the queue
is empty, the process will be complete and the output list will contain the list of nodes in the order
they were visited in the breadth-first traversal.

12.4 Applications

12.1: Develop an Ada program to demonstrate the three types of depth-first traversal of a binary
tree (NLR, LNR, and LRN). This program should build a dynamically linked memory model of a
binary tree. The placement of the nodes should use the following rules:

1. To place a value X in the binary tree, start at the root node.


2. If X is < = the current node value then move to the left-child node if available.
3. If X is > the current node value then move to the right-child node if available.
4. Repeat steps 2, 3, and 4 until you encounter a location where there is no child node.
5. If there is no child node then create one at this location and place X in this new node.

We will first define the data structure for the dynamic memory tree node. This record will include
a field for the data type being stored in the node and pointers for left-child and right-child.

type treenode;
type treepointer is access treenode;
type treenode is record
data : integer;
lchild : treepointer;
rchild : treepointer;
end record;

The pseudocode listed in the five steps of the problem statement suggests the use of a recursive
procedure. The procedure placenode( ), shown on the next page, implements this pseudocode.
The value being placed, val, is passed into placenode( ) along with the tree pointer initially
pointing to the root of the tree. If root is initially set to point to null then the first value placed in
the tree causes a new node to be created immediately.

283
procedure placenode(val : in integer; node : in out treepointer) is
begin
if node=null then
node:= new treenode'(val,null,null);
elsif val<=node.data then
placenode(val,node.lchild);
else
placenode(val,node.rchild);
end if;
end placenode;

Once the tree has some nodes, the root is no longer pointing to null so the value being placed,
val, is compared to the value of the current node. If val <= node.data then placenode( ) makes a
recursive call with the left-child pointer of the current node being passed as the new node; else,
the recursive call is to placenode( ) with the right-child pointer of the current node. Eventually, a
null node is reached and a new node is created to hold val.

The three methods of depth-first tree traversal follow the pseudocode of Figure 12-13. In this
demonstration, we will create recursive procedures climbtree_NLR( ), climbtree_LNR( ), and
climbtree_LRN( ) to traverse the binary tree created using placenode( ). A complete listing of our
demonstration program is provided below.

with ada.text_io, ada.integer_text_io;


use ada.text_io, ada.integer_text_io;
procedure tree_traversal_demo is
type treenode;
type treepointer is access treenode;
type treenode is record
data : integer;
lchild : treepointer;
rchild : treepointer;
end record;
val : integer;
root : treepointer;

procedure placenode(val : in integer; node : in out treepointer) is


begin
if node=null then
node:= new treenode'(val,null,null);
elsif val<=node.data then
placenode(val,node.lchild);
elsif val>node.data then
placenode(val,node.rchild);
end if;
end placenode;

procedure climbtree_LNR(node : in treepointer) is


begin
if not(node.lchild=null) then
climbtree_LNR(node.lchild);
end if;
put(node.data,0);
put(" ");
if not(node.rchild=null) then
climbtree_LNR(node.rchild);
end if;
end climbtree_LNR;

procedure climbtree_LRN(node : in treepointer) is


begin

284
if not(node.lchild=null) then
climbtree_LRN(node.lchild);
end if;
if not(node.rchild=null) then
climbtree_LRN(node.rchild);
end if;
put(node.data,0);
put(" ");
end climbtree_LRN;

procedure climbtree_NLR(node : in treepointer) is


begin
put(node.data,0);
put(" ");
if not(node.lchild=null) then
climbtree_NLR(node.lchild);
end if;
if not(node.rchild=null) then
climbtree_NLR(node.rchild);
end if;
end climbtree_NLR;

begin
loop
put("Enter next value or -999 to quit... ");
get(val);
exit when val=-999;
placenode(val,root);
end loop;
if not(root=null) then
climbtree_LNR(root);
new_line;
climbtree_LRN(root);
new_line;
climbtree_NLR(root);
end if;
end tree_traversal_demo;

In each case, we will call the climbtree( ) procedure with root as the starting node. The output of
an example execution of tree_traversal_demo is shown below.

Enter next value or -999 to quit... 3


Enter next value or -999 to quit... 6 root
Enter next value or -999 to quit... 5
Enter next value or -999 to quit... 7 3
Enter next value or -999 to quit... 8
Enter next value or -999 to quit... 8 2 6
Enter next value or -999 to quit... 2
Enter next value or -999 to quit... 9 1 5 7
Enter next value or -999 to quit... 1
Enter next value or -999 to quit... 0 0 8
Enter next value or -999 to quit... -999
8 9
0 1 2 3 5 6 7 8 8 9
0 1 2 5 8 9 8 7 6 3
3 2 1 0 6 5 7 8 8 9

Figure 12-17: Example output of tree_traversal_demo

The values 3, 6, 5, 7, 8, 8, 2, 8, 1, and 0 are placed into a binary tree using the placenode( )
procedure. This produces a binary tree as shown on the right above. Since the value 3 was the
285
first value placed, it is in the root node. The remaining values are placed to the left or right of the
current node depending on their relative magnitudes.

The three rows of values at the bottom of the output are from climbtree_LNR( ), climbtree_NLR( ),
and climbtree_LRN( ) respectively. Do you notice any useful properties exhibited by any of these
tree traversal methods?

12.2: Use breadth-first tree traversal to create an Ada program that generates all the
permutations of a list of characters input by the user.

We can see the relationship between permutations and the permutation tree as shown in Figure
12-15 as a guide in solving this problem. Each path in the permutation tree represents one of the
possible permutations of the list of characters. We will use a queue to hold the nodes being
visited. The nodes being placed into the queue should each contain an array of characters
(called perm) for each of the permutations we are building. We will use a corresponding Boolean
array (called used) as a way to keep up with which characters have been used so far.

maxn : constant integer := 10;


type permlist is array(1..maxn) of character;
type usedlist is array(1..maxn) of boolean;
type qnodetype is record
perm : permlist; -- the list of chars in on of the permutations
used : usedlist; -- tagged which characters have been used
n : integer; -- the number of characters used so far
end record;

We have created a record qnodetype to hold these fields as shown above. Since we need a
queue, we can make use of the adt_queue generic created in the previous chapter. We will
instantiate a queue of qnodetype, which we will call bft_queue.

package bft_queue is new adt_queue(qnodetype);


use bft_queue;

According to the problem statement, the user will enter a string of characters to permute. Since
there are n! permutations possible for n characters we will limit our demonstration to a maximum
of 10 characters (maxn=10). To simplify user input we will read the characters as a single string
and then parse the string into a character array. The get_line(str,leng) procedure obtains the
number of characters, leng, as well as the string, str, of characters.

put("Enter a list of (<=10) characters to permute... ");


get_line(str,leng);
for i in 1..leng loop
chars(i):=str(i);
end loop;

Initially, we will not have begun any permutations so the perm field of our first node in the queue
will be empty, and all the entries of the boolean array used will be set to false. The queue Q is
primed with a node representing this initial condition before we enter the code segment to
perform the breadth-first traversal.

for i in 1..maxn loop


node.perm(i):=' ';
node.used(i):=false;
end loop;
node.n:=0;
enqueue(node,Q);

The core of this program is a while..loop that takes nodes from the front of the queue, updates the
perm and used arrays, and puts each new permutation node back into the back of the queue.

286
This process is repeated until the queue is empty. When a node is encountered for which all
characters have been used, the character string in perm is displayed.

while not(is_empty(Q)) loop


dequeue(node,Q);
if node.n=leng then -- if all characters have been used then
display_list(node); -- the permutation is complete and ready
end if; -- to display
node.n:=node.n+1;
for i in 1..leng loop
tnode:=node;
if tnode.used(i)=false then -- for each character that has
tnode.perm(tnode.n):=chars(i); -- not yet been used a partial
tnode.used(i):=true; -- permutation is created with
enqueue(tnode,Q); -- the current perm and one of
end if; -- the new characters appended
end loop;
end loop;

A complete listing of this program is provided below with a partial example output for the string
ABCDE.

Enter a list of (<=10) characters to permute... ABCDE


ABCDE
ABCED
ABDCE
:
EDCAB
EDCBA

with ada.text_io, ada.integer_text_io, adt_queue;


use ada.text_io, ada.integer_text_io;
procedure permutation_tree is
maxn : constant integer := 10;
type permlist is array(1..maxn) of character;
type usedlist is array(1..maxn) of boolean;
type qnodetype is record
perm : permlist;
used : usedlist;
n : integer;
end record;

package bft_queue is new adt_queue(qnodetype);


use bft_queue;

Q : queue;
node, tnode : qnodetype;
chars: permlist;
str : string(1..maxn);
leng : integer;

procedure display_list(X : in qnodetype) is


begin
for i in 1..X.n loop
put(X.perm(i));
end loop;
new_line;
end display_list;

begin

287
for i in 1..maxn loop
node.perm(i):=' ';
node.used(i):=false;
end loop;
node.n:=0;
enqueue(node,Q);
put("Enter a list of (<=10) characters to permute... ");
get_line(str,leng);
for i in 1..leng loop
chars(i):=str(i);
end loop;
while not(is_empty(Q)) loop
dequeue(node,Q);
if node.n=leng then
display_list(node);
end if;
node.n:=node.n+1;
for i in 1..leng loop
tnode:=node;
if tnode.used(i)=false then
tnode.perm(tnode.n):=chars(i);
tnode.used(i):=true;
enqueue(tnode,Q);
end if;
end loop;
end loop;
end permutation_tree;

12.3: Modify the program written for application 12.2 to generate the sum of subsets for the input
character string.

In this case, we only need to permit the partial permutations to be displayed. In the top of the
while..loop, we are testing to see if all the character have been used before we print. To generate
the sum of subsets we will replace

if node.n=leng then
display_list(node);
end if;
with
display_list(node);
An example output from this modified program is provided below.

Enter a list of (<=10) characters to permute... ABC


A
B
C
AB
AC
BA
BC
CA
CB
ABC
ACB
BAC
BCA
CAB
CBA

288
Exercises

12.1 For the tree shown below give,


a. the root node a
b. the internal nodes
c. the leaf nodes
b c
d. siblings of d
e. children of f d e f
f. parent of g
g h i j k

12.2 Sketch an example tree that matches each of the following descriptions.
a. a 10 node tree of height 3 b. an 8 node tree with no internal nodes
c. a complete binary tree with the same number of internal nodes as leaf nodes
d. a 10 node tree in which each node is either degree 3 or degree 1

12.3 Which of the binary trees below are complete and which are full?

a.
b.

c. d.

12.4 Give a mathematical expression for the number of nodes or the number of edges (as
indicated) for trees with the following properties.
a. The number of nodes in a full binary tree of height n.
b. The number of nodes in a permutation tree in which each path from the root to a leaf node
represents one of the m! (m factorial) tours of m cities.
c. The maximum number of nodes in a tree of height n in which every node is degree k or
degree zero.
d. The minimum height of a complete binary tree containing n nodes.

289
12.5 Give the best data structure for implementing a binary tree for each of the following
applications.
a. an application in which we frequently will need to access the parent of a node in the binary
tree.
b. an application in which we will need to associate a large amount of data (such as a multi-
field record data type) with each node
c. an application in which we anticipate a sparse binary tree of relatively large height
d. an application in which we will frequently need to scan the contents of a full binary tree and
we know the size of the tree before compile time

12.6 Write an Ada function parent_of(n) that returns the index of the parent node in an edge list
representation of a general tree, given the index of the child node. For example, in Figure 12-8
parent_of(11) would return the value 10, and parent_of(7) would return the value 2.

12.7 Write an Ada function max_degree(T) that traverses a tree (you get to choose the data
representation for the tree T) and returns the maximum degree of any node in the tree.

12.8 Write an Ada program that can read an edge list representation of a general tree from a text
file and build a dynamic memory representation of the tree equivalent to that shown in Figure 12-
10. (See Exercise 12.9 for the reverse operation.)

12.9 Write an Ada program that can scan a dynamic memory representation of a general tree as
shown in Figure 12-10 and save this tree as an edge list representation in a text file. (See
Exercise 12.8 for the reverse operation.)

12.10 Write an Ada program to accomplish the task described in Exercise 12.8, except with the
dynamic memory representation of a general tree as illustrated in Figure 12-11.

12.11 Write an Ada program to accomplish the task described in Exercise 12.9, except with the
dynamic memory representation of a general tree as illustrated in Figure 12-11.

12.12 Using the program provided in Application 12.1, implement and test a function that
traverses the tree and a count of the number of nodes in the binary tree.

290
Chapter 13 - Searching and Sorting Algorithms

13.1 Linear Search (unordered lists)

13.2 Binary Search (ordered lists)

13.3 Brute-Force Sorting Routines


Exchange Sort
Insertion Sort
Bubble Sort
Worst-Case Performance
Shell Sort

13.4 Optimal Sorting (by key comparison)


TreeSort
Run Time Performance of TreeSort
HeapSort
Analysis of HeapSort
QuickSort

13.5 Hashing
Hash Function
Hash Tables
Implementing a Hash Table
Analysis of Hashing

291
Chapter 13 - Searching and Sorting Algorithms
In this chapter, we will study the methods of searching and sorting a list of elements that satisfy
the condition that they are elements of a well ordered set.

Before studying methods for sorting, we need to have a basic understanding of the property of
order. A set of elements, S, is said to be a well ordered set if and only if there exists a relation, <,
defined on the set, which satisfies three properties:
(1) for any elements a, b in S either a = b, a < b, or b < a.
(2) for every a, b, c in S with a < b and b < c then a < c.
(3) every non-empty subset of S has a least element.

Since we will be dealing with elements represented in a computer, all our sets will be finite and
have a meaningful order. Numeric types can be ordered according to their encoded values while
strings and characters can be ordered according to their ASCII values. Enumerated types have
their order specified when they are defined. Since every data type that can be represented in a
computer has an order and every set is finite, every set represented on a computer will have a
least (first) element. In contrast, note that the set of all integers is not well ordered since there is
no least element. When a set is well ordered we can sort its elements into a unique arrangement
and we can search for a specific element of the set.

We will informally extend our searching and sorting operations onto partially ordered sets. This
means that we will work with sets for which the relation ≤ is defined. This means that the set can
contain elements that have the same key value. Formally, for every pair of elements a, b of a
partially ordered set P the relation a ≤ b or b ≤ a is true.

The more important issue for us is that there can exist two elements a, b in P for which a=b. In
these cases we will allow an arbitrary ordering of these equivalent values. For example, the set
P = { 0,1,2,2,4,7,8} contains 7 elements, two of which have the value 2. We will not distinguish
between these values and so we will accept, as sorted, a list with the 2's properly placed between
1 and 4 but in either order relative to each other. We will also accept either 2 (or both) when
searching for this value in the set P.

It may appear that we are too concerned about this issue, however, when we begin working with
structured data types (elements with multiple fields) with repeated keys, we will need to be clear
about their proper ordering. For example, we may need to require that one field of a structured
data type is unique to each element of the set (i.e. the set must be well ordered with respect to
the primary key) or we may use hierarchical methods to order the elements (e.g. sort by first
name then last name).

13.1 Linear Search (unordered lists)

Assume you are given an unsorted list of integers S and you are asked if a particular value x is in
the list. How do you answer this question? First of all, we are required only to state whether the
value x is in S. Since the list of elements is in no particular order the value x could be the first
one we check or it could be the last element or it may not be in the list at all. To be sure, we may
have to check every element of S. A simple linear search algorithm is given below.

For a list of size n we will preset a boolean variable found to FALSE and then compare each
element of S to the value of x. If we find a matching value we will set found to TRUE. After
scanning the list S we will report the value of found.

Written as a boolean function, we have,

function search(S : listype; n, x : integer) return boolean is


found : boolean :=false;
begin
for i in 1..n loop
292
if S(i)=x then
found:=true;
end if;
end loop;
return found;
end search;

where we pass to the function the list S, the size of the list n (assuming that this value is not
fixed), and the search value x. We could have used the length attribute to determine the upper
limit of the for..loop rather than passing the value of n as an argument of the function search( ).
function search(S : listype; x : integer) return boolean is
found : boolean := false;
begin
for i in 1..S'length loop
if S(i)=x then
found:=true;
end if;
end loop;
return found;
end search;

We could also use the attribute functions S'first and S'last to make the function search( ) even
more general, since the index range of S may not start at 1.
function search(S : listype; x : integer) return boolean is
found : boolean := false;
begin
for i in S'first..S'last loop
if S(i)=x then
found:=true;
end if;
end loop;
return found;
end search;

For a list S of size n, each of the search( ) functions shown above will perform n comparisons
regardless of where the value of x is in the list S. We can reduce the number of comparisons in
some instances by forcing an exit from the for..loop as soon as a matching value is found.
Compare the performance of the search( ) functions above to the following version.
function search_2(S : listype; n, x : integer) return boolean is
found : boolean :=false;
begin
for i in 1..n loop
if S(i)=x then
found:=true;
exit;
end if;
end loop;
return found;
end search_2;

To make an estimate of how much faster search_2( ) runs than search( ) we need to assume
some probabilities. Let's assume that the list S contains uniformly distributed random values, the
search value x is randomly chosen in the same range, and x is in S for half of the trials. With
these assumptions we note that for 50% of the trials, x is not in S, so we will have to compare all
n elements of S to x. For the other 50% of the trials, x is in S at some random uniformly
distributed position, so our expected (average) position for x is at index n/2. As a function of n,
the expected number of key comparisons is therefore,
Nexp = 0.5(n) + 0.5(n/2) = 0.75n
293
By exiting the list early we can expect a 25% reduction in the number of key comparisons. This is
a minimal improvement and would not be considered a valuable modification of the function. It
may seem odd that we would not want to minimize the number of operations in some instances if
the worst case performance of our function is not improved by the modification. However, if the
time saved by exiting the list early is essential to the efficient operation of our program then we
probably can't afford to wait for the answer when x is not in S. For a program modification to
provide a significant improvement in performance, it should improve the worst case performance
as well. In the next section, we show a better way to search a list.

13.2 Binary Search (ordered lists)

In the previous section, we looked at searching an unordered list and found an order O(n) run
time performance. This was because in the worst case we had to compare every element in the
list to our search value. Now we will consider a search method that uses a sorted list S to
improve on this performance.

Assume that we have a sorted (e.g. non-decreasing order) list S and we want to find if a particular
value x is in this list. We won't worry now about how the list got sorted. With a sorted list we can
use the method of binary search as defined in the stepwise algorithm below:

Given a sorted list S of n elements determine if x is in S. Formally, the term sorted is defined as
an arrangement of elements of S = [S(1), S(2), . . . , S(n)] in which S(i) ≤ S(j) for all i<j, i, j in [1..n].

Step 1: Define the upper and lower indices of the list S as LO and HI. Initially these will
be set to LO=1 and HI=n.
Step 2: Choose an element of the list S(k) to compare with x, where k=(LO+HI)/2 (integer
div)
Step 3: If S(k) = x then return TRUE else if S(k)>x then HI=k-1 else LO=k+1.
Step 4: If HI ≤ LO then return FALSE else go to Step 2.

You may be familiar with the binary search algorithm from the number guessing game called
HILO. Let's play the HILO number guessing game to illustrate how binary search works. In
HILO, one player picks a number between 1 and 100 (or whatever upper limit is agreed upon).
The other player makes guesses, and the first player states whether the guess is too high, too
low, or correct.

HILO uses the binary search algorithm described above except that in HILO the range of values 1
to 100 is also the index of the values (i.e. position of the value) in the list S. In other words S(i)=i
for all i in [1..100].

Using the binary search method we set LO=1 and HI=100. Our first guess is (1+100)/2 = 50. If
player 1 says "too high", we know that the value we are seeking is between 1 and 49. If player 1
says "too low", we know that the value is between 51 and 100. Let's say our guess was too low.
In this case, we set LO=51 and compute a new guess as (51+100)/2 = 75. Let's say that this time
our guess is too high. Setting HI=74 we note that the secret number must be between 51 and 74.
How many guesses would it take to find the secret number in the worst case?

For each guess, the binary search method eliminates half the remaining numbers. Starting with
100 numbers and eliminating half the remaining numbers at each guess we see that it will take
seven guesses, worst case, to narrow the search to a single value. 100, 50, 25, 12, 6, 3, 1.

As defined in the stepwise algorithm above, binary search can determine if a particular value x is
in a sorted list S of size n by testing only a small fraction of the values in the list. How do we
express the maximum number of comparisons in terms of n? If half the remaining elements of
the list are eliminated at each comparison, all we need to do is to compute the number of times
we must divide n by 2 in order to obtain a value of 1. Written as a mathematical expression, we
are looking for the value of k for which,

294
n
=1
2k
or
n = 2k
so
k = log 2 n

Hence, the order of run time of binary search is O(log2n), for a list of size n. Logarithmic growth is
very slow. To demonstrate this, compute the number of key comparisons required for the binary
3 6 100
search of a list of size 10 , 10 , and 10 (=1 googol). Even a googol-sized list could be
searched in less than 400 key comparisons, although it would be extremely expensive to buy
enough RAM to hold a googol integers.

The function bin_search( ) shown below implements the binary search algorithm.

function bin_search(S : listype; x : integer) return boolean is


found : boolean := false;
lo : integer := S'first;
hi : integer := S'last;
guess : integer;
done : boolean := false;
begin
while not(done) loop
guess:=(hi+lo)/2;
done:=(hi<=lo);
if S(guess)=x then
found:=true;
done:=true;
elsif S(guess)>x then
hi:=guess-1;
else
lo:=guess+1;
end if;
end loop;
return found;
end bin_search;

In the next section, we will learn about a variety of methods for sorting data values. It is important
to understand that, while equality (=) is defined for any data type, the less-than (<) relation and
other relations of inequality are not. We have been using integer data types in our examples for
which inequality relations are well understood. We could have used floats, characters, or string
types as well, but other data types such as structured types do not have a meaningful relation of
inequality. We will have to address this issue again when we build a sorting procedure with a
generic data type.

13.3 Brute-Force Sorting Routines

An important operation on ordered sets is the arrangement of the elements (sorting a set) into a
non-decreasing or non-increasing order. We will investigate a number of commonly used
methods for sorting that are based on a comparison of key values using the relation < as defined
at the beginning of this chapter.

Exchange Sort

The exchange sort is probably the most used sorting method although it is not the most efficient.
In exchange sort (assume a sort into non-decreasing order), we start by scanning a list of
elements to find the smallest element based on the sort key value. Once found we place this
element in the first position of the list. Then we begin with the second element and find the

295
smallest element in the remainder of the list (making sure we don't consider the first element
again) and make it the second element. This operation repeats by moving our starting point one
position forward until we are comparing the next to last element with the last element and
arranging them into non-decreasing order. The exchange sort can be implemented to operate on
a simple fixed-size list using a nested pair of for..loops.

for i in 1..n-1 loop


for j in i+1..n loop
if S(i)>S(j) then
exchange(S(i),S(j));
end if;
end loop;
end loop;

The procedure exchange( ) is not built into the Ada language but its operation is clear. It
exchanges the value at location S(i) with the value at location S(j) in the list S. This is identical to
the swap( ) procedure we have used before.

Each time a value S(j) is found in the list that is smaller than S(i), the positions of these two
values are exchanged. Note that the inner loop starts at an index one greater than the current
index i of the outer loop. This is to ensure that we do not consider any value that has already be
placed in order earlier in the list.

i i i
4 3 i 1
16 j 16 16
16
4
25 25 3 25 25
3 j 4 4
8 8 8
8
19 19 19
19
2 2 2 j
3
11 11 11
11
1 1 1 2
10 10 10 10
7 7 7
7
12 12 12 j
12
initial list exchange(S(1),S(4)) next exchange end of first pass

Figure 13-1: First Pass of Exchange Sort on a Sample List

We can reduce the number of operations required by finding the smallest value in the remainder
of the list before exchanging any values.

for i in 1..n-1 loop


minval:=S(i);
imin:=i;
for j in i+1..n loop
if minval>S(j) then
minval:=S(j);
imin:=j;
end if;
end loop;
exchange(S(i),S(imin));
end loop;

In the modified exchange sort, we scan the remaining portion of the list to find the smallest
remaining value and its position in the list. At the end of the scan we exchange this value with the
296
value in the ith position. On the first pass of the outer for..loop we find the smallest value in the
entire list and place it in the first position. On the ith scan we find the (i+1)th smallest value and
place it in position i.

i 4 imin i i
4 1
16 j
16 16
25 25 25
3 3 3
8
8 8
19
19 19
2
2 2
11 11 11
1 1 imin 4 imin
10
10 10
7
7 7
12 j j
12 12
initial list end of first pass one exchange per pass

Figure 13-2:Modified Exchange Sort

The exchange( ) procedure can be implemented as,

procedure exchange(a,b : in out datatype) is


tmp : datatype;
begin
tmp:=a;
a:=b;
b:=tmp;
end exchange;

As we will see, the exchange( ) procedure is used in a wide variety of sorting routines, but first
let's look at a sort method that avoids the exchange( ) procedure altogether.

Insertion Sort

The insertion sort avoids the use of a separate exchange( ) procedure by integrating it into the
sort process.

In the example source code below, we see that the for..loop makes n passes through a list S of n
items, with the index of the loop running from S'first to S'last. Let's walk through the execution of
this code in detail. At the beginning of the first pass, the value of j is set to the first index in the
list S and the variable tmp is set to the first value in the list. Since j = S'first, the while..loop is not
entered and the first value in the list is assigned its original value by the assignment S(j):=tmp.

procedure insertion_sort(S : in out listype) is


j : integer;
tmp : integer;
begin
for i in S'first..S'last loop
j:=i;
tmp:=S(i);
while not(j=S'first) and then tmp<S(j-1) loop
S(j):=S(j-1);
j:=j-1;
297
end loop;
S(j):=tmp;
end loop;
end insertion_sort;

On the second pass, the value of j starts at the second index in the list and tmp is assigned the
value of the second item in the list S. The while..loop compares the value of tmp with the
previous value in the list S (in this case the first item in S). If tmp is less than this value, S(j) is
assigned the value S(j-1) and the index j is decremented. Since this is the second pass, the
while..loop is executed only once, and the exchange is completed by setting S(j) to the value tmp.
At the end of the second pass, the first two values in S are in ascending order.

tmp
tmp tmp tmp tmp
7
4 3 2 2
j 7 i j 7 j 4 0 0
4 7 i 4 1 1
3 3 7 i j 3 j 3
8 8 8 4 3
9 9 9 7 4
1 1 1 8 7
0 0 0 9 8
2 2 2 2 i 9 i
5 5 5 5 5
8 8 8 8 8
6 6 6 6 6

Figure 13-3: Example Execution of Insertion Sort

Let's assume that we have completed k-1 passes of the for..loop. At the beginning of the kth
pass, j will be assigned the kth index value and tmp will be assigned the kth value in the list S.
The while loop will be executed k-1 times. Each time, the value of tmp is found to be less than
the current value of S being considered. Each time tmp is found to be less than a value in S, the
values in S are moved up one position. At the end of the while..loop, the value of tmp is inserted
into the position left behind by the the larger values. After n passes, each value S(k) in S will be
no greater than all the values above S(k).

Bubble Sort

Another popular sort by key comparison is called the bubble sort. In this method, we compare
adjacent elements in the list and exchange them if they are not in the proper order. We repeat
this process for every element pair in the list until we can scan the entire list without finding a
consecutive pair of elements that is out of order.

Although similar to Insertion Sort, this method is simpler to understand. Unfortunately, bubble
sort has one of the worst run time performances of any sorting method in common use. As we
will see, it can be twice as bad as exchange sort in the worst case. The following code segment
is the body of a bubble sort procedure.

done := false;
while not(done) loop
done:=true;
for i in 1..n-1 loop
if S(i)>S(i+1) then
exchange(S(i),S(i+1));
done:=false;
end if;
298
end loop;
end loop;

The name bubble sort comes from the effect this sorting method has on the values. The smaller
values move to the top of the list like bubbles. I suppose this sorting method could have been
called the sinker sort since the larger values sink to the bottom.

Worst-Case Performance

Sorting algorithms provide us an opportunity to practice our algorithm analysis techniques to


evaluate run time performance. When attempting to determine the expected performance of an
algorithm, we typically consider the worst-case performance. This is not because we are
pessimists but rather because we need to know the maximum number of operations required to
solve a problem of a given size so that we can estimate the upper bound on run time. As with
most algorithms, the run time for a sorting algorithm is a function of the size of the list being
sorted.

For a list of size n, we see that to find the first element of the ordered list, the exchange sort will
require that the if..then statement must be executed n-1 times. The comparisons performed in
the if..then statement are called key comparisons. Once the first element is in place, there must
be n-2 key comparisons to place the second value and so on. In total, we will compare values
S(i) to S(j) a number of times given as Ntot below.

Ntot = (n-1) + (n-2) + (n-3) + . . . + 3 + 2 + 1

We can use Gauss's method to obtain a more convenient (closed-form) expression for Ntot. We
can build a sum by adding Ntot to itself in the following manner,

(n-1) + (n-2) + (n-3) + . . . + 3 + 2 + 1


+ 1 + 2 + 3 + . . . + (n-3) + (n-2) + (n-1)
n + n + n + . . . + n + n + n = 2 x Ntot

Reversing the list and adding the pairs of terms gives us a constant value of n for each of the n-1
term-pairs. But this total is exactly twice the value of Ntot,
nx(n-1) = 2xNtot
dividing by 2 gives,
Ntot = nx(n-1)/2 = n2/2 - n/2

This means that for a list of size n, the exchange sort must compare key values a total of n(n-1)/2
times. In Chapter 10 we introduced the concept of order of run time, which states that we are
only concerned with the growth rate of the run time as a function of the size of the problem. The
2
order of run time growth for exchange sort is O(n ).

The bubble sort algorithm is more difficult to analyze since its run time is a function of the initial
ordering of the data in the list. For example, if the list S is already sorted into non-descending
order then the conditional S(i)>S(i+1) is not true for any i=1,..,n-1 and the value of the boolean
variable done will remain true for every key comparison. This means that bubble sort will
recognize that a sorted list is sorted in a single pass through the list. Therefore, for a sorted list,
the number of key comparisons would be n-1 or order O(n). Since we don't often get asked to
sort lists that are already sorted we need to consider the expected run time or the worst case
performance of the bubble sort algorithm. In the worst case, for a list of n elements, one of the
elements will have to be moved n-1 positions (e.g. the least element is at the end of the list), so
that, in the worst case, we would have to invoke the for..loop n-1 times requiring a total of (n-
1)x(n-1) key comparisons.

Ntot = (n-1)x(n-1) = n2-2n+1

299
which is approximately twice as many comparisons as exchange sort. Bubble sort has the same
2
order of run time, namely O(n ) as exchange sort in the worst case. However, when the initial
misplacement of elements is small, bubble sort can be more efficient than exchange sort.

Shell Sort

The shell sort algorithm was developed by Donald Shell in 1959. It is the most efficient of the
2
order O(n ) class of sorting algorithms. There are a wide variety of implementations for this
algorithm that are more efficient than Shell's original version shown below, but this procedure
illustrates the algorithm concept.

procedure shell_sort(S : in out listype) is


step : integer := S'length/2;
j : integer;
tmp : integer;
begin
while step>0 loop
for i in S'first+step..S'last loop
tmp:=S(i);
j:=i;
while j>=S'first+step and then tmp<S(j-step) loop
S(j):=S(j-step);
j:=j-step;
end loop;
S(j):=tmp;
-- display the list S here
end loop;
step:=step/2;
end loop;
end shell_sort;

A detailed analysis of the run time performance of this algorithm is beyond the scope of this book,
but we can run the algorithm to show that it is efficient for a randomly ordered list. The initial step
size in this implementation is one half the list size.

13.4 Optimal Sorting (by key comparison)

The sorting methods discussed in the previous section are the simplest and therefore the most
2
popular for sorting small lists. As the size of the list grows the O(n ) run times begins to become
significant. In exchange sort and bubble sort we are comparing the same pairs of values many
times. This is because we are holding the values in a linear list and scanning the elements of the
list in sequence each time we search for the next smallest item in the list. It would be better if we
could reduce the number of comparisons required to place the next item in the list.

It turns out that we can use a method similar to binary search to reduce the number of key
comparisons required from n down to log2n for each element being placed. Since there are n
elements to be placed we will be able to achieve a run time of O(n log2n). It is conjectured that
this is the best possible performance (in the worst case) of any sort based on key comparisons.

TreeSort

The first optimal sort procedure we will study is called tree sort. As its name implies, it is based
on the creation of a binary tree data structure. Tree sort makes use of recursion for node
placement and for extraction of the sorted list using an in-order depth-first traversal. The pointer
record for tree sort should have fields for the values being sorted, the left and right child pointers,
and possibly a variable to keep a count of repeated values.

type node;
type pointer is access node;
type node is record
300
val : integer;
count : integer;
left : pointer;
right : pointer;
end record;

For each value to be sorted, apply the following recursive procedure

procedure placenode(curnode : in out node, x : in integer) is


begin
if curnode=null then
curnode := new node'(x,1,null,null);
elsif curnode.val>x then
placenode(curnode.left,x);
elsif curnode.val<x then
placenode(curnode.right,x);
else
curnode.count:=curnode.count+1;
end if;
end placenode;

We define a pointer type called root and always call the placenode( ) procedure with root as the
initial node. This way the value being placed is compared with the value at root first and then
sent to either the left child or the right child until a matching value is found or a new node is
created to hold the value being placed. This procedure is easier to visualize if we consider the
placement of a new value in an existing sort tree as shown below.

root 50 1

22 1 87 2

12 1 35 1 65 1 90 1

31 1 41 1 88 1 98 1

93 1
33 1

Figure 13-4: Example Sort Tree for Integer Values

Given the sort tree in Figure 13-4, place a new value 42 using the placenode( ) procedure. We
place the value 42 by calling,
placenode(root,42)
301
Since 42 is less than 50, placenode( ) makes the recursive call
placenode(curnode.left,42)
Now we are at the node containing the value 22. Since 42>22, placenode( ) makes the recursive
call
placenode(curnode.right,42)
which takes us to the node holding the value 35. Again placenode( ) calls
placenode(curnode.right,42)
taking us to the node containing the value 41. Since 41<42 placenode( ) calls
placenode(curnode.right,42)
but this time there is no pointer record. Therefore, placenode creates a new node at the right
child pointer of the node holding the value 41 and loads the value 42 in it; placenode( ) needed
only four key comparisons to find the proper location for the new value.

When we are finished placing nodes, we have a binary search tree loaded with values. But how
do we extract the sorted list from this tree? To obtain the list of nodes we must traverse (climb)
the tree picking the values as we go. We can use the LNR_traversal that we derived in the
previous chapter. This algorithm was called an in-order traversal. As you may recall, the
LNR_traversal is a depth-first traversal in which we apply the following recursive rules.
Starting at the root..
(1) If there is a left child node then PUSH and move to the left child node, else...
(2) Evaluate the current node (i.e. display the value at this node)...
(3) If there is a right child node then PUSH and move to the right child node, else POP

In this context the terms PUSH and POP refer to the stack holding the sequence of node
traversals. We will climb the tree from Figure 13-4 to illustrate this recursive procedure.

root stack
50 1
()

list=(12,22,31,33,35,41,50,65,87,88,90,93,98)

22 1 87 2

12 1 35 1 65 1 90 1

stack
22
first node
50
evaluated
31 1 41 1 88 1 98 1
list = ( )

93 1
33 1
stack
35
stack
50
87

list = (12, 22, 31)


list = (12,22,31,33,35,41,50)
Figure 13-5: In-Order Traversal of a Binary Sort Tree

302
We start at the root, and as long as there is a left child node, we PUSH the current node and
move to the left child node. The node containing the value 12 does not have a left child node so
we output the 12 and attempt to move to the right child node. This node does not have a right
child node so we POP the stack returning us to the node containing the value 22. We evaluate
this node by outputting the value 22 and move to the right child node of this node (node holding
35). Since this node has a left child, we PUSH the current node onto the stack and move to the
node containing 31. This process of,
(1) moving left,
(2) evaluating, and
(3) moving right
is continued until the stack is empty and all nodes have been traversed and evaluated.

The following climb_tree( ) procedure shows how the in-order traversal is implemented in Ada.
procedure climb_tree(curnode : in out pointer) is
begin
if curnode.left/=null then -- go left, if possible
climb_tree(curnode.left);
end if;
put(curnode.val,5); -- evaluate this node
if curnode.right/=null then -- go right, if possible
climb_tree(curnode.right);
end if;
end climb_tree;
This code is deceptively simple in appearance. This is because stack management is handled by
the computer. When a recursive routine is compiled, the compiler includes the necessary
routines for pushing the routine parameters onto and popping them off of a stack. The
parameters that must be placed on the stack are whatever is needed to completely define (and to
reconstruct) the state of the routine at the time of the recursive call.

Run Time Performance of Tree Sort

The number of key comparisons required to place a node in a sort tree (in the worst case) is
equal to the height of the tree. Recall that the height of the tree is the maximum number of edges
between the root and the most distant leaf node. If the sort tree is balanced then the height of an
n-node binary tree is log2n. Unfortunately, we cannot predict the shape of the sort tree at any
time during the placing of nodes. Consider the trees generated by the following orders of values
encountered in a tree sort.

Figure 13-6: Sort Tree Height Depends on the Order of Node Placement
303
In the first case shown in Figure 13-6, the values are placed in random order so the height of the
resulting tree is relatively small. As shown in the second case, order in the arrangement of nodes
being placed creates an unbalanced structure in the sort tree. In this case, the even nodes in
order followed by the odd nodes in order create a V-shaped sort tree. What shape sort tree
would result from placing the nodes if they were already sorted? What if they were in reverse
order? (see Exercises)

There are methods for keeping a sort tree balanced, but they are fairly costly in number of
computations required. If our data values being placed in the sort tree are randomly arranged,
how unbalanced would we expect our sort tree to be? Figure 13-7 below shows typical cases for
randomly arranged values in lists of size 50, 100, 500, 1000, 5000, and 10,000.

N = 50 N=100

N=500 N=1000

N=5000 N=10000

Figure 13-7: Random Unbalanced Sort Trees

304
For randomly arranged sets, the height of the sort tree grows slowly with increasing set size. In
the sample cases shown in Figure 13-7 we have,
__N Tree Height log2N (log2N)
50 12 5.643 6
100 10 6.644 7
500 19 8.966 9
1000 20 9.966 10
5000 25 12.284 13
10000 29 13.288 14

Compare these values with the log2N. If your calculator doesn’t have base-2 logarithms, you can
use the following relationship.
log2X = log2(10) · log10(X)
where,
log210 = 3.321928095…

Since an N-node balanced binary tree would have height equal to the smallest integer greater
than or equal to log2N (called the superior or just sup( )), we wish to compare the (log2N) to our
experimentally derived sort tree heights. Through N=10,000 we observe a tree height of
approximately twice the optimal balanced tree. Based on this analysis we might expect a
performance no worse than twice the optimal performance of O(n log2n). However, we must be
very careful about making general predictions about algorithm performance growth rates based
on such a limited analysis.

HeapSort

So far, we have built a binary search procedure that requires only O(log2n) key comparisons to
search an ordered list of size n, we have recognized that the binary search could be used to sort
.
n items with a total of O(n log2n) key comparisons, and we have demonstrated that the tree sort
algorithm approaches this performance. The question remains, "Can we do better?" The
problem with the tree sort algorithm is that it creates a complicated dynamic data structure that
requires additional overhead and may not result in the optimal performance.

What we would like to do is to develop a sorting method that uses a simple fixed size list and still
takes advantage of the efficiency of the tree sort. The Heap sort achieves this goal by taking
advantage of the array representation of a binary tree (see Fig 12-6). A binary heap is an array
data structure that can be viewed as a complete binary tree.
array
1
1 2 3 4 5 6 7 8 9 10 11 12

2 3
Parent(i) -> i/2
LeftChild(i) -> 2*i
RightChild(i) -> 2*i+1
4 5 6 7

8 9 10 11 12
complete binary tree
Figure 13-8: A Complete Binary Tree Stored in an Array Data Structure
Using the array S( ) to hold the binary tree, we can use the relationships given in Figure 13-8 and
the index value i to directly access the parent or children of node i. In addition to this indexing
property, we will need another property to implement the heap sort algorithm. The heap
property states, "For every node i other than the root node, the value of the parent must be
greater than or equal to the value of node i. That is,
S ( Parent (i )) ≥ S (i )
305
This means that the root will hold the largest element of the heap and that the "root" node of any
sub tree of the heap will be the largest value of that sub tree. We will need to maintain the heap
property as part of the heap sort algorithm so we need to find an efficient method to heapify a
binary tree. We will define a method to heapify a tree when the sub trees attached to the root are
already heaps but the root node may be less than one or more of its children. Consider the
following example.
viiolates heap property
We place a new value at the root of a binary 6
tree in which the left child and right child are
heap heap
the "roots" of sub trees that are already heaps.
23 17
In order to make the tree a heap, we look at the
values of the three nodes comprised of the root
node and its two children. The largest value,
23, is in the left-child node so we exchange the 14 8 2 7
root node and the left-child node values.
3 12 6 2 1

When we make this exchange, the left sub tree 23


is no longer a heap so we must repeat the
operation. This time we are comparing the
values 6, 14, and 8 (i.e. the node that received
6 17
the value 6 and its children).

Since 14 is the maximum value, we exchange


14 8 2 7
this value with 6.

3 12 6 2 1

One additional exchange leaves the value 6 at 23


a leaf node. The resulting binary tree is now a
heap. The values of the nodes in the heap
satisfy the heap property but otherwise, they
are not sorted. 14 17

We have no assurance that the values in the


left sub tree of the root are smaller than the 12 8 2 7
values in the right sub tree. The only absolute
relationship we know is that the root contains
3 6 6 2 1
the largest value in the list.

Figure 13-9: Example Execution of the Heapify Algorithm

The heapify algorithm can be described in the following steps:

Step 1 - Given the parent node index, get the left-child and the right-child indices.
Step 2 - If there is a left-child node, compare it with the parent node and save the largest
Step 3 - If there is a right-child node, compare it with the value from Step 2 and save the largest
Step 4 - If parent node is not the largest, exchange it with the largest node
Repeat until done.

We can implement this algorithm as a recursive procedure.

306
procedure heapify(s : in out listype; heapsize,parent : in integer) is
left : integer;
right : integer;
largest : integer;
begin

left:=2*parent; -- Step 1
right:=2*parent+1;

if left<=heapsize and then s(left)>s(parent) then -- Step 2


largest:=left;
else
largest:=parent;
end if;

if right<=heapsize and then s(right)>s(largest) then -- Step 3


largest:=right;
end if;

if not(largest=parent) then -- Step 4


exchange(s(parent),s(largest));
heapify(s,heapsize,largest);
end if;

end heapify;

This procedure will place the root node value at the proper position to create a heap only if every
other node in the tree already satisfies the heap property. In order to convert a binary tree into a
heap, we would need to call heapify for every node. Actually, we only need to worry about the
first half of the list since the remaining values are all leaf nodes and, therefore, do not have child
nodes to consider. The build_heap( ) procedure shown below uses heapify to build a heap from
a binary tree.

procedure build_heap(s : in out listype; n : in integer) is


begin
for i in reverse 1..n/2 loop
heapify(s,n,i);
end loop;
end build_heap;

Recall that in a complete binary tree, half the nodes are leaf nodes. You may want to review
Figure 13-8 to gain a better understanding of why we don't need to heapify the second half of the
list.

Remember that a heap is not a search tree. The only thing we know for sure is that every parent
node is at least as large as its children, therefore the root is the largest value in the heap. We will
now make use of this fact to complete the heap sort algorithm. Since the root S(1) contains the
largest value we will exchange it with the nth (i.e. last) value in the list S. We will heapify the
remainder of the list S omitting the nth value. After each heapify operation we will exchange the
value of S(1) and S(n-i) for the ith iteration. Repeating this operation n times will leave the values
in the list sorted in ascending order.

307
30 29 17 24 13 4 9 23 0 2 30
heapify
29 17

exchange
24 13 4 9

23 0 2
2 29 17 24 13 4 9 23 0 30

29 24 17 23 13 4 9 2 0 30 29
heapify
24 17
exchange
23 13 4 9

2 0 30
0 24 17 23 13 4 9 2 29 30

24 23 17 2 13 4 9 0 29 30 24
heapify
23 17
exchange
2 13 4 9

0 29 30
0 23 17 2 13 4 9 24 29 30

Figure 13-10: Partial Example Execution of Heap sort

In the example above, we have executed the heap sort for three iterations, which has properly
placed the last three values into ascending order. Once we have the heapify( ) and build_heap( )
procedures, the heap sort algorithm is simple to implement.
build_heap(S,n);
for i in reverse 2..n loop
exchange(S(1),S(i));
heapify(S,i-1,1);
end loop;
In this code segment, we assume that the list S initially contains n unsorted values. Build_heap( )
is called once to convert this list into a heap. Then the largest value S(1) is exchanged with the
last value S(n). When S(n) gets this largest value, we no longer consider it in our algorithm. We
assume that the list is now of length n-1. Since the value in S(1) has changed, we need to
heapify the remainder of the list. We do this by calling heapify(S,i-1,1) since i is the previous
length of the list (initially n). As the heap gets shorter, we leave behind the values in S arranged
in ascending order. A complete Ada listing of the heap sort program is provided below:
with ada.text_io, ada.integer_text_io, random;
use ada.text_io, ada.integer_text_io, random;
procedure heap sort is
type listype is array(1..10000)of integer;
s : listype;
n : integer;
procedure exchange(a,b :in out integer) is
tmp : integer;
begin
tmp:=a;
a:=b;
b:=tmp;
308
end exchange;
procedure heapify(s :in out listype; heapsize,parent :in integer) is
left, right, largest : integer;
begin
left:=2*parent;
right:=2*parent+1;
if left<=heapsize and then s(left)>s(parent) then
largest:=left;
else
largest:=parent;
end if;
if right<=heapsize and then s(right)>s(largest) then
largest:=right;
end if;
if not(largest=parent) then
exchange(s(parent),s(largest));
heapify(s,heapsize,largest);
end if;
end heapify;
procedure build_heap(s : in out listype; n : in integer) is
begin
for i in reverse 1..n/2 loop
heapify(s,n,i);
end loop;
end build_heap;
begin
put("Enter number of values... ");
get(n);
for i in 1..n loop
s(i):=rnd(3*n);
end loop;
build_heap(s,n);
for i in reverse 2..n loop
exchange(s(1),s(i));
heapify(s,i-1,1);
end loop;
for i in 1..n loop
put(s(i),6);
end loop;
end heap sort;

Analysis of HeapSort

An analysis of HeapSort is straightforward. Starting with the main program, we see that
build_heap( ) calls the heapify( ) procedure n/2 times. Following build_heap( ), the main loop
calls exchange( ) and heapify( ) approximately n times. Since the number of operations in
exchange( ) is fixed, we only need to consider the number of operations (worst case) performed
by heapify( ) as a function of the size of the heap n. We have seen that heapify moves from the
.
root to a leaf node recursively. Therefore, in the worst case, it requires 3 log2n key comparisons
to complete the heapify operation. For a list of size n we have a total number of operations,
(n/2).log2n + (n)( 3.log2n)
or
O(n.log2n).
.
In review, heap sort is an efficient, O(n log2n) sorting algorithm that can be implemented with an
array data structure and requires no additional memory allocation. That is, the list is sorted in the
same memory space used to hold the original unsorted data set.

309
QuickSort

We will consider one more popular sorting method, called quicksort. The worst case performance
2
of this algorithm is O(n ), but as we will see it tends to do much better than this in practice.
Quicksort uses the divide-and-conquer problem solving approach to split the sorting problem into
smaller and smaller sub problems until the sorting operation is trivial (sorts of lists of size 1). At
the top-level, quick sort is defined recursively as,
procedure quicksort(S : in out listype; lo,hi : in integer) is
pivot : integer;
begin
if lo<hi then
partition(S,lo,hi,pivot);
quicksort(S,lo,pivot-1);
quicksort(S,pivot+1,hi);
end if;
end quicksort;
where partition( ) rearranges the list so that all the values between S(lo) and S(pivot-1) are less
than or equal to S(pivot) and all the values between S(pivot+1) and S(hi) are greater than
S(pivot). An example of the partition( ) procedure is given below:
procedure partition(s : in out listype;
lo,hi : in integer; pivot : out integer) is
x,j : integer;
begin
x:=s(lo); -- this will be the pivot value
j:=lo;
for i in lo+1..hi loop -- decides where the pivot will go
if s(i)<x then
j:=j+1;
exchange(s(i),s(j)); -- ensures each value is on the
end if; -- proper side of the pivot position
end loop;
pivot:=j;
exchange(s(lo),s(pivot)); -- places the pivot value
end partition;
Figure 13-11 below shows an example run of quicksort. In our version of the program, the pivot
value is the first value in the sublist being partitioned. For Step 1 below, the sublist is the entire
list S. Step 2 shows that all the values of S that are ≤ the pivot value (10) are placed on the left
of s(pivot) and all values > than S(pivot) are moved to the right of this value.

1. 10 14 17 3 15 8 18 19 3 1

2. 1 3 8 3 10 17 18 19 14 15

3. 1 3 8 3 10 17 18 19 14 15

4. 1 3 8 3 10 14 15 17 18 19

5. 1 3 8 3 10 14 15 17 18 19

6. 1 3 3 8 10 14 15 17 18 19

7. 1 3 3 8 10 14 15 17 18 19
Figure 13-11: Example Run of QuickSort for a List of 10 Integers

310
In Step 2 we see that there are now two sublists, one on either side of the pivot value, 10. Note
that this will be the final position of the pivot value when the list is sorted. Can you see why this is
true?

Step 3 shows the operation for both calls of quicksort( ) (although this is not the order in which
they will be called). The pivot value for the left sub list is 1, which is also the minimum value in the
list. This creates the worst-case condition for the QuickSort algorithm since it breaks a k-element
sub list into a list of size 1 and a list of size k-1. If the pivot value were always the minimum or
2
maximum value in the sub list, QuickSort would require O(n ) operations to sort a list of size n.
Fortunately, the situation demonstrated in the right sub list in Step 3 is more typical. In this case,
the pivot value divides the right sub list into two sublists of similar size.

Step 4 shows the final positions of all the pivot values used so far. The pivot values shown in
Step 5 result in sub lists of size 1, so lo=hi and no more recursive calls are made to quicksort( ).
The QuickSort algorithm has the additional advantage that, since the elements of S are sorted in
place, no additional work is needed to recombine the partial solutions.

13.5 Hashing

We started this chapter looking for an efficient way to search for a value in a list. We said that, in
an unordered list of n elements, we might have to check each of the n elements to find the value
or to discover that the value was not in the list. We found that, if the elements were arranged in a
non-decreasing order we could reduce our search to O(log2n) key comparisons. Unfortunately,
.
the most efficient sorting algorithm we could find requires O(n log2n) key comparisons.

Let's take a moment to reconsider our original goal, which is - to recover items placed in a list, or
to determine if an item is in a list in the most efficient manner possible. Can we do better? There
are many applications in which the elements in the list are changed frequently, such as a member
account and password list for an Internet Service Provider (ISP). If we want to use a binary
search to access user account information, the list must be resorted each time a member is
added or removed. Since the list cannot be searched while it is being sorted, all users attempting
to logon would have to wait. Alternatively, the ISP may leave the list unordered and perform a
linear search to find the user account/password for each logon. As the number of logons and the
number of accounts increase, the time delay increases.

Since the number of key comparisons required to find a value is a function of the size of the list,
we could reduce the search time by dividing a large list into smaller sub lists. We could then use
the key value to choose the sub list into which to place each element. When the sub lists have no
members in common and together they contain all the values in the original list, they are called a
partition.
carver

carlisle
anderson carter
allen baines casey zinglehoffer
arlington bates cofer ... zuckerman
aster benson conner zule
azule boyer cutter zytes

Figure 13-12: Partitioning a List into Sublists

For example, we could partition the list according to the first letter of the customer's last name.
When we need to find a customer account, we will search only the corresponding sub list. This
will help a little, but when there are tens of thousands of customers, the sub lists will be very
large, and some lists will be much larger than others.

We need a way to control the number of sub lists and to ensure a uniform distribution of elements
across the sub lists. This can be accomplished using a hash function.
311
Hash Function

A hash function converts a key value into some integer value between 0 and maxval-1, the
maximum value allowed for the hash function. Let's build a hash function for ASCII character
strings. We will add together the ASCII values of each character modulo maxval.

function hashval(str : string; maxval : integer) return integer is


val : integer:=0;
begin
for i in 1..str'length loop
val:=(val+(character'pos(str(i)))) mod maxval;
end loop;
return val;
end hashval;

This function accepts an ASCII character string str and returns an integer value between 0 and
maxval-1 which can be used to select a sublist to hold the data associated with the input string.
The hash function uses modulo arithmetic to keep the hash number inside the specified range.

Hash functions can be generated in many different ways. The goal for designing a hash function
is that the hash values it generates are uniformly distributed over the range of possible values.
This is called simple uniform hashing. Sometimes we also require that input strings that are
similar generate hash values that are far apart, but for now, we will limit our concern to simple
uniform hashing functions and their applications.

Hash Tables

The hash value generated by the hash function can be used to place and access values in a hash
table. One method of hashing uses the value from the hash function as an index into an array of
pointers. These pointers point to the first elements of sublists that hold the elements being
stored. The sublists can be implemented as fixed-size lists or linked-lists as shown below.

0 null

1 null

2 null
3
null
4
hash value

null
5
null
6
null
7
null
8
:

null
n-2

n-1 null

Figure 13-13: A Hash Table with Linked Lists

When no stored element has been associated with a particular hash value, the corresponding
pointer points to null. When more that one element has been associated with a hash value, the
corresponding pointer in the hash table points to a linked list of records containing the elements.
The maximum number of key comparisons required to find an element is equal to the number of
records in the longest linked list. Ideally, we would like to associate each element with its own
pointer so that the hash value will take us directly to the value we seek. We can come close to

312
this ideal situation if we make the size of the hash table at least as large as the number of
elements to be stored, and we find a hash function that uniformly hashes the key values.

Implementing a Hash Table

In order to make use of the hash function, we need an efficient implementation of the hash table
such as the one shown in Figure 13-13. Let us assume that we are hashing strings of characters
and that we will want a hash table with at least numtab entries.

subtype wordtype is string(1..20);


type hashnode;
type hashlink is access hashnode;
type hashnode is record
word : wordtype;
next : hashlink;
end record;
type hashtabletype is array(0..numtab-1)of hashlink;
hashtable : hashtabletype;

This data structure provides a subtype to hold the input strings, a pointer record to build the linked
lists (sub lists), and the hash table itself as a fixed-size list of up to numtab entries. Note that the
hash table is a list of pointers rather than pointer records. This will make the implementation of
the hashing operations simpler.

As stated earlier, we would like each element to have its own entry in the hash table, but this is
not usually possible. The reason is that the hash function may generate the same hash value for
more than one input string. When this occurs it is called a collision and must be handled in
some manner so that both entries can be saved and later retrieved when needed.

In the approach we are discussing here, we build a linked list to hold the entries that have the
same hash value. Figure 13-14 below shows the result of hashing 10 randomly generated
character strings (each between 1 and 5 characters) into a hash table with 10 entries.

0 zwri mjs null


1 null
2 cvprw null
3 xzemo null
4 h clefr lse null
5 tve null
6 qhjg null
7 fs null
8 null

9 null
Figure 13-14: Example Hash Table for Randomly Generated Character Strings
This example was generated using the data structure and the hash function defined above. As
shown in the procedure hash( ) on the next page, first, the hash value was derived from the input
string and the number of table entries. Next, a new record was created to hold the string. Finally,
the corresponding pointer in the hash table was checked to see if it was null. If it was null, the
new record was appended to this pointer, otherwise it was inserted as the first element of the
linked list.

313
procedure hash(str : in string; numtab : in integer) is
hval : integer;
curnode : hashlink;
begin
hval:=hashval(str,numtab);
curnode:=new hashnode;
curnode.word:=" ";
curnode.word(1..str'length):=str;
if hashtable(hval)=null then
hashtable(hval):=curnode;
curnode.next:=null;
else
curnode.next:=hashtable(hval);
hashtable(hval):=curnode;
end if;
end hash;

A similar method is used to determine if a particular string is in the hash table. The procedure
search_hashtable( ), shown below, accepts a candidate input string str and the number of hash
table entries. Since the string value stored in the hash table records are of type wordtype, the
input string must first be converted to a string of this type before it can be compared with the hash
table entries.

The advantage of the hash table is that we do not have to search the entire table to see if there is
an entry matching the candidate string. We use the hash value of the candidate string to select
the correct position in the hash table and then search only those entries that have the same hash
value. Note the use of the and then construct in the conditional. You may recall that this is a
short circuit operator, which permits the evaluation of curnode before the field curnode.word is
evaluated. The conditional test following the and then will be invoked only if the conditional
appearing before the and then is TRUE. If curnode=null then the conditional
curnode.word=a_word would raise a constraint error.

procedure search_hashtable(str : in string; numtab : in integer) is


a_word : wordtype :=" ";
hval : integer;
curnode : hashlink;
begin
a_word(1..str'length):=str;
hval:=hashval(str,numtab);
curnode:=hashtable(hval);
while curnode/=null and then curnode.word/=a_word loop
curnode:=curnode.next;
end loop;
if curnode=null then
put("not in table");
else
put("is in table");
end if;
end search_hashtable;

In this version of search_hashtable( ), we simply display a message stating whether the


candidate string matches an entry in the hash table. In a real application, other actions would be
taken. For example, in the case of the ISP logon, a match would mean that the user account was
found and a password test would be invoked. Otherwise, the user would receive a message
indicating that the logon attempt was invalid.

Analysis of Hashing

The number of key comparisons required to determine if a particular candidate string is in a hash
table, in the worst case, is the maximum number of entries in any sub list. For a particular hash
314
table, we can compute the expected performance by summing the positions (posi) of each of the
entries (i.e. their positions in their corresponding sublists) and dividing by the number of entries.
n
1
N avg = pos i
n i =1

In the example hash table shown in Figure 13-14, there are 7 entries in position 1, 2 in position 2,
and 1 in position 3 for a total of (7)(1) + (2)(2) + (1)(3) = 14. Therefore, the average or expected
number of key_comparisons is Navg = 1.4. With a hash table at least twice a large as the number
of entries and a good hash function, O(1) average performance can be achieved. Typical values
for Navg are less than 1.05, which means that only 5% of the searches require more than one key
comparison.

315
Exercises

13.1 Give an estimate of the order of operations (big O) required to perform each of the following.
Give your answers in terms of the size of the list n and any other pertinent parameters. Include in
your answer a description of the algorithm you used and why you chose it.
a. search an unordered list of n items to find a particular item x.
b. search an ordered list of n items to find a particular item x.
c. find the smallest value in an unordered list of n items.
d. find the largest value in an ordered list of n items.
e. find the kth largest value in an unordered list of n items (k<=n).
f. find the kth largest value in an ordered list of n items (k<=n).
g. sort an unordered list of n items into ascending order (sort by key comparison).
h. find a pair of items x and y in an unordered list that are the closest.
i. find a pair of items x and y in an ordered list that are the closest.
j. find a pair of items in an ordered list that are the farthest apart.
k. arrange a list so that the sum of the absolute differences between the n-1 adjacent pairs of
elements is a minimum. Mathematically
n −1
Sum = X i +1 − X i
i =1
l. arrange a list so that the sum of the absolute differences between the n-1 adjacent pairs of
elements is a maximum.

13.2 You are given an algorithm that solves a problem of size 10 in 1 second. Compute the size
of the problem n, which would require 1 hour to complete given that the run-time performance of
the algorithm is,
n
a. O(n) c. O(2 )
2
b. O(n ) d. O(log2n)

13.3 In section 13.3, we compared two versions of the Exchange Sort. In the first version, we
exchanged the new minimum with the old minimum each time we encountered a smaller value in
the remainder of the list. In the second version, we checked all the values in the remainder of the
list before making the exchange.

a. How many times would you expect version 1 to encounter a smaller value in one pass
through a randomly generated, unordered list? Give your answer as a function of n, the
size of the list. An equivalent code segment is given below. What is your estimate of
count?

minval:=S(1);
count:=0;
for i in 2..n loop
if S(i)<minval then
minval:=S(i);
count:=count+1;
end if;
end loop;
put(count);

b. Implement an Ada program to test your estimate, and comment on the results.

316
13.4 Answer the following questions, assuming that tree sort has a run time performance no
. 2
worse than 2n log2n while exchange sort has a run time performance no better than n /2.
-8
a. If your computer can complete one instruction (e.g. a key comparison) in 2x10 seconds
(i.e. 20 nanoseconds), how large a list could you sort using exchange sort in 30 seconds?
You may assume that all the operations necessary to prepare for sorting have been
completed.

b. How long would it take the same computer to sort the same list using tree sort?

c. Using the same computer, how large a list could be sorted by tree sort in 30 seconds?

13.5 Given a sort tree like the one shown in Figure 13-5, how would you modify the traversal
algorithm to obtain a list in non-ascending order rather than non-descending order?
.
13.6 Both the tree sort and heap sort algorithms achieve O(n log2n) run time performance.
Compare and contrast these two sorting algorithms in the following areas:
a. simplicity of the data structure
b. uncertainty in the data set (e.g. How much do you need to know about the maximum
problem size before you implement each algorithm?)
c. ease of implementation (e.g. encoding and debugging)
d. algorithm predictability (e.g. Compare likelihood that something unexpected might occur,
like array bounds exceeded or stack overflow.)
2
13.7 Under what conditions would you expect the worst case performance, O(n ) for QuickSort?
Describe a simple fix for this problem.

13.8 Implement a demonstration of hashing using the method described in Section 13.5. Use
your demonstration to hash randomly generated character strings as you experimentally analyze
the following:
a. What is the average number of key comparisons required when searching a hash table
when the number of entries is equal to the size of the table?
b. What is the average number of key comparisons required when searching a hash table
when the number of entries is half the size of the table?
c. What is the effect (if any) when the size of the hash table is a multiple of the number of
hash values produced by the hash function? (i.e. let the hash function range =(0..n-1) and
.
the hash table size = k n.)

You may use the following function to generate your random strings. This function generates
lowercase letter strings of between 1 and 5 characters.

function genword(n : integer) return string is


a_word : string(1..20);
begin
for i in 1..n loop
a_word(i):=character'val(97+rnd(25));
end loop;
return a_word;
end genword;

13.9 Build a hash function for floating point values in the range (0.0..1.0) that satisfies the simple
uniform hashing goal. This function should return a hash value between 0 and maxval-1.

317
13.10 Implement a hash table using the hash function from Exercise 13.9 to hash 500,000
randomly generated floating points values between 0.0 and 1.0. Perform an analysis of your
hash table in terms of the number of entries associated with each position.

a. Give the number of hash table positions with 0, 1, 2, . . . , Nmax entries.


b. Compute the average number of key comparisons required to determine if a
particular value is in the table.
c. Discuss the performance of your hash function with respect to the random number
generator used to generate the 500,000 random values.

318
Chapter 14 - Graphs

14.1 Graph Definitions

14.2 State Machines

14.3 Graph Data Representations


Adjacency Matrix
Dynamic Memory Representations

14.4 Graph Traversals


Depth-First Traversal
Breadth-First Traversal
Priority-Traversal

14.5 Implementing Graph Traversals


Implementing a Depth-First Traversal
Implementing a Breadth-First Traversal

319
Chapter 14 - Graphs
In this chapter, we give a formal introduction to graphs as abstract data types. We investigate the
various machine representations for graphs and review a number of sample applications using
these representations.

14.1 Graph Definitions

A simple undirected graph G consists of a set of vertices V (also called nodes) and a set of edges
E. The elements of E are defined as pairs of elements of V, ek = (u,v), such that u is not equal to
v and (u,v) is an element of E implies that (v,u) is also an element of E. (In other words, (u,v)
and (v,u) represent the same edge).

Graphs can be represented pictorially by nodes (circles) and edges (lines) as shown below:

Simple Connected Graph Graph

Directed Acyclic Graph Multigraph

Figure 14-1: Types of Graphs

Multigraphs allow multiple edges between the same pair or vertices and loops (i.e. edges from
and to the same vertex).

The edges of a directed graph are called arcs and have a direction as indicated by an arrow.
Unlike an edge, an arc (u,v) in a directed graph does not imply that the arc (v,u) is also in the
directed graph.

A path in a graph is any sequence of nodes that are connected by edges or arcs. For any path of
n nodes, there must be an edge (arc) for every consecutive pair of nodes in the sequence. That
is, P=(v1, v2, . . . ,vn) is a path in a graph G=(V,E) if for every pair of nodes vi,vi+1 where i=1,n-1
there is an edge (arc) e=(vi,vi+1) in the set of edges (arcs) E.

An acyclic graph is a graph with no cycles. That is, there is no path along edges in the graph (or
along arcs in a directed graph) that leads from a vertex back to the same vertex.

Two vertices u, v in a graph are said to be adjacent if there is an edge e (or arc) connecting u to
v. The vertices u and v are called the endpoints of e.

The degree of a vertex v is given as deg(v) and is the number of edges incident with v. That is,
the number of edges for which v is an endpoint. For a directed graph, we define the in-degree of

320
a vertex as the number of arcs entering the vertex and the out-degree as the number of arcs
leaving the vertex.

In a weighted graph, the edges are assigned values representing some important aspect of the
application being modeled by the graph. For example, the nodes of a weighted graph could be
cities so that the weights would be the distances between the cities. If the nodes of a weighted
graph were internet nodes and the edges were the connections between the nodes, weights
could be assigned to the edges representing the information bandwidth or throughput of each
connection.

A simple but useful theorem: 2 |E| = Σdeg(V) for all V in G=(V,E), where |E| is the cardinality of E
(i.e. the number of members of the set E). In words, this theorem states that the sum of the
degrees of each of the vertices in a graph is equal to twice the number of edges in the graph.
This is true since each edge has two endpoints and therefore contributes 2 to the total degree
count.

A complete graph is one in which there is an edge between every pair of vertices.

A cycle or ring is a connected graph in which there is exactly one path from any node back to
itself. Many local area networks (LANs) are based on ring topologies in which each computer is
represented by a node in the cycle or ring graph. In a ring, each node is connected to two other
nodes.

An n-vertex wheel is a graph composed of a cycle with n-1 vertices plus one additional vertex
connected to the other n-1 vertices. The additional vertex has degree n-1 and, in the application
of a computer network, provides an additional level of connectivity (redundancy).

A bipartite graph G=(V,E) is one in which V can be partitioned into two disjoint subsets V1 and V2
such that every edge connects a vertex in V1 and a vertex in V2. Notice that no two nodes in a
group are connected. Determining if a particular graph is bipartite is equivalent to the problem of
finding if there are any cycles of odd length in the graph. For example, if there are edges (u,v),
(u,w), and (v,w) in the edge list E of a graph G=(V,E) for any three vertices u,v, and w in V then G
cannot be bipartite. Why?

cycle or ring wheel complete graph, K5 bipartite graph


Figure 14-2: Special Graphs
n
A hypercube is a 2 vertex graph in which each vertex is connected to exactly n other vertices.
There are a number of parallel computer algorithms that run efficiently on a collection of
processors arranged in a hypercube topology.
n
Actually, we can define a virtual hypercube topology for a collection of 2 processors that are
connected in any type of physical (i.e. actual) network. All we need to do is to define which
machines are adjacent (connected to each other) in our hypercube. We can keep up with
n
adjacency of processors using a clever processor addressing scheme. For 2 processors, we will
use an n-bit address ensuring that adjacent (connected) processors will have addresses that
4
differ in exactly one bit. For example, in a 2 = 16 processor (i.e. 4 dimensional) hypercube the
processor with address 0000 is connected to processors with addresses (0001, 0010, 0100 and
1000).

321
What are the addresses of the processors connected to processor (1101) in a level 4 hypercube?

0000 0001
00 01

11 0100 a
10
1000
n=2, 22=4
0
n=0, 2 =1
000 001
0 1 b

100 101
n=1, 21=2
010 011 1111

0010
110 111

n=4, 24=16
n=3, 23=8 c

Figure 14-3: Hypercubes for N= 1,2 3 and 4

Sometimes we are interested in the mimimum distance or shortest path between any two
processors in a computer network. The distance between two processors in the hypercube is
equal to the number of bits difference between their respective addresses; therefore, we can
directly compare processor addresses to determine this distance. We can also generate the path
as a sequence of connected nodes in the hypercube through which to move data between
corresponding processors. For example, to send data from processor 0000 to 1111 we can
follow the path 0000 -> 0001 -> 0101 -> 1101 -> 1111. How many different minimal-length paths
are there between processor 0000 and 1111? (See Problem 14.2 in the Exercises.)

14.2 State Machines

Another important use for graphs in computer science is in the representation of finite state
machines (FSMs). A finite state machine is a model of computation composed of (1) an input
alphabet or list of valid symbols that must be considered, (2) a set of states, (3) a list of transitions
from the current state to the next state based on the current state and the current symbol (from
the alphabet) being read, (4) a particular state defined as the start state, and (5) one or more
states designated as accept states.

The input is scanned by the FSM, and for each symbol read, the state is updated according to the
set of state transitions. When the entire input string has been scanned, the final state is
compared to the set of accept states, and if the final state is one of the accept states, the input
string is said to be accepted or recognized by the FSM.

At this point, an example would be helpful. Let an FSM be defined over the alphabet {0,1} (i.e.
binary strings), let the start state be State=0, and let the accept state be State=3. Consider the
following FSM for recognizing binary strings containing at least three 1's.

1 1 1 1
start

0 1 2 3

0 0 0
0
Figure 14-4: An FSM to Recognize Binary Strings with at least Three 1's

322
In the FSM in Figure 14-4, the double circle indicates the accept state, the start state is labeled
and the transitions between states are the arcs of the FSM graph and are labeled according to
the input symbol currently being read. The binary strings,

01010000 reject
01001110 accept
0111 accept
110000 reject
01010100 accept

are labeled according to whether the FSM accepts or rejects each candidate string. When the
FSM is in state 3 at the end of the end of the candidate string then the FSM accepts the string.
The strings accepted by this FSM are the set of binary strings containing at least three 1's.

Now consider the FSM to recognize the set of binary strings containing at least three consecutive
1's. In this case, we need to keep a record of the number of 1's encountered. We return to the
start state when a 0 is read and we have not read at least three consecutive 1's. Once we have
read the necessary three consecutive 1's, we will remain in the accept state regardless of the
remainder of the binary string. This FSM has the following graphical representation.

1 1 1 1
start

0 1 2 3
0

0 0
0

Figure 14-5: An FSM to Recognize Binary Strings with at least Three Consecutive 1's

Applying this FSM to the sample binary strings listed below gives the indicated results.

0000110011001100 reject
00100101000 accept
1010101010000000 reject
11111111 accept

This FSM is similar to the previous example except that we restart our count each time we
encounter a zero until we have achieved the required result of three 1's in a row. Once we reach
this goal, we will accept the string.

Let's look at one more example of how directed graphs are used to represent FSMs. Consider
the FSM for accepting a string that can be interpreted as an integer. In this case, our alphabet is
exanded to include any valid ASCII character. This will include the digits 0 through 9, the upper
and lowercase letters, and all punctuation. We will define a valid integer to be any number of
leading blanks, followed by a possible + or - sign, followed by one or more digits, followed by any
number of trailing blanks. Some examples of valid and non-valid integers (according to this
definition) are listed below. The symbol shown represents a blank space.

Valid Integers Not Integers__


123 123.456
123456 12+345
-543 Hello There
9 9 3 5

323
The FSM for recognizing (accepting) integer strings as previously defined is given by,

0..9

+/-
0..9
b
b
start
+/-
0 1 2 3 4 b = blank
0..9 0..9 = a digit
b 0..9 b
+/- = plus or minus sign
else
else = any other ASCII character
else
else
else

else
5

else
Figure 14-6: An FSM to Accept Valid Integer Strings

where 0..9 is any ASCII digit, +/- is either a plus sign or a minus sign, b stands for spaces or
blanks, and else represents any ASCII character not explicitly referenced in the transitions for the
current state. It is important to note that else refers to a different symbol set depending on the
current state. When in State 0, else represents any ASCII character that is not a digit, a blank
space, or a sign. In State 2, else represents any ASCII character that is not a digit. In State 5,
else represents any ASCII character. Finally, in this example we have two accept states (3 and
4). If the FSM is left in any of these states the scanned string can be interpreted as an integer.

14.3 Graph Data Representations

For graph data structures to be helpful in computer applications we must have an effective
method for representing them in a computer program. We will review a few of the more common
methods for representing graphs.

Adjacency Matrix

For a graph G with n vertices, we create an nxn boolean (or equivalent) matrix. We label the row
and columns of the matrix with the names of the vertices. Each element of the matrix represents
a potential edge in the graph as defined by its associated vertex pair. We set an element of the
matrix to TRUE if there is an edge connecting the two corresponding vertices; otherwise, we set
the element to FALSE. We can use 1's and 0's to indicate the presence or absence of an edge.
Similarly, we can use a numeric value to represent the weight associated with an edge in a
weighted graph.

Figure 14-7: A Graph and Its Adjacency Matrix

324
In the previous example, the 1's represent the presence of an edge and a blank indicates that
there is no edge connecting the corresponding pair of vertices. The dashes indicate that there
can be no edge connecting a vertex to itself. For a weighted graph, the edge value will replace
the 1's.
Adjacency List - For each vertex, we list the
vertices connected to this vertex by an edge in
the graph. For an ordinary n-vertex connected
graph, the number of vertices connected to a
particular vertex is no greater than n-1.
Therefore, the edge list can be as large as n(n-
1), or n(n-1)/2 if each edge is represented only
once.
If the graph has many more edges than
vertices (approaching a complete graph) then
the adjacency matrix is the preferred method of
representation. If the graph is sparse (i.e. the
number of edges is much less than the
maximum number of edges) then an edge list Figure 14-8: Sample Adjacency List
is probably preferred.

Dynamic Memory Representations

The graph representation methods described above use static data structures such as arrays and
lists. There is a limit on the size of a graph that can be represented as an adjacency matrix.
Some applications deal with graphs that are too large to be fully represented in memory all at
once. Sometimes we need to use a method of representation that permits arbitrary growth and/or
restructuring of the graph during program execution. We can use dynamic memory to accomplish
this. Representing graphs using pointer records and pointers should be considered only in those
cases in which the adjacency matrix or adjacency list representations are not feasible.

A graphical representation of the dynamic memory data structure for our six-node sample graph
is provided below. In this representation, we have two types of nodes. There is a record for each
node as shown in the node list. A node record has a pointer to the next node and another pointer
to an incidence list. The incidence record contains two pointers. One pointer points to the next
incidence record for this node and the other points to a node that is adjacent to this node.

Figure 14-9: A Dynamic Memory Graph Representation

325
In our example, node A is adjacent to 4 nodes
so its incidence list has 4 records. The edge
pointers for node A point to records B, C, D,
and E. The complexity of the data structure for
this simple graph demonstrates that this
representation method should be considered
only if warranted by the application.
One area where such representations are
facet normals
helpul is computer graphics in which it is
important to be able to access adjacent facets
in a data model of a three-dimensional object.
Each facet corresponds to a record containing
the facet's normal (a vector perpendicular to
the facet surface). The type of shading to be
applied to the surface depends on the normals
Figure 14-10: 3D Wireframe Data Model
of the neighboring facets, which are directly
accessible through the incident list.

14.4 Graph Traversals

An important operation on graphs and trees is the ability to move through the data representation,
testing each vertex and/or edge value. This evaluate-and-move operation is referred to as a
traversal. There are two popular methods of traversal used in solving many of the problems
involving graphs and trees. As we saw in Chapter 12, they are depth-first traversal and breadth-
first traversal. With minor modification, these methods can also be applied to graphs in general.

In any traversal, we need to establish a convention for ordering the vertices. In our examples, we
will use alpha-numeric ordering of the vertex labels. This means that if two or more vertices can
be chosen next, we will select the vertex with a label that would come first in an alpha-numeric
sort. Once we have represented a graph in memory, the data structure will incorporate an order
of the nodes. We will use the alpha-numeric ordering convention when we are discussing graph
traversals separate from a computer implementation or data structure.

Depth-First Traversal

In a depth-first traversal (DFT) the left-most child of a vertex is expanded before any of its
siblings. A depth-first traversal is best implemented using a recursive algorithm (implies the use
of a stack data structure). The ordering rule is applied recursively so that we obtain the following
order of vertices in a depth-first traversal from the root of the tree shown below.
ABEMNFGCHDIOPQJKLR.
A

B C D

E F G H I J K L

M N O P Q R

Figure 14-11: An Example Generic Tree


326
Starting with the root node A, we have a choice of B,C, and D to expand. We choose B (by our
alpha-numeric ordering convention) and place B at the top of our stack. Next we consider the
children of B and choose E. The children of E are M and N so we choose M. At this point, we
have reached a leaf node (i.e. a node with no children) so we pop the stack returning to node E
and expand the next child of E which is node N. Node N is also a leaf node so we pop the stack
back to E. However, E has no more unexpanded children so we pop the stack again back to
node B. The unexpanded children of B are F and G which are added to our list in order. Popping
back to A gives us access the the second child of A which is C. Node C has a single child H
which is added to the list. We pop back to A to pick up its remaining child node D and so on.

A DFT can be implemented for a graph as well as a tree. In a graph traversal, we need to keep a
record of the nodes that have been traversed so that they are not entered more than once. In the
example below, we will perform a depth-first search of the graph shown below starting at node 1.

Figure 14-12: Sample Graph Showing the Beginning of a Depth-First Traversal (DFT)

The nodes adjacent to node 1 are 2, 3, and 4 so we choose 2 (by our convention). From node 2
we choose 4 since node 1 has already been expanded. From node 4 we choose node 5 since it
is the first unexpanded node in an alpha-numeric ordering of 1, 2, and 5 (actually it’s the only
unexpanded node reachable from node 4). From node 5 we choose 3 and finally 6.

Figure 14-13: Completed DFT

Breadth-First Traversal

In a breadth-first traversal (BFT) all the children of the current node (i.e. the node currently being
evaluated) are placed onto the queue before any of their children are considered. A breadth-first
traversal is obtained by following the stepwise procedure below:
Step 1: Place the starting node onto the queue.
Step 2: If the queue is not empty, retrieve the node that is at the front of the queue
and add its label to the order-of-traversal list, otherwise STOP.
Step 3: Place all the unencountered children of this node onto the queue (in alpha-
numeric order) and tag them as having been encountered.
Step 4: Return to Step 2.

327
The order-of-traversal list is a list of all the reachable nodes in the graph or tree arranged in the
order in which they were encountered in the breadth-first traversal. Note that, just as in the
depth-first traversal, we have to keep track of which nodes have been encountered so that we
don't use any node more that once.

Let's try this procedure on the sample graph shown below. We will start with node E and use our
alpha-numeric ordering convention for arranging the nodes nodes encountered at each step.

We choose node E as our arbitrary start node (the actual


A B C
starting node would be determined by the requirements of
the application or problem being solved). We place E onto
our queue and tag it as a used node.
D E F

Queue: E <-front of queue


G H I
Tagged: A B C D E F G H I

Order-Of-Traversal: [empty] Figure 14-14: Sample BFT Graph

The node at the front of the queue is E; we remove it and


A B C
add its label to the Order-Of-Traversal (OOT) list. The
children of E are A, C, and H. Since none of these nodes
have been encountered, they are all placed onto the queue.
D E F
Queue: H C A <-front of queue

Tagged: A B C D E F G H I
G H I

Order-Of-Traversal: E

Node A is now at the front of the queue so we remove it, add


its label to the OOT list, and place the unused children of A A B C
onto the queue.

Queue: D B H C <-front of queue D E F

Tagged: A B C D E F G H I
G H I
Order-Of-Traversal: E A

Node C is the next node at the front of the queue. We


remove C add its label to the OOT list and place its child A B C
node I onto the queue.

Queue: I D B H <-front of queue


D E F
Tagged: A B C D E F G H I

Order-Of-Traversal: E A C G H I

328
Node H is the next node at the front of the queue. The
children of H are D, E, F, G, and I but only F and G are new
nodes, so we place these two nodes onto the queue. A B C

Queue: G F I D B <-front of queue

Tagged: A B C D E F G H I D E F

Order-Of-Traversal: E A C H
G H I
Since all the nodes have been encountered we can flush the
queue as we add the nodes to the OOT list. The final list is,

Order-Of-Traversal: E A C H B D I F G

Priority Traversal

So far we have looked at two different ways to traverse the nodes in a graph. Both of these
methods choose the order of traversal based on the position of the nodes in the graph data
representation. Now we will study a graph traversal method that chooses the order to traverse
the nodes based on some criterion specific to the application. As the nodes are encountered,
they will be placed into a priority queue and ranked according to "best choice" for the next node to
traverse. The "best choice" will depend on the specific problem being solved.

Let's look at an example problem from operations research, a discipline related to computer
science. In Figure 14-15 below we see a weighted directed graph. The weights on the arcs
represent costs for moving between the pairs of nodes. Movement out of a node must be along
an arc in the direction indicated by its arrow. The goal is to find a minimal cost path from node S
(source) to node T (target). We will use a priority traversal to find the lowest cost path.

1 1
A D G
5 4 1 4
2
2 1
2 5 1 6
S B E H T
1 7 5 1
1 1
3
3 6
C F I

Figure 14-15: A Weighted-Directed Graph for Minimal Cost Path Problem

We will use a lower bound estimate of the cost to complete a path as the criterion for ordering the
nodes in our priority traversal. From an initial inspection of the graph, we find that the minium
weight arc is 1 and that the minimum length path is 4. We place the node S into our queue to
start the priority traversal.
back -> S -> front

There are three choices from S, namely A, B, and C with respective weights 5, 2, and 1. We will
order these nodes according to their contributions to the cost of a path from S to T. So, we add
these nodes to the priority queue in the following order

back -> (SA) (SB) (SC) -> front


5 2 1

We can scan the graph and quickly see that the path S->C->E will not lead to a minimal-cost
solution, but the computer must work with the data representation of the graph and cannot "peek
ahead" to find dead-ends or high-cost traps. Figure 14-16, on the next page, shows the graph
from the point of view of our traversal algorithm.
329
A
5

2
S
1
B
?

Figure 14-16: State of Graph from the Point of View of the Priority Traversal Algorithm
back -> (SCE)(SA)(SCF)(SB) -> front
8 5 4 2
Since node C is at the front of the priority queue, the algorithm evaluates this node and adds the
nodes reachable from node C into priority queue in the order of the total cost of reaching the
corresponding nodes. It is important to note that a particular path cannot enter a node more than
once, but that two different paths can include the same node. This is because only one of the
candidate paths will be part of the solution.

1 1
A D G
5 4 1 8
2
2 1
2
S B
5
E
1
? H
6
T
1 7 5 2
1 3
3
3 6
C F I

Figure 14-17: Portion of Graph "Visible" to Priority Queue Algorithm

back -> (SCE)(SBE)(SA)(SCF)(SBA) -> front


8 7 5 4 4
The next best path (at the front of the priority queue) is S->B->A with a cost of 4. From this path,
we expand from node A to nodes D and E.
back -> (SCE)(SBAE)(SBE)(SA)(SBAD)(SCF) -> front
8 8 7 5 5 4
We have chosen to place new sub-paths in front of existing sub-paths of the same cost. This is
because the new paths are generally closer to the final solution and therefore are more likely to
be part of the optimal solution.
back -> (SCFI)(SCE)(SBAE)(SBE)(SCFE)(SA)(SBAD) -> front
10 8 8 7 7 5 5

1 1
A D G
5 4 1 8
2
2 1
2
S B
5
E
1
? H
6
T
1 7 5 2
1 3
3
3 6
C F I

Figure 14-18: State of the Priority Queue Graph following the expansion of the SCF Sub-Path

This process continues until each of the sub-paths reaches the goal node T.

330
back -> (SCFI)(SCE)(SBAE)(SBE)(SCFE)(SBADE)(SBADG)(SA) -> front
10 8 8 7 7 7 6 5

back -> (SCFI)(SAE)(SCE)(SBAE)(SBE)(SCFE)(SAD)(SBADE)(SBADG) -> front


10 9 8 8 7 7 7 6 6

back -> (SCFI)(SAE)(SCE)(SBAE)(SBE)(SCFE)(SAD)(SBADE)[SBADGT] -> front


10 9 8 8 7 7 6 6 10

The sequence of nodes S->B->A->D->G-T is a complete path from the source node S to the
target node T which has a cost of 10. Now we have a complete path that represents a feasible
solution and is a candidate for the optimal solution. Any sub-paths of cost 10 or greater can be
removed from consideration since they cannot be part of a solution that is better than this one.

Complete solutions are not added to the priority queue since there are no other nodes to expand.
However, we use them to remove the sub-paths such as S->C->F->I because its cost is already
10 and, therefore, cannot lead to a better solution. In this example, we will eventually discover
that the minimal cost path has a cost of 10.

Improving Algorithm Performance

Since we know the minimum number of edges required to complete a path, and we know that the
minimum value of each edge is 1, we can reduce the total number of nodes to evalute in the
priority queue. Consider the sub-paths in the last priority queue previously illustrated.

back ->(SAE)(SADE)(SCE)(SBAE)(SBE)(SCFE)(SADG)(SBADEG)(SBADEH)-> front


9 9 8 8 7 7 7 7 7
min # arcs 2 2 2 2 2 2 1 1 1

Comparing the sub-paths with the example solution, we can eliminate SAE, SADE, SCE and
SBAE from further consideration. Since these sub-paths require at least two more edges to
complete with costs at least 1 each, they cannot result in a total cost less than 10.

14.5 Implementing Graph Traversals

Implementing a Depth-First Traversal

There are many ways to implement a depth-first traversal of a graph. One of the most efficient is
as a recursive algorithm for a DFT of a graph. We will represent the graph in an adjacency
matrix.

type graphtype is array(1..100,1..100) of integer;


type nodelistype is array(1..100) of integer;

We have also created a 1-dimensional array data type for the node list and the traversal list. The
node list will be used to keep track of which nodes have been placed in the traversal list. Now we
need to declare variables.

graph : graphtype;
nodes : nodelistype;
start_node : integer;
trav_list : nodelistype;
trav_num : integer;
num : integer;

The array graph is the adjacency matrix, nodes is the node list, start_node is the label of the node
at which we will begin our traversals, trav_list will hold the list of node labels indicating the order
in which the nodes are evaluated, trav_num will be used as the index of the next available
position in trav_list, and num is the number of nodes in the graph.
331
procedure DFT(node : in integer) is
begin
nodes(node):=1;
trav_list(trav_num):=node;
trav_num:=trav_num+1;
for col in 1..num loop
if graph(node,col)=1 and nodes(col)=0 then
DFT(col);
end if;
end loop;
end DFT;

In our example, we call DFT(1) which places node 1 in the trav_list and checks for other unused
nodes adjacent to node 1. Nodes 2, 3, and 4 are all adjacent to node 1, but as soon as DFT(2) is
called, the copy of DFT(1) is pushed onto the process control stack and DFT(2) begins its
operation. This means that DFT(4) is called by DFT(2) before DFT(3) is called by DFT(1). You
should work though the execution of this program by hand in order to better understand this
process.

Implementing a Breadth-First Traversal

In the following section we develop an implementation of the breadth-first traversal algorithm as


an Ada procedure. As usual, our first task is to choose a data structure. Since we want to use a
queue to maintain our order of nodes, we will naturally make use of our ADT generic adt_queue.
We will instantiate an integer queue as part of our BFT program declaration.

package my_queue is new adt_queue(integer);


use my_queue;

We will use an adjacency matrix to hold the graph description. The data table below will be
saved as a text file named bft_graph.dat.

9
ABCDEFGHI
0 1 0 1 1 0 0 0 0
1 0 0 0 0 1 1 0 0
0 0 0 0 1 0 0 0 1
1 0 0 0 0 0 0 1 0
1 0 1 0 0 0 0 1 0
0 1 0 0 0 0 0 1 0
0 1 0 0 0 0 0 1 1
0 0 0 1 1 1 1 0 1
0 0 1 0 0 0 1 1 0

The first number is the number of nodes in the graph, which we will call nnodes. The next line is
a list of 9 node labels. We are using the same example graph that we used in our previous
example as shown in Figure 12-14. The adjacency matrix is the 9x9 array of 1's and 0's. Each
row and column correspond to a node while the 1's represent edges connecting pairs of nodes.
For example, the first row (which is associated with node A) has 1's in columns 2, 4, and 5
corresponding to edges connecting node A to nodes B, D, and E.

We need a data structure to hold these data in our BFT program.

maxnodes : constant integer :=9;


type adjmatype is array(1..maxnodes,1..maxnodes)of integer;
adjmat : adjmatype;
nnodes : integer;

332
Although we have only 9 nodes in our sample graph, we can set maxnodes to handle larger
graphs. We must be careful to use maxnodes everywhere we refer to the maximum number of
nodes so that we will have to change only one value.

We also need data structures to hold the list of labels (node symbols A through I) and lists for the
order of traversal and to keep a record of the tagged (used) nodes.

type symlistype is array(1..maxnodes)of character;


type numlistype is array(1..maxnodes)of integer;

label : symlistype;
oot : symlistype;
noot : integer := 0;
used : numlistype;

The oot array will hold the list of node labels in the order they are encountered in the BFT. This
will be the output of our program. The integer noot will keep track of how many node labels have
been placed on the oot list as the program runs.

The used list will be initially set to all 0's. As nodes are added to the back of the queue, the
corresponding element of the used list will be set to 1. The BFT program will refer to these
values to see if a node has been used before it is added to the queue.

Before performing the traversal, we will need to load the data from the text file into the program
data structures and to initialize the used list. Once the data has been loaded, we will need to
select a starting node for the traversal. In our demonstration program, we will ask the user to
choose a starting node (by number). We will then enqueue this starting index into Q and tag this
node as used.

put("Enter index of starting node... ");


get(start);

enqueue(Q,start);
used(start):=1;

Once the starting node has been placed into the queue, we can exercise our BFT algorithm by
dequeuing each node (index) from the queue, adding this node to the order of traversal (oot) list,
and placing the unused children of this node at the back of the queue, in order.

while not(is_empty(Q)) loop


dequeue(Q,curval);
noot:=noot+1;
oot(noot):=label(curval);
for j in 1..nnodes loop
if adjmat(curval,j)=1 and used(j)=0 then
enqueue(Q,j);
used(j):=1;
end if;
end loop;
end loop;

At the top of the while..loop we take the next node index from the front of the queue Q and place
its label into the oot list. Note that we use the index curval to choose the correct node label from
the label list.

At the bottom of the while..loop we scan all the nodes in the graph, testing to see if a node is
adjacent to the current node and if it has not yet been used. If both of these conditions are true
then we enqueue this node and tag it as used.

333
This process repeats until the queue is empty. Note that once all the nodes have been placed in
the queue, the for..loop at the bottom of the while..loop could be skipped and the remaining
nodes in the queue could be dequeued and placed into the oot list. The data below shows the
output for our BFT demonstration program.

Enter graph file name... bft_graph.dat

A B C D E F G H I
A 0 1 0 1 1 0 0 0 0
B 1 0 0 0 0 1 1 0 0
C 0 0 0 0 1 0 0 0 1
D 1 0 0 0 0 0 0 1 0
E 1 0 1 0 0 0 0 1 0
F 0 1 0 0 0 0 0 1 0
G 0 1 0 0 0 0 0 1 1
H 0 0 0 1 1 1 1 0 1
I 0 0 1 0 0 0 1 1 0

A B C D E F G H I
1 2 3 4 5 6 7 8 9

Enter index of starting node... 5


E A C H B D I F G

A complete listing of the BFT_demo.adb program is given below.

with ada.text_io, ada.integer_text_io, adt_queue;


use ada.text_io, ada.integer_text_io;
procedure BFT_demo is

maxnodes : constant integer :=30;


type adjmatype is array(1..maxnodes,1..maxnodes)of integer;
type symlistype is array(1..maxnodes)of character;
type numlistype is array(1..maxnodes)of integer;

package my_queue is new adt_queue(integer);


use my_queue;

fname : string(1..20);
fleng : integer;
datin : file_type;
Q : qtype;
start : integer;
curval : integer;
adjmat : adjmatype;
nnodes : integer;
oot : symlistype;
noot : integer:=0;
label : symlistype;
used : numlistype;

procedure loadgraph is
begin
put("Enter graph file name... ");
get_line(fname,fleng);
open(datin,in_file,fname(1..fleng));

get(datin,nnodes);
for i in 1..nnodes loop

334
get(datin,label(i));
used(i):=0;
end loop;

for i in 1..nnodes loop


for j in 1..nnodes loop
get(datin,adjmat(i,j));
end loop;
end loop;
close(datin);

new_line(2);
put(" ");
for i in 1..nnodes loop
put(label(i));
put(" ");
end loop;
new_line;

for i in 1..nnodes loop


put(" ");
put(label(i));
for j in 1..nnodes loop
put(adjmat(i,j),3);
end loop;
new_line;
end loop;
new_line;
end loadgraph;

begin
loadgraph;
for i in 1..nnodes loop
put(" ");
put(label(i));
end loop;
new_line;
for i in 1..nnodes loop
put(i,3);
end loop;
new_line(2);

put("Enter index of starting node... ");


get(start);

enqueue(Q,start);
used(start):=1;

while not(is_empty(Q)) loop


dequeue(Q,curval);
noot:=noot+1;
oot(noot):=label(curval);
for j in 1..nnodes loop
if adjmat(curval,j)=1 and used(j)=0 then
enqueue(Q,j);
used(j):=1;
end if;
end loop;
end loop;

for i in 1..nnodes loop


335
put(oot(i));
put(" ");
end loop;

end BFT_demo;

Graphs and trees are important structures in computer science because they give us efficient
methods for modeling complex systems. In the next chapter, we will use these structures and the
associated traversal methods to implement efficient algorithms for managing large data sets.

336
Exercises

14.1 How many edges are in,


a. an N-node complete graph?
b. an N-node cycle?
c. an N-node wheel?
d. an N-level hypercube?

14.2 How many minimal-length paths are there between nodes 0000 and 1111 in a 16-node
hypercube?

14.3 Create finite-state machines to recognize the following sets. Assume an alphbet {0,1}.
a. all binary strings with an even number of 1's.
b. all binary strings with an odd number of 1's and an even number of 0's.
c. all binary strings containing the substring 1101.
d. all binary strings that do not contain the substring 1001.

14.4 Modify the integer recognizer FSM below to reject strings with leading or trailing blanks.

0..9

+/-
0..9 b
b
start
+/-
0 1 2 3 4 b = blank
0..9 b 0..9 = a digit
b
+/- = plus or minus sign
else
else = any other ASCII character
else
else else

else
5

else

14.5 Create an FSM to recognize strings representing time in the form HH:MM:SS where MM and
SS are each between 00 and 59.

14.6 Write an Ada program that determines the maximum degree of any node in the graph. Your
program should return the label of the node and its degree. Choose an appropriate graph data
representation (adjacency matrix or adjacency list) and explain your choice.

14.7 Write an Ada program that finds cycles in a directred graph.

14.8 Write an Ada program that determines the height of binary tree as given in a list
representation.

14.9 Which traversal method (DFT or BFT) would be best for each of the following operations on
a graph?
a. Finding the shortest path between a pair of nodes.
b. Finding the shortest cycle from a node back to the same node
c. Finding a cycle involving all the nodes in the graph (called a Hamiltonian Path)
d. Finding a subset of edges the make up a spanning tree (connects all nodes).

337
338
Chapter 15 - Graph Algorithms

15.1 What are Graph Algorithms?

15.2 Example Problems for Graph Algorithms


Euler Circuit
Hamiltonian Cycle
Maximal Matching
Floyd’s All Pairs Shortest Path
The Assignment Problem

15.3 Algorithm Design and Implementation

15.4 Applications
Finding a Hamiltonian Cycle
Minimum Spanning Tree Problem
Munkres Assignment Algorithm

339
Chapter 15 - Graph Algorithms
In this chapter, we introduce the concept of a graph algorithm. We review the five major problem
solving methods and their relationship to the design, implementation, testing, and analysis of
graph algorithms.

15.1 What are Graph Algorithms?

Graph Algorithms are problem-solving methods in which the data describing the problem or the
solution to the problem can best be represented as a graph. Since trees are special instances of
graphs, algorithms that use tree data structures are included in this definition. We have seen a
number of problems already for which graph algorithms are well suited. Among these are a
number of searching and sorting problems. We have used basic graph algorithms to traverse
trees and other graphs in depth-first and breadth-first fashions. These techniques can be
combined with other problem-solving techniques to solve a wide variety of important problems.

15.2 Example Problems for Graph Algorithms

We review a few well-known problems and the graph algorithms that solve them.

Euler Circuit

The origins of many areas of mathematics are lost to history; however, the origin of Graph Theory
is well known. In the Town of Konigsberg, Germany in the early 1700's, there were seven
bridges crossing the river Pregel. The citizens of the town enjoyed taking walks and many
wondered if it were possible to choose a path so that one would cross each of the bridges exactly
once. This question attracted the attention of the Swiss mathematician Leonhard Euler, who
eventually proved that there was no path or circuit that achieved this goal.

A
A
B C
C
B
D
D

Figure 15-1: The Bridges of Konigsberg "County"

In the process of solving this problem, Euler invented Graph Theory. The Konigsberg Bridge
problem can be reduced to a multi-graph as shown in Figure 15-1 above. The nodes of the graph
represent the four land masses, A through D, and the edges represent the bridges. Euler showed
that in order for there to be a closed path (circuit) that included all the edges exactly once, each
node would have to have an even degree. The problem of finding a closed path through a graph
that traverses all the edges of the graph exactly once has come to be known as the Euler Circuit
problem.

Hamiltonian Cycle

In the Hamiltonian Cycle problem, the goal is to find a closed path or cycle that includes all the
nodes of the graph exactly once. Some graphs have Hamiltonian Cycles and some don't, as
shown in Figure 15-2 on the next page.

340
a. Hamiltonian Cycle: 1,5,6,3,2,4 b. No Hamiltonian Cycle
Figure 15-2: Example Graphs for the Hamiltonian Cycle Problem
A Hamiltonian cycle (also called a tour) of a graph is a path that starts at a given vertex, visits
each vertex in the graph exactly once, and ends at the starting vertex. Some connected graphs
do not contain Hamiltonian circuits and others do. We may use backtracking to discover if a
particular connected graph has a Hamiltonian circuit.

To find a Hamiltonian Cycle in a graph we can start at any node and perform a depth-first
traversal of the graph. In this case, we will want to find a branch of the traversal that does not
encounter any dead ends.

As shown in Figure 15-3, if there is a Hamiltonian cycle in an n-node graph then a depth-first
traversal of the graph will result in an embedded tree of depth n.

Figure 15-3: Embedded Trees for the Graphs shown in 15-2

A state space tree for this problem is as follows. Put the starting vertex at level 0 in the tree; call
this the zeroth vertex on the path (we may arbitrarily select any node as the starting node since
we will be traversing all nodes in the cycle). At level 1, create a child node for the root node for
each remaining vertex that is adjacent to the first vertex. At each node in level 2, create a child
node for each of the adjacent vertices that are not in the path from the root to this vertex, and so
on. Notice that the graph nodes may be represented by more than one node in the state space
tree. This is true since a particular graph node may be part of more than one path.

Maximal Matching

Assume we have a graph made up of two groups of nodes. Every node in Group I is connected
to at least one node in Group II and vice versa. However, no two nodes in the same group are
connected. We are interested in finding the maximum number of pairings of nodes in Group I
with nodes in Group II. Any node can be a member of, at most, one pairing. Graphs of the type
described here are called bipartite graphs. Given a bipartite graph Gn,m we are to find a maximal
341
matching (max number of pairs) between the n nodes of group I and the m nodes of group II.
There is a greedy algorithm for the maximal matching problem:

Augmenting Path Algorithm


1. Make an initial matching
2. Create a series of nodes starting with an unmatched node, alternating between matched
and unmatched edges ending with an unmatched edge (this is an augmenting path).
3. Exchange the matched and unmatched edges in the series found in Step 2.
4. Repeat Steps 2 & 3 until no augmenting path can be found.

Example: Find a maximal matching for the bipartite graph shown below.

Figure 15-4: Bipartite Graph

Choose an arbitrary initial pairing. Continue picking match pairs until no additional matches are
possible. In this example, nodes C, R, and S cannot be matched. Once A is matched with P, R
and S cannot be matched with A, and C cannot be matched with P.

Matching edges are shown in bold


Figure 15-5: Initial Matching

We will now build an augmenting path starting from node S.

S-A=P-C

Figure 15-6: Augmenting Path S-A=P-C

342
We exchange the matched and unmatched edges in this augmenting path to create an additional
match.
A is matched to S
B is matched to Q
C is matched to P

This is a maximal matching because there are no more unmatched nodes in one of the two
groups.

Figure 15-7: Bipartite Graph with a Maximal Matching A=S, B=Q, C=P

Floyd’s All Pairs Shortest Path

Another popular graph optimization problem is called All-Pairs Shortest Path. In this problem,
you are to compute the minimal path from every node to every other node in a directed, weighted
graph. A directed graph uses arcs rather than edges to connect the nodes. As discussed in the
previous chapter, arcs are edges with a direction. In the example below, there is an arc from V2
to V3 but not from V3 to V2.

The array shown in Figure 15-8 is a data representation of the directed weighted graph shown.
Each row and column represents a particular node in the graph, while each entry in the body of
the array is the weight of an arc connecting the corresponding nodes. A dash (-) represents an
infinite length or no arc between the corresponding pair of nodes.

Floyd's algorithm uses the Dynamic Programming Method to compute the shortest paths between
every pair of nodes. (The value 6 in the 2nd row and 3rd column is read, "The arc connecting V2
to V3 has length 6.”)

Figure 15-8: Floyd's All Pairs-Shortest Path Algorithm

343
Floyd's algorithm replaces the current candidate minimal distance between the ith node and the
jth node with the minimum of this distance and the sum of the distance from the ith node to the
kth node and the distance from the kth node to the jth node. This operation is repeated for each
node Vi, Vj and for each intermediate node Vk. The fact that Floyd's algorithm gives the minimal
path length for every node pair is not simple to prove. It is particularly difficult to see how it is
possible that the order in which the intermediate nodes Vk are chosen does not affect the result.
How would the results be altered if we were to exchange positions of pairs of rows and columns
in the array graph representation?

The Assignment Problem

The assignment problem is a classic optimization problem in computer algorithms. In the


assignment problem you are attempting to find the "best" assignment of one group of objects to
another. In the original application you are assigning jobs to workers.

Let C be an n x n square matrix representing the costs of each of n workers to perform any of n
jobs. The assignment problem is to assign jobs to workers so as to minimize the total cost.
Since each worker can perform only one job and each job can be assigned to only one worker,
the assignments constitute an independent set of the matrix C.

Figure 15-9: Example of the Assignment Problem

An arbitrary assignment is shown above in which worker a is assigned job q, worker b is assigned
job s, and so on. The total cost of this assignment is 23. Can you find a lower cost assignment?
Can you find the minimal cost assignment? Remember that each assignment must be unique in
its row and column.
A brute-force algorithm for solving the assignment problem involves generating all independent
sets of the matrix C, computing the total costs of each assignment and a search of all
assignments to find a minimal-sum independent set. The complexity of this method is driven by
the number of independent assignments possible in an nxn matrix. There are n choices for the
first assignment, n-1 choices for the second assignment, and so on, giving n! possible
assignment sets. Therefore, this approach has, at least, an exponential runtime complexity.

Figure 15-10: Example Independent Set in a 5 x 5 Matrix

As each assignment is chosen, that row and column are eliminated from consideration. The
question is raised as to whether there is a better algorithm. In fact, there exists a polynomial
runtime complexity algorithm for solving the assignment problem developed by James Munkres in
the late 1950s despite the fact that some references still describe this as a problem of exponential
complexity.

344
15.3 Algorithm Design and Implementation

The graph algorithms described in the previous sections give us a better idea of the value of
structured programming in computer-based problem solving. Most of the computer programming
you will be doing in your computer science career will be object-oriented. The value of object-
oriented programming is in its ability to support code reuse. It is much easier to modify, update,
and manage existing object oriented programs due to the high level of abstraction, encapsulation,
and inheritance possible.

However, another important part of computer programming is the design and implementation of
new code. In this regard, structured programming techniques offer a notable advantage. The
ability to directly create and manipulate complicated data structures as suggested by the above
examples is much simpler using a structured language. Attempting to build graph algorithms
using an object oriented approach requires the availability of a large number of classes and
methods not provided in standard OOP libraries. This forces the programmer to adapt existing
classes and methods and extend them to support multidimensional arrays and other data
structures. Unfortunately, the high level of encapsulation hides important details from the
programmer which can result in extremely inefficient code.

The alternative solution is to build the classes and methods from scratch using essentially
structured programming techniques covered in this text. In this way, object-oriented design tools
can be used to organize procedural programming constructs and data structures into efficient
objects and methods on those objects. This requires expertise in both object-oriented
programming and structured programming methods.

For example, consider the problem of Depth-First Traversal (DFT). Example: A graph is an
object G={V,E} which is a set of nodes V and a set of edges E. If we want to develop a method
for DFT we must provide a data structure that allows us to tag nodes as "used" and to efficiently
determine which nodes are adjacent to each other. While the DFT class and its methods can be
written in an OOP style, the underlying code must be structured in nature, not object-oriented.

As you gain experience in object-oriented programming, remember to use the lessons learned in
structured programming, to develop efficient and powerful solutions to complex problems.

15.4 Applications

15-1: Develop an algorithm to find a Hamiltonian cycle in a graph.

Data Structure - First we will define a data structure to hold the graph representation. In this
problem, we will use an adjacency matrix W.

Figure 15-11: Example Graph and its Representation as an Adjacency Matrix

345
We define a one-dimensional array p of integers that will contain the labels of the vertices in order
of traversal.

Depth-First Traversal - We can define a recursive algorithm hamiltonian(p,index ) that performs a


depth-first traversal of the embedded state-space tree. This algorithm has the following form.
Assume that we start with vertex v1. The procedure hamiltonian( p,index) must perform correctly
for any stage in the development of a Hamiltonian circuit. Let index be a location in the list p such
that p(index) is the most recent node entered. Let n be the number of nodes in the graph. The
initial call to hamiltonian(p,1) should be made with vertex v1 already loaded into p(1). The
pseudocode below contains the termination component and the recursive component as shown.

hamiltonian(p,index)
if index>n then
path is complete --termination component
display the values in p
else
for each node v in G --recursive component
if v can be added to the path then
add v to path p and call hamiltonian(p,index+1)
end hamiltonian

Termination Component - A Hamiltonian circuit has been created in p if the value of index (the
next position in the path list) is bigger than the number of nodes in the graph. If so, then this copy
of p must contain a path back to the vertex v1. That is, p contains the indices of the nodes of the
graph in the order they must be traversed to complete a Hamiltonian circuit. If index is not
greater than n then there is still work to do.

Recursive Component - The else part of the pseudocode builds all the children (if any) of
p(index), the node most recently added to the list p. The underlined phrase, v can be added to
the path, needs more explanation. A new node can be added to the path only under one of the
following criteria:
1. there are less than n nodes in p so far and v is not already in the path p and there
is an edge connecting v to p(i)
or
2. there are exactly n nodes in p already and v is the first node in p and there is an
edge connecting v to p(n)

Implementation of hamiltonian(p,index) - The Ada source code below is a working version of


hamiltonian( ). The path list p is defined as an in parameter (i.e. pass by value) so that each call
to hamiltonian will carry with it, its own copy of the path. We make a local copy of p called ptmp
so that we can add another node to p to be passed along in each recursive call to hamiltonian( ).
This version of hamiltonian( ) generates and displays all Hamiltonian circuits for the input graph.
We could easily add a global Boolean to terminate the program as soon as the first Hamiltonian
circuit is displayed.

procedure hamiltonian(p : in path_type; index : in integer) is


ptmp : path_type;
begin
if index>n then
display(p);
else
ptmp:=p;
for j in 1..n loop
if add_node_ok(p,index,j) then

346
ptmp(index+1):=j;
hamiltonian(ptmp,index+1);
end if;
end loop;
end if;
end hamiltonian;

Implementation of add_node_ok(p,index,j) - Typical of recursive algorithms, the real work is


hidden in the recursive conditional. The Boolean function add_node_ok( ) returns true if all the
conditions listed in (1) or (2) for the recursive component have been satisfied. This
implementation is not the most efficient possible. You are strongly encouraged to follow your own
path (no pun) when encoding complicated logical constructs.

function add_node_ok(p: path_type;


index : integer;
j : integer) return boolean is
isok : boolean;
begin
if W(p(index),j) then --A. there is an edge
isok:=true;
if index<n then --B. path is not done
for k in 1..index loop
if p(k)=j then --C. new node has not
isok:=false; -- been used already
end if;
end loop;
else
if j>1 then --D. new node is not node 1
isok:=false;
end if;
end if;
else --E. there is no edge
isok:=false;
end if;
return isok;
end add_node_ok;

The adjacency matrix W( ) has been implemented as a 2-D Boolean array in which W(i,j)=true
indicates that there is an edge connecting node i to node j in the graph. This function is testing if
it is OK to add the node j to the path list p. We first check to see if there is an edge between the
node most recently added to the path p(index) and the new node j being considered (A) (else at
E). If there is an edge then we check to see if we are at the end of a Hamiltonian circuit (i.e.
index=n). If index is less than n then we are not at the end of a circuit (B) and we then make sure
that the new node j is not already in the path p( ) (C). If index is equal to n then we just have to
make sure that we have returned to node 1 (D).

15-2: Implementing an Algorithm for the Minimum Spanning Tree Problem

The graph shown in Figure 15-12 is called a weighted graph because its edges have values or
weights assigned to them. The Minimum Spanning Tree (MST) problem is to find a tree that is
made up of all the nodes in the graph and a subset of the edges such that the sum of the edge
weights is a minimum.

347
Figure 15-12: Weighted Graph and Two Different Data Representations

To implement an algorithm for the minimum spanning tree problem, we need a data-
representation for weighted graphs. To solve the problem manually, we can look at a picture of
the graph, but for computer analysis, an edge list or adjacency matrix representation is preferred.
As shown in the figure above, the weight of each edge is included as a parameter in the edge list
and the weight replaces the Boolean value in an adjacency matrix. The MST problem can be
implemented using either of these data structures.

Prim's Algorithm is an efficient method for finding the minimum spanning tree in a weighted
graph. This algorithm is stated formally as follows:

Given a weighted graph G consisting of a set of vertices V and a set of edges E with weights wi,j,
where,
G = {V , E}
V = {set of all vi , i = 1..n}
E = {set of all ei , j = (vi , v j , wi , j )}

Prepare a vertex list VP and an edge list EP (that are initially empty) to hold the vertices and
edges selected by Prim's Algorithm.

1. Choose any starting vertex vi and place it in the vertex list VP.

2. Find the smallest weight edge ei,j incident with a vertex in the vertex list whose inclusion in the
edge list will not create a cycle. This can be done by verifying that the other vertex vj is not
already in the vertex list.

3. Include this edge in the edge list EP and the associated vertex vj in the vertex list VP.

4. Repeat Steps 2 and 3 until all vertices of the graph are in the vertex list VP.

The solution to the MST is the edge list and the sum of the weights of the edges in the edge list
EP is the minimum weight (sometimes we say minimal since there may be more than one
minimum) spanning tree.

The type of graph traversal being performed in Prim's Algorithm is neither purely depth-first nor
breadth-first since the only edges that are examined are those incident with vertices in the
selected vertex list. A simple way to implement Steps 2 and 3 is in a loop as shown in the
pseudo-code below:
wmin=some_large_value
for every edge ek=(vi,vj,wij) in E
if [(vi in VP and vj not in VP)
or (vi not in VP and vj in VP)] and wij<wmin then

348
wmin = wij
emin=ek
end if
end loop
include emin in EP and vi and vj in VP.

15-3: Exercising Munkres' Assignment Algorithm

The following 6-step algorithm is a modified form of the original Munkres' Assignment Algorithm
(sometimes referred to as the Hungarian Algorithm). This algorithm describes the manual
manipulation of a two-dimensional matrix by starring and priming zeros and by covering and
uncovering rows and columns. This is because, at the time of publication (1957), few people had
access to a computer and the algorithm was exercised by hand.

Step 0: Create an nxm matrix called the cost matrix in which each element
represents the cost of assigning one of n workers to one of m jobs. Rotate the
matrix so that there are at least as many rows as columns and let k=min(n,m).

Step 1: For each row of the matrix, find the smallest element and subtract it from
every element in its row. Go to Step 2.

Step 2: Find a zero (Z) in the resulting matrix. If there is no starred zero in its
row or column, star Z. Repeat for each element in the matrix. Go to Step 3.

Step 3: Cover each column containing a starred zero. If K columns are covered,
the starred zeros describe a complete set of unique assignments. In this case,
Go to DONE, otherwise, Go to Step 4.

Step 4: Find a noncovered zero and prime it. If there is no starred zero in the
row containing this primed zero, Go to Step 5. Otherwise, cover this row and
uncover the column containing the starred zero. Continue in this manner until
there are no uncovered zeros left. Save the smallest uncovered value and Go to
Step 6.

Step 5: Construct a series of alternating primed and starred zeros as follows.


Let Z0 represent the uncovered primed zero found in Step 4. Let Z1 denote the
starred zero in the column of Z0 (if any). Let Z2 denote the primed zero in the row
of Z1 (there will always be one). Continue until the series terminates at a primed
zero that has no starred zero in its column. Unstar each starred zero of the
series, star each primed zero of the series, erase all primes and uncover every
line in the matrix. Return to Step 3.

Step 6: Add the value found in Step 4 to every element of each covered row,
and subtract it from every element of each uncovered column. Return to Step 4
without altering any stars, primes, or covered lines.

DONE: Assignment pairs are indicated by the positions of the starred zeros in
the cost matrix. If C(i,j) is a starred zero, then the element associated with row i
is assigned to the element associated with column j.

349
Workers = { a, b, c}
Jobs = {p, q, r}

1. Step 0 2. Step 1

3. Step 2 5. Step 4
4. Step 3

6. Step 6 7. Step 4 8. Step 5

9. Step 3 10. Step 4 11. Step 6

12. Step 4 13. Step 6 14. Step 4

15. Step 5 16. Step 3 17. Done

Figure 15-13: Step-by-Step Execution of Munkres' Assignment Algorithm


Some of these descriptions require careful interpretation. In Step 4, for example, a possible
situation is that there is a noncovered zero which gets primed and, if there is no starred zero in its
row, the program goes onto Step 5. The other possible way out of Step 4 is that there are no
noncovered zeros at all, in which case the program goes to Step 6.
350
By applying Rule 4 to the step-algorithm, we decide to make each step its own procedure. Now
we can apply Rule 8 by using a case statement in a loop to control the ordering of step execution.

procedure munkres is
n : constant integer := 20;
C : is array(1..n,1..n) of float;
M : is array(1..n,1..n) of integer;
Row,Col : is array(1..n) of integer;
stepnum : integer;
done : boolean;
procedure step1(stepnum: in out integer) is
:
procedure step2(stepnum: in out integer) is
:
procedure step3(stepnum: in out integer) is
:
begin
done:=false;
stepnum:=1;
while not(done) loop
case stepnum is
when 1 => step1(stepnum);
when 2 => step2(stepnum);
when 3 => step3(stepnum);
when 4 => step4(stepnum);
when 5 => step5(stepnum);
when 6 => step6(stepnum);
when others => done:=true;
end case;
end loop;
end munkres;

In each pass of the loop, the step procedure called sets the value of stepnum for the next pass.
When the algorithm is finished, the value of stepnum is set to some value outside the range 1..6
so that done will be set to true and the program will end. In the completed program, the tagged
(starred) zeros flag the row/column pairs that have been assigned to each other. We will discuss
the implementation of a procedure for each step of Munkres' Algorithm below. We will assume
that the cost matrix C(i,j) has already been loaded with the first index referring to the row number
and the second index referring to the column number.

Munkres' Algorithm Implementation

Step 1 - For each row of the matrix, find the smallest element and subtract it from every element
in its row. Go to Step 2. We can define a local variable called minval that is used to hold the
smallest value in a row. Notice that there are two loops over the index j appearing inside an outer
loop over the index i. The first inner loop over the index j searches a row for the minval. Once
minval has been found, this value is subtracted from each element of that row in the second inner
loop over j. The value of step is set to 2 just before stepone ends.

procedure stepone(step : in out integer) is


minval : integer;
begin
for i in 1..n loop
minval:=C(i,1);
for j in 2..n loop
if minval>C(i,j) then
minval:=C(i,j);
end if;
end loop;
351
for j in 1..n loop
C(i,j):=C(i,j)-minval;
end loop;
end loop;
step:=2;
end stepone;

Step 2 - Find a zero (Z) in the resulting matrix. If there is no starred zero in its row or column,
star Z. Repeat for each element in the matrix. Go to Step 3. In this step, we introduce the mask
matrix M, which has the same dimensions as the cost matrix and is used to star and prime zeros
of the cost matrix. If M(i,j)=1 then C(i,j) is a starred zero; if M(i,j)=2 then C(i,j) is a primed zero.
We also define two vectors R_cov and C_cov that are used to "cover" the rows and columns of
the cost matrix C. In the nested loop (over indices i and j), we check to see if C(i,j) is a zero value
and if its column or row is not already covered. If not then we star this zero (i.e. set M(i,j)=1) and
cover its row and column (i.e. set R_cov(i)=1 and C_cov(j)=1). Before we go on to Step 3, we
uncover all rows and columns so that we can use the cover vectors to help us count the number
of starred zeros.

procedure steptwo(step: in out integer) is


begin
for i in 1..n loop
for j in 1..n loop
if C(i,j)=0 and C_cov(j)=0 and R_cov(i)=0 then
M(i,j):=1;
C_cov(j):=1;
R_cov(i):=1;
end if;
end loop;
end loop;
for i in 1..n loop
C_cov(i):=0;
R_cov(i):=0;
end loop;
step:=3;
end steptwo;

Step 3 - Cover each column containing a starred zero. If K columns are covered, the starred
zeros describe a complete set of unique assignments. In this case, Go to DONE, otherwise, Go
to Step 4. Once we have searched the entire cost matrix, we count the number of independent
zeros found. If we have found (and starred) K independent zeros then we are done. If not, we
proceed to Step 4.

procedure stepthree(step : in out integer) is


count : integer;
begin
for i in 1..n loop
for j in 1..n loop
if M(i,j)=1 then
C_cov(j):=1;
end if;
end loop;
end loop;
count:=0;
for j in 1..n loop
count:=count + C_cov(j);
end loop;
if count>=n then
step:=7;
else
step:=4;
352
end if;
end stepthree;

Step 4 - Find a noncovered zero and prime it. If there is no starred zero in the row containing this
primed zero, Go to Step 5. Otherwise, cover this row and uncover the column containing the
starred zero. Continue in this manner until there are no uncovered zeros left. Save the smallest
uncovered value and Go to Step 6.

In this step, statements such as "find a noncovered zero" are clearly distinct operations that
deserve their own functional blocks. We have decomposed this step into a main procedure and
three subprograms (2 procedures and a boolean function).

procedure stepfour(step : in out integer) is


row,col : integer;
done : boolean;
procedure find_a_zero(row,col : out integer) is
i,j : integer;
done: boolean;
begin
row:=0;
col:=0;
i:=1;
done:=false;
loop
j:=1;
loop
if C(i,j)=0 and R_cov(i)=0 and C_cov(j)=0 then
row:=i;
col:=j;
done:=true;
end if;
j:=j+1;
exit when j>n;
end loop;
i:=i+1;
if i>n then done:=true; end if;
exit when done;
end loop;
end find_a_zero;
function star_in_row(row : integer) return boolean is
tbool : boolean;
begin
tbool:=false;
for j in 1..n loop
if M(row,j)=1 then
tbool:=true;
end if;
end loop;
return tbool;
end star_in_row;
procedure find_star_in_row(row, col : in out integer) is
begin
col:=0;
for j in 1..n loop
if M(row,j)=1 then
col:=j;
end if;
end loop;
end find_star_in_row;

353
begin
done:=false;
while not(done) loop
find_a_zero(row,col);
if row=0 then
done:=true;
step:=6;
else
M(row,col):=2;
if star_in_row(row) then
find_star_in_row(row,col);
R_cov(row):=1;
C_cov(col):=0;
else
done:=true;
step:=5;
Z0_r:=row;
Z0_c:=col;
end if;
end if;
end loop;
end stepfour;

Step 5 - Construct a series of alternating primed and starred zeros as follows. Let Z0 represent
the uncovered primed zero found in Step 4. Let Z1 denote the starred zero in the column of Z0 (if
any). Let Z2 denote the primed zero in the row of Z1 (there will always be one). Continue until the
series terminates at a primed zero that has no starred zero in its column. Unstar each starred
zero of the series, star each primed zero of the series, erase all primes, and uncover every line in
the matrix. Return to Step 3. You may notice that Step 5 seems vaguely familiar. It is a verbal
description of the augmenting path algorithm (for solving the maximal matching problem) which
we discussed in Lecture 3. We decompose the operations of this step into a main procedure and
five relatively simple subprograms.

procedure stepfive(step : in out integer) is


count : integer;
done : boolean;
r,c : integer;
procedure find_star_in_col(c : in integer; r : in out integer)
is
begin
r:=0;
for i in 1..n loop
if M(i,c)=1 then
r:=i;
end if;
end loop;
end find_star_in_col;
procedure find_prime_in_row(r : in integer; c : in out integer)
is
begin
for j in 1..n loop
if M(r,j)=2 then
c:=j;
end if;
end loop;
end find_prime_in_row;
procedure convert_path is
begin
for i in 1..count loop
if M(path(i,1),path(i,2))=1 then

354
M(path(i,1),path(i,2)):=0;
else
M(path(i,1),path(i,2)):=1;
end if;
end loop;
end convert_path;
procedure clear_covers is
begin
for i in 1..n loop
R_cov(i):=0;
C_cov(i):=0;
end loop;
end clear_covers;
procedure erase_primes is
begin
for i in 1..n loop
for j in 1..n loop
if M(i,j)=2 then
M(i,j):=0;
end if;
end loop;
end loop;
end erase_primes;
begin
count:=1;
path(count,1):=z0_r;
path(count,2):=z0_c;
done:=false;
while not(done) loop
find_star_in_col(path(count,2),r);
if r>0 then
count:=count+1;
path(count,1):=r;
path(count,2):=path(count-1,2);
else
done:=true;
end if;
if not(done) then
find_prime_in_row(path(count,1),c);
count:=count+1;
path(count,1):=path(count-1,1);
path(count,2):=c;
end if;
end loop;
convert_path;
clear_covers;
erase_primes;
step:=3;
end stepfive;

Step 6 - Add the value found in Step 4 to every element of each covered row, and subtract it from
every element of each uncovered column. Return to Step 4 without altering any stars, primes, or
covered lines. Notice that this step uses the smallest uncovered value in the cost matrix to
modify the matrix. Even though this step refers to the value being found in Step 4, it is more
convenient to wait until you reach Step 6 before searching for this value. It may seem that since
the values in the cost matrix are being altered, we would lose sight of the original problem.
However, we are only changing certain values that have already been tested and found not to be
elements of the minimal assignment. Also, we are only changing the values by an amount equal
to the smallest value in the cost matrix, so we will not jump over the optimal (i.e. minimal
assignment) with this change.

355
procedure stepsix(step : in out integer) is
minval : integer;
procedure find_smallest(minval : out integer) is
begin
minval:=integer'last;
for i in 1..n loop
for j in 1..n loop
if R_cov(i)=0 and C_cov(j)=0 then
if minval>C(i,j) then
minval:=C(i,j);
end if;
end if;
end loop;
end loop;
end find_smallest;
begin
find_smallest(minval);
for i in 1..n loop
for j in 1..n loop
if R_cov(i)=1 then
C(i,j):=C(i,j)+minval;
end if;
if C_cov(j)=0 then
C(i,j):=C(i,j)-minval;
end if;
end loop;
end loop;
step:=4;
end stepsix;

The implementation of Munkres' Assignment Algorithm is a challenging task, but the use of good
programming practice and structured programming methods reduces the level of complexity. Any
stepwise algorithm can be implemented using the while_loop and case statement construct
demonstrated in this example.

done:=false;
stepnum:=1;
while not(done) loop
case stepnum is
when 1 => step1(stepnum);
when 2 => step2(stepnum);
when 3 => step3(stepnum);
when 4 => step4(stepnum);
when 5 => step5(stepnum);
when 6 => step6(stepnum);
when others => done:=true;
end case;
end loop;

356
Exercises

15.1 Based on the definition of graph algorithm given in this chapter, briefly describe and sketch
the graph corresponding to each of the following:

a. Faculty, Students and Courses in a schedule


b. The possible paths in a simple maze
c. The few square blocks of city streets in which all the streets have one-way traffic
d. The possible routes in a road atlas from a starting city to a destination city

15.2 Explain why all connected graphs must have an even number of nodes of odd degree. (See
Konigsberg Bridge Problem).

15.3 Find a maximal matching of the bipartite graph below, starting with the initial matching
shown in bold.

A P

B Q

C R

D S

E T

15.4 Find a Hamiltonian Circuit for each of the graphs below, if one exists.

A A

G G
B E B E

H F H F

I J I J

C D C D
a. the pentagon b. the pentagram

15.5 Give a Euler Circuit for each of the graphs shown in Exercise 15.4 above, if any.

15.6 For a graph to have an Euler circuit all nodes must be of even degree. What are the
necessary conditions for a graph to have an Euler path (i.e. the starting and ending nodes may be
different). Hint: Create a few small graphs with and without Euler paths.

357
358
Chapter 16 - Numerical Algorithms

16.1 Introduction

16.2 Drawing the Graph of a Function – Graph_It

16.3 Finding a Zero of a Function – Bisection

16.4 Integrating a Function – Trapezoidal Rule and Simpson’s Rule

359
Chapter 16 - Numerical Algorithms
16.1 Introduction

Almost every topic that you have considered in this textbook so far has dealt with discrete data.
That is, data that assumes discrete values that can be represented by either enumerated types or
by integers. This is not necessarily the case. Clearly, any of the data stored in arrays, graphs,
trees, and other data structures can be of floating point type as opposed to discrete types.
However, most of the algorithms involved do not really depend upon the continuous nature of real
numbers.

Recall that the mathematical concept of real numbers is as a continuum of values: between any
two real numbers there are infinitely many other real numbers. Section 7 of Chapter 2 provides
details of the internal representation of single-precision floating point types. The floating point
types in any computer are, at best, only a discrete model of the continuous mathematical
concept. Thus, computer implementations of numerical algorithms are inherently inaccurate.
This is due to the inherent inaccuracy of the data that the algorithms act upon. In contrast,
consider the non-numerical algorithms we have studied in previous chapters. Those non-
numerical algorithms produce a correct answer, but they can take a long time to finish their
computation. Computer implementations of numerical algorithms can, at best, provide a
reasonable estimate to the correct answer.

The primary purpose of numerical algorithms is to use a computer to approximate things that we
can not calculate analytically. For example, an analytical solution to the equation x =
2
2 is
x = ± 2 , but what is the numerical value of 2 ? We might remember that this value is
approximately 1.414, or we could use a calculator to take the square root of the value 2. There is
an algorithm to calculate square roots by hand that was used before the invention of the
calculator. Both the calculator and the hand-algorithm use numerical methods to approximate the
value of square roots. The fact is that some numbers, like 2 , cannot be expressed exactly with
a finite number of digits. Numerical algorithms deal with these types of numbers by using special
techniques to keep the value "acceptably innacurate".

Similarly, if an engineer could determine the value of a definite integral or solve a differential
equation analytically, then they should. Very often, however, the analytical solution is just not
possible. Also, frequently, the solution is possible but not feasible. For example, a problem may
be analytically solvable but just too big to complete by hand. The invention of the first computer
in this country was motivated by such a problem. The ENIAC was designed to more quickly
compute artillery tables used to determine range of fire for howitzers during WWII. It is in
situations such as these that other scientists rely upon computer scientists to help them.

The field of Numerical Analysis or Numerical Methods deals with the design, implementation, and
analysis of algorithms to solve mathematical problems that can not be solved effectively in any
other way. It is a very interesting blend of mathematics and computer science. Such algorithms
include methods for:
• Solving non-linear equations
• Solving systems of linear equations
• Determining eigenvalues and eigenvectors
• Interpolation and approximation of data sets
• Numerical integration and differentiation
• Solving differential equations
• Optimization

This chapter will present a useful computational tool to draw the graph of any bounded
mathematical function implemented as a function subprogram in Ada and an introduction to
algorithms for solving a non-linear equation and for approximation of a definite integral.

360
16.2 Drawing the Graph of a Function – Graph_It

When you are to sketch the graph of a function, the usual strategy is to calculate a table of values
of the function, plot these points in the X-Y plane, and “connect the dots” in some smooth,
intuitive way. This section presents the procedure Graph_It to draw the graph of a function. It
uses a “brute-force” method to draw the graph. Rather than calculate relatively few function
values and “connect the dots”, we calculate as many points as the chosen graphics resolution
can display and then plot them all. That way, no “connecting the dots” is necessary. The graph
is the collection of all of the dots.

The user provides the graph_it procedure with five input parameters:
• A, the beginning value for the independent variable X,
• B, the ending value for X
• X_Size, the width of the graphics window
• Y_Size, the height of the graphics window
• F, a pointer to the function to be graphed

The algorithm will create a list of y-values, yi, of the function for x values varying between A and B
in steps of size (B-A)/X_Size. This provides the collection of points to be plotted. However, the
y-values need to be scaled before they are plotted. The smallest y-value Y_Min should be plotted
at the bottom of the graphics window at y-coordinate 0, the largest y-value Y_Max should be
plotted at the top of the graphics window at y-coordinate Y_Size, and all of the rest of the values
should be graphed at the proportional locations between these extremes.

In the process of scaling, (yi - Y_min) is multiplied by the factor Y_Size/(Y_max – Y_min) and
rounded to the nearest integer. The procedure draws horizontal and vertical lines representing
the x-axis and y-axis. The Adagraph library will not show these lines if they are outside the visible
window and will not give any error messages. Finally, the procedure waits for a key to be
pressed so that the user can view the graph. The program below shows the graph_it procedure,
two functions that are to be graphed, and the main program that invokes the graph_it procedure
for each function.

with adagraph, ada.text_io, ada.integer_text_io, ada.float_text_io,


ada.numerics.elementary_functions;
use adagraph, ada.text_io, ada.integer_text_io, ada.float_text_io,
ada.numerics.elementary_functions;

procedure graph_a_function is
x_size, y_size : positive;
a, b : float;

type function_pointer is access function (x: float) return float ;

procedure graph_it (a: in float; b : in float; x_size : in positive;


y_size : in positive; f : in function_pointer ) is

y : array (0 .. x_size) of float;


ymin, ymax, yscale : float;
output_a, output_b, output_ymax, output_ymin : string (1 .. 9);
dx : constant float := (b - a) / float (x_size);

begin
y(0) := f(a);
ymin := y(0);
ymax := y(0);
for i in 1..x_size loop
y(i) := f(a + float(i) * dx);
if y(i) > ymax then
ymax := y(i);

361
end if;
if y(i) < ymin then
ymin := y(i);
end if;
end loop;

-- these put statements place the floats into strings to be displayed


-- in the window title bar

put(output_a, a, 2,3,0);
put(output_b, b, 2,3,0);
put(output_ymax, ymax, 2,3,0);
put(output_ymin, ymin, 2,3,0);
open_graph_window(x_size, y_size);
set_window_title("Graph_It: A=" & output_a &", B="
& output_b & ", Minimum Y="
& output_ymin & ", Maximum Y=" & output_ymax);

if ymax /= ymin then


yscale := float(y_size)/(ymax-ymin);
for i in 0..x_size loop
put_pixel( i, integer( (y(i)-ymin)*yscale ) );
end loop;
draw_line( 0, integer( -ymin*yscale ), x_size,
integer( -ymin*yscale) ); -- Draw the X-axis
else --Function is constant, graph is horizontal line
display_text(1, y_size/2, "Graph is the horizontal line Y ="
& output_ymax);
end if;

draw_line( integer( -a/(b-a)*float(x_size) ), 0,


integer( -a/(b-a)* float(x_size) ), y_size); --Draw the Y-axis
wait_for_key;
close_graph_window;
end graph_it;

function sine (x : float ) return float is


begin
return sin(x);
end sine;

function foo (x : float ) return float is


begin
return exp( -x ** 2 );
end foo;

begin -- main procedure

put( "Enter the Horizontal Window size (in pixels): ");


get( x_size );
put( "Enter the Vertical Window size (in pixels): ");
get( y_size );

a := -2.0 * ada.numerics.pi;
b := 2.0 * ada.numerics.pi;

graph_it(a, b, x_size, y_size, sine'access);

graph_it( -3.0, 3.0, x_size, y_size, foo'access);

end graph_a_function;
362
The graphs produced with X_Size = 600 and Y_Size = 400 are shown in Figure 16-1 and 16-2.

Figure 16-1: Example Graph of y=sin x Output from Graph_A_Function

2
Figure 16-2: Example Graph of y = e − x Output from Graph_A_Function

363
16.3 Finding a Zero of a Function – Bisection

The Bisection algorithm is used to estimate a zero of a function. That is, a value of the
independent variable x, call it x0, such that x0 is sufficiently close to the value of xroot, where
f(xroot)=0. Given a (continuous) function f(x) and an interval [A, B] with the property that f(A) and
f(B) differ in sign (i.e., f(A) is positive and f(B) is negative or vice versa), the algorithm
successively bisects the interval, replacing one endpoint or the other with the midpoint, so that
each new interval is half as wide as the predecessor and still keeps the root bracketed. Thus, it
generates a sequence of intervals that converge toward the value of xroot. It is similar in logic to
the Binary Search algorithm applied to a sorted array, discussed in Chapter 13. The parameters
of the Bisection algorithm are:
• Left, the left endpoint of the bracketing interval
• Right, the right endpoint of the interval
• Epsilon, the desired maximum width of the final bracketing interval
• Func, a pointer to the function whose root is sought
• Max_Iterations, the maximum number of iterations to be executed
• Answer, the best approximation to the root
• Debug, a Boolean indicating whether intermediate output is desired
2
The program below uses the Bisect procedure to find the root of the polynomial x – 2 between 1
and 2, i.e. 2.
with ada.text_io, ada.integer_text_io, ada.float_text_io;
use ada.text_io, ada.integer_text_io, ada.float_text_io;

procedure find_a_zero is
type function_pointer is access function (x: float) return float;

sqrt2 : float;

procedure bisect( left, right, epsilon : in float;


func : function_pointer; max_iterations : natural;
answer : out float; debug : boolean := false )is

a, b, f_at_a, f_at_c :float;

begin
a := left;
b := right;
f_at_a := func(a);
answer := (a + b) / 2.0;

if debug then
put("Iteration: ");
put( 0, 1);
put(" Answer= ");
put(answer, 3, 7, 3);
new_line;
end if;

for k in 1.. max_iterations loop


f_at_c := func(answer);

if f_at_c = 0.0 then


return;
elsif f_at_a * f_at_c < 0.0 then -- Root between A and Answer
b := answer; -- Replace right endpoint with midpoint

364
else -- Root is between Answer and B
a := answer; -- Replace left endpoint with midpoint
f_at_a := f_at_c;
end if;

answer := (a+b) / 2.0;

if debug then
put("Iteration: ");
put( k, 1);
put(" Answer= ");
put(answer, 3, 7, 3);
new_line;
end if;

if abs (b-a) < epsilon then


return;
end if;

end loop;

end bisect;

function foo( x : float ) return float is


begin
return x ** 2 - 2.0;
end foo;

begin
bisect( 1.0, 2.0, 1.0e-6, foo'access, 30, sqrt2, true);
put("Bisect returns ");
put( sqrt2, 3,6,0);
new_line;
end find_a_zero;

The output produced by this program is:

Iteration: 0 Answer= 1.5000000E+00


Iteration: 1 Answer= 1.2500000E+00
Iteration: 2 Answer= 1.3750000E+00
Iteration: 3 Answer= 1.4375000E+00
Iteration: 4 Answer= 1.4062500E+00
Iteration: 5 Answer= 1.4218750E+00
Iteration: 6 Answer= 1.4140625E+00
Iteration: 7 Answer= 1.4179688E+00
Iteration: 8 Answer= 1.4160156E+00
Iteration: 9 Answer= 1.4150391E+00
Iteration: 10 Answer= 1.4145508E+00
Iteration: 11 Answer= 1.4143066E+00
Iteration: 12 Answer= 1.4141846E+00
Iteration: 13 Answer= 1.4142456E+00
Iteration: 14 Answer= 1.4142151E+00
Iteration: 15 Answer= 1.4141998E+00
Iteration: 16 Answer= 1.4142075E+00
Iteration: 17 Answer= 1.4142113E+00
Iteration: 18 Answer= 1.4142132E+00
Iteration: 19 Answer= 1.4142141E+00
Iteration: 20 Answer= 1.4142137E+00
Bisect returns 1.414214

365
16.4 Integrating a Function – Trapezoidal Rule and Simpson’s Rule

The final application demonstrated in this chapter is numerical integration. Recall that the definite
integral can be interpreted as the “net” area between the graph of the function y = f(x) and the x-
axis. That is, we consider areas above the x-axis as positive and areas below the x-axis as
negative. Two widely used algorithms are the composite Trapezoidal Rule and the composite
Simpson’s Rule. The parameters to both procedures are:
• A, the lower limit of integration
• B, the upper limit of integration
• F, a pointer to the integrand function
• N, the number of subdivisions of the interval (should be even for Simpson’s Rule)
• Answer, the calculated estimate of the definite integral

The trapezoidal rule divides the interval [A, B] into N equal subintervals, calculates the area of a
trapezoid on each subinterval, and then adds all of these areas together to determine an
approximation for the value of the integral. Simpson's rule also divides the interval into N equal
subintervals, but N must be an even integer. It uses the subintervals in pairs, calculates the area
under a parabola on each pair of subintervals, and then adds these areas together to determine
an approximation for the integral. Simpson's Rule just happens to always produce the exact
rd
answer for polynomials of 3 degree or less.
1
The following program uses both methods to calculate approximations to x 3 dx , doubling the
0
number of subdivisions of the interval each time through the main loop.

with ada.text_io, ada.integer_text_io, ada.float_text_io;


use ada.text_io, ada.integer_text_io, ada.float_text_io;

procedure integration is

type function_pointer is access function ( x: float ) return float;


trapezoidal, simpsons : float;

procedure trapezoid( a, b : in float; f: in function_pointer;


n : in positive; answer: out float) is
h, sum : float;
begin
h := (b - a) / float(n);
sum := 0.0;

for k in 1..n-1 loop


sum := sum + f( a + float(k) * h );
end loop;

answer := h / 2.0 * (f(a) + f(b) + 2.0*sum);

end trapezoid;

procedure simpson( a, b : in float; f: in function_pointer;


n : in positive; answer: out float) is
limit : positive := n;
sum1, sum2, h : float;
begin

-- N and Limit should be even, add 1 if not


if limit mod 2 = 1 then
limit := limit + 1;
end if;

366
h := (b - a) / float( limit );
sum1 := 0.0;
sum2 := 0.0;

for k in 0..limit/2-1 loop


sum1 := sum1 + f( a + float(2*k+1) * h );
end loop;

for k in 1..limit/2-1 loop


sum2 := sum2 + f( a + float(2*k) * h );
end loop;
answer := h/3.0 * ( f(a) + f(b) + 4.0 * sum1 + 2.0 * sum2);

end simpson;

function cube( x : float ) return float is


begin
return x ** 3 ;
end cube;

begin -- main procedure

put_line("Subdivisions Trapezoid Simpsons");


for k in 1..10 loop
trapezoid(0.0, 1.0, cube'access, 2**k, trapezoidal);
simpson(0.0, 1.0, cube'access, 2**k, simpsons);
put(2**k, 8);
set_col(16);
put(trapezoidal, 2,7,0);
set_col(31);
put(simpsons, 2,7,0);
new_line;
end loop;

end integration;

The output produced by this program is:

Subdivisions Trapezoid Simpsons


2 0.3125000 0.2500000
4 0.2656250 0.2500000
8 0.2539063 0.2500000
16 0.2509766 0.2500000
32 0.2502441 0.2500000
64 0.2500610 0.2500000
128 0.2500153 0.2500000
256 0.2500038 0.2500000
512 0.2500010 0.2500000
1024 0.2500002 0.2500000

367
Exercises

Graph_It:

16.1 Graph the function defined by F(x) = 0 if |x| < 0.1 and F(x) = 1/x, otherwise, on the interval
[-2, 2]. Experiment with various values of X_Size and Y_Size. What happens if you change 0.1
to 0.01? What about 0.001?

16.2 Modify Graph_It to draw the graphs of several functions all in the same graph window. You
will need to use an array of function pointers.

16.3 Modify Graph_it to draw a “scatter plot” of an array of points in the X-Y plane.

Bisect:

16.4 The bisection algorithm requires that the function be continuous and the initial search
interval [A,B] includes a root of the function. What happens if the initial interval [A,B] does not
bracket the root? What happens if the function is not continuous?

16.5 Use Bisect to find all the solutions to tan x = x, for x in [0, 15]. Hint: Consider f(x) = x – tan x
and sketch the graph of this function, carefully. Watch out for places where f(x) is not continuous.

Integration:

16.6 Use Simpson’s rule to approximate the integral from 0 to 2π of sin x and cos x.

16.7 Use Simpson’s rule to generate a table of values for the function erf(x)

2 x 2
erf ( x ) = e −t dt
π 0

16.8 Use your solution to Exercise 16.7 to draw a graph of erf(x) for x in [0, 3]

16.9 Use your solution to Exercise 16.8 to find a value of x so that erf(x) = 0.95.

368
Table of ASCII Symbols and Control Characters

The American Standard Code for Information Interchange (ASCII) provides a numeric value for
each printable symbol as well as many non-printable (control) characters. This code was
established in 1967 and has been used to encode and decode textual information between
computers and other machines. Many of the control characters are obsolete since they only have
meaning for mechanical printing displays. The table below gives the decimal value
corresponding to each character and a description of the control characters used to format the
display of the printable characters.

Value Symbol/Command Description

000 NUL (null char.)


001 SOH (start of header)
002 STX (start of text)
003 ETX (end of text)
004 EOT (end of transmission)
005 ENQ (enquiry)
006 ACK (acknowledgment)
007 BEL (bell)
008 BS (backspace)
009 HT (horizontal tab)
010 LF (line feed)
011 VT (vertical tab)
012 FF (form feed)
013 CR (carriage return)
014 SO (shift out)
015 SI (shift in)
016 DLE (data link escape)
017 DC1 (XON) (device control 1)
018 DC2 (device control 2)
019 DC3 (XOFF)(device control 3)
020 DC4 (device control 4)
021 NAK (negative acknowledgement)
022 SYN (synchronous idle)
023 ETB (end of transmission block)
024 CAN (cancel)
025 EM (end of medium)
026 SUB (substitute)
027 ESC (escape)
028 FS (file separator)
029 GS (group separator)
030 RS (request to send)
031 US (unit separator)
032 SP (' ' space)
033 ! (exclamation mark)
034 " (double quote)
035 # (number sign)
036 $ (dollar sign)
037 % (percent)
038 & (ampersand)
039 ' (single quote)
040 ( (left/opening parenthesis)
041 ) (right/closing parenthesis)
042 * (asterisk)
043 + (plus)
044 , (comma)
045 - (minus or dash)
046 . (dot)

369
047 / (forward slash)
048 0
049 1
050 2
051 3
052 4
053 5
054 6
055 7
056 8
057 9
058 : (colon)
059 ; (semi-colon)
060 < (less than)
061 = (equal sign)
062 > (greater than)
063 ? (question mark)
064 @ ('at' symbol)
065 A
066 B
067 C
068 D
069 E
070 F
071 G
072 H
073 I
074 J
075 K
076 L
077 M
078 N
079 O
080 P
081 Q
082 R
083 S
084 T
085 U
086 V
087 W
088 X
089 Y
090 Z
091 [ (open square bracket)
092 \ (back slash)
093 ] (close square bracket)
094 ^ (caret)
095 _ (underscore)
096 ` (grave accent)
097 a
098 b
099 c
100 d
101 e
102 f
103 g
104 h
105 i
106 j
107 k
370
108 l
109 m
110 n
111 o
112 p
113 q
114 r
115 s
116 t
117 u
118 v
119 w
120 x
121 y
122 z
123 { (open brace)
124 | (vertical bar)
125 } (close brace)
126 ~ (tilde)
127 DEL (delete)

371
ADA.NUMERICS.ELEMENTARY_FUNCTIONS Package Specification

The ADA.NUMERICS.ELEMENTARY_FUNCTIONS package gives us a library of mathematical


functions for the standard FLOAT data type. For more information about this package and its
use, you should refer to the Ada Language Reference Manual (LRM) provided as part of the
Ada_GIDE.

0001 -- Filename ADA.NUMERICS.ELEMENTARY_FUNCTIONS.


0002 -- Ada 95 standard package supplying mathematical and
0003 -- trigonometric functions for the pre-declared FLOAT type.
0004
0005 package ADA.NUMERICS.ELEMENTARY_FUNCTIONS is
0006
0007 -- Square root.
0008 function SQRT (X : in FLOAT) return FLOAT;
0009
0010 -- Natural Logarithm and Logarithm to given base.
0011 function LOG (X : in FLOAT) return FLOAT;
0012 function LOG (X, BASE : in FLOAT) return FLOAT;
0013
0014 -- Exponent to base e and exponentiation.
0015 function EXP (X : in FLOAT) return FLOAT;
0016 function "**" (LEFT, RIGHT : in FLOAT) return FLOAT;
0017
0018 -- Trigonometric functions.
0019 function SIN (X : in FLOAT) return FLOAT;
0020 function SIN (X, CYCLE : in FLOAT) return FLOAT;
0021 function COS (X : in FLOAT) return FLOAT;
0022 function COS (X, CYCLE : in FLOAT) return FLOAT;
0023 function TAN (X : in FLOAT) return FLOAT;
0024 function TAN (X, CYCLE : in FLOAT) return FLOAT;
0025 function COT (X : in FLOAT) return FLOAT;
0026 function COT (X, CYCLE : in FLOAT) return FLOAT;
0027 function ARCSIN (X : in FLOAT) return FLOAT;
0028 function ARCSIN (X, CYCLE : in FLOAT) return FLOAT;
0029 function ARCCOS (X : in FLOAT) return FLOAT;
0030 function ARCCOS (X, CYCLE : in FLOAT) return FLOAT;
0031
0032 function ARCTAN(Y : in FLOAT; X : in FLOAT := 1.0) return FLOAT;
0033 function ARCTAN(Y : in FLOAT; X : in FLOAT := 1.0;
0034 CYCLE : in FLOAT) return FLOAT;
0035
0036 function ARCCOT (X : in FLOAT; Y : in FLOAT := 1.0) return FLOAT;
0037 function ARCCOT (X : in FLOAT; Y : in FLOAT := 1.0;
0038 CYCLE : in FLOAT) return FLOAT;
0039
0040 function SINH (X : in FLOAT) return FLOAT;
0041 function COSH (X : in FLOAT) return FLOAT;
0042 function TANH (X : in FLOAT) return FLOAT;
0043 function COTH (X : in FLOAT) return FLOAT;
0044 function ARCSINH (X : in FLOAT) return FLOAT;
0045 function ARCCOSH (X : in FLOAT) return FLOAT;
0046 function ARCTANH (X : in FLOAT) return FLOAT;
0047 function ARCCOTH (X : in FLOAT) return FLOAT;
0048
0049 end ADA.NUMERICS.ELEMENTARY_FUNCTIONS;

372
RANDOM Package Specification and Body

This text discusses the generation of pseudo-random sequences of numbers. The following
package specification and body are provided to illustrate the methods used to generate random
sequences with a variety of distributions. The method of generating the uniform (Ranu) and the
Gaussian or normal (Rang) distributions is based on algorithms presented in The Handbook of
1
Mathematical Functions, by Abramowitz and Stegun.

----------------------------------------------------------------------------
-- --
-- Specification for package RAND --
-- --
-- Function Description --
-- --
-- RANU - uniform random floating point values in (0.0..1.0) --
-- RND(M) - uniform random integer values in (0..M) --
-- RNDL(M) - uniform random long-integer values in (0..M) --
-- RANG - normally distributed floating point values with --
-- mean = 0.0 and std. deviation = 1.0 --
-- REXP(M) - exponential distributed floating point values with --
-- mean = M --
-- --
-- Computer Science and Information Systems Department --
-- Murray State University, P.O. Box 9, Murray KY 42071 --
-- (bug fix in RND(M) by Jason Taylor, clock seed) --
----------------------------------------------------------------------------
with ada.calendar;

package random is

function Ranu return Float;


function Rnd(Maxint : Integer) return Integer;
function Rndl(Maxlint : Long_Integer) return Long_Integer;
function Rang return Float;
function Rexp(Mean : Float) return Float;

private

Seed : Long_Integer :=
long_integer(float(ada.calendar.seconds(ada.calendar.clock))*1000.0);
Maxi : Long_Integer := Long_Integer'Last;

end random;

----------------------------------------------------------------------------
-- RANDOM Package Body --
----------------------------------------------------------------------------

with Ada.Numerics.Elementary_Functions;
use Ada.Numerics.Elementary_Functions;

package body random is

function Ranu return Float is

A : constant Long_Integer := 16_807;


M : constant Long_Integer := 2_147_483_647;
Q : constant Long_Integer := 127_773;
R : constant Long_Integer := 2_836;

begin

Seed :=A*(Seed mod Q) - R*(Seed/Q);


if Seed <=0 then
Seed:=Seed+M;
end if;

373
return Float(Seed)/Float(Maxi);

end Ranu;

function Rnd(Maxint : Integer) return Integer is


begin
return Integer(Float(Maxint+1)*Ranu-0.5);
end Rnd;

function Rndl(Maxlint : Long_Integer) return Long_Integer is


begin
return Long_Integer(Float(Maxlint)*Ranu);
end Rndl;

function Rang return Float is

Py : constant Float := 3.1415926;


R1,R2 : Float;

begin
R1:=Ranu;
R2:=Ranu;
return Float(Sqrt(-2.0*Log(R1))*Cos(2.0*Py*R2));
end Rang;

function Rexp(Mean : float) return float is


t : integer;
n : integer:=0;
u,v : float;
done : boolean:=false;
begin
t:=0;
loop
u:=ranu;
v:=ranu;
n:=1;
if v<u then
while v<u loop
v:=v+ranu;
n:=n+1;
end loop;
if n=2*(n/2) then
t:=t+1;
else
done:=true;
end if;
else
done:=true;
end if;
exit when done;
end loop;
return mean*(float(t)+u);
end rexp;
end random;

1
The Handbook of Mathematical Functions, Abramowitz and Stegun, www.math.sfu.ca/~cbm/aands/

374
Glossary

abstract data type (ADT) - A set of data objects and an associated collection of operations on
those objects. Usually the values of the data objects in an ADT can only be modified through use
of operations associated with the ADT.

access time - the elapsed time between a data request from secondary storage and its being
available for processing.

actual parameter - The name or expression association with a formal parameter in a subprogram
call or in a generic instantiation.

address - an integer value representing the location of a unit of data in a computer's memory

algorithm - the description of a step-by-step process for solving a problem or performing some
action. Each step of a computer algorithm must be clear and concise enough to be translated
into some computer language.

alphanumeric - Refers to the uppercase and lowercase letters A..Z and a..z and the digits 0..9.

ALU - (Arithmetic Logic Unit) The unit of a Von Neumann architecture computer responsible for
performing arithmetic and logical operations on data values.

analog - As form of information or signal in which there is a direct correspondence between the
analog value and some physical condition. For example a voltage level from a sensor which
corresponds to a temperature would be an analog signal. (see also analog to digital converter)

analog to digital converter - (A/D converter) A device or a method for converting an analog signal
level into a numeric value. A/D converters are used to convert measurements of the real-world
into data that can be processed by a computer.

ANSI - (American National Standards Institute) This is an organization charged with maintaining
computer language and data representation standards for the computer industry.

application error - A type of error in a computer program in which the algorithm or process
intended is not properly implemented. A program with an application error is syntactically correct
and will run to completion normally, however, in one or more possible data conditions will not give
the correct result. (see also GIGO).

argument - A variable to which a data value may be assigned, such as the values being passed
into a function.

array - A group of identifiers given a common name and of the same type. The individual
identifiers in an array are accessed with an array subscript. For example, the array S below
consists of 100 integers. The 42nd integer identifier in the array S is represented by S(42).

S : array (1..100) of integer;

array length - The number of individual identifiers of the specified type in an array.

artificial intelligence (AI) - An area of computer science in which computers are designed and
programmed to learn and/or adapt to new situations. The term AI is somewhat out of date, and
has been replaced with more specific terms such as expert systems, natural language
understanding systems and automated reasoning. A somewhat cynical definition of AI is
"anything that a computer can't do yet".

ASCII - (American Standard Code for Information Interchange) The most popular code for
defining characters (letters, digits and special symbols) for display and textual data exchange
between computers. The standard ASCII character set uses seven bits but ASCII data is

375
commonly held in bytes (i.e. 8 bit binary values) with the leading bit set to 0 (i.e.
00000000..011111111). Some important ASCII values for programmers to remember are,

Decimal Value Binary Value Hexadecimal Value Character


32 00100000 20 blank space
48 00110000 30 0 (zero digit)
65 01000001 41 A
97 01100001 61 a

assembler - A program that translates an assembly language program into machine language.

assembly language - A low-level programming language with statements that are human-
readable but have a one-to-one correspondence with machine language statements.

assignment statement - A type of statement in a programming language in which a value, or the


result of an expression is placed into a variable identifier's allocated memory location. Typically
the name of the identifier is on the left side of an assignment operator, and the expression or
value being placed into the identifier's memory location is on the right side of the assignment
operator. For example,

X := 42.1;

Y := sqrt( X*X - 1.0);

are assignment statements in which X is assigned the value 42.1 and then Y is assigned the
result of the indicated computation.

attribute - An operator that returns a characteristic of an identifier. We can use the 'pos and 'val
attributes of the character type to convert between characters and their corresponding ASCII
values. In the example code below the uppercase letter A and converted to a lowercase a.

chr := 'A'';
char_val := character'pos(chr);
chr := character'val(char_val + 32);

batch processing - A type of programming in which data input, output and processing does not
involve programmer interaction.

baud - Also called baud rate, this is the rate at which the state of a data line can be changed. For
serial data transfer the baud rate is the same as the data transfer rate measured in bits per
second.

binary - A base-two number system.

bit - A single digit in a base-two number system.

block - A group of data that are stored or transferred together. A block of code is functionally
related collection of statements in a programming language.

body - The set of statements within a loop, conditional construct, function, procedure or package.

Boolean - A data type that can take only the values True or False. A Boolean expression is any
assertion that can take the value True or False.

booting a computer - The process of loading the operating system into a computer's memory and
passing the flow of control to the operating system kernel.

bps - (bits per second) The unit of measurement for rate of digital data transfer.

376
branch - One of the possible alternatives for the flow of control in an executing computer
program. Typically used to refer to a subroutine call, (e.g. branch to subroutine). Also refers to
the return of flow of control to the top of a loop (e.g. branch-not-negative BNN START,X in which
a program will branch to the address labeled START if the value of X is not negative, otherwise
the program will continue to the next statement in the program.)

bug - An error is a computer program.

bus - A collection of wires over which data or instructions are transferred over a computer.

byte - A binary data word consisting of 8-bits.

card reader - An input device used with early computers for entering data and program
instructions from punch cards.

cathode ray tube (CRT) - A type of device in a computer monitor in which a beam of electrons
cause a two-dimensional array of phosphorous dots emit light to produce a visual pattern.

central processing unit (CPU) - The main electronic component in a computer system which
contains the arithmetic logic unit (ALU) the control unit, registers and other circuitry to perform the
fetch-execute cycle of a Von Neumann architecture computer.

character - Any symbol from the ASCII set.

code - Also called source code. The implementation of an algorithm in a computer language.

coding - The task of converting an algorithm into a particular programming language.

command language - A special language in which a computer operator can directly interact with
an operating system or other systems level program.

compilation - The conversion of a high-level language program into machine code.

compilation unit - Any portion of an Ada program that can be independently compiled.
Compilation units include main programs, packages and generics.

compiler - A translation program to convert a program written in a high-level language into


machine code.

computational constructs - The types of control structures provided in a computer language.


Most computer languages support the constructs (1) hierarchy, (2) sequence, (3) alternation, (4)
repetition, (5) concurrency, and (6) recursion.

computer - Also called a finite state machine, A computer is a device that can execute machine
code programs. A computer consists of a central processing unit, a memory unit, some type of
permanent storage device for holding data and programs, and input/output devices for interfacing
with the user.

computer program - The implementation of an algorithm in some particular computer language.

concatenation - The combining of two or more substrings of characters into a new string in which
one substring follows the other.

concurrent - Refers to the independent execution of more than one code block without regard for
the order of execution between the two blocks. Concurrent processes may execute in parallel,
however the computational construct of concurrency does not require parallel operation or
multiple CPUs. Ada provides for concurrent execution through a construct called tasking. (See
also parallel.)

377
conditional branch - A change in the normal flow of control in a computer program which is the
result of a particular value or range of values occurring in a conditional statement.

control statement - A type of statement of a computer language that is used to alter the normal
flow of control

control structure - Also called computational constructs, any statement or set of statements in a
computer program that is used to determine the order in which operations are performed.

control unit - A unit in a Von Neumann architecture computer responsible for decoding and
executing an instruction.

constant - An identifier that is given a value in the declaration block which cannot be altered
during program execution.

correlation - A connection relating two or more values. Also the level to which one value is
dependent upon another.

CPU - See central processing unit.

crash - A term to describe the non-normal termination of an executing computer program due to a
runtime error or hardware failure.

CRT - See cathode ray tube.

cursor - A symbol used to indicate the location of input to or output from a text file or IDE editor
screen.

data - Information provided in a machine readable form. Data can be represented in text files or
binary files or can be held in RAM during program execution.

data abstraction - The separation of the intrinsic properties of data from its implementation in a
computer. (see also abstract data type).

data base - An organized collection of data for computer storage and retrieval.

data base management system (DBMS) - A computer program that is used to access, and modify
a data base.

data structure - A description of the names and types of variable and constant identifiers used in
a computer program.

data type - A description of a size and encoding schemes of the identifiers used in a computer
program. Some data types are predefined in a computer language, such as integer, float and
character in Ada. Other data types are defined within the program itself, such as a constrained
string and structured data types.

DBMS - See data base management system.

debug - The task of finding and correcting the errors in a computer program.

declaration - A statement in a computer language that describes a variable or constant identifier.

device independence - The property of a computer program that permits its implementation and
execution on different platforms and under different operating systems.

digital - Data or signals that have been converted to binary values for storage and manipulation
by a computer.

digitize - The process of converting signals or other real-world information into binary values.
378
discrete - Not continuous. The property of a data or physical quantities that vary in quantized
steps.

discrete data types - Types of data that can take only particular values. Discrete data types
include integers, characters and Boolean types.

disk operating system (DOS) - An operating system for which the disk is the primary storage
device. DOS commands permit the computer operator to read, write, create, delete and manage
disk files.

DOS - See disk operating system.

driver - A program or set of executable software components that support the interface of some
peripheral device with the computer.

EBCDIC (Extended Binary Coded Decimal Interchange Code) - A standard encoding scheme for
representing data in larger computers. This encoding standard has been replaced by ASCII in
large part.

editor - A computer program that permits the user to create and modify text files usually for the
purpose of writing computer programs.

enumeration data type - An Ada data type in which a list of n user-defined names are ordered
and associated with the integer values 0..n-1.

event-controlled loop - A loop in a computer program which is exited when some event occurs.

event-counter - A variable that is incremented each time some event occurs.

event-driven program - A program in which the flow of control is determined by internally


generated events such as data values falling within some specified range or by external I/O
events, such as mouse actions or keyboard entries. Computer games are examples of event-
driven programs.

exception - The result of some event in an Ada program that disrupts the normal operation of the
program. If the proper exception handler has been implemented, the flow of control will be
transferred to this code segment when an exception is raised.

exception handler - A code segment in an Ada program that is executed when an exception
occurs.

execute - To run a computer program.

exponent - The power to which the mantissa is raised in a floating point number. Also the power
of 10 in a number written in scientific notation. The digit 2 is the exponent in the example below,
while the value 4.56789 is the mantissa. (See also mantissa.)
2
456.789 = 4.56789 x 10

expression - A statement describing one or more arithmetic operations with constant or variable
identifiers.

fetch and execute cycle - The process of moving an instruction from memory, into the CPU and
then performing operations indicated by the instruction.

field - One of the data elements of a record.

file - A collection of data placed into secondary storage and given a name for its access.

379
file data type - The data type in a computer language used to designate and access a file in
secondary storage.

file server - A computer used primarily for providing remote access to data files.

file terminator - A special symbol used to indicate the end of a data file. In Ada the file terminator
for text files can be recognized by a Boolean function end_of_file( ).

finite state machine - A computing device or a mathematical description of a computing device


which executes implementations of computer algorithms. The term finite state refers to the fact
that the machine can be in only a finite number of distinct states. (See also, computer.)

floating-point type - A data type used to hold real numbers (i.e. values that can contain a whole
number part and a fractional part and include a decimal point).

flow of control - The order of execution of statements in a computer program.

formal parameter - A parameter declared in a function or procedure specification. (see also actual
parameter).

formatting - The arrangement of a program output by specifying the number of spaces to be used
to display numeric values and other data types.

FORTRAN - An early programming language whose name is short for FORmula TRANslation.

function - A subprogram which is called from within an expression. The function has a particular
return type and is replaced in the expression with its return value.

function call - The invocation of a function subprogram from a main procedure or another
subprogram.

generic - An Ada code block in which one or more data types are left unspecified until the generic
is instantiated during program compilation.

global parameter - An identifier whose scope is the main program and all subprograms.

GIGO - (Garbage In, Garbage Out) A computer jargon reference to a computer program or its
associated input data set which contains errors.

GNU/LINUX - A free operating system and kernel based on the UNIX operating system.

hardware - The electronics and other physical components of a computer system.

heuristic - A type of program description in which there is no guarantee that a correct result will be
obtained.

hexadecimal - A base-16 value. Base-2 or binary values can be converted into hexadecimal
values by dividing the binary value into groups of four bits (starting at the least significant bit) and
replacing each group with the corresponding hexadecimal digit. Hexadecimal is used to more
compactly represent binary data. (See also octal.)

0 0000 10010101000101111110110 base 2 value


1 0001 100 1010 1000 1011 1111 0110
2 0010 4 A 8 B F 6
3 0011 4A8BF6 base 16 value
4 0100
5 0101
6 0110
7 0111
8 1000
380
9 1001
A 1010
B 1011
C 1100
D 1101
E 1110
F 1111

hierarchy - A computational construct of a programming language which permits modular


implementations of increasing levels of complexity.

high-level language (HLL) - A human readable language in which a single statement can
represent many assembly language or machine language statements. Ada, C++ and Visual
Basic are a few examples of high-level languages.

infinite loop - A repetition construct for which there is no loop termination condition or in which
the loop termination condition will never occur.

infinite recursion - A type of recursive program that continues to call itself never reaching a
termination condition.

information hiding - The property of an abstract data type that permits the management of data
structures without the need to deal with the details of specific elements of the data structure.

in mode - The mode of an Ada file type or parameter used for passing data into a procedure or
program.

in out mode - The mode of an Ada procedure parameter used for passing data into and out of a
procedure.

instantiation - The specification of the data type or types in a generic.

integer - A discrete numeric data type limited to whole numbers.

integrated development environment (IDE) - A program that combines a text editor, compiler,
linker and debugger for the development of computer programs.

interactive program - A computer program that accepts and responds to user input and provides
user output during execution.

I/O - Input and Output. A term referring to the transfer of data into and out of a computer
program.

interface - A mechanism for connecting two computers to each other or permitting a user to
interact with a computer.

interpreter - A program that converts and executes the statements in a high-level language into
machine code, one line at a time. (compare with compiler).

iteration - A single pass through the body of a loop or code segment.

kernel - The portion of the operating system that is essential for proper operation of the computer.
The kernel remains in active memory so long as the computer is operational.

language processor - Any program that converts one programming language into another.
Typically a language processor converts a source program into machine readable form.

library unit - The body of a main procedure or subprogram that has no declaration component.

381
limited private type - A data used in a package that cannot be accessed by the main program that
is using the package.

line terminator - A special symbol that indicates the end of a line of text in a text file. In Ada the
file terminator can be recognized by a Boolean function end_of_line( ).

linker - A program that combines library units and other program components into an executable
program.

listing - A copy or a display of the source code of a computer program.

literal - An actual value of any data type written directly into a program.

local variable - A variable whose scope is limited to a subprogram or block of code. Variables
declared in a subprogram are local variables.

loader - An operating system program that moves a stored program into memory and prepares it
for execution.

logic error - An error in a computer program that is due to an incorrect algorithm. Logic errors
include missing steps in an algorithm, unnecessary or incorrect steps or steps out of order. (see
also application error).

logical operator - Operators representing operations on Boolean data types. Logical operators
include AND, OR, and NOT in the Ada programming language.

loop - The implementation of the repetition construct in a programming language.

loop body - The block of statements that are executed in program loop.

loop exit - The method used to terminate a loop. In Ada the generic loop uses the exit when
conditional as a loop exit. In the example code segment below values are summed in the loop.
When a negative value of X is entered, the flow of control exits the loop and the average is
computed.

sum:=0;
n:=0;
loop
get(X);
exit when X<0; -- loop exit, negative X is a sentinel value
n:=n+1;
sum:=sum+X;
end loop;
avg:=float(sum)/float(n); -- next statement executed upon exit

loop parameter - The iteration counter for a for...loop. In the example Ada code below, i is the
iteration counter. As shown in this example the loop parameter can be used in arithmetic
expressions and as an array index. The scope of the loop parameter in Ada is limited to the loop
body in which it is defined.

for i in 1..n loop


S(i):= i*i +1;
end loop;

low-level programming language - A programming language with statements that perform


operations similar to those in a machine language statement. (see also assembly language).

machine language - The language that is directly readable (executable) by a computer. A


machine language program is written in binary. All computer programming languages must be
converted into machine language before they can be executed by a computer.
382
mantissa - The part of a floating point number that represents the precision of the number
separate from the exponent which represents power of the base. (See also exponent.)

mean - Another name for the average of a set of values.

median - The number that is closest to the middle of an ordered list of numbers.

memory - The part of a computer that holds programs currently being executed. When a program
is to be executed, the loader copies it from secondary storage (e.g. the hard drive) into memory.
Memory is active and can hold information only when the computer is operating. When the
computer is shut down or power is removed, the contents of memory is lost. In order to keep a
permanent record of the contents of memory it must be copied into some secondary storage.

memory unit - The component of a Von Neumann architecture computer that holds data and
programs in execution.

microcomputer - A small computer designed for a single user. Microcomputers include desktop
computers, notebook computers, handheld computers and some high-end personal digital
assistants (PDAs).

microcontroller - A small programmable processing device that is somewhat less capable than a
microcomputer's CPU but is designed to be used as a dedicated processor in an embedded
system. Some advanced microcontrollers such as the Basic Stamp, PICStik and Basic-X are
programmable in a high-level language.

mod - In Ada, mod is the function that implements the modulo operator. Given any integer a and
some positive integer n we say that a mod n is the value remaining when a is divided by n. For
example 5 mod 4 = 1 and 8 mod 4 = 0. We need to be careful when the argument a is negative.
The range of values output by (mod n) is [0..n-1] so the mod function never returns a negative
value. The graphical representation of (mod n) shown below illustrates this function. There are n
values on a (mod n) wheel, 0 through n-1. Starting with the pointer on 0, we compute the value of
a mod n by rotating the pointer clockwise through a steps on the mod n wheel. In the example
below we find that the value of 5 mod 4 equals 1. For negative values we rotate the pointer
counter-clockwise so 5 mod 4 gives the value 3.

0
0 0

3 1
3 1 3 1

2
2 2
-5 mod 4 = 3
mod 4 5 mod 4 = 1

Compare the mod function with the rem (remainder) function.

mode - The value that occurs most frequently in a set of numbers.

modem - Stands for MOdulator/DEModulator, a peripheral device that permits communications


between two or more computers though some analog interface. In a phone modem, digital words
are converted into audible tones (modulated), transmitted over the phone lines to another modem
and then converted back into digital words (demodulated) in remote computer.

module - A block of code that can be independently compiled and stored to be later combined
with other modules to make an executable program. In Ada modules are called units.
383
monitor - An output device such as a CRT to display textual and graphic information to the user.

named association - In Ada a type of association between actual and formal parameters in which
the formal parameter is followed by an arrow symbol => and then the actual parameter. Compare
named association with positional association.

nested control structure - The capability of a programming language to implement the


computational construct of hierarchy. In the example below, an if...then construct is nested inside
a case construct when choice = 1. Another way to nest control structures is through subroutine
calls such as when choice = 2.

case choice is
when 1 => if x>0 then
x:=x-1;
else
x:=x+1;
end if;
when 2 => update_X(x);
when others => null;
end case;

network - A group of computers and other peripheral devices such as routers connected by lines
of communication. The internet is the World's largest network of networks.

nibble - A nibble (sometimes spelled nybble) is a four-bit binary value or one-half a byte. A nibble
can be represented by a single hexadecimal digit.

null - Generally refers to nothing such as an empty set. In Ada the null statement is used as a
place-holder for a code block in a control structure and indicates that no operation is to be
performed.

null string - A string data type that contains zero characters.

object program - The machine code version of a computer program.

octal - A base-8 value. Base-2 or binary values can be converted into octal values by dividing the
binary value into groups of three bits (starting at the least significant bit) and replacing each group
with the corresponding octal digit. Octal is used to more compactly represent binary data. (See
also hexadecimal.)

0 0000 10010101000101111110110 base 2 value


1 0001 10 010 101 000 101 111 110 110
2 0010 2 2 5 0 5 7 6 6
3 0011 22505766 base 8 value
4 0100
5 0101
6 0110
7 0111

offline - Processing, data entry or program editing that occurs when on connected to the
computer.

online - A condition that refers to peripherals or other computers that are connected a computer
while it is in use.

opcode (operation code) - The portion of a machine language instruction that indicates what
operations are to be performed.

384
operand - The variable or constant identifiers being operated upon in an expression. In the
example below, the identifiers, PI and R are operands of this expression while Area is not an
operand.

Area := PI * R**2;

operating system - The software that makes the computer usable. The set of programs that
manage all of the resources of a computer. (See also GNU/LINUX, and kernel.)

operation - The result of the execution of any single machine language or high-level language
instruction in a computer. For machine language, the transfer of a value from memory into a
computer register is an operation. For a high-level language, incrementing a value such as
X:=X+1 is an operation. Note that this high-level language operation represents several machine
language operations.

ordering - Arranging a set of values into a specified sequence. We refer to the process of
arranging a set of numeric values into increasing or decreasing order as sorting. Sets of values
are held in computer memory in an array or list. The array index is used as a means of defining
an order for the values.

ordinal data type - See discrete data type.

out mode - The mode of an Ada file type or parameter used for passing data out of a procedure
or program.

overflow - Occurs when a value is too large to be held in its specified data type.

overloading - The use of a particular operator, function or procedure name for more than one
meaning. Overloading is an important capability in a programming language. In Ada any
ambiguities in meaning are resolved by comparing the number and type of operands or
parameters in the overloaded object. For example we use the same get( ) procedure call to get
integers and floats.

package - Used in Ada to encapsulate a logically related collection of data types, functions and
procedures that can be separately compiled and invoked in a main procedure by using the with
statement.

package body - The portion of a package that contains the code describing the operation of its
functions and procedures.

package specification - The portion of a package that describes the interface for its functions and
procedures but does not include their executable block.

paper-tape reader and punch - Input and output peripherals in early computer systems. These
components were also parts of a remote communication device called a teletype.

parallel - Referring to data communications, when more than one bit is transferred
simultaneously. Referring to processing, when more than one operation is performed
simultaneously. Parallel computation implies more than one CPU. (See also parallel.)

parameter list - The set of parameters used to communicate with a subprogram.

parity - One or more bits used to verify that data has been transmitted without errors. For single
bit parity checking, a one (1) or a zero (0) bit is appended to a transmitted word to maintain an
even number (or an odd number) of ones in the word. The computer receiving the data word
checks to see if the parity (evenness or oddness) is preserved.

path - The particular statements executed as the flow of control moves through an executing
program. In programs with many data dependent conditionals, it can be difficult or impossible to
test every path that can occur during program execution.
385
PC - Personal Computer.

peripheral - Any hardware device or component that enhances the performance or usefulness of
a computer system.

pixel - The smallest element of a digital image.

portability - As related to software, portability refers to the ability to compile and execute a
computer program on more than one platform and using more than one operating system.
Because of its close adherence to a standard, Ada is one of the most portable programming
languages in use.

positional association - In Ada a type of association between actual and formal parameters in
which the order of the formal parameters must match the order of the actual parameters.
Compare positional association with named association.

precedence rules - The rules built into a computer language that determine which operation is to
be performed next when an expression does not explicitly force a particular order (e.g. with the
use of parentheses).

precision - The number of significant digits in a floating point value.

private type - A structured data type declared in a package. The private type can be used in the
program that is using the package but the individual fields of the private type are not accessible
from the main program. (See also limited private type.)

procedure - A control structure that permits the replacement of a sequence of statements with a
single statement referred to as a procedure call.

procedure call - A statement in a main program or subprogram block of statements that transfers
the flow of control to the called subprogram. When this subprogram has completed, the flow of
control returns to the next statement in the calling block of statements.

process - A program in execution. A process is similar to a thread except that it has is memory
space and does not share data with any other process.

processor - See also, CPU.

program unit - In Ada, refers to a function, procedure, package or generic.

PROM - Programmable Read-Only Memory

prompt - A message provided by a program to let the user know what input is expected or to
provide an explanation of a program output.

propagation error - An error and its associated message generated by a compiler caused by an
actual error earlier in the program source code. During debugging the programmer should avoid
attempts to correct propagation errors since they will be eliminated when the actual syntax error
causing them is corrected. When in doubt, the programmer should only correct the first error
appearing the list of errors generated by the compiler and then recompile to verify that the
propagation errors have also been eliminated.

pseudocode - Not a real programming language, pseudocode is a readable description of an


algorithm that can be easily implemented in an actual programming language by an experienced
programmer. The creation of pseudocode is an important part of the software development
process.

RAM - Random Access Memory. This is the active memory of a computer.


386
random access - The ability to read and write words in memory or data in a file in any order.
Compare with text files in which the data must be read or written sequentially.

range - The limits on the set of all possible values between a first and a last value. In Ada a
range is defined by the notation,

first_value..last_value

and is used to define loop limits and array bounds as shown below:

type
listype is array(1..100) of integer;

for i in 1..n loop


-- loop body
end loop;

real memory - The RAM in a computer. Real memory also refers to the actual memory space
allocated to a virtual memory mapping system. (See also virtual memory.)

real number - Also called a float or floating-point number. This is a value that can include a whole
number portion, a fractional portion and a decimal point.

real time - When a program executes in conjunction with the occurrence of events in the real
world, the program is said to be running in real time. Examples of real time programs are
automatic flight control systems, traffic light controllers, automated manufacturing control systems
and computer games.

record - A structured data type that can contain field of different data types.

recursion - One of the computational constructs of a programming language. Recursion is


implemented by letting a subprogram call itself or a to call another subprogram that leads to a call
of this original subprogram.

recursive algorithm - An algorithm that includes one or more recursive calls. If properly formed a
recursive algorithm must include a termination condition that will eventually be reached in which a
recursive call is not made.

relational operator - An operator that is used to compare the magnitudes of numeric data types.

rem - This Ada function a rem n returns the remainder of dividing a by n. The rem function
returns the same value as the mod function for non-negative values of a. When a is negative,
however the rem function returns a negative value. We can compare mod and rem with a simple
Ada program such as the one provided below.

with ada.text_io, ada.integer_text_io;


use ada.text_io, ada.integer_text_io;
procedure mod_vs_rem is
begin
for i in -5..5 loop
put(i,2);
put(" mod 4 = ");
put(i mod 4,2);
put(" ");
put(i,2);
put(" rem 4 = ");
put(i rem 4,2);
new_line;
end loop;
387
end mod_vs_rem;

-5 mod 4 = 3 -5 rem 4 = -1
-4 mod 4 = 0 -4 rem 4 = 0
-3 mod 4 = 1 -3 rem 4 = -3
-2 mod 4 = 2 -2 rem 4 = -2
-1 mod 4 = 3 -1 rem 4 = -1
0 mod 4 = 0 0 rem 4 = 0
1 mod 4 = 1 1 rem 4 = 1
2 mod 4 = 2 2 rem 4 = 2
3 mod 4 = 3 3 rem 4 = 3
4 mod 4 = 0 4 rem 4 = 0
5 mod 4 = 1 5 rem 4 = 1

reserved word - A word that has a pre-defined meaning in a programming language. Reserved
words cannot be used as identifiers in a program.

return - The statement indicating the value to be returned by a function.

ROM - Read-Only Memory

runtime error - A logical error in a program that leads to the abnormal termination of the program
during execution. Compare with syntax error, logical error and application error.

scope - The block of statements in a program in which a particular identifier has meaning. The
scope of a variable can be global or limited to a particular subprogram or block of statements.

scripting - A scripting program is a type of program that generates an output which is another
programming language. Some Web scripting programs generate HTML code as an output.

secondary storage - A permanent storage device on a computer such as a hard drive. Before a
computer program can be executed it must be copied from secondary storage into memory by the
loader utility program.

semantics - The meaning of the statements in a computer language.

sentinel - Also called sentinel value. A special meaning given to a particular value or range of
values that are used to redirect the flow of control in a computer program. (See also, loop exit.)

sequential file - A type of file which can be accessed only in a particular order. A text file is an
example of a sequential file.

serial - A type of data transfer in which information is sent one bit at a time along a single signal
line.

short-circuit operator - A type of Boolean operator which stops the evaluation of a Boolean
expression as soon as the truth value of the expression is known. Short-circuit operators are
typically used to prevent run-time errors when parameters are out of range or values will result in
exceptions being raised.

if (x/=0 and then y/x > 1) then


val:=val+1;
end if;

if not (n in 1..10) or else (S(n)>0) then


pos_val:=S(n);
end if;

388
side effect - A change in the state of a system or in the value of an identifier that is not a defined
in the interface description. A common side effect occurs when a function changes the value of a
global identifier or generates an output in addition to returning it computed value. Generally side
effects are discouraged as bad programming practice.

significant digits - The digits in the mantissa of a floating point number that affect its value (not
leading or trailing zeros).

simulation - A computer program that models some real-world event or process. Simulations are
important to reduce the cost of testing for product development, as well as, reducing the risk to
human life and property.

software development life cycle - The phases of software development including problem
definition, algorithm design, program implementation and debugging, module verification, system
validation, and program documentation.

sorting - Arranging a set of numeric values into increasing or decreasing order.

source (source program or source code) - The implementation of an algorithm in a computer


language.

standard deviation - The range of values about the mean of a normal distribution in which 68% of
the samples occur.

structured data type - Comprised of more than one simple or scalar data type. The elements of a
structured data type can be all the same data type as in an array or they can be of different types
as in a record.

type listype is array(1..100,1..100) of integer;

type emptype is record


name : wordtype;
age : integer;
wage : float;
end record;

structured programming - A top-down program design approach in which a problem is broken


down into smaller and smaller subproblems until a solution can be implemented. Structured
programming makes use of subprograms and other hierarchical constructs.

stub - A subprogram shell which is used to as a place-holder for a complete function or procedure
to be implemented later.

procedure load_data(datin : in file_type; S : out listype) is


begin
null;
end load_data;

subprogram - Sometimes called a subroutine, a function or procedure that is called from a main
procedure or another subprogram.

subtype - A restricted type definition usually over a limited range of another type.

subtype wordtype is string(1..20);

syntax - The symbols and reserved words of a programming language and the rules for forming
executable statements.

syntax error - An error in the spelling or grammar of a computer program. Programs containing
syntax errors will not compile successfully. (See also propagation error.)
389
tail recursion - A type of recursion in which the only recursive call is the last statement in the
procedure. Tail recursion is a good indication that the recursive algorithm can be replaced with a
simple loop algorithm.

termination condition - The condition in a recursive algorithm that bypasses the recursive call.

text file - A sequential file comprised entirely of ASCII characters.

thread - An executable unit that can run concurrently with other threads. A thread is similar to a
process except that it shares data space with other threads as well as the parent program that
spawned it.

two-dimensional array - An array with two subscripts representing a table of data composed of
rows and columns of the same data type. In the example below table is a two-dimensional array
of 10,000 integers.

type tabletype is array(1..100,1..100) of integer;


table : tabletype;

underflow - The condition occurring when a value is too small to be represented in its associated
data type.

UNIX - A proprietary operating system developed by AT&T Bell Laboratories. (See also,
GNU/LINUX).

validation - One of the steps in the software development life cycle in which a program is tested to
make sure that it is solving the defined problem.

variable - An identifier whose value can change during program execution.

verification - One of the steps in the software development life cycle in which a program is tested
to make sure that it implements the designed algorithm.

virtual memory - An abstract model of an arbitrarily large memory system. Ada uses a virtual
memory mapping system in which the program and data space can be much larger than the
actual space provided for a program in real memory. For each instruction and data value, a
virtual memory location is translated into a real memory address during program execution.
When a virtual memory location does not match any real address, a page or segment of the
program is copied from secondary storage into real memory. The page or segment being written
over is copied to secondary storage for later use.

visible - In Ada, this term refers to the accessibility of a value. (See also, scope.)

word - A basic unit of memory that corresponds to a single memory address.

390
Index
-, 45 artifacts, 182
*, 45 artificial intelligence (AI), 375
**, 45 ASCII, 39, 375
/=, 42 Aspray, William, 7, 14
:=, 44 assembler, 376
+, 45 assembly language, 376
<, 41 Assembly Language, 9
<=, 41 assignment operator, 44
=, 42 Assignment Problem, 344
>, 42 assignment statement, 376
>=, 42 Assignment Statements, 44
Abacus, 5 attribute, 376
abs, 45 Attribute Functions, 37
abstract data type (ADT), 375 Augmenting Path, 342
accept states, 322 Babbage, Charles, 4
access time, 375 Babylonians, 3
actual parameter, 375 Bach, 90
acyclic graph, 320 Backus, John, 9
Ada, Augusta, 4 base-12 number system, 3
ada.integer_text_io, 17, 18 Basic, 10
ADA.NUMERICS.ARGUMENT.ERROR, 77 batch processing, 376
ada.numerics.elementary_functions, 76 baud, 376
ada.numerics_elementary.functions, 163 binary, 34, 376
AdaGIDE, 96 binomial coefficients, 132
Adagraph, 156 bipartitie graph, 321
AdaGraph, 138 bit, 376
address, 375 block, 376
Adjacency List, 325 boat anchor, 13
Adjacency Matrix, 324 body, 376
Aiken, Howard H., 13 Boeing 777, 11
Algol-60, 10 Boole, George, 5
Algol-68, 10 Boolean, 376
algorithm, 375 Boolean Operators, 43
All Pairs Shortest Path, 343 Boolean Types, 41
alphanumeric, 375 booting a computer, 376
alternation, 11 bootstrap, 8
ALU - (Arithmetic Logic Unit), 375 bootstrapping, 13
analog, 375 bps, 376
analog to digital converter, 375 branch, 377
analytical engine, 5 Breadth-First Traversal, 327, 332
Analytical Engine, 5 bug, 377
and, 43 Building an Executable, 24
ANSI, 375 Burks, Arthur, 6
application error, 375 bus, 377
Application Errors, 27 byte, 377
arcs, 320 C, 10
arctan( ), 118 C++, 10
argument, 375 Cartesian coordinate system, 136
arguments, 118 case, 68
Arithmetic Logic Unit (ALU), 7 cathode ray tube (CRT), 377
Arithmetic Operators, 44 Central Processing Unit, 7
array, 375 central processing unit (CPU), 377
array length, 375 character, 377
Arrays, 97 Character Types, 39
Arrays of Records, 101 Chunnel, 11

391
close( ), 94 DEVICE_ERROR, 72
COBOL, 10 differential equations, 360
code, 377 digital, 378
coding, 377 digitize, 378
color lookup table (CLUT), 138 digits, 37
Color_Type, 156 directed graph, 320
combinations, 132 discrete, 379
command language, 377 discrete data types, 379
Comment Blocks, 20 disk operating system (DOS), 379
compilation, 377 Display_Text( ), 161
compilation unit, 377 DOS, 379
compiler, 377 Draw_Box( ), 160
Compiler, 24 Draw_Circle( ), 160
complete graph, 321 Draw_Ellipse( ), 160
complex arithmetic operations, 172 Draw_To( ), 161
complex numbers, 172 driver, 379
complex plane, 172 Dynamic Memory, 325
composite type, 56 EBCDIC, 379
computational constructs, 10, 377 Eckert, Presper, 6
computer, 377 edges, 320
Computer Program, 16 Edison Effect, 5
concatenation, 377 Edison, Thomas, 5
concurrency, 11 editor, 379
concurrent, 377 EDSAC, 6
conditional branch, 378 EDVAC, 6
Connected Graph, 320 EDVAC, Draft Report, 6
constant, 16, 21, 378 eigenvalues, 360
continuum, 360 eigenvectors, 360
control statement, 378 else, 65
control structure, 378 elsif, 65
control unit, 378 encapsulation, 10
Control Unit, 7 Encoding, 28
core dump, 13 END_ERROR, 72
cos( ), 118 end_of_file, 95
CPU, 378 end_of_line, 95
crash, 378 ENIAC, 6, 7
create( ), 94 Enumerated Types, 38
CRT, 378 enumeration data type, 379
cycle, 321 Escher, 90
da Vinci, Leonardo, 13 Euler Circuit, 340
data, 378 Event Driven Model, 20
data abstraction, 378 event-controlled loop, 379
data base, 378 event-counter, 379
data base management system (DBMS), event-driven program, 379
378 exception, 379
data structure, 378 exception handler, 379
data type, 378 Exception Handling, 71
DATA_ERROR, 72 Excess Notation, 51
DBMS, 378 excess-127, 51
De Forest, Lee, 5 exchange sort, 107
debug, 378 executable block, 16
declaration, 378 Executable Block, 20
declaration block, 16, 19 execute, 379
Declarations, 19 exit when, 82
Declarative Languages, 10 exp( ), 118
degree, 320 exponent, 51
Department of Defense, 11 expression, 379
Depth-First Traversal, 326, 331 Extended Character Codes, 158
deterministic, 181 face time, 13
device independence, 378 fetch and execute cycle, 379
392
field, 379 Identifier Extent, 122
file, 94, 379 Identifier Scope, 122
file data type, 380 Identifiers, 21
file server, 380 IEEE Single Precision Floating Point, 51
file terminator, 380 if...then, 64
file_mode, 94 image, 37
finite state machine, 181, 380 Imperative LanguagesSee Procedural
finite state machines, 322 Languages, 10
first, 37 in mode, 381
flavor of UNIX, 17 in out mode, 381
Floating Point, 35 in range operator, 42
floating-point type, 380 in-degree, 320
Flood_Fill( ), 160 infinite loop, 381
flow of control, 380 infinite recursion, 381
Floyd's algorithm, 343 infinity, floating-point, 61
for...loop, 84 information hiding, 381
formal parameter, 380 inheritance, 10
formatting, 380 input alphabet, 322
FORTRAN, 9, 380 Input-Process-Output Model, 20
function, 380 insertion sort, 109
function call, 380 instantiation, 381
Functional languages, 10 Instantiation, 169
Functions, 118 integer, 381
Gate's Law, 13 Integers, 34
generic, 380 integrated development environment (IDE),
Generics, 168 381
get( ), 94 interactive program, 381
get_line( ), 94 interactive version, 59
GIGO, 13, 380 interface, 381
global parameter, 380 Interpolation, 360
GNAT_Ada IDE, 23 interpreter, 381
GNU/Linux, 17 Interpreter, 23
GNU/LINUX, 380 iteration, 381
Gödel, 90 Java, 10
Goto_XY( ), 161 kernel, 381
graph, 320 keyboard input buffer, 41
Graph Algorithms, 340 King, Augusta Ada, 13
Graph_It, 361 language processor, 381
Graphics Packages, 156 last, 37
greatest common divisor, 132 Laws of Thought, 5
GRINIXAL, 71 LAYOUT_ERROR, 72
hacker, 13 least common multiple, 132
Hailstone Problem, 90 least significant bit (LSB), 48
Hamiltonian cycle, 345 library unit, 381
Hamiltonian Cycle, 340 limit cycle, 182
hardware, 380 limited private type, 382
heuristic, 380 Limited Private Type, 168
hexadecimal, 380 line terminator, 382
hierarchy, 11, 381 linker, 382
high-level language (HLL), 381 Linking, 24
High-Level Language (HLL), 9 LISP, 10
High-level languages, 17 literal, 382
histogram, 116 loader, 382
Hofstadter, Douglas, 90 log( ), 118
honey pot, 13 logic error, 382
Hybrid Model, 20 logical operator, 382
hypercube, 321 Logical Piano, 13
Hypercubes, 322 logical_filename, 94
I/O, 381 long_float, 36
IBM 701, 6 long_long_float, 36
393
loop, 82, 382 Object-oriented languages, 10
loop body, 382 object-oriented programming, 345
loop exit, 382 octal, 384
loop parameter, 382 offline, 384
low-level programming language, 382 online, 384
Lull, Ramon, 13 opcode, 384
Machine Code, 8 op-code, 8
machine language, 382 open( ), 94
magic smoke, 13 operand, 385
mantissa, 51, 383 operating system, 385
Math Package, 163 operation, 385
Matrix multiplication, 115 Operator Overloading, 45
Mauchly, John, 6 Optimization, 360
max, 37 or, 43
Maximal Matching, 341 order of precedence, 45
mean, 383 ordering, 385
Mechanical Calculators, 4 order-of-traversal, 328
median, 383 ordinal data type, 385
memory, 383 Organum Mathematicum, 13
memory unit, 383 orthographic plot, 154
Memory Unit, 7 others, 69
Menu Driven Model, 20 Oughtred, Willam, 13
microcomputer, 383 out mode, 385
microcontroller, 383 out-degree, 321
min, 37 overflow, 50, 385
Minimum Spanning Tree Problem, 347 overloading, 385
Miranda, 10 package, 17, 385
mod, 45, 383 package body, 385
mode, 383 Package Body, 165
MODE_ERROR, 72 package specification, 385
Modeling, 140 Package Specification, 165
modem, 383 Packages, 156
module, 383 palindrome, 125
monitor, 384 paper-tape reader and punch, 385
most significant bit (MSB), 48 parallel, 385
Mouse Event Handler, 157 parameter list, 385
Multigraphs, 320 Parameter Passing, 121
Munkres' Assignment Algorithm, 349 parity, 385
Munkres, James, 344 Pascal, 10
NAME_ERROR, 72 Pascal, Blaised, 13
named association, 384 Pascaline, 5, 13
Napier's Bones, 13 path, 320, 385
negative numbers, 4 PC, 386
Nested Conditionals, 70 peripheral, 386
nested control structure, 384 permutations, 132
Nested loops, 85 physical_filename, 94
network, 384 Pi, 21
nibble, 384 pixel, 386
non-linear equations, 360 pixels, 137
non-terminal, 67 PL/1, 10
not, 43 place-value number system, 3
not-a-number (NaN), 61 Place-Value Number Systems, 3
NT_Console Package, 161 Polar coordinate system, 136
null, 69, 384 polymorphism, 10
null string, 384 portability, 17, 386
Numeric Data Types, 34 pos, 37
Numerical Algorithms, 360 positional association, 386
Numerical integration and differentiation, POV-Ray, 192
360 precedence rules, 386
object program, 384 precision, 386
394
pred, 37 sentinel, 388
Prim's Algorithm, 348 sentinel value, 83
Priority Traversal, 329 sequence, 11
private type, 386 Sequential access, 94
Private Type, 167 sequential file, 94, 388
procedural languages, 10 serial, 388
procedure, 16, 386 set_col( ), 79
procedure call, 386 Shannon, Claude, 5
Procedures, 121 Shifting, 141
process, 386 short circuit., 43
processor, 386 short-circuit operator, 388
Program Layout, 21 side effect, 389
program unit, 386 Signed-Magnitude, 47
Programmer-Defined Exceptions, 72 significant digits, 389
Prolog, 10 Simpson’s Rule, 366
PROM, 386 Simula, 10
prompt, 386 simulation, 389
propagation error, 386 sin( ), 118
Propagation Errors, 26 skip_line( ), 94
pseudocode, 386 Smalltalk, 10
pseudo-random numbers, 181 software, 8
put_line( ), 105 software development life cycle, 389
Python, 10 software development lifecycle, 27
quadratic formula, 80 sorting, 389
RAM, 386 source, 389
random access, 387 Special Keys, 158
random access memory (RAM), 94 sqrt( ), 118, 163
random numbers, 180 standard deviation, 98, 389
range, 387 Stanhope Demonstrator, 13
real memory, 387 Stanhope, Charles, 13
real number, 387 start state, 322
real time, 387 STATUS_ERROR, 72
record, 387 stepped rekoner, 4
Records, 101 Stepped Rekoner, 5, 13
recursion, 11, 387 stored-program computer, 6
recursive algorithm, 387 String Types, 39
Recursive Component, 346 strongly typed, 46
relational operator, 387 structured programming, 389
Relational Operators, 41 stub, 389
rem, 45, 387 subprogram, 389
repetition, 11 subroutines, 16, 18
reserved word, 388 subtype, 389
Reserved Words, 22 succ, 37
reserved words, Ada, 22 syntax, 389
reset( ), 94 syntax error, 389
return, 388 Syntax Errors, 24
return statement, 119 Syntax Notation, 29
reverse, 109 systems of linear equations, 360
ROM, 388 tail recursion, 390
Rotation, 143 tally stick, 2
runtime error, 388 tautology, 76
Run-Time Errors, 26 TCP/IP, 13
Scaling, 142 Termination Component, 346
Schickard, Wilhelm, 13 termination condition, 390
Schott, Gaspard, 13 text editor, 23
scope, 388 Text Editor, 22
scripting, 388 text file, 94, 390
secondary storage, 388 Text Files, 94
semantics, 388 Thomas Arithmometer, 13
semi-perimeter formula, 73 thread, 390
395
tic-tac-toe, 129 Visualization, 136
Trapezoidal Rule, 366 Volvos, 11
two-dimensional array, 390 Von Neumann architecture, 7
Two's Complement, 48 Von Neumann Architecture, 6
Type Conversions, 46 Von Neumann, John, 6
underflow, 390 Warnings, 26
UNIX, 390 weighted graph, 321
Unsigned Integers, 47 wheel, 321
use, 29 when, 68
USE_ERROR, 72 when others, 68
vacuum tubes, 5 Where_X, 161
val, 37 Where_Y, 161
validation, 390 while...loop, 85
Validation, 29 width, 37
value, 37 Wireframe Data Model, 326
vaporware, 13 with, 18, 29
variable, 16, 21, 390 word, 34, 390
Verifcation, 28 word in memory, 7
verification, 390 wumpus, 13
vertices, 320 xor, 43
virtual memory, 390 Zero of a Function, 364
visible, 390 Zuse, Konrad, 13

396
397

S-ar putea să vă placă și