workbk
workbk
workbk
January 2011
Advanced Programming
Course Notes
George C. Wells
& %
Copyright c 2011. G.C. Wells, All Rights Reserved.
Permission is granted to make and distribute verbatim copies of this manual provided the copyright notice
and this permission notice are preserved on all copies, and provided that the recipient is not asked to
waive or limit his right to redistribute copies as allowed by this permission notice.
Permission is granted to copy and distribute modified versions of all or part of this manual or transla-
tions into another language, under the conditions above, with the additional requirement that the entire
modified work must be covered by a permission notice identical to this permission notice.
Acknowledgements
Parts of these notes are adapted from previous course notes written by Pat Terry and David Sewry. In
addition, Peter Wentworth has been a valuable source of good ideas. Any errors or lack of clarity are, of
course, a result of my failure to distil their collective wisdom.
Contents
I Introduction 1
3 Driven to Abstraction! 25
3.1 The Need for Abstraction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
3.1.1 Some Examples of Abstraction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
3.2 Abstraction in Modern Programming Languages . . . . . . . . . . . . . . . . . . . . . . . 28
3.2.1 Procedural Abstractions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
i
3.2.2 Abstract Data Types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
3.2.3 Information Hiding: Client and Implementor Views . . . . . . . . . . . . . . . . . . 30
3.3 Closing Comments . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
ii
6.3 Graphs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115
6.3.1 Definitions and Terminology . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116
6.3.2 Representation of Graphs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116
8 Big-O 145
8.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 145
8.2 Algorithmic Complexity . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 146
8.2.1 The Impact of the Input Data . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 146
8.2.2 Big-O Notation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147
8.3 Some Examples . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 149
8.3.1 Very Simple Examples . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 149
8.3.2 A More Realistic Example . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 151
9 Searching 154
9.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 154
9.1.1 Implementation Issues . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 154
9.2 Searching Techniques Revisited . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 156
9.2.1 Simple Sequential Search . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 156
9.2.2 Searching a Hash Table . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 156
9.3 Binary Search Techniques . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 157
9.3.1 Binary Search . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 157
9.3.2 Interpolated Binary Search . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 160
9.3.3 The Relationship Between the Binary Search and Binary Search Trees . . . . . . . 161
10 Sorting 163
iii
10.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 163
10.2 Simple Sorting Methods . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 163
10.2.1 The Bubble Sort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 164
10.2.2 The Insertion Sort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166
10.2.3 The Selection Sort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 168
10.2.4 Summary of Simple Sorting Methods . . . . . . . . . . . . . . . . . . . . . . . . . . 170
10.3 Indirect Sorting . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 170
10.4 More Efficient Sorting Methods . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171
10.4.1 The Quick Sort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 172
10.4.2 The Merge Sort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 176
10.5 Concluding Remarks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 179
Index 183
Bibliography 185
iv
A.7.1 Dictionary.java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 218
A.7.2 Pair.java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 219
A.7.3 DictionaryPair.java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 220
A.7.4 ListDictionary.java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 222
A.7.5 Concordance.java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 225
A.7.6 InternalHashTable.java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 226
A.7.7 ExternalHashTable.java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 230
A.8 Binary Searches: BinarySearch.java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 235
A.9 Sorting: Sort.java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 236
v
List of Figures
vi
List of Tables
vii
Part I
Introduction
1
This section lays the foundation for the rest of the course. It starts by covering the motivation for studying
the topics included under the banner of “Advanced Programming”. It also introduces some important
new concepts, and revises some programming language features with which you may be familiar already.
2
Chapter 1
Objectives
1.1 Introduction
Information and Communication Technology (ICT) has been responsible for some of the most amazing
and complex innovations that mankind has ever developed, and our society is now almost completely
dependent on ICT. One of the key enablers of this process of innovation is the ability to construct new
systems — the process of programming.
Most of the programs that you have written and seen up until now have been very small in comparison
with “real life” computer programs. In realistic applications it is not unusual for programs to be tens
of thousands of lines long. For example, there is a open-source GIS (Geographic Information System)
package that has over 900 000 lines of code. The techniques which work well for writing small programs
to solve small problems start to fall apart very rapidly when applied to such life-size problems.
When dealing with introductory programming both student and teacher tend to focus on the language
details, and too little emphasis is given to the algorithms and data structures that are being used. In this
way the forest tends to get lost in the trees — the problems of dealing with syntax, compilers, etc. obscure
the bigger pictures of problem solving and program design. This course attempts to redress the balance
partially, by considering a number of common data structures and algorithms. The trade-offs that apply
to selecting a particular data structure or algorithm are considered, as are methods for analysing the
relative merits of differing approaches.
To be more specific in attempting to answer the question posed by the title of this chapter, this course:
3
• introduces some criteria for judging program quality
• illustrates a number of important data structures, and some of their areas of application
These aspects might be loosely categorised into two main areas: data structures and algorithms.
A further, extremely important aspect of dealing with large problems is that of design. This is an aspect
that we will not be considering explicitly in this course, but will leave to later courses. Tackling a large
programming project without a proper design would be like trying to construct a building without an
architectural plan. Such an approach might work for a garden shed, but is likely to result in major
problems if applied to a three-bedroomed house, and total disaster if used for a multistorey office block!
Similarly, writing small programs can be tackled with little formal planning, but writing a program of
any realistic size requires careful design and planning if it is to succeed.
Correctness The program should solve the problem that it aims to solve, correctly and completely.
Ease of use The program should be as easy to use as possible.
Generality and efficiency Rather than solve a single, specific problem, the program should solve as
broad a range of related problems as possible. In addition, the resources (e.g. computer time and
memory space) required by the program should be minimised.
4
Portability The program should be able to run on a variety of computers and/or operating systems
with minimum modification for each installation.
Clarity The program should be as easy to read and understand as possible.
Ease of coding and testing The program should be written in such a way that it can be completed
with the minimum of effort.
Amenability to modification The program should be so constructed as to be easy to modify without
courting disaster.
Before discussing each of these criteria in more detail, a distinction must be made between three types
of programs, each of which place different priorities on the above criteria:
One-shot Programs These programs are used only once, and are then discarded. Since the time
spent writing such a program far exceeds the time spent using it, ease of coding has top priority.
Generality, efficiency and amenability to modification are of no importance.
Production Programs These programs are used frequently and most programs fall into this category.
They will most likely be used for a long time and by many people. A possible priority listing might
be:
• correctness
• ease of use
• clarity
• portability
• generality
• ease of modification
• ease of coding
Efficiency would probably be on a par with clarity for programs like editors and compilers, and
below ease of modification for less frequently used programs.
Service Routines and Program Components These are methods and components that perform ba-
sic operations (for example, sorting a list of data items, calculating a square root, a GUI button
component, or a report-generator). The program code for these library routines and components is
normally fairly short and is used many times. Efficiency is critically important.
Since production programs make up the majority of programs written, they will be the main focus of
attention when discussing the individual criteria.
1.2.1 Correctness
Programs that do not work correctly are of no use whatsoever. Unfortunately, and despite the best of
intentions, the first version of any program will contain errors.
Although programming techniques have improved immensely since the early days, the process
of finding and correcting errors in programming — known graphically if inelegantly as “de-
bugging” — still remains a most difficult, confused and unsatisfactory operation. The chief
impact of this state of affairs is psychological. Although we are happy to pay lip-service to
the adage that to err is human, most of us like to make a small private reservation about
our own performance on special occasions when we really try. It is somewhat deflating to be
5
shown publicly and incontrovertibly by a machine that even when we do try, we in fact make
as many mistakes as other people. If your pride cannot recover from this blow, you will never
make a programmer. (Christopher Strachey)
The majority of programs work most of the time, but for the remainder, they fail or give incorrect results.
The people who use a particular program frequently get to know its habits and learn to live with them,
but to the people who rarely use the program it becomes highly annoying.
Some common errors are:
1. Incomplete problem solution Although the general case is solved, the special case is left unattended.
A careful study of the scope of the problem before attempting any solution will avoid many errors.
But bear the program’s objectives in mind all the time.
2. Initialisation In some systems, such as Visual BASIC, all variables are initialised but this should
not be assumed, especially if the program is to be portable. In the case of Java, the system either
initialises all variables (to a zero, or null, value) or else enforces initialisation by the programmer.
Of course, care must still be taken that the default initialisation is appropriate.
3. Off-by-one errors This occurs when a loop is executed one too many or one too few times.
4. Real numbers Real (floating point) numbers (i.e. float and double values in Java) are not stored
exactly on a computer. Frequent addition or multiplication can compound the error.
5. Typographical errors The difference between 1 (the digit one) and I (the upper case letter “i”) and
l (the lower case letter “L”), and 0 (the digit zero) and O (the upper case letter “o”) is sometimes
very marginal. The fact that i is a favourite loop control variable (although it should not be) and
1 a common starting point in a loop, makes matters worse.
6. Precedence errors The arithmetic expression:
a×b
x=
c×d
is coded correctly in Java as:
x = (a * b) / (c * d);
x = a * b / c * d;
1. Input: the program should be able to accept any input and not “crash”. In addition, the user
should not have to do any counting, for example, type in the number of integers to be submitted
before actually typing in the integers themselves — let the machine do all the counting. The user
should not be expected to employ obscure codes in place of an English word. For example, if dealing
with the input of colours the system should accept strings like “red” or “blue”, rather than numeric
codes like 16 or 3. In modern systems the use of menus of options and graphical user interfaces has
greatly improved ease of use, but care must still be taken to design user interfaces carefully and
consistently.
6
2. Output: all output should be sufficient, clear and self-explanatory. Do not display or print out
unnecessary data.
1.2.4 Portability
A portable program is a program that will execute on a variety of machines or on different operating
systems with no, or more realistically, very few modifications. This can be achieved by sticking as close
to the language standard as possible. Of course, this kind of portability was one of the important design
considerations for the Java language, and as a result Java programs are far more portable than most
others.
1.2.5 Clarity
Clarity is a measure of how easy the program is to read and understand. Factors that affect clarity are:
• program structure
• choice of symbolic names for classes, variables, methods, etc. (use meaningful names, and name
unfamiliar constants)
• comments
• physical layout
7
1.3 Techniques for Improving Program Quality
There are some simple techniques and habits that can make a great difference in constructing good quality
software. Three techniques that we will consider here are the use of preconditions and postconditions, the
use of program assertions, and automated mechanisms for producing documentation.
Another common notation is <entry>, used to indicate the value of a parameter on entry. For example:
Preconditions and postconditions are only comments, and so the programmer must write them initially,
and then keep them up to date as changes are made to the method. When writing a method careful
thought should be given to what assumptions are being made at the start of the method and what it will
guarantee to do by the end of the method. Of course, if the calling code violates the preconditions then
there is no guarantee that the postconditions will be upheld. For example, if the squareRoot method
above is called with a negative value for the parameter x, it may be justified in returning garbage.
1.3.2 Assertions
Assertions are a mechanism that allow us to introduce the idea of “checkpoints” in our programs. An
assertion states a fact about the state of the program at the point at which the assertion is made. Many
programming languages do not support assertions and so they can only be included as comments, as
we did for preconditions and postconditions above. Originally Java did not support assertion checking.
However, this was rectified with the release of Java version 1.4.
1 In the rest of the discussion we will refer only to methods in the usual Java manner, but be aware that the use of
preconditions and postconditions is a generally useful technique, and can be applied in any programming language to
functions and to procedures.
8
The Java assertion mechanism allows us to embed assertions in our programs in such a way that they
will be checked as the program runs. An assert statement will throw an exception2 if the assertion does
not hold true. The Java assertion mechanism has two alternative forms:
Consider the following simple example, using the first form above:
. . .
skipSpaces();
ch = (char)System.in.read();
assert ch != ’ ’;
If we had made an error in writing the skipSpaces method then the error would be detected and the
program would terminate with a message like:
java.lang.AssertionError
at MyApp.main(MyApp.java:12)
Exception in thread "main"
The second form of the assert statement has an expression of any type as a second field. This is
converted to a string and used to form the error message given by the AssertionError class. In practice,
the second expression is usually a simple string giving some form of helpful error message, as in the
following example.
. . .
skipSpaces();
ch = (char)System.in.read();
assert ch != ’ ’ : "Character is not a space";
In some cases it may be difficult, or even impossible, to express an assertion using a simple conditional
expression. In these situations one needs to revert to using a simple comment to state the assertion. For
example:
class PlayingCard
{ ... }
Just as with preconditions and postconditions, the responsibility for the use of assertions rests with the
programmer. The discipline of writing assertions into your programs has the two-fold benefit of forcing
you to evaluate exactly what your program is meant to be doing at strategic points in its execution, and
providing for run-time checking (in situations where this is possible).
Note that the assertion checking in Java is disabled by default. If it is required that assertions are checked,
then it is possible to enable assertion checking in Java programs at runtime. This allows the checking
2 Actually an error is thrown: AssertionError.
9
to be performed during program development and testing, but then be disabled for efficiency when the
program is shipped. Further details are available in the Java documentation.
Alert readers may have realised that there is a close link between assertions and pre- and postconditions.
Both of these techniques make statements about the expected state of our programs at well-defined points
in their execution. For example, it is very likely that the postcondition of the shuffleCards method
referred to above will state something like “The deck of n cards is in random order”. For this reason one
sometimes sees methods starting with an assertion, or assertions, checking that the preconditions hold.
For example, returning to our squareRoot method we might have something like the following:
In this way the possibility that the precondition may not be met by the calling code is taken into account,
and an error message will be generated if the assertion fails.
However, the Java programming guidelines produced by Sun state that using exceptions is a better way
of handling preconditions for public methods. This ensures that the exception conditions are properly
documented and handled. So a better solution to the problem described above would be:
10
* @author George Wells
* @version 1.1 (7 November 2000)
*/
public class Roots
{
/** The approximate square root is found using the Newton-Raphson method.
* @param x The value whose square root is to be calculated.
* @return The approximate square root of x.
* @throws IllegalArgumentException if x < 0.
*/
public static double squareRoot (double x)
{ . . .
} // squareRoot
} // class Roots
When run through the Javadoc processor this file produces HTML documentation that looks something
like the following. Note how the documentation comments have been reproduced here, and how the
documentation tags have affected the presentation of the information.
Class Roots
java.lang.Object
b Roots
Constructor Summary
Roots()
Method Summary
static double squareRoot(double x)
The approximate square root is found using the Newton-Raphson method.
Constructor Detail
Roots
public Roots()
11
Method Detail
squareRoot
public static double squareRoot(double x)
The approximate square root is found using the Newton-Raphson method.
Parameters:
x - The value whose square root is to be calculated.
Returns:
The approximate square root of x.
Throws:
java.lang.IllegalArgumentException - if x < 0.
All the Java API documentation is produced using Javadoc, so the style of this documentation should
look very familiar. Because the documentation is created using HTML, the usual HTML tags may also be
used to assist in the formatting of the documentation. Further information about the use of the Javadoc
tool, the available tags, etc. can be found by referring to the Java documentation.
Automatically producing documentation from program code helps improve program quality by allowing
programmers to write the code and the documentation together, with only one set of files to maintain.
This aids greatly in ensuring that documentation and code remain in agreement. Simplifying the doc-
umentation process also increases the likelihood of documentation being produced at all (programmers
are notorious for disregarding the production of documentation!).
The code for the data structures and algorithms that we will be considering during this course is docu-
mented using Javadoc (see Appendix A, p. 187), so you will see many examples of the use of documentation
comments. However, to save space, many of the code segments discussed in the main body of the notes
do not have the full documentation comments included. Of course, for more information on the classes
discussed in these notes, you may also refer to the online documentation produced by the Javadoc tool.
12
Skills
• You should be able to write preconditions and postconditions for any methods you develop
• You should be able to write assertions for any code you develop, and to use the assertion
mechanisms in Java
• You should be able to use Javadoc to produce system documentation
13
Chapter 2
Objectives
Many of the ADTs we will be considering are implemented using interfaces, and many of the algorithms
that we will be looking at make use of recursion. This chapter gives a brief introduction (possibly revision
for some readers) to the use of these techniques in Java.
2.1 Recursion
Recursion is a method for repeating activity in a program. The use of recursion is perhaps best ap-
preciated for problems where iterative algorithms (i.e. using loops for repetition) start to become very
awkward. Some of the best examples of this arise when we come to consider tree ADTs and algorithms
for manipulating and traversing such trees. For the moment we will have to be satisfied with some simpler
(and rather overdone!) examples.
2.1.1 Introduction
The word recursion is defined in the dictionary as “the act or process of returning or running back”[6].
In the context of Computer Science, this term is used when an algorithm (or a method) refers back to
itself, or calls itself.
Recursion can be thought of quite simply as solving a problem in terms of a simpler form of the same
problem. For example, consider a simple robot (R) that is trying to catch a target (T) in a room. The
robot is incapable of detecting the speed and direction of the target — all it can detect is the current
position of the target.
14
The robot can follow a very simple algorithm of taking one step towards the target, and then following
the same process again and again, until it reaches the target. This might result in the following sequence
of steps:
15
Expressed in terms of a pseudo-code algorithm, we might have something like the following:
ALGORITHM: CatchTarget
IF atTarget THEN
STOP
Take one step towards target
CatchTarget // Recursive use of the algorithm
There are a number of points to notice about this approach. Firstly, we have expressed the problem
(CatchTarget) in terms of itself (the last line of the algorithm refers to the entire algorithm again) —
this is the essence of recursion. However, each time we “reuse” the algorithm, it is in a slightly “simpler”
case (i.e. we are one step closer to catching the target). Lastly, and very importantly, we have a stop case
— when we reach the target we are finished. Without the stop case the algorithm would never terminate,
as each and every attempt to catch the target would result in the algorithm being invoked again.
n! = n × (n − 1) × (n − 2) × . . . × 3 × 2 × 1
The key to using recursion here is to see that this can be expressed as:
n! = n × (n − 1)!
since
(n − 1)! = (n − 1) × (n − 2) × . . . × 3 × 2 × 1
In other words, the factorial of a positive integer number can be defined to be that number multiplied by
the factorial of the preceding number (n! = n × (n − 1)!). As always, the recursion has to stop somewhere,
and to do this we introduce the stop case that 0! = 1.
With this understanding of the recursive approach to solving this problem, we can express the recursive
factorial method in Java as follows:
16
public int factorial (int n)
// Recursive method to calculate the factorial of n
// PRE: n >= 0
// POST: if n! < Integer.MAX_VALUE then factorial returns n!
// else a nonsense value is returned
{ if (n == 0)
return 1; // The stop case
else
return n * factorial(n-1); // The recursive call
} // factorial
As is often the case, it must be noted that the iterative solution to this problem is far more efficient. The
recursive solution is elegant, and serves to illustrate a simple recursive method, but should never be used
in practice.
Exercise 2.1 Write an iterative, rather than recursive, method to calculate factorials. Which
is easier to understand?
Write a program that calls both methods repeatedly, measuring the time taken by them both.
Which is more efficient?
ALGORITHM: ReadAndWriteBackwards
READ ch
IF ch != ’\n’ THEN
ReadAndWriteBackwards // Write out the rest of the string backwards
WRITE ch
ch = (char)System.in.read();
if (ch != ’\n’)
readAndWriteBackwards();
System.out.print(ch);
} // readAndWriteBackwards
17
Note that this works because each recursive call to the readAndWriteBackwards method has its own
copy of the local variable ch, which effectively stores a character while the recursion proceeds. If we used
a class or instance variable for this purpose the program would no longer work correctly.
Make sure that you understand how these recursive methods work.
Exercise 2.2 The Fibonacci series is another widely overused example of recursion. The
nth Fibonacci number (fn ) is defined to be the sum of the previous two Fibonacci numbers
(fn−1 + fn−2 ). The first two Fibonacci numbers (f1 and f2 ) are defined to be 1. The first few
values in the series are: 1, 1, 2, 3, 5, 8, 13, etc. More formally, we can define the Fibonacci
series:
1 if n = 1 or n = 2
fn =
fn−1 + fn−2 if n > 2
Write a recursive Java method to calculate the nth number in the Fibonacci series.
Exercise 2.3 An important class of graphics images are what are known as fractals. Fractal
images have the interesting property that they are similar when examined at any level of
magnification. Here we consider a simple case of recursive fractals, which produce images that
look like quite realistic mountain ranges.
Such an image can easily be produced recursively as follows: we start off with the problem of
drawing a horizontal line across the centre of the drawing area; we then break this into two
subproblems of drawing half a line, but with the centre point of the line moved slightly up
or down in a vertical direction by a random factor proportional to the sub-line length. To
illustrate this, the first two steps might be as follows:
The same process is then applied to the problem of drawing the four quarter-length lines, and
so on. The stop case in this situation is when the length of a line drops below some suitable
threshold (say, five pixels in length).
Write a recursive Java applet that will display a random fractal, as illustrated above.
18
Exercise 2.4 The Towers of Hanoi is another common (and much better) example of recur-
sion. An ancient legend in the Far East says that there is a monastery that contains three
towers. The monks in the monastery are moving a set of 64 disks from one tower to another,
one disk at a time. When the job is completed the world will supposedly come to an end! The
process of moving the disks is a little complicated by the fact that the disks are all of different
sizes, and the rules state that a disk may not be placed on top of a smaller disk. The initial
configuration, assuming that there are only four disks, would look like the following:
Exercise 2.5 In bioinformatics, gene sequences are expressed as long strings of information
coded using the letters A, C, T and G (these stand for the four distinct bases that comprise
genes). When analysing DNA, the data that is obtained is sometimes incomplete. In these
cases an X is introduced into the string to show that there was an undecipherable base present.
So, we might get a short sequence like “ACCCGTAAXGGCTAGXXGGCT”. In order to match
this with other, known sequences we need to be able to generate all possible strings from this
form of data (i.e. replace all the X’s with all possible combinations of A, C, T and G). There
may be any number of X’s in any arbitrary position(s) in the given sequence.
Recursion allows us to generate these sequences quite easily. We can formulate the problem of
generating all possible sequences as follows:
In this way we can methodically generate all the possible gene sequences.
Write a Java program that uses this recursive approach to solving the problem.
19
Figure 2.1: A Class Hierarchy for Employee Classes.
20
Figure 2.2: Fitting in PAs (won’t work in Java).
21
Figure 2.4: Using an Interface.
It is important to note that the PersonalAssistant and Manager classes do not inherit any properties
or methods from the ManagerialResponsibilities interface.
How do we express this in Java? It is in fact very simple. The outlines of the classes and the interface that
are required are as follows. Study this closely and compare it with the diagrammatic view in Figure 2.4.
22
public class PersonalAssistant extends Secretary
implements ManagerialResponsibilities
{ ...
public void manage ()
{ ...
} // manage
...
} // class PersonalAssistant
Note how the interface, which is shown completely above, does not contain a body for the manage method,
simply the “heading” followed immediately by a semicolon. What this means in practice is that the classes
which implement this interface (i.e. Manager and PersonalAssistant above) must provide this method,
as indicated above. In this way, classes that cannot inherit some behaviour (such as the ability to
“manage” here) can be forced to provide the required method(s) by requiring that they implement an
interface.
As we can also see from this example, it is possible for a class to both extend another class (using
inheritance) and also to implement an interface. In fact, a class can implement many interfaces if
necessary, and so it is not unusual to see classes like the following:
where IntA, IntB and IntC are all interfaces. This class now has four distinct types of behaviour: that
which it has inherited from SomeOtherClass, and that which it has been forced to provide through
implementing the three interfaces.
MangerialResponsibilities mgr;
This is a reference variable that can be used to refer to an object belonging to any class that implements
the MangerialResponsibilities interface. So, we could do any of the following:
or even:
since the Director class has inherited the necessary behaviour (i.e. the manage method) from the Manager
class which implemented the interface.
23
As is the case with polymorphism through inheritance, we lose some capabilities in this way. All we
can use the mgr variable for is the behaviour defined in the MangerialResponsibilities interface.
We have effectively lost the access to the specific features of the actual class being used (Manager,
PersonalAssistant or Director above). In this case, all we can do with the mgr reference is call the
manage method:
mgr.manage();
However, as usual, we can use the instanceof operator to find out exactly what type of object we are
dealing with, and type-casts to get a reference of the type required. For example:
As well as allowing us to work around the limitations of single inheritance, interfaces provide a convenient
solution in any situation where multiple classes that are not related by inheritance must still share some
common behaviour.
Note: The class diagram format used in this chapter and throughout these notes is a highly simplified
form of the class diagrams used in the Unified Modeling Language (UML). In general, the use of “proper”
UML class diagrams is to be preferred.
We will return to the subject of interfaces and see further examples of their use in the later chapters of
this course.
Skills
24
Chapter 3
Driven to Abstraction!
Objectives
We believe that the bulk of problems with contemporary software follow from the fact that it is
too complex. Complexity per se is not the culprit, of course; rather it is our human limitations
in dealing with it. Dijkstra1 said it extremely well when he wrote of “our human inability to do
much”. It is our human limitations, our inability to deal with all relations and ramifications
of a complex situation simultaneously, which lies at the root of our software problems. In
other contexts we (humans) have developed an exceptionally powerful technique for dealing
with complexity. We abstract from it. Unable to master the entirety of a complex object, we
choose to ignore its inessential details, dealing instead with a generalized model of the object
— we consider its “essence”.[17]
25
simple, controls for getting us from one place to another (in some cases with the side effect of attracting
members of the opposite sex!). The controls of a motor car serve as the interface between the driver and
the hidden maths, physics and chemistry that actually get us to our destination. Any excessive concern
for the physical functioning of the vehicle is likely to overwhelm the driver with far too much tedious
detail.
In much the same way that we treat everyday objects as abstractions, most readers will have an abstract
view of a computer as a programmable device capable of a certain level of calculation and interaction.
Of course, there are many levels of detail below this which are conveniently ignored most of the time.
These levels can be thought of as forming a hierarchy of abstract views of a computer system where, at
each succeeding level, the conceptual “machine” with which one is dealing is further and further removed
from the actual details of the circuits and electronic signals. This hierarchy is depicted in Figure 3.1.
At the lowest level we have laws of quantum physics determining the energy levels of electrons in semi-
conductor materials. This is a level of detail that only really concerns researchers in the field of computer
chip manufacturing.
Just above the quantum physics level we find the electronic circuits (the hardware) that actually perform
the computations in a computer system. Typically, these circuits can do simple arithmetic and logical
operations and store numeric values. The “programming” (if we can call it that) of this level is done by
electronic engineers, who design the circuits.
In the levels above the hardware we have different software elements that control the action of the
hardware. The microcode level (or microprogramming level) is the very lowest level of software and
consists of instructions that control the switches in the hardware circuits directly. The microprogramming
of a computer is usually done by the manufacturer of the computer, and so very few programmers ever
work at this level.
One step up from the microcode level is machine code. Since we seldom work at the microcode level,
this level defines what we refer to as the conventional machine level. It is the lowest level of abstraction
of which most programmers are aware. The machine code for a computer consists of binary information
that controls the circuitry of the computer. Typically, a single machine code instruction will be made up
of several microcode instructions. At this level there are usually instructions to specify arithmetic and
logical operations, program control (such as iteration, selection and branching), access to data stored in
the memory of the computer, etc. Due to the difficulty of working with a purely numeric representation
for the instructions we seldom deal with this level directly. The instructions to add two numbers together
at this level might look as follows2 :
2 This and the following examples of machine level and assembly language instructions are for the Intel 80x86/Pentium
family of processors.
26
1010 0001 0000 0000 0000 0000
0000 0011 0000 0110 0000 0000 0000 0010
Since this is fairly meaningless and hard to comprehend, we usually use hexadecimal (base 16) represen-
tation for such values (which isn’t a lot easier to understand, but much easier to write!). In hexadecimal
notation the above sequence of machine code instructions is as follows:
A1 0000
03 06 0002
Programming at this level is sometimes referred to as working in a first generation language. When the
first generation of computers was developed in the late 1940’s this was the only way of programming
them.
Above the machine code level we have the operating system. This consists of routines (usually supplied
by the computer manufacturer) to simplify access to the resources of the computer, such as memory,
disks, terminals, printers, etc. For example, the operating system may have routines that will allocate a
block of memory for use by a program, to print a character on a printer, or to read a line of input from
a keyboard. The provision of these facilities in the operating system simplifies the task of programmers
working at higher levels since they do not need to concern themselves with the complex operations needed
to access the peripheral devices connected to a computer system. The other advantage of an operating
system is that it makes the programs more independent of the hardware on which they execute, since the
details of the operation of the hardware are kept from the program by the operating system.
Above the operating system level we have the assembly language level. At this level we can control the
execution of the circuits by writing instructions in a form of English-like notation. This notation usually
has a direct, one-to-one correspondence to the machine code instructions, but is easier for humans to
comprehend than binary or hexadecimal. At this level we might represent the sequence of instructions
needed to add two numbers as follows:
Of course the hardware of the computer cannot execute this form of instruction and so it must be
translated into machine code by a program called an assembler. At this level we can also make use of the
facilities offered by the operating system in order to access peripheral devices. Assembly languages are
sometimes referred to as second generation languages since they emerged soon after the early, primitive
first generation languages.
Above the assembly language level we find the high level languages. This is the level at which most
programmers work, and with which they are most familiar. It is at this level that we find Java, Python,
C++, C], Delphi, and other common computer languages. The languages found at this level are often
referred to as third generation languages, as they were developed after assembly languages. The first
third generation language was FORTRAN, which was developed by IBM in the 1950’s. A feature that
distinguishes high level languages from the preceding generations is that they are far more machine inde-
pendent. An assembly language instruction or machine code is usually specific to a particular computer.
On the other hand, a program written in a high level language can be executed on any of a wide range of
computers, without too much trouble. Of course, since the hardware circuits only “understand” binary
data a high level language has to be translated into machine code. This is the task performed by a
compiler or an interpreter.
The notation used by high level languages is usually fairly formal and precise, but far more readable by
humans. For example, to add two numbers in Java we could write:
num1 + num2
27
which is easier to understand than the assembly language version above. In a language like COBOL it is
even clearer still!
Within the range of high level languages we find that there is something of a subhierarchy. Some languages
(such as C) are actually fairly close to assembly language, and provide some access to the underlying levels
of abstraction. Other high level languages (such as Java) are much further removed from the underlying
levels.
The fourth generation languages (often abbreviated to 4GLs) came along after the high level languages.
They are typically used for business applications and database access. The aim of fourth generation
languages is to make it easier for business people with no knowledge of programming to access information
quickly and easily.
The fifth generation languages are not very well defined. They are distinguished by their use of artificial
intelligence techniques to make them extremely easy to use.
When working at any of these levels of abstraction we can usually safely ignore the details and complexity
of the underlying levels. Just as the car driver would be swamped trying to think about the chemistry
and physics at work while driving a car, so too would a programmer be overwhelmed if he or she had
to consider the binary translation and electrical signals corresponding to every statement in a high-level
programming language.
These examples of abstraction in everyday life and in computer systems illustrate the power of abstraction
in allowing us to ignore the details of complex systems. While we may ignore these details, we can still
use these complex systems very easily, thanks to the high-level, abstract view that we have of them. Of
course, this kind of abstraction is not the main focus of this course — we wish to find out how to build
complex software systems that can make use of abstraction in order to simplify the software development
process. In order to be able to do this, computer languages provide mechanisms that allow us to write
programs using different types of abstraction.
28
3.2.2 Abstract Data Types
Of more interest to us in this course is the concept of an abstract data type (ADT). An ADT can be
formally defined in the following way.
Most computer languages have built-in data types that provide the programmer with abstract views of
simple numeric and character values. These are abstractions in the sense that we can use them without
too much concern for the binary and electronic representations of the values.
Many programmers are happy that, for the most part, they can manipulate numbers (e.g. double float-
ing point values) without concern for the internal representation (implementation) of these values, and
without knowledge of the hardware operations used to support the data manipulation. The programmer
is, quite rightfully, content with the concept of a floating point number and totally oblivious of the un-
derlying implementation. The notion of concept and implementation being separated is the foundation
of abstract data types, or data abstraction.
The basic Java data types double, int, char, etc., each supported by a set of operations, are abstract
data types. A declaration of the form:
double number;
is an abstraction of the underlying memory circuitry used to house the floating point value number, and
the instruction:
is an abstraction of the electronic circuitry used to implement such an arithmetic operation. The same
argument applies to the other built-in Java types. The reader probably has not thought of doubles in
such a way because these primitive types are predefined. And therein lies the power of abstraction —
there is no need to know or to comprehend the details of implementation, but merely to understand the
concept.
In terms of the formal definition given above, the int type in Java could be characterised as follows (you
may not have seen all of the operations specified here, but don’t worry about that):
As well as providing us with built-in abstractions, modern computer languages allow us to create our
own new ADTs. In Java it is the class mechanism which allows us to do this. At one level, classes in
Java are very like records in other programming languages, and of limited use in constructing ADTs. The
features in Java that extend the simple idea of a record and make the class a truly powerful mechanism
for constructing ADTs are the ability to specify the visibility of fields within the class (e.g. using public
and private access control), and the ability to specify not only the data values for the class but also the
29
operations (using methods). These mechanisms give us the ability to develop our own new data types
and integrate them with the existing abstractions built into the Java language.
An example of such an ADT is a frequency table. This has a set of possible values (the frequency counts
for several different ranges or “bins”) and a set of possible operations (e.g. add a new entry, return the
frequency for some range of values, print the table, print a frequency graph).
As another example, consider a data structure that could be used for a dictionary, such as might be used
by the Rhodes Dictionary Unit. This has a set of values (words and their associated meanings) and a set
of operations (e.g. open, close, add a new entry, delete an entry, look up the meaning of a word, etc.).
In the rest of this course we will study many more examples of abstract data types.
Interfaces, with polymorphism, are also a powerful abstraction mechanism in Java. When we declare
a parameter (or variable) to be of an interface type, we are effectively taking an abstract view of the
actual object that is being referred to. For example, using the interface example of the previous chapter,
the following method does not need to know whether it is actually dealing with a Manager object, a
PersonalAssistant object or a Director object. All that is relevant is that the object referred to by
the parameter m has a manage method — the delegate method does not need to know anything else
about the actual parameter.
To try to emphasise these two different points of view we will mark sections of the notes that deal
with the client view by using highlighting as you see here.
Class Diagrams
To help the reader grasp the fundamental details of the various data structures, we will use a simplified
form of UML class diagram, as shown in Figure 3.2. The top part of the diagram simply names the class.
The middle section shows the details of the data members, and the bottom section the details of the
function members (or methods). Private members are shown with a grey background (as for the data
members here) and public members are shown normally. This helps to emphasise the difference between
30
ClassName
data members
methods
the client and implementor views since the public members are the part of the class that are visible to
the client programs, while the private members are those parts that are only visible and of interest to
the implementor.
Essentially, my work involves dealing with problems, and solving them — at the moment,
that involves developing new products for Internet Solutions. If I think about all I learned at
university, probably, the single most powerful construct I learned (actually it appears in both
Mathematics and Computer Science) is the concept of abstraction. Abstraction can be applied
to almost any complex problem to break it down into manageable chunks. That becomes a huge
help when dealing with complex projects, when I am trying to get my mind around how to deal
with something. I might not be writing programs, or solving theorems, but the concepts that
apply there apply all over the business world. Having that formalism as a backing gives me
an infinite lead over my peers who do not have tools like that at their disposal.
(Geoff Rehmet, 2002)
Skills
• You should understand how we use abstraction in everyday life, and in the context of complete
computer systems
• You should understand the abstractions which modern computer languages provide
• You should know that modern computer languages allow us to construct our own abstractions
• You should know what is meant by the term abstract data type
• You should understand the difference between the client and implementor views of an ADT
31
Part II
32
This section of the course considers a number of important data structures. Chapter 4 takes a look at
arrays and linked lists which form the building blocks for many of the more complex structures which
we then consider in the following chapters. These range from simple stacks and queues through to more
complex structures such as trees and hash tables. Common techniques for the implementation of these
data structures as abstract data types will be considered, together with some examples of their use.
33
Chapter 4
Objectives
4.1 Introduction
Many problems involve the use of lists. In real life we have lists of courses which students can take,
shopping lists, lists of New Year’s resolutions, lists of cabinet ministers’ names, etc. And, in a rather
recursive way, the previous sentence was a list of lists! You are already familiar with the way in which
we can handle lists of data in programming languages by using arrays. This chapter will build on this
basic concept in two ways. The first section takes the simple concept of an array in Java and extends
it using some of the more advanced features of Java. The second section shows how we can get around
some of the problems associated with using arrays by using dynamic data structures. In both cases we
will be dealing with lists of integers, but obviously the principles apply to lists of any other objects. This
is reinforced in the last section of this chapter when we consider examples of generic data structures:
lists of any type of object. In doing this, we take advantage of some of the language features introduced
in Java 5.0.
4.2 Vectors
Arrays in Java have a few problems associated with them. One of the most obvious is that the size of
the array is fixed when the array is created and cannot be changed. Fortunately, Java provides us with
the necessary mechanisms to develop our own alternative structures.
In this section we are going to develop a data structure to hold lists of integer values and manipulate
these in ways very similar to arrays. In fact we will be using an array as the basic building block of our
34
• Constructing an empty list (initialisation)
• Adding an element to a list
new data structure. Let us start by thinking about the operations that we might want to perform on a
list. We obviously want to add new elements to the list and be able to access them, and will probably
want to remove elements. We will also want to display the contents of the list. We might need to know
the length of the list, and it might be useful to search the list for the position of a given element. We
will also need to provide a constructor, as usual. These requirements are summarised in Table 4.1 and
the class diagram below. They are requirements that apply generally to all lists and not just to the lists
of integers that we will be dealing with in this section.
IntegerVector
data, numElements
Data Members
Let us see how we can build such a data structure in Java. The starting point is, of course, to create a
new class. We will call it IntegerVector. The data members of this class will need to hold the contents
of the list together with information about the list such as the number of elements it actually contains
at any given moment. This gives us the following starting point:
. . .
} // class IntegerVector
Note that we will call our new data structure a vector 1 to differentiate it from a normal array in Java,
but it is really much the same sort of thing, with a few extra operations. We have arranged to have the
data, together with the other bits of housekeeping information, stored as private fields in the class. This
means that we will only be able to access the data through the public methods that we write. This is a
1 Vector is the technical term used by mathematicians to refer to an indexed list of values.
35
very common pattern for developing ADTs, as it provides complete control over the clients’ access to the
contents of the ADT objects. The full listing of this file (IntegerVector.java) is given in Appendix A
of the notes.
Constructors
The first operation that we need to consider is the creation of a new vector, i.e. the constructor or
constructors that we need. The main constructor will simply create the vector with a given size.
Note the check on the precondition that the initial size specified is positive. Another useful constructor
would be one that allows us to make use of a default initial size. This can simply make use of the first
constructor above, by using the keyword this, as we see in the following code.
public IntegerVector ()
{ this(100);
} // constructor
These constructors allow us (thinking of our client role now) to declare vectors in either of the two
ways illustrated by the following examples:
That completes the constructors required for the implementation of the new data structure. What about
accessing the data structure? The vectors described by what we have so far start off “empty” — we can
see above that the data member numElements is set to zero as part of the initialisation of the class. So,
how can we add a new element to the vector? A very simple way is to add the new element to the end
of the list (assuming that there is space for it):
Note how the business of adding the new item and updating the number of elements is very neatly carried
out by the single Java statement data[numElements++] = item. If you are not sure how this works refer
back to your introductory Java notes on the ++ operator, and then try a few examples to see what is
happening.
This add operator will do very well for a simple list, but what if we wanted to add an item to the middle
of our list (or at the beginning, for that matter)? In this case we need to specify the position in the
vector where the new item is to be added.
36
public void add (int item, int position)
{ if (numElements + 1 > data.length)
throw new NoSpaceAvailableException("no space available");
if (position < 0)
throw new IllegalArgumentException("position is negative");
if (position >= numElements) // Add at end
data[numElements++] = item;
else
{ int k;
for (k = numElements-1; k >= position; k--)
data[k+1] = data[k]; // Move elements up
data[k+1] = item; // Put item in place
numElements++;
}
} // add
Notice how we have a further precondition check now to make sure that the position parameter has a
positive value. We also have two cases that arise. The first is that the new item is to be added to the
end of the vector (this is essentially the same as we had before). The second, more difficult, case is that
the item is to be added into the middle of the list. In this case we have to make space for it by moving
existing items in the list up. This is the purpose of the for loop above. Try out a few examples and
satisfy yourself as to how this works.
From the client perspective, we can now add items to our vectors as follows:
v1.add(3);
v1.add(-8);
v2.add(39);
v1.add(21, 1); // Put item in position 1 of v1
What will these two vectors (v1 and v2) contain after this series of operations?
Having provided a mechanism that allows us to put items in vectors, it would be rather a nice idea if we
could display the contents of a vector. A simple way of displaying the contents of an IntegerVector is
to provide a method that outputs the contents of the vector. For example, if we assumed that we would
always want to display the vector using System.out, then we could write such a method as follows:
37
This could be used in the following way:
v1.output();
However, it would be far more useful if we provided a toString() method for the IntegerVector
class. This will be used automatically by the compiler if we use System.out.println() to display
an IntegerVector:
System.out.println(v1);
In particular, this would not force us always to use System.out to display the vector, but would
allow us to use any output stream. Furthermore, it can also be used in any other situation where
we need to display a vector (for example, in a textbox in a GUI display).
Note how we use a StringBuffer to build up the string representation of the vector and then convert it
to a String once the job is completed. The StringBuffer class is a standard Java class that allows us to
manipulate strings in ways that are a lot more efficient than manipulating String objects directly. This
is because String objects in Java cannot be changed once they are created. As a result, manipulating
strings requires that the methods of the String class create new String objects for each result (a time-
consuming operation). The contents of a StringBuffer, on the other hand, can be freely changed without
requiring the creation of new objects.
With this method in place we can display vectors very simply. For example:
Accessor Methods
Moving on to the subject of accessing entries in an IntegerVector, we need to provide two methods
here: one to retrieve a data value (get) and one to alter a data value (set). These sorts of methods are
usually referred to as accessor methods or just accessors.
38
public void set (int index, int item)
{ if (index < 0 || index >= numElements)
throw new IndexOutOfBoundsException("index is out of range");
data[index] = item;
} // set
The if statements simply check that the index passed as a parameter is in the correct range of values
(i.e. that it corresponds to an existing element of our vector). The return statement in the get method
then returns the correct element of the vector, while the set method updates the value of the specified
element. As we commented earlier, this is a common pattern for ADTs like this, and the use of “get”
and “set” (or some variation on these names) is a common convention.
These methods allow us to access our vectors as shown in the following example:
Other Methods
Now we can create vectors, we can add elements to them and we can access and display the contents of
our vectors. This comprises a minimal set of operations. To make our vectors more useful we can add
some of the further operations we mentioned in the introduction to this section. The first of these allows
us to find an item in a list:
This is quite straightforward. The only real “catch” is the way we return −1 to indicate that the item
could not be found in the vector.
The next method allows us to remove an item from a vector:
Again this is fairly simple, we just need to move the other items down in the list and decrease the number
of elements. Notice again the use of an assertion to check the preconditions for the method.
The last method that we need to consider is one to tell us the number of items contained in the vector.
As we already have this information stored in a private data field, this is a trivial operation.
39
public int length ()
// Return number of elements in an IntegerVector
{ return numElements; }
Some Comments
So now we have a class which allows us to construct and use lists of integer values. In itself this is probably
not very interesting or useful, but the principles that we have in place here are just as applicable to lists
of ingredients and instructions for recipes, lists of parts for a space shuttle, lists of student records, and
many other applications.
One problem with this class, as we have developed it here, is that the maximum number of elements that
can be contained in the vector is fixed when we construct a new vector. If we make the size too small
we will be in trouble because the add operation will eventually fail. On the other hand, if we make the
size too large we will be wasting memory. Exercise 4.9 below discusses one way around the first of these
problems. The next section introduces a very useful technique that will allow us to solve both of these
kinds of space problem at once.
Exercise 4.1 Rather than using the toString() method, we can access the contents of an
IntegerVector directly when we want to display them. In this case we can find out the number
of elements (using the length method) and then access the elements (using the get method).
Write a program which displays a vector in this way.
Exercise 4.2 Write a method deleteAll which will delete all the elements in a vector (this
is very easy when you see how to do it — think about how we know how many elements there
are in a vector).
Exercise 4.3 Write a method to allow the input of vectors. This should first read an integer
giving the length of the vector followed by that number of elements. For example: 3 12 74 89
should be read as a vector of three values (12, 74 and 89).
Exercise 4.4 Write a method for the IntegerVector class that will reverse the contents of
the list.
Exercise 4.5 Write a second, overloaded position method for the IntegerVector class that
takes a “starting point” (the position in the list from which it should start searching for the
given item). This can be used to find the position of duplicate values in the list.
40
Exercise 4.6 Another useful operation for many ADTs is the ability to assign one to another.
For example:
Exercise 4.7 We might want to provide another method called add for our vector class to allow
two IntegerVectors to be added together. There are (at least) two different interpretations for
what this might mean for vectors of integers. What are they? Write the method to implement
one of the two possible meanings.
Exercise 4.8 The set method has an assertion which fails if an attempt is made to access
an element which has not been added to the vector. Change this to allow such accesses to be
made, as long as the access is still within the bounds of the size of the vector (this will allow
us to use our vectors more like normal arrays in Java). What implications does this have?
Exercise 4.9 Following on from the previous exercise, we can get around the size constraints
for our vectors if we are prepared to increase the size when accesses are made out of range
(particularly when adding new items to the vector). Change the add and set operators so that
such accesses cause the size of the vector to be increased. To do this you will need to allocate
a new data array of the required size, and then copy across all the elements from the existing
data array.
Exercise 4.10 Develop a student record class and then change the IntegerVector class to
handle lists of student records rather than integers. Is this very difficult?
Exercise 4.11 Develop a class for lists with specified upper and lower bounds, and range
checking, as in Visual BASIC. Your class should be able to be used as follows:
41
4.3 Linked Lists
In Exercise 4.9 above we explored one way of getting around the size constraints that apply to the vectors
that we developed in the previous section. This approach will work but involves the rather cumbersome
copying of the existing data every time we need to increase the size of the vector. Dynamic data structures
provide us with a much better solution to this problem. Instead of setting aside a fixed amount of space
for the items in a list, we can initially set aside no space at all and then allocate just enough space for
each new element as it is added to the list. We can get around the copying requirement by keeping each
data item in its own area of memory and linking these all together using references (often called pointers
in this context). This is referred to as a dynamic data structure (because it is so easily changed). More
specifically, we refer to this as a linked list, due to the way in which the items are kept together using
references (or pointers, or links, as they are sometimes called in this context).
Let us work through the development of a class that will allow us to store lists of integers using this
technique. As we do so, compare what we are doing here with the vector class of the previous section,
and think about the differences between the two approaches. The class diagram for this new class is
shown below. Notice the similarities between it and the class diagram for the IntegerVector class.
IntegerList
first, numElements
Data Members
The full listing of this “linked list of integers” class (IntegerList.java) can be found in Appendix A of
the notes. The private section of the class is as follows:
This gets very interesting as we have one class (ListNode) declared inside another class (IntegerList).
ListNode is what is known in Java as an inner class (for obvious reasons!). The class diagram for the
ListNode class is shown below.
ListNode
data, next
42
We need to think very carefully about the visibility of the members of these classes. If we examine the
declaration of ListNode we see that its members (it only has two) are both public. But, because the
declaration of ListNode itself is as private class ListNode we can only access these fields and use
the ListNode class from within the IntegerList class. In other words, the inner class ListNode and its
members data and next are effectively only visible to the methods of IntegerList. This is one of the
rare occasions where it is permissible to use public data members in a class.
The ListNode class is the key element in building the linked lists which we are using for our lists of
integers. A single instance of this class can be pictured in the following way:
In itself that is not particularly useful, but we can create a list made up of a number of these individual
nodes, using the next references to link them together:
The only remaining problem is to locate the start of this list of nodes. This is the purpose of the first
reference variable in the IntegerList class. If we add this into the picture we get:
Of course, this shows a list to which several elements have been added. We need to consider how an
empty list is first created, and how we can add elements to such a list.
Constructor
The creation of an empty list is handled by the constructor for the IntegerList class:
43
{ first = null;
numElements = 0;
} // IntegerList constructor
Thus, when we create a list (e.g. IntegerList list1 = new IntegerList();) we have the following
situation:
With the constructor behind us, we can start to think of the operations that we might want to perform
on our lists. These will be the same operations as we identified at the start of this chapter. The first is
adding a new element to a list.
The simplest possible case is where we always add new elements to the beginning of a list (why is this
the simplest case?). If we choose this approach our add method will look like the following:
Let’s assume that we are adding the value 17 into a list which already contains the values 3 and –8. The
existing list will look like this:
After creating the new node (line 3) and setting its data field (line 4) we will have the following picture:
44
The next step (line 5) is to set the next field of the new node. This is assigned the value of the first
reference, giving:
The final linking step (line 6) sets the first pointer to point to the new node, giving:
45
The last thing we do is increment the number of elements (line 7). After leaving the add method the
local variable node is destroyed, and we are left with the following picture:
This is exactly what we want, with the new node first in the list of three nodes.
Exercise 4.12 Work through the add method when adding an element to a list that was
previously empty, and satisfy yourself that it is correct in this case too. Draw diagrams of the
changing situation like those above.
This is all very well, but it is rather limiting if we can only add new items to the front of the list. What
if we want to add new nodes to the end of the list, or in the middle of the list? The following version of
the add method allows us to add a new item anywhere in the list, with the default being at the end of
the list. From the client’s point of view this is identical to the add method for the IntegerVector class,
and numbers the positions in the list from zero in the same way.
46
4 ListNode node = new ListNode();
5 node.data = item;
6 ListNode curr = first,
7 prev = null;
8 for (int k = 0; k < position && curr != null; k++)
9 // Find position
10 { prev = curr;
11 curr = curr.next;
12 }
13 node.next = curr;
14 if (prev != null)
15 prev.next = node;
16 else
17 first = node;
18 numElements++;
19 } // add
This is complicated by the need to find the correct position in the list. This is done by the for loop at
lines 8–12. Notice how we need to check both that k < position and that curr != null in this for
loop. This is to make sure that we don’t “run off the end” of the list if position has been given a value
greater than the length of the list. In this case curr will become null at the end of the list and the new
node will be added at the end.
Exercise 4.13 Trace through the execution of this method when adding a new element to the
beginning, middle and end of a list, and satisfy yourself that it works correctly in all three of
these cases. Does it work correctly when adding an item to an empty list?
Accessor Methods
The next methods that we need to consider are the get and set operations which will allow us to access
the contents of the linked list a little like an array. The definition of the get and set methods to do this
is:
47
} // set
This allows us, as clients of the IntegerList class, to use lists in the following way:
Efficiency Now, while the methods that we have developed above appear to allow us to do exactly
the same things with an IntegerList as we can do with the IntegerVector class, there is a very big
difference in the way in which they are implemented, and this has major implications for the efficiency
of the accessing operations. For an array, as used in the IntegerVector class, the items are stored in
consecutive memory locations and so accessing any element of the array is a simple matter of adding
an offset (the index) to the starting address of the array to find the location of the item which is being
referred to. This is a quick and efficient operation — a simple addition. For our IntegerList class, on
the other hand, the get and set operations involve a for loop working its way through each item in the
list, counting as it goes along until the right element is found. This is potentially a very slow, inefficient
operation, particularly if the list is very long and the items being referred to are located near the end of
the list. As a result, one should use these methods very carefully with the linked list implementation.
The next method we will consider is the toString method to allow us to display the contents of an
IntegerList conveniently:
Notice again the use of a StringBuffer for efficiency. Note too the use of the for loop in this method.
This is a common idiom for working through linked lists in Java. This makes use of a pointer (curr
in the example above), which starts off at the node pointed to by the first pointer of the list we are
working through. While the curr pointer is not null (i.e. while we have not reached the end of the list)
we work through from one element to the next by setting curr to the next field of the current node
(curr = curr.next).
48
Other Methods
The idiom used above in the toString method is used again, in a particularly interesting way, in the
next method we consider: the position method. This is used to find a particular element in a list.
Here the body of the for loop is completely empty! All the “work” of the loop is done in the control
section. While this is a common idiom, it is necessary to identify it clearly as an empty loop, hence the
very clearly indented and commented ; terminating the for loop. Note that the && operator in Java uses
short-circuit evaluation of conditional expressions, and so the test curr != null && curr.data != item
is quite safe.
The remove method is as follows:
There are two points to note about this method. Firstly, the checks in lines 2 and 10 are testing the
same thing in two different ways, namely that the value of the parameter position is within the range
of items in the list. The assertion will only fail if the list is “broken” in some way (i.e. if there is some
“internal” problem with our implementation of the linked list). Secondly, we again need a special case
for the deletion of the first node in a list, just as we did for adding the first node in a list. This is done
by the if statement in lines 11–14.
The last method that we need to consider is length, which reports on the number of elements in the list.
This is identical to the equivalent method in the IntegerVector class. Why?
49
Some Comments
That completes our basic set of operations for lists. One of the features of this class is that the client
interface is almost identical to that of the IntegerVector class. The major implication of this is that
client programs using lists of integers do not have to be changed2 to use one or the other of the two
different classes. What we have successfully done is to hide all the details of the implementation from the
client. We can choose the implementation (array or linked list) that we want in different circumstances
with very little impact on the client programs. This highlights the power of abstraction.
Exercise 4.14 Write a method deleteAll which will delete all the elements in a list. How
does this differ from the same method for the IntegerVector class (Exercise 4.2)?
Exercise 4.15 Write a method to allow the input of lists of integers (see Exercise 4.3).
Exercise 4.16 Write a method for the IntegerList class that will reverse the contents of the
list (see Exercise 4.4).
Exercise 4.17 Write an overloaded position method for the IntegerList class that will
allow duplicate values to be located (see Exercise 4.5).
Exercise 4.18 Write an assignment method, assign for the IntegerList class (see Exercise
4.6).
Exercise 4.19 The common special case of adding a new item to the end of the list can
be simplified by keeping a reference to the last item in the list (say, last). Modify the
IntegerList class to use this approach.
Exercise 4.20 Quite often programs follow a common pattern of getting an element of a
linked-list ADT immediately followed by setting the value. For example:
if (lst.get(k) == 3)
lst.set(k, 9);
If the get method “remembers” the value of the index and keeps a pointer to this element of
the list, then we can jump straight there for the subsequent set operation. Furthermore, we
can also make use of this mechanism to jump into the middle of the list for subsequent accesses
at or beyond this point (e.g. a get operation at position k+1 does not need to start from the
beginning of the list again).
The “remembered” (or “cached”) values of the index and reference into the middle of the list
can be set by any of the methods that find things in the middle of the list (i.e. get, set and
position). Similarly, the cached values can be used by both get and set.
We need to be very careful when we implement this: any intervening changes to the list (using
the add or remove methods) will potentially invalidate the optimisation.
Modify the IntegerList class to use these optimisations.
2 The only change necessary is to declare objects as IntegerList rather than IntegerVector. This is a trivial naming
issue.
50
4.4 Generic Lists
In the last section we commented on the powerful data hiding aspects of Java classes, and on how we can
implement lists of integers in different ways without affecting the clients of the classes. This is very useful,
but of course the restriction that we are dealing only with lists of integers remains. Java provides us with
mechanisms that allow us to develop lists of any class type. Most simply, we can use polymorphism for
this purpose. In Java 5.0, some new features were added to the Java programming language, which allow
us to develop generic classes with greater safety and ease-of-use.
Data Members
The class itself is very similar to the previous examples, but we now use Object in the place of int in
the definition of our ObjectList class. The first place where this is apparent is in the declaration of the
inner ListNode class. Here are all the data members of the ObjectList class:
Notice how the data field is now defined to be of type Object. In the same way, in the methods of our
new class we can use Object rather than int, but the algorithms stay exactly as they were before.
The Methods
. . .
public ObjectList () // Constructor
{ first = null;
numElements = 0;
} // ObjectList constructor
51
public void add (Object item, int position)
// Place the new item in a ObjectList
{ if (position < 0)
throw new IllegalArgumentException("position is negative");
ListNode node = new ListNode();
node.data = item;
ListNode curr = first,
prev = null;
for (int k = 0; k < position && curr != null; k++)
// Find position
{ prev = curr;
curr = curr.next;
}
node.next = curr;
if (prev != null)
prev.next = node;
else
first = node;
numElements++;
} // add
52
{ if (index < 0 || index >= numElements)
throw new IndexOutOfBoundsException("index is out of range");
ListNode curr = first;
for (int k = 0; curr != null && k < index; k++)
curr = curr.next;
assert curr != null;
return curr.data;
} // get
} // class ObjectList
If you compare these methods with the equivalent ones from the IntegerList class you will see that the
algorithms are almost identical. All that has changed is that a few parameters and variables are now
declared as type Object rather than int. Also, in the position method, we have had to use .equals
for the comparison rather than ==.
53
Using the ObjectList ADT
Note: This entire section deals with the client view, and will not be highlighted.
The previous section covered the implementor’s view of a generic class using polymorphism. How do we
use this from the client perspective? For our generic list class we can create a new list in the following
way:
We can then use iList in much the same way as objects of our earlier IntegerList class. The only
restriction is that, since the ObjectList class works with objects, we need to make use of the Integer
wrapper class:
iList.add(new Integer(3));
iList.add(new Integer(-7));
iList.add(new Integer(56));
In fact, thanks to a further new feature of Java 5.0 (autoboxing, which automatically converts primitive
types to an object of the equivalent wrapper class), we can dispense with the explicit creation of new
Integer objects:
iList.add(3);
iList.add(-7);
iList.add(56);
It is important to note that the Integer objects are still created in this case (the two code segments
above have exactly the same effect). The only difference is that the compiler manages the creation of the
wrapper objects automatically in the second example.
Returning to the use of our generic class, if we wrote a student record class, we could create a list of
student records:
And so on, and so on! Our ObjectList class is completely generic, and can be used to work with lists of
any type of object we require, even mixed lists containing different types of object.
One point that we need to bear in mind is that the get method returns a reference of type Object. This
means that when we retrieve an object from one of these lists we need to take some care. In particular,
the following code will not work, as we will get a compiler error:
Student st;
st = classList.get(5); // Retrieve sixth student
How can we retrieve the objects then? The way to do this is to use a type cast to convert the Object
reference returned by get to the correct type. If we are completely sure that the object we are retrieving
is of a certain type (Student in our example above) we can simply write the following:
54
Student st;
st = (Student)classList.get(5); // Retrieve sixth student
If we are less certain of ourselves, it is possible to be really careful and check first before doing the type
cast (which would result in a run-time error if we are wrong). This makes use of the Java instanceof
operator. This is a boolean operator that takes a reference variable and a class name, and gives a result
of true if the object is of the correct type. It can be used as in the following example:
Student st;
Object obj = classList.get(5); // Retrieve sixth object
if (obj instanceof Student) // Check type
st = (Student)obj; // It’s a Student so the type cast will work
else
System.err.println("Class list contains non-student object!");
Exercise 4.21 Develop a generic version of the array-based IntegerVector class using poly-
morphism. Call your new class ObjectVector. How different is it to the IntegerVector
version?
The problem is that the ObjectList class is able to hold any and all kinds of objects, and the compiler
cannot detect whether the kind of usage that we see above is sensible in the context of the program that
is using an ObjectList.
In order to solve these kinds of problems, Java 5.0 introduced generics. This feature allows us to write
a class to handle data of some unspecified type, but with strong type-checking by the compiler and
automatic type conversions. We will now see how this can be used to develop a truly generic list class.
The key to creating a generic list class is to parameterise the type of the data that is to be stored in the
list. In other words, client programs need to be able to say: “we want a list of objects of type X”. The
compiler then needs to be able to check that the list is being used correctly (e.g. that we don’t try to
store integers in a class list of students).
The syntax that is used for this in Java is quite simple:
55
This states that the GenericList class will work with objects of some unspecified type, called T here.
Within the class we can use the type parameter T as if it is a class type. So, for example, the ListNode
inner class now becomes:
The type parameter T is then specified by the client program, when the class is used:
The compiler can, and will, now check to ensure that only Student objects are stored in the class
list. Any attempt to store the wrong kind of data in the list will result in a compile-time error.
Returning to the rest of the class, writing the methods, etc. is quite easy, as all we need to do is change
each mention of Object in the ObjectList class to the type parameter T. The class is shown below (with
comments removed to save some space).
public GenericList ()
{ first = null;
numElements = 0;
} // GenericList constructor
56
else
first = node;
numElements++;
} // add
57
int k;
for (k = 0;
curr != null && !curr.data.equals(item);
k++, curr = curr.next)
; // Search for item in GenericList
if (curr == null) // item was not found
return -1;
else
return k;
} // position
} // class GenericList
Note: This entire section deals with the client view, and will not be highlighted.
As already noted, using the class is a simple matter of specifying the type of the data to be stored. This
allows us to do things like:
There are a few things to note that are illustrated by these examples. Firstly, because of the type
information that the compiler now has, it can ensure that only integers are stored in iList, and only
Student objects in classList. This provides far more safety from accidental programming errors than
previously.
Secondly, as is clear from the example above, because the compiler knows the return type of the get
58
method, no type-cast is required as was the case previously3 .
Exercise 4.22 Develop a generic version of the IntegerVector class using the new generic fea-
tures in Java 5.0. Call your new class GenericVector. How different is it to the IntegerVector
version (and ObjectVector, if you have done Exercise 4.21)? (Note that the java.util pack-
age already contains a class called Vector that provides very similar functionality).
Skills
• You should know how to use the following Java features: class constructors and polymorphism
• You should be familiar with the use of linked lists, and with the Java syntax and idioms used
for linked lists
• You should be aware of the advantages of data-hiding, from the client and implementor view-
points
• You should know how polymorphism in Java can be used to create generic data structures
• You should know how the generic facilities in Java 5.0 can be used to create generic data
structures, and how these differ from the use of polymorphism
• You should be aware that the Java class libraries already contain classes for many common
ADTs
3 In fact, a type-cast is still required, but it is inserted automatically by the compiler, using the type information, which
it has available.
59
Chapter 5
Objectives
• To consider problem solving algorithms as a particular application of these abstract data types
• To study the following implementation techniques for linked lists: doubly-linked lists, circ-
ularly-linked lists and list header nodes
5.1 Introduction
In chapter four we developed classes for simple lists of items. Stacks and queues are just specialised lists.
They are defined by the way in which we add items to and remove items from the lists. For a stack, items
are always added and removed at one end (more usually called the top). This is analogous to the way we
deal with a stack of plates in a kitchen, or a stack of tins or boxes in a supermarket: a new item is put
on the top of the stack, and when an item is removed it is the top one again. On the other hand, items
are added to a queue at one end (usually called the tail ), and are removed from the other end (usually
called the head ). Again this is analogous to the kinds of queues that we are used to seeing in places like
post offices and banks: people join the end of the queue and leave it when they have reached the head of
the queue.
Both of these behaviours arise very frequently in programming computers, and so these are probably two
of the most common data structures. In this chapter we will study the implementation of generic classes
for stack and queue data structures, and will consider some of the possible uses of these structures. In
the last section of this chapter we will consider a more general data structure: the double ended queue,
or deque 1 as it is usually called. The deque will also be used to introduce some further techniques for
1 Pronounced “deck”. Some authors spell it dequeue.
60
implementing linked lists.
5.2 Stacks
As already mentioned, stacks are characterised by the fact that items are added to and removed from a
single end. One effect of this is that when we remove an item, it is the item that was most recently added
to the stack. For this reason a stack is also commonly referred to as a “last in, first out” (or LIFO) list.
We call the operation which adds an item to a stack push, and the operation which removes an item from
a stack pop. We thus speak of “pushing” and “popping” values.
Since a stack is just a special case of a list we can choose to implement it in either of the two ways that
we used for general lists in the last chapter (i.e. using an array, or using a linked list). We will consider
both of these approaches in turn.
top -1
0 1 2 3 ... max
stack ? ? ? ? ... ?
If we push the character ’G’ onto this empty stack the picture becomes:
top 0
0 1 2 3 ... max
stack G ? ? ? ... ?
If we now push the character ’e’ onto this stack the picture becomes:
top 1
0 1 2 3 ... max
stack G e ? ? ... ?
And so on. By the time we have pushed on all the letters of the name “George” we would have the
following picture (assuming max > 5):
top 5
0 1 2 3 4 5 ... max
stack G e o r g e ... ?
If we now pop an item from the stack we will get the character ’e’. And the picture becomes:
top 4
0 1 2 3 4 5 ... max
stack G e o r g ? ... ?
61
Another pop operation would give the character ’g’ and the value of top would become 3, and so on.
Of course, the usual disadvantage of an array arises with this implementation, namely that the size of
the stack is fixed (at max + 1 characters in the example above). If this limit is made too large we waste a
lot of space in memory. If it is too small we may run out of space for pushing items during the execution
of a program.
How can we develop an array-based stack as a Java class? As far as private data members are concerned
we will need to have both the top of stack index and an array of data items. This leads us to the outline
of the class shown in the following class diagram (we will call our class ArrayStack to differentiate it
from the one we will develop later using a linked list):
ArrayStack
data, topIndex
We will develop this class so that it conforms to the following generic interface describing the functionality
required for all stack implementations.
} // interface Stack
Note the use of generics in Java to specify that a stack may work with any type of object.
62
{ this(100);
} // Constructor
...
} // class ArrayStack
There is one small issue here to do with the use of the generic features in Java 5.0. For various reasons,
which we won’t go into here (see [2] for details), we cannot create an array using a generic type parameter,
such as T in this case. This is relatively easy to work around, as we are allowed to create an array of
Object and type-cast this, as we see in the first constructor in the code above. Otherwise, note how
similar this is to the equivalent parts of the IntegerVector class.
Operations
Moving on to the more interesting parts of this class, how do we implement the push and pop operations?
This is quite straightforward in fact, and only takes a few lines of Java:
Note that the use of the prefix ++ operator in the push method and the postfix -- operator in the pop
method is very important. Why? Notice the checks on the preconditions here.
While these operations are all we need in order to use stacks, they are very minimal. Two more simple
operations provide us with greatly improved functionality. These methods allow us to examine the data
item on the top of the stack without removing it from the stack, and to tell whether or not the stack is
empty.
Note just how little the top method differs from the pop method. This completes our array-based stack
implementation. The complete listing of this class (ArrayStack.java) can be found in Appendix A. We
will consider the use of this data structure in client programs a little later.
63
Exercise 5.1 Write a method to delete all the items on the stack (this is very easy to do).
Exercise 5.2 Write a toString() method that can be used to print or display the contents
of a stack. In what order should the contents be displayed?
Exercise 5.3 Exercise 4.9 (see p. 41) gives a suggestion for dealing with the space constraints
discussed above. Implement these ideas for the ArrayStack class.
To add an element to the stack we need to perform the following sequence of steps:
• Set the “next” pointer of the new node to point to the current top of stack
• Set the top of stack to point to the new node
That’s all there is to it. Let’s add the value ’G’ to our stack. After the first of these two steps we get:
64
If we now add the character ’e’ to our stack we will get the following picture after the first step:
By the time we have pushed all of the letters in the name “George” onto our stack we will have the
following picture:
So, pushing items onto the linked list stack is very simple, what about popping items off the stack? Well,
this turns out to be just as easy. As far as the pointers are concerned all that needs to be done is to set
the “top” pointer to the “next” field of the current top item. This would give:
As long as we still have a reference to the old first element we can retrieve its data (the letter ’e’ in this
case) and then return it, giving:
Let’s have a look at the Java class we will need to implement this form of the stack data structure.
65
The class diagram is shown below. We will call the class ListStack to distinguish it from the array
implementation of the previous section, but will implement exactly the same interface.
ListStack
topNode
Data Members
. . .
} // class ListStack
Just as we did for the general lists of the last chapter we have again used an inner class to describe the
nodes of the linked list with data and next fields (see the class diagram below). The only other private
information is the top of stack pointer, topNode.
StackNode
data, next
Constructor
The constructor for this class is very simple. All we need to do is ensure that the top of stack pointer is
correctly initialised (although this is not strictly necessary in Java):
Operations
Considering the push operation first, we need to create a new node and link it into the list in the way
described previously. This gives the following code:
66
1 public void push (T item)
2 { StackNode node = new StackNode();
3 node.data = item;
4 node.next = topNode;
5 topNode = node;
6 } // push
We start by creating a new node (line 2), and then setting its data field to the value of the item we are
pushing onto the stack (line 3). Lines 4 and 5 then do the necessary juggling of the pointers: setting
the next field of the new node to point to the top of the stack, and then resetting the top of the stack
to point to the new node. Note that the order of these two operations is very important. What would
happen if we reversed them?
The pop operation is just as simple:
1 public T pop ()
2 { if (topNode == null)
3 throw new EmptyException("stack is empty");
4 T tmpData = topNode.data;
5 topNode = topNode.next;
6 return tmpData;
7 } // pop
Note the check in line 2 to ensure that the necessary precondition (that the stack is not empty) is met.
We need to use a temporary variable here. In line 4 we use tmpData to store the data field of the node we
are about to remove from the list. This is done so that we can return this value at the end of the method
after we have reset the topNode pointer. Line 5 resets the top of stack pointer, effectively removing the
node from the top of the stack (and making it eligible for garbage collection). We can still access the
data that it contained using the tmpData variable, of course, and this is used in line 6 to return the data
value.
The top method is a lot simpler. Since we do not need to delete the node in this case, we are spared the
necessity of using a temporary reference to the data field.
The last method to consider is the isEmpty method, which allows us to check the state of the stack. This
is very simple (indeed we have had if statements checking this precondition in the pop and top methods
already).
And that is essentially all there is to the linked list implementation of a stack. As usual this implemen-
tation has the advantage that space is allocated as it is needed. This has two implications. Firstly, no
space is wasted — nodes are allocated only for items that are currently on the stack. Secondly, the only
limit on the size of the stack is the total amount of memory available. The complete listing of this class
(ListStack.java) can be found in Appendix A of the notes.
67
Exercise 5.4 Write a toString method for this class.
Note: This entire section deals with the client view, and will not be highlighted.
As was mentioned in the introduction to this chapter, stacks have many uses in computer programs. In
this section we will have a look at a very simple application which uses a stack. This is the problem of
reversing a string, which we solved recursively in chapter three (see p. 17). The “last in, first out” nature
of a stack is exactly what we need in order to reverse a string. We work through the string character by
character, pushing each one onto a stack. When we reach the end we can pop each one off the stack and
write it out. The code for this is very easy:
import java.io.*;
import cs2.ListStack; // Or ArrayStack
char ch = (char)System.in.read();
while (ch != ’\n’)
{ st.push(ch);
ch = (char)System.in.read();
}
System.out.print("Backwards: ");
while (! st.isEmpty())
System.out.print(st.pop());
System.out.println();
} // main
} // class TestStack
Notice how we can use either of our stack implementations interchangeably, simply by importing the
appropriate class and using the corresponding class name (as shown in the comments in the program
above). This is due to the fact that we kept to the same interface for both classes. Indeed, if we had
chosen to do so, we could have initially developed the array implementation and then replaced it (using
the same file and class names) with the linked list implementation. In this case the program above would
require no changes at all. Furthermore we could have used the interface name (i.e. Stack) as the type for
the variable st, giving us even more flexibility. This is one of the major advantages of the information
hiding provided by Java classes (and similar features in other languages, such as MODULEs in Modula-
2). Client programs, such as this example, can be highly immune to changes in the implementation of
facilities which they use.
68
Exercise 5.5 A stack is a useful data structure for checking for the correct use of parentheses
(brackets) in programming languages. The way this works is that when an opening (left)
bracket is found, it is pushed onto a stack. When a right bracket is found, the top element
of the stack is popped and checked to make sure that it matches the right bracket. If a right
bracket is found, but the stack is empty then the brackets are mismatched. At the end of the
checking process, if there are still elements on the stack, then the brackets are mismatched.
Use one of the two stack implementations from this chapter to write a program to check the
brackets in a piece of text (e.g. a Java program) in this way.
Exercise 5.6 A useful way of handling arithmetic expressions is to use what is known as
Reverse Polish Notation (usually abbreviated to RPN, and also called postfix notation). In
this method of calculation the operands are entered first and then the operation is specified.
For example, to add two and five you would enter: 2 5 +. It becomes very easy, if a lit-
tle unnatural, to express complex expressions in this way. For example, (2 + 5) * 10 - 3
becomes 2 5 + 10 * 3 -, and (2 + 5) * (10 - 3) becomes 2 5 + 10 3 - *. Note from
these examples how there is no need for parentheses in the RPN form.
A stack can be used to easily evaluate RPN expressions, using the following approach:
read a word
if the word is a number then
push it onto the stack
else if the word is an operator then
pop the operands off the stack
perform the operation
push the result onto the stack
Write a Java program to do this using either of the two stack classes developed in this chapter.
5.3 Queues
As discussed in the introduction, queues are characterised by the fact that items are added at one end
and removed from the other. This means that the item which is removed is the one that has been in
the queue for the longest time. For this reason, a queue is also commonly referred to as a “first in, first
out” (or FIFO) list. This behaviour also leads to a slightly more complicated implementation than was
the case for a stack. In this section we will again consider both array and linked list implementations of
queues.
head -1 tail -1
0 1 2 3 4
queue
As items are added to the queue the tail index is incremented, and as they are removed the head index
is incremented. The main problem that arises with simple array-based queues is the way in which such a
69
queue tends to “move” through the array. For example, if we add the letters “abc” to the queue we will
get:
head 0 tail 2
0 1 2 3 4
queue a b c
If we remove two items (i.e. ‘a’ and ‘b’) from this queue we get:
head 2 tail 2
0 1 2 3 4
queue c
If we attempt to add three more letters (“pqr”) we will get overflow. There is no more space at the tail
of the queue, although there are still unused elements in the array.
head 2 tail 5
0 1 2 3 4
queue c p q r
This is known as the travelling queue problem. There are a number of solutions that can be developed
to get around this. A simple one is to move up the data in the array when overflow becomes a problem.
This approach would leave us with the following situation:
head 0 tail 3
0 1 2 3 4
queue c p q r
While this solves the travelling queue problem it is potentially very inefficient, particularly if the queue
is long and always close to full. A better solution is to make use of a circular queue. In this situation the
array conceptually loops back on itself, so that the result of the operation above would be the following:
head 2 tail 0
0 1 2 3 4
queue r c p q
Removing an item from the queue would give us ’c’, and the following situation:
head 3 tail 0
0 1 2 3 4
queue r p q
head 3 tail 1
0 1 2 3 4
queue r z p q
And so we could continue. The maximum size of the queue is still fixed (at five in this example) but the
problem of overflowing the queue when there are still vacant slots is prevented. Let’s look at how we
could implement this as a Java class.
70
The Queue Interface
Again, we will use a common interface for both of the queue implementations that we will develop, and
will make use of the generic features in Java 5.0.
} // interface Queue
Data Members
Constructor
The constructor is also very similar to that for the ArrayStack class:
Operations
Adding an item to the queue involves incrementing the tail index and inserting the new element. However,
there are several points which need to be handled carefully. Firstly, we need to ensure the “wrap around”
71
behaviour required for the circular queue. This can be done as follows:
tl = tl + 1;
if (tl >= data.length)
tl = 0;
An alternative way that is often seen is to use the “mod” operator (% in Java) as follows:
tl = (tl + 1) % data.length;
The if statement in line 5 is used to catch the case where the queue becomes full (the tail index catches
up with the head index). While this is a precondition, it is difficult to check at the beginning of the
method, so we check it only after updating the tl subscript. The if statement in line 8 deals with the
necessity of updating the head index when the first element is added to an empty queue.
The remove method is also fairly simple. The important points here are that we cannot remove an item
from an empty queue, and also the way we handle the removal of the last item from a queue.
The if statement in line 2 checks that we are not attempting to remove an item from an empty queue.
The first part of the next if statement (line 5) handles the case where the last item is removed, setting
72
both the head and tail indices to −1 to signify that the queue is empty again. Note again the wrap-around
behaviour in the else clause (lines 8–10) required for the circular queue to work correctly.
The last two methods allow us to examine the item at the head of the queue and to tell whether the
queue is empty. They are both quite simple and similar to their counterparts in the stack implementation.
The full implementation of the ArrayQueue class can be found in Appendix A of the notes (the file is
ArrayQueue.java).
Exercise 5.7 Write a method to remove all the elements of a queue (again, this is very easy
to do).
Exercise 5.9 Rather than using the circular queue implementation, modify the class to use
the alternative approach discussed at the beginning of this section (i.e. moving the data values
up when the tail of the queue reaches the end of the array). Is this any easier to implement?
Design a program to test the efficiency of the two implementations and measure which is the
most efficient for short queues and long queues.
If we add three items to this queue (the letters “abc”) we will get:
73
Removing a letter (i.e. ’a’) from this queue will give:
One point to note is that we could do without the tail pointer if we were prepared to follow the linked
list from the head every time we wanted to add an item to the tail of the queue. This would obviously
be a lot less efficient than the approach illustrated above.
ListQueue
hd, tl
Data Members
Turning to the implementation of a queue using a linked list, the data members of the new class are as
follows:
Constructor
As is often the case for linked list data structures, the constructor we require is very simple:
74
Operations
Most of this should be reasonably familiar, or at least not too difficult by now. The main subtleties here
are the different cases which arise when updating the head and tail pointers. The first part of this process
(in lines 5 and 6) is used to link the new node into the list of elements, if there are any existing elements.
The tail pointer is then updated to point to the new element (in line 7). The last step (lines 8 and 9) is
a check to make sure that the head pointer is updated if we have just placed the first item in a queue.
Trace through this sequence of steps, say for adding the letter ’d’ to the last example queue above, and
for adding a new element to an empty queue.
The remove method is as follows:
As for our stack class, note the need for the temporary data variable (declared and initialised in line 4).
The heart of this method is in the central part: on line 5 the head pointer is updated to remove the first
element from the list (there is still a reference to this element’s data in the tmpData variable, of course).
Lines 6 and 7 then deal with the case where the last element has just been removed from the queue and
the tail pointer needs to be set to null to reflect this.
Lastly, the head and isEmpty methods are again very simple:
The full listing of this class can be found in the file ListQueue.java in Appendix A of the notes.
75
Exercise 5.10 Write a method to delete all the items in a queue.
Exercise 5.11 As suggested in the discussion above (p. 74), we do not need the tail pointer
if we are prepared to follow the list from the head each time we add an element to the queue.
Modify the class to use this approach, and then write a test program to measure the difference
in efficiency (repeatedly add and remove elements using both queue implementations, and
measure the time taken in each case).
Exercise 5.12 Modify the ListQueue class to create a priority queue class. A priority queue
differs from a normal queue in that each item on the queue has an associated integer priority
value. When an item is added to the priority queue, it is not necessarily added at the end of
the queue, but is placed in its correct position with regard to the priority of the other items
in the queue (usually in ascending order, from head to tail). When an item is removed from a
priority queue, it is always the item at the head of the queue — i.e. the one with the highest
priority.
You will need to change the add method to take a priority value, along with the item being
added. You will also need to provide a new method to retrieve the priority value of the item
at the head of the queue. Note that, as a result of these changes, the class you develop will no
longer conform to the Queue interface.
Note: This entire section deals with the client view, and will not be highlighted.
A number of applications in Computer Science, particularly in the field of artificial intelligence, require
searching through all, or many, of the possible solutions to a problem. Queues can be very useful for
keeping track of partial solutions that still need to be explored further. In this section we will develop a
simple problem-solving program which makes use of a queue in this way. The problem we will tackle is
that of finding a path through a maze.
We can represent a maze as a matrix (i.e. a two-dimensional array) of boolean values indicating whether
a block is open or closed. An example of such a maze is shown below (for simplicity, the true values are
shown as 1’s and the false values as 0’s).
0 0 0 0 0 0 0 0 0 0
0 1 1 1 1 1 1 1 1 0
0 1 0 1 0 0 0 1 0 0
0 1 0 1 1 1 0 1 0 0
0 0 1 1 0 1 0 1 0 0
0 0 1 0 0 1 0 1 0 0
Entrance→ 1 1 1 0 1 1 0 0 1 1 →Exit
0 0 0 0 1 0 1 0 1 0
0 1 1 1 1 1 1 1 1 0
0 0 0 0 0 0 0 0 0 0
The search strategy that we will use makes use of a queue to keep track of the positions that we still
have to use as starting points for further unexplored paths through the maze. In addition, we need to
keep track of which positions in the maze we have previously explored so that we do not repeat any
76
paths through the maze. This can be handled easily with a second boolean matrix, beenThere. Initially
all positions in beenThere will be false. When we visit a position we can set the corresponding element
of beenThere to true, indicating that that position has now been explored and can be ignored if we
come back to it in the course of traversing the maze. With this in mind we can develop the first part of
our program. We will use the linked list implementation of a queue since we cannot be sure how many
potential positions will need to be queued at any time. We will store the positions as records with fields
for the row and column numbers of a point in the maze. The full listing of the program can be found in
Appendix A of the notes (QSearch.java).
import cs2.*;
import java.io.*;
The approach we take to tackling the maze is first to find the entrance. To keep the problem simple we
will require that there is only one gap in the first column (the entrance) and only one gap in the last
column (the exit), and no other gaps in the “outer walls” at all. So finding the entrance is simply a case
of going down the first column to find a gap (a location where the maze matrix has a true value). Once
this is found we can put it onto the queue as the first position which needs to be explored as part of
a potential solution to the problem. At this stage our program looks like this (do not worry about the
readMaze method for now — it simply reads in the matrix of values describing the maze):
readMaze("MAZE");
77
addPosition(r, 0);
...
} // solveMaze
The addPosition method simply creates a new Position object and places it onto the queue:
We can now picture our main data structures as follows. Note that the subscripts for the maze and
beenThere matrices have been shown to clarify the discussion, and that the representation of the queue
has been simplified. Note too that this maze is slightly different to the one shown above.
maze
0 1 2 3 4 5 6 7 8 9
0 0 0 0 0 0 0 0 0 0 0
1 0 1 1 1 1 1 1 1 1 0
2 0 1 0 1 0 0 0 1 0 0
3 0 1 0 1 1 1 0 1 0 0
4 0 1 1 1 0 1 0 1 0 0
5 0 1 0 0 0 1 0 1 0 0
6 1 1 1 1 1 1 0 0 1 1
7 0 1 0 0 1 0 1 0 1 0
8 0 1 1 1 1 1 1 1 1 0
9 0 0 0 0 0 0 0 0 0 0
beenThere
0 1 2 3 4 5 6 7 8 9
0 0 0 0 0 0 0 0 0 0 0
1 0 0 0 0 0 0 0 0 0 0
2 0 0 0 0 0 0 0 0 0 0
3 0 0 0 0 0 0 0 0 0 0
4 0 0 0 0 0 0 0 0 0 0
5 0 0 0 0 0 0 0 0 0 0
6 0 0 0 0 0 0 0 0 0 0
7 0 0 0 0 0 0 0 0 0 0
8 0 0 0 0 0 0 0 0 0 0
9 0 0 0 0 0 0 0 0 0 0
78
With the initial state set up in this way we can enter the main part of the algorithm. This is a loop that
continues until either the exit of the maze is reached or else there are no more entries on the queue still
to be examined (in which case we can conclude that there is no path through the maze). Inside the loop
we remove the next entry from the queue of unexplored positions. We mark the beenThere matrix to
record the fact that we have now explored this position and then try all possible moves from that point.
In outline we have the following:
while (! posQueue.isEmpty())
{ Position nextPos;
The only changes to our data structures at this point are that the queue is empty and position 6,0 in the
beenThere matrix is now set to true (1 in the diagram). How do we handle trying all possible moves
from this position? We need to try the four possible directions which we can move in (up, down, left and
right). For each of these we need to check: (1) that there is no wall in the maze at that position, and
(2) that we have not yet visited that position in the maze. If both of these conditions are met then we
can add the new position to the queue as a position that we still need to explore. This comes out as the
following section of code (replacing the comment // Try all possible moves from this position in
the code shown above).
// Try to move up
if (maze[r-1][c] && ! beenThere[r-1][c])
addPosition(r-1, c);
// Try to move right
if (maze[r][c+1] && ! beenThere[r][c+1])
addPosition(r, c+1);
// Try to move down
if (maze[r+1][c] && ! beenThere[r+1][c])
addPosition(r+1, c);
79
// Try to move left
if (c > 0 && maze[r][c-1] && ! beenThere[r][c-1])
addPosition(r, c-1);
For our current position (6,0) we cannot move up, down or left (note the need to check that c > 0 when
trying to go left, so that we do not attempt to walk back out the entrance!), only right. After this our
data structures will look as follows:
maze
0 1 2 3 4 5 6 7 8 9
0 0 0 0 0 0 0 0 0 0 0
1 0 1 1 1 1 1 1 1 1 0
2 0 1 0 1 0 0 0 1 0 0
3 0 1 0 1 1 1 0 1 0 0
4 0 1 1 1 0 1 0 1 0 0
5 0 1 0 0 0 1 0 1 0 0
6 1 1 1 1 1 1 0 0 1 1
7 0 1 0 0 1 0 1 0 1 0
8 0 1 1 1 1 1 1 1 1 0
9 0 0 0 0 0 0 0 0 0 0
beenThere
0 1 2 3 4 5 6 7 8 9
0 0 0 0 0 0 0 0 0 0 0
1 0 0 0 0 0 0 0 0 0 0
2 0 0 0 0 0 0 0 0 0 0
3 0 0 0 0 0 0 0 0 0 0
4 0 0 0 0 0 0 0 0 0 0
5 0 0 0 0 0 0 0 0 0 0
6 1 0 0 0 0 0 0 0 0 0
7 0 0 0 0 0 0 0 0 0 0
8 0 0 0 0 0 0 0 0 0 0
9 0 0 0 0 0 0 0 0 0 0
At this point we go around the while loop again. This time we remove the position 6,1 from the queue
and mark it as visited in the beenThere matrix. Now when we try all possible moves we find that we
can move up (to 5,1), right (to 6,2) and down (to 7,1). We are prevented from moving left (to 6,0)
although this position is open in the maze because it has been marked in the beenThere matrix. Our
data structures now look as follows (some of the rows have been edited out to save space):
80
maze
0 1 2 3 4 5 6 7 8 9
0 0 0 0 0 0 0 0 0 0 0
···
5 0 1 0 0 0 1 0 1 0 0
6 1 1 1 1 1 1 0 0 1 1
7 0 1 0 0 1 0 1 0 1 0
8 0 1 1 1 1 1 1 1 1 0
9 0 0 0 0 0 0 0 0 0 0
beenThere
0 1 2 3 4 5 6 7 8 9
0 0 0 0 0 0 0 0 0 0 0
···
5 0 0 0 0 0 0 0 0 0 0
6 1 1 0 0 0 0 0 0 0 0
7 0 0 0 0 0 0 0 0 0 0
8 0 0 0 0 0 0 0 0 0 0
9 0 0 0 0 0 0 0 0 0 0
On the next iteration of the while loop we remove 5,1 from the queue and will explore all possible moves
from the position (only to 4,1 in this case) adding them to the end of the queue. On the next iteration
we remove position 6,2 and explore the moves from there (only to 6,3) adding them to the queue. The
next iteration gives position 7,1 and we add 8,1 to the queue as the only possible move from there. In
this way the algorithm continues until such time as we run out of positions to explore or we reach the
exit.
Let’s consider how this algorithm works. At the stage shown diagrammatically above, the queue holds all
the positions that can be reached in two steps from the entrance. As we explore these positions we add to
the end of the queue all the positions which can be reached in three steps from the entrance. Exploring
these will lead to all positions four steps from the entrance being added to the end of the queue. In this
way the search continues dealing with all paths through the maze of a particular length before moving
onto the next length. This means that we are guaranteed to find the shortest possible path through the
maze.
The technique that we have used here is referred to as a breadth-first search. This comes from viewing
the possible solutions to the problem as a tree. For example, we can picture the tree for the point up to
which we traced the execution of the program above as shown in Figure 5.1. At each stage of the search
we try all the possibilities at one level of the tree before moving onto the next level.
Of course, this is not the only strategy we could use for the search. The queue in this problem is being
used to remember positions that need to be explored further. Any data structure can be used to hold
these positions. If we chose a stack rather than a queue what would be the effect? In fact, this leads us
81
Figure 5.1: A Tree View of the Bread-first Search Strategy.
Exercise 5.13 Change the maze-solving program to use a stack rather than a queue, and see
what effect this has.
Exercise 5.14 Change the maze-solving program to use a priority queue (see Exercise 5.12)
rather than a normal queue to provide a prioritised search. If the horizontal distance to the
right-hand outer wall is used as the priority value this will prefer routes that are making
progress towards the right. Make the necessary changes to the maze-searching program and
see what effect this has.
82
Exercise 5.15 It would be useful if our program reported on the route found to reach the
exit of the maze. One way of doing this is to change the beenThere matrix. Instead of simply
storing a true/false value, we can use it to hold the coordinates of the position that led us
to each point in the path. We can then follow the path back from the exit to work out how
we got there. Make this change to the program and get it to print out the route at the end.
Note that the beenThere matrix will still need to be initialised to some default value to show
that no positions have been visited as the program starts. For the first, simple maze the data
structures would look like this at the end:
maze
0 1 2 3 4 5 6 7 8 9
0 0 0 0 0 0 0 0 0 0 0
1 0 1 1 1 1 1 1 1 1 0
2 0 1 0 1 0 0 0 1 0 0
3 0 1 0 1 1 1 0 1 0 0
4 0 0 1 1 0 1 0 1 0 0
5 0 0 1 0 0 1 0 1 0 0
6 1 1 1 1 1 1 0 0 1 1
7 0 0 0 0 1 0 1 0 1 0
8 0 1 1 1 1 1 1 1 1 0
9 0 0 0 0 0 0 0 0 0 0
beenThere
0 1 2 3 4 5 6 7 8 9
0 0,0 0,0 0,0 0,0 0,0 0,0 0,0 0,0 0,0 0,0
1 0,0 1,2 1,3 2,3 1,3 1,4 1,5 1,6 1,7 0,0
2 0,0 1,1 0,0 3,3 0,0 0,0 0,0 1,7 0,0 0,0
3 0,0 2,1 0,0 4,3 3,3 3,4 0,0 2,7 0,0 0,0
4 0,0 0,0 5,2 4,2 0,0 3,5 0,0 3,7 0,0 0,0
5 0,0 0,0 6,2 0,0 0,0 4,5 0,0 4,7 0,0 0,0
6 -,- 6,0 6,1 0,0 6,5 5,5 0,0 0,0 7,8 6,8
7 0,0 0,0 0,0 0,0 6,4 0,0 8,6 0,0 8,8 0,0
8 0,0 8,2 8,3 8,4 7,4 8,4 8,5 8,6 8,7 0,0
9 0,0 0,0 0,0 0,0 0,0 0,0 0,0 0,0 0,0 0,0
83
Exercise 5.16 Another classic problem solving situation involves a farmer, a goat, a lion and
a cabbage! The farmer finds himself on one bank of a river with a goat, a (semi-domesticated!)
lion and a cabbage that he needs to get across the river. Unfortunately his only means of
transport is a small canoe which can only hold the farmer and one other item at any timea .
The problem is that the goat will eat the cabbage if the farmer is not on the same bank, and
likewise the lion will eat the goat if the farmer is not there. How can he get his cargo safely to
the far bank? Obviously there are a number of different states (combinations of the four items
on each bank) and some of them are “safe” and some not. We can try to solve the farmer’s
problem by starting with the state where all the items are on the first bank. We can then try
all possible moves from this position and queue the “safe” ones for further exploration. This
process continues until all the items are on the second bank or no further possibilities exist.
Devise a notation (a data structure) for representing the states in this problem, and then use
the general outline of our maze-solving algorithm to find a solution to the farmer’s problem.
Alter your program to use a depth-first search (i.e. based on a stack rather than a queue) and
see what effect this has on the solution found.
a It is a very large cabbage!
Doubly-Linked Lists
A doubly-linked list is made up of nodes with a data field as usual, but with two pointer fields: one
pointing to the left neighbour and one to the right neighbour. In this case, a list containing the letters
“abc” would look like the following:
84
This form of list allows us to add or remove nodes from either end relatively easily.
Exercise 5.17 Develop algorithms to add and remove elements at either end of a deque im-
plemented as a doubly-linked list.
Circularly-Linked Lists
With this kind of structure we can go one step further and turn it into a circularly-linked list. Here we
take the null pointers at either end of the list and use them to join up with the other end of the list. If
we choose this approach the picture above changes to this:
Note that with this style of implementation one has to be very careful not to end up in infinite loops
when traversing a list. As there are no longer any null pointers to check for, we need to construct the
termination conditions of such loops along the following lines:
if (left != null)
{ curr = left;
do
{ // Handle the current node
. . .
curr = curr.rt;
} while (curr != left);
}
There is one other optimisation we can make to the deque data structure we have here. Note that we
can now find the right end of the deque by following just one pointer from the left end (the left field
of the left-most node points to the right-most node). This means that we can remove the right pointer
completely and represent our deque as shown here:
85
Exercise 5.18 Modify your algorithms from Exercise 5.17 to take this new structure into
account.
The last new idea we need to introduce in this section is that of a list-head node, or just a header node
as it is sometimes called. In many of the algorithms we have studied so far there have been special cases
to deal with adding the first element to a list, or removing the last element from a list. These need not
arise, if the list is never empty! We can ensure this by artificially inserting an unused node into the list.
This is what we call the list-head node (or header node). Of course, our algorithms that manipulate the
data in such lists must be extremely careful not to access the header node as if it contained valid data.
The doubly- and circularly-linked deque shown above would look like this with a header node:
An empty deque would simply have the header node linked back to itself. The situation would look like
this:
86
5.4.2 A Java Class for Deques
So then, how can we implement a deque ADT using these kinds of advanced linked-list techniques? Let’s
look at the data structures and algorithms that we will need. The class diagram for the main class is
shown, but we have not shown the (very simple) class diagram for the inner DequeNode class.
Deque
header
addLeft, addRight,
removeLeft, removeRight,
rightHead, leftHead,
isEmpty
Data Members
The data members are quite simple, and would be the same for any doubly-linked data structure whether
it was circularly-linked or not, and whether it had a header node or not:
Constructor
Turning to the algorithms, the constructor for the class will need to set up the header node. Note how
this is linked back to itself in the way illustrated in the previous diagram.
Operations
Considering first the operation of adding an item to the deque, we will need two versions of this: one to
add items to the left end of the deque and one to add items to the right end. We will just consider the
addLeft method here; the other is very similar:
87
4 newNode.rt = header.rt;
5 newNode.lt = header;
6 header.rt.lt = newNode;
7 header.rt = newNode;
8 } // addLeft
The main point to notice here is that there are no special cases for adding an item to an empty list, since
this situation no longer arises. However, linking the new node into the list is a little more complicated,
since four pointers need to be updated. Let’s consider adding the first node into a previously empty
deque. We will assume that it is a character deque, and that we are adding the character ’a’. After
creating the new node (line 2) and initialising its data field (line 3) we will have the following situation:
88
Having initialised the two pointer fields in the new node, the next steps are to change the links in the
remainder of the list. The first of these steps (see line 6: header.rt.lt = newNode;) would change the
left pointer of any previous node in the list. Since we are adding the first node this has the effect of
changing the left pointer of the header node itself to point to the new node:
The last step (line 7: header.rt = newNode;) sets the right link of the header node to point to the new
node:
89
This is the end result. We can drop the temporary newNode pointer from the diagram, and redraw it
more clearly as follows:
Note how no special case was needed for the fact that this was the first node in the list. Trace through
the addition of a second node in the same way and satisfy yourself as to how it works in this case.
Turning to the next of the methods, we have removeLeft to take an element from a deque (there is also
removeRight which is, again, very similar):
The main thing to notice in this method is the if statement to check that there is a node that can be
removed from the deque. We can no longer just check for a null pointer as there is now always something
to point at (even if it is just the header node). We can tell if the deque is empty in this case by checking
to see if the header node points back at itself. We have chosen to do this in the method above by checking
the right pointer (header.rt == header), but could just have easily used the left pointer in the same
way. Trace through the way this method removes a node from a deque.
The last few methods are again very simple. There are two methods for examining the two end nodes
without removing them from the deque (only rightHead is shown here) and one to check if the deque is
empty (again we could have used either the left or right pointer of the header node for this):
90
public T rightHead () // Return item at right end
{ if (header.lt == header)
throw new EmptyException("deque is empty");
return header.lt.data;
} // rightHead
That ends our look at deques and various alternative implementation techniques. It is important to note
that doubly-linked lists, circularly-linked lists and header nodes are not restricted to the implementation
of deques but can be applied to many data structures. Indeed, doubly-linked lists simplify many of the
algorithms we have looked at already as the extra pointer generally removes the need for a “previous”
pointer when inserting and deleting nodes from a list. In the same way, the use of a header node simplifies
many of the linked-list algorithms we have looked at for inserting and removing nodes, by removing the
special cases needed for handling empty lists.
Exercise 5.19 Change the implementation of the GenericList class from the previous chap-
ter to use a doubly-linked list. Once you have done this, change it to make use of a header
node as well.
Exercise 5.20 Add a method to the Deque class to delete all the elements of the list (excluding
the header node, obviously!).
Exercise 5.21 Write a toString method for the Deque class that can be used to display the
nodes in a deque from left to right.
Exercise 5.22 Rewrite the Deque class to use a circular array rather than a linked list.
Note: This entire section deals with the client view, and will not be highlighted.
The deque is a very general data structure. In fact, we can define the previous data structures that we
looked at in this chapter in terms of a deque. A stack is simply a deque where we restrict addition and
removal of nodes to one end of the deque. Similarly, a queue can be implemented as a deque where
addition occurs at only one end, and removal at the other.
One possible use of a general deque would be in the kind of problem-solving algorithm we looked at with
queues. If we can judge that some potential solutions to a problem might be more likely to give a result
than others we could add them to the head of the queue rather than to the end as in the breadth-first
technique. This would make use of a deque in which we added and removed items from one end and just
added items at the other end (sometimes called an output-restricted deque). The effect of this on our
searching strategy would be to search in a way that was neither breadth-first nor depth-first but partially
prioritised. The success of this would depend on how well we could assess which states might lead to a
result. For example, for the maze problem we might give priority to states where the column position
91
ADT Array Implementation Linked List Implementation
List of int IntegerVector IntegerList
Generic Lists ObjectList and GenericList
Stack ArrayStack ListStack
Queue ArrayQueue ListQueue
Deque Deque
was further to the right (i.e. we appeared to be moving towards the exit). Of course, it is very easy to
construct mazes for which this strategy is poor. For other problems it may be easier to assess the merits
of different partial solutions and give higher priority to those that are likely to lead to a solution faster.
Further applications of deques would be in programs where one needed to model situations where items
could be handled in the more general way embodied in a deque. For example, in playing some card games
it might be possible to add and remove cards from either the top or the bottom of the pack of cards.
When writing a program to play such a game, a deque would be the natural data structure to represent
the pack of cards.
Exercise 5.23 Rewrite the maze-solving program, using a deque to implement the prioritised
search strategy outlined above.
The following quote is taken from the Sun Developer Network’s Core Java Technologies Tech Tips newslet-
ter of 14 December 2005:
Why use a deque? Deques are useful data structures for recursive problems, such as searching
through a maze or parsing source [code]. As you move along a path, you save “good” spots,
adding more data along the way (that is, while you think the path is good). If the path turns
bad, you pop off the bad bits, returning to the last good spot. Here, you would add and
remove from the same end, like a stack. Once you find your way through, you start back at
the beginning to reveal the solution, which is the other end. Other typical examples include
operating system schedulers and bad card dealers who like to deal from the bottom of the
deck — to themselves at least.
92
Skills
• You should be familiar with the following abstract data types: stack, queue and deque
• You should be able to implement these abstract data types using arrays and linked lists
• You should be able to use singly-linked lists, doubly-linked lists, circularly-linked lists and
header nodes
• You should be familiar with some of the uses and characteristics of stacks, queues and deques,
particularly in the context of problem solving algorithms
93
Chapter 6
Objectives
• To consider nonlinear abstract data types, specifically general trees, binary trees, binary search
trees and graphs
• To study the implementation of these abstract data types using dynamic data structures
• To consider some applications of these abstract data types
6.1 Introduction
All the data structures we have considered up until now (lists, stacks, queues and deques) have been
linear. That is, there has been a very simple relationship between one item in the structure and the next.
Each item has had at most two neighbouring items and they have been clearly “on either side” of it. In
this chapter we will be considering data structures that do not have a simple linear relationship between
the items in the data structure. These are trees and graphs. In some ways, as we will see, a tree is just a
restricted form of the more general graph data structure (similar to the way in which queues and stacks
can be viewed as restricted forms of deques). We will begin by studying trees.
6.2 Trees
In fact, we have already met a tree structure in this course, when we considered search strategies for
problem solving algorithms in the last chapter. There we illustrated the positions that could be reached
in a maze as a tree of coordinates (see Figure 5.1, p. 82). In everyday life many situations involve tree-like
structures. A common example is a person’s family tree, as in Figure 6.1. Other good examples are the
hierarchical naming structures used by botanists and zoologists for classifying plants and animals. We
have also seen many examples of inheritance hierarchies, which, in Java, are trees.
94
Figure 6.1: A Family Tree.
The trees S1 to Sm are referred to as subtrees. According to the above definition, a subtree may be empty.
In our example (Figure 6.1) the first node labelled “George” is the root of the entire tree. Below it there
are three subtrees, each of which has two subtrees. The nodes are connected by branches (sometimes
called edges). A node which has no descendants is called a leaf (the nodes labelled “Gary”, “Gayle”,
“George”, “Elizabeth”, “Robert” and “Lorna” are examples of leaves in the tree above). Two nodes
that share the same parent are called siblings (for example, “Eric” and “Anne”, or “Gary” and “Gayle”
above). A node above another node with a branch connecting them is called a parent. In the example
above we can say that the node labelled “Anne” is the parent of the node labelled “Lorna”. Similarly
we identify nodes as children. The node labelled “Elizabeth” is a child of the node labelled “Eric”. The
depth of a node is the length of the path from that node to the root (the node labelled “Robert” has a
depth of 2 above). The outdegree of a node is the number of branches leaving the node (this is 3 for the
first node labelled “George”, 2 for the node labelled “Colin”, and 0 for the node labelled “Robert”). In
general, any leaf node will have an outdegree of zero. A related concept is the indegree of a node (i.e. the
number of branches entering a node). For a tree this can only be zero (for the root) or one (for all other
nodes).
Notice how we usually draw trees “upside down”, that is with the root at the top. Another important
thing to notice about our formal definition of a tree is that it is recursive. Expressed a little less formally,
we have defined a tree as a root node connected to several trees (its subtrees). This naturally recursive
definition leads us into a situation where many of the algorithms that deal with trees are best and most
easily expressed using recursion, as we will see.
A very important class of trees are binary trees1 . These are trees where the maximum outdegree of any
1 In fact, any tree, no matter what its outdegree, can be converted to a binary tree, as we will see shortly.
95
Figure 6.2: A Binary Tree (an Ancestor Tree).
node is two — trees with only zero, one or two children beneath each node. In such a case, we talk of the
left subtree and the right subtree of a node. If the outdegree is either zero or two (i.e. all non-leaf nodes
have exactly two children), we say that the tree is a proper binary tree.
Much of the rest of this chapter is concerned with binary trees. An example of a binary tree would be
an ancestor tree (the inverse of a family tree). For my immediate ancestors we get the tree shown in
Figure 6.2. Notice how every node here has an outdegree of two or zero. Since no one has more (or less!)
than two biological parents, any ancestor tree will be a proper binary tree.
Note that the links between the nodes here allow us to move down the tree easily but there is no way
of moving back up the tree. In fact, this is not particularly serious when we use recursive methods
to traverse the trees. If we are not going to use recursive methods it is still possible to use the above
structure, but it may make life easier if we add a third link to each node, pointing back to the parent
node. In this case our tree would look like this (we have reshaped the boxes representing nodes to save
some space):
96
This structure allows us to move freely up and down through the tree. Algorithms using this structure
need to be extremely careful about the order in which they traverse the tree.
That deals with binary trees, but how might we represent more general trees? If we know the maximum
outdegree of any node we can set aside that number of pointers in each node to point to the child nodes.
This technique might result in a considerable number of unused pointer fields, but it is simple and quite
efficient. If we are faced with the situation where the outdegree of the nodes cannot be predicted, and
may be arbitrarily large, then we will be forced to use a more general list of pointers to keep track of
the child nodes. One way of doing this would be to use a list ADT (such as the GenericList class from
chapter four, or the Java Vector class). This would allow us to add as many child nodes as were needed
for each node in the tree. Fortunately such cases arise very rarely, and we can generally restrict ourselves
to dealing with only binary trees.
How would we convert this to a binary tree? Considering the root node (a) first it has only a left-most
child (b) and no sibling. It will form the root of the new binary tree, with only one child:
97
Considering the node labelled b next, it has a left-most child (e) and a right sibling (c). This gives:
Considering e, it has neither a left-most child nor a sibling, so it will have no children in the new tree.
In the case of c, we have f as the left-most child and d as the right sibling. This leads to:
And so on. Eventually we get to the binary tree shown in Figure 6.3, which is equivalent to the general
tree with which we began.
In most cases, we can traverse this binary tree as easily (and visiting the same nodes in the same order)
as we would have done with the original, general tree. The binary trees created from general trees in this
way tend to be rather “long and thin”, and are not proper binary trees. However there are algorithms
that allow us to reorganise these trees to give us a “flatter” structure if we so desire.
To recap on this section and the previous section: we have now seen how any tree can be represented by
a binary tree. We have also seen how a binary tree can be represented by nodes with three fields: one
for the data, one for the left subtree pointer and one for the right subtree pointer. And, in some cases,
we might need to add a pointer to the parent node as well. We will now turn to the construction of a
generic Java class for binary trees.
98
Figure 6.3: Binary Tree Converted From a General Tree.
Data Members
With this structure in mind, let’s turn to the implementation of the Tree class. The first section is again
quite simple, with just a data field and two pointers to the subtrees:
99
The interesting thing to note here is how we represent a tree by just a single node (indeed, our class might
be better named TreeNode). This stems from the recursive nature of the tree: any node is effectively the
root of some subtree within the overall structure.
Constructors
When creating a new node we will need to specify the value stored in the node. Furthermore we might
want to specify existing trees as the left and right subtrees. We can use overloaded constructors to achieve
this:
Operations
The rest of the methods listed above are very simple. Here are a few of them (right and addRight are
very similar to the equivalent left subtree operations here):
public T getData ()
// Access the data value in the root node of a tree
{ return data; }
Notice how we have checked the precondition to make sure that we can safely add a subtree to a node
without losing a previously existing one.
Exercise 6.1 Write a pair of replace methods for the Tree class that will work like the add
methods, but replacing the existing subtree.
Exercise 6.2 Write a method for the Tree class that will allow client programs to modify the
data stored in a node in the tree. Call your method setData.
100
Figure 6.4: Knowledge-Base Tree.
Exercise 6.3 Working with the trees we have developed in this section is a little difficult
because we have no way of removing subtrees. Write a remove method to do this.
Turning away from the implementation view for a moment, how would we use such trees?
Note: This entire section deals with the client view, and will not be highlighted.
As an example, we will consider the simple child’s game of “guess the animal”. The user thinks of an
animal, and the computer has to try to guess what it is by asking questions that require a “yes/no”
answer. How does this involve trees? Well, the computer has a knowledge base of questions and animals,
which is arranged into a tree structure. The internal nodes are questions, where the left subtree is
considered when the answer is “yes” and the right subtree when the answer is “no”. A simple example
of such a knowledge base is shown in Figure 6.4.
The computer starts at the root of the tree, and then works left (if the answer is “yes”) or right (“no”),
until a leaf node is reached, at which point it has identified the animal (as best as it can). The following
program trace shows an example of a user’s interaction with this program (the user’s inputs are shown
in italics to distinguish them from the program’s output):
Turning to the implementation, let’s look first at the main part of the program, which uses this tree data
structure:
101
3 initTree();
4 System.out.println("Let’s play guess the animal.");
5 pos = root;
6 while (pos != null)
7 { if (pos.left() != null) // Must be a question
8 { System.out.println(pos.getData());
9 if (answer())
10 pos = pos.left();
11 else
12 pos = pos.right();
13 }
14 else // Must be an answer
15 { System.out.println("It’s a " + pos.getData() + ".");
16 break;
17 }
18 }
19 if (pos == null)
20 System.out.println("Sorry, I don’t know the animal.");
21 } // play
The initTree method sets up the tree containing the knowledge base — we will come back to it shortly.
The variable pos is used to move through the tree. We start off setting it to point to the root node. Note
that the while loop is usually terminated by the break in line 16, and not by the condition on line 6. In
line 7 we check to see whether the current node has a left subtree. If it has a subtree then it must be a
question node, and the program asks the question in line 8. The answer method is part of this program.
It simply waits for the user to enter a string. If the first letter is a “y” (upper- or lowercase) it returns
true, if the first letter is “n” it returns false, otherwise it prints out a message asking the user to enter
“yes” or “no” and repeats the process until a valid reply is given. The answer given by the user is used
to make the decision to follow either the left subtree (if the answer was “yes”) or the right subtree. If
the node was not a question, but an animal name, the program prints out the name (line 15) and then
exits the while loop. Note how we use the left and right methods to work through the tree and the
getData method to retrieve the string from a node for output.
The initTree method is responsible for setting up the tree. Ideally, this should be done by reading the
knowledge base in from a file. For this simple example it has been done “by hand” as part of the program.
This illustrates how the addLeft and addRight methods can be used.
p = root.right();
p.addLeft(new Tree<String>("bird"));
p.addRight(new Tree<String>("Does it bark?"));
p = p.right();
p.addLeft(new Tree<String>("dog"));
p.addRight(new Tree<String>("cat"));
102
p.addLeft(new Tree<String>("duck"));
p.addRight(new Tree<String>("fish"));
} // initTree
The root of the tree (referenced by the variable root) is a class variable. The local variable p is used
to work through the tree creating new nodes and building up the structure. This method works in a
“top-down” way starting at the root and working down to the leaves. We could also have done this in
a bottom-up fashion by using the other constructor (the one with subtrees as extra parameters). In this
case we would have created two leaf nodes (say the ones labelled “dog” and “cat”) and then created the
parent question node in this way:
Exercise 6.4 Rewrite the initTree method using the bottom-up approach discussed above.
Be careful to ensure that the root is correctly initialised. Which version of initTree is easier
to understand?
Exercise 6.5 Develop a file layout that will allow you to specify the knowledge base as a text
file, and rewrite initTree to take a file name and initialise the tree from this.
Exercise 6.6 Another way to construct the knowledge base is to get the user to help. In this
case the program starts off with a tree containing just one node. This contains an answer (say
“cat”). The program immediately guesses that the user is thinking of a cat. The user then has
to respond “yes” or “no”. If the response is “no”, the program asks the user to enter both the
correct answer and a question that can be used to differentiate between the wrong answer and
the correct animal. This is used to restructure the tree incorporating the new animal and the
new question. In this way the program can “learn” about new animals. Rewrite the program
to use this approach (you will need to do Exercise 6.1, Exercise 6.2 or Exercise 6.3 first).
In-order: first the left subtree is traversed, then the root node is visited and then the right subtree is
traversed (LNR).
Pre-order: first the root node is visited, then the left subtree is traversed, followed by the right subtree
(NLR).
Post-order: first the left subtree is traversed, then the right subtree is traversed and finally the root
node is visited (LRN).
103
Figure 6.5: Example Binary Tree.
The names LNR, etc. come from abbreviating the phrases “Left, Node, Right”, etc., which describe the
order of traversing the subtrees of a node. This might be made more clear by considering an example.
Consider the tree shown in Figure 6.5. If we print out the nodes as we visit them we will get the following
output from the four different traversal methods:
Traversal Output
In-order (LNR) d b e a f c g
Pre-order (NLR) a b d e c f g
Post-order (LRN) d e b f g c a
Breadth-order
a b c d e f g
(top-down)
Note that in addition to the first three traversals we could also do NRL, RNL and RLN traversals, but
these are uncommon as we more usually work through a tree from left to right. We could also do the
breadth-order traversal from the bottom-up, but again this is rather unusual.
How could we do such traversals with the Tree class we have developed? The existing methods that
we have defined for the class are sufficient to write methods to do such traversals. The simplest way of
writing traversal methods like these is using recursion. Here is one that would print out the nodes in a
tree of characters as used in the illustration above. This method does an in-order traversal.
Exercise 6.7 Write similar methods to do pre-order and post-order traversals of trees.
104
Figure 6.6: A Binary Search Tree.
We could write methods like this every time we needed to do a traversal of a tree. However these are
very common operations and so the traversals are good candidates to be written as methods of the class.
The problem with that is that the task to be performed each time may be different. Here we needed
to print out the contents of each node as we visited it. In another application we might need to add
the (numeric) contents of the node to a running total. We can overcome this problem by providing a
method that returns an iterator that can be used to work (or iterate) through the contents of the tree.
An iterator is just an object with methods that allow us to access the contents of some data structure.
We will return to this topic in the next section.
105
• Constructor
BinarySearchTree
root
A further change to the structure can be made in terms of how we “enclose” the nodes. The original tree
class we developed gave the user of the class control over following the links, etc. For a binary search
tree, because the structure is better defined, we can hide the entire tree structure from the user. This is
more like the approach that we have used for all our previous linear abstract data types. One impact of
this decision is that the (public) interface methods to which the user has access need to be duplicated
with internal (private) methods. With all of this in mind, the skeleton of our new class is shown below.
You will note that the generic type used for the class is specified differently here (as class BinarySearch-
Tree<T extends Comparable>). This introduces a new feature of the generic mechanisms in Java,
namely that generic type parameters can be bounded . In this case, what we are saying is that the
BinarySearchTree class can hold any type of object, as long as the object extends the Comparable inter-
face2 . In other words, we are restricting the types of classes that can be used with the BinarySearchTree
class. This is needed in this case, because we need to be able to compare the data items in the binary
search tree in order to maintain the ordering of the nodes.
Reminder Comparable is an interface that requires a method called compareTo. This method is used
to allow general comparisons between objects. It returns zero if the two objects are equal, a negative
value if the object is less than the one that it is being compared with and a positive integer (greater than
zero) otherwise. Many of the standard library classes implement this interface (e.g. the Integer wrapper
class). The following example shows how the compareTo method can be used. The program prints true
in all three cases.
2 Actually, it must implement the interface, not extend it, but the generic type parameter specification uses the extends
106
System.out.println(nine.compareTo(five) > 0);
System.out.println(five.compareTo(nine) < 0);
System.out.println(x.compareTo(five) == 0);
107
private void buildLRNIterator (BSTreeNode root,
TreeIterator t)
{ ... } // buildNLRIterator
// ------------------------------------------------------------
} // class BinarySearchTree
We will come back to the details in a moment. For now, notice how almost all the public interface
methods simply call on overloaded private methods to accomplish the necessary tasks.
Data Members
Turning to the private data members in this class, we have a nested class definition for the nodes (just as
for the stacks, lists, etc. of the previous chapters) and a reference (called root) to the start of the data
108
structure. Note that we have chosen to implement a constructor for the BSTreeNode inner class. This
simplifies the addition of new nodes slightly. Note too how we have included a link to the parent node of
each node in the tree. This is needed for some of the algorithms, as we will see shortly.
BSTreeNode
data, lt, rt, parent
Let’s now have a look at the implementation of the private, internal methods that actually manipulate
the trees. These are, again, highly recursive. The first is the insert method, used to insert a new object
into the tree. It is given the object and the root of an existing tree as parameters.
This is fairly straightforward. It needs to find the correct place in the tree to add in the new node. One
of the features of binary search trees is that we never have to do such an insertion at an interior point in
the tree: we always add new nodes as leaves in the tree. As an example, consider adding the letter ’b’
to the example tree below (we do permit duplicates in the tree).
Our definition of the binary search tree says that values less than that stored in a node will be found in
the left subtree. When we start looking for the insertion point in the tree above this means we must take
the right leg (to the node labelled ’d’). At this point we have to go left, as ’b’ is less than ’d’. This
brings us to the node labelled ’c’ and we have to go left again, as ’b’ is less than ’c’. At this point we
reach a dead end. The process of searching for the insertion point can be illustrated as follows:
109
The final step is to add a new node containing the second ’b’ at this point, giving:
This tree still has all the properties we require of a binary search tree. In particular, an in-order (LNR)
traversal will visit the nodes in ascending order: abbcd. Trace through the action of this method for the
node addition described above. Satisfy yourself that it works in all possible cases (such as adding to an
empty tree, adding a value smaller/greater than any already in the tree, etc.).
Deleting Nodes
While adding a new node to a binary search tree and maintaining its essential properties is reasonably
straightforward, the same is not true of deleting a node. When we delete an internal node we need to
replace it with something so that the subtrees are not just left dangling. This gives rise to four individual
cases where the node to be deleted has: (1) no subtrees, (2) only a left subtree, (3) only a right subtree,
or (4) both left and right subtrees.
The most difficult of these cases is (4), where the node has two subtrees. For example, what are we to
do when deleting the node labelled ’b’ below?
110
One way of solving this problem is to delete the smallest node in the right subtree (i.e. the node labelled
’c’ above) and replace the node that we are deleting with this one. Let’s see how this works. When
we come to delete the node labelled ’c’ we still have the problem of dealing with the right subtree (the
node chosen for deletion in this way will never have a left subtree, of course — why?). This can simply
be moved up a level to replace the deleted node. This gives:
How does this get us closer to deleting the node we really need to delete? Well, we can very easily replace
the contents of the node labelled ’b’ with the contents of the newly deleted node (i.e. ’c’). This gives:
This tree is exactly what we require: a binary search tree with the ’b’ node deleted.
Now that we know how to go about it, we can write the code. It turns out to be easiest to write a separate
method to delete the smallest element from a given subtree (the first step in the process described above).
Since this is a rather unusual method and is only of interest to the implementation of this ADT, we will
not make it public, but rather private. We will call it deleteMin:
111
return deleteMin(root.lt);
else // Delete this node
{ T tmpData = root.data;
replaceInParent(root.parent, root, root.rt);
return tmpData;
}
} // deleteMin
The replaceInParent method that is used here is another private utility method that simplifies the
altering of the links in the parent of a deleted node:
The deleteMin method can then be used by the private remove method to help deal with the difficult
case of deleting a node with two subtrees. What about the other three cases mentioned above? Obviously,
if a node has no subtrees then deleting it is trivial: we simply delete the node and set the parent’s pointer
to null (consider deleting the node labelled ’f’ above). If the node to be deleted has only a left or a
right subtree, it is almost as simple: we can simply replace the node by the child node. Consider a few
examples and satisfy yourself that this strategy will work.
We can now write the remove method itself. It consists primarily of a series of nested if statements to
decide which of the four possible cases holds. This listing has been commented with the number of the
case as in the discussion above.
112
// Has no subtrees: CASE 1
replaceInParent(root.parent, root, null);
else
if (root.lt == null)
// Has only right subtree: CASE 3
replaceInParent(root.parent, root, root.rt);
else // Has only left subtree: CASE 2
replaceInParent(root.parent, root, root.lt);
} // else
} // else
} // if
} // remove
This is probably the most complex algorithm that we have seen so far. Study it carefully and make sure
that you know exactly how it works.
The next method to consider is contains which returns an indication of whether or not a particular
value is found in the tree or not. This involves a fairly straightforward recursive search through the tree.
Note that this method uses both the equals and the compareTo methods.
The final point to consider is the way that the iterators work. You will recall that the public methods
all have the following form (this method returns an in-order iterator):
This essentially creates a specific Iterator object and returns it. The client can then use the Iterator
object to work through the nodes in the tree. We need to consider three things now: (1) what methods
does the Iterator interface require, (2) what does the TreeIterator class do, and (3) how do the
buildXXXIterator methods work?
113
The Iterator Interface The Iterator interface is a very simple Java interface that specifies three
methods. Note that it is a generic interface and so can iterate through collections of any specified data
type.
These methods allow a client program using an iterator to access the current data item in the ADT
through which the client is working, to move on to the next data item, and to tell if the iteration is
completed (i.e. that there are no more data items to examine). The use of an iterator in the context of
a tree is illustrated below (see p. 115).
The TreeIterator Class The TreeIterator class is an inner class that simply builds up a list of
nodes in a vector (we use the Vector class from the java.util package, but we could have used our
own GenericList class or any similar ADT). It then provides the methods required by the Iterator
interface to allow clients to work through the list of nodes that it contains.
public TreeIterator ()
{ v = new Vector<T>();
} // constructor
public T get ()
{ return v.get(index);
} // get
} // class TreeIterator
Note how the Vector class is itself generic and so needs to be created here to work with a list of whatever
type T the BinarySearchTree is working with.
Creating the Iterators The TreeIterator objects are initialised by the buildXXXIterator methods.
The buildLNRIterator method is as follows (again, the others are very similar):
114
private void buildLNRIterator (BSTreeNode root,
TreeIterator t)
{ if (root != null)
{ buildLNRIterator(root.lt, t);
t.add(root.data);
buildLNRIterator(root.rt, t);
}
} // buildLNRIterator
Essentially, all this does is traverse the tree in the correct order (i.e. in-order here), adding the data in
the nodes to the given TreeIterator. This is then the iterator that is returned to the client, allowing it
to work through the nodes in the same order.
How can we use these iterators? It is actually very simple. The following example shows how an
in-order traversal of a tree of characters can be performed in order to print out the contents of the
tree. The steps are to first obtain an iterator, using one of the getXXXIterator methods, then to
loop using the iterator’s atEnd method to detect the end of the process, and the next method to
obtain the next object from the tree.
Exercise 6.9 Provide iterators for the vector and list ADTs in Chapter 4.
Exercise 6.10 Many of the private methods in the BinarySearchTree class (contains,
insert, deleteMin and remove) can easily be written in a non-recursive way. Rewrite them
without using recursion.
Exercise 6.11 Write a method for this class that provides a breadth-order iterator (see Ex-
ercise 6.8 for a way of doing this).
The main uses of binary search trees are in sorting and searching. A file of data can be read in and
the data records placed into a binary search tree. At the end of this process, an in-order traversal
of the tree will produce the data in ascending order. If the data is originally in a random order
this method is a very efficient way of sorting. Additionally, once data has been placed in the binary
search tree it can be searched and accessed very efficiently. We will return to this point in Chapter 9.
6.3 Graphs
Graphs are even less structured than trees in that any two nodes in a graph can be connected. There is no
concept of a “parent/child” relationship: two nodes are either connected or they are not. The traditional
115
Figure 6.7: An Example of a Graph.
example of this is a route map showing the main roads between cities. For example, the diagram in
Figure 6.7 shows a graph representing the roads joining the major cities in South Africa.
The nodes in a graph are usually called vertices (the singular word is vertex ), and the connecting lines
are called edges or arcs. The arcs may be directed (meaning that they have a definite start and end —
this is usually shown by placing an arrowhead on the end of the arc or along its length), or undirected,
as in the example in Figure 6.7. Furthermore, the arcs may be weighted, meaning that they have some
value associated with them. For an example such as the major road graph that we have here, the weights
usually correspond to some measurement like the distance between the two vertices connected by the arc.
This method represents a graph using a square matrix, indexed by the vertex labels, showing which
vertices are connected. For the example graph in Figure 6.7 we would have the following matrix:
116
Pretoria Jo’burg Durban E.L. P.E. C.T. Bloem
Pretoria 1 1 0 0 0 0 0
Jo’burg 1 1 1 0 0 0 1
Durban 0 1 1 1 0 0 0
E.L. 0 0 1 1 1 0 1
P.E. 0 0 0 1 1 1 1
C.T. 0 0 0 0 1 1 1
Bloem 0 1 0 1 1 1 1
Note that, by convention, a vertex is considered to be joined to itself and so the diagonal of this matrix
is filled with ones. The rest of the matrix is filled with ones where there exists a link between the two
vertices represented by the row and column index. This matrix is for an undirected graph and so is
symmetrical about the diagonal. A directed graph on the other hand would not be symmetrical and a
convention would need to be established to specify the direction of a link. For example, we might specify
that a one in a position meant that there was an arc from the vertex labelling the row to the vertex
labelling the column.
This representation is simple, and is often used for that reason. However, it can be wasteful of space,
especially in cases where there are many vertices and few arcs (most of the entries will be zeroes). An
alternative representation, which prevents this, is the edge list representation.
This method associates with each vertex the list of vertices to which it is connected. For the South
African roads example we would have the following representation:
In an actual computer implementation, this might be done using a list of references or pointers (for
example, we could use our own GenericList class from chapter four). This might look something like
the following:
This can be extended to include the weighting information for the arcs if this is a requirement of the
problem.
One of the vital points to bear in mind when dealing with graph algorithms is that it is easy to get into
an infinite loop, visiting the same nodes over and over. Great care must be taken to prevent this.
117
Graphs occur in many areas of Computer Science. Their uses in describing networks, such as
networks of roads, communication networks (e.g. telephone lines and exchanges) and computer
networks, should be obvious. In addition they can be used for describing some state-space problems
(similar to the solution trees discussed in the maze-solving algorithm of the last chapter).
You will be studying graph theory further in a later course, so we will leave it at this for now.
Skills
• You should be familiar with the following abstract data types: tree, binary tree and binary
search tree
• You should be acquainted with the general concepts of graphs, and the terminology used to
describe them
• You should be familiar with implementation techniques for these abstract data types
• You should be familiar with some of the uses and characteristics of these abstract data types
118
Chapter 7
Objectives
• To consider the following abstract data types: dictionaries and hash tables
• To study the implementation of these abstract data types
• To introduce some of the important issues for the efficient implementation of hash tables
• To consider the application of these abstract data types
7.1 Introduction
In this chapter we will be looking at some abstract data types for storing information in ways similar to
those used in databases. This introduces the concept of data items composed of a key and an associated
value. We insist that the keys are unique and can be used to identify specific data items. The simplest
data structure like this is a dictionary, where a key is used to access some related data. This is obviously
analogous to a normal dictionary in which the key corresponds to the word, and the value to its definition.
The initial dictionary implementation that we will consider makes use of a sorted list of data items. This
is rather inefficient, and leads us onto the topics of hashing and hash tables, which provide more efficient
means of implementing dictionaries and similar structures. Hashing techniques also support very efficient
searching, a topic that is vital for many applications in the ICT field, not least search engines such as
Google.
7.2 Dictionaries
As mentioned above, we now think of our data as having two parts: a key and a value. In fact, the value
may be a complete record with several fields. The key is generally somewhat simpler, but this is not a
strict requirement. Typical key values would be student or employee numbers, or, in the context of an
English dictionary, the keyword. The important thing about keys is that they uniquely identify a specific
item of data. All access to the data is done using the key values: client programs have no concept of the
119
position of a data item in the data structure.
} // interface Dictionary
120
Pair
Dictionary getKey, getValue,
setValue,
insert, remove, hashCode, equals
get, contains,
isEmpty,
makeEmpty,
getIterator
ListDictionary
insert, remove,
get, contains,
isEmpty, makeEmpty,
getIterator
Data Members
The full class diagram for the ListDictionary, ListNode and DictionaryPair classes is shown in
Figure 7.1, showing the “uses” and inheritance relationships between these three classes.
• We will be keeping the list of data for this class in ascending order of the keys. For this reason,
we have bounded the generic type for the keys (i.e. K), just as we did for the BinarySearchTree
class in the previous chapter. In essence, while the Dictionary interface states that we can have a
dictionary of any type K for the keys and any type V for the values, we are restricting that here to
121
say that the keys must implement the Comparable interface for the purposes of the ListDictionary
class.
• The inner class (ListNode) used to build the linked list of data in this case extends the Dictionary-
Pair class. The DictionaryPair class is a simple class that allows us to represent data that is
composed of a pair of values: a key and a data value. There are also a number of supporting
methods required, which we will discuss as we come across the need for them. The important fact
to note is that, once a pair object has been constructed, the key value cannot be changed, although
the associated data value may be. The entire DictionaryPair class is shown below.
public K getKey ()
{ return key;
} // getKey
public V getValue ()
{ return value;
} // getValue
What will the lists that we construct using the ListDictionary class look like? We stated above that we
would keep the lists sorted on the key values. For a trivial example where the key is an integer, and the
associated value is a string with the equivalent number in Roman numerals, we might have something
122
like the following (note that the diagram has been simplified slightly):
This example does not show another important point about our dictionary class, namely that the keys
must be unique. If, for some odd reason, we came to add another node with a key of 7, we would replace
the existing node rather than have two nodes with the same key. With this in mind, we can consider
the next part of the ListDictionary class. This is a private method that is used by several of the other
methods. It either inserts a new node into the list with a specified key and returns a pointer to it, or
returns a pointer to the existing node with that key.
Notice how we only insert the key field here, and then only when necessary — the value is left undefined.
This is dealt with by the public methods that make use of this private, “internal” method.
123
Inserting a Key
Moving on to the public methods of the class, we have the usual constructor, etc. The first method
of more interest is the insert method. We will develop three overloaded versions of this, all of which
make use of the private findNode method we have already seen. These variations allow one to optionally
specify a value (the key, of course, must be specified whenever we insert a new entry) or to provide the
data as an existing Pair object.
Note the assertions in these methods. They are there to check that the private findNode method has
done its job correctly (i.e. they are checking the postconditions of the findNode method).
Removing a Key
The next public method is the counterpart to insert, remove, and again is quite straightforward:
124
// else entry not found - ignore
} // remove
Accessing an Entry
The next method we define is to access an entry in a dictionary list, by specifying the key. The interesting
feature of this method is that it creates a new entry (using the private findNode method) if there is no
existing one. So whatever entry a client program accesses is guaranteed to exist. Due to the way we have
written the findNode method this becomes very simple. The definition of the get method is as follows:
The next method overcomes a potential problem with the kind of access that we have chosen to use. If,
for example, the key type being used was a string type then there is no simple way to run a for loop over
all the key values. In fact, even if the key type is something like int, we still have a problem because
the values of the keys may not be contiguous (as was the case with the Roman numbers example we
looked at earlier). In order to get around this we make use of an iterator again. In this case, the iterator
will allow us to work through the dictionary in the order specified by the keys. The inner class and the
method that provides the client with an iterator object are shown below:
} // class ListDictionaryIterator
125
{ return new ListDictionaryIterator(dict);
} // getIterator
This is a little simpler than the iterators for trees, as the data is already in a list and the iterator object
simply has to keep track of where in the list it is currently positioned (using the nextEntry variable).
The use of the generic mechanisms for this class is a little more complex and quite interesting. The
generic specification Iterator<Pair<K, V>> effectively states that we are dealing with an Iterator of
Pairs. The Pair objects in turn are composed of two generic types, K and V. We will see how this is used
in practice shortly.
Other Operations
The last three methods are a simple check on whether the dictionary contains a given entry, a check on
whether the dictionary is empty, and a mechanism to delete all the entries in a dictionary:
Note: This entire section deals with the client view, and will not be highlighted.
A common text-processing problem is that of producing a concordance (or index) for a document. We
will develop a simple concordance program that records all the words that appear in a file and keeps
track of the line numbers of all occurrences of each word. In order to do this we will need to use strings
(the words) as the key type for the dictionary. What can we use for the value type? Well, the values we
need to store are the line numbers of the appearances of a word. To do this we need some sort of list
of integers. Fortunately, we have several possible ways of keeping lists of integers, so can use any one of
them. In fact, we will use the IntegerList class from chapter four. Writing the concordance program is
then very easy:
126
In this way we proceed through the text building up lists of line numbers
for all the words in the text. The last part of the program then makes use
of the getIterator method for the dictionary to work through all the
entries printing them out.
This program makes use of the StringTokenizer class. This is a standard Java class that allows us to
break a string up into words (it’s a little like an iterator for the words in a string). We take each word
from the string and use it to access the dictionary ADT (using the get method). This will automatically
create an entry for the word if it was not in the dictionary already. In any case, it returns the associated
value, which in this case is an IntegerList containing the list of line numbers. If this is null then we
know that this is the first time we have seen the word, so we create a new IntegerList, add the current
line number and then insert an entry for the current word with the new list. If the list is not null then
we have seen the word before and all that is required is to add the current line number to the list.
In this way, the program proceeds through the text, building up lists of line numbers for all the words in
the text. The last part of the program then makes use of an iterator for the dictionary to work through
all the entries, printing them out.
As an example, the output of this program when given the text shown in Table 7.1 (i.e. the text of an
earlier version of the previous paragraph) as input was:
127
In: [1]
The: [2]
all: [2, 3]
building: [1]
dictionary: [3]
entries: [4]
for: [2, 3]
getIterator: [3]
in: [2]
last: [2]
line: [1]
lists: [1]
makes: [2]
method: [3]
numbers: [1]
of: [1, 2, 3]
out.: [4]
part: [2]
printing: [4]
proceed: [1]
program: [2]
text: [1]
text.: [2]
the: [1, 2, 2, 2, 3, 3, 3]
them: [4]
then: [2]
this: [1]
through: [1, 3]
to: [3]
up: [1]
use: [2]
way: [1]
we: [1]
words: [2]
work: [3]
This program is a little simplistic in the way that it handles punctuation symbols (see the second entry for
“text” above) and mixed-case words (like “The”), but it serves to illustrate the use of the dictionary and
the process of producing a concordance. We should probably also deal with the multiple line entries that
show up for common words (like the word “the” above). These problems are addressed by the following
exercises.
Exercise 7.1 Develop your own class for words. This should remove any letters other than
alphanumeric characters, hyphens and underscores and ignore case differences. You will need
to implement the Comparable interface for your class (i.e. provide a compareTo method for it).
Use this class as the key type for the concordance program.
Exercise 7.2 Develop an integer set class. This will be similar to the integer list class
(IntegerList) used above, but should ignore multiple entries of values. Use this class for
the value type in the concordance program.
128
Key Hash Value
advanced 26
computer 76
elephant 60
function 16
george 83
hash 70
programming 59
science 39
second 36
words 59
year 8
Here h is the hashed value, and SIZE is the number of entries in the array being used. The code in the
last line that reads (h & 0x7FFFFFFF) performs a bit-wise AND operation. What will the effect of this
be?
As examples, some random words (the keys) and their corresponding hash values when using this hash
function (SIZE has been taken as 100 for this example) are shown in Table 7.2. There are several points
to notice about this. Firstly, the process of hashing removes any ordering that was originally present
129
in the data. These words, in alphabetic order originally, would be assigned to entries in the hash table
that bear no resemblance to that order. Secondly, the keys “programming” and “words” have hashed to
exactly the same value (59). (This is bound to happen if we think about, since we are taking a potentially
infinite set of possible words, and mapping them to a set of only 100 integer values). This is known as a
collision. Dealing with collisions is one of the most important aspects of hash tables.
Collision Handling
One way of preventing collisions is to find a so-called perfect hashing function. A perfect hashing function
is guaranteed to map a number of distinct values to separate integer values. Such hash functions can
be constructed for very limited domains. For example, there are perfect hash functions for the sets of
reserved words for various programming languages. For the reserved words in Modula-2, a perfect hashing
function is:
where key is the reserved word being hashed. This gives forty unique values in the range 0–138 for the
forty reserved words in Modula-2. While this is a perfect hashing function it is less than optimal as the
other 99 entries in a table using this approach would be unused. If we wish to make use of every entry
in the table (looked at from another perspective, if we wanted to use a table with only forty entries for
the Modula-2 reserved words) we need a perfect minimal hashing function. This must not only map the
strings of interest to a set of unique values, but these values must be continuous. Such a function can be
constructed for the set of Modula-2 reserved words, but will not be presented here.
So, perfect hashing functions are a solution to the problem of collisions. The only problem is that, as
stated above, they are only available for very limited domains (such as small sets of reserved words in
programming languages). In general, it is extremely unlikely that we are going to be able to construct
a perfect hashing function for the keys we are using. Indeed, in many applications (for example, the
concordance program) we may not even know what the keys are until the program is run. In these cases
we need to choose the best hashing function we can find, and then make other arrangements to deal with
the collisions. These give rise to two types of hash tables: those with internal hashing and those with
external hashing.
130
We will restrict our discussion to linear probing where we examine successive positions as described above.
However, you should be aware that other probing techniques exist where the entries to be examined are
not just the immediately following ones. In some techniques a second hash function is applied; in others
a constant, or variable, amount is added to the given position to generate the probe sequence; and so on.
There is one important consequence of this, which arises when we remove items from the hash table. If
we remove the word “programming” from the hash table, we cannot simply mark the entry as unoccupied
again. If we did so then a search to locate the word “words” would fail even though it was still in the
hash table. A solution to this problem is to store the state of the entry in a field. This state can take
on one of three values: empty, occupied or deleted. When searching for an entry we can only halt when
we reach an empty entry. A deleted entry may have been there when we originally inserted the item for
which we are looking and so we must keep on looking.
Another consequence of this approach is that collisions will become more frequent as the table fills up.
When this happens we get a phenomenon known as clustering where groups of keys occupy a number of
neighbouring entries. In this regard, we distinguish between primary clustering and secondary clustering.
Primary clustering is caused by several keys mapping to the same location and so occupying successive
entries. Secondary clustering occurs when a key maps to an entry which is already occupied by a displaced
key. In other words a previous collision has caused a collision between two keys with different hash values.
As the table fills up both types of clusters grow, collisions increase in frequency, and the efficiency of
accessing the hash table decreases. The only real solution to this is to make the table somewhat larger
than it is anticipated will actually be needed. (Perhaps a better solution is to rather use external hashing,
but we will return to that topic in the next section). Alternative probing techniques may partially decrease
the problem of secondary clustering.
A further performance penalty can also arise when we have frequent deletions from the table. In this case
searches for particular keys may have to consider a large number of deleted entries. If deletions do occur
frequently then it is a good idea to reconstruct the table from time to time, compacting the clusters and
removing the unused entries.
With all of this in mind we can turn to the implementation of a Java class for a hash table.
InternalHashTable
table, numEntries
hash,
insert, remove, contains,
get, getIterator,
isEmpty, makeEmpty
TableEntry
key, value,
occupied
131
Data Members and the Hash Function
The first part of the file (InternalHashTable.java) is as shown below. The three possible “status
values” of the entries are handled as follows:
occupied The entry in the array is not null, and the occupied flag is true
deleted The entry in the array is not null, and the occupied flag is false
We will also use the same DictionaryPair class to hold the data (i.e. key/value pairs) as we used for
the ListDictionary class, and extend this to hold the “occupied” status of each entry.
Another important consideration is how we handle the hash function. This class has no idea of what
data type might be used for the keys that it stores, so how is it to work out a hash value? Fortunately
Java comes to our rescue here: all Java objects have a built-in hash function method as standard (it is
defined in the Object class and so it is inherited by all Java classes). This method is called hashCode
and returns an int value.
The nested TableEntry class allows us to record the key and the value associated with an entry (inherited
from the DictionaryPair class), as well as its status (using the occupied field, as discussed above). The
variable table is a reference to the array used to hold the entries, and numEntries keeps track of the
number of occupied and deleted slots (i.e. the ones that are not genuinely empty). Since the Java
hashCode method returns any integer value, we need the hash method which ensures the hashcode is
positive, and scales the hashcode value by taking the remainder after dividing by the size of the array
being used.
132
Constructors
Turning to the public part of the class, we have the usual constructors for this type of class, allowing the
size of the table to be specified by the client if necessary.
public InternalHashTable ()
// Constructor
{ this(DEF_SIZE);
} // Constructor
This simply has to initialise the private data members of the class. The second constructor simply calls
on the first with the size set to the defined default value (101).
Inserting a Key
Moving on to the more interesting methods, insert is responsible for adding a new entry to the hash
table. This involves hashing the key to find the position in the hash table and then resolving a collision
if it occurs. This is further complicated by the need to keep unique keys in the hash table. If we find an
existing or deleted entry with the same key, we simply update the value. Notice how we “wrap around”
the end of the array as we increment index. This is very similar to the technique we used for a circular
queue previously (see p. 70). We also make sure that there is always at least one empty slot in the hash
table. This simplifies the probing loops as they are assured of stopping when an empty slot is found.
There is also a very simple variation on the insert method that takes only a key value.
133
Removing a Key
The converse of inserting a key is performed by the remove method. This is quite straightforward again.
It involves searching for the item (hashing the key and resolving any collisions). If the item is found then
the occupied field is simply changed to false.
Accessing an Entry
The next method, get, is similar in some ways to the insert method as it must insert a new entry if
the specified key value is not yet present in the hash table. Notice here again how we ensure that there
is always at least one empty slot to ensure that the probing process works correctly.
Other Operations
The contains method is similar to get as it also has to search for a given key and be careful of the table
being full. The isEmpty and makeEmpty methods are quite straightforward.
134
// Tell whether the hash table contains aKey
{ int index = hash(aKey);
while (table[index] != null && !table[index].getKey().equals(aKey))
{ index = (index + 1);
if (index >= table.length) // wraparound
index = 0;
}
return (table[index] != null &&
table[index].occupied &&
table[index].getKey().equals(aKey));
} // contains
The last thing to consider is how to handle iterators for this class. One important point to note about
this is that it is not possible to iterate over the entries in the table in any kind of order. This is due
to the fact that the hashing function will have randomised the positions at which the keys are placed.
There is not much we can do about this except to work through the table from the beginning to the end.
Unfortunately, this has a very large impact on the kinds of applications for which we might want to use
a hash table. If the order of the information is of any importance then a hash table will not be a suitable
implementation. Anyway, here is the inner class and the getIterator method:
public HashTableIterator ()
{ for (index = 0; index < table.length; index++)
if (table[index] != null && table[index].occupied)
// First non-empty slot
break;
} // constructor
135
public void next ()
{ while (++index < table.length)
{ if (table[index] != null && table[index].occupied) // Found more data
{ break;
}
}
} // next
Note again how we use an iterator of Pair<K, V> objects. Notice too how the constructor of the inner
class sets up the iterator by searching for the first occupied entry.
Some Comments
That ends our hash table implementation using internal hashing. We have used the same interface
(Dictionary) that we had for the ListDictionary class, and so it can be used as a more efficient
mechanism for applications such as the concordance problem we looked at previously. The only difference
in the use of this class (and an aesthetically unpleasing one, unfortunately) is that the results will not be
in alphabetic order when we print out the concordance when using a hash table.
Exercise 7.3 Modify the implementation of the InternalHashTable class to report on colli-
sions. See what effect choosing different table sizes has on the number of collisions. Experiment
with other probing techniques and see what effect they have on the number of collisions.
Exercise 7.4 Write a rebuild method for this class. This should create a new array and
then work through the existing array copying only the currently occupied entries across (if the
new array is in place as the array pointed to by table then the existing insert method can
be used to help with this). Your rebuild method should optionally allow the user to change
the size of the array. Here is a suggested outline:
136
Figure 7.2: An External Hashing Table.
commented, external hashing provides a different solution to this problem. The approach used in external
hashing is to think of the hash table as providing a number of “buckets”. The hash function is applied
to the key value in order to find the bucket into which the entry must be placed. This means that the
buckets may hold several entries (all those whose keys hash to the same value). In this way collisions are
avoided by allowing several keys to map to the same value. The disadvantage is that, as the number of
entries per bucket rises, we are forced to search through the entries in the bucket. Of course, this is no
worse than the probing we have to do with internal hashing.
Let’s first consider a very simple (and rather ineffective) approach to external hashing. If we are building
up a table of data about people (perhaps employees, or students) we might settle for the very simple
hashing function of taking the first letter of a person’s surname and using this (as a value between 0
and 25) as the hash value. This works well in theory. For example, we could take the name “Wells”.
This hashes to the value 22, and so we would add this entry to bucket number 22. The problem here
arises from the fact that the distribution of first letters of surnames is not very even. A simple analysis
of some names shows that about half of the entries will end up in only five of the twenty-six buckets.
So, some buckets will be very full and others almost empty. All that this really means, of course, is that
our hashing function is too simple. Any function that hashed surnames more effectively would give us a
more even distribution of entries over the buckets.
When implementing external hashing, we have to consider how we will implement the buckets. A simple
approach is to build linked lists of entries. More efficient methods might be to use binary search trees, or
even further hash tables for the buckets. We will restrict our discussion to considering simple, unsorted
linked lists for the buckets. For the set of words we used to introduce the concepts of hashing we would
have the data structure shown in Figure 7.2 (only the keys are shown for simplicity).
137
HashTable
table
hash,
insert, remove, contains,
get, getIterator, isEmpty,
makeEmpty
EntryNode
key, value,
next
Let’s implement a Java class to do external hashing. As before, we will use the standard Java hashCode
method. We also set a default number of buckets.
Notice the declaration of the data member called table. This is declared to be an array of EntryNodes.
This allows us to create an array of lists, since each EntryNode is part of a linked list of other nodes. Each
element of the array table represents one bucket. Note that we have dispensed with the numEntries
field here, and have no direct way of telling how many items are stored in the hash table.
138
The constructors are very simple in this case, and will not be shown here.
Inserting a Key
What do we need to do when we insert a new entry? Well, the hashing function can be used to identify
the correct bucket. We then need to work through the linked list to ensure that we do not end up with
duplicate entries. If this is a duplicate then we update the existing entry, otherwise we insert a new entry
into the list. Notice how the work of the for loop is all taken care of in the control section so the body
is empty.
Other Operations
The rest of the methods are quite straightforward. We just have to keep in mind that we are dealing
with an array of pointers to linked lists.
139
} // remove
Notice how we have had to implement the isEmpty method. It searches through the table and returns
false as soon as it finds the first entry. If it reaches the end of the table without finding any entries
then it returns true. The makeEmpty method is used to clear all the linked lists (giving the Java garbage
collector quite a bit of work to do as a result!).
The iterator class for this ADT is a little more tricky than the previous ones as it needs to work through
the array, and also through the linked lists for the buckets. The inner class that takes care of this is as
follows:
140
{ private int index;
private EntryNode<K, V> nextEntry;
public HashTableIterator ()
{ for (index = 0; index < table.length; index++)
if (table[index] != null) // First non-empty bucket
break;
if (index < table.length) // We have some data
nextEntry = table[index];
} // constructor
Again, the ExternalHashTable class uses the same interface as the first dictionary class and the hash
table class using internal hashing. While it suffers from the same aesthetic problems as the previous hash
table implementation, it has one major advantage. That is that its size is not limited by the size of the
array used — it can contain any number of entries (at least until we run out of memory for dynamically
allocated objects). Of course, as the number of entries grows very much larger than the number of
buckets, the linked lists start to grow very long and so we start to lose some of the performance benefits
of using a hash table.
Exercise 7.5 Modify the ExternalHashTable class to include a count of the number of en-
tries. Be careful as there are a number of places where we can add new entries. This simplifies
the isEmpty method considerably.
Exercise 7.6 Implement an external hash table class that makes use of binary search trees
for the buckets. What difference would you expect this make to the performance of the hash
table, when the hash table has very few entries and when it has many entries?
141
ADT Advantages Disadvantages
ListDictionary Ordered Slow access
Flexible size
InternalHashTable Fast access Unordered
Fixed size
ExternalHashTable Fast access Unordered
Flexible size
Exercise 7.7 Write a program that inserts 5000 random integers in the range 0. . . 19 999
into a dictionary (i.e. any class that implements the Dictionary interface). The program
should time how long it takes to search the dictionary for a further 5000 random integers in
the same range, using the contains method. Do this for all three of the ADTs considered in
this chapter. For the two hash tables the experiment should be repeated, setting the size of
the table to 5001 (i.e. just big enough for the data) and then to 10 000. Draw up a table of
your results. Does this agree with what you would expect?
Skills
• You should be familiar with the following abstract data types: dictionaries, hash tables using
internal hashing with open addressing, and hash tables using external hashing
• You should be familiar with the central idea of a dictionary or hash table data structure: a
list of values indexed by some key value
• You should be acquainted with the important concepts of hash tables and the terminology
used to describe them
• You should know about some of the issues that affect the performance of hash tables
• You should be familiar with implementation techniques for these abstract data types
142
Part III
143
As we have been through the course so far, we have often commented about the efficiency of various
algorithms. In this section we want to lay the foundation for a more thorough analysis of the performance
of algorithms. In particular, we need to explore methods that will allow us to compare algorithms without
any concern for issues such as the speed of the computer being used, or the quality of the compilers, etc.
You may wonder why we are so concerned with the efficiency and performance of algorithms. After all,
the speed and power of the computers that we use is increasing rapidly. We will also deal with these
kinds of questions in this section.
144
Chapter 8
Big-O
Objectives
8.1 Introduction
Many algorithms appear to be simple and reasonably efficient. However, careful analysis may reveal
that they are completely unsuitable except for trivial sizes of problem. One startling illustration of this
comes from considering the problem of solving sets of simultaneous equations. This is a vitally important
aspect of many scientific and engineering applications (e.g. aircraft design and weather prediction). In
these cases there may be thousands, or even millions, of equations to be solved. A simple method for
solving simultaneous equations is Cramer’s Rule, which you may have encountered in a maths course. If
you have not, do not panic! The following discussion is very general in nature.
The key step in Cramer’s method is the calculation of a value known as the determinant. It can easily be
shown that the number of operations required to calculate the determinant is proportional to n!, where
n is the number of simultaneous equations in the set.
Now, never mind engineering applications with thousands of simultaneous equations, let us consider a
set of just thirty equations. You will agree that this is a small, if not tiny, set. From the preceding
discussion we can see that the number of operations required to calculate the determinant in this case
is 30! ≈ 3 × 1032 operations, a rather large number. But computers are very fast at calculations, surely
this poses no real problem? Well, some of the fastest computers in the world today can perform about
109 arithmetic operations per second. How long will it take to calculate the determinant for Cramer’s
Rule? Only 1016 years, which is more than the estimated age of the universe!! Even if the performance of
145
computers over the next few years increases at a mind-boggling rate we still are faced with an intractable
problem.
Giving up on Cramer’s Rule, we might try another approach. Using the same kind of technique that we
usually use when solving simultaneous equations by hand leads to an algorithm known as the tri-diagonal
method. This requires about 10n operations, and our set of thirty simultaneous equations can then be
solved in 10−7 seconds. This is doing rather better than Cramer’s rule, and suggests that the tri-diagonal
method might be a better approach to solving scientific and engineering problems!
Hopefully this example has brought home the need for the careful analysis of the efficiency and perfor-
mance of algorithms. Merely seizing upon the first algorithm that springs to mind may lead one to an
extremely inefficient solution. This example has also focused on the time taken by an algorithm. Another
very important aspect of the efficiency of algorithms is the amount of resources (memory, disk space,
etc.) that they use. We will mainly be concentrating on time issues in this course, but will have occasion
to mention other resource issues from time to time.
We can usually characterise the data by some abstract quantity. For example, when discussing Cramer’s
Rule in the introduction to this chapter, we talked about a set of n simultaneous equations. The actual
data (the variables and constants making up the equations) were of no real interest to the analysis we
performed — all we needed to know about was how many equations there were.
For many algorithms it is a quantity such as the number of data items that is exactly what is required for
a general analysis of the algorithmic complexity. However, you should not fall into the trap of thinking
146
this is always the case. For example, some tree algorithms depend only on the depth of the tree, and not
on the number of data items stored in the tree1 . Some algorithms also depend on more than a single
value. For example, there are string searching algorithms that depend on both the length of the search
string and the length of the text being searched.
The second aspect when considering the input data is that we may need to consider three different
characteristics of the algorithm. These are the (1) best case performance, (2) worst case performance,
and (3) average case performance. These are obviously determined by the input data. If the input data is
“good” in some sense then we might expect the best case performance. If there is some “problem” with
the input data then we might expect the worst case performance. In general, we are likely to encounter
the average case performance. To fully characterise the complexity of an algorithm we need to consider
all of these situations.
Some algorithms are very efficient at the best and average cases, but break down badly when faced with
the worst case. In such a situation, if the worst case was likely to arise, we might settle for a compromise
and select an algorithm with slightly worse performance in the best and average cases but with better
worst case performance. Indeed, the input data that might produce the worst case for one algorithm may
produce the best case for another, as we will see.
If we examine the behaviour of this function we find that the dominant term is 3.5n2 : as n grows larger,
the contribution of the other terms becomes negligible. Similar reasoning allows us to dispense with the
constant 3.5, and we are left with the situation where we can characterise the efficiency of the algorithm
by the single quantity n2 . We then say the algorithm has a complexity of the order of n2 . More succinctly,
we say that the algorithmic complexity is O(n2 ). In this way we have characterised the essential behaviour
of the algorithm: its performance is proportional to the square of the number of data items. If we find
another algorithm to solve the same problem whose efficiency is directly proportional to the number of
data items, we say it is O(n), and can state that it is more efficient than the first.
We formally define the concept of order notation as follows.
We say that an algorithm is O(f (n)), or that the algorithm is of the order of f (n), if there
exist positive constants c and n0 such that the time t required to execute the algorithm is
determined by:
That is, the time (or, in general, other resource) requirements of the algorithm grow no faster than a
constant (c) times f (n), as long as n is greater than some cut-off value n0 .
1 This is a slightly artificial example, as the depth of the tree is often proportional to the number of nodes in the tree.
147
n=3 6 9 12 50 100 1000
1 10−6 s 10−6 s 10−6 s 10−6 s 10−6 s 10−6 s 10−6 s
log n 2 × 10−6 s 3 × 10−6 s 3 × 10−6 s 4 × 10−6 s 6 × 10−6 s 7 × 10−6 s 10−5 s
n 3 × 10−6 s 6 × 10−6 s 9 × 10−6 s 10−5 s 5 × 10−5 s 10−4 s 10−3 s
n log n 5 × 10 s 2 × 10 s 3 × 10 s 4 × 10 s 3 × 10 s 7 × 10−4 s
−6 −5 −5 −5 −4
10−2 s
n2 9 × 10−6 s 4 × 10−5 s 8 × 10−5 s 10−4 s 3 × 10−3 s 0.01s 1s
n3 3 × 10−5 s 2 × 10−4 s 7 × 10−4 s 2 × 10−3 s 0.13s 1s 16.7m
2n 8 × 10−6 s 6 × 10−5 s 5 × 10−4 s 4 × 10−3 s 36y 4 × 1016 y 3 × 10287 y
Times are expressed in seconds (s), minutes (m), or years (y).
There are a number of general issues that we can note about the use of order notation. Firstly, as described
above, we can ignore less significant terms of the function describing the behaviour of an algorithm. If
we are to do this we need to understand the ranking of the complexities which commonly occur when
considering algorithms. The following list summarises the most common orders found, and they are
ranked from the least significant to the most:
1 Constant complexity The algorithm is independent of any external factor. Few useful algorithms
are this favourable.
log n Logarithmic complexity This is very good. The time (or other resource) required by the algo-
rithm grows very slowly as n increases.
n! Factorial complexity These are even worse! We saw an example of this in the introduction where
even n = 30 was totally impractical.
Occasionally one comes across an algorithm with some other complexity such as log log n, or nn , but
these are relatively rare. The relative rate of increase of the more common functions is depicted in the
graph in Figure 8.1 (note that the y-axis is logarithmic).
Looking at this subject from another perspective, we can ask the question: if one operation takes a certain
time to execute, how long will it take to complete an algorithm with one of the above complexities? A
partial table of results is given in Table 8.1, where the time taken to execute one operation has been
taken as one microsecond (10−6 s). Bearing in mind that for many applications even 1000 data items is
not a lot, this gives a good indication of just how intractable some algorithms are.
Yet another way of looking at this is to consider how much bigger a problem we can solve if computer
power increases dramatically. For example, if we had access to computers 1 000 000 time more powerful
than those available today how much bigger can n grow? Well, if we have an O(n) algorithm we will be
able to solve a problem 1 000 000 times as big (i.e. for 1 000 000n). If we have an O(n2 ) algorithm we
148
Figure 8.1: Illustration of Some Common Order Functions.
(Taken from [20])
would only be able to solve a problem of size 1000n. In other words although the computer is 1 000 000
times more powerful, we cannot increase our problem size by anything like the same ratio. If the algorithm
is O(2n ) then we can solve a problem of size n + 20. In other words, we can only add 20 items to our
dataset even though the computer has grown 1 000 000 times more powerful!
One last point worth making about order notation before we look at how it is applied is that we have lost
some information about the exact nature of the algorithm when we state just the order. For example,
from our definition, we can see that when we say that an algorithm is O(n), we have discarded any
constant factor and any lesser terms (i.e. the real function governing the performance might be 23n + 15).
This may affect our choice of algorithm, if (1) these constants have extreme values, and (2) n is small.
A common example is in sorting. The best sorting algorithms we have are O(n log n). Simple sorting
algorithms are generally O(n2 ). However the constants of proportionality are often rather larger for the
more complex sorting algorithms. So, if we are only sorting small lists of data we might be better off
choosing a simple, O(n2 ) sorting algorithm.
149
What about the following algorithm?
This has a loop and so looks more interesting. In fact it is also O(1). The reason is that there is no
dependence on the input data (assuming that the commented do something is just a simple statement,
or sequence of statements). Whether this algorithm is given one item of data or 1 000 000, it is still going
to loop only ten times, giving a complexity of O(1).
Of more interest are algorithms that loop, processing data repeatedly. The simplest of these might just
work through the input data performing some operation on each item.
What is the order of this algorithm? The number of operations is directly dependent on the number
of data items, and so this is an O(n) algorithm. Again, this is assuming that the process of “doing
something” does not involve further loops or recursion dependent on the input data.
The following algorithm shows two loops processing some data:
This gives rise to an algorithmic complexity of O(n2 ). Be careful not to confuse this with the situation
that we might have in dealing with a two dimensional matrix. We might have two loops in such a case:
In this case we still have an O(n) algorithm, since the n data items have simply been arranged into a
matrix of NUMBER OF ROWS by NUMBER OF COLS.
Here is another algorithm with two loops:
What do we have here? Well, the first time around the inner loop we will execute it n times. The next
time will give n − 1 iterations, and the next n − 2, and so on. By the time we are finished we will have
the following total number of iterations of the “do something” part of the algorithm:
n + (n − 1) + (n − 2) + · · · + 2 + 1
At first this might not seem very helpful, but in fact this is just an arithmetic progression and can be
simplified to:
n n2 n
(n + 1) = +
2 2 2
150
8.3.2 A More Realistic Example
Let’s consider the problem of searching through an unsorted list in order to find whether or not it contains
an item. This is something that we have done several times now. The quantity n, which will determine
the complexity of this search, is the number of items in the list. The operations that characterise this
process are comparisons between the value that we are searching for and the items in the list.
What is the worst case that could arise? It is obviously when the item that we are searching for is not
to be found in the list at all. The only way that we can tell this is to look through the whole list. In this
case the number of comparisons is n, and so the algorithm is exhibiting O(n) behaviour.
The best case will be when the item we are looking for is the very first item in the list. In this case we
perform only one comparison and so have constant complexity. However, this case is extremely unlikely
to arise in practice!
What about the average case? To analyse this we need to make some assumptions. If we assume that
the item we are looking for will always be found, then, on average, we will need to search through half
the list before we find the item we are looking for. This gives n/2 comparisons, which is still O(n), of
course. If that assumption seems unreasonable, what if we rather assumed that on average the item we
are searching for is not to be found in the list half the time? In this case half the searches will require
n comparisons to ascertain that the item is not present, and the other half will require, on average, n/2
comparisons as before. This gives an overall result of 3n/4 comparisons as the average case based on
these assumptions. This is still O(n).
So, a simple sequential searching algorithm is essentially O(n), no matter how we look at it. In the next
section of the course we will continue considering these kinds of efficiency issues as we look at searching
and sorting algorithms.
Skills
• You should understand the need for measuring the efficiency of algorithms
• You should be familiar with Big-O notation and how it is used
151
Part IV
152
Searching and sorting are very common operations that are needed in many programs. Consequently, it is
worth making a detailed study of these important classes of algorithms. They also make a useful vehicle
for the further exploration of the concepts of algorithmic complexity introduced in the last chapter. This
is due the fact that the analysis of these algorithms is (1) very well understood, and (2) generally quite
straightforward. Lastly, this section introduces some balance into our study of advanced programming —
up until now we have focused mainly on data structures, and the subject of algorithms has been rather
neglected.
153
Chapter 9
Searching
Objectives
9.1 Introduction
In the last chapter we analysed the complexity of a simple sequential search algorithm for an unsorted
list of elements. In chapter seven we implemented a dictionary data structure using a sorted list of items.
This had a search method defined (called contains — see p. 126). We also implemented two hash table
data structures that had similar methods. Lastly, the binary tree class from chapter six also had such a
method to tell whether or not an item was present. So searching is not something new. What can we
add? In this chapter we will revisit some of these ideas, and introduce two new techniques: the binary
search algorithm and the interpolated binary search algorithm.
In general, the items being searched for or sorted will be records of some kind, such as student records,
or account details. In any such case we can usually think of the data as having two parts (a key and an
154
associated value), just as we did in chapter seven. To simplify our discussion, we will ignore the associated
values, and focus only on searching and sorting keys. To further simplify things, we will consider only
numeric keys in our examples. While this may seem rather unrealistic, the principles that we will be
considering are just as valid as if we were using more realistic data (such as student records with student
numbers as keys).
The second general consideration is that of the underlying data structure. Many of the data structures we
have seen in this course have been based on linked lists of nodes containing the data. However, for many
sorting and searching algorithms these are not the best data structures to use. In these cases arrays are
often more efficient, and sometimes are the only data structures that can be used. Why is this? When
we consider how we access the data in a linked list, we usually have only one, or maybe two, entry points
into the list and must work our way through the list to find the particular entry of interest. In fact, we
can state that accessing an entry in a linked list is an O(n) operation. On the other hand, accessing an
entry in array can be done directly: it is an O(1) operation. When searching and sorting, we often need
to access any item of the dataset. If this involves an expensive O(n) search for the given node, the overall
complexity of the algorithm may become excessively high.
In general then we will be concentrating on using arrays, rather than linked lists, as we look at searching
and sorting algorithms. Is this a great restriction? Not really, as we can quite easily set up an array to
help us access elements of a linked list directly. For example, if we have a linked list (of integers) like the
following:
we can count the number of elements and then create an array of references, or pointers, (called index
below) that will allow us to access the linked list directly:
We can then use index to “leap into the middle” of the linked list as we search or sort it.
When we come to implement general searching and sorting algorithms in Java we need some way to
compare the values of objects, while using as wide a range of objects as possible. We have already seen,
and used, the mechanism that allows us to do this, in the form of the Comparable interface. You will
recall that this specifies a method called compareTo that allows us to compare any two objects. For our
purposes in this section of the course we will usually be working with arrays of data, declared as follows:
Comparable[] list;
155
9.2 Searching Techniques Revisited
As mentioned in the introduction, we have already seen a number of search algorithms. In this section
we will briefly consider their complexity.
156
would be 3c/4 = 3.75. So, in this specific case, the number of comparisons is only 0.0375n. This is far
less than 3n/4 or n/2. So, although we are still dealing with an O(n) algorithm, it is likely to be more
efficient than the simple sequential searches.
To prevent the reader from believing that analysing the performance of hash tables is a very simple
process, we will present some actual results here, without showing their derivation. For internal hashing,
if the item is found in the table, the number of comparisons is given by the following formula:
n −1
1 + (1 − M)
2
n
1+
2M
0 1 2 3 4 5 6 7 8 9
3 8 12 15 20 21 29 37 39 42
157
We will start our search at position 41 . The value at this position is 20 and we know that the value
we are looking for, if it is present, must lie to the right of this position. We now restrict our search to
positions 5 through 9. With only one comparison we have managed to eliminate half of the entries in
the list (those in positions 0 through 4). Looking halfway along the list between positions 5 and 9 we
consider position 7. The value we find here is 37 and we know that the value we are looking for must lie
to the left of this position. We now restrict our search to the sublist between positions 5 and 6. Halfway
between these values will take us to position 5. The value we find here is 21 and we know that the item
we are looking for must be to the right of this position. That restricts our search to the sublist between
positions 6 and 6. Halfway between these positions is still position 6, and here we find the item we were
looking for.
This example illustrates the worst case, as we needed the maximum number of comparisons to find this
item. If we had been searching for the value 20 we would have found it after only one comparison. In
any event, we have found the item we were looking for using only four comparisons to search our list of
ten items. This is the worst case that can possibly arise. Using a binary search on a list of ten items, the
most comparisons we will ever have to do is four.
Implementation
Let’s look at the algorithm, and then we will return to the analysis of this algorithm’s complexity. The
binary search is very easy to code in Java. We make use of three indices: left and right to keep track
of the sublist that we are currently considering, and look to keep track of the midpoint of the sublist.
The method returns the index of the item if it is found, or a special value of −1 if it is not.
Notice how we form the condition controlling the do loop. We need to stop, either when we find the item
we are looking for, or when we have ascertained that the item is not present. This can be detected by
the left and right indices “crossing over” so that the left index has a value greater than that of the right
index.
1 To find the halfway mark we average the two positions we are considering, using integer division, which truncates the
158
In passing, note here how the Comparable interface provides us with a very useful level of abstraction:
we do not need to know anything about the objects that we are searching through, except that they have
a compareTo method, and we don’t even need to know how that does the actual comparison.
Exercise 9.1 The calculation used to find the midpoint of the array is not always safe, as
reported by Joshua Bloch (one of the leading Java developers at Sun) in the following ar-
ticle: http://googleresearch.blogspot.com/2006/06/extra-extra-read-all-about-it-
nearly.html. Test this for a suitably large array (which can simply contain sequential integer
values) and demonstrate the bug (hint: search for large values). Then implement any of the
solutions that Bloch suggests and establish that the modified search is safe for the cases that
caused the problem to arise initially.
Complexity Analysis
Turning to the efficiency of this search algorithm, we stated above that the number of comparisons for a
list of ten items would never be greater than four. What is the general relationship between the length of
the list and the number of comparisons? Since we halve the list at each stage, the number of comparisons
required will also halve. Put slightly differently, the number of comparisons needed for a list of length n
(i.e. Cn ) is one plus the number of comparisons needed for a list of length n/2, or:
Cn = Cn/2 + 1
Furthermore, the number of comparisons needed for a list of length one is just one:
C1 = 1
How does this help us work out the complexity? We start by assuming that n is a power of 2, let’s say
n = 2k . Then we can rewrite our first equation as follows:
C2k = C2k−1 + 1
This is what is known as a recurrence relation. We can use the formula itself to substitute for the first
term on the right hand side. This gives:
C2k = C2k−2 + 1 + 1
...
C2k = C20 + k
C2k = k + 1
Now, since we originally said we were assuming that n = 2k , we have log2 n = k. Rewriting this last
equation above then gives:
Cn = log2 n + 1
This is the answer we wanted: the number of comparisons required for a list of length n is of the order
log2 n, if n is an exact power of two. In fact, it can be shown that even if n is not an exact power of
two the number of comparisons is still proportional to log2 n. We usually drop the base when taking
logarithms2 , and so this gives us all we need to be able to state that the binary search algorithm is
2 The actual base used has a small effect on the constant of proportionality, which we disregard anyway.
159
Number of Comparisons Required
Sequential Search Binary Search
n
(Average Case) (Worst Case)
10 5 4
32 16 5
64 32 6
128 64 7
1 000 500 10
1 000 000 500 000 20
O(log n). From the ranking of order functions in the previous chapter we can tell that this is a better
search algorithm than the sequential searches, which are all O(n).
Turning to some actual examples, we saw above that we could search a list of ten items using four
comparisons. This may not seem like a great saving over the five comparisons required on average for a
sequential search. Table 9.1 illustrates some other examples showing the differences for various values of
n. Note that this is not really a fair comparison as it gives the worst case figures for the binary search
and the average case figures for the sequential search. As we can see from the table, the efficiency of the
binary search becomes very much more apparent as the size of the dataset being searched increases.
Implementation
In essence, the modified method is as shown below. In practice, unfortunately, it is complicated by the
need to prevent division by zero and to ensure that the interpolating calculations are done using floating
point arithmetic. The steps required to deal with these problems have been left out below to simplify
the code (the full, correct version of this method is shown in Appendix A).
160
// POST: returns -1 if item is not found,
// otherwise returns the index of item
{ int left = 0,
right = list.length-1,
look;
do
{ look = left + (right-left) * ((item-list[left]) / (list[right]-list[left]));
if (look < left)
look = left;
if (look > right)
look = right;
if (list[look] > item)
right = look - 1;
else
left = look + 1;
} while (list[look] != item && left <= right);
if (list[look] == item)
return look;
else
return -1;
} // intBinarySearch
Note that this relies on the values in the list having some numeric value. If they do not, then the method
of interpolation will have to be adjusted accordingly.
Algorithmic Complexity
So, how efficient is the interpolated binary search? The full analysis is not very simple, but it can be
shown that, if the keys are evenly distributed, the method is O(log log n). This is quite remarkable.
An experiment gave the following results (average number of comparisons) when searching through a list
of 1000 random integers for 10 000 random values:
So, on average, the interpolated binary search can find an item in a list of 1000 values using about three
comparisons! This agrees well with the theory since log2 1000 ≈ 10 and log2 log2 1000 ≈ 3. For a list
of one million items the interpolated binary search is capable of finding an item using only four or five
comparisons.
Exercise 9.2 Plot a graph of n, log n and log log n for n = 8, 16, 64, 128, 512 and 1024.
Exercise 9.3 Write a program that fills an array with 1000 random integer values and then
performs 10 000 searches, counting the number of comparisons needed. Do your results agree
with those given above?
9.3.3 The Relationship Between the Binary Search and Binary Search Trees
Perhaps unsurprisingly, the binary search algorithm we have seen in this section is closely related to
the binary search tree ADT that we saw in chapter six. How can this be? Here we are dealing with a
161
0 1 2 3 4 5 6 7 8 9
3 8 12 15 20 21 29 37 39 42
search technique for arrays of data — a sequential data structure. In chapter six we were dealing with
a nonlinear, tree structure of dynamically allocated nodes. In fact, the binary search algorithm works
through the data in exactly the same way that a search through an equivalent binary search tree would.
The key point here is the binary search tree must be equivalent. This means that the element found
at the midpoint of the array must be the one at the root of the binary tree, and the subtrees below it
should display the same properties. That is, the element found at the midpoint of one of the sublists in
the binary search should be the root of an equivalent subtree in the binary search tree. For the example
array we had at the beginning of this section the equivalent binary search tree is as shown in Figure 9.1.
Work through the search patterns for these two data structures for a few examples and see how they
effectively work in exactly the same way. In a sense, the binary search algorithm works through the array
in a tree-like way, subdividing it just as the subtrees do in a binary search tree. Notice how the maximum
depth in our binary search tree is four. This corresponds directly to the fact we had that the maximum
number of comparisons we would ever need to use in a binary search on an array of ten items is four.
The last matter arising from this is that we can now discuss the efficiency of searching a binary search
tree. As long as the tree is balanced (i.e. the depths of all the leaf nodes differ by no more than one,
just as we have in Figure 9.1) then the algorithmic complexity of searching through a binary search tree
is O(log n). The analysis of this is very similar to that which led us to the result for the binary search
algorithm.
Skills
• You should be familiar with the binary search algorithm and the interpolated binary search
algorithm
• You should be able to discuss the algorithmic complexities of simple sequential searches and
the two binary search methods
• You should be aware of the relationship between the binary search algorithm and a binary
search tree
162
Chapter 10
Sorting
Objectives
• To study the following sorting algorithms: Bubble Sort, Insertion Sort, Selection Sort, Quick
Sort and Merge Sort
10.1 Introduction
You should already be familiar with some sorting techniques from your first year of Computer Science.
In addition, we slipped one sorting method in anonymously in chapter seven when we looked at the
dictionary data structure. Sorting is important in many applications. For one, if we wish to use efficient
searching techniques like the binary search then we need to have a sorted list of data. Furthermore, even
when data is already sorted we might need it in another order. A common application that lecturers have
is sorting class records (in alphabetic order) into descending mark order so that they can get an overview
of the overall performance of their students in a test, etc.
In this chapter we will look at several simple sorting techniques. Some of these should already be known
to you. We will then look at some more advanced (and efficient) sorting methods. It is important to
bear in mind that the field of sorting algorithms is a vast one, and in a course like this we will only be
scratching the surface.
As we discussed at the beginning of the previous chapter, we will make use of the Comparable interface
to write generic sorting routines.
163
studying for that reason. We will be considering the Bubble Sort, the Selection Sort and the Insertion
Sort.
3 10 7 2 5 11 4 6
If we consider this list, we start by comparing 3 and 10. They are in order so we change nothing. We
now compare 10 and 7. They are out of order so we swap them, giving:
3 7 10 2 5 11 4 6
3 7 2 10 5 11 4 6
And so on:
At this point we can see that the largest element in the original list (i.e. 11) has found its way to the
end of the list. It is as if it has “bubbled up” through the list, and this is where the Bubble Sort gets its
name. We will redraw the list as follows to indicate that the last element is now in the correct position:
3 7 2 5 10 4 6 11
We now start all over again at the start of the list. We do not have to work all the way through to the
end of the list this time, as we know that the last element is in the correct place.
3 7 2 5 10 4 6 11 Compare 3 and 7.
164
The second biggest value (10) has now “bubbled up” into the correct position.
We now start the third pass. In this pass the number 7 will bubble up to the top of the list. We will not
illustrate this pass step by step, but the final picture is:
2 3 5 4 6 7 10 11
2 3 4 5 6 7 10 11
When we do the next pass, we notice something interesting: we do not swap any pairs of numbers
anywhere in the list. The algorithm can use this fact to tell (as we can see clearly from the diagram
above) that the list is now in order, and it need not waste any time on doing further passes over the list.
Implementation
That is how the algorithm works, what does it look like in Java? The code is as follows:
There are some points to note about this method. Firstly, the variable madeSwap is used for the purpose
we mentioned above: as soon as we do a pass and make no swaps, we know we can stop sorting as the
list is now in order. The second point to note is how we control the inner loop (the one using k). This
runs from 0 to list.length-pass-1, where pass gives the current pass number. This means that, as
each number bubbles its way to the top of the list, we don’t consider it again. In essence, we are ignoring
the part of the list that was shaded in grey above.
Complexity Analysis
Considering the best case first, if the list is already sorted before we start what will happen? The
algorithm will only work through the list once, making no swaps, and then terminate. At this stage we
will have done n − 1 comparisons and no swaps. We cannot hope to do any better than this O(n) result.
The worst case would be if the list is in reverse order when we start. In this case the inner for loop is
executed the following number of times:
(n − 1) + (n − 2) + (n − 3) + · · · + 2 + 1
165
This is just an arithmetic progression similar to the one we saw in chapter eight, and the result is:
n
(n − 1)
2
This is O(n2 ). In this worst case, this gives both the number of comparisons, and the number of swaps
made (since every comparison results in a swap).
The average case analysis is more difficult, and we will not go into the details here. The end result is as
follows:
4 (n − 1)(n
1
Number of comparisons: + 2)
4 (n − 1)
n
Number of swaps:
Both of these are O(n2 ), of course. If you think about the way in which we move the data in this sort,
it is not too surprising that it is very inefficient — each item of the data moves only one position at a
time. The Bubble Sort cannot be very successful, unless the list is already sorted, or almost sorted.
Exercise 10.1 A further improvement to the performance of the Bubble Sort can be made
by keeping track on each pass of the position at which the last swap was made. The next pass
need only run as far as this position, not right up to list.length-pass-1. Why is this so?
Implement this improvement to the above algorithm and measure the performance benefit for
a list of random numbers.
3 10 7 2 5 11 4 6
We will work from the right end of the list back to the left. Initially we have the last item, which we can
think of as a sorted sublist of just one item1 . We’ll shade the sorted sublist as before to emphasise this
point.
3 10 7 2 5 11 4 6
We now start the actual sorting process. We take the next number from the unsorted part of the list (4
in the example above) and insert it into the correct place in the sorted part of the list. In this instance
the number 4 doesn’t actually move at all.
3 10 7 2 5 11 4 6
We now consider the next number in the unsorted part of the list (11) and insert it into the sorted part of
the list. To do this we need to put the value 11 somewhere safe temporarily, and shift the other numbers
down:
3 10 7 2 5 4 6 6 11
166
We can then put 11 in its correct position:
3 10 7 2 5 4 6 11
We now consider the next number 5, and insert it into the correct position (between 4 and 6) in the same
way. This gives:
3 10 7 2 4 5 6 11
3 10 7 2 4 5 6 11 Insert 2.
3 10 2 4 5 6 7 11 Insert 7.
3 2 4 5 6 7 10 11 Insert 10.
2 3 4 5 6 7 10 11 Insert 3.
Implementation
The first loop here (i.e. the for loop) considers each element of the unsorted part of the list in turn. The
inner loop (i.e. the while loop) is the one which moves the elements of the sorted sublist down until the
correct position for inserting the tmp value is found.
Complexity Analysis
Turning to the efficiency of the Insertion Sort, the worst case arises when the original list is in reverse
order. In this situation the inner (while) loop always has to run to the end of the list. This gives the
following number of comparisons and data movements:
n
1 + 2 + ... + (n − 2) + (n − 1) = (n − 1)
2
167
This is again O(n2 ).
The best case is when the list is already sorted. In this case the outer (for) loop still runs n − 1 times,
but the inner loop is never executed. We make one trivial assignment when we replace the tmp value back
where it came from. This does neither any good nor any harm. Checking to prevent it would probably
be less efficient than just doing it. So, the best case complexity is O(n).
The average case complexity for the Insertion Sort is not too difficult to work out. The outer loop always
executes n − 1 times as we have just seen. On average the item to be inserted is going to be halfway
along the sorted sublist, giving half the number of comparisons and movements that we had in the worst
case. So, the complexity is:
n
(n − 1)
4
Comments
Before we leave the Insertion Sort there are a few other points we should note about it. Firstly, it is an
ideal sorting method to use while the data is being read into a program. We simply insert each new item
into the correct place in the list as it is read. Secondly, unlike the other sorting methods that we are
considering, it is well suited to use with linked lists of data. We can begin by removing any item from
the original list and using it as the start of a new linked list. We can then work through the old linked
list item by item, inserting each one into the correct position in the new list. This works well, and avoids
the data movement present in the array version: we are simply updating pointers in the two lists. If you
look back to the findNode method in the ListDictionary class in chapter seven you will see that this
made use of a form of Insertion Sort, placing each new item into the correct position in a linked list as
it was entered.
Exercise 10.2 Implement the Insertion Sort for linked lists of data items, as discussed above.
3 10 7 2 5 11 4 6
We now look through the whole list for the smallest value. This is the number 2. We swap this item with
the one at the start of the list, giving:
2 10 7 3 5 11 4 6
We now repeat the process, looking through the unsorted sublist for the smallest remaining value. This
is 3, and we swap this with the value at the start of this sublist (i.e. 10):
2 3 7 10 5 11 4 6
168
And so we continue. The next smallest value is 4. This is swapped with 7, and so on:
2 3 4 10 5 11 7 6
2 3 4 5 10 11 7 6
2 3 4 5 6 11 7 10
2 3 4 5 6 7 11 10
2 3 4 5 6 7 10 11
Implementation
Here the variable minPos is used to keep track of the position in the unsorted sublist where the smallest
item is to be found. The outer loop works through the list, and gives the position of the start of the
unsorted sublist at any time. The inner loop is used to find the smallest element remaining in the unsorted
sublist.
Complexity Analysis
This algorithm is unusual in that it has no particularly good or bad cases: the amount of work done is
exactly the same for any ordering of the original input list. The outer loop always executes n − 1 times,
and the inner loop varies from n − 1 to 1. This gives a total of n2 (n − 1) comparisons again. So this
algorithm is always O(n2 ) with regard to the number of comparisons required. When we consider the
amount of data movement, however, the picture is a lot better. We only swap two items once we know
which is the smallest remaining value. In this we way we only make n − 1 swaps in total. This is much
better, in general, than either of the other simple sorting techniques we have studied. This is especially
true if the data items are large records. Of course, it doesn’t have the good O(n) best case performance
of the other two simple sorts.
169
Sort Operation Worst Case Average Case Best Case
2 (n − 1) 4 (n − 1)(n + 2) n−1
n 1
Bubble Comparisons
2 (n − 1) 4 (n − 1)
n n
Swaps 0
2 (n − 1) 4 (n − 1) n−1
n n
Insertion Comparisons
2 (n − 1) 4 (n − 1) n−1
n n
Moves
2 (n − 1) 2 (n − 1) − 1)
n n n
Selection Comparisons 2 (n
Swaps n−1 n−1 n−1
Exercise 10.3 Write a program that will allow you to test these algorithms with sorted,
reverse ordered and random lists of data. Time the execution of all three algorithms, and
verify the theoretical results shown in Table 10.1.
Exercise 10.4 Modify the sorting algorithms so that they count the number of comparisons
and data exchanges/moves that they perform. Write a program that will allow you to test these
algorithms with sorted, reverse ordered and random lists of data (or modify your program from
Exercise 10.3). Use this to verify the theoretical results shown in Table 10.1.
0 1 2 3 4
604G1234 602W4567 603C9823 605B3465 603A9182
... ... ... ... ...
The secondary array that we require, with the subscripts in order, looks like this:
170
0 1 2 3 4
1 4 2 0 3
What this tells us is that the element of the student list which should be in the first position is the
one with subscript 1 (i.e. 602W4567). The last item in the list should be the one with subscript 3 (i.e.
605B3465). In other words, we can use this secondary list of subscripts to access the original list in
ascending order, even though the original list has not been physically sorted!
Implementation
How can we implement this idea? Well, we start off with a secondary list which is in simple subscript
order. It will look like the following:
0 1 2 3 4
0 1 2 3 4
We then sort this list, but using the values of the student numbers when we do the comparisons. We
can use any sorting algorithm we like. The only thing that we need to remember is that the list we are
sorting contains subscripts and not the actual data. Let’s consider the Insertion Sort for example. Notice
how this now makes indirect accesses to the list array.
Compare this with the original Insertion Sort, and trace through how it sorts the example list of student
records shown above.
Exercise 10.5 Rewrite the Bubble Sort and Selection Sort algorithms to make use of this
technique.
171
10.4.1 The Quick Sort
The Quick Sort was invented by C.A.R. Hoare, a very famous computer scientist, in 1960. It has become
one of the most popular sorting methods. The central idea in the Quick Sort is to choose one of the items
in the list (it doesn’t really matter which one). This is called the partition element. We then divide the
list into two sublists: one consisting of those items smaller than the partition element, and one consisting
of those items greater than the partition element. We then repeat the process for the two sublists. Let’s
see how it works for our example:
3 10 7 2 5 11 4 6
First, we need to decide how to pick the partition element. One of the simplest (and usually quite
effective) methods is just to pick the first number in the list. In our example that gives us the value 3
and we need to partition the list into two sublists: those less than 3 and those greater than 3. The result
of this can be imagined as follows:
2 3 7 10 5 11 4 6
While we have drawn this as three separate lists, the data is, of course, still stored in one array. We are
just going to consider parts of it as separate sublists from now on. Another point which this raises is that
the choice of the partition element was not particularly good here. Ideally, we would like the partition
element to fall exactly in the middle of the list after we have partitioned the lists. The last point to notice
about this is that the partition element is now in its correct place: all the values to its left are less than
it and all the values to its right are greater. We need not consider the partition element again.
Returning to the example, the left sublist only contains a single number and so needs no further sorting.
We can turn our attention to the right sublist and apply the Quick Sort method to this. We choose the
value 7 as our partition element, and get the following picture:
2 3 4 6 5 7 11 10
We now have two further sublists to sort, and so we reapply the Quick Sort algorithm to each of these in
turn. Considering the left sublist first, we get 4 as the partition element. This gives an empty left sublist
and 6 and 5 in the right sublist:
2 3 4 6 5 7 11 10
Quick Sorting the list with 6 and 5 in it gives just 5 in the left sublist and an empty right sublist. We
are now finished with everything to the left of 7 and can return to consider its right sublist:
2 3 4 5 6 7 11 10
Taking 11 as the partition element we get a left sublist with just 10 in it and no right sublist. This gives
the final sorted list:
2 3 4 5 6 7 10 11
There are two things to note about the Quick Sort. Firstly, the method is inherently recursive. We
partition the list into two sublists, and then apply the Quick Sort to them independently. Secondly,
the key step is the partitioning of the list into sublists about a partition element. We will consider the
implementation of the partitioning algorithm first.
172
Implementation
The partitioning method needs to be able to work with any sublist, so we will pass it the whole list,
together with the indices of the left and right ends of the sublist with which it is to work. We also need
to return the index of the position where the partition element ends up so that the Quick Sort algorithm
can work with the left and right sublists. In Java this is as follows:
private static int partition (Comparable list[], int start, int end)
// Partition list between start and end, returning the partition point.
// The value at list[start] is used as the partition element.
// This method is used by the Quick Sort.
{ int left = start,
right = end;
Comparable tmp;
This method makes use of the two subscripts left and right to do the partitioning. The right subscript
first works down from the end of the list until a value less than or equal to the partition element is
encountered. This value should be in the left sublist. We then start the second nested while loop which
works the left subscript up from the beginning of the list until a value greater than the partition element
is found, or else we reach the right subscript. If we have not reached the right subscript, we swap the
two values indicated by left and right as they are in the wrong sublists. When left reaches right
we simply exchange the partition element (list[start]) with the item at position right (we could also
use left, since they are equal). We then return right as the position where the partition element can
now be found.
The partition method does almost all the work of the Quick Sort algorithm and so the actual Quick
Sort method itself is very simple. This version is recursive:
private static void recursiveQS (Comparable list[], int start, int end)
// Recursive Quick Sort list between start and end
{ if (start < end)
{ int partitionPoint = partition(list, start, end);
recursiveQS(list, start, partitionPoint-1);
173
recursiveQS(list, partitionPoint+1, end);
}
} // recursiveQS
We have chosen to define a trivial quickSort method which has exactly the same parameter list as the
simple sorts we looked at earlier. This method simply sets up the parameters which the actual Quick
Sort method (recursiveQS) uses and then calls it. Notice just how simple the recursiveQS method is.
It calls the partition method, and then recursively Quick Sorts the two sublists.
A Non-Recursive Implementation
The algorithm above serves to illustrate the way in which the Quick Sort works, but we have commented
before on the fact that recursion is often an inefficient way to handle repetitive tasks. We can write a
non-recursive Quick Sort algorithm, but we need some way of remembering which sublists we need to
come back to and sort. We can do this using a stack or a queue. We will do it here using the Stack class
from the java.util package, but we could just as easily have used one of our own from chapter five.
This version of the Quick Sort algorithm uses exactly the same partition method we saw previously.
while (! s.empty())
{ p = s.pop(); // Get next sublist to sort
while (p.start < p.end)
{ int partitionPos = partition(list, p.start, p.end);
// Now push left sublist
Pair tmp = new Pair();
tmp.start = p.start;
tmp.end = partitionPos-1;
s.push(tmp);
// Start work on right sublist
p.start = partitionPos+1;
}
}
} // iterativeQuickSort
Sublists are represented by pairs of subscripts: the start and end of the sublist. We have declared a
small, inner class (Pair) to hold these pairs of values, and have created a stack to hold the pairs. The
174
method works by pushing the left sublist onto the stack each time we partition part of the list, and then
continuing straight away to sort the right sublist.
Exercise 10.6 The iterative version of the Quick Sort above removes the recursion success-
fully. However it may run out of space for the stack when working with very large lists. One
way to help prevent this is to push the smaller of the left and right sublists and work on the
longer of the two. Modify the algorithm above to do this.
Exercise 10.7 We saw above how the choice of the first item as the partition element could
cause one of the sublists to be much smaller than the other. A method for preventing this is
to choose the median of the first, middle and last values in the list as the partition element.
Implement this improvement in either of the Quick Sort versions above and measure what, if
any, improvement it makes.
Exercise 10.8 Another method for preventing the poor choice of the partition element is
simply to choose a random element of the list as the partition element. Implement this im-
provement in either of the Quick Sort versions above and measure what, if any, improvement
it makes.
Exercise 10.9 Another modification which is often made to the Quick Sort is to use a simpler
sorting technique once the length of the sublists drops below a certain threshold. Make this
change to the either of the Quick Sort versions. Use the Selection Sort and set the threshold
length to ten. Measure the impact that this has on the performance of the Quick Sort algorithm.
Complexity Analysis
Now, let’s turn to the analysis of the Quick Sort. This is not affected by the implementation as a recursive
or iterative method. All that this decision affects is the magnitude of the constants, which we drop from
our order notation anyway.
Let’s consider the best case performance first. We stated above that the best case was when the partition
element divided the list into two sublists of equal length. If we let the number of comparisons performed
for a list of n items be Cn then we get the following recurrence relation:
Cn = 2Cn/2 + n
C1 = 0
In other words, the number of comparisons needed to sort a list of n items is the n comparisons done by
the partitioning algorithm, plus the two sets of comparisons done for the two sublists (i.e. 2Cn/2 )
This recurrence relation can be solved in manner similar to that we used when analysing the binary
search in the last chapter. We start by assuming that n is a power of 2: n = 2k . This gives:
C2k = 2C2k−1 + 2k
C2k C k−1
= 2k−1 + 1
2k 2
175
Using this equation to substitute for the first term on the right hand side we get:
C2k C k−2
k
= 2k−2 + 1 + 1
2 2
Continuing substituting like this k times we get to:
C2k
= 1 + 1 + ··· + 1 + 1 = k
2k
In other words:
C2k = 2k k
We started by assuming that n = 2k , so we have k = log2 n. This all means that we can rewrite the
above equation as:
Cn = n log2 n
This is the result that we wanted: the number of comparisons done by the Quick Sort, in the best case,
is n log2 n. This means the Quick Sort algorithm is O(n log n) in the best case.
The average case is rather harder to analyse. We expect the partition point, on average, to fall in the
middle of the list, and so it is not unreasonable to expect the average case performance to be similar to
the best case performance. In fact, it can be shown that the number of comparisons in the average case
is approximately 1.38n log2 n, so the algorithm is still O(n log n).
What about the worst case performance? This turns out to be the downfall of the Quick Sort. For
example, if the list is already sorted then the choice of the first item as the partition element will mean
that we always have an empty left sublist and a right sublist of n − 1 elements. This means that we need
the following number of comparisons:
n
(n − 1) + (n − 2) + ... + 2 + 1 = (n − 1)
2
and the algorithm degenerates to O(n2 ). If the list was originally in reverse order, we would get the same
poor performance. Of course, Exercises 10.7 and 10.8 above suggested modifications to the algorithm,
which can help prevent the worst case from arising.
Exercise 10.10 Write a program that will allow you to test the Quick Sort with sorted, reverse
ordered and random lists of data. Time its execution for all three input lists and verify the
results shown above.
Exercise 10.11 Implement the modifications suggested in Exercises 10.7 and 10.8, and see
what effect they have on the results of Exercise 10.10.
176
2 3 7 10
4 5 6 11
We can merge these by starting at the beginning of both lists and working through them, copying the
smaller of the two current items to a new array. For example, given the two lists above we would start
at the beginning of the first list and copy the value 2 to the new list:
62 3 7 10
4 5 6 11
2
We will cross out each number once we have dealt with it. Considering the two numbers now at the
beginning of the two lists we have 3 and 4, so we copy 3 to the new list:
62 63 7 10
4 5 6 11
2 3
62 63 7 10
64 5 6 11
2 3 4
62 63 7 10
64 65 6 11 Choose 5.
2 3 4 5
62 63 7 10
64 65 66 11 Choose 6.
2 3 4 5 6
62 63 67 10 Choose 7.
64 65 66 11
2 3 4 5 6 7
62 63 67 6 10 Choose 10.
64 65 66 11
2 3 4 5 6 7 10
At this stage we have finished with the first list. All that remains to be done is to copy whatever is left
in the second list into the new array. Of course, if the original lists had been different this might have
been the other way around. Also, in general, we will have more than just one element left over to copy
when one of the lists is exhausted. Copying the remnant of the second list over (in this case just the
single value, 11) gives:
62 63 67 6 10
64 65 66 6 11
2 3 4 5 6 7 10 11
177
So, from two already sorted lists we have managed to create a new sorted list. How does this help us
with sorting? As already stated, the Merge Sort works by splitting an unsorted list into two separate
halves, sorting them, then merging the resulting sorted lists. How does it sort the two halves? Well, it
splits them each in two, sorts them and then merges the resulting sublists. In this way the Merge Sort is
recursive. The stop case for the recursion arises when the length of the sublist to be sorted is one. This
is obviously sorted, as a trivial case of a list. With this in mind we can look at the Merge Sort algorithm
itself.
Implementation
private static void recursiveMS (Comparable list[], int start, int end)
// Recursive function to perform Merge Sort
{ if (start < end)
{ int midPoint = (start + end) / 2;
recursiveMS(list, start, midPoint);
recursiveMS(list, midPoint+1, end);
merge(list, start, midPoint, end);
}
} // recursiveMS
As we had for the Quick Sort, the Merge Sort is written as a number of methods. The mergeSort method
simply calls the recursive method (recursiveMS) to do the actual sorting. This takes the start and end
of the sublist which it is to sort as parameters. It divides the list into two, and then calls itself recursively.
Once the two sublists are sorted they are merged using the following merge method. This differs slightly
from our previous discussion in that it has to merge two sublists contained in one array.
public static void merge (Comparable list[], int first, int mid, int last)
// Merge list from first to mid with list from mid+1 to last
{ Comparable[] tmp = new Comparable[last-first+1]; // Temporary array for merging
int i = first, // Subscript for first sublist
j = mid+1, // Subscript for second sublist
k = 0; // Subscript for merged list
// Merge sublists together
while (i <= mid && j <=last)
if (list[i].compareTo(list[j]) < 0)
tmp[k++] = list[i++];
else
tmp[k++] = list[j++];
// Copy remaining tail of one sublist
while (i <= mid)
tmp[k++] = list[i++];
while (j <= last)
tmp[k++] = list[j++];
// Now copy tmp back into list
for (k = first; k <= last; k++)
178
list[k] = tmp[k-first];
} // merge
This method starts by creating a new temporary array tmp into which it can merge the two sublists. It
then performs the merge operation, as we described it above. Notice the two while loops which copy the
remnant of one of the original lists across. If you look carefully at the conditions under which the first
while loop terminates you will see that only one of the other two while loops is ever executed. Once
the two sublists are merged into tmp, the contents of tmp are copied back into the original array.
Exercise 10.12 Just as we had for the Quick Sort (see Exercise 10.9), the Merge Sort can
be modified to use one of the simple sorting techniques of Section 10.2 when the size of the
sublists drops below some threshold. This reduces the amount of recursion and also the amount
of copying data to and from the temporary arrays used in merging. Implement this modification
and measure the impact it has on the efficiency of the Merge Sort algorithm. Try different
threshold values and see which gives the best results.
Exercise 10.13 Develop an iterative version of the Merge Sort, using a stack in the same way
as we did with the iterative version of the Quick Sort.
Complexity Analysis
Let’s consider the algorithmic complexity of the Merge Sort. In this method we always divide the list
into two equal sublists (assuming the length of the list is a power of two). An analysis similar to that of
the best case of the Quick Sort reveals that the Merge Sort is also O(n log n). If the length of the list
is not an exact power of two the actual complexity is very slightly worse, but is still O(n log n). This is
independent of whether the list is sorted or randomly ordered at the start. So, we do not have the same
poor worst case behaviour that we saw with the Quick Sort. Why then do we not use the Merge Sort
more frequently? The problem with the Merge Sort arises not from its time complexity, which, as we
have just seen, is always very good, but with its space complexity. Because we need the temporary arrays
for the merging process this algorithm requires twice as much memory as the Quick Sort. When the lists
that we are sorting are very large this is too high a price to pay and the Quick Sort is to be preferred. In
general, the special case which produces the O(n2 ) behaviour of the Quick Sort is unlikely to arise. Even
if it does arise, a better choice of the partition element can help overcome the deficiencies of the Quick
Sort. For these reasons it, rather than the Merge Sort, is one of the most widely used sorting techniques
in practice.
Exercise 10.14 Plot a graph of n2 and n log n for n = 8, 16, 64, 128, 512 and 1024.
179
Exercise 10.15 Modify the Quick Sort and Merge Sort algorithms so that they count the
number of comparisons that they perform. Write a program that will allow you to test these
algorithms with sorted, reverse ordered and random lists of data, and verify the theoretical
results found for these sorts.
Exercise 10.16 Shell’s Diminishing Increment Sort (usually just called the Shell Sort) is a
relatively simple, but quite efficient sorting technique. It is similar in some respects to the
Bubble Sort (but far more efficient than the Bubble Sort). Like the Bubble Sort, the idea is to
compare and swap pairs of values in a list, but using a “gap” between the pairs (i.e. comparing
the k th element with the one at position k + gap). The initial gap is set to half the length of
the list. When a pass is made with no items being swapped, then the gap is halved, and the
process repeated. This continues until the gap is zero.
Implement this sorting algorithm, and test its efficiency in comparison to the other sorts in
this chapter.
Skills
• You should be familiar with the sorting algorithms of this chapter: Bubble Sort, Insertion
Sort, Selection Sort, Quick Sort and Merge Sort
• You should be able to discuss the algorithmic complexities of these sorting algorithms
• You should be aware that there are other sorting algorithms available
180
Index of Data Structures and
Algorithms
This section gives an index of the major data structures and algorithms that have been discussed in these
notes.
Data Structures
3 Stacks
3.1 Array implementation: ArrayStack . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
3.2 Linked list implementation: ListStack . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
4 Queues
4.1 Array implementation: ArrayQueue . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70
4.2 Linked list implementation: ListQueue . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
5 Deques
5.1 Linked list implementation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
5.1.1 Implementation using doubly-linked lists . . . . . . . . . . . . . . . . . . . . . . . . 84
5.1.2 Implementation using doubly-linked lists with header nodes . . . . . . . . . . . . . 87
6 Trees
6.1 General Binary Tree . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
6.2 Binary Search Tree . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105
7 Graphs
181
7.1 Adjacency matrix representation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116
7.2 Edge list representation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117
8 Dictionary 120
9 Hash Tables
9.1 Internal hashing with open addressing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131
9.2 External hashing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137
Algorithms
5 Searching Algorithms
5.1 Sequential Search . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 156
5.2 Binary Search . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 157
5.3 Interpolated Binary Search . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 160
6 Sorting Algorithms
6.1 Bubble Sort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 164
6.2 Insertion Sort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166
6.3 Selection Sort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 168
6.4 Quick Sort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 172
6.4.1 Recursive Quick Sort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 172
6.4.2 Iterative Quick Sort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 174
6.5 Merge Sort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 176
182
Index
183
array implementation, 68
circular queue, 69
linked list implementation, 72
travelling queue problem, 69
Quick Sort, 172
searching
binary search, 156
interpolated binary search, 159
Selection Sort, 168
single inheritance, 19
sorting
Bubble Sort, 164
indirect, 170
Insertion Sort, 166
Merge Sort, 177
Quick Sort, 172
Selection Sort, 168
stacks, 60
array implementation, 60
linked list implementation, 63
StringTokenizer, 126
Towers of Hanoi, 18
travelling queue problem, 69
trees, 93
traversals, 102
tri-diagonal method, 145
vectors, 33
184
Bibliography
[1] D.A. Bailey. Java Structures: Data Structures in Java for the Principled Programmer. McGraw-Hill,
1999.
[2] G. Bracha. Generics in the Java programming language. URL: http://java.sun.com/j2se/1.5/-
pdf/generics-tutorial.pdf, 2004.
[3] T.A. Budd. Classic Data Structures in Java. Addison Wesley Longman, 2001.
[4] F.M Carrano and J.J. Prichard. Data Abstraction and Problem Solving with Java: Walls and Mirrors.
Addison Wesley Longman, 2001.
[5] W.J. Collins. Data Structures and the Java Collections Framework. McGraw-Hill, 2002.
[6] Collins Concise Dictionary. HarperCollins, 5th edition, 2001.
[7] N. Dale, D.T. Joyce, and C. Weems. Object-Oriented Data Structures Using Java. Jones and Bartlett,
2nd edition, 2006. Superb coverage of advanced programming in Java — highly recommended.
[8] H.M. Deitel and P.J. Deitel. Java: How to Program. Prentice Hall, 3rd edition, 1999.
[9] A. Drozdek. Data Structures and Algorithms in Java. Brooks/Cole, 2001.
[10] M.T. Goodrich and R. Tamassia. Data Structures and Algorithms in Java. John Wiley and Sons,
4th edition, 2006.
[14] B.R. Preiss. Data Structures and Algorithms with Object-Oriented Design Patterns in Java. John
Wiley and Sons, 2000.
[15] G.W. Rowe. An Introduction to Data Structures and Algorithms with Java. Prentice Hall, 1998.
[16] S. Sahni. Data Structures, Algorithms, and Applications in Java. McGraw-Hill, 2000.
[17] M. Shaw. The impact of abstraction concerns on modern programming languages. Technical Report
Computer Science Technical Report CMU-CS-80-116, Carnegie-Mellon University, 1980.
[18] D.A. Watt and D.F. Brown. Java Collections: An Introduction to Abstract Data Types, Data Struc-
tures and Algorithms. John Wiley and Sons, 2001.
[19] M.A. Weiss. Data Structures and Algorithm Analysis in Java. Addison-Wesley, 1999.
185
[20] C. Willis and D. Paddon. Abstraction and Specification with Modula-2. Pitman, 1992.
[21] R. Winder and G. Roberts. Developing Java Software. John Wiley and Sons, 2nd edition, 2000.
A good general introduction to Java, with very good coverage of advanced data structures and
algorithms.
186
Appendix A
File Listings
187
* <BR><I>Precondition:</I> There is space available for another value.
* <BR><I>Precondition:</I> The position is in range.
* <BR><I>Postcondition:</I> The value <CODE>item</CODE> appears at
* <CODE>position</CODE> in the vector, or at the end of the list if
* <CODE>position</CODE> is greater than the original length of the list.
* @param item The integer value to be added to the vector.
* @param position The position in the vector where the item should
* be added.
* @throws IllegalArgumentException if <CODE>position</CODE> is negative.
* @throws NoSpaceAvailableException if no space is available.
*/
public void add (int item, int position)
{ if (numElements + 1 > data.length)
throw new NoSpaceAvailableException("no space available");
if (position < 0)
throw new IllegalArgumentException("position is negative");
if (position >= numElements) // Add at end
data[numElements++] = item;
else
{ int k;
for (k = numElements-1; k >= position; k--)
data[k+1] = data[k]; // Move elements up
data[k+1] = item; // Put item in place
numElements++;
}
} // add
188
public int length ()
{ return numElements; }
189
return s.toString();
} // toString
} // class IntegerVector
A.1.2 IntegerList.java
package cs2;
190
if (prev != null)
prev.next = node;
else
first = node;
numElements++;
} // add
191
ListNode curr = first;
for (int k = 0; curr != null && k < index; k++)
curr = curr.next;
assert curr != null;
return curr.data;
} // get
} // class IntegerList
192
A.1.3 ObjectList.java
package cs2;
/** Simple class to handle generic lists of objects, using linked lists.
* @author George Wells
* @version 3.0 (25 February 2010)
*/
public class ObjectList
{ private class ListNode
{ public Object data;
public ListNode next;
} // inner class ListNode
193
/** Place a new item at the end of a ObjectList.
* <BR><I>Postcondition:</I> The object <CODE>item</CODE> appears at
* the end of the list.
* @param item The object to be added to the list.
*/
public void add (Object item)
{ add(item, numElements);
} // add
194
/** Change the value of an element in an ObjectList.
* <BR><I>Precondition:</I> <CODE>index</CODE> is in range.
* @param index The position of the item to be changed in the list.
* @param item The new value for the item.
* @throws IndexOutOfBoundsException if <CODE>index</CODE> is invalid.
*/
public void set (int index, Object item)
{ if (index < 0 || index >= numElements)
throw new IndexOutOfBoundsException("index is out of range");
ListNode curr = first;
for (int k = 0; curr != null && k < index; k++)
curr = curr.next;
assert curr != null;
curr.data = item;
} // get
} // class ObjectList
A.1.4 GenericList.java
package cs2;
195
* @version 3.0 (25 February 2010)
*/
public class GenericList<T>
{ private class ListNode
{ public T data;
public ListNode next;
} // inner class ListNode
196
*/
public void add (T item)
{ add(item, numElements);
} // add
197
* @throws IndexOutOfBoundsException if <CODE>index</CODE> is invalid.
*/
public void set (int index, T item)
{ if (index < 0 || index >= numElements)
throw new IndexOutOfBoundsException("index is out of range");
ListNode curr = first;
for (int k = 0; curr != null && k < index; k++)
curr = curr.next;
assert curr != null;
curr.data = item;
} // get
} // class GenericList
A.2 Stacks
A.2.1 Stack.java
package cs2;
198
public interface Stack<T>
{
/** Push a new item onto a stack.
* <BR><I>Postcondition:</I> The stack is not empty.
* @param item The item to be pushed onto the stack.
*/
public void push (T item);
} // interface Stack
A.2.2 ArrayStack.java
package cs2;
199
/** Create a new stack with a default capacity of 100 items.
*/
public ArrayStack ()
{ this(100);
} // Constructor
/** Return a copy of the item on the top of the stack, without removing
* it.
* <BR><I>Precondition:</I> The stack is not empty.
* @return The value of the item on the top of the stack.
* @throws EmptyException if the stack is empty.
*/
public T top ()
{ if (topIndex < 0)
throw new EmptyException("stack is empty");
return data[topIndex];
} // top
} // class ArrayStack
A.2.3 ListStack.java
package cs2;
200
/** Simple generic stack class using a linked list.
* @author George Wells
* @version 3.0 (25 February 2010)
*/
public class ListStack<T> implements Stack<T>
{ private class StackNode
{ public T data;
public StackNode next;
} // inner class StackNode
201
} // top
} // class ListStack
A.3 Queues
A.3.1 Queue.java
package cs2;
} // interface Queue
A.3.2 ArrayQueue.java
package cs2;
202
* @author George Wells
* @version 3.0 (25 February 2010)
*/
public class ArrayQueue<T> implements Queue<T>
{ /** The array of data.
*/
private T[] data;
/** Index of the item at the head (front) of the queue.
*/
private int hd;
/** Index of the item at the tail (back) of the queue.
*/
private int tl;
203
if (hd == tl) // Was last element
hd = tl = -1;
else
{ hd = (hd + 1);
if (hd >= data.length)
hd = 0; // wraparound
}
return tmpData;
} // remove
/** Return a copy of the item at the front of a queue, without removing it.
* <BR><I>Precondition:</I> The queue is not empty.
* @return The value of the item at the front of the queue.
* @throws EmptyException if the queue is empty.
*/
public T head ()
{ if (hd == -1)
throw new EmptyException("queue is empty");
return data[hd];
} // head
} // class ArrayQueue
A.3.3 ListQueue.java
package cs2;
204
{ hd = tl = null; }
} // class ListQueue
205
A.3.4 QSearch.java
// Program using a queue to perform a breadth-first maze search.
// George Wells -- 7 November 2000
import cs2.*;
import java.io.*;
try
{ readMaze("MAZE");
}
catch (IOException e)
{ System.err.println("Error reading data file");
e.printStackTrace();
206
System.exit(1);
}
// Now do search
while (! posQueue.isEmpty())
{ Position nextPos;
// Remove next position from queue and try all possible moves
nextPos = posQueue.remove();
c = nextPos.c;
r = nextPos.r;
// Try to move up
if (maze[r-1][c] && ! beenThere[r-1][c])
addPosition(r-1, c);
// Try to move right
if (maze[r][c+1] && ! beenThere[r][c+1])
addPosition(r, c+1);
// Try to move down
if (maze[r+1][c] && ! beenThere[r+1][c])
addPosition(r+1, c);
// Try to move left
if (c > 0 && maze[r][c-1] && ! beenThere[r][c-1])
addPosition(r, c-1);
}// while
} // class QSearch
207
A.4 The Iterator Interface: Iterator.java
package cs2;
208
header.rt = header;
} // Constructor
209
DequeNode tmpPtr = header.lt;
T tmpData = tmpPtr.data;
header.lt = tmpPtr.lt;
tmpPtr.lt.rt = header;
return tmpData;
} // removeRight
/** Return a copy of the item at the left end of the deque.
* <BR><I>Precondition:</I> The deque is not empty.
* <BR><I>Postcondition:</I> The item at the left end
* of the deque is returned.
* @return The value of the item at the left end of the deque.
* @throws EmptyException if the deque is empty.
*/
public T leftHead ()
{ if (header.rt == header)
throw new EmptyException("deque is empty");
return header.rt.data;
} // leftHead
/** Return a copy of the item at the right end of the deque.
* <BR><I>Precondition:</I> The deque is not empty.
* <BR><I>Postcondition:</I> The item at the right end
* of the deque is returned.
* @return The value of the item at the right end of the deque.
* @throws EmptyException if the deque is empty.
*/
public T rightHead ()
{ if (header.lt == header)
throw new EmptyException("deque is empty");
return header.lt.data;
} // rightHead
} // class Deque
A.6 Trees
A.6.1 Tree.java
package cs2;
210
public class Tree<T>
{ /** The data stored in this node.
*/
private T data;
/** Pointer to left subtree.
*/
private Tree<T> lt;
/** Pointer to right subtree.
*/
private Tree<T> rt;
211
*/
public void addRight (Tree<T> right) // Add a right subtree to a node
{ if (rt != null)
throw new UnsupportedOperationException("subtree already present");
rt = right;
} // addRight
} // class Tree
A.6.2 Animal.java
/* Simple guessing game in which the computer tries to guess the
name of an animal the user has in mind by asking simple questions
with yes/no answers.
George Wells -- 7 November 2000 */
import cs2.Tree;
import java.io.*;
p = root.right();
p.addLeft(new Tree<String>("bird"));
p.addRight(new Tree<String>("Does it bark?"));
p = p.right();
p.addLeft(new Tree<String>("dog"));
p.addRight(new Tree<String>("cat"));
while (true)
{ try
{ ans = in.readLine();
212
}
catch (IOException e)
{ e.printStackTrace();
}
char ch = ans.charAt(0);
if (ch == ’y’ || ch == ’Y’)
return true;
else
if (ch == ’n’ || ch == ’N’)
return false;
else
System.out.println("Please answer yes or no.");
}
} // answer
} // class Animal
A.6.3 BinarySearchTree.java
package cs2;
import java.util.Vector;
213
{ public T data;
public BSTreeNode lt, // Pointer to left subtree
rt, // Pointer to right subtree
parent; // Pointer to parent node
public TreeIterator ()
{ v = new Vector<T>();
} // constructor
public T get ()
{ return v.get(index);
} // get
214
else // Add to right subtree
if (root.rt != null)
insert(value, root.rt);
else
root.rt = new BSTreeNode(value, root);
} // insert
215
} // else
} // else
} // if
} // remove
// ------------------------------------------------------------------
216
else
insert(newValue, root);
} // insert
/** Remove an item from the tree. If the item is duplicated in the tree, only the
* first instance found is removed.
* <BR><I>Precondition:</I> None.
* <BR><I>Postcondition:</I> If the item was found in the tree, it has been removed.
* @param value The item to be removed from the tree.
*/
public void remove (T value)
{ remove(value, root); }
/** Obtain an interator that can be used to work through all of the data contained
* in the tree using an in-order traversal.
* <P><B>Note:</B> This iterator makes a copy of the contents of the tree.
* Any subsequent changes to the structure of the tree will <EM>not</EM> be
* reflected by this iterator.
* @return An <CODE>Iterator</CODE> allowing the contents of the tree to be
* accessed in order.
*/
public Iterator<T> getLNRIterator ()
{ TreeIterator t = new TreeIterator();
buildLNRIterator(root, t);
return t;
} // LNRTraversal
/** Obtain an interator that can be used to work through all of the data contained
* in the tree using a pre-order traversal.
* <P><B>Note:</B> This iterator makes a copy of the contents of the tree.
* Any subsequent changes to the structure of the tree will <EM>not</EM> be
* reflected by this iterator.
* @return An <CODE>Iterator</CODE> allowing the contents of the tree to be
* accessed in pre-order.
*/
public Iterator<T> getNLRIterator ()
{ TreeIterator t = new TreeIterator();
buildNLRIterator(root, t);
return t;
} // NLRTraversal
/** Obtain an interator that can be used to work through all of the data contained
* in the tree using a post-order traversal.
* <P><B>Note:</B> This iterator makes a copy of the contents of the tree.
* Any subsequent changes to the structure of the tree will <EM>not</EM> be
* reflected by this iterator.
* @return An <CODE>Iterator</CODE> allowing the contents of the tree to be
* accessed in post-order.
*/
217
public Iterator<T> getLRNIterator ()
{ TreeIterator t = new TreeIterator();
buildLRNIterator(root, t);
return t;
} // LRNTraversal
} // class BinarySearchTree
218
* @param aKey The key of the item to be accessed.
* @return The value associated with the specified key is returned
* (<CODE>null</CODE> if the key was not previously present).
*/
public V get (K aKey);
/** Obtain an interator for a dictionary. The iterator should allow all
* data items stored in a dictionary to be accessed. Implementations may,
* or may not, provide a specific ordering by key values. The iterator’s
* <CODE>get</CODE> method should return a <CODE>Pair</CODE> object
* allowing access to both the key and the associated value.
* <BR><I>Precondition:</I> None.
* <BR><I>Postcondition:</I> An iterator for a dictionary is returned.
* @return An iterator for a dictionary.
*/
public Iterator<Pair<K, V>> getIterator ();
} // interface Dictionary
A.7.2 Pair.java
package cs2;
/** Interface describing a (key, value) pair (as used in dictionary and hash
* table data structures).
* @author George Wells
* @version 2.0 (3 January 2005)
*/
public interface Pair<K, V>
{ /** Access the key.
* @return The key value contained in a pair.
*/
public K getKey ();
219
public V getValue ();
/** Return a hash code for the key in a pair. This may utilise the
* standard Java <CODE>hashCode</CODE> method.
* @return The hash code of the key.
*/
public int hashCode ();
/** Test keys for equality. If the parameter is an object implementing the
* <CODE>Pair</CODE> interface then the keys should be compared for
* equality. Otherwise the key contained in this pair should be compared
* directly with the parameter.
* @return <CODE>true</CODE> if the keys are equal, <CODE>false</CODE>
* otherwise.
*/
public boolean equals (Object o);
} // interface Pair
A.7.3 DictionaryPair.java
package cs2;
/** Create a pair, initialising both the key and associated value.
* <BR><I>Precondition:</I> None.
* <BR><I>Postcondition:</I> The key and value are initialised.
* @param aKey The key.
* @param aValue The value initially associated with this key.
*/
public DictionaryPair (K aKey, V aValue)
{ key = aKey;
value = aValue;
} // constructor
220
/** Create a pair, initialising only the key.
* <BR><I>Precondition:</I> None.
* <BR><I>Postcondition:</I> The key is initialised (the associated value
* is <CODE>null</CODE>).
* @param aKey The key.
*/
public DictionaryPair (K aKey)
{ key = aKey;
} // constructor
/** Return a hash code for this pair. This simply utilises the standard
* Java <CODE>hashCode</CODE> method for the <EM>key</EM>.
* @return The hash code of the key.
*/
public int hashCode ()
{ return key.hashCode();
} // hashcode
/** Test keys for equality. If the parameter is an object implementing the
* <CODE>Pair</CODE> interface then the keys are compared for equality.
* Otherwise the key contained in this pair is compared directly with the
* parameter.
* @return <CODE>true</CODE> if the keys are equal, <CODE>false</CODE>
* otherwise.
*/
public boolean equals (Object o)
{ if (o instanceof Pair)
return key.equals(((Pair)o).getKey());
else
return key.equals(o);
} // equals
221
} // class DictionaryPair
A.7.4 ListDictionary.java
package cs2;
/** Implementation of a simple dictionary ADT. Requires that the keys implement the
* <CODE>Comparable</CODE> interface.
* The keys must be unique.
* @author George Wells
* @version 3.0 (25 February 2010)
*/
public class ListDictionary<K extends Comparable<? super K>, V> implements Dictionary<K, V>
{ private class ListNode extends DictionaryPair<K, V>
{ public ListNode next;
/** Find the node in the linked list containing the specified key. If the key
* is not found then it is inserted.
* <BR><I>Postcondition:</I> The dictionary is not empty.
* @param aKey The key to be located or inserted.
* @return A reference to the node containing the specified key.
*/
private ListNode findNode (K aKey)
222
{ ListNode prev = null;
ListNode curr = dict;
while (curr != null && aKey.compareTo(curr.getKey()) > 0)
{ prev = curr;
curr = curr.next;
}
if (curr == null || aKey.compareTo(curr.getKey()) != 0) // Insert new entry
{ ListNode n = new ListNode(aKey);
n.next = curr;
if (prev == null)
dict = n;
else
prev.next = n;
curr = n;
}
return curr;
} // findNode
/** Insert a new item into the dictionary or update an existing one.
* If the specified key is already present then the existing value is
* replaced by that specified here.
* <BR><I>Postcondition:</I> The dictionary is not empty.
* @param aKey The key to be added.
* @param aValue The associated value to be stored with the key.
*/
public void insert (K aKey, V aValue)
{ ListNode curr;
curr = findNode(aKey);
assert (curr != null && aKey.equals(curr.getKey()));
curr.setValue(aValue);
} // insert
/** Insert a new item into the dictionary or update an existing one.
* If the specified key is already present then the existing value
* is replaced by that specified here.
* <BR><I>Postcondition:</I> The dictionary is not empty.
* @param p The key/value pair to be added/updated.
*/
public void insert (Pair<K, V> p)
{ ListNode curr;
curr = findNode(p.getKey());
assert (curr != null && curr.getKey().equals(p.getKey()));
curr.setValue(p.getValue());
223
} // insert
/** Remove an item from the dictionary. If the specified key is not found, no action
* is taken.
* <BR><I>Postcondition:</I> The item specified by the given key is not present.
* @param aKey The key of the entry to be removed.
*/
public void remove (K aKey)
{ ListNode curr = dict, prev = null;
while (curr != null && aKey.compareTo(curr.getKey()) > 0)
{ prev = curr;
curr = curr.next;
}
if (curr != null && aKey.compareTo(curr.getKey()) == 0) // Remove this dictionary entry
{ if (prev == null)
dict = curr.next;
else
prev.next = curr.next;
}
// else entry not found - ignore
} // remove
224
public boolean contains (K aKey)
{ ListNode curr = dict;
while (curr != null && aKey.compareTo(curr.getKey()) > 0)
curr = curr.next;
return (curr != null && aKey.compareTo(curr.getKey()) == 0);
} // contains
/** Obtain an interator for the dictionary. The iterator allows all
* data items stored in the dictionary to be accessed. The iterator provided by
* this method orders the items into ascending order of the keys.
* The iterator’s <CODE>get</CODE>
* method returns a <CODE>Pair</CODE> object allowing access to both the
* key and the associated value.
* <BR><I>Precondition:</I> None.
* <BR><I>Postcondition:</I> An iterator for the dictionary is returned.
* @return An iterator for the dictionary.
*/
public Iterator<Pair<K, V>> getIterator ()
{ return new ListDictionaryIterator(dict);
} // getIterator
} // class ListDictionary
A.7.5 Concordance.java
// Test dictionaries and hash tables
import cs2.*;
import java.io.*;
import java.util.StringTokenizer;
225
if (lst == null) // First entry for this word
{ lst = new IntegerList();
lst.add(lineNo);
dict.insert(word, lst);
}
else // Simply add new line number
lst.add(lineNo);
}
lineNo++;
}
} // class Concordance
A.7.6 InternalHashTable.java
package cs2;
226
private class HashTableIterator implements Iterator<Pair<K, V>>
{ private int index;
public HashTableIterator ()
{ for (index = 0; index < table.length; index++)
if (table[index] != null && table[index].occupied) // First non-empty slot
break;
} // constructor
/** Function used to generate a suitable hash value for a specified key. This method
* uses the standard Java <CODE>hashCode</CODE> method, and ensures that the result
* is positive and the correct range of values to be used as a subscript for
* <CODE>table</CODE>.
* @param akey The key to be hashed.
* @return The hash value, <I>h</I> (0 <= <I>h</I> < <CODE>table.length</CODE>).
*/
private int hash (K aKey)
{ return ((aKey.hashCode() & 0x7FFFFFFF) % table.length); // Allow for negative hashcodes
} // hash
/** Create a new hash table, with a default capacity (currently 101 items).
* <BR><I>Postcondition:</I> The array used by the hash table is initialised and is empty.
*/
public InternalHashTable ()
{ this(DEF_SIZE);
} // Constructor
227
* <BR><I>Postcondition:</I> The array used by the hash table is empty.
*/
public void makeEmpty ()
{ for (int k = 0; k < table.length; k++)
{ table[k] = null;
}
numEntries = 0;
} // makeEmpty
/** Insert a new item or update an existing one in the hash table.
* There is a requirement that the keys are unique. If
* the key is found in the table, then the associated value is
* replaced with that specified here.
* <BR><I>Precondition:</I> None.
* <BR><I>Postcondition:</I> The item is added or updated.
* @param aKey The key for the item to be added to the hash table.
* @param aValue The value associated with the key.
* @throws NoSpaceAvailableException if the hash table’s capacity is exceeded.
*/
public void insert (K aKey, V aValue)
{ int index = hash(aKey);
while (table[index] != null && !table[index].getKey().equals(aKey))
{ index = (index + 1);
if (index >= table.length) // wraparound
index = 0;
}
if (table[index] == null) // Insert new entry
{ if (numEntries + 1 >= table.length) // Out of space?
throw new NoSpaceAvailableException("no space available in hash table");
table[index] = new TableEntry<K, V>(aKey, aValue);
table[index].occupied = true;
numEntries++;
}
else // Update existing or deleted entry
{ table[index].setValue(aValue);
if (! table[index].occupied) // Undelete it
{ table[index].occupied = true;
}
}
} // insert
/** Insert a new key into the hash table. There is a requirement that the keys are unique.
* If the key is found in the table, then nothing is changed.
* <BR><I>Precondition:</I> None.
* <BR><I>Postcondition:</I> The key is added if not present previously.
* @param aKey The key to be added to the hash table.
* @throws NoSpaceAvailableException if the hash table’s capacity is exceeded.
*/
public void insert (K aKey)
{ int index = hash(aKey);
while (table[index] != null && !table[index].getKey().equals(aKey))
{ index = (index + 1);
if (index >= table.length) // wraparound
index = 0;
}
if (table[index] == null) // Insert new entry
228
{ if (numEntries + 1 >= table.length) // Out of space?
throw new NoSpaceAvailableException("no space available in hash table");
table[index] = new TableEntry<K, V>(aKey);
table[index].occupied = true;
numEntries++;
}
else // Update deleted entry if necessary
{ if (! table[index].occupied) // Undelete it
{ table[index].occupied = true;
}
}
} // insert
/** Access an entry in the hash table, creating it if necessary. Note that this method
* adds the specified key if it is not already present.
* <BR><I>Precondition:</I> None.
* <BR><I>Postcondition:</I> The value associated with the specified key is returned.
* @param aKey The key of the item to be accessed.
* @return The value associated with the specified key is returned (<CODE>null</CODE> if
* the key was not previously present).
* @throws NoSpaceAvailableException if the hash table’s capacity is exceeded.
*/
public V get (K aKey)
{ int index = hash(aKey);
while (table[index] != null && !table[index].getKey().equals(aKey))
{ index = (index + 1);
if (index >= table.length) // wraparound
index = 0;
}
if (table[index] == null || !table[index].occupied) // Insert new entry
{ if (numEntries + 1 >= table.length) // Out of space?
throw new NoSpaceAvailableException("no space available in hash table");
table[index] = new TableEntry<K, V>(aKey);
table[index].occupied = true;
numEntries++;
}
assert aKey.equals(table[index].getKey());
229
return table[index].getValue();
} // get
/** Obtain an interator for this hash table. The iterator allows all
* data stored in the hash table to be accessed. The order of the items is
* determined by the hash codes of the key values and is unlikely to make any sense
* to a human.<BR><B>Note:</B>The iterator’s <CODE>get</CODE>
* method returns a <CODE>Pair</CODE> object allowing access to both the
* key and the associated value.
* <BR><I>Precondition:</I> None.
* <BR><I>Postcondition:</I> An iterator for the hash table is returned.
* @return An iterator for the hash table.
*/
public Iterator<Pair<K, V>> getIterator ()
{ return new HashTableIterator();
} // getIterator
} // class InternalHashTable
A.7.7 ExternalHashTable.java
package cs2;
230
/** This class implements a simple, external hashtable dictionary using a linked list
* for the buckets.
* @author George Wells
* @version 3.0 (25 February 2010)
*/
public class ExternalHashTable<K, V> implements Dictionary<K, V>
{ /** Default table size.
*/
private static final int DEF_SIZE = 101;
public HashTableIterator ()
{ for (index = 0; index < table.length; index++)
if (table[index] != null) // First non-empty bucket
break;
if (index < table.length) // We have some data
nextEntry = table[index];
} // constructor
231
} // inner class HashTableIterator
/** Function used to generate a suitable hash value for a specified key. This method
* uses the standard Java <CODE>hashCode</CODE> method, and ensures that the result
* is positive and the correct range of values to be used as a subscript for
* <CODE>table</CODE>.
* @param akey The key to be hashed.
* @return The hash value, <I>h</I> (0 <= <I>h</I> < <CODE>table.length</CODE>).
*/
private int hash (K aKey)
{ return ((aKey.hashCode() & 0x7FFFFFFF) % table.length); // Allow for negative hashcodes
} // hash
/** Create a new hash table, with a default number of "buckets" (currently 101).
* <BR><I>Postcondition:</I> The array used by the hash table is initialised and is empty.
*/
public ExternalHashTable ()
{ this(DEF_SIZE);
} // Constructor
/** Insert a new item or update an existing one in the hash table. There is a
* requirement that the keys are unique. If the key is found in the table, then
* the associated value is replaced with that specified here.
* <BR><I>Precondition:</I> None.
* <BR><I>Postcondition:</I> The item is added or updated.
* @param aKey The key for the item to be added to the hash table.
* @param aValue The value associated with the key.
*/
public void insert (K aKey, V aValue)
{ int index = hash(aKey);
// Look for aKey in linked list
EntryNode<K, V> c;
for (c = table[index]; c != null && !c.getKey().equals(aKey); c = c.next)
;
if (c == null) // Insert new node
{ EntryNode<K, V> n = new EntryNode<K, V>(aKey, aValue);
n.next = table[index];
table[index] = n;
}
232
else // Update existing entry
c.setValue(aValue);
} // insert
/** Insert a new key into the hash table. There is a requirement that the keys
* are unique. If the key is found in the table, then nothing is changed.
* <BR><I>Precondition:</I> None.
* <BR><I>Postcondition:</I> The key is added if not present previously.
* @param aKey The key to be added to the hash table.
*/
public void insert (K aKey)
// Insert new element or update existing one
{ int index = hash(aKey);
// Look for aKey in linked list
EntryNode<K, V> c;
for (c = table[index]; c != null && !c.getKey().equals(aKey); c = c.next)
;
if (c == null) // Insert new node
{ EntryNode<K, V> n = new EntryNode<K, V>(aKey);
n.next = table[index];
table[index] = n;
}
} // insert
/** Access an entry in the hash table, creating it if necessary. Note that this method
* adds the specified key if it is not already present.
* <BR><I>Precondition:</I> None.
* <BR><I>Postcondition:</I> The value associated with the specified key is returned.
* @param aKey The key of the item to be accessed.
* @return The value associated with the specified key is returned (<CODE>null</CODE>
233
* if the key was not previously present).
*/
public V get (K aKey)
{ int index = hash(aKey);
EntryNode<K, V> c = table[index];
while (c != null && !c.getKey().equals(aKey))
c = c.next;
if (c == null) // Insert new entry
{ EntryNode<K, V> n = new EntryNode<K, V>(aKey);
n.next = table[index];
table[index] = n;
c = n;
}
assert aKey.equals(c.getKey());
return c.getValue();
} // get
/** Obtain an interator for this hash table. The iterator allows all
* data stored in the hash table to be accessed. The order of the items is
* determined by the hash codes of the key values and is unlikely to make any sense
* to a human.<BR>
* <B>Note:</B>The iterator’s <CODE>get</CODE> method returns a <CODE>Pair</CODE>
* object allowing access to both the key and the associated value.
* <BR><I>Precondition:</I> None.
* <BR><I>Postcondition:</I> An iterator for the hash table is returned.
* @return An iterator for the hash table.
*/
234
public Iterator<Pair<K, V>> getIterator ()
{ return new HashTableIterator();
} // getIterator
} // class ExternalHashTable
/** Search through list of entries for an item using an interpolated binary
* search.
* <BR><I>Precondition:</I> The list must be sorted into ascending order.
* <BR><I>Postcondition:</I> Returns -1 if item is not found, otherwise
* returns the index of item.
* <P>This method requires that the data consists of <CODE>double</CODE>
* values for the interpolation.
* <P>This search is of order log log <I>n</I>.
235
* @param list The list of items (<CODE>double</CODE> values) to be
* searched.
* @param item The item (a <CODE>double</CODE> value) being searched for.
* @return -1 if <CODE>item</CODE> is not found, otherwise returns the
* index of item.
*/
public static int intBinarySearch (double list[], double item)
{ int left = 0,
right = list.length-1,
look;
do
{ if (left != right)
look = (int)(left + ((item-list[left]) /
(list[right]-list[left])) *
(double)(right-left));
else
look = left;
if (look < left)
look = left;
if (look > right)
look = right;
if (list[look] > item)
right = look - 1;
else
left = look + 1;
} while (list[look] != item && left <= right);
if (list[look] == item)
return look;
else
return -1;
} // intBinarySearch
} // class BinarySearch
import java.util.Stack;
/** Sort a list of items into ascending order using the Bubble Sort.
* <BR><I>Precondition:</I> The list contains data that implements
* the <CODE>Comparable</CODE> interface.
* <BR><I>Postcondition:</I> The list is in ascending order.
* <P>This sort is of order <I>n</I><SUP>2</SUP>.
236
* @param list The list of items to be sorted.
*/
public static void bubbleSort (Comparable list[])
{ boolean madeSwap = true; // Flag to tell if we can stop early
/** Sort a list of items into ascending order using the Insertion Sort.
* <BR><I>Precondition:</I> The list contains data that implements
* the <CODE>Comparable</CODE> interface.
* <BR><I>Postcondition:</I> The list is in ascending order.
* <P>This sort is of order <I>n</I><SUP>2</SUP>.
* @param list The list of items to be sorted.
*/
public static void insertionSort (Comparable list[])
{ for (int k = list.length-2; k >= 0; k--)
{ Comparable tmp = list[k];
int j = k+1;
while (j < list.length && tmp.compareTo(list[j]) > 0)
// Move data down
{ list[j-1] = list[j];
j++;
}
list[j-1] = tmp;
}
} // insertionSort
/** Sort a list of items into ascending order using the Selection Sort.
* <BR><I>Precondition:</I> The list contains data that implements
* the <CODE>Comparable</CODE> interface.
* <BR><I>Postcondition:</I> The list is in ascending order.
* <P>This sort is of order <I>n</I><SUP>2</SUP>, but does only <I>n</I>
* movements of the data.
* @param list The list of items to be sorted.
*/
public static void selectionSort (Comparable list[])
{ for (int k = 0; k < list.length-1; k++)
{ int minPos = k;
for (int j = k+1; j < list.length; j++)
if (list[j].compareTo(list[minPos]) < 0)
minPos = j;
// Now swap the k’th and smallest items
Comparable tmp = list[k];
list[k] = list[minPos];
list[minPos] = tmp;
}
237
} // selectionSort
/** Partition a list between given start and end points, returning the
* partition point. The value at <CODE>list[start]</CODE> is used as the
* partition element. This method is used by the Quick Sort.
* <BR><I>Precondition:</I> The list contains data that implements
* the <CODE>Comparable</CODE> interface.
* <BR><I>Postcondition:</I> The items in the list before the partition
* point are less than or equal to the partition element, and the items
* in the list after the partition point are greater than the partition
* element.
* @param list The list of items to be partitioned.
* @param start The index of the first item in the list to be considered.
* @param end The index of the last item in the list to be considered.
* @return The index of the partition point.
*/
private static int partition (Comparable list[], int start, int end)
{ int left = start,
right = end;
Comparable tmp;
while (left < right)
{ // Work from right end first
while (list[right].compareTo(list[start]) > 0)
right--;
// Now work up from start
while (left < right && list[left].compareTo(list[start]) <= 0)
left++;
if (left < right)
{ tmp = list[left];
list[left] = list[right];
list[right] = tmp;
}
}
// Exchange the partition element with list[right]
tmp = list[start];
list[start] = list[right];
list[right] = tmp;
return right;
} // partition
/** Sort a list of items into ascending order using a recursive form of the
* Quick Sort.
* <BR><I>Precondition:</I> The list contains data that implements
* the <CODE>Comparable</CODE> interface.
* <BR><I>Postcondition:</I> The list between <CODE>start</CODE> and
* <CODE>end</CODE> is in ascending order.
* @param list The list of items to be sorted.
* @param start The index of the first item in the list to be considered.
* @param end The index of the last item in the list to be considered.
*/
private static void recursiveQS (Comparable list[], int start, int end)
{ if (start < end)
{ int partitionPoint = partition(list, start, end);
recursiveQS(list, start, partitionPoint-1);
238
recursiveQS(list, partitionPoint+1, end);
}
} // recursiveQS
/** Sort a list of items into ascending order using a recursive form of the
* Quick Sort.
* <BR><I>Precondition:</I> The list contains data that implements
* the <CODE>Comparable</CODE> interface.
* <BR><I>Postcondition:</I> The list is in ascending order.
* <P>This sort is of order <I>n</I>log <I>n</I>.
* @param list The list of items to be sorted.
*/
public static void quickSort (Comparable list[])
// Quick Sort the list - actually just calls recursiveQS
{ recursiveQS(list, 0, list.length-1);
} // quickSort
/** Sort a list of items into ascending order using an iterative form of
* the Quick Sort.
* This makes use of the <CODE>java.util.Stack</CODE> class.
* <BR><I>Precondition:</I> The list contains data that implements
* the <CODE>Comparable</CODE> interface.
* <BR><I>Postcondition:</I> The list is in ascending order.
* <P>This sort is of order <I>n</I>log <I>n</I>.
* @param list The list of items to be sorted.
*/
public static void iterativeQuickSort (Comparable list[])
// Quick Sort list of length elements using a stack
{ class Pair // Store (start,end) pairs
{ public int start, end;
} // class Pair
Stack<Pair> s = new Stack<Pair>();
Pair p = new Pair();
p.start = 0;
p.end = list.length-1;
s.push(p); // Starting pair on the stack - the whole list to sort
while (! s.empty())
{ p = s.pop(); // Get next sublist to sort
while (p.start < p.end)
{ int partitionPos = partition(list, p.start, p.end);
// Now push left sublist
Pair tmp = new Pair();
tmp.start = p.start;
tmp.end = partitionPos-1;
s.push(tmp);
// Start work on right sublist
p.start = partitionPos+1;
}
}
} // iterativeQuickSort
/** Merge two sorted sublists of items into a single sorted list.
* <BR><I>Precondition:</I> The sublist between <CODE>first</CODE> and
* <CODE>mid</CODE> is in ascending order, and the sublist between
* <CODE>mid+1</CODE> and <CODE>last</CODE> is in ascending order.
239
* <BR><I>Precondition:</I> Space is available to create an array of
* <CODE>last-first+1</CODE> elements.
* <BR><I>Postcondition:</I> The list between <CODE>first</CODE> and
* <CODE>last</CODE> is in ascending order.
* @param list The list of items to be merged.
* @param first The index of the first element of the first sublist to be
* considered.
* @param mid The index of the midpoint between the two sublists to be
* considered.
* @param last The index of the last element of the second sublist to be
* considered.
*/
public static void merge (Comparable list[], int first, int mid, int last)
{ Comparable[] tmp = new Comparable[last-first+1];
// Temporary array for merging
int i = first, // Subscript for first sublist
j = mid+1, // Subscript for second sublist
k = 0; // Subscript for merged list
// Merge sublists together
while (i <= mid && j <=last)
if (list[i].compareTo(list[j]) < 0)
tmp[k++] = list[i++];
else
tmp[k++] = list[j++];
// Copy remaining tail of one sublist
while (i <= mid)
tmp[k++] = list[i++];
while (j <= last)
tmp[k++] = list[j++];
// Now copy tmp back into list
for (k = first; k <= last; k++)
list[k] = tmp[k-first];
} // merge
/** Sort a list of items into ascending order using a recursive form of the
* Merge Sort.
* <BR><I>Precondition:</I> The list contains data that implements
* the <CODE>Comparable</CODE> interface.
* <BR><I>Postcondition:</I> The list between <CODE>start</CODE> and
* <CODE>end</CODE> is in ascending order.
* @param list The list of items to be sorted.
* @param start The index of the first item in the list to be considered.
* @param end The index of the last item in the list to be considered.
*/
private static void recursiveMS (Comparable list[], int start, int end)
{ if (start < end)
{ int midPoint = (start + end) / 2;
recursiveMS(list, start, midPoint);
recursiveMS(list, midPoint+1, end);
merge(list, start, midPoint, end);
}
} // recursiveMS
/** Sort a list of items into ascending order using a recursive form of the
* Merge Sort.
* <BR><I>Precondition:</I> The list contains data that implements
240
* the <CODE>Comparable</CODE> interface.
* <BR><I>Postcondition:</I> The list is in ascending order.
* <P>This sort is of order <I>n</I>log <I>n</I>, but has 2<I>n</I> space
* requirements.
* @param list The list of items to be sorted.
*/
public static void mergeSort (Comparable list[])
{ recursiveMS(list, 0, list.length-1);
} // mergeSort
} // class Sort
241