Data Structures and The Java Collections Framework 3rd Edition
Data Structures and The Java Collections Framework 3rd Edition
Third Edition
William J. Collins
Lafayette College
This book was set in 10/12 Times Roman by Laserwords and printed and bound by Malloy Lithographers. The cover was printed by Malloy
Lithographers.
Founded in 1807, John Wiley & Sons, Inc. has been a valued source of knowledge and understanding for more than 200 years, helping
people around the world meet their needs and fulf ll their aspirations. Our company is built on a foundation of principles that include
responsibility to the communities we serve and where we live and work. In 2008, we launched a Corporate Citizenship Initiative, a global
effort to address the environmental, social, economic, and ethical challenges we face in our business. Among the issues we are addressing
are carbon impact, paper specifica ions and procurement, ethical conduct within our business and among our vendors, and community and
charitable support. For more information, please visit our website: www.wiley.com/go/citizenship.
Copyright © 2011 John Wiley & Sons, Inc. All rights reserved. No part of this publication may be reproduced, stored in a retrieval system or
transmitted in any form or by any means, electronic, mechanical, photocopying, recording, scanning or otherwise, except as permitted under
Sections 107 or 108 of the 1976 United States Copyright Act, without either the prior written permission of the Publisher, or authorization
through payment of the appropriate per-copy fee to the Copyright Clearance Center, Inc. 222 Rosewood Drive, Danvers, MA 01923,
website www.copyright.com. Requests to the Publisher for permission should be addressed to the Permissions Department, John Wiley &
Sons, Inc., 111 River Street, Hoboken, NJ 07030-5774, (201)748–6011, fax (201)748–6008, website http://www.wiley.com/go/permissions.
Evaluation copies are provided to qualif ed academics and professionals for review purposes only, for use in their courses during the next
academic year. These copies are licensed and may not be sold or transferred to a third party. Upon completion of the review period, please
return the evaluation copy to Wiley. Return instructions and a free of charge return shipping label are available at www.wiley.com/go/
returnlabel. Outside of the United States, please contact your local representative.
—W.J.C.
This page intentionally left blank
BRIEF CONTENTS
Preface xvii
CHAPTER 0
Introduction to Java 1
CHAPTER 1
Object-Oriented Concepts 27
CHAPTER 2
Additional Features of Programming and Java 59
CHAPTER 3
Analysis of Algorithms 105
CHAPTER 4
The Java Collections Framework 133
CHAPTER 5
Recursion 155
CHAPTER 6
Array-Based Lists 233
CHAPTER 7
Linked Lists 267
CHAPTER 8
Stacks and Queues 329
vii
viii BRIEF CONTENTS
CHAPTER 9
Binary Trees 377
CHAPTER 10
Binary Search Trees 401
CHAPTER 11
Sorting 457
CHAPTER 12
Tree Maps and Tree Sets 501
CHAPTER 13
Priority Queues 551
CHAPTER 14
Hashing 599
CHAPTER 15
Graphs, Trees, and Networks 643
APPENDIX 1
Additional Features of the JAVA Collections Framework 701
APPENDIX 2
Mathematical Background 705
APPENDIX 3
Choosing a Data Structure 721
References 725
Index 727
CONTENTS
CHAPTER 0 Modifie 39
1.3.2 Inheritance and Constructors 43
Introduction to Java 1 1.3.3 The Subclass Substitution Rule 43
1.3.4 Is-a versus Has-a 47
Chapter Objectives 1
1.4 Information Hiding 48
0.1 Java Fundamentals 1
1.5 Polymorphism 48
0.1.1 Primitive Types 2
1.6 The Unifie Modeling Language 49
0.1.2 The char Type 2
Summary 52
0.2 Classes 3
0.2.1 The String Class 4 Crossword Puzzle 54
0.2.2 Using javadoc Notation for Method Concept Exercises 55
Specification 5 Programming Exercises 56
0.2.3 Equality of References and Equality of Programming Project 1.1: A CalendarDate
Objects 7 Class 58
0.2.4 Local Variables 9
0.2.5 The Scanner Class 12
0.3 Arrays 17
CHAPTER 2
Additional Features of Programming
0.4 Arguments and Parameters 19 and Java 59
0.5 Output Formatting 22
Chapter Objectives 59
Crossword Puzzle 24
2.1 Static Variables, Constants and Methods 59
Programming Exercises 25
2.2 Method Testing 61
2.2.1 More Details on Unit Testing 64
ix
x CONTENTS
2.8 Overriding the Object Class’s equals 4.2 Some Details of the Java Collections
Method 94 Framework 136
Summary 97 4.2.1 Abstract Classes 137
4.2.2 Parameterized Types 140
Crossword Puzzle 98
4.2.3 The Collection Interface 141
Concept Exercises 99
4.2.4 The List Interface 147
Programming Exercises 100
Summary 150
Programming Project 2.1: An Integrated Web
Crossword Puzzle 151
Browser and Search Engine, Part 1 102
Concept Exercises 152
Programming Exercises 152
CHAPTER 3 Programming Project 4.1: Wear a Developer’s Hat
and a User’s Hat 153
Analysis of Algorithms 105
Chapter Objectives 105
3.1 Estimating the Efficienc of Methods 105
3.1.1 Big-O Notation 106
CHAPTER 5
3.1.2 Getting Big-O Estimates Quickly 110
Recursion 155
3.1.3 Big-Omega, Big-Theta and Plain Chapter Objectives 155
English 116 5.1 Introduction 155
3.1.4 Growth Rates 117
5.2 Factorials 156
3.1.5 Trade-Offs 119
5.2.1 Execution Frames 159
3.2 Run-Time Analysis 121
5.3 Decimal to Binary 162
3.2.1 Timing 121
3.2.2 Overview of the Random Class 122 5.4 Towers of Hanoi 167
5.4.1 Analysis of the move Method 177
Summary 126
5.5 Searching an Array 179
Crossword Puzzle 127
5.6 Backtracking 191
Concept Exercises 128
5.6.1 An A-maze-ing Application 195
Programming Exercises 130
5.7 Indirect Recursion 208
Programming Project 3.1: Let’s Make a
5.8 The Cost of Recursion 209
Deal! 131
Summary 210
Crossword Puzzle 211
CHAPTER 4 Concept Exercises 212
Programming Exercises 214
The Java Collections
Framework 133 Programming Project 5.1: Iterative Version of the
Towers of Hanoi 219
Chapter Objectives 133
Programming Project 5.2: Eight Queens 221
4.1 Collections 133
Programming Project 5.3: A Knight’s Tour 222
4.1.1 Collection Classes 134
4.1.2 Storage Structures for Collection Programming Project 5.4: Sudoku 225
Classes 136 Programming Project 5.5: Numbrix 227
CONTENTS xi
CHAPTER 9
11
Binary Trees 377
CHAPTER
Chapter Objectives 377
Sorting 457
9.1 Definitio of Binary Tree 377
9.2 Properties of Binary Trees 378 Chapter Objectives 457
9.3 The Binary Tree Theorem 383 11.1 Introduction 457
CONTENTS xiii
12
13
CHAPTER
Tree Maps and Tree Sets 501 CHAPTER
Priority Queues 551
Chapter Objectives 501
12.1 Red-Black Trees 501 Chapter Objectives 551
12.1.1 The Height of a Red Black 13.1 Introduction 551
Tree 503 13.2 The PriorityQueue Class 552
12.2 The Map Interface 504 13.3 Implementation Details of the
12.3 The TreeMap Implementation of the PriorityQueue Class 553
SortedMap Interface 509 13.3.1 Fields and Method Definition in
12.3.1 The TreeMap Class’s Fields and the PriorityQueue Class 557
Embedded Entry Class 512 13.4 The heapSort Method 567
12.3.2 Method Definition in the TreeMap 13.4.1 Analysis of heapSort 572
Class 513 13.5 Application: Huffman Codes 573
12.4 Application of the TreeMap Class: a Simple 13.5.1 Huffman Trees 575
Thesaurus 517 13.5.2 Greedy Algorithm Design
12.4.1 Design, Testing, and Implementation Pattern 578
of the Thesaurus Class 518 13.5.3 The Huffman Encoding
12.4.2 Design and Testing of the Project 578
ThesaurusUser Class 521 Summary 590
12.4.3 Implementation of the Crossword Puzzle 591
ThesaurusUser Class 523 Concept Exercises 592
xiv CONTENTS
15
Programming Project 13.2: An Integrated Web
Browser and Search Engine, Part 5 597 CHAPTER
Graphs, Trees, and Networks 643
APPENDIX 1 APPENDIX 3
Additional Features of the JAVA Choosing a Data Structure 721
Collections Framework 701
A3.1 Introduction 721
A1.1 Introduction 701 A3.2 Time-Based Ordering 721
A1.2 Serialization 701 A3.3 Index-Based Ordering 721
A1.3 Fail-Fast Iterators 703 A3.4 Comparison-Based Ordering 722
A3.5 Hash-Based Ordering 723
A3.6 Space Considerations 723
APPENDIX 2 A3.7 The Best Data Structure? 724
Mathematical Background 705
A2.1 Introduction 705
References 725
A2.2 Functions and Sequences 705
A2.3 Sums and Products 706 Index 727
A2.4 Logarithms 707
A2.5 Mathematical Induction 708
A2.6 Induction and Recursion 719
Concept Exercises 719
This page intentionally left blank
PREFACE
This book is intended for an object-oriented course in data structures and algorithms. The implementation
language is Java, and it is assumed that students have taken a f rst course in programming, not necessarily
in Java. That course should have covered the fundamental statements and data types, as well as arrays.
Chapter 0 supplies the material on Java that is fundamental to the remaining chapters, so it serves as a
review for those with previous experience in Java, and brings Java novices up to speed.
if a wrapper-class element appears where a primitive value is needed, unboxing automatically converts
that element to the corresponding primitive value. Finally, the enhanced for statement—often called a
“for-each” statement—has a sleek structure for iterating through a collection. The net effect of these new
features of Java is to improve productivity by relegating to the compiler many of the “boiler-plate” details
related to casting and iterating.
PEDAGOGICAL FEATURES
This text offers several features that may improve the teaching environment for instructors and the learning
environment for students. Each chapter starts with a list of objectives, and most chapters conclude with
several major programming assignments. Each chapter also has a crossword puzzle, from Crossword
PREFACE xix
Weaver—to help students learn the key words and phrases in an enjoyable setting—and a variety of
exercises. The answers to all of the exercises are available to the instructor.
Each data structure is carefully described, with the specification for each method given in javadoc
notation. Also, there are examples of how to call the method, and the results of that call. To reinforce
the important aspects of the material and to hone students’ coding skills in preparation for programming
projects, there is a suite of 23 lab experiments. The organization of these labs is described later in this
preface.
SUPPORT MATERIAL
The website for all of the support material is www.wiley.com/college/collins/
That website has links to the following information for students:
• The suite of 23 labs. Lab 0 starts with a brief overview of the lab format.
• The source codes for all classes developed in the text.
• Applets for projects that have a strong visual component.
Additionally, instructors can obtain the following from the website:
• PowerPoint slides for each chapter (approximately 1500 slides).
• Answers to every exercise and lab experiment.
In Chapter 10, we look at binary search trees, including a BinarySearchTree class, and explain the
value of balanced binary search trees. Rotations are introduced as the mechanism by which re-balancing
is accomplished, and AVL trees are offered as examples of balanced binary search trees. An AVLTree
class, as a subclass of BinarySearchTree, is outlined; the crucial methods, fixAfterInsertion and
fixAfterDeletion, are left as programming projects.
Sorting is the theme of Chapter 11. Estimates of the lower bounds for comparison-based sorts are
determined. A few simple sorts are defined and then we move on to two sort methods provided by the
framework. Quick Sort sorts an array of a primitive type, and Merge Sort works for an array of objects
and for implementations of the List interface. A lab experiment compares all of these sort algorithms on
randomly-generated integers.
The central topic of Chapter 12 is how to use the TreeMap class. A map is a collection in which
each element has a unique key part and also a value part. In the TreeMap implementation of the Map
interface, the elements are stored in a red-black tree, ordered by the elements’ keys. There are labs to
guide students through the details of re-structuring after an insertion or removal. The application consists
of searching a thesaurus for synonyms, and JUnit 4 testing is again illustrated. The TreeSet class has a
TreeMap fiel in which each element has the same, dummy value-part. The application of the TreeSet
class is a simple spell-checker, which is also thoroughly tested.
Chapter 13 introduces the PriorityQueue class. This class is part of the Java Collections Frame-
work and, like the Stack class and Queue interface in Chapter 8, allows methods that violate the definitio
of a priority queue. The class utilizes a heap to provide insertions in constant average time, and removal of
the smallest-valued element in logarithmic worst time. The application is in the area of data compression:
Given a text file generate a minimal, prefix-fre encoding. There is a project assignment to convert the
encoded message back to the original text f le.
Chapter 14 investigates hashing. The Java Collections Framework has a HashMap class for elements
that consist of unique-key/value pairs. Basically, the average time for insertion, removal, and searching is
constant! This average speed is exploited in an application (and JUnit 4 tests) to create a simple symbol
table. The Java Collections Framework’s implementation of hashing, using chained hashing, is compared
to open-address hashing.
The most general data structures—graphs, trees, and networks—are presented in Chapter 15. There
are outlines of the essential algorithms: breadth-fir t traversal, depth-firs traversal, f nding a minimum
spanning tree, and f nding the shortest or longest path between two vertices. The only class developed
is the (directed) Network class, with an adjacency-map implementation. Other classes, such as Undi
rectedGraph and UndirectedNetwork, can be straightforwardly define as subclasses of Network.
The Traveling Salesperson Problem is investigated in a lab, and there is a programming project to solve
that problem—not necessarily in polynomial time! Another backtracking application is presented, with the
same BackTrack class that was introduced in Chapter 5.
The website includes all programs developed in each chapter, all JUnit 4 tests, and applets, where
appropriate, to animate the concepts presented.
APPENDIXES
Appendix 1 has two additional features of the Java Collections Framework. Each of the collection classes in
the framework is serializable, that is, an instance of the class can be conveniently stored to an output stream,
and the instance can later be re-constituted from an input stream (de-serialization). Framework iterators
are fail-fast: During an iteration through a collection, there should be no insertions into or removals from
xxii PREFACE
the collection except by the iterator. Otherwise, the integrity of that iterator may be compromised, so an
exception will be thrown as soon as the iterator’s unreliability has been established.
Appendix 2 contains the background that will allow students to comprehend the mathematical aspects
of the chapters. Summation notation and the rudimentary properties of logarithms are essential, and the
material on mathematical induction will lead to a deeper appreciation of recursion as well as the analysis
of binary trees.
Appendix 3, “Choosing a Data Structure,” can be viewed as a summary of the eight major data
structures in the book. These collection classes are categorized according to their ordering of elements (for
example, time-based for stacks and queues) and according to their performance of standard operations (for
example, the TreeMap class requires logarithmic-in-n time to search for a key). Table A3.1 summarizes
the summary.
ACKNOWLEDGEMENTS
Joshua Bloch, lead designer of the Java Collections Framework, gave valuable insights on type parameters
and their impact on the Java Collections Framework.
Chun Wai Liew of Lafayette College helped to incorporate JUnit into this edition of the text.
I am indebted to the suggestions and corrections of the reviewers: Dean Hendrix (Auburn University),
Benjamin Kuperman (Oberlin College), Andrew Haas (State University of New York—Albany), Kathy
Liszka (University of Akron), Paul Bladek (Edmonds Community College), Siva Jasthi (Metropolitan State
University), Hashem Anwari (Northern Virginia Community College), Alan Peslak (Nova Southeastern
University), H. K. Dai (Oklahoma State University), Jiang Li (Austin Peay State University), Paul J.
Wagner (University of Wisconsin—Eau Claire), John Mallozzi (Iona College), Robert Goldberg (Queens
College), and Michael Clancy (University of California—Berkeley).
Introduction to Java CHAPTER 0
This is a book about programming: specifically, about understanding and using data structures and
algorithms. The Java Collections Framework has a considerable number of data structures and
algorithms. Subsequent chapters will focus on what the framework is and how to use the framework
in your programs. For this information to make sense to you, you will need to be familiar with certain
aspects of Java that we present in this chapter. All of the material is needed, either for the framework
itself or to enable you to use the framework in your programming projects.
CHAPTER OBJECTIVES
1. Learn (or review) the fundamentals of Java, including classes, objects and messages.
2. Be able to use javadoc in writing method specifications.
3. Incorporate the Scanner class into your programming.
4. Understand the significance of the fact that a copy of the argument is stored in the corre-
sponding parameter when a method is called.
5. Understand the details of arrays and output formatting.
The main method in this program calls another method, println, to produce the following output to the
console window:
Hello, world!
1
2 CHAPTER 0 Introduction to Java
Console output, that is, output to the console window on your monitor, is handled by the methods
System.out.print, System.out.println, and System.out.printf (see Section 0.5).
int score;
By a standard abuse of language, we say that score is a variable instead of saying that score is an
identifie for a variable. An assignment statement allows us to store a value in a variable. For example,
score = 0;
stores the value 0 in the variable score. A subsequent assignment can change the value stored:
score = 88;
The left-hand side of an assignment statement must be a variable, but the right-hand side can be an arbitrary
expression: any legal combination of symbols that has a value. For example, we can write
score = (score + 3) / 10;
If score had the value 88 prior to the execution of this assignment statement, then after its execution, score
would have the value 9. Note that the division operator, /, returns the result of integer division because
the two operands, 91 and 10, are both integers.
Another operator in the int type is %, the modulus operator, which returns the integer remainder
after integer division. For example, 91 % 10 returns the remainder, 1, when 91 is divided by 10. Similarly,
87 % 2 returns 1, (−37) % 5 returns −2, and 10 % 91 returns 10.
Java supports eight primitive types, summarized in Table 0.1.
Then delimiter is a variable of type char and contains the value ' '. The Unicode collating sequence
also includes other—that is, non-Roman, alphabets—such as Greek, Cyrillic, Arabic, and Hebrew. The
Unicode collating sequence holds up to 65,536 (= 216 ) distinct characters, but only about half of them have
been assigned as of now. To include a character such as ☺ in a program, you provide an escape sequence:
0.2 Classes 3
a sequence of symbols that starts with the backslash character, ‘\’ and designates a single character. For
example, the escape sequence for the smiley-face character, ☺, is ‘\u263A’, so we can write
char c = '\u263A';
An escape sequence is also needed to print a double quote—otherwise, the double quote would signify
the end of the string to be output. For example, the execution of
System.out.println ("His motto was \"Don't sweat the nickels and dimes!\"");
0.2 Classes
In addition to primitive types such as int and char, Java provides programmers with the ability to create
powerful new types called “classes.” Given a problem, we develop classes—or utilize already existing
classes—that correspond to components of the problem itself. A class combines the passive components
(fields and active components (methods) into a single entity. This grouping increases program modularity:
4 CHAPTER 0 Introduction to Java
the separation of a program into components that are coherent units. Specifically a class is isolated from
the rest of the program, and that makes the whole program easier to understand and to modify.
In Section 0.2.1, we investigate the class concept in more detail by looking at a specifi example:
the String class, the most widely used of Java’s pre-declared classes.
Then s1 is not a String object, but a variable that can contain the address of a String object.1 In order
for s1 to actually contain such a reference, the space for a String object must be allocated, then the field
in that newly created String object must be initialized, and finally the address of that String object
must be assigned to s1. We combine these three steps into a single assignment statement. For example, if
we want s1 to be a reference to an empty String object, we write:
s1 = new String();
The right-hand side of this assignment statement accomplishes several tasks. The new operator allocates
space in memory for a String object, calls a special method known as a “constructor” to initialize the
field in the object, and returns the address of that newly created object; that address is assigned to s1. A
constructor is a method whose name is the same as the class’s name and whose purpose is to initialize
the object’s fields In this example, the f elds are initialized to the effect that the newly created String
object represents an empty string, that is, a string that contains no characters.
The constructor just mentioned has no parameters, and is called the default constructor. The String
class also has a constructor with a String-reference parameter. Here is the heading of that constructor:
public String (String original)
The parameter original is of type reference-to-String. When this constructor is called, the
argument—inside the parentheses—will be assigned to the parameter, and then the body of the
constructor (the statements inside the braces) will be executed. For an example of a call to this
1 In the languages C and C++, a variable that can contain the address of another variable is called a pointer variable.
0.2 Classes 5
constructor, the following statement combines the declaration of a reference variable and the assignment
to that variable of a reference to a newly constructed String object:
String s2 = new String ("transparent");
When this statement is executed, the space for a new String object is allocated, the field in that
newly created String object are initialized to the effect that the new String object represents the string
"transparent", and the address of that new String object is assigned to the String reference s2.
Now that s1 and s2 contain live references, the objects referenced by s1 and s2 can invoke
any String method.2 For example, the length method takes no parameters and returns the number of
characters in the calling object, that is, the object that invokes the length method. We can write
System.out.println (s1.length());
then the output will be 11 because the calling object contains the string "transparent".
The default constructor and the constructor with a String-reference parameter have the same name,
String, but have different parameter lists. Java allows method overloading: the ability of a class to have
methods with the same method identifie but different parameter lists. In order to clarify exactly what
method overloading entails, we defin a method’s signature to consist of the method identifie together
with the number and types of parameters, in order. Method overloading is allowed for methods with
different signatures. For example, consider the following method headings:
public String findLast (int n, String s)
In this example, the f rst method’s parameter list starts with an int parameter, but the second method’s
parameter list starts with a String parameter, so the two methods have different signatures. It is legal for
these two methods to be define in the same class; that is, method overloading is permitted. Contrast this
example with the following:
public String findLast (int n, String s)
Here the two methods have the same signature—notice that the return type is irrelevant in determining
signature—so it would be illegal to defin these two methods in the same class.
2 Except String constructors, which are invoked by the new operator. For that reason, and the fact that constructors do not have a return
type, the developers of the Java language do not classify a constructor as a method (see Arnold, 1996). But for the sake of simplicity, we
lump constructors in with the methods of a class.
6 CHAPTER 0 Introduction to Java
The method specificatio will include javadoc notation. javadoc is a program that converts Java
source code and a specially formatted block of comments into Application Programming Interface (API)
code in Hypertext Markup Language (HTML) for easy viewing on a browser. Because javadoc is available
on any system that has Java, javadoc format has become the standard for writing method specifications
Each comment block starts with “/**”, each subsequent line starts with “*”, and the fina line in a block
has “*/”. The complete specificatio consists of the javadoc comments and the method heading:
/**
* Returns a copy of the substring, between two specified indexes, of this String
* object.
*
* @param beginIndex – the starting position (inclusive) of the substring.
* @param endIndex – the final position (exclusive) of the substring.
*
* @return the substring of this String object from indexes beginIndex (inclusive)
* to endIndex (exclusive).
*
* @throws IndexOutOfBoundsException – if beginIndex is negative, or if
* beginIndex is greater than endIndex, or if endIndex is greater than
* length().
*
*/
public String substring (int beginIndex, int endIndex)
The f rst sentence in a javadoc comment block is called the postcondition: the effect of a legal call to
the method. The comment block also indicates parameters (starting with @param), the value returned
(@return), and what exceptions can be thrown (@throws). An exception, such as IndexOutOfBounds
Exception, is an object created by an unusual condition, typically, an attempt at invalid processing.
Section 2.2 covers the topic of exceptions, including how they are thrown and how they are caught. To
avoid confusing you, we will omit @throws comments for the remainder of this chapter.
To illustrate the effect of calls to this method, here are several calls in which the calling object is
either an empty string referenced by s1 or the string “transparent” referenced by s2:
s1.substring (0, 0) // returns reference to an empty string
s1.substring (0, 1) // error: 2nd argument > length of calling object
s2.substring (1, 4) // returns reference to copy of "ran", a 3-character string
s2.substring (5, 10) // returns reference to copy of "paren", a 5-character string
s2.substring (5, 11) // returns reference to copy of "parent", a 6-character string
There are several points worth mentioning about the comment block. In the postcondition and elsewhere,
“this String object” means the calling object. What is returned is a reference to a copy of the substring.
And the last character in the designated substring is at endIndex -1, not at endIndex.
The javadoc comment block just given is slightly simpler than the actual block for this substring
method in the String class. The actual javadoc comment block includes several html tags: <pre>,
<blockquote>, and <code>. And if you viewed the description of that method from a browser—that is,
after the javadoc program had been executed for the String class—you would see the comments in an
easier-to-read format. For example, instead of
0.2 Classes 7
* @return the substring of this String object from indexes beginIndex (inclusive)
* to endIndex (exclusive).
Returns:
the substring of this String object from indexes beginIndex (inclusive)
to endIndex (exclusive).
The on-line Java documentation is generated with javadoc. And the documentation about a method in one
class may include a hyperlink to another class. For example, the heading for the next() method in the
Scanner class is given as
So if you are looking at the documentation of the next() method and you want to see some information
on the String class, all you need to do is click on the String link.
Throughout the remainder of this text, we will regularly use javadoc to provide information about a
method. You should try to use javadoc to describe your methods.
In object-oriented parlance, when a method is invoked, a message is being sent to the calling object.
The term “message” is meant to suggest that a communication is being sent from one part of a program
to another part. For example, the following message returns the length of the String object referenced
by s2:
s2.length()
This message requests that the object referenced by s2 return its length, and the value 11 is returned.
The form of a message consists of a reference followed by a dot—called the member-selection opera-
tor —followed by a method identifie followed by a parenthesized argument list.
Make sure you understand the difference between a null reference (such as s3), and a reference
(such as s1) to an empty string. That distinction is essential to an understanding of Java’s object-reference
model.
The distinction between objects and references is prominent in comparing the equals method and
the == operator. Here is the method specificatio for equals:
/**
* Compares this String object to a specified object:
8 CHAPTER 0 Introduction to Java
* The result is true if and only if the argument is not null and is a String object
* that represents the same sequence of characters as this String object.
*
* @param anObject - the object to compare this String against.
*
* @return true - if the two String objects are equal; false otherwise.
*
*/
public boolean equals (Object anObject)
The parameter’s type suggests that the calling object can be compared to an object of any type, not just
to a String object. Of course, false will be returned if the type is anything but String. The Object
class is discussed in Chapter 1.
The == operator simply compares two references: true is returned if and only if the two references
contain the same address. So if str1 and str2 are referencing identical String objects that are at different
addresses,
str1.equals (str2)
will return true because the String objects are identical, but
str1 == str2
will return false because the str1 and str2 contain different addresses.
Finally, you can create a String object without invoking the new operator. For example,
String str0 = "yes",
str3 = "yes";
Because the underlying strings are identical, only one String object is constructed, and both str0 and
str3 are references to that object. In such cases, we say that the String object has been interned .
Figure 0.1 has several examples, and contrasts the String method equals with the reference
operator ==.
The reason the output is different for the f rst and third calls to println in Figure 0.1 is that the
equals method compares strings and the == operator compares references. Recall that each time the new
operator is invoked, a new String object is created. So, as shown in Figure 0.2, s4 is a reference to
restful
s4
restful
s5
peaceful
s6
s7
s8
restful
s9
FIGURE 0.2 An internal view of the references and objects in Figure 0.1
a String object whose value is “restful”, and s5 is a reference to a different String object whose value
is also “restful”.
Local variables must be explicitly initialized before they are used. For example, suppose we have
public void run()
{
int k;
Compilation will fail, with an error message indicating that “variable k might not have been initialized.”
The phrase “might not have been initialized” in the error message suggests that the compiler does not
perform a detailed analysis of the method’s code to determine if, in fact, the variable has been properly
initialized. For example, the same error message will be generated by the following method:
public void run()
{
int k;
if (flag)
k = 20;
if (!flag)
k = 21;
System.out.println (isPrime (k));
} // method run
Clearly, if we look at this method as a whole, the variable k does receive proper initialization. But if
each statement is considered on its own, no such guarantee can be made. The following slight variant is
acceptable because the if-else statement is treated as a unit:
public void run()
{
int k;
if (flag)
k = 20;
else
k = 21;
System.out.println (isPrime (k));
} // method run
0.2 Classes 11
The scope of an identifie is the region of a program to which that identifier’ declaration applies. In
the isPrime method, the scope of the parameter n is the entire function definition but the scope of the
variable j is only the for statement. A compile-time error results if an attempt is made to access an
identifie outside of its scope: for example, if we tried to print out the value of j outside of the for
statement in the isPrime method. This restriction of identifier to specifi code segments—and not, for
example, to an entire method—promotes modularity.
To see how it is possible to declare the same identifie more than once in a class, we need to defin
what a “block” is. A block is an enclosed sequence of declarations and/or statements enclosed in curly
braces { }. For a f eld identifier its enclosing block is the entire class enclosed by the curly braces. It is
permissible to re-declare the fiel identifie within a method in the class. But it is illegal to re-declare
a local identifie within its block. Special case: for a variable identifie declared in the header of a for
statement, its scope is the entire for statement. So it is possible to have two for statements in the same
method with identical variable identifier declared in the headers of those for statements (but that identifie
cannot also be declared outside of those for statements as a local variable of the method). The following
class illustrates the scopes of several identifiers
public class Scope
{
boolean t = true;
int x = 99;
x = 5.3;
for (int t = 0; t < 3; t++)
{
int i = t + 4;
System.out.println (i + t + x);
} // end of int t’s scope; end of i’s scope
} // class Scope; end of boolean t’s scope; end of int x’s scope; end of double sample’s scope
All three field — t, x and sample —are re-declared within the method sample. But the scope of those
three f elds includes the method original. And note that there is no ambiguity when sample is used
12 C H A P T E R 0 Introduction to Java
both as a fiel identifie and as a method identifier because a method identifie is always followed by
parentheses.
The following declares a Scanner object that will read from the fil named “myFile.dat”:
Scanner scanner = new Scanner (new File ("myFile.dat"));
If, instead, we want to scan over the String object line, we can declare the following:
Scanner lineScanner = new Scanner (line);
We can now use the Scanner object sc declared above to read in an int value representing a test score:
int score = sc.nextInt( );
In order to understand how the nextInt method works, we need to introduce some terminology. A scanner
subdivides the text into tokens separated by delimiters. In the case of the nextInt method, the delimiters
are whitespace characters: blanks, end-of-line markers, newline characters, tabs, and so on. The tokens are
everything else. The scanning proceeds as follows: First, all whitespace is skipped over. Then the token is
read in. If the characters in the token represent an int value, that value is stored in the variable score.
In Section 2.2 of Chapter 2, there is a discussion of what happens if the token does not represent an int
value.
We can read in and add up scores until a sentinel of −1 is read. In the following program, we need
to utilize the class java.util.Scanner in the package java.util. Because we will often want several
classes from java.util, we specify that we want all of the classes from that package to be available.
How? By denoting java.util.* in the import directive, we notify the compiler that the entire package
java.util is to be made available.
In this program, and in all subsequent programs in this book, the main method consists of a single
line. A new instance of the class is created with a call to the class’s default constructor (automatically
supplied by the compiler), and this new instance invokes its run method. Here is the complete f le:
import java.util.*; // for the Scanner class
int score,
sum = 0;
while (true)
{
System.out.print (INPUT_PROMPT);
score = sc.nextInt();
if (score == SENTINEL)
break;
sum += score;
} // while
System.out.println (SUM_MESSAGE + sum);
} // method run
} // class Sum
A noteworthy feature of this program is the structure of the while statement. The loop continues until
the sentinel is read in. The execution of the break statement causes an abrupt exit of the loop; the next
statement to be executed is the println that outputs the sum. The loop has only one entrance and only
one exit, and that helps us to understand the action of the loop. Also, there is only one place where the
prompt is printed, and only one place where input is read.
In that program, why did we use a sentinel instead of allowing the end user to terminate the loop
by not entering more values? When sc.next() is called, the program will pause until a non-whitespace
value is entered (followed by a pressing of the Enter key). In other words, a sentinel is needed to terminate
keyboard input. For scanning a line or a file there will rarely be a sentinel, so a call to the next() method
should be preceded by a call to the Scanner class’s hasNext() method, which returns true if there is
still another token to be scanned in, and false otherwise. This will ensure that the call to next() will not
cause an abnormal termination due to the lack of a next token.
In the class Sum, the end-user was prompted to enter a single int value per line. In general, a
line may have several int values, or even no int values. For example, we could have the following:
Scanner sc = new Scanner (System.in);
85
95 87
The variables score1, score2, and score3 will now have the values 85, 95, and 87, respectively.
For a slightly more complicated example, the following program reads from a file Each line in the
fil consists of a student’s name and grade point average. The output is the name of the student with the
highest grade point average. There is no sentinel. Instead, the scanning continues as long as the input fil
has another token, that is, any sequence of characters excluding whitespace. As indicated previously, the
hasNext( ) method returns true if and only if there are any tokens remaining in the file The next( )
method returns the next token as a string.
import java.util.*; // for the Scanner class
String name,
bestStudent = null;
double gpa,
highestGPA = NEGATIVE_GPA;
while (fileScanner.hasNextLine())
{
Scanner lineScanner = new Scanner (fileScanner.nextLine());
name = lineScanner.next();
gpa = lineScanner.nextDouble();
if (gpa > highestGPA)
0.2 Classes 15
{
highestGPA = gpa;
bestStudent = name;
} // if
} // while
if (highestGPA == NEGATIVE_GPA)
System.out.println (NO_VALID_INPUT);
else
System.out.println (BEST_MESSAGE + bestStudent);
} // method run
} // class HighestGPA
Larry 3.3
Curly 3.7
Moe 3.2
The corresponding output is:
The student with the highest grade point average is Curly
In the above program, the name of the input fil was “hard-wired,” that is, actually specifie in the code.
It is more realistic for the end-user to enter, from the keyboard, the input-fil path. Then we need two
scanner objects: one to read in the input-fil path, and another to read the input fil itself. Because a
fil path may contain blank spaces, we cannot invoke the next() method to read in the input-fil path.
Instead, we call the nextLine() method, which advances the scanner past the current line, and returns
(the remainder of) the current line, excluding any end-of-line marker. Here is the code that replaces the
declaration and assignment to fileScanner in that program:
final String IN_FILE_PROMPT = "Please enter the path for the input file: ";
System.out.print (IN_FILE_PROMPT);
Here are sample input and output for the resulting program, with the input in boldface:
Please enter the path for the input file students.dat
Keep in mind that the next() method skips over whitespace and returns the next token, but the
nextLine() method skips past the current line, and returns that current line, excluding any end-of-line
marker. Similarly, the hasNext() method returns true if and only if there is another token in
the text, while the hasNextLine() method returns true if and only if there is at least one more
character—including whitespace—in the text. So if the text has a line of blanks remaining, or even an
extra end-of-line marker, hasNext() will return false, but hasNextLine( ) will return true.
The above example illustrates a pattern we will see over and over in the remaining chapters. A
keyboard scanner scans in the path of an input file a f le scanner scans in the lines in the f le, and a line
scanner scans over a single line.
In the next example, a scanner retrieves each word in a line, and the word is converted to lower-case
and printed. The scanner is declared in a method whose only parameter is a line to be parsed into words:
public void run()
{
split ("Here today gone tomorrow");
} // method run
while (sc.hasNext())
System.out.println (sc.next().toLowerCase());
} // method split
here
today
gone
tomorrow
Unfortunately, if the input contains any non-alphabetic, non-whitespace characters, those characters will
be included in the tokens. For example, if the call is
split ("Here today, gone tomorrow.");
here
today,
gone
tomorrow.
We can override the default delimiter of whitespace with the useDelimiter (String pattern)
method, which returns a (reference to a) Scanner object. For example, if we want the delimiter to be
any positive number of non-alphabetic characters, we can explicitly indicate that as follows:
Scanner sc = new Scanner (line).useDelimiter ("[∧ a-zA-Z]+");
In the argument to the useDelimiter method, the brackets specify a group of characters, the ‘∧ ’ specifie
the complement of the characters that follow, and ‘+’ is shorthand for any positive number of occurrences
0.3 Arrays 17
of the preceding group. In other words, we are definin a delimiter as any sequence of one or more
occurrences of characters that are non-alphabetic. So if we have included the above useDelimiter call,
and we have
split ("Here today, gone tomorrow.");
here
today
gone
tomorrow
Finally, suppose we want to allow a word to have an apostrophe. Then we include the apostrophe in the
class whose complement define the delimiters:
Scanner sc = new Scanner (line).useDelimiter ("[∧ a-zA-Z’]+");
If the call is
split ("You’re 21?? I’ll need to see some ID!");
you’re
i’ll
need
to
see
some
id
Cultural Note: The Scanner class enables a user to process a regular expression: a format for identifying
patterns in a text. The arguments to the useDelimiter methods shown previously are simple examples
of regular expressions. In general, regular expressions provide a powerful but somewhat complex means
of findin strings of interest in a text. For more information on handling regular expressions in Java, see
http://www.txt2re.com and (Habibi, 2004).
0.3 Arrays
An array is a collection of elements, of the same type, that are stored contiguously in memory. Contiguous
means “adjacent,” so the individual elements are stored next to each other.3 For example, we can create
an array of f ve String objects as follows:
String [ ] names = new String [5];
3 Actually, all that matters is that, to a user of an array, the elements are stored as if they were contiguous.
18 C H A P T E R 0 Introduction to Java
Here the new operator allocates space for an array of f ve String references (each initialized to null by
the Java Virtual Machine), and returns a reference to the beginning of the space allocated. This reference
is stored in names.
In order to specify one of the individual elements in an array, an index is used. An index is an
integer expression in square brackets; the value of the expression determines which individual element is
being denoted. The smallest allowable index is 0. For example,
names [0] = "Cromer";
will store a reference to “Cromer” at the zeroth entry in the array (referenced by) names.
The size of an array is f xed once the array has been created, but the size need not be determined at
compile-time. For example, we can do the following:
public void processInput (String s)
{
int n = new Scanner (s).nextInt();
When the processInput method is executed at run time, names will be assigned a reference to an array
of n string references, all of which are initialized to null. An array is an object, even though there is
no corresponding class. We loosely refer to names as an “array,” even though “array reference” is the
accurate term.
The capacity of an array, that is, the maximum number of elements that can be stored in the array,
is stored in the array’s length field For example, suppose we initialize an array when we create it:
double [ ] weights = {107.3, 112.1, 114.4, 119.0, 117.4};
For an array x, the value of any array index must be between 0 and x.length-1, inclusive. If the value
of an index is outside that range, ArrayIndexOutOfBoundsException (See Section 2.3 of Chapter 2
for a discussion of exception handling) will be thrown, as in the following example:
final int MAX = 10;
For the firs ten iterations of the loop, with i going from 0 through 9, each element in the array is initialized
to 0.00. But when i gets the value 10, an ArrayIndexOutOfBoundsException is thrown because i’s
value is greater than the value of salaries.length-1, which is 9.
0.4 Arguments and Parameters 19
triple (k);
System.out.println (k);
} // method run
} // class Nothing
The output will be 30. When the method triple (int n) is called, a copy of the value of the argument
k is assigned to the parameter n. So at the beginning of the execution of the call, n has the value 30. At the
end of the execution of the call, n has the value 90, but the argument k still has the value 30. Incidentally,
the name of the parameter has no effect: the result would have been the same if the parameter had been
named k instead of n.
Next, we look at what happens if the argument is of type reference. As with primitive arguments,
a reference argument will not be affected by the method call. But a much more important issue is “What
about the object referenced by the argument?” If the object is an array, then the array can be affected by
the method call. For example, here is a simple program in which an array is modifie during a method
call:
public class Swap
{
public static void main (String[ ] args)
{
new Swap().run();
} // method main
} // class Swap
12.2 8.0
In the array referenced by x, the elements at indexes 1 and 2 have been swapped.
For objects other than arrays, whether the object can be affected by the method call depends on the
class of which the object is an instance. For example, the String class is immutable, that is, there are
no methods in the String class that can modify an already constructed String object. So if a String
reference is passed as an argument in a method call, the String object will not be affected by the call.
The following program illustrates the immutability of String objects.
public class Immutable
{
public static void main (String[ ] args)
{
new Immutable().run();
} // method main
The output will be “yes”. The flip method constructed a new String object, but did not affect the
original String object referenced by s. Figure 0.3 shows the relationship between the argument s and the
parameter t at the start of the execution of the call to the flip method. Figure 0.4 shows the relationship
between s and t at the end of the execution of the method call.
0.4 Arguments and Parameters 21
s
yes
FIGURE 0.3 The relationship between the argument s and the parameter t at the start of the execution of the
flip method in the class Immutable
s
yes
t
no
FIGURE 0.4 The relationship between the argument s and the parameter t at the end of the execution of the
flip method in the class Immutable
Some built-in classes are mutable. For example, the following program illustrates the mutability of Scanner
objects.
import java.util.*; // for the Scanner class
System.out.println (sc.next());
advance (sc);
System.out.println (sc.next());
} // method run
} // class Mutable
The string “no” was returned when scanner.next() was called in the advance method, and that call
advanced the current position of the Scanner object (referenced by both sc and scanner) beyond where
“no” is. Because of this alteration of the Scanner object, the second call to println in the run method
prints out “maybe”.
To summarize this section, an argument is never affected by a method call. But if the argument is a
reference, the corresponding object may (for example, an array object or a Scanner object) or may not
(for example, a String, Integer, or Double object) be affected by the call.
System.out.println (grossPay);
The f xed format, “0.00”, is the argument to the DecimalFormat constructor. That format specifie that
the String object returned by the format method will have at least one digit to the left of the decimal
point and exactly two digits to the right of the decimal point. The fractional part will be rounded to two
decimal digits; for example, suppose we have
DecimalFormat d = new DecimalFormat ("0.00");
$2,800.40
The DecimalFormat class is in the package java.text, so that package must be imported at the begin-
ning of the f le in which the formatting is performed.
An alternative to the output formatting just described is provided by the printf method, which is
similar to the printf function in the C language. For example, the following will print grossPay with at
least one digit to the left of the decimal point and exactly two digits (rounded) to the right of the decimal
point:
System.out.printf ("%1.2f", grossPay);
The f rst f eld is called the format, an expression in quotes that starts with a percent sign. The character
‘f’—called a “flag —signifie that grossPay will be printed with fractional digits. The “1.2”—the “width”
and “precision”—signifie that there will be at least one digit (even if it is a zero) to the left of the decimal
point, and exactly two digits (rounded) to the right of the decimal point. For example, suppose we have
double grossPay = 1234.567;
1234.57
If there are additional values to be printed, the format for each value starts with a percent sign inside the
format. For example,
double grossPay = 1234.567;
We can ensure there will be a comma between the hundreds and thousands digits by including the comma
flag ‘,’. For example, suppose we have
double grossPay = 1234.567;
More details on formatting in the printf method can be found in the Format class in the package
java.util.
24 C H A P T E R 0 Introduction to Java
CROSSWORD PUZZLE
5 6
www.CrosswordWeaver.com
ACROSS DOWN
3. A variable that can contain the address of an object. 1. The ability of a class to have methods with
the same method identifier but different
6. The region of a program to which an identifier’s parameter lists.
declaration applies.
2. A method’s identifier together with the
7. The separation of a program into components that number and types of parameters in order.
are coherent units.
4. Adjacent, as when individual elements are
8. A new, that is, non-primitive type. stored next to each other.
PROGRAMMING EXERCISES
0.1 The package java.lang includes the Integer class. In that class, what is the def nition of MAX_VALUE ? The
number 0x7fffffff is in hexadecimal , that is, base 16, notation. To indicate hexadecimal notation, start with
“0x”, followed by the hexadecimal value. In hexadecimal, there are sixteen digits: 0, 1, 2, . . ., 9, a, b, c, d,
e, and f. The digits 0 through 9 have the same value as in decimal, and the letters ‘a’ through ‘f’ have the
following decimal values:
a 10
b 11
c 12
d 13
e 14
f 15
Integer.MAX_VALUE + 1
Math.abs (Integer.MIN_VALUE)
Test your hypotheses with a small program that calls the System.out.println method.
0.2 Suppose we have the following:
int a = 37,
b = 5;
System.out.println (a - a / b * b - a % b);
Hypothesize what the output will be. Test your hypothesis by executing a small program that includes that code.
Can you fin another pair of positive int values for a and b that will produce different output? Explain.
0.3 Hypothesize the output from the following:
System.out.println (1 / 0);
System.out.println (1.0 / 0);
Test your hypotheses by executing a small program that includes that code.
0.4 In the String class, read the specifica ion for the indexOf method that takes a String parameter. Then
hypothesize the output from the following
Test your hypothesis by executing a small program that includes that code.
26 C H A P T E R 0 Introduction to Java
0.5 In the String class, read the specifica ion for the indexOf method that takes a String parameter and an
int parameter. Then hypothesize the output from the following
Test your hypothesis by executing a small program that includes that code.
0.6 Write and run a small program in which an input string is read in and the output is the original string with each
occurrence of the word “is” replaced by “was”. No replacement should be made for an embedded occurrence,
such as in “this” or “isthmus”.
0.7 Write and run a small program in which an input string is read in and the output is the original string with each
occurrence of the word “is” replaced by “is not”. No replacement should be made for an embedded occurrence,
such as in “this” or “isthmus”.
0.8 Write and run a small program in which the end user enters three lines of input. The firs line contains a string,
the second line contains a substring to be replaced, and the third line contains the replacement substring. The
output is the string in the firs line with each occurrence of the substring in the second line replaced with
the substring in the third line. No replacement should be made for an embedded occurrence, in the f rst line of
the substring in the second line.
0.9 Study the following method:
Trace the execution of this method when n = 7. In the same class as the method mystery, develop a main
method and a run method to show that the while statement in the method mystery successfully terminates
for any positive integer n less than 100. Cultural note: It can be shown that this method terminates for all int
values greater than 0, but it is an open question whether the method terminates for all integer values greater
than 0.
Object-Oriented Concepts CHAPTER 1
CHAPTER OBJECTIVES
1. Compare a user’s view of a class with a developer’s view of that class.
2. Understand how inheritance promotes code re-use.
3. Understand how polymorphic references can be useful.
4. Be able to create class diagrams in the Unified Modeling Language.
Currently, one of the f elds in the String class is an int fiel named count. But an expression such as
name.count
would be a violation of the Principle of Data Abstraction because whether or not the String class has a
count fiel is an implementation detail. The developer of the String class is free to make any changes
to the String class that do not affect the method specifications
27
28 C H A P T E R 1 Object-Oriented Concepts
Most programming languages, including Java, have features that enable a developer of a class to force
users to adhere to the Principle of Data Abstraction. These enforcement features are collectively known as
“information hiding.” We will discuss information hiding in Section 1.5 after we have introduced several
of the relevant features.
We noted earlier that the Principle of Data Abstraction is a benefi to users of a class because they
are freed from reliance on implementation details of that class. This assumes, of course, that the class’s
method specification provide all of the information that a user of that class needs. The developer of a class
should create methods with suff cient functionality that users need not rely on any implementation details.
That functionality should be clearly spelled out in the method specifications In particular, the user should
be told under what circumstances a method call will be legal, and what the effect of a legal call will be.
In a method specification the f rst sentence in a javadoc comment block is called the postcondition:
the effect of a legal call to the method. The information relating to the legality of a method call is
known as the precondition of the method. The precondition may be stated explicitly within or after the
postcondition (for example, “The array must be sorted prior to making this call.”) or implicitly from the
exception information (for example, any call that throws an exception is illegal). The interplay between a
method’s precondition and postcondition determines a contract between the developer and the user. The
terms of the contract are as follows:
If the user of the method ensures that the precondition is true before the method is invoked, the developer
guarantees that the postcondition will be true at the end of the execution of the method.
We can summarize our discussion of classes so far by saying that from the developer’s perspective, a class
consists of field and the definition of methods that act on those f elds. A user’s view is an abstraction
of this: A class consists of method specifications
The Java Collections Framework is, basically, a hierarchy of thoroughly tested classes that are
useful in a variety of applications. The programs in this book will use the Java Collections Framework,
so those programs will not rely on the definition of the framework’s methods. We will provide method
specification and an overview of most of the classes. Occasionally, to give you experience in reading the
code of professional programmers, you will get to study the f elds and some of the method definitions
In Section 1.2, we see the value of classes that have undefine methods.
/**
* Draws this Figure object centered at the given coordinates.
*
1.2 Abstract Methods and Interfaces 29
* @param x – the X coordinate of the center point of where this Figure object
* will be drawn.
* @param y – the Y coordinate of the center point of where this Figure object
* will be drawn.
*
*/
public void draw(int x, int y)
/**
* Moves this Figure object to a position whose center coordinates are specified.
*
* @param x – the X coordinate of the center point of where this Figure object
* will be moved to.
* @param y – the Y coordinate of the center point of where this Figure object
* will be moved to.
*
*/
public void move (int x, int y)
Each different type of figur will have to provide its own definition for the draw and move methods.
But by requiring that those definition adhere to the above specifications we introduce a consistency to
any application that uses the figur classes. A user of one of those classes knows the exact format for the
draw and move methods—and that will still be true for classes corresponding to new f gure-types.
Java provides a way to enforce this consistency: the interface. Each method heading is followed by a
semicolon instead of a definition Such a method is called an abstract method . An interface is a collection
of abstract methods and constants. There are no define methods and no f elds. For example, here is the
interface for f gures:
public interface Figure
{
final static int MAX_X_COORD = 1024;
/**
* Draws this Figure object centered at the given coordinates.
*
* @param x – the X coordinate of the center point of where this Figure
* object will be drawn.
* @param y – the Y coordinate of the center point of where this Figure
* object will be drawn.
*
*/
void draw(int x, int y);
/**
* Moves this Figure object to a position whose center coordinates are
* specified.
*
30 C H A P T E R 1 Object-Oriented Concepts
} // interface Figure
The interface Figure has two constants (MAX_X_COORD and MAX_Y_COORD) and two abstract methods,
(draw and move). In any interface, all of the method identifier and constant identifier are public, so the
declarations need not include the visibility modifie public.
When a class provides method definition for an interface’s methods, the class is said to implement
the interface. The class may also defin other methods. For example, here is part of a declaration of the
Circle class:
public class Circle implements Figure
{
// declaration of fields:
private int xCoord,
yCoord,
radius;
} // class Circle
The reserved word implements signals that class Circle provides method definition for the methods
whose specification are in the interface Figure. Interfaces do not include constructors because construc-
tors are always class specific The incompleteness of interfaces makes them uninstantiable, that is, we
cannot create an instance of a Figure object. For example, the following is illegal:
Figure myFig = new Figure(); // illegal
In the method specification in the Figure interface, the phrase “this Figure object” means “this object
in a class that implements the Figure interface.”
Of what value is an interface? In general, an interface provides a common base whose method
specification are available to any implementing class. Thus, an interface raises the comfort level of users
1.2 Abstract Methods and Interfaces 31
because they know that the specification of any method in the interface will be adhered to in any class
that implements the interface. In practice, once a user has seen an interface, the user knows a lot about any
implementing class. Unlike the interface, the implementing class will have constructors, and may defin
other methods in addition to those specifie in the interface.
A user is interested in abstract data types—interfaces—and a class’s method specifications while a devel-
oper focuses on data structures, namely, a class’s f elds and method definitions For example, one of the
Java Collections Framework’s interfaces is the List interface; one of the classes that implement that inter-
face is LinkedList. When we work with the List interface or the LinkedList method specifications
we are taking a user’s view. But when we consider a specifi choice of field and method definition in
LinkedList, we are taking a developer’s view.
In Chapter 0, we viewed the String class from the user’s perspective: what information about the
String class is needed by users of that class? A user of a class writes code that includes an instance
of that class. Someone who simply executes a program that includes an instance of a class is called an
end-user. A developer of a class actually creates field and method definitions In Section 1.2.2, we will
look at the developer’s perspective and compare the user’s and developer’s perspectives. Specifically
we create an interface, and utilize it and its implementing classes as vehicles for introducing several
object-oriented concepts.
/**
* Returns this Employee object’s name.
*
* @return this Employee object’s name.
*
*/
String getName();
/**
* Returns this Employee object’s gross pay.
*
* @return this Employee object’s gross pay.
*
*/
double getGrossPay();
/**
* Returns a String representation of this Employee object with the name
* followed by a space followed by a dollar sign followed by the gross
* weekly pay, with two fractional digits (rounded).
*
* @return a String representation of this Employee object.
*
*/
String toString();
} // interface Employee
The identifie MONEY is a constant identifie —indicated by the reserved word final. The reason for
declaring a MONEY object is to facilitate the conversion of a double value such as gross pay into a string
in dollars-and-cents format suitable for printing. Instead of having a separate copy of the MONEY object
for each instance of each class that implements the Employee interface, there is just one MONEY object
shared by all instances of implementing classes. This sharing is indicated by the reserved word static.
The phrase “this Employee object” means “the calling object in a class that implements the
Employee interface.”
The Employee interface’s method specification are all that a user of any implementing class will
need in order to invoke those methods. A developer of the class, on the other hand, must decide what field
to have and then defin the methods. A convenient categorization of employees is full-time and part-time.
Let’s develop a FullTimeEmployee implementation of the Employee interface.
1.2 Abstract Methods and Interfaces 33
For example, a developer may well decide to have two f elds: name (a String reference) and
grossPay (a double). The complete method definition are developed from the field and method spec-
ifications For example, here is a complete declaration of the FullTimeEmployee class; the next few
sections of this chapter will investigate various aspects of the declaration.
import java.text.DecimalFormat;
/**
* Initializes this FullTimeEmployee object to have an empty string for the
* name and 0.00 for the gross pay.
*
*/
public FullTimeEmployee()
{
final String EMPTY_STRING = "";
name = EMPTY_STRING;
grossPay = 0.00;
} // default constructor
/**
* Initializes this FullTimeEmployee object’s name and gross pay from a
* a specified name and gross pay.
*
* @param name - the specified name.
* @param grossPay - the specified gross pay.
*
*/
public FullTimeEmployee (String name, double grossPay)
{
this.name = name;
this.grossPay = grossPay;
} // 2-parameter constructor
/**
* Returns the name of this FullTimeEmployee object.
*
* @return the name of this FullTimeEmployee object.
*
*/
public String getName()
{
34 C H A P T E R 1 Object-Oriented Concepts
return name;
} // method getName
/**
* Returns the gross pay of this FullTimeEmployee object.
*
* @return the gross pay of this FullTimeEmployee object.
*
*/
public double getGrossPay()
{
return grossPay;
} // method getGrossPay
/**
* Returns a String representation of this FullTimeEmployee object with the
* name followed by a space followed by a dollar sign followed by the gross
* weekly pay, with two fractional digits (rounded), followed by "FULL TIME".
*
* @return a String representation of this FullTimeEmployee object.
*
*/
public String toString()
{
final String EMPLOYMENT_STATUS = "FULL TIME";
} // class FullTimeEmployee
In the two-parameter constructor, one of the parameters is name. The reserved word this is used to
distinguish between the scope of the f eld identifie name and the scope of the parameter name. In any
class, the reserved word this is a reference to the calling object, so this.name refers to the name fiel
of the calling object, and name by itself refers to the parameter name. Similarly, this.grossPay refers
to the calling object’s grossPay field and grossPay by itself refers to the parameter grossPay.
In the other methods, such as toString(), there is no grossPay parameter. Then the appearance
of the identifie grossPay in the body of the toString() method refers to the grossPay fiel of the
object that called the toString() method.
The same rule applies if a method identifie appears without a calling object in the body of a method.
For example, here is the definitio of the hasNextLine() method in the Scanner class:
public boolean hasNextLine()
{
saveState();
String result = findWithinHorizon(
".*("+LINE_SEPARATOR_PATTERN+")|.+$", 0);
1.2 Abstract Methods and Interfaces 35
revertState();
return (result != null);
}
There is no calling-object reference specifie for the invocation of the saveState() method, and so
the object assumed to be invoking saveState() is the object that called hasNextLine(). Similarly,
the methods findWithinHorizon and revertState are being called by that same object. Here is the
general rule, where a member is either a f eld or a method:
If an object has called a method and a member appears without an object reference in the method
definition, the member is part of the calling object.
As you can see from the declaration of the FullTimeEmployee class, in each method definitio there is
at least one fiel that appears without an object reference. In fact, in almost every method definitio in
almost every class you will see in subsequent chapters, there will be at least one fiel that appears without
an object (reference). Then the fiel is part of the calling object.
/**
* Determines and prints out the best paid of the full-time employees
* scanned in from a specified file.
*
*/
public void run() throws FileNotFoundException // see Section 2.3
36 C H A P T E R 1 Object-Oriented Concepts
{
final String INPUT_PROMPT = "Please enter the path for the file of employees: ";
final String BEST_PAID_MESSAGE =
"\n\nThe best-paid employee (and gross pay) is ";
String fileName;
System.out.print (INPUT_PROMPT);
fileName = new Scanner (System.in).nextLine();
Scanner sc = new Scanner (new File (fileName));
if (bestPaid == null)
System.out.println (NO_INPUT_MESSAGE);
else
System.out.println (BEST_PAID_MESSAGE + bestPaid.toString());
} // method run
/**
* Returns the best paid of all the full-time employees scanned in.
*
* @param sc – the Scanner object used to scan in the employees.
*
* @return the best paid of all the full-time employees scanned in,
* or null there were no employees scanned in.
*
*/
public FullTimeEmployee findBestPaid (Scanner sc)
{
FullTimeEmployee full,
bestPaid = new FullTimeEmployee();
while (sc.hasNext())
{
full = getNextEmployee (sc);
if (full.getGrossPay() > bestPaid.getGrossPay())
bestPaid = full;
} //while
if (bestPaid.getGrossPay() == 0.00)
return null;
return bestPaid;
} // method findBestPaid
/**
* Returns the next full-time employee from the file scanned by a specified Scanner
1.3 Inheritance 37
* object.
*
* @param sc – the Scanner object over the file.
*
* @return the next full-time employee scanned in from sc.
*
*/
private FullTimeEmployee getNextEmployee (Scanner sc)
{
Scanner lineScanner = new Scanner (sc.nextLine());
} // class Company
The above code is available from the Chapter 1 directory of the book’s website. If the fil name scanned
in from System.in is “full.in1”, and the corresponding fil contains
a 1000.00
b 3000.00
c 2000.00
As noted earlier, we should use existing classes whenever possible. What if a class has most, but not all,
of what is needed for an application? We could simply scrap the existing class and develop our own, but
that would be time consuming and inefficient Another option is to copy the needed parts of the existing
class and incorporate those parts into a new class that we develop. The danger with that option is that
those parts may be incorrect or inefficient If the developer of the original class replaces the incorrect or
inefficien code, our class would still be erroneous or inefficient A better alternative is to use inheritance,
explained in Section 1.3.
1.3 Inheritance
We should write program components that are reusable. For example, instead of definin a method that
calculates the average gross pay of 10 employees, we would achieve wider applicability by definin a
method that calculates the average gross pay of any number of employees. By writing reusable code, we
not only save time, but we also avoid the risk of incorrectly modifying the existing code.
One way that reusability can be applied to classes is through a special and powerful property of
classes: inheritance. Inheritance is the ability to defin a new class that includes all of the f elds and some
or all of the methods of an existing class. The previously existing class is called the superclass. The new
class, which may declare new field and methods, is called the subclass. A subclass may also override
38 C H A P T E R 1 Object-Oriented Concepts
existing methods by giving them method definition that differ from those in the superclass.1 The subclass
is said to extend the superclass.
For an example of how inheritance works, let’s start with the class FullTimeEmployee define in
Section 1.2.2. Suppose that several applications use FullTimeEmployee. A new application involves f nd-
ing the best-paid, full-time hourly employee. For this application, the information on an hourly employee
consists of the employee’s name, hours worked (an int value) and pay rate (a double value). Assume
that each employee gets time-and-a-half for overtime (over 40 hours in a week). If the hourly employee
did not work any overtime, the gross pay is the hours worked times the pay rate. Otherwise, the gross pay
is 40 times the pay rate, plus the overtime hours times the pay rate times 1.5.
We could alter FullTimeEmployee by adding hoursWorked and payRate field and modifying the
methods. But it is risky to modify, for the sake of a new application, a class that is being used successfully
in existing applications. The underlying concept is known as the Open-Closed Principle: Every class
should be both open (extendible through inheritance) and closed (stable for existing applications).
Instead of rewriting FullTimeEmployee, we will create HourlyEmployee, a subclass of
FullTimeEmployee. To indicate that a class is a subclass of another class, the subclass identifie is
immediately followed by the reserved word extends. For example, we can declare the HourlyEmployee
class to be a subclass of FullTimeEmployee as follows:
public class HourlyEmployee extends FullTimeEmployee
{
...
Each HourlyEmployee object will have the information from FullTimeEmployee —name and gross
pay—as well as hours worked and pay rate. These latter two will be field in the HourlyEmployee
class. To lead us into a discussion of the relationship between the FullTimeEmployee field and the
HourlyEmployee fields here is a constructor to initialize an HourlyEmployee instance from a name,
hours worked, and pay rate (MAX_REGULAR_HOURS is a constant identifie with a current value of 40, and
OVERTIME_FACTOR is a constant identifie with a current value of 1.5).
/**
* Initializes this full-time HourlyEmployee object’s name, hours worked, pay rate, and
* gross pay from a a specified name, hours worked and pay rate. If the hours worked
* is at most MAX_REGULAR_HOURS, the gross pay is the hours worked times
* the pay rate. Otherwise, the gross pay is MAX_REGULAR_HOURS times the
* pay rate, plus the pay rate times OVERTIME_FACTOR for all overtime hours.
*
* @param name - the specified name.
* @param hoursWorked - the specified hours worked.
* @param payRate - the specified pay rate.
*
*/
public HourlyEmployee (String name, int hoursWorked, double payRate)
{
this.name = name;
this.hoursWorked = hoursWorked;
this.payRate = payRate;
1 Don’t confuse method overriding with method overloading (discussed in Section 0.2.1 of Chapter 0): having two methods in the same class
Notice that in the definitio of this 3-parameter constructor for HourlyEmployee, the name and
grossPay field from the FullTimeEmployee class are treated as if they had been declared as field in
the HourlyEmployee class. The justificatio for this treatment is that an HourlyEmployee object is also
a FullTimeEmployee object, so every FullTimeEmployee fiel is also an HourlyEmployee field But
the name and grossPay field in the FullTimeEmployee class were given private visibility, which pre-
cludes their usage outside of the declaration of the FullTimeEmployee class. Can we change the visibility
of those f elds to public? That would be a bad choice, because then any user’s code would be allowed
to access (and modify) those f elds. What we need is a visibility modifie for a superclass fiel that allows
access by subclass methods but not by arbitrary user’s code. The solution is found in the next section.
These declarations enable any subclass of FullTimeEmployee to access the name and grossPay field
as if they were declared within the subclass itself. This makes sense because an HourlyEmployee object
is a FullTimeEmployee object as well. So the HourlyEmployee class has two inherited f elds (name
and grossPay) as well as those explicitly declared in HourlyEmployee (hoursWorked, payRate, and
for convenience, regularPay and overtimePay).
The subclass HourlyEmployee can access all of the f elds, from FullTimeEmployee, that have
the protected modifier Later on, if a subclass of HourlyEmployee is created, we would want that
subclass’s methods to be able to access the HourlyEmployee field —as well as the FullTimeEmployee
fields So the declarations of the HourlyEmployee field should also have the protected modifier
protected int hoursWorked;
The HourlyEmployee class will have a default constructor as well as the 3-parameter constructor define
earlier in Section 1.3. The FullTimeEmployee methods getName and getGrossPay are inherited as is
by the HourlyEmployee class. The getHoursWorked, getPayRate, getRegularPay, and getOver
timePay methods are explicitly define in the HourlyEmployee class.
The toString() method from the FullTimeEmployee class will be overridden in Hourly
Employee to include the word “HOURLY”. The override can be accomplished easily enough: we copy
the code from the toString() method in FullTimeEmployee, and append “HOURLY” to the String
returned:
return name + MONEY.format (grossPay) + "HOURLY";
But, as noted at the end of Section 1.2.3, copying code is dangerous. Instead, the definitio of toString()
in the HourlyEmployee class will call the toString() method in the FullTimeEmployee class. To
call a superclass method, use the reserved word super as the calling object:
return super.toString() + "HOURLY";
/**
* Initializes this full-time HourlyEmployee object to have an empty string for
* the name, 0 for hours worked, 0.00 for the pay rate, grossPay, regularPay
* and overtimePay.
*
*/
public HourlyEmployee()
{
hoursWorked = 0;
payRate = 0.00;
regularPay = 0.00;
overtimePay = 0.00;
} // default constructor
1.3 Inheritance 41
/**
* Initializes this full-time HourlyEmployee object’s name and gross pay from a
* a specified name, hours worked and pay rate. If the hours worked is
* at most MAX_REGULAR_HOURS, the gross pay is the hours worked times
* the pay rate. Otherwise, the gross pay is MAX_REGULAR_HOURS time the
* pay rate, plus the pay rate times OVERTIME_FACTOR for all overtime hours.
*
* @param name - the specified name.
* @param hoursWorked - the specified hours worked.
* @param payRate - the specified pay rate.
*
*/
public HourlyEmployee (String name, int hoursWorked, double payRate)
{
this.name = name;
this.hoursWorked = hoursWorked;
this.payRate = payRate;
/**
* Returns the hours worked by this full-time HourlyEmployee object.
*
* @return the hours worked by this full-time HourlyEmployee object.
*
*
public int getHoursWorked()
{
return hoursWorked;
} // method getHoursWorked
/**
* Returns the pay rate of this full-time HourlyEmployee object.
*
* @return the pay rate this full-time HourlyEmployee object.
*
*/
42 C H A P T E R 1 Object-Oriented Concepts
/**
* Returns the regular pay of this full-time HourlyEmployee object.
*
* @return the regular pay this full-time HourlyEmployee object.
*
*/
public double getRegularPay()
{
return regularPay;
} // method getRegularPay
/**
* Returns the overtime pay of this full-time HourlyEmployee object.
*
* @return the overtime pay this full-time HourlyEmployee object.
*
*/
public double getOvertimePay()
{
return overtimePay;
} // method getOvertimePay
/**
* Returns a String representation of this full-time HourlyEmployee object with the
* name followed by a space followed by a dollar sign followed by the gross pay
* (with two fractional digits) followed by "FULL TIME HOURLY".
*
* @return a String representation of this full-time HourlyEmployee object.
*
*/
public String toString()
{
final String FULL_TIME_STATUS = "HOURLY";
} // class HourlyEmployee
A f nal note on the visibility modifie protected: It can be applied to methods as well as to fields
For example, the visibility modifie for the getNextEmployee method in the Company class should be
changed from private to protected for the sake of potential subclasses of Company. One such subclass
is introduced in Section 1.3.3.
1.3 Inheritance 43
Section 1.3.2 continues our discussion of inheritance by examining the interplay between inheritance
and constructors.
public C (String s)
{
super (s.length()); // explicitly calls B’s int-parameter constructor
...
} // String-parameter constructor
So if a superclass explicitly define a default (that is, zero-parameter) constructor, there are no restrictions
on its subclasses. Similarly, if the superclass does not defin any constructors, the compiler will
automatically provide a default constructor, and there are no restrictions on the subclasses. But if a
superclass define at least one constructor and does not defin a default constructor, the f rst statement in
any subclass’s constructor must explicitly invoke a superclass constructor.
import java.io.*;
/**
* Returns the next hourly employee from the specified Scanner object.
*
* @param sc – the Scanner object used to scan in the next employee.
*
* @return the next hourly employee scanned in from sc.
*
*/
protected HourlyEmployee getNextEmployee (Scanner sc)
{
Scanner lineScanner = new Scanner (sc.nextLine());
} // class HourlyCompany
The left-hand side of this assignment is (a reference to) a FullTimeEmployee object. But the value
returned by the call to getNextEmployee is a reference to an HourlyEmployee object. Such an arrange-
ment is legal because an HourlyEmployee is a FullTimeEmployee. This is an application of the Subclass
Substitution Rule:
Specifically the left-hand side of the above assignment is a reference to a FullTimeEmployee object,
so a reference to a FullTimeEmployee object is called for on the right-hand side of that assignment.
So it is legal for that right-hand side expression to be a reference to an HourlyEmployee object; it is
important to note that for an HourlyEmployee object, the toString() method includes “HOURLY”.
The returned reference is assigned to the FullTimeEmployee reference full, which is then used to
update the FullTimeEmployee reference bestPaid. When the value of bestPaid is returned to the
run method and the message bestPaid.toString() is sent, the output includes “HOURLY”. Why?
The reason is worth highlighting:
When a message is sent, the version of the method invoked depends on the run-time type of the object
referenced, not on the compile-time type of the reference.
Starting with the construction of the new HourlyEmployee object in the getNextEmployee method,
all of the subsequent references were to an HourlyEmployee object. So the version of the toString()
method invoked by the message bestPaid.toString() was the one in the HourlyEmployee class.
Let’s take a closer look at the Subclass Substitution Rule. Consider the following:
FullTimeEmployee full = new FullTimeEmployee ();
full = hourly;
On the right-hand side of this last assignment statement, the compiler expects a reference-to-
HourlyEmployee, so a reference-to-FullTimeEmployee is unacceptable: a FullTimeEmployee is not
necessarily an HourlyEmployee. Note that the left-hand side of an assignment statement must consist
of a variable, which is an expression. But that left-hand-side variable is not evaluated in the execution of
the assignment statement, so the Subclass Substitution Rule does not apply to the left-hand side.
Now suppose we had the following:
FullTimeEmployee full = new FullTimeEmployee ();
full = hourly;
hourly = full; // still illegal
46 C H A P T E R 1 Object-Oriented Concepts
After the assignment of hourly to full, full contains a reference to an HourlyEmployee object. But
the assignment:
hourly = full;
still generates a compile-time error because the declared type of full is still reference-to-FullTime
Employee. We can avoid a compile-time error in this situation with a cast: the temporary conversion of
an expression’s type to another type. The syntax for a cast is:
(the new type)expression
full = hourly;
hourly = (HourlyEmployee) full;
To put it anthropomorphically, we are saying to the compiler, “Look, I know that the type of full is
reference-to-FullTimeEmployee. But I promise that at run-time, the object referenced will, in fact, be
an HourlyEmployee object.” The cast is enough to satisfy the compiler, because the right-hand side of
the last assignment statement now has type reference-to-HourlyEmployee. And there is no problem at
run-time either because—from the previous assignment of hourly to full —the value on the right-hand
side really is a reference-to-HourlyEmployee.
But the following, acceptable to the compiler, throws a ClassCastException at run-time:
FullTimeEmployee full = new FullTimeEmployee ();
The run-time problem is that full is actually pointing to a FullTimeEmployee object, not to an
HourlyEmployee object.
The complete project, HourlyCompany, is in the ch1 directory of the book’s website. Lab 1’s
experiment illustrates another subclass of FullTimEmployee.
Before we can give a f nal illustration of the Subclass Substitution Rule, we need to introduce the
Object class. The Object class, declared in the f le java.lang.Object.java, is the superclass of
all classes. Object is a bare-bones class, whose methods are normally overridden by its subclasses. For
example, here are the method specificatio and definitio of the equals method in the Object class:
/**
* Determines if the calling object is the same as a specified object.
*
* @param obj - the specified object to be compared to the calling object.
*
* @return true - if the two objects are the same.
1.3 Inheritance 47
*
*/
public boolean equals (Object obj)
{
return (this == obj);
} // method equals
The definitio of this method compares references, not objects, for equality. So true will be returned if and
only if the calling object reference contains the same address as the reference obj. For example, consider
the following program fragment:
Object obj1 = new Object(),
obj2 = new Object(),
obj3 = obj1;
For that reason, this method is usually overridden by the Object class’s subclasses. We saw an example
of this with the String class’s equals method in Section 0.2.3 of Chapter 0. In that equals method,
the parameter’s type is Object, and so, by the Subclass Substitution Rule, the argument’s type can be
String, a subclass of Object. For example, we can have
if (message.equals ("nevermore"))
And because each class interacts with other classes through its method specifications we can change the
class’s f elds and method definition as desired as long as the method specification remain intact.
The next section of this chapter considers the extent to which a language can allow developers of a
class to force users of that class to obey the Principle of Data Abstraction.
1.5 Polymorphism
One of the major aids to code re-use in object-oriented languages is polymorphism. Polymorphism —from
the Greek words for “many” and “shapes”—is the ability of a reference to refer to different objects in a
class hierarchy. For a simple example of this surprisingly useful concept, suppose that sc is a reference
to an already constructed Scanner object. We can write the following:
FullTimeEmployee employee; // employee is of type reference-to-FullTimeEmployee
The version of the toString() method executed depends on the type of the object that employee is
referencing. If the scanned line consists of “full time”, then employee is assigned a reference to an instance
of class FullTimeEmployee, so the FullTimeEmployee class’s version of toString() is invoked. On
the other hand, if the scanned line consists of any other string, then employee is assigned a reference to an
instance of class HourlyEmployee, so the HourlyEmployee class’s version of toString() is invoked.
In this example, employee is a polymorphic reference: the object referred to can be an instance
of class FullTimeEmployee or an instance of class HourlyEmployee, and the meaning of the message
employee.toString() reflect the point made in Section 1.3.3: When a message is sent, the version of
the method invoked depends on the type of the object, not on the type of the reference. What is important
here is that polymorphism allows code re-use for methods related by inheritance. We need not explicitly
call the two versions of the toString() method.
The previous code raises a question: how can the Java compiler determine which version of
toString() is being invoked? Another way to phrase the same question is this: How can the
method identifie toString be bound to the correct definitio —in FullTimeEmployee or in Hourly
Employee —at compile time, when the necessary information is not available until run time? The answer
is simple: The binding cannot be done at compile-time, but must be delayed until run time. A method
that is bound to its method identifie at run time is called a virtual method .
In Java, almost all methods are virtual. The only exceptions are for static methods (discussed in
Section 2.1) and for final methods (the final modifie signifie that the method cannot be overridden
in subclasses.) This delayed binding—also called dynamic binding or late binding —of method identifier
to methods is one of the reasons that Java programs execute more slowly than programs in most other
languages.
Polymorphism is a key feature of the Java language, and makes the Java Collections Framework
possible. We will have more to say about this in Chapter 4, when we take a tour of the Java Collections
Framework.
Method specification are method-level documentation tools. Section 1.6 deals with class-level doc-
umentation tools.
FullTimeEmployee
# name: String
# grossPay:int
+ FullTimeEmployee()
+ FullTimeEmployee (name: String, grossPay:double)
+ getName(): String
+ getGrossPay():double
+ toString(): String
Inheritance is illustrated by a solid arrow from the subclass to the superclass. For example, Figure 1.2
shows the relationship between the HourlyEmployee and FullTimeEmployee classes in Section 1.3.
A dashed arrow illustrates the relationship between a class and the interface that class implements.
For example, Figure 1.3 augments the class diagrams from Figure 1.2 by adding the diagram for the
Employee interface.
FullTimeEmployee
# name: String
# grossPay:int
+ FullTimeEmployee()
+ FullTimeEmployee (name: String, grossPay:double)
+ getName(): String
+ getGrossPay():double
+ toString(): String
HourlyEmployee
# hourWorked:int
# payRate:double
# regularPay:double
# overtimePay:double
+ HourlyEmployee()
+ HourlyEmployee (name: String, hoursWorked: int, payRate: double)
+ getHoursWorked(): int
+ getPayRate(): double
+ getRegularPay(): double
+ getOvertimePay(): double
+ toString(): String
FIGURE 1.2 In UML, the notation for inheritance is a solid arrow from the subclass to the superclass
1.6 The Unified Modeling Language 51
<<interface>>
Employee
+ getName(): String
+ getGrossPay():double
+ toString(): String
FullTimeEmployee
# name: String
# grossPay:int
+ FullTimeEmployee()
+ FullTimeEmployee (name: String, grossPay:double)
+ getName(): String
+ getGrossPay():double
+ toString(): String
HourlyEmployee
# hoursWorked:int
# payRate:double
# regularPay:double
# overtimePay:double
+ HourlyEmployee()
+ HourlyEmployee (name: String, hoursWorked: int, payRate:double)
+ getHoursWorked():int
+ getPayRate():double
+ getRegularPay():double
+ getOvertimePay(): double
+ toString(): String
FIGURE 1.3 A UML illustration of the relationship between an interface, an implementing class, and a subclass
Company
+ Company()
+ main (String[ ] args)
+ run()
+ findBestPaid (Scanner sc): FullTimeEmployee
+ getNextEmployee (Scanner sc): FullTimeEmployee
FullTimeEmployee
# name: String
# grossPay:int
+ FullTimeEmployee()
+ FullTimeEmployee (name: String, grossPay:double)
+ getName(): String
+ getGrossPay():double
+ toString(): String
FullTimeEmployee
String
FIGURE 1.5 Aggregation in UML: the FullTimeEmployee class has a String fiel
For example, Figure 1.5 shows that the FullTimeEmployee class has a String field To avoid clutter,
the f gure simply has the class name in each class diagram.
Graphical tools such as UML play an important role in outlining a project. We will be developing
projects, starting in Chapter 5, as applications of data structures and algorithms. Each such project will
include UML class diagrams.
SUMMARY
This chapter presents an overview of object-oriented pro- abstraction. Data abstraction —the separation of method
gramming. Our focus, on the use of classes rather than specification from f eld and method definition —is a way
on their implementation details, is an example of data for users of a class to protect their code from being
Summary 53
affected by changes in the implementation details of the 2. Inheritance of a class’s f elds and methods by sub-
class used. classes.
The three essential features of an object-oriented
3. Polymorphism: the ability of a reference to refer to
language are:
different objects.
1. Encapsulation of field and methods into a single The Unifie Modeling Language (UML) is an industry-
entity—the class—whose implementation details standard, graphical language that illustrates the modeling
are hidden from users. of projects.
54 C H A P T E R 1 Object-Oriented Concepts
CROSSWORD PUZZLE
1 2
7 8
10
www.CrosswordWeaver.com
ACROSS DOWN
CONCEPT EXERCISES
1.1 Given that HourlyEmployee and SalariedEmployee are subclasses of FullTimeEmployee, suppose
we have:
full = salaried;
Which one of the following assignments would be legal both at compile-time and at run-time?
import java.util.*;
A a;
if (code == 0)
a = new A();
else // non-zero int entered
a = new D();
System.out.println (a);
} // method run
} // class Polymorphism
56 C H A P T E R 1 Object-Oriented Concepts
class A
{
public String toString ()
{
return "A";
} // method toString
} // class A
class D extends A
{
public String toString ()
{
return "D";
} // method toString
} // class D
1.3 In the Employee class, modify the toString method so that the gross pay is printed with a comma to the
left of the hundreds digit. For example, if the name is “O’Brien,Theresa” and the gross pay is 74400.00, the
toString method will return
O’Brien,Theresa $74,400.00
1.4 What can you infer about the identifie out from the following message?
System.out.println ("Eureka!");
What is the complete declaration for the identifi r out? Look in java.lang.System.java.
PROGRAMMING EXERCISES
1.1 Here is a simple class—but with method specifi ations instead of method definition —to f nd the highest age
from the ages scanned in:
/**
* Initializes this Age object.
*
*/
public Age ()
/**
* Returns the highest age of the ages scanned in from the keyboard.
Programming Exercises 57
} // class Age
/**
* Returns this FullTimeEmployee object’s name.
*
* @return a (reference to a) copy of this FullTimeEmployee object’s
* name.
*
*/
public String getName ()
/**
* Sets this FullTimeEmployee object’s name to a specifed string.
*
* @param nameIn – the String object whose value is assigned to this
* FullTimeEmployee object’s name.
*
*/
public void setName (String nameIn)
1.4 Create a class to determine which hourly employee in a fil received the most overtime pay. The name of the
fil is to be scanned in from System.in.
1.5 In the toString() method of the FullTimeEmployee class, there is a call to the format method. The
heading of that method is
3. return, in String form, the next date after this CalendarDate object; for example, if this CalendarDate
object represents January 31, 2012, the return value would be “February 1, 2012”;
4. return, in String form, the date prior to this CalendarDate object; for example, if this CalendarDate object
represents January 1, 2013, the return value would be “December 31, 2012”;
5. return, in String form, the day of the week on which this CalendarDate object falls; for example, if this
CalendarDate object represents the date December 20, 2012, the return value would be “Thursday”;
Part b: Develop the CalendarDate class, that is, determine what field to declare and then defin the methods.
Part c: Create a project to test your CalendarDate class. Call each CalendarDate method at least twice.
Additional Features of CHAPTER 2
In Chapter 1, the primary goal was to introduce object-oriented concepts, such as interfaces, inheritance
and polymorphism, in the context of the Java programming language. This chapter introduces more
topics on programming in general and Java in particular, and illustrates how they can aid your
programming efforts. For example, Java’s exception-handling mechanism provides programmers with
significant control over what happens when errors occur.
CHAPTER OBJECTIVES
1. Distinguish between static members and instance members.
2. Be able to develop JUnit tests for a class’s methods.
3. Be able to create try blocks and catch blocks to handle exceptions.
4. Compare file input/output with console input/output.
5. Understand the fundamentals of the Java Virtual Machine.
6. Be able to override the Object class’s equals method.
7. Understand the interplay between packages and visibility modifiers.
Then the object referenced by oldEmployee will have its own copy of the instance variables name and
grossPay, and the objects referenced by currentEmployee and newEmployee will have their own
copies also.
1
In Section 4.2.3.1, we will see that a class may also have another class as a member.
59
60 C H A P T E R 2 Additional Features of Programming and Java
In addition to instance variables, which are associated with a particular object in a class, we can
declare static variables, which are associated with the class itself. The space for a static variable—also
known as a class variable—is shared by all instances of the class. A f eld is designated as a static variable
by the reserved modifie static. For example, if a count fiel is to maintain information about all objects
of a class Student, we could declare the fiel count to be a static variable in the Student class:
protected static int count = 0;
This static variable could be incremented, for example, whenever a Student constructor is invoked. Then
the variable count will contain the total number of Student instances created.
A class may also have constant identifiers also called “symbolic constants” or “named constants”.
A constant identifie is an identifie that represents a constant, which is a variable that can be assigned
to only once. The declaration of a constant identifie includes the reserved word final —indicating only
one assignment is allowed—as well as the type and value of the constant. For example, we can write
protected final static int SPEED_LIMIT = 65.0;
Constant identifier promote both readability (SPEED_LIMIT conveys more information than 65.0) and
maintainability (because SPEED_LIMIT is declared in only one place, it is easy to change its value
throughout the class). There should be just one copy of the constant identifie for the entire class, rather
than one copy for each instance of the class. So a constant identifie for a class should be declared as
static; constants within a method cannot be declared as static. At the developer’s discretion, constant
identifier for a class may have public visibility. Here are declarations for two constant class identifiers
public final static char COMMAND_START = ‘$’;
To access a static member inside its class, the member identifie alone is suff cient. For example, the above
static fiel count could be accessed in a method in the Student class as follows:
count++;
In order to access a static member outside of its class, the class identifie itself is used as the qualifier
For example, outside of the wrapper class Integer, we can write:
if (size == Integer.MAX_VALUE)
Here is the declaration for an often-used constant identifie in the System class:
public final static PrintStream out = nullPrintStream();
Because out is declared as static, its calls to the PrintStream class’s println method include the
identifie System rather than an instance of the System class. For example,
System.out.println ("The Young Anarchists Club will hold a special election next week" +
"to approve the new constitution.");
The static modifie is used for any constant identifie that is define outside of a class’s methods. The
static modifie is not available within a method. For example, in the definitio of the default constructor
in the FullTimeEmployee class, we had:
final String EMPTY_STRING = "";
It would have been illegal to use the static modifie for this constant.
2.2 Method Testing 61
The constant identifie MONEY can be used in any class that implements the Employee interface. Recall
that any constant or method identifie in an interface automatically has public visibility.
Java also allows static methods. For example, the Math class in the package java.lang has a floor
method that takes a double argument and returns, as a double, the largest value that is less than or equal
to the argument and equal to an integer. We can write
System.out.println (Math.floor (3.7)); // output: 3.0
When floor is called, there is no calling object because the effect of the method depends only on the
double argument. To signify this situation, the class identifie is used in place of a calling object when
floor is called. A method that is called without a calling object is known as a static method, as seen
in the following heading
public static double floor (double a)
The execution of every Java application (excluding applets, servlets, and so on) starts with a static main
method. And static methods are not virtual; that is, static methods are bound to method identifier
at compile time, rather than at run time. The reason is that static methods are associated with a class
itself rather than an object, so the issue of which object is invoking the method does not arise.
Testing can reveal the presence of errors but not the absence of errors.
The testing software we utilize is JUnit: the “J” stands for “Java,” and each method in a project is
referred to as a “unit.” JUnit is an Open Source (that is, free) unit-testing product—available from
www.junit.org—that allows the methods in a class to be tested systematically and without human
intervention—for example, without keyboard input or Graphical User Interface (GUI) mouse clicks.
The web page http://junit.sourceforge.net/README.html#Installation has information on installation. In
general, the success or failure of an individual test is determined by whether the expected result of the
test matches the actual result. The output from testing provides details for each failed test. For a simple
example, here is a test of the toString() method in the FullTimeEmployee class:
@Test
public void toStringTest1()
{
FullTimeEmployee full = new FullTimeEmployee ("a", 150.00);
62 C H A P T E R 2 Additional Features of Programming and Java
“@Test ” is referred to as an annotation. The assertEquals method determines whether its arguments
are equal. In this case, if the two strings are equal, the test is passed. Otherwise, the test fails.
The assertEquals method is an overloaded method in the Assert class of the org.junit pack-
age. The heading for the version called above is
public static void assertEquals (java.lang.String expected, java.lang.String actual)
There are other versions of the method to compare primitive values. For example, we could have
int median = roster.findMedian();
assertEquals (82, median);
Also, there is a version to compare any two objects. Here is the method heading:
public static void assertEquals (java.lang.Object expected, java.lang.Object actual)
According to the Subclass Substitution Rule, the arguments can be instances of any class—because any
class is a subclass of Object. In fact, expected is a polymorphic reference when the AssertEquals
method is invoked: the code executed depends on the types of the objects involved. For example, with
String objects, this version has the same effect as the version in which both parameters have type String.
(But this version is slightly slower than the String-parameter version due to the run-time test to make
sure that actual is an instance of the String class.)
Finally, there are several assertArrayEquals methods for comparing two arrays of int values,
two arrays of double values, two arrays of Object references, and so on.
The details of running JUnit test classes will depend on your computing environment. Typically,
you will run the tests in an Integrated Development Environment (IDE) such as Eclipse or DrJava, and the
output of testing may combine text and graphics (for example, a green bar if all tests were successful, and
a red bar if one or more tests failed). For the sake of generality, the following test class is not tied to any
IDE, but simply prints the string returned by the getFailures() method in the runClasses class of
the package org.junit. If you are running tests in an IDE, the main method from the class below will
be ignored. Here is a complete class for testing the toString( ) method in the FullTimeEmployee
class—followed by a discussion of the details:
import org.junit.*;
import static org.junit.Assert.*;
import org.junit.runner.Result;
import static org.junit.runner.JUnitCore.runClasses;
@Test
public void toStringTest1()
{
full = new FullTimeEmployee ("a", 150.00);
expected = "a $150.00 FULL TIME";
assertEquals (expected, full.toString());
} // method toStringTest1
@Test
public void toStringTest2()
{
full = new FullTimeEmployee ("b", 345.678);
expected = "b $345.678 FULL TIME"; // error!
assertEquals (expected, full.toString());
} // method toStringTest2
@Test
public void toStringTest3()
{
full = new FullTimeEmployee();
expected = " $0.00 FULL TIME";
assertEquals (expected, full.toString());
} // method toStringTest3
} // class FullTimeEmployeeTest
The line
import static org.junit.Assert.*;
allows static methods—such as assertEquals —in the Assert class to be accessed without specifying
the class name. The main method runs this cluster of tests, one after the other, in no particular order.
Because of the mistake in test 2, the output from the program is
Tests run = 3
Tests failed = [toStringTest2(FullTimeEmployeeTest): expected:<b $345.6[7]8 FULL TIME>
but was:<b $345.6[]8 FULL TIME>]
Note that the mistake—the extra “7” in the expected value—was in the test, not in the method being
tested, and was written just so you would know what is output when a test fails.
When should these tests be developed and run? Most unit-testing enthusiasts recommend that
The advantage to pre-definitio testing is that the testing will be based on the method specificatio only,
and will not be influence by the method definition Furthermore, the tests should be run both before and
after the method is define (and after any subsequent changes to the method definition) That will illustrate
the transition from a method that fails the tests to a method that passes the tests. But how can a method
64 C H A P T E R 2 Additional Features of Programming and Java
be compiled before it is defined To satisfy the Java compiler, each method definitio can be a stub: a
definitio that has only enough code to avoid a compile-time error. For example, here is a stub for the
toString( ) method in the FullTimeEmployee class:
public String toString()
{
return null;
} // method toString
When FullTimeEmployeeTest was run with this stub for the toString( ) method, all three tests failed.
(Of course, the mistake in test2 ensures that test will fail anyway.) Because this chapter introduces unit
testing intermingled with several important language features, the method testing in this chapter will be
presented after the method has been fully defined In subsequent chapters we will adhere to the test-firs
paradigm.
In Section 2.3.2 we will see how to create a stub that will fail any test even if the return type of the
method to be tested is boolean.
Here is a CompanyTest class to test both the getNextEmployee and findBestPaid methods. Note that
the @Before annotation precedes any method that is automatically invoked just prior to each test.
import org.junit.*;
import static org.junit.Assert.*;
import org.junit.runner.Result;
import static org.junit.runner.JUnitCore.runClasses;
import java.util.*;
2.2 Method Testing 65
@Before
public void runBeforeEveryTest()
{
company = new Company();
} // method runBeforeEveryTest
@Test
public void getNextEmployeeTest1()
{
sc = new Scanner ("Lucas 350.00");
expected = "Lucas $350.00 FULL TIME";
assertEquals (expected, company.getNextEmployee (sc).toString());
} // method getNextEmployeeTest1
@Test
public void findBestPaidTest1() throws IOException
{
sc = new Scanner (new File ("company.in1"));
best = company.findBestPaid (sc);
expected = "b $150.00 FULL TIME";
assertEquals (expected, best.toString());
} // method findBestPaidTest1
@Test
public void findBestPaidTest2() throws IOException
{
sc = new Scanner (new File ("company.in2"));
best = company.findBestPaid (sc);
assertEquals (null, best);
} // method findBestPaidTest2
} // class CompanyTest
66 C H A P T E R 2 Additional Features of Programming and Java
The f le company.in2 is empty. When the above tests were run with the versions of getNextEmployee
(Scanner sc) and findBestPaid (Scanner sc) from Chapter 1, all three test cases were successful.
For the HourlyEmployee class, the only method worthy of testing is the three-parameter construc-
tor. As noted earlier in this section, constructors cannot be tested in isolation. Instead, we will test the
getRegularPay, getOvertimePay, and getGrossPay methods. These accessor methods are worthy of
testing since they do more than simply returning values passed to a constructor. The important aspect
of the following HourlyEmployeeTest class is the testing of a boundary condition: the comparison
(> =, >, <=, <) of a variable to some fixe value; the result of the comparison determines the action
taken. Specifically the hoursWorked is compared to 40 to determine the regular pay, overtime pay, and
gross pay. To make sure that all boundary cases are covered, there are separate tests for hoursWorked equal
to 39, 40, and 41. In comparing the expected result with the actual result for regularPay, overTimePay,
and grossPay, we should not compare double values for exact equality. So we utilize a three-parameter
assertEquals method, with the third parameter the (very small) difference we will allow between the
expected and actual double values.
@Test
public void test1()
{
hourly = new HourlyEmployee ("andrew", 39, 10.00);
assertEquals (390.00, hourly.getRegularPay(), DELTA);
2.2 Method Testing 67
@Test
public void test2()
{
hourly = new HourlyEmployee ("beth", 40, 20.00);
assertEquals (800.00, hourly.getRegularPay(), DELTA);
assertEquals (0.00, hourly.getOvertimePay(), DELTA);
assertEquals (800.00, hourly.getGrossPay(), DELTA);
} // method test2
@Test
public void test3()
{
hourly = new HourlyEmployee ("terry", 41, 20.00);
assertEquals (800.00, hourly.getRegularPay(), DELTA);
assertEquals (30.00, hourly.getOvertimePay(), DELTA);
assertEquals (830.00, hourly.getGrossPay(), DELTA);
} // method test3
@Test
public void test4()
{
hourly = new HourlyEmployee ("karen", 50, 10);
assertEquals (400.00, hourly.getRegularPay(), DELTA);
assertEquals (150.00, hourly.getOvertimePay(), DELTA);
assertEquals (550.00, hourly.getGrossPay(), DELTA);
} // method test4
} // class HourlyEmployeeTest
What about testing other methods? There is no rule to determine which methods in a class should be
tested. The best strategy is to assume that a method contains subtle flaw that can be revealed only by
rigorous testing.
This can be a challenge to programmers, who tend to view their work favorably, even glowingly (“a thing
of beauty and a joy forever”). As such, programmers are ill-suited to test their own methods because the
purpose of testing is to uncover errors. Ideally the person who constructs test data should hope that the
method will fail the test. If a method fails a test and the method is subsequently revised, all tests of that
method should be re-run.
In the next section, we introduce Java’s exception-handling facilities, and consider the interplay
between exception-handling and testing.
68 C H A P T E R 2 Additional Features of Programming and Java
return lastName + ", " + firstName + " " + middleName.charAt (0) + ".";
} // method rearrange
The problem with this method, as currently defined is that the execution of the method can terminate
abnormally. How? If the argument corresponding to fullName is a (reference to a) String object that
does not have at least three components separated by whitespace, a NoSuchElementException object
will be thrown. In this case, the execution of the method will terminate abnormally. Instead of an abnormal
termination, we want to allow execution to continue even if the argument corresponding to fullName
is not a reference to a String that consists of those three components. That is, we “try” to split up
fullName, and “catch” the given exception. The revised specificatio and definitio are
2.3 Exception Handling 69
/**
* Returns a specified full name in the form "last-name, first-name middle-initial.".
*
* @param fullName – a (non-null reference to a) String object that represents the
* specified full name, which should be in the form
* "first-name middle-name last-name".
*
* @return the name in the form "last-name, first-name middle-initial." if fullName
* has three components. Otherwise, return
* "java.util.NoSuchElementException: the name is not of the form
* "first-name middle-name last-name"".
*
*/
public String rearrange (String fullName)
{
String result;
try
{
Scanner sc = new Scanner (fullName);
result = lastName + ", " + firstName + " " + middleName.charAt (0) + ".";
} // try
catch (NoSuchElementException e)
{
result = e.toString() + ": " + ": The name is not of the form \"first-name " +
"middle-name last-name\"";
} // catch
return result;
} // method rearrange
In the execution of this method, the f ow of control is as follows. Inside the try block, if fullName can
be split into first middle, and last names, the three calls to sc.next( ) and the subsequent assignment
to result will be executed. The entire catch block will be skipped over, and the return statement
will be executed. But if fullName cannot be split into first middle, and last names, one of the calls
to sc.next( ) will throw a NoSuchElementException object, the try block will be exited, and the
statement inside the catch block will executed. Then the return statement will be executed, as before.
In the catch block, the parameter e is (a reference to) the NoSuchElementException object
created and thrown during the execution of one of the calls to sc.next( ). Specifically e.toString( )
is the string "java.util.NoSuchElementException". We will see shortly, in Section 2.3.1, how an
exception can be thrown in one method and caught in another method.
Here is a test class for the rearrange method (assume that method is in the NameChange class,
which may consist of nothing except the rearrange method):
import org.junit.*;
import static org.junit.Assert.*;
import org.junit.runner.Result;
70 C H A P T E R 2 Additional Features of Programming and Java
@Before
public void runBeforeEveryTest( )
{
change = new NameChange();
} // method runBeforeEveryTest
@Test
public void rearrangeTest1()
{
result = change.rearrange ("John Quincy Adams");
assertEquals ("Adams, John Q.", result);
} // method rearrangeTest1
@Test
public void rearrangeTest2()
{
result = change.rearrange ("John Adams");
assertEquals (EXCEPTION, result.substring (0, EXCEPTION_LENGTH));
} // method rearrangeTest2
@Test
public void rearrangeTest3()
{
result = change.rearrange ("John");
assertEquals (EXCEPTION, result.substring (0, EXCEPTION_LENGTH));
} // method rearrangeTest3
@Test
public void rearrangeTest4()
{
result = change.rearrange ("");
assertEquals (EXCEPTION, result.substring (0, EXCEPTION_LENGTH));
} // rearrangeTest4
} // class NameChangeTest
2.3 Exception Handling 71
In this example, the exception was handled—in the catch block—of the rearrange method. In the next
section, we see how to handle exceptions that are not caught within the method in which they are thrown.
What can go wrong in a call to this method? One possible error, as indicated in the @throws sections of
the javadoc specification if the string scanned in from sc is not empty but does not consist of an integer,
InputMismatchException will be thrown. This exception is not caught in the isLeapYear method,
so the exception is propagated back to the method that called isLeapYear. For example, the following
LeapYear class has a run() method that scans f ve lines from System.in, and determines which lines
contain leap years and which lines contain non-integers.
2
Because the earth makes one full rotation around the sun in slightly less than 365.25 days, not every year divisible by 4 is a leap year.
Specifica ly, a leap year must be both divisible by 4 and either not divisible by 100 or divisible by 400. So 2000 was a leap year, but 2100
will not be a leap year.
72 C H A P T E R 2 Additional Features of Programming and Java
} // class LeapYear
For input of
2000
2100
201o
2.3 Exception Handling 73
2010
2008
The above catch block includes a call to sc.nextLine(). If that call had been omitted, the output for
the above input would be
true
false
The input is not an integer.
The input is not an integer.
The input is not an integer.
Why? When the third call to sc.nextInt() in isLeapYear throws InputMismatchException for
“201o”, the scanner remains positioned on the third line instead of advancing to the fourth line. Then
the next two calls to sc.nextInt( ) also throw InputMismatchException for “201o”. We needed to
include sc.nextLine( ) in the catch block to ensure that the scanner skips over the illegal input.
It is worth noting that in a method’s specification only propagated exceptions are included in the
@throws javadoc comments. Any exception that is caught within the method definitio itself (such as we
did in the rearrange method of Section 2.3) is an implementation detail, and therefore not something
that a user of the method needs to know about.
Incidentally, without too much trouble we can modify the above run method to accommodate an
arbitrary number of input values. To indicate the end of the input, we need a value—called a sentinel —that
is not a legal year. For example, we can use "***" as the sentinel. When that value is entered from the
keyboard, InputMismatchException is thrown in the isLeapYear method and caught in the run
method, at which point a break statement terminates the execution of the scanning loop. Here is the
revised run method:
public void run()
{
final String SENTINEL = "***";
{
if (sc.nextLine().equals (SENTINEL))
break;
System.out.println (" The input is not an integer.\n");
} // catch
} // while
} // method run
If a propagated exception is not caught in a method, the exception is propagated back to the calling method.
If the calling method does not handle the exception, then the exception is propagated back to the method
that called the calling method itself. Ultimately, if the exception has not been caught even in the main
method, the program will terminate abnormally and a message describing the exception will be printed.
The advantage to propagating an exception is that the exception can be handled at a higher level in the
program. Decisions to change how exceptions are handled can be made in one place, rather than scattered
throughout the program. Also, the higher level might have facilities not available at lower levels, such as
a Graphical User Interface (GUI) window for output.
For a complete test suite of the isLeapYear method, we cannot scan over System.in because such
tests would require human intervention. Another option is to scan over a string of lines that contain the
values to be tested. But in JUnit, test methods can be invoked in any order, and the results of one test
do not affect any other test. So to ensure that the calls to the scanner would start on successive lines, we
would have to place all tests in one method. This would be legal but inappropriate because the tests are
independent of each other.
The following test suite for the isLeapYear method has each test in a separate method, and
includes tests for InputMismatchException, NoSuchElementException, NullPointerException
and IllegalArgumentException.
Here is the test class:
import org.junit.*;
import static org.junit.Assert.*;
import org.junit.runner.Result;
import static org.junit.runner.JUnitCore.runClasses;
import java.util.*;
@Before
public void runBeforeEveryTest()
{
leap = new LeapYear();
} // method runBeforeEveryTest
@Test
public void leapYearTest1()
{
answer = leap.isLeapYear (new Scanner ("2000"));
assertEquals (true, answer);
} // method leapYearTest1
@Test
public void leapYearTest2()
{
answer = leap.isLeapYear (new Scanner ("2100"));
assertEquals (false, answer);
} // method leapYearTest2
@Test
public void leapYearTest3()
{
answer = leap.isLeapYear (new Scanner ("1582"));
assertEquals (false, answer);
} // method leapYearTest3
} // method leapYearTest7
} // class LeapYearTest
What if the exception propagated in the method being tested is not the exception expected in the testing
method? Then the testing method will generate an error message that the exception thrown was not the one
expected. Finally, what if the method being tested propagates an exception but no exception was expected
by the testing method? For example, at the start of leapYearTest5, suppose we replaced
@Test (expected = NoSuchElementException.class)
with
@Test
The keyword null signifie that an exception was thrown but no exception was expected. In JUnit,
an error in running a test method occurs if an unexpected exception is thrown or if an expected exception
is not thrown. So it is an error if an exception is thrown but a different exception is expected. Errors
are included in the string returned by the getFailures()method in the runClasses class, but the term
failure is often applied only to those situations in which an assertion is tested and fails. Because testing
assertions is what unit testing is all about, errors must be removed before serious testing can begin.
We define the above isLeapYear method before we introduced the exception-propagation feature
needed to test that method. What if, as is normally the case, we wanted to test a method before the method
is defined Specifically how can we create a stub that will generated an error message for all of the
above tests? If the stub returns true, leapYearTest1() will succeed, and if the stub returns false,
leapYearTest2() will succeed. Clearly, the stub cannot return either true or false. Instead, the stub
will throw an exception other than the exceptions thrown according to the specifications For example,
public boolean isLeapYear (Scanner sc)
{
throw new UnsupportedOperationException();
} // method isLeapYear
When the test suite LeapYearTest was run on this stub, every test generated an error message (that is
good news), and the output (formatted for readability) was
Tests run = 7
Tests failed =
leapYearTest1(LeapYearTest): null
leapYearTest2(LeapYearTest): null,
leapYearTest3(LeapYearTest): null,
leapYearTest4(LeapYearTest): Unexpected exception,
expected<java.util.InputMismatchException> but
was<java.lang.UnsupportedOperationException>,
leapYearTest5(LeapYearTest): Unexpected exception,
expected<java.util.NoSuchElementException> but
was<java.lang.UnsupportedOperationException>,
leapYearTest6(LeapYearTest): Unexpected exception,
expected<java.lang.NullPointerException> but
was<java.lang.UnsupportedOperationException>,
2.3 Exception Handling 77
This indicates that the sample method might throw an IOException object. If so, the exception will be
propagated back to the method that called sample. That calling method must either catch IOException
or append the same throws clause to its method heading. And so on. Checked exceptions are propagated
for the same reason that other exceptions are propagated: It might be preferable to handle all exceptions
at a higher level for the sake of uniformity, or there might be better facilities (such as a GUI window)
available at the higher level.
For an example of how a checked exception can be handled in a method, we can revise the run( )
method from Section 2.3.1 to scan lines from a fil and determine which lines consist of leap years. The
name of the f le will be read from the keyboard in a loop that continues until the name corresponds to an
existing f le. Here is the revised run( ) method:
public void run()
{
final String INPUT_PROMPT = "Please enter the file name: ";
String fileName;
while (true)
{
System.out.print (INPUT_PROMPT);
fileName = keyboardScanner.next();
try
{
Scanner sc = new Scanner (new File (fileName));
while (sc.hasNext())
try
{
System.out.println (isLeapYear (sc));
} // try to scan a year
catch (InputMismatchException e)
{
System.out.println ("The input is not an integer.");
sc.nextLine();
} // catch input mismatch
break;
78 C H A P T E R 2 Additional Features of Programming and Java
The break statement—to exit the outer loop—is executed when the fil name scanned from the
keyboard represents an existing f le, and that fil has been scanned for leap years. The inner-loop
condition— sc.hasNext() —is slightly preferable to sc.hasNextLine(). In particular, if the last line
in the f le is blank, sc.hasNext() will return false and the execution of the inner loop will terminate, as
desired. But if the last line in the fil is blank, sc.hasNextLine() will return true, and the subsequent
call to sc.nextInt() in the isLeapYear method will throw NoSuchElementException. Of course,
if that exception is caught in the run() method, then sc.hasNextLine() will not be a problem.
A checked exception must be caught or must be specifie in a throws clause, and the compiler
“checks” to make sure this has been done. Which exceptions are checked, and which are unchecked? The
answer is simple: run-time exceptions are not checked, and all other exceptions are checked. Figure 2.1
shows Java’s exception hierarchy, including IOException with its subclasses (such as FileNotFound
Exception), and RuntimeException with its subclasses (such as NullPointerException).
Why are run-time exceptions not checked? The motivation behind this is that an exception such as
NullPointerException or NumberFormatException, can occur in almost any method. So appending
a throws clause to the heading of such a method would burden the developer of the method without
providing any helpful information to the reader of that method.
When an exception is thrown, the parameter classes of the subsequent catch blocks are tested, in
order, until (unless) one is found for which the thrown exception is an instance of that class. So if you
Exception
IOException RuntimeException …
FileNotFoundException …
InputMismatchException
ArrayIndexOutOfBoundsException StringIndexOutOfBoundsException
FIGURE 2.1 The exception hierarchy. In the unifie modeling language, inheritance is represented with an
arrow from a subclass to its superclass
2.3 Exception Handling 79
want to ensure that all run-time exceptions are caught in a method, you can insert the following as the last
catch block:
catch (RuntimeException e)
{
// code to handle the exception
} // catch RuntimeException
If you do have a catch block for RuntimeException, make sure that catch block is not followed by a
catch block for a subclass of RuntimeException. For example, because NullPointerException is
a subclass of RuntimeException, the following sequence of catch blocks will generate a compile-time
error:
catch (RuntimeException e)
{
// code to handle the exception
} // catch RuntimeException
catch (NullPointerException e) // error!
{
// code to handle the exception
} // catch NullPointerException
The error message will inform you that the second catch block is unreachable code.
An exception can be explicitly thrown by the programmer, who gets to decide which exception class
will be instantiated and under what circumstances the exception will be thrown. For example, suppose we
want a method to return the smaller of two double values that represent prices obtained by comparison
shopping. If the prices are too far apart—say, if the difference is greater than the smaller price—we throw
an exception instead of returning the smaller price. The mechanism for explicitly throwing the exception
is the throw statement, which can be placed anywhere a statement is allowed. For example, the code may
be as in the following smaller method (the Math class’s static method abs returns the absolute value
of its argument):
public class Compare
{
public static void main (String[ ] args)
{
new Compare().run();
} // method main
} // class Compare
80 C H A P T E R 2 Additional Features of Programming and Java
If the given comparison is true, the throw statement is executed, which creates a new instance of the
exception class ArithmeticException by calling the constructor that takes a String argument. The
exception will be propagated back to the method that called smaller and the execution of the smaller
method will immediately terminate. In the above example, the exception is not caught, so the program
terminates. The output is
4.0
java.lang.ArithmeticException: difference too large
The choice of ArithmeticException as the exception class to be instantiated is somewhat arbitrary.
A user can even create new exception classes. For example,
public class UnreasonablenessException extends RuntimeException
{
public UnreasonablenessException (String s)
{
super (s);
} // constructor with String parameter
} // class UnreasonablenessException
This creates a new instance of the class UnreasonablenessException. The above program would
terminate with the message:
UnreasonablenessException: difference too large
3 Actually, RuntimeException consists of several constructors, each of which merely invokes the corresponding constructor in Exception,
the superclass of RuntimeException. The Exception class passes the buck to its superclass, Throwable, where the low-level details are
dealt with.
2.4 File Output 81
If your try or catch blocks may throw uncaught exceptions, you should include a finally
block—otherwise, any code placed after the last catch block may not be executed. Finally, a finally
block is required by the Java language if you have a try block without a catch block.
Lab 2 provides the opportunity for you to practice exception-handling.
The handling of input-output exceptions is one of the essential features of fil processing, discussed in
Section 2.4.
The PrintWriter object that is referenced by printWriter can now invoke the print and println
methods. For example,
printWriter.println (line);
82 C H A P T E R 2 Additional Features of Programming and Java
The output is not immediately stored in the fil "scores.out". Instead, the output is stored in a buffer:
a temporary storage area in memory. After all calls to print and println have been made by print
Writer’s object, that object’s close method must be called:
printWriter.close();
The close method flushe the buffer to the fil "scores.out" and closes that f le.
The f le-processing program we will develop in this section is based on a program from Section 0.2.5
of Chapter 0. That program calculates the sum of scores read in from the keyboard. Here is a slightly
modifie version of that program, with a separate method to scan in and add up the scores:
import java.util.*; // for the Scanner class
System.out.print (INPUT_PROMPT);
/**
* Returns the sum of the scores scanned in.
*
* @param sc – a (non-null reference to a) Scanner object from
* which the scores are scanned in.
*
* @return the sum of the scores scanned in from sc.
*
* @throws InputMismatchException – if a value scanned in from sc is not an
* integer.
*
*/
public int addScores (Scanner sc)
{
2.4 File Output 83
int score,
sum = 0;
while (true)
{
score = sc.nextInt();
if (score == SENTINEL)
break;
sum += score;
} // while
return sum;
} // method addScores
} // class Scores1
In the next version, the output goes to a file To enable someone reading that f le to confir that the
result is correct for the given input, each score is written to the output file IOException is caught for
output-fil creation. The corresponding try block encompasses the creation of the output fil and the input
loop. For the sake of simplicity, there is no try block to catch input-mismatch exceptions (arising from
input values that are not integers).
import java.util.*;
import java.io.*;
try
{
Scanner sc = new Scanner (System.in);
printWriter = new PrintWriter (new BufferedWriter
(new FileWriter ("scores.out")));
System.out.print (INPUT_PROMPT);
addScores (sc, printWriter);
} // try
84 C H A P T E R 2 Additional Features of Programming and Java
catch (IOException e)
{
System.out.println (e);
} // catch IOException
finally
{
printWriter.println (RESULT + sum);
printWriter.close();
} // finally
} // method run
int score,
sum = 0;
while (true)
{
score = sc.nextInt();
if (score == SENTINEL)
break;
printWriter.println (score);
sum += score;
} // while
return sum;
} // method addScores
} // class Scores2
catch (InputMismatchException e)
{
printWriter.println (e + " " + sc.nextLine());
} // catch InputMismatchException
} // while
if (!atLeastOneScore)
throw new RuntimeException ("The input contains no legal scores. ");
The call to nextLine( ) in the catch block of the addScores method allows the offending input to be
printed to the output file and also allows the scanner to skip over that line (otherwise, the input prompt
will be continuously repeated, and the output fil will continuously get copies of the exception message.
The most important fact to remember about fil output is that the f le writer must be explicitly closed,
or else the f le will be incomplete, and probably empty (depending on whether there was an intermediate
flushin of the buffer). As we will illustrate in the next class, Scores3, we can ensure that a f le writer
is closed when (if) a program terminates by enveloping the construction of the fil writer in a try block,
which is followed by a finally block that closes the fil writer.
For this f nal version of the program, we scan from an input fil (with one score per line) instead
of from the keyboard. As we saw in Section 0.2.5—f le input is almost identical to console input. For
example, to read from the fil "scores.in1", we start with
Scanner fileScanner = new Scanner (new File ("scores.in1"));
Warning: This assumes that the fil scores.in1 is in the expected directory. For some Integrated
Development Environments, the input fil is assumed to be in the directory that is one level up from the
source-fil directory. Sometimes, you may need to specify a full path, such as
Scanner fileScanner = new Scanner (new File
("c:\\projects\\score_project\\scores.in1"));
Two back-slashes are required because a single back-slash would be interpreted as the escape character.
Input file seldom end with a sentinel because it is too easy to forget to add the sentinel at the end
of the f le. Instead, scanning continues as long as the next() or nextLine() method returns true. So for
fil input, we write
while (fileScanner.hasNext())
86 C H A P T E R 2 Additional Features of Programming and Java
For the sake of simplicity, if there is only one input file we will not worry about closing that f le at the
end of the program: it will automatically be closed. And when it is re-opened in a subsequent program, its
contents will be unchanged. A program that leaves many input file unclosed can run out of fil descriptors,
and an IOException will be thrown.
As noted earlier in this section, closing an output fil entails copying the f nal contents of the f le
buffer to the file so we should explicitly close each output fil before the end of a program. Of course, if
the program does not terminate—due to an infinit loop, for example—the fil buffer will not be copied
(unless the fil was closed before the infinit loop).
The following program combines fil input and f le output. For the sake of generality, the program
does not “hardwire” the f le names (for example, "scores.in" and "scores.out"). In response to
prompts, the end-user enters, from the keyboard, the names of the input and output files If there is no
input fil with the given name, FileNotFoundException is caught, an error message is printed, and the
end-user is re-prompted to enter the name of the input file To allow this iteration, the try and catch
blocks that involve throwing and handling IOException are placed in an outer while loop.
What if there is no f le corresponding to the output fil name? Normally, this is not a problem: an
empty output fil with that name will be created. But if fil name is too bizarre for your system, such as
!@#$%^&*()
Here is the program, whose general structure is the same for all file-processin programs:
import java.util.*;
import java.io.*;
int sum = 0;
try
{
while (true)
{
try
{
System.out.print (IN_FILE_PROMPT);
fileScanner=new Scanner (new File (keyboardScanner.nextLine()));
System.out.print (OUT_FILE_PROMPT);
printWriter=new PrintWriter (new BufferedWriter
(new FileWriter (keyboardScanner.nextLine())));
sum = addScores (fileScanner, printWriter);
break;
} // try
catch (IOException e)
{
System.out.println (e);
} // catch
} // while files not OK
} // try
catch (NumberFormatException e)
{
System.out.println (e);
} // catch NumberFormatException
finally
{
printWriter.println (RESULT + sum);
printWriter.close();
} // finally
} // method run
/**
* Returns the sum of the scores scanned in.
*
* @param fileScanner – the Scanner object from which the scores are scanned
*
* @param printWriter – the PrintWriter object to which the scores are written.
* If a score generates InputMismatchException, the message
88 C H A P T E R 2 Additional Features of Programming and Java
int score,
sum=0;
boolean atLeastOneScore=false;
while (fileScanner.hasNext())
{
try
{
score=fileScanner.nextInt();
printWriter.println (score);
sum+=score;
atLeastOneScore=true;
} // try
catch (InputMismatchException e)
{
printWriter.println (e+": "+fileScanner.nextLine());
} // catch InputMismatchException
} // while more scores in input file
if (!atLeastOneScore)
throw new NumberFormatException (NO_LEGAL_SCORES_MESSAGE);
return sum;
} // method addScores
} // class Scores3
Note that the message printWriter.close() is not in a catch block because the printWriter should
be closed whether or not any exceptions are thrown.
Assume that the f le "scores.in1" consists of the following four lines:
82
8z
77
99
Also, assume that there is no f le named "scores.in0" or "scores3.in" in the working directory.
Whether there is already a fil named "scores.out1" or not is irrelevant. Here is a sample keyboard
session, with input in boldface:
Please enter the name of the input file: scores.in0
java.io.FileNotFoundException: scores.in0 (The system cannot find the file specified)
Please enter the name of the input file: scores3.in
2.4 File Output 89
With fil input, it is not suff cient that the fil exist in order to associate a fil scanner with that file Your
code must also account for the possibility that the f le does not exist. The easy way to accomplish this is
to include a throws FileNotFoundException clause immediately after the heading of the method that
associates a f le scanner with the f le. The drawback to this approach is that if the f le name is incorrect—if
either the f le does not exist or the fil name is misspelled—then the end-user will not have the opportunity
to correct the mistake.
A better alternative, as we did in the run() method of the class Scores3, is to include a try block
and catch block for FileNotFoundException. To enable end-users to recover from incorrect fil names,
those blocks should be within a loop that continues until a correct fil name has been entered.Similarly,
to construct a fil writer, IOException must be caught or declared in a throws clause. That is why, in
the above program, the type of the relevant catch-block parameter is IOException instead of FileNot
FoundException.
There is a common thread in the above examples. The run() method handles the aspects of the
program that require the end user’s intervention, such as input from the keyboard or from a GUI window,
or interpretation, such as output to the console window or to a GUI window. Accordingly, the method
called by the run() method should be testable in JUnit.
The major problem in testing the addScores method above is that the method outputs information
to a f le. So we will create an expected output fil from a given input file and check to make sure the
expected output fil matches the actual f le generated by the addScores method. The expected fil will
have one line for each line in the input file and will not include the f nal sum – because that value is not
printed in the addScores method. We will also need an exception test for an input fil with no legitimate
scores, and exception tests if either the fileScanner or printWriter argument is null. Here is part
of the Scores3Test.java file
import org.junit.*;
import static org.junit.Assert.*;
import org.junit.runner.Result;
import static org.junit.runner.JUnitCore.runClasses;
import java.util.*;
import java.io.*;
@Test
public void scores3Test1() throws IOException
{
Scanner fileScanner = new Scanner (new File ("scores3.in1"));
PrintWriter printWriter = new PrintWriter (new BufferedWriter
(new FileWriter ("scores3.out1")));
} // class Scores3Test
scores3.in1
80
x
50
y
scores3.in2
x
y
scores3.exp
80
java.util.InputMismatchException: x
50
java.uti.InputMismatchException: y
instead of
The main advantage is platform independence. It doesn’t matter whether your computer’s operating
system is Windows, Linux, or something else, the results of running your Java program will be exactly
(well, almost exactly) the same on all platforms. A second benefi is customized security. For example, if
the bytecode fil is coming from the web, the virtual machine will not allow the application to read from
or write to the local disk. But such activities would be allowed for a local application.
The Java Virtual Machine oversees all aspects of your program’s run-time environment. In
Sections 2.6.1 and 2.6.2, we investigate two tasks of the Java Virtual Machine.
First, the new operator allocates space for a FullTimeEmployee object. Then, to ensure that each
fiel has at least a minimal level of initialization, the Java Virtual Machine initializes all of the class’s
field according to their types. Reference f elds are initialized to null, integer field to 0, f oating-point
field to 0.0, char field to the character at position 0 in the Unicode collating sequence, and boolean field
to false. Then the specifie constructor is called. Finally, the starting address of the newly constructed
FullTimeEmployee object is returned.
There is an important consequence of this pre-initialization by the Java Virtual Machine. Even if a
default constructor has an empty body—such as the one supplied by the Java compiler if your class does
not declare any constructors—all field in the class will still get initialized.
Unlike f elds, local variables are not automatically initialized. Section 0.2.4 has the details.
Fortunately, you need not worry about garbage collection. The Java run-time system includes a
method that performs automatic garbage collection. This method will be invoked if the new operator is
invoked but there is not enough memory available for the object specified With the supersizing of memory
in recent years, this is an increasingly rare occurrence. To free up unused memory, the space for any object
to which there are no references can be de-allocated. The garbage collector will seek out big chunks of
garbage f rst, such as an array. In any event, this is all taken care of behind the scenes, so your overall
approach to the topic of garbage collection should be “Don’t worry. Be happy.”
Section 2.6 investigates the relationship between packages and visibility modifiers
2.7 Packages
A package is a collection of related classes. For each such class, the f le in which the class is declared
starts with the package declaration. For example, a fil in a package of classes related to neural networks
might start with
package neuralNetwork;
For another example, the Scanner class, part of the package java.util, is in the f le Scanner.java,
which starts with
package java.util;
If a f le includes an instance of the Scanner class, that class can be “imported” into the f le. This is done
with an import directive, starting with the reserved word import:
import java.util.Scanner;
Many of the classes you create will utilize at least one class from the package java.util, so you can simply
import the whole package:
import java.util.*; //the asterisk indicates that all files from java.util will be available
Occasionally, you may prefer to use the fully qualifie name. For example, suppose your project uses two
classes named Widget: one in the package com.acme and one in the package com.doodads. To declare
(a reference to) an instance of the latter, you could write
com.doodads.Widget myWidget;
Every Java f le must have a class with the visibility modifie public. Also, the name of that public class
must be the same as the name of the f le—without the .java extension. At the beginning of the f le,
there must be import directives for any package (or file needed by the f le but not part of the f le. An
exception is made for the package java.lang, which is automatically imported for any f le.
A class member with no visibility modifie is said to have default visibility. A member with default
visibility can be accessed by any object (or class, in the case of a static member) in the same package
as the class in which the member is declared. That is why default visibility is sometimes referred to as
94 C H A P T E R 2 Additional Features of Programming and Java
“package-friendly visibility.” All classes without a package declaration are part of an unnamed package.
But there may be more than one unnamed package so, as a general rule, if your project contains more
than one class f le, each fil should include a package declaration.
Technically, it is possible for a Java f le to have more than one class with public visibility; all but one
of those classes must be nested , that is, declared within another class. The Java Collections Framework,
part of the package java.util, has many nested classes. Except for nested classes, a Java f le is allowed to
have only one class with public visibility. Every other non-nested class must have default visibility.
Because of the way the Java language was developed, protected visibility is not restricted to
subclasses. In general, if an identifie in a class has protected visibility, that identifie can also be accessed
in any class that is in the same package as the given class. For example, any class—whether or not a
subclass—that is in the same package as FullTimeEmployee can access the name and grossPay field
of a FullTimeEmployee object.
In the Java Collections Framework, most of the f elds have default visibility or private visibility.
Almost no field have protected visibility: Subclassing across package boundaries is discouraged in
the Java Collections Framework. Why? The main reason is philosophical: a belief that the efficienc to
users of the subclass is not worth the risk to the integrity of the subclass if the superclass is subsequently
modified This danger is not merely hypothetical. In Java 1.1, a class in java.security was a subclass of
the Hashtable class. In Java 2, the Hashtable class was modified and this opened a security hole in
the subclass. Subclassing represents more of a commitment than mere use. So even if a class permits
subclassing, it is not necessarily the wisest choice.
The bottom line is that protected visibility is even less restrictive than default visibility. This
corruption of the meaning of protected visibility may make you reluctant to designate your field as
protected. An alternative is to designate the f elds as private, but to create public methods to get
and set the values of those private fields As described in Programming Exercise 1.3, an accessor method
returns a copy of a f eld (or a copy of the object referenced, if the fiel is a reference), and a mutator
method alters a f eld (or the object referenced by the f eld). The usefulness of this approach diminishes as
the number of f elds increases.
The f nal topic in this chapter looks at the importance of overriding the Object class’s equals
method, the barriers to overriding that method, and how those barriers are overcome.
This method, as with the other methods in the Object class, is intended to be overridden by subclasses,
which can compare f eld values, for example. The object class has no fields so what does it compare?
2.8 Overriding the Object Class’s equals Method 95
It compares references, specifically the calling object reference with the argument reference. Here is the
definition
public boolean equals (Object obj)
{
return this == obj;
} // method equals
As we saw in Section 1.3.2, in any class, the reserved word this is a reference to the calling object. For
example, suppose the call is
obj1.equals (obj2)
Then in the definitio of the equals method, this is a reference to the object that is also referenced by
obj1, and obj is a reference to the object that is also referenced by obj2.
Because the Object class’s equals method compares references, any class with an equals method
should defin its own version of that method. For example, suppose we decide to add an equals method
to the FullTimeEmployee class. The f rst question is: Should we overload, that is,
public boolean equals (FullTimeEmployee full)
Overloading equals —that is, having a different parameter list than the version inherited from the Object
class—can be done fairly simply. The only obstacle is that double values should not be directly tested
for equality; note, for example, that System.out.println (.4==10.0 - 9.6) outputs “false”, (but
System.output.println (.4==1.0 - .6) outputs “true”). Here is the definition
Recall that the format method rounds off the value in the grossPay field so we need not compare
grossPay and full.grossPay for equality. This version compares objects, not references, and so the
value true would be printed by each of the following:
System.out.println (new FullTimeEmployee ("a", 100.00).equals
(new FullTimeEmployee ("a", 100.00)));
The overloaded version works well as long as the type of the calling object is known, at compile-time,
to be FullTimeEmployee (or subclass of FullTimeEmployee). Sadly, that is not always the case. For
example, many of the classes in the Java Collections Framework store a collection of objects. Those classes
96 C H A P T E R 2 Additional Features of Programming and Java
have a contains method to determine if a given object occurs in the collection. The contains method’s
heading is
public boolean contains (Object obj)
Typically, in testing for containment, the equals method is invoked, with obj as the calling object. For
a given application, the collection may consist of FullTimeEmployee objects. But when the equals
method—called by contains —is compiled, the only information available about the calling object is its
declared type: Object. Therefore, the compiler generates bytecode for a call to the equals method in the
Object class, which takes an Object parameter. At run time, when the class of the object (referenced
by) obj is available, the version of the Object-parameter equals method executed will be the one in
the Object class unless that method has been overridden. Whether the equals method has been overloaded
is irrelevant!
Now that we have established the significanc of overriding the Object class’s equals method,
let’s see how to do it. We will take the FullTimeEmployee class as an example. The basic idea is simple:
if the type of the argument object is not FullTimeEmployee, return false. Otherwise, as we did earlier
in this section, compare the values returned by the toString( ) method of the calling object and the
argument object. Here are some sample results:
System.out.println (new FullTimeEmployee ("a", 100.00).equals
("yes")); // false
System.out.println (new FullTimeEmployee ("a", 100.00).equals
(new FullTimeEmployee ("a", 100.00))); // true
System.out.println (new FullTimeEmployee ("a", 100.00).equals
(new FullTimeEmployee ("b", 100.00))); // false
System.out.println (new FullTimeEmployee ("a", 100.00).equals
(new FullTimeEmployee ("a", 200.00))); // false
1. Every class whose instances might be elements of a collection should have an equals method that
overrides the Object class’s equals method.
2. The instanceof operator returns true if and only if, at run-time, the object referenced by the left
operand is an instance of the class that is the right operand.
3. Before comparing the calling object with the argument object, cast the parameter type, Object, to
the class in which equals is being defined
Programming Exercise 2.11 has even more information about the equals method.
Summary 97
SUMMARY
The static modifie is used for identifier that apply to to the output fil by a call to the close method.
a class as a whole, rather than to a particular instance of The Java run-time, also known as the Java Vir-
a class. Constants should be declared to be static, because tual Machine, is a program that interprets and exe-
then there will be only one copy of the constant, instead of cutes the bytecode output from a Java compiler. Among
one copy for each instance of the class. To access a static other tasks, the Java Virtual Machine is responsible for
identifi r outside of its class, the class identifi r—rather pre-initialization of f elds, de-allocation of inaccessible
than an object—is the qualif er. objects, and managing threads.
JUnit is an Open Source software product that A package is a collection of related classes. An
allows the methods in a class to be tested without human identifi r with no visibility modifi r is said to have default
intervention. The tests are developed as soon as the visibility. Java is “package friendly.” That is, an identif er
method specification are created. In general, methods with default visibility can be accessed by any object (or
should be designed to facilitate testing without human class, in the case of a static member) in the same package
intervention, so input from System.in and output to as the class in which the identifi r is declared. If a given
System.out should be avoided in methods to be tested. class’s identif er has protected visibility, that identifi r
An exception is an object that signals a special can be accessed in any subclass of the given class, even in
situation, usually that an error has occurred. An exception a different package. Unfortunately, that identifi r may also
can be handled with try/catch blocks. The sequence of be accessed in any class—even if not a subclass—within
statements in the try block is executed. If, during exe- the given package’s class.
cution, an exception is thrown (indicating that an error The equals method in the Object class should
has occurred), the appropriate catch block is executed be overridden for any class C whose instances might
to specify what, if anything, is to be done. become elements of a collection. The overriding method
File output is similar to console-oriented output, invokes the instanceof method to return false for
except that a PrintWriter object is explicitly created any argument object that is not an instance of class C, and
to write to the specifie output file The output is not then casts the Object class to class C in order to make
immediately sent to the output file but rather to a buffer. the appropriate comparison(s).
At the conclusion of fil processing, the buffer is f ushed
98 C H A P T E R 2 Additional Features of Programming and Java
CROSSWORD PUZZLE
1 2 3
4 5
10
www.CrosswordWeaver.com
ACROSS DOWN
6. An object created by an unusual condition, typically, an 1. The kind of exception for which the compiler
attempt at invalid processing. confirms that the exception is caught within
the method or that a throws clause is appended
8. An identifier associated with a class itself rather than with to the method’s heading.
an instance of the class is called a _______ identifier.
2. When an exception is thrown in a method that
9. A reserved-word modifier associated with a location that does not catch the exception, the transferring of
can be assigned to only once. control back to the calling method is referred to
as ___________ the exception.
10. A method in the PrintWriter class that ensures a file
is complete by flushing the output buffer. 3. A class member that can be accessed in any
class within the same package as the given class
or in any subclass of the given class is said to
have ________ visibility.
CONCEPT EXERCISES
2.1 The System class in java.lang has a class constant identif er that has been extensively used in Chapters
0, 1 and 2. What is that constant identifier Why should a class’s constant identifier be static ? Should a
method’s constant identifier be static ? Explain.
2.2 Create a catch block that will handle any exception. Create a catch block that will handle any input/output
exception. Create a catch block that will handle any run-time exception.
2.3 What is wrong with the following skeleton?
try
{
...
} // try
catch (IOException e)
{
...
} // catch IOException
catch (FileNotFoundException e)
{
...
} // catch FileNotFoundException
2.4 Suppose fileScanner is a Scanner object for reading from an input file and printWriter is a Print
Writer object for writing to an output fil . What will happen if, at the end of a program, you forget to close
fileScanner ? What will happen if, at the end of a program, you do not close printWriter ?
2.5 What does “bottom-up” testing mean with respect to the classes in a project?
2.6 Suppose we create a two-dimensional array (literally, an array in which each element is an array). The following
creates an int array with 50000 rows and 100000 columns:
int [ ][ ] a = new int [50000][100000];
If this code is executed, the program terminates abnormally, and the message is
java.lang.OutOfMemoryError
Exception in thread "main"
Why wasn’t memory re-allocated by the garbage collector? Hypothesize whether this abnormal termination be
handled with a try-block and catch-block. Test your hypothesis and explain.
2.7 Can a protected fie d be accessed outside of the class in which it is declared and subclasses of that class?
What does the following statement mean? “Subclassing represents more of a commitment than mere use.”
2.8 Arrays are strange objects because there is no array class. But an array object can call methods from the
Object class. Determine and explain the output from the following code:
a [3] = 7;
b [3] = 7;
System.out.println (a.equals(b));
100 C H A P T E R 2 Additional Features of Programming and Java
PROGRAMMING EXERCISES
2.1 Develop the specificatio for a method that scans one line that is supposed to contain three double values
and returns the largest. Throw all possible exceptions. Start with a stub for your method and create a test class
to test your method. Re-test your method as you defin it. Finally, include a main method and a run( )
method that calls the method you developed.
2.2 Develop the specificatio for a method that scans (what are supposed to be) double values from a fil and
returns the largest. Throw all possible exceptions. Start with a stub for your method and create a test class
to test your method. Re-test your method as you defin it. Finally, include a main method and a run( )
method that calls the method you developed.
2.3 Modify the run method for the Company class to scan from an input fil and write to an output file Include
a re-prompt if either the input or output path is incorrect.
2.4 Hypothesize what is wrong with the following method:
Test your hypothesis by calling this method from a run( ) method. Can a try-block and catch-block
handle the problem? Explain.
2.5 Hypothesize the output from the following:
Test your hypothesis. Provide the code in the String class that explains why the output is what it is.
2.6 Give an example to show that private visibility is more restrictive than default visibility. Give an example
to show that default visibility is more restrictive than protected visibility. Give an example to show that
protected visibility is more restrictive than public visibility. In each case, test your code to make sure
that the more restrictive choice generates a compile-time error message. No error message should be generated
for the less restrictive choice.
2.7 Protectedness transfers across packages, but only within a subclass, and only for objects whose type is that
subclass. For a bare-bones illustration, suppose we have class A declared in package APackage:
package APackage;
public class A
{
protected int t;
} // class A
Also, suppose that classes C and D are subclasses of A and that C and D are in a different package from
A. Then within class D, the t fie d is treated as if it were declared in D instead of in A. Here are possible
declarations for classes C and D:
import APackage.*;
Class D is declared in another fi e. For each of the four accesses of t in the following declaration of class D,
hypothesize whether the access is legal or illegal:
import APackage.*;
} // class D
Test your hypotheses by creating and running a project that includes the above files
2.8 Re-do Programming Exercise 1.2 to print out the number of above-average salaries. Use an array f eld to hold
the salaries, and assume there will be at most 10 salaries in the input.
2.9 Study the specificatio of the arraycopy method in the System class, and then write a short program that
uses the arraycopy method to copy all the elements of an array to another array. Output the elements in
the destination array to make sure the copying actually occurred.
2.10 Re-do Programming Exercise 2.8 if the input can contain an arbitrary number of salaries.
Hint: Start with an array of length 10. Whenever the number of salaries in the input exceeds the current
length of the array f eld, create a new array of twice that length, copy the old array to the new array—see
Programming Exercise 2.9—and then assign the new array (reference) to the old array (reference).
2.11 According to the full method specifica ion in the Object class, any override of the Object class’s equals
method should satisfy the following fiv properties:
1. refl xivity, that is, for any non-null reference x,
x.equals (x)
should return true.
2. symmetry, that is, for any non-null references x and y,
x.equals (y)
should return the same result as
y.equals (x)
y.equals (z)
102 C H A P T E R 2 Additional Features of Programming and Java
x.equals (z)
x.equals (y)
should consistently return true or consistently return false, provided no information used in equals
comparisons on the objects is modified
5. actuality, that is, for any non-null reference x,
x.equals (null)
1. The size of the window will be 700 pixels wide and 500 pixels high.
2. The upper-left-hand corner of the window will have x-coordinate 150 and y-coordinate 250.
3. Each of the four buttons on the top of the window will be 80 pixels wide and 30 pixels high. The
foreground color of the Home button will be green, and the foreground color of the other three buttons will
be red.
4. The input line will be 50 pixels wide.
5. The output area will be scrollable in both directions.
Programming Exercises 103
6. The only tags allowed in a page are link tags, for example,
In this example, the only part that will appear in the output area is browser4.
7. For simplicity, all links (such as browser.in4 above) will come from the same directory, and all
link ‘‘nicknames’’ (such as browser4 above) will consist of a single word.
8. In the output area, the foreground color of each link’s nickname should be blue.
9. A line in a page may have several link tags, but no tag will be split between two lines.
10. If a page clicked on or typed in does not exist, the following error message should appear in the output
area:
At that point, the end user can click on the Home button, can enter a new file path in the input line, or
can close the application.
Hints:
If the end user now clicks on browser2, the contents of browser.in2 will be displayed. Here are the contents
of browser.in1, browser.in2, browser.in4, and browser.in5 (browser.in3 does not exist):
browser.in1:
browser.in2:
browser.in4
browser.in5:
As noted in Section 2.2, a correct method is one that satisfies its specification. In defining a method, the
first goal is to make sure the method is correct, and unit testing allows us to increase our confidence
in a method’s correctness. But if the method’s execution time or memory requirements are excessive,
the method may be of little value to the application. This chapter introduces two tools for measuring a
method’s efficiency. The first tool provides an estimate, based on studying the method, of the number of
statements executed and number of variables allocated in a trace of the method. The second tool entails
a run-time analysis of the method. Both tools are useful for comparing the efficiency of methods, and
the tools can complement each other.
CHAPTER OBJECTIVES
1. Be able to use Big-O (and Big-Theta) notation to estimate the time and space requirements of
methods.
2. Be able to conduct run-time analyses of methods.
/**
* Returns the number of elements in a non-empty array that are greater than
* the mean of that array.
*
* @param a – an array of double values
* @param mean – the sum of the elements in a, divided by a.length.
*
* @return the number of elements in a that are greater than mean
*
*/
public static int aboveMeanCount (double[ ] a, double mean)
{
int n = a.length,
count = 0;
There are six statements that will be executed only once: the assignment of the arguments to the parameters
a and mean; the initialization of n, count and i; and the return of count. Within the for statement,
i will be compared to n a total of n + 1 times, i will be incremented n times and the comparison of
a [i] to mean will be made n times. If n − 1 elements have the value 1.0 and the other element has the
value 0.0, then a [i] will be greater than mean a total of n − 1 times, so count will be incremented
n − 1 times. The total number of statements executed in the worst case, that is, worstTime(n), is
6 + (n + 1) + n + n + (n − 1) = 4n + 6
Sometimes we will also be interested in the average-case performance of a method. We defin average-
Time(n) to be the average number of statements executed in a trace of the method. This average is taken
over all invocations of the method, and we assume that each set of n parameter/input values for a call
is equally likely. For some applications, that assumption is unrealistic, so averageTime(n) may not be
relevant.
In the for loop of the just completed example, a [i] will be greater than mean, on average, half
of the time, so count will be incremented only n/2 times. Then averageTime(n) is 3.5n + 7.
Occasionally, especially in Chapters 5 and 11, we will also be interested in estimating the space
requirements of a method. To that end, we defin worstSpace(n) to be the maximum number of variables
allocated in a trace of the method, and averageSpace(n) to be the average number of variables allocated
in a trace of the method. For an array, we treat each element—that is, indexed variable—to be a separate
variable. So an array of length n would contribute n variables. The aboveMeanCount method does not
create an array; worstSpace(n) = averageSpace(n) = 5.
The basic idea behind Big-O notation is that we often want to determine an upper bound for the
behavior of a function, that is, to determine how bad the performance of the function can get. For example,
suppose we are given a function f. If some function g is, loosely speaking, an upper bound for f , then
we say that f is Big-O of g. When we replace “loosely speaking” with specifics we get the following
definition
Let g be a function that has non-negative integer arguments and returns a non-negative value for all
arguments. A function f is said to be O(g) if for some positive constant C and some non-negative
constant K,
f(n) ≤ C g(n) for all n ≥ K.
g(n) = n 3 , for n = 0, 1, 2, . . .
Example 3.1
Let f be the function worstTime defined for the aboveMeanCount method in Section 3.1 and repeated here:
Then
f(n) = 4n + 6, for n = 0, 1, 2, . . .
SOLUTION
We need to find non-negative constants C and K such that f(n) ≤ C∗ n for all n ≥ K. We will show that each
term in the definition of f is less than or equal to some constant times n for n greater than or equal to some
non-negative integer. Right away, we get:
4n ≤ 4n for n ≥ 0, and
6 ≤ 6n for n ≥ 1.
So for any n ≥ 1,
f(n) ≤ 4n + 6n = 10n.
∗n for all n ≥ K. This shows that f is O(n).
That is, for C = 10 and K = 1, f(n) ≤ C
Example 3.2
Let a and b be positive constants. Show that if f is O(loga n) then f is also O(logb n).
SOLUTION
Assume that f is O(loga n). Then there are non-negative constants C and K such that for all n ≥ K,
f(n) ≤ C ∗ loga n
By a fundamental property of logarithms (see Section A2.4 of Appendix 2),
loga n = (loga b) ∗ (logb n) for any n > 0.
Let C1 = C ∗ loga b.
Then for all n ≥ K, we have
f(n) ≤ C ∗ loga n = C ∗ loga b ∗ logb n = C1 ∗ logb n,
and so f is O(logb n).
Because the base of the logarithm is irrelevant in Big-O estimates, the base is usually omitted. For example,
you will often see O(log n) instead of O(log2 n) or O(log10 n).
The f nal example in this introduction to Big-O notation illustrates the significanc of nested loops
in estimating worstTime(n).
3.1 Estimating the Efficiency of Methods 109
Example 3.3
SOLUTION
For this nested loop, every trace will entail the execution of the same number of statements. So worstTime(n)
and averageTime(n) will be equal. And that is frequently the case.
In the outer loop, the initialization of i is executed once, the continuation condition, i < n, is
executed n + 1 times, and i is incremented n times. So far, we have
1 + (n + 1) + n
statements executed. For each of the n iterations of the outer loop, the initialization of j is executed once, the
continuation condition, j < n, is executed n + 1 times, j is incremented n times, and the call to println
is executed n times. That increases the number of statements executed by
n(1 + (n + 1) + n)
1 + (n + 1) + n + n(1 + (n + 1) + n) = 2n2 + 4n + 2
Since the same number of statements will be executed in every trace, we have
worstTime(n) = 2n2 + 4n + 2
In Example 3.3, the number of statements executed in the outer loop is only 2n + 2, while 2n 2 + 2n
statements are executed in the inner loop. In general, most of the execution time of a method with nested
loops is consumed in the execution of the inner(most) loop. So that is where you should devote your
efforts to improving efficiency
Note that Big-O notation merely gives an upper bound for a function. For example, if f is O(n 2 ),
then f is also O(n 2 + 5n + 2), O(n 3 ) and O(n 10 + 3). Whenever possible, we choose the smallest element
from a hierarchy of orders, of which the most commonly used are shown in Figure 3.1. For example,
if f (n) = n + 7 for n = 0, 1, 2, . . . , it is most informative to say that f is O(n)—even though f is also
O(n log n) and O(n 3 ). Similarly, we write O(n) instead of O(2n + 4) or O(n − log n), even though
O(n) = O(2n + 4) = O(n − log n); see Exercise 3.9.
Figure 3.2 shows some more examples of functions and where they fi in the order hierarchy.
110 C H A P T E R 3 Analysis of Algorithms
O(1) ⊂ O(log n) ⊂ O(n 1/2 ) ⊂ O(n) ⊂ O(n log n) ⊂ O(n 2 ) ⊂ O(n 3 ) ⊂ . . . ⊂ O(2n ) . . .
FIGURE 3.1 Some elements in the Big-O hierarchy. The symbol “⊂” means “is contained in”. For example,
every function that is in O(1) is also in O(log n)
One danger with Big-O notation is that it can be misleading when the values of n are small. For example,
consider the following two functions for n = 0, 1, 2, . . . .
f (n) = 1000 n is O(n)
and
g(n) = n 2 /10 is O(n 2 )
But f (n) is larger than g(n) for all values of n less than 10,000.
The next section illustrates how easy it can be to approximate worstTime(n)—or averageTime(n)—
with the help of Big-O notation.
For example, for the following constructor from Chapter 1, worstTime(n) is O(1):
public HourlyEmployee (String name, int hoursWorked, double payRate)
{
this.name = name;
this.hoursWorked = hoursWorked;
this.payRate = payRate;
overtimePay = 0.00;
} // if
else
{
regularPay = MAX_REGULAR_HOURS * payRate;
overtimePay = (hoursWorked - MAX_REGULAR_HOURS) * (payRate * 1.5);
} // else
grossPay = regularPay + overtimePay;
} // 3-parameter constructor
Note that the execution of S may entail the execution of millions of statements. For example:
double sum = 0;
for (int i = 0; i < 10000000; i++)
sum += Math.sqrt (i);
The reason that worstTime(n) is O(1) is that the number of loop iterations is constant and
therefore independent of n. In fact, n does not even appear in the code. In any subsequent
analysis of a method, n will be given explicitly or will be clear from the context, so you
needn’t worry about “What is n?”
Skeleton 2. worstTime(n) is O(log n):
while (n > 1)
{
n = n / 2;
S
} // while
Let t(n) be the number of times that S is executed during the execution of the while
statement. Then t(n) is equal to the number of times that n can be divided by 2 until
n equals 1. By Example A2.2 in Section A2.5 of Appendix 2, t(n) is the largest integer
≤ log2 n. That is, t(n) = floo (log2 n).1 Since floo (log2 n) ≤ log2 (n) for any positive
integer n, we conclude that t(n) is O(log n) and so worstTime(n) is also O(log n).
The phenomenon of repeatedly splitting a collection in two will re-appear time and again
in the remaining chapters. Be on the lookout for the splitting: it signals that worstTime(n)
will be O(log n).
1 floor(x) returns the largest integer that is less than or equal to x.
112 C H A P T E R 3 Analysis of Algorithms
As an example of the Splitting Rule, here—from the Arrays class in the package
java.util —is the most widely known algorithm in computer science: the Binary Search
Algorithm. Don’t get hung up in the details; we will study a binary search algorithm
carefully in Chapter 5. Here, n refers to the size of the array being searched.
/**
* Searches the specified array of ints for the specified value using the
* binary search algorithm. The array must be sorted (as
* by the sort method, above) prior to making this call. If it
* is not sorted, the results are undefined. If the array contains
* multiple elements with the specified value, there is no guarantee which
* one will be found.
*
* @param a the array to be searched.
* @param key the value to be searched for.
* @return index of the search key, if it is contained in the list;
* otherwise, (-(insertion point) - 1). The
* insertion point is defined as the point at which the
* key would be inserted into the array a: the index of the first
* element greater than the key, or a.length, if all
* elements in the array are less than the specified key. Note
* that this guarantees that the return value will be greater than
* or equal to 0 if and only if the key is found.
* @see #sort(int[ ])
*/
public static int binarySearch(int[ ] a, int key)
{
int low = 0;
int high = a.length-1;
At the start of each loop iteration, the area searched is from index low through index high,
and the action of the loop reduces the area searched by half. In the worst case, the key is not
in the array, and the loop continues until low > high. In other words, we keep dividing
n by 2 until n = 0. (Incidentally, this repeated dividing by 2 is where the “binary” comes
from in Binary Search Algorithm.) Then, by the Splitting Rule, worstTime(n) is O(log n)
for the loop. And with just a constant number of statements outside of the loop, it is clear
that worstTime(n) is O(log n) for the entire method.
Skeleton 3. worstTime(n) is O(n):
for (int i = 0; i < n; i++)
{
S
} // for
The reason that worstTime(n) is O(n) is simply that the for loop is executed n times. It
does not matter how many statements are executed during each iteration of the for loop:
suppose the maximum is k statements, for some positive integer k . Then the total number
of statements executed is ≤ kn. Note that k must be positive because during each iteration,
i is incremented and tested against n.
As we saw in Section 3.1, worstTime(n) is O(n) for the aboveMeanCount method. But
now we can obtain that estimate simply by noting that the loop is executed n times.
public static int aboveMeanCount (double[ ] a, double mean)
{
int n = a.length,
count = 0;
for (int i = 0; i < n; i++)
if (a [i] > mean)
count++;
return count;
} // method aboveMeanCount
For another example of a method whose worstTime(n) is O(n), here is another method
from the Arrays class of the package java.util. This method performs a sequential
search of two arrays for equality; that is, the search starts at index 0 of each array, and
compares the elements at each index until either two unequal elements are found or the
end of the arrays is reached.
/**
* Returns true if the two specified arrays of longs are
* equal to one another. Two arrays are considered equal if both
* arrays contain the same number of elements, and all corresponding pairs
* of elements in the two arrays are equal. In other words, two arrays
* are equal if they contain the same elements in the same order. Also,
* two array references are considered equal if both are null.
*
* @param a one array to be tested for equality.
* @param a2 the other array to be tested for equality.
* @return true if the two arrays are equal.
*/
public static boolean equals (long[ ] a, long[ ] a2)
{
114 C H A P T E R 3 Analysis of Algorithms
if (a==a2)
return true;
if (a==null || a2==null)
return false;
return true;
} // method equals
The for loop is executed n times. For each iteration of the for loop, the while loop
is executed floo (log2 n) times—see Example 2 above—which is ≤ log2 n. Therefore
worstTime(n) is O(n log n). We needed to include the variable m because if the inner loop
started with while (n > 1), the outer loop would have terminated after just one iteration.
In Chapter 11, we will encounter several sorting algorithms whose worstTime(n) is
O(n log n), where n is the number of items to be sorted.
Skeleton 5. worstTime(n) is O(n 2 ):
a. .a
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
{
S
} // for j
The number of times that S is executed is n 2 . That is all we need to know to conclude
that worstTime(n) is O(n 2 ). In Example 3.3, we painstakingly counted the exact number
of statements executed and came up with the same result.
b. .b
for (int i = 0; i < n; i++)
for (int k = i; k < n; k++)
{
S
} // for k
3.1 Estimating the Efficiency of Methods 115
There are n − 1 iterations of the outer loop; when the smallest values are at indexes x
[0], x [1], . . . x [n − 2], the largest value will automatically be at index x [n − 1]. So
the total number of inner-loop iterations is
n−1
(n − 1) + (n − 2) + . . . + 1 = i = n(n − 1)/2
i =1
For the firs segment, worstTime(n) is O(n), and for the second segment, worstTime(n)
is O(n 2 ), so for both segments together, worstTime(n) is O(n + n 2 ), which is equal to
O(n 2 ). In general, for the sequence
A
B
if worstTime(n) is O(f ) for A and worstTime(n) is O(g) for B, then worstTime(n) is
O(f + g) for the sequence A, B.
Let g be a function that has non-negative integer arguments and returns a non-negative value for all
arguments. We define (g) to be the set of functions f such that for some positive constant C and
some non-negative constant K,
f(n) ≥ C g(n) for all n ≥ K.
If f is in (g) we say that f is “Big-Omega of g”. Notice that the definitio of Big-Omega differs from
the definitio of Big-O only in that the last line has f (n) ≥ Cg(n) instead of f (n) ≤ Cg(n), as we had for
Big-O.
All of the Big-O examples from Section 3.1.1 are also Big-Omega examples. Specifically in
Example 3.1, the function f define by
f (n) = 2n 2 + 4n + 2, for n = 0, 1, 2, . . .
2 2
is (n ): for C = 2 and K = 0, f (n) > Cn for all n ≥ K . Also, for all of the code skeletons and methods
in Section 3.1.2, we can replace O with as a bound on worstTime(n).
Big-Omega notation is used less frequently than Big-O notation because we are usually more inter-
ested in providing an upper bound than a lower bound for worstTime(n) or averageTime(n). That is, “can’t
be any worse than” is often more relevant than “can’t be any better than.” Occasionally, knowledge of a the-
oretical lower bound can guide those trying to come up with an optimal algorithm. And in Chapter 11, we
establish the important result that for any comparison-based sort method, averageTime(n)—and therefore,
worstTime(n)—is (n log n).
A somewhat artificia example shows that Big-O and Big-Omega are distinct. Let f be the function
define by
f (n) = n, for n = 0, 1, 2, . . .
Clearly, f is O(n), and therefore, f is also O(n 2 ).
But f is not (n 2 ). And that same function f is clearly
(n), and therefore (1). But f is not O(1). In fact, the Big-Omega hierarchy is just the reverse of the
Big-O hierarchy in Figure 3.1. For example,
(n 2 ) ⊂ (n log n) ⊂ (n) ⊂ (1)
3.1 Estimating the Efficiency of Methods 117
In most cases the same function will serve as both a lower bound and an upper bound, and this leads us
to the definitio of Big-Theta:
Let g be a function that has non-negative integer arguments and returns a non-negative value for all
arguments. We define (g) to be the set of functions f such that for some positive constants C1 and
C2 , and some non-negative constant K,
C1 g(n) ≤ f(n) ≤ C2 g(n) for all n ≥ K.
The idea is that if f is (g), then eventually (that is, for all n ≥ K ), f (n) is bounded below by some
constant times g(n) and also bounded above by some constant times g(n). In other words, to say that a
function f is (g) is exactly the same as saying that f is both O(g) and (g). When we establish that a
function f is (g), we have “nailed down” the function f in the sense that f is, roughly, bounded above
by g and also bounded below by g.
As an example of Big-Theta, consider the function f define by
f (n) = 2n 2 + 4n + 2, for n = 0, 1, 2, . . .
We showed in Example 3.3 that f is O(n 2 ), and earlier in this section we showed that f is (n 2 ). We
conclude that f is (n 2 ).
For ease of reading, we adopt plain-English terms instead of Big-Theta notation for several families
of functions in the Big-Theta hierarchy. For example, if f is (n), we say that f is “linear in n”. Table 3.1
shows some English-language replacements for Big-Theta notation.
We prefer to use plain English (such as “constant,” “linear,” and “quadratic”) whenever possible.
But as we will see in Section 3.1.5, there will still be many occasions when all we specify is an upper
bound—namely, Big O—estimate.
Big-Theta English
worstTime(n)
quadratic in n
linear-logarithmic in n
linear in n
logarithmic in n
constant
n
FIGURE 3.3 The graphs of worstTime(n) for several families of functions
Figure 3.4 indicates why Big-Theta differences eventually dominate constant factors in estimating
the behavior of a function. For example, if n is suff ciently large, t1 (n) = n 2 /100 is much greater than
t2 (n) = 100 n log2 n. But the phrase “if n is suff ciently large” should be viewed as a warning. Note that
t1 is smaller than t2 for arguments less than 100,000. So whether Big-Theta (or Big-O or Big-Omega) is
relevant may depend on how large the size of your problem might get.
Figure 3.4 has a concrete example of the differences between several families in the Big-Theta
hierarchy. For a representative member of the family—expressed as a function of n —the time to execute
that many statements is estimated when n equals one billion.
Some of the differences shown in Figure 3.4 are worth exploring. For example, there is a huge
difference between the values of log2 n and n. In Chapter 10, we will study a data structure—the binary
search tree—for which averageTime(n) is logarithmic in n for inserting, removing, and searching, but
worstTime(n) is linear in n for those methods.
3.1 Estimating the Efficiency of Methods 119
FIGURE 3.4 Estimated time to execute a given number of statements for various functions of n when n =
1,000,000,000 and 1,000,000 statements are executed per second. For example, to execute n log2 n statements
takes approximately 7 hours
Another notable comparison in Figure 3.4 is between n log2 n and n 2 . In Chapter 11, on sort
methods, we will see tangible evidence of this difference. Roughly speaking, there are two categories
of sort methods: fast sorts, whose averageTime(n) is linear-logarithmic in n, and simple sorts, whose
averageTime(n) is quadratic in n.
All of the methods we have seen so far are polynomial-time methods. A polynomial-time method
is one for which worstTime(n) is O(n i ) for some positive integer i . For example, a method whose
worstTime(n) is (O n 2 ) is a polynomial-time method. Similarly, a method whose worstTime(n) is (O log n)
is polynomial-time because (O log n) ⊂ O (n).
When we try to develop a method to solve a given problem, we prefer polynomial-time methods
whenever possible. For some methods, their run time is so long it is infeasible to run the methods for large
values of n. Such methods are in the category of exponential-time methods. An exponential-time method is
one whose worstTime(n) is (x n ) for some real number x > 0. Then we say worstTime(n) is exponential
in n. For example, a method whose worstTime(n) is (2n ) is an exponential-time method. Chapter 5 has
an example of an exponential-time method, and Labs 7 and 9 have two more exponential-time methods.
As you might expect, a polynomial-time method cannot also be exponential-time (Concept Exercise 3.10).
The existence of exponential-time methods gives rise to an interesting question: For a given
exponential-time method, might there be a polynomial-time method to solve the same problem? In some
cases, the answer is no. An intractable problem is one for which any method to solve the problem is
an exponential-time method. For example, a problem that requires 2n values to be printed is intractable
because any method to solve that problem must execute at least (2n ) statements. The problem in
Chapter 5 for which an exponential-time method is supplied is an intractable problem. The problem in
Lab 9 is also intractable, but the problem in Lab 7 has a polynomial-time solution.
Lab 23 investigates the Traveling Salesperson Problem, for which the only known methods to solve
the problem are exponential-time methods. The most famous open question in computer science is whether
the Traveling Salesperson Problem is intractable. There may be a polynomial-time method to solve that
problem, but no one has found one, and most experts believe that no such method is possible.
If we are working on a single method only, it may be feasible to optimize that method’s
averageTime(n) and worstTime(n), with the intent of optimizing execution time. But for the management
of an entire project, it is usually necessary to strike a balance. The next section explores the relevance of
other factors, such as memory utilization and project deadlines.
3.1.5 Trade-Offs
In the previous section we saw how to estimate a method’s execution-time requirements. The same Big-O
(or Big-Omega or Big-Theta) notation can be used to estimate the memory requirements of a method.
Ideally, we will be able to develop methods that are both fast enough and small enough. But in the real
120 C H A P T E R 3 Analysis of Algorithms
world, we seldom attain the ideal. More likely, we will encounter one or more of the following obstacles
during programming:
1. The program’s estimated execution time may be longer than acceptable according to the performance
requirements. Performance requirements, when given, state the time and space upper-bounds for all
or part of a program.
2. The program’s estimated memory requirements may be larger than acceptable according to the per-
formance requirements. This situation frequently arises for hand-held devices.
3. The program may require understanding a technique about which the programmer is only vaguely
familiar. This may create an unacceptable delay of the entire project.
Often, a trade-off must be made: a program that reduces one of the three obstacles may intensify the other
two. For example, if you had to develop a project by tomorrow, you would probably ignore time and
space constraints and focus on understanding the problem well enough to create a project. The point is
that real-life programming involves hard decisions. It is not nearly enough that you can develop programs
that run. Adapting to constraints such as those mentioned above will make you a better programmer by
increasing your flexibility
We can incorporate eff ciency concerns into the correctness of a method by including performance
requirements in the method’s specificatio (but see Programming Exercise 3.5). For example, part of the
specificatio for the Quick Sort method in Chapter 11 is:
The worstTime (n) is O(n 2 ).
Then for a definitio of that method to be correct, worstTime(n) would have to be O(n 2 ). Recall that
the Big-O estimates provide upper bounds only. But the class developer is free to improve on the upper
bounds for average time or worst time. For example, there is a way to defin that sort method so that
worstTime(n) is linear-logarithmic in n.
We want to allow developers of methods the f exibility to improve the eff ciency of those methods
without violating the contract between users and developers. So any performance requirements in method
specification will be given in terms of upper-bounds (that is, Big-O) only. Here are three conventions
regarding the Big-O estimates in method specifications
0. If a class stores a collection of elements, then unless otherwise noted, the variable n refers to the
number of elements in the collection.
1. For many methods, worstTime(n) is O(1). If no estimate of worstTime(n) is given, you may assume
that worstTime(n) is O(1).
2. Often, averageTime(n) has the same Big-O estimate as worstTime(n), and then we will specify the
worstTime(n) estimate only. When they are different, we will specify both.
When we analyze the time (or space) efficienc of a specifi method definition we will determine lower
as well as upper bounds, so we will use Big-Theta notation—or the English-language equivalent: constant,
linear-in-n, and so on.
Up until now, we have separated concerns about correctness from concerns about efficiency Accord-
ing to the Principle of Data Abstraction, the correctness of code that uses a class should be independent
of that class’s implementation details. But the eff ciency of that code may well depend on those details. In
other words, the developer of a class is free—for the sake of efficienc —to choose any combination of
field and method definitions provided the correctness of the class’s methods do not rely on those choices.
For example, suppose a class developer can create three different versions of a class:
3.2 Run-Time Analysis 121
In most cases, the appropriate choice is B. Choosing C would violate the Principle of Data Abstraction
because the correctness of a program that uses C could depend on C’s fields
Big-O analysis provides a cross-platform estimate of the efficienc of a method. The following
section explores an execution-time tool for measuring eff ciency.
In multi-programming environments such as Windows, it is very difficu t to determine how long a single
task takes.
Why? Because there is so much going on behind the scenes, such as the maintaining the desktop clock,
executing a wait-loop until a mouse click occurs, and updating information from your mailer and browser.
At any given time, there might be dozens of such processes under control of the Windows Manager. And
each process will get a time slice of several milliseconds. The bottom line is that the elapsed time for a
task is seldom an accurate measure of how long the task took.
Another problem with seeking an exact measure of eff ciency is that it might take a very long
time—O(forever). For example, suppose we are comparing two sorting methods, and we want to determine
the average time each one takes to sort some collection of elements. The time may depend heavily on
the particular arrangement of the elements chosen. Because the number of different arrangements of n
distinct elements is n!, it is not feasible to generate every possible arrangement, run the method for each
arrangement, and calculate the average time.
Instead, we will generate a sample ordering that is in “no particular order.” The statistical concept
corresponding to “no particular order” is randomness. We will use the time to sort a random sample as an
estimate of the average sorting time. We start with a discussion of timing because, as we will see later,
one aspect of randomness depends on the result of a timing method.
3.2.1 Timing
To assist in the timing of methods, Java supplies nanoTime(), a static method in the System class of
java.lang. This method returns a long whose value is the number of nanoseconds—that is, billionths
of a second—elapsed since some f xed but arbitrary time. To estimate how much execution time a task
consumes, we calculate the time immediately before and immediately after the code for the task. The
difference in the two times represents the elapsed time. As noted previously, elapsed time is a very, very
crude estimate of the time the task consumed. The following code serves as a skeleton for estimating the
time expended by a method:
final String ANSWER_1 = "The elapsed time was ";
long startTime,
finishTime,
elapsedTime;
startTime = System.nanoTime();
This skeleton determines the elapsed time for the task in seconds, with fractional digits. For example,
if startTime has the value 885161724000 and finishTime has the value 889961724000, then
elapsedTime has the value 4800000000, that is, four billion and eight hundred million. Then
elapsedTime/NANO_FACTOR has the value 4.8 (seconds).
We will use the time to process a random sample of values as an estimate of the average processing
time. Section 3.2.2 contains an introduction to—or review of—the Random class, part of the package
java.util.
*
*/
public int nextInt (int n)
For example, a call to nextInt (100) will return a random integer in the range from 0 to 99, inclusive.
For another example, suppose we want to simulate the roll of a die. The value from one roll of a
die will be an integer in the range 1 . . . 6, inclusive. The call to nextInt (6) returns an int value in
the range from 0 to 5, inclusive, so we need to add 1 to that returned value. Here is the code to print out
that pseudo-random die roll:
Random die = new Random();
System.out.println (oneRoll);
The value calculated by the nextInt (int n) method depends on the seed it is given. The variable
seed is a private long fiel in the Random class. The initial value of seed depends on the constructor
called. If, as above, the Random object is created with the default constructor, then seed is initialized to
System.nanoTime(). The other form of the constructor has a long parameter, and seed is initialized
to the argument corresponding to that parameter. Each time the method nextInt (int n) is called,
the current value of the seed is used to determine the next value of the seed, which determines the int
returned by the method.
For example, suppose that two programs have
Random die = new Random (800);
This repeatability can be helpful when we want to compare the behavior of programs, as we will in
Chapters 5—15. In general, repeatability is an essential feature of the scientifi method.
If we do not want repeatability, we use the default constructor. Recall that the default constructor
initializes the seed to System.nanoTime().
Here are two other random-number generators in the Random class:
/**
* Returns a pseudo-random int in the range from Integer.MIN_VALUE to
* Integer.MAX_VALUE.
*
*
* @return a pseudo-random int in the range from Integer.MIN_VALUE to
* Integer.MAX_VALUE.
124 C H A P T E R 3 Analysis of Algorithms
*
*/
public int nextInt ()
/**
* Returns a pseudo-random double in the range from 0.0 (inclusive) to
* 1.0 (exclusive).
*
*/
public double nextDouble ()
The following program combines randomness and timing with repeated calls to the selectionSort
method of Section 3.1.2. The higher levels of the program—input, output, and exception handling—are
handled in the run( ) method. The randomTimer method generates an array of random integers, calls
the selectionSort method, and calculates the elapsed time. The randomTimer method is not unit-tested
because timing results will vary widely from computer to computer. The unit tests for selectionSort
and other sort methods are in the Chapter 11 directory of the website’s source code section.
import java.util.*;
long elapsedTime;
while (true)
{
try
{
System.out.print (INPUT_PROMPT);
int n = sc.nextInt();
if (n == SENTINEL)
break;
elapsedTime = randomTimer (n);
3.2 Run-Time Analysis 125
System.out.println (ANSWER_1 +
(elapsedTime / NANO_FACTOR) + ANSWER_2);
} // try
catch (Exception e)
{
System.out.println (e);
sc.nextLine();
} // catch
} // while
} // method run
/**
* Determines the elapsed time to sort a randomly generated array of ints.
*
* @param n – the size of the array to be generated and sorted.
*
* @return the elapsed time to sort the randomly generated array.
*
* @throws NegativeArraySizeException – if n is less than 0.
*
*/
public long randomTimer (int n)
{
Random r = new Random();
long startTime,
finishTime,
elapsedTime;
/**
* Sorts a specified array of int values into ascending order.
* The worstTime(n) is O(n * n).
*
* @param x – the array to be sorted.
*
*/
public static void selectionSort (int [ ] x)
{
126 C H A P T E R 3 Analysis of Algorithms
} // class TimingRandom
The number of iterations of the while loop is independent of n, so for the run() method, worstTime(n)
is determined by the estimate of worstTime(n) for randomTimer. The randomTimer method has a loop
to generate the array, and worstTime(n) for this generation is O(n). Then randomTimer calls selection
Sort. In Section 3.1.2, we showed that worstTime(n) for selectionSort is O(n 2 ). Since the number of
iterations is the same for any arrangement of the n elements, averageTime(n) is O(n 2 ). In fact, n 2 provides
a crude lower bound as well as a crude upper bound, so averageTime(n) is quadratic in n. Then we expect
the average run time—over all possible arrangements of n doubles—to be quadratic in n. As suggested in
Section 3.2, we use the elapsed time to sort n pseudo-random doubles as an approximation of the average
run time for all arrangements of n doubles.
The elapsed time gives further credence to that estimate: for n = 50000, the elapsed time is 19.985
seconds, and for n = 100000, the elapsed time is 80.766 seconds. The actual times are irrelevant since
they depend on the computer used, but the relative times are significant when n doubles, the elapsed
time quadruples (approximately). According to Section 3.1.4 on growth rates, that ratio is symptomatic of
quadratic time.
Randomness and timing are also combined in the experiment in Lab 4: You are given the unreadable
(but runnable) bytecode versions of the classes instead of source code.
SUMMARY
Big-O notation allows us to quickly estimate an upper Big- notation (when the upper-bound and lower-bound
bound on the time/space efficienc of methods. Because are roughly the same).
Big-O estimates allow function arguments to be arbitrarily Run-time analysis allows methods to be tested on
large integers, we treat methods as algorithms by ignoring a specifi computer. But the estimates produced are often
the space requirements imposed by Java and a particular very crude, especially in a multiprogramming environ-
computing environment. In addition to Big-O notation, ment. Run-time tools include the nanoTime() method
we also looked at Big- notation (for lower bounds) and and several methods from the Random class.
Crossword Puzzle 127
CROSSWORD PUZZLE
1
2 3 4 5
6 7
10
www.CrosswordWeaver.com
ACROSS DOWN
7. The private long field in the Random class whose 1. The rule that states “In general, if during each
initial value depends on the constructor called. loop iteration, n is divided by some constant
greater than 1, worstTime(n) will be O(log n)
9. A finite sequence of explicit instructions to solve a for that loop.”
problem in a finite amount of time.
2. A problem for which for which any method to
10. A function of g that is both Big O of g and Big solve the problem is an exponential-time method
Omega of g is said to be _________ of g. is said to be ______________.
CONCEPT EXERCISES
3.1 Create a method, sample (int n), for which worstTime(n) is O(n) but worstTime(n) is not linear in n.
Hint: O(n) indicates that n may be (crudely) viewed as an upper bound, but linear-in-n indicates that n may
be (crudely) viewed as both an upper bound and a lower bound.
3.2 Study the following algorithm:
i = 0;
while (!a [i].equals (element))
i++;
Assume that a is an array of n elements and that there is at least one index k in 0 ... n - 1 such that a
[k].equals (element).
Use Big-O notation to estimate worstTime(n). Use Big- and Big- notation to estimate worstTime(n). In
plain English, estimate worstTime(n).
3.3 Study the following method:
/**
* Sorts a specified array of int values into ascending order.
* The worstTime(n) is O(n * n).
*
* @param x – the array to be sorted.
*
*/
public static void selectionSort (int [ ] x)
{
// Make x [0 ... i] sorted and <= x [i + 1] ... x [x.length -1]:
for (int i = 0; i < x.length - 1; i++)
{
int pos = i;
for (int j = i + 1; j < x.length; j++)
if (x [j] < x [pos])
pos = j;
int temp = x [i];
x [i] = x [pos];
x [pos] = temp;
} // for i
} // method selectionSort
a. For the inner for statement, when i = 0, j takes on values from 1 to n - 1, and so there are n - 1
iterations of the inner for statement when i = 0. How many iterations are there when i = 1? When
i = 2?
b. Determine, as a function of n, the total number of iterations of the inner for statement as i takes on
values from 0 to n – 2.
c. Use Big-O notation to estimate worstTime(n). In plain English, estimate worstTime(n)—the choices are
constant, logarithmic in n, linear in n, linear-logarithmic in n, quadratic in n and exponential in n.
3.4 For each of the following functions f , where n = 0, 1, 2, 3, . . . , estimate f using Big-O notation and plain
English:
Concept Exercises 129
a. f (n) = (2 + n) * (3 + log(n))
b. f (n) = 11 * log(n) + n/2 − 3452
c. f (n) = 1 + 2 + 3 +· · · + n
d. f (n) = n * (3 + n) − 7 * n
e. f (n) = 7 * n + (n − 1) * log (n − 4)
f. f (n) = log (n 2 )+ n
(n + 1) ∗
log(n + 1) − (n + 1) + 1
g. f (n) =
n
h. f (n) = n + n/2 + n/4 + n/8 + n/16 + · · ·
3.5 In the Order Hierarchy in Figure 3.1, we have . . . , O(log n), O(n 1/2 ), . . . . Show that, for integers n > 16,
log2 n < n 1/2 . Hint from calculus: Show that for all real numbers x > 16, the slope of the function log2 x
is less than the slope of the function x 1/2 . Since log2 (16) == 161/2 , we conclude that for all real numbers
x > 16, log2 x < x 1/2 .
3.6 For each of the following code segments, estimate worstTime(n) using Big notation or plain English. In
each segment, S represents a sequence of statements in which there are no n-dependent loops.
a. for (int i = 0; i * i < n; i++)
S
c. int k = 1;
for (int i = 0; i < n; i++)
k *= 2;
for (int i = 0; i < k; i++)
S
Hint: In each case, 2 is part of the answer.
3.7 .a. Suppose we have a method whose worstTime(n) is linear in n. Estimate the effect of tripling n on run
time—the actual time to execute the method in a particular computing environment. That is, estimate
runTime(3n) in terms of runTime(n).
b. Suppose we have a method whose worstTime(n) is quadratic in n. Estimate the effect of tripling n on
run time—the actual time to execute the method in a particular computing environment. That is, estimate
runTime(3n) in terms of runTime(n).
c. Suppose we have a method whose worstTime(n) is constant. Estimate the effect of tripling n on run
time—the actual time to execute the method in a particular computing environment. That is, estimate
runTime(3n) in terms of runTime(n).
3.8 This exercise proves that the Big-O families do not constitute a strict hierarchy. Consider the function f ,
define for all non-negative integers as follows:
n, if n is even;
f (n) =
0, if n is odd
Defin a function g on all non-negative integers such that f is not O(g) and g is not O(f ).
130 C H A P T E R 3 Analysis of Algorithms
3.9 Show that O(n) = O(n + 7). Hint: use the definitio of Big-O.
3.10 Show that if f (n) is polynomial in n, f (n) cannot be exponential in n.
3.11 Suppose, for some method, worstTime(n) = n n . Show that the method is an exponential-time method (that
is, worstTime(n) is (x n ) for some real number x > 1.0). But show that worstTime(n) is not (x n )—that is,
Big Theta of x n —for any real number x > 1.0.
3.12 This exercise illustrates some anomalies of (1).
a. Defin f (n) to be 0 for all n ≥ 0. Show that f is not (1), but f is (0).
b. Defin f (n) to be (n + 2)/(n + 1) for all n ≥ 0. Show that f is (1)—and so can be said to be
“constant”—even though f is not a constant function.
3.13 .a. Assume that worstTime(n) = C (statements) for some constant C and for all values of n ≥ 0. Determine
worstTime(2n) in terms of worstTime(n).
b. Assume that worstTime(n) = log2 n (statements) for all values of n ≥ 0. Determine worstTime(2n) in
terms of worstTime(n).
c. Assume that worstTime(n) = n (statements) for all values of n ≥ 0. Determine worstTime(2n) in terms of
worstTime(n).
d. Assume that worstTime(n) = n log2 n (statements) for all values of n ≥ 0. Determine worstTime(2n) in
terms of worstTime(n).
e. Assume that worstTime(n) = n 2 (statements) for all values of n ≥ 0. Determine worstTime(2n) in terms
of worstTime(n).
f. Assume that worstTime(n) = 2n (statements) for all values of n ≥ 0. Determine worstTime(n + 1) in terms
of worstTime(n). Determine worstTime(2n) in terms of worstTime(n).
3.14 If worstTime(n) is exponential in n for some method sample, which of the following must be true about
that method?
a. worstTime(n) is O(2n ).
b. worstTime(n) is (2n ).
c. worstTime(n) is (2n ).
d. worstTime(n) is O(n n ).
e. none of the above.
PROGRAMMING EXERCISES
3.1 In mathematics, the absolute value function returns a non-negative integer for any integer argument. Develop
a run method to show that the Java method Math.abs (int a) does not always return a non-negative
integer.
Hint: See Programming Exercise 0.1.
Programming Exercises 131
3.2 Assume that r is (a reference to) an object in the Random class. Show that the value of the following
expression is not necessarily in the range 0 . . . 99:
Math.abs (r.nextInt()) % 100
Hint: See Programming Exercise 3.1.
3.3 Develop a run method that initializes a Random object with the default constructor and then determines the
elapsed time for the nextInt() method to generate 123456789.
3.4 Suppose a method specificatio includes a Big-O estimate for worstTime(n). Explain why it would be impos-
sible to create a unit test to support the Big-O estimate.
3.5 In the binarySearch method in Section 3.1.2, the average of low and high was calculated by the following
expression
(low + high) > > 1
Compare that expression to
low + ((high - low) > > 1)
The two expressions are mathematically equivalent, and the f rst expression is slightly more effi ient, but will
return an incorrect result for some values of low and high. Find values of low and high for which the
firs expression returns an incorrect value for the average of low and high. Hint: The largest possible int
value is Integer.MAX_VALUE, approximately 2 billion.
For the sake of repeatability, the following system tests used a seed of 100 for the random-number generator.
System Test 1:
Please enter the number of times the game will be played: 10000
System Test 2:
Please enter the number of times the game will be played: 10000
Based on the output, what are your answers to the two questions given above?
Suppose, instead of working with three doors, the number of doors is input, along with the number of times
the game will be played. Hypothesize how likely it is that the always-switching contestant will win. Modify and
then run your project to confir or reject your hypothesis. (Keep hypothesizing, and modifying and running your
project until your hypothesis is confirmed.
Hint for Hypothesis: Suppose the number of doors is n, where n can be any positive integer greater than
2. For an always-switching contestant to win, the initial guess must be incorrect, and then the f nal guess must be
correct. What is the probability, with n doors, that the initial guess will be incorrect? Given that the initial guess is
incorrect, how many doors will the always-switching contestant have to choose from for the f nal guess (remember
that the announcer will eliminate one of those doors)? The probability that the always-switching contestant will
win is the probability that the initial guess is incorrect times the probability that the f nal guess is then correct.
The Java Collections CHAPTER 4
Framework
The Java Collections Framework is an assortment of related interfaces and classes in the package
java.util. For most of the classes in the Java Collections Framework, each instance is a collection,
that is, each instance is composed of elements. The collection classes can have type parameters, a new
feature of Java, so that a user can specify the type of the elements when declaring an instance of a
collection class. In this chapter, we will take a brief tour of the Java Collection Framework’s collection
classes, along with the new features that enhance the utilization of those classes.
CHAPTER OBJECTIVES
1. Understand what a collection is, and how contiguous collections differ from linked collections.
2. Be able to create and manipulate parameterized collections.
3. Identify several of the methods in the Collection interface.
4. Describe a design pattern in general, and the iterator design pattern in particular.
5. Compare the ArrayList and LinkedList implementations of the List interface.
6. Be able to utilize boxing/unboxing and the enhanced for statement.
4.1 Collections
A collection is an object that is composed of elements. The elements can be either values in a primitive
type (such as int) or references to objects. For a familiar example, an array is a collection of elements,
of the same type, that are stored contiguously in memory. Contiguous means “adjacent,” so the individual
elements are stored next to each other1 . For example, we can create an array of f ve String elements
(strictly speaking, each element is a reference to a String object) as follows:
String [ ] names = new String [5];
Here the new operator allocates space for an array of f ve String references, (each initialized to null by
the Java Virtual Machine), and returns a reference to the beginning of the space allocated. This reference
is stored in names.
1 Actually, all that matters is that, to a user of an array, the elements are stored as if they were contiguous, so an element can be accessed
directly from its index.
133
134 C H A P T E R 4 The Java Collections Framework
There is an important consequence of the fact that arrays are stored contiguously: an individual
element in an array can be accessed without firs accessing any of the other individual elements. For
example, names [2] can be accessed immediately—we need not access names [0] and names [1]
firs in order to reach names [2]. This random access property of arrays will come in handy in several
subsequent chapters. In each case we will need a storage structure in which an element can be accessed
quickly given its relative position, so an array will be appropriate in each case.
There are several drawbacks to arrays. First, the size of an array is f xed: Space for the entire array (of
primitive values or references) must be allocated before any elements can be stored in the array. If that size
is too small, a larger array must be allocated and the contents of the smaller array copied to the larger array.
Another problem with arrays is that the programmer must provide all the code for operating on an
array. For example, inserting and deleting in an array may require that many elements be moved. Suppose
an array’s indexes range from 0 to 999, inclusive, and there are elements stored in order in the locations at
indexes 0 to 755. To insert an element into the location with index 300, we must firs move the elements at
indexes 300 to 755 into the locations at indexes 301 to 756. Figure 4.1 shows the effect of such an insertion.
. .
. .
. .
999 999
FIGURE 4.1 Insertion in an array: to insert “Kalena” at index 300 in the array on the left, the elements at
indexes 300, 301, . . . , 756 must firs be moved, respectively, to indexes 301, 302, . . . , 757
In your programming career up to now, you have had to put up with the above disadvantages of
arrays. Section 4.1.1 describes an alternative that is almost always superior to arrays: instances of collection
classes.
primitive type are not objects, so we cannot create an instance of a collection class in which each element
is of type int. But for each primitive type, there is a corresponding class, called a wrapper class, whose
purpose is to enable a primitive type to be represented by (that is, wrapped inside) a class. For example,
there is an Integer class, and we can create an Integer object from an int j as follows:
new Integer (j)
The new operator returns a reference to an Integer object. Table 4.1 provides several important
conversions.
int i;
Integer myInt;
String s;
Object obj;
TO OBTAIN FROM EXAMPLE
The Java Collections Framework includes a number of collection classes that have wide applicability.
All of those collection classes have some common methods. For example, each collection class has an
isEmpty method whose method specificatio is:
/**
* Determines if this collection has no elements.
*
* @return true – if this collection has no elements.
*
*/
public boolean isEmpty()
Suppose myList is an instance of the collection class ArrayList, and myList has four elements. The
execution of
System.out.println (myList.isEmpty());
will produce output of
false
Of course, a method specificatio does not indicate how the method’s task will be accomplished. In
subsequent chapters, we will investigate some of the details for several collection classes. But we can now
introduce a simple classificatio of collection classes according to the way the elements are stored.
136 C H A P T E R 4 The Java Collections Framework
FIGURE 4.2 Part of a linked collection—a singly-linked list —in which each entry contains an element and a
reference to the next entry in the linked collection
FIGURE 4.3 Part of a linked collection—a doubly-linked list —in which each entry contains an element, a
reference to the previous entry and a reference to the next entry
Eric null
Jack nullnull
FIGURE 4.4 Part of a linked collection—a binary search tree—in which each entry contains an element and
references to three other entries
each of those classes has an isEmpty method. In fact, the definition of many of the methods are the same
in several classes. One of the unifying tools in the framework is the interface, which imposes method
headings on implementing classes. Section 4.2.1 introduces another, similar unifying tool: the abstract
class.
/**
* Returns a String object.
*
* @return a String object.
*
*/
public abstract String getClassName();
} // class Parent
138 C H A P T E R 4 The Java Collections Framework
An abstract class is denoted by the abstract modifie at the beginning of its declaration. And within
an abstract class, each abstract method’s heading must include the modifie abstract before the return
type, and a semicolon after the method heading. Because the Parent class lacks a definitio for one of
its methods, we cannot instantiate the Parent class. That is, we cannot defin a Parent object:
Parent p = new Parent (); // illegal because Parent is an abstract class
} // class Child1
} // class Child2
The main benefi of abstract methods is that they promote f exibility (define methods may be, but need
not be, overridden in subclasses) and consistency (abstract-class headings must be identical in subclasses).
For example, we can now do the following:
Parent p;
int code;
if (code == 1)
p = new Child1();
else
4.2 Some Details of the Java Collections Framework 139
p = new Child2();
System.out.println (p.getPrefix() + p.getClassName());
The variable p is a polymorphic reference, so the version of getClassName called depends on the
type— Child1 or Child2 —of the object referenced by p. The output will be “I am Child1” or “I am
Child2”, depending on the value of the variable code.
The Java Collections Framework has quite a few abstract classes: AbstractCollection,
AbstractList, AbstractSet, and others. Typically, one of these classes will declare as abstract any
method whose definitio depends on field in the subclasses, and defin any method whose definitio
does not depend on those f elds.
For now, a practical application of abstract classes is developed in Lab 5.
Here are a few more details on the relationship between interfaces, abstract classes and fully define
classes:
1. If a class implements some but not all of the methods in an interface, then the class would have to
be declared as an abstract class—and therefore cannot be instantiated.
2. An interface can extend one or more other interfaces. For example, we could have:
public interface Container extends Collection, Comparable
{...
Container has abstract methods of its own, and also inherits abstract methods from the interfaces
Collection and Comparable.
3. A class can extend at most one other class; by default, the Object class is the superclass of every
class. Multiple inheritance—the ability of a class to have more than one immediate superclass—is
illegal in Java. Multiple inheritance is illegal because of the danger of ambiguity. For example, view-
ing a teaching assistant as both a student and an employee, we could have a TeachingAssistant
class that is the immediate subclass of classes Student and StaffMember. Now suppose classes
Student and StaffMember each has its own getHolidays() method. If we define
TeachingAssistant teacher = new TeachingAssistant();
This feature, especially when combined with feature 3, allows us to come close to achieving multiple
inheritance. We can write:
class NewClass extends OldClass implements Interface1, Interface2
{
140 C H A P T E R 4 The Java Collections Framework
There is no ambiguity when a method is invoked because any methods in an interface are abstract, and
any non-final superclass method can be explicitly overridden —that is, re-define —in the subclass. For
example, suppose OldClass, Interface1, and Interface2 all have a writeOut() method, and we
have
NewClass myStuff = new NewClass();
...
myStuff.writeOut();
Which version of the writeOut method will be invoked? Certainly not the version from Interface1 or
Interface2, because those methods must be abstract. If NewClass implements a writeOut() method,
that is the one that will be invoked. Otherwise, the version of writeOut define in (or inherited by)
OldClass will be invoked.
Only elements of type Double can be inserted into gpaList; an attempt to insert a String or Integer
element will be disallowed by the compiler. As a result, you can be certain that any element retrieved
from gpaList will be of type Double.
Let’s see how elements can be inserted and retrieved from gpaList. In the ArrayList class, the
add method inserts the element argument at the end of the ArrayList object. For example,
gpaList.add (new Double (2.7));
will append to the end of gpaList a (reference to a) Double object whose double value is 2.7.
For retrievals, the get method returns the element in the ArrayList object at a specifie index. So
we can access the element at index 0 as follows:
Double gpa = gpaList.get (0);
Notice that we don’t need to cast the expression on the right-hand side to Double because the element at
index 0 of gpaList must be of type Double.
Now suppose we want to add that grade point average to a double variable sum, initialized to 0.0.
The method doubleValue() in the Double class returns the double value corresponding to the calling
Double object. The assignment to sum is
sum = sum + gpa.doubleValue();
commas. Typically, a parameterized type starts with a collection-class identifier and the element type is
enclosed in angle brackets. A parameterized type is sometimes called a “generic type”, and the language
feature permitting parameterized types is called “generics”.
Parameterized collection classes improve your productivity as a programmer. You don’t have to
remember what the element type of a collection is, because that type is specifie when the collection
is declared, as we did with ArrayList<Double>. If you make a mistake and try to insert an element
of type String for example, you will be notifie at compile-time. Without parameterized types, the
insertion would be allowed, but the assignment of (Double)gpaList.get(0) to gpa would generate a
ClassCastException at run time. And this exception, if uncaught, could crash a critical program.
In the previous example, the conversions from double to Double and from Double to double
are annoyances. To simplify your working with parameterized collection classes, the Java compiler auto-
matically translates primitive values into wrapper objects: the technical term is boxing. For example, the
insertion into gpaList can be accomplished as follows:
gpaList.add (2.7); // instead of gpaList.add (new Double (2.7));
Unboxing translates a wrapper object into its primitive value. For example, to increment the above double
variable sum by the value of the Double object gpa, we simply write
sum = sum + gpa; // instead of sum = sum + gpa.doubleValue();
Unboxing eliminates the need for you to invoke the doubleValue() method, and that makes your code
easier to read.
The general idea behind parameterized types and boxing/unboxing is to simplify the programmer’s
work by assigning to the compiler several tasks that would otherwise have to be performed by the
programmer.
Section 4.2.3 introduces the backbone of the Java Collections Framework: the Collection interface.
In this example, FullTimeEmployee is the actual class of the elements: the class that replaces the type
parameter E when the ArrayList class is instantiated.
142 C H A P T E R 4 The Java Collections Framework
E
<<interface>>
Collection
+ clear()
+ hashCode(): int
+ isEmpty(): boolean
+ iterator(): Iterator<E>
+ size(): int
+ toArray(): Object[ ]
FIGURE 4.5 The Collection interface. In UML, a type parameter—in this case, E —is shown in a dashed
rectangle in the upper-right-hand corner of the interface or class
If you wanted to, you could create your own class that fully implements the Collection interface.
That is, sort of, what happens in Lab 6. Only a few methods are realistically defined the others just throw
an exception. For example,
public int hashCode()
{
throw new UnsupportedOperationException();
}
Such definition satisfy the compiler, so the resulting class, ArrayCollection, is instantiable. That is,
we can create and initialize an ArrayCollection object:
ArrayCollection<Integer> collection = new ArrayCollection<Integer>();
4.2.3.1 Iterators
The Collection interface provides a core of methods useful for applications. But each application will
almost certainly have some specialized tasks that do not correspond to any method in the Collection
interface. Important Note: In the following examples, “Collection object” is shorthand for “object in
a class that implements the Collection interface”, and “Collection class” is shorthand for “class that
implements the Collection interface.”
1. Given a Collection object of students, print out each student who made the Dean’s List.
2. Given a Collection object of words, determine how many are four-letter words.
3. Given a Collection object of club members, update the dues owed for each member.
4. Given a Collection object of full-time employees, calculate the average salary of the employees.
Surely, we cannot create a class that would provide a method for any task in any application—the number
of methods would be limitless. But notice that in each of the four examples above, the task entails access-
ing each element in a Collection object. This suggests that we need to allow users of a Collection
class to be able to construct a loop that accesses each element in a Collection object. As we will
see when we look at classes that implement the Collection interface, developers can straightforwardly
construct such a loop. Why? Because a developer has access to the field in the class, so the devel-
oper knows how the class is organized. And that enables the developer to loop through each element in
the instance.
According to the Principle of Data Abstraction, a user’s code should not access the implementation
details of a Collection class. The basic problem is this: How can any implementation of the
Collection interface allow users to loop through the elements in an instance of that class without
violating the Principle of Data Abstraction? The solution is in the use of iterators. Iterators are objects
that allow the elements of Collection objects to be accessed in a consistent way without accessing the
field of the Collection class.
Inside each class that implements the Collection interface, there is an iterator class that allows
a user to access each element in the collection. Each iterator class must itself implement the following
Iterator interface:
public interface Iterator<E>
{
/**
* Determines if this Iterator object is positioned at an element in
* this Collection object.
*
* @return true – if this Iterator object is positioned at an element
* in this Collection object.
*
*/
boolean hasNext ();
/**
* Advances this Iterator object, and returns the element this
* Iterator object was positioned at before this call.
*
* @return the element this Iterator object was positioned at when
144 C H A P T E R 4 The Java Collections Framework
/**
* Removes the element returned by the most recent call to next().
* The behavior of this Iterator object is unspecified if the underlying
* collection is modified – while this iteration is in progress – other
* than by calling this remove() method.
*
* @throws IllegalStateException – if next() had not been called
* before this call to remove(), or if there had been an
* intervening call to remove() between the most recent
* call to next() and this call.
*
void remove ();
} // interface Iterator<E>
For each class that implements the Collection interface, its iterator class provides the methods for
traversing any instance of that Collection class. In other words, iterators are the behind-the-scenes
workhorses that enable a user to access each element in any instance of a Collection class.
How can we associate an iterator object with a Collection object? The iterator() method
in the Collection class creates the necessary connection. Here is the method specificatio from the
Collection interface:
/**
* Returns an Iterator object over this Collection object.
*
* @return an Iterator object over this Collection object.
*
*/
Iterator<E> iterator( );
The value returned is (a reference to) an Iterator object, that is, an object in a class that implements
the Iterator interface. With the help of this method, a user can iterate through a Collection object
For example, suppose that myColl is (a reference to) an instance of a Collection object with String
elements, and we want to print out each element in myColl that starts with the letter ‘a’. We f rst create
an iterator object:
Iterator<String> itr = myColl.iterator();
The variable itr is a polymorphic reference: it can be assigned a reference to an object in any class
that implements the Iterator<String> interface. And myColl.iterator() returns a reference to an
Iterator<String> object that is positioned at the beginning of the myColl object.
4.2 Some Details of the Java Collections Framework 145
Because of the two calls to itr.next(), if the next word returned during a loop iteration starts with the
letter ‘a’, the word after that word will be printed.
Very often, all we want to do during an iteration is to access each element in the collection. For such
situations, Java provides an enhanced for statement (sometimes referred to as a for-each statement). For
example, the previous (correct) iteration through myColl can be abbreviated to the following:
for (String word : myColl)
if (word.charAt (0) == 'a')
System.out.println (word);
The colon should be interpreted as “in”, so the control part of this for statement can be read “For each
word in myColl.” The effect of this code is the same as before, but some of the drudgery—creating and
initializing the iterator, and invoking the hasNext() and next() methods—has been relegated to the
compiler.
Here is a complete example of iterating over a Collection object by using an enhanced for
statement. You don’t have to know the details of ArrayList class, the particular implementation of the
Collection interface. You will learn those details in Chapter 6. For the sake of simplicity, Arithm
eticException and InputMismatchException are caught in the same catch block.
// Calculates the mean grade-point-average
import java.util.*;
MAX_GPA = 4.0,
SENTINEL = -1.0;
double oneGPA,
sum = 0.0;
while (true)
{
try
{
System.out.print (INPUT_PROMPT);
oneGPA = sc.nextDouble();
if (oneGPA == SENTINEL)
break;
if (oneGPA < MIN_GPA || oneGPA > MAX_GPA)
throw new ArithmeticException (RANGE_ERROR);
gpaList.add (oneGPA); // inserts at end of gpaList
} // try
catch (Exception e)
{
System.out.println (e + "\n");
sc.nextLine();
} // catch Exception
} // while
for (Double gpa : gpaList)
sum += gpa;
if (gpaList.size() > 0)
System.out.println (MESSAGE +
(sum / gpaList.size()));
else
System.out.println(NO_VALID_INPUT);
} // method run
} // class EnhancedFor
4.2 Some Details of the Java Collections Framework 147
The enhanced for statement simplifie your code, and that makes your programs easier to understand.
So you should use an enhanced for statement whenever possible, that is, if you were to use an iterator
instead, the only iterator methods invoked would be hasNext() and next(). You cannot use an enhanced
for statement if the collection may be modifie during the iteration. For example, if you wanted to delete,
from gpaList, each grade-point-average below 1.0, you would need to explicitly set up an iterator:
Iterator<Double> itr = gpaList.iterator();
while (itr.hasNext())
if (itr.next() < 1.0)
itr.remove();
2 The Stack class also implements the List interface with an underlying array, but the defi ition of a stack severely restricts access to the
{
public static void main (String[ ] args)
{
new RandomList ().run();
} // method main
// See if 22 is in randList:
if (randList.contains (22))
System.out.println ("Yes, 22 is in randList.");
else
System.out.println ("No, 22 is not in randList.");
} // class RandomList
4.2 Some Details of the Java Collections Framework 149
<<interface>> E
Collection
E
<<interface>>
List
E
AbstractList
E E E
ArrayList LinkedList Stack
FIGURE 4.6 Part of the Java Collections Framework hierarchy dealing with the List interface. In UML, an
abstract-class identifi r is italicized
The line
System.out.println (randList);
is equivalent to
System.out.println (randList.toString());
The toString method returns a String representation of randList. Every class in the Java Collections
Framework has a toString() method, so all the elements in an instance of one of those classes can be
output with a single call to println.
Because an ArrayList object stores its elements in an underlying array, when the element at index
6 is removed, each element at a higher index is moved to the location at the next lower index. So the
element that was at index 7 is then at index 6, the element that was at index 8 is then at index 7, and so
on. When a new element is inserted at index 5, each element located at that index or higher is moved to
the next higher index. So the element that was at index 5 is then at index 6, the element that was at index
6 is then at index 7, and so on.
The output is
[93, 70, 57, 97, 9, 20, 84, 12, 97, 65]
No, 22 is not in randList.
97 is at index 3
150 C H A P T E R 4 The Java Collections Framework
We could not use an enhanced for statement to iterate over randList because we needed to remove
some of that object’s elements, not merely access them.
In the program, randList is declared as a polymorphic reference and then immediately initialized
as a reference to an ArrayList object. To re-run the program with a LinkedList object, the only change
is the constructor call:
List<Integer> randList = new LinkedList<Integer>();
How do the two versions compare? Part of the program—printing the Integer at index 3—is executed
more quickly with an ArrayList object because of the random-access ability of the underlying array. And
part of it—removing all even Integer elements—is executed more quickly with a LinkedList object.
That’s because an entry in a linked list can be removed by adjusting links: no movement of elements is
needed. In general, there is no “best” implementation of the List interface.
SUMMARY
A collection is an object that is composed of elements. To simplify the programmer’s work of inserting
The elements may be stored contiguously, that is, at con- elements into an instance of a parameterized class,
secutive locations in memory. Another option is a linked Java automatically boxes primitive values into the corre-
structure, in which each element is stored in a special sponding wrapper elements. Similarly, wrapper elements
object called an entry that also includes a reference to retrieved from a parameter-class instance are automati-
another entry. cally unboxed into the corresponding primitive value. A
A collection class is a class of which each instance further simplifi ation of Java is the enhanced for state-
is a collection. The Java Collections Framework, part ment, which automates most of the routine code to access
of the package java.util, includes a number of collec- each element during an iteration.
tion classes that have wide applicability. Each of those The Collection interface consists of 15 method
classes can be parameterized , which means that the ele- specifica ions for accessing and manipulating an instance
ment class is specifie when the collection-class object of a class that implements the Collection interface.
is created. And for any instance of one of those classes, The List interface adds several index-related
an iterator can be defined An iterator is an object that methods to the Collection interface. The List inter-
allows an instance of a collection class to loop through face is partially implemented by the AbstractList
the elements in that class without violating the Principle class, and fully implemented by the ArrayList and
of Data Abstraction. LinkedList classes.
Crossword Puzzle 151
CROSSWORD PUZZLE
1 2
4 5
10
www.CrosswordWeaver.com
ACROSS DOWN
5. Objects that allow the elements of Collection objects 1. A class in which each instance is a collection
to be accessed in a consistent way without accessing the of elements.
fields of the Collection class.
2. The translation, by the compiler, of a wrapper
9. A class or interface identifier followed, in angle brackets, object into its primitive value.
by a list of one or more class identifiers separated by
commas. 3. A generic programming technique that can be
applied in a variety of situations.
10. A class whose purpose is to enable a primitive type to be
represented by (that is, wrapped inside) a class. 4. The property by which an individual element in
an array can be accessed without first accessing
any of the other individual elements.
CONCEPT EXERCISES
4.1 What is a collection? What is a collection class? What is a Collection class? Give an example of a collection
that is not an instance of a collection class. Programming Project 4.1 has an example of a collection class that
is not a Collection class.
4.2 An array is a collection, even though there is no array class. But an array of objects can be converted into an
instance of the ArrayList class. Look in the f le Arrays.java in the package java.util to determine the generic
algorithm (that is, static method) that converts an array of objects into an ArrayList of those objects. How
can that ArrayList then be printed without a loop?
4.3 .a. Identify each of the following as either an interface or a class:
Collection
LinkedList
Iterator
AbstractList
PROGRAMMING EXERCISES
4.1 For each of the following, create and initialize a parameterized instance, add two elements to the instance, and
then print out the instance:
a. an ArrayList object, scoreList, of Integer objects;
b. a LinkedList object, salaryList, of Double objects;
4.2 Develop a main method in which two ArrayList objects are created, one with String elements and one
with Integer elements. For each list, add three elements to the list, remove the element at index 1, add an
element at index 0, and print out the list.
4.3 Find an ArrayList method, other than a constructor, that is not also a method in the LinkedList class.
Find a LinkedList method, other than a constructor, that is not also a method in the ArrayList class.
4.4 Suppose we have the following:
LinkedList<String> team = new LinkedList<String> ();
team.add ("Garcia");
Iterator<String> itr = team.iterator();
Integer player = itr.next ();
What error message will be generated? When (at compile-time or at run-time)? Test your hypotheses.
4.5 Use the ArrayList class three times. First, create an ArrayList object, team1, with elements of type
String. Add three elements to team1. Second, create team2, another ArrayList object with elements of
type String. Add four elements to team2. Finally, create an ArrayList object, league, whose elements
are ArrayList objects in which each element is of type String. Add team1 and team2 to league.
Programming Exercises 153
/**
* Initializes this Sequence object to be empty, with an initial capacity of ten
* elements.
*
*/
public Sequence()
/**
* Initializes this Sequence object to be empty, with a specified initial
* capacity.
*
* @param capacity – the initial capacity of this Sequence object.
*
* @throw IllegalArgumentException – if capacity is non-positive.
*
*/
public Sequence (int n)
/**
* Returns the number of elements in this Sequence object.
*
* @return the number of elements in this Sequence object.
*
*/
public int size()
/**
* Appends a specified element to this Sequence object.
*
* @param element – the element to be inserted at the end of this
* Sequence object.
*
*/
public void append (E element)
/**
* Returns the element at a specified index in this Sequence object.
* The worstTime(n) is constant, where n is the number of elements in this
* Sequence object.
*
* @param k – the index of the element returned.
*
* @return the element at index k in this Sequence object.
*
* @throws IndexOutOfBoundsException – if k is either negative or greater
* than or equal to the number of elements in this Sequence
* Sequence object.
*
*/
/**
* Changes the element at a specified index in this Sequence object.
* The worstTime(n) is constant, where n is the number of elements in this
* Sequence object.
*
* @param k – the index of the element returned.
* @param newElement – the element to replace the element at index k in
* this Sequence object.
*
* @throws IndexOutOfBoundsException – if k is either negative or greater
* than or equal to the number of elements in this Sequence
* object.
*
*/
public void set (int k, E newElement)
Part 1 Create unit tests based on the method specification and stubs.
protected E [ ] data;
protected int size; // the number of elements in the Sequence, not the
// capacity of the data array
Note 1: for the append method, if the data array is currently full, its capacity must be increased before the new
element can be appended. See Programming Exercise 2.10 to see how to accomplish the expansion.
Note 2: for methods that may throw an exception, do not include catch blocks. Instead, the exception will be
propagated, so the handling can be customized for the application.
One of the skills that distinguish a novice programmer from an experienced one is an understanding
of recursion. The goal of this chapter is to give you a feel for situations in which a recursive method
is appropriate. Along the way you may start to see the power and elegance of recursion, as well as its
potential for misuse. Recursion plays a minor role in the Java Collections Framework: two of the sort
methods are recursive, and there are several recursive methods in the TreeMap class. But the value of
recursion extends far beyond these methods. For example, one of the applications of the Stack class
in Chapter 8 is the translation of recursive methods into machine code. The sooner you are exposed to
recursion, the more likely you will be able to spot situations where it is appropriate—and to use it.
CHAPTER OBJECTIVES
1. Recognize the characteristics of those problems for which recursive solutions may be
appropriate.
2. Compare recursive and iterative methods with respect to time, space, and ease of development.
3. Trace the execution of a recursive method with the help of execution frames.
4. Understand the backtracking design pattern.
5.1 Introduction
Roughly, a method is recursive if it contains a call to itself.1 From this description, you may initially fear
that the execution of a recursive method will lead to an infinit sequence of recursive calls. But under
normal circumstances, this calamity does not occur, and the sequence of calls eventually stops. To show
you how recursive methods terminate, here is the skeleton of the body of a typical recursive method:
if (simplest case)
solve directly
else
make a recursive call with a simpler case
This outline suggests that recursion should be considered whenever the problem to be solved has these
two characteristics;
1. The simplest case(s) can be solved directly.
2. Complex cases of the problem can be reduced to simpler cases of the same form as the original
problem.
1
A formal def nition of “recursive” is given later in this chapter.
155
156 C H A P T E R 5 Recursion
Incidentally, if you are familiar with the Principle of Mathematical Induction, you may have observed that
these two characteristics correspond to the base case and inductive case, respectively. In case you are not
familiar with that principle, Section A2.5 of Appendix 2 is devoted to mathematical induction.
As we work through the following examples, do not be inhibited by old ways of thinking. As each
problem is stated, try to frame a solution in terms of a simpler problem of the same form. Think recursively!
5.2 Factorials
Given a positive integer n, the factorial of n, written n!, is the product of all integers between n and 1,
inclusive. For example,
∗ ∗ ∗
4! = 4 3 2 1 = 24
and
∗ ∗ ∗ ∗ ∗
6! = 6 5 4 3 2 1 = 720
Another way to calculate 4! is as follows:
∗
4! = 4 3!
This formulation is not helpful unless we know what 3! is. But we can continue to calculate factorials in
terms of smaller factorials (Aha!):
∗
3! = 3 2!
∗
2! = 2 1!
Note that 1! Can be calculated directly; its value is 1. Now we work backwards to calculate 4!:
∗ ∗
2! = 2 1! = 2 1=2
∗ ∗
3! = 3 2! = 3 2=6
Finally, we get
∗ ∗
4! = 4 3! = 4 6 = 24
For n > 1, we reduce the problem of calculating n! to the problem of calculating (n − 1)!. We stop
reducing when we get to 1!, which is simply 1. For the sake of completeness2 , we defin 0! to be 1.
There is a fina consideration before we specify, test and defin the factorial method: what about
exceptions? If n is less than zero, we should throw an exception— IllegalArgumentException is
appropriate. And because n! is exponential in n, the value of n! will be greater than Long.MAX_VALUE
for not-very-large values of n. In fact, 21!> Long.MAX_VALUE, so we should also throw IllegalArgum
entException for n > 20.
Here is the method specification
/**
* Calculates the factorial of a non-negative integer, that is, the product of all
* integers between 1 and the given integer, inclusive. The worstTime(n) is O(n),
2 The calculation of 0! occurs in the study of probability: The number of combinations of n things taken k at a time is calculated as n!/(k ! (n
− k )!). When n = k , we get n!/(n!) (0!), which has the value 1 because 0! = 1. And note that 1 is the number of combinations of n things
taken n at a time.
5.2 Factorials 157
Note that factorial has a static modifie in its heading (see Section 2.1 in Chapter 2). Why? All
the information needed by the method is provided by the parameter, and the only effect of a call to the
method is the value returned. So a calling object would neither affect nor be affected by an invocation
of the method. As noted in Chapter 2, we adhere to the test-firs model. So the test class, based on the
method specificatio only, is developed before the method itself is defined Here is the test class, with
special emphasis on boundary conditions, and including the usual stub within the test class itself:
import org.junit.*;
import static org.junit.Assert.*;
import org.junit.runner.Result;
import static org.junit.runner.JUnitCore.runClasses;
import java.util.*;
@Test
public void factorialTest1()
{
assertEquals (24, factorial (4));
} // method factorialTest1
@Test
public void factorialTest2()
{
assertEquals (1, factorial (0));
} // method factorialTest2
@Test
158 C H A P T E R 5 Recursion
@Test
public void factorialTest4()
{
assertEquals (2432902008176640000L, factorial (20));
} // method factorialTest4
} // class FactorialTest
final String ERROR_MESSAGE = "The value of n must be >= 0 and <= " +
Integer.toString (MAX_INT);
/**
* Calculates n!.
*
* @param n the integer whose factorial is calculated.
*
* @return n!.
*
*/
protected static long fact (int n)
{
if (n <= 1)
return 1;
return n * fact (n - 1);
} // method fact
The testing of the wrapper method, factorial, incorporates the testing of the wrapped method, fact.
The book’s website has similar test classes for all recursive functions in this chapter.
Within the method fact, there is a call to the method fact, and so fact, unlike factorial, is a
recursive method. The parameter n has its value reduced by 1 with each recursive call. But after the f nal
call with n = 1, the previous values of n are needed for the multiplications. For example, when n = 4,
the calculation of n * fact (n - 1) is postponed until the call to fact (n - 1) is completed. When
this finall happens and the value 6 (that is, fact (3)) is returned, the value of 4 for n must be available
to calculate the product.
Somehow, the value of n must be saved when the call to fact (n - 1) is made. That value must
be restored after the call to fact (n - 1) is completed so that the value of n * fact (n - 1) can be
calculated. The beauty of recursion is that the programmer need not explicitly handle these savings and
restorings; the compiler and computer do the work.
Step 0:
n = 4
Frame 0
return 4 * fact(3);
Step 1:
n = 3
Frame 1
return 3 * fact(2);
n = 4
Frame 0
return 4 * fact(3);
Step 2:
n = 2
Frame 2
return 2 * fact(1);
n = 3
Frame 1
return 3 * fact(2);
n = 4
Frame 0
return 4 * fact(3);
Step 3:
n = 1
Frame 3
return 1;
1
n = 2
Frame 2
return 2 * fact(1);
n = 3
Frame 1
return 3 * fact(2);
n = 4
Frame 0
return 4 * fact(3);
5.2 Factorials 161
Step 4:
n = 2
Frame 2
return 2 *(1);
2
n = 3
Frame 1
return 3 * fact(2);
n = 4
Frame 0
return 4 * fact(3);
Step 5:
n = 3
Frame 1
return 3 * 2;
6
n = 4
Frame 0
return 4 * fact(3);
Step 6:
n = 4
Frame 0
return 4 * 6;
24
Recursion can often make it easier for us to solve problems, but any problem that can be solved
recursively can also be solved iteratively. An iterative method is one that has a loop instead of a recursive
call. For example, here is an iterative method to calculate factorials. No wrapper method is needed because
there are no recursive calls.
/**
* Calculates the factorial of a non-negative integer, that is, the product of all
* integers between 1 and the given integer, inclusive. The worstTime(n) is O(n),
* where n is the given integer.
*
* @param n the non-negative integer whose factorial is calculated.
*
* @return the factorial of n
*
* @throws IllegalArgumentException if n is less than 0 or greater than 20 (note
* that 21! > Long.MAX_VALUE).
162 C H A P T E R 5 Recursion
*
*/
public static long factorial (int n)
{
final int MAX_INT = 20; // because 21! > Long.MAX_VALUE
final String ERROR_MESSAGE = "The value of n must be >= 0 and <= " +
Integer.toString (MAX_INT);
long product = n;
if (n == 0)
return 1;
for (int i = n-1; i > 1; i–)
product = product * i;
return product;
} // method factorial
This version of factorial passed all of the tests in FactorialTest. For this version of factorial,
worstTime(n) is linear in n, the same as for the recursive version. But no matter what value n has, only
three variables (n, product and i) are allocated in a trace of the iterative version, so worstSpace(n)
is constant, versus linear in n for the recursive version. Finally, the iterative version follows directly
from the definitio of factorials, whereas the recursive version represents your firs exposure to a new
problem-solving technique, and that takes some extra effort.
So in this example, the iterative version of the factorial method is better than the recursive
version. The whole purpose of the example was to provide a simple situation in which recursion was
worth considering, even though we ultimately decided that iteration was better. In the next example, an
iterative alternative is slightly less appealing.
*
* @param n the non-negative integer, in decimal notation.
*
* @return a String representation of the binary equivalent of n.
*
* @throws IllegalArgumentException if n is negative.
*/
public static String getBinary (int n)
The test class for this method can be found on the book’s website, and includes the following test:
@Test (expected = IllegalArgumentException.class)
public void getBinaryTest5()
{
getBinary (-1);
} // method getBinaryTest5
There are several approaches to solving this problem. One of them is based on the following
observation:
The rightmost bit has the value of n % 2 ; the other bits are the binary equivalent of n/2. (Aha!)
For example, if n is 12, the rightmost bit in the binary equivalent of n is 12% 2, namely, 0; the remaining
bits are the binary equivalent of 12/2, that is, the binary equivalent of 6. So we can obtain all the bits as
follows:
12 / 2 = 6; 12 % 2 = 0
6 / 2 = 3; 6 % 2 = 0
3 / 2 = 1; 3 % 2 = 1
When the quotient is 1, the binary equivalent is simply 1. We concatenate (that is, join together) these bits
from the bottom up, so that the rightmost bit will be joined last. The result would then be
1100
The following table graphically illustrates the effect of calling getBinary (12):
1
1
0
0
164 C H A P T E R 5 Recursion
This discussion suggests that we must perform all of the calculations before we return the result. Speaking
recursively, we need to calculate the binary equivalent of n/2 before we append the value of n % 2. In
other words, we need to append the result of n % 2 to the result of the recursive call.
We will stop when n is 1 or 0, and 0 will occur only if n is initially 0. As we did in Section 5.2,
we make getBinary a wrapper method for the recursive getBin method. The method definition are:
public static String getBinary (int n)
{
if (n < 0)
throw new IllegalArgumentException();
return getBin (n);
} // method getBinary
We are assured that the simple case of n <= 1 will eventually be reached because in each execution of
the method, the argument to the recursive call is at most half as big as the method parameter’s value.
Here is a step-by-step, execution-frame trace of the getBin method after an initial call of getBin
(12):
The f nal value returned is the string:
1100
And that is the binary equivalent of 12.
As we noted earlier, the order of operands in the String expression of the last return statement
in getBin enables us to postpone the f nal return until all of the bit values have been calculated. If the
order had been reversed, the bits would have been returned in reverse order. Recursion is such a powerful
tool that the effects of slight changes are magnified
Step 0:
n = 12
Frame 0
return getBin (6) + Integer.toString (0);
Step 1:
n = 6
Frame 1
return getBin (3) + Integer.toString (0);
n = 12
Frame 0
return getBin (6) + Integer.toString (0);
5.3 Decimal to Binary 165
Step 2:
n = 3
Frame 2
return getBin (1) + Integer.toString (1);
n = 6
Frame 1
return getBin (3) + Integer.toString (0);
n = 12
Frame 0
return getBin (6) + Integer.toString (0);
Step 3:
n = 1
return “1”; Frame 3
“1”
n = 3
Frame 2
return getBin (1) + Integer.toString (1);
n = 6
Frame 1
return getBin (3) + Integer.toString (0);
n = 12
Frame 0
return getBin (6) + Integer.toString (0);
Step 4:
n = 3
Frame 2
return “1” + Integer.toString (1);
“11”
n = 6
Frame 1
return getBin (3) + Integer.toString (0);
166 C H A P T E R 5 Recursion
n = 12
Frame 0
return getBin (6) + Integer.toString (0);
Step5:
n = 6
Frame 1
return “11” + Integer.toString (0);
“110”
n = 12
Frame 0
return getBin (6) + Integer.toString (0);
Step 6:
n = 12
Frame 0
return “110” + Integer.toString (0);
“1100”
As usually happens with recursive methods, the time and space requirements for getBin are esti-
mated by the number of recursive calls. The number of recursive calls is the number of times that n can
be divided by 2 until n equals 1. As we saw in Section 3.1.2 of Chapter 3, this value is floo (log2 n), so
worstTime(n) and worstSpace(n) are both logarithmic in n.
A user can call the getBinary method from the following run method (in a BinaryUser class
whose main method simply calls new BinaryUser().run()):
int n;
while (true)
{
try
{
5.4 Towers of Hanoi 167
System.out.print (INPUT_PROMPT);
n = sc.nextInt();
if (n == SENTINEL)
break;
System.out.println (RESULT_MESSAGE + getBinary (n));
} // try
catch (Exception e)
{
System.out.println (e);
sc.nextLine();
}// catch Exception
}// while
} // method run
You are invited to develop an iterative version of the getBinary method. (See Programming Exercise 5.2.)
After you have completed the iterative method, you will probably agree that it was somewhat harder to
develop than the recursive method. This is typical, and probably obvious: recursive solutions usually flo
more easily than iterative solutions to those problems for which recursion is appropriate. Recursion is
appropriate when larger instances of the problem can be reduced to smaller instances that have the same
form as the larger instances.
For the next problem, an iterative solution is much harder to develop than a recursive solution.
1
2
3
4
A B C
FIGURE 5.1 The starting position for the Towers of Hanoi game with four disks
The object of the game is to move all of the disks from pole ‘A’ to pole ‘B’; pole ‘C’ is used for
temporary storage3 . The rules of the game are:
1. Only one disk may be moved at a time.
2. No disk may ever be placed on top of a smaller disk.
3. Other than the prohibition of rule 2, the top disk on any pole may be moved to either of the other
two poles.
3 In some versions, the goal is to move the disks from pole ‘A’ to pole ‘C’, with pole ‘B’ used for temporary storage.
168 C H A P T E R 5 Recursion
1
2
4 3
A B C
FIGURE 5.2 The game configuratio for the Towers of Hanoi just before moving disk 4 from pole ‘A’ to pole
‘B’
We will solve this problem in the following generalization: Show the steps to move n disks from an origin
pole to a destination pole, using the third pole as a temporary. Here is the method specificatio for this
generalization:
/**
* Determines the steps needed to move n disks from an origin to a destination.
* The worstTime(n) is O(2n ).
*
* @param n the number of disks to be moved.
* @param orig the pole where the disks are originally.
* @param dest the destination pole
* @param temp the pole used for temporary storage.
*
* @return a String representation of the moves needed, where each
* move is in the form "Move disk ? from ? to ?\n".
*
* @throws IllegalArgumentException if n is less than or equal to 0.
*/
public static String moveDisks (int n, char orig, char dest, char temp)
The test class for moveDisks can be found on the book’s website, and includes the following test:
@Test
public void moveDisksTest1()
{
assertEquals ("Move disk 1 from A to B\nMove disk 2 from A to C\n" +
"Move disk 1 from B to C\nMove disk 3 from A to B\n" +
"Move disk 1 from C to A\nMove disk 2 from C to B\n" +
"Move disk 1 from A to B\n", moveDisks (3, 'A', 'B', 'C'));
} // method moveDisksTest1
Let’s try to play the game with the initial configuratio given in Figure 5.1. We are immediately faced
with a dilemma: Do we move disk 1 to pole ‘B’ or to pole ‘C’? If we make the wrong move, we may
end up with the four disks on pole ‘C’ rather than on pole ‘B’.
Instead of trying to figur out where disk 1 should be moved initially, we will focus our attention
on disk 4, the bottom disk. Of course, we can’t move disk 4 right away, but eventually, disk 4 will have
to be moved from pole ‘A’ to pole ‘B’. By the rules of the game, the configuratio just before moving
disk 4 must be as shown in Figure 5.2.
Does this observation help us to f gure out how to move 4 disks from ‘A’ to ‘B’? Well, sort of. We
still need to determine how to move three disks (one at a time) from pole ‘A’ to pole ‘C’. We can then
move disk 4 from ‘A’ to ‘B’. Finally, we will need to determine how to move three disks (one at a time)
from ‘C’ to ‘B’.
5.4 Towers of Hanoi 169
The significanc of this strategy is that we have reduced the problem from f guring how to move four
disks to one of figurin how to move three disks. (Aha!) We still need to determine how to move three
disks from one pole to another pole.
But the above strategy can be re-applied. To move three disks from, say, pole ‘A’ to pole ‘C’, we
firs move two disks (one at a time) from ‘A’ to ‘B’, then we move disk 3 from ‘A’ to ‘C’, and f nally,
we move two disks from ‘B’ to ‘C’. Continually reducing the problem, we eventually face the trivial task
of moving disk 1 from one pole to another.
There is nothing special about the number 4 in the above approach. For any positive integer n we
can describe how to move n disks from pole ‘A’ to pole ‘B’: if n = 1, we simply move disk 1 from pole
‘A’ to pole ‘B’. For n > 1,
1. First, move n − 1 disks from pole ‘A’ to pole ‘C’, using pole ‘B’ as a temporary.
2. Then move disk n from pole ‘A’ to pole ‘B’.
3. Finally, move n − 1 disks from pole ‘C’ to pole ‘B’, using pole ‘A’ as a temporary.
This does not quite solve the problem because, for example, we have not described how to move n − 1
disks from ‘A’ to ‘C’. But our strategy is easily generalized by replacing the constants ‘A’, ‘B’, and ‘C’
with variables origin, destination, and temporary. For example, we will initially have
origin = 'A'
destination = 'B'
temporary = 'C'
Then the general strategy for moving n disks from origin to destination is as follows:
If n is 1, move disk 1 from origin to destination.
Otherwise,
1. Move n − 1 disks (one at a time) from origin to temporary;
2. Move disk n from origin to destination;
3. Move n − 1 disks (one at a time) from temporary to destination.
The following recursive method incorporates the above strategy for moving n disks. If n = 1, the String
representing the move, namely, "Move disk 1 from " + orig + " to " + dest + "\n" is simply
returned. Otherwise, the String object returned consists of three String objects concatenated together,
namely, the strings returned by
move (n - 1, orig, temp, dest)
"Move disk " + n + " from " + orig + " to " + dest + "\n"
move (n - 1, temp, dest, orig)
When the f nal return is made, the return value is the complete sequence of moves. This String object
can then be printed to the console window, to a GUI window, or to a file For the sake of efficiency the
test for n ≤ 0 is made—once—in a wrapper method moveDisks that calls the move method. Here is the
method specificatio for moveDisks:
/**
* Determines the steps needed to move disks from an origin to a destination.
* The worstTime(n) is O(2n ).
*
* @param n the number of disks to be moved.
170 C H A P T E R 5 Recursion
The test class for moveDisks can be found on the book’s website. The definition of moveDisks and
move are as follows:
public static String moveDisks (int n, char orig, char dest, char temp)
{
if (n <= 0)
throw new IllegalArgumentException();
return move (n, orig, dest, temp);
} // method moveDisks
/**
* Determines the steps needed to move disks from an origin to a destination.
* The worstTime(n) is O(2n ).
*
* @param n the number of disks to be moved.
* @param orig the pole where the disks are originally.
* @param dest the destination pole
* @param temp the pole used for temporary storage.
*
* @return a String representation of the moves needed.
*
*/
public static String move (int n, char orig, char dest, char temp)
{
final String DIRECT_MOVE =
"Move disk " + n + " from " + orig + " to " + dest + "\n";
if (n == 1)
return DIRECT_MOVE;
String result = move (n - 1, orig, temp, dest);
result += DIRECT_MOVE;
result += move (n - 1, temp, dest, orig);
return result;
} // method move
It is diff cult to trace the execution of the move method because the interrelationship of parameter and
argument values makes it diff cult to keep track of which pole is currently the origin, which is the
destination and which is the temporary. In the following execution frames, the parameter values are the
argument values from the call, and the argument values for subsequent calls come from the method code
and the current parameter values. For example, suppose the initial call is:
move (3, 'A', 'B', 'C');
5.4 Towers of Hanoi 171
Then the parameter values at step 0 will be those argument values, so we have:
n = 3
orig = 'A'
dest = 'B'
temp = 'C'
The values of those arguments are obtained from the parameters’ values, so the statements are
equivalent to:
String result = move (2, 'A', 'C', 'B');
result = "Move disk 3 from A to B\n";
result = move (2, 'C', 'B', 'A');
return result;
Make sure you understand how to obtain the parameter values and argument values before you try to
follow the trace given below.
Here is a step-by-step, execution-frame trace of the move method when the initial call is:
move (3, 'A', 'B', 'C');
value of result
Step 0:
n = 3
orig = 'A'
dest = 'B'
temp = 'C'
Step 1:
n = 2
orig = 'A'
dest = 'C'
temp = 'B'
n = 3
orig = 'A'
dest = 'B'
temp = 'C'
Step 2:
n = 1
orig = 'A'
dest = 'B'
temp = 'C'
n = 2
orig = 'A'
dest = 'C'
temp = 'B'
Move disk 1 from A to B
String result = move (1, 'A', 'B', 'C');
result += "Move disk 2 from A to C\n";
result += move (1, 'B', 'C', 'A');
return result;
n = 3
orig = 'A'
dest = 'B'
temp = 'C'
Step 3:
n = 2
orig = 'A'
dest = 'C'
temp = 'B'
Move disk 1 from A to B
String result = move (1, 'A', 'B', 'C'); Move disk 2 from A to C
result += "Move disk 2 from A to C\n";
result += move (1,'B','C','A');
return result;
5.4 Towers of Hanoi 173
n = 3
orig = 'A'
dest = 'B'
temp = 'C'
Step 4:
n = 2
orig = 'A'
dest = 'C'
temp = 'B'
n = 3
orig = 'A'
dest = 'C'
temp = 'B'
Step 5:
n = 1
orig = 'B'
dest = 'C'
temp = 'A'
n = 2
orig = 'A'
dest = 'C'
temp = 'B' Move disk 1 from A to B
Move disk 2 from A to C
String result = move (1, 'A', 'B', 'C'); Move disk 1 from B to C
result += "Move disk 2 from A to C\n";
result += move (1,'B','C','A');
return result;
174 C H A P T E R 5 Recursion
n = 3
orig = 'A'
dest = 'B'
temp = 'C'
Step 6:
n = 3
orig = 'A'
dest = 'B' Move disk 1 from A to B
temp = 'C'
Move disk 2 from A to C
String result = move (2, 'A', 'C', 'B'); Move disk 1 from B to C
result += "Move disk 3 from A to B\n"; Move disk 3 from A to B
result += move (2, 'C', 'B', 'A');
return result;
Step 7:
n = 3
orig = 'A'
dest = 'B'
temp = 'C'
Step 8:
n = 2
orig = 'C'
dest = 'B'
temp = 'A'
n = 3
orig = 'A'
dest = 'B'
temp = 'C'
Step 9:
n = 1
orig = 'C'
dest = 'A'
temp = 'B'
n = 2
orig = 'C'
dest = 'B' Move disk 1 from A to B
temp = 'A' Move disk 2 from A to C
Move disk 1 from B to C
String result = move (1, 'C', 'A', 'B'); Move disk 3 from A to B
result += "Move disk 2 from C to B\n"; Move disk 1 from C to A
result += move (1, 'A', 'B', 'C');
return result;
n = 3
orig = 'A'
dest = 'B'
temp = 'C'
Step 10:
n = 2
orig = 'C'
Move disk 1 from A to B
dest = 'B'
temp = 'A' Move disk 2 from A to C
Move disk 1 from B to C
String result = move (1, 'C', 'A', 'B'); Move disk 3 from A to B
result += "Move disk 2 from C to B\n"; Move disk 1 from C to A
result += move (1,'A','B','C'); Move disk 1 from C to B
return result;
n = 3
orig = 'A'
dest = 'B'
temp = 'C'
Step 11:
n = 2
orig = 'C'
dest = 'B'
temp = 'A'
n = 3
orig = 'A'
dest = 'B'
temp = 'C'
Step 12:
n = 1
orig = 'A'
dest = 'B'
temp = 'C'
n = 2
orig = 'C' Move disk 1 from A to B
dest = 'B' Move disk 2 from A to C
temp = 'A' Move disk 1 from B to C
Move disk 3 from A to B
String result = move (1, 'C', 'A', 'B'); Move disk 1 from C to A
result += "Move disk 2 from C to B\n";
Move disk 2 from C to B
result += move (1,'A','B','C');
return result; Move disk 1 from A to B
n = 3
orig = 'A'
dest = 'B'
temp = 'C'
Notice the disparity between the relative ease in developing the recursive method and the rela-
tive difficult in tracing its execution. Imagine what it would be like to trace the execution of move
(15,'A','B','C'). Fortunately, you need not undergo such torture. Computers handle this type of
tedious detail very well. You “merely” develop the correct program and the computer handles the execu-
tion. For the move method—as well as for the other recursive methods in this chapter—you can actually
prove the correctness of the method. See Exercise 5.5.
The significanc of ensuring the precondition (see the @throws specification is illustrated in the
move method. For example, let’s see what would happen if no exception were thrown and move were
called with 0 as the firs argument. Since n would have the value 0, the condition of the if statement would
be false, and there would be a call to move (-1,...). Within that call, n would still be unequal to 1, so
there would be a call to move (-2,...) then to move (-3,....), move (-4,...), move (-5,...),
and so on. Eventually, saving all those copies of n would overflo an area of memory called the stack . This
phenomenon known is as infinit recursion. A StackOverflowError —not an exception—is generated,
and the execution of the project terminates. In general, infinit recursion is avoided if each recursive call
makes progress toward a “simplest” case. And, just to be on the safe side, the method should throw an
exception if the precondition is violated.
A recursive method does not explicitly describe the considerable detail involved in its execution.
For this reason, recursion is sometimes referred to as “the lazy programmer’s problem-solving tool.” If
you want to appreciate the value of recursion, try to develop an iterative version of the move method.
Programming Project 5.1 provides some hints.
move (n-2, ...) move (n-2, ...) move (n-2, ...) move (n-2, ...)
argument. From each of those calls, we get two more calls to move, and each of those four calls has n - 2
as the f rst argument. This process continues until, finally we get calls with 1 as the f rst argument.
To calculate the total number of calls to the move method, we augment the tree in Figure 5.3 by
identifying levels in the tree, starting with level 0 at the top, and include the number of calls at each level.
At level 0, the number of calls is 1(= 20 ). At level 1, the number of calls is 2 (= 21 ). In general, at level
k there are 2k calls to the move method. Because there are n levels in the tree and the top is level 0, the
bottom must be level n − 1, where there are 2n−1 calls to the move method. See Figure 5.4.
From Figure 5.4, we see that the total number of calls to the move method is
n−1
20 + 21 + 22 + 23 + ... + 2n−1 = 2k
k =0
By Example A2.6 in Appendix 2, this sum is equal to 2n − 1. That is, the number of calls to the move
method is 2n − 1. We conclude that, for the move method, worstTime(n) is exponential in n; specifically
worstTime(n) is (2n ). In fact, since any definitio of the move method must return a string that has
2n − 1 lines, the Towers of Hanoi problem is intractable. That is, any solution to the Towers of Hanoi
problem must take exponential time.
The memory requirements for move are modest because although space is allocated when move is
called, that space is deallocated when the call is completed. So the amount of additional memory needed
for move depends, not simply on the number of calls to move, but on the maximum number of started-
but-not-completed calls. We can determine this number from the execution frames. Each time a recursive
call is made, another frame is constructed, and each time a return is made, that frame is destroyed. For
example, if n = 3 in the original call to move, then the maximum number of execution frames is 3. In
general, the maximum number of execution frames is n. So worstSpace(n) is linear in n.
We now turn our attention to a widely known search technique: binary search. We will develop a
recursive method to perform a binary search on an array. Lab 9 deals with the development of an iterative
version of a binary search.
level # of calls
move (n, ...) 0 20
move (n-2, ...) move (n-2, ...) move (n-2, ...) move (n-2, ...) 2 22
FIGURE 5.4 The relationship between level and number of calls to the move method in the tree from Figure 5.3
5.5 Searching an Array 179
For example, the String class implements the Comparable<String> interface, so we can write the
following:
String s = "elfin";
The output will be greater than 0 because “elfin is lexicographically greater than “elastic”; in other
words, “elfin comes after “elastic” according to the Unicode values of the characters in those two strings.
Specifically the ‘f” in “elfin comes after the ‘a’ in “elastic”.
The simplest way to conduct the search is sequentially: start at the f rst location, and keep checking
successively higher locations until either the element is found or you reach the end of the array. This
search strategy, known as a sequential search, is the basis for the following generic algorithm (that is,
static method):
/**
* Determines whether an array contains an element equal to a given key.
* The worstTime(n) is O(n).
*
* @param a the array to be searched.
* @param key the element searched for in the array a.
*
* @return the index of an element in a that is equal to key, if such an element
* exists; otherwise, -1.
*
* @throws ClassCastException, if the element class does not implement the
* Comparable interface.
180 C H A P T E R 5 Recursion
*
*/
public static int sequentialSearch (Object[ ] a, Object key)
{
for (int i = 0; i < a.length; i++)
if (((Comparable) a [i]).compareTo (key) == 0)
return i;
return -1;
} // sequentialSearch
Because the element type of the array parameter is Object, the element type of the array argument can be
any type. But within the sequentialSearch method, the compiler requires that a [i] must be cast to
a type that implements the Comparable<Object> interface. For the sake of simplicity, we use the “raw”
type Comparable instead of the equivalent Comparable<Object>.
The sequentialSearch method is not explicitly included in the Java Collections Framework. But
it is the basis for several of the method definition in the ArrayList and LinkedList classes, which are
in the framework.
For an unsuccessful sequential search of an array, the entire array must be scanned. So both
worstTime(n) and averageTime(n) are linear in n for an unsuccessful search. For a successful sequen-
tial search, the entire array must be scanned in the worst case. In the average case, assuming each location
is equally likely to house the element sought, we probe about n/2 elements. We conclude that for a
successful search, both worstTime(n) and averageTime(n) are also linear in n.
Can we improve on these times? Definitely In this section we will develop an array-based search
technique for which worstTime(n) and averageTime(n) are only logarithmic in n. And in Chapter 14,
we will encounter a powerful search technique—hashing—for which averageTime(n) is constant, but
worstTime(n) is still linear in n.
Given an array to be searched and a value to be searched for, we will develop a binary search, so
called because the size of the region searched is divided by two at each stage until the search is completed.
Initially, the firs index in the region is index 0, and the last index is at the end of the array. One important
restriction is this: A binary search requires that the array be sorted .
We assume, as above, that the array’s element class implements the Comparable interface.
Here is the method specification identical to one in the Arrays class in the package java.util:
/**
* Searches the specified array for the specified object using the binary
* search algorithm. The array must be sorted into ascending order
* according to the <i>natural ordering</i> of its elements (as by
* <tt>Sort(Object[ ]</tt>), above) prior to making this call. If it is
* not sorted, the results are undefined. If the array contains multiple
* elements equal to the specified object, there is no guarantee which
* one will be found. The worstTime(n) is O(log n).
*
* @param a the array to be searched.
* @param key the value to be searched for.
*
* @return index of the search key, if it is contained in the array;
* otherwise, <tt>(-(<i>insertion point</i>) - 1)</tt>. The
* <i>insertion point</i> is defined as the point at which the
* key would be inserted into the array: the index of the first
* element greater than the key, or <tt>a.length</tt>, if all
* elements in the array are less than the specified key. Note
5.5 Searching an Array 181
In javadoc, the html tag <tt> signifie code, <i> signifie italics, and > signifie the greater than
symbol, ‘>’. The symbol ‘>’ by itself would be interpreted as part of an html tag. The “#” in one of the
@see lines creates a link to the given sort method in the document generated through javadoc; that line
expands to
See Also:
sort(Object[])
The BinarySearchTest class is available from the book’s website, and includes the following test (names
is the array of String elements from Figure 5.5):
@Test
public void binarySearchTest6()
{
assertEquals (-11, binarySearch (names, "Joseph"));
} // method binarySearchTest6
For the sake of utilizing recursion, we will focus on the firs and last indexes in the region being searched.
Initially, first = 0 and last = a.length - 1. So the original version of binarySearch will be a
wrapper that simply calls
return binarySearch (a, 0, a.length - 1, key);
For definin this version of the binarySearch method, the basic strategy is this: We compare the element
at the middle index of the current region to the key sought. If the middle element is less than the key, we
recursively search the array from the middle index + 1 to index last. If the middle element is greater
than the key, we recursively search the array from index first to the middle index − 1. If the middle
element is equal to the key, we are done.
Assume, for now, that first <= last. Later on we’ll take care of the case where first > last.
Following the basic strategy given earlier, we start by f nding the middle index:
int mid = (first + last) >> 1;
The right-hand-side expression uses the right-shift bitwise operator, >>, to shift the binary representation
of (first + last) to the right by 1. This operation is equivalent to, but executes faster than
int mid = (first + last) / 2;
182 C H A P T E R 5 Recursion
The middle element is at index mid in the array a. We need to compare (the element referenced by) a
[mid] to (the element referenced by) key. The compareTo method is ideal for the comparison, but that
method is not define in the element class, Object. Fortunately, the compareTo method is define in any
class that implements the Comparable interface. So we cast a [mid] to a Comparable object and then
call the method compareTo:
Comparable midVal = (Comparable)a [mid];
int comp = midVal.compareTo (key);
If the result of this comparison is <0, perform a binary search on the region from mid + 1 to last and
return the result of that search. That is:
if (comp < 0)
return binarySearch (a, mid + 1, last, key);
Otherwise, if comp > 0, perform a binary search on the region from first to mid - 1 and return the
result. That is,
if (comp > 0)
return binarySearch (a, first, mid - 1, key);
Ada a [0]
Ben a [1]
Carol a [2]
Dave a [3]
Ed a [4]
Frank a [5]
Gerri a [6]
Helen a [7]
Iggy a [8]
Joan a [9]
FIGURE 5.5 The state of the program at the beginning of the method called binarySearch (names, 0, 9,
"Frank"). The parameter list is Object[ ] a, int first, int last and Object key. (For simplicity, we
pretend that names is an array of Strings rather than an array of references to Strings)
5.5 Searching an Array 183
The middle element, “Ed”, is less than “Frank”, so we perform a binary search of the region from
mid + 1 to last. The call is
binarySearch (a, mid + 1, last, key);
The parameter first gets the value of the argument mid + 1. During this execution of binarySearch,
the assignment
mid = (first + last) >> 1;
gives mid the value (5 + 9)/2, which is 7, so midVal is “Helen”. See Figure 5.6.
The middle element, “Helen”, is greater than “Frank”, so a binary search is performed on the region
from indexes 5 through 6. The call is
binarySearch (a, first, mid - 1, key);
The parameter last gets the value of the argument mid - 1. During this execution of binarySearch,
the assignment
mid = (first + last) >> 1;
gives mid the value (5 + 6)/2, which is 5, so the middle element is “Frank”. See Figure 5.7.
Success! The middle element is equal to key, so the value returned is mid, the index of the middle
element.
The only unresolved issue is what happens if the array does not have an element equal to key. In
that case, we want to return -insertion Point - 1, where insertionPoint is the index where key
could be inserted without disordering the array. The reason we don’t return -insertionPoint is that we
would have an ambiguity if insertionPoint were equal to 0: a return of 0 could be interpreted as the
index where key was found.
How can we determine what value to give insertionPoint? If first > last initially, we must
have an empty region, with first = 0 and last = -1, so insertionPoint should get the value of
Ada a [0]
Ben a [1]
Carol a [2]
Dave a [3]
Ed a [4]
Frank a [5]
Gerri a [6]
Helen a [7]
Iggy a [8]
Joan a [9]
FIGURE 5.6 The state of the program at the beginning of the binary search for “Frank” in the region from
indexes 5 through 9
184 C H A P T E R 5 Recursion
Ada a [0]
Ben a [1]
Carol a [2]
Dave a [3]
Ed a [4]
Frank a [5]
Gerri a [6]
Helen a [7]
Iggy a [8]
Joan a [9]
FIGURE 5.7 The state of the program at the beginning of the binary search for “Frank” in the region from
indexes 5 through 6
first. Otherwise we must have first <= last during the firs call to binarySearch. Whenever
first <= last at the beginning of a call to binarySearch, we have
first <= mid <= last
But any element with an index less than first must be less than key, and any element with an index
greater than last must be greater than key, so when we f nish, first is the smallest index of any element
greater than key. That is where key should be inserted.
5.5 Searching an Array 185
Here is a BinarySearchUser class that allows an end-user to enter names for which a given array will
be searched binarily:
public class BinarySearchUser
{
public static void main (String[ ] args)
{
new BinarySearchUser ().run();
} // method main
final String NOT_FOUND = "That name was not found, but could be " +
"inserted at index ";
String name;
int index;
System.out.println (ARRAY_MESSAGE);
186 C H A P T E R 5 Recursion
while (true)
{
System.out.print (INPUT_PROMPT);
name = sc.next();
if (name.equals(SENTINEL))
break;
index = binarySearch (names, 0, names.length - 1, name);
if (index >= 0)
System.out.println (FOUND + index);
else
System.out.println (NOT_FOUND + (-index - 1));
} // while
} // method run
public static int binarySearch(Object[ ] a, int first, int last, Object key)
{
if (first <= last)
{
int mid = (first + last) >> 1;
Comparable midVal = (Comparable)a [mid];
int comp = midVal.compareTo (key);
if (comp < 0)
return binarySearch (a, mid + 1, last, key);
if (comp > 0)
return binarySearch (a, first, mid - 1, key);
return mid; // key found
} // if first <= last
return -first - 1; // key not found; belongs at a[first]
} // method binarySearch
} // class BinarySearchUser
Here is a step-by-step, execution-frame trace of the binarySearch method after an initial call of
binarySearch (names, 0, 9, "Dan");
Step 0:
first = 0
last = 9
key = “Dan” Frame 0
mid = 4
midVal = “Ed”
comp is > 0
Step 1:
first = 0
last = 3
key = “Dan”
mid = 1 Frame 1
midVal = “Ben”
comp is < 0
first = 0
last = 9
key = “Dan” Frame 0
mid = 4
m idVal = “Ed”
comp is > 0
Step 2:
first = 2
last = 3
key = “Dan”
mid = 2 Frame 2
midVal = “Carol”
comp is < 0
first = 0
last = 3
key = “Dan” Frame 1
mid = 1
midVal = “Ben”
comp is < 0
first = 0
last = 9
key = “Dan”
mid = 4 Frame 0
midVal = “Ed”
comp is > 0
Step 3:
a = [“Ada”, “Ben”, “Carol”, “Dave”, “Ed”,”Frank”,
“Gerri”, “Helen”, “Iggy”, “Joan”]
first = 3
last = 3
key = “Dan” Frame 3
mid = 3
m idVal = “Dave”
comp is > 0
first = 2
last = 3
key = “Dan”
mid = 2 Frame 2
midVal = “Carol”
comp is < 0
first = 0
last = 3
key = “Dan” Frame 1
mid = 1
midVal = “Ben”
comp is < 0
first = 0
last = 9
key = “Dan”
mid = 4 Frame 0
midVal = “Ed”
comp is > 0
Step 4:
a = [“Ada”, “Ben”, “Carol”, “Dave”, “Ed”,”Frank”,
“Gerri”, “Helen”, “Iggy”, “Joan”]
first = 3
last = 2 Frame 4
key = “Dan”
return −3−1;
−4
first = 3
last = 3
key = “Dan”
mid = 3 Frame 3
midVal = “Dave”
comp is > 0
−4
first = 2
last = 3
key = “Dan” Frame 2
mid = 2
midVal = “Carol”
comp is < 0
−4
first = 0
last = 3
key = “Dan”
mid = 1 Frame 1
midVal = “Ben”
comp is < 0
−4
first = 0
last = 9
key = “Dan”
mid = 4 Frame 0
midVal = “Ed”
comp is > 0
−4
How long does the binarySearch method take? We need to make a distinction between an unsuc-
cessful search, in which the element is not found, and a successful search, in which the element is found.
We start with an analysis of an unsuccessful search.
During each execution of the binarySearch method in which the middle element is not equal to
key, the size of the region searched during the next execution is, approximately, halved. If the element
sought is not in the array, we keep dividing by 2 as long as the region has at least one element. Let n
represent the size of the region. The number of times n can be divided by 2 until n = 0 is logarithmic in
n —this is, basically, the Splitting Rule from Chapter 3. So for a failed search, worstTime(n) is logarithmic
in n. Since we are assuming the search is unsuccessful, the same number of searches will be performed
in the average case as in the worst case, so averageTime(n) is logarithmic in n for a failed search.
The worst case for a successful search requires one less call to the binarySearch method than the
worst case (or average case) for an unsuccessful search. So for a successful search, worstTime(n) is still
logarithmic in n. In the average case for a successful search, the analysis—see Concept Exercise 5.15—is
more complicated, but the result is the same: averageTime(n) is logarithmic in n.
During each call, a constant amount of information is saved: the entire array is not saved, only
a reference to the array. So the space requirements are also logarithmic in n, for both successful and
unsuccessful searches and for both the worst case and the average case.
5.6 Backtracking 191
In the Arrays class of the java.util package, there is an iterative version of the binary search
algorithm. In Lab 8, you will conduct an experiment to compare the time to recursively search an array
of int s, iteratively search an array of int s, and iteratively search an array of Integer s. Which of the
three do you think will be slowest?
Lab 9 introduces another recursive method whose development is far easier than its iterative coun-
terpart. The method for generating permutations is from Roberts’ delightful book, Thinking Recursively
[Roberts, 1986].
Section 5.6 deals with another design pattern (a general strategy for solving a variety of problems):
backtracking. You have employed this strategy whenever you had to re-trace your steps on the way to
some goal. The BackTrack class also illustrates the value of using interfaces.
5.6 Backtracking
The basic idea with backtracking is this: From a given starting position, we want to reach a goal position.
We repeatedly choose, maybe by guessing, what our next position should be. If a given choice is valid—that
is, the new position might be on a path to the goal—we advance to that new position and continue. If a
choice leads to a dead end, we back up to the previous position and make another choice. Backtracking
is the strategy of trying to reach a goal by a sequence of chosen positions, with a re-tracing in reverse
order of positions that cannot lead to the goal.
For example, look at the picture in Figure 5.8. We start at position P0 and we want to fin a path to
the goal state, P14. We are allowed to move in only two directions: north and west. But we cannot “see”
any farther than the next position. Here is a strategy: From any position, we firs try to go north; if we
are unable to go north, we try to go west; if we are unable to go west, we back up to the most recent
position where we chose north and try to choose west instead. We never re-visit a position that has been
discovered to be a dead end. The positions in Figure 5.8 are numbered in the order they would be tried
according to this strategy.
Figure 5.8 casts some light on the phrase “re-tracing in reverse order.” When we are unable to go
north or west from position P4, we firs back up to position P3, where west is not an option. So we back
up to P2. Eventually, this leads to a dead end, and we back up to P1, which leads to the goal state.
When a position is visited, it is marked as possibly being on a path to the goal, but this marking
must be undone if the position leads only to a dead end. That enables us to avoid re-visiting any dead-end
position. For example, in Figure 5.8, P5 is not visited from P8 because by the time we got to P8, P5 had
already been recognized as a dead end.
We can now refin our strategy. To try to reach a goal from a given position, enumerate over all
positions directly accessible from the given position, and keep looping until either a goal has been reached
or we can no longer advance to another position. During each loop iteration, get the next accessible
position. If that position may be on a path to a goal, mark that position as possibly leading to a goal and,
if it is a goal, the search has been successful; otherwise, attempt to reach a goal from that position, and
mark the position as a dead end if the attempt fails.
192 C H A P T E R 5 Recursion
P15 (GOAL)
P14 P4
P13 P7 P3
P12 P6 P5 P2
P11 P10 P9 P8 P1
P0
FIGURE 5.8 Backtracking to obtain a path to a goal. The solution path is P0, P1, P8, P9, P10, P11, P12, P13,
P14, P15
Make sure you have a good understanding of the previous paragraph before you proceed. That
paragraph contains the essence of backtracking. The rest of this section and Section 5.6.1 are almost
superfluou by comparison.
Instead of developing a backtracking method for a particular application, we will utilize a generalized
backtracking algorithm from Wirth [1976, p.138]. We then demonstrate that algorithm on a particular
application, maze searching. Four other applications are left as programming projects in this chapter.
And Chapter 15 has another application of backtracking: a programming project for searching a network.
Backtracking is a design pattern because it is a generic programming technique that can be applied in a
variety of contexts.
The BackTrack class below is based on one in Noonan [2000]. The details of the application
class will be transparent to the BackTrack class, which works through an interface, Application. The
Application interface will be implemented by the particular application.
A user (of the BackTrack class) supplies:
• the class implementing the Application interface (note: to access the positions available from a
given position, the iterator design-pattern is employed, with a nested iterator class);
• a Position class to defin what “position” means for this application;
The Application methods are generalizations of the previous outline of backtracking. Here is the Appli
cation interface:
import java.util.*;
/**
* Indicates that a given position is possibly on a path to a goal.
*
* @param pos the position that has been marked as possibly being on a
* path to a goal.
*/
void markAsPossible (Position pos);
/**
* Indicates whether a given position is a goal position.
*
* @param pos the position that may or may not be a goal position.
*
* @return true if pos is a goal position; false otherwise.
*/
boolean isGoal (Position pos);
/**
* Indicates that a given position is not on any path to a goal position.
*
* @param pos the position that has been marked as not being on any path to
* a goal position.
*/
void markAsDeadEnd (Position pos);
/**
* Converts this Application object into a String object.
*
* @return the String representation of this Application object.
*/
String toString();
/**
* Produces an Iterator object that starts at a given position.
*
* @param pos the position the Iterator object starts at.
*
* @return an Iterator object that accesses the positions directly
* available from pos.
*/
Iterator<Position> iterator (Position pos);
} // interface Application
The BackTrack class has two responsibilities: to initialize a BackTrack object from a given application
object, and to try to reach a goal position from a given position. The method specification are
/**
* Initializes this BackTrack object from an application.
194 C H A P T E R 5 Recursion
*
* @param app the application
*/
public BackTrack (Application app)
/**
* Attempts to reach a goal through a given position.
*
* @param pos the given position.
*
* @return true if the attempt succeeds; otherwise, false.
*/
public boolean tryToReachGoal (Position pos)
The only f eld needed is (a reference to) an Application. The definitio of the constructor is straightfor-
ward. The definitio of the tryToReachGoal method is based on the outline of backtracking given above:
To “enumerate over all positions accessible from the given position,” we create an iterator. The phrase
“attempt to reach a goal from that position” becomes a recursive call to the method tryToReachGoal.
The complete BackTrack class, without any application-specifi information, is as follows:
import java.util.*;
/**
* Initializes this BackTrack object from an application.
*
* @param app the application
*/
public BackTrack (Application app)
{
this.app = app;
} // constructor
/**
* Attempts to reach a goal through a given position.
*
* @param pos the given position.
*
* @return true if the attempt succeeds; otherwise, false.
*/
public boolean tryToReachGoal (Position pos)
{
Iterator<Position> itr = app.iterator (pos);
while (itr.hasNext())
{
5.6 Backtracking 195
pos = itr.next();
if (app.isOK (pos))
{
app.markAsPossible (pos);
if (app.isGoal (pos) || tryToReachGoal (pos))
return true;
app.markAsDeadEnd (pos);
} // pos may be on a path to a goal
} // while
return false;
} // method tryToReachGoal
} // class BackTrack
Let’s focus on the tryToReachGoal method, the essence of backtracking. We look at the possible choices
of moves from the pos parameter. There are three possibilities:
1. One of those choices is a goal position. Then true is returned to indicate success.
2. One of those choices is valid but not a goal position. Then another call to tryToReachGoal is made,
starting at the valid choice.
3. None of the choices is valid. Then the while loop terminates and false is returned to indicate
failure to reach a goal position from the current position.
The argument to tryToReachGoal represents a position that has been marked as possibly being on a
path to a goal position. Whenever a return is made from tryToReachGoal, the pre-call value of pos is
restored, to be marked as a dead end if it does not lead to a goal position.
Now that we have developed a framework for backtracking, it is straightforward to utilize this
framework to solve a variety of problems.
1 1 1 0 1 1 0 0 0 1 1 1 1
1 0 1 1 1 0 1 1 1 1 1 0 1
1 0 0 0 1 0 1 0 1 0 1 0 1
1 0 0 0 1 1 1 0 1 0 1 1 1
1 1 1 1 1 0 0 0 0 1 0 0 0
0 0 0 0 1 0 0 0 0 0 0 0 0
0 0 0 0 1 1 1 1 1 1 1 1 1
FIGURE 5.9 A maze: 1 represents a corridor and 0 represents a wall. Assume the starting position is in the
upper left-hand corner, and the goal position is in the lower right-hand corner
196 C H A P T E R 5 Recursion
A successful traversal of this maze will show a path leading from the start position to the goal
position. We mark each such position with the number 9. Because there are two possible paths through
this maze, the actual path chosen will depend on how the iterator class orders the possible choices. For
the sake of specificity assume the order of choices is north, east, south, and west. For example, from the
position at coordinates (5, 8), the firs choice would be (4, 8), followed by (5, 9), (6, 8), and (5, 7).
From the initial position at (0, 0), the following positions are recorded as possibly being on a
solution-path:
(0, 1) // moving east
(0, 2) // moving east
(1, 2) // moving south
(1, 3) // moving east
(1, 4) // moving east
(0, 4) // moving north
(0, 5) // moving east;
This last position is a dead end, so we “undo” (0, 5) and (0, 4), backtrack to (1, 4) and then record the
following as possibly leading to the goal:
(2, 4) // moving south
(3, 4) // moving south
(3, 5) // moving east;
From here we eventually reach a dead end. After we undo (3, 5) and re-trace to (3, 4), we advance—without
any further backtracking—to the goal position. Figure 5.10 uses 9’ to show the corresponding path through
the maze of Figure 5.9, with dead-end positions marked with 2’s.
For this application, a position is simply a pair: row, column. The Position class is easily developed:
public class Position
{
protected int row,
column;
/**
* Initializes this Position object to (0, 0).
*/
public Position ()
{
row = 0;
9 9 9 0 2 2 0 0 0 2 2 2 2
1 0 9 9 9 0 2 2 2 2 2 0 2
1 0 0 0 9 0 2 0 2 0 2 0 2
1 0 0 0 9 2 2 0 2 0 2 2 2
1 1 1 1 9 0 0 0 0 1 0 0 0
0 0 0 0 9 0 0 0 0 0 0 0 0
0 0 0 0 9 9 9 9 9 9 9 9 9
FIGURE 5.10 A path through the maze of Figure 5.9. The path positions are marked with 9’s and the dead-end
positions are marked with 2’s
5.6 Backtracking 197
column = 0;
} // default constructor
/**
* Initializes this Position object to (row, column).
*
* @param row the row this Position object has been initialized to.
* @param column the column this Position object has been initialized to.
*/
public Position (int row, int column)
{
this.row = row;
this.column = column;
} // constructor
/**
* Determines the row of this Position object.
*
* @return the row of this Position object.
*/
public int getRow ()
{
return row;
} // method getRow
/**
* Determines the column of this Position object.
*
* @return the column of this Position object.
*/
public int getColumn ()
{
return column;
} // method getColumn
} // class Position
For this application, the Application interface is implemented in a Maze class. The only f elds are a
grid to hold the maze and start and f nish positions. Figure 5.11 has the UML diagrams for the Maze class
and Application interface.
Except for the Maze class constructor and the three accessors (getGrid was developed for the sake of
testing), the method specification for the Maze class are identical to those in the Application interfaces
given earlier. For the embedded MazeIterator class, the constructor’s specificatio is provided, but the
method specification for the hasNext, next and remove methods are boilerplate, so we need not list
them. Here are the specification for the Maze and MazeIterator constructors:
/**
* Initializes this Maze object from a file scanner over a file.
*
* @param fileScanner - the scanner over the file that holds the
* maze information.
*
* @throws InputMismatchException - if any of the row or column values are non-
* integers, or if any of the grid entries are non-integers.
* @throws NumberFormatException - if the grid entries are integers but neither
* WALL nor CORRIDOR
198 C H A P T E R 5 Recursion
<<interface>>
Application
+ toString(): String
Maze
# grid: byte[ ][ ]
Position
# start: Position
# row: int
# finish: Position
+ toString(): String
+ getStart(): Position
+ getFinish(): Position
+ getGrid(): byte[ ][ ]
FIGURE 5.11 The class diagram for the Maze class, which implements the Application interface and has
grid, start, and finish field
*/
public Maze (Scanner fileScanner)
/**
* Initializes this MazeIterator object to start at a given position.
*
* @param pos the position the Iterator objects starts at.
*/
public MazeIterator (Position pos)
5.6 Backtracking 199
The MazeTest class, available on the book’s website, starts by declaring a maze fiel and then creating
a maze (the one shown in Figure 5.9) from a file
protected Maze maze;
@Before
public void runBeforeEachTest() throws IOException
{
fileScanner = new Scanner (new File ("maze.txt"));
maze = new Maze (fileScanner);
} // method runBeforeEachTest
@Test
public void isOKTest2()
{
Position pos = new Position (6, 12);
assertEquals (true, maze.isOK (pos));
} // isOKTest2
@Test
public void isOKTest3()
{
Position pos = new Position (7, 0);
assertEquals (false, maze.isOK (pos));
} // isOKTest3
@Test
public void isOKTest4()
{
Position pos = new Position (0, 13);
assertEquals (false, maze.isOK (pos));
} // isOKTest4
Here is the complete Maze class, including the embedded MazeIterator class:
import java.util.*;
/**
* Initializes this Maze object from a file scanner over a file.
*
* @param fileScanner - the scanner over the file that holds the
* maze information.
*
* @throws InputMismatchException - if any of the row or column values are non-
* integers, or if any of the grid entries are non-integers.
* @throws NumberFormatException - if the grid entries are integers but neither
* WALL nor CORRIDOR
*/
public Maze (Scanner fileScanner)
{
int rows = fileScanner.nextInt(),
columns = fileScanner.nextInt();
/**
* Determines if a given position is legal and not a dead end.
*
* @param pos - the given position.
*
* @return true if pos is a legal position and not a dead end.
*/
public boolean isOK (Position pos)
{
return pos.getRow() >= 0 "" pos.getRow() < grid.length ""
pos.getColumn() >= 0 "" pos.getColumn() < grid [0].length ""
grid [pos.getRow()][pos.getColumn()] == CORRIDOR;
5.6 Backtracking 201
} // method isOK
/**
* Indicates that a given position is possibly on a path to a goal.
*
* @param pos the position that has been marked as possibly being on a path
* to a goal.
*/
public void markAsPossible (Position pos)
{
grid [pos.getRow ()][pos.getColumn ()] = PATH;
} // method markAsPossible
/**
* Indicates whether a given position is a goal position.
*
* @param pos the position that may or may not be a goal position.
*
* @return true if pos is a goal position; false otherwise.
*/
public boolean isGoal (Position pos)
{
return pos.getRow() == finish.getRow() ""
pos.getColumn() == finish.getColumn();
} // method isGoal
/**
* Indicates that a given position is not on any path to a goal position.
*
* @param pos the position that has been marked as not being on any path to a
* goal position.
*/
public void markAsDeadEnd (Position pos)
{
grid [pos.getRow()][pos.getColumn()] = DEAD_END;
} // method markAsDeadEnd
/**
* Converts this Application object into a String object.
*
* @return the String representation of this Application object.
*/
public String toString ()
{
String result = "\n";
202 C H A P T E R 5 Recursion
/**
* Produces an Iterator object, over elements of type Position, that starts at a given
* position.
*
* @param pos - the position the Iterator object starts at.
*
* @return the Iterator object.
*/
public Iterator<Position> iterator (Position pos)
{
return new MazeIterator (pos);
} // method iterator
/**
* Returns the start position of this maze.
*
* @return – the start position of this maze
*
*/
public Position getStart()
{
return start;
} // method getStart
/**
* Returns the finish position of this maze.
*
* @return – the finish position of this maze
*
*/
public Position getFinish()
{
return finish;
} // method getFinish
/**
* Returns a 2-dimensional array that holds a copy of the maze configuration.
*
* @return - a 2-dimensional array that holds a copy of the maze configuration.
*
*/
5.6 Backtracking 203
return gridCopy;
} // method getGrid
/**
* Initializes this MazeIterator object to start at a given position.
*
* @param pos the position the Iterator objects starts at.
*/
public MazeIterator (Position pos)
{
row = pos.getRow();
column = pos.getColumn();
count = 0;
} // constructor
/**
* Determines if this MazeIterator object can advance to another
* position.
*
* @return true if this MazeIterator object can advance; false otherwise.
*/
public boolean hasNext ()
{
return count < MAX_MOVES;
} // method hasNext
/**
* Advances this MazeIterator object to the next position.
*
* @return the position advanced to.
*/
public Position next ()
{
Position nextPosition = new Position();
204 C H A P T E R 5 Recursion
switch (count++)
{
case 0: nextPosition = new Position (row-1, column); // north
break;
case 1: nextPosition = new Position (row, column+1); // east
break;
case 2: nextPosition = new Position (row+1, column); // south
break;
case 3: nextPosition = new Position (row, column-1); // west
} // switch;
return nextPosition;
} // method next
} // class MazeIterator
} // class Maze
To show how a user might utilize the Maze class, we develop a MazeUser class. The MazeUser class
creates a maze from a fil scanner. There is a method to search for a path through the maze. The output
is either a solution or a statement that no solution is possible. The method specification (except for the
usual main method) are
/**
* Runs the application.
*/
public void run()
/**
* Searches for a solution path through the maze from the start position
*
*
* @param maze – the maze to be searched
*
* @return true – if there is a path through the maze; otherwise, false.
*
*/
public boolean searchMaze (Maze maze)
Figure 5.12 has the UML class diagrams that illustrate the overall design. Because the Position class is
quite simple and its diagram is in Figure 5.11, its class diagram is omitted.
The implementation of the MazeUser class is as follows:
import java.io.*;
5.6 Backtracking 205
MazeUser
+ run()
Maze BackTrack
+ getGrid(): byte[ ][ ]
FIGURE 5.12 The UML class diagrams for the maze-search project
import java.util.*;
{
final String INPUT_PROMPT =
"\n\nPlease enter the path for the file whose first line contains the " +
"number of rows and columns,\nwhose 2nd line the start row and column, " +
"whose 3rd line the finish row and column, and then the maze, row-by-row: ";
String fileName;
while (true)
{
try
{
System.out.print (INPUT_PROMPT);
fileName = keyboardScanner.next();
fileScanner = new Scanner (new File (fileName));
break;
} // try
catch (IOException e)
{
System.out.println ("\n" + e);
} // catch IOException
} // while
try
{
maze = new Maze (fileScanner);
System.out.println (INITIAL_STATE + maze);
Scanner stringScanner = new Scanner (maze.toString());
Position start = new Position (stringScanner.nextInt(), stringScanner.nextInt()),
finish = new Position (stringScanner.nextInt(), stringScanner.nextInt());
if (!maze.isOK (start))
System.out.println (START_INVALID);
else if (!maze.isOK (finish))
System.out.println (FINISH_INVALID);
5.6 Backtracking 207
else
{
if (searchMaze (maze, start))
System.out.println (SUCCESS);
else
System.out.println (FAILURE);
System.out.println (FINAL_STATE + maze);
} // else valid search
} // try
catch (InputMismatchException e)
{
System.out.println ("\n" + e + ": " + fileScanner.nextLine());
} // catch InputMismatchException
catch (NumberFormatException e)
{
System.out.println ("\n" + e);
} // catch NumberFormatException
catch (RuntimeException e)
{
System.out.println ("\n" + e);
System.out.println (FINAL_STATE + maze);
} // catch NumberFormatException
} // method run
/**
* Performs the maze search.
*
* @param maze – the maze to be searched.
*
* @return true – if there is a path through this maze; otherwise, false
*
*/
public boolean searchMaze (Maze maze)
{
Position start = maze.getStart();
maze.markAsPossible (start);
BackTrack backTrack = new BackTrack (maze);
if (maze.isGoal (start) || backTrack.tryToReachGoal (start))
return true;
maze.markAsDeadEnd (start);
return false;
} // method searchMaze
} // class MazeUser
In this project, and in general, the run method is not tested because it involves end-user input and output.
All of the files including the Application interface and the BackTrack, Position, Maze, MazeTest,
MazeUser, and MazeUserTest (for the searchMaze method) classes, are available from the book’s
website.
208 C H A P T E R 5 Recursion
1 0 1 1 0 1 1 0
1 0 1 1 0 1 1 0
1 0 1 1 0 1 1 0
. . . . . . . .
. . . . . . . .
. . . . . . . .
1 0 1 1 0 1 1 0
1 0 1 1 0 1 1 0
1 1 1 1 1 1 1 1
FIGURE 5.13 A worst-case maze: in columns 1, 4, 7, . . . , every row except the last contains a 0; every other
position in the maze contains a 1. The start position is in the upper-left corner, and the finis position is in the
lower-right corner
How long does the tryToReachGoal method in the BackTrack class take? Suppose the maze has
n positions. In the worst case, such as in Figure 5.13, every position would be considered, so worstTime(n)
is linear in n. And with more than half of the positions on a path to the goal position, there would be at
least n/2 recursive calls to the tryToReachGoal method, so worstSpace(n) is also linear in n.
Projects 5.2, 5.3, 5.4, and 5.5 have other examples of backtracking. Because the previous project
separated the backtracking aspects from the maze traversing aspects, the BackTrack class and Applica
tion interface are unchanged. The Position class for Projects 5.2, 5.3, and 5.5 is the same Position
class declared earlier, and the Position class for Project 5.4 is only slightly different.
We will re-visit backtracking in Chapter 15 in the context of searching a network. And, of course,
the BackTrack class and Application interface are the same as given earlier.
At the beginning of this chapter we informally described a recursive method as a method that called
itself. Section 5.7 indicates why that description does not suff ce as a definitio and then provides a
definition
A B C D
That is, A calls B, B calls C, and C calls D. When D is being executed, the active methods are
We can now defin “recursive.” A method is recursive if it can be called while it is active. For example,
suppose we had the following sequence of calls:
A B C D
Then B, C, and D are recursive because each can be called while it is active.
When a recursive method is invoked, a certain amount of information must be saved so that infor-
mation will not be written over during the execution of the recursive call. This information is restored
when the execution of the method has been completed. This saving and restoring, and other work related
to the support of recursion, carry some cost in terms of execution time and memory space. Section 5.8
estimates the cost of recursion, and attempts to determine whether that cost is justified
as the iterative version. At best, developing the recursive method will take far less time than the iterative
version, and have similar time/space performance. See, for example, the move, tryToReachGoal, and
permute methods. Of course, it is possible to design an ineff cient recursive method, such as the original
version of fib in Lab 7, just as iterative methods can have poor performance.
In this chapter we have focused on what recursion is. We postpone to Chapter 8 a discussion of
the mechanism, called a stack , by which the compiler implements the saving and restoring of activation
records. As we saw in Chapter 1, this abstraction—the separation of what is done from how it is done—is
critically important in problem solving.
SUMMARY
The purpose of this chapter was to familiarize you with b. the value of each argument: a copy of the corre-
the basic idea of recursion so you will be able to under- sponding argument is made (if the type of the argu-
stand the recursive methods in subsequent chapters and ment is reference-to-object, the reference is copied);
to design your own recursive methods when the need
c. the values of the method’s other local variables;
arises.
A method is recursive if it can be called while it Activation records make recursion possible because they
is active—an active method is one that either is being hold information that might otherwise be destroyed if
executed or has called an active method. the method called itself. When the execution of the cur-
If an iterative method to solve a problem can read- rent method has been completed, a return is made to the
ily be developed, then that should be done. Otherwise, address specifie in the current activation record. The
recursion should be considered if the problem has the previous activation record is then used as the frame of
following characteristics: reference for that method’s execution.
Any problem that can be solved with recursive
1. Complex cases of the problem can be reduced to methods can also be solved iteratively, that is, with a
simpler cases of the same form as the original loop. Typically, iterative methods are slightly more effi
problem. cient than their recursive counterparts because far fewer
2. The simplest case(s) can be solved directly. activation records are created and maintained. But the
elegance and coding simplicity of recursion more than
For such problems, it is often straightforward to develop compensates for this slight disadvantage.
a recursive method. Whenever any method (recursive or A backtracking strategy advances step-by-step
not) is called, a new activation record is created to pro- toward a goal. At each step, a choice is made, but when
vide a frame of reference for the execution of the method. a dead end is reached, the steps are re-traced in reverse
Each activation record contains order; that is, the most recent choice is discarded and a
new choice is made. Backtracking was deployed for the
a. the return address, that is, the address of the state- maze-search application above, and can be used in Pro-
ment that will be executed when the call has been gramming Projects 5.2 (eight-queens), 5.3 (knight’s tour),
completed; 5.4 (Sudoku), and 5.5 (Numbrix).
Crossword Puzzle 211
CROSSWORD PUZZLE
5 6
10
www.CrosswordWeaver.com
ACROSS DOWN
3. The strategy of trying to reach a goal by 1. The mechanism by which the compiler
a sequence of chosen positions, with a implements the saving and restoring of
re-tracing in reverse order of positions that activation records.
cannot lead to the goal.
2. In the binarySearch method, the index
6. For the move method, worstSpace(n) is where the key could be inserted without
_________ in n. disordering the array.
CONCEPT EXERCISES
5.1 What is wrong with the following underlying method for calculating factorials?
/**
* Calculates the factorial of a non-negative integer, that is, the product of all
* integers between 1 and the given integer, inclusive. The worstTime(n) is O(n),
* where n is the given integer.
*
* @param n the non-negative integer whose factorial is calculated.
*
* @return the factorial of n
*
*/
public static long fact (int n)
{
if (n <= 1)
return 1;
return fact (n+1) / (n+1);
} // fact
5.2 Show the f rst three steps in an execution-frames trace of the move method after an initial call of
move (4, 'A', 'B', 'C');
5.3 Perform an execution-frames trace to determine the output from the following incorrect version of the
recPermute method (from Lab 9) after an initial call to
permute ("ABC");
invokes
recPermute ([‘A’, ‘B’, ‘C’], 0);
/**
* Finds all permutations of a subarray from a given position to the end of the array.
*
* @param c an array of characters
* @param k the starting position in c of the subarray to be permuted.
*
* @return a String representation of all the permutations.
*
*/
public static String recPermute (char[ ] c, int k)
{
if (k == c.length - 1)
return String.valueOf (c) + "\n";
else
{
String allPermutations = new String();
char temp;
Concept Exercises 213
5.4 Perform an execution-frames trace to determine the output from the following incorrect version of the
recPermute method (from Lab 9) after an initial call to
permute ("ABC");
invokes
/**
* Finds all permutations of a subarray from a given position to the end of the array.
*
* @param c an array of characters
* @param k the starting position in c of the subarray to be permuted.
*
* @return a String representation of all the permutations.
*
*/
public static String recPermute (char[ ] c, int k)
{
if (k == c.length - 1)
return String.valueOf (c) + "\n";
else
{
String allPermutations = new String();
char temp;
5.5 Use the Principle of Mathematical Induction (Appendix 1) to prove that the move method in the Towers of
Hanoi example is correct, that is, for any integer n > = 1, move (n, orig, dest, temp) returns the
steps to move n disks from pole orig to pole dest.
Hint: for n = 1, 2, 3, . . . , let Sn be the statement:
move (n, orig, dest, temp) returns the steps to move n disks from any pole orig to any
other pole dest.
a. base case. Show that S1 is true.
b. inductive case. Let n be any integer greater than 1 and assume Sn−1 is true. Then show that Sn is true.
According the code of the move method, what happens when move (n, orig, dest, temp) is
called and n is greater than 1?
5.6 In an execution trace of the move method in the Towers of Hanoi application, the number of steps is equal to
the number of recursive calls to the move method plus the number of direct moves. Because each call to the
move method includes a direct move, the number of recursive calls to the move method is always one less
than the number of direct moves. For example, in the execution trace shown in the chapter, n = 3. The total
number of calls to move is 2n − 1 = 7. Then the number of recursive calls to move is 6, and the number
of direct moves is 7, for a total of 13 steps (recall that we started at Step 0, so the last step is Step 12). How
many steps would there be for an execution trace with n = 4?
5.7 Show that, for the recursive binarySearch method, averageTime(n) is logarithmic in n for a successful
search.
Hint: Let n represent the size of the array to be searched. Because the average number of calls is a non-
decreasing function of n, it is enough to show that the claim is true for values of n that are one less than a
power of 2. So assume that
n = 2k − 1, for some positive integer k .
In a successful search,
one call is sufficien if the item sought is half-way through the region to be searched;
two calls are needed if the item sought is one-fourth or three-fourths of the way through that region;
three calls are needed if the item sought is one-eighth, three-eighths, f ve-eighths or seven-eighths of the way
through the region;
and so on.
The total number of calls for all successful searches is
(1 ∗
1) + (2 ∗
2) + (3 ∗
4) + (4 ∗
8) + (5 ∗
16) + · · · + (k ∗
2k −1 )
The average number of calls, and hence an estimate of averageTime(n), is this sum divided by n. Now use
the result from Exercise 2.6 of Appendix 2 and the fact that
k = log2 (n + 1)
5.8 If a call to the binarySearch method is successful, will the index returned always be the smallest index
of an item equal to the key sought? Explain.
PROGRAMMING EXERCISES
5.1 Develop an iterative version of the getBinary method in Section 5.3. Test that method with the same
BinaryTest class (available on the book’s website) used to test the recursive version.
Programming Exercises 215
5.2 Develop an iterative version of the permute method (from Lab 9). Here is the method specification
/**
* Finds all permutations of a specified String.
*
* @param s - the String to be permuted.
*
* @return a String representation of all the permutations, with a line separator
* (that is, "\n") after each permutation.
*/
public static String permute (String s)
For example, if the original string is “BADCGEFH”, the value returned would be
ABCDEFGH
ABCDEFHG
ABCDEGFH
ABCDEGHF
ABCDEHFG
and so on. Test your method with the same PermuteTest method developed in Lab 9 to test the recursive
version.
Hint: One strategy starts by converting s to a character array c. Then the elements in c can be easily
swapped with the help of the index operator, [ ]. To get the f rst permutation, use the static method sort in
the Arrays class of java.util. To give you an idea of how the next permutation can be constructed from the
current permutation, suppose, after some permutations have been printed,
char[ ] c = s.toCharArray();
while (!finished)
{
if (p == 0)
finished = true;
else
{
// In p ... n-1, find the largest index i such that c [i] > c [p - 1].
...
} // else
} // while
return perms;
} // method permute
In the above example, p - 1 = 2 and i = 4, so c [p - 1], namely, ‘E’ is swapped with c [i],
namely, ‘F’.
Explain how strings with duplicate characters are treated differently in this method than in the recursive
version.
5.3 Given two positive integers i and j , the greatest common divisor of i and j , written
gcd (i, j)
/**
* Finds the greatest common divisor of two given positive integers
*
* @param i – one of the given positive integers.
* @param j – the other given positive integer.
*
Programming Exercises 217
Big hint: According to Euclid’s algorithm, the greatest common divisor of i and j is j if i % j = 0. Otherwise,
the greatest common divisor of i and j is the greatest common divisor of j and (i % j).
5.4 A palindrome is a string that is the same from right-to-left as from left-to-right. For example, the following
are palindromes:
ABADABA
RADAR
OTTO
MADAMIMADAM
EVE
For this exercise, we restrict each string to upper-case letters only. (You are asked to remove this restriction
in the next exercise.)
Test and develop a method that uses recursion to check for palindromes. The only parameter is a string that
is to be checked for palindromity. The method specifica ion is
/**
* Determines whether a given string of upper-case letters is a palindrome.
* A palindrome is a string that is the same from right-to-left as from left-to-right.
*
* @param s – (a reference to) the given string
*
* @return true – if the string s is a palindrome; otherwise, false.
*
* @throws NullPointerException – if s is null.
* @throws IllegalArgumentException – if s is the empty string.
*
*/
public static boolean isPalindrome (String s)
5.5 Expand the recursive method (and test class) developed in Programming Exercise 5.4 so that, in testing to
see whether s is a palindrome, non-letters are ignored and no distinction is made between upper-case and
lower-case letters. Throw IllegalArgumentException if s has no letters. For example, the following
are palindromes:
Madam, I’m Adam.
Able was I ’ere I saw Elba.
A man. A plan. A canal. Panama!
Hint: The toUpperCase() method in the String class returns the upper-case String corresponding to
the calling object.
218 C H A P T E R 5 Recursion
5.6 .a. Test and develop a wrapper method power and an underlying recursive method that return the result of
integer exponentiation. The method specificatio of the wrapper method is
/**
* Calculates the value of a given integer raised to the power of a second integer.
* The worstTime(n) is O(n), where n is the second integer.
*
* @param i – the base integer (to be raised to a power).
* @param n – the exponent (the power i is to be raised to).
*
* @return the value of i to the nth power.
*
* @throws IllegalArgumentException – if n is a negative integer or if i raised to
* to the n is greater than Long.MAX_VALUE.
*
*/
public static long power (long i, int n)
Hint: We def ne 00 = 1, so for any integer i , i 0 = 1. For any integers i > 0 and n > 0,
i n = i∗ i n−1
b. Develop an iterative version of the power method.
c. Develop an underlying recursive version called by the power method for which worstTime(n) is logarithmic
in n.
Hint: If n is even, power (i, n) = power (i * i, n/2); if n is odd, power (i, n) = i *
in - 1 = i * power (i * i, n/2).
For testing parts b and c, use the same test suite you developed for part a.
5.7 Test and develop a recursive method to determine the number of distinct ways in which a given amount of
money in cents can be changed into quarters, dimes, nickels, and pennies. For example, if the amount is 17
cents, then there are six ways to make change:
1 dime, 1 nickel and 2 pennies;
1 dime and 7 pennies;
3 nickels and 2 pennies;
2 nickels and 7 pennies;
1 nickel and 12 pennies;
17 pennies.
Here are some amount/ways pairs. The firs number in each pair is the amount, and the second number is the
number of ways in which that amount can be changed into quarters, dimes, nickels and pennies:
17 6
5 2
10 4
25 13
42 31
61 73
99 213
Programming Exercises 219
For the sake of simplifying the ways method, either develop an enumerated type Coin or develop a coins
method that returns the value of each denomination. Thus, coins (1) returns 1, coins (2) returns 5,
coins (3) returns 10, and coins (4) returns 25.
Hint: The number of ways that one can make change for an amount using coins no larger than a quarter is
equal to the number of ways that one can make change for amount —25 using coins no larger than a quarter
plus the number of ways one can make change for amount using coins no larger than a dime.
5.8 Modify the maze-search application to allow an end user to enter the maze information directly, instead of in
a f le. Throw exceptions for incorrect row or column numbers in the start and f nish positions.
5.9 Modify the maze-search application so that diagonal moves would be valid.
Hint: only the MazeIterator class needs to be modified
00000
Each bit position corresponds to a disk: the rightmost bit corresponds to disk 1, the next rightmost bit to
disk 2, and so on.
After 31 moves, the counter will contain all ones, so no further moves will be needed or possible. In general,
2n − 1 moves and 2n − 1 increments will be made.
2. In which direction should that disk be moved?
If n is odd, then odd-numbered disks move clockwise:
A B C
A B C
If n is even, even-numbered disks move clockwise and odd-numbered disks move counter clockwise.
If we number the poles 0, 1, and 2 instead of ‘A’, ‘B’, and ‘C’, then movements can be accomplished simply
with modular arithmetic. Namely, if we are currently at pole k , then
k = (k + 1) % 3;
k = (k + 2) % 3;
achieves a counter-clockwise move. For the pole on which the just moved disk resides, we cast back to a
character:
char(k + ‘A’)
Analysis A chess board has eight rows and eight columns. In the game of chess, the queen is the most powerful
piece: she can attack any piece in her row, any piece in her column, and any piece in either of her diagonals. See
Figure 5.14.
FIGURE 5.14 Positions vulnerable to a queen in chess. The arrows indicate the positions that can be
attacked by the queen ‘Q’ in the center of the figur
The output should show the chess board after the placement of the eight queens. For example:
0 1 2 3 4 5 6 7
0 Q
1 Q
2 Q
3 Q
4 Q
5 Q
6 Q
7 Q
Hint: There must be exactly one queen in each row and exactly one queen in each column. There is no input:
start with a queen at (0, 0), and place a queen in each column. A valid position is one that is not in the same
row, column or diagonal as any queen placed in a previous column. The QueensIterator constructor should
advance to row 0 of the next column. The next method should advance to the next row in the same column. So
0 1 2 3 4 5 6 7
0
1
2
3 K7 K0
4 K6 K1
5 K
6 K5 K2
7 K4 K3
FIGURE 5.15 For a knight (K) at coordinates (5, 3), the legal moves are to the grid entries labeled K0
through K7
For simplicity, the knight starts at position (0, 0). Assume the moves are tried in the order given in Figure 5.15. That is,
from position (row, column), the order tried is:
(row − 2, column + 1)
Programming Exercises 223
(row − 1, column + 2)
(row + 1, column + 2)
(row + 2, column + 1)
(row + 2, column − 1)
(row + 1, column − 2)
(row − 1, column − 2)
(row − 2, column − 1)
0 1 2 3 4 5 6 7
0 1 3
1 2 4
2 10
3 5
4 9
5 6
6 8
7 7
FIGURE 5.16 The f rst few valid moves by a knight that starts at position (0, 0) and iterates according to the
order shown in Figure 5.15. The integer at each fi led entry indicates the order in which the moves were made
For the nine moves, starting at (0, 0), in Figure 5.16, no backtracking occurs. In fact, the first 36 moves are
never backtracked over. But the total number of backtracks is substantial: over 3 million. The solution obtained by the
above order of iteration is:
0 1 2 3 4 5 6 7
0 1 38 55 34 3 36 19 22
1 54 47 2 37 20 23 4 17
2 39 56 33 46 35 18 21 10
3 48 53 40 57 24 11 16 5
4 59 32 45 52 41 26 9 12
5 44 49 58 25 62 15 6 27
6 31 60 51 42 29 8 13 64
7 50 43 30 61 14 63 28 7
Notice that the 37th move, from position (1, 3), does not take the first available choice—to position (3, 2)—nor the
second available choice—to position (2, 1). Both of those choices led to dead ends, and backtracking occurred. The
third available choice, to (0, 1), eventually led to a solution.
0 1 2 3 4 5 6 7
0 1 38 55 34 3 36 19 22
1 54 47 2 37 20 23 4 17
2 39 56 33 46 35 18 21 10
3 48 53 40 57 24 11 16 5
4 59 32 45 52 41 26 9 12
5 44 49 58 25 62 15 6 27
6 31 60 51 42 29 8 13 64
7 50 43 30 61 14 63 28 7
Note: The lines are not part of the output; they are included for readability.
System Test 2
Enter the starting row and column: 3 5
0 1 2 3 4 5 6 7
0 33 42 35 38 31 40 19 10
1 36 57 32 41 20 9 2 17
2 43 34 37 30 39 18 11 8
3 56 51 58 21 28 1 16 3
4 59 44 29 52 47 22 7 12
5 50 55 46 27 62 15 4 23
6 45 60 53 48 25 6 13 64
7 54 49 26 61 14 63 24 5
This solution requires 11 million backtracks. Some starting positions, for example (0, 1), require over 600 million
backtracks. But for every possible starting position, there is a solution.
Programming Exercises 225
5 3 7
6 1 9 5
9 8 6
8 6 3
4 8 3 1
7 2 6
6 2 8
4 1 9 5
8 7 9
The rules of the game are simple: Replace each blank cell with a single digit so that each row, each column, and
each minigrid contain the digits 1 through 9. For example, in the above grid, what digit must be stored in the cell
at row 6 and column 5? (The row numbers and column numbers start at 0, so the cell at (6, 5) is in the upper
right-hand corner of the bottom center minigrid.) The value cannot be
By a process of elimination, we conclude that the digit 7 should be placed in the cell at (6, 5). Using logic only,
you can determine the complete solution to the puzzle. If you click on the link above, you will see the solution.
0 1 6 4 6 3
0 3 1 5 0 7
0 5 4 5 3 9
0 7 5 5 5 1
1 2 8 5 8 4
1 3 3 6 0 5
1 5 5 6 8 2
1 6 6 7 2 7
2 0 2 7 3 2
2 8 1 7 5 6
3 0 8 7 6 9
3 3 4 8 1 4
3 5 7 8 3 5
3 8 6 8 5 8
4 2 6 8 7 7
For the SudokuTest class, include a test to make sure the iterator method starts at the appropriate row
and column, and the next method advances the position’s digit. For one test of the isOK method, the initial
configuratio should have 2 in (0, 0) and 1 in (0, 1); after several calls to the next() method, the isOK method
should return true. To test the isGoal method, the initial configuratio should be a complete solution except for
a blank in (8, 8); isGoal should eventually return true.
Also include tests for the following:
InputMismatchException, if the row or column is not an int, or if the value is not a byte.
IllegalArgumentException, if the value is not a single digit or duplicates the value in the same row, same
column, or same minigrid.
31 0 33 0 39 0 45 0 47
0 29 0 35 0 41 0 49 0
25 0 0 0 0 0 0 0 51
0 23 0 0 0 0 0 53 0
9 0 0 0 0 0 0 0 79
0 1 0 0 0 0 0 77 0
7 0 0 0 0 0 0 0 81
0 3 0 17 0 67 0 73 0
5 0 15 0 65 0 69 0 71
31 32 33 34 39 40 45 46 47
30 29 28 35 38 41 44 49 48
25 26 27 36 37 42 43 50 51
24 23 22 21 58 57 54 53 52
9 10 11 20 59 56 55 78 79
8 1 12 19 60 61 76 77 80
7 2 13 18 63 62 75 74 81
6 3 14 17 64 67 68 73 72
5 4 15 16 65 66 69 70 71
Design, test and write and program to solve any Numbrix puzzle. The f rst line of the input fil will contain the
grid’s side length, and each subsequent line will contain the row, column and value of some initial entry in the
grid.
31 0 33 0 39 0 45 0 47
0 29 0 35 0 41 0 49 0
25 0 0 0 0 0 0 0 51
0 23 0 0 0 0 0 53 0
9 0 0 0 0 0 0 0 79
0 1 0 0 0 0 0 77 0
7 0 0 0 0 0 0 0 81
0 3 0 17 0 67 0 73 0
5 0 15 0 65 0 69 0 71
31 32 33 34 39 40 45 46 47
30 29 28 35 38 41 44 49 48
25 26 27 36 37 42 43 50 51
24 23 22 21 58 57 54 53 52
9 10 11 20 59 56 55 78 79
8 1 12 19 60 61 76 77 80
7 2 13 18 63 62 75 74 81
6 3 14 17 64 67 68 73 72
5 4 15 16 65 66 69 70 71
9 4 8 79
0 0 31 5 1 1
0 2 33 5 7 77
0 4 39 6 0 7
0 6 45 6 8 81
0 8 47 7 1 3
1 1 29 7 3 17
1 3 35 7 5 67
1 5 41 7 7 73
1 7 49 8 0 5
2 0 25 8 2 15
2 8 51 8 4 65
3 1 23 8 6 69
3 7 53 8 8 71
4 0 9
Programming Exercises 229
9 0 7 0 1 0 73 0 77
0 11 0 5 0 71 0 75 0
15 0 0 0 0 0 0 0 79
0 17 0 0 0 0 0 65 0
25 0 0 0 0 0 0 0 63
0 27 0 0 0 0 0 61 0
37 0 0 0 0 0 0 0 59
0 41 0 33 0 53 0 51 0
39 0 43 0 45 0 47 0 49
9 8 7 6 1 72 73 76 77
10 11 12 5 2 71 74 75 78
15 14 13 4 3 70 81 80 79
16 17 18 19 20 69 66 65 64
25 24 23 22 21 68 67 62 63
26 27 28 29 30 55 56 61 60
37 36 35 34 31 54 57 58 59
38 41 42 33 32 53 52 51 50
39 40 43 44 45 46 47 48 49
9 4 8 63
0 0 9 5 1 27
0 2 7 5 7 61
0 4 1 6 0 37
0 6 73 6 8 59
0 8 77 7 1 41
1 1 11 7 3 33
1 3 5 7 5 53
1 5 71 7 7 51
1 7 75 8 0 39
2 0 15 8 2 43
2 8 79 8 4 45
3 1 17 8 6 47
3 7 65 8 8 49
4 0 25
(continued on next page)
230 C H A P T E R 5 Recursion
InputMismatchException, if the row or column is not an int, or if the value is not an int.
ArrayIndexOutOfBoundsException, if the row or column is not in the range 0 . . . grid length, inclusive.
IllegalArgumentException, if the value is greater than grid length squared or duplicates a value already in
the grid.
Hint: The implementation is simplifi d if you assume that one of the original values in the grid is 1, as in System
Tests 1 and 2. After you have tested your implementation with that assumption, remove the assumption. Here are
two system tests in which 1 is not one of the original values in the grid:
75 76 81 66 65 14 13 8 7
74 0 0 0 0 0 0 0 6
73 0 0 0 0 0 0 0 5
72 0 0 0 0 0 0 0 4
55 0 0 0 0 0 0 0 23
54 0 0 0 0 0 0 0 24
45 0 0 0 0 0 0 0 25
44 0 0 0 0 0 0 0 26
43 42 41 40 39 34 33 28 27
75 76 81 66 65 14 13 8 7
74 77 80 67 64 15 12 9 6
73 78 79 68 63 16 11 10 5
72 71 70 69 62 17 2 3 4
55 56 57 58 61 18 1 22 23
54 53 52 59 60 19 20 21 24
45 46 51 50 37 36 31 30 25
44 47 48 49 38 35 32 29 26
43 42 41 40 39 34 33 28 27
Programming Exercises 231
9 4 8 23
0 0 75 5 0 54
0 1 76 5 8 24
0 2 81 6 0 45
0 3 66 6 8 25
0 4 65 7 0 44
0 5 14 7 8 26
0 6 13 8 0 43
0 7 8 8 1 42
0 8 7 8 2 41
1 0 74 8 3 40
1 8 6 8 4 39
2 0 73 8 5 34
2 8 5 8 6 33
3 0 72 8 7 28
3 8 4 8 8 27
4 0 55
25 0 0 0 0
0 3 0 0 0
0 0 0 16 0
0 5 0 0 0
0 0 9 0 0
A solution has been found:
The f nal state is as follows:
25 24 23 22 21
2 3 18 19 20
1 4 17 16 15
6 5 10 11 14
7 8 9 12 13
Note: The f le numbrix.in4 consists of the following:
5
0 0 25
1 1 3
2 3 16
3 1 5
4 2 9
This page intentionally left blank
Array-Based Lists CHAPTER 6
We begin this chapter by introducing the Java Collection Framework’s List interface, which extends
the Collection interface by providing some index-related methods. For example, there is a get
method that returns the element at a given index. In any List object, that is, in any instance of a class
that implements the List interface, the elements are stored in sequence, according to an index. For
example, a List object pets might have the elements arranged as follows: ‘‘dog’’, ‘‘cat’’, ‘‘iguana’’,
‘‘gerbil’’. Here ‘‘dog’’ is at index 0, and ‘‘gerbil’’ is at index 3.
The main focus of this chapter is the user’s view of the ArrayList class. We start by investigating the
method specifications We then briefl turn to the developer’s view: The Java Collection Framework’s
ArrayList class implements the List interface with an underlying array that allows constant-time access
of any element from its index. We finis up the chapter with an application in the area of public-key
cryptography.
As with all of the other collection classes in the Java Collections Framework, the ArrayList class is
parameterized, and the element class is the type parameter, so it would be more appropriate to refer to the
class as ArrayList<E>. When a user creates an instance of the ArrayList<E> class, the user specifie
the element type that will replace the type parameter E. For example, to create an empty ArrayList
object whose elements must be of type (reference to) String, we write
ArrayList<String> myList = new ArrayList<String>();
As we saw in Chapter 4, the only stipulation on the element type is that it cannot be a primitive type,
such as int (but the wrapper class Integer is acceptable).
Chapter 7 covers another List implementation, the LinkedList class The ArrayList and
LinkedList classes have their own advantages and disadvantages: there is no “best” List implemen-
tation. A major goal of the two chapters is to help you recognize situations in which one of the classes
would be preferable to the other.
CHAPTER OBJECTIVES
1. Recognize the methods in the List interface that are not in the Collection interface.
2. Understand the user’s view of the ArrayList class.
3. Be able to decide when an ArrayList is preferable to an array—and vice versa.
4. Understand the VeryLongInt class from both the user’s view and the developers’ view.
233
234 C H A P T E R 6 Array-Based Lists
// Replaces the element that was at position index in this List object with the
// parameter element, and returns the previous occupant. The worstTime(n) is O(n).
E set (int index, E element);
// Returns the index of the first occurrence of obj in this List object, if obj
// appears in this List object.. Otherwise, returns -1. The worstTime(n) is O(n).
int indexOf (Object obj);
// Inserts element at position index in this List object; every element that
// was at a position >= index before this call is now at the next higher position.
// The worstTime(n) is O(n).
void add (int index, E element);
// Removes and returns the element at position index in this List object; every
// element that was at a position > index before this call is now at the next lower
// position. The worstTime(n) is O(n).
E remove (int index);
Any implementation of this interface may improve on the time-estimate upper bounds for the methods;
and, in fact, for the ArrayList class (see following), worstTime(n) is O(1) for both the get and set
methods. We cannot give examples of calls to the List methods because interfaces cannot be instantiated,
but the above fiv methods should give you the idea that many of the methods in a List object are
index based. Of course, we also have some holdovers from the Collection interface: the methods size,
isEmpty, contains, clear, and so on. And the add (E element) method specifie that the element
is inserted at the end of the list.
Section 6.2 introduces the ArrayList class, which implements the List interface. We will empha-
size the user’s perspective of the ArrayList class by studying the method specifications In Section 6.3,
we take a quick look at the developer’s perspective: the actual f elds and method definition in the Java
Collections Framework. Then we return to the user’s view with an application of the ArrayList class.
Figure 6.1 has the big picture from the user’s perspective: the method heading for each public method
in the ArrayList class. Except for the constructors, the headings are in alphabetical order by method
identifier The type parameter E may appear as the return type as well as the element type of a parameter.
Section 6.2.1 has more detail: the method specifications with examples, for several ArrayList
methods.
FIGURE 6.1 Public methods in the class ArrayList<E>, where E is the type parameter. Except for the
constructors, the method headings are in alphabetical order by method identifie
Each method’s time requirements are specifie with Big-O notation because we are merely estab-
lishing an upper bound: a specifi implementation of the method may reduce that upper bound. If no time
estimate for a method is given, you may assume that worstTime(n) is constant. If a method’s average-time
estimate is the same as the worst-time estimate, only the worst-time estimate is given.
The following method specification give you a user’s view of the ArrayList class. For each
method, we include an example and a comparison with an array.
1. Constructor with initial-capacity parameter
/**
* Initializes this ArrayList object to be empty, with the specified initial capacity.
*
* @param initialCapacity the initial capacity of the list.
*
* @throws IllegalArgumentException – if the specified initial capacity is negative
*
*
*/
public ArrayList (int initialCapacity)
Example The following creates an empty ArrayList object called fruits, with String elements
and an initial capacity of 100:
simply constructs an empty ArrayList object with a default initial capacity (namely, 10).
Comparison to an array: An array object can be constructed with a specifie initial capacity. For
example,
String [ ] vegetables = new String [10];
makes vegetables an array object with null references at indexes 0 through 9. Unlike an
ArrayList object, an array object can consist of primitive elements. For example,
constructs an array object whose elements will be of type double and whose initial capacity is 200.
2. Copy constructor
/**
* Constructs a list containing the elements of the specified collection, in the order
* they are stored in the specified collection. This ArrayList object has an
* initial capacity of 110% the size of the specified collection. The worstTime(n)
* is O(n), where n is the number of elements in the specified collection.
*
6.2 The ArrayList Class 237
Example Suppose that myList is an ArrayList object whose elements are the Strings ‘‘yes’’,
‘‘no’’, and ‘‘maybe’’. We can create another ArrayList object that initially contains a copy of myList
as follows:
ArrayList<String> newList = new ArrayList<String> (myList);
At this point, all of the elements in objList are of type Integer, but we can add elements of type
Object (and, therefore, elements of type Integer) to objList.
It might seem that it would be sufficien for the parameter type to be Collection<E> instead
of Collection<? extends E>. After all, an instance of the class ArrayList<Object> is legal
as the argument corresponding to a parameter of type Collection<E>, so by the Subclass Sub-
stitution Rule, an instance of any subclass of ArrayList<Object> would also be legal. But even
though Integer is a subclass of Object, ArrayList<Integer> is not allowed as a subclass of
ArrayList<Object>.1 Otherwise, the following code fragment would be able to violate the type
restrictions by adding a string to an ArrayList of Integer:
ArrayList<Integer> intList = new ArrayList<Integer>();
ArrayList<Object> objList = intList; // illegal!
objList.add ("oops");
1 Thisphenomenon is called invariant subtyping, and it is required for type safety. Why? The element type of a parameterized collection is
not available at run time, so the type checking of elements cannot be done at run time. This is in contrast to arrays, whose element type is
available at run time. As a result, arrays use covariant subtyping; for example, String[ ] is a subclass of Object[ ].
238 C H A P T E R 6 Array-Based Lists
For example, if myList is an ArrayList object, we can create a shallow copy of myList as follows:
ArrayList<String> newList = (ArrayList<String>)myList.clone();
Unfortunately, there is no assurance of type safety, so the assignment will be made even if myList is
an ArrayList object with Integer elements. See Programming Exercise 6.4 for details. For more
discussion of clone drawbacks, see Bloch 2001, pages 45–52.
Comparison to an array: An array object can be copied with the static method arraycopy in
the System of the package java.lang. For example,
System.arraycopy (vegetables, i, moreVeggies, 0, 3);
performs a shallow copy of the array object vegetables, starting at index i, to the array object
moreVeggies, starting at index 0. A total of 3 elements are copied.
Note. According to the general contract of the add method in the Collection interface, true is
returned if the element is inserted. So this ArrayList method will always return true. Then why
bother to have it return a value? Because if we replace the return type boolean with void, then
the ArrayList class would no longer implement the Collection interface. Incidentally, there are
some implementations—the TreeSet class, for example—of the Collection interface that do not
allow duplicate elements, so false will sometimes be returned when a TreeSet object calls this
version of the add method.
Example We can insert items at the end of an ArrayList object as follows:
The ArrayList object fruits will now have “oranges” at index 0, “apples” at index 1, “durian”
at index 2, and “apples” at index 3.
6.2 The ArrayList Class 239
Then
System.out.println (fruits.size());
will output 4.
Comparison to an array: Arrays have nothing that corresponds to a size() method. The length
fiel contains the capacity of the array, that is, the maximum number of elements that can be inserted
into the array, not the current number of elements in the array.
5. The get method
/**
* Returns the element at the specified index.
*
* @param index – the index of the element to be returned.
*
* @return the element at the specified index
*
* @throws IndexOutOfBoundsException – if index is less than 0 or greater
* than or equal to size()
*/
public E get (int index)
Note: Since no time estimates are given, you may assume that worstTime(n) is constant.
Example Suppose we start by constructing an ArrayList object:
ArrayList<String> fruits = new ArrayList<String> (100);
240 C H A P T E R 6 Array-Based Lists
fruits.add ("oranges");
fruits.add ("apples");
fruits.add ("durian");
fruits.add ("apples");
Then
Comparison to an array: The get method is similar to, but weaker than, the index operator for
arrays. For example, suppose we start by constructing an array object:
String [ ] vegetables = new String [10];
vegetables [0] = "carrots";
vegetables [1] = "broccoli";
vegetables [2] = "spinach";
vegetables [3] = "corn";
Then
System.out.println (vegetables [1]);
fruits.add ("durian");
fruits.add ("apples");
Then
will change the element at index 2 to ‘‘bananas’’ and output ‘‘durian’’, the element that had been at
index 2 before the set method was invoked.
Comparison to an array: As noted in the comparison for the get method, an array’s index operator
can be used on the left-hand side of an assignment statement. For example, if vegetables is an
array object,
vegetables [1] = "potatoes";
Then
oranges
cherries
apples
durian
apples
242 C H A P T E R 6 Array-Based Lists
Comparison to an array: For an insertion anywhere except at the end of the array object, the code
must be written to open up the space. For example, suppose we start by constructing an array object:
String [ ] vegetables = new String [10];
vegetables [0] = "carrots";
vegetables [1] = "broccoli";
vegetables [2] = "spinach";
vegetables [3] = "corn";
The array vegetables now consists of “carrots”, “lettuce”, “broccoli”, “spinach”, “corn”, null,
null, null, null, null. Note that an insertion in a full array will throw an ArrayIndexOutOf
Bounds exception.
8. The remove method with an index parameter
/**
* Removes the element at the specified index in this ArrayList object.
* All elements that were at positions greater than the specified index have
* been moved to the next lower position. The worstTime(n) is O(n).
*
* @param index – the index of the element to be removed.
*
* @return the element removed the specified index
*
* @throws IndexOutOfBoundsException – if index is less than 0 or greater
* than or equal to size()
*/
public E remove (int index)
The output will be ‘‘durian’’, and fruits will now contain ‘‘oranges’’, ‘‘apples’’, and ‘‘apples’’.
Comparison to an array: For removal anywhere except at the end of an array, the code must be
written to close up the space. For example, suppose we start by creating an array object:
String [ ] vegetables = new String [10];
vegetables [0] = "carrots";
vegetables [1] = "broccoli";
vegetables [2] = "spinach";
6.2 The ArrayList Class 243
The array vegetables now consists of “carrots”, “broccoli”, “corn”, “potatoes”, “squash”, null,
null, null, null and null.
Then
Note: The type of the parameter element is Object, not E, so the following is legal:
System.out.println (fruits.indexOf (new Integer (8)));
Of course, the output will be −1, because all the elements in fruits are of type String.
Comparison to an array: An explicit search must be conducted to determine if an element occurs
in an array. For example, suppose we start by creating an array object:
String [ ] vegetables = new String [10];
vegetables [0] = "carrots";
vegetables [1] = "broccoli";
vegetables [2] = "spinach";
244 C H A P T E R 6 Array-Based Lists
If myVeg is a String variable, we can print the index of the firs occurrence of myVeg in the
vegetables array as follows:
If myVeg does not occur in the array object vegetables, −1 will be output.
These represent just a sampling of the ArrayList class’s methods, but even at this point you can see
that an ArrayList object is superior, in most respects, to an array object. For example, an ArrayList
object’s size and capacity are automatically maintained, but an array object’s size and capacity must be
explicitly maintained by the programmer.
import java.util.*;
import java.io.*;
String inFilePath,
word;
6.2 The ArrayList Class 245
try
{
System.out.print ("\n Please enter the path for the input file: ");
inFilePath = keyboardScanner.nextLine();
fileScanner = new Scanner (new File (inFilePath));
while (fileScanner.hasNext())
{
word = fileScanner.next();
System.out.println (word);
aList.add (word);
} // while not end of file
System.out.print ("\n\n Please enter the word you want to search for: ");
word = keyboardScanner.next();
if (aList.indexOf (word) >= 0)
System.out.println (word + " was found.\n\n");
else
System.out.println (word + " was not found.\n\n");
System.out.print ("Please enter the word you want to upper case: ");
word = keyboardScanner.next();
int position = aList.indexOf (word);
if (position >= 0)
{
aList.set (position, word.toUpperCase());
System.out.println (word + " was converted to upper-case.\n\n");
} // if word is in aList
else
System.out.println (word +
" was not found, so not upper-cased.\n\n");
246 C H A P T E R 6 Array-Based Lists
} // class ArrayListExample
When this program was run, the f le a.in1 contained the following words, one per line:
Don’t get mad Don’t get even Get over it all and get on with
Please enter the word you want to convert to upper case: over
over was converted to upper-case.
In the above program, each removal takes linear time. Programming Exercise 6.8 suggests how to perform
all removals in a single loop. And you are invited, in Programming Exercise 6.9, to endure the grind of
converting the program from ArrayList-based to array-based.
In Sections 6.2.3 and 6.2.4, we briefl put on a developer’s hat and look at the ArrayList class
heading, field and a few method definitions In Section 6.3, we return to a user’s perspective with an
application of the ArrayList class.
This says that the ArrayList class is a subclass of the class AbstractList, and implements four
interfaces: List, RandomAccess, Cloneable, and Serializable. Figure 6.2 has a UML diagram to
indicate where the ArrayList class f ts in the Java Collections Framework, with a solid-line arrow from
an extension (to a class or interface) and a dashed-line arrow from a class to an interface implemented by
the class.
The AbstractCollection class provides a minimal implementation of the Collection interface,
just as the AbstractList class provides a “bare bones” implementation of the List interface. As we
saw in Section 6.1, the List interface extends the Collection interface by including some index-related
methods, such as get (int index) and remove (int index).
Basically, a class that implements the Cloneable interface must have a method that returns a shallow
copy of the calling object. For a description of the clone() method, see Note 4 on the copy constructor
(method number 2) in Section 6.2.1. The RandomAccess interface ensures that if an implementation of
the List interface satisfie the random-access property (with an underlying array), then any sub-list of that
list will also satisfy the random-access property. The Serializable interface, discussed in Appendix 1,
has to do with saving objects to a stream (such as a disk file) which is called serialization, and restoring
those object from the stream, called deserialization.
E
<<interface>>
Collection
E E
<<interface>> AbstractCollection
List
E
<<interface>> <<interface>> <<interface>>
AbstractList
Cloneable RandomAccess Serializable
E
ArrayList
FIGURE 6.2 The UML diagram to illustrate the relative position of the ArrayList<E> class in the Java
Collections Framework
248 C H A P T E R 6 Array-Based Lists
It may come as no surprise to you that the ArrayList class has an array f eld:
private transient E[ ] elementData;
The reserved word transient indicates that this fiel is not saved during serialization (see Appendix 1).
That is, each element would be saved, but not the entire array. The f eld is private instead of protected
because the developers of the Java Collections Framework were opposed to giving users who subclass
direct access to a superclass’s f elds. See Section 2.6 for a discussion of this choice.
The only other fiel define in the ArrayList class is
private int size;
So an ArrayList object has an array f eld to store the elements and an int fiel to keep track of the
number of elements.
We will finis up our developer’s view of the ArrayList class by studying the implementation of
the add method that appends an element to the end of the calling ArrayList object.
The call to the ensureCapacity method expands the underlying array, if necessary, to accommodate the
new element; we’ll get to the details of that method momentarily. Then the new element, element, is
inserted at index size in the array, size is incremented, and true is returned. Suppose that fruits has
been constructed as an empty ArrayList by a default-constructor call, and the next message is
fruits.add ("oranges");
After that message is processed, the elementData and size field in fruits will have the contents
shown in Figure 6.3.
Now let’s get back to the ensureCapacity method. If the underlying array is not fille to capac-
ity, then the call to ensureCapacity does nothing. But if size == elementData.length, then the
argument size + 1 must be greater than elementData.length, so we need to expand the array. First,
the array’s current reference, elementData, is copied to oldData:
E oldData [ ] = elementData;
This does not make a copy of the array, just a copy of the reference. Then a new array object is constructed:
elementData = (E[ ]) new Object [newCapacity];
where (because the argument was size + 1) the variable newCapacity was given a value about 50%
larger than oldData.length. The cast was necessary because the new operator must be applied to a
“real” type, not to a type parameter (such as E). Finally, the arraycopy method in the System class is
6.2 The ArrayList Class 249
elementData
oranges
null
null
null
size
null
1
null
null
null
null
null
FIGURE 6.3 The contents of the elementData and size field in the ArrayList object fruits after the
message fruits.add ("oranges") is sent. As usual, we pretend that the non-null elements in the array are
objects; in fact, the elements are references to objects
called to copy all the elements from oldData to elementData; the number of elements copied is the
value of size.
Here is the complete definition
public void ensureCapacity(int minCapacity)
{
modCount++; // discussed in Appendix 1
int oldCapacity = elementData.length;
if (minCapacity > oldCapacity)
{
E oldData[ ] = elementData;
int newCapacity = (oldCapacity * 3) / 2 + 1;
if (newCapacity < minCapacity) // can’t happen if argument is size + 1
newCapacity = minCapacity;
elementData = (E[ ]) new Object [newCapacity];
System.arraycopy(oldData, 0, elementData, 0, size);
}
}
To see the effect of an expansion, suppose that the ArrayList object fruits already has ten elements
and the following message is sent:
fruits.add ("cantaloupes");
Figure 6.4 shows the effect of this message on the elementData and size field of fruits.
What are the time estimates of the one-parameter add method? Let n represent the number of
elements in the calling ArrayList object. In the worst case, we will have n = elementData.length,
and so, in the ensureCapacity method, we will have minCapacity > oldCapacity. Then the call to
arrayCopy entails copying n elements from oldData to elementData. We conclude that worstTime(n)
is linear in n.
250 C H A P T E R 6 Array-Based Lists
fruits.elementData
oranges [0]
bananas [1]
kiwi [2]
apples [3]
fruits.size
pears [4]
11
oranges [5]
grapes [6]
plums [7]
peaches [8]
apricots [9]
cantaloupes [10]
null [11]
null [12]
null [13]
null [14]
null [15]
FIGURE 6.4 The contents of the elementData and size field in the ArrayList object fruits, if fruits
already had ten elements when the message fruits.add ("cantaloupes") was sent. As usual, we pretend
that the non-null elements in the array are objects instead of references
What about the average case? The only occasion for copying occurs when n = elementD
ata.length. But then, by the details of the ensureCapacity method, no copying would have occurred
in the previous n/3 (approximately) calls to the one-parameter add method. So in n/3 + 1 calls to that
add method, the total number of elements copied would be n, and the average number of elements copied
per call would be about 3. We conclude, since the only non-constant-time code in the ensureCapacity
method is in the initialization of elementData and in the call to arrayCopy, that averageTime(n) is
constant for the one-parameter add method.
Incidentally, the developers of the ArrayList class could have doubled oldCapacity instead of
increasing it by about 50%. There is a trade-off: with doubling, additional space is allocated immediately,
but then there will be a longer period before the next re-sizing occurs. In fact, in the C++ analogue of
the ArrayList class, the old capacity is doubled when a re-sizing occurs.
The previous examination of f elds and implementation details is intended just to give you the f avor
of the developer’s view of the ArrayList class. A few more ArrayList method-definition are covered
in Lab 10. Of course, all of the ArrayList definition are available in the ArrayList (or AbstractList
or AbstractCollection) class of java.util.
6.3 Application: High-Precision Arithmetic 251
You are now prepared to do Lab 10: More Details on the ArrayList Class
Section 6.3 presents an application of the ArrayList class, so the emphasis once again is on the user’s
viewpoint.
2 An integer p > 1 is prime if the only positive-integer factors of p are 1 and p itself.
252 C H A P T E R 6 Array-Based Lists
Then veryLong will be initialized to the VeryLongInt object whose integer value is
11223344556677889900. The ‘?’ is ignored because it is not a digit character. The value is greater
than the largest int value.
[5, 2, 4, 8, 1]
3. /**
* Increments this VeryLongInt object by a specified VeryLongInt object.
* The worstTime(n) is O(n), where n is the number of digits in the larger of this
* VeryLongInt object (before the call) and the specified VeryLongInt object.
*
* @param otherVeryLong – the specified VeryLongInt object to be added to
* this VeryLongInt object.
*
6.3 Application: High-Precision Arithmetic 253
Example Suppose that newInt and oldInt are VeryLongInt objects with values of 328 and 97,
respectively, and the message sent is
newInt.add (oldInt);
Note: This method performs the arithmetic operation of addition. Contrast that to the ArrayList
class’s one-paramter add method, which appends the argument to the calling ArrayList object.
The book’s website has a VeryLongIntTest class, with the following fields
protected VeryLongInt very;
That class also includes the following tests, one for a constructor call with an argument that has no digits,
and one for a simple addition:
@Test (expected = IllegalArgumentException.class)
public void testConstructorWithNoDigits()
{
very = new VeryLongInt ("x t?.o");
} // method testConstructorWithNoDigits
@Test
public void testAdd()
{
very = new VeryLongInt ("99");
VeryLongInt other = new VeryLongInt ("123");
very.add (other);
answer = very.toString();
assertEquals ("[2, 2, 2]", answer);
} // method testAdd
“a VeryLongInt object has-an ArrayList field than “a VeryLongInt object is-an ArrayList object.”
The only f eld in the VeryLongInt class will be an ArrayList object whose elements are of type
Integer:
protected ArrayList<Integer> digits;
Each element in the ArrayList object digits will be an Integer object whose value is a single digit
(Exercise 6.6 expands each value to a five-digi integer).
Figure 6.5 has the UML diagram for the VeryLongInt class.
VeryLongInt
# digits: ArrayList<Integer>
+ VeryLongInt (s: String)
+ toString(): String
+ add (otherVeryLong: VeryLongInt)
Finally, we append that digit (as an Integer) to digits. The ArrayList fiel digits never needs
resizing during the execution of this constructor because that fiel is constructed with initial capacity of
s.length(). Here is the code:
public VeryLongInt (String s)
{
final char LOWEST_DIGIT_CHAR = '0';
char c;
int digit;
6.3 Application: High-Precision Arithmetic 255
How long will this method take? Assume that there are n characters in the input. Then the loop will be
executed n times. For the ArrayList class’s one-parameter add method, averageTime (n) is constant,
so for this constructor, averageTime(n) is linear in n (that is, O(n) and (n)). As we saw in the analysis
of that add method, if n represents the number of elements in the ArrayList object, worstTime(n) is
O(n) for n calls to that add method, So for this constructor in the VeryLongInt class, worstTime(n)
is O(n). In fact, because worstTime(n) ≥ averageTime(n) and averageTime(n) is (n), worstTime(n) must
be (n). We conclude that worstTime(n) is linear in n.
For the toString() method, we simply invoke the ArrayList class’s toString() method:
public String toString()
{
return digits.toString();
} // method toString
For an example of a call to this method, if veryLong is a VeryLongInt object with a value of 6713, the
output from the call to
System.out.println (veryLong); // same as System.out.println (veryLong.toString());
will be
[6, 7, 1, 3]
For this method, worstTime(n) is linear in n, the number of digits in the calling VeryLongInt object.
To convince yourself of this estimate, look at the definitio of the toString() method in the Abstract
Collection class, a superclass of ArrayList.
Finally, we tackle the add (VeryLongInt otherVeryLong) method in the VeryLongInt class.
We obtain partial sums by adding otherVeryLong to the calling object digit-by-digit, starting with the
least significan digit in each number. Each partial sum, divided by 10, is appended to the end of the
ArrayList object sumDigits, which is initially empty.
Because we will be using the ArrayList class’s one-parameter add method on the partial sums,
we must reverse sumDigits after adding so that the most significan digit will end up at index 0. For
example, suppose newInt is a VeryLongInt object with the value 328 and oldInt is a VeryLongInt
object with the value 47. If the message is
newInt.add (oldInt);
256 C H A P T E R 6 Array-Based Lists
then after adding and appending the partial sums to the VeryLongInt object sum, sum will have the value
573. When this is reversed—by the generic algorithm reverse in the Collections class of the package
java.util —the sum will be correct. Note that the add method in the ArrayList class is used to
append a digit to the end of sumDigits; the ArrayList class’s add method does not perform arithmetic.
Here is the definitio of the add method in the VeryLongInt class:
public void add (VeryLongInt otherVeryLong)
{
final int BASE = 10;
int largerSize,
partialSum,
carry = 0;
if (carry == 1)
sumDigits.add (carry);
Collections.reverse (sumDigits);
digits = sumDigits;
} // method add
The call to the least method with an argument of i returns the ith least significan digit in the calling
object’s digits field The units (rightmost) digit is considered the 0th least significan digit, the tens digit
is considered the 1st least significan digit, and so on. For example, suppose that the calling VeryLongInt
object has the value 3284971, and i has the value 2. Then the digit returned by the call to least (2) will
be 9 because 9 is the 2nd least significan digit in the calling object’s digits field the 0th least-significan
digit is 1 and the 1st least-significan digit is 7. The method definitio is:
/** Returns the ith least significant digit in digits if i is a non-negative int less than
* digits.size(). Otherwise, returns 0.
*
* @param i – the number of positions from the right-most digit in digits to the
* digit sought.
*
* @return the ith least significant digit in digits, or 0 if there is no such digit.
*
* @throws IndexOutOfBoundsException – if i is negative.
Summary 257
*
*/
protected int least (int i)
{
if (i >= digits.size())
return 0;
return digits.get (digits.size() - i - 1);
} // least
For the least method, worstTime(n) is constant because for the size and get methods in the ArrayList
class, worstTime(n) is constant.
We can now estimate the time requirements for the VeryLongInt class’s add method. Assume, for
simplicity, that the calling object and otherVeryLongInt are very long integers with n digits. There will
be n iterations of the for loop in the definitio of the add method, and during each iteration, a digit is
appended to sumDigits. For appending n elements to the end of an ArrayList, worstTime(n) is linear
in n; see Exercise 6.2. The reverse generic algorithm also takes linear-in-n time, so for the add method
in the VeryLongInt class, worstTime(n) is linear in n.
The book’s website has a class, VeryLongIntUser, to demonstrate how an end user might work
with the VeryLongInt class. The run method inputs a line from the keyboard, calls a process method
to parse the line and invoke the appropriate method,, and outputs the result of processing to the screen.
For the testing of that process method, see the test class, VeryLongIntUserTest, also on the book’s
website.
Programming Exercise 6.7 expands on the VeryLongInt class. You should complete that exercise
before you attempt to do Lab 11.
You are now prepared to do Lab 11: Expanding the VeryLongInt Class
Exercise 7.7 explores the modification needed to develop the VeryLongInt class with digits a
LinkedList fiel instead of an ArrayList field
SUMMARY
In this chapter we introduced the List interface, which created, and the old array is copied to the larger array.
extends the Collection interface by adding several This is similar to what hermit crabs do each time they
index-based methods. We then studied the ArrayList outgrow their shell. A further advantage of ArrayList
class, an implementation of the List interface that allows object over arrays is that, for inserting and deleting, users
random-access—that is, constant-time access—of any are relieved of the burden of writing the code to make
element from its index. Using an ArrayList object is space for the new entry or to close up the space of the
similar to using an array, but one important difference deleted entry.
is that ArrayList objects are automatically resizable. The application of the ArrayList class was
When an ArrayList outgrows the current capacity of in high-precision arithmetic, an essential component of
its underlying array, an array of 1.5 times that size is public-key cryptography.
258 C H A P T E R 6 Array-Based Lists
CROSSWORD PUZZLE
1 2 3
7 8
10
www.CrosswordWeaver.com
ACROSS DOWN
7. A constructor that initializes the calling 4. Because the elements in any Collection
object to a copy of the argument object are references, the ArrayList’s
corresponding to the given parameter. copy constructor is said to produce a
______ copy.
9. The fact that ArrayList<String> is not
a subclass of ArrayList<Object> is an 8. A positive integer greater than 1 that
example of ____________ subtyping. has no positive-integer factors other
than 1 and itself is called a
10. In public-key cryptography, information ____________ number.
is encoded and decoded using
______________.
Programming Exercises 259
CONCEPT EXERCISES
6.1 State two advantages, and one disadvantage, of using an ArrayList object instead of an array object.
6.2 Show that, for the task of appending n elements to an ArrayList object, worstTime(n) is linear in n.
6.3 The one-parameter add method in the ArrayList class always returns true. Would it make sense to change
the return type from boolean to void ? Explain.
6.4 For the one-parameter add method in the ArrayList class, estimate worstSpace(n) and averageSpace(n).
6.5 In choosing f elds for the VeryLongInt class, we decided to use, rather than inherit from, the ArrayList
class. Why?
Hint: How much commonality is there between the methods in the ArrayList class and the methods in the
VeryLongInt class?
6.6 Suppose you modifie the VeryLongInt class as follows: each element in digits consists of a f ve-digit
integer. What effect do you think this will have on Big-O time? What about run-time?
6.7 Suppose, in developing the VeryLongInt class, we decide that digits will contain the integer in reverse
order. For example, if the constructor call is:
we would have (Integer elements with values) 6, 8, 3 in positions 0 through 2, respectively of digits.
Re-design this constructor so that worstTime(n) is still linear in n.
6.8 Which parts of the VeryLongInt methods would have to be re-written if digits were an array object of
int elements instead of an ArrayList object of Integer elements?
6.9 How can a user of the VeryLongInt class easily create a VeryLongInt object that is a copy of an already
existing VeryLongInt object?
PROGRAMMING EXERCISES
6.1 Hypothesize the output from the following code, and then test your hypothesis with a small program that
includes the code:
letters.add ("f");
letters.add (1, "i");
letters.add ("e");
letters.add (1, "r");
letters.add ("e");
letters.add (4, "z");
System.out.println (letters);
letters.remove ("i");
int index = letters.indexOf ("e");
letters.remove (index);
letters.add (2, "o");
System.out.println (letters);
260 C H A P T E R 6 Array-Based Lists
6.2 For each of the following program segments, hypothesize if the segment would generate a compile-time error,
a run-time exception, or neither. Then test your hypotheses with a main method that includes each segment.
a. ArrayList<String> myList = new ArrayList<String>();
myList.add ("yes");
myList.add (7);
myList.add ("Karen");
myList.add ("Don");
myList.add ("Mark");
Hypothesize what the contents of myList, temp, and sameList will be after this last insertion. Then test
your hypothesis with a main method that includes the code.
6.4 Hypothesize what will happen when the following code fragment is run, and then test your hypothesis:
ArrayList<String> original = new ArrayList<String>();
original.add ("yes");
ArrayList<Integer> copy = (ArrayList<Integer>)original.clone();
Hint: This exercise illustrates why the copy constructor is superior to the clone() method.
6.5 Expand the VeryLongInt class by testing and definin methods that have the following method specifica
tions:
a. /**
* Initializes this VeryLongInt object from a given int.
*
* @param n – the int from which this VeryLongInt is initialized.
*
* @throws IllegalArgumentException – if n is negative.
*
*/
public VeryLongInt (int n)
Programming Exercises 261
b. /**
* Returns the number of digits in this VeryLongInt object.
*
* @return the number of digits in this VeryLongInt object.
*
*/
public int size()
c. /**
* Returns true if this VeryLongInt object is less than another VeryLongInt
* object. The worstTime(n) is O(n).
*
* @param otherVeryLong – the other VeryLongInt object.
*
* @return true – if this VeryLongInt is less than otherVeryLong.
*
* @throws NullPointerException – if otherVeryLong is null
*
*/
public boolean less (VeryLongInt otherVeryLong)
d. /**
* Returns true if this VeryLongInt object is greater than another VeryLongInt
* object. The worstTime(n) is O(n).
*
* @param otherVeryLong – the other VeryLongInt object.
*
* @return true – if this VeryLongInt is greater than otherVeryLong.
*
* @throws NullPointerException – if otherVeryLong is null
*
*/
public boolean greater (VeryLongInt otherVeryLong)
e. /**
* Returns true if this VeryLongInt object is equal to a specified object.
* The worstTime(n) is O(n).
*
* @param obj – the specified object that this VeryLongInt is compared to.
*
* @return true – if this VeryLongInt is equal to obj.
*
*/
public boolean equals (Object obj)
f. /**
* Stores a Fibonacci number in this VeryLongInt object.
*
* @param n – the index in the Fibonacci sequence
*
* @throws IllegalArgumentException – if n is not positive
*
262 C H A P T E R 6 Array-Based Lists
*/
public void fibonacci (int n)
tempInt.fibonacci (100);
Hint: Mimic the iterative design of the Fibonacci function from Lab 7. Both i and n will be ordinary int
variables, but previous, current and temp will be VeryLongInt objects. After the loop, instead of
returning current, the calling object is modifi d by assigning to digits a copy of current.digits.
6.6 Assume that myList is (a reference to) an ArrayList<Double> object and that both i and j are int
variables with values in the range from 0 to myList.size() -1, inclusive. Hypothesize what the following
accomplishes, and then test your hypothesis.
6.7 Describe how to fin the method definition for the ArrayList class in your computing environment.
6.8 Modify the simple program in Section 6.2.2 so that all removals are performed in a single loop.
Hint: Create a temporary ArrayList object to hold the un-removed elements. What is a drawback to this
approach?
6.9 Convert the simple program in Section 6.2.2 into one that uses an array object instead of an ArrayList
object.
6.10 Modify the simple program in Section 6.2.2 to use a binary search instead of the sequential search used in
the call to the indexOf method. The Collections class in java.util has a binarySearch method
and a sort method.
6.11 Suppose scoreList is an ArrayList object of Integer elements, and the following message is sent:
scoreList.remove (3);
Does this message remove the element at index 3, or remove the f rst occurrence of new Integer (3)?
Test your hypothesis.
6.12 Suppose we create the following ArrayList instance:
And then we insert several words into words. Write the code to print out each element of words that has
exactly four letters. You should have three different versions of the code:
a. using an index;
b. using an explicit iterator;
c. using an enhanced for statement.
/**
* In a given ArrayList, remove all duplicates.
* The worstTime(n) is O(n2 ).
*
* @param list - the given ArrayList.
*
Programming Exercises 263
For example, suppose myList consists of references to Integer objects with the following values, in
sequence
3, 8, 6, 4, 8, 7, 8, 9, 4
Then the ArrayList returned by the call to uniquefy (myList) will consist of references to Integer
objects with the following values, in sequence
3, 8, 6, 4, 7, 9
/** Stores in this VeryLongInt object the product of its pre-call value and the value
* of a specified VeryLongInt object. The worstTime(n) is O(n * n), where n is
* the maximum of the number of digits in the pre-call value of this
* VeryLongInt object and the number of digits in the specified VeryLongInt object.
*
* @param otherVeryLong – the specified VeryLongInt object to be multiplied by
* this VeryLongInt object.
*
* @throws NullPointerException – if otherVeryLong is null
*
*/
public void multiply (VeryLongInt otherVeryLong)
For factorial:
/**
* Stores, in this VeryLongInt object, the product of all integers between 1 and
* specified integer n. The worstTime(n) is O(n log (n!)): n multiplications, and
* each product has fewer digits than log (n!), the number of digits in n!
*
* @param n – the number whose factorial will be stored in this VeryLongInt
* object.
*
* @throws IllegalArgumentException – if n is negative.
*
*/
public void factorial (int n)
Problem Convert a text file into a list of tokens, one per line.
Analysis A program that transforms an input file in one form into an output file in another form is called a filter. For
the sake of further parts of this project, you will transform an input file into a list. Develop a Filter class with the
following two methods:
/**
* Initializes this Filter object from the paths for the input file
* and common words.
*
* @param inFilePath - the path for the input file.
* @param commonFilePath - the path for the file with the common words.
*
* @throws IOException - if either file does not exist.
*
*/
public Filter (String inFilePath, String commonFilePath) throws IOException
/**
* Creates the ArrayList of tokens from the input file.
*
* @return – an ArrayList of tokens.
*
*
*/
public ArrayList<String> createList()
1. The tokens in the returned ArrayList will not include common words such as “a”, “and”, and “in”. The
end-user will specify a f le of common words, one per line. Here are the (sample) contents of that fi e:
a
an
and
are
Programming Exercises 265
did
down
in
the
where
to
You should assume that the f le of common words is large enough so that it should be read in only once, and
stored without a lot of unused space (the title of this chapter is a hint for the storage structure). Each search
of the common words should take O(log n) time in the worst case. The f le of common words may not be
in alphabetical order, but the stored common words should be in alphabetical order (see Collections.java in
java.util).
2. The tokens will not include tags from the input file that is, all characters between ‘<’ and ‘>’. You may
assume that each ‘<’ will be followed on the same line by a matching ‘>’. You may assume that the text
between two link tags consists of a single word. That is, you might have <a href = . . . >singleword</a>.
For example, suppose a line in the input fil consists of
3. Other than the restrictions of 1 and 2 above, the returned ArrayList will consist of all words, lowercased, in
the input file each word has only letters, digits, hyphens, and apostrophes.
4. After unit-testing your Filter class, develop a FilterUser class similar to the VeryLongIntUser class in
Section 6.3.3. The FilterUser class scans in the path names for the input fil and the common-words f le.
Include appropriate messages and re-prompts for incorrect input.
The f lter you will be creating in this project is essential for a search engine because the relevance of a
document is based on the words the document contains.
Here is sample input for the FilterUser class:
kubla.in1
common.in1
and the fil “common.in1” is as shown above, then the contents of the returned ArrayList will be
caverns
browser2
kubla.in3
common.in1
If the f le “kubla.in3” consists of:
down
Hint for removing tags: Treat an entire tag as a delimiter. See http://www.txt2re.com for details.
Linked Lists CHAPTER 7
In this chapter we continue our study of collection classes by introducing the LinkedList class, part
of the Java Collections Framework. Like the ArrayList class, the LinkedList class implements the
List interface, so you are already familiar with most of the LinkedList method headings. There are
some significant performance differences between the two classes. For example, LinkedList objects
lack the random-access feature of ArrayList objects: to access a LinkedList’s element from an
index requires a loop. But LinkedList objects allow constant-time insertions and deletions, once the
insertion-point or deletion-point has been accessed.
We will start with a general discussion of linked lists, and then introduce a simple linked structure, the
SinglyLinkedList class. This toy class serves mainly to prepare you for the more powerful, and more
complicated, LinkedList class. The application of the LinkedList class, a line editor, takes advantage
of a LinkedList iterator’s ability to insert or remove in constant time.
CHAPTER OBJECTIVES
1. Be able to develop new methods for the SinglyLinkedList class.
2. Understand the LinkedList class from a user’s perspective.
3. Given an application that requires a list, be able to decide whether an ArrayList or a
LinkedList would be more appropriate.
4. Compare several choices of fields for the LinkedList class and, for each choice, be able to
create a LinkedList object.
Each element is contained in an object, called an Entry object, which also includes a reference, called a link ,
to the Entry object that contains the next element in the list.
For the Entry object that holds the last element, there is no “next” element.
For example, Figure 7.1 shows part of a linked list.
Some linked lists also satisfy the following property:
Each Entry object includes a link to the Entry object that contains the previous element in the list.
267
268 C H A P T E R 7 Linked Lists
A linked list that satisfie the second property is called a doubly-linked list. Otherwise, it is called a
singly-linked list. For example, Figure 7.2 shows part of a doubly-linked list with three elements, and they
happen to be in alphabetical order.
We have intentionally omitted any indication of how the f rst and last elements are identified and what
is stored in their previous and next links, respectively. In Section 7.3, we’ll see that there are several options.
Most of this chapter is devoted to doubly-linked lists, but we will start by studying singly-linked
lists because, as you might imagine, they are easier to develop (they are also less powerful).
The next fiel in an Entry holds a reference to another Entry object. A reference to an Entry object is
called a link . For example, Figure 7.3 depicts a sequence of linked entries; each element is a (reference
to a) String object. We use an arrow to indicate that the next fiel at the base of the arrow contains
7.2 The SinglyLinkedList Class—A Singly-Linked, Toy Class! 269
a reference to the Entry object pointed to by the tip of the arrow. And, for the sake of simplicity, we
pretend that the type of element is String rather than reference-to-String. In the last Entry object,
the next fiel has the value null, which indicates that there is no subsequent Entry object.
The Entry class will be embedded in the SinglyLinkedList class. A class that is embedded in
another class is called a nested class. This embedding allows the SinglyLinkedList class to access the
two f elds in an Entry object directly (a good thing, too, because the Entry class has no methods). The
Entry class has protected visibility for the sake of future subclasses of SinglyLinkedList.
The SinglyLinkedList class will implement the Collection interface in the Java Collections
Framework. As with all other Collection classes, the SinglyLinkedList class is parameterized, with
E as the type parameter:
public class SinglyLinkedList<E> extends AbstractCollection<E>
implements List<E>
We need not provide realistic implementations for each of the abstract methods in the List interface. For
methods we are not interested in, their definition will simply throw an exception. To start with, we will
implement only f ve methods: a default constructor, isEmpty, addToFront, size, and contains. Here
are the method specifications
1. Default constructor
/**
* Initializes this SinglyLinkedList object to be empty, with elements to be of
* type E.
*
*/
public SinglyLinkedList()
Note. Saying that a SinglyLinkedList object is empty means that the collection has no elements
in it.
2. The isEmpty method
/**
* Determines if this SinglyLinkedList object has no elements.
*
* @return true – if this SinglyLinkedList object has no elements; otherwise,
* false.
*
*/
public boolean isEmpty ()
Note 1. Elements are inserted only at the front of a SinglyLinkedList object (This allows for
a simpler implementation). For example, suppose the SinglyLinkedList object referenced by
myLinked consists of “yes”, “no”, and “maybe” in that order, and the message is
myLinked.addToFront ("simple");
Then the SinglyLinkedList object referenced by myLinked will consist of “simple”, “yes”, “no”,
and “maybe” in that order.
Note 2. The method identifie addToFront is used instead of add because the add (E element)
in the List interface specifie that element must be inserted at the end of the list, and that is
somewhat more diff cult than inserting at the front.
4. The size method
/**
* Determines the number of elements in this SinglyLinkedList object.
* The worstTime(n) is O(n).
*
* @return the number of elements.
*
*/
public int size ()
Example Suppose the SinglyLinkedList object referenced by myLinked consists of the elements
‘‘simple’’, ‘‘yes’’, ‘‘no’’, and ‘‘maybe’’ in that order. If the message is
System.out.println (myLinked.size ());
Note. The user of this method is responsible for ensuring that the equals method is explicitly define
for the class that includes obj and the elements in the SinglyLinkedList. Otherwise, as noted in
Section 2.7, the Object class’s version of equals will be applied:
public boolean equals (Object obj)
{
return (this == obj);
}
This methods test whether the reference to the calling object contains the same address as the reference
obj. Because equality-of-references is tested instead of equality-of-elements, false will be returned
if the calling-object reference and obj are references to distinct but identical objects!
Here, from the book’s website, is a test suite for these methods:
import org.junit.*;
import static org.junit.Assert.*;
import org.junit.runner.Result;
import static org.junit.runner.JUnitCore.runClasses;
import java.util.*;
@Before
public void runBeforeEachTest()
{
list = new SinglyLinkedList<String>();
} // method runBeforeEachTest
@Test
public void testSize1()
{
assertEquals (0, list.size());
} // method testSize1
272 C H A P T E R 7 Linked Lists
@Test
public void testAdd()
{
list.addToFront ("Greg");
list.addToFront ("Brian");
list.addToFront ("Berkin");
assertEquals ("[Berkin, Brian, Greg]", list.toString());
// Note: AbstractCollection implements toString()
} // testAdd
@Test
public void testSize2()
{
list.addToFront ("Greg");
list.addToFront ("Brian");
list.addToFront ("Berkin");
assertEquals (3, list.size());
} // testSize2
@Test
public void testContains1()
{
list.addToFront ("Greg");
list.addToFront ("Brian");
list.addToFront ("Berkin");
assertEquals (true, list.contains("Brian"));
} // testContains1
@Test
public void testContains2()
{
list.addToFront ("Greg");
list.addToFront ("Brian");
list.addToFront ("Berkin");
assertEquals (false, list.contains("Jack"));
} // testContains2
@Test
public void testContains3()
{
list.addToFront ("Greg");
list.addToFront ("Brian");
list.addToFront ("Berkin");
assertEquals (false, list.contains(7));
} // testContains2
} // class SinglyLinkedTest
All tests failed initially, with the usual stub (throw new UnsupportedOperationExcep
tion( );) for each SinglyLinkedList method.
7.2 The SinglyLinkedList Class—A Singly-Linked, Toy Class! 273
These few methods do not provide much in the way of functionality: we cannot remove an element
and, what’s worse, we cannot even retrieve the elements. But we have enough to consider field and
method definitions
To make scoreList a reference to an empty SinglyLinkedList object, all the default constructor has
to do is to initialize the head fiel to null. Since a reference fiel is automatically initialized to null,
we need not defin the default constructor, but we will do so for the sake of being explicit:
public SinglyLinkedList()
{
head = null;
} // default constructor
Now we can move on to the definition of the isEmpty, addToFront, size, and contains methods.
How can the isEmpty() method determine if the list has no elements? By testing the head field
public boolean isEmpty ()
{
return head == null;
} // method isEmpty
The definitio of the addToFront (E element) method is not quite so easy to develop. For inspiration,
suppose we add a fourth element to the front of a singly-linked list consisting of the three elements from
Figure 7.3. Figure 7.4 shows the picture before the fourth element is added.
According to the method specificatio for the addToFront method, each new element is inserted at
the front of a SinglyLinkedList object. So if we now add “calm” to the front of this list, we will get
the list shown in Figure 7.5.
In general, how should we proceed if we want to insert the element at the front of the calling
SinglyLinkedList object? We start by constructing a new Entry object and assigning (a reference to)
the new element to that Entry object’s element field What about the Entry object’s next field The
reference we assign to the next fiel should be a reference to what had been the f rst Entry before this
head
head
calm
FIGURE 7.5 The SinglyLinkedList object from Figure 2.5 after inserting “calm” at the front of the list
call to addToFront. In other words, we should assign head to the next fiel of the new Entry object.
Finally, we adjust head to reference the new Entry object. The complete definitio is:
public void addToFront (E element)
{
Entry<E> newEntry = new Entry<E>();
newEntry.element = element;
newEntry.next = head;
head = newEntry;
} // method addToFront
Figures 7.6a through 7.6d show the effect of executing the firs four statements in this method when “calm”
is inserted at the front of the SinglyLinkedList object shown in Figure 7.4.
For the definitio of the size method, we initialize a local int variable, count, to 0 and a local
Entry reference, current, to head. We then loop until current is null, and increment count and
current during each loop iteration. Incrementing count is a familiar operation, but what does it mean to
head
mellow serene peaceful null
newEntry
null null
FIGURE 7.6a The f rst step in inserting “calm” at the front of the SinglyLinkedList object of Figure 7.4:
constructing a new Entry object (whose two fi lds are automatically pre-initialized to null)
head
mellow serene peaceful null
newEntry
calm null
FIGURE 7.6b The second step in inserting “calm” at the front of the SinglyLinkedList object of Figure 7.4:
assigning the object-reference element to the element fi ld of the newEntry object
7.2 The SinglyLinkedList Class—A Singly-Linked, Toy Class! 275
head
mellow serene peaceful null
newEntry
calm
FIGURE 7.6c The third step in inserting “calm” at the front of the SinglyLinkedList object of Figure 7.4:
assigning head to the next fi ld of the newEntry object
head
newEntry
calm
FIGURE 7.6d The fourth step in inserting “calm” at the front of the SinglyLinkedList object of Figure 7.4.
The SinglyLinkedList object is now as shown in Figure 7.5
“increment current ”? That means to change current so that current will reference the next Entry
after the one current is now referencing. That is, we set
current = current.next;
The loop goes through the entire SinglyLinkedList object, and so worstTime(n) is linear in n (as is
averageTime(n)). Note that if we add a size fiel to the SinglyLinkedList class, the definitio of the
size() method becomes a one-liner, namely,
return size;
But then the definitio of the addToFront method would have to be modifie to maintain the value of
the size field See Programming Exercise 7.7.
Finally, for now, we develop the contains method. The loop structure is similar to the one in
the definitio of the size method, except that we need to compare element to current.element. For
the sake of compatibility with the LinkedList class in the Java Collections Framework, we allow null
276 C H A P T E R 7 Linked Lists
elements in a SinglyLinkedList object. So we need a separate loop for the case where element is
null. Here is the code:
1 In Project 7.1, two additional f elds are added to the SinglyLinkedListIterator class.
7.2 The SinglyLinkedList Class—A Singly-Linked, Toy Class! 277
We will fully implement only three methods: a default constructor, next() and hasNext(). The
remove() method will simply throw an exception. Here is an outline of the class:
/**
* Initializes this SinglyLinkedListIterator object
*
*/
protected SinglyLinkedListIterator()
{
...
} // default constructor
/**
* Determines if this Iterator object is positioned at an element in this
* SinglyLinkedIterator object.
*
* @return true - if this Iterator object is positioned at an element;
* otherwise, false.
*/
public boolean hasNext()
{
...
} // method hasNext
/**
* Returns the element this Iterator object was (before this call)
* positioned at, and advances this Iterator object.
*
* @return - the element this Iterator object was positioned at.
*
* @throws NullPointerException – if this Iterator object was
* not postioned at an element before this call.
*/
public E next()
{
...
} // method next
Note that the SinglyLinkedIterator class has E as its type parameter because SinglyLinkedList
Iterator implements Iterator<E>.
In definin the three methods, there are three “next”s we will be dealing with:
You will be able to determine the correct choice based on the context—and the presence or absence of
parentheses.
An interface does not have any constructors because an interface cannot be instantiated, so the
Iterator interface had no constructors. But we will need a constructor for the SinglyLinkedList
Iterator class. Otherwise, the compiler would generate a default constructor and the Java Virtual
Machine would simply, but worthlessly, initialize the next fiel to null. What should the constructor
initialize the next fiel to? Where we want to start iterating? At the head of the SinglyLinkedList:
protected SinglyLinkedListIterator()
{
next = head;
} // default constructor
This method can access head because the SinglyLinkedListIterator class is embedded in the
SinglyLinkedList class, where head is a f eld.
The hasNext() method should return true as long as the next fiel (in the SinglyLinkedList
Iterator class, not in the Entry class) is referencing an Entry object:
The definitio of the remove() method will simply throw an exception, so all that remains is the definitio
of the next() method. Suppose we have a SinglyLinkedList of two String elements and the default
constructor for the SinglyLinkedListIterator has just been called. Figure 7.7 shows the current
situation (as usual, we pretend that a String object itself, not a reference, is stored in the element fiel
of an Entry object):
head
Karen Marie null
next
FIGURE 7.7 The contents of the next fi ld in the SinglyLinkedListIterator class just after the
SinglyLinkedListIterator’s constructor is called
7.2 The SinglyLinkedList Class—A Singly-Linked, Toy Class! 279
Since we are just starting out, what element should the next() method return? The element returned
should be “Karen”, that is, next.element. And then the next fiel should be advanced to point to
the next Entry object That is, the SinglyLinkedListIterator’s next fiel should get the reference
stored in the next fiel of the Entry object that the SinglyLinkedListIterator’s next fiel is
currently pointing to. We can’t do anything after a return, so we save next.element before advancing
next, and then we return (a reference to) the saved element. Here is the definition
public E next()
{
E theElement = next.element;
next = next.next;
return theElement;
} // method next
Now that we have a SinglyLinkedListIterator class, we can work on the problem of iterating through
a SinglyLinkedList object. First, we have to associate a SinglyLinkedListIterator object with
a SinglyLinkedList object. The iterator() method in the SinglyLinkedList class creates the
necessary connection:
/**
* Returns a SinglyLinkedListIterator object to iterate over this
* SinglyLinkedList object.
*
*/
public Iterator<E> iterator()
{
return new SinglyLinkedListIterator();
} // method iterator
The value returned is a (reference to a) SinglyLinkedListIterator. The specifie return type has
to be Iterator<E> because that is what the iterator() method in the Iterator interface calls for.
Any class that implements the Iterator interface—such as SinglyLinkedListIterator —can be the
actual return type.
With the help of this method, a user can create the appropriate iterator. For example, if myLinked
is a SinglyLinkedList object of Boolean elements, we can do the following:
Iterator<Boolean> itr = myLinked.iterator();
The variable itr is a polymorphic reference: it can be assigned a reference to an object in any class
(for example, SinglyLinkedListIterator) that implements the Iterator<Boolean> interface. And
myLinked.iterator() returns a reference to an object in the SinglyLinkedListIterator class,
specifically to an object that is positioned at the beginning of the myLinked object.
The actual iteration is straightforward. For example, to print out each element:
while (itr.hasNext ())
System.out.println (itr.next ());
Now that we have added a new method to the SinglyLinkedList class, we need to test this method, and
that entails testing the next() method in the SinglyLinkedListIterator class. The book’s website
includes these tests. The other methods in the SinglyLinkedClass were re-tested, and passed all tests.
For a complete example of iteration, the following program method reads in a non-empty list of
grade-point-averages from the input, stores each one at the front of a SinglyLinkedList object, and
then iterates through the collection to calculate the sum. Finally, the average grade-point-average is printed.
import java.util.*;
double oneGPA,
sum = 0.0;
while (true)
{
try
{
System.out.print (INPUT_PROMPT);
oneGPA = sc.nextDouble();
if (oneGPA == SENTINEL)
break;
gpaList.addToFront (oneGPA);
} // try
catch (Exception e)
{
System.out.println (e);
sc.nextLine();
} // catch
7.3 Doubly-Linked Lists 281
} // while
for (Double gpa : gpaList)
sum += gpa;
if (gpaList.size() > 0)
System.out.println(AVERAGE_MESSAGE + (sum/gpaList.size ()));
else
System.out.println (NO_VALID_INPUT);
} // method run
} // class SinglyLinkedExample
Also, there are several Programming Exercises and a Programming Project related to the
SinglyLinkedList class. But now that you have some familiarity with links, we turn to the
focal point of this chapter: doubly-linked lists.
First we need to get a reference to the Entry object that holds “serene”; that will take linear-in-n time,
on average, where n is the size of the linked list. After that, as shown in Figure 7.8, the insertion entails
constructing a new Entry object, storing “placid” as its element, and adjusting four links (the previous and
next links for “placid”, the next link for the predecessor of “serene”, and the previous link for “serene”).
In other words, once we have a reference to an Entry object, we can insert a new element in front of that
Entry object in constant time.
The process for removal of an element in a doubly-linked list is similar. For example, suppose we
want to remove “mellow” from the partially shown linked list in Figure 7.8. First, we get a reference
to the Entry object that houses “mellow”, and this takes linear-in-n time, on average. Then, as shown
in Figure 7.9, we adjust the next link of the predecessor of “mellow” and the previous link of the
successor of “mellow”. Notice that there is no need to adjust either of the links in the Entry object that
houses “mellow” because that object is not pointed to by any other Entry object’s links.
The bottom line in the previous discussion is that it takes linear-in-n time to get a reference to
an Entry object that houses an element, but once the reference is available, any number of insertions,
removals or retrievals can be accomplished in constant time for each one.
282 C H A P T E R 7 Linked Lists
placid
FIGURE 7.8 The partially shown doubly-linked list from Figure 7.2 after “placid” is inserted in front of “serene”
placid
FIGURE 7.9 The partially shown linked list from Figure 7.8 after “mellow” removed
Now that you have a rough idea of what a doubly-linked list looks like and can be manipulated, we
are ready to study the LinkedList class, the Java Collection Framework’s design and implementation of
doubly-linked lists. First as always, we start with a user’s perspective, that is, with the method specifications
public LinkedList()
public LinkedList (Collection<? extends E> c )
public boolean add (E element) // element inserted at back; worstTime(n) is constant
public void add (int index, E element)
public void addAll (Collection<? extends E> c)
public boolean addAll (int index, Collection<? extends E> c)
public boolean addFirst (E element)
public boolean addLast (E element)
public void clear( ) // worstTime(n) is O(n)
public Object clone()
public boolean contains (Object obj)
public boolean containsAll (Collection<?> c)
public E element()
public boolean equals (Object obj)
public E get (int index) // worstTime(n) is O(n)
public E getFirst ()
public E getLast ()
public int hashCode()
public int indexOf (Object obj)
public boolean isEmpty()
public Iterator<E> iterator()
public int lastIndexOf (Object obj)
public ListIterator<E> listIterator() // iterate backward or forward
public ListIterator<E> listIterator (final int index)
public boolean offer (E element)
public E peek()
public E poll()
public E pop()
public void push (E e)
public E remove()
public boolean remove (Object obj)
public E remove (int index)
public boolean removeAll (Collection<?> c)
public E removeFirst() // worstTime(n) is constant
public E removeLast() // worstTime(n) is constant
public boolean retainAll (Collection<?> c)
public E set (int index, E element) // worstTime(n) is O(n)
public int size( )
public List<E> subList (int fromIndex, int toIndex)
public Object[ ] toArray( )
public T[ ] toArray (T[ ] a)
public String toString()
FIGURE 7.10 Method headings for the public methods in the LinkedList class. Except for the constructors,
the headings are in alphabetical order by method identifie
And this last message takes only constant time. But if myList were a non-empty ArrayList object, the
same message,
myList.remove (0)
There are some other performance differences between LinkedList objects and ArrayList objects.
Here are method specification of some other LinkedList methods that have different worst times than
their ArrayList counterparts.
1. The one-parameter add method
/**
* Appends a specified element to (the back of) this LinkedList object.
*
* @param element – the element to be appended.
*
* @return true – according to the general contract of the Collection interface’s
* one-parameter add method.
*
*/
public boolean add (E element)
Note. The worstTime(n) is constant. With an ArrayList object, the worstTime(n) is linear in n
for the one-parameter add method, namely, when the underlying array is at full capacity. This
represents a significan difference for a single insertion. For multiple back-end insertions, the time
estimates for the ArrayList and LinkedList classes are similar. Specifically for n back-end
insertions, worstTime(n) is linear in n for both the ArrayList class and the LinkedList class.
And averageTime(n), for a single call to add, is constant for both classes.
The LinkedList object fruits now contains, in order, ‘‘apples’’, ‘‘kumquats’’, ‘‘durian’’, and ‘‘limes’’.
Note. This method represents a major disadvantage of the LinkedList class compared to the
ArrayList class. As noted in the method specification the LinkedList version of this method has
worstTime(n) in O(n)—in fact, worstTime(n) is linear in n in the current implementation. But for
7.3 Doubly-Linked Lists 285
the ArrayList version, worstTime(n) is constant. So if your application has a preponderance of list
accesses, an ArrayList object is preferable to a LinkedList object.
Example Suppose the LinkedList object fruits consists of ‘‘apples’’, ‘‘kumquats’’, ‘‘durian’’,
and ‘‘limes’’, in that order. Then the message
fruits.get (1)
Example Suppose the LinkedList object fruits consists of ‘‘apples’’, ‘‘kumquats’’, ‘‘durian’’,
and ‘‘limes’’, in that order. We can change (and print) the element at index 2 with the following:
The elements in the LinkedList object fruits are now ‘‘apples’’, ‘‘kumquats’’, ‘‘kiwi’’, and ‘‘limes’’,
and the output will be ‘‘durian’’.
When we looked at the ArrayList class, iterators were ignored. That neglect was due to the fact
that the random-access property of ArrayList objects allowed us to loop through an ArrayList
object in linear time by using indexes. LinkedList objects do not support random access (in constant
time), so iterators are an essential component of the LinkedList class.
There are two LinkedList methods that return a (reference to a) ListIterator object, that is, an
object in a class that implements the ListIterator interface. Their method specification are as follows:
1. The start-at-the-beginning listIterator method
/**
* Returns a ListIterator object positioned at the beginning of this LinkedList
* object.
*
* @return a ListIterator object positioned at the beginning of this LinkedList
* object.
*
*/
public ListIterator<E> listIterator()
Example Suppose that fruits is a LinkedList object. Then we can create a ListItr object to
iterate through fruits as follows:
Example Suppose the LinkedList object fruits consists of ‘‘apples’’, ‘‘kumquats’’, ‘‘durian’’,
and ‘‘limes’’, in that order. The following statement creates a ListIterator object positioned at
‘‘durian’’:
Figure 7.11 has the method headings for all of the methods in the ListItr class. We will look at some
of the details—from a user’s viewpoint—of these methods shortly.
We can iterate forwardly with an enhanced for statement (or the pair hasNext() and next()), just
as we did with the SinglyLinkedList class in Section 7.2.2. For example, suppose the LinkedList
object fruits consists of “kumquats”, “bananas”, “kiwi”, and “apples”, in that order. We can iterate
through fruits from the firs element to the last element as follows:
for (String s : fruits)
System.out.println (s);
7.3 Doubly-Linked Lists 287
FIGURE 7.11 Method headings for all of the public methods in the ListItr class. For each method,
worstTime(n) is constant !
For backward iterating, there is a hasPrevious() and previous() pair. Here are their method specifi
cations:
IT1. The hasPrevious method
/**
* Determines whether this ListIterator object has more elements when traversing
* in the reverse direction.
*
* @return true – if this ListIterator object has more elements when traversing
* in the reverse direction; otherwise, false.
*
*/
public boolean hasPrevious()
Example Suppose the LinkedList object fruits consists of the elements ‘‘kumquats’’,
‘‘bananas’’, ‘‘kiwi’’, and ‘‘apples’’, in that order. The output from
ListIterator<String> itr = listIterator (2);
System.out.println (itr.hasPrevious());
will be
true
will be
false
288 C H A P T E R 7 Linked Lists
Example Suppose the LinkedList object fruits consists of ‘‘kumquats’’, ‘‘bananas’’, ‘‘kiwi’’,
and ‘‘apples’’, in that order. If we have
ListIterator<String> itr = fruits.listIterator();
System.out.println (itr.next() + " " + itr.next() + " " + itr.previous());
Think of the “current” position in a LinkedList object as the index where the ListIterator is
positioned. Here is how the next() and previous() methods are related to the current position:
• The next() method advances to the next position in the LinkedList object, but returns the
element that had been at the current position before the call to next().
• The previous() method firs retreats to the position before the current position, and then returns
the element at that retreated-to position.
The next() method is similar to the post-increment operator ++, and the previous() method is
similar to the pre-decrement operator --. Suppose, for example, we have
int j = 4,
k = 9;
System.out.println (j++);
System.out.println (--k);
apples
kiwi
bananas
kumquats
Of course, the LinkedList object fruits has not changed. It still consists of “kumquats”,
“bananas”, “kiwi”, and “apples”, in that order.
We can do even more. The ListItr class also has add, remove and set methods. Here are
the method specification and examples (as usual, E is the type parameter representing the class
of the elements in the LinkedList object):
IT3. The add method
/**
* Inserts a specified element into the LinkedList object in front of (before) the element
* that would be returned by next(), if any, and in back of (after) the element that would
* be returned by previous(), if any. If the LinkedList object was empty before this
* call, then the specified element is the only element in the LinkedList object.
*
* @param element – the element to be inserted.
*
*/
public void add (E element)
Example Suppose the LinkedList object fruits consists of ‘‘kumquats’’, ‘‘bananas’’, ‘‘kiwi’’,
and ‘‘apples’’, in that order. We can insert repeatedly insert ‘‘pears’’ after each element in fruits
as follows:
During the f rst iteration of the above while loop, the call to next() returns “kumquats” and
(before returning) advances to “bananas”. The f rst call to add ("pears") inserts “pears” in front
of “bananas”. During the second iteration, the call to next() returns “bananas” and advances to
“kiwi”. The second call to add ("pears") inserts “pears” in front of “kiwi”. And so on. At the
completion of the while statement, the LinkedList object fruits consists of
Note. If the ListItr is not positioned at any element (for example, if the LinkedList object is
empty), each call to the ListIterator class’s add method will insert an element at the end of
the LinkedList object.
290 C H A P T E R 7 Linked Lists
Example Suppose the LinkedList object fruits consists of ‘‘kumquats’’, ‘‘pears’’, ‘‘bananas’’,
‘‘pears’’, ‘‘kiwi’’, ‘‘pears’’, ‘‘apples’’, and ‘‘pears’’, in that order. We can remove every other element
from fruits as follows:
Now fruits consists of ‘‘kumquats’’, ‘‘bananas’’, ‘‘kiwi’’, and ‘‘apples’’, in that order. If we eliminate
the if statement from the above loop, every element except the first element will be removed.
Example Suppose the LinkedList object fruits consists of ‘‘kumquats’’, ‘‘bananas’’, ‘‘kiwi’’,
and ‘‘apples’’, in that order. We can iterate through fruits and capitalize the first letter of each fruit
as follows:
7.3 Doubly-Linked Lists 291
String aFruit;
char first;
while (itr.hasNext())
{
aFruit = itr.next();
first = Character.toUpperCase (aFruit.charAt (0));
aFruit = first + aFruit.substring (1); // substring from index 1 to end
itr.set (aFruit);
} // while
The LinkedList object fruits now consists of ‘‘Kumquats’’, ‘‘Bananas’’, ‘‘Kiwi’’, and ‘‘Apples’’.
Programming Exercise 7.4 considers all possible sequences of calls to the add, next, and remove
methods in the ListItr class.
As noted in Figure 7.11, all of the ListItr methods take only constant time. So if you iterate through
a LinkedList object, for each call to the ListItr object’s add or remove method, worstTime(n) is
constant. With an ArrayList object, for each call to add (int index, E element) or remove (int
index), worstTime(n) is linear in n. And the same linear worst-time would apply for adding and removing
if you decided to iterate through an ArrayList. The bottom line here is that a LinkedList object is
faster than an ArrayList object when you have a lot of insertions or removals.
What if you need to access or replace elements at different indexes in a list? With an ArrayList
object, for each call to get (int index) or set (int index, E element), worstTime(n) is constant.
With a LinkedList object, for each call to get (int index) or set (int index, E element),
worstTime(n) is linear in n. If instead, you iterate through a LinkedList object, and use the ListItr
methods next() and set (E element) for accessing and replacing elements, each iteration takes linear-
in-n time. So if the elements to be accessed or replaced are at indexes that are far apart, an ArrayList
object will be faster than a LinkedList object.
To summarize the above discussion:
If a large part of the application consists of iterating through a list and making insertions and/or
removals during the iterations, a LinkedList object can be much faster than an ArrayList
object.
If the application entails a lot of accessing and/or replacing elements at widely varying indexes,
an ArrayList object will be much faster than a LinkedList object.
import java.util.*;
import java.io.*;
String inFilePath,
word;
try
{
System.out.print ("\n\nPlease enter the path for the input file: ");
inFilePath = keyboardScanner.nextLine();
fileScanner = new Scanner (new File (inFilePath));
while (fileScanner.hasNext())
aList.add (fileScanner.next());
System.out.print ("\nPlease enter the word you want to search for: ");
word = keyboardScanner.next();
if (aList.indexOf (word) >= 0)
System.out.println (word + " was found.\n\n");
else
System.out.println (word + " was not found.\n\n");
else if (removalCount == 1)
System.out.println ("The only instance of " + word +
" was removed.\n\n");
else
System.out.println ("All " + removalCount + " instances of " +
word + " were removed.\n\n");
System.out.print (
"Please enter the word you want to convert to upper case: ");
word = keyboardScanner.next();
String currentWord;
boolean found = false;
itr = aList.listIterator();
while (itr.hasNext() && !found)
{
currentWord = itr.next();
if (word.equals (currentWord))
{
itr.set (word.toUpperCase());
System.out.println (word +
" was converted to upper case.\n\n");
found = true;
} // found word to convert to upper case
} // while
if (!found)
System.out.println (word +
" was not found, so not upper-cased.\n\n");
System.out.println ("Here is the final version:\n" + aList);
} // try
catch (IOException e)
{
System.out.println (e);
} // catch
} // method run
} // class LinkedListExample
For removing all instances of a word, the iterator-based version above is clearly faster than repeatedly
invoking aList.remove (word). For converting a word to upper case, the iterator-based version above
requires only one iteration. The version in Section 6.2.2 requires two iterations: one to get the index,
in aList, of the word to by upper cased, and one more for the call to aList.set (index, word.
toUpperCase()).
294 C H A P T E R 7 Linked Lists
Now that you have seen both the ArrayList and LinkedList classes, you can run a timing experiment
on them.
In Section 7.3.5, we briefl look at a developer’s view of the LinkedList class. Specifically we compare
various alternatives for the field in the LinkedList class. For the choice made in the Java Collections
Framework, we develop a LinkedList object, and then, to give you the f avor of that implementation,
we investigate the definitio of the two-parameter add method.
The embedded Entry class had two f elds, an element and a reference to the next entry:
protected E element;
Clearly, it will take linear-in-n time to add an element to the back of a SinglyLinkedList object. We can
get around this diff culty by adding to the SinglyLinkedList class a tail fiel that holds a reference
to last entry in a SinglyLinkedList object. Figure 7.12 shows an example of a SinglyLinkedList
object with these f elds.
We can now defin the addLast method without much diff culty (see Programming Exercise 7.3.a).
Implementing the removeLast presents a much more serious problem. We would need to change the
(reference stored in the) next fiel of the Entry object preceding the Entry object referenced by tail.
And for that task, a loop is needed, so worstTime(n) would be linear in n. That would violate the
performance requirement of the removeLast method that specifie worstTime (n) must be constant.
7.3 Doubly-Linked Lists 295
head tail
Betsy Don Eric null
So we must abandon a singly-linked implementation of the LinkedList class because of the given
performance specifications But the idea mentioned previously—having head and tail field —suggests
a viable alternative. The nested Entry class will support a doubly-linked list by having three fields
protected E element;
Figure 7.13 shows this doubly-linked version of the three-element list from Figure 7.12.
head tail
null Betsy Don Eric null
With this version, we can implement the LinkedList class with method definition that satisfy
the given performance specifications You will get to f esh out the details of the doubly-linked, head&tail
implementation if you undertake Project 7.4.
The Java Collection Framework’s implementation of the LinkedList class is doubly-linked, but
does not have head and tail fields Instead, there is a header field which contains a reference to a
special Entry object, called a “dummy entry” or “dummy node.” We will discuss the significanc of the
dummy entry shortly. The class starts as follows:
public class LinkedList<E> extends AbstractSequentialList<E>,
implements List<E>,
Queue<E>,
java.lang.Cloneable,
java.io.Serializable
{
private transient int size = 0;
The size fiel keeps track of the number of elements in the calling LinkedList object. As noted in
Chapter 6, the transient modifie merely indicates that this fiel is not saved if the elements in a
LinkedList object are serialized, that is, saved to an output stream. (Appendix 1 discusses serialization.)
The nested Entry class has three f elds, one for an element, and two for links. The only method in
the Entry class is a constructor that initializes the three fields Here is the complete Entry class
296 C H A P T E R 7 Linked Lists
The element fiel will hold (a reference to) the Entry object’s element; next will contain a reference
to the Entry one position further in the LinkedList object, and previous will contain a reference to
the Entry one position earlier in the LinkedList object.
Under normal circumstances, an object in a nested class has implicit access back to the enclosing
object. For example, the nested ListItr class accesses the header fiel of the enclosing LinkedList
object. But if the nested class is declared to be static, no such access is available. The Entry class is
a stand-alone class, so it would have been a waste of time and space to provide such access.
We can now make sense of the definitio of the header fiel in the LinkedList class. That fiel
initially references an Entry in which all three f elds are null; see Figure 7.14.
The header fiel always points to the same dummy entry, and the dummy entry’s element fiel
always contains null. The next fiel will point to the Entry object that houses the f rst element in the
LinkedList object, and the previous fiel will point to the Entry object that houses the last element in
the LinkedList object. Having a dummy entry instead of head and tail field ensures that every Entry
object in a linked list will have both a previous Entry object and a next Entry object. The advantage to
this approach is that insertions and removals can be made without making a special case for the f rst and
last elements in the list.
As shown in Figure 7.15, the default constructor makes the previous and next field in the dummy
entry point to the dummy entry itself. It turns out that this simplifie the definition of the methods that
insert or delete elements.
Next, we append an element to that empty LinkedList object:
names.add ("Betsy");
7.3 Doubly-Linked Lists 297
null
names.size
0
At this point, “Betsy” is both the firs element in names and the last element in names. So the dummy
entry both precedes and follows the Entry object that houses “Betsy”. Figure 7.16 shows the effect of the
insertion.
In general, adding an element at the end of a LinkedList object entails inserting the corresponding
Entry object just before the dummy entry. For example, suppose the following message is now sent to
the LinkedList object in Figure 7.16:
names.add ("Eric");
What is the effect of appending “Eric” to the end of the LinkedList object names? Eric’s Entry object
will come before the dummy entry and after Betsy’s Entry object. See Figure 7.17.
names.
header
null Betsy
names.size
FIGURE 7.16 The effect of inserting “Betsy” at the back of the empty LinkedList object in Figure 7.15
names.
header
null Betsy Eric
names.size
2
FIGURE 7.17 A two-element LinkedList object. The firs element is “Betsy” and the second element is “Eric”
298 C H A P T E R 7 Linked Lists
As you can see from Figure 7.17, a LinkedList object is stored circularly. The dummy entry
precedes the f rst entry and follows the last entry. So we can iterate through a LinkedList object in the
forward direction by starting at the firs entry and repeatedly calling the next() method until we get to
the dummy entry. Or we can iterate through a LinkedList object in the reverse direction by starting at
the dummy entry and repeatedly calling the previous() method until we get to the firs entry.
Finally, let’s see what happens when the two-parameter add method is invoked. Here is a sample call:
names.add (1, "Don");
To insert “Don” at index 1, we need to insert “Don” in front of “Eric”. To accomplish this, we need to
create an Entry object that houses “Don”, and adjust the links so that Entry object follows the Entry
object that houses “Betsy” and precedes the Entry object that houses “Eric”. Figure 7.18 shows the result.
names.
header
null Betsy Don Eric
names.size
3
FIGURE 7.18 The LinkedList object from Figure 7.17 after the insertion of “Don” in front of “Eric” by the
call names.add (1, "Don")
From the above examples, you should note that when an element is inserted in a LinkedList object,
no other elements in the list are moved . In fact, when an element is appended with the LinkedList class’s
one-parameter add method, there are no loops or recursive calls, so worstTime(n) is constant. What about
an insertion at an index? Section 7.4 investigates the definitio of the two-parameter add method.
For inserting an element at position index, the hard work is getting a reference to the Entry object that is
currently at position index. This is accomplished—in the private method entry —in a loop that starts
at header and moves forward or backward, depending on whether index < size/2.
Once a reference, e, to the appropriate Entry has been obtained, element is stored in a new
entry that is put in front of e by adjusting a few previous and next references. These adjustments are
accomplished in the private addBefore method:
/**
* Inserts an Entry object with a specified element in front of a specified Entry object.
*
* @param element – the element to be in the inserted Entry object.
* @param e – the Entry object in front of which the new Entry object is to be
* inserted.
*
* @return – the Entry object that houses the specified element and is in front
* of the specified Entry object.
*
*/
private Entry<E> addBefore (E element, Entry<E> e)
{
Entry<E> newEntry = new Entry<E>(element, e, e.previous);// insert newEntry in
// front of e
newEntry.previous.next = newEntry; // make newEntry follow its predecessor
newEntry.next.previous = newEntry; // make newEntry precede its successor, e
size++;
modCount++; // discussed in Appendix 1
return newEntry;
}
The bottom line in all of this is that to insert an Entry object in front of another Entry object, worstTime(n)
is constant, but to get (a reference to) the Entry object at a given index, worstTime(n) is linear in n
(because of the loop in the entry method). The insertion is accomplished by adjusting references, not by
moving elements.
The actual definitio of the two-parameter add method is a one-liner:
public void add (int index, A element)
{
addBefore(element, (index==size ? header : entry(index)));
}
300 C H A P T E R 7 Linked Lists
The ‘?’ and ‘:’ are part of the shorthand for the usual if/else statement2 . The advantage of having a
dummy entry is that every entry, even the f rst or last entry, has a predecessor and a successor, so there
is no need for a special case to insert at the front or back of a LinkedList object.
The f ow of the remove (int index) method is similar to that of add (int index, E
element). We f rst get a reference, e, to the Entry object at position index, and then adjust the
predecessor and successor of e’s Entry.
As an application of the LinkedList class, we develop a line editor in Section 7.5.
$Insert
Water, water every where,
And all the boards did shrink;
Water, water every where,
Nor any drop to drink.
This can be read as “If first is greater than second, assign to big the value of first. Otherwise, assign to big the value of second.”
The syntax for a conditional expression is:
The semantics is this: if the condition has the value true, then the value of the conditional expression is the value of expression_t.
Otherwise, the value of the conditional expression is the value of expression_f. If you like to write cryptic code, you’ll love the conditional
operator, one of the legacies that Java got from C. Note that it is the only ternary operator in Java. That means it has three operands.
7.4 Application: A Line Editor 301
Then after the insertions, the text would be as follows, with a caret ‘> ’ indicating the current line:
Now is the
time for
>citizens to come to
the
aid of their country.
The sequence
$Insert
all
good
Now is the
time for
all
good
>citizens to come to
the
aid of their country.
2. $Delete m n
Each line in the text between lines m and n, inclusive, will be deleted. The current line becomes the
firs line after the last line deleted. So if the last line of text is deleted, the current line is beyond any
line in the text.
For example, suppose the text is
Now is the
time for
all
>good
citizens to come to
the
aid of their country.
>the
aid of their country.
If the next command is
$Delete 3 3
then the text becomes:
Now is the
time for
the
>
The following error messages should be printed when appropriate:
Error: The f rst line number is greater the second.
Error: The f rst line number is less than 0.
Error: The second line number is greater than the last line number.
Error: The command should be followed by two integers.
3. $Line m
Line m becomes the current line. For example, if the text is
Mairzy doats
an dozy doats
>an liddle lamsy divy.
then the command
$Line 0
will make line 0 the current line:
>Mairzy doats
an dozy doats
an liddle lamsy divy.
An error message should be printed if m is either less than 0 or greater than the number of lines in
the text or if no integer is entered. See command 2 above.
4. $Done
This terminates the execution of the text editor. The entire text is printed.
An error message should be printed for any illegal command, such as “$End”, “$insert”, or “Insert”.
System Test 1 (Input is boldfaced):
Please enter a line; a command must start with a $.
$Insert
Each error message can then be printed in the EditorUser class when the exception is caught. RunTime
Exception is the superclass of most the exceptions thrown during execution: NullPointerException,
NumberFormatException, NoSuchElementException, and so on.
Here are the method specification for a default constructor and the f ve methods outlined previously.
To ensure that each command method is properly invoked, a user has access only to the interpret method,
which in turn invokes the appropriate command method.
/**
* Initializes this Editor object.
*
7.4 Application: A Line Editor 305
*/
public Editor()
/**
* Intreprets whether a specified line is a legal command, an illegal command
* or a line of text.
*
* @param s – the specified line to be interpreted.
*
* @return the result of carrying out the command, if s is a legal command, and
* return null, if s is a line of text.
*
* @throws RunTimeException – if s is an illegal command; the argument
* indicates the specific error.
*
*/
public String interpret (String s)
/**
* Inserts a specified line in front of the current line.
*
* @param s – the line to be inserted.
*
* @throws RunTimeException – if s has more than MAX_LINE_LENGTH
* characters.
*
*/
protected void insert (String s)
/**
* Deletes a specified range of lines from the text, and sets the current line
* to be the line after the last line deleted.
*
* @param m – the beginning index of the range of lines to be deleted.
* @param n – the ending index of the range of lines to be deleted.
*
* @throws RunTimeException – if m is less than 0 or if n is less than m or if
* n is greater than or equal to the number of lines of text.
*
*/
protected void delete (int m, int n)
/**
* Makes a specified index the index of the current line in the text.
*
* @param m – the specified index of the current line.
*
306 C H A P T E R 7 Linked Lists
/**
* Returns the final version of the text.
*
* @return the final version of the text.
*
*/
protected String done()
The book’s website includes a test suite, EditorTest. EditorTest is a subclass of Editor to allow the
testing of the protected methods in the Editor class. For example, here is a simple test of the insert
method:
@Test
public void testInsert()
{
editor.interpret ("$Insert");
editor.insert ("a");
editor.insert ("b");
String actual = editor.interpret ("$Done"),
expected = " a\n b\n> \n";
assertEquals (expected, actual);
} // method testInsert
Note that this test does not access the protected field of the Editor class because there is no guarantee
that those f elds will be relevant to the definitio of the insert method. Recall from Chapter 2 that unit
testing applies only to a method’s specification
Another interesting feature of the EditorTest class is that tests that expect a RuntimeException
to be thrown must have a catch block to ensure that the appropriate exception message is included, and
must throw RuntimeException within that catch block! For example, here is one of the tests:
@Test (expected = RuntimeException.class)
public void testInterpretBadLine()
{
try
{
editor.interpret ("$Delete 7 x");
} // try
catch (RuntimeException e)
{
assertEquals ("java.lang.RuntimeException: " +
Editor.TWO_INTEGERS_NEEDED, e.toString());
throw new RuntimeException();
} // catch RuntimeException
} // method testInterpretBadLine
7.4 Application: A Line Editor 307
There is one more issue related to EditorTest: what can we use as a stub for each method in Editor
so that all of the tests will initially fail? We cannot use the normal stub
throw new UnsupportedOperationException();
The delete method can be invoked only if the command line has two integers. So we will have an
auxiliary method, protected void tryToDelete (Scanner sc), which calls delete provided there
are two integers in the command line. There is a similar auxiliary method for the setCurrentLineNumber
method. The Figure 7.19 has the UML diagram for the Editor class.
Editor
# text: LinkedList<String>
# current: ListIterator<String>
# inserting: boolean
+ Editor()
# done(): String
current = text.listIterator();
inserting = false;
} // default constructor
We can estimate the time requirements for this method because it does not call any other methods in the
Editor class. In general, the time requirements for a given method depend on the time for the methods
called by the given method. The worstTime(n), where n is the number of lines of text, is constant. For
the remainder of the Editor class’s methods, we postpone an estimate of worstTime(n) until all of the
methods have been defined
The interpret method proceeds as follows. There are special cases if the line is blank or if the f rst
character in the line is not ‘$’: the insert method is invoked if inserting is true; otherwise, a bad-line
exception is thrown. If the f rst character in the line is ‘$’, the line is scanned and action appropriate to the
command is taken. For the $Delete and $Line commands, the remaining tokens must firs be checked—to
make sure they are integers—before the delete and setCurrentLineNumber methods can be called.
That allows the delete and setCurrentLineNumber methods to have int parameters.
Here is the definitio of the interpret method:
public String interpret (String s)
{
Scanner sc = new Scanner (s);
String command;
The definitio of the insert method is straightforward. The only error checking is for a too-long line;
otherwise, the parameter s is inserted into the text in front of the current line. The method definitio is:
protected void insert (String s)
{
if (s.length() > MAX_LINE_LENGTH)
throw new RuntimeException (LINE_TOO_LONG +
MAX_LINE_LENGTH + "\n");
current.add (s);
} // insert
The $Delete command can fail syntactically, if the line does not have two integers, or semantically, if the
firs line number is either greater than the second or less than zero, or if the second line number is greater
than the last line in the text. The tryToDelete method checks for syntax errors:
protected void tryToDelete (Scanner sc)
{
int m = 0,
n = 0;
try
{
int m = sc.next();
int n = sc.next();
}// try
catch (RuntimeException e)
{
throw new RuntimeException (TWO_INTEGERS_NEEDED);
} // not enough integer tokens
delete (m, n);
} // method tryToDelete
The call to the delete method must be outside of the try block so that the run-time exceptions thrown
within the delete method will pass through tryToDelete and back to the method that calls interpret,
instead of being caught in tryToDelete’s catch block.
The delete method checks for semantic errors. If there are no errors, The ListIterator object
current is positioned at line m, and a loop removes lines m through n. Then current will automatically
be positioned beyond the last line removed. Here is the definitio of the delete method:
protected void delete (int m, int n)
{
if (m > n)
throw new RuntimeException (FIRST_GREATER);
if (m < 0)
throw new RuntimeException (FIRST_LESS_THAN_ZERO);
if (n >= text.size())
throw new RuntimeException (SECOND_TOO_LARGE);
current = text.listIterator (m);
for (int i = m; i <= n; i++)
7.4 Application: A Line Editor 311
{
current.next();
current.remove ();
} // for
} // method delete
The tryToSetCurrentLineNumber method is similar to tryToDelete, except there is only one integer
expected on the command line:
protected void tryToSetCurrentLineNumber (Scanner sc)
{
int m = 0;
try
{
int m = sc.next();
} // try
catch (RuntimeException e)
{
throw new RuntimeException (INTEGER_NEEDED);
} // no next token or token not an integer
setCurrentLineNumber (m);
} // method tryToSetCurrentLineNumber
The setCurrentLineNumber method, called if there are no syntactic errors, checks for semantic errors,
and if none are found, re-positions current to the line whose line number is m. Here is the definitio of
the setCurrentLineNumber method:
protected void setCurrentLineNumber (int m)
{
if (m < 0)
throw new RuntimeException (M_LESS_THAN_ZERO);
if (m > text.size())
throw new RuntimeException (M_TOO_LARGE);
current = text.listIterator (m);
} // method setCurrentLineNumber
Finally, the done method returns a String representation of the text: suitable for printing. We create itr,
a ListIterator object (specifically a ListItr object) to iterate through the LinkedList object text.
The current line should have a ‘> ’ in front of it. But how can we determine when the line that itr is
positioned at is the same as the line that current is positioned at? Here is one possibility:
itr.equals (current)
The ListItr class does not defin an equals method, so the Object class’s version of equals is
invoked. But that method compares references, not objects. The references will never be the same since
they were, ultimately, allocated by different calls to the ListItr constructor. So that approach will not
work.
Alternatively, we could compare elements:
itr.next().equals (current.next())
312 C H A P T E R 7 Linked Lists
But this could give incorrect information if the text had duplicate lines. The safe way to compare is by
the nextIndex() method, which returns the index of the element that the iterator is positioned at. Here
is the method definition
protected String done()
{
ListIterator<String> itr = text.listIterator();
while (itr.hasNext())
if (itr.nextIndex() == current.nextIndex())
s = s + "> " + itr.next() + ‘\n’;
else
s = s + " " + itr.next() + ‘\n’;
if (!current.hasNext())
s = s + "> " + ‘\n’;
return s;
} // method done
Figure 7.20 has all the UML class diagrams for this project.
EditorUser
+ run():
Editor
# text: LinkedList<String>
# current: ListIterator<String>
# inserting: boolean
+ Editor()
# done(): String
The book’s website has the EditorUserTest class to test the editText method.
String inFilePath,
outFilePath;
while (!pathsOK)
{
try
{
System.out.print (IN_FILE_PROMPT);
inFilePath = keyboardScanner.nextLine();
fileScanner = new Scanner (new File (inFilePath));
System.out.print (OUT_FILE_PROMPT);
outFilePath = keyboardScanner.nextLine();
printWriter = new PrintWriter (new FileWriter (outFilePath));
pathsOK = true;
} // try
catch (IOException e)
{
System.out.println (IO_EXCEPTION_MESSAGE + e);
} // catch I/O exception
} // while
editText (fileScanner, printWriter);
printWriter.close();
} // method run
The editText() method loops through the input file with a try-block and a catch-block to handle all
of the editing errors that may occur. During each loop iteration, the interpret method is called. The
return value from this call will be null unless the command is “$Done”, in which case the fina text is
printed.
Here is the definition
public void editText (Scanner fileScanner, PrintWriter printWriter)
{
final String FINAL_MESSAGE =
"\n\n***********************\nHere is the final text:\n";
while (true)
Summary 315
{
try
{
line = fileScanner.nextLine();
printWriter.println (line);
result = editor.interpret (line);
} // try
catch (RuntimeException e)
{
printWriter.println (e);
} // catch RuntimeException
if (line.equals (Editor.DONE_COMMAND))
{
printWriter.println (FINAL_MESSAGE + result);
break;
} // line is done command
} // while
} // method editText
This method accesses the public constant DONE_COMMAND from the Editor class. That enables us to
avoid the dangerous practice of definin the same constant identifie twice. The danger is that this identifie
might be re-define in a subsequent application, for example, if the developer of the Editor class decided
to change the command-start symbol from ‘$’ to ‘#’.
SUMMARY
A linked list is a List object (that is, an object in a topics of links and iterators, and thus to prepare you for
class that implements the List interface) in which the the focal point of the chapter: the LinkedList class,
following property is satisfied part of the Java Collections Framework. LinkedList
objects lack the random-access ability of ArrayList
Each element is contained in an object, called an objects. But, by using an iterator, an element can be added
Entry object, that also includes a reference, called to or removed from a LinkedList in only constant
a link , to another Entry object. For each Entry time; for adding to or removing from an ArrayList
object except the one that holds the last element in object, worstTime(n) is linear in n. This advantage of
the collection, the link is to the Entry object that LinkedList objects is best suited for consecutive inser-
contains the next element in the collection. tions and deletions because, for the task of getting to the
index of the firs insertion or deletion, worstTime(n) is
A linked list that also satisfie the following property:
linear in n.
Each Entry object except the firs also includes a The Java Collection Framework’s implementation
link to the Entry object that contains the previous of the LinkedList class stores the elements in a circu-
element. lar, doubly-linked structure with a dummy entry. Another
possible implementation is a non-circular, doubly-linked
is called a doubly-linked list . Otherwise, it is called a structure with head and tail fields
singly-linked list . The application, a simple line editor, took advan-
The SinglyLinkedList class implements a tage of the LinkedList class’s ability to quickly
singly-linked list. The purpose of developing the make consecutive insertions and deletions anywhere in
SinglyLinkedList class is to introduce you to the a LinkedList object.
316 C H A P T E R 7 Linked Lists
CROSSWORD PUZZLE
1 2
3
4 5
6
7
9
10
www.CrosswordWeaver.com
ACROSS DOWN
6. A class that is embedded in another 1. A program that edits text, line-by-line.
class is called a ___________ class.
2. In the LinkedList class, the method that
8. The only operator in Java that has three associated the calling object with an
operands (separated by ‘?’ and ‘:’). iterator that can move forward or
backward is __________ ().
10. A linked list in which each Entry object
includes a link to the Entry object that 3. The worstTime(n) for the public
contains the previous element in the list. methods in the Listltr class.
CONCEPT EXERCISES
7.1 In the SinglyLinkedList class, defin the following method without using an iterator.
/**
* Finds the element at a specified position in this LinkedList object.
* The worstTime(n) is O(n).
*
* @param index – the position of the element to be returned.
*
* @return the element at position index.
*
* @throws IndexOutOfBoundsException – if index is less than 0 or greater than
* or equal to size().
*/
public E get (int index)
a. Defin the addLast method for this design. Here is the method specification
/**
* Appends a specified element to (the back of) this LinkedList object.
*
* @param element – the element to be appended.
*
* @return true.
*
*/
public boolean addLast (E element)
b. The def nition of the removeLast() method would need to make null the next fi ld in the Entry
object before the Entry object tail. Could we avoid a loop in the def nition of removeLast() if, in the
LinkedList class, we added a beforeTail fi ld that pointed to the Entry object before the Entry
object tail ? Explain.
318 C H A P T E R 7 Linked Lists
7.6 How can you distinguish between a call to the add (E element) method in the LinkedList class and
a call to the add (E element) method in the ListItr class?
7.7 Explain how to remove “Don” from the LinkedList object in Figure 7.18. Explain why, for the definitio
of the method remove (Object obj), worstTime(n) is linear in n?
7.8 In the Java Collections Framework, the LinkedList class is designed as a circular, doubly-linked list with a
dummy entry (pointed to by the header field) What is the main advantage of this approach over a circular,
doubly-linked list with head and tail fields
7.9 For the three methods in the EditorUser class, estimate worstTime(n), where n represents the number of
lines of text.
PROGRAMMING EXERCISES
7.1 Use the SinglyLinkedList class three times. First, create a SinglyLinkedList object, team1,
with elements of type String. Add three elements to team1. Second, create team2, another
SinglyLinkedList object with elements of type String. Add four elements to team2. Finally, create
a SinglyLinkedList object, league, whose elements are SinglyLinkedList objects of teams.
Add team1 and team2 to league.
7.2 Hypothesize the output from the following method segment:
itr.add (‘f’);
itr.add (‘t’);
itr.previous();
itr.previous();
itr.add (‘e’);
itr.add (‘r’);
itr.next();
itr.add (‘e’);
itr.add (‘c’);
itr = letters.listIterator();
itr.add (‘p’);
System.out.println (letters);
7.4 Rewrite the code in Exercise 7.2 with a native array. For example, you would start with:
ListIterator<Double> itr;
weights.add (5.3);
weights.add (2.8);
itr = weights.listIterator();
with
But the add method will now take quadratic time because the least method will now take linear time.
Modify the least method—including its heading—so that its worstTime(n) will be constant. Make the
corresponding changes to the add method so that method will take only linear time.
320 C H A P T E R 7 Linked Lists
7.8 Rewrite the insert method in the Editor class to insert the given line after the current line. For example,
if the text is
>I was
older then
and the command is
$Insert
so much
then the text becomes
I was
>so much
older then
The newly added line becomes the current line.
7.9 Modify the EditorUser class to work with commands entered from the keyboard instead of a f le. The
output should go to the console window. Test your changes by entering, from the keyboard, the lines in
editor.in1 from the Editor directory on the book’s website.
7.10 Unit test and defin the following method:
/**
* Removes the first and last 4-letter word from a given LinkedList<String> object.
* Each word will consist of letters only.
* The worstTime(n) is O(n).
*
* @param list – the LinkedList<String> object.
*
* @throws NullPointerException – if list is null.
* @throws NoSuchElementException - if list is not null, but list has no 4-letter
* words or only one 4-letter word.
*
*/
public static void bleep (LinkedList<String> list)
/**
* Adds a specified element at the back of this SinglyLinkedList object.
*
* @param element – the element to be inserted.
*
Programming Exercises 321
* @return true.
*
*/
public boolean add (E element)
/**
* Inserts the elements of this SinglyLinkedList object into an array in the same
* order as in this SinglyLinkedList object. The worstTime(n) is O(n).
*
* @return a reference to the array that holds the same elements, in the same
* order, as this SinglyLinkedList object.
*
*/
public Object [ ] toArray ()
/**
* Determines if this SinglyLinkedList object contains all of the elements from a
* specified collection.
*
* @param c – the specified collection.
*
* @return true – if this SinglyLinkedList object contains each element of c;
* otherwise, return false.
*
* @throws NullPointerException – if c is null.
*
*/
public boolean containsAll (Collection<?> c)
/**
* Determines if this SinglyLinkedList object is equal to obj.
*
* @param obj – an object whose equality to this SinglyLinkedList object is
* being tested.
*
For unit testing, modify the SinglyLinkedTest class from the book’s website.
322 C H A P T E R 7 Linked Lists
public E next ()
{
if (lastReturned != null)
previous = lastReturned;
lastReturned = next;
next = next.next;
return lastReturned.element;
} // method next
2. For unit testing of your remove() method, update the SinglyLinkedTest class.
head
true false maybe
The only methods you need to implement are the f ve methods listed in Section 7.2 and the iterator() method
from Section 7.2.2. You will also need to test those methods.
Programming Exercises 323
5. $Change %X%Y%
Effect: In the current line, each occurrence of the string given by X will be replaced by the string given by Y.
$Change %ear%are%
$Change %wa%whe%
Notes:
a. If either X or Y contains a percent sign, it is the end-user’s responsibility to choose another delimiter. For
example,
$Change #0.16#16%#
$Change %of %%
c. If the delimiter occurs fewer than three times, the error message to be generated is
6. $Last
Effect: The line number of the last line in the text has been returned.
The command
$Last
will cause 3 to be returned. The text and the designation of the current line are unchanged.
7. $GetLines m n
Effect: Each line number and line in the text, from lines m through n, inclusive, will be returned.
The command
$GetLines 0 2
The text and the designation of the current line are unchanged.
Programming Exercises 325
Note: If no line numbers are entered, the entire text should be returned. For example,
$GetLines
$Insert
You can fool
some of the people
some of the times,
but you cannot foul
all of the people
all of the time.
$Line 2
$GetLines 2 1
Error: The first line number is greater than the second.
$ GetLines 2 2
2 some of the times,
$Change %s%%
$GetLines 2 2
2 some of the time,
$Change %o%so
Error: Delimiter must occur three times. Please try again.
$Change %o%so%
$GetLines 2 2
2 some sof the time,
Change
Error: a command should start with $.
$Change %sof%of%
$GetLines 2 2
2 some of the time,
$Line 0
$Insert
Lincoln once said that
you can fool
some of the people
all the time and
all of the time and
$Last
10
$GetLines 0 10
0 Lincoln once said that
1 you can fool
2 some of the people
3 all the time and
4 all of the time and
5 You can fool
6 some of the people
7 some of the time,
8 but you cannot foul
9 all of the people
10 all of the time.
$Line 5
$Change %Y%y%
$GetLines 5 5
5 you can fool
$Line 6
$Change %some%all%
$GetLines 6 6
6 all of the people
$Line 8
$Change %ul%ol%
$GetLines 8 8
8 but you cannot fool
$Line 9
$Change %ee%eo%
$GetLines 9 9
9 all of the people
$Delete 3 3
$GetLines 0 10
Error: The second line number is greater than the number of the last line in
the text.
$Last
9
$GetLines 0 9
0 Lincoln once said that
1 you can fool
2 some of the people
3 all of the time and
4 you can fool
Programming Exercises 327
System Test 2
$Insert
Life is full of
successes and lessons.
$Delete 1 1
$Insert
wondrous oppurtunities disguised as
hopeless situations.
$Last
2
$GetLines
0 Life is full of
1 wondrous oppurtunities disguised as
2 hopeless situations.
$Line 1
$Change %ur%or%
$GetLines 0 2
0 Life is full of
1 wondrous opportunities disguised as
2 hopeless situations.
$Done
Here is the final text:
Life is full of
> wondrous opportunities disguised as
hopeless situations.
328 C H A P T E R 7 Linked Lists
System Test 1:
click on browser2, browser4, back (browser2 appears), enter browser.in5 in the
input line, click on back (browser2 appears), forward (browser5 appears)
In this chapter we introduce two more abstract data types: stacks and queues. Stacks and queues
can be modified in only a very limited way, so there are straightforward implementations, that is,
data structures, of the corresponding data types. Best of all, stacks and queues have a wide variety of
applications. We’ll start with stacks because a stack is somewhat easier to implement than a queue.
CHAPTER OBJECTIVES
1. Understand the defining properties of stacks and queues, and how these properties are violated
by the Java Collections Framework’s Stack class and Queue interface.
2. For both stacks and queues, be able to develop contiguous and linked implementations that
do not violate their defining properties.
3. Explore the use of stacks in the implementation of recursion and in converting from infix
notation to postfix notation.
4. Examine the role of queues in computer simulation.
8.1 Stacks
A stack is a f nite sequence of elements in which the only element that can be removed is the element
that was most recently inserted. That element is referred to as the top element on the stack.
For example, a tray-holder in a cafeteria holds a stack of trays. Insertions and deletions are made
only at the top. To put it another way, the tray that was most recently put on the holder will be the next
one to be removed. This definin property of stacks is sometimes called “Last In, First Out,” or LIFO.
In keeping with this view, an insertion is referred to as a push, and a removal as a pop. For the sake of
alliteration, a retrieval of the top element is referred to as a peek .
Figure 8.1a shows a stack with three elements and Figures 8.1b, c and d show the effect of two pops
and then a push.
In Section 8.1.1, we defin the Stack class, and note its assets and liabilities.
17
13 13 21
28 28 28 28
(a) (b) pop (c) pop (d) push 21
FIGURE 8.1 A stack through several stages of pops and pushes: 17 and 13 are popped and then 21 is pushed. In
each figure the highest element is the top element
do you think would be faster for the pop and push methods? For removing the front element in an array,
worstTime(n) and averageTime(n) are both linear in n, whereas removing the last element in an array takes
only constant time in both the worst and average cases. For inserting at the front of an array, worstTime(n)
and averageTime(n) are both linear in n, whereas inserting at the back of an array takes only constant
time on average, but linear-in-n time in the worst case (when the array is full). So it is clearly faster for
the top element to be at the back of the underlying array. See Figure 8.2.
Here are the Stack class’s heading and method specification for the only constructor and the push,
pop and peek methods1 :
/**
* Creates an empty Stack.
*/
public Stack()
/**
/**
* Removes the element at the top of this stack and returns that
* element.
* The worstTime(n) and averageTime(n) are constant.
*
* @return the element at the top of this stack.
* @throws EmptyStackException if this stack is empty.
*/
public E pop()
/**
* Returns the element at the top of this stack without removing it
* from the stack.
1 Strictly
speaking, the pop and peek method headings include the modifie synchronized: a keyword related to concurrent programming,
which is beyond the scope of this book and irrelevant to our discussion. For more details on synchronization and concurrent programming,
see the Java Tutorials at java.sun.com.
8.1 Stacks 331
null null null null null null null null null null
0 1 2 3 4 5 6 7 8 9
after ‘b is pushed
FIGURE 8.2 The effect of three pushes and a pop on the underlying array of an instance of the Java Collection
Framework’s Stack class. The Stack class’s constructor automatically creates an array of length 10
The following program utilizes each of the above methods, as well as several List methods you saw in
Chapters 6 and 7.
import java.util.∗;
{
new StackExample().run();
} // method main
System.out.println ("\nHere are the contents of the stack, from top to bottom:");
for (int i = myStack.size() - 1; i >= 0; i–)
System.out.println (myStack.get (i));
System.out.println ("\nHere are the contents of the stack, starting from index 0:");
for (Character c : myStack)
System.out.println (c);
System.out.println (
"\nHere are the contents of the stack, from top to bottom, during destruction:");
while (!myStack.isEmpty())
System.out.println (myStack.pop());
System.out.println ("The stack now has " + myStack.size() + " elements.");
} // method run
} // class StackExample
Here are the contents of the stack, from top to bottom, during destruction:
d
b
a
The stack now has 0 elements.
The f rst iteration in the above program uses a list index and the get method to access each element,
starting at the top of the stack. The second iteration illustrates a curious feature of the Stack class: The
standard iteration—with an enhanced for statement—accesses the elements starting from the bottom of
the stack! The same bottom-up access would apply if you called
System.out.println (myStack);
The third iteration represents destructive access: the top element is repeatedly popped and printed until the
stack is empty.
badStack.push ("Larry");
badStack.push ("Curly");
badStack.push ("Moe");
We will let the convenience of the Stack class override our disapproval of the just noted defect. But
if you regard the violation as a fatal f aw, you have (at least) two options, one utilizing inheritance and
one utilizing aggregation. For the firs option, you can undertake Programming Project 8.4, and create a
PureStack class that extends Stack, but throws UnsupportedOperationException for any attempt
to remove an element that is not at the top of the stack. And the iterator goes from top to bottom, instead
of from bottom to top. This is, essentially, the approach used for the Stack class in C#, a member of
Microsoft’s. NET family of languages.
Another option for a PureStack class is to allow access, modificatio or removal only of the most
recently inserted element. This stringency would allow only a few methods: a default constructor, a copy
constructor (so you can non-destructively access a copy of a stack, instead of the original), push, pop,
peek, size, and isEmpty. This is the idea behind the stack class in C++, a widely used language.
A straightforward way to create such a PureStack class is with aggregation: the only f eld is a list
whose type is ArrayList or LinkedList. For example, the definitio of the pop method—in either
implementation—is as follows:
public E pop()
{
return list.remove (list.size() - 1);
} // method pop
334 C H A P T E R 8 Stacks and Queues
In fact, all the definition —in either implementation—are one-liners. See Programming Exercise 8.1
for an opportunity to develop the LinkedList implementation, and Programming Exercise 8.2 for the
ArrayList implementation.
Now let’s look at a couple of important applications.
*
* @throws IllegalArgumentException if n is negative.
*/
public static String getBinary (int n)
{
if (n < 0)
throw new IllegalArgumentException();
return getBin (n); // RA1
} // method getBinary
The getBin method has the formal parameter n as its only local variable, and so each activation record
will have two components:
a. the return address;
b. the value of the formal parameter n.
Also, because the getBin method returns a String reference, a copy of that String reference is pushed
onto the stack just before a return is made. For simplicity, we will pretend that the String object itself
is pushed.
Assume that the value of n is 6. When getBin is called from the getBinary method, an activation
record is created and pushed onto the stack, as shown in Figure 8.3.
RA1
n 6
Activation Stack
FIGURE 8.3 The activation stack just prior to getBin’s firs activation. RA1 is the return address
Since n > 1, getBin is called recursively with 3 (that is, 6/2) as the value of the argument. A
second activation record is created and pushed onto the stack. See Figure 8.4.
RA2
n 3
RA1
n 6
Activation Stack
(two records)
FIGURE 8.4 The activation stack just prior to the second activation of getBin
336 C H A P T E R 8 Stacks and Queues
Since n is still greater than 1, getBin is called again, this time with 1 (that is, 3/2) as the value of
the argument. A third activation record is created and pushed. See Figure 8.5.
RA2
n 1
RA2
n 3
RA1
n 6
Activation Stack
(three records)
FIGURE 8.5 The activation stack just prior to the third activation of getBin
Since n ≤ 1, the top activation record is popped, the String “1” is pushed onto the top of the stack
and a return is made to the address RA2. The resulting stack is shown in Figure 8.6.
The concatenation at RA2 in getBin is executed, yielding the String "1" + Integer.toString
(3 % 2), namely, "11". The top activation record on the stack is popped, the String “11” is pushed,
and another return to RA2 is made, as shown in Figure 8.7.
“1”
RA2
n 3
RA1
n 6
Activation Stack
(two records)
FIGURE 8.6 The activation stack just after the completion of the third activation of getBin
“11”
RA1
n 6
Activation Stack
FIGURE 8.7 The activation stack just after the completion of the second activation of getBin
8.1 Stacks 337
The concatenation at RA2 is "11" + Integer.toString (6 % 2), and the value of that String
object is “110”. The stack is popped once more, leaving it empty, and “110”, the binary equivalent of 6,
is pushed. Then a return to RA1—at the end of the getBinary method—is made.
The above discussion should give you a general idea of how recursion is implemented by the
compiler. The same stack is used for all method calls. And so the size of each activation record must be
saved with each method call. Then the correct number of bytes can be popped. For the sake of simplicity,
we have ignored the size of each activation record in the above discussion.
The compiler must generate code for the creation and maintenance, at run time, of the activation
stack. Each time a call is made, the entire local environment must be saved. In most cases, this overhead
pales to insignificanc relative to the cost in programmer time of converting to an iterative version, but
this conversion is always feasible.
On those rare occasions when you must convert a recursive method to an iterative method, one
option is to simulate the recursive method with an iterative method that creates and maintains its own
stack of information to be saved. For example, Project 8.3 requires an iterative version of the (recursive)
tryToReachGoal method in the backtracking application from Chapter 5. When you create your own
stack, you get to decide what is saved. For example, if the recursive version of the method contains a
single recursive call, you need not save the return address. Here is an iterative, stack-based version of the
getBinary method (see Programming Exercise 5.1 for an iterative version that is not stack-based, and
see Programming Exercise 8.3 for a related exercise).
/**
*
* Determines the binary equivalent of a non-negative integer. The worstTime(n)
* is O(log n).
*
* @param n the non-negative integer, in decimal notation.
*
* @return a String representation of the binary equivalent of n.
*
* @throws IllegalArgumentException if n is negative.
*/
public static String getBinary (int n)
{
Stack<Integer> myStack = new Stack<Integer>();
if (n < 0)
throw new IllegalArgumentException( );
myStack.push (n % 2);
while (n > 1)
{
n /= 2;
myStack.push (n % 2);
} // pushing
while (!myStack.isEmpty())
binary += myStack.pop();
return binary;
} // method getBinary
338 C H A P T E R 8 Stacks and Queues
What is most important is that you not overlook the cost, in terms of programmer time, of making
the conversion from a recursive method to an iterative method. Some recursive methods, such as the
factorial method, can easily be converted to iterative methods. Sometimes the conversion is nontrivial,
such as for the move and tryToReachGoal methods of Chapter 5 and the permute method of Lab 9.
Furthermore, the iterative version may well lack the simple elegance of the recursive version, and this may
complicate maintenance.
You certainly should continue to design recursive methods when circumstances warrant. That is,
whenever the problem is such that complex instances of the problem can be reduced to simpler instances
of the same form, and the simplest instance(s) can be solved directly. The above discussion on the activation
stack enables you to make better-informed tradeoff decisions.
a + b
b - c * d
(b - c) * d
a - c - h / b * c
a - (c - h) / (b * c)
For the sake of simplicity, we initially restrict our attention to expressions with single-letter identifiers
parentheses and the binary operators +, −, ∗, and /.
The usual rules of arithmetic apply:
1. Operations are normally carried out from left to right. For example, if we have
a + b - c
then the multiplication will be carried out before the addition. For
a - b + c ∗ d
the subtraction is performed first then the multiplication and, finally the addition.
We can interpret this rule as saying that multiplication and division have “higher precedence” than
addition and subtraction.
8.1 Stacks 339
3. Parentheses may be used to alter the order indicated by rules 1 and 2. For example, if we have
a - (b + c)
a - c - h / b * c a - (c - h) / (b * c)
1 2 1 2
3
3
4 4
FIGURE 8.9 The order of evaluation for the last two expressions in Figure 8.8
The f rst widely used programming language was FORTRAN (from FORmula TRANslator), so
named because its compiler could translate arithmetic formulas into machine-level code. In early (pre-1960)
compilers, the translation was performed directly. But direct translation is awkward because the machine-
level code for an operator cannot be generated until both of its operands are known. This requirement
leads to difficultie when either operand is a parenthesized subexpression.
Infi Postfi
a - b + c * d a b - c d * +
a + c - h/b * r a c + h b/r * -
a + (c - h)/(b * r) a c h - b r * /+
FIGURE 8.10 Several arithmetic expressions in both infi and postfi notation
How can we convert an arithmetic expression from infi notation into postfi notation? Let’s view the
infi notation as a string of characters and try to produce the corresponding postfi string. The identifier in
the postfi string will be in the same order as they are in the infi string, so each identifie can be appended
340 C H A P T E R 8 Stacks and Queues
to the postfi string as soon as it is encountered. But in postfi notation, operators must be placed after their
operands. So when an operator is encountered in the inf x string, it must be saved somewhere temporarily.
For example, suppose we want to translate the infi string
a - b + c ∗ d
into postfi notation. (The blanks are for readability only—they are not, for now, considered part of the
infi expression.) We would go through the following steps:
‘a’ is appended to postfix, which is now the string "a"
When ‘+’ is encountered, we note that since it has the same precedence as ‘-’, the subtraction should be
performed firs by the left-to-right rule (Rule 1, above). So the ‘-’ is appended to the postfi string, which
is now "ab-" and ‘+’ is saved temporarily. Then ‘c’ is appended to postfix which now is "ab-c".
The next operator, ‘∗’, must also be saved somewhere temporarily because one of its operands
(namely ‘d’) has not yet been appended to postfix But ‘∗’ should be retrieved and appended to postfi
before ‘+’ since multiplication has higher precedence than addition.
When ‘d’ is appended to postfix the postfi string is "ab-cd". Then ‘∗’ is appended, making the
postfi string "ab-cd∗". Finally, ‘+’ is appended to postfix and the fina postfi representation is
"ab-cd∗+"
The temporary storage facility referred to in the previous paragraph is handled conveniently with a stack
to hold operators. The rules governing this operatorStack are:
else
For example, Figure 8.11 shows the history of the operator stack during the conversion of
a + c - h / b ∗ r
FIGURE 8.11 The conversion of a + c - h/b ∗ r to postf x notation. At each stage, the top of
operatorStack is shown as the rightmost element
How are parentheses handled? When a left parenthesis is encountered in the infi string, it is imme-
diately pushed onto operatorStack, but its precedence is define to be lower than the precedence of any
binary operator. When a right parenthesis is encountered in the infi string, operatorStack is repeatedly
popped, and the popped element appended to the postfi string, until the operator on the top of the stack
is a left parenthesis. Then that left parenthesis is popped but not appended to postfix and the scan of the
infi string is resumed. This process ensures that parentheses will never appear in postfi notation.
For example, when we translate a ∗ (b + c) into postfix the operators ‘∗’, ‘(’, and ‘+’ would
be pushed and then all would be popped (last-in, first-out when the right parenthesis is encountered. The
postfi form is
a b c + ∗
For a more complex example, Figure 8.12 (next page) illustrates the conversion of
x - (y ∗ a / b - (z + d ∗ e) + c) / f
Infix Expression: x - (y ∗ a / b - (z + d ∗ e) + c) / f
Postfix Expression: x y a * b / z d e * + - c + f / -
FIGURE 8.12 The conversion of x - (y * a/b - (z + d * e) + c)/f from infi to postf x. At each stage,
the top of operatorStack is shown as the rightmost element
More complex expressions can be accommodated by expanding the matrix. For the conversion, there would
be a switch statement with one case for each matrix entry.
8.1.4.3 Tokens
A program that utilized a transition matrix would probably not work with the characters themselves because
there are too many possible (legal) values for each character. For example, a transition matrix that used
a row for each legal infi character would need 52 rows just for an identifier And if we changed the
specification to allow multi-character identifiers we would need millions of rows!
Instead, the legal characters would usually be grouped together into “tokens.” A token is the smallest
meaningful unit in a program. Each token has two parts: a generic part that holds its category and a specifi
8.1 Stacks 343
FIGURE 8.13 The transition matrix for converting simple expressions from infi notation to postfi notation
part that enables us to recapture the character(s) tokenized. For converting simple infi expressions to
postfix the token categories would be: identifier, rightPar, leftPar, addOp (for ‘+’ and ‘-’),
multOp (for ‘∗’ and ‘/’), and empty (for a dummy value). The specifi part would contain the index,
in the infi string, of the character tokenized. For example, given the infi string
(first + last) / sum
to tokenize "last", we would set its category to identifier and its index to 9.
The structure of tokens varies widely among compilers. Typically, the specifi part of a variable
identifier’ token contains an address into a table, called a symbol table. At that address would be stored
the identifier an indication that it is a variable identifier its type, initial value, the block it is declared in
and other information helpful to the compiler. There is a symbol table in the project of Lab 15, and the
creation of a symbol table is the subject of an application in Chapter 14.
In Lab 15, a complete infix-to-postf project is developed, with tokens and massive input-editing.
You are now prepared to do Lab 15: Converting from Infix to Postfix
2 Prefi notation was invented by Jan Lukasiewicz, a Polish logician. It is sometimes referred to as Polish Notation. Postf x notation is then
Infi Prefi
a - b - a b
a - b * c - a * b c
(a - b) * c * - a b c
a - b + c * d + - a b * c d
a + c - h / b * d - + a c * / h b d
a + (c - h) / (b * d) + a / - c h * b d
FIGURE 8.14 Several arithmetic expressions in both infi and prefi notation
How can we convert an arithmetic expression from infi to prefix As in infix-to-postfi we will
need to save each operator until both of its operands have been obtained. But we cannot simply append
each identifie to the prefi string as soon as it is encountered. Instead, we will need to save each identifier
in fact, each operand, until its operator has been obtained.
The saving of operands and operators is easily accomplished with the help of two stacks, operand
Stack and operatorStack. The precedence rules for operatorStack are exactly the same as we saw
in converting from infi to postfix Initially, both stacks are empty. When an identifie is encountered
in the infi string, that identifie is pushed onto operandStack. When an operator is encountered, it is
pushed onto operatorStack if that stack is empty. Otherwise, one of the following cases applies:
1. If the operator is a left parenthesis, push it onto operatorStack (but left parenthesis has lowest
precedence).
2. If the operator has higher precedence than the top operator on operatorStack, push the operator
onto operatorStack.
3. If the operator’s precedence is equal to or lower than the precedence of the top operator on operat
orStack, pop the top operator, opt1, from operatorStack and pop the top two operands, opnd1
and opnd2, from operandStack. Concatenate (join together) opt1, opnd2, and opnd1 and push the
result string onto operandStack. Note that opnd2 is in front of opnd1 in the result string because
opnd2 was encountered in the infi string—and pushed onto operandStack —before opnd1.
4. If the operator is a right parenthesis, treat it as having lower priority than +, −, ∗, and /. Then Case
3 will apply until a left parenthesis is the top operator on operatorStack. Pop that left parenthesis.
The above process continues until we reach the end of the infi expression. We then repeat the
following actions from case 3 (above) until operatorStack is empty:
Pop opt1 from operatorStack.
Pop opnd1 and opnd2 from operandStack.
Concatenate opt1, opnd2 and opnd1 together and push the result onto operandStack.
When operatorStack is finall empty, the top (and only) operand on operandStack will be the prefi
string corresponding to the original infi expression.
For example, if we start with
a + b ∗ c
a a
1. ---------------------------- ---------------------------- ----------------------------
infi operandStack operatorStack
+ a +
2. ---------------------------- ---------------------------- ----------------------------
infi operandStack operatorStack
b
b a +
3. ---------------------------- ---------------------------- ----------------------------
infi operandStack operatorStack
b *
* a +
4. ---------------------------- ---------------------------- ----------------------------
infi operandStack operatorStack
c
b *
c a +
5. ---------------------------- ---------------------------- ----------------------------
infi operandStack operatorStack
*bc
a +
6. ---------------------------- ---------------------------- ----------------------------
infi operandStack operatorStack
+a*bc
7. ---------------------------- ---------------------------- ----------------------------
infi operandStack operatorStack
Then the elements on the two stacks during the processing of the f rst right parenthesis would be as follows:
h -
c (
) a +
1. ---------------------------- ---------------------------- ----------------------------
infi operandStack operatorStack
346 C H A P T E R 8 Stacks and Queues
-ch (
a +
2. ---------------------------- ----------------------------
operandStack operatorStack
-ch
a +
3. ---------------------------- ----------------------------
operandStack operatorStack
During the processing of the second right parenthesis in the infi string, we would have
d *
b (
-ch /
) a +
1. ---------------------------- ---------------------------- ----------------------------
infi operandStack operatorStack
*bd (
-ch /
a +
2. ---------------------------- ----------------------------
operandStack operatorStack
The end of the infi expression has been reached, so operatorStack is repeatedly popped.
*bd
-ch /
a +
3. ---------------------------- ----------------------------
operandStack operatorStack
/-ch*bd
a +
4. ---------------------------- ----------------------------
operandStack operatorStack
+a/-ch*bd
5. ---------------------------- ----------------------------
operandStack operatorStack
8.2 Queues
A queue is a f nite sequence of elements in which:
1. insertion is allowed only at the back of the sequence;
2. removal is allowed only at the front of the sequence.
The term enqueue (or add ) is used for inserting an element at the back of a queue, dequeue (or remove)
for removing the f rst element from a queue, and front (or element) for the f rst element in a queue.
A queue imposes a chronological order on its elements: the firs element enqueued, at the back, will
eventually be the f rst element to be dequeued, from the front. The second element enqueued will be the
second element to be dequeued, and so on. This defi ing property of queues is sometimes referred to as
“First Come, First Served,” “First In, First Out,” or simply FIFO.
Figure 8.15 shows a queue through several stages of insertions and deletions.
The examples of queues are widespread:
front back
element element
a. A queue with four elements.
front back
element element
b. The queue of Figure 8.15.a after Kim is enqueued.
front back
element element
c. The queue from Figure 8.15.b after Brian is dequeued.
We could continue giving queue examples almost indefinitely Later in this chapter we will develop an
application of queues in the fiel of computer simulation.
Section 8.2.1 presents the Queue interface—part of the Java Collections Framework.
/**
* Retrieves, but does not remove, the head of this queue.
*
* @return the head of this queue.
* @throws NoSuchElementException if this queue is empty.
*/
E element();
} // interface Queue
Also, the Queue interface inherits the following add method from the Collection interface:
/**
* Ensures that this collection contains the specified element.
*
* @param element: element whose presence in this collection is to be ensured.
* @return true if this collection changed as a result of the call; otherwise, false.
*
* @throws ClassCastException class of the specified element prevents it
* from being added to this collection.
* @throws NullPointerException if the specified element is null and this
* collection does not support null elements.
*
*/
boolean add (E element);
So the Queue interface includes the essential methods of the queue data type3 .
3 The Queue interface also has a poll() method that is equivalent to the remove() method except that poll() returns null if the queue
is empty, a peek() method that is equivalent to the element() method except that peek() returns null if the queue is empty, and an
offer (E element) method that is equivalent to the add (E element) method except that offer (E element) is better suited if the
queue imposes insertion restrictions and a full queue is commonplace.
8.2 Queues 349
// Figure 8.15.a.
queue.add ("Brian");
queue.add ("Jane");
queue.add ("Karen");
queue.add ("Bob");
// Figure 8.15.b.
queue.add ("Kim");
// Figure 8.15.c.
queue.remove();
For the three key methods— add (E element), remove(), and element() —worstTime(n) is constant.
That is because in a LinkedList object, insertion at the back and removal or access of the front
element take only constant time. And we have the same fla as we had for the Stack class earlier in
this chapter: There are LinkedList methods—such as remove (int index) and add (int index,
E element) —that violate the definitio of a queue. Whenever a LinkedList object implements the
Queue interface, we declare the object with the Queue interface as in the previous example. This is a
heads-up to users that insertions should occur only at the back and deletions only at the front.
You can overcome this defective implementation of the Queue interface by extending the
LinkedList class to a PureQueue class that throws UnsupportedOperationException for any
of the offending methods. Or you can take an austere approach and create a PureQueue class by
aggregation: The only f eld will be a LinkedList (why not ArrayList ?) object, list, and the only
methods will be a default constructor, a copy constructor, isEmpty(), size(), add (E element),
remove(), and element(). For example, the definitio of the remove() method is
/**
* Retrieves and removes the head of this queue.
* The worstTime(n) is constant and averageTime(n) is constant.
*
* @return the head of this queue.
* @throws NoSuchElementException if this queue is empty.
*/
public E remove()
{
return list.removeFirst();
} // method remove()
The definitio of each of the other six methods is also a one-liner. Observe that if list were an ArrayList
object, worstTime(n) and averageTime(n) for this method would be linear in n.
Now that we have seen several possible implementations of the Queue interface, we turn our attention
to applications.
350 C H A P T E R 8 Stacks and Queues
4 According to legend, a trainee once panicked because one of his simulated engines failed during a simulated blizzard. He “bailed out” of
his simulated cockpit and broke his ankle when he hit the unsimulated floor
8.2 Queues 351
develop
System Computer Model
validate run
Interpretation Output
interpret
30 years ago. You probably would have disregarded the effects of aerosol sprays and refrigerants that
released chlorofluorocarbons Many scientists now suspect that chlorofluorocarbon may have a significan
impact on the ozone layer and thus on all land organisms.
Another disadvantage of computer simulation is that its results are often interpreted as predictions,
and prediction is always a risky business. For this reason, a disclaimer such as the following usually
precedes the results of a computer simulation: “If the relationships among the variables are as described
and if the initial conditions are as described, then the consequences will probably be as follows . . . ”
Analysis We assume that there is one station in the car wash, that is, there is one ‘‘server.’’ Each car takes
exactly ten minutes to get washed. At any time there will be at most five cars waiting—in a queue—to be
washed. If an arrival occurs when there is a car being washed and there are five cars in the queue, that arrival
is turned away as an ‘‘overflow’’ and not counted. Error messages should be printed for an arrival time that
is not an integer, less than zero, greater than the sentinel, or less than the previous arrival time.
The average waiting time is determined by adding up the waiting times for each car and dividing by the
number of cars. Here are the details regarding arrivals and departures:
1. If an arrival and departure occur during the same minute, the departure is processed f rst.
2. If a car arrives when the queue is empty and no cars are being washed, the car starts getting washed
immediately; it is not put on the queue.
3. A car leaves the queue, and stops waiting, once the car starts through the ten-minute wash cycle.
The following is a sample list of arrival times:
5 5 7 12 12 13 14 18 19 25 999 (a sentinel)
To calculate the waiting time for each car that is washed, we subtract its arrival time from the time when
it entered the car wash. The firs arrival, at time 5, entered the wash station right away, so its waiting time
352 C H A P T E R 8 Stacks and Queues
was 0. For the second arrival, also at time 5, it was enqueued at time 5, and then dequeued and entered
the wash station when the firs car left the wash station—at time 15. So the waiting time for the second
arrival was 10 minutes. Here is the complete simulation:
The sum of the waiting times is 223. The number of cars is 8 (the two overflow at 14 and 19 minutes
are not counted), so the average waiting time is 27.875 minutes.
Formally, we supply system tests to specify the expected behavior (that is, in terms of input and
output) of the program. The system tests are created before the program is written and provide an indication
of the program’s correctness. But as we noted in Section 2.4, testing can establish the incorrectness—but
not the correctness—of a program. JUnit testing of any class can commence after the class has been
designed, that is, after the method specification have been developed.
System Test 1 (the input is in boldface):
Please enter the next arrival time. The sentinel is 999: 5
Please enter the next arrival time. The sentinel is 999: 999
8.2 Queues 353
Please enter the next arrival time. The sentinel is 999: 1000
java.lang.IllegalArgumentException The input must consist of a non-negative integer
less than the sentinel.
Please enter the next arrival time. The sentinel is 999: 999
354 C H A P T E R 8 Stacks and Queues
/**
* Washes all cars that are still unwashed after the final arrival.
*
* @return - a string that represents the history of the car wash
* after all arrivals have been processed (washed or turned away).
*/
public LinkedList<String> finishUp()
8.2 Queues 355
/**
* Returns the history of this CarWash object’s arrivals and departures, and the
* average waiting time.
*
* @return the history of the simulation, including the average waiting time.
*
*/
public LinkedList<String> getResults()
The process and finishUp methods return the history of the car wash (times, events, and waiting times),
not because that is needed for the definition of the process and finishUp methods, but for the sake
of testing those methods. Recall the maxim from Chapter 2: In general, methods should be designed to
facilitate testing.
In the CarWashTest class, carWash is a f eld, and CarWashTest extends CarWash to enable
protected methods in CarWash to be accessed in CarWashTest. Here, for example, are tests for arrivals
and overflow
@Test
public void twoArrivalsTest()
{
carWash.processArrival (5);
results = carWash.processArrival (7);
assertTrue (results.indexOf ("5\tArrival") != -1);
assertTrue (results.indexOf ("7\tArrival") > results.indexOf ("5\tArrival"));
} // method twoArrivalsTest
@Test
public void overflowTest()
{
carWash.processArrival (5);
carWash.processArrival (7);
carWash.processArrival (8);
carWash.processArrival (12);
carWash.processArrival (12);
assertTrue (carWash.processArrival (13).toString().
indexOf (CarWash.OVERFLOW) == -1); // no overflow for arrival at 13
assertTrue (carWash.processArrival (14).toString().
indexOf (CarWash.OVERFLOW) > 0); // overflow for arrival at 14
} // method overflowTest
The complete project, including all test classes, is available from the book’s website.
point in the simulation, nextArrivalTime contains 28 and nextDepartureTime contains 24. Then the
next event in the simulation will be a departure at time 24. If the two times are the same, the next event
will be an arrival (see note 1 of Analysis). What if there is no car being washed? Then the next event will
be an arrival. To make sure the next event is an arrival no matter what nextArrivalTime holds, we will
set nextDepartureTime to a large number—say 10000—when there is no car being washed.
The cars waiting to be washed should be saved in chronological order, so one of the variables
needed will be the queue carQueue. Each element in carQueue is a Car object, so we temporarily
suspend development of the CarWash class in order to determine the methods the Car class should have.
When a car leaves the queue to enter the wash station, we can calculate that car’s waiting time
by subtracting the car’s arrival time from the current time. So the Car class will provide, at least, a
getArrivalTime() method that returns the arrival time of the car that was just dequeued. Beyond that,
all the Car class needs is a constructor to initialize a Car object from nextArrivalTime when a Car
object is enqueued. The method specification for the Car class are:
/**
* Initializes this Car object from the specified time of the next arrival.
*
*/
public Car (int nextArrivalTime)
/**
* Determines the arrival time of this Car object.
*
* @return the arrival time of this Car object.
*
*/
public int getArrivalTime()
We now resume the determination of variables in CarWash. As indicated in the previous paragraph,
we should have waitingTime and currentTime variables. To calculate the average waiting time, we
need numberOfCarsWashed and sumOfWaitingTimes. Finally, we need a variable, results, to hold
each line of output of the simulation. We could simply make results a String variable, but then the
concatenation operations would become increasingly expensive. Instead, each line will be appended to a
linked list:
LinkedList<String> results;
At this point, we have amassed eight variables. Which of these should be f elds? A simple heuristic (rule
of thumb) is that most of a class’s public, non-constructor methods should access most of the class’s
field (see Riel, [1996] for more details). Clearly, the process method will need all of the variables. The
finishUp method will handle the remaining departures, so that method must have access to carQueue,
results, sumOfWaitingTimes, waitingTime, currentTime, and nextDepartureTime; these will
be fields The only other fiel is numberOfCars, needed by the getResults method. There is no need
to make nextArrivalTime a f eld (it is needed only in the process method). Here are the constant
identifier and field in the CarWash class:
public final String OVERFLOW = " (Overflow)\n";
protected static final int INFINITY = 10000; // indicates no car being washed
protected static final int WASH_TIME = 10; // minutes to wash one car
Figure 8.17 has the UML diagrams for the CarWash, Car, and LinkedList classes. For the sake of
brevity, the LinkedList field and methods are not shown, and the Queue interface’s methods are not
shown because they are implemented by the LinkedList class.
CarWash
# carQueue: Queue<Car>
# results: LinkedList<String>
# currentTime: int
# nextDepartureTime: int
# numberOfCars: int
# waitingTime: int
# sumOfWaitingTimes: int
Car
+ CarWash() # arrivalTime: int
+ process (nextArrivalTime: int): LinkedList<String>
+ Car()
+ finishUp(): LinkedList<String> * 1
+ Car (nextArrivalTime: int)
+ getResults(): LinkedList<String>
+ getArrivalTime(): int
# processArrival (nextArrivalTime: int): LinkedList<String>
# processDeparture(): LinkedList<String>
E
LinkedList
FIGURE 8.17 The class diagrams for CarWash and associated classes
358 C H A P T E R 8 Stacks and Queues
The process method takes the nextArrivalTime read in from the calling method. Then the decision
is made, by comparing nextArrivalTime to nextDepartureTime, whether the next event is an arrival
or departure. According to the specification of the problem, we keep processing departures until the
next event is an arrival, that is, until nextArrivalTime < nextDepartureTime. Then the arrival at
nextArrivalTime is processed. By creating processArrival and processDeparture methods, we
avoid getting bogged down in details, at least for now.
public LinkedList<String> process (int nextArrivalTime)
{
final String BAD_TIME =
"The time of the next arrival cannot be less than the current time.";
To process the arrival given by nextArrivalTime, we f rst update currentTime and check for an
overflow If this arrival is not an overflow numberOfCars is incremented and the car either starts getting
washed (if the wash station is empty) or is enqueued on carQueue. Here is the code:
/**
* Moves the just arrived car into the car wash (if there is room on the car queue),
* or turns the car away (if there is no room on the car queue).
*
* @param nextArrivalTime – the arrival time of the just-arrived car.
*
*/
protected LinkedList<String> processArrival (int nextArrivalTime)
{
final String ARRIVAL = "\tArrival";
currentTime = nextArrivalTime;
8.2 Queues 359
This method reveals how the Car class gets involved: there is a constructor with nextArrivalTime as
its argument. Here is the complete definitio of the Car class:
public class Car
{
protected int arrivalTime;
/**
* Initializes this Car object.
*
*/
public Car() { } // for the sake of subclasses of Car
/**
* Initializes this Car object from the specified time of the next arrival.
*
*/
public Car (int nextArrivalTime)
{
arrivalTime = nextArrivalTime;
} // constructor with int parameter
/**
* Determines the arrival time of this Car object.
*
* @return the arrival time of this Car object.
*
*/
public int getArrivalTime()
{
return arrivalTime;
} // method getArrivalTime
} // class Car
360 C H A P T E R 8 Stacks and Queues
For this project, we could easily have avoided the Car class, but a subsequent extension of the project
might relate to more information about a car: the number of axles, whether it is a convertible, and so on.
To process a departure, we firs update currentTime and results. Note that the waiting time for
the departing car was calculated when that car entered the wash station—during the previous call to the
processDeparture method. We check to see if there are any cars on carQueue. If so, we dequeue
the front car, calculate its waiting time, add that to sumOfWaitingTimes, and begin washing that car.
Otherwise, we set waitingTime to 0 and nextDepartureTime to a large number to indicate that no
car is now being washed. Here is the definition
/**
* Updates the simulation to reflect the fact that a car has finished getting washed.
*
*/
protected LinkedList<String> processDeparture()
{
final String DEPARTURE = "\tDeparture\t\t";
int arrivalTime;
currentTime = nextDepartureTime;
results.add (Integer.toString (currentTime) + DEPARTURE +
Integer.toString (waitingTime) + "\n");
if (!carQueue.isEmpty())
{
Car car = carQueue.remove();
arrivalTime = car.getArrivalTime();
waitingTime = currentTime - arrivalTime;
sumOfWaitingTimes += waitingTime;
nextDepartureTime = currentTime + WASH_TIME;
} // carQueue was not empty
else
{
waitingTime = 0;
nextDepartureTime = INFINITY; // no car is being washed
} // carQueue was empty
return results;
} // method processDeparture
if (numberOfCars == 0)
results.add (NO_CARS_MESSAGE);
else
results.add (AVERAGE_WAITING_TIME_MESSAGE + Double.toString (
(double) sumOfWaitingTimes / numberOfCars));
return results;
} // method getResults
There is no CarWashUserTest class because the CarWashUser class has no testable methods. Figure 8.18
has the UML class diagrams for this project.
The run method repeatedly—until the sentinel is reached—reads in a value for nextArrivalTime.
Unless an exception is thrown (for example, if the value is not an int), carWash.process (nextArr
ivalTime) is called. When the sentinel is read in, the loop is exited and carWash.finishUp() and
printResults (carWash) are called. Here is the code:
final String INPUT_PROMPT = "\nPlease enter the next arrival time (or " +
SENTINEL + " to quit): ";
CarWashUser
+ run()
+ printResults()
CarWash
# carQueue: Queue<Car>
# results: LinkedList<String>
# currentTime: int
Car
# nextDepartureTime: int
# arrivalTime: int
# numberOfCars: int
1 * + Car()
# waitingTime: int
+ Car (nextArrivalTime: int)
# sumOfWaitingTimes: int
+ getArrivalTime(): int
+ CarWash()
+ finishUp(): LinkedList<String>
+ getResults(): LinkedList<String>
# processDeparture(): LinkedList<String>
LinkedList
int nextArrivalTime;
while (true)
{
System.out.print (INPUT_PROMPT);
try
{
nextArrivalTime = sc.nextInt();
if (nextArrivalTime == SENTINEL)
8.2 Queues 363
break;
if (nextArrivalTime < 0 || nextArrivalTime > SENTINEL)
throw new NumberFormatException (OUT_OF_RANGE);
carWash.process (nextArrivalTime);
} // try
catch (Exception e)
{
System.out.println(e);
sc.nextLine();
} // catch
} // while
carWash.finishUp();
printResults (carWash);
} // method run
For the run method, worstTime(n) is linear in n, where n is the number of lines of input. There are loops
in the definition of the CarWash methods process and finishUp, but those loops are independent of
n; in fact, the number of iterations of either loop is at most 5, the maximum size of the car queue.
For example, F (0) = exp(0) = 1; that is, it is certain that the next arrival will occur at least 0 minutes from
now. Similarly, F (meanArrivalTime) = exp(−1) ∼ 0.4.F (10000 ∗ meanArrivalTime) is approximately 0.
The graph of the function F is shown in Figure 8.19.
364 C H A P T E R 8 Stacks and Queues
1·0 •
•
•
F(x) •
0·4 •
•
•
meanArrivalTime
To generate the arrival times randomly, we introduce an integer variable called timeTillNext,
which will contain the number of minutes from the current time until the next arrival. We determine
the value for timeTillNext as follows. According to the distribution function F given previously, the
probability that the next arrival will take at least timeTillNext minutes is given by
exp(–timeTillNext / meanArrivalTime)
This expression represents a probability, specifically a floatin point number that is greater than 0.0 and
less than or equal to 1.0. To randomize this probability, we associate the expression with the value of a
random variable, randomDouble, in the same range. So we set
randomDouble = random.nextDouble();
Then randomDouble contains a double value that is greater than or equal to 0.0 and less than 1.0. So
1— randomDouble will contain a value that is greater than 0.0 and less than or equal to 1.0. This is what
we want, so we equate 1—randomDouble with exp(–timeTillNext/meanArrivalTime):
1 –randomDouble = exp (–timeTillNext / meanArrivalTime)
To solve this equation for timeTillNext, we take logs of both sides:
log (1 –randomDouble) = –timeTillNext / meanArrivalTime
Now each side is multiplied by—meanArrivalTime, to yield
timeTillNext = –meanArrivalTime ∗ log (1 –randomDouble)
In Java code, we get:
timeTillNext = (int)Math.round (-meanArrivalTime ∗ Math.log (1 - randomDouble));
The f rst car will arrive one minute after the car wash opens and the second car will arrive four minutes
later, at minute 5. The third car will arrive three minutes later, at minute 8.
You are now prepared to do Lab 16: Randomizing the Arrival Times
SUMMARY
A stack is a f nite sequence of elements in which inser- intermediate form between infi and machine language.
tions and deletions can take place only at one end of For this conversion, worstTime(n) is linear in n, the size
the sequence, called the top of the stack. Because the of the infi expression.
most recently inserted element is the next element to be A queue is a f nite sequence of elements in which
removed, a stack is a last-in-first-ou (LIFO) structure. insertions can take place only at the back, and removals
Compilers implement recursion by generating code for can take place only at the front. Because the f rst ele-
pushing and popping activation records onto a run-time ment inserted will be the f rst element to be removed, a
stack whose top record holds the state of the method cur- queue is a f rst-in-first-ou (FIFO) structure. The inher-
rently being executed. Another stack application occurs ent fairness of this firs -come-firs -served restriction has
in the translation of infi expressions into machine code. made queues important components of many systems.
With the help of an operator stack, an infi expression Specifically queues play a key role in the development of
can be converted into a postfi expression, which is an computer models to study the behavior of those systems.
366 C H A P T E R 8 Stacks and Queues
CROSSWORD PUZZLE
3 4
5 6
8 9
10
11
www.CrosswordWeaver.com
ACROSS DOWN
3. A two-dimensional array that directs the 1. The first widely used programming language.
conversion from infix notation to postfix
notation. 2. The smallest meaningful unit in a program.
7. The immediate superclass the Stack class. 4. The area of main memory that is allocated for
a run-time stack.
10. The information saved whenever a method
is called. 5. The worstTime(n) for the add (E element),
remove() and element() methods in the
11. A notation in which each operator LinkedList implementation of the Queue
immediately follows its operands. interface.
9. A simplification of a system.
Concept Exercises 367
CONCEPT EXERCISES
8.1 What advantage was obtained by implementing the List interface before declaring the Stack class?
8.2 Suppose we define
Show what the LinkedList object (referenced by) queue will look like after each of the following messages
is sent:
a. queue.add (2000);
b. queue. add (1215);
c. queue. add (1035);
d. queue. add (2117);
e. queue.remove();
f. queue. add (1999);
g. queue.remove();
8.3 Re-do Exercise 8.2, parts a through g, for a stack instead of a queue. Start with
stack.push (2000);
8.4 Suppose that elements "a", "b", "c", "d", "e" are pushed, in that order, onto an initially empty stack,
which is then popped four times, and as each element is popped, it is enqueued into an initially empty queue.
If one element is then dequeued from the queue, what is the next element to be dequeued?
8.5 Use a stack of activation records to trace the execution of the recursive fact method after an initial call of
fact (4). Here is the method definition
/**
/**
* Calculates n!.
*
* @param n the integer whose factorial is calculated.
*
* @return n!.
*
*/
protected static long fact (int n)
{
if (n <= 1)
return 1;
return n ∗ fact (n - 1);
} // method fact
368 C H A P T E R 8 Stacks and Queues
4
5 5
8 8 8
----- ----- ----- -----
9 7
8 72 72 65
----- ----- ----- -----
Convert the following expression into postfi notation and then use a stack to evaluate the expression:
5 + 2 ∗ (30 − 10/5)
PROGRAMMING EXERCISES
8.1 Declare and test the PureStack class (see Section 8.1.2) with a LinkedList field
Hint: Each of the def nitions is a one-liner.
8.2 Declare and test the PureStack class (see Section 8.1.2) with an ArrayList field
Hint: For the sake of efficiency the top of the stack should be at index size() - 1.
8.3 Develop an iterative, stack-based version of the ways method in Programming Exercise 5.7. Test your method
with unit testing.
Programming Exercises 369
Problem Expand the Car Wash Simulation Project with random arrival and service times. Use unit testing of new
methods after you have specified those methods.
Analysis The arrival times—with a Poisson distribution—should be generated randomly from the mean arrival time.
Speedo has added a new feature: The service time is not necessarily 10 minutes, but depends on what the customer
wants done, such as wash only, wash and wax, wash and vacuum, and so on. The service time for a car should
be calculated just before the car enters the wash station—that’s when the customer knows how much time will be
taken until the customer leaves the car wash. The service times, also with a Poisson distribution, should be generated
randomly from the mean service time with the same random-number generator used for arrival times.
The input consists of three positive integers: the mean arrival time, the mean service time, and the maximum
arrival time. Repeatedly re-prompt until each value is a positive integer.
Calculate the average waiting time and the average queue length, both to one fractional digit5 . The average
waiting time is the sum of the waiting times divided by the number of customers.
The average queue length is the sum of the queue lengths for each minute of the simulation divided by the
number of minutes until the last customer departs. To calculate the sum of the queue lengths, we add, for each
minute of the simulation, the total number of customers on the queue during that minute. We can calculate this
sum another way: we add, for each customer, the total number of minutes that customer was on the queue. But
this is the sum of the waiting times! So we can calculate the average queue length as the sum of the waiting times
divided by the number of minutes of the simulation until the last customer departs. And we already calculated the
sum of the waiting times for the average waiting time.
Also calculate the number of overflows Use a seed of 100 for the random-number generator, so the output
you get should have the same values as the output from the following tests.
5Given a double d, you can print d rounded to one fractional digit as follows: System.out.println (Math.round (d
* 10)/10.0);
The average waiting time was 1.7 minutes per car. The average queue length was 1.4 cars per minute. The
number of overflow was 3.
Programming Exercises 371
b ∗ a > a + c
6
2
7
The variable b gets the value 6, a gets 2, and c gets 7. The operator * has precedence over >, and + has
precedence over >, so the value of the above expression is true (12 is greater than 9).
Each variable will be given as an identifier consisting of lower-case letters only. All variables will be
integer-valued. There will be no constant literals. The legal operators and precedence levels—high to low—are:
*, /, %
+, - (that is, integer addition and subtraction)
>, >=, <=, <
==, !=
&&
|| <
Parenthesized subexpressions are legal. You need not do any input editing, that is, you may assume that the input
is a legal expression.
Hint See Lab 15 on converting infi to postfix and Concept Exercise 8.8. After constructing the postfix queue,
create values, an ArrayList object with Integer elements. The values object corresponds to symbolTable,
the ArrayList of identif ers. Use a stack, runTimeStack, for pushing and popping Integer and Boolean
elements. Because runTimeStack contains both Integer and Boolean elements, it should not have a type
argument.
The original version of the project is in the Chapter 5 subdirectory on the book’s website.
Programming Exercises 375
In this chapter we ‘‘branch’’ out from the linear structures of earlier chapters to introduce what is
essentially a non-linear construct: the binary tree. This brief chapter focuses on the definition and
properties of binary trees, and that will provide the necessary background for the next four chapters.
Chapters 10 through 13 will consider various specializations of binary trees: binary search trees, AVL
trees, decision trees, red-black trees, heaps, and Huffman trees. There is no question that the binary tree
is one of the most important concepts in computer science. Finally, to round out the picture, Chapter 15
presents the topic of trees in general.
CHAPTER OBJECTIVES
1. Understand binary-tree concepts and important properties, such as the Binary Tree Theorem
and the External Path Length Theorem.
2. Be able to perform various traversals of a binary tree.
A binary tree t is either empty or consists of an element, called the root element, and two distinct
binary trees, called the left subtree and right subtree of t.
We denote those subtrees as leftTree(t) and rightTree(t), respectively. Functional notation, such as
leftTree(t), is utilized instead of object notation, such as t.leftTree(), because there is no binary-tree
data structure. Why not? Different types of binary trees have widely differing methods—even different
parameters lists—for such operations as inserting and removing. Note that the definitio of a binary tree
is recursive, and many of the definition associated with binary trees are naturally recursive.
In depicting a binary tree, the root element is shown at the top, by convention. To suggest the
association between the root element and the left and right subtrees, we draw a southwesterly line from
the root element to the left subtree and a southeasterly line from the root element to the right subtree.
Figure 9.1 shows several binary trees.
The binary tree in Figure 9.1a is different from the binary tree in Figure 9.1b because B is in the
left subtree of Figure 9.1a but B is not in the left subtree of Figure 9.1b. As we will see in Chapter 15,
those two binary trees are equivalent when viewed as general trees.
A subtree of a binary tree is itself a binary tree, and so Figure 9.1a has seven binary trees: the whole
binary tree, the binary tree whose root element is B, the binary tree whose root element is C, and four
377
378 C H A P T E R 9 Binary Trees
A A
B C C B
(a) (b)
14
−
* + 18 16
A − D E 52 55 75 20
B C 61 63 58
(c) (d)
58
37 75
25 61
15 30 68
28 32
36
(e)
empty binary trees. Try to calculate the total number of subtrees for the tree in Figures 9.1 c, d, and e,
and hypothesize the formula to calculate the number of subtrees as a function of the number of elements.
The next section develops several properties of binary trees, and most of the properties are relevant
to the material in later chapters.
leaves(t ) = 1
else
leaves(t ) = leaves(leftTree(t )) + leaves(rightTree(t ))
This is a mathematical definition not a Java method. The last line in the above definitio states that the
number of leaves in t is equal to the number of leaves in t’s left subtree plus the number of leaves in t’s
right subtree. Just for practice, try to use this definitio to calculate the number of leaves in Figure 9.1a.
Of course, you can simply look at the whole tree and count the number of leaves, but the above definitio
of leaves(t) is atomic rather than holistic.
Each element in a binary tree is uniquely determined by its location in the tree. For example, let t
be the binary tree shown in Figure 9.1c. There are two elements in t with value ‘–’. We can distinguish
between them by referring to one of them as “the element whose value is ‘–’ and whose location is at
the root of t” and the other one as “the element whose value is ‘–’ and whose location is at the root of
the right subtree of the left subtree of t.” We loosely refer to “an element” in a binary tree when, strictly
speaking, we should say “the element at such and such a location.”
Some binary-tree concepts use familial terminology. Let t be the binary tree shown in Figure 9.2.
We say that x is the parent of y and that y is the left child of x . Similarly, we say that x is the parent
of z and that z is the right child of x .
In a binary tree, each element has zero, one, or two children. For example, in Figure 9.1d, 14 has
two children, 18 and 16; 55 has 58 as its only child; 61 is childless, that is, it is a leaf. For any element w
in a tree, we write parent(w ) for the parent of w , left(w ) for the left child of w and right(w ) for the right
child of w .
In a binary tree, the root element does not have a parent, and every other element has exactly one
parent. Continuing with the terminology of a family tree, we could defin sibling, grandparent, grandchild,
firs cousin, ancestor and descendant. For example, an element A is an ancestor of an element B if B is
in the subtree whose root element is A. To put it recursively, A is an ancestor of B if A is the parent of
B or if A is an ancestor of the parent of B . Try to defin “descendant” recursively.
If A is an ancestor of B, the path from A to B is the sequence of elements, starting with A and
ending with B , in which each element in the sequence (except the last) is the parent of the next element.
For example, in Figure 9.1e, the sequence 37, 25, 30, 32 is the path from 37 to 32.
Informally, the height of a binary tree is the number of branches between the root and the farthest
leaf, that is, the leaf with the most ancestors. For example, Figure 9.3 has a binary tree of height 3.
The height of the tree in Figure 9.3 (next page) is 3 because the path from E to S has three branches.
Suppose for some binary tree, the left subtree has a height of 12 and the right subtree has a height of 20.
What is the height of the whole tree? The answer is 21.
In general, the height of a tree is one more than the maximum of the heights of the left and right
subtrees. This leads us to a recursive definitio of the height of a binary tree. But f rst, we need to know
what the base case is, namely, the height of an empty tree. We want the height of a single-element tree
to be 0: there are no branches from the root element to itself. But that means that 0 is one more than the
maximum heights of the left and right subtrees, which are both empty. So we need to defin the height of
an empty subtree to be, strangely enough, −1.
y z
FIGURE 9.2 A binary tree with one parent and two children
380 C H A P T E R 9 Binary Trees
T A
I O N
level
E 0
T A 1
I O N 2
S 3
else
level(x ) = 1+ level(parent(x ))
An element’s level is also referred to as that element’s depth. Curiously, the height of a non-empty binary
tree is the depth of the farthest leaf.
A two-tree is a binary tree that either is empty or in which each non-leaf has 2 branches going down
from it. For example, Figure 9.5a has a two-tree and the tree in Figure 9.5b is not a two-tree.
Recursively speaking, a binary tree t is a two-tree if:
A binary tree t is full if t is a two-tree with all of its leaves on the same level. For example, the tree in
Figure 9.6a is full and the tree in Figure 9.6b is not full.
Recursively speaking, a binary tree t is full if:
t is empty
or
t ’s left and right subtrees have the same height and both are full.
Of course, every full binary tree is a two-tree but the converse is not necessarily true. For example, the
tree in Figure 9.6b is a two-tree but is not full. For full binary trees, there is a relationship between the
q v
c g
r c d z t
(b)
f e t g
(a)
FIGURE 9.5 (a) a two tree; (b) a binary tree that is not a two tree
50 50
20 90 20 90
5 28 80 99 80 99
(a) (b)
FIGURE 9.6 (a) a full binary tree; (b) a binary tree that is not full
382 C H A P T E R 9 Binary Trees
A1
A2 A3
A4 A5 A6 A7
FIGURE 9.7 A full binary tree of height 2; such a tree must have exactly 7 elements
height and number of elements in the tree. For example, the full binary tree in Figure 9.7 has a height of
2, so the tree must have exactly 7 elements:
How many elements must there be in a full binary tree of height 3? Of height 4? For a full binary
tree t, can you conjecture the formula for the number of elements in t as a function of height(t)? The
answer can be found in Section 9.3.
A binary tree t is complete if t is full through the next-to-lowest level and all of the leaves at the
lowest level are as far to the left as possible. By “lowest level,” we mean the level farthest from the root.
Any full binary tree is complete, but the converse is not necessarily true. For example, Figure 9.8a
has a complete binary tree that is not full. The tree in Figure 9.8b is not complete because it is not full at
the next-to-lowest level: C has only one child. The tree in Figure 9.8c is not complete because leaves I
and J are not as far to the left as they could be.
A A A
B C B C B C
D E F G D E F D E F G
H IJ G HI J H I J
(a) (b) (c)
In a complete binary tree, we can associate a “position” with each element. The root element is
assigned a position of 0. For any nonnegative integer i , if the element at position i has children, the
position of its left child is 2i + 1 and the position of its right child is 2i + 2. For example, if a complete
binary tree has ten elements, the positions of those elements are as indicated in Figure 9.9.
If we use a left shift (that is, operator <<) of one bit, we can achieve the same effect as multiplying
by 2, but much faster. Then the children of the element at position i are at positions
(i << 1) + 1 and (i << 1) + 2
In Figure 9.9, the parent of the element at position 8 is in position 3, and the parent of the element in
position 5 is in position 2. In general, if i is a positive integer, the position of the parent of the element
in position i is in position (i − 1) >> 1; we use a right shift of one bit instead of division by 2.
The position of an element is important because we can implement a complete binary tree with a
contiguous collection such as an array or an ArrayList. Specifically we will store the element that is at
position i in the tree at index i in the array. For example, Figure 9.10 shows an array with the elements
from Figure 9.8a.
9.3 The Binary Tree Theorem 383
1 2
3 4 5 6
7 8 9
FIGURE 9.9 The association of consecutive integers to elements in a complete binary tree
A B C D E F G H I J
0 1 2 3 4 5 6 7 8 9
FIGURE 9.10 An array that holds the elements from the complete binary tree in Figure 9.8a
If a complete binary tree is implemented with an ArrayList object or array object, the random-
access property of arrays allows us to access the children of a parent (or parent of a child) in constant
time. That is exactly what we will do in Chapter 13.
We have shown how we can recursively calculate leaves(t), the number of leaves in a binary tree
t, and height(t), the height of a binary tree t. We can also recursively calculate the number of elements,
n(t), in t:
if t is empty
n(t ) = 0
else
n(t ) = 1 + n(leftTree(t )) + n(rightTree(t ))
200
x 120
FIGURE 9.11 A binary tree t with (n(t ) + 1)/2 leaves that is not a two tree
Note: Because 2.0 is the denominator of the division in part 1, the quotient is a f oating point value.
For example, 7/2.0 = 3.5. We cannot use integer division because of part 3: let t be the binary tree in
Figure 9.11.
For the tree in Figure 9.11, leaves(t) = 2 = (n(t) + 1)/2 if we use integer division. But t is not a
two-tree. Note that (n(t) + 1)/2.0 = 2.5.
Parts 3 and 4 each entail two sub-parts. For example, for part 3, we must show that if t is a non-empty
two-tree, then
n(t) + 1
leaves(t) =
2.0
And if
n(t) + 1
leaves(t) =
2.0
then t must be a non-empty two-tree.
All six parts of this theorem can be proved by induction on the height of t. As it turns out, most
theorems about binary trees can be proved by induction on the height of the tree. The reason for this is
that if t is a binary tree, then both leftTree(t) and rightTree(t) have height less than height(t), and so the
Strong Form of the Principle of Mathematical Induction (see Section A2.5 of Appendix 2) often applies.
For example, Example A2.5 of Appendix 2 has a proof of part 1. The proofs of the remaining parts are
left as Exercises 9.13 and 9.14, with a hint for each exercise.
Suppose t is a full binary tree (possibly empty), then from part 4 of the Binary Tree Theorem, and
the fact that any empty tree has height of −1, we have the equation
n(t) + 1
= 2height(t)
2.0
If we solve this equation for height(t), we get
height(t) = log2 ((n(t) + 1)/2.0)
= log2 (n(t) + 1) − 1
So we can say that the height of a full tree is logarithmic in n, where n is the number of elements in the
tree; we often use n instead of n(t) when it is clear which tree we are referring to. Even if t is merely
complete, its height is still logarithmic in n. See Concept Exercise 9.7. On the other hand, t could be a
chain. A chain is a binary tree in which each non-leaf has exactly one child. For example, Figure 9.12
has an example of a binary tree that is a chain.
If t is a chain, then height(t) = n(t) − 1, so for chains the height is linear in n. Much of our work
with trees in subsequent chapters will be concerned with maintaining logarithmic-in-n height and avoiding
linear-in-n height. Basically, for inserting into or removing from certain kinds of binary trees whose height
is logarithmic in n, worstTime(n) is logarithmic in n. That is why, in many applications, binary trees are
preferable to lists. Recall that with both ArrayList objects and LinkedList objects, for inserting or
removing at a specifi index, worstTime(n) is linear in n.
9.4 External Path Length 385
50
10
20
30
25
FIGURE 9.12 A binary tree that is a chain: each non-leaf has exactly one child
Proof It follows from the Binary Tree Theorem that if a nonempty binary tree is full and has height h, then
the tree has 2h leaves. And we can obtain any binary tree by ‘‘pruning’’ a full binary tree of the same height;
b c
d e
f g
h i j k
l m
in so doing we reduce the number of leaves in the tree. So any non-empty binary tree of height h has no
more than 2h leaves. To put that in a slightly different way, if k is any positive integer, any nonempty binary
tree of height floor(log2 k) has no more than k leaves. (We have to use the floor function because the height
must be an integer, but log2 k might not be an integer.)
Now suppose t is a nonempty binary tree whose height is floor(log2 k) for some positive integer k. By
the previous paragraph, t has no more than k leaves. How many of those leaves will be at level floor(log2 k),
the level farthest from the root? To answer that question, we ask how many leaves must be at a level less
than floor(log2 k). That is, how many leaves must there be in the subtree t of t formed by removing all leaves
at level floor(log2 k)? The height of t is floor(log2 k) − 1. Note that
floor(log2 k) − 1 = floor(log2 k − 1)
= floor(log2 k − log2 2)
= floor(log2 (k/2))
By the previous paragraph, the total number of leaves in t is no more than k/2. But every leaf in t that is at a
level less than floor(log2 k) is also a leaf in t . And so there must be, at least, k/2 leaves at level floor(log2 k).
Each of those k/2 leaves contributes floor(log2 k) to the external path length, so we must have
E(t) ≥ (k/2) floor (log2 k)
Note: This result is all we will need in Chapter 11, but at a cost of a somewhat more complicated proof,
we could show that E(t) ≥ k log2 k for any non-empty two-tree with k leaves. (See Cormen, [2002] for
details.)
Traversal 1. inOrder Traversal: Left-Root-Right The basic idea of this recursive algorithm is that we
firs perform an inOrder traversal of the left subtree, then we process the root element, and f nally, we
perform an inOrder traversal of the right subtree. Here is the algorithm—assume that t is a binary tree:
inOrder (t )
{
if (t is not empty)
{
inOrder (leftTree (t ));
process the root element of t ;
inOrder (rightTree (t ));
} // if
} // inOrder traversal
Let n represent the number of elements in the tree. Corresponding to each element there are 2 subtrees,
so there will be 2n recursive calls to inOrder(t). We conclude that worstTime(n) is linear in n. Ditto for
averageTime(n).
9.5 Traversals of a Binary Tree 387
31
47 50
42 25
We can use this recursive description to list the elements in an inOrder traversal of the binary tree
in Figure 9.14.
The tree t in Figure 9.14 is not empty, so we start by performing an inOrder traversal of leftTree(t),
namely,
47
This one-element tree becomes the current version of t. Since its left subtree is empty, we process the root
element of this t, namely 47. That completes the traversal of this version of t since rightTree(t) is empty.
So now t again refers to the original tree. We next process t’s root element, namely,
31
50
42 25
This becomes the current version of t. We start by performing an inOrder traversal of leftTree(t), namely,
42
Now this tree with one element becomes the current version of t. Since its left subtree is empty, we process
t’s root element, 42. The right subtree of this t is empty. So we have completed the inOrder traversal of
the tree with the single element 42, and now, once again, t refers to the binary tree with 3 elements:
50
42 25
50
25
Since the left subtree of this single-element tree t is empty, we process the root element of t, namely 25.
We are now done since t’s right subtree is also empty.
388 C H A P T E R 9 Binary Trees
31
47 50
42 25
47 31 42 50 25
Figure 9.15 shows the original tree, with arrows to indicate the order in which elements are processed:
The inOrder traversal gets its name from the fact that, for a special kind of binary tree—a binary
search tree—an inOrder traversal will process the elements in order. For example, Figure 9.16 has a binary
search tree:
An inOrder traversal processes the elements of the tree in Figure 9.16 as follows:
In a binary search tree, all of the elements in the left subtree are less than the root element, which is less
than all of the elements in the right subtree. What recursive property do you think will be part of the
definitio of a binary search tree so that an inOrder traversal processes the elements in order? Hint: the
binary tree in Figure 9.17 is not a binary search tree:
We will devote Chapters 10 and 12 to the study of binary search trees.
31
25 47
42 50
31
25 47
50 42
Traversal 2. postOrder Traversal: Left-Right-Root The idea behind this recursive algorithm is that we
perform postOrder traversals of the left and right subtrees before processing the root element. The algorithm,
with t a binary tree, is:
postOrder (t )
{
if (t is not empty)
{
postOrder (leftTree (t ));
postOrder (rightTree (t ));
process the root element of t ;
} // if
} // postOrder traversal
Just as with an inOrder traversal, the worstTime(n) for a postOrder traversal is linear in n because there
are 2n recursive calls to postOrder(t).
Suppose we conduct a postOrder traversal of the binary tree in Figure 9.18.
A postOrder traversal of the binary tree in Figure 9.18 will process the elements in the path shown
in Figure 9.19.
In a linear form, the postOrder traversal shown in Figure 9.19 is
A B C+∗
A +
B C
A +
B C
FIGURE 9.19 The path followed by a postOrder traversal of the binary tree in Figure 9.18
390 C H A P T E R 9 Binary Trees
We can view the above binary tree as an expression tree: each non-leaf is a binary operator whose operands
are the associated left and right subtrees. With this interpretation, a postOrder traversal produces postfi
notation.
Traversal 3. preOrder Traversal: Root-Left-Right Here we process the root element and then perform
preOrder traversals of the left and right subtrees. The algorithm, with t a binary tree, is:
preOrder (t )
{
if (t is not empty)
{
process the root element of t ;
preOrder (leftTree (t ));
preOrder (rightTree (t ));
} // if
} // preOrder traversal
As with the inOrder and postOrder algorithms, worstTime(n) is linear in n.
For example, a preOrder traversal of the binary tree in Figure 9.20 will process the elements in the
order indicated in Figure 9.21.
If we linearize the path in Figure 9.21, we get
∗ A+B C
A +
B C
A +
B C
FIGURE 9.21 The path followed by a preOrder traversal of the elements in the expression tree of Figure 9.20
9.5 Traversals of a Binary Tree 391
B C
D E F
G H
I J
element sought is found, so the traversal may not be completed. For an example of a depth-firs search,
Figure 9.22 shows a binary tree and the path followed by a depth-firs search for H.
The backtracking strategy from Chapter 5 includes a depth-firs search, but at each stage there may
be more than two choices. For example, in the maze-search, the choices are to move north, east, south,
or west. Because moving north is the f rst option, that option will be repeatedly applied until either the
goal is reached or moving north is not possible. Then a move east will be taken, if possible, and then as
many moves north as possible or necessary. And so on. In Chapter 15, we will re-visit backtracking for a
generalization of binary trees.
B C
D E F
G H
I J
if (t is not empty)
{
queue.enqueue (t );
while (queue is not empty)
{
tree = queue.dequeue();
process tree’s root;
if (leftTree (tree) is not empty)
queue.enqueue (leftTree (tree));
if (rightTree (tree) is not empty)
queue.enqueue (rightTree (tree));
} // while
} // if t not empty
} // breadthFirst traversal
During each loop iteration, one element is processed, so worstTime(n) is linear in n.
We used a queue for a breadth-firs traversal because we wanted the subtrees retrieved in the same
order they were saved (First-In, First-Out). With inOrder, postOrder, and preOrder traversals, the subtrees
are retrieved in the reverse of the order they were saved in (Last-In, First-Out). For each of those three
traversals, we utilized recursion, which, as we saw in Chapter 8, can be replaced with an iterative, stack-
based algorithm.
We will encounter this type of traversal again in Chapter 15 when we study breadth-firs traversals of
structures less restrictive than binary trees. Incidentally, if we are willing to be more restrictive, specifically
if we require a complete binary tree, then the tree can be implemented with an array, and a breadth-firs
traversal is simply an iteration through the array. The root element is at index 0, the root’s left child at
index 1, the root’s right child at index 2, the root’s leftmost grandchild at index 3, and so on.
Summary 393
SUMMARY
A binary tree t is either empty or consists of an ele- For a binary tree t , the external path length of t ,
ment, called the root element , and two distinct binary written E(t ), is the sum of the distances from the root
trees, called the left subtree and right subtree of t . This to the leaves of t . A lower bound for comparison-based
is a recursive defin tion, and there are recursive defin tions sorting algorithms can be obtained from the
for many of the related terms: height, number of leaves,
number of elements, two-tree, full tree, and so on. The
inter-relationships among some of these terms is given External Path Length Theorem: Let t be a
by the binary tree with k > 0 leaves. Then
E(t) ≥ (k/2) floor (log2 k)
CROSSWORD PUZZLE
2 3
4 5 6
10
www.CrosswordWeaver.com
ACROSS DOWN
4. In a binary tree, an element with no children. 1. A synonym of “level,” the function that calculates the
length of the path from the root to a given element.
7. The ______ path length of a binary tree is the sum of
the depths of all the leaves in the tree. 2. Another name for “preOrder” traversal.
8. A binary tree t is a ______ if t has at most one element 3. A __________ is a binary tree in which each non-leaf
or leftTree(t) and rightTree(t) are non-empty has exactly one child.
two-trees.
5. A binary tree t is __________ if t is a two-tree with all
9. The only traversal in this chapter described of its leaves on the same level.
non-recursively.
6. An algorithm that processes each element in a binary
10. A binary tree is ________ if t is full through the tree exactly once.
next-to-lowest level and all of the leaves at the lowest
level ara as far to the left as possible.
Concept Exercises 395
CONCEPT EXERCISES
9.1 Answer the questions below about the following binary tree:
C D
E F G
n. What would the output be if the elements were written out during a postOrder traversal?
o. What would the output be if the elements were written out during a preOrder traversal?
p. What would the output be if the elements were written out during a breadth-firs traversal?
9.2 a. Construct a binary tree of height 3 that has 8 elements.
b. Can you construct a binary tree of height 2 that has 8 elements?
c. For n going from 1 to 20, determine the minimum height possible for a binary tree with n elements.
d. Based on your calculations in part c, try to develop a formula for the minimum height possible for a binary
tree with n elements, where n can be any positive integer.
e. Use the Principle of Mathematical Induction (Strong Form) to prove the correctness of your formula in
part d.
9.3 a. What is the maximum number of leaves possible in a binary tree with 10 elements? Construct such a tree.
b. What is the minimum number of leaves possible in a binary tree with 10 elements? Construct such
a tree.
9.4 a. Construct a two-tree that is not complete.
b. Construct a complete tree that is not a two-tree.
c. Construct a complete two-tree that is not full.
d. How many leaves are there in a two-tree with 17 elements?
e. How many leaves are there in a two-tree with 731 elements?
f. A non-empty two-tree must always have an odd number of elements. Why?
Hint: Use the Binary Tree Theorem and the fact that the number of leaves must be an integer.
g. How many elements are there in a full binary tree of height 4?
h. How many elements are there in a full binary tree of height 12?
i. Use induction (original form) on the height of the tree to show that any full binary tree is a two tree.
j. Use the results from part i and the Binary Tree Theorem to determine the number of leaves in a full binary
tree with 63 elements.
k. Construct a complete two-tree that is not full, but in which the heights of the left and right subtrees are
equal.
9.5 For the following binary tree, show the order in which elements would be visited for an inOrder, postOrder,
preOrder, and breadthFirst traversal.
Concept Exercises 397
60
38 97
14 48 68
25 62 85
9.6 Show that a binary tree with n elements has 2n + 1 subtrees (including the entire tree). How many of these
subtrees are empty?
9.7 Show that if t is a complete binary tree, then
height(t ) = floo (log2 (n(t )))
Hint: Let t be a complete binary tree of height k ≥ 0, and let t 1 be a full binary tree of height k − 1. Then
n(t 1) + 1 ≤ n(t ). Use Part 4 of the Binary Tree Theorem to show that floor(lo 2 (n(t 1) + 1)) = k , and use
Part 1 of the Binary Tree Theorem to show that floor(lo 2 (n(t ))) < k + 1.
9.8 The Binary Tree Theorem is stated for non-empty binary trees. Show that parts 1, 2, and 4 hold even for an
empty binary tree.
9.9 Give an example of a non-empty binary tree that is not a two-tree but
leaves(t ) = (n(t ) + 1) / 2
Hint: The denominator is 2, not 2.0, so integer division is performed.
9.10 Let t be a non-empty tree. Show that if
n(t ) + 1
leaves(t ) =
2.0
then either both subtrees of t are empty or both subtrees of t are non-empty.
Note: Do not use Part 3 of the Binary Tree Theorem. This exercise can be used in the proof of Part 3.
9.11 Show that in any complete binary tree t , at least half of the elements are leaves.
Hint: if t is empty, there are no elements, so the claim is vacuously true. If the leaf at the highest index is a
right child, then t is a two-tree, and the claim follows from part 3 of the Binary Tree Theorem. Otherwise, t
was formed by adding a left child to the complete two-tree with n(t ) − 1 elements.
9.12 Compare the inOrder traversal algorithm in Section 9.5 with the move method from the Towers of Hanoi
application in Section 5.4 of Chapter 5. They have the same structure, but worstTime(n) is linear in n for the
inOrder algorithm and exponential in n for the move method. Explain.
398 C H A P T E R 9 Binary Trees
9.13 Let t be a nonempty binary tree. Use the Strong Form of the Principle of Mathematical Induction to prove
each of the following parts of the Binary Tree Theorem:
n(t ) + 1
a. ≤ 2height(t)
2.0
n(t ) + 1
b. If t is a two-tree, then leaves(t ) =
2.0
n(t ) + 1
c. If t is a full tree, then = 2height(t)
2.0
Hint: The outline of the proof is the same as in Example A2.5 of Appendix 2.
9.14 Let t be a nonempty binary tree. Use the Strong Form of the Principle of Mathematical Induction to prove
each of the following parts of the Binary Tree Theorem:
n(t ) + 1
a. If leaves(t ) = then t is a two-tree.
2.0
n(t ) + 1
b. If = 2height(t) then t is a full tree.
2.0
Hint: The proof for both parts has the same outline. For example, here is the outline for part a:
In the inductive case, let h be any nonnegative integer and assume that S0 , S1 , . . ., Sh are all true. To show
that Sh+1 is true, let t be a binary tree of height h + 1 such that
n(t ) + 1
leaves(t ) =
2.0
First, show that
n(leftTree(t )) + 1 n(rightTree(t )) + 1
leaves(leftTree(t )) + leaves(rightTree(t )) = +
2.0 2.0
For any non-negative integers a, b, c, and d, if
2 3
4 5 6
7 8 9 10
11 12
1 + 2 + 3 + 4 + . . . + (h + 1) = n
See Example A2.1 in Appendix 2.
Note: This exercise is contrived but, in fact, the estimate of the average height of a binary tree is the same
as the answer to this exercise (see Flajolet, [1981]).
This page intentionally left blank
Binary Search Trees CHAPTER 10
In Chapter 9, you studied an important conceptual tool: the binary tree. This chapter presents
the binary search tree data type and a corresponding data structure, the BinarySearchTree class.
BinarySearchTree objects are valuable collections because they require only logarithmic time, on
average, for inserting, removing, and searching (but linear time in the worst case). This performance
is far better than the linear average-case time for insertions, removals and searches in an array,
ArrayList or LinkedList object. For example, if n = 1, 000, 000, 000, log2 n < 30.
The BinarySearchTree class is not part of the Java Collections Framework. The reason for this
omission is that the framework already includes the TreeSet class, which boasts logarithmic time for
inserting, removing, and searching even in the worst case. The BinarySearchTree class requires linear-
in-n time, in the worst case, for those three operations. So the BinarySearchTree class should be viewed
as a “toy” class that is simple enough for you to play with and will help you to better understand the
TreeSet class. The implementation of the TreeSet class is based on a kind of “balanced” binary search
tree, namely, the red-black tree.
To further prepare you for the study of red-black trees and the TreeSet class, this chapter also
explains what it means to say that a binary search tree is balanced. To add some substance to that discussion,
we introduce the AVL tree data type, which is somewhat simpler to understand than the red-black tree
data type.
CHAPTER OBJECTIVES
1. Compare the time efficiency of the BinarySearchTree class’s insertion, removal and
search methods to that of the corresponding methods in the ArrayList and LinkedList
classes.
2. Discuss the similarities and differences of the BinarySearchTree class’s contains method
and the binarySearch methods in the Arrays and Collections classes.
3. Explain why the BinarySearchTree class’s remove method and the TreeIterator class’s
next method are somewhat difficult to define.
4. Be able to perform each of the four possible rotations.
5. Understand why the height of an AVL tree is always logarithmic in n.
401
402 C H A P T E R 10 Binary Search Trees
15 25 28 30 32 36 37 50 55 59 61 68 75
As we have define a binary search tree, duplicate elements are not permitted. Some definition have “less
than or equal to” and “greater than or equal to” in the above definition For the sake of consistency with
some classes in the Java Collections Framework that are based on binary search trees, we opt for strictly
less than and strictly greater than.
Section 10.1.1 describes the BinarySearchTree class. As we noted at the beginning of this chapter,
this is a “toy” class that provides a gentle introduction to related classes in the Java Collections Framework.
Much of the code for the BinarySearchTree class is either identical to or a simplificatio of code in the
TreeMap class of the Java Collections Framework. Overall, the BinarySearchTree class’s performance
is not good enough for applications—because its worst-case height is linear in n —but you will be asked
to add new methods and to modify existing methods.
50
37 75
25 61
15 30 55 68
28 32 59
36
From now on, when we refer to the “natural” order of elements, we assume that the element’s class
implements the Comparable interface; then the natural order is the order imposed by the compareTo
method in the element’s class. For example, the Integer class implements the Comparable interface,
and the “natural” order of Integer objects is based on the numeric comparison of the underlying int
values. To illustrate this ordering, suppose we have the following:
Integer myInt = 25;
The output will be less than 0 because 25 is less than 107. Specifically the output will be −1 because
when the compareTo method is used for numeric comparisons, the result is either −1, 0, or 1, depending
on whether the calling object is less than, equal to, or greater than the argument.
404 C H A P T E R 10 Binary Search Trees
For a more involved example, the following Student class has name and gpa fields The compareTo
method will use the alphabetical ordering of names; for equal names, the ordering will be by decreasing
grade point averages. For example,
new Student ("Lodato", 3.8).compareTo (new Student ("Zsoldos", 3.5))
returns 1 because 3.4 is less than 3.6, and the return value reflect decreasing order. Similarly,
new Student ("Dufresne", 3.8).compareTo (new Student ("Dufresne", 3.6))
public Student() { }
/**
* Initializes this Student object from a specified name and gpa.
*
* @param name – the specified name.
* @param gpa – the specified gpa.
*
*/
public Student (String name, double gpa)
{
this.name = name;
this.gpa = gpa;
} // constructor
/**
* Compares this Student object with a specified Student object.
* The comparison is alphabetical; for two objects with the same name,
* the comparison is by grade point averages.
*
* @param otherStudent – the specified Student object that this Student
* object is being compared to.
*
* @return -1, if this Student object’s name is alphabetically less than
* otherStudent’s name, or if the names are equal and this
* Student object’s grade point average is at least DELTA
* greater than otherStudent’s grade point average;
10.1 Binary Search Trees 405
/**
* Determines if this Student object’s name and grade point average are
* the same as some specified object’s.
*
* @param obj – the specified object that this Student object is being
* compared to.
*
* @return true – if obj is a Student object and this Student object has the
* same name and almost (that is, within DELTA) the same point average as obj.
*
*/
public boolean equals (Object obj)
{
if (! (obj instanceof Student))
return false;
return this.compareTo ((Student)obj) == 0;
} // method equals
/**
* Returns a String representation of this Student object.
*
* @return a String representation of this Student object: name, blank,
406 C H A P T E R 10 Binary Search Trees
} // class Student
Rather than starting from scratch, we will let the BinarySearchTree class extend some class already
in the Framework. Then we need implement only those methods whose definition are specifi to the
BinarySearchTree class. The AbstractSet class is a good place to start. That class has garden-variety
implementations for many of the Set methods: isEmpty, toArray, clear, and the bulk operations
(addAll, containsAll, removeAll and retainAll). So the class heading is
public class BinarySearchTree<E> extends AbstractSet<E>
Figure 10.2 shows the relationships among the classes and interfaces we have discussed so far.
E
<<interface>>
Collection
E
<<interface>>
Set
E
AbstractSet
+ size():int(abstract)
+ toString(): String
…
E
BinarySearchTree
+ BinarySearchTree()
+ size():int
+ add (element: E):boolean
…
FIGURE 10.2 A UML diagram that includes part of the BinarySearchTree class
10.1 Binary Search Trees 407
Here are the method specification for the methods we will explicitly implement:
/**
* Initializes this BinarySearchTree object to be empty, to contain only elements
* of type E, to be ordered by the Comparable interface, and to contain no
* duplicate elements.
*
*/
public BinarySearchTree()
/**
* Initializes this BinarySearchTree object to contain a shallow copy of
* a specified BinarySearchTree object.
* The worstTime(n) is O(n), where n is the number of elements in the
* specified BinarySearchTree object.
*
* @param otherTree - the specified BinarySearchTree object that this
* BinarySearchTree object will be assigned a shallow copy of.
*
*/
public BinarySearchTree (BinarySearchTree<? extends E> otherTree)
/**
* Returns the size of this BinarySearchTree object.
*
* @return the size of this BinarySearchTree object.
*
*/
public int size( )
/**
* Returns an iterator positioned at the smallest element in this
* BinarySearchTree object.
*
* @return an iterator positioned at the smallest element (according to
* the element class’s implementation of the Comparable
* interface) in this BinarySearchTree object.
*
*/
public Iterator<E> iterator()
/**
* Determines if there is an element in this BinarySearchTree object that
* equals a specified element.
* The worstTime(n) is O(n) and averageTime(n) is O(log n).
*
* @param obj – the element sought in this BinarySearchTree object.
*
408 C H A P T E R 10 Binary Search Trees
/**
* Ensures that this BinarySearchTree object contains a specified element.
* The worstTime(n) is O(n) and averageTime(n) is O(log n).
*
* @param element – the element whose presence is ensured in this
* BinarySearchTree object.
*
* @return true – if this BinarySearchTree object changed as a result of this
* method call (that is, if element was actually inserted); otherwise,
* return false.
*
* @throws ClassCastException – if element is not null but cannot be compared
* to the elements of this BinarySearchTree object.
* @throws NullPointerException – if element is null.
*
*/
public boolean add (E element)
/**
* Ensures that this BinarySearchTree object does not contain a specified
* element.
* The worstTime(n) is O(n) and averageTime(n) is O(log n).
*
* @param obj – the object whose absence is ensured in this
* BinarySearchTree object.
*
* @return true – if this BinarySearchTree object changed as a result of this
* method call (that is, if obj was actually removed); otherwise,
* return false.
*
* @throws ClassCastException – if obj is not null but cannot be compared to the
* elements of this BinarySearchTree object.
* @throws NullPointerException – if obj is null.
*
*/
public boolean remove (Object obj)
10.1 Binary Search Trees 409
These method specifications together with the method specification of methods not overridden from the
AbstractSet class, constitute the abstract data type Binary Search Tree: all that is needed for using
the BinarySearchTree class. For example, here is a small class that creates and manipulates three
BinarySearchTree objects: two of String elements and one of Student elements (for the Student
class declared earlier in this section):
public class BinarySearchTreeExample
{
public static void main (String[ ] args)
{
new BinarySearchTreeExample().run();
} // method main
tree1.add ("yes");
tree1.add ("no");
tree1.add ("maybe");
tree1.add ("always");
tree1.add ("no"); // not added: duplicate element
if (tree1.remove ("often"))
System.out.println ("How did that happen?");
else
System.out.println (tree1.remove ("maybe"));
System.out.println (tree1);
BinarySearchTree<String> tree2 =
new BinarySearchTree<String> (tree1);
System.out.println (tree2);
BinarySearchTree<Student> tree3 =
new BinarySearchTree<Student>();
} // class BinarySearchTreeExample
The output is
true
[always, no, yes]
410 C H A P T E R 10 Binary Search Trees
@Before
public void RunBeforeEachTest()
{
tree = new BinarySearchTree<String>();
} // method RunBeforeEachTest
@Test
public void testAdd()
{
tree.add ("b");
tree.add ("a");
tree.add ("c");
tree.add ("e");
tree.add ("c");
tree.add ("d");
assertEquals ("[a, b, c, d, e]", tree.toString());
} // method testAdd
@Test
public void testContains()
{
tree.add ("a");
tree.add ("b");
tree.add ("c");
assertEquals (true, tree.contains ("a"));
assertEquals (true, tree.contains ("b"));
assertEquals (true, tree.contains ("c"));
assertEquals (false, tree.contains ("x"));
assertEquals (false, tree.contains (""));
} // method testContains
@Test
public void testRemove()
{
10.1 Binary Search Trees 411
tree.add ("b");
tree.add ("a");
tree.add ("c");
assertEquals (true, tree.remove ("b"));
assertEquals (2, tree.size());
assertEquals (false, tree.remove ("b"));
assertEquals (2, tree.size());
} // method testRemove
The complete test class, available from the book’s website, includes an override of the Abstract
Set class’s equals (Object obj) method to return false when comparing two BinarySearchTree
objects that have the same elements but different structures.
The Entry class, as you may have expected, has an element field of type (reference to) E, and left
and right fields of type (reference to) Entry<E>. To facilitate going back up the tree during an iteration,
the Entry class will also have a parent field of type (reference to) Entry<E>. Figure 10.3 shows the
representation of a BinarySearchTree object with elements of type (reference to) String. To simplify
the f gure, we pretend that the elements are of type String instead of reference to String.
Here is the nested Entry class:
protected static class Entry<E>
{
protected E element;
Eric null
size
4
right = null,
parent;
/**
* Initializes this Entry object.
*
* This default constructor is defined for the sake of subclasses of
* the BinarySearchTree class.
*/
public Entry() { }
/**
* Initializes this Entry object from element and parent.
*
*/
public Entry (E element, Entry<E> parent)
{
this.element = element;
this.parent = parent;
} // constructor
} // class Entry
Recall, from Section 7.3.5, that the Entry class nested in the Java Collection Framework’s LinkedList
class was given the static modifier The BinarySearchTree’s nested Entry class is made static for
the same reason: to avoid the wasted time and space needed to maintain a reference back to the enclosing
object.
Now that we have declared the f elds and one of the nested classes, we are ready to tackle the
BinarySearchTree method definitions
root = null;
size = 0;
} // default constructor
We could have omitted the assignments to the root and size field because each fiel is given a default
initial value (null for a reference, 0 for an int, false for a boolean, and so on) just prior to the
invocation of the constructor. But explicit assignments facilitate understanding; default initializations do
not.
It is easy enough to defin a copy constructor that iterates through otherTree and adds each element
from otherTree to the calling object. But then the iteration over otherTree will be inOrder, so the new
tree will be a chain, and worstTime(n) will be quadratic in n. To obtain linear-in-n time, we construct the
new tree one entry at a time, starting at the root. Because each entry has an element, a parent, a left child,
and a right child, we create the new entry from otherTree’s entry and the new entry’s parent. The new
entry’s left (or right) child is then copied recursively from otherTree’s left (or right) child and from the
new entry—the parent of that child.
We start with a wrapper method that calls the recursive method:
int comp;
if (obj == null)
throw new NullPointerException();
The cast of obj to a Comparable object is necessary for the compiler because the Object class does not
have a compareTo method.
How long does this method take? For this method, indeed, for all of the remaining methods in this
section, the essential feature for estimating worstTime(n) or averageTime(n) is the height of the tree.
Specifically for the contains method, suppose the search is successful; a similar analysis can be used for
an unsuccessful search. In the worst case, we will have a chain, and will be seeking the leaf. For example,
suppose we are seeking 25 in the binary search tree in Figure 10.4.
In such a case, the number of loop iterations is equal to the height of the tree. In general, if n is the
number of elements in the tree, and the tree is a chain, the height of the tree is n − 1, so worstTime(n) is
linear in n for the contains method.
We now determine averageTime(n) for a successful search. Again, the crucial factor is the height of
the tree. For binary search trees constructed through random insertions and removals, the average height
H is logarithmic in n —(see Cormen, [2002]). The contains method starts searching at level 0, and each
loop iteration descends to the next lower level in the tree. Since averageTime(n) requires no more than H
iterations, we immediately conclude that averageTime(n) is O(log n). That is, averageTime(n) is less than
or equal to some function of log n.
10.1 Binary Search Trees 415
50
10
20
30
25
But we can make contains a wrapper method for a protected, recursive containsElement method:
public boolean contains (Object obj)
{
return containsElement (root, obj);
} // method contains
416 C H A P T E R 10 Binary Search Trees
if (comp == 0)
return true;
if (comp < 0)
return containsElement (p.left, obj);
return containsElement (p.right, obj);
} // method containsElement
This recursive version would be nominally less efficien —in both time and space—than the iterative
version. And it is this slight difference that sinks the recursive version. For the iterative version is virtually
identical to one in the TreeMap class, part of the Java Collections Framework, where eff ciency is prized
above elegance. Besides, some of the luster of recursion is diminished by the necessity of having a wrapper
method.
31
25 47
42 50
The insertion is made in a loop that starts by comparing 45 to 31, the root element. Since 45 > 31, we
advance to 47, the right child of 31. See Figure 10.6.
Because 45 < 47, we advance to 42, the left child of 47, as indicated in Figure 10.7.
At this point, 45 > 42, so we would advance to the right child of 42 if 42 had a right child. It does
not, so 45 is inserted as the right child of 42. See Figure 10.8.
10.1 Binary Search Trees 417
25 47
42 50
FIGURE 10.6 The effect of comparing 45 to 31 in the binary search tree of Figure 10.5
42 50
FIGURE 10.7 The effect of comparing 45 to 47 in the binary search tree of Figure 10.6
45
FIGURE 10.8 The effect of inserting 45 into the binary search tree in Figure 10.7
In general, the search fails if the element to be inserted belongs in an empty subtree of temp. Then the
element is inserted as the only element in that subtree. That is, the inserted element always becomes a leaf
in the tree. This has the advantage that the tree is not re-organized after an insertion.
Here is the complete definitio (the loop continues indefinitel until, during some iteration, true or
false is returned):
int comp;
while (true)
{
comp = ((Comparable)element).compareTo (temp.element);
if (comp == 0)
return false;
if (comp < 0)
if (temp.left != null)
temp = temp.left;
else
{
temp.left = new Entry<E> (element, temp);
size++;
return true;
} // temp.left == null
else if (temp.right != null)
temp = temp.right;
else
{
temp.right = new Entry<E> (element, temp);
size++;
return true;
} // temp.right == null
} // while
} // root not null
} // method add
The timing estimates for the add method are identical to those for the contains method, and depend on
the height of the tree. To insert at the end of a binary search tree that forms a chain, the number of iterations
is one more than the height of the tree. The height of a chain is linear in n, so worstTime(n) is linear in
n. And, with the same argument we used for the contains method, we conclude that averageTime(n) is
logarithmic in n.
In Programming Exercise 10.4, you get the opportunity to defin the add method recursively.
The definitio of the remove method in the BinarySearchTree class is more complicated than the
definitio of the add method from Section 10.1.2.4. The reason for the extra complexity is that the
remove method requires a re-structuring of the tree—unless the element to be removed is a leaf. With
the add method, the inserted element always becomes a leaf, and no re-structuring is needed.
The basic strategy is this: we f rst get (a reference to) the Entry object that holds the element to be
removed, and then we delete that Entry object. Here is the definition
public boolean remove (Object obj)
{
Entry<E> e = getEntry (obj);
10.1 Binary Search Trees 419
if (e == null)
return false;
deleteEntry (e);
return true;
} // method remove
Of course, we need to postpone the analysis of the remove method until we have developed both the
getEntry and deleteEntry methods. The protected method getEntry searches the tree—in the
same way as the contains method define in Section 10.1.2.3—for an Entry object whose element is
obj. For example, Figure 10.9 shows what happens if the getEntry method is called to get a reference
to the Entry whose element is 50.
80
20 110
15 50 90
105
FIGURE 10.9 The effect of calling the getEntry method to get a reference to the Entry whose element is 50.
A copy of the reference e is returned
if (obj == null)
throw new NullPointerException();
420 C H A P T E R 10 Binary Search Trees
Entry<E> e = root;
while (e != null)
{
comp = ((Comparable)obj).compareTo (e.element);
if (comp == 0)
return e;
else if (comp < 0)
e = e.left;
else
e = e.right;
} // while
return null;
} // method getEntry
The analysis of the getEntry method is the same as for the contains method in Section 10.1.2.3:
worstTime(n) is linear in n and averageTime(n) is logarithmic in n.
The structure of the while loop in the getEntry method is identical to that in the contains
method. In fact, we can re-defin the contains method to call getEntry. Here is the new definition
now a one-liner:
public boolean contains (Object obj)
{
return (getEntry (obj) != null);
} // method contains
For the deleteEntry method, let’s start with a few simple examples of how a binary search tree is
affected by a deletion; then we’ll get into the details of definin the deleteEntry method. As noted,
removal of a leaf requires no re-structuring. For example, suppose we remove 50 from the binary search
tree in Figure 10.10:
80
20 110
15 50 90
8 17 105
To delete 50 from the tree in Figure 10.10, all we need to do is change to null the right fiel of 50’s parent
Entry —the Entry object whose element is 20. We end up with the binary search tree in Figure 10.11.
In general, if p is (a reference to) the Entry object that contains the leaf element to be deleted, we
firs decide what to do if p is the root. In that case, we set
root = null;
10.1 Binary Search Trees 421
80
20 110
15 90
8 17 105
FIGURE 10.11 The binary search tree from Figure 10.10 after the removal of 50
Otherwise, the determination of which child of p.parent gets the value null depends on whether p is
a left child or a right child:
if (p == p.parent.left)
p.parent.left = null;
else
p.parent.right = null;
Notice how we check to see if p is a (reference to) a left child: if p equals p’s parent’s left child.
It is almost as easy to remove an element that has only one child. For example, suppose we want
to remove 20 from the binary search tree in Figure 10.11. We cannot leave a hole in a binary search tree,
so we must replace 20 with some element. Which one? The best choice is 15, the child of the element to
be removed. So we need to link 15 to 20’s parent. When we do, we get the binary search tree shown in
Figure 10.12.
80
15 110
8 17 90
105
FIGURE 10.12 The binary search tree from Figure 10.11 after 20 was removed by replacing 20’s entry with
15’s entry
In general, let replacement be the Entry that replaces p, which has exactly one child. Then
replacement should get the value of either p.left or p.right, whichever is not empty—they cannot
both be empty because p has one child. We can combine this case, where p has one child, with the previous
case, where p has no children:
422 C H A P T E R 10 Binary Search Trees
Entry<E> replacement;
if (p.left != null)
replacement = p.left;
else
replacement = p.right;
Finally, we come to the interesting case: when the element to be removed has two children. For example,
suppose we want to remove 80 from the binary search tree in Figure 10.12.
As in the previous case, 80 must be replaced with some other element in the tree. But which one?
To preserve the ordering, a removed element should be replaced with either its immediate predecessor (in
this case, 17) or its immediate successor (in this case, 90). We will, in fact, need a successor method for an
inOrder iterator. So assume we already have a successor method that returns an Entry object’s immediate
successor. In general, the immediate successor, s, of a given Entry object p is the leftmost Entry object
in the subtree p.right. Important: the left child of this leftmost Entry object will be null. (Why?)
The removal of 80 from the tree in Figure 10.12 is accomplished as follows: f rst, we copy the
successor’s element to p.element, as shown in Figure 10.13.
Next, we assign to p the value of (the reference) s. See Figure 10.14.
Then we delete p’s Entry object from the tree. As noted earlier, the left child of p must now be
null, so the removal follows the replacement strategy of removing an element with one no children or
one child. In this case, p has a right child (105), so 105 replaces p’s element, as shown in Figure 10.15.
Because the 2-children case reduces to the 0-or-1-child case developed earlier, the code for removal
of any Entry object starts by handling the 2-children case, followed by the code for the 0-or-1-child case.
As we will see in Section 10.2.3, it is beneficia for subclasses of BinarySearchTree if the
deleteEntry method returns the Entry object that is actually deleted. For example, if the deleteEntry
method is called to delete an entry that has two children, the successor of that entry is actually removed
from the tree and returned.
10.1 Binary Search Trees 423
p 90
15 110
8 17 s 90
105
FIGURE 10.13 The f rst step in the removal of 80 from the binary search tree in Figure 10.12: 90, the
immediate successor of 80, replaces 80
90
15 110
p 90
105
FIGURE 10.14 The second step in the removal of 80 in the binary search tree of Figure 10.12: p points to the
successor entry
90
15 110
p 105
FIGURE 10.15 The f nal step in the removal of 80 from the binary search tree in Figure 10.12: p’s element (90)
is replaced with that element’s right child (105)
*
* @return the Entry object that was actually deleted from this BinarySearchTree
* object.
*
*/
protected Entry<E> deleteEntry (Entry<E> p)
{
size–;
Entry<E> replacement;
if (p.left != null)
replacement = p.left;
else
replacement = p.right;
return p;
} // method deleteEntry
10.1 Binary Search Trees 425
We still have the successor method to develop. Here is the method specification
/**
* Finds the successor of a specified Entry object in this BinarySearchTree.
* The worstTime(n) is O(n) and averageTime(n) is constant.
*
* @param e – the Entry object whose successor is to be found.
*
* @return the successor of e, if e has a successor; otherwise, return null.
*
*/
protected Entry<E> successor (Entry<E> e)
This method has protected visibility to reflec the fact that Entry —the return type and parameter
type—has protected visibility.
How can we fin the successor of an Entry object? For inspiration, look at the binary search tree
in Figure 10.16.
In the tree in Figure 10.16, the successor of 50 is 55. To get to this successor from 50, we move
right (to 75) and then move left as far as possible. Will this always work? Only for those entries that
have a non-null right child. What if an Entry object—for example, the one whose element is 36—has a
null right child? If the right child of an Entry object e is null, we get to e’s successor by going back
up the tree to the left as far as possible; the successor of e is the parent of that leftmost ancestor of e.
For example, the successor of 36 is 37. Similarly, the successor of 68 is 75. Also, the successor of 28 is
30; since 28 is a left child, we go up the tree to the left zero times—remaining at 28—and then return
that Entry object’s parent, whose element is 30. Finally, the successor of 75 is null because its leftmost
ancestor, 50,has no parent.
Here is the method definition
protected Entry<E> successor (Entry<E> e)
{
if (e == null)
return null;
50
37 75
25 61
15 30 55 68
28 32 59
36
To estimate worstTime(n) for the successor method, suppose the following elements are inserted into
an initially empty binary search tree: n, 1, 2, 3, . . . , n − 1. The shape of the resulting tree is as shown in
Figure 10.17.
n−1
FIGURE 10.17 A binary search tree in which f nding the successor of n − 1 requires n − 3 iterations
For the binary search tree in Figure 10.17, obtaining the successor of n − 1 requires n − 3 iterations,
so worstTime(n) is linear in n. For averageTime(n), note that an element in the tree will be reached at most
3 times: once to get to its left child, once as the successor of that left child, and once in going back up the
tree to get the successsor of its rightmost descendant. So the total number of loop iterations to access each
element is at most 3n, and the average number of loop iterations is 3n/n = 3. That is, averageTime(n) is
constant.
We can briefl summarize the steps needed to delete an entry:
a. If the entry has no children, simply set to null the corresponding subtree-link from the entry’s parent
(if the entry is the root, set the root to null).
10.1 Binary Search Trees 427
b. If the entry has one child, replace the parent-entry link and the entry-child link with a parent-child
link.
c. If the entry has two children, copy the element in the entry’s immediate successor into the entry to
be deleted, and then delete that immediate successor (by part a or part b).
Finally, we can estimate the time for the remove method. The remove method has no loops or recursive
calls, so the time for that method is determined by the time for the getEntry and deleteEntry methods
called by the remove method. As noted above, for the getEntry method, worstTime(n) is linear in n
and averageTime(n) is logarithmic in n. The deleteEntry method has no loops or recursive calls, but
calls the successor method, whose worstTime(n) is linear in n and whose averageTime(n) is constant.
We conclude that for the remove method, worstTime(n) is linear in n and averageTime(n) is logarithmic
in n.
To complete the development of the BinarySearchTree class, we develop the embedded TreeIt
erator class in Section 10.1.2.6.
Before we get to definin the three methods mentioned above, we should defin a default constructor.
Where do we want to start? That depends on how we want to iterate. For a preOrder or breadthFirst
iteration, we would start at the root Entry object. For an inOrder or postOrder iteration, we would start
at the leftmost Entry object. We will want to iterate over the elements in a BinarySearchTree in
ascending order, so we want to initialize the next fiel to the leftmost (that is, smallest) Entry object
in the BinarySearchTree. To obtain that f rst Entry object, we start at the root and go left as far as
possible:
/**
* Positions this TreeIterator to the smallest element, according to the Comparable
* interface, in the BinarySearchTree object.
* The worstTime(n) is O(n) and averageTime(n) is O(log n).
*
*/
protected TreeIterator()
{
next = root;
if (next != null)
while (next.left != null)
next = next.left;
} // default constructor
428 C H A P T E R 10 Binary Search Trees
To estimate the time for this default constructor, the situation is the same as for the contains and add
methods in the BinarySearchTree class: worstTime(n) is linear in n (when the tree consists of a chain
of left children), and averageTime(n) is logarithmic in n.
The hasNext() method simply checks to see if the next fiel has the value null:
/**
* Determines if there are still some elements, in the BinarySearchTree object this
* TreeIterator object is iterating over, that have not been accessed by this
* TreeIterator object.
*
* @return true – if there are still some elements that have not been accessed by
* this TreeIterator object; otherwise, return false.
*
*/
public boolean hasNext()
{
return next != null;
} // method hasNext
The definitio of the next() method is quite simple because we have already define the successor
method:
/**
* Returns the element in the Entry this TreeIterator object was positioned at
* before this call, and advances this TreeIterator object.
* The worstTime(n) is O(n) and averageTime(n) is constant.
*
* @return the element this TreeIterator object was positioned at before this call.
*
* @throws NoSuchElementException – if this TreeIterator object was not
* positioned at an Entry before this call.
*
*/
public E next()
{
if (next == null)
throw new NoSuchElementException();
lastReturned = next;
next = successor (next);
return lastReturned.element;
} // method next
Finally, the TreeIterator class’s remove method deletes the Entry that was last returned. Basically, we
call deleteEntry (lastReturned). A slight complication arises if lastReturned has two children.
For example, suppose lastReturned references the Entry object whose element is (the Integer whose
value is) 40 in the BinarySearchTree object of Figure 10.18.
For the BinarySearchTree object of Figure 10.18, if we simply call
deleteEntry (lastReturned);
10.1 Binary Search Trees 429
lastReturned 40
20 75
next 50 80
then next will reference an Entry object that is no longer in the tree. To avoid this problem, we set
next = lastReturned;
before calling
deleteEntry (lastReturned);
Then the tree from Figure 10.18 would be changed to the tree in Figure 10.19.
lastReturned
next 50
20 75
80
FIGURE 10.19 A binary search tree in which the element referenced by lastReturned is to be removed.
Before deleteEntry (lastReturned) is called, next is assigned the value of lastReturned
After deleteEntry (lastReturned) is called for the tree in Figure 10.19, we get the tree in
Figure 10.20.
next 50
20 75
80
FIGURE 10.20 The tree from Figure 10.19 after deleteEntry (lastReturned) is called
For the tree in Figure 10.20, next is positioned where it should be positioned. We then set last
Returned to null to preclude a subsequent call to remove() before a call to next().
Here is the method definition
/**
* Removes the element returned by the most recent call to this TreeIterator
* object’s next() method.
* The worstTime(n) is O(n) and averageTime(n) is constant.
430 C H A P T E R 10 Binary Search Trees
*
* @throws IllegalStateException – if this TreeIterator’s next() method was not
* called before this call, or if this TreeIterator’s remove() method was
* called between the call to the next() method and this call.
*
*/
public void remove()
{
if (lastReturned == null)
throw new IllegalStateException();
if (lastReturned.left != null && lastReturned.right != null)
next = lastReturned;
deleteEntry(lastReturned);
lastReturned = null;
} // method remove
Lab 17 provides run-time support for the claim made earlier that the average height of a Binary
SearchTree is logarithmic in n.
1 The
corresponding methods in the ArrayList and LinkedList classes are add (int index, E element), remove (Object obj) and
contains (Object obj). Note that ArrayList objects and LinkedList objects are not necessarily in order.
10.2 Balanced Binary Search Trees 431
For all of these balanced binary search trees, the basic mechanism that keeps a tree balanced is the
rotation. A rotation is an adjustment to the tree around an element such that the adjustment maintains
the required ordering of elements. The ultimate goal of rotating is to restore some balance property that
has temporarily been violated due to an insertion or removal. For example, one such balance property is
that the heights of the left and right subtrees of any element should differ by at most 1. Let’s start with a
simple classificatio of rotations: left and right.
In a left rotation, some adjustments are made to the element’s parent, left subtree and right subtree.
The main effect of these adjustments is that the element becomes the left child of what had been the
element’s right child. For a simple example, Figure 10.21 shows a left rotation around the element 50.
Note that before and after the rotation, the tree is a binary search tree.
50 90
90 50 100
100
Figure 10.22 has another example of a left rotation, around the element 80, that reduces the height
of the tree from 3 to 2.
The noteworthy feature of Figure 10.22 is that 85, which was in the right subtree of the before-
rotation tree, ends up in the left subtree of the after-rotation tree. This phenomenon is common to all left
rotations around an element x whose right child is y. The left subtree of y becomes the right subtree of
x . This adjustment is necessary to preserve the ordering of the binary search tree: Any element that was
in y’s left subtree is greater than x and less than y. So any such element should be in the right subtree of
x (and in the left subtree of y). Technically, the same phenomenon also occurred in Figure 10.21, but the
left subtree of 50’s right child was empty.
80 90
60 90 80 120
85 120 60 85 100
100
Figure 10.23 shows the rotation of Figure 10.22 in a broader context: the element rotated around is
not the root of the tree. Before and after the rotation, the tree is a binary search tree.
432 C H A P T E R 10 Binary Search Trees
50 50
30 80 30 90
20 40 60 90 20 40 80 120
85 120 60 85 100
100
FIGURE 10.23 The left rotation around 80 from Figure 10.22, but here 80 is not the root element
Figure 10.23 illustrates another aspect of all rotations: all the elements that are not in the rotated
element’s subtree are unaffected by the rotation. That is, in both trees, we still have:
50
30
20 40
If we implement a rotation in the BinarySearchTree class, no elements are actually moved; only the
references are manipulated. Suppose that p (for “parent”) is a reference to an Entry object and r (for
“right child”) is a reference to p’s right child. Basically, a left rotation around p can be accomplished in
just two steps:
p.right = r.left; // for example, look at 85 in Figure 10.22
r.left = p;
Unfortunately, we also have to adjust the parent fields and that adds quite a bit of code. Here is the complete
definitio of a leftRotate method in the BinarySearchTree class (a similar definitio appears in the
TreeMap class in Chapter 12):
/**
* Performs a left rotation in this BinarySearchTre object around a specified
* Entry object.
*
* @param p – the Entry object around which the left rotation is performed
*
* @throws NullPointerException – if p is null or p.right is null.
*
* @see Cormen, 2002.
protected void rotateLeft (Entry<E> p)
{
10.2 Balanced Binary Search Trees 433
Entry<E> r = p.right;
p.right = r.left;
if (r.left != null)
r.left.parent = p;
r.parent = p.parent;
if (p.parent == null)
root = r;
else if (p.parent.left == p)
p.parent.left = r;
else
p.parent.right = r;
r.left = p;
p.parent = r;
} // method rotateLeft
This indicates how much of a bother parents can be! But on the bright side, no elements get moved, and
the time is constant.
What about a right rotation? Figure 10.24 shows a simple example: a right rotation around 50.
50 35
35 10 50
10
Does this look familiar? Figure 10.24 is just Figure 10.21 with the direction of the arrow reversed.
In general, if you perform a left rotation around an element and then perform a right rotation around the
new parent of that element, you will end up with the tree you started with.
Figure 10.25 shows a right rotation around an element, 80, in which the right child of 80’s left child
becomes the left child of 80. This is analogous to the left rotation in Figure 10.22.
80 60
60 100 30 80
30 70 55 70 100
55
Here are details on implementing right rotations in the BinarySearchTree class. Let p be a
reference to an Entry object and let l (for “left child”) be a reference to the left child of p. Basically, a
right rotation around p can be accomplished in just two steps:
p.left = l.right; // for example, look at 70 in the rotation of Figure 10.8
l.right = p;
Of course, once we include the parent adjustments, we get a considerably longer—but still constant
time—method. In fact, if you interchange “left” with “right” in the definitio of the leftRotate method,
you get the definitio of rightRotate.
In all of the rotations shown so far, the height of the tree was reduced by 1. That is not surprising;
in fact, reducing height is the motivation for rotating. But it is not necessary that every rotation reduce the
height of the tree. For example, Figure 10.26 shows a left rotation—around 50—that does not affect the
height of the tree.
90 90
50 100 70 100
30 70 50 80
80 30
FIGURE 10.26 A left rotation around 50. The height of the tree is still 3 after the rotation
It is true that the left rotation in Figure 10.26 did not reduce the height of the tree. But a few minutes
of checking should convince you that no single rotation can reduce the height of the tree on the left side
of Figure 10.26. Now look at the tree on the right side of Figure 10.26. Can you figur out a rotation that
will reduce the height of that tree? Not a right rotation around 70; that would just get us back where we
started. How about a right rotation around 90? Bingo! Figure 10.27 shows the effect.
90 70
70 100 50 90
50 80 30 80 100
30
FIGURE 10.27 A right rotation around 90. The height of the tree has been reduced from 3 to 2
10.2 Balanced Binary Search Trees 435
The rotations in Figures 10.26 and 10.27 should be viewed as a package: a left rotation around 90’s
left child, followed by a right rotation around 90. This is referred to as a double rotation. In general, if p
is a reference to an Entry object, then a double rotation around p can be accomplished as follows:
leftRotate (p.left);
rightRotate (p);
Figure 10.28 shows another kind of double rotation: a right rotation around the right child of 50, followed
by a left rotation around 50.
50 50 70
10 80 10 70 50 80
70 90 80 10 75 90
75 75 90
FIGURE 10.28 Another kind of double rotation: a right rotation around 50’s right child, followed by a left
rotation around 50
Before we move on to Section 10.2.1 with a specifi kind of balanced binary search tree, let’s list
the major features of rotations:
1. There are four kinds of rotation:
a. Left rotation;
b. Right rotation;
c. A left rotation around the left child of an element, followed by a right rotation around the element
itself;
d. A right rotation around the right child of an element, followed by a left rotation around the element
itself.
2. Elements not in the subtree of the element rotated about are unaffected by the rotation.
3. A rotation takes constant time.
4. Before and after a rotation, the tree is still a binary search tree.
5. The code for a left rotation is symmetric to the code for a right rotation: simply swap the words
“left” and “right”.
Section 10.2.1 introduces the AVL tree, a kind of binary search tree that employs rotations to maintain
balance.
AVL trees are named after the two Russian mathematicians, Adelson-Velski and Landis, who invented
them in 1962. Figure 10.29 shows three AVL trees, and Figure 10.30 shows three binary search trees that
are not AVL trees.
25 50 50
40 60 20 80
30 70 10 70 100
91 103
25 50 50
10 40 60 20 80
20 30 70 10 70 100
20 80 91 103
101
FIGURE 10.30 Three binary search trees that are not AVL trees
The f rst tree in Figure 10.30 is not an AVL tree because its left subtree has height 1 and its right
subtree has height −1. The second tree is not an AVL tree because its left subtree is not an AVL tree;
neither is its right subtree. The third tree is not an AVL tree because its left subtree has height 1 and its
right subtree has height 3.
In Section 10.2.2, we show that an AVL tree is a balanced binary search tree, that is, that the height
of an AVL tree is always logarithmic in n. This compares favorably to a binary search tree, whose height
is linear in n in the worst case (namely, a chain). The difference between linear and logarithmic can be
huge. For example, suppose n = 1, 000, 000, 000, 000. Then log2 n is less than 40. The practical import of
this difference is that insertions, removals and searches for the AVLTree class take far less time, in the
worst case, than for the BinarySearchTree class.
Claim If t is a non-empty AVL tree, height(t) is logarithmic in n, where n is the number of elements in t.
Proof We will show that, even if an AVL tree t has the maximum height possible for its n elements, its
height will still be logarithmic in n. How can we determine the maximum height possible for an AVL tree with
n elements? As Kruse (see Kruse [1987]) suggests, rephrasing the question helps us get the answer. Given
a height h, what is the minimum number of elements in any AVL tree of that height?
For h = 0, 1, 2, . . . , let minh be the minimum number of elements in an AVL tree of height h. Clearly,
min0 = 1 and min1 = 2. The values of min2 and min3 can be seen from the AVL trees in Figure 10.31.
In general, if h1 > h2, then minh1 is greater than the number of elements needed to construct an AVL
tree of height h2. That is, if h1 > h2, then minh1 > minh2 . In other words, minh is an increasing function of h.
Suppose that t is an AVL tree with h height and minh elements, for some value of h > 1. What can we
say about the heights of the left and right subtrees of t? By the definition of height, one of those subtrees
must have height h − 1. And by the definition of an AVL tree, the other subtree must have height of h − 1 or
h − 2. In fact, because t has the minimum number of elements for its height, one of its subtrees must have
height h − 1 and minh−1 elements, and the other subtree must have height h − 2 and minh−2 elements.
A tree always has one more element than the number of elements in its left and right subtrees. So we
have the following equation, called a recurrence relation:
Now that we can calculate minh for any positive integer h, we can see how the function minh is related to
the maximum height of an AVL tree. For example, because min6 = 33 and min7 = 54, the maximum height
of an AVL tree with 50 elements is six.
The above recurrence relation looks a lot like the formula for generating Fibonacci numbers (see Lab 7).
The term Fibonacci tree refers to an AVL tree with the minimum number of elements for its height. From the
above recurrence relation and the values of min0 and min1 , we can show, by induction on h, that
50 90
39 80 50 100
60 39 80 129
60
FIGURE 10.31 AVL trees of heights 2 and 3 in which the number of elements is minimal
438 C H A P T E R 10 Binary Search Trees
Rewriting this in a form suitable for a Big-O claim, with 1/ log2 (3/2) < 1.75:
h ≤ 1.75 ∗ log2 (minh ), for any nonnegative integer h.
If t is an AVL tree with h height and n elements, we must have minh ≤ n, so for any such AVL tree,
h ≤ 1.75 ∗ log2 (n).
This implies that the height of any AVL tree is O(log n). Is O(log n) a tight upper bound; that is, is the height of
any AVL tree logarithmic in n? Yes, and here’s why. For any binary tree of height h with n elements,
h ≥ log2 (n + 1) − 1
by part 2 of the Binary Tree Theorem. We conclude that any AVL tree with n elements has height that is
logarithmic in n, even in the worst case.
To give you a better idea of how AVL trees relate to binary search trees, we sketch the design and
implementation of the AVLTree class in Section 10.2.3. To complete the implementation, you will need to
tackle Programming Projects 10.3 and 10.4. Those projects deal with some of the details of the add and
remove methods, respectively.
The purpose of this additional f eld in the Entry class is to make it easier to maintain the balance of an
AVLTree object. If an Entry object has a balanceFactor value of ‘=’, the Entry object’s left subtree
has the same height as the Entry object’s right subtree. If the balanceFactor value is ‘L’, the left
subtree’s height is one greater than the right subtree’s height. And a balanceFactor value of ‘R’ means
that the right subtree’s height is one greater than the left subtree’s height. Figure 10.32 shows an AVL
tree with each element’s balance factor shown below the element.
50
R
20 80
L R
10 70 100
= = =
92 103
= =
FIGURE 10.32 An AVL tree with the balance factor under each element
10.2 Balanced Binary Search Trees 439
/**
* Initializes this AVLEntry object from a specified element and a
* specified parent AVLEntry.
*
* @param element – the specified element to be housed in this
* AVLEntry object.
* @param parent – the specified parent of this AVLEntry object.
*
*/
protected AVLEntry (E element, AVLEntry<E> parent)
{
this.element = element;
this.parent = parent;
} // constructor
} // class AVLEntry
The only methods that the AVLTree class overrides from the BinarySearchTree class are those that
involve the AVLEntry class’s balanceFactor field Specifically the AVLTree class will override the
add and deleteEntry methods from the BinarySearchTree class. The re-balancing strategy is from
Sahni (see Sahni [2000]).
One intriguing feature of the AVLTree class is that the contains method is not overridden from the
BinarySearchTree class, but worstTime(n) is different: logarithmic in n, versus linear in n for the Bina
rySearchTree class. This speed reflect the fact that the height of an AVL tree is always logarithmic in n.
The definitio of the add method in the AVLTree class resembles the definitio of the add method
in the BinarySearchTree class. But as we work our way down the tree from the root to the insertion
point, we keep track of the inserted AVLEntry object’s closest ancestor whose balanceFactor is ‘L’
or ‘R’. We refer to this Entry object as imbalanceAncestor. For example, if we insert 60 into the
AVLTree object in Figure 10.33, imbalanceAncestor is the Entry object whose element is 80:
50
R
20 80
L R
10 70 100
= = =
92 103
= =
After the element has been inserted, BinarySearchTree-style, into the AVLTree object, we call
a f x-up method to handle rotations and balanceFactor adjustments. Here is the definitio of the add
method:
public boolean add (E element)
{
if (root == null)
{
if (element == null)
throw new NullPointerException();
root = new AVLEntry<E> (element, null);
size++;
return true;
} // empty tree
else
{
AVLEntry<E> temp = (AVLEntry<E>)root,
imbalanceAncestor = null; // nearest ancestor of
// element with
// balanceFactor not ‘=’
int comp;
while (true)
{
comp = ((Comparable)element).compareTo (temp.element);
if (comp == 0)
return false;
if (comp < 0)
{
if (temp.balanceFactor != ‘=’)
imbalanceAncestor = temp;
if (temp.left != null)
temp = (AVLEntry<E>)temp.left;
else
{
temp.left = new AVLEntry<E> (element, temp);
fixAfterInsertion ((AVLEntry<E>)temp.left,
imbalanceAncestor);
size++;
return true;
} // temp.left == null
} // comp < 0
else
{
if (temp.balanceFactor != ‘=’)
imbalanceAncestor = temp;
if (temp.right != null)
temp = (AVLEntry<E>)temp.right;
else
10.2 Balanced Binary Search Trees 441
{
temp.right = new AVLEntry<E>(element, temp);
fixAfterInsertion ((AVLEntry<E>)temp.right,
imbalanceAncestor);
size++;
return true;
} // temp.right == null
} // comp > 0
} // while
} // root not null
} // method add
This code differs from that of the BinarySearchTree class’s add method in three respects:
1. The new entry is an instance of AVLEntry<E>.
2. The imbalanceAncestor variable is maintained.
3. The fixAfterInsertion method is called to re-balance the tree, if necessary.
The definitio of the fixAfterInsertion method is left as Programming Project 10.3. The bottom line
is that, for the add method, worstTime(n) is O(log n). In fact, because the while loop in the add method
can require as many iterations as the height of the AVL tree, worstTime(n) is logarithmic in n.
The definitio of the deleteEntry method (called by the inherited remove method) starts by
performing a BinarySearchTree -style deletion, and then invokes a fixAfterDeletion method. For-
tunately, for the sake of code re-use, we can explicitly call the BinarySearchTree class’s deleteEntry
method, so the complete definitio of the AVLTree class’s deleteEntry method is simply:
protected Entry<E> deleteEntry (Entry<E> p)
{
AVLEntry<E> deleted = (AVLEntry<E>)super.deleteEntry (p);
fixAfterDeletion (deleted.element, (AVLEntry<E>)deleted.parent);
return deleted;
} // method deleteEntry
Of course, we are not done yet; we are not even close: The definitio of the fixAfterDeletion method
is left as Programming Project 10.4. For the remove method, worstTime(n) is O(log n). In fact, because
we start with a BinarySearchTree -style deletion, worstTime(n) is logarithmic in n.
The book’s website includes an applet that will help you to visualize insertions in and removals from
an AVLTree object.
into maintaining the balance of an AVLTree object, the average height of a BinarySearchTree object
is about 50% larger than the average height of an AVLTree object: 2.1 log2 n versus 1.44 log2 n. But
the extra maintenance makes AVLTree insertions slightly slower, in spite of the height advantage, than
BinarySearchTree insertions.
In Chapter 12, we present another kind of balanced binary search tree: the red-black tree. Insertions
in red-black trees are slightly faster (but less intuitive), on average, than for AVL trees, and that is why
the red-black tree was selected as the underlying structure for the Java Collection Framework’s TreeMap
and TreeSet classes. Both of these classes are extremely useful; you will get some idea of this from the
applications and Programming Projects in Chapter 12.
SUMMARY
A binary search tree t is a binary tree such that either t A binary search tree is balanced if its height is
is empty or logarithmic in n, the number of elements in the tree.
The balancing is maintained with rotations. A rotation
1. each element in leftTree(t ) is less than the root ele-
is an adjustment to the tree around an element such
ment of t ;
that the adjustment maintains the required ordering of
2. each element in rightTree(t ) is greater than the root elements. This chapter introduced one kind of balanced
element of t ; binary search tree: the AVL tree. An AVL tree is a binary
search tree that either is empty or in which:
3. both leftTree(t ) and rightTree(t ) are binary search
trees.
The BinarySearchTree class maintains a sorted col- 1. the heights of the root’s left and right subtrees differ
lection of Comparable elements. The time estimates for by at most 1, and
searching, inserting, and deleting depend on the height of 2. the root’s left and right subtrees are AVL trees.
the tree. In the worst case—if the tree is a chain—the
height is linear in n, the number of elements in the tree.
The average height of a binary search tree is logarithmic The AVLTree class is a subclass of the Binary
in n. So for the contains, add, and remove meth- SearchTree class. The only overridden methods are
ods, worstTime(n) is linear in n and averageTime(n) is those related to maintaining balance: add and delete
logarithmic in n. Entry.
Crossword Puzzle 443
CROSSWORD PUZZLE
1 2 3
6 7 8
10
www.CrosswordWeaver.com
ACROSS DOWN
1. An AVL tree with the minimum 2. The given definitions of the contains,
number of elements for its height add and remove methods in the
BinarySearchTree class are _____.
4. The feature of a BinarySearchTree
object most important in estimating 3. An adjustment to a binary search
worstTime(n) and averageTime(n) tree around an element that
for the contains, add and remove maintains the required ordering of
methods elements.
6. The only field in the AVLEntry class 5. The fourth field in the nested Entry
that is not inherited from the nested class of the BinarySearchTree class.
Entry class of the The other three fields are element,
BinarySearchTree class left and right.
CONCEPT EXERCISES
10.1 a. Show the effect of making the following insertions into an initially empty binary search tree:
60
70
b. .
30
20 50
40 80
70 100
Concept Exercises 445
c. .
30
20 50
40 80
45 70 100
60 75
10.5 In each of the following binary search trees, perform a right rotation around 50.
a. .
50
40
30
b. .
60
50 70
40 55
30 45
c. .
30
20 50
40 80
48 60 100
55 75
446 C H A P T E R 10 Binary Search Trees
10.6 In the following binary search tree, perform a double rotation (a left rotation around 20 and then a right
rotation around 50) to reduce the height to 2.
50
20 90
10 40
30
10.7 In the following binary search tree, perform a “double rotation” to reduce the height to 2:
50
20 80
70 100
60
fi (h + 3) − 1 ≥ (3/2)h
Hint: Use the Strong Form of the Principle of Mathematical Induction and note that, for h > 1,
10.9 Suppose we defin maxh to be the maximum number of elements in an AVL tree of height h.
a. Calculate max3 .
b. Determine the formula for maxh for any h ≥ 0.
Hint: Use the Binary Tree Theorem from Chapter 9.
c. What is the maximum height of an AVL tree with 100 elements?
10.10 Show that the height of an AVL tree with 32 elements must be exactly 5.
Hint: calculate max4 (see Concept Exercise 10.9) and min6 .
10.11 For the contains method in the BinarySearchTree class, worstTime(n) is linear in n. The AVLTree
class does not override that method, but for the contains method in the AVLTree class, worstTime(n) is
logarithmic in n. Explain.
Concept Exercises 447
10.12 The following program generates a BinarySearchTree object of n elements. Draw the tree when
n = 13. For any n ≥ 0, provide a (that is, Big Theta) estimate of height(n), that is, the height of the
BinarySearchTree object as a function of n.
import java.util.*;
tree.add (1.0);
list.add (1.0);
int k = 2;
while (tree.size() < n)
addLevel (tree, n, k++, list);
System.out.println (tree.height());
} // method run
double d = itr.next();
tree.add (d - 1.0);
newList.add (d - 1.0);
for (int i = 0; i < k && tree.size() < n; i++)
{
tree.add (d + SMALL );
newList.add (d + SMALL);
if (itr.hasNext())
d = itr.next();
} // for
448 C H A P T E R 10 Binary Search Trees
list.clear();
list.addAll (newList);
} // method addLevel
} // class weirdBST
PROGRAMMING EXERCISES
10.1 In the BinarySearchTree class, test and develop a leaves method. Here is the method specification
/**
* Returns the number of leaves in this BinarySearchTree object.
* The worstTime(n) is O(n).
*
* @return – the number of leaves in this BinarySearchTree object.
*
*/
public int leaves()
Test your method by adding tests to the BinarySearchTreeTest class available from the book’s website.
Hint: A recursive version, invoked by a wrapper method, can mimic the definitio of leaves(t) from Section
9.1. Or, you can also develop an iterative version by creating a new iterator class in which the next method
increments a count for each Entry object whose left and right field are null.
10.2 Modify the BinarySearchTree class so that the iterators are fail-fast (see Appendix 1 for details on
fail-fast iterators). Test your class by adding tests to the BinarySearchTreeTest class available from
the book’s website.
10.3 Modify the BinarySearchTree class so that BinarySearchTree objects are serializable (see
Appendix 1 for details on serializability). Test your class by adding tests to the BinarySearchTreeTest
class available from the book’s website.
10.4 Create a recursive version of the add method.
Hint: Make the add method a wrapper for a recursive method. Test your version with the relevant tests in
the BinarySearchTreeTest class available from the book’s website.
10.5 In the BinarySearchTree class, modify the getEntry method so that it is a wrapper for a recursive
method. Test your version with the relevant tests in the BinarySearchTreeTest class available from
the book’s website.
10.6 (This exercise assumes you have completed Programming Projects 10.3 and 10.4.) Create a test suite for the
AVLTree class.
Hint: Make very minor modification to the BinarySearchTreeTest class available from the book’s
website. Use your test suite to increase your confid nce in the correctness of the methods you define in
Programming Projects 10.3 and 10.4.
10.7 (This exercise assumes you have completed Programming Projects 10.3 and 10.4.) In the AVLTree class,
test and defin the following method:
/**
* The height of this AVLTree object has been returned.
* The worstTime(n) is O(log n).
Programming Exercises 449
*
* @return the height of this AVLTree object.
*
*/
public int height()
Hint: Use the balanceFactor fi ld in the AVLEntry class to guide you down the tree. Test your method
by adding tests to the BinarySearchTreeTest class available from the book’s website.
E element;
int parent,
left,
right;
Similarly, the BinarySearchTreeArray<E> class might have the following three f elds:
Entry<E> [ ] tree;
int root,
size;
The root Entry object is stored in tree [0], and a null reference is indicated by the index −1. For example,
suppose we create a binary search tree by entering the String objects “dog”, “turtle”, “cat”, “ferret”. The tree
would be as follows:
dog
cat turtle
ferret
0 dog −1 2 1
1 turtle 0 3 −1
2 cat 0 −1 −1
3 ferret 1 −1 −1
...
The method definition are very similar to those in the BinarySearchTree class, except that an expression such
as
root.left
is replaced with
tree [root].left
/**
* Returns a String representation of this BinarySearchTree object.
* The worstTime(n) is linear in n.
*
* @return a String representation – that incorporates the structure–of this
* BinarySearchTree object.
*
*/
public String toTreeString()
Note 1: The String returned should incorporate the structure of the tree. For example, suppose we have the
following:
tree.add (55);
tree.add (12);
tree.add (30);
tree.add (97);
System.out.println (tree.toTreeString());
12 97
30
In what sense is the above approach better than developing a printTree method in the BinarySearchTree
class?
Hint: If imbalanceAncestor is null, then each ancestor of the inserted AVLEntry object has a balanceFactor
value of ‘=’. For example, Figure 10.34 shows the before-and-after for this case.
There are three remaining cases when the balanceFactor value of imbalanceAncestor is ‘L’. The three
cases when that value is ‘R’ can be obtained by symmetry.
Case 1: imbalanceAncestor.balanceFactor is ‘L’ and the insertion is made in the right subtree of imbal
anceAncestor. Then no rotation is needed. Figure 10.35 shows the before-and-after for this case.
Case 2: imbalanceAncestor.balanceFactor is ‘L’ and the insertion is made in the left subtree of the left
subtree of imbalanceAncestor. The restructuring can be accomplished with a right rotation around
imbalanceAncestor. Figure 10.36 shows the before-and-after in this case.
Case 3: imbalanceAncestor.balanceFactor is ‘L’ and the inserted entry is in the right subtree of the left
subtree of imbalanceAncestor. The restructuring can be accomplished with a left rotation around the
left child of imbalanceAncestor followed by a right rotation around imbalanceAncestor. There are
three subcases to determine the adjustment of balance factors:
3a: imbalanceAncestor’s post-rotation parent is the inserted entry. Figure 10.37 shows the before-
and-after in this case.
50 50
= R
25 70 25 70
= = = L
15 30 60 90 15 30 60 90
= = = = = = L =
55 55
= =
FIGURE 10.34 On the left-hand side, an AVLTree object just before the call to fixAfterInsertion; the
element inserted was 55, and all of its ancestors have a balance factor of ‘=’. On the right-hand side, the
AVLTree object with adjusted balance factors
Programming Exercises 453
50 50
L =
25 70 25 70
R = R L
15 30 60 90 15 30 60 90
= L = = = L L =
28 55 28 55
= = = =
FIGURE 10.35 On the left-hand side, an AVL tree into which 55 has just been inserted. The balance factors
of the other entries are pre-insertion. On the right-hand side, the same AVL tree after the balance factors
have been adjusted. The only balance factors adjusted are those in the path between 55 (exclusive) and 50
(inclusive)
50 20
L =
20 70 10 50
= = R =
10 30 60 90 5 15 30 70
= = = = = L = =
5 15 25 35 13 25 35 60 90
= = = = = = = = =
13
=
FIGURE 10.36 On the left side, what was an AVL tree has become imbalanced by the insertion of 13. The
balance factors of the other entries are pre-insertion. In this case, imbalanceAncestor is the AVLEntry
object whose element is 50. On the right side, the restructured AVL tree with adjusted balanced factors
50 40
L =
30 30 50
= = =
40
=
FIGURE 10.37 On the left side, what was an AVL tree has become imbalanced by the insertion of 40. The
balance factors of the other entries are pre-insertion. In this sub-case, imbalanceAncestor is the AVLEntry
object whose element is 50. On the right side, the restructured AVL tree with adjusted balanced factors
50 40
L =
20 90 20 50
= = = R
10 40 70 100 10 30 45 90
= = = = = R = =
5 15 30 45 5 15 35 70 100
= = = = = = = = = =
35
=
FIGURE 10.38 On the left side, what was an AVL tree has become imbalanced by the insertion of 35. The
balance factors of the other entries are pre-insertion. In this case, imbalanceAncestor is the AVLEntry object
whose element is 50. On the right side, the restructured AVL tree with adjusted balanced factors
50 40
L =
20 90 20 50
= = L =
10 40 70 100 10 30 45 90
= = = = = = L =
5 15 30 45 5 15 42 70 100
= = = = = = = = =
42
=
FIGURE 10.39 On the left side, what was an AVL tree has become imbalanced by the insertion of 42. The
balance factors of the other entries are pre-insertion. In this case, imbalanceAncestor is the AVLEntry
object whose element is 50. On the right side, the restructured AVL tree with adjusted balanced factors
Programming Exercises 455
/**
* Restores the AVL properties, if necessary, by rotations and balance-factor
* adjustments between the element actually deleted and a specified ancestor
* of the AVLEntry object actually deleted.
* The worstTime(n) is O(log n).
*
* @param element – the element actually deleted from this AVLTree object.
* @param ancestor – the specified ancestor (initially, the parent) of the
* element actually deleted.
*
*/
protected void fixAfterDeletion (E element, AVLEntry<E> ancestor)
Hint: Loop until the tree is an AVL tree with appropriate balance factors. Within the loop, suppose the element
removed was in the right subtree of ancestor (a symmetric analysis handles the left-subtree case). Then there
are three subcases, depending on whether ancestor.balanceFactor is ‘=’, ‘R’, or ‘L’. In all three subcases,
ancestor.balanceFactor must be changed. For the ‘=’ subcase, the loop then terminates. For the ‘R’ subcase,
ancestor is replaced with ancestor.parent and the loop continues. For the ‘L’ subcase, there are three sub-
subcases, depending on whether ancestor.left.balanceFactor is ‘=’, ‘R’, or ‘L’. And the ‘R’ sub-subcase
has three sub-sub-subcases.
This page intentionally left blank
Sorting CHAPTER 11
One of the most common computer operations is sorting, that is, putting a collection of elements in
order. From simple, one-time sorts for small collections to highly efficient sorts for frequently used
mailing lists and dictionaries, the ability to choose among various sort methods is an important skill in
every programmer’s repertoire.
CHAPTER OBJECTIVES
1. Compare the Comparable interface to the Comparator interface, and know when to use
each one.
2. Be able to decide which sort algorithm is appropriate for a given application.
3. Understand the limitations of each sort algorithm.
4. Explain the criteria for a divide-and-conquer algorithm.
11.1 Introduction
Our focus will be on comparison-based sorts; that is, the sorting entails comparing elements to other
elements. Comparisons are not necessary if we know, in advance, the f nal position of each element. For
example, if we start with an unsorted list of 100 distinct integers in the range 0 . . . 99, we know without
any comparisons that the integer 0 must end up in position 0, and so on. The best- known sort algorithm
that is not comparison-based is Radix Sort: see Section 11.5.
All of the sort algorithms presented in this chapter are generic algorithms, that is, static methods:
they have no calling object and operate on the parameter that specifie the collection to be sorted. Two of
the sort methods, Merge Sort and Quick Sort, are included in the Java Collections Framework, and can be
found in the Collections or Arrays classes in the package java.util.
The parameter list may include an array of primitive values (ints or doubles, for example), an
array of objects, or a List object—that is, an instance of a class that implements the List interface. In
illustrating a sort algorithm, we gloss over the distinction between an array of ints, an array of Integer
objects and a List object whose elements are of type Integer. In Section 11.3, we’ll see how to
sort objects by a different ordering than that provided by the compareTo method in the Comparable
interface.
In estimating the efficienc of a sorting method, our primary concerns will be averageTime(n) and
worstTime(n). In some applications, such as national defense and life-support systems, the worst-case
performance of a sort method can be critical. For example, we will see a sort algorithm that is quite fast,
both on average and in the worst case. And we will also look at a sort algorithm that is extremely fast, on
average, but whose worst-case performance is achingly slow.
457
458 C H A P T E R 11 Sorting
The space requirements will also be noted, because some sort algorithms make a copy of the collection
that is to be sorted, while other sort algorithms have only negligible space requirements.
Another criterion we’ll use for measuring a sort method is stability. A stable sort method preserves
the relative order of equal elements. For example, suppose we have an array of students in which each
student consists of a last name and the total quality points for that student, and we want to sort by total
quality points. If the sort method is stable and before sorting, (“Balan”, 28) appears at an earlier index
than (“Wang”, 28), then after sorting (“Balan” 28) will still appear at an earlier index than (“Wang” 28).
Stability can simplify project development. For example, assume that the above array is already in order
by name, and the application calls for sorting by quality points; for students with the same quality points
the ordering should be alphabetical. A stable sort will accomplish this without any additional work to
make sure students with the same quality points are ordered alphabetically.
Table 11.1 at the end of the chapter provides a summary of the sorting methods we will investigate.
Each sort method will be illustrated on the following collection of 20 int values:
59 46 32 80 46 55 50 43 44 81 12 95 17 80 75 33 40 61 16 87
Here is the method specificatio and one of the test cases for the ?Sort method, where ?Sort can
represent any of the sort methods from Section 11.2:
/**
* Sorts a specified array of int values into ascending order.
* The worstTime(n) is O(n * n).
*
* @param x - the array to be sorted.
*
* @throws NullPointerException - if x is null.
*
*/
public static void ?Sort (int[ ] x)
59 46 32 80 46 55 50 43 44 81 12 95 17 80 75 33 40 61 16 87
We first place x [1], 46, where it belongs relative to x [0], 59. One swap is needed, and this gives us:
46 59 32 80 46 55 50 43 44 81 12 95 17 80 75 33 40 61 16 87
The underlined values are in their correct order. We then place x [2], 32, where it belongs relative to the
sorted subarray x [0] ... x [1], and two swaps are required. We now have
32 46 59 80 46 55 50 43 44 81 12 95 17 80 75 33 40 61 16 87
Next, we place x [3], 80, where it belongs relative to the sorted subarray x [0] ... x [2]; this step
does not require any swaps, and the array is now
32 46 59 80 46 55 50 43 44 81 12 95 17 80 75 33 40 61 16 87
We then place x [4], 46, where it belongs relative to the sorted subarray x [0] ... x [3]. Two swaps
are required, and we get
32 46 46 59 80 55 50 43 44 81 12 95 17 80 75 33 40 61 16 87
This process continues until, finally, we place x [19], 87, where it belongs relative to the sorted subarray
x [0] ... x [18]. The array is now sorted:
12 16 17 32 33 40 43 44 46 46 50 55 59 61 75 80 80 81 87 95
At each stage in the above process, we have an int variable i in the range 1 through 19, and we place
x [i] into its proper position relative to the sorted subarray x [0], x [1], . . . , x [i-1]. During each
iteration, there is another loop in which an int variable k starts at index i and works downward until either
k = 0 or x [k – 1] <= x[k]. During each inner-loop iteration, x [k] and x [k –1] are swapped.
{
for (int i = 1; i < x.length; i++)
for (int k = i; k > 0 && x [k -1] > x [k]; k--)
swap (x, k, k -1);
} // method insertionSort
For example, if scores is an array of int values, we could sort the array with the following call:
insertionSort (scores);
Analysis Let n be the number of elements to be sorted. The outer for loop will be executed exactly n–1
times. For each value of i, the number of iterations of the inner loop is equal to the number of swaps required
to sift x [i] into its proper position in x [0], x [1], . . . , x [i-1]. In the worst case, the collection starts
out in decreasing order, so i swaps are required to sift x [i] into its proper position. That is, the number
of iterations of the inner for loop will be
n−1
1 + 2 + 3 + ··· + n − 2 + n − 1 = i = n(n − 1)/2
i=1
The total number of outer-loop and inner-loop iterations is n−1 + n(n−1)/2, so worstTime (n) is quadratic
in n. In practice, what really slows down insertionSort in the worst case is that the number of swaps
is quadratic in n. But these can be replaced with single assignments (see Concept Exercise 11.9).
To simplify the average-time analysis, assume that there are no duplicates in the array to be sorted.
The number of inner-loop iterations is equal to the number of swaps. When x [1] is sifted into its proper
place, half of the time there will be a swap and half of the time there will be no swap.1 Then the expected
number of inner-loop iterations is (0 + 1)/2.0, which is 1/2.0. When x [2] is sifted into its proper place,
the expected number of inner-loop iterations is (0 + 1 + 2)/3.0, which is 2/2.0. In general, when sifting
x [i] to its proper position, the expected number of loop iterations is
(0 + 1 + 2 + · · · + i)/(i + 1.0) = i/2.0
The total number of inner-loop iterations, on average, is
n−1
1/2.0 + 2/2.0 + 3/2.0 + · · · + (n − 1)/2.0 = i /2.0 = n(n−1)/4.0
i =1
We conclude that averageTime (n) is quadratic in n. In the Java Collections Framework, Insertion Sort
is used for sorting subarrays of fewer than 7 elements. Instead of a method call, there is inline code
(off contains the f rst index of the subarray to be sorted, and len contains the number of elements to
be sorted):
// Insertion sort on smallest arrays
if (len < 7)
{
for (int i=off; i<len+off; i++)
for (int k=i; k>off && x[k-1]>x[k]; k--)
swap(x, j, j-1);
return;
}
For small subarrays, other sort methods—usually faster than Insertion Sort—are actually slower because
their powerful machinery is designed for large-sized arrays. The choice of 7 for the cutoff is based on
empirical studies described in Bentley [1993]. The best choice for a cutoff will depend on machine-
dependent characteristics.
An interesting aspect of Insertion Sort is its best-case behavior. If the original array happens to be
in ascending order—of course the sort method does not “know” this—then the inner loop will not be
executed at all, and the total number of iterations is linear in n. In general, if the array is already in order
or nearly so, Insertion Sort is very quick. So it is sometimes used at the tail end of a sort method that
takes an arbitrary array of elements and produces an “almost” sorted array. For example, this is exactly
what happens with the sort method in C++’s Standard Template Library.
The space requirements for Insertion Sort are modest: a couple of loop-control variables, a temporary
for swapping, and an activation record for the call to swap (which we lump together as a single variable).
So worstSpace (n) is constant; such a sort is called an in-place sort.
Because the inner loop of Insertion Sort swaps x [k-1] and x [k] only if x [k-1] > x [k],
equal elements will not be swapped. That is, Insertion Sort is stable.
59 46 32 80 46 55 50 43 44 81 12 95 17 80 75 33 40 61 16 87
The smallest value in the array, 12, is swapped with the value 59 at index 0, and we now have (with the
sorted subarray underlined)
12 46 32 80 46 55 50 43 44 81 59 95 17 80 75 33 40 61 16 87
462 C H A P T E R 11 Sorting
Now 16, the smallest of the values from index 1 on, is swapped with the value 46 at index 1:
12 16 32 80 46 55 50 43 44 81 59 95 17 80 75 33 40 61 46 87
Then 17, the smallest of the values from index 2 on, is swapped with the value 32 at index 2:
12 16 17 80 46 55 50 43 44 81 59 95 32 80 75 33 40 61 46 87
Finally, during the 19th loop iteration, 87 will be swapped with the value 95 at index 18, and the whole array
will be sorted:
12 16 17 32 33 40 43 44 46 46 50 55 59 61 75 80 80 81 87 95
In other words, for each value of i between 0 and x.length - 1, the smallest value in the subarray from
x [i] to x [x.length –1] is swapped with x [i]. Here is the method definition:
/**
* Sorts a specified array of int values into ascending order.
* The worstTime(n) is O(n * n).
*
* @param x - the array to be sorted.
*
* @throws NullPointerException - if x is null.
*
*/
public static void selectionSort (int [ ] x)
{
// Make x [0 . . . i] sorted and <= x [i + 1] . . .x [x.length –1]:
for (int i = 0; i < x.length –1; i++)
{
int pos = i;
for (int k = i + 1; k < x.length; k++)
if (x [k] < x [pos])
pos = k;
swap (x, i, pos);
} // for i
} // method selectionSort
Analysis First, note that the number of loop iterations is independent of the initial arrangement of elements,
so worstTime(n) and averageTime(n) will be identical. There are n–1 iterations of the outer loop; when the
smallest values are at indexes x[0], x[1], . . . x[n–2], the largest value will automatically be at index x[n–1].
During the first iteration, with i = 0, there are n–1 iterations of the inner loop. During the second iteration of the
outer loop, with i = 1, there are n–2 iterations of the inner loop. The total number of inner-loop iterations is
n−1
(n–1) + (n–2) + . . . + 1 = i = n(n–1)/2
i=1
We conclude that worstTime(n) is quadratic in n. For future reference, note that only n –1 swaps are made.
11.2 Simple Sorts 463
The worstSpace(n) is constant: only a few variables are needed. But Selection Sort is not stable; see
Concept Exercise 11.14.
As we noted in Section 11.2.1, Insertion Sort requires only linear-in-n time if the array is already
sorted, or nearly so. That is a clear advantage over Selection Sort, which always takes quadratic-in-n time.
In the average case or worst case, Insertion Sort takes quadratic-in-n time, and so a run-time experiment
is needed to distinguish between Insertion Sort and Selection Sort. You will get the opportunity to do this
in Lab 18.
59 46 32 80 46 55 50 43 44 81 12 95 17 80 75 33 40 61 16 87
Because 59 is greater than 46, those two elements are swapped, and we have
46 59 32 80 46 55 50 43 44 81 12 95 17 80 75 33 40 61 16 87
Then 59 and 32 are swapped, 59 and 80 are not swapped, 80 and 46 (at index 4) are swapped, and so on.
After the first iteration, x contains
46 32 59 46 55 50 43 44 80 12 81 17 80 75 33 40 61 16 87 95
The last swap during the first iteration was of the elements 95 and 87 at indexes 18 and 19, so in the second
iteration, the final comparison will be between the elements at indexes 17 and 18. After the second iteration,
the array contains
32 46 46 55 50 43 44 59 12 80 17 80 75 33 40 61 16 81 87 95
The last swap during the second iteration was of the elements 81 and 16 at indexes 16 and 17, so in the
third iteration, the final comparison will be between the elements at indexes 15 and 16.
Finally, after 18 iterations, and many swaps, we end up with
12 16 17 32 33 40 43 44 46 46 50 55 59 61 75 80 80 81 87 95
Analysis If the array starts out in reverse order, then there will be n–1 swaps during the first outer-loop
iteration, n–2 swaps during the second outer-loop iteration, and so on. The total number of swaps, and
inner-loop iterations, is
n−1
(n–1) + (n–2) + . . . + 1 = i = n(n–1)/2
i=1
What about averageTime(n)? The average number of inner-loop iterations, as you probably would have
guessed (!), is
What is clear from the firs term in this formula is that averageTime(n) is quadratic in n.
It is not a big deal, but Bubble Sort is very eff cient if the array happens to be in order. Then, only n
inner-loop iterations (and no swaps) take place. What if the entire array is in order, except that the smallest
element happens to be at index x.length –1 ? Then n(n –1)/2 inner-loop iterations still occur!
Swaps take place when, for some index i, the element at index i is greater than the element at index
i + 1. This implies that Bubble Sort is stable. And with just a few variables needed (the space for the
array was allocated in the calling method), worstSpace(n) is constant.
What drags Bubble Sort down, with respect to run-time performance, is the large number of swaps that
occur, even in the average case. You will get f rst-hand experience with Bubble Sort’s run-time sluggishness
if you complete Lab 18. As Knuth [1973] says, “In short, the bubble sort seems to have nothing going for
it, except a catchy name and the fact that it leads to some interesting theoretical problems.”
11.3 The Comparator Interface 465
and change heading of the swap method and the type of temp in that method. See Programming
Exercise 11.3.
As we saw in Section 10.1.1, the String class implements the Comparable interface with a com
pareTo method that reflect a lexicographic ordering. If names is an array of String objects, we can
sort names into lexicographical order with the call
selectionSort (names);
This raises an interesting question: What if we did not want the “natural” ordering? For example, what if
we wanted String objects ordered by the length of the string?
For applications in which the “natural” ordering—through the Comparable interface—is inappro-
priate, elements can be compared with the Comparator interface. The Comparator interface, with type
parameter T (for “type”) has a method to compare two elements of type T:
/**
* Compares two specified elements.
*
* @param element1 - one of the specified elements.
* @param element2 - the other specified element.
*
* @return a negative integer, 0, or a positive integer, depending on
* whether element1 is less than, equal to, or greater than
* element2.
466 C H A P T E R 11 Sorting
*
*/
int compare (T element1, T element2);
We can implement the Comparator interface to override the natural ordering. For example, we can
implement the Comparator interface with a ByLength class that uses the “natural” ordering for String
objects of the same length, and otherwise returns the difference in lengths. Then the 3-character string
“yes” is considered greater than the 3-character string “and,” but less than the 5-character string “maybe.”
Here is the declaration of ByLength:
public class ByLength implements Comparator<String>
{
/**
* Compares two specified String objects lexicographically if they have the
* same length, and otherwise returns the difference in their lengths.
*
* @param s1 - one of the specified String objects.
* @param s2 - the other specified String object.
*
* @return s1.compareTo (s2) if s1 and s2 have the same length;
* otherwise, return s1.length() - s2.length().
*
*/
public int compare (String s1, String s2)
{
int len1 = s1.length(),
len2 = s2.length();
if (len1 == len2)
return s1.compareTo (s2);
return len1 - len2;
} // method compare
} // class ByLength
One advantage to using a Comparator object is that no changes need be made to the element class: the
compare method’s parameters are the two elements to be ordered. Leaving the element class unchanged
is especially valuable when, as with the String class, users are prohibited from modifying the class.
Here is a definitio of Selection Sort, which sorts an array of objects according to a comparator that
compares any two objects:
/**
* Sorts a specified array into the order specified by a specified Comparator
* object.
* The worstTime(n) is O(n * n).
*
* @param x - the array to be sorted.
* @param comp - the Comparator object used for ordering.
*
* @throws NullPointerException - if x and/or comp is null.
*
*/
11.3 The Comparator Interface 467
To complete the picture, here is a small program method that applies this version of selectionSort
(note that the enhanced for statement also works for arrays):
import java.util.*; // for the Comparator interface
} // class SelectionSortExample
The material in this section will be helpful in Section 11.4.1, where we will encounter one of the sort
methods in the Java Collections Framework. For this method, called Merge Sort, the element type cannot
be primitive; it must be (reference to) Object, or subclass of Object. The method comes in four flavors
468 C H A P T E R 11 Sorting
the collection to be sorted can be either an array object or a List object, and the ordering may be
according to the Comparable interface or the Comparator interface. And Chapter 13 has a sort method
with similar f exibility.
In Section 11.4, we consider two important questions: For comparison-based sorts, is there a lower
bound for worstTime(n)? Is there a lower bound for averageTime(n)?
a1 < a2?
yes no
a1 a3 a2 a3 a1 a2 a2 a3 a1 a3 a2 a1
2 For the sake of simplicity, we assume the collection to be sorted does not contain any duplicates.
11.4 How Fast Can we Sort? 469
Sorting Fact 1:
For comparison-based sorts, worstTime(n) is (n log n).
What does Sorting Fact 1 say about upper bounds? We can say, for example, that for any comparison-based
sort, worstTime(n) is not O(n). But can we say that for any comparison-based sort, worstTime(n) is O(n
log n)? No, because for each of the sorts in Section 11.2, worstTime(n) is O(n 2 ). We cannot even be sure,
at this point, that there are any comparison-based sorts whose worstTime(n) is O(n log n). Fortunately,
this is not some lofty, unattainable goal. For the comparison-based sort algorithms in Sections 11.4.1 and
13.4, worstTime(n) is O(n log n). When we combine that upper bound with the lower bound from Sorting
Fact 1, we will have several sort methods whose worstTime(n) is linear-logarithmic in n.
What about averageTime(n)? For any comparison-based sort, n log n is a lower bound of
averageTime(n) as well. To obtain this result, suppose t is a decision tree for sorting n elements. Then
t has n! leaves. The average, over all n! permutations, number of comparisons to sort the n elements is
the total number of comparisons divided by n!. In a decision tree, the total number of comparisons is the
sum of the lengths of all paths from the root to the leaves. This sum is the external path length of the
decision tree. By the External Path Length Theorem in Chapter 9, E(t) >= (n!/2) floo (log2 (n!)). So we
get, for any positive integer n:
averageTime(n)>= average number of comparisons
= E(t)/n!
>= (n!/2)floo (log2 (n!))/n!
= (1/2)floo (log2 (n!))
>= (1/4)log2 (n!)
>= (n/8)log2 (n/2) [by Concept Exercise 11.7]
We conclude that n log n is a lower bound of averageTime(n). That is,
Sorting Fact 2:
For comparison-based sorts, averageTime(n) is (n log n).
We noted that there are several sort methods whose worstTime(n) is linear-logarithmic in n. We can use
that fact to show that their averageTime(n) must also be linear-logarithmic in n. Why? Suppose we have
a sort algorithm for which worstTime(n) is linear-logarithmic in n. That is, crudely, n log n is both an
upper bound and a lower bound of worstTime(n). But averageTime(n) <= worstTime(n), so if n log n is
an upper bound of worstTime(n), n log n must also be an upper bound of averageTime(n). According to
Sorting Fact 2, n log n is a lower bound on averageTime(n). Since n log n is both an upper bound and a
lower bound of averageTime(n), we conclude that averageTime(n) must be linear-logarithmic in n. That is,
Sorting Fact 3:
For comparison-based sorts, if worstTime(n) is linear-logarithmic in n, then averageTime(n) must be
linear-logarithmic in n.
470 C H A P T E R 11 Sorting
In Sections 11.4.1 and 11.4.3, we will study two sort algorithms, Merge Sort and Quick Sort, whose
averageTime(n) is linear-logarithmic in n. For Merge Sort, worstTime(n) is also linear-logarithmic in n,
while for Quick Sort, worstTime(n) is quadratic in n. Strangely enough, Quick Sort is generally considered
the most eff cient all-around sort. Quick Sort’s worst-case performance is bad, but for average-case, run-
time speed, Quick Sort is the best of the lot.
This method has the identifie sort because it is the only method in the Arrays class of the package
java.util for sorting an array of objects according to the Comparable interface. Later in this chapter
we will encounter a different sort method—also with the identifie sort —for sorting an array of values
from a primitive type. The distinction is easy to make from the context, namely, whether the argument is
an array of objects or an array from a primitive type such as int or double.
Example To start with a small example of Merge Sort, here are the int values in an array of Integer
objects:
59 46 32 80 46 55 87 43 44 81
We use an auxiliary array, aux. We f rst clone the parameter a into aux. (Cloning is acceptable here because
an array object cannot invoke a copy constructor.) We now have two arrays with (separate references to)
identical elements. The recursive method mergeSort is then called to sort the elements in aux back into
a. Here is the definitio of the sort method:
public static void sort (Object[ ] a)
{
Object aux[ ] = (Object [ ])a.clone();
mergeSort (aux, a, 0, a.length);
} // method sort
11.4 How Fast Can we Sort? 471
The reason we have two arrays is to make it easier to merge two sorted subarrays into a larger subarray.
The reason for the int parameters low and high is that their values will change when the recursive calls
are made. Note that high’s value is one greater than the largest index of the subarray being mergeSorted.3
When the initial call to mergeSort is made with the example data, Insertion Sort is not performed
because high – low >= 7. (The number 7 was chosen based on run-time experiments.) Instead, two
recursive calls are made:
mergeSort (a, aux, 0, 5);
mergeSort (a, aux, 5, 10);
When the f rst of these calls is executed, high – low = 5 − 0 < 7, so Insertion Sort is performed on the
firs fiv elements of aux:
a [0 . . . 4] = {59, 46, 32, 80, 46}
In general, the two arrays will be identical until an Insertion Sort is performed. When the second recursive
call is made, high – low = 10–5, so Insertion Sort is performed on the second fiv elements of aux:
a [5 . . . 9] = {55, 87, 43, 44, 81}
Upon the completion of these two calls to mergeSort, the ten elements of aux, in two sorted subarrays
of size 5, are merged back into a, and we are done. The merging is accomplished with the aid of two
indexes, p and q. In this example, p starts out as 0 (the low index of the left subarray of aux) and q starts
out as 5 (the low index of the right subarray of aux). In the following figure arrows point from p and q
to the elements at aux [p] and aux [q]:
aux 32 46 46 59 80 43 44 55 81 87
p q
3 Technically, there is a f fth parameter. But since we assume that the entire array is being sorted, we can ignore that parameter.
472 C H A P T E R 11 Sorting
The smaller of aux [p] and aux [q] is copied to a [p] and then the index, either p or q, of that smaller
element is incremented:
aux 32 46 46 59 80 43 44 55 81 87
p q
a 32
The process is repeated: the smaller of aux [p] and aux [q] is copied to the next location in the array
a, and then the index, either p or q, of that smaller element is incremented:
aux 32 46 46 59 80 43 44 55 81 87
p q
a 32 43
The next three iterations will copy 44, 46, and 46 into a. The process continues until all of the elements
from both subarrays have been copied into a. Since each iteration copies one element to a, merging two
subarrays of the same size requires exactly twice as many iterations as the size of either subarray. In
general, to merge n/k subarrays, each of size k , requires exactly n iterations.
Figure 11.2 summarizes the sorting the above array of ten elements.
aux 59 46 32 80 46 55 87 43 44 81
mergeSort(aux, a, 0,10)
a 59 46 32 80 46 55 87 43 44 81
mergeSort mergeSort
(a, aux,0, 5) (a, aux, 5, 10)
Insertion Sort Insertion Sort
aux 32 46 46 59 80 43 44 55 81 87
merge
a 32 43 44 46 46 55 59 80 81 87
Figure 11.3 incorporates the above example in merge sorting an array of 20 elements. Two pairs of
recursive calls are required to merge sort 20 elements.
The successive calls to mergeSort resemble a ping-pong match:
aux −−−−−−→ a −−−−−−→ aux −−−−−−→ a −−−−−−→ aux −−−−−−→ a
11.4 How Fast Can we Sort? 473
aux 59 46 32 80 46 55 50 43 44 81 12 95 17 80 75 33 40 61 16 87
a 59 46 32 80 46 55 50 43 44 81 12 95 17 80 75 33 40 61 16 87
mergeSort mergeSort
(a, aux, 0, 10) (a, aux 10, 20)
aux 59 46 32 80 46 55 50 43 44 81 12 95 17 80 75 33 40 61 16 87
a 32 46 46 59 80 43 44 50 55 81 12 17 75 80 95 16 33 40 61 87
merge merge
aux 32 43 44 46 46 50 55 59 80 81 12 16 17 33 40 61 75 80 81 95
merge
a 12 16 17 32 33 40 43 44 46 46 50 55 59 61 75 80 80 80 81 87 95
The original call, from within the sort method, is always of the form
mergeSort (aux, a, . . .);
So after all of the recursive calls have been executed, the sorted result ends up in a. After the Insertion
Sorting has been completed for the two successive subarrays in the recursive call to mergeSort, a merge
of those sorted subarrays is performed, and that completes a recursive call to mergeSort.
Here is the complete mergeSort method, including an optimization (starting with //If left
subarray . . .) that you can safely ignore.
/**
* Sorts, by the Comparable interface, a specified range of a specified array
* into the same range of another specified array.
* The worstTime(k) is O(k log k), where k is the size of the subarray.
*
* @param src - the specified array whose range is to be sorted into another
* specified array.
474 C H A P T E R 11 Sorting
Analysis We want to get an upper bound on worstTime(n), where n is the number of elements to be sorted.
There are four phases: cloning, calls to mergeSort, Insertion Sorting, and merging.
36 because Insertion Sort’s worst time is quadratic in the number of elements. So the total number of
iterations for this phase is less than 36n.
Finally, the merging back into double-sized subarrays takes, approximately, L times the number of
iterations per level, that is, log2 (n/6) times the number of iterations executed at any level. At any level,
exactly n elements are copied from a to aux (or from aux to a). So the total number of iterations is,
approximately, n log2 (n/6).
The total number of iterations is less than
n + log2 (n/6) + 36n + log2 (n/6) ∗ n
From this we conclude that worstTime(n) is less than
n + log2 (n/6) + 36n + nlog2 (n/6).
That is, worstTime(n) is O(n log n). By Sorting Fact 1 in Section 13.3, for any comparison-based sort,
worstTime(n) is (n log n). Since n log n is both an upper bound and a lower bound of worstTime(n),
worstTime(n) must be linear-logarithmic in n (that is, (n log n): Big Theta of n log n) That implies, by
Sorting Fact 3, that averageTime(n) is also linear-logarithmic in n.
Not only is mergeSort as good as you can get in terms of estimates of worstTime(n) and
averageTime(n), but the actual number of comparisons made is close to the theoretical minimum (see
Kruse [1987], pages 251–254).
What is worstSpace(n)? The temporary array aux, of size n, is created before mergeSort is called.
During the execution of mergeSort, activation records are created at each level. At the f rst level, two
activation records are created; at the second level, four activation records are created; and so on. The total
number of activation records created is
L
L
2 + 4 + 8 + 16 + . . . + 2 = 2i = 2L+1 − 2
i =1
(The result on the sum of powers of 2 is from Exercise A2.6 in Appendix 2). Since L ∼ log2 (n/7), and
n
2log2 = n
we conclude that the total number of activation records created is linear in n. When we add up the linear-
in-n space for aux and the linear-in-n space for activation records, we conclude that worstSpace(n) is
linear in n.
Both the Insertion Sorting phase and the merging phase preserve the relative order of elements. That
is, mergeSort is a stable sort.
The essential difference between this version and the Comparable version is that an expression such as
(Comparable)dest[j-1]).compareTo(dest[j])>0
is replaced with
c.compare(dest[j-1], dest[j])>0
476 C H A P T E R 11 Sorting
For example, suppose words is an array of String objects. To perform Merge Sort on words by the
lengths of the strings (but lexicographically for equal-length strings), we utilize the ByLength class from
Section 11.3:
Arrays.sort (words, new ByLength());
The Collections class, also in the package java.util, has two versions—depending on whether or
not a comparator is supplied—of a Merge Sort method. Each version has a List parameter and starts
by copying the list to an array. Then the appropriate version of sort from the Arrays class is called.
Finally, during an iteration of the list, each element is assigned the value of the corresponding element in
the array. Here, for example, is the Comparator version:
/**
* Sorts a specified List object of elements from class E according to a
* specified Comparator object.
* The worstTime(n) is O(n log n).
*
* @param list - the List object to be sorted.
* @param c - the Comparator object that determines the ordering of elements.
*
*/
public static void sort (List list, Comparator<? super T> c)
{
Object a[ ] = list.toArray();
Arrays.sort(a, c);
ListIterator i = list.listIterator();
for (int j=0; j<a.length; j++)
{
i.next();
i.set(a[j]);
} // for
} // method sort
Both versions of Merge Sort in the Collections class work for any class that implements the List
interface, such as ArrayList and LinkedList. The run-time will be somewhat slower than for the
Arrays -class versions because of the copying from the list to the array before sorting and the copying
from the array to the list after sorting.
One limitation to the current versions of Merge Sort is that they do not allow an array of primitives
to be merge sorted. The effect of this restriction can be overcome by merge sorting the corresponding
array of objects. For example, to Merge Sort an array of int values, create an array of Integer objects,
convert each int value to the corresponding Integer object, apply Merge Sort to the array of Integer
objects, then convert the Integer array back to an array of int values. But this roundabout approach
will increase the run time for merge sorting.
4 Actually, there are fourteen versions, because for each primitive type, there is a version that allows Quick Sort to be applied to an entire
The basic idea behind the sort1 method is this: we f rst partition the array x into a left subarray and a
right subarray so that each element in the left subarray is less than or equal to each element in the right
subarray. The sizes of the subarrays need not be the same. We then Quick Sort the left and right subarrays,
and we are done. Since this last statement is easily accomplished with two recursive calls to sort1, we
will concentrate on the partitioning phase.
Let’s start with the essentials; in Section 11.4.3.1, we’ll look at some of the f ner points. The f rst
task in partitioning is to choose an element, called the pivot, that each element in x will be compared to.
Elements less than the pivot will end up in the left subarray, and elements greater than the pivot will end
up in the right subarray. Elements equal to the pivot may end up in either subarray.
What makes Quick Sort fast? With other sorts, it may take many comparisons to put an element
in the general area where is belongs. But with Quick Sort, a partition can move many elements close to
where they will finall end up. This assumes that the value of the pivot is close to the median5 of the
elements to be partitioned. We could, of course, sort the elements to be partitioned and then select the
median as the pivot. But that begs the question of how we are going to sort the elements in the firs place.
How about choosing x [off] —the element at the start index of the subarray to be sorted—as the
pivot? If the elements happen to be in order (a common occurrence), that would be a bad choice. Why?
Because the left subarray would be empty after partitioning, so the partitioning would reduce the size of
the array to be sorted by only one. Another option is to choose x [off + len/2] as the pivot, that is, the
element in the middle position. If the range happens to be in order, that is the perfect choice; otherwise,
it is as good a blind choice as any other.
With a little extra work, we can substantially increase the likelihood that the pivot will split the
range into two subarrays of approximately equal size. The pivot is chosen as the median of the elements
at indexes off, off + len/2, and off + len -1. The median of those three elements is taken as a
simply calculated estimate of the median of the whole range.
Before looking at any more details, let’s go through an example.
Example We start with the usual sample of twenty values given earlier:
59 46 32 80 46 55 50 43 44 81 12 95 17 80 75 33 40 61 16 87
In this case, we choose the median of the three int values at indexes 0, 10, and 19. The median of 59, 12,
and 87 is 59, so that is the original pivot.
5 The median of a collection of values is the value that would be in the middle position if the collection were sorted. For example, the median
of
100 32 77 85 95
is 85. If the collection contains an even number of values, the median is the average of the two values that would be in the two middle
positions if the collection were sorted. For example, the median of
100 32 77 85 95 80
is 82.5.
11.4 How Fast Can we Sort? 479
We now want to move to the left subarray all the elements that are less than 59 and move to the right
subarray all the elements that are greater than 59. Elements with a value of 59 may end up in either
subarray, and the two subarrays need not have the same size.
To accomplish this partitioning, we create two counters: b, which starts at off and moves upward,
and c, which starts at off + len - 1 and moves downward. There is an outer loop that contains two
inner loops. The firs of these inner loops increments b until x [b] >= pivot. Then the second inner
loop decrements c until x [c] <= pivot. If b is still less than or equal to c when this second inner
loop terminates, x [b] and x [c] are swapped, b is incremented, c is decremented, and the outer loop
is executed again. Otherwise, the outer loop terminates.
The reason we loop until x [b] >= pivot instead of x [b] > pivot is that there might not be
any element whose value is greater than the pivot. In Section 11.4.3.1, we’ll see a slightly different loop
condition to avoid stopping at, and therefore swapping, the pivot.
For the usual sample of values, pivot has the value 59, b starts at 0 and c starts at 19. In Figure 11.4,
arrows point from an index to the corresponding element in the array:
pivot
59
59 46 32 80 46 55 50 43 44 81 12 95 17 80 75 33 40 61 16 87
b c
The f rst inner loop terminates immediately because x [b] = 59 and 59 is the pivot. The second
inner loop terminates when c is decremented to index 18 because at that point, x [c] = 16 < 59. When
59 and 16 are swapped and b and c are bumped, we get the situation shown in Figure 11.5.
pivot
59
16 46 32 80 46 55 50 43 44 81 12 95 17 80 75 33 40 61 59 87
b c
FIGURE 11.5 The state of partitioning after the firs iteration of the outer loop
Now b is incremented twice more, and at that point we have x [b] = 80 > 59. Then c is decremented
once more, to where x [c] = 40 < 59. After swapping x [b] with x [c] and bumping b and c, we
have the state shown in Figure 11.6.
During the next iteration of the outer loop, b is incremented f ve more times, c is not decremented,
81 and 33 are swapped, then the two counters are bumped, and we have the state shown in Figure 11.7.
480 C H A P T E R 11 Sorting
pivot
59
16 46 32 40 46 55 50 43 44 81 12 95 17 80 75 33 80 61 59 87
b c
FIGURE 11.6 The state of partitioning after the second iteration of the outer loop
pivot
59
16 46 32 40 46 55 50 43 44 33 12 95 17 80 75 81 80 61 59 87
b c
FIGURE 11.7 The state of partitioning after the third iteration of the outer loop
During the next iteration of the outer loop, b is incremented once (x [b] = 95), and c is decremented
twice (x [c] = 17). Then 95 and 17 are swapped, and b and c are bumped. See Figure 11.8.
All of the elements in the subarray at indexes 0 through c are less than or equal to the pivot, and
all the elements in the subarray at indexes b through 19 are greater than or equal to the pivot.
We finis up by making two recursive calls to sort1:
sort1 (x, off, c + 1 - off); // for this example, sort1 (x, 0, 12);
sort1 (x, b, off + len –b); // for this example, sort1 (x, 12, 8);
In this example, the call to sort1 (x, off, c + 1 - off) will choose a new pivot, partition the
subarray of 12 elements starting at index 0, and make two calls to sort1. After those two calls (and their
recursive calls) are completed, the call to sort1 (x, b, off + len –b) will choose a new pivot, and
so on. If we view each pair of recursive calls as the left and right child of the parent call, the execution
of the calls in the corresponding binary tree follows a preOrder traversal: the original call, then the left
child of that call, then the left child of that call, and so on. This leftward chain stops when the subarray
to be sorted has fewer than two elements.
pivot
59
16 46 32 40 46 55 50 43 44 33 12 17 95 80 75 81 80 61 59 87
b c
FIGURE 11.8 The state of partitioning after the outer loop is exited
11.4 How Fast Can we Sort? 481
After partitioning, the left subarray consists of the elements from indexes off through c, and the
right subarray consists of the elements from indexes b through off + len –1. The pivot need not end up
in either subarray. For example, suppose at some point in sorting, the subarray to be partitioned contains
15 45 81
The pivot, at index 1, is 45, and both b and c move to that index in searching for an element greater than
or equal to the pivot and less than or equal to the pivot, respectively. Then (wastefully) x [b] is swapped
with x [c], b is incremented to 2, and c is decremented to 0. The outer loop terminates, and no further
recursive calls are made because the left subarray consists of 15 alone, and the right subarray consists of
81 alone. The pivot is, and remains, where it belongs.
Similarly, one of the subarrays may be empty after a partitioning. For example, if subarray to be
partitioned is
15 45
The pivot is 45, both b and c move to that index, 45 is swapped with itself, b is incremented to 2 and c
is decremented to 0. The left subarray consists of 15 alone, the pivot is in neither subarray, and the right
subarray is empty.
Here is the method definitio for the above-described version of sort1 (the version in the Arrays
class has a few optimizations, discussed in Section 11.4.3.1):
/**
* Sorts into ascending order the subarray of a specified array, given
* an initial index and subarray length.
* The worstTime(k) is O(n * n) and averageTime(n) is O(n log n),
* where n is the length of the subarray to be sorted.
*
* @param x - the array whose subarray is to be sorted.
* @param off - the start index in x of the subarray to be sorted.
* @param len - the length of the subarray to be sorted.
*
*/
private static void sort1(int x[ ], int off, int len)
{
// Choose a pivot element, v
int m = off + (len >> 1),
l = off,
n = off + len - 1;
int b = off,
c = off + len - 1;
while(true)
{
while (b <= c && x [b] < v)
b++;
while (c >= b && x [c] > v)
c--;
482 C H A P T E R 11 Sorting
if (b > c)
break;
swap (x, b++, c--);
} // while true
if (c + 1 –off > 1)
sort1 (x, off, c + 1 –off);
if (off + len –b > 1)
sort1 (x, b, off + len -b);
} // method sort1
/**
* Finds the median of three specified elements in a given array..
*
* @param x - the given array.
* @param a - the index of the first element.
* @param b - the index of the second element.
* @param c - the index of the third element
*
* @return the median of x [a], x [b], and x [c].
*
*/
private static int med3(int x[], int a, int b, int c) {
return (x[a] < x[b] ?
(x[b] < x[c] ? b : x[a] < x[c] ? c : a) :
(x[b] > x[c] ? b : x[a] > x[c] ? c : a));
} // method med3
/**
* Swaps two specified elements in a specified array.
*
* @param x – the array in which the two elements are to be swapped.
* @param a – the index of one of the elements to be swapped.
* @param b – the index of the other element to be swapped.
*
*/
private static void swap(int x[], int a, int b) {
int t = x[a];
x[a] = x[b];
x[b] = t;
} // method swap
Analysis We can view the effect of sort1 as creating an imaginary binary search tree, whose root element
is the pivot and whose left and right subtrees are the left and right subarrays. For example, suppose we call
Quick Sort for the following array of 15 integers
68 63 59 77 98 87 84 51 17 12 8 25 42 35 31
11.4 How Fast Can we Sort? 483
51
25 77
12 35 63 87
8 17 31 42 59 68 84 98
FIGURE 11.9 The imaginary binary search tree created by repeated partitions, into equal sized subarrays, of the
array [68 63 59 77 98 87 84 51 17 12 8 25 42 35 31]
The f rst pivot chosen is 51; after the firs partitioning, the pivot of the left subarray is 25 and the pivot
of the right subarray is 77. Figure 11.9 shows the full binary-search-tree induced during the sorting of
the given array. In general, we get a full binary search tree when each partition splits its subarray into
two subarrays that have the same size. We would also get such a tree if, for example, the elements were
originally in order or in reverse order, because then the pivot would always be the element at index off
+ len/2, and that element would always be the actual median of the whole sequence.
Contrast the above tree with the tree shown in Figure 11.10. The tree in Figure 11.10 represents the
partitioning generated, for example, by the following sequence of 38 elements (the pivot is the median of
1, 37, and 36):
36
1 37
0 34
3 35
2 32
.
.
.
FIGURE 11.10 Worst-case partitioning: each partition reduces by only 2 the size of the subarray to be Quick
Sorted. The corresponding binary search tree has a leaf at every non-root level. The subtrees below 32 are not
shown
Quick Sort’s space needs are due to the recursive calls, so the space estimates depend on the longest
chain of recursive calls, because that determines the maximum number of activation records in the run-
time stack. In turn, the longest chain of recursive calls corresponds to the height of the induced binary
search tree. In the average case, that height is logarithmic in n, and we conclude that averageSpace(n) is
logarithmic in n. In the worst case, that height is linear in n, so worstSpace(n) is linear in n.
Quick Sort is not a stable sort. For example, in the example given at the beginning of this section,
there are two copies of 80. The one at index 3 is swapped into index 16, and the one at index 13 remains
where it starts.
Quick Sort is another example of the Divide-and-Conquer design pattern. Each of the recursive calls
to sort1 can be done in parallel, and the combined effect of those calls is a sorted array.
260
FIGURE 11.11 The calculation of the pivot as the median of three medians. The median of (139, 287, 275) is
275; the median of (407, 258, 191) is 258; the median of (260, 126, 305) is 260. The median of (275, 258, 260)
is 260, and that is chosen as the pivot
be calculated for a sample arrangement of the array x. These extra comparisons have a price, but they
increase the likelihood of an even split during partitioning.
There are two additional refinements both related to the pivot. Instead of incrementing b until an
element greater than or equal to the pivot is found, the search is given by
while (b <= c && x[b] <= v)
A similar modificatio is made in the second inner loop. This appears to be an optimization because the
pivot won’t be swapped, but the inner-loop conditions also test b <= c. That extra test may impede the
speed of the loop more than avoiding needless swaps would enhance speed.
That refinemen enables another pivot-related refinement For the sort1 method define above, the
pivot may end up in one of the subarrays and be included in subsequent comparisons. These comparisons
can be avoided if, after partitioning, the pivot is always stored where it belongs. Then the left subarray
will consist of elements strictly less than the pivot, and the right subarray will consist of elements strictly
greater than the pivot. Then the pivot—indeed, all elements equal to the pivot—will be ignored in the rest
of the Quick Sorting.
To show you how this can be accomplished, after the execution of the outer loop the relation of
segments of the subarray to the pivot v will be as shown in Figure 11.12.
At this point, equal-to-pivot elements are swapped back into the middle of the subarray. The left and
right subarrays in the recursive calls do not include the equal-to-pivot elements.
As the partitioning of a subarray proceeds, the equal-to-pivot elements are moved to the beginning
and end of the subarray with the help of a couple of additional variables:
int a = off,
d = off + len – 1;
= v < v > v = v
off c b off+len-1
FIGURE 11.12 The relationship of the pivot v to the elements in the subarray to be partitioned. The leftmost
segment and rightmost segment consist of copies of the pivot
486 C H A P T E R 11 Sorting
The index a will be one more than highest index of an equal-to-pivot element in the left subarray, and d
will be one less than the lowest index of an equal-to-pivot element in the right subarray. In the f rst inner
loop, if x [b] = v, we call
swap (x, a++, b);
Figure 11.13 indicates where indexes a and d would occur in Figure 11.12.
= v < v > v = v
off a c b d off+len-1
For an example that has several copies of the pivot, suppose we started with the following array of
20 ints:
59 46 59 80 46 55 87 43 44 81 95 12 17 80 75 33 40 59 16 50
During partitioning, copies of 59 are moved to the leftmost and rightmost part of the array. After b and c
have crossed, we have the arrangement shown in Figure 11.14.
59 59 46 50 46 55 16 43 44 40 33 12 17 80 75 95 81 80 87 59
a c b d
Now all the duplicates of 59 are swapped into the middle, as shown in Figure 11.15.
59 59 46 50 46 55 16 43 44 40 33 12 17 80 75 95 81 80 87 59
FIGURE 11.15 The swapping of equal-to-pivot elements to the middle of the array
12 17 46 50 46 55 16 43 44 40 33 59 59 59 75 95 81 80 87 80
FIGURE 11.16 The array from Figure 11.15 after the swapping
11.4 How Fast Can we Sort? 487
The duplicates of 59, at indexes 11, 12, and 13, are in their fina resting place.
Here, from the Arrays class, is the complete definitio (the swap and med3 method definition were
given in Section 11.4.3):
/**
* Sorts into ascending order the subarray of a specified array, given
* an initial index and subarray length.
* The worstTime(n) is O(n * n) and averageTime(n) is O(n log n),
* where n is the length of the subarray to be sorted.
*
* @param x - the array whose subarray is to be sorted.
* @param off - the start index in x of the subarray to be sorted.
* @param len - the length of the subarray to be sorted.
*
*/
private static void sort1(int x[ ], int off, int len)
{
/**
* Swaps the elements in two specified subarrays of a given array.
* The worstTime(n) is O(n), where n is the number of pairs to be swapped.
*
* @param x – the array whose subarrays are to be swapped.
* @param a – the start index of the first subarray to be swapped.
* @param b – the start index of the second subarray to be swapped.
* @param n – the number of elements to be swapped from each subarray.
*
*/
private static void vecswap(int x[], int a, int b, int n) {
for (int i=0; i<n; i++, a++, b++)
swap(x, a, b);
} // method vecswap
With these optimizations, the results of the analysis in Section 11.4.3 still hold. For example, we now
show that if Quick Sort is applied to a large array, worstTime(n) will still be quadratic in n. For n> 40,
the worst case occurs when the 9 elements involved in the calculation of the median of medians are the
fiv smallest and four largest elements. Then the f fth-smallest element is the best pivot possible, and the
partitioning will reduce the size of the subarray by 5. In partitioning the subarray of size n − 5, we may
have the four smallest and f ve largest tested for median of medians, and the size will again be reduced
by only 5. Since the number of iterations at each level is, approximately, the size of the subarray to be
11.5 Radix Sort 489
85 3 19 43 20 55 42 91 21 85 73 29
490 C H A P T E R 11 Sorting
lists
0 20 null
1 91 21 null
2 42 null
3 3 43 73 null
5 85 55 85 null
9 19 29 null
FIGURE 11.17 An array of linked lists after each element in the original array is appended to the linked list that
corresponds to the element’s units digit
After each of these is appended to the linked list corresponding to its units (that is, rightmost) digit, the
array of linked lists will be as shown in Figure 11.17.
Then, starting at the beginning of each list, elements in lists [0], lists [1], and so on are
stored back in a. See Figure 11.18.
20 91 21 42 3 43 73 85 55 85 19 29
FIGURE 11.18 The contents of the array a, with the elements ordered by their units digits
The elements in the array a have now been ordered by their units digits.
In the next outer-loop iteration, each element in a is appended to the list corresponding to the
element’s tens digit, as shown in Figure 11.19.
Finally (because the integers had at most two digits), starting at the beginning of each list, the
integers in lists [0], lists [1], and so on are stored back in a:
3 19 20 21 29 42 43 55 73 85 85 91
The elements in a have been ordered by their tens digits, and for numbers with the same tens digits, they
have been ordered by their units digits. In other words, the array a is sorted.
What happens in general? Suppose we have two integers x and y, with x < y. Here’s how Radix
Sort ensures that x ends up at a smaller index in the array. If x has fewer digits than y, then in the fina
iteration of the outer loop, x will be placed in lists [0] and y will be placed in a higher-indexed list
because y’s leftmost digit is not zero. Then when the lists are stored back in the array, x will be at a
smaller index than y.
If x and y have the same number of digits, start at the leftmost digit in each number and, moving
to the right, f nd the f rst digit in x that is smaller than the corresponding digit in y. (For example, if
x = 28734426 and y = 28736843, the thousands digit in x is less than the thousands digit in y.) Then at
the start of that iteration of the outer loop, x will be placed in a lower indexed list than y. Then when the
11.5 Radix Sort 491
lists
0 3 null
1 19 null
2 20 21 29 null
4 42 43 null
5 55 null
7 73 null
8 85 85 null
9 91 null
FIGURE 11.19 The array of linked lists after each element in the array a has been appended to the linked list
corresponding to its tens digit
lists are stored back in the array, x will be at a smaller index than y. And from that point on, the relative
positions of x and y in the array will not change because they agree in all remaining digits.
There is a slight diff culty in converting the outline in the previous paragraphs into a Java method
definition The following statement is illegal:
LinkedList<Integer>[ ] lists = new LinkedList<Integer> [10];
The reason is that arrays use covariant subtyping (for example, the array Double[ ] is a subtype of
Object[ ]), but parameterized types use invariant subtyping (for example, LinkedList<Double> is
not a subtype of LinkedList<Object>). We cannot create an array whose elements are parameterized
collections. But it is okay to create an array whose elements are raw (that is, unparameterized) collections.
So we will create the array with the raw type LinkedList, and then construct the individual linked lists
with a parameterized type.
Here is the method definition
/**
* Sorts a specified array into ascending order.
* The worstTime(n) is O(n log N), where n is the length of the array, and N is the largest
* number (in absolute value) of the numbers in the array.
*
* @param a - the array to be sorted.
*
*/
public static void radixSort (int [ ] a)
{
final int RADIX = 10;
long quotient = 1; // the type is long because the largest number may have
// 10 digits; the successive quotients are 1, 10, 100, 1000,
// and so on. 10 to the 10th is too large for an int value.
Analysis Suppose N is the largest integer in the array. The number of outer-loop iterations must be at least
ceil (log10 N), so worstTime(n, N) is O(n log N). If the array also includes negative integers, N is chosen as the
largest number in absolute value. Each array element is also stored in a linked list, and so worstSpace(n) is
linear in n.
The elements are stored first-in first-ou in each list, and that makes Radix Sort stable.
Note: The elements in this example of Radix Sort are of type int, but with a slight change, the element
type could also be String, for example. There would be one list for each possible character in the String
class. Because each Unicode character occupies 16-bits, the number of distinct characters is 216 = 65, 536
characters. That would require 65,536 linked lists! Instead, the allowable character set would probably be
reduced to ASCII, an 8-bit code, so there would be only 28 = 256 characters, and therefore 256 lists.
Lab 18 includes Radix Sort in a run-time experiment on sort methods.
You are now prepared to do Lab 18: Run-times for Sort Methods.
Summary 493
SUMMARY
Table 11.1 provides a thumbnail sketch of the sort algo-
rithms presented in this chapter.
Table 11.1 Important features of sort algorithms from Chapter 11. Run-time rank is based on the time to sort
n randomly-generated integers. The restrictions on element type are for the versions of Merge Sort and Quick
Sort in the Java Collections Framework (JCF). For Radix Sort, N refers to the largest number in the collection
Element Type
Sort Algorithm Restriction Stable? worstTime(n) averageTime(n); run-time rank worstSpace(n)
CROSSWORD PUZZLE
1 2 3
6 7
www.CrosswordWeaver.com
ACROSS DOWN
1. The worstTime(n) for the three simple 1. The sorting algorithm whose average
sorts is ______ in n. run-time performance is fastest
8. The number of different versions of 3. The only one of the three simple sorts
the Quick Sort algorithm in the Arrays that is not stable
class
4. In Quck Sort partitioning, the element
9. Given n elements to be sorted, a that every element in a subarray is
______ is a binary tree in which each compared to
non-leaf represents a comparison
between two elements and each leaf 5. For comparison-based sorts,
represents a sorted sequence of the n averageTime(n) is BigOmega
elements. (__________).
CONCEPT EXERCISES
11.1 Trace the execution of each of the six sort methods—Insertion Sort, Selection Sort, Bubble Sort, Merge Sort,
Quick Sort, and Radix Sort—with the following array of values:
10 90 45 82 71 96 82 50 33 43 67
11.2 a. For each sort method, rearrange the list of values in Concept Exercise 11.1 so that the minimum number
of element-comparisons would be required to sort the array.
b. For each sort method, rearrange the list of values in Concept Exercise 11.1 so that the maximum number
of element-comparisons would be required to sort the sequence.
11.3 Suppose you want a sort method whose worstTime(n) is linear-logarithmic in n, but requires only linear-in-n
time for an already sorted collection. None of the sorts in this chapter have those properties. Create a sort
method that does have those properties.
Hint: Add a front end to Merge Sort to see if the collection is already sorted.
11.4 For the optimized Quick Sort in Section 11.4.3.1, fin an arrangement of the integers 0 . . . 49 for which the
firs partition will produce a subarray of size 4 and a subarray of size 44. Recall that because the number of
values is greater than 40, the pivot is the “super-median,” that is, the median of the three median-of-threes.
11.5 a. Suppose we have a sort algorithm whose averageTime(n) is linear-logarithmic in n. For example, either
Merge Sort or Quick Sort would qualify as such an algorithm. Let runTime(n) represent the time, in
seconds, for the implementation of the algorithm to sort n random integers. Then we can write:
runTime(n) ≈ k (c) ∗ n ∗ logc n seconds,
where c is a an integer variable and k is a function whose value depends on c. Show that runTime(cn) ≈
runTime(n) ∗(c + c/ logc n).
b. Use the technique in Concept Exercise 11.5.a to estimate runTime(200000) if runTime(100000) = 10.0
seconds.
11.6 Show that seven comparisons are sufficien to sort any collection of f ve elements.
Hint: Compare the firs and second elements. Compare the third and fourth elements. Compare the two
larger elements from the earlier comparisons. With three comparisons, we have an ordered chain of three
elements, with the fourth element less than (or equal to) one of the elements in the chain. Now compare the
fift element to the middle element in the chain. Complete the sorting in three more comparisons. Note that
ceil(log2 5!) = 7, so some collections of fiv elements cannot be sorted with 6 comparisons.
11.7 Show that log2 n! >= n/2 log2 (n/2) for any positive integer n.
Hint: For any positive integer n,
n
n/2
n! = i>= (n/2) = (n/2)n/2
i =1 i =1
11.8 Show how Quick Sort’s partitioning can be used to develop a method, median, that f nds the median of an
array of int values. For the method median, averageTime(n) must be linear in n.
Hint: Suppose we want to f nd the median of x [0 . . . 10000]. Of course, if we Quick Sort the array, the
median would be in x [5000], but then averageTime(n) would be linear-logarithmic in n. To get an idea of
how to proceed, let’s say that the fi st partition yields a left subarray x [0 . . . 3039] and a right subarray x
[3055 . . . 10000], with copies of the pivot in x [3040 . . . 3054]. Since every int value in the left subarray is
496 C H A P T E R 11 Sorting
less than every int value in the right subarray, which subarray must contain the median? The other subarray
can be ignored from then on, so the array is not completely sorted.
11.9 Consider the following, consecutive improvements to Insertion Sort:
a. Replace the call to the method swap with in-line code:
public static void insertionSort (int[ ] x)
{
int temp;
b. Notice that in the inner loop in part a, temp is repeatedly assigned the original value of x [i]. For
example, suppose the array x has
32 46 59 80 35
and j starts at 4. Then 35 hops its way down the array, from index 4 to index 1. The only relevant
assignment from temp is that last one. Instead, we can move the assignments to and from temp out of
the inner loop:
int temp,
k;
Will these changes affect the estimates for worstTime(n) and averageTime(n)?
11.10 If x is an array, Arrays.sort (x) can be called. Will x be Merge Sorted or Quick Sorted? How is the
determination made?
11.11 Show how Merge Sort can be used to sort an array of primitives with the help of the wrapper classes.
11.12 The Java Collection Framework’s version of Quick Sort can be applied only to an array of a primitive type,
such as int or double. Exactly what would have to be changed to create a Quick Sort method that could
be applied to an array of objects?
11.13 If Merge Sort is applied to a collection with 25 elements, what are the values of the index arguments for the
firs two recursive calls?
11.14 Give an example to show that Selection Sort is not a stable sort.
Hint: you need only three elements.
Programming Exercises 497
PROGRAMMING EXERCISES
11.1 For Concept Exercise 11.9, conduct a timing experiment to estimate the run-time effect of the changes made.
11.2 In the Java Collections Framework version of Quick Sort, special care is taken during partitioning to make
sure that the pivot, and elements equal to the pivot, are not in either of the subarrays created. Estimate—in
percentage terms—how much faster Quick Sort would run, on average, if this special care were not taken.
Conduct a timing experiment to test your hypothesis.
11.3 In the med3 method, replace the two applications of the conditional operator with if statements.
11.4 For the original version of Quick Sort in Section 11.4.3, replace the inner-loop conditions from
while (x [b] < v) and while (x [c] > v)
to
while (b <= c && x [b] <= v) and while (c >= b && x [c] >= v)
Create a small program method to apply this version of Quick Sort to the following array of int values:
46 59
Explain the results.
11.5 Develop a version of Radix Sort to sort an array of String objects. You may assume that each String
object contains only ASCII characters, and that the maximum size of any String object is 30. Use JUnit to
test your radixSort method.
Hint: Instead of the quotient variable, use the charAt method in the String class.
11.6 Modify the radixSort method in Section 11.5 to use an ArrayList instead of an array.
Hint: Start with
ArrayList<LinkedList<Integer>> lists = new ArrayList<LinkedList<Integer>>(RADIX);
For convenience, you may assume that each name will have a middle name.
Suppose the fil persons.dat consists of the following:
System Test 1:
Please enter the path for the f le to be sorted.
persons.dat
The f le persons.dat has been sorted.
The f le persons.dat will now consist of
For a larger system test, randomly generated, use the same name for each person. The social security numbers
will be randomly generated ints in the range 0 . . . 999999999. For example, part of the fil might have
aaa 238749736
aaa 701338476
aaa 408955917
Use unit testing to increase your confid nce in the correctness of your methods.
Hint: This would be a fairly simple problem if we could be certain that the entire f le would f t in main memory.
Unfortunately, this is not the case. Suppose we want to sort a large f le of objects from the class Person. For
specific ty, we assume that an object in the Person class occupies 50 bytes and that the maximum storage for an
array is 500,000 bytes. So the maximum size of an array of Person objects is 10,000.
We start by reading in the fi e of persons, in blocks of k persons each. Each block is Merge Sorted and
stored, in an alternating fashion, on one of two temporary f les: leftTop and leftBottom. Figure 11.20 illustrates
the effect of this firs stage in f le sorting.
We then go through an alternating process which continues until all of the elements are sorted and in a single
fil . The temporary f les used are leftTop, leftBottom, and rightBottom; personsFile itself plays the role of rightTop.
At each stage, we merge a top and bottom pair of f les, with the resulting double-sized blocks stored alternately
on the other top and bottom pair. The code for merging sorted blocks in two f les into sorted, double-sized blocks
in another f le is essentially what was done—using subarrays instead of fil blocks—at the end of Merge Sort.
Here is that code
// Merge sorted subarrays in src into dest.
for (int i = low, p = low, q = mid; i < high; i++) {
Programming Exercises 499
Because the averageTime(n) is optimal, namely linear-logarithmic in n, a sorting method such as this is often used
for a system sort utility.
leftTop personsFile
… 5 3 1 1 2 3 4 5 ...
Merge Sort
leftBottom
… 6 4 2
FIGURE 11.20 The f rst stage in fi e sorting: each of the unsorted blocks in personsFile is Merge Sorted and
stored in leftTop or leftBottom
leftTop personsFile
… 5 3 1 1&2 5&6 ...
merge
leftBottom rightBottom
… 6 4 2 3&4 7&8 ...
FIGURE 11.21 The f rst merge pass in fil sorting. The f les leftTop and leftBottom contain sorted blocks,
and personsFile and rightBottom contain double-sized sorted blocks
This page intentionally left blank
Tree Maps and Tree Sets CHAPTER 12
We begin this chapter by introducing another kind of balanced binary tree: the red-black tree. Red-black
trees provide the underpinning for two extremely valuable classes: the TreeMap class and the TreeSet
class, both of which are in the Java Collections Framework. Each element in a TreeMap object has two
parts: a key part—by which the element is compared to other elements—and a value part consisting of
the rest of the element. No two elements in a TreeMap object can have the same key. A TreeSet object
is a TreeMap object in which all the elements have the same value part. There are applications of both
the TreeMap class (a simple thesaurus) and the TreeSet class (a spell-checker). TreeMap objects and
TreeSet objects are close to ideal: For inserting, removing and searching, worstTime(n) is logarithmic
in n.
CHAPTER OBJECTIVES
1. Be able to define what a red-black tree is, and be able to distinguish between a red-black tree
and an AVL tree.
2. Understand the Map interface and the overall idea of how the TreeMap implementation of the
Map interface is based on red-black trees.
3. Compare TreeMap and TreeSet objects.
1 Equivalently, we could defin the rule in terms of paths from the root element to an empty subtree, because an element with one child also
has an empty subtree, and a leaf has two empty subtrees. When this approach is taken, the binary search tree is expanded to include a special
kind of element, a stub leaf, for each such empty subtree.
501
502 C H A P T E R 12 Tree Maps and Tree Sets
30
5 45
2 9 40 50
41
Note that one of the paths is to the element 40, which has one child. So the paths described are not
necessarily to a leaf.
A red-black tree is a binary search tree that is empty or in which the root element is colored black ,
every other element is colored red or black and the following properties are satisfied
Red Rule: If an element is colored red, none of its children can be colored red.
Path Rule: The number of black elements must be the same in all paths from the root element to elements
with no children or with one child.
For example, Figure 12.1 shows a red-black tree in which the elements are values of Integer
objects and colored red or black
Observe that this is a binary search tree with a black root. Since no red element has any red children,
the Red Rule is satisfied Also, there are two black elements in each of the f ve paths (one path ends at
40) from the root to an element with no children or one child, so the Path Rule is satisfied In other words,
the tree is a red-black tree.
The tree in Figure 12.2 is not a red-black tree even though the Red Rule is satisfie and every path
from the root to a leaf has the same number of black elements. The Path Rule is violated because, for
example, the path from 70 to 40 (an element with one child) has three black elements, but the path from
70 to 110 has four black elements. That tree is badly unbalanced: most of its elements have only one child.
The Red and Path rules preclude most single children in red-black trees. In fact, if a red element has any
children, it must have two children and they must be black. And if a black element has only one child,
that child must be a red leaf.
The red-black tree in Figure 12.1 is fairly evenly balanced, but not every red-black tree has that
characteristic. For example, Figure 12.3 shows one that droops to the left.
30
5 45
2 9 40 50
41
70
60 80
50 90
10 100
40 130
20 120
30 110
50
30 90
20 40
10
You can easily verify that this is a black-rooted binary search tree and that the Red Rule is satisfied
For the Path Rule, there are exactly two black elements in any path from the root to an element with no
children or with one child. That is, the tree is a red-black tree. But there are limits to how unbalanced
a red-black tree can be. For example, we could not hang another element under element 10 without re-
balancing the tree. For if we tried to add a red element, the Red Rule would no longer be satisfied And
if we tried to add a black element, the Path Rule would fail.
If a red-black tree is complete, with all black elements except for red leaves at the lowest level, the
height of that tree will be minimal, approximately log2 n. To get the maximum height for a given n, we
would have as many red elements as possible on one path, and all other elements black. For example,
Figure 12.3 contains one such tree, and Figure 12.4 contains another. The path with all of the red elements
will be about twice as long as the path(s) with no red elements. These trees lead us to hypothesize that
the maximum height of a red-black tree is less than 2 log2 n.
50
30 90
20 40
80 131
60 85 100 150
140 160
135
bushiness leads us to believe that a red-black tree is balanced, that is, has height that is logarithmic in n,
even in the worst case. Compare that with the worst-case height that is linear in n for a binary search tree.
As shown in Example A2.6 of Appendix 2,
The height of a red-black tree is always logarithmic in n, the size of the tree.
How do red-black trees compare to AVL trees? The height of an AVL tree is also logarithmic in n. The
definitio of a red-black tree is slightly more “relaxed” than the definitio of an AVL tree. So any AVL tree
can be colored to become a red-black tree, but the converse is not true (see Concept Exercises 12.6 and
12.7). That is, red-black trees can have larger heights than AVL trees with the same number of elements. It
can be shown (see Weiss [2002]) that the average height of an AVL tree with n elements is, approximately,
1.44 log2 n, versus 2 log2 n for a red-black tree. For example, if n is one million, the average height of an
AVL tree with n elements is about 29, and the average height of a red-black tree with n elements is about 40.
In Section 12.2, we introduce the Map interface, and in Section 12.3, a class that implements the Map
interface. That class, the TreeMap class, is based on a red-black tree, and is part of the Java Collections
Framework. The developers of the framework found that using a red-black tree for the underlying structure
of the TreeMap class provided slightly faster insertions and removals than using an AVL tree.
2 Recall, from Chapter 4, that a collection is an object that is composed of elements. A collection is not necessarily a Collection object,
that is, a collection need not implement the Collection interface. For example, an array is a collection but not a Collection object.
12.2 The Map Interface 505
example, we could have the following map, in which all of the social security numbers are unique, but
two elements have the same name:
A dictionary is another example of a map. The key is the word being define and the value consists of the
definition punctuation, and etymology. The term dictionary is sometimes used as a synonym for “map”.
In this sense, a dictionary is simply a collection of key-value pairs in which there are no duplicate keys.
The Java Collections Framework has a Map interface that provides method headings for the abstract-
data-type map. The Map interface does not extend the Collection interface because many Map methods
are oriented towards the key-value relationship. In fact, the type parameters are K (for the key class) and V
(for the value class). But the Map interface has some standard methods such as size, equals, and clear.
Here are specification for several of the other methods in the Map interface—no time estimates are given
because different implementations have substantially different estimates:
Note 1: The phrase “this map ” refers to an object in a class that implements the Map interface.
Note 2: The put method is somewhat more versatile than an add method because the put method
handles replacement—of the values associated with a given key—as well as insertion of a new
key-value pair.
2. The containsKey method
/**
* Determines if this Map object contains a mapping for a specified key.
506 C H A P T E R 12 Tree Maps and Tree Sets
*
* @param key - the specified key.
*
* @return true – if there is at least one mapping for the specified key in
* this Map object; otherwise, return false.
*
* @throws ClassCastException – if key cannot be compared with the keys
* currently in the map.
*
* @throws NullPointerException – if key is null and this Map object uses
* the natural order, or the comparator does not allow null keys.
*
*/
boolean containsKey (Object key);
Note: The value null might also be returned if the given key maps to null . To distinguish
between this situation and the no-matching-key situation, the containsKey method can be used.
For example, if persons is an object in a class that implements the Map interface and key is an
object in the key class, we can do the following:
12.2 The Map Interface 507
Note: Recall, from Chapter 10, that a set is a collection of elements in which duplicates are not
allowed. We can view a Map object as just a set of key-value pairs. The advantage to this view is
that we can then iterate over the Map object, and the elements returned will be the key-value pairs
of the Map object. Why is this important? The Map interface does not have an iterator() method,
so you cannot iterate over a Map object except through a view. And the Map interface has a public,
nested Entry interface that has getKey() and getValue() methods.
For example, suppose that persons is an instance of a class that implements the Map interface, and
that the element class has a social security number as the (Integer) key and a name as the (String)
value. Then we can print out the name of each person whose social security number begins with 555 as
follows:
for (Map.Entry<Integer, String> entry : persons.entrySet())
if (entry.getKey() / 1000000 == 555)
System.out.println (entry.getValue());
508 C H A P T E R 12 Tree Maps and Tree Sets
There are also keySet() and values() methods that allow iterating over a Map viewed as a set of keys
and as a collection of values, respectively. The term “collection of values” is appropriate instead of “set
of values” because there may be duplicate values.
Section 12.3 has an implementation of the Map interface, namely, the TreeMap class. Chapter 14
has another implementation, the HashMap class. The TreeMap class, since it is based on a red-black tree,
boasts logarithmic time, even in the worst case, for insertions, removals, and searches. The HashMap
class’s claim to fame is that, on average, it takes only constant time for insertions, removals and searches.
But its worst-case performance is poor: linear in n.
The TreeMap class actually implements a slight extension of the Map interface, namely, the Sort
edMap interface. The SortedMap interface mandates that for any instance of any implementing class, the
elements will be in “ascending” order of keys (for example, when iterating over an entry-set view). The
ordering is either the natural ordering—if the key class implements the Comparable interface—or an
ordering supplied by a comparator. Here are several new methods:
/**
* Returns the comparator for this sorted map, or null, if the map uses the
* keys’ natural ordering. The comparator returned, if not null, must implement
* the Comparator interface for elements in any superclass of the key class.
*
* @return the comparator for this sorted map, or null, if this sorted
* map uses the keys’ natural ordering.
*
*/
Comparator<? super K> comparator();
/**
* Returns the first (that is, smallest) key currently in this sorted map.
*
* @return the first (that is, smallest) key currently in this sorted map.
*
* @throws NoSuchElementException, if this sorted map is empty.
*
*/
K firstKey();
/**
* Returns the last (that is, largest) key currently in this sorted map.
*
* @return the last (that is, largest) key currently in this sorted map.
*
* @throws NoSuchElementException, if this sorted map is empty.
*
*/
K lastKey();
12.3 The TreeMap Implementation of the SortedMap Interface 509
System.out.println (students);
} // class TreeMapExample
The reason that the students object is alphabetically ordered by student names is that the key class
is String. As we saw in Section 10.1.1, the String class implements the Comparable interface
with a compareTo method that reflect an alphabetical ordering. For applications in which the “natu-
ral” ordering—through the Comparable interface—is inappropriate, elements can be compared with the
Comparator interface, discussed in Section 11.3. In the TreeMap class, there is a special constructor:
/**
* Initializes this TreeMap object to be empty, with keys to be compared
* according to a specified Comparator object.
*
* @param c – the Comparator object by which the keys in this TreeMap
* object are to be compared.
*
*/
public TreeMap (Comparator<? super K> c)
We can implement the Comparator interface to override the natural ordering. For example, suppose we
want to create a TreeMap of Integer keys (and Double values) in decreasing order. We cannot rely on
the Integer class because that class implements the Comparable interface with a compareTo method
that reflect increasing order. Instead, we create a class that implements the Comparator interface by
reversing the meaning of the compareTo method in the Integer class:
public class Decreasing implements Comparator<Integer>
{
/**
* Compares two specified Integer objects.
*
* @param i1 – one of the Integer objects to be compared.
* @param i2 – the other Integer object.
*
* @return the value of i2’s int – the value of i1’s int.
*
*/
public int compare (Integer i1, Integer i2)
{
return i2.compareTo (i1);
} // method compare
} // class Decreasing
Notice that the Decreasing class need not specify a type parameter since that class is implementing the
Comparator interface parameterized with Integer.
12.3 The TreeMap Implementation of the SortedMap Interface 511
For another example, here is the ByLength class from Section 11.3:
public class ByLength implements Comparator<String>
{
/**
* Compares two specified String objects lexicographically if they have the
* same length, and otherwise returns the difference in their lengths.
*
* @param s1 – one of the specified String objects.
* @param s2 – the other specified String object.
*
* @return s1.compareTo (s2) if s1 and s2 have the same length;
* otherwise, return s1.length() – s2.length().
*
*/
public int compare (String s1, String s2)
{
int len1 = s1.length(),
len2 = s2.length();
if (len1 == len2)
return s1.compareTo (s2);
return len1 – len2;
} // method compare
} // class ByLength
The following class utilizes the ByLength class with a TreeMap object in which the keys are
words—stored in order of increasing word lengths—and the values are the number of letters in the words.
import java.util.*;
System.out.println (wordLengths);
} // method run
} // class TreeMapByLength
Now that we have seen a user’s view of the TreeMap class, Sections 12.3.1 and 12.3.2 will spend a little
time looking “under the hood” at the f elds, the embedded Entry class and the method definitions Then
Section 12.4 will present an application of the TreeMap class: creating a thesaurus.
To fla illegal modification (see Appendix 1) to the structure of the tree during an iteration:
private transient int modCount = 0;
The only other fiel in the TreeMap class is used for comparing elements:
private Comparator comparator = null;
This fiel gives a user of the TreeMap class a choice. If the user wants the “natural” ordering, such as
alphabetical order for String keys or increasing order for Integer keys, the user creates a TreeMap
instance with the default constructor. Then the keys’ class must implement the Comparable interface, so
comparisons are based on the compareTo method in the key class. Alternatively, as we saw in Section
12.3, a user can override the “natural” ordering by supplying a Comparator object in the constructor call:
TreeMap<String, Integer> wordLengths =
new TreeMap<String, Integer>(new ByLength());
The designers of the Java Collections Framework’s TreeMap class chose a red-black tree as the underlying
structure because it had a slight speed advantage over an AVL tree for insertions, removals, and searches.
We will now start to get into the red-black aspects of the TreeMap class. There are two constant identifier
that supply the colors:
private static final boolean RED = false;
These constant identifier apply, not to the tree as a whole, but to the Entry objects in the tree. The
Entry class, embedded in the TreeMap class, is similar to the Entry class that is embedded in the
BinarySearchTree class, except that the TreeMap class’s Entry class has key and value field (instead
of just an element field) and a color field
12.3 The TreeMap Implementation of the SortedMap Interface 513
Every Entry object’s color fiel is initialized to BLACK. But during an insertion, the inserted Entry
object is colored RED; this simplifie the maintenance of the Path Rule. The Entry class also has a
default-visibility constructor to initialize the key, value, and parent fields And there are a few public
methods, such as getKey() and getValue(), which are useful in iterating over the entries in a TreeMap
object after a call to the entrySet(), keySet(), or values(), methods.
To finis up our overview of the TreeMap implementation of the Map interface, we consider a few
method definition in Section 12.3.2.
Then, just as we did in the add method of the AVLTree class, we work our way down the tree until we
fin where key is or belongs. Except that the put method:
• splits off the Comparator case from the Comparable case;
• returns t.setValue (value) if key and t.key are the same, and then value replaces t.value
and the old value is returned;
• after an insertion, calls a special method, fixAfterInsertion, to re-color and rotate the tree if the
Red Rule is no longer satisfie (the Path Rule will still be satisfie because the newly inserted entry
is colored RED at the start of fixAfterInsertion).
Here is the complete definition
public V put (K key, V value)
{
Entry<K, V> t = root;
if (t == null)
{
root = new Entry<K,V>(key, value, null);
size = 1;
514 C H A P T E R 12 Tree Maps and Tree Sets
modCount++;
return null;
}
int cmp;
Entry<K,V> parent;
Notice that the fixAfterInsertion method is not called when an insertion is made at the root. So root
remains BLACK in that case.
The definitio of the fixAfterInsertion method is not intuitively obvious. In fact, even if you
study the code, it makes no sense. Red-black trees were originally developed in Bayer [1972]. The algo-
rithms for inserting and removing in these trees, called “2-3-4 trees,” were lengthy but the overall strategy
was easy to understand. Shorter but harder to follow methods were supplied when the red-black coloring
was imposed on these structures in Guibas [1978].
Lab 19 investigates the fixAfterInsertion method in some detail.
You are now ready for Lab 19: The fixAfterInsertion Method
In Section 12.1.1, we stated that the height of any red-black tree is logarithmic in n, the number
of elements in the tree. So for the part of the put method that find where the element is to be inserted,
worstTime(n) is logarithmic in n. Then a call to fixAfterInsertion is made, for which worstTime(n)
is also logarithmic in n. We conclude that, for the entire put method, worstTime(n) is logarithmic in n.
The remove method, only slightly changed from that of the BinarySearchTree class, gets the
Entry object corresponding to the given key and then deletes that Entry object from the tree:
public V remove (K key)
{
Entry<K, V> p = getEntry (key);
if (p == null)
return p;
V oldValue = p.value;
deleteEntry (p);
return oldValue;
} // method remove
The getEntry method is almost identical to the BinarySearchTree class’s getEntry method
except—as we saw with the put method—that there is a split of the Comparator and Comparable
cases.
The deleteEntry method mimics the BinarySearchTree class’s (and AVLTree class’s) delete
Entry method, except now we must ensure that the Path Rule is still satisfie after the deletion. To see
how we might have a problem, suppose we want to delete the entry with key 50 from the TreeMap object
in Figure 12.5. The value parts are omitted because they are irrelevant to this discussion, and we pretend
that the keys are of type int ; they are actually of type reference-to-Integer.
p 50
30 100
20 40 70 110
FIGURE 12.5 A TreeMap (with value parts not shown) from which 50 is to be deleted
516 C H A P T E R 12 Tree Maps and Tree Sets
70
30 100
20 40 p 70 110
FIGURE 12.6 An intermediate stage in the deletion of 50 from the TreeMap object of Figure 12.5
Just as we did with the BinarySearchTree class’s deleteEntry method, the successor’s key (namely,
70) replaces 50 and then p references that successor. See Figure 12.6 above.
If we were performing a BinarySearchTree -style deletion, we would simply unlink p’s Entry
object and be done. But if we unlink that Entry object from the TreeMap object of Figure 12.5, the Path
Rule would be violated. Why? There would be only one black element in the path from the root to 100
(an element with one child), and two black elements in the path from the root to the leaf 110. To perform
any necessary re-coloring and re-structuring, there is a fixAfterDeletion method.
Here is the definitio of the deleteEntry method, which is very similar to the definitio of the
deleteEntry method in both the BinarySearchTree and AVLTree classes:
// Fix replacement
if (p.color == BLACK)
fixAfterDeletion(replacement);
}
else if (p.parent == null)
{ // return if we are the only node.
root = null;
}
else
{ // No children. Use self as phantom replacement and unlink.
if (p.color == BLACK)
fixAfterDeletion(p);
if (p.parent != null)
{
if (p == p.parent.left)
p.parent.left = null;
else if (p == p.parent.right)
p.parent.right = null;
} // non-null parent
} // p has no children
} // method deleteEntry
The fixAfterDeletion method, the subject of Lab 20, has even more cases than the fixAfterInser
tion method.
You are now ready for Lab 20: The fixAfterDeletion Method
The ch12 directory on the book’s website includes an applet that will help you to visualize insertions in
and removals from a red-black tree:
In Section 12.4, we develop an application of the TreeMap class to print out the synonyms of given
words.
The problem we want to solve is this: given a thesaurus f le and a fil of words whose synonyms are
requested, print the synonym of each word entered to a fil
Analysis If there is no file for either path input, or if the output file path is illegal, an error message should
be printed, followed by a re-prompt. The thesaurus file will be in alphabetical order. For each word entered
from the requests file, the synonyms of that word should be output to the synonyms file, provided the word’s
synonyms are in the thesaurus file. Otherwise, a synonyms-not-found message should be printed. In the
following system test, assume that the thesaurus shown earlier in this section is in the file ‘‘thesaurus.in1’’,
and ‘‘requests.in1’’ consists of
one
two
close
We will create two classes to solve this problem: a Thesaurus class to store the synonym information, and
a ThesaurusUser class to handle the input/output.
/**
* Adds a specified line of synonyms to this Thesaurus object.
* The worstTime(n) is O(log n).
*
* @param line – the specified line of synonyms to be added to this
* Thesaurus object.
* @throws NullPointerException – if line is null.
*
*/
public void add (String line)
/**
* Finds the LinkedList of synonyms of a specified word in this Thesaurus.
* The worstTime(n) is O(log n).
*
* @param word – the specified word, whose synonyms are to be
* returned.
*
* @return the LinkedList of synonyms of word.
*
* @throws NullPointerException – if word is null.
*
*/
public LinkedList<String> getSynonyms (String word)
/**
* Returns a String representation of this Thesaurus object.
* The worstTime(n) is O(n).
*
* @return a String representation of this Thesaurus object in the
* form {word1=[syn11, syn12,...], word2=[syn21, syn22,...],...}.
*
*/
public String toString()
Here are two tests in the ThesaurusTest class, which has thesaurus (an instance of the Thesaurus
class) as a f eld:
@Test (expected = NullPointerException.class)
public void testAddLineNull()
{
thesaurus.add (null);
} // method testAddLineNull
@Test
public void testAdd1()
{
thesaurus.add ("therefore since because ergo");
520 C H A P T E R 12 Tree Maps and Tree Sets
The implementation of the Thesaurus class is fairly straightforward; most of the work is done in the put
and get methods of the TreeMap class. The Thesaurus class’s add method tokenizes the line, saves the
firs token as the key, and saves the remaining tokens in a LinkedList object as the value.
Here are the method definition and time estimates:
public Thesaurus()
{
thesaurusMap = new TreeMap<String, LinkedList<String>>();
} // default constructor
if (sc.hasNext())
{
String word = sc.next();
while (sc.hasNext())
synonymList.add (sc.next());
thesaurusMap.put (word, synonymList);
} // if
} // method add
For the put method in the TreeMap class, worstTime(n) is logarithmic in n, and so that is also the time
estimate for the add method. Note that the while loop takes constant time because it is independent of
n, the number of lines in the thesaurus.
Here is the one-line getSynonyms method:
public LinkedList<String> getSynonyms (String word)
{
return thesaurusMap.get (word);
} // method getSynonyms
For the getSynonyms method, worstTime(n) is logarithmic in n because that is the time estimate for the
TreeMap class’s get method.
The definitio of the toString() method is just as simple:
public String toString()
{
return thesaurusMap.toString();
} // method toString
12.4 Application of the TreeMap Class: a Simple Thesaurus 521
For this method, worstTime(n) is linear in n —the iteration over the entries accesses a key and value in
constant time.
/**
* Outputs the synonyms of the words in the file scanned to a specified file.
* The worstTime(n, m) is O(m log n), where n is the number of lines in
* the thesaurus, and m is the number of words in the file scanned.
*
* @param thesaurus - the thesaurus of words and synonyms.
* @param requestFileScanner - the Scanner over the file that holds the
* words whose synonyms are requested.
* @param synonymPrintWriter - the PrintWriter for the file that will hold
* the synonyms of the words in the request file.
*
*/
public void findSynonyms (Thesaurus thesaurus, Scanner requestFileScanner,
PrintWriter synonymPrintWriter)
The run method is not testable because it deals mainly with end-user input and output. The findSynonyms
method is testable, and here is one of those tests (user, of type ThesaurusUser and line, of type
String, are f elds in ThesaurusUserTest):
@Test
public void testProcessFilesNormal() throws IOException
{
Scanner thesaurusFileScanner = new Scanner (new File ("thesaurus.in1")),
requestFileScanner = new Scanner (new File ("requests.in1"));
line = sc.nextLine();
assertEquals ("Here are the synonyms of confined: [close, cramped]", line);
line = sc.nextLine();
assertEquals ("Here are the synonyms of near: [close]", line);
line = sc.nextLine();
assertEquals ("x does not appear in the thesaurus.", line);
line = sc.nextLine();
assertEquals ("Here are the synonyms of singular: [one, unique]", line);
} // method testProcessFilesNormal
ThesaurusUser
+ run()
+ findSynonyms (thesaurus:Thesaurus,
requestFileScanner: Scanner,
synonymPrintWriter: PrintWriter)
Thesaurus
+ Thesaurus()
+ toString(): String
The ThesaurusUser’s run method scans a fil path (and keeps scanning until a legal fil path is scanned
in), adds each line in the fil to the thesaurus, and then f nds the synonyms of each word in a f le whose
path is scanned in:
public void run()
{
final String THESAURUS_FILE_PROMPT =
"\nPlease enter the path for the thesaurus file: ";
PrintWriter synonymPrintWriter;
String thesaurusFilePath,
requestFilePath,
synonymFilePath;
while (!pathsOK)
{
try
{
System.out.print (THESAURUS_FILE_PROMPT);
thesaurusFilePath = keyboardScanner.nextLine();
524 C H A P T E R 12 Tree Maps and Tree Sets
System.out.print (REQUEST_FILE_PROMPT);
requestFilePath = keyboardScanner.nextLine();
requestFileScanner = new Scanner (new File (requestFilePath));
System.out.print (SYNONYM_FILE_PROMPT);
synonymFilePath = keyboardScanner.nextLine();
synonymPrintWriter = new PrintWriter (new BufferedWriter
(new FileWriter (synonymFilePath)));
pathsOK = true;
findSynonyms (thesaurus, requestFileScanner, synonymPrintWriter);
synonymFileWriter.close();
} // try
catch (IOException e)
{
System.out.println (e);
} // catch
} // while !pathsOK
} // method run
Intuitively, since it takes logarithmic-in-n time for each insertion into the thesaurus, it should take linear-
logarithmic-in-n time for n insertions. But the f rst insertion is into an empty tree, the second insertion
is into a tree with one element, and so on. Specifically for i = 1, 2, . . . , n, it takes approximately log2 i
loop iterations to insert the i th element into a red-black tree. To insert n elements, the total number of
iterations is, approximately,
n
log2 i = log2 n! //sum of logs = log of product
i =2
≈ n log2 n //by the logarithmic form of Stirling’s
//approximation of factorials (see Zwilliger [2000])
In other words, our intuition is correct, and worstTime(n) is linear-logarithmic in n for f lling in the
thesaurus. To estimate the worst time for the entire run method, we firs need to develop and then
estimate the worst time for the findSynonyms method, because findSynonyms is called by the run
method.
The findSynonyms method consists of a read-loop that continues as long as there are words left in
the requests f le. For each word scanned, the synonyms of that word are fetched from the thesaurus and
output to the synonyms f le; an error message is output if the word is not in the thesaurus. Here is the
method definition
public void findSynonyms (Thesaurus thesaurus, Scanner requestFileScanner,
PrintWriter synonymPrintWriter)
{
final String WORD_NOT_FOUND_MESSAGE =
" does not appear in the thesaurus.";
12.5 The TreeSet Class 525
String word;
LinkedList<String> synonymList;
while (requestFileScanner.hasNext())
{
word = requestFileScanner.next();
synonymList = thesaurus.getSynonyms (word);
if (synonymList == null)
synonymPrintWriter.println (word + WORD_NOT_FOUND_MESSAGE);
else
synonymPrintWriter.println (SYNONYM_MESSAGE + word +
": " + synonymList);
} // while
} // method findSynonyms
To estimate how long the findSynonyms method takes, we must take into account the number of words
entered from the requests f le as well as the size of thesaurusMap. Assume there are m words entered
from the requests f le. (We cannot use n here because that represents the size of thesaurusMap.) Then
the while loop in findSynonyms is executed O(m) times. During each iteration of the while loop,
there is a call to the get method in the TreeMap class, and that call takes O(log n) time. So worstTime(n,
m) is O(m log n); in fact, to utilize the notation from Chapter 3, worstTime(n, m) is (m log n) because
m log n provides a lower bound as well as an upper bound on worstTime(n, m). The worstTime function
has two arguments because the total number of statements executed depends on both n and m.
We can now estimate the worst time for the run method. For the loop to fil in the thesaurus,
worstTime(n) is linear-logarithmic in n, and for the call to findSynonyms, worstTime(n, m) is (m log n).
We assume that the number of requests will be less than the size of thesaurusMap. Then for the run
method, worstTime(n, m) is (n log n), that is, linear logarithmic in n.
The next topic in this chapter is the TreeSet class, which is implemented as a TreeMap in which
each Entry object has the same dummy value-part.
instead of an ArrayList, LinkedList, BinarySearchTree, or array. For those four collections, if the
elements are to be maintained in order from smallest to largest, worstTime(n) is linear in n for insertions
and removals.
How does the TreeSet class compare with the AVLTree class? The TreeSet class is superior
because it is part of the Java Collections Framework. As a result, the class is available to you on any Java
compiler. Also, the methods have been thoroughly tested. Finally, you are not restricted to the “natural”
ordering of elements: You can override that ordering with a comparator.
We already saw most of the TreeSet methods when we studied the BinarySearchTree and
AVLTree classes in Chapter 10.
The following class illustrates both the default constructor and the constructor with a comparator
parameter, as well as a few other methods:
import java.util.*;
mySet.add ("happy");
mySet.add ("always");
mySet.add ("yes");
mySet.add ("serene");
System.out.println (START + mySet);
if (mySet.add ("happy"))
System.out.println ("ooops");
else
12.5 The TreeSet Class 527
int sum = 0;
for (Integer i : scores)
sum += i;
System.out.println (SUM + sum);
} // method run
} // class TreeSetExample
After we take a brief look at the implementation of the TreeSet class, we will return to a user’s view by
developing an application on spell checking.
528 C H A P T E R 12 Tree Maps and Tree Sets
“Navigable” means that the set (or map) can be iterated over from back to front as well as from front to
back.
To explicitly construct a TreeSet object from a given SortedSet object, usually a TreeSet object,
there is a constructor with default visibility (that is, accessible only from within the package java.util):
/**
* Initializes this TreeSet object from a specified NavigableMap object.
*
* @param m – the NavigableMap that this TreeSet object is initialized from.
*
*/
TreeSet<E> (NavigableMap<E, Object> m)
{
this.m = m;
} // constructor with map parameter
Given the TreeSet field and this constructor, we can straightforwardly implement the rest of the TreeSet
methods. In fact, most of the definition are one-liners. For example, here are the definition of the default
constructor, the constructor with a comparator parameter, and the contains, add, and remove methods:
/**
* Initializes this TreeSet object to be empty, with the elements to be
* ordered by the Comparable interface.
*
*/
public TreeSet()
{
this (new TreeMap<E, Object>());
} // default constructor
/**
* Initializes this TreeSet object to be empty, with elements to be ordered
* by a specified Comparator object.
*
12.5 The TreeSet Class 529
/**
* Determines if this TreeSet object contains a specified element.
* The worstTime(n) is O(log n).
*
* @param obj – the specified element sought in this TreeSet object.
*
* @return true – if obj is equal to at least one of the elements in this
* TreeSet object; otherwise, return false.
*
* @throws ClassCastException – if obj cannot be compared to the
* elements in this TreeSet object.
*
*/
public boolean contains (Object obj)
{
return m.containsKey (obj);
} // method contains
/**
* Inserts a specified element where it belongs in this TreeSet object,
* unless the element is already in this TreeSet object.
* The worstTime(n) is O(log n).
*
* @param element – the element to be inserted, unless already there, into
* this TreeSet object.
*
* @return true – if this element was inserted; return false – if this element
* was already in this TreeSet object.
*
*/
public boolean add (E element)
{
return m.put (element, PRESENT) == null;
} // method add
/**
* Removes a specified element from this TreeSet object, unless the
* element was not in this TreeSet object just before this call.
530 C H A P T E R 12 Tree Maps and Tree Sets
Section 12.5.2 has an application of the TreeSet class: developing a spell checker.
Here are the contents of a small dictionary fil called “dictionary.dat”, a small document f le called
“document.dat” and the words in the latter that are not in the former.
// the dictionary file:
a
algorithms
asterisk
coat
equal
he
pied
pile
plus
programs
separate
she
12.5 The TreeSet Class 531
structures
wore
To isolate the spell-checking details from the input/output aspects, we create two classes: SpellChecker
and SpellCheckerUser.
/**
* Inserts a specified word into the dictionary of words.
* The worstTime(n) is O(log n), where n is the number of words in the
* dictionary of words.
*
* @param word - the word to be inserted into the dictionary of words.
*
* @return a String representation of the dictionary.
*
* @throws NullPointerException - if word is null.
*
*/
public String addToDictionary (String word)
/**
* Inserts all of the words in a specified line into the document of words.
* The worstTime(m) is O(log m), where m is the number of (unique) words
532 C H A P T E R 12 Tree Maps and Tree Sets
/**
* Determines all words that are in the document but not in the dictionary.
* The worstTime(m, n) is O(m log n), where m is the number of words
* in the document, and n is the number of words in the dictionary.
*
* @return a LinkedList consisting of all the words in the document that
* are not in the dictionary.
*
*/
public LinkedList<String> compare()
Here are two of the tests in SpellCheckerTest, which has spellChecker (always initialized to an
empty SpellChecker instance) as a f eld:
@Test
public void testAddToDocument1()
{
String actual = spellChecker.addToDocument ("A man, a plan, a canal. Panama!");
assertEquals ("[a, canal, man, panama, plan]", actual);
} // method testAddToDocument1
@Test
public void testSeveralMisspellings1()
{
spellChecker.addToDictionarySet ("separate");
spellChecker.addToDictionarySet ("algorithms");
spellChecker.addToDictionarySet ("equals");
spellChecker.addToDictionarySet ("asterisk");
spellChecker.addToDictionarySet ("wore");
spellChecker.addToDictionarySet ("coat");
spellChecker.addToDictionarySet ("she");
spellChecker.addToDictionarySet ("equals");
spellChecker.addToDictionarySet ("plus");
spellChecker.addToDictionarySet ("he");
spellChecker.addToDictionarySet ("pied");
spellChecker.addToDictionarySet ("a");
spellChecker.addToDictionarySet ("pile");
spellChecker.addToDictionarySet ("programs");
spellChecker.addToDictionarySet ("structures");
12.5 The TreeSet Class 533
The dictionarySet fiel holds the words in the dictionary file The documentSet fiel holds each
unique word in the document f le—there is no purpose in storing multiple copies of any word.
The definition of the default constructor and addToDictionarySet methods hold no surprises:
public SpellChecker()
{
dictionarySet = new TreeSet<String>();
documentSet = new TreeSet<String>();
} // default constructor
The definitio of addToDocumentSet (String line) is slightly more complicated. The line is tok-
enized, with delimiters that include punctuation symbols. Each word, as a token, is converted to lower-case
and inserted into documentSet unless the word is already in documentSet. Here is the definitio (see
Section 0.2.5 of Chapter 0 for a discussion of the useDelimiter method):
public String addToDocument (String line)
{
final String DELIMITERS = "[∧a-zA-Z]+";
String word;
while (sc.hasNext())
{
word = sc.next().toLowerCase();
documentSet.add (word);
} // while line has more tokens
return documentSet.toString();
} // method addToDocument
Let m represent the number of words in documentSet. Each call to the TreeSet class’s add method takes
logarithmic-in-m time. The number of words on a line is independent of m, so for the addToDocument
method, worstTime(m) is logarithmic in m.
534 C H A P T E R 12 Tree Maps and Tree Sets
The compare method iterates through documentSet; each word that is not in dictionarySet is
appended to a LinkedList object of (possibly) misspelled words. Here is the definition
public LinkedList<String> compare()
{
LinkedList<String> misspelled = new LinkedList<String>();
For iterating through documentSet, worstTime(m) is linear in m, and for each call to the TreeSet class’s
contains method, worstTime(n) is logarithmic in n. So for the compare method in the SpellChecker
class, worstTime(n, m) is O(m log n). In fact, worstTime(n, m) is (m log n).
In Chapter 14, we will encounter another class that implements the Set interface: the HashSet
class. In this class, the average time for insertions, removals and searches is constant! So we can re-
do the above problem with HashSet object for dictionarySet and documentSet. No other changes
need be made! For that version of the spell-check project, averageTime(n) would be constant for the
addToDictionary method, and averageTime(m) would be constant for the addToDocument method.
For the compare method, averageTime(m, n) would be linear in m. But don’t sell your stock in TreeSets-
R-Us. For the HashSet version of the SpellChecker class, the worstTime(m, n) for compare, for
example, would be (mn).
SpellCheckerUser
+ run( )
SpellChecker
# dictionarySet: TreeSet<String>
# documentSet: TreeSet<String>
+ SpellChecker()
+ compare(): LinkedList<String>
while (!pathOK)
{
try
{
System.out.print (FILE_PROMPT);
filePath = keyboardScanner.nextLine();
fileScanner = new Scanner (new File (filePath));
pathOK = true;
if (fileType.equals (DICTIONARY))
536 C H A P T E R 12 Tree Maps and Tree Sets
while (fileScanner.hasNext())
spellChecker.addToDictionary (fileScanner.nextLine());
else if (fileType.equals (DOCUMENT))
while (fileScanner.hasNext())
spellChecker.addToDocument (fileScanner.nextLine());
} // try
catch (IOException e)
{
System.out.println (e);
} // catch
} // while
fileType = DOCUMENT;
} // for
LinkedList<String> misspelled = spellChecker.compare();
if (misspelled == null)
System.out.println (ALL_CORRECT);
else
System.out.println (MISSPELLED + misspelled);
} // method run
To create the dictionary, worstTime(n) is linear-logarithmic in n because each of the n words in the
dictionary fil is stored in a TreeSet object, and for each insertion in a TreeSet object, worstTime(n) is
logarithmic in n. By the same reasoning, to create the document, worstTime(m) is linear-logarithmic in m
(the size of the document f le). Also, worstTime (m, n) is (m log n) for the call to the SpellChecker
class’s compare method.
We conclude that, for the run method, worstTime (m, n) is linear-logarithmic in max (m, n).
The book’s website includes the SpellCheckerTest class. There is no SpellCheckerUserTest
class because SpellCheckerUser’s main and run methods are untestable.
SUMMARY
A red-black tree is a binary search tree that is empty or identifi d—and a value part, which contains the rest of
in which the root element is colored black, every other the element. The elements are stored in a red-black tree
element is colored either red or black, and for which the in key-ascending order, according to their “natural” order
following two rules hold: (implementing the Comparable interface) or by the
order specifie by a user-supplied Comparator object
1. Red Rule: if an element is colored red, none of its (there is a constructor in which the Comparator object
children can be colored red. is supplied). For the containsKey, get, put, and
2. Path Rule: the number of black elements is the remove methods, worstTime(n) is logarithmic in n.
same in all paths from the root to elements with A TreeSet object is a Collection object in
one child or with no children. which duplicate elements are not allowed and in which
the elements are stored in order (according to the Compa
The height of a red-black tree is always logarithmic in n, rable ordering or a user-supplied Comparator). The
the number of elements in the tree. TreeSet class is implemented in the Java Collections
The Java Collections Framework implements red- Framework as a TreeMap in which each element has
black trees in the TreeMap class. In a TreeMap object the same dummy value-part. For the contains, add,
each element has a key part—by which the element is and remove methods, worstTime(n) is logarithmic in n.
Crossword Puzzle 537
CROSSWORD PUZZLE
2 3
4 5 6
10
www.CrosswordWeaver.com
ACROSS DOWN
7. The two components of each element 1. The TreeMap field used to compare
in a map collection elements (by their keys)
8. In the TreeMap class, the value 2. In the simple thesaurus project, the
returned by the put method when an class that handles input and output
element with a new key is inserted
3. For the containskey, get, put and
9. What a TreeSet object never has remove methods in the TreeMap
class, worstTime(n) is _____ in n.
10. In a red-black tree, what the only child
of a black element must be 4. The type of the constant identifiers
RED and BLACK
CONCEPT EXERCISES
12.1 Construct a red-black tree of height 2 with six elements. Construct a red-black tree of height 3 with six
elements.
12.2 Construct two different red-black trees with the same three elements.
12.3 What is the maximum number of black elements in a red-black tree of height 4? What is the minimum
number of black elements in a red-black tree of height 4?
12.4 It is impossible to construct a red-black tree of size 20 with no red elements. Explain.
12.5 Suppose v is an element with one child in a red-black tree. Explain why v must be black and v ’s child must
be a red leaf.
12.6 Construct a red-black tree that (when the colors are ignored) is not an AVL tree.
12.7 Guibas and Sedgewick [1978] provide a simple algorithm for coloring any AVL tree into a red-black tree:
For each element in the AVL tree, if the height of the subtree rooted at that element is an even integer and
the height of its parent’s subtree is odd, color the element red; otherwise, color the element black.
For example, here is an AVL tree from Chapter 10:
50
20 80
10 70 100
91 103
In this tree, 20’s subtree has a height of 1, 80’s subtree has a height of 2, 10’s subtree has a height of 0
and 70’s subtree has a height of 0. Note that since the root of the entire tree has no parent, this algorithm
guarantees that the root will be colored black. Here is that AVL tree, colorized to a red-black tree:
50
20 80
10 70 100
91 103
Create an AVL tree of height 4 with min4 elements (that is, the minimum number of elements for an AVL
tree of height 4), and then colorize that tree to a red-black tree.
Programming Exercises 539
12.8 Suppose, in the definitio of red-black tree, we replace the Path Rule with the following:
Pathetic Rule: The number of black elements must be the same in all paths from the root element
to a leaf.
a. With this new def nition, describe how to construct a red-black tree of 101 elements whose height is 50.
b. Give an example of a binary search tree that cannot be colored to make it a red-black tree (even with this
new definition)
12.9 Show the effect of making the following insertions into an initially empty TreeSet object:
PROGRAMMING EXERCISES
12.1 Suppose we are given the name and division number for each employee in a company. There are no duplicate
names. We would like to store this information alphabetically, by name. For example, part of the input might
be the following:
Misino, John 8
Nguyen, Viet 14
Panchenko, Eric 6
Dunn, Michael 6
Deusenbery, Amanda 14
Taoubina, Xenia 6
Deusenbery, Amanda 14
Dunn, Michael 6
Misino, John 8
Nguyen, Viet 14
Panchenko, Eric 6
Taoubina, Xenia 6
How should this be done? TreeMap? TreeSet? Comparable? Comparator? Develop a small unit-test
to test your hypotheses.
540 C H A P T E R 12 Tree Maps and Tree Sets
12.2 Re-do Programming Exercise 12.1, but now the ordering should be by increasing division numbers, and
within each division number, by alphabetical order of names. For example, part of the input might be the
following:
Misino, John 8
Nguyen, Viet 14
Panchenko, Eric 6
Dunn, Michael 6
Deusenbery, Amanda 14
Taoubina, Xenia 6
Dunn, Michael 6
Panchenko, Eric 6
Taoubina, Xenia 6
Misino, John 8
Deusenbery, Amanda 14
Nguyen, Viet 14
How should this be done? TreeMap? TreeSet? Comparable? Comparator? Develop a small unit-test
to test your hypotheses.
12.3 Declare two TreeSet objects, set1 and set2, whose elements come from the same Student class.
Each student has a name and grade point average. In set1, the students are in alphabetical order. In set2,
the students are in decreasing order of GPAs. Insert a few students into each set and then print out the set.
Include everything needed for this to work, including the two declarations of TreeSet objects, the insertion
messages, the declaration of the Student class, and any other necessary class(es).
a
algorithms
asterisk
Programming Exercises 541
coat
equal
he
pied
pile
plus
programs
separate
structures
wore
System Test 2:
Please enter the name of the dictionary file.
dictionary.dat
In the Input line, please enter the name of the document file.
doc2.dat
Analysis
1. The f rst line of input will contain the path to the text f le. The second line of input will contain the path to the
output file
2. Each word in the text consists of letters only (some or all may be in upper-case), except that a word may also
have an apostrophe.
3. Words in the text are separated from each other by at least one non-alphabetic character, such as a punctuation
symbol, a blank, or an end-of-line marker.
4. The output should consist of the words, lower-cased and in alphabetical order; each word is followed by its
frequency.
5. For the entire program, worstTime(n) is O(n log n), where n is the number of distinct words in the text.
Assume that doc1.in contains the following file
System Test 1:
Please enter the path to the text file.
doc1.in
Please enter the path to the output file.
doc1.out
(Here are the contents of doc1.out after the completion of the program.)
Here are the words and their frequencies:
a: 1
big: 1
counts: 1
Programming Exercises 543
have: 1
in: 2
including: 1
it: 1
many: 1
may: 1
number: 1
of: 1
program: 1
text: 2
the: 2
this: 1
words: 3
System Test 2:
Please enter the path to the text file.
doc2.in
Please enter the path to the output file.
doc2.out
(Here are the contents of doc2.out after the completion of the program.)
Here are the words and their frequencies:
a: 1
bear: 1
fuzzy: 4
had: 1
hair: 1
he: 1
no: 1
was: 2
wasn’t: 1
wuzzy: 3
Analysis
1. The f rst line of input will contain the path to the text f le. The second line of input will contain the path to the
output file
2. Each word in the text consists of letters only (some or all may be in upper-case), except that a word may also
have an apostrophe.
3. The words in the text are separated from each other by at least one non-alphabetic character, such as a
punctuation symbol, a blank, or an end-of-line marker.
4. The output should consist of the words, lower-cased and in alphabetical order; each word is followed by each
line number that the word occurs in. The line numbers should be separated by commas.
6. For the entire program, worstTime(n) is O(n log n), where n is the number of distinct words in the text.
Assume that doc1.in contains the following file
System Test 1:
Please enter the path to the text fi e.
doc1.in
Please enter the path to the output file
doc1.out
(Here are the contents of doc1.out after the completion of the program.)
Here is the concordance:
a: 2
big: 4
counts: 1
have: 3
in: 2, 4
including: 4
it: 4
many: 3
may: 3
number: 2
Programming Exercises 545
of: 2
program: 1
text: 2, 3
the: 1, 3
this: 1
words: 2, 3, 4
System Test 2:
Please enter the path to the text fi e.
doc2.in
Please enter the path to the output file
doc2.out
(Here are the contents of doc2.out after the completion of the program.)
Here is the concordance:
a: 1
bear: 1
fuzzy: 1, 2, 3
had: 2
hair: 2
he: 4
no: 2
was: 1, 4
wasn’t: 3
wuzzy: 1, 2, 3
Voter Ballot
1 Moe, Curly
2 Curly, Larry
3 Larry
4 Gerry, Larry, Moe
5 Larry, Moe, Gerry, Curly
6 Gerry, Moe
7 Curly, Moe
In this example, the winner is Moe. If there is a tie for most approvals, all of those tied candidates are considered
winners—there may then be a run-off election to determine the f nal winner.
For a given f le of ballots, the output—in the console window—should have:
1. the candidate(s) with the most votes, in alphabetical order of candidates’ names;
2. the vote totals for all candidates, in alphabetical order of candidates’ names;
3. the vote totals for all candidates, in decreasing order of votes; for candidates with equal vote total, the ordering
should be alphabetical.
Let C represent the number of candidates, and V represent the number of voters. For the entire program,
worstTime(C, V) is O(V log C). You may assume that C is (much) smaller than V.
Assume that p5.in1 contains the following:
Moe, Curly
Curly, Larry
Larry
Gerry, Larry, Moe
Larry, Moe, Gerry, Curly
Gerry, Moe
Curly, Moe
Karen
Tara, Courtney
Courtney, Tara
Here are the vote totals for all candidates, in decreasing order of vote totals:
Here are the vote totals for all candidates, in decreasing order of vote totals:
browser.in6 7
because “neural” appears 3 times on the web page, and “network” appears 4 times (“networking” does not count).
The end user may now click on browser.in6 to display that page.
Start with your definitio of the two specifie methods in your Filter class from Part 2 of this project. Use
those methods to get each word in the web page—excluding an expanded f le of common words, and so on—and
determine the frequency of each such word on that web page. Then, for each word in the search string, add up
the frequencies.
The only class you will modify is your listener class. Here is the expanded f le of common words, which
will be stored in the f le common.in1:
all
an
and
be
but
did
down
for
if
in
Programming Exercises 549
is
not
or
that
the
through
to
were
where
would
For each of the n words in the web page, the worstTime(n) for incrementing that word’s frequency must
be logarithmic in n. Also, for each word in the search string, calculating its frequency in the web page must also
take logarithmic-in-n time, even in the worst case.
For testing, assume search.in1 contains just a few fi es, for example,
browser.in6
browser.in7
browser.in8
browser.in7:
In Xanadu did Kubla Khan
A stately pleasure-dome decree:
Where Alph, the sacred river, ran
Through caverns<a href=browser.in2>browser2</a> measureless to man
Down to a sunless sea.
browser.in8:
Please enter a search string in the input line and then press the Enter key.
neural
If the end-user now clicks on the Back button, the input prompt should appear in the output window. If the end-user
clicks on the Back button again, the search page for “neural network” should re-appear.
System Test 2:
Please enter a search string in the input line and then press the Enter key.
network
NOTE: The Search button should be green as soon as the GUI window is opened, and only one search string can
be entered for each press of the Search button.
Priority Queues CHAPTER 13
In this chapter, we examine the priority queue data type. A priority queue is a collection in which only the
element with highest priority can be removed, according to some method for comparing elements. This
restriction allows an implementation—the Java Collections Framework’s PriorityQueue class—with
an add method whose average time is constant. The PriorityQueue class can be enhanced by including
heapSort, a fast sort method for which worstSpace(n) is constant. The chapter concludes by using a
priority queue to generate a Huffman Tree—a necessary component of a popular data-compression
technique called Huffman compression.
CHAPTER OBJECTIVES
1. Understand the defining property of a priority queue, and how the Java Collections Framework’s
PriorityQueue class violates that property.
2. Be able to perform the heap operations of siftUp and siftDown.
3. Compare Heap Sort to Merge Sort with respect to time and space requirements.
4. Examine the Huffman data-compression algorithm.
5. Determine the characteristic of a greedy algorithm.
13.1 Introduction
A variation of the queue, the priority queue is a commonplace structure. The basic idea is that we have
elements waiting in line for service, as with a queue. But removals are not strictly on a f rst-in-first-ou
basis. For example, patients in an emergency room are treated according to the severity of their injuries,
not according to when they arrived. Similarly, in air-traff c control, when there is a queue of planes waiting
to land, the controller can move a plane to the front of the queue if the plane is low on fuel or has a sick
passenger.
A shared printer in a network is another example of a resource suited for a priority queue. Normally,
jobs are printed based on arrival time, but while one job is printing, several others may enter the service
queue. Highest priority could be given to the job with the fewest pages to print. This would optimize
the average time for job completion. The same idea of prioritized service can be applied to any shared
resource: a central processing unit, a family car, the courses offered next semester, and so on.
Here is the definition
A priority queue is collection in which removal is of the highest-priority element in the collection,
according to some method for comparing elements.
551
552 C H A P T E R 13 Priority Queues
For example, if the elements are of type (reference to) Integer and comparisons use the “natural” ordering,
then the highest-priority element is the one whose corresponding int has the smallest value in the priority
queue. But if elements are of type Integer and the comparisons use the reverse of the natural ordering,
then the highest-priority element is the one whose corresponding int has the largest value in the priority
queue. By default, the smallest-valued element has highest priority.
This definitio says nothing about insertions. In the PriorityQueue class, the method add (E
element) takes only constant time on average, but linear-in-n time in the worst case. What underlying
structure do these times suggest?
You might wonder what happens if two or more elements are tied for highest priority. In the interest
of fairness, the tie should be broken in favor of the element that has been in the priority queue for
the longest time. This appeal to fairness is not part of the definition and is not incorporated into the
PriorityQueue class. Lab 21 provides a solution to this problem.
Most of this chapter is devoted to an important application of priority queues: Huffman encoding.
Also, Chapter 15 has two widely used priority-queue applications: Prim’s minimum-spanning-tree algorithm
and Dijkstra’s shortest-path algorithm. For a lively discussion of the versatility of priority queues, see Dale
[1990].
/**
* Initializes a PriorityQueue object with the specified initial capacity
* that orders its elements according to the specified comparator.
*
* @param initialCapacity the initial capacity for this priority queue.
13.3 Implementation Details of the PriorityQueue Class 553
/**
* Inserts a specified element into this PriorityQueue object.
* The worstTime(n) is O(n) and averageTime(n) is constant.
*
* @param element –the element to be inserted into this PriorityQueue object.
* @return true
* @throws NullPointerException –if element is null.
* @throws ClassCastException if the specified element cannot be compared
* with elements currently in the priority queue according
* to the priority queue’s ordering.
*
*/
public boolean add (E element)
/**
* Retrieves and removes the highest priority element of this PriorityQueue object.
* The worstTime(n) is O(log n).
*
* @return the highest priority element of this PriorityQueue object.
* @throws NoSuchElementException if this queue is empty.
*/
public E remove()
/**
* Retrieves, but does not remove, the highest priority element of this PriorityQueue
* object.
* The worstTime(n) is constant.
*
* @return the highest priority element of this PriorityQueue object.
* @throws NoSuchElementException if this queue is empty.
*/
public E element()
26
32 30
48 50 80 31
107 80 55
FIGURE 13.1 A heap with ten (reference to) Integer elements; the int values of the Integer elements are
shown
of ten Integer elements with the “natural” ordering. Notice that a heap is not a binary search tree. For
example, duplicates are allowed in a heap, unlike the prohibition against duplicates for binary search trees.
The ordering in a heap is top-down, but not left-to-right: the root element of each subtree is less
than or equal to each of its children, but some left siblings may be less than their right siblings and some
may be greater than or equal to. In Figure 13.1, for example, 48 is less than its right sibling, 50, but 107
is greater than its right sibling, 80. Can you fin an element that is smaller than its parent’s sibling?
A heap is a complete binary tree. We saw in Chapter 9 that a complete binary tree can be implemented
with an array. Figure 13.2 shows the array version of Figure 13.1, with each index under its element. An
iteration over a PriorityQueue object uses the index-ordering of the array, so the iteration would be 26,
32, 30, 48, 50, 80, 31, 107, 80, 55.
Recall from Chapter 9 that the random-access feature of arrays is convenient for processing a
complete binary tree: Given the index of an element, that element’s children can be accessed in constant
time. For example, with the array as shown in Figure 13.2, the children of the element at index i are at
indexes (i << 1) + 1 and (i << 1) + 2. And the parent of the element at index j is at index (j -
1) >> 1.
26 32 30 48 50 80 30 107 80 55 ...
0 1 2 3 4 5 6 7 8 9
FIGURE 13.2 The array representation of the heap from Figure 13.1
As we will see shortly, the ability to quickly swap the values of a parent and its smaller-valued child
makes a heap an efficien storage structure for the PriorityQueue class.
Here is program that creates and maintains two PriorityQueue objects in which the elements are
of type Student, declared below. Each line of input—except for the sentinel—consists of a name and the
corresponding grade point average (GPA). The highest-priority student is the one whose GPA is lowest.
In the Student class, the Comparable interface is implemented by specifying increasing order of grade
point averages. To order another heap by alphabetical order of student names, a ByName comparator class
is defined
import java.util.*;
final String PROMPT1 = "Please enter student’s name and GPA, or " ;
final String RESULTS1 = "\nHere are the student names and GPAs, " +
"in increasing order of GPAs:";
final String RESULTS2 = "\nHere are the student names and GPAs, " +
"in alphabetical order of names:";
String line;
while (true)
{
System.out.print (PROMPT1 + SENTINEL + PROMPT2);
line = sc.nextLine();
if (line.equals (SENTINEL))
break;
pq1.add (new Student (line));
pq2.add (new Student (line));
} // while
System.out.println (RESULTS1);
while (!pq1.isEmpty())
System.out.println (pq1.remove());
System.out.println (RESULTS2);
while (!pq2.isEmpty())
System.out.println (pq2.remove());
} // method run
} // class PriorityQueueExample
// in another file
import java.util.*;
public class Student implements Comparable<Student>
556 C H A P T E R 13 Priority Queues
{
protected String name;
/**
* Initializes this Student object from a specified String object.
*
* @param s - the String object used to initialize this Student object.
*
*/
public Student (String s)
{
Scanner sc = new Scanner (s);
name = sc.next();
gpa = sc.nextDouble();
} // constructor
/**
* Compares this Student object to a specified Student object by
* grade point average.
*
* @param otherStudent - the specified Student object.
*
* @return a negative integer, 0, or a positive integer, depending
* on whether this Student object’s grade point average is less than,
* equal to, or greater than otherStudent’s grade point average.
*
*/
public int compareTo (Student otherStudent)
{
if (gpa < otherStudent.gpa)
return -1;
if (gpa > otherStudent.gpa)
return 1;
return 0;
} // method compareTo
/**
* Returns a String representation of this Student object.
*
* @return a String representation of this Student object: name " " gpa
*
*/
public String toString()
{
return name + " " + gpa;
} // method toString
13.3 Implementation Details of the PriorityQueue Class 557
} // class Student
// in another file:
import java.util.*;
} // class ByName
The Student class implements the Comparable interface with a compareTo method that returns −1, 0,
or 1, depending on whether the calling Student object’s grade point average is less than, equal to, or
greater than another student’s grade point average. So, as you would expect, the Student with the lowest
grade point average is the highest-priority element. Suppose the input for the PriorityQueueExample
program is as follows:
Soumya 3.4
Navdeep 3.5
Viet 3.5
***
Here is the output of the students as they are removed from pq1:
Soumya 3.4
Viet 3.5
Navdeep 3.5
Notice that Viet is printed before Navdeep even though they have the same grade point average, and
Navdeep was input earlier than Viet. As mentioned earlier, the PriorityQueue class—like life—is
unfair. In Section 13.3.1, we look at more details of the PriorityQueue class, which will help to explain
how this unfairness comes about. Lab 21 will show you how to remedy the problem.
For pq2 in the PriorityQueueExample program above, the output of students would be
Navdeep 3.5
Soumya 3.4
Viet 3.5
And—similar to the TreeMap class—we also have size, comparator, and modCount fields
/**
* The number of elements in the priority queue.
558 C H A P T E R 13 Priority Queues
*/
private int size = 0;
/**
* The comparator, or null if priority queue uses elements’
* natural ordering.
*/
private final Comparator<? super E> comparator;
/**
* The number of times this priority queue has been
* structurally modified. See AbstractList class and Appendix 1 for gory details.
*/
private transient int modCount = 0;
The following constructor definitio creates a priority queue with a specifie initial capacity and an ordering
supplied by a specifie comparator:
/**
* Initializes a PriorityQueue object with the specified initial capacity
* that orders its elements according to the specified comparator.
*
* @param initialCapacity the initial capacity for this priority queue.
* @param comparator the comparator used to order this priority queue.
* If null then the order depends on the elements’ natural ordering.
* @throws IllegalArgumentException if initialCapacity is less than 1
*/
public PriorityQueue (int initialCapacity, Comparator<? super E> comparator)
{
if (initialCapacity < 1)
throw new IllegalArgumentException();
queue = new Object[initialCapacity];
this.comparator = comparator;
} // constructor
Now let’s defin the add (E element), element(), and remove() methods. The add (E element)
method expands that array if it is full, and calls an auxiliary method, siftUp, to insert the element and
restore the heap property. Here are the specification and definition
/**
* Inserts the specified element into this priority queue.
13.3 Implementation Details of the PriorityQueue Class 559
For example, Figure 13.3 shows what the heap in Figure 13.1 will look like just before siftUp (10,
new Integer (28)) is called.
26
32 30
48 50 80 31
107 80 55 ?
FIGURE 13.3 The heap from Figure 13.1 just before 28 is inserted. The size is 10.
And here is the specificatio and definitio for siftUpComparable (the definitio for siftUpUsing-
Comparator differs only in the use of the compare method instead of compareTo):
/**
* Inserts item x at position k, maintaining heap invariant by
* promoting x up the tree until it is greater than or equal to
* (according to the elements’ implementation of the Comparable
* interface) its parent, or is the root.
* The worstTime(n) is O(log n) and averageTime(n) is constant.
*
* @param k the position to fill
* @param x the item to insert
*/
private void siftUpComparable(int k, E x) {
Comparable<? super E> key = (Comparable<? super E>) x;
while (k > 0) {
int parent = (k - 1) >>> 1;
Object e = queue[parent];
if (key.compareTo((E) e) >= 0)
break;
queue[k] = e;
k = parent;
}
queue[k] = key;
}
The siftUpComparable (int k, E x) method restores the heap property by repeatedly replacing
queue [k] with its parent and dividing k by 2 (with a right shift of 1 to accomplish dividing by 2
faster) until x is greater than or equal to the new parent. Then the while loop is exited and x is stored
at queue[k]. Figure 13.4 shows the result of a call to siftUpComparable (10, new Integer (28))
on the complete binary tree in Figure 13.3:
When called by the add (E element) method, the siftUp method starts at queue [size - 1]
as the child and replaces that child with its parent until the parent is greater than or equal to the element to
13.3 Implementation Details of the PriorityQueue Class 561
26
28 30
48 32 85 31
107 80 55 50
FIGURE 13.4 The heap formed by invoking siftUp (10, new Integer(28)) on the complete binary tree in
Figure 13.3
be inserted. Then the element is inserted at that parent index. For example, let’s begin with the complete
binary tree in Figure 13.3, repeated here, with 28 to be inserted:
26
32 30
48 50 80 31
107 80 55 ?
The child index is 10. Because 28 is less than 50, we replace queue[10] with 50, and set the child index
to 4(= (10 − 1) >>> 1). See Figure 13.5.
26
32 30
48 50 85 31
107 80 55 50
FIGURE 13.5 The heap of Figure 13.3 after replacing queue[10] with queue[4]
562 C H A P T E R 13 Priority Queues
We now set the parent index to 1(= (4 − 1) >>> 1), and compare 28 with the parent’s value, namely
32. Because 28 is less than 32, we replace queue[4] with queue[1], and set the parent index to
0(= (1 − 1) >>> 1). Because 28 is <= queue[0], 28 is inserted at queue[1] and that gives us the heap
shown in Figure 13.6.
26
28 30
48 32 85 31
107 80 55 50
FIGURE 13.6 The heap formed when 28 is added to the heap in Figure 13.1
The key to the eff ciency of the siftUp method is that a parent’s index is readily computable from
either of its children’s indexes: if k contains a child’s index, then its parent’s index must be
(k –1) / 2
And because queue is an array object, the element at that parent index can be accessed or modifie in
constant time.
In the worst case for the siftUp method, the element inserted will have a smaller value than any of
its ancestors, so the number of loop iterations will be, approximately, the height of the heap. As we saw
in Chapter 9, the height of a complete binary tree is logarithmic in n. So worstTime(n) is logarithmic in
n, which satisfie the worst-time estimate from the specificatio of siftUp.
In the average case, about half of the elements in the heap will have a smaller value than the element
inserted, and about half will have a larger value. But heaps are very bushy: at least half of the elements are
leaves. And because of the heap properties, most of the larger-valued elements will be at or near the leaf
level. In fact, the average number of loop iterations is less than 3 (see Schaffer [1993]), which satisfie the
specificatio that averageTime(n) be constant. Programming Exercise 13.3 outlines a run-time experiment
to support this claim.
Now we can analyze the add (E element) method. The worst case occurs when the array queue
is full. Then a new, larger array is constructed and the old array is copied over. So worstTime(n) is linear
in n. But this doubling occurs infrequently—once in every n insertions—so averageTime(n) is constant,
just as for siftUp.
The remove() method’s main task is to delete the root element. But simply doing this would leave a hole
where the root used to be. So, just as we did when we removed a BinarySearchTree object’s element
that had two children, we replace the removed element with some other element in the tree. Which one?
The obvious answer is the smaller of its children. And ultimately, that must happen in order to preserve
the heap properties. But if we start by replacing the root element with its smaller child, we could open up
another hole, as is shown in the following picture:
10 20
Warning:not a heap!
30 20 30
50 60 50 60
When 10 is replaced with 20, its smaller child, the resulting tree is not a heap because it is not a complete
binary tree.
A few minutes reflectio should convince you that, to preserve the completeness of the binary tree,
there is only one possible replacement for the root element: the last element in the heap—at index size
–1. Of course, we will then have a lot of adjustments to make; after all, we have taken one of the largest
elements and put it at the top of the heap. These adjustments are handled in a siftDown method that
starts at the new root and moves down to the leaf level. So the remove() method decrements size by
1, saves the element at index 0 in result, saves the element (no longer in the heap) at index size in x,
calls siftDown(0, x) to place x where it now belongs, and returns result.
Here is the method definition
/**
* Removes the smallest-valued element from this PriorityQueue object.
* The worstTime(n) is O(log n).
*
* @return the element removed.
*
* @throws NoSuchElementException – if this PriorityQueue object is empty.
*
*/
public E remove ()
{
if (size == 0)
throw new NoSuchElementException();
int s = --size;
564 C H A P T E R 13 Priority Queues
modCount++;
E result = (E) queue[0];
E x = (E) queue[s];
queue[s] = null; // to prevent memory leak
if (s != 0)
siftDown(0, x);
return result;
} // method remove
26
33 30
48 50 80 31
107 80 55
If we now call remove(), then 26 is saved in result and 55 is saved in x, just before the call to
siftDown. See Figure 13.7.
26
32 30
48 50 80 31
result x
107 80 26 55
Notice that since we started with a heap, the heap property guarantees that the root’s left and right
subtrees are still heaps. So siftDown’s task is to maintain the heap property while putting x where it
belongs.
In contrast to siftUp, the siftDown method works its way down the tree, starting at the index
supplied as the argument, in this case, 0. Each parent is replaced with its smaller-valued child until,
13.3 Implementation Details of the PriorityQueue Class 565
eventually, either x’s value is less than or equal to the smaller-valued child (and then is stored at the
parent index) or the smaller-valued child is a leaf (and then x is stored at that child index). For example,
in the tree of Figure 13.7, 30 is smaller than 32, so 30 becomes the root element, and we have the tree
shown in Figure 13.8.
We still need one more iteration because 55 is greater than the smaller of the children (namely, 80
and 31 at indexes 5 and 6. So the element at index 6 (namely, 31) replaces the element at index 2 (namely,
30). That smaller-valued child was a leaf, so 55 is inserted at that leaf index and we end up with the heap
shown in Figure 13.9.
If k contains the parent index, then k << 1 is the index of the left child and (k << 1) + 1 is the
index of the right child (if the parent has a right child). And because queue is an array object, the elements
at those indexes can be accessed or modifie in constant time.
Here is the definitio of siftDown:
private void siftDown(int k, E x)
{
if (comparator != null)
siftDownUsingComparator(k, x);
30
32 30
48 50 80 31
result x
107 80 26 55
FIGURE 13.8 The heap from Figure 13.7 after the smaller-valued of the root’s children is replaces the root. The
element 55 has not yet been removed
30
32 31
48 50 80 55
107 80
FIGURE 13.9 The heap formed from the heap in Figure 13.8 by replacing the element at index 2 with 31 and
then inserting 55 at index 6
566 C H A P T E R 13 Priority Queues
else
siftDownComparable(k, x);
}
And here is siftDownComparable (which differs from siftDownUsingComparator in that the ele-
ments’ class implements the Comparable interface):
/**
* Maintains the heap properties in this PriorityQueue object while, starting at a
* specified index, inserts a specified element where it belongs.
* The worstTime(n) is O(log n).
*
* @param k –the specified position where the restoration of the heap
* will begin.
* @param x –the specified element to be inserted.
*
*/
private void siftDownComparable(int k, E x) {
Comparable<? super E> key = (Comparable<? super E>)x;
int half = size >>> 1; // loop while a non-leaf
while (k < half) {
int child = (k << 1) + 1; // assume left child is least
Object c = queue[child];
int right = child + 1;
if (right < size &&
((Comparable<? super E>) c).compareTo((E) queue[right]) > 0)
c = queue[child = right];
if (key.compareTo((E) c) <= 0)
break;
queue[k] = c;
k = child;
}
queue[k] = key;
}
In the worst case for siftDownComparable, the loop will continue until a leaf replaces its parent.
Since the height of a heap is logarithmic in n, worstTime(n) is logarithmic in n. Because a heap is a
complete binary tree, the average number of loop iterations will be nearly log2 n, so averageTime(n) is
also logarithmic in n.
In Lab 21, you will create a descendant, FairPQ, of the PriorityQueue class. As its name suggests,
the FairPQ class resolves ties for highest-priority element in favor of seniority. For elements with equal
priority, the highest-priority is chosen as the element that has been on the heap for the longest time.
You are now ready for Lab 21: Incorporating Fairness in Priority Queues
Section 13.4 shows how to modify the PriorityQueue class to obtain the heapSort method. Actually,
the PriorityQueue class was created many years after the invention of the heapSort method, which
was originally based on the idea of using an array to store a heap.
13.4 The heapSort Method 567
Assume, for now, that this method is in the PriorityQueue class. Then the following is a test of that
method:
public void testSample()
{
Integer [ ] expected ={12, 16, 17, 32, 33, 40, 43, 44, 46, 46,
50, 55, 59, 61, 75, 80, 80, 81, 87, 95};
Integer [ ] actual = {59, 46, 32, 80, 46, 55, 50, 43, 44, 81,
12, 95, 17, 80, 75, 33, 40, 61, 16, 87};
new PriorityQueue().heapSort (actual);
assertArrayEquals (expected, actual);
} // method testSample
Here is the basic idea of the heapSort method: We firs initialize the queue and size fields
queue = a;
int length = queue.length;
size = length;
We then heapify the array queue, that is, convert the array into a heap. After queue has become a heap,
we sort queue into reverse order. Important: After each iteration of this loop, the heap will consist of the
elements of queue from index 0 through index size —1, not through index queue.length–1. After the
firs sift down, the smallest element (the one with highest priority) will be at index queue.length - 1,
and the array from indexes 0 through queue.length - 2 will constitute a heap. After the second sift
down, the second smallest element will be at index queue.length - 2, and the array from indexes 0
through queue.length - 3 will constitute a heap. After all of the swaps and fi downs, the elements in
the array queue will be in reverse order, so by reversing the order of the elements, the array will be in order.
For an example, let’s start with an array of (references to Integer objects with the following) int
values:
59 46 32 80 46 55 87 43 44 81 95 12 17 80 75 33 40 61 16 50
The value 59 is stored at index 0, the value 46 at index 1, and so on. Figure 13.10 (next page) shows the
fiel queue viewed as a complete binary tree.
568 C H A P T E R 13 Priority Queues
59
46 32
80 46 55 87
43 44 81 95 12 17 80 75
33 40 61 16 50
Of course, queue is not yet a heap. But we can accomplish that subgoal by looping as i goes
from size - 1 down to 0. At the start of each iteration, the complete binary tree rooted at index i
is a heap except, possibly, at index i itself. The call to siftDown (i, queue [i]) method in the
PriorityQueue class converts the complete binary tree rooted at index i into a heap. So, basically, all
we need to do is to repeatedly call siftDown (i, queue [i]) to create the heap from the bottom up.
We can reduce the number of loop iterations by half by taking advantage of the fact that a leaf is
automatically a heap, and no call to siftDown is needed. So the loop will start at the last non-leaf index.
For the complete binary tree rooted at that index, its left and right subtrees will be heaps, and we can
call siftDown with that index as argument. In the above example, the last non-leaf index is 9, that is,
20/2 − 1. Since queue [19] is less than queue [9] —that is, since 50 is less than 81—the effect of the
firs iteration is that those two elements are swapped, giving the complete binary tree in Figure 13.11.
59
46 32
80 46 55 87
43 44 50 95 12 17 80 75
33 40 61 16 81
FIGURE 13.11 The complete binary tree of Figure 13.10 after swapping 50 with 81
During the remaining 9 loop iterations (at indexes 8 through 0), we wind our way back up the tree.
For the ith iteration, siftDown (i, queue [i]) is applied to the complete binary tree rooted at index
i, with the left and right subtrees being heaps. The effects of the calls to siftDown (i, queue [i])
are as follows:
13.4 The heapSort Method 569
(i = 8) 44 16
61 16 61 44
(i = 7) 43 33
33 40 43 40
(i = 6) 87 75
80 75 80 87
(i = 5) 55 12
12 17 55 17
(i = 4) 46 46
50 95 50 95
81 81
(i = 3) 80 16
33 16 33 44
43 40 61 44 43 40 61 80
(i = 2) 32 12
12 75 17 75
55 17 80 87 55 32 80 87
570 C H A P T E R 13 Priority Queues
(i = 1) 46 16
16 46 33 46
33 44 50 95 40 44 50 95
43 40 61 80 81 43 46 61 80 81
After the execution of siftDown (i, queue [i]) at index 0 in the fina iteration, we get the heap
shown in Figure 13.12.
12
16 17
33 46 32 75
40 44 50 95 55 59 80 87
43 46 61 80 81
FIGURE 13.12 The effect of making a heap from the complete binary tree in Figure 13.10
We now sort the array into reverse order. To accomplish this, we have another loop, this one with
i going from index 0 to index queue.length - 1. During each loop iteration, we save the element
at queue [–size] in x, store queue [0] in queue [size] and then call siftDown (0, x). For
example, if i = 0 and size = 20, the element at queue [19] is saved in x, the smallest element (at
index 0) is stored at queue [19], and siftDown (0, x) is called. The complete binary tree from
index 0 through index 18 is a complete binary tree except at index 0, so the call to siftDown (0, x)
will restore heapity to this subarray, without affecting the smallest element (now at index 19). The result
of 20 of these loop iterations is to put the 20 elements in descending order in the array. Figure 13.13
shows this array in the form of a complete binary tree.
When we treat the complete binary tree in Figure 13.13 as an array, and reverse the elements in this
array, the elements are in ascending order:
12 16 17 32 33 40 43 44 46 46 50 55 59 61 75 80 80 81 87 95
Here is the heapSort method:
public void heapSort (Object[ ] a)
{
queue = a;
13.4 The heapSort Method 571
95
87 81
80 80 75 61
59 55 50 46 46 44 43 40
33 32 17 16 12
FIGURE 13.13 The complete binary tree resulting from the heap of Figure 13.12 after the 20 swaps and sift downs
// Reverse queue:
for (int i = 0; i < length / 2; i++)
{
x = (E)queue [i];
queue [i] = queue [length - i - 1];
queue [length - i - 1] = x;
} // reverse queue
} // method heapSort
We earlier assumed that the heapSort method was part of the PriorityQueue class. Currently, at least,
that is not the case. So how can we test and perform run-time experiments on that method? Surely, we
cannot modify the PriorityQueue class in java.util. And we cannot make heapSort a method in an
extension of the PriorityQueue class because the queue and size field in that class have private
visibility. What we do is copy the PriorityQueue class into a local directory, replace
package java.util;
with
import java.util.*;
572 C H A P T E R 13 Priority Queues
And add the heapSort method to this copy of the PriorityQueue class. The method can then be tested
and experimented with.
1. To convert queue to a heap, there is a loop with n/2 − 1 iterations and with i as the loop-control
variable, and in each iteration, siftDown (i, queue [i]) is called. In the worst case, siftDown
(i, queue [i]) requires log2 (n) iterations, so the total number of iterations in converting a to a
heap is O(n log n). In fact, it can be shown (see Concept Exercise 13.12) that this total number of
iterations is linear in n.
2. For each of the n calls to siftDown with 0 as the firs argument, worstTime(n) is O(log n). So the
total number of iterations for this loop is O(n log n) in the worst case.
3. The reversing loop is executed n/2 times.
The total number of iterations in the worst case is O(n) + O(n log n) + O(n). That is, worstTime(n) is
O(n log n) and so, by Sorting Fact 1 from Section 11.4, worstTime(n) is linear-logarithmic in n. Therefore,
by Sorting Fact 3, averageTime(n) is also linear-logarithmic in n.
The space requirements are meager: a few variables. There is no need to make a copy of the elements
to be sorted. Such a sort is called an in-place sort.
That heapSort is not stable can be seen if we start with the following array object of quality-of-life
scores and cities ordered by increasing quality-of-life scores:
20 Portland
46 Easton
46 Bethlehem
Converting this array object to a heap requires no work at all because the heap property is already satisfied
20 Portland
46 Easton 46 Bethlehem
In the second loop of heapSort, Portland and Bethlehem are swapped and siftDown is called with 0 as
the f rst argument:
46 Bethlehem
Notice that, in the call to siftDown, (46 Bethlehem) is not swapped with (46 Easton) because its child
(46 Easton) is not less than (46 Bethlehem). After two more iterations of the second loop and a reversal
of the elements of the array, we have
(20 Portland) (46 Bethlehem) (46 Easton)
The positions of (46 Bethlehem) and (46 Easton) have flippe from the original array object, so the
heapSort method is not stable.
a=0
b=1
c = 00
d = 01
e = 10
f = 11
g = 000
This would reduce the size of the encoded f le E by about one-third (unless the character ‘g’ occurred very
frequently). But this encoding leads to ambiguities. For example, the bit sequence 001 could be interpreted
as “ad” or as “cb” or as “aab”, depending on whether we grouped the firs two bits together or the last
two bits together, or treated each bit individually.
The reason the above encoding scheme is ambiguous is that some of the encodings are prefixe of
other encodings. For example, 0 is a prefi of 00, so it is impossible to determine whether 00 should be
interpreted as “aa” or “c”. We can avoid ambiguities by requiring that the encoding be prefix-fre , that is,
no encoding of a character can be a prefi of any other character’s encoding.
1 Recall that ceil(x) returns the smallest integer greater than or equal to x.
574 C H A P T E R 13 Priority Queues
One way to guarantee prefix-fre bit encodings is to create a binary tree in which a left branch is
interpreted as a 0 and a right branch is interpreted as a 1. If each encoded character is a leaf in the tree,
then the encoding for that character could not be the prefi of any other character’s encoding. In other
words, the path to each character provides a prefix-fre encoding. For example, Figure 13.14 has a binary
tree that illustrates a prefix-fre encoding of the characters ‘a’ through ‘g’.
To get the encoding for a character, start with an empty encoding at the root of the binary tree, and
continue until the leaf to be encoded is reached. Within the loop, append 0 to the encoding when turning left
and append 1 to the encoding when turning right. For example, ‘b’ is encoded as 01 and ‘f’ is encoded as
1110. Because each encoded character is a leaf, the encoding is prefix-fre and therefore unambiguous. But
it is not certain that this will save space or transmission time. It all depends on the frequency of each charac-
ter. Since three of the encodings take up two bits and four encodings take up four bits, this encoding scheme
may actually take up more space than the simple, three-bits-per-character encoding introduced earlier.
This suggests that if we start by determining the frequency of each character and then make up the
encoding tree based on those frequencies, we may be able to save a considerable amount of space. The
idea of using character frequencies to determine the encoding is the basis for Huffman encoding (Huffman
[1952]). Huffman encoding is a prefix-fre encoding strategy that is guaranteed to be optimal—among
prefix-fre encodings. Huffman encoding is the basis for the Unix compress utility, and also part of the
JPEG (Joint Photographic Experts Group) encoding process.
We begin by calculating the frequency of each character in a given message M. Note that the time
for these calculations is linear in the length of M. For example, suppose that the characters in M are the
letters ‘a’ . . . ‘g’ as shown previously, and their frequencies are as given in Figure 13.15.
The size of M is 100,000 characters. If we ignored frequencies and encoded each character into a
unique 3-bit sequence, we would need 300,000 bits to encode the message M. We’ll soon see how far this
is from an optimal encoding.
0 1
0 1 0 1
a b c
0 1
0 1 0 1
d e f g
FIGURE 13.14 A binary tree that determines a prefix-fre encoding of ‘a’ . . . ‘g’
a: 5,000
b: 2,000
c: 10,000
d: 8,000
e: 22,000
f: 49,000
g: 4,000
FIGURE 13.15 Sample character-frequencies for a message of 100,000 characters from ‘a’ . . . ‘g’
13.5 Application: Huffman Codes 575
Once we have calculated the frequency of each character, we will insert each character-frequency
pair into a priority queue ordered by increasing frequencies. That is, the front character-frequency pair
in the priority queue will have the least frequently occurring character. These characters will end up
being farthest from the root in the prefix-fre tree, so their encoding will have the most bits. Conversely,
characters that occur most frequently will have the fewest bits in their encodings.
Initially we insert the following pairs into the priority queue:
(a:5000) (b:2000) (c:10000) (d:8000) (e:22000) (f:49000) (g:4000)
In observance of the Principle of Data Abstraction, we will not rely on any implementation details of the
PriorityQueue class. So all we know about this priority queue is that an access or removal would be of
the pair (b:2000). We do not assume any order for the remaining elements, but for the sake of simplicity,
we will henceforth show them in increasing order of frequencies—as they would be returned by repeated
calls to remove().2 Note that there is no need to sort the priority queue.
(6000)
0 1
b g
(11000)
0 1
a (6000)
0 1
b g
2 In fact, if we did store the elements in a heap represented as an array, the contents of that array would be in the following order:
(b:2000),(a:5000),(g:4000),(d:8000),(e:22000),(f:49000),(c:10000).
576 C H A P T E R 13 Priority Queues
When ‘d’ and ‘c’ are removed, they cannot yet be connected to the main tree, because neither of their
frequencies is at the root of that tree. So they become the left and right branches of another tree, whose
root—their sum—is added to the priority queue. We temporarily have two Huffman trees:
(11000)
0 1
a (6000)
0 1
b g
and
(18000)
0 1
d c
When the pair (:11000) is removed, it becomes the left branch of the binary tree whose right branch is
the next pair removed, (:18000). The sum becomes the root of this binary tree, and that sum is added the
priority queue, so we have the following Huffman tree:
(29000)
0 1
(11000) (18000)
0 1 0 1
a (6000) d c
0 1
b g
When the next two pairs are removed, ‘e’ becomes the left branch and (:29000) the right branch of the
Huffman tree whose root, (:51000), is added to the priority queue. Finally the last two pairs, (f:49000)
and (:51000) are removed and become the left and right branches of the f nal Huffman tree. The sum of
those two frequencies is the frequency of the root, (:100000), which is added as the sole element into the
priority queue. The fina Huffman tree is shown in Figure 13.16.
13.5 Application: Huffman Codes 577
(100000)
0 1
f (51000)
0 1
e (29000)
0 1
(11000) (18000)
0 1 0 1
a (6000) d c
0 1
b g
FIGURE 13.16 The Huffman tree for the character-frequencies in Figure 13.15
To get the Huffman encoding for ‘d’, for example, we start at the leaf ‘d’ and work our way back up
the tree to the root. As we do, each bit encountered is pre-pended—placed at the front of—the bit string
that represents the Huffman encoding. So the encoding in stages, is
0
10
110
1110
That is, the encoding for ‘d’ is 1110. Here are the encodings for all of the characters:
a: 1100
b: 11010
c: 1111
d: 1110
e: 10
f: 0
g: 11011
It is now an easy matter to translate the message M into an encoded message E. For example, if the
message M starts out with
fad. . .
Then the encoded message E starts out with the encoding for ‘f’ (namely, 0) followed by the encoding for
‘a’ (namely, 1100) followed by the encoding for ‘d’ (namely, 1110). So E has
011001110. . .
What happens on the receiving end? How easy is it to decode E to get the original message M? Start at
the root of the tree and the beginning of E, take a left branch in the tree for a 0 in E, and take a right
branch for a 1. Continue until a leaf is reached. That is the firs character in M. Start back at the top of
the tree and continue reading E. For example, if M starts with “cede”, then E starts with 111110111010.
Starting at the root of the tree, the four ones at the beginning of E lead us to the leaf ‘c’. Then we go
back to the root, and continue reading E with the f fth 1. We take right branch for that 1 and then a left
578 C H A P T E R 13 Priority Queues
branch for the 0, and we are at ‘e’. The next few bits produce ‘d’ and ‘e’. In other words, we now have
“cede”, and that, as expected, is the start of M.
The size of the message E is equal to the sum, over all characters, of the number of bits in the
encoding of the character times the frequency of that character in M. So to get the size of E in this
example, we take the product of the four bits in the encoding of ‘a’ and the 5000 occurrences of ‘a’, add
to that the product of the fiv bits in the encoding of ‘b’ and the 2000 occurrences of ‘b’, and so on. We get:
(4 ∗ 5000) + (5 ∗ 2000) + (4 ∗ 10000) + (4 ∗ 8000) +
(2 ∗ 22000) + (1 ∗ 49000) + (5 ∗ 4000)
= 215, 000
This is about 30% less than the 300,000 bits required with the f xed-length, 3-bits-per-character encoding
discussed earlier. So the savings in space required and transmission time is significant But it should be
noted that a f xed-length encoding can usually be decoded more quickly than a Huffman encoding; for
example, the encoded bits can be interpreted as an array index—the entry at that index is the character
encoded.
r 0111
y 1010
**
001010011111101100101000011101010110001111100111000110
In the encoding, the f rst character is the new-line marker, '\n'. When the output fil is viewed, the f rst
line is blank because a new-line feed is carried out. The second line starts with a space, followed by the
code for the new-line. This may seem a bit strange because the f rst line in the encoding is
\n 0110
But when this line is printed, the new-line feed is carried out instead of '\n' being printed. So the space
between '\n' and 0110 starts the second line. Subsequent lines start with the character encoded, a space,
and the code for that character.
For System Test 2, assume that the f le huffman.in2 contains the following message:
In Xanadu did Kubla Khan
A stately pleasure-dome decree:
Where Alph, the sacred river, ran
Through caverns measureless to man
Down to a sunless sea.
r 1001
s 1101
t 10000
u 11100
v 010001
w 0100111
y 0100100
**
01001010101101001100000001010001111011100101111100011111111010100111
01110000110011110100010100111011111000010110001001011101110110000000
10000011111010100100101001001111010110001101111001001011010000011110
11000110011011101111100111100101001011011001010010001001010111111011
10010111010010111110100100111111001000101100001111101110111010001100
10100101111110101100100111101000101110010010001011001000010110001001
10111111110011100011100001101011111101110010000010001011100101011101
10111001101100011011110010010111110101111011101101100001100010111001
10000101100010100110110000100111010110110000110001010001011101111000
10111101011110111011011101011000010000110001
As always, we embrace modularity by separating the input-output details, in a HuffmanUser class, from
the Huffman-encoding details, in a Huffman class. We start with the lower-level class, Huffman, in Section
13.5.3.1.
String code;
Entry left,
right,
parent;
} // class Entry
We can now present the method specification for the Huffman class. For the second through fift of the
following method specifications the return values are only for the sake of testing.
/**
* Initializes this Huffman object.
*
*/
public Huffman()
/**
* Updates the frequencies of the characters in a scanned-in line.
*
* @param line – the line scanned in.
*
* @return - a cumulative array of type Entry in which the frequencies have been
* updated for each character in the line.
*
*/
public Entry[ ] updateFrequencies (String line)
/**
* Creates the priority queue from the frequencies.
*
* @return - the priority queue of frequencies (in increasing order).
*
*/
public PriorityQueue<Entry> createPQ()
/**
* Creates the Huffman tree from the priority queue.
*
* @return - an Entry representing the root of the Huffman tree.
*
*/
public Entry createHuffmanTree()
/**
* Calculates and returns the Huffman codes.
*
* @return - an array of type Entry, with the Huffman code
* for each character.
*
*/
public Entry[ ] calculateHuffmanCodes()
/**
* Returns, as a String object, the characters and their Huffman codes.
582 C H A P T E R 13 Priority Queues
*
* @return the characters and their Huffman codes.
*
*/
public String getCodes()
/**
* Returns a String representation of the encoding of a specified line.
*
* @param line – the line whose encoding is returned.
*
* @return a String representation of the encoding of line.
*
*/
public String getEncodedLine (String line)
By convention, since no time estimates are given, you may assume that for each method, worstTime(n) is
constant, where n is the size of the original message.
The Huffman methods must be tested in sequence. For example, testing the getCodes method
assumes that the updateFrequencies, createPQ, createHuffmanTree, and calculateHuffman
Codes are correct. Here is a test of the getCodes method, with huffman a f eld in the HuffmanTest
class:
@Test
public void testGetCodes()
{
huffman.updateFrequencies ("aaaabbbbbbbbbbbbbbbbccdddddddd");
huffman.createPQ();
huffman.createHuffmanTree();
huffman.calculateHuffmanCodes();
assertEquals ("\n 0000\na 001\nb 1\nc 0001\nd 01\n", huffman.getCodes());
} // method testGetCodes
All of the files including test files are available from the book’s website.
At this point, we can determine the field that will be needed. Clearly, from the discussion in section
13.5.1, we will need a priority queue. Given a character in the input, it will be stored in a leaf in the
Huffman tree, and we want to be able to access that leaf-entry quickly. To allow random-access, we can
choose an ArrayList fiel or an array f eld. Here the nod goes to an array f eld because of the speed
of directly utilizing the index operator, [ ], versus indirect access with the ArrayList’s get and set
methods. Note that if the original message fil is very large, most of the processing time will be consumed
in updating the frequencies of the characters and, later, in calculating the codes corresponding to the
characters in the message file Both of these tasks entail accessing or modifying entries.
That gives us two fields
protected Entry [ ] leafEntries;
For the sake of simplicity, we restrict ourselves to the 256 characters in the extended ASCII character set,
so we have one array slot for each ASCII character:
public final static int SIZE = 256;
13.5 Application: Huffman Codes 583
For example, if the input character is ‘B’, the information for that character is stored at index (int)’B’,
which is 66. If the encoding for ‘B’ is 0100 and the frequency of ‘B’ in the input message is 2880, then
the entry at leafEntries [66] would be
(to parent)
The left and right references are null because leaves have no children. The reason that we use references
rather than indexes for parent, left, and right is that some of the entries represent sums, not leaf-
characters, so those entries are not in leafEntries. Every leaf is an entry, but not every entry is a leaf.
pq = new PriorityQueue<Entry>();
} // default constructor
The updateFrequencies method adds 1 to the frequency for each character in the parameter line. The
new-line character is included in this updating to ensure that the line structure of the original message will
be preserved in the encoding. Here is the method definition
public Entry[ ] updateFrequencies (String line)
{
Entry entry;
return leafEntries;
} // method updateFrequencies
As noted earlier, worstTime(n) for this method is constant because n refers to the size of the entire message
file and this method works on a single line.
The priority queue is created from the entries with non-zero frequencies:
public PriorityQueue<Entry> createPQ()
{
Entry entry;
The Huffman tree is created “on the fly. Until the priority queue consists of a single entry, a pair of
entries is removed from the priority queue and becomes the left and right children of an entry that contains
the sum of the pair’s frequencies; the sum entry is added to the priority queue. The root of the Huffman
tree is returned (for the sake of testing). Here is the definition
public Entry createHuffmanTree()
{
Entry left,
right,
sum;
right = pq.remove();
right.code = "1";
pq.add (sum);
} // while
return pq.element(); // the root of the Huffman tree
} // method createHuffmanTree
The Huffman codes are determined for each leaf entry whose frequency is nonzero. We create the code
for that entry as follows: starting with an empty string variable code, we pre-pend the entry’s code fiel
13.5 Application: Huffman Codes 585
(either “0” or “1”) to code, and then replace the entry with the entry’s parent. The loop stops when the
entry is the root. The fina value of code is then inserted as the code fiel for that entry in leafEntries.
For example, suppose part of the Huffman tree is as follows:
Then the code for ‘B’ would be “0100”, and this value would be stored in the code fiel of the Entry at
index 66 of leafEntries —recall that (int)'B' = 66 in the ASCII (and Unicode) collating sequence.
Here is the method definition
public Entry[ ] calculateHuffmanCodes()
{
String code;
Entry entry;
In the getCodes method, a String object is constructed from each character and its code, and that
String object is then returned:
public String getCodes()
{
Entry entry;
Finally, in the getEncodedLine method, a String object is constructed from the code for each character
in line, appended by the new-line character, and that String object is returned:
public String getEncodedLine (String line)
{
Entry entry;
/**
* Saves the Huffman codes and the encoded message to a file.
* The worstTime(n) is O(n).
*
* @param printWriter - the PrintWriter object that holds the Huffman codes
* and the encoded message.
* @param inFilePath - the String object that holds the path for the file
* that contains the original message.
13.5 Application: Huffman Codes 587
The book’s website has unit tests for the createEncoding and saveEncodedMessage methods,
and Figure 13.17 has the UML diagram for the HuffmanUser class.
HuffmanUser
+ run()
+ createEncoding
(fileScanner: Scanner): String
+ saveEncodedMessage (
printWriter: PrintWriter,
inFilePath: String,
fileScanner: Scanner)
Entry Huffman
+ getCodes(): String
} // method main
while (!pathsOK)
{
try
{
System.out.print (IN_FILE_PROMPT);
inFilePath = keyboardScanner.nextLine();
fileScanner = new Scanner(new File (inFilePath));
System.out.print (OUT_FILE_PROMPT);
outFilePath = keyboardScanner.nextLine();
printWriter = new PrintWriter (new FileWriter (outFilePath));
pathsOK = true;
} // try
catch (IOException e)
{
System.out.println (e);
} // catch
} // while !pathOK
createEncoding (fileScanner, huffman);
saveEncodedMessage (printWriter, inFilePath, huffman);
} // method run
In that run method, the null assignments are needed to avoid a compile-time (“ . . . may not have been
initialized”) error for the arguments to the saveEncodedMessage method. That method is called outside
of the try block in which inFilePath and printWriter are initialized.
13.5 Application: Huffman Codes 589
In order to create the encoding, we will scan the input fil and update the frequencies line-by-line.
We then create the priority queue and Huffman tree, and calculate the Huffman codes. Most of those tasks
are handled in the Huffman class, so we can straightforwardly defin the createEncoding method:
public String createEncoding (Scanner fileScanner, Huffman huffman)
{
String line;
while (fileScanner.hasNextLine())
{
line = fileScanner.nextLine();
huffman.updateFrequencies (line);
} // while
fileScanner.close(); // re-opened in saveEncodedMessage
huffman.createPQ();
huffman.createHuffmanTree();
huffman.calculateHuffmanCodes();
return getCodes();
} // method createEncoding
For the saveEncodedMessage method, we firs write the codes to the output file and then, for each line
in the input file write the encoded line to the output file Again, the hard work has already been done in
the Huffman class. Here is the definitio of saveEncodedMessage:
public void saveEncodedMessage (PrintWriter printWriter, String inFilePath,
Huffman huffman)
{
String line;
try
{
printWriter.print (huffman.getCodes());
printWriter.println ("**"); // to separate codes from encoded message
Scanner fileScanner = new Scanner (new File (inFilePath));
while (fileScanner.hasNextLine())
{
line = fileScanner.nextLine();
printWriter.println (huffman.getEncodedLine (line));
} // while
printWriter.close();
} // try
catch (IOException e)
{
System.out.println (e);
} // catch IOException
} // method saveEncodedMessage
590 C H A P T E R 13 Priority Queues
Later, another user may want to decode the message. This can be done in two steps:
1. The encoding is read in and the Huffman tree is re-created. For this step, only the essential structure
of the Huffman tree is needed, so the Huffman class is not used and the Entry class will have only
three f elds:
Entry left,
right;
The root entry is created, and then each encoding is iterated through, which will create new entries
and, ultimately, a leaf entry.
2. The encoded message is read in, decoded through the Huffman tree, and the output is the decoded
message, which should be identical to the original message. Decoding the encoded message is the
subject of Programming Project 13.1.
SUMMARY
This chapter introduced the priority queue: a collection The PriorityQueue class’s methods can be
in which removal allowed only of the highest-priority ele- adapted to achieve the heapSort method, whose
ment in the sequence, according to some method for com- worstTime(n) is linear logarithmic in n and whose
paring elements. A priority queue may be implemented in worstSpace(n) is constant.
a variety of ways. The most widely used implementation An application of priority queues is in the area
is with a heap. A heap t is a complete binary tree such of data compression. Given a message, it is possible to
that either t is empty or encode each character, unambiguously, into a minimum
number of bits. One way to achieve such a minimum
1. the root element of t is smallest element in t, accord-
encoding is with a Huffman tree. A Huffman tree is a
ing to some method for comparing elements;
two-tree in which each leaf represents a distinct character
2. the left and right subtrees of t are heaps. in the original message, each left branch is labeled with
Because array-based representations of complete binary a 0, and each right branch is labeled with a 1. The Huff-
trees allow rapid calculation of a parent’s index from a man code for each character is constructed by tracing the
child’s index and vice versa, the heap can be represented path from that leaf character back to root, and pre-pending
as an array. This utilizes an array’s ability to randomly each branch label in the path.
access the element at a given index.
Crossword Puzzle 591
CROSSWORD PUZZLE
1 2
3
4
5 6
www.CrosswordWeaver.com
ACROSS DOWN
1. The main advantage of Heap Sort 1. The averageTime(n) for the add
over Merge Sort (E element) in the
PriorityQueue class
4. Why an array is used to implement
a heap 2. Every heap must be a _____
binary tree.
5. Every Huffman Tree is a
____________. 3. In a __________ algorithm, locally
optimal choices lead to a globally
6. A PriorityQueue object is not allowed optimal solution.
to have any ____ elements.
7. The field in the PriorityQueue
8. The prefix-free binary tree class that holds the elements
constructed from the priority queue
of character-frequency pairs
CONCEPT EXERCISES
13.1 Declare a PriorityQueue object—and the associated Comparator -implementing class—in which the
highest-priority element is the String object of greatest length in the priority queue; for elements of
equal length, use lexicographical order. For example, if the elements are “yes”, “maybe”, and “no”, the
highest-priority element would be “maybe”.
13.2 In practical terms, what is the difference between the Comparable interface and the Comparator inter-
face? Give an example in which the Comparator interface must be used.
13.3 Show the resulting heap after each of the following alterations is made, consecutively, to the following heap:
26
28 30
48 32 80 31
107 80 55 50
a. add (29);
b. add (30);
c. remove ();
d. remove ();
13.4 For the following character frequencies, create the heap of character-frequency pairs (highest priority =
lowest frequency):
a: 5,000
b: 2,000
c: 10,000
d: 8,000
e: 22,000
f: 49,000
g: 4,000
13.5 Use the following Huffman code to translate “faced” into a bit sequence:
a: 1100
b: 1101
c: 1111
d: 1110
e: 10
f: 0
Concept Exercises 593
13.6 Use the following Huffman tree to translate the bit sequence 11101011111100111010 back into letters ‘a’
. . . ‘g’:
(100000)
0 1
f (51000)
0 1
e (29000)
0 1
(11000) (18000)
0 1 0 1
a (6000) d c
0 1
b g
13.7 If each of the letters ‘a’ through ‘f’ appears at least once in the original message, explain why the following
cannot be a Huffman code:
a: 1100
b: 11010
c: 1111
d: 1110
e: 10
f: 0
13.8 Must a Huffman tree be a two-tree? Explain.
13.9 Provide a message with the alphabet ‘a’ . . . ‘e’ in which two of the letters have a Huffman code of 4 bits.
Explain why it is impossible to create a message with the alphabet ‘a’ . . . ‘e’ in which two of the letters
have a Huffman code of 5 bits. Create a message with the alphabet ‘a’ . . . ‘h’ in which all of the letters have
a Huffman code of 3 bits.
13.10 In Figure 13.16, the sum of the frequencies of all the non-leaves is 215,000. This is also the size of the
encoded message E. Explain why in any Huffman tree, the sum of the frequencies of all non-leaves is equal
to the size of the encoded message.
13.11 Give an example of a PriorityQueue object of ten unique elements in which, during the call to
remove(), the call to siftDown would entail only one swap of parent and child.
13.12 This exercise deals with Heap Sort, specifica ly, the number of iterations required to create a heap from an
array in reverse order. Suppose the elements to be sorted are in an array of Integer s with the following
int values:
c. Let n be one less than a power of 2—the complete binary tree will be full—and suppose the n elements
are in reverse order. If we let h = floo (log2 n), we can develop a formula for calculating the number of
while -loop iterations in the n/2 calls to siftDown. (For clarity, we will start calculating at the root
level, even though the calls to siftDown start at level h − 1.) At the root level, there is one element,
and the call to siftDown entails h loop iterations. At the next level down, there are two elements, and
each of the corresponding calls to siftDown entails h − 1 loop iterations. At the next lowest level down,
there are 4 elements, and each call to siftDown entails h − 2 iterations. And so on. Finally, at level
h − 1, the next-to-leaf level, there are 2h−1 elements, and each call to siftDown (i) entails one loop
iteration. The total number of while -loop iterations is:
h−1
1 ∗ h + 2 ∗ (h − 1) + 4 ∗ (h − 2) + 8 ∗ (h − 3) + . . . + 2h−1 ∗ 1 = 2i (h − i )
i =0
Show that this sum is equal to n − floo (log2 n) − 1; there are also n/2 calls to siftDown. That is, for
creating a heap from a full binary tree, worstTime(n) is linear in n.
Hint: By Exercise A2.6 in Appendix 2,
h−1
2i = 2h − 1
i =0
By Exercise A2.3 in Appendix 2,
h−1
i 2i = (h − 2)2h + 2
i =0
By the Binary Tree Theorem, if t is a (non-empty) full binary tree of n elements and height h,
(n + 1)/2 = 2h
d. Let t be a complete binary tree with n elements. Show that to make a heap from t , worstTime(n) is linear
in n.
Hint: Let h be the height of t . For the number of while -loop iterations in siftDownComparable
or siftDownComparator, compare the number of iterations, in the worst case, to make a heap from
t with
i. the number of iterations, in the worst case, to make a heap from the full binary tree t1 of height h − 1,
and
ii. the number of iterations, in the worst case, to make a heap from the full binary tree t2 of height h.
PROGRAMMING EXERCISES
13.1 In Section 13.3.1, the PriorityQueueExample class creates a heap, pq1, of Student objects; the
Student class implements the Comparable interface. Re-write the project so that the Comparator
interface is implemented instead for pq1. Re-run the project to confir your revisions.
13.2 Conduct a run-time experiment to support the claim that, on average, Heap Sort takes more time and uses
less space than Merge Sort.
13.3 Conduct a run-time experiment to support the claim that averageTime(n) is constant for the add method in the
PriorityQueue class. Hint: Copy the PriorityQueue class into one of your directories; replace the
line package java.util; with import java.util.*;. Add a siftUpCount fi ld, initialize it to
0, and increment it by 1 during the while loop in siftUpComparable (and in siftUpComparator).
Defin a public getSiftUpCount method that returns the value of siftUpCount. Create another class
whose run method scans in a value for n, initializes a PriorityQueue<Integer> object with an initial
capacity of n, f lls in that PriorityQueue object with n randomly generated integer values, and prints out
the value of siftUpCount divided (not integer division) by n. That quotient should be slightly less than 2.3.
Programming Exercises 595
Analysis For the entire project, worstTime(n) must be O(n), where n is the size of the original message. As shown in
the output file in the Huffman project of this chapter, the input file will consist of two parts:
A sample input file huffman.ou1, is shown in the following. The f rst line contains the carriage-return character,
followed by a space, followed by its encoding. The carriage-return character, when viewed, forces a line-feed.
So the f rst line below is blank, and the second starts with a space, followed by the code for the carriage-return
character.
0010
0111
a 000
b1
c 0011
d 010
e 0110
**
1000010011100110110010011001110011000101110100001001001001100000100111
010000010011100110000100010111111111111111111111111111110010
Suppose the fil huffman.ou2 contains the following (the message is from Coleridge’s “Rubiyat of Omar Khayam”):
10001
101
, 001000
- 0100000
. 0100001
: 0010100
A 001011
D 0100110
I 0100101
K 001110
T 0011011
W 0010101
X 0011000
a 000
b 0011001
c 110010
d 11110
e 011
g 0011010
h 11111
i 001111
l 11101
m 110011
n 0101
o 11000
p 001001
r 1001
s 1101
t 10000
u 11100
v 010001
w 0100111
y 0100100
**
01001010101101001100000001010001111011100101111100011111
11101010011101110000110011110100010100111011111000010110
00100101110111011000000010000011111010100100101001001111
01011000110111100100101101000001111011000110011011101111
10011110010100101101100101001000100101011111101110010111
01001011111010010011111100100010110000111110111011101000
11001010010111111010110010011110100010111001001000101100
10000101100010011011111111001110001110000110101111110111
00100000100010111001010111011011100110110001101111001001
01111101011110111011011000011000101110011000010110001010
01101100001001110101101100001100010100010111011110001011
1101011110111011011101011000010000110001
System Test 2:
Please enter the name of the input file huffman.ou2
Note: For the sake of overall efficiency re-creating the Huffman tree will be better than sequentially searching
the codes for a match of each sequence of bits in the encoded message.
In saving the results, the data structure must satisfy the following:
to insert a result into the data structure, worstTime(n) must be O(n), and averageTime(n) must be constant;
to remove a result from the data structure, worstTime(n) and averageTime(n) must be O(log n);
browser.in6 7
browser.in8 2
browser.in7 0
System Test 2: (Assume search.in1 contains browser.in10, 11, 12, 13 –shown below)
Please enter a search string in the input line and then press the Enter key.
neural network
neural network
browser.in10 8
browser.in13 8
browser.in11 6
browser.in12 6
browser.in11:
In Xanadu did <a href=browser.in1>browser1</a> Kubla Khan
A stately pleasure-dome decree:
Where Alph, the neural network sacred river, ran
Through caverns neural network measureless to man
Down to a network sunless sea.
network
browser.in12:
Neural surgeons have a network. But the decree is a decree is
a network and a network is a network, neural or not.
browser.in13:
Note 1: In your code, do not use complete path names, such as “h:\Project3\home.in1”. Instead, use “home.in1”.
Hashing CHAPTER 14
We start this chapter by reviewing some search algorithms from earlier chapters as a prelude to the
introduction of a new search technique: hashing. The basic idea with hashing is that we perform some
simple operation on an element’s key to obtain an index in an array. The element is stored at that index.
With an appropriate implementation, hashing allows searches (as well as insertions and removals)
to be performed in constant average time. Our primary focus will be on understanding and using
the HashMap class in the Java Collections Framework, but we will also consider other approaches to
hashing. The application will use hashing to insert identifiers into a symbol table.
CHAPTER OBJECTIVES
1. Understand how hashing works, when it should be used, and when it should not be used.
2. Explain the significance of the Uniform Hashing Assumption.
3. Compare the various collision handlers: chaining, offset-of-1, quotient-offset.
599
600 C H A P T E R 14 Hashing
...
The contains method in the ArrayList class and the containsValue method in the TreeMap class
use sequential searches similar to the above.
For a successful sequential search of a collection, we assume that each of the n elements in
the collection is equally likely to be sought. So the average number of loop iterations is (1 + 2 + . . .
+ n)/n, which is (n + 1)/2, and the largest possible number of loop iterations is n. We conclude that both
averageTimeS (n) and worstTimeS (n) are linear in n.
14.2 Review of Searching 601
For an unsuccessful sequential search, even if the collection happens to be ordered, we must access
all n elements before concluding that the given element is not in the collection, so averageTimeU (n) and
worstTimeU (n) are both linear in n. As we will see in Sections 14.2.2 and 14.2.3, ordered collections can
improve on these times by employing non-sequential searches.
/**
* Searches a specified array for a specified element.
* The array must be sorted in ascending order according to the natural ordering
* of its elements (that is, by the compareTo method); otherwise, the results are
* undefined.
* The worstTime(n) is O(log n).
*
* @param a –the array to be searched.
* @param key –the element to be searched for in the array.
*
* @return the index of an element equal to key - if the array contains at least one
* such element; otherwise, –insertion point –1, where insertion point is
* the index where key would be inserted. Note that the return value is
* greater than or equal to zero only if the key is found in the array.
*
*/
public static int binarySearch (Object[ ] a, Object key)
{
int low = 0;
int high = a.length-1;
if (cmp < 0)
low = mid + 1;
else if (cmp > 0)
high = mid - 1;
else
return mid; // key found
} // while
return –(low + 1); // key not found
} // method binarySearch
A binary search is much faster than a sequential search. For either a successful or unsuccessful search, the
number of elements to be searched is n = high + 1 - low. The while loop will continue to divide n
602 C H A P T E R 14 Hashing
by 2 until either key is found or high + 1 = low, that is, until n = 0. The number of loop iterations
will be the number of times n can be divided by 2 until n = 0. By the Splitting Rule in Chapter 3, that
number is, approximately, log2 n (also, see Example A2.2 of Appendix 2). So we get averageTimeS (n) ≈
worstTimeS (n) ≈ averageTimeU (n) ≈ worstTimeU (n), which is logarithmic in n.
There is also a Comparator -based version of this method; the heading is
public static <T> int binarySearch (T[ ] a, T key, Comparator<? super T> c)
For example, if words is an array of String objects ordered by the length of the string, we can utilize
the ByLength class from Section 11.3 and call
System.out.println (Arrays.binarySearch (words, "misspell", new ByLength()));
The height of a red-black tree t is logarithmic in n, the number of elements (see Example 2.6 in Appendix
2). For an unsuccessful search, the getEntry method iterates from the root to an empty subtree. In the
worst case, the empty subtree will be a distance of height(t) branches from the root, and the number of
iterations will be logarithmic in n. That is, worstTimeU (n) is logarithmic in n.
The number of iterations in the average case depends on bh(root): the number of black elements in
the path from the root to an element with no children or with one child. The path length from the root
to such an element is certainly less than or equal to the height of the tree. As shown in Example 2.6 of
Appendix 2, bh(root) ≥ height(t)/2. So the number of iterations for an unsuccessful search is between
height(t)/2 and height(t). That is, averageTimeU (n) is also logarithmic in n.
14.3 The HashMap Implementation of the Map Interface 603
For a successful search, the worst case occurs when the element sought is a leaf, and this requires
only one less iteration than an unsuccessful search. That is, worstTimeS (n) is logarithmic in n. It is
somewhat more diff cult to show that averageTimeS (n) is logarithmic in n. But the strategy and result are
the same as in Concept Exercise 5.7, which showed that for the recursive version of the binarySearch
method, averageTimeS (n) is logarithmic in n.
Section 14.3 introduces a class that allows us to break through the log n barrier for insertions,
removals and searches.
Here is an example of the creation of a simple HashMap object. The entire map, both keys and values, is
printed. Then the keys alone are printed by iterating through the map viewed as a Set object with keys as
elements. Finally, the values alone are printed by iterating through the map as if it were a Collection
of values.
import java.util.*;
} // class HashExample
Notice that the output is not in order of increasing keys, nor in order of increasing values. (When we get to
the details of the HashMap class, we’ll see how the order of elements is determined.) This unsortedness is
one of the few drawbacks to the HashMap class. The HashMap class is outstanding for insertions, removals,
and searches, on average, but if you also need a sorted collection, you should probably use a TreeMap
instead.
In trying to decide on field for the HashMap class, you might at firs be tempted to bring back
a contiguous or linked design from a previous chapter. But if the elements are unordered, we will need
sequential searches, and these take linear time. Even if the elements are ordered and we utilize that ordering,
the best we can get is logarithmic time for searching.
The rest of this chapter is devoted to showing how, through the miracle of hashing, we can achieve
searches—and insertions and removals—in constant time, on average. After we have define what hashing
is and how it works, we will spend a little time on the Java Collection Framework’s implementation of
the HashMap class. Then we will consider an application—hashing identifier into a symbol table—before
we consider alternate implementations.
14.3.1 Hashing
We have already ruled out a straightforward contiguous design, but just to ease into hashing, let’s look at
a contiguous design with the following two f elds:
transient Entry[ ] table; // an array of entries;
Recall, from Chapter 6, that the transient modifie indicates that the f eld will not be saved during
serialization. Appendix 1 includes a discussion of serialization.
We firs present a simple example and then move on to something more realistic. Suppose that the
array is constructed to hold 1024 mappings, that is, 1024 key&value pairs. The key is (a reference to) an
Integer object that contains a three-digit int for a person’s ID. The value will be a String holding a
person’s name, but the values are irrelevant to this discussion, so they will be ignored. We start by calling
a constructor that initializes the table length to 1024:
HashMap<Integer, String> persons = new HashMap<Integer, String>(1024);
14.3 The HashMap Implementation of the Map Interface 605
1 null
2 null
.
.
.
1023 null
FIGURE 14.1 The design representation of an empty HashMap object (simplifie design)
At what index should we store the element with key 251? An obvious choice is index 251. The
advantages are several:
a. The element can be directly inserted into the array without accessing any other elements.
b. Subsequent searches for the element require accessing only location 251.
c. The element can be removed directly, without firs accessing other elements.
We now add three elements to this HashMap object:
persons.put (251, "Smolenski");
persons.put (118, "Schwartz");
persons.put (335, "Beh-Forrest");
Figure 14.2 shows the contents of the HashMap object. We show the int values, rather than the Integer
references, for the keys of inserted elements, and the names are omitted.
So far, this is no big deal. Now, for a slightly different application, suppose that table.length
is still 1024, but each element has a social-security-number as the key. We need to transform the key
into an index in the array, and we want this transformation to be accomplished quickly, that is, with
few loop iterations. To allow fast access, we perform a bit-wise “and” of the social security number and
table.length - 1. For example, the element with a key value of 214-30-3261—the hyphens are for
readability only—has a binary representation of
00001100110001100000001000011101
The binary representation of 1023 is
00000000000000000000001111111111
The result of applying the bit-wise and operator, & , to a pair of bits is 1 if both bits are 1, and otherwise
0. We get
00001100110001100000001000011101
& 00000000000000000000001111111111
00000000000000000000001000011101
The decimal value of the result is 541, and so the element whose key is 214-30-3261 is stored at index
541. Similarly, the element with a key value of 033-51-8000 would be stored at location 432.
606 C H A P T E R 14 Hashing
table size
0 null 3
1 null
2 null
118 118
251 251
335 335
1023 null
FIGURE 14.2 The HashMap object of Figure 14.1 after three elements have been inserted. For each non-null
element, all that is shown is the int corresponding to the Integer key. The value parts are omitted.
Figure 14.3 shows the resulting HashMap object after the following two insertions:
persons.put (214303261, "Albert");
persons.put (033518000, "Schranz");
You might already have noticed a potential pitfall with this scheme: two distinct keys might produce the
same index. For example, 214-30-3261 and 323-56-8157 both produce the index 541. Such a phenomenon
is called a collision, and the colliding keys are called synonyms. We will deal with collisions shortly. For
now we simply acknowledge that the possibility of collisions always exists when the size of the key space,
that is, the number of legal key values, is larger than the table capacity.
Hashing is the process of transforming a key into an index in a table. One component of this trans-
formation is the key class’s hashCode() method. The purpose of this method is to perform some easily
computable operation on the key object. The key class, which may be Integer, String, FullTimeEm
ployee, or whatever, usually define its own hashCode() method. Alternatively, the key class can inherit
a hashCode() method from one of its ancestors; ultimately, the Object class define a hashCode()
method.1 Here is the definitio of the String class’s hashCode() method:
/**
* Returns a hash code for this String object.
*
1 Butthe hashCode() method in the Object class will most likely return the reference itself, that is, the machine address, as an int . Then
the hashCode() method applied to two equivalent objects would return different int values!
14.3 The HashMap Implementation of the Map Interface 607
table size
0 null 2
1 null
2 null
432 033518000
541 214303261
1023 null
FIGURE 14.3 A HashMap object with two elements. The key is a social security number. The value is a name,
but that is irrelevant, so it is not shown
return h;
} // method hashCode
The multiplication of partial sums by 31 increases the likelihood that the int value returned will be greater
than table.length, so the resulting indexes can span the entire table. For example, suppose we have
System.out.println ("graduate".hashCode());
In the HashMap class, the hashCode() method for a key is supplemented by a static hash method whose
only parameter is the int returned by the hashCode() method. The hash function scrambles that int
value. In fact, the basic idea of hashing is to “make hash” out of the key. The additional scrambling is
accomplished with a few right shifts and bit-wise “exclusive-or” operators (return 1 if the two bits are
different, otherwise 0).
Here is the hash method:
static int hash(int h)
{
h ∧= (h >>> 20) ∧ (h >>> 12);
return h ∧ (h >>> 7) ∧ (h >>> 4);
} // method hash
For example,
"graduate".hashCode()
returns
90004811
This value is bit-wise “anded” with table.length - 1 to obtain an index in the range from 0 to
table.length - 1. For example, if table.length = 1024,
returns
598
And that is the index “graduate” hashes to. The overall strategy is
hash (key.hashCode()) & table.length - 1
key −−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−→ index
One of the requirements of the HashMap class is that table.length must be a power of 2. (This
requirement is specifi to the Java Collections Framework, and does not apply generally.) For example, if
table.length = 1024 = 210 , then the index returned by
consists of the rightmost 10 bits of hash (key). Because a poor-quality hashCode() method may not
adequately distinguish the rightmost bits in the keys, the additional scrambling from the bit-wise operators
in the hash method is usually enough to prevent a large number of collisions from occurring.
You may have noticed that, because the table size is a power of 2, the result of
hash (key.hashCode()) & table.length - 1
The advantage of the former expression is that its calculation is quite a bit faster than for the latter expres-
sion. Modular arithmetic is computationally expensive, but is commonly utilized in hashing (outside of the
Java Collections Framework) because its effectiveness does not require that the table size be a power of 2.
How would you go about developing your own hashCode() method? To see how this might be done,
let’s develop a simple hashCode() method for the FullTimeEmployee class from Chapter 1. The method
should involve the two field — name and grossPay —because these distinguish one FullTimeEmployee
object from another. We have already seen the String class’s hashCode() method. All of the wrapper
classes, including Double, have their own hashCode() method, so we are led to the following method:
public int hashCode()
{
return name.hashCode() + new Double (grossPay).hashCode();
} // method hashCode in class FullTimeEmployee
For example, if a FullTimeEmployee object has the name “Dominguez” and a gross pay of 4000.00,
the value returned by the hashCode() method will be −319687574. When this hashCode() method is
followed by the additional bit manipulation of the HashMap class’s hash method, the result will probably
appear to be quite random.
The f nal ingredient in hashing is the collision handler, which determines what happens when two
keys hash to the same index. As you might expect, if a large number of keys hash to the same index, the
performance of a hashing algorithm can be seriously degraded. Collision handling will be investigated in
Section 14.3.3. But f rst, Section 14.3.2 reveals the major assumption of hashing: basically, that there will
not be a lot of keys hashing to the same index.
Hashing is most eff cient when the values of index are spread throughout the table. This notion is referred
to as the Uniform Hashing Assumption. Probabilistically speaking, the Uniform Hashing Assumption
states that the set of all possible keys is uniformly distributed over the set of all table indexes. That is,
each key is equally likely to hash to any one of the table indexes.
No class’s hashCode method will satisfy the Uniform Hashing Assumption for all applications,
although the supplemental scrambling in the hash function makes it extremely likely (but not guaranteed)
that the assumption will hold.
Even if the Uniform Hashing Assumption holds, we must still deal with the possibility of collisions.
That is, two distinct keys may hash to the same index. In the HashMap class, collisions are handled
by a simple but quite effective technique called “chaining”. Section 14.3.3 investigates chaining, and an
alternate approach to collisions is introduced in Section 14.5.
14.3.3 Chaining
To resolve collisions, we store at each table location the singly-linked list of all elements whose keys
have hashed to that index in table. This design is called chained hashing because the elements in each
list form a chain. We still have the table and size field from Section 14.3.1:
/**
* An array; at each index, store the singly-linked list of entries whose keys hash
610 C H A P T E R 14 Hashing
* to that index.
*
*/
transient Entry[ ] table;
/**
* The number of mappings in this HashMap object.
*/
transient int size;
Each element is stored in an Entry object, and the embedded Entry class starts as follows:
static class Entry<K,V> implements Map.Entry<K,V>
{
final K key;
V value;
The final modifie mandates that the key and hash field can be assigned to only once. A singly-linked
list, instead of the LinkedList class, is used to save space: There is no header, and no previous field
The Entry class also has a four-parameter constructor to initialize each of the f elds.
To see how chained hashing works, consider the problem just stated of storing persons with social
security numbers as keys. Since each key is (a reference to) an Integer object, the hashCode() method
simply returns the corresponding int, but the hash method suff ciently garbles the key that the int
returned seems random. Initially, each location contains an empty list, and insertions are made at the front of
the linked list. Figure 14.4 shows what we would have after inserting elements with the following Integer
keys (each is shown as an int, followed by the index the key hashed to) into a table of length 1024:
key index
62488979 743
831947084 440
1917270349 911
1842336783 208
1320358464 440
1102282446 319
1173431176 440
33532452 911
The keys in Figure 14.4 were “rigged” so that there would be some collisions. If the keys were chosen
randomly, it is unlikely there would have been any collisions, so there would have been eight linked lists,
each with a single entry. Of course, if the number of entries is large enough, there will be collisions and
multi-entry lists. And that raises an interesting question: Should the table be subject to re-sizing? Suppose
the initial table length is 1024. If n represents the number of entries and n can continue to increase, the
average size of each linked list will be n/1024, which is linear in n. But then, for searching, inserting, and
removing, worstTime(n) will be linear in n —a far cry from the constant time that was promised earlier.
14.3 The HashMap Implementation of the Map Interface 611
table Size
0 null 8
831947084 null
FIGURE 14.4 A HashMap object into which eight elements have been inserted. For each Entry object, only the
key and next field are shown. At all indexes not shown there is a null Entry.
Given that re-sizing must be done under certain circumstances, what are those circumstances? We
will re-size whenever the size of the map reaches a pre-set threshold. In the HashMap class, the default
threshold is 75% of table.length. So when size is >= (int)(table.length * 0.75), the table
will be re-sized. That means that the average size of each linked list will be less than one, and that is how
constant average time can be achieved for inserting, deleting, and searching.
There are two additional f elds relating to this discussion: loadFactor (how large can the ratio
of size to table length get before resizing will occur) and threshold (how large can size get before
resizing will occur). Specifically we have
/**
* the maximum2 ratio of size / table.length before re-sizing will occur
*
*/
final float loadFactor
/**
* (int)(table.length * loadFactor); when size++ >= threshold, re-size table
*
*/
int threshold;
2 This definitio is non-standard. In hashing terminology, load factor is simply the ratio of the number of elements in the collection to its
capacity. In the Framework’s HashMap and HashSet classes, load factor is the upper bound of that ratio.
612 C H A P T E R 14 Hashing
/**
* The maximum capacity, 2 to the 30th power, used if a higher value
* is implicitly specified by either of the constructors with arguments.
*
*/
static final int MAXIMUM_CAPACITY = 1 << 30; // = 230
/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
The value for loadFactor presents an interesting time-space tradeoff. With a low value (say, less than
1.0), searches and removals are fast, and insertions are fast until an insertion triggers a re-sizing. That
insertion will require linear-in-n time and space. On the other hand, with a high value for loadFactor,
searches, removals, and insertions will be slower than with a low loadFactor, but there will be fewer
re-sizings. Similary, if table.length is large, there will be fewer re-sizings, but more space consumed.
Ultimately, the application will dictate the need for speed (how small a load factor) and the memory
available (how large a table).
Now that we have decided on the field in the HashMap and Entry classes, we can don our devel-
oper’s hat to tackle the implementation of a couple of HashMap methods: a constructor and containsKey.
this.loadFactor = loadFactor;
threshold = (int)(capacity * loadFactor);
table = new Entry[capacity];
init(); // allows readObject to handle subclasses of HashMap (Appendix 1, A1.2)
}
This constructor is utilized, for example, in the definitio of the default constructor:
/**
* Constructs an empty <tt>HashMap</tt> with the default initial capacity
* (16) and the default load factor (0.75).
*/
public HashMap()
{
this.loadFactor = DEFAULT_LOAD_FACTOR;
threshold = (int)(DEFAULT_INITIAL_CAPACITY *
DEFAULT_LOAD_FACTOR);
table = new Entry[DEFAULT_INITIAL_CAPACITY];
init();
}
We finis up this section by looking at the definitio of the containsKey method. The other signature
methods for a HashMap object— put, get, and remove —share the same basic strategy as containsKey:
hash the key to an index in table, and then search the linked list at table [index] for a matching
key. The containsKey method simply calls the getEntry method:
/**
* Determines if this HashMap object contains a mapping for the
* specified key.
* The worstTime(n) is O(n). If the Uniform Hashing Assumption holds,
* averageTime(n) is constant.
*
* @param key The key whose presence in this HashMap object is to be tested.
*
* @return true - if this map contains a mapping for the specified key.
*
*/
public boolean containsKey(Object key)
{
return getEntry(key) != null;
}
614 C H A P T E R 14 Hashing
Here is the definitio of the getEntry method (the indexFor method simply returns hash &
table.length - 1):
/**
* Returns the entry associated with the specified key in the
* HashMap. Returns null if the HashMap contains no mapping
* for the key.
*/
final Entry<K,V> getEntry(Object key)
{
int hash = (key == null) ? 0 : hash(key.hashCode());
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next)
{
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
Notice that both the hash and key field are compared. Concept Exercise 14.4 indicates why both f elds
are checked.
Before we leave this section on the implementation of the HashMap class, there is an important detail
about insertions that deserves mention. When a re-sizing takes place, that is, when
size++ >= threshold
the table is doubled in size. But the old entries cannot simply be copied to the new table: They must be
re-hashed. Why? Because the index where an entry is stored is calculated as
hash & table.length –1
When table.length changes, the index of the entry changes, so a re-hashing is required. For example,
hash ("myList") & 1023 // 1023 = 210 –1
returns 1255.
After the keys have been re-hashed, subsequent calls to the containsKey method, for example, will
search the linked list at the appropriate index.
In Section 14.3.5, we show that the containsKey method takes only constant time, on average.
a successful search will examine a list that has about n/m elements3 , on average. A successful, sequential
search of such a list requires, approximately, n/2m loop iterations, so the average time appears to depend
on both n and m. We get
The value of loadFactor is set in the HashMap object’s constructor, and is f xed for the lifetime of
the HashMap object. That is, averageTimeS (n, m) is less than a constant. Therefore averageTimeS (n, m)
must be constant (in the “plain English” sense of notation). In other words, the averageTime is inde-
pendent of m (and even independent of n). So we conclude that averageTimeS (n) is constant. Similarly,
averageTimeU (n) is constant. We will avoid the subscript and simply write averageTime(n) whenever the
estimates are the same for both successful and unsuccessful searches. See Section 14.5 for a situation
where the estimates are different.
So far we have blithely ignored any discussion of worstTime. This is the Achilles’ heel of hashing,
just as worstTime was the vulnerable aspect of Quick Sort. The Uniform Hashing Assumption is more
often a hope than a fact. If the hashCode method is inappropriate for the keys in the application, the
additional scrambling in the hash function may not be enough to prevent an inordinate number of keys
to hash to just a few locations, leading to linear-in-n searches. Even if the Uniform Hashing Assumption
holds, we could have a worst-case scenario: the given key hashes to index, and the number of keys
at table [index] is linear in n. So worstTime (n, m), for both successful and unsuccessful searches,
is linear in n. The independence from m allows us to write that worstTime(n) is linear in n for both
successful and unsuccessful searches for the containsKey method.
Similarly, for the get and remove methods, worstTime(n) is linear in n. What about
worstTime(n, m) for the put method? In the worst case, the size of the underlying table will be at the
threshold, so we will need to double the size of the table, and then iterate through all m of the linked lists
(many will be empty) to re-hash the n elements into the new table. So it appears that worstTime(n, m)
is (n + m). But at the threshold, n/m = loadFactor (a constant), so m = n/loadFactor. Then
worstTime(n, m) = worstTime(n) = (n + n/loadFactor), since m is a function of n. We conclude
that worstTime(n) is (n), or in plain English, worstTime(n) is linear in n.
The bottom line is this: unless you are confiden that the Uniform Hashing Assumption is reasonable
for the key space in the application or you are not worried about linear-in-n worst time, use a TreeMap
object, with its guarantee of logarithmic-in-n worst time for searching, inserting, and deleting.
The f nal topic in our introduction to the HashMap class is the HashIterator class.
This section will clear up a mystery from when we f rst introduced the HashMap class: in what order
are iterations performed? The answer is fairly straightforward. Starting at index table.length-1 and
working down to index 0, the linked list at each index is traversed in sequential order. Starting at the
back makes the continuation condition slightly more efficient index > 0 (a constant) instead of index
< table.length (a variable). In each singly-linked list, the sequential order is last-in-first-ou . For
example, Figure 14.5 (next page) is a repeat of the HashMap object from Figure 14.4.
3 In common hashing terminology, the load factor is simply the ratio of n to m. In the Java Collections Framework, load factor is the maximum
table Size
0 null 8
831947084 null
FIGURE 14.5
Just as with the TreeMap class, the iteration can be by keys, by values, or by entries. For example:
for (K key : myMap.keySet())
for (V value : myMap.values())
for (Map.Entry<K, V> entry : myMap.entrySet())
An iteration through a HashMap object must peruse the singly linked list at each index in the table. Even if
most of those linked lists are empty, each one must be checked. The total number of loop iterations is size
+ table.length, and this indicates a drawback to having a very large table. If the Uniform Hashing
14.3 The HashMap Implementation of the Map Interface 617
Assumption holds, then the time estimates for the next() method are the same as for the containsKey
method in the HashMap class. In the terminology of Section 14.3.5, for example,
Since loadFactor is fixed we conclude that averageTime(n, m) = averageTime(n) is constant for the
next() method.
In the worst case, a call to the next() method will start at the end of the linked list at table
[table.length - 1], with empty linked lists at indexes table.length - 2, table.length - 3,
. . ., 2, 1, and a non-empty linked list at table [0]. We conclude that worstTime(n, m) is linear in m.
If you would like to have control about the order in which elements in a hash map are iterated over,
Programming Exercise 14.6 will show you the way.
In Section 14.3.7, we look at the premiere application of the HashMap class: the hashing of identifier
into a symbol table by a compiler.
Maintaining a symbol table is a good environment for hashing: Almost all of the activity consists of
retrieving a symbol-table entry, given a key. And a reasonable upper bound on the number of identifier
is the size of the program, so we can initialize the size of the symbol table to avoid re-sizing.
To avoid getting bogged down in the myriad details of symbol-table creation and maintenance, we
will solve the following restricted problem: Given a f le of reserved words and a program in a source file
print to a fil a symbol table of word symbols, with each word symbol designated as a reserved word or
identifier One interesting feature of this project is that the input is itself a program.
The main issue is how to separate identifier from reserved words. The list of legal reserved words4
is fixed so we will read in a f le that contains the reserved words before we read in the source file For
example, suppose the fil “reserved.dat” contains the list, one per line, of reserved words in Java. And the
fil “Sample.java” contains the following program:
import java.io.*;
are literals. But we include them in with the reserved words because they cannot be used as identifiers
618 C H A P T E R 14 Hashing
{
public static void main (String[ ] args)
{
int i = 75;
Integer top,
bottom;
/* all */ int z1 = 1;
/* int z2 = 2;
int z3 = 3;
int z4 = 4;
*/
int z5 = 5;
/* int z6 = 6;*/
String ans = "All string literals, such as this, are ignored.";
char x = ’x’;
for (int j = 0; j < i; j++)
System.out.println (i + " " + j);
} // method main
} // class Sample
At the end of the execution of the program the f le hasher.out will have:
Here is the symbol table:
ans=identifier
top=identifier
boolean=reserved word
interface=reserved word
rest=reserved word
for=reserved word
continue=reserved word
long=reserved word
abstract=reserved word
double=reserved word
instanceof=reserved word
println=identifier
throws=reserved word
super=reserved word
throw=reserved word
short=reserved word
do=reserved word
byte=reserved word
import=reserved word
if=reserved word
future=reserved word
package=reserved word
switch=reserved word
14.3 The HashMap Implementation of the Map Interface 619
catch=reserved word
return=reserved word
x=identifier
outer=reserved word
String=identifier
System=identifier
z5=identifier
transient=reserved word
out=identifier
j=identifier
synchronized=reserved word
else=reserved word
args=identifier
while=reserved word
Double=identifier
goto=reserved word
_yes=identifier
var=reserved word
extends=reserved word
operator=reserved word
Integer=identifier
case=reserved word
final=reserved word
Sample=identifier
native=reserved word
null=reserved word
$name=identifier
float=reserved word
class=reserved word
implements=reserved word
private=reserved word
false=reserved word
main=identifier
char=reserved word
volatile=reserved word
const=reserved word
cast=reserved word
bottom=identifier
protected=reserved word
this=reserved word
static=reserved word
generic=reserved word
i=identifier
z1=identifier
void=reserved word
int=reserved word
byvalue=reserved word
break=reserved word
new=reserved word
default=reserved word
620 C H A P T E R 14 Hashing
inner=reserved word
true=reserved word
public=reserved word
finally=reserved word
try=reserved word
We will filte out comments, string literals, and import statements. Then each line (what remains of it) will
be tokenized. The delimiters will include punctuation, parentheses, and so on. For the sake of simplicity,
both reserved words and identifier will be saved in a symbol table, implemented as a HashMap object.
Each key will be a word, either a reserved word or an identifier and each value will be an indication of
the word’s type, either “reserved word” or “identifier”
We will create a Hasher class to solve this problem. The responsibilities of the Hasher class are
as follows:
1. Scan the reserved words and hash each one (with “reserved word” as the value part) to the symbol
table.
2. Scan the lines from the source file For each identifie in each line, post the identifie (with “identifier
as the value part) to the symbol table, unless that identifie already appears as a key in the symbol
table.
3. For each mapping in the symbol table, print the mapping to the output file
/**
* Reads the source file and posts identifiers to the symbol table.
* The averageTime(n, m) is O(n), and worstTime(n, m) is O(n * n), where
* n is the number of identifiers and m is the size of the symbol table.
*
* @param sourceFileScanner –a scanner over the source file.
* contains the reserved words.
* @param symbolTable - the HashMap that holds the symbol table.
*/
public void readSourceCode (Scanner sourceFileScanner,
* HashMap<String, String> symbolTable)
/**
* Outputs the symbol table to a file.
14.3 The HashMap Implementation of the Map Interface 621
The Hasher class has two constant identifier and one field
protected final String IDENTIFIER = "identifier";
Figure 14.6 has the class diagram for the Hasher class:
Hasher
FIGURE 14.6
622 C H A P T E R 14 Hashing
Scanner keyboardScanner,
reservedFileScanner = null,
sourceFileScanner = null;
while (!pathsOK)
{
try
{
keyboardScanner = new Scanner (System.in);
System.out.print (RESERVED_FILE_PROMPT);
String reservedFilePath = keyboardScanner.nextLine();
reservedFileScanner = new Scanner (new File (reservedFilePath));
System.out.print (SOURCE_FILE_PROMPT);
String sourceFilePath = keyboardScanner.nextLine();
File sourceFile = new File (sourceFilePath);
sourceFileScanner = new Scanner (sourceFile);
System.out.print (SYMBOL_TABLE_FILE_PROMPT);
String symbolTablePrintPath = keyboardScanner.nextLine();
symbolTablePrintWriter = new PrintWriter (
new FileWriter (symbolTablePrintPath));
pathsOK = true;
} // try
catch (Exception e)
{
System.out.println (e);
14.3 The HashMap Implementation of the Map Interface 623
} // catch
} // while !pathsOK
readReservedWords (reservedFileScanner, symbolTable);
readSourceCode (sourceFileScanner, symbolTable);
printSymbolTable (symbolTablePrintWriter, symbolTable);
} // method run
while (true)
{
if (!reservedFileScanner.hasNextLine())
break;
reservedWord = reservedFileScanner.nextLine();
symbolTable.put (reservedWord, RESERVED_WORD);
} // while not end of file
} // method readReservedWords
It is easy enough to determine if a token is the firs occurrence of an identifier It must start with a letter,
and not already be in symbolTable (recall that the reserved words were posted to symbolTable earlier).
The hard part of the method is filterin out comments, such as in the following:
/* all */ int z1 = 1;
/* int z2 = 2;
int z3 = 3;
int z4 = 4; */ int z5 = 5; /* int z6 = 6;*/
String line,
word;
int start,
finish;
while (true)
{
if (!sourceFileScanner.hasNextLine())
624 C H A P T E R 14 Hashing
break;
line = sourceFileScanner.nextLine();
line = line.trim();
// Ignore // comments
if ((start = line.indexOf("//")) >= 0)
line = line.substring(0, start);
return symbolTable.toString();
} // method readSourceCode
Let n be the number of identifier in the source file and let m be the size of the source file Because
m is fixed only n is relevant for estimating. Based on the analysis of containsKey method in Section
14.3.5, we extrapolate that each call to the HashMap methods get and put will take constant time on
average, and linear-in-n time in the worst case. We conclude that for processing all n identifier in the
readSourceCode method, averageTime(n) is linear in n and worstTime(n) is quadratic in n.
Finally, the printSymbolTable method iterates through the entries in symbolTable and prints
each one to the output file
public void printSymbolTable (PrintWriter symbolTablePrintWriter,
HashMap<String, String> symbolTable)
{
final String HEADING = "Here is the symbol table:\n";
symbolTablePrintWriter.println (HEADING);
for (Map.Entry<String, String> entry : symbolTable.entrySet())
symbolTablePrintWriter.println (entry);
} // method printSymbolTable
An iteration through a HashMap object requires n + m iterations in all cases, so worstTime(n, m) and
averageTime(n, m) are both (n + m). We can say, crudely, that n + m is both a lower bound and an
upper bound of worstTime(n, m) and averageTime(n, m) for the printSymbolTable method.
One interesting feature of the above program is that Hasher.java itself can be the input file The
output fil will then contain the symbol table of all word symbols in the Hasher class.
The word symbols in the output fil are not in order. To remedy this, instead of printing out each
entry, we could insert the entry into a TreeMap object, and then print out the TreeMap object, which
would be in order. This sorting would take linear-logarithmic in n time; on average, that would be longer
than the rest of the application.
To finis up the Java Collections Framework treatment of hashing, the next section takes a brief look
at the HashSet class.
names.add ("Kihei");
names.add ("Kula");
626 C H A P T E R 14 Hashing
names.add ("Kaanapali");
System.out.println (names.contains ("Kapalua")); // Output: false
System.out.println (names.remove ("Kula") + " " + names.size()); // Output: true 2
Lab 22 covers the crucial aspect of HashMap objects and HashSet objects: their speed.
You are now prepared to do Lab 22: Timing the Hash Classes
As indicated, the Java Collections Framework’s implementation of hashing uses chaining to handle colli-
sions. Section 14.5 explores another important collision handler, one that avoids linked lists.
In this example, table.length is 1024, but in general, we will not require that the table length be a power
of two. In fact, we will see in Section 14.5.2 that we may want the table length to be a prime number.
There are a couple of minor details with open addressing:
a. to ensure that an open location will be found if one is available, the table must wrap around: if the
location at index table.length - 1 is not open, the next index tried is 0.
b. the number of entries cannot exceed the table length, so the load factor cannot exceed 1.0. It will
simplify the implementation and efficienc of the containsKey, put, and remove methods if the
table always has at least one open (that is, empty) location. So we require that the load factor be
strictly less than 1.0. Recall that with chaining, the load factor can exceed 1.0.
14.5 Open-Address Hashing (optional) 627
0 null
80 301-33-6785
212 033-52-0048
213 214-30-3261
214 214-30-3495
529 298-71-9753
754 587-71-1904
919 081-90-3292
920 735-66-8100
1023 null
FIGURE 14.7 A table to which 8 elements have been inserted. Open addressing, with an offset of 1, handles
collisions
Let’s see what is involved in the design and implementation of a HashMap class with open addressing
and an offset of 1. We’ll have many of the same f elds as in the chained-hashing design: table, size,
loadFactor, and threshold. The embedded Entry class will have hash, key, and value fields but
no next field We will focus on the containsKey, put, and remove methods: They will have to be
re-define because now there are no linked lists.
we want to remove the entry with key 033-52-0048 from the table in Figure 14.7. If we simply make that
entry null, we will get the table in Figure 14.8.
Do you see the pitfall with this removal strategy? The path taken by synonyms of 033-52-0048 has
been blocked. A search for the entry with key 214-30-3495 would be unsuccessful, even though there is
such an entry in the table.
Instead of nulling out a removed entry, we will add another f eld to the Entry class:
boolean markedForRemoval;
This fiel is initialized to false when an entry is inserted into the table. The remove method sets this
fiel to true . The markedForRemoval field when true , indicates that its entry is no longer part of
0 null
80 301-33-6785
212 null
213 214-30-3261
214 214-30-3495
529 298-71-9753
754 587-71-1904
919 081-90-3292
920 735-66-8100
1023 null
FIGURE 14.8 The effect of removing the entry with key 033-52-0048 from the table in Figure 14.7 by nulling
out the entry at index 212
14.5 Open-Address Hashing (optional) 629
the hash map, but allows the offset-of-1 collision handler to continue along its path. Figure 14.9 shows a
table after 8 insertions, and Figure 14.10 shows the subsequent effect of the message
remove (new Integer (033-52-0048))
A search for the entry with the key 214303495 would now be successful. In the definitio of the remove
method, a loop is executed until a null or matching entry is found. Note that an entry’s key is examined
only if that entry is not marked for removal.
The containsKey method loops until an empty or matching entry is found. As with the remove
method, an entry’s key is checked only if that entry is not marked for removal. The definitio of the
0 null
80 301-33-6785 false
1023 null
FIGURE 14.9 A table to which 8 elements have been inserted. Open addressing, with an offset of 1, handles
collisions. In each entry, only the key and markedForRemoval field are shown.
630 C H A P T E R 14 Hashing
0 null
80 301-33-6785 false
1023 null
FIGURE 14.10 The table from Figure 14.9 after the message remove (new Integer (033520048)) has been
sent
containsKey method is only slightly revised from the chained-hashing version. For example, here we
use modular arithmetic instead of the & operator because the table length need not be a power of 2.
/**
* Determines if this HashMap object contains a mapping for the
* specified key.
* The worstTime(n) is O(n). If the Uniform Hashing Assumption holds,
* averageTime(n) is constant.
*
* @param key The key whose presence in this HashMap object is to be tested.
14.5 Open-Address Hashing (optional) 631
*
* @return true - if this map contains a mapping for the specified key.
*
*/
public boolean containsKey (Object key)
{
Object k = maskNull (key); // use NULL_KEY if key is null
int hash = hash (k);
int i = indexFor (hash, table.length);
Entry e = table [i];
while (e != null)
{
if (!e.markedForRemoval && e.hash == hash && eq (k, e.key))
return true;
e = table [(++i) % table.length]; // table.length may not be a power of 2
} // while
return false;
} // method containsKey
With the help of the markedForRemoval field we solved the problem of removing an element without
breaking the offset-of-1 path. The put method hashes a key to an index and stores the key and value at
that index if that location is unoccupied: either a null key or marked for removal.
211 null
215 null
FIGURE 14.11 The path traced by keys that hash to 212 overlaps the paths traced by keys that hash to 213 or
214
632 C H A P T E R 14 Hashing
Clearly, the offset-of-1 collision handler is susceptible to primary clustering. The problem with
primary clustering is that we get ever-longer paths that are sequentially traversed during searches, insertions,
and removals. Long sequential traversals are the bane of hashing, so we should try to solve this problem.
What if we choose an offset of, say, 32 instead of 1? We would still have primary clustering: keys
that hashed to index would overlap the path traced by keys that hashed to index + 32, index + 64, and
so on. In fact, this could create an even bigger problem than primary clustering. For example, suppose
the table size is 128 and a key hashes to index 45. Then the only locations that would be allowed in that
cluster have the following indexes:
45, 77, 109, 13
Once those locations fil up, there would be no way to insert any entry whose key hashed to any one of
those indexes. The reason we have this additional problem is that the offset and table size have a common
factor. We can avoid this problem by making the table size a prime number, instead of a power of 2. But
then we would still have primary clustering.
with
e = table [(i + offset) % table.length];
To see how this works in a simple setting (ignoring the hash method), let’s insert the following keys into
a table of size 19:
33
72
71
55
112
109
These keys were created, not randomly, but to illustrate that keys from different collisions, even from the
same collision, do not follow the same path. Here are the relevant moduli and quotients:
The f rst key, 33, is stored at index 14, and the second key, 72, is stored at index 15. The third key,
71, hashes to 14, but that location is occupied, so the index 14 is incremented by the offset 3 to yield index
17; 71 is stored at that location. The fourth key, 112, hashes to 17 (occupied); the index 17 is incremented
by the offset 5. Since 22 is beyond the range of the table, we try index 22% 19, that is, 3, an unoccupied
location. The key 112 is stored at index 3. The fift key, 55, hashes to 17 (occupied) and then to (17 + 2)
% 19, that is, 0, an empty location. The sixth key, 109, hashes to 14 (occupied) and then to (14 + 5) %
19, that is 0 (occupied), and then to (0 + 5) % 19, that is, 5, an unoccupied location.
Figure 14.12 shows the effect of the insertions:
0 55
1
2
3 112
4
5 109
6
7
8
9
10
11
12
13
14 33
15 72
16
17 71
18
FIGURE 14.12 The effect of inserting six entries into a table; the collision handler uses the hash value divided
by the table size as the offset
This collision handler is known as double hashing, or the quotient-offset collision handler. There
is one last problem we need to address before we get to the analysis: what happens if the offset is a
multiple of the table size? For example, suppose we try to add the entry whose key is 736 to the table in
Figure 14.12. We have
736 % 19 = 14
736/19 = 38
Because location 14 is occupied, the next location tried is (14 + 38) % 19, which is 14 again. To avoid
this impasse, we use 1 as the offset whenever key/table.length is a multiple of table.length. This is an
infrequent occurrence: it happens, on average, once in every m keys, where m = table.length. Concept
634 C H A P T E R 14 Hashing
Exercise 14.5 shows that if this collision handler is used and the table size is a prime number, the sequence
of offsets from any key covers the whole table.
Because the table size must be a prime number, the call to the resize method—within the put
method—must be changed from the offset-of-1 version:
resize (2 * table.length);
The static method nextPrime returns the smallest prime larger than the argument.
If you undertake Programming Project 14.1, you will get to f ll in the details of double hashing.
If we make the Uniform Hashing Assumption, the average time for insertions, removals and searches is
constant (see Collins [2003, pp 554–556]). Figure 14.13 summarizes the time estimates for successful and
unsuccessful searches (that is, invocations of the containsKey method). For purposes of comparison, the
information in Section 14.3.5 on chained hashing is included.
Figure 14.14 provides some specifics the expected number of loop iterations for various ratios of n
to m. For purposes of comparison, the information in Section 14.6 on chained hashing is included.
Chaining:
averageTimeS (n, m) ≈ n/2m iterations
Double Hashing:
averageTimeS (n, m) ≈ (m/n) ln(1/(1 − n/m)) iterations
Chained Hashing:
successful 0.13 0.25 0.38 0.45 0.50
unsuccessful 0.25 0.50 0.75 0.90 0.99
Double Hashing:
successful 1.14 1.39 1.85 2.56 4.65
unsuccessful 1.33 2.00 4.00 10.00 100.00
FIGURE 14.14 Estimated average number of loop iterations for successful and unsuccessful calls to the
containsKey method, under both chained hashing and double hashing. In the f gure, n = number of elements
inserted; m = table.length
Summary 635
A cursory look at Figure 14.14 suggests that chained hashing is much faster than double hashing.
But the figure given are mere estimates of the number of loop iterations. Run-time testing may, or may
not, give a different picture. Run-time comparisons are included in Programming Project 14.1.
The main reason we looked at open-address hashing is that it is widely used; for example, some
programming languages do not support linked lists. Also, open-address hashing can save space relative to
chained hashing, which requires n + m references versus only m references for open-address hashing.
Even if the Uniform Hashing Assumption applies, we could still, in the worst case, get every key to
hash to the same index and yield the same offset. So for the containsKey method under double hashing,
worstTimeS (n, m) and worstTimeU (n, m) are linear in n.
For open addressing, we eliminated the threat of primary clustering with double hashing. Another
way to avoid primary clustering is through quadratic probing: the sequence of offsets is 12 , 22 , 32 , . . . .
For details, the interested reader may consult Weiss [2002].
SUMMARY
In this chapter, we studied the Java Collections Frame- A HashSet object is simply a HashMap in which
work’s HashMap class, for which key-searches, inser- each key has the same dummy value. Almost all of the
tions, and removals can be very fast, on average. This HashSet methods are one-liners that invoke the corre-
exceptional performance is due to hashing, the process sponding HashMap method.
of transforming a key into an index in a table. A hashing An alternative to chaining is open addressing.
algorithm must include a collision handler for the pos- When open addressing is used to handle collisions, each
sibility that two keys might hash to the same index. A location in the table consists of entries only; there are no
widely used collision handler is chaining. With chaining, linked lists. If a key hashes to an index at which another
the HashMap object is represented as an array of singly key already resides, the table is searched systematically
linked lists. Each list contains the elements whose keys until an open address, that is, empty location is found.
hashed to that index in the array. With an offset of 1, the sequence searched if the key
Let n represent the number of elements currently in hashes to index, is
the table, and let m represent the capacity of the table. The
index, index + 1, index + 2, . . .,
load factor is the maximum ratio of n to m before rehash-
table.length - 1, 0, 1, . . ., index - 1.
ing will take place. The load factor is an upper-bound
estimate of the average size of each list, assuming that The offset-of-1 collision handler is susceptible to pri-
the hash method scatters the keys uniformly throughout mary clustering: the phenomenon of having accelerating
the table. With that assumption—the Uniform Hashing growth in the size of collision paths. Primary clustering
Assumption—the average time for successful and unsuc- is avoided with double hashing: the offset is the (posi-
cessful searches depends only on the ratio of n to m. The tivized) hash value divided by table.length. If the
same is true for insertions and removals. If we double the Uniform Hashing Assumption holds and the table size is
table size whenever the ratio of n to m equals or exceeds prime, the average time for both successful and unsuc-
the load factor, then the size of each list, on average, will cessful searches with double hashing is constant.
be less than or equal to the load factor. This shows that
with chained hashing, the average time to insert, remove,
or search is constant.
636 C H A P T E R 14 Hashing
CROSSWORD PUZZLE
1 2
www.CrosswordWeaver.com
ACROSS DOWN
5. For any iteration through a HashMap 3. The ______ Hashing Assumption states that
object worstTime(n, m) for the next() the set of all possible keys is uniformly
method is _______. distributed over the set of all table indexes.
CONCEPT EXERCISES
14.1 Why does the HashMap class use singly-linked lists instead of the LinkedList class?
14.2 Suppose you have a HashMap object, and you want to insert an element unless it is already there. How could
you accomplish this?
Hint: The put method will insert the element even if it is already there (in which case, the new value will
replace the old value).
14.3 For each of the following methods, estimate averageTime(n) and worstTime(n):
a. making a successful call—that is, the element was found—to the contains method in the LinkedList
class;
b. making a successful call to the contains method in the ArrayList class;
c. making a successful call to the generic algorithm binarySearch in the Arrays class; assume the
elements in the array are in order.
d. making a successful call to the contains method in the BinarySearchTree class;
e. making a successful call to the contains method in the TreeSet class;
f. making a successful call to the contains method in the HashSet class—you should make the Uniform
Hashing Assumption.
14.4 This exercise helps to explain why both the hash and key fie ds are compared in the containsKey and
put (and remove) methods of the HashMap class.
a. Suppose the keys in the HashMap are from a class, Key, that overrides the Object class’s equals
method but does not override the Object class’s hashCode method. (This is in violation of the contract
for the Object class’s hashCode method, namely, equal objects should have the same hash code). The
Object class’s hashCode method converts the Object reference to an int value. So it would be
possible for a key to be constructed with a given reference, and an identical key constructed with a different
reference. For example, we could have:
If the hash field were not compared in the containsKey and put methods, what would be returned
by each of the following messages:
myMap.containsKey (key2)
b. In some classes, the hashCode method may return the same int value for two distinct keys. For example,
in the String class, we can have two distinct String objects—even with the same characters—that
have the same hash code:
String key1 = "! @"; // exclamation point, blank, at sign
32769 32769
If the key field were not compared in the containsKey and put methods, what would be returned by
each of the following messages:
myMap.containsKey (key2)
14.5 Assume that p is a prime number. Use modular algebra to show that for any positive integers index and offset
(with offset not a multiple of p), the following set has exactly p elements:
{(index + k ∗ offset)% p; k = 0, 1, 2, . . . , p − 1}
14.6 Compare the space requirements for chained hashing and open-address hashing with quotient offsets. Assume
that a reference occupies four bytes and a boolean value occupies one byte. Under what circumstances
(size, loadFactor, table.length) will chained hashing require more space? Under what circumstances
will double hashing require more space?
14.7 We noted in Chapter 12 that the term dictionary, viewed as an arbitrary collection of key-value pairs, is a
synonym for map. If you were going to create a real dictionary, would you prefer to store the elements in a
TreeMap object or in a HashMap object? Explain.
14.8 In open-addressing, with the quotient-offset collision handler, insert the following keys to a table of size 13
(ignore the hash method):
20
33
49
22
26
202
140
508
9
Programming Exercises 639
PROGRAMMING EXERCISES
14.1 Construct a HashMap object of the 25,000 students at Excel University. Each student’s key will be that
student’s unique 6-digit ID. Each student’s value will be that student’s grade point average. Which constructor
should you use and why?
14.2 Construct a HashMap object of the 25,000 students at Excel University. Each student’s key will be that
student’s unique 6-digit ID. Each student’s value will be that student’s grade point average and class rank.
Insert a few elements into the HashMap object. Note that the value does not consist of a single component.
14.3 Construct a HashSet object with Integer elements and an initial capacity of 2000. What is the load factor?
What is the table length (Hint: It is greater than 2000)? Insert a few random Integer elements into the
HashSet object.
14.4 As a programmer who uses the HashSet class, test and defin test a toSortedString method:
/**
* Returns a String representation of a specified HashSet object, with the natural
* ordering,
* The worstTime(n) is O(n log n).
*
* @param hashSet –the HashSet object whose String representation, sorted, is
* to be returned.
*
* @return a String representation of hashSet, with the natural ordering.
*
*/
public static <E> String toSortedString (HashSet<E> hashSet)
Note: As a user, you cannot access any of the f elds in a HashSet class.
14.5 Given the following program fragment, add the code to print out all of the keys, and then add the code to
print out all of the values:
14.6 The LinkedHashMap class superimposes a LinkedList object on a HashMap object, that is, there is a
doubly-linked list of all of the elements in the HashMap object. For example, suppose we have
System.out.println (ageMap);
The output will refl ct the order in which elements were inserted, namely,
Revise the above code segment so that only the pets are printed, and in alphabetical order.
searches. For example, for each possible search word, each fi e in which the search word occurs and the frequency
of occurrences are saved. When an end-user clicks on the Search button and then enters a search string, the cached
results allow for quick searches.
Initially, you will be given a fil , “search.ser”, that contains a search-string HashMap in serialized form (see
Appendix 1 for details on serializing and de-serializing). Each key—initially—is a search word, and each value is
a f le-count HashMap in which the key is a fil name and the value is the frequency of the search word in the f le.
You will need to de-serialize the search-string HashMap. When the end-user enters a search string, there are
two possibilities. If the search string already appears as a key in the search-string HashMap, the fi e names and
counts in the file-coun HashMap are put into a priority queue and Part 5 of the project takes over. If the search
string does not appear as a key in the search-string HashMap, the individual words in the search string are used to
search the search-string HashMap, and the combined result becomes a new entry in the search-string HashMap.
For example, suppose the universe of web pages consists of the f les browser.in10, in11, in12, and in13
from Part 5 (Programming Project 13.2). The search-string HashMap starts out with keys that include “neural”,
“decree”, and “network”. If the end-user clicks on the Search button and then enters “network”, the output will be
browser.in13 4
browser.in12 4
browser.in11 4
browser.in10 4
But if the end-user searches for “neural network”, that string is not in the search-string HashMap. So the results
from the individual words “neural” and “network” are combined, and the output will be
Here are the results of the new search for neural network:
browser.in13 8
browser.in10 8
browser.in12 6
browser.in11 6
The search string “neural network” and the results will now be added to the search-string HashMap. If, later in the
same run, the end-user searches for “neural network”, the output will be
Here are the results of the old search for neural network:
browser.in13 8
browser.in10 8
browser.in12 6
browser.in11 6
System Test 1:
(Assume the end-user searches for “neural network”.)
browser.in13 8
browser.in10 8
browser.in12 6
browser.in11 6
browser.in13 4
browser.in12 4
browser.in11 4
browser.in10 4
browser.in13 8
browser.in10 8
browser.in12 6
browser.in11 6
System Test 2:
(Assume the end-user searches for “network decree”.)
Here are the results of the new search for network decree:
browser.in12 6
browser.in13 5
browser.in11 5
browser.in10 5
NOTE 1: By the end of each execution of the project, the fil “search.ser” should contain the updated search-string
HashMap in serialized form. For example, by the end of System Test 1, “neural network” will be one of the search
strings serialized in “search.ser”. You may update “search.ser” after each search.
NOTE 2: The original f le search.ser is available from the ch14 directory of the book’s website.
NOTE 3: If the search string does not occur in any of the f les, the output should be “The search string does not
occur in any of the f les.”
Graphs, Trees, and Networks CHAPTER 15
There are many situations in which we want to study the relationship between objects. For example,
in Facebook, the objects are individuals and the relationship is based on friendship. In a curriculum,
the objects are courses and the relationship is based on prerequisites. In airline travel, the objects are
cities; two cities are related if there is a flight between them. It is visually appealing to describe such
situations graphically, with points (called vertices) representing the objects and lines (called edges)
representing the relationships. In this chapter, we will introduce several collections based on vertices
and edges. Finally, we will design, test, and implement one of those collections in a class, Network,
from which the other structures can be defined as subclasses. That class uses several classes— TreeMap,
PriorityQueue, and LinkedList —that are in the Java Collections Framework. But neither the
Network class nor the subclasses are currently part of the Java Collections Framework.
CHAPTER OBJECTIVES
1. Define the terms graph and tree for both directed/undirected and weighted/unweighted
collections.
2. Compare breadth-first iterators and depth-first iterators.
3. Understand Prim’s greedy algorithm for finding a minimum-cost spanning tree and Dijkstra’s
greedy algorithm for finding the minimum-cost path between vertices.
4. Be able to find critical paths in a project network.
5. Be able to utilize, expand, and extend the Network class.
Vertices: A, B, C, D, E
The vertex pair in an edge is enclosed in parentheses to indicate the pair of vertices is unordered. For
example, to say there is an edge from A to B is the same as saying there is an edge from B to A. That’s
why “undirected” is part of the term defined Figure 15.1 depicts this undirected graph, with each edge
represented as a line connecting its vertex pair.
From the illustration in Figure 15.1 we could obtain the original formulation of the undirected graph
as a collection of vertices and edges. And furthermore, Figure 15.1 gives us a better grasp of the undirected
643
644 C H A P T E R 15 Graphs, Trees, and Networks
B D
C E
graph than the original formulation. From now on we will use illustrations such as Figure 15.1 instead of
the formulations.
Figure 15.2a contains several additional undirected graphs. Notice that the number of edges can be
fewer than the number of vertices (Figures 15.2a and b), equal to the number of vertices (Figure 15.1) or
greater than the number of vertices (Figure 15.2c).
producer
leads supporting
Raleigh
Charlotte
Atlanta
Tallahassee
Miami
FIGURE 15.2b An undirected graph with eight vertices and seven edges
15.1 Undirected Graphs 645
B E
A C F G H
FIGURE 15.2c An undirected graph with eight vertices and eleven edges
An undirected graph is complete if it has as many edges as possible. What is the number of edges in
a complete undirected graph? Let n represent the number of vertices. Figure 15.3 shows that when n = 6,
the maximum number of edges is 15.
A B
C D
E F
FIGURE 15.3 An undirected graph with 6 vertices and the maximum number (15) of edges for any undirected
graph with 6 vertices
Can you determine a formula that holds for any positive integer n? In general, start with any one of
the n vertices, and construct an edge to each of the remaining n − 1 vertices. Then from any one of those
n − 1 vertices, construct an edge to each of the remaining n − 2 vertices (the edge to the f rst vertex was
constructed in the previous step). Then from any one of those n − 2 vertices, construct an edge to each
of the remaining n − 3 vertices. This process continues until, at step n − 1, a f nal edge is constructed.
The total number of edges constructed is:
n−1
(n − 1) + (n − 2) + (n − 3) + . . . + 2 + 1 = i = n(n − 1)/2
i =1
This last equality can either be proved directly by induction on n or can be derived from the proof—in
Example A2.1 of Appendix 2—that the sum of the firs n positive integers is equal to n (n + 1)/2.
Two vertices are adjacent if they form an edge. For example, in Figure 15.2b, Charlotte and Atlanta
are adjacent, but Atlanta and Raleigh are not adjacent. Adjacent vertices are called neighbors.
A path is a sequence of vertices in which each successive pair is an edge. For example, in
Figure 15.2c,
A, B, E, H
646 C H A P T E R 15 Graphs, Trees, and Networks
is a path from A to H because (A, B), (B, E), and (E, H) are edges. Another path from A to H is
A, C, F, D, G, H
For a path of k vertices, the length of the path is k − 1. In other words, the path length is the number of
edges in the path. For example, in Figure 15.2c the following path from C to A has a length of 3:
C, F, D, A
C, A
In general, there may be several paths with fewest edges between two vertices. For example, in Figure 15.2c,
A, B, E
and
A, C, E
A, B, D, C, A
B, E, H, G, D, A, B
is a cycle, as is
E, C, A, B, E
The undirected graph in Figures 15.2a and b are acyclic, that is, they do not contain any cycles. In
Figure 15.2a,
is not a cycle since the edge (producer, director) is repeated—recall that an edge in an undirected graph
is an unordered pair.
An undirected graph is connected if, for any given vertex, there is a path from that vertex to any
other vertex in the graph. Informally, an undirected graph is connected if it is “all one piece.” For example,
all of the graphs in the previous figure are connected. The following undirected graph, with six vertices
and f ve edges, is not connected:
0 1 5
2 3 4
15.2 Directed Graphs 647
Vertices: A, T, V, W, Z
Edges: A, T, A, V, T, A, V, A, V, W, W, Z, Z, W, Z, T
Pictorially, these edges are represented by arrows, with the arrow’s direction going from the firs vertex in
the ordered pair to the second vertex. For example, Figure 15.4 contains the directed graph just defined
A path in a directed graph must follow the direction of the arrows. Formally a path in a directed
graph is a sequence of k > 1 vertices V0 , V1 , . . . , Vk −1 such that V0 , V1 , V1 , V2 , . . . , Vk −2 , Vk −1 are
edges in the directed graph. For example, in Figure 15.4,
A, V, W, Z
is a path from A to Z because A, V , V , W , and W , Z are edges in the directed graph. But
A, T, Z, W
is not a path because there is no edge from T to Z, (In other words, Z is not a neighbor of T, although T
is a neighbor of Z.) A few minutes checking should convince you that for any two vertices in Figure 15.4,
there is a path from the firs vertex to the second.
A digraph D is connected if, for any pair of distinct vertices x and y, there is a path from x to y.
Figure 15.4 is a connected digraph, but the digraph in Figure 15.5 is not connected (try to figur out why):
From these examples, you can see that we actually could have define the term “undirected graph”
from “directed graph”: an undirected graph is a directed graph in which, for any two vertices V and W,
A T
V W
A T
V W
if there is an edge from V to W, there is also an edge from W to V. This observation will come in handy
when we get to developing Java classes.
In the next two sections, we will look at specializations of graphs: trees and networks.
15.3 Trees
An undirected tree is a connected, acyclic, undirected graph with one element designated as the root
element. Note that any element in the graph can be designated as the root element. For example, here is
the undirected tree from Figure 15.2a; producer is designated as the root element.
producer
leads supporting
On most occasions, we are interested in directed trees, that is, trees that have arrows from a parent to
its children. A tree, sometimes called a directed tree, is a directed graph that either is empty or has an
element, called the root element such that:
a. there are no edges coming into the root element;
b. every non-root element has exactly one edge coming into it;
c. there is a path from the root to every other element.
For example, Figure 15.6 shows that we can easily re-draw the undirected tree as a directed tree:
Many of the binary-tree terms from Chapter 9—such as “child”, “leaf”, “branch”—can be extended
to apply to arbitrary trees. For example, the tree in Figure 15.6 has four leaves and height 2. But “full”
does not apply to trees in general because there is no limit to the number of children a parent can have. In
fact, we cannot simply defin a binary tree to be a tree in which each element has at most two children.
Why not? Figure 15.7 has two distinct binary trees that are equivalent as trees.
producer
leads supporting
A A
B B
FIGURE 15.7 Two distinct binary trees, one with an empty right subtree and one with an empty left subtree
We can defin a binary tree to be a (directed) tree in which each vertex has at most two children,
labeled the “left” child and the “right” child of that vertex.
Trees allow us to study hierarchical relationships such as parent-child and supervisor-supervisee.
With arbitrary trees, we are not subject to the at-most-two-children restriction of binary trees.
15.4 Networks
Sometimes we associate a non-negative number with each edge in a graph (which can be directed or
undirected). The non-negative numbers are called weights, and the resulting structure is called a weighted
graph or network . For example, Figure 15.8 has an undirected network in which each weight represents
the distance between cities for the graph of Figure 15.2b.
Of what value is a weighted digraph, that is, why might the direction of a weighted edge be signif-
icant? Even if one can travel in either direction on an edge, the weight for going in one direction may
be different from the weight going in the other direction. For example, suppose the weights represent the
462 421
Raleigh
Charlotte 165
244
Atlanta
277
Tallahassee
625
Miami
FIGURE 15.8 An undirected network in which vertices represent cities, and each edge’s weight represents the
distance between the two cities in the edge
650 C H A P T E R 15 Graphs, Trees, and Networks
E
15.0
10.0
A B
4.0 3.0
2.0 1.0
C D G
5.0
0.0 2.0 4.0
F H
4.0
time for a plane fligh between two cities. Due to the prevailing westerly winds, the time to f y from New
York to Los Angeles is usually longer than the time to f y from Los Angeles to New York.
Figure 15.9 shows a weighted digraph in which the weight of the edge from vertex D to vertex F is
different from the weight of the edge going in the other direction.
With each path between two vertices in a network, we can calculate the total weight of the path.
For example, in Figure 15.9, the path A, C, D, E has a total weight of 10.0. Can you fin a shorter path
from A to E, that is, a path with smaller total weight1 ? The shortest path from A to E is A, B, D, E with
total weight 8.0. Later in this chapter we will develop an algorithm to f nd a shortest path (there may be
several of them) between two vertices in a network.
The weighted digraph in Figure 15.9 is not connected because, for example, there is no path from
B to C. Recall that a path in a digraph must follow the direction of the arrows.
Now that we have seen how to defin a graph or tree (directed or undirected, weighted or unweighted),
we can outline some well-known algorithms. The implementation of those algorithms, and their analysis,
will be handled in Section 15.6.3.
15.5.1 Iterators
There are several kinds of iterators associated with directed or undirected graphs, and these iterators can
also be applied to trees and networks (directed or undirected). First, we can simply iterate over all of the
vertices in the graph. The iteration need not be in any particular order. For example, here is an iteration
over the vertices in the weighted digraph of Figure 15.9:
A, B, D, F, G, C, E, H
1 This is different from the meaning of “shorter” in the graph sense, namely, having fewer edges in the path.
15.5 Graph Algorithms 651
In addition to iterating over all of the vertices in a graph, we are sometimes interested in iterating over
all vertices reachable from a given vertex. For example, in the weighted digraph of Figure 15.9, we might
want to iterate over all vertices reachable from A, that is, over all vertices that are in some path that starts
at A. Here is one such iteration:
A, B, C, D, E, F, H
The vertex G is not reachable from A, so G will not be in any iteration from A.
Raleigh
Charlotte
Atlanta
Tallahassee
Miami
We will perform a breadth-firs iteration starting at Atlanta and visit neighbors in alphabetical order. So
we start with
r, v
Atlanta
The r and v above Atlanta indicate that Atlanta has been marked as reachable and visited. The neighbors
of Atlanta are now marked as reachable:
r, v r r r
Atlanta, Charlotte, Miami, Tallahassee
652 C H A P T E R 15 Graphs, Trees, and Networks
Each of those neighbors of Atlanta is then visited in turn, and when a city is visited, each of its neighbors is
marked as reachable—unless that neighbor was marked as reachable previously. When we visit Charlotte,
we have
r, v r, v r r r r r
Atlanta, Charlotte, Miami, Tallahassee, Louisville, Raleigh, Washington
Notice that the firs neighbor of Charlotte, Atlanta, is ignored because we have already marked as reachable
(and visited) Atlanta. Then Miami is visited (its only neighbor has already been marked as reachable),
Tallahassee (its only neighbor has already been marked as reachable), Louisville (its only neighbor has
already been marked as reachable), Raleigh (its only neighbor has already been marked as reachable), and
Washington, whose neighbor Salisbury is now marked as reachable. We now have
r, v r, v r, v r, v r, v r, v r, v r
Atlanta, Charlotte, Miami, Tallahassee, Louisville, Raleigh, Washington Salisbury
When we visit Salisbury we are done because Salisbury has no not-yet-reached neighbors marked as not
reachable. In other words, we have iterated through all cities reachable from Atlanta, starting at Atlanta:
The order of visiting cities would be different if we started a breadth-firs iteration at Louisville:
Let’s do some preliminary work on the design of the BreadthFirstIterator class. When a vertex has
been marked as reachable, that vertex will, eventually, be visited. To make sure that vertex is not re-visited,
we will keep track of which vertices have already been marked as reachable. To do this, we will store the
marked-as-reachable vertices in some kind of collection. Specifically we want to visit the neighbors of
the current vertex in the order in which those neighbors were initially stored in the collection. Because we
want the vertices removed from the collection in the order in which they were added to the collection, a
queue is the appropriate collection. And we will also want to keep track of the current vertex.
We can now develop high-level algorithms for the BreadthFirstIterator methods—the details
will have to be postponed until we create a class—such as Network, in which BreadthFirstIterator
will be embedded. The constructor enqueues the start vertex and marks all other vertices as not reachable:
public BreadthFirstIterator (Vertex start)
{
for every vertex in the graph:
mark that vertex as not reachable.
mark start as reachable.
queue.enqueue (start);
} // algorithm for constructor
The hasNext() method returns !queue.isEmpty(). The next() method removes the front vertex from
the queue, makes that vertex the current vertex, and enqueues each neighbor of the current vertex that has
not yet been marked as reachable:
public Vertex next()
{
current = queue.dequeue();
15.5 Graph Algorithms 653
The analysis of this algorithm is postponed until Section 15.6.3, when we will have all of the details
associated with the BreadthFirstIterator class.
The remove() method deletes from the graph the current vertex, that is, the vertex most recently
returned by a call to the next() method. All edges going into or out of that current vertex are also deleted
from the graph.
For an example of how the queue and the next() method work together in a breadth-firs iteration,
suppose we create a weighted digraph by entering the sequence of edges and weights in Figure 15.10.
A B 4.0
A C 2.0
A E 15.0
B D 1.0
B E 10.0
C D 5.0
D E 3.0
D F 0.0
F D 0.0
F H 4.0
G H 4.0
FIGURE 15.10 A sequence of edges and weights to generate the weighted digraph in Figure 15.9
The weighted digraph created is the same one shown in Figure 15.9:
E
15.0
10.0
A B
4.0 3.0
2.0 1.0
C D G
5.0
0.0 2.0 4.0
F H
4.0
To conduct a breadth-firs iteration starting at A, for example, we firs enqueue A in the constructor. The
firs call to next() dequeues A, enqueues B, C and E, and returns A. The second call to next() dequeues
B, enqueues D, and returns B. Figure 15.11 shows the entire queue generated by a breadth-firs iteration
starting at A.
654 C H A P T E R 15 Graphs, Trees, and Networks
FIGURE 15.11 A breadth-firs iteration of the vertices starting at A. The vertices are enqueued—and therefore
dequeued—in the same order in which they were entered in Figure 15.10
Notice that vertex G is missing from Figure 15.11. The reason is that G is not reachable from A,
that is, there is no path from A to G. If we performed a breadth-firs iteration from any other vertex, there
would be even fewer vertices visited than in Figure 15.11. For example, a breadth-firs iteration starting
at vertex B would visit
B, D, E, F, H
in that order.
Breadth-firs iterators are especially useful in iterating over a (directed) tree. The start vertex is the
root and, as we saw in Chapter 9, the vertices are visited level-by-level: the root, the root’s children, the
root’s grandchildren, and so on.
A, B, D, G, I, H, J, K, L, C, E, F
B C
D E F
G H
I J
K L
FIGURE 15.12 A binary tree and the order in which its elements would be visited during a pre-order traversal
neighbors, and mark as reachable each not-yet-marked-as-reachable neighbor of that vertex. Another path
is begun starting with that unvisited vertex. This continues as long as there are vertices reachable from the
start vertex that have not yet been found to be reachable.
For example, let’s perform a depth-firs iteration of the following graph, starting at Atlanta—we
assume that the vertices were initially entered in alphabetical order:
Raleigh
Charlotte
Atlanta
Tallahassee
Miami
656 C H A P T E R 15 Graphs, Trees, and Networks
We then visit the most recently marked as reachable vertex, namely Tennessee, not Charlotte. Tallahassee’s
only neighbor has already been marked as reachable, so we visit the next-most-recently marked-as-reachable
vertex: Miami. Miami’s only neighbor has already been marked as reachable, so we visit Charlotte, and
mark as reachable Louisville, Raleigh and Washington, in that order. We now have
r, v r, v r, v r, v r r r
Atlanta, Charlotte, Miami, Tallahassee, Louisville, Raleigh, Washington
Washington, the most recently marked-as-reachable vertex, is visited, and its only not yet marked-as-
reachable neighbor, Salisbury, is marked as reachable. We now have
r, v r, v r, v r, v r r r, v r
Atlanta, Charlotte, Miami, Tallahassee, Louisville, Raleigh, Washington Salisbury
Now Salisbury is the most recently marked-as-reachable vertex, so Salisbury is visited. Finally, Raleigh
and then Louisville are visited. The order in which vertices are visited is as follows:
For a depth-firs iteration starting at Charlotte, the order in which vertices are visited is:
With a breadth-firs iteration, we saved vertices in a queue so that the vertices were visited in the order
in which they were saved. With a depth-firs iteration, the next vertex to be visited is the most recently
reached vertex. So the vertices will be stacked instead of queued. Other than that, the basic strategy of a
depth-firs iterator is exactly the same as the basic strategy of a breadth-firs iterator. Here is the high-level
algorithm for next():
public Vertex next()
{
current = stack.pop();
The analysis of this algorithm is the same as that of the next() method in the BreadthFirstIterator
class: see Section 15.6.3.
15.5 Graph Algorithms 657
Suppose, as we did above, we create a weighted digraph from the following input, in the order given:
A B 4.0
A C 2.0
A E 15.0
B D 1.0
B E 10.0
C D 5.0
D E 3.0
D F 0.0
F D 0.0
F H 4.0
G H 4.0
The weighted digraph created is the same one shown in Figure 15.9:
E
15.0
10.0
A B
4.0 3.0
2.0 1.0
C D G
5.0
0.0 2.0 4.0
F H
4.0
Figure 15.13 shows the sequence of stack states and the vertices returned by next() for a depth-firs
iteration of the weighted digraph in Figure 15.9, as generated from the input in Figure 15.10.
We could have developed a backtrack version of the next() method. The vertices would be visited
in the same order, but recursion would be utilized instead of an explicit stack.
When should you use a breadth-firs iterator and when should you use a depth-firs iterator? If you
are looping through all vertices reachable from the start vertex, there’s not much reason to pick. The only
FIGURE 15.13 A depth-firs iteration of the vertices reachable from A. We assume the vertices were entered as
in Figure 15.10
658 C H A P T E R 15 Graphs, Trees, and Networks
difference is the order in which the vertices will be visited. But if you are at some start vertex and you
are searching for a specifi vertex reachable from that start vertex, there can be a difference. If, somehow,
you know that there is a short path from the start vertex to the vertex sought, a breadth-firs iterator is
preferable: the vertices on a path of length 1 from the start vertex are visited, then the vertices on a path
of length 2, and so on. On the other hand, if you know that the vertex sought may be very far from the
start vertex, a depth-firs search will probably be quicker (see Figure 15.12).
15.5.2 Connectedness
In Section 15.1, we define an undirected graph to be connected if, for any given vertex, there is a path
from that vertex to any other vertex in the graph. For example, the following is a connected, undirected
graph:
Raleigh
Charlotte
Atlanta
Tallahassee
Miami
For a digraph, that is, a directed graph, connectedness means that for any two distinct vertices, there is
a path—that follows the directions of the arrows—between them. A breadth-firs or depth-firs iteration
over all vertices in a graph can be performed only if the graph is connected. In fact, we can use the ability
to iterate between any two vertices as a test for connectedness of a digraph.
Given a digraph, let itr be an iterator over the digraph. For each vertex v returned by itr.next(),
let bfitr be a breadth-firs iterator starting at v. We check to make sure that the number of vertices
reachable from v (including v itself) is equal to the number of vertices in the digraph.
Here is a high-level algorithm to determine connectedness in a digraph:
public boolean isConnected()
{
for each Vertex v in this digraph
{
// Count the number of vertices reachable from v.
Construct a BreadthFirstIterator, bfItr, starting at v.
int count = 0;
while (bfItr.hasNext())
15.5 Graph Algorithms 659
{
bfItr.next();
count++;
} // while
if (count < number of vertices in this digraph)
return false;
} // for
return true;
} // algorithm for isConnected
For an undirected graph, the isConnected() algorithm is somewhat simpler: there is no need for an
outer loop to iterate through the entire graph. See Concept Exercise 15.4.
In the next two sections, we outline the development of two important network algorithms. Each
algorithm is sufficientl complex that it is named after the person (Prim, Dijkstra) who invented the
algorithm.
3.0
B E
5.0 28.0
18.0 20.0
A C F
D G
2.0
FIGURE 15.14 A connected network in which the vertices represent houses and the weights represent the cost,
in hundreds of dollars, to connect the two houses
660 C H A P T E R 15 Graphs, Trees, and Networks
B E
5.0 28.0
18.0 20.0
A C F
8.0 4.0
D G
3.0
B E
5.0 28.0
A C F
7.0 4.0
D G
2.0
FIGURE 15.16 Another spanning tree for the network in Figure 15.14
that (v, w) is an edge with weight wweight, save the ordered triple v, w, wweight in a collection—we’ll see
what kind of collection shortly. Then loop until T has as many vertices as the original network. During each
loop iteration, remove from the collection the triple x, y, yweight for which yweight is the smallest weight
of all triples in the collection; if y is not already in T, add y and the edge (x, y) to T and save in the collection
every triple y, z, zweight such that z is not already in T and (y, z) is an edge with weight zweight.
What kind of collection should we have? The collection should be ordered by weights; we need to
be able to add an element, that is, a triple, to the collection and to remove the triple with lowest weight. A
priority queue will perform these tasks quickly. Recall from Chapter 13 that for the PriorityQueue class,
averageTime(n) for the add (E element) method is constant, and averageTime(n) for the remove()
method is logarithmic in n.
To see how Prim’s algorithm works, let’s start with the network in Figure 15.14, repeated here:
3.0
B E
5.0 28.0
18.0 20.0
A C F
D G
2.0
Initially, the tree T and the priority queue pq are both empty. Add A to T, and add to pq each triple of
the form A, w, wweight where (A, w) is an edge with weight wweight. Figure 15.17 shows the contents
15.5 Graph Algorithms 661
T pq
A <A, B, 5.0>
<A, D, 7.0>
<A, C, 18.0>
FIGURE 15.17 The contents of T and pq at the start of Prim’s algorithm as applied to the network in Figure 15.14
of T and pq at this point. For the sake of readability, the triples in pq are shown in increasing order of
weights; strictly speaking, all we know for sure is that the element() and remove() methods return the
triple with smallest weight.
When the lowest-weighted triple, A, B, 5.0 is removed from pq, the vertex B and the edge (A, B) are
added to T, and the triple B, E, 3.0 is added to pq. See Figure 15.18.
T pq
B
<B, E, 3.0>
5.0 <A, D, 7.0>
<A, C, 18.0>
A
FIGURE 15.18 The contents of T and pq during the application of Prim’s algorithm to the network in Figure 15.14
During the next iteration, the triple B, E, 3.0 is removed from pq, the vertex E and edge (B, E) are
added to T, and the triple E, C, 28.0 is added to pq. See Figure 15.19.
T pq
3.0
B E
<A, D, 7.0>
5.0 <A, C, 18.0>
<E, C, 28.0>
A
FIGURE 15.19 The contents of T and pq during the application of Prim’s algorithm to the network in Figure 15.14
During the next iteration, the triple A, D, 7.0 is removed from pq, the vertex D and the edge (A, D) are
added to T, and the triples D, F, 8.0 and D, G, 2.0 are added to pq. See Figure 15.20.
T pq
3.0
B E
<D, G, 2.0>
5.0 <D, F, 8.0>
<A, C, 18.0>
A <E, C, 28.0>
7.0
FIGURE 15.20 The contents of T and pq during the application of Prim’s algorithm to the network in Figure 15.14
662 C H A P T E R 15 Graphs, Trees, and Networks
During the next iteration, the triple D, G, 2.0 is removed from pq, the vertex G and the edge (D, G) are
added to T, and the triple G, F, 4.0 is added to pq. See Figure 15.21.
T pq
3.0
B E
<G, F, 4.0>
5.0 <D, F, 8.0>
<A, C, 18.0>
A <E, C, 28.0>
7.0
D G
2.0
FIGURE 15.21 The contents of T and pq during the application of Prim’s algorithm to the network in Figure 15.14
During the next iteration, the triple G, F, 4.0 is removed from pq, the vertex F and the edge (G, F) are
added to T, and the triple F, C, 20.0 is added to pq. See Figure 15.22.
T pq
3.0
B E
<D, F, 8.0>
5.0 <A, C, 18.0>
<F, C, 20.0>
A F <E, C, 28.0>
7.0 4.0
D G
2.0
FIGURE 15.22 The contents of T and pq during the application of Prim’s algorithm to the network in Figure 15.14
During the next iteration, the triple D, F, 8.0 is removed from pq. But nothing is added to T or pq
because F is already in T!
During the next iteration, the triple A, C, 18.0 is removed from pq, the vertex C and the edge (A,
C) are added to T, and nothing is added to pq. The reason nothing is added to pq is that, for all of C’s
edges, (C, A), (C, E) and (C, F), the second element in the pair is already in T. See Figure 15.23.
Even though pq is not empty, we are f nished because every vertex in the original network is also
in T.
From the way T is constructed, we know that T is a spanning tree. We can show, by contradiction,
that T is a minimum spanning tree. In general, a proof by contradiction assumes some statement to be
false, and shows that some other statement—known to be true—must also be false. We then conclude that
the original statement must have been true.
15.5 Graph Algorithms 663
T pq
3.0
B E
<F, C, 20.0>
5.0 <E, C, 28.0>
18.0
A C F
7.0 4.0
D G
2.0
FIGURE 15.23 The contents of T and pq after the last iteration in the application of Prim’s algorithm to the
network in Figure 15.14
Assume that T is not a minimum spanning tree. Then during some iteration, a triple x, y, yweight
is removed from pq and the edge (x, y) is added to T, but there is some vertex v, already in T, and w,
not in T, such that edge (v, w) has lower weight than edge (x, y). Pictorially:
T not in T
x y
v w
Note that since v is already in T, the triple starting with v, w, ? must have been added to pq earlier. But
the edge (v, w) could not have a lower weight than the edge (x, y) because the triple x, y, y weight was
removed from pq, not the triple starting with v, w, ?. This contradicts the claim that (v, w) had lower
edge weight than (x, y). So T, with edge (x, y) added, must still be minimum.
Can Prim’s algorithm be applied to a connected, directed network? Consider the following network:
{<a, b, 8.0>, <b, c, 5.0>, <c, a, 10.0>}. Prim’s algorithm would give a different result depending on
which vertex was chosen as the root. We can—and will—apply Prim’s algorithm to a connected directed
network provided it is equivalent to its undirected counterpart. That is, for any pair of vertices u and v, if
v is a neighbor of u then u is a neighbor of v, and the weights of the two edges are the same.
Prim’s algorithm is another example of the greedy-algorithm design pattern (the Huffman encoding
algorithm in Chapter 13 is also a greedy algorithm). During each loop iteration, the locally optimal
choice is made: the edge with lowest weight is added to T. This sequence of locally optimal—that is,
greedy—choices leads to a globally optimal solution: T is a minimum spanning tree.
Another greedy algorithm appears in Section 15.5.4. Concept Exercise 15.7 and Lab 23 show that
greed does not always succeed.
are greedy, and both use a priority queue. The shortest-path algorithm, due to Edsgar Dijkstra [1959], is
essentially a breadth-firs iteration that starts at v1 and stops as soon as v2’s pair is removed from the
priority queue pq. (Dijkstra’s algorithm actually find a shortest path from v1 to every other vertex in the
network.) Each pair consists of a vertex w and the sum of the weights of all edges on the shortest path so
far from v1 to w.
The priority queue is ordered by lowest total weights. To keep track of total weights, we have a
map, weightSum, in which each key is a vertex w and each value is the sum of the weights of all the
edges on the shortest path so far from v1 to w. To enable us to re-construct the shortest path when we
are through, there is another map, predecessor, in which each key is a vertex w, and each value is the
vertex that is the immediate predecessor of w on the shortest path so far from v1 to w.
Basically, weightSum maps each given vertex to the minimum total weight, so far, of the path
from v1 to the given vertex. Initially, pq consists of vertex v1 and its weight, 0.0. On each iteration we
greedily choose the vertex-weight pair x, total weight in pq that has the minimum total weight among
all vertex-weight pairs in pq. If there is a neighbor y of x whose total weight can be reduced by the path
v1,. . ., x, y , then y’s path and minimum weight are altered, and y (and its new total weight) is added
to pq. For example, we might have the partial network shown in Figure 15.24.
8 5
15
v1 y
FIGURE 15.24 The minimum-weight path from vertex v1 to vertex y had been 15, but the path from vertex v1
through vertex x to vertex y has a lower total weight
Then the total weight between v1 and y is reduced to 13, the pair y, 13 is added to pq and y’s
predecessor becomes x. Eventually, this yields the shortest path from v1 to v2, if there is a path between
those vertices.
To start, weightSum associates with each vertex a very large total weight, and predecessor
associates with each vertex the value null. We then refin those initializations by mapping v1’s weightSum
to 0, mapping v1’s predecessor to v1 itself, and adding (v1, 0.0) to pq. This completes the initialization
phase.
Suppose we want to f nd the shortest path from A to E in the network from Figure 15.9, repeated
here:
E
15.0
10.0
A B
4.0 3.0
2.0 1.0
C D G
5.0
0.0 2.0 4.0
F H
4.0
15.5 Graph Algorithms 665
For simplicity, we ignore G because, in fact, G is not reachable from A. Initially, we have
weightSum predecessor pq
A, 0.0 A A, 0.0
B, 10000.0 null
C, 10000.0 null
D, 10000.0 null
E, 10000.0 null
F, 10000.0 null
H, 10000.0 null
After initializing, we keep looping until E is removed from pq (that is, until the shortest path is found) or
pq is empty (that is, there is no path from A to E). During the firs iteration of this loop, the minimum
(and only) pair, A, 0.0, is removed from pq. Since that pair’s weight, 0.0, is less than or equal to A’s
weightSum value, we iterate, in an inner loop, over the neighbors of A. For each neighbor of A, if A’s
weightSum value plus that neighbor’s weight is less than the neighbor’s weightSum value, weightSum,
and predecessor are updated, and the neighbor and its total weight (so far) are added to pq. The effects
are shown in Figure 15.25.
weightSum predecessor pq
FIGURE 15.25 Dijkstra’s shortest-path algorithm, after the firs iteration of the outer loop
After the processing shown in Figure 15.25, the outer loop is executed for a second time: the pair
C, 2.0 is removed from pq and we iterate (the inner loop) over the neighbors of C. The only vertex
on an edge from C is D, and the weight of that edge is 5.0. This weight plus 2.0 (C’s weight sum) is
7.0, which is less than D’s weight sum, 10000.0). So in weightSum, D’s weight sum is upgraded to 7.0.
Figure 15.26 shows the effect on weightSum, predecessor, and pq.
Figure 15.26 indicates that at this point, the lowest-weight path from A to D has a total weight of
7.0. During the third iteration of the outer loop, B, 4.0 is removed from pq and we iterate over the
neighbors of B, namely, D and E. The effects are shown in Figure 15.27.
At this point, the lowest-weight path to D has a total weight of 5.0 and the lowest-weight path to E
has a total weight of 14.0. During the fourth iteration of the outer loop, D, 5.0 is removed from pq, and
we iterate over the neighbors of D, namely, F and E. Figure 15.28 shows the effects of this iteration.
During the f fth outer-loop iteration, F, 5.0 is removed from pq, the neighbors of F, namely D and
H are examined, and the collections are updated. See Figure 15.29.
666 C H A P T E R 15 Graphs, Trees, and Networks
weightSum predecessor pq
FIGURE 15.26 The state of the application of Dijkstra’s shortest-path algorithm after the second iteration of the
outer loop
weightSum predecessor pq
FIGURE 15.27 The state of the application of Dijkstra’s shortest-path algorithm after the third iteration of the
outer loop
weightSum predecessor pq
FIGURE 15.28 The state of the application of Dijkstra’s shortest-path algorithm after the fourth iteration of the
outer loop
During the sixth iteration of the outer loop, D, 7.0 is removed from pq. The minimum total weight,
so far, from A to D is recorded in weightSum as 5.0. So there is no inner-loop iteration.
During the seventh iteration of the outer loop, E, 8.0 is removed from pq. Because E is the vertex
we want to fin the shortest path to, we are done. How can we be sure there are no shorter paths to E?
If there were another path to E with total weight t less than 8.0, then the pair E, t would have been
removed from pq before the pair E, 8.0.
15.5 Graph Algorithms 667
weightSum predecessor pq
FIGURE 15.29 The state of the application of Dijkstra’s shortest-path algorithm after the f fth iteration of the
outer loop
We construct the shortest path, as a LinkedList of vertices, from predecessor: starting with an
empty LinkedList object, we prepend E; then prepend D, the predecessor of E; then prepend B, the
predecessor of D; f nally, prepend A, the predecessor of B. The fina contents of the LinkedList object
are, in order,
A, B, D, E
There are a few details that are missing in the above description of Dijkstra’s algorithm. For example,
how will the vertices, edges, and neighbors be stored? What are worstTime(n) and averageTime(n)? To
answer these questions, we will develop a class in Section 15.6.3, and f ll in the missing details, not only
of Dijkstra’s algorithm, but of all our graph-related work.
18
A D
3 15 4 10
12 5 19
S B E T
6 5 3
33
C F
the length of a longest path, represents the number of days required to complete the project. For example,
in Figure 15.30, there are two longest paths:
S, A, B, E, T
and
S, C, F, T
Each of those paths has a length of 42, so the project, as scheduled, will take 42 days. Dijkstra’s algorithm
can easily be modifie to f nd a longest path in an acyclic network. The next section, on topological sorting,
can be used to determine if a network is acyclic.
In a project network, some activities can occur concurrently, and some activities can be delayed
without affecting the project length. A project manager may want to know which activities can be delayed
without delaying the entire project. First, we need to make some calculations.
For each event w, we can calculate ET(w), the earliest time that event w can be completed. For
event S, ET(S) is 0. For any other event w, ET(w) is the maximum of {ET(v) + weight (v, w)} for any
event v such that v, w forms an edge. For example, in Figure 15.30, ET(C) = 6, ET (B) = max {12, 18,
11} = 18, ET(E) = 23, ET (D) = max {21, 27} = 27, and so on.
For each event w, we can also calculate LT(w), the latest time by which w must be completed to
avoid delaying the entire project. For event T, LT(T) is the project length; for any other event w, LT(w)
is the minimum of {LT(x) − weight(w,x)} for any event x such that w, x forms an edge. For example,
in Figure 15.30, LT(T) = 42, LT(D) = 32, LT(E) = min {28, 23} = 23, LT(B) = 18, LT (C) = min {6,
13} = 6, and so on.
Finally, ST(y, z), the slack time of activity y, z, is calculated as follows:
An activity with a slack time of zero is called a critical activity, and any path that consists only of critical
activities is called a critical path. The idea is that special attention should be given to any critical activity
because any delay in that activity will increase the project length.
Activities that are not on a critical path are less constrained: They may be delayed or take longer
than scheduled without affecting the length of the entire project. For example, in Figure 15.30, the activity
A, D has a slack time of 11 days: That activity can be delayed or take longer than scheduled, as long as
the activity is completed by day 32. The significanc of this is that resources allocated to activity A, D
can be diverted to a critical task, and thereby speed up the completion of the entire project.
Programming Project 15.3 entails findin the slack time for each project in a project network. In
order to calculate the earliest times for each event, the events must be ordered so that for example, when
ET(w) is to be calculated for some vertex w, the value of ET(v) must already be available for each vertex
v such that v, w forms an edge. Section 15.5.5.1 describes that kind of ordering.
15.6 A Network Class 669
Notice that D had to come after both A and E because A, D and E, D are edges in the digraph. Another
topological order is
S, A, C, F, B, E, D, T
Any network that can be put into topological order must be acyclic. Concept Exercise 15.12 has more
information about topological order, and Programming Exercise 15.5 suggests how to perform a topological
sort.
Furthermore, we can view a digraph as a network in which all weights have the value of 1.0. The Digraph
class will have the following method definition
/**
* Ensures that a given edge with a given weight is in this UndirectedNetwork
* object.
*
* @param v1 – the first vertex of the edge.
* @param v2 – the second vertex of the edge (the neighbor of v1).
*
* @return true – if this UndirectedNetwork object changed as a result of this call.
*
*/
public boolean addEdge (Vertex v1, Vertex v2)
{
return super.addEdge (v1, v2, 1.0);
} // method addEdge in DiGraph class, a subclass of Network
Figure 15.31 shows the inheritance hierarchy. Technically, the hierarchy is not a tree because in the Unifie
Modeling Language, the arrows go from the subclass to the superclass.
In the following section, we develop a (directed) Network class, that is, a weighted digraph
class. The development of the subclasses—DirectedWeightedTree, DirectedTree, Digraph,
UndirectedNetwork, UndirectedWeightedTree, UndirectedGraph, Tree and UndirectedTree
—are provided on the book’s website or are programming exercises.
The class heading, with Vertex as the type parameter, is
public class Network<Vertex> implements Iterable<Vertex>, java.io.Serializable
Network
UndirectedTree
FIGURE 15.31 The inheritance hierarchy for the collections in this chapter
15.6 A Network Class 671
The Iterable interface has only one method, iterator(), which returns an iterator that implements
hasNext(), next(), and remove(). By implementing the Iterable interface, we are able to utilize
the enhanced for statement for Network objects. We have not mentioned the Iterable interface up to
now because we have applied the enhanced for statement only to instances of classes that implement the
Collection interface. The start of that interface is
An important issue in developing the (directed) Network class is to decide what public methods the class
should have: these constitute the abstract data-type network, that is, the user’s view of the Network class.
For the Network class, we have vertex-related methods, edge-related methods, and network-as-a-whole
methods. In the method specifications V represents the number of vertices and E represents the number
of edges.
/**
* Ensures that a specified Vertex object is an element of this Network object.
* The worstTime(V, E) is O(log V).
*
* @param vertex – the Vertex object whose presence is ensured.
*
* @return true – if vertex was added to this Network object by this call; returns
* false if vertex was already an element of this Network object when
* this call was made.
*
* @throws NullPointerException - if vertex is null.
*
*/
public boolean addVertex (Vertex vertex)
/**
* Ensures that a specified Vertex object is not an element of this Network object.
672 C H A P T E R 15 Graphs, Trees, and Networks
/**
* Determines the weight of an edge in this Network object.
* The worstTime(V, E) is O(log V).
*
* @param v1 – the beginning Vertex object of the edge whose weight is sought.
* @param v2 – the ending Vertex object of the edge whose weight is sought.
*
* @return the weight of edge <v1, v2>, if <v1, v2> forms an edge; return – 1.0 if
* <v1, v2> does not form an edge in this Network object.
*
* @throws NullPointerException – if v1 is null and/or v2 is null.
*
*/
public double getEdgeWeight (Vertex v1, Vertex v2)
/**
* Determines if this Network object contains an edge specified by two vertices.
* The worstTime(V, E) is O(log V).
*
* @param v1 – the beginning Vertex object of the edge sought.
15.6 A Network Class 673
/**
* Ensures that an edge is in this Network object.
* The worstTime(V, E) is O(log V).
*
* @param v1 – the beginning Vertex object of the edge whose presence
* is ensured.
* @param v2 – the ending Vertex object of the edge whose presence is
* ensured.
* @param weight – the weight of the edge whose presence is ensured.
*
* @return true – if the given edge (and weight) were added to this Network
* object by this call; return false, if the given edge (and weight)
* were already in this Network object when this call was made.
*
* @throws NullPointerException – if v1 is null and/or v2 is null.
*
*/
public boolean addEdge (Vertex v1, Vertex v2, double weight)
/**
* Ensures that an edge specified by two vertices is absent from this Network
* object.
* The worstTime (V, E) is O (V log V).
*
* @param v1 – the beginning Vertex object of the edge whose absence is
* ensured.
* @param v2 – the ending Vertex object of the edge whose absence is
* ensured.
*
* @return true – if the edge <v1, v2> was removed from this Network object
* by this call; return false if the edge <v1, v2> was not in this
* Network object when this call was made.
*
* @throws NullPointerException – if v1 is null and/or v2 is null.
*
*/
public boolean removeEdge (Vertex v1, Vertex v2)
674 C H A P T E R 15 Graphs, Trees, and Networks
Finally, we have the method specification for those methods that apply to the network as a whole, including
three f avors of iterators:
/**
* Initializes this Network object to be empty, with the ordering of
* vertices by an implementation of the Comparable interface.
*/
public Network()
/**
* Initializes this Network object to a shallow copy of a specified Network
* object.
*
* @param network – the Network object that this Network object is
* initialized to a shallow copy of.
*
* @throws NullPointerException – if network is null.
*
*/
public Network (Network<Vertex> network)
/**
* Determines if this Network object contains no vertices.
*
* @return true – if this Network object contains no vertices.
*
*/
public boolean isEmpty()
/**
* Determines the number of vertices in this Network object.
*
* @return the number of vertices in this Network object.
*
*/
public int size()
/**
* Determines if this Network object is equal to a given object.
*
* @param obj – the object this Network object is compared to.
*
* @return true – if this Network object is equal to obj.
*
15.6 A Network Class 675
*/
public boolean equals (Object obj)
/**
* Returns a LinkedList<Vertex> object of the neighbors of a specified Vertex object.
* The worstTime(V, E) is O(V).
*
* @param v – the Vertex object whose neighbors are returned.
*
* @return a LinkedList<Vertex> object of the vertices that are neighbors of v.
*
* @throws NullPointerException – if v is null.
*
*/
public LinkedList<Vertex> neighbors (Vertex v)
/**
* Returns an Iterator object over the vertices in this Network object.
*
* @return an Iterator object over the vertices in this Network object.
*
*/
public Iterator<Vertex> iterator()
/**
* Returns a breadth-first Iterator object over all vertices reachable from
* a specified Vertex object.
* The worstTime(V, E) is O(V log V).
*
* @param v – the start Vertex object for the Iterator object returned.
*
* @return a breadth-first Iterator object over all vertices reachable from v.
*
* @throws IllegalArgumentException – if v is not an element of this Network
* object.
*
* @throws NullPointerException – if vertex is null.
*
*/
public Iterator<Vertex> breadthFirstIterator (Vertex v)
/**
* Returns a depth-first Iterator object over all vertices reachable from
* a specified Vertex object.
* The worstTime(V, E) is O(V log V).
676 C H A P T E R 15 Graphs, Trees, and Networks
*
* @param v – the start Vertex object for the Iterator object returned.
*
* @return a depth – first Iterator object over all vertices reachable from v.
*
* @throws IllegalArgumentException – if v is not an element of this Network
* object.
*
* @throws NullPointerException – if vertex is null.
*
*/
public Iterator<Vertex> depthFirstIterator (Vertex v)
/**
* Determines if this (directed) Network object is connected.
* The worstTime(V, E) is O(V * V * log V).
*
* @return true – if this (directed) Network object is connected.
*
*/
public boolean isConnected()
/**
* Returns a minimum spanning tree for this connected Network object
* in which for any vertices u and v, if v is a neighbor of u then u is
* a neighbor of v, and the weights of those two edges are the same.
* The worstTime(V, E) is O(E log V).
*
* @return a minimum spanning tree for this connected Network object.
*
*/
public UndirectedWeightedTree<Vertex> getMinimumSpanningTree()
/**
* Finds a shortest path between two specified vertices in this Network
* object, and the total weight of that path.
* The worstTime(V, E) is O(E log V).
*
* @param v1 – the beginning Vertex object.
* @param v2 – the ending Vertex object.
*
* @return a LinkedList object containing the vertices in a shortest path
* from Vertex v1 to Vertex v2. The last element in the Linked
* List object is the total weight of the path, or -1.0 if there is no path.
15.6 A Network Class 677
*
* @throws NullPointerException – if v1 is null and/or v2 is null.
*
*/
public LinkedList<Object> getShortestPath (Vertex v1, Vertex v2)
/**
* Returns a String representation of this Network object.
* The averageTime(V, E) is O(V * V).
*
* @return a String representation of this Network object, with
* each vertex v, each neighbor w of that vertex, and the
* weight of the corresponding edge. The format is
* {v1=[w11 weight11, w12 weight12,...],
* v2=[w21 weight21, w22 weight22,...],
* ...
* vn=[wn1 weightn1, wn2 weightn2,...]}
*
*/
public String toString()
Here is a test of the getShortestPath method (network is a f eld in the NetworkTest class):
@Test
public void testShortest3()
{
network.addEdge ("S", "A", 2);
network.addEdge ("S", "B", 6);
network.addEdge ("S", "C", 5);
network.addEdge ("A", "D", 8);
network.addEdge ("B", "C", 2);
network.addEdge ("B", "D", 3);
network.addEdge ("B", "E", 2);
network.addEdge ("D", "T", 5);
network.addEdge ("D", "E", 3);
network.addEdge ("E", "T", 1);
network.addEdge ("C", "F", 2);
network.addEdge ("F", "T", 10);
The book’s website has the NetworkTest class, along with the UndirectedNetwork and Undirect
edWeightedTree classes.
Before we turn to developer’s issues in Section 15.6.2, we illustrate some of the Network class’s
methods in a class whose run method essentially consists of one method call after another. Each edge in
the f le network.in1 is two-way for the sake of the getMinimumSpanningTree method. (There is no
attempt at modularization.)
678 C H A P T E R 15 Graphs, Trees, and Networks
import java.util.*;
import java.io.*;
try
{
Scanner sc = new Scanner (new File ("network.in1"));
String start,
finish,
vertex1,
vertex2;
double weight;
if (networkIsConnected)
System.out.println ("spanning tree: " + network.getMinimumSpanningTree());
} // class NetworkExample
A C 18
C A 18
A D 7
D A 7
B E 3
E B 3
C E 28
E C 28
C F 20
F C 20
D F 8
F D 8
D G 2
G D 2
G F 4
F G 4
The shortest path from A to F and its total weight are [A, C, F, 38.0]
the weight of that edge, we will associate each adjacent vertex w with the weight of the edge v, w. In
other words, we will associate each vertex v in the Network object with a map that associates each vertex
w adjacent to v with the weight of the edge v, w.
This suggests that we will need a map in which each value is itself a map. We have two Map
implementations in the Java Collections Framework: HashMap and TreeMap. For inserting, removing,
and searching in a HashMap, averageTime(n) is constant (if the Uniform Hashing Assumption holds), but
worstTime(n) is linear in n for those three operations (even if the Uniform Hashing Assumption holds). It
is the user’s responsibility to ensure that the Uniform Hashing Assumption holds. Furthermore, the time
(on average and in the worst case) to iterate through a HashMap object is linear in n + m, where m is the
capacity of the hash table.
For a TreeMap object, the operations of insertion, removal, and search take logarithmic in n time,
both on average and in the worst case. The elements can be ordered “naturally” by implementing the
Comparable interface, or unnaturally with an implementation of an instance of the Comparator interface.
For both maps we choose the TreeMap class, whose performance in inserting, removing, and
searching is superb even in the worst case, over the HashMap class, whose average-time performance
is spectacular, but whose worst-time performance is pitiful.
In summary, the only f eld in the Network class is
protected TreeMap<Vertex, TreeMap<Vertex, Double>> adjacencyMap;
For each key v in adjacencyMap, each value will be a TreeMap object in which each key is a neighbor
w of v and each value is the weight of the edge v, w. For the sake of simplicity, we assume the vertices
will be ordered “naturally”. That is, the class corresponding to the type parameter Vertex must implement
the Comparable interface.
Figure 15.32 shows a simple network, and Figure 15.33 shows an internal representation (which
depends on the order in which vertices are inserted).
Mark
10.0 8.3
7.4
Karen Don Tara
Courtney
Tara (15.0)
FIGURE 15.33 The internal representation of the network in Figure 15.32. The value associated with a key is
shown in parentheses. “Don (_)”, the left child of the root, indicates that “Don” has no neighbors
In the TreeMap class, the worstTime(n) for the containsKey method is logarithmic in n, and so the
worstTime(V , E ) for the containsVertex method is logarithmic in V , where V represents the number
of vertices and E represents the number of edges. The averageTime(V , E ) for this method—and for all
the other methods in the Network class—is the same as worstTime(V , E ).
Adding a vertex to a Network object is straightforward:
public boolean addVertex (Vertex vertex)
{
if (adjacencyMap.containsKey (vertex))
return false;
adjacencyMap.put (vertex, new TreeMap<Vertex, Double>());
return true;
} // method addVertex
In the TreeMap class, the timing of the containsKey and put methods is logarithmic in the number of
elements in the map, and so worstTime(V , E ) is logarithmic in V .
The definitio of removeVertex requires some work. It is easy to remove a Vertex object v from
adjacencyMap, and thus remove its associated TreeMap object of edges going out from vertex. But
each edge going into v must also be removed. To accomplish this latter task, we will iterate over the
entries in adjacencyMap and remove from the associated neighbor map any edge whose vertex is v. Here
is the definition
public boolean removeVertex (Vertex v)
{
if (!adjacencyMap.containsKey (v))
return false;
15.6 A Network Class 683
neighborMap.remove (v);
} // for each vertex in the network
adjacencyMap.remove (v);
return true;
} // removeVertex
How long does this take? Iterating over the entries in adjacencyMap takes linear-in-V time, and remov-
ing a vertex from the neighbor map takes logarithmic-in-V time, so for the removeVertex method,
worstTime(V , E ) is linear-logarithmic in V .
Next, we’ll develop the definitio of an edge-related method. To count the number of edges in a
Network object, we use the fact that the size of any vertex’s associated neighbor map represents the
number of edges going out from that vertex. So we iterate over all the entries in adjacencyMap, and
accumulate the sizes of the associated neighbor maps. Here is the definition
public int getEdgeCount()
{
int count = 0;
The getEdgeCount method iterates over all entries in adjacencyMap, so worstTime(V , E ) is linear
in V .
Before we get to the methods that deal with the Network object as a whole, we need to say a few
words about the BreadthFirstIterator class (similar comments apply to the DepthFirstIterator
class).
The main issue with regard to the BreadthFirstIterator class is how to ensure a quick determi-
nation of whether a given vertex has been reached. To this end, we make reachable a TreeMap object,
with Vertex keys and Boolean values. The heading and field of this embedded class are:
protected class BreadthFirstIterator implements Iterator<Vertex>
{
protected Queue<Vertex> queue;
From this point, the method definition in the BreadthFirstIterator class closely follow the
algorithms in Section 15.5.1 For the next() method, we iterate over the current vertex’s neighbor map,
and check if each neighbor has been marked as reachable. For the next() method, worstTime(V , E ) is
linear in V log V .
Last, but by no means least, is the getShortestPath method to fin the shortest path from vertex
v1 to vertex v2. We start by fillin in some of the details from the earlier outline. For the sake of speed,
684 C H A P T E R 15 Graphs, Trees, and Networks
weightSum will be a TreeMap object that associates each Vertex object w with the sum of the weights of
all the edges on the shortest path so far from v1 to w. Similarly, predecessor will be a TreeMap object
that associates each vertex w with the vertex that is the immediate predecessor of w on the shortest path so
far from v1 to w. The only unusual f eld declaration is for the priority queue of vertex-weight pairs. The
PriorityQueue class has a single type parameter, so we create a VertexWeightPair class (nested in
the Network class) for the type argument. The VertexWeightPair class will have vertex and weight
fields a two-parameter constructor to initialize those f elds, and a compareTo method to order pairs by
their weights.
The heading and variables in the getShortestPath method are as follows:
/**
* Finds a shortest path between two specified vertices in this Network
* object.
* The worstTime(V, E) is O(E log E).
*
* @param v1 – the beginning Vertex object.
* @param v2 – the ending Vertex object.
*
* @return a LinkedList object containing the vertices in a shortest path
* from Vertex v1 to Vertex v2.
*
*/
public LinkedList<Object> getShortestPath (Vertex v1, Vertex v2)
{
final double MAX_PATH_WEIGHT = Double.MAX_VALUE;
PriorityQueue<VertexWeightPair> pq =
new PriorityQueue<VertexWeightPair>();
Vertex vertex,
to = null,
from;
VertexWeightPair vertexWeightPair;
double weight;
If either v1 or v2 is not in the Network, we return an empty LinkedList and we are done. Otherwise,
we perform the initializations referred to in Section 15.5.4. Here is this initialization code:
if (v1 == null || v2 == null)
throw new NullPointerException();
if (! (adjacencyMap.containsKey (v1) && adjacencyMap.containsKey (v2)))
return new LinkedList<Object>();
Iterator<Vertex> netItr = breadthFirstIterator(v1);
while (netItr.hasNext())
{
vertex = netItr.next();
weightSum.put (vertex, MAX_PATH_WEIGHT);
15.6 A Network Class 685
Now we f nd the shortest path, if there is one, from v1 to v2. As noted in Section 15.5.4, we have an
outer loop that removes a vertex-weight pair <from, totalWeight> from pq and then an inner loop that
iterates over the neighbors of from. The purpose of this inner loop is to see if any of those neighbors to
can have its weightSum value reduced to from’s weightSum value plus the weight of the edge <from,
to>. Here are these nested loops:
boolean pathFound = false;
while (!pathFound && !pq.isEmpty())
{
vertexWeightPair = pq.remove();
from = vertexWeightPair.vertex;
if (from.equals (v2))
pathFound = true;
else if (vertexWeightPair.weight <= weightSum.get(from))
{
for (Map.Entry<Vertex, Double> entry : adjacencyMap.get (from).entrySet())
{
to = entry.getKey();
weight = entry.getValue();
if (weightSum.get (from) + weight < weightSum.get (to))
{
weightSum.put (to, weightSum.get (from) + weight);
predecessor.put (to, from);
pq.add (new VertexWeightPair (to,weightSum.get (to)));
} // if
} // while from’s neighbors have not been processed
} // else path not yet found
} // while not done and priority queue not empty
All that remains is to create the path. We start by inserting v2 into an empty LinkedList object, and
then, using the predecessor map, keep prepending predecessors until v1 is prepended. Finally, we add
v2’s total weight, as a Double object, to the end of this LinkedList object, and return the LinkedList
object. Note that this LinkedList object has Object as the type parameter because the list contains both
vertices and Double values. Here is the remaining code in the getShortestPath method:
LinkedList<Object> path = new LinkedList<Object>();
if (pathFound)
{
Vertex current = v2;
while (!(current.equals (v1)))
{
path.addFirst (current);
current = predecessor.get (current);
} // while not back to v1
path.addFirst (v1);
path.addLast (weightSum.get (v2));
686 C H A P T E R 15 Graphs, Trees, and Networks
} // if path found
else
path.addLast (-1.0);
return path;
We now estimate worstTime(V , E ) for the getShortestPath method. We’ll f rst establish upper bounds
for worstTime(V , E ). For each edge x, y, y will be added to pq provided weightSum.get (x) +
weight of x, y is less than weightSum.get (y)
Because the vertex at the end of each edge may be added to pq, the size of pq —and therefore
the number of outer-loop iterations—is O(E ). During each outer loop iteration, one vertex-weight pair is
removed from pq, and this requires O(log E ) iterations (in the remove() method). Also, for each removal
of a vertex y, there is an inner loop in which each edge y, z is examined to see if z should be added to
pq. The total number of edges examined during all of these inner-loop iterations is O(E ).
From the previous paragraph, we see that the total number of iterations, even in the worst case, is
O(E log E + E ). We conclude that worstTime(V , E ) is O(E log E + E ). Also,
O(E log E + E ) = O(E log E ) = O(E log V )
The last equality follows from the fact that log E <= log V 2 = 2 log V . We have worstTime(V , E ) is
O(E log V ). If the network is connected (even as an undirected graph), V − 1 <= E , and so log E > =
log(V − 1). Then all of the upper bounds on iterations from above are also lower bounds, and we conclude
that worstTime(V , E ) is (E log V ).
Lab 23 introduces the best-known network problem, further explores the greedy-algorithm design
pattern, and touches on the topic of very hard problems.
You are now ready for Lab 23: The Traveling Salesperson Problem
The f nal topic in this chapter is backtracking. In Chapter 5, we saw how backtracking could be used to
solve a variety of applications. Now we expand the application domain to include networks and therefore,
graphs and trees.
finis city, f nd a path in which each edge’s distance is less than the previous edge’s distance. Figure 15.34
has sample data; the start and f nish cities are given f rst, followed by each edge:
Figure 15.35 depicts the network generated by the data in Figure 15.34.
One solution to this problem is the following path:
Boston Washington
Albany Washington 371
Boston Albany 166
Boston Hartford 101
Boston NewYork 214
Boston Trenton 279
Harrisburg Philadelphia 106
Harrisburg Washington 123
NewYork Harrisburg 168
NewYork Washington 232
Trenton Washington 178
FIGURE 15.34 A network: The firs line contains the start and f nish cities; each other line contains two cities
and the distance from the firs city to the second city
Boston
168
Harrisburg
Philadelphia
Washington
FIGURE 15.35 A network of cities; each edge weight represents the distance between the cities in the edge
688 C H A P T E R 15 Graphs, Trees, and Networks
The lowest-total-weight path is illegal for this problem because the distances increase (from 214 to 232):
214 232
Boston NewYork Washington
When a dead-end is reached, we can backtrack through the network. The basic strategy with backtracking
is to utilize a depth-firs search starting at the start position. At each position, we iterate through the
neighbors of that position. The order in which neighboring positions are visited is the order in which the
corresponding edges are initially inserted into the network. So we are guaranteed to fin a solution path if
one exists, but not necessarily the lowest-total-weight solution path.
Here is the sequence of steps in the solution generated by backtracking:
Boston
Albany
(dead-end, distance increased; re-trace to Boston)
Boston
X
Albany Hartford
(dead-end; re-trace to Boston)
Boston
X X
Harrisburg
Philadelphia
(dead-end; re-trace to Harrisburg)
Boston
X X
Harrisburg
X
Philadelphia Washington
(Success!)
Summary 689
The framework introduced in Chapter 5 supplies the BackTrack class and Application interface.
What about the Position class? That class must be modified row and column have no meaning in a
network. And the MazeUser and Maze classes must be revised as well. The details are left as Programming
Project 15.2.
SUMMARY
An undirected graph consists of a collection of distinct 3. Determining if a given graph is connected, that is,
elements called vertices, connected to other elements by if for any two vertices, there is a path from the firs
distinct vertex-pairs called edges. If the pairs are ordered, vertex to the second;
we have a directed graph. A tree, sometimes called a
4. Finding a minimum spanning tree for a network;
directed tree, is a directed graph that either is empty or
has an element, called the root element such that: 5. Finding a shortest path between two vertices in a
1. There are no edges coming into the root element; network.
CROSSWORD PUZZLE
1 2
4 5
www.CrosswordWeaver.com
ACROSS DOWN
7. The vertices v1, v2, … of a digraph 3. Another name for a weighted graph
are in _____order if vi precedes vj in
the ordering whenever <vi, vj> forms 5. A path in which the first and last vertices
an edge in the digraph. are the same and there are no repeated
edges
8. An acyclic, directed network with a
single source and a single sink 6. A graph is _____ if, for any given vertex,
there is a path from that vertex to any
9. The type of the only field in the other vertex in the graph.
Network class
8. In a graph, a sequence of vertices in
which each successive pair is an edge
Concept Exercises 691
CONCEPT EXERCISES
15.1 Draw a picture of the following undirected graph:
Vertices: A, B, C, D, E
Edges: {A, B}, {C, D}, {D, A}, {B, D}, {B, E}
15.2 a. Draw an undirected graph that has four vertices and as many edges as possible. How many edges does
the graph have?
b. Draw an undirected graph that has fiv vertices and as many edges as possible. How many edges does
the graph have?
c. What is the maximum number of edges for an undirected graph with V vertices, where V is any non-
negative integer?
d. Prove the claim you made in part (c).
Hint: Use induction on V.
e. What is the maximum number of edges for a directed graph with V vertices?
A C E
Assume the vertices were inserted into the graph in alphabetical order.
a. Perform a breadth-firs iteration of the undirected graph.
b. Perform a depth-firs iteration of the undirected graph.
15.4 Develop a high-level algorithm, based on the isConnected( ) algorithm in Section 15.5.2, to determine
if an undirected graph is connected.
15.5 For the network given below, determine the shortest path from A to H by brute force, that is, list all paths
and see which one has the lowest total weight.
18
C F
3 15 4 10
12 5 19
A B D H
6 5 3
22
E G
692 C H A P T E R 15 Graphs, Trees, and Networks
15.6 For the network given in Exercise 15.5, use Dijkstra’s algorithm (getShortestPath) to f nd the shortest
path from A to H.
15.7 Prim’s algorithm (getMinimumSpanningTree) and Dijkstra’s algorithm (getShortestPath) are
greedy: the locally optimal choice has the highest priority. In these cases, greed succeeds in the sense
that the locally optimal choice led to the globally optimal solution. Do all greedy algorithms succeed for all
inputs? In this exercise we explore coin-changing algorithms. In one situation, the greedy algorithm succeeds
for all inputs. In the other situation, the greedy algorithm succeeds for some inputs and fails for some inputs.
Suppose you want to provide change for any amount under a dollar using as few coins as possible. Since
“fewest” is best, the greedy (that is, locally optimal) choice at each step is the coin with largest value whose
addition will not surpass the original amount. Here is a method to solve this problem:
/**
* Prints the change for a given amount, with as few coins (quarters, dimes,
* nickels and pennies) as possible.
*
* @param amount – the amount to be given in change.
*
* @throws NumberFormatException – if amount is less than 0 or greater than
* 99.
*
*/
public static void printFewest (int amount)
{
if (amount < 0 || amount > 99)
throw new NumberFormatException();
For example, suppose that amount has the value 62. Then the output will be
25
25
10
1
1
Five is the minimum number of coins needed to make 62 cents from quarters, nickels, dimes, and pennies.
a. Show that the above algorithm is optimal for any amount between 0 and 99 cents, inclusive. Hint: First,
consider an amount of 0. Then amounts of 5, 10, or 25; then amounts of 15 or 20. Then add 25 to any
Programming Exercises 693
of the amounts in the previous sentence. After all legal amounts divisible by 5 have been considered,
consider all legal amounts that are not divisible by 5.
b. Give an example to show that a greedy algorithm is not optimal for all inputs if nickels are not available.
That is, if we have
…
for (int i = 0; i < 3; i++)
…
then the algorithm will not be optimal for some inputs.
15.8 Ignore the direction of arrows in the f gure for Exercise 15.5. Then that figur depicts an undirected network.
Use Prim’s algorithm to f nd a minimum spanning tree for that undirected network.
15.9 Ignore the direction of arrows and assume all weights are 1.0 in the figur for Exercise 15.5. Use Dijkstra’s
algorithm to f nd a shortest path (fewest edges) from A to H.
15.10 From the given implementation of the Network class, defin the removeEdge method in the Undirect
edNetwork class.
15.11 Re-order the edges in Figure 15.35 so that the solution path generated by backtracking is different from the
lowest-total-weight solution path.
15.12 For the digraph in Figure 15.30, there were two topological orders given. Find three other topological orders.
If a digraph has a cycle, can its vertices be put into a topological order? Explain.
PROGRAMMING EXERCISES
15.1 Modify the specifi ation of Dijkstra’s algorithm to f nd the shortest paths from a given vertex v to all other
vertices in a network. Unit-test and defin your method.
15.2 Modify the specificatio of Dijkstra’s algorithm to f nd a longest path between two vertices. Unit-test and
defin your method. The method assumes that the network is acyclic. Why?
15.3 In the program in Section 15.6.1, the getMinimumSpanningTree method is not called if the network is
not connected. In that program, comment out the line
if (networkIsConnected)
Run that program to create a network that is not connected but for which the getMinimumSpanningTree
method still succeeds.
Hint: Not all edges are used in obtaining a minimum spanning tree.
15.4 In the Network class, unit-test and defin a method to produce a topological order. Here is the specification
/**
* Sorts this acyclic Network object into topological order.
* The worstTime(V, E) is O(V log V).
*
* @return an ArrayList object of the vertices in topological order. Note: if the
* size of the ArrayList object is less than the size of this Network object,
* the Network object must contain a cycle, and the ArrayList will not
* contain all the network’s vertices in topological order.
694 C H A P T E R 15 Graphs, Trees, and Networks
*
*/
public ArrayList<vertex> sort()
Hint: First, construct a TreeMap object, inCount, that maps each vertex w to the number of vertices
to which w is adjacent. For example, if the Network object has three edges of the form <?, w>, then
inCount will map w to 3 (technically, to new Integer (3) ). After inCount has been constructed, push
onto a stack (or enqueue onto a queue) each vertex that inCount maps to 0. Then loop until the stack is
empty. During each loop iteration,
1. pop the stack;
2. append the popped vertex v to an ArrayList object, orderedVertices;
3. decrease the value, in inCount ’s mapping, of any vertex w such that <v, w> forms an edge;
4. if inCount now maps w to 0, push w onto the stack.
After the execution of the loop, the ArrayList object, containing the vertices in a topological order, is
returned.
ANALYSIS: Each line of each input fil will contain a pair of cities and the weight of the edge connecting the
firs city to the second city.
Assume that the input fil tsp.in1 contains the following edges and weights:
ab6
ba6
ac3
ca3
ad5
da5
bc4
cb4
bd7
db7
cd9
dc9
Assume that the input fil tsp.in2 contains the following edges and weights:
a b 12
b a 12
ac4
ca4
a d 10
d a 10
ae3
ea3
b c 99
cb9
bd7
db7
be5
eb5
cd9
dc9
c e 19
e c 19
de4
ed4
Please enter two cities and their distance; the sentinel is ***
Boston NewYork 214
Please enter two cities and their distance; the sentinel is ***
Boston Trenton 279
Please enter two cities and their distance; the sentinel is ***
Harrisburg Washington 123
Please enter two cities and their distance; the sentinel is ***
NewYork Harrisburg 168
Please enter two cities and their distance; the sentinel is ***
NewYork Washington 232
Please enter two cities and their distance; the sentinel is ***
Trenton Washington 178
Please enter two cities and their distance; the sentinel is ***
***
The initial state is as follows:
Please enter two cities and their distance; the sentinel is ***
Boston Trenton 279
Please enter two cities and their distance; the sentinel is ***
Boston NewYork 214
Programming Exercises 697
Please enter two cities and their distance; the sentinel is ***
Harrisburg Washington 123
Please enter two cities and their distance; the sentinel is ***
NewYork Harrisburg 168
Please enter two cities and their distance; the sentinel is ***
NewYork Washington 232
Pease enter two cities and their distance; the sentinel is ***
Trenton Washington 178
Note: The solution to this System Test is different from the solution to System Test 1 because in this System Test,
the Boston-Trenton edge is entered before the Boston-NewYork edge.
/**
* Determines the slack time for each activity in this Project Network object.
* The worstTime(V, E) is O(V log V).
*
* @return a TreeMap object that maps each activity, that is, each
* edge triple <v1, v2, weight>, to the slack time for that activity.
*
*/
public TreeMap<EdgeTriple, Double> getSlackTimes()
Hint: First, create a TreeMap object, inMap, that maps each vertex v to the TreeMap object of vertex-weight pairs
<w, weight> such that <w, v> forms an edge with weight weight. That is, inMap is similar to adjacencyMap
except that each vertex v is mapped to the vertices coming into v; in adjacencyMap, each vertex v is mapped to
the vertices going out from v.
Then loop through the ArrayList object (from Programming Exercise 15.4) in topological order. For each
vertex v, use inMap to calculate ET(v). (The functional notation, “ET(v)”, suggests a mapping, and earliestTime
can be another HashMap object!) Then loop through the ArrayList object in reverse order to calculate LT(v) for
each vertex v. Then calculate the slack time for each vertex.
browser.in10:
browser.in11:
In Xanadu did <a href = browser.in12>browser12</a> Kubla Khan
A stately pleasure-dome decree:
Where Alph, the neural <a href =
browser.in10>browser10</a> network sacred river, ran
Through caverns neural network measureless to man
Down to a network sunless sea.
network
browser.in12:
browser.in13:
browser.in14:
Note: The above file are different from the same-named file in Programming Project 13.2. The original version
of search.ser is available from the book’s web site.
Incorporating hyperlink connectivity into a search engine was one of the innovations of Google, and the
graduate students who created Google (Sergei Brin and Larry Page) are decabillionaires.
System Test 1:
(The end-user searches for “neural network”)
browser.in10 11
browser.in13 9
browser.in11 8
browser.in12 8
browser.in14 6
(The end-user searches for “network”)
Here are the results of the old search for “network”
browser.in10 7
browser.in12 6
browser.in11 6
browser.in13 5
browser.in14 4
browser.in10 11
browser.in13 9
browser.in11 8
browser.in12 8
browser.in14 6
System Test 2:
(The end-user searches for “neural network”)
Here are the results of the old search for “neural network”
browser.in10 11
browser.in13 9
browser.in11 8
browser.in12 8
browser.in14 6
browser.in10 8
browser.in12 8
browser.in11 7
browser.in13 6
browser.in14 5
In System Test 2, the search for “neural network” is referred to as an “old” search because search.ser was updated
when System Test 1 ended. The f le search.ser contains search information, but not connectivity information.
Additional Features of the JAVA APPENDIX 1
Collections Framework
A1.1 Introduction
The Java Collections Framework has several features beyond those encountered so far in this book. This
appendix focuses on two of those features: serialization and fail-fast iterators.
A1.2 Serialization
Suppose we have gone to the trouble of creating a large and complex HashMap object as part of a project.
After we have used that HashMap object in an execution of the project, we might want to save the
HashMap, on f le, so that we can later resume the execution of the project without having to re-construct
the HashMap. Fortunately, this is easily done with just a couple of statements.
How? All of the collection classes in the Java Collections Framework implement the Serializable
interface that is in the package java.io. This interface has no methods, but merely provides information
to the Java virtual machine about sending instances of the class to/from a stream (a sequence of bytes).
Specifically any class that implements the Serializable interface will be able to copy any object in the
class to an output stream—that is, to “serialize” the elements in the object. “Deserialization” reconstructs
the original object from an input stream.
For a simple example, suppose we have created an ArrayList object named fruits, whose ele-
ments are of type String. We can create an ObjectOutputStream and then write fruits to the
FileOutputStream object whose path is “fruits.ser” as follows:
try
{
ObjectOutputStream oos = new ObjectOutputStream (
new FileOutputStream ("fruits.ser"));
oos.writeObject (fruits);
} // try
catch (IOException e)
{
System.out.println (e);
} // catch
The ArrayList object fruits has been serialized , that is, is saved as a stream of bytes. The fil qualifier
“ser”, is an abbreviation of “serializable,” but you are free to use any qualifie you want, or no qualifier
The definitio of the writeObject method depends on the class of the object serialized. For example,
here is the definitio in the ArrayList class:
/**
* Save the state of the <tt>ArrayList</tt> instance to a stream (that
* is, serialize it).
701
702 A P P E N D I X 1 Additional Features of the JAVA Collections Framework
The size of the ArrayList object is saved f rst, and then the length of the elementData array f eld, so
that an array of the exact same capacity can be created when the ArrayList object is reconstructed from
the f le. Finally, the elements in the ArrayList object are saved to the f le.
In another program, or in a later execution of the same program, we can de-serialize fruits. To
accomplish that task, we create an ObjectInputStream object to read the stream of bytes, from the
FileInputStream whose path is “fruits.ser”, into fruits:
try
{
ObjectInputStream ois = new ObjectInputStream (
new FileInputStream ("fruits.ser"));
fruits = (ArrayList<String>)ois.readObject ();
} // try
catch (Exception e)
{
System.out.println (e);
} // catch
In the readObject method, the f rst value read from the stream represents the size of the ArrayList
object, and the next value represents the length of the underlying array, and f nally, the individual elements
are read, one at a time.
An object that is saved and then retrieved in another program (or later execution of the same program)
is called persistent. In this example, the ArrayList object fruits is persistent. And the same mechanism
shown above can be used to create persistent instances of any of the other collection classes in the Java
Collections Framework.
You can make your own classes serializable. Right after the class heading, add
implements java.io.Serializable;
A1.3 Fail-Fast Iterators 703
If all that have to be saved are f elds, you needn’t defin writeObject and readObject methods: the
default serialization/deserialization will handle f elds. In the ArrayList example just listed, the defaults
were not enough; the length of the array elementData and the elements themselves had to be saved. That
is why the ArrayList class explicitly define writeObject and readObject methods.
list.add ("humble");
list.add ("meek");
list.add ("modest");
The program constructs a LinkedList object, list, of three elements. An iterator, itr, starts iterating
over list. When itr calls its next() method, the element “humble” at index 0 is returned, and itr
advances to index 1. At this point, list removes the element at index 2. The second call to the next()
method should be f agged as illegal. Why? The element “meek” at index 1 could be returned, but itr
should not be allowed to advance to and print the element at index 2 because there is no longer an element
at index 2. So what is best for the programmer is to have the error detected when the second call to the
next() method is made, rather than having the error detected later in the program.
And that is exactly what happens. When this program was run, the value “humble” was output,
and there was an exception thrown: ConcurrentModificationException. This exception was thrown
when the second call to the next() method was made.
The idea is this: Once you start iterating through a collection in the Java Collections Framework,
you should not modify the collection except with messages to that iterator. Otherwise, the integrity of that
iterator may be compromised, so ConcurrentModificationException is thrown. Such iterators are
fail-fast: the exception is thrown as soon as the iterator may be invalid. The alternative, waiting until the
iterator is known to be invalid, may be far more difficul to detect.
704 A P P E N D I X 1 Additional Features of the JAVA Collections Framework
The mechanism for making iterators fail-fast involves two fields one in the collection, and one in the
iterator. We have studied six Collection classes within the Java Collections Framework: ArrayList,
LinkedList, Stack, PriorityQueue, TreeSet, and HashSet. Each of these classes has a modCount
fiel 1 that is initialized to 0. Each time the collection is structurally modified modCount —for “modificatio
count”—is incremented by 1.
The iterator class embedded in the Collection class has an expectedModCount field which is ini-
tialized to modCount in the iterator’s constructor. Whenever the iterator structurally modifie the collection
(for example, with a call to itr.remove()), both expectedModCount and modCount are incremented
by 1. Also, whenever the iterator object itself is modifie (for example, with a call to itr.next() or
itr.remove()), there is a test:
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
If modCount and expectedModCount are unequal, that means the collection has been structurally modi-
fied but not by the iterator. Then for example, as in the program at the beginning of this section, a call to
the next() method might advance to an element that is no longer in the collection. Instead of returning
a possibly incorrect value, the next() method throws ConcurrentModificationException.
If, for some reason, you want to bypass this fail-fast protection, you can catch ConcurrentModi
ficationException and do nothing in the catch block.
1 For
the ArrayList and LinkedList classes, modCount is inherited from AbstractList. TreeMap and HashMap explicitly declare the
modCount field TreeSet and HashSet utilize the modCount fi ld in the backing map fie d (an instance of TreeMap and HashMap, respectively).
Mathematical Background APPENDIX 2
A2.1 Introduction
Mathematics is one of the outstanding accomplishments of the human mind. Its abstract models of real-life
phenomena have fostered advances in every f eld of science and engineering. Most of computer science
is based on mathematics, and this book is no exception. This appendix provides an introduction to those
mathematical concepts referred to in the chapters. Some exercises are given at the end of the appendix, so
that you can practice the skills while you are learning them.
A finit sequence t is a function such that for some positive integer k , called the length of the
sequence, the domain of t is the set { 0, 1, 2, . . . , k-1 }. For example, the following define a f nite
sequence of length 4:
t(0) = “Karen”
t(1) = “Don”
t(2) = “Mark”
t(3) = “Courtney”
Because the domain of each finit sequence starts at 0, the domain is often left implicit, and we write
t = “Karen”, “Don”, “Mark”, “Courtney”
Of course, there is nothing special about the letter “i .” We can write, for example,
10
(1/j )
j =1
as shorthand for
1 + 1/2 + 1/3 + · · · + 1/10
Similarly, if n > = m,
n
(k 2−k )
k =m
is shorthand for
m2−m + (m+1)2−(m+1) + · · · + n2−n
A2.4 Logarithms 707
Another abbreviation, less frequently seen than summation notation, is product notation. For example,
4
a[k ]
k =0
is shorthand for
a [0] * a [1] * a [2] * a [3] * a [4]
A2.4 Logarithms
John Napier, a Scottish baron and part-time mathematician, firs described logarithms in a paper he pub-
lished in 1614. From that time until the invention of computers, the principal value of logarithms was in
number-crunching: logarithms enabled multiplication (and division) of large numbers to be accomplished
through mere addition (and subtraction).
Nowadays, logarithms have only a few computational applications—for example, the Richter scale
for measuring earthquakes. But logarithms provide a useful tool for analyzing algorithms, as you saw (or
will see) in Chapter 3 through 15.
We defin logarithms in terms of exponents, just as subtraction can be define in terms of addition,
and division can be define in terms of multiplication.
Given a real number b > 1, we refer to b as the base. The logarithm, base b, of any real number
x > 0, written
logb x
is define to be that real number y such that
by = x
For example, log2 16 = 4 because 24 = 16. Similarly, log10 100 = 2 because 102 = 100. What is log2 64?
What is log8 64? Estimate log10 64.
The following relations can be proved from the above definitio and the corresponding properties of
exponents. For any real value b > 1 and for any positive real numbers x and y,
Properties of Logarithms
1. logb 1 = 0
2. logb b = 1
3. logb (xy) = logb x + logb y
4. logb (x/y) = logb x—logb y
5. logb bx = x
6. blogb x = x
7. logb xy = y logb x
708 A P P E N D I X 2 Mathematical Background
From these equations, we can obtain the formula for converting logarithms from one base to another.
For any bases a and b> 1 and for any x > 0,
logb x = logb a log a x {by property 5}
= (loga x )(logb a){by property 7}
The base e ( ≈ 2.718) has special significanc in calculus; for this reason logarithms with base e are called
natural logarithms and are written in the shorthand ln instead of loge .
To convert from a natural logarithm to a base 2 logarithm, we apply the base-conversion formula
just derived. For any x > 0,
ln x = (log2 x )(ln 2)
Dividing both sides of this equation by ln 2, we get
log2 x = ln x / ln 2
We assume the function ln is predefined so this equation can be used to approximate log2 x . Similarly,
using base-10 logarithms,
log2 x = log10 x / log10 2
The function ln and its inverse exp provide one way to perform exponentiation. For example, suppose we
want to calculate x y where x and y are of type double and x > 0. We firs rewrite x y :
y
x y = e ln(x ) {by Property 6, above}
= e y ln x {by Property 7}
This last expression can be written in Java as
To help you to understand why this principle makes sense, suppose that S1 , S2 , . . . is a sequence of
statements for which cases 1 and 2 are true. By case 1, S1 must be true. By case 2, since S1 is true, S2
must be true. Applying case 2 again, since S2 is true, S3 must be true. Continually applying case 2 from
this point, we conclude that S4 is true, and then that S5 is true, and so on. This indicates that the conclusion
in the principle is reasonable.
To prove a claim by mathematical induction, we firs state the claim in terms of a sequence of
statements S1 , S2 , . . . . We then show that S1 is true—this is called the base case. Finally, we need to
prove case 2, the inductive case.
Here is an outline of the strategy for a proof of the inductive case: let n be any positive integer and
assume that Sn is true. To show that Sn+1 is true, relate Sn+1 back to Sn , which is assumed to be true.
The remainder of the proof often utilizes arithmetic or algebra.
Proof We start by stating the claim in terms of a sequence of statements. For n = 1, 2, . . ., let Sn be the
statement
n
i = n(n + 1)/2
i=1
1. Base case.
1
i = 1 = 1(2)/2
i=1
Therefore S1 is true.
2. Inductive case. Let n be any positive integer and assume that Sn is true. That is,
n
i = n(n + 1)/2
i=1
We need to show that Sn+1 is true, namely,
n+1
i = (n + 1)(n + 2)/2
i=1
We relate Sn+1 back to Sn by making the following observation: The sum of the first n+1 integers is the
sum of the first n integers plus n+1. That is,
n+1
n
i= i + (n + 1)
i=1 i=1
= n(n + 1)/2 + (n + 1) //because Sn is assumed true
= n(n + 1)/2 + 2(n + 1)/2
= (n(n + 1) + 2(n + 1))/2
= (n + 2)(n + 1)/2
710 A P P E N D I X 2 Mathematical Background
We conclude that Sn+1 is true (whenever Sn is true). So, by the Principle of Mathematical Induction, the
statement Sn is true for any positive integer n.
The difference between this version and the previous version is in the inductive case. Here, when we want
to establish that Sn+1 is true, we can assume that S1 , S2 , . . ., Sn are true.
Before you go any further, try to convince (or at least, persuade) yourself that this version of the
principle is reasonable. At firs glance, you might think that the strong form is more powerful than the
original version. But in fact, they are equivalent.
We now apply the strong form of the Principle of Mathematical Induction to obtain a simple but
important result.
Show that for any positive integer n, the number of iterations of the following loop statement is floor(log2 n):
while (n > 1)
n = n / 2;
Proof For n = 1, 2, . . ., let t(n) be the number of loop iterations. For n = 1, 2, . . ., let Sn be the statement:
t(n) = floor(log2 n)
1. Base case. When n = 1, the loop is not executed at all, and so t(n) = 0 = floor (log2 n). That is S1 is true.
2. Inductive case. Let n be any positive integer and assume that S1 , S2 , . . ., Sn are all true. We need to
show that Sn+1 is true. There are two cases to consider:
a. n+1 is even. Then the number of iterations after the first iteration is equal to t((n + 1)/2). Therefore,
we have
t(n+1) = 1 + t((n+1)/2)
= 1 + floor(log2 ((n+1)/2)) {by the induction hypothesis}
= 1 + floor(log2 (n+1) − log2 (2)) {because log of quotient equals difference of logs}
= 1 + floor(log2 (n+1) − 1)
= 1 + floor(log2 (n+1)) − 1
= floor(log2 (n+1))
Thus Sn+1 is true.
A2.5 Mathematical Induction 711
b. n+1 is odd. Then the number of iterations after the first iteration is equal to t(n/2). Therefore, we
have
t(n + 1) = 1 + t(n/2)
= 1 + floor(log2 (n/2)) {by the induction hypothesis}
= 1 + floor(log2 n − log2 2) {log of quotient equals difference of logs}
= 1 + floor(log2 n − 1)
= 1 + floor(log2 n) − 1
= floor(log2 n)
= floor(log2 (n + 1) {since log2 (n + 1) cannot be an integer}
Thus Sn+1 is true.
Therefore, by the strong form of the Principle of Mathematical Induction, Sn is true for any positive
integer n.
Before we leave this example, we note that an almost identical proof shows that in the worst case for
an unsuccessful binary search, the number of iterations is
floor(log2 n) + 1
In the original and “strong” forms of the Principle of Mathematical Induction, the base case consists of a
proof that S1 is true. In some situations we may need to start at some integer other than 1. For example,
suppose we want to show that
n! > 2n
for any n> = 4. (Notice that this statement is false for n = 1, 2, and 3.) Then the sequence of statements
is S4 , S5 , . . . For the base case we need to show that S4 is true.
In still other situations, there may be several base cases. For example, suppose we want to show
that
fi (n) < 2n
for any positive integer n. (The method fib, define in Lab 7, calculates Fibonacci numbers.) The base
cases are:
fi (1) < 21
and
fi (2) < 22
These observations lead us to the following:
The general form extends the strong form by allowing the sequence of statements to start at any integer
(K ) and to have any number of base cases (SK , SK +1 ,. . .,SL ). If K = L = 1, the general form reduces to
the strong form.
The next two examples use the general form of the Principle of Mathematical Induction to prove
claims about Fibonacci numbers.
Show that
fib(n) < 2n
for any positive integer n.
Proof For n = 1, 2, . . ., let Sn be the statement
fib(n) < 2n
In the terminology of the general form of the Principle of Mathematical Induction, K = 1 because the
sequence starts at 1; L = 2 because there are two base cases.
1. fib(1) = 1< 2 = 21 , and so S1 is true.
fib(2) = 1< 4 = 22 , and so S2 is true.
2. Let n be any integer ≥ 2 and assume that S1 , S2 , . . ., Sn are true. We need to show that Sn+1 is true (that
is, we need to show that fib(n+1)<2n+1 ).
By the definition of Fibonacci numbers,
fib(n+1) = fib(n) + fib(n−1), forn ≥ 2.
Since S1 , S2 , . . ., Sn are true, we know that Sn−1 and Sn are true. Thus
fib(n−1) < 2n−1
and
fib(n) < 2n
We then get
fib(n+1) = fib(n) + fib(n−1)
< 2n + 2n−1
< 2n + 2n
= 2n+1
And so fib (n+1) is true.
We conclude, by the general form of the Principle of Mathematical Induction, that
fib(n) < 2n
for any positive integer n.
You could now proceed, in a similar fashion, to develop the following lower bound for Fibonacci numbers:
fi (n) >(6/5)n
for all integers n ≥ 3.
Hint: Use the general form of the Principle of Mathematical Induction, with K = 3 and L = 4.
A2.5 Mathematical Induction 713
Now that lower and upper bounds for Fibonacci numbers have been established, you might wonder
if we can improve on those bounds. We will do even better. In the next example we verify an exact, closed
formula for the nth Fibonacci number. A closed formula is one that is neither recursive nor iterative.
Before you look at the proof below, calculate a few values to convince yourself that the formula actually does
provide the correct values.
Proof For n = 1,2, . . ., let Sn be the statement
⎡ ⎤
√ √
1 ⎣ 1+ 5 n 1− 5 n ⎦
fib(n) = √ −
5 2 2
√ √
1+ 5 1− 5
Let x = 2 and let y = 2 .
Note that
√ √ √
(1 + 5)2 1+2 5+5 3+ 5
x2 = = = =x+1
4 4 2
Similarly, y2 = y + 1.
We now proceed with the proof.
1.
⎡ ⎤
√ √
1 ⎣ 1+ 5 1 1− 5 1 ⎦
fib(1) = √ − = 1, so S1 is true
5 2 2
The next example establishes part 1 of the Binary Tree Theorem in Chapter 9: the number of leaves is at
most the number of elements in the tree plus one, all divided by 2.0. The induction is on the height of the
tree and so the base case is for single-item tree, that is, a tree of height 0.
Let t be a non-empty binary tree, with leaves(t) leaves and n(t) elements. We claim that
n(t) + 1
leaves(t) ≤
2.0
Proof For k = 0, 1, 2, . . ., let Sk be the statement: For any nonempty binary tree t of height k,
n(t) + 1
leaves(t) ≤
2.0
2. Let k be any integer ≥ 0, and assume that S0 , S1 , . . ., Sk are true. We need to show that Sk+1 is true.
Let t be a non-empty binary tree of height k+1. Both leftTree(t) and rightTree(t) have height ≤ k, so both
satisfy the induction hypothesis. That is,
n(leftTree(t)) + 1
leaves(leftTree(t)) ≤
2.0
and
n(rightTree(t)) + 1
leaves(rightTree(t)) ≤
2.0
But each leaf in t is either in leftTree(t) or in rightTree(t). That is,
leaves(t) = leaves(leftTree(t)) + leaves(rightTree(t))
Then we have
n(leftTree(t)) + 1 n(rightTree(t)) + 1
leaves(t) ≤ +
2.0 2.0
n(leftTree(t)) + n(rightTree(t)) + 1 + 1
=
2.0
Except for the root element of t, each element in t is either in leftTree(t) or in rightTree(t), and so
n(t) = n(leftTree(t)) + n(rightTree(t)) + 1
Substituting this equation’s left-hand side for its right-hand side in the previous inequality, we get
n(t) + 1
leaves(t) ≤
2.0
That is, Sk+1 is true.
Therefore, by the general form of the Principle of Mathematical Induction, Sk is true for any nonnegative
integer k. This completes the proof of the claim.
The next example, relevant to Chapter 12, shows that the height of any red-black tree is logarithmic in n.
As noted in Chapter 12, the TreeMap and TreeSet classes require only logarithmic time—even in the worst
case—to insert, remove, or search. The reason for this speed is that those classes are based on red-black
trees, whose height is always logarithmic in the number of elements in the tree. The proof that red-black
trees are balanced utilizes the General Form of the Principle of Mathematical Induction.
Theorem The height of any red-black tree is logarithmic in n, the number of elements in the tree.
Claim 1. Let y be the root of a subtree of a red-black tree. The number of black elements is the same in a
path from y to any one of its descendants with no children or one child.
Suppose x is the root of a red-black tree, and y is the root of a subtree. Let b0 be the number of black
elements from x (inclusive) to y (exclusive). For example, in Figure A2.1, if x is 50 and y is 131, b0 = 1, the
number of black elements in the path from 50 through 90; 131 is not counted in b0 .
716 A P P E N D I X 2 Mathematical Background
50
30 90
20 40
80 131
60 85 100 150
140 160
135
Let b1 be the number of black elements from y (inclusive) to any one of its descendants with no children
or one child (inclusive), and let b2 be the number of black elements from y (inclusive) to any other one of its
descendants with no children or one child (inclusive). Figure A2.2 depicts this situation.
For example, in Figure A2.1, suppose that y is 131 and the two descendants of y are 100 and 135.
Then b0 = 1 because 50 is black; b1 = 2 because 131 and 100 are black; b2 = 2 because 131 and 140 are
black.
In general, by the Path Rule for the whole tree, we must have b0 + b1 = b0 + b2 . This implies that
b1 = b2 . In other words, the number of black elements is the same in any path from y to any of its descendants
that have no children or one child. We have established Claim 1.
Now that Claim 1 has been verified, we can make the following definition. Let y be an element in a
red-black tree; we define the black-height of y, written bh(y), as follows:
bh(y) = the number of black elements in any path from y to any descendant of
y that has no children or one child.
x
\
.
. b0 black elements from x (inclusive) to y (exclusive)
.
\
y
/ \
. .
b1 . . b2
. .
/ /
z1 z2
FIGURE A2.2 Part of a red-black tree rooted at x; y is a descendant of x, and z1, and z2 are two arbitrarily
chosen descendants of y that have no children or one child. Then b0 represents the number of black descendants
in the path from x up to but not including y; b1 and b2 represent the number of black elements in the path from
y to z1 and z2 , respectively
A2.5 Mathematical Induction 717
60
30 85
7 49 70 98
10 65 80 91 111
61 75 82 99 115
By Claim 1, the number of black elements must be the same in any path from an element to any of its
descendants with no children or one child. So black-height is well defined. For an example of black height,
in Figure 12.3 from Chapter 12, the black-height of 50 is 2, the black height of 20, 30, 40, or 90 is 1, and the
black-height of 10 is 0. Figure A2.3 shows a red-black tree in which 60 has a black height of 3 and 85 has a
black height of 2
The left and right subtrees of t have height ≤ k, and so the induction hypothesis applies and we have
n(leftTree(t)) ≥ 2bh(v1 ) − 1
and
n(rightTree(t)) ≥ 2bh(v2 ) − 1
The number of elements in t is one more than the number of elements in leftTree(t) plus the number of
elements in rightTree(t).
Putting all of the above together, we get:
This complete the proof of the inductive case when the root of t has two children.
Therefore, by the Principle of Mathematical Induction, Claim 2 is true for all non-empty subtrees of
red-black trees.
Finally, we get to show the important result that the height of any non-empty red-black tree is logarithmic in
n, where n represents the number of elements in the tree.
Theorem For any non-empty red-black tree t with n elements, height(t) is logarithmic in n.
Proof Let t be a non-empty red-black tree. By the Red Rule, at most half of the elements in the path from
the root to the farthest leaf can be red, so at least half of those elements must be black. That is,
bh(root(t)) ≥ height(t)/2
From Claim 2,
n(t) ≥ 2bh(root(t)) − 1
≥ 2height(t)/2 − 1
This inequality implies that height(t) is O(log n). By the Binary Tree Theorem in Chapter 9,
This theorem states that red-black trees never get far out of balance. For an arbitrary binary search tree on
the other hand, the height can be linear in n—for example, if the tree is a chain.
Concept Exercises 719
CONCEPT EXERCISES
A2.1 Use mathematical induction to show that, in the Towers of Hanoi game from Chapter 5, moving n disks from
pole a to pole b requires a total of 2n − 1 moves for any positive integer n.
A2.2 Use mathematical induction to show that for any positive integer n,
n
n
Af (i ) = A f (i )
i =1 i =1
Base Case: If n = 1, there is only one dog in the set, so all dogs in that set have the same hair color. Thus, S1 is
true.
Inductive Case: Let n be any positive integer and assume that Sn is true; that is, in any set of n dogs, all the dogs
in the group have the same hair color. We need to show that Sn+1 is true. Suppose we have a set of n + 1 dogs:
d1 , d2 , d3 , . . . , dn , dn+1
The set d1 , d2 , d3 , . . ., dn has size n, so by the Induction hypothesis, all the dogs in that set have the same hair
color.
The set d2 , d3 , . . ., dn , dn+1 also has size n, so by the Induction hypothesis, all the dogs in that set have the same
hair color.
But the two sets have at least one dog, dn , in common. So whatever color that dog’s hair is must be the color of
all dogs in both sets, that is, of all n + 1 dogs. In other words, Sn+1 is true.
Therefore, by the Principle of Mathematical Induction, Sn is true for any non-negative integer n.
Choosing a Data Structure APPENDIX 3
A3.1 Introduction
This appendix serves as a brief summary of much of the material from Chapters 6–8 and 12–15. In par-
ticular, we will categorize the eight major collection classes (ArrayList, LinkedList, Stack, Queue1 ,
TreeMap, PriorityQueue, HashMap, Network) from those chapters in two different ways. The f rst
classificatio will be by the ordering of the elements in the collection: time-based, index-based, comparison-
based and hash-based. The second classificatio will be by the time to perform common operations on an
element in the collection: access, insertion, deletion and search. For each such operation, averageTime(n)
will be provided, that is, the average time (assuming each event is equally likely) to perform the operation
in a collection of n elements. Whenever the estimate of averageTime(n) differs from the corresponding
estimate for worstTime(n), both estimates will be given.
1 Implemented by LinkedList.
721
722 A P P E N D I X 3 Choosing a Data Structure
searching for an element in an ArrayList object takes linear-in-n time, but only logarithmic-in-n time if
the elements in the ArrayList object are in order according to a comparator.
Superficially a LinkedList object is a pitiful choice: Both averageTime(n) and worstTime(n) are
linear-in-n for accessing (or replacing) the element at a given index, as well as for inserting an element
at—or deleting an element from—a given index. But LinkedList objects sparkle during an iteration:
It takes only constant time to access (or replace) the “current” element, or to insert an element in front
of the current element or to delete the current element. Finally, searching for an arbitrary element in a
LinkedList object takes linear-in-n time.
The bottom line, as noted in Section 7.3.3 of Chapter 7, is this:
If the application entails a lot of accessing and/or replacing elements at widely varying indexes, an
ArrayList object will be much faster than a LinkedList object. But if a large part of the application
consists of iterating through a list and making insertions and/or removals during the iterations, a
LinkedList object can be much faster than an ArrayList object.
aspect in a network is the relationship, called an “edge” between two neighboring vertices. In the Network
class, the only f eld is a TreeMap object in which each key is a vertex, and each value is the TreeMap
object in which each key is a neighbor of the vertex, and each value is the weight of the edge connecting
the vertex to the neighbor.
As you might expect, most of the usual operations have times that are similar to those of the
TreeMap class. Specifically with V representing the number of vertices and E the number of edges, the
worstTime(V , E ) and averageTime(V , E ) are logarithmic in V for each of the following operations:
a. accessing the neighbors of a given vertex;
b. adding a new vertex to the network;
c. searching the network for a given vertex.
In removing a vertex from a network, we must also remove all edges going to that vertex, and for this
operation worstTime(V , E ) and averageTime(V , E ) are linear-logarithmic in V .
Index-Based
ArrayList con lin lin lin*
( at given index ) (any)
Comparison Based
TreeMap log log log log
Hash Based
HashMap con [lin] con [lin] con [lin] con [lin]
ACM/IEEE-CS Joint Curriculum Task Force, Computing Flajolet, P. and A. Odlyzko, “The Average Height of
Curricula 1991 , Association for Computing Machin- Binary Trees and Other Simple Trees,” Raports
ery, New York, 1991. de Recherche, #56, Institut National de Recherche
Adel’son-Vel’skii, G.M., and E. M. Landis, “An Algo- en Informatique et en Informatique, February
rithm for the Organization of Information,” Soviet 1981.
Mathematics, Vol. 3, 1962: 1259–1263. Fowler, M., and K. Scott, UML Distilled , Second Edition,
Albir, S. S., UML in a Nutshell , O’Reilly & Associates, Addison-Wesley, Reading, MA, 2000.
Inc., Sebastopol, CA, 1998. Gamma, E., R. Helm, R. Johnson, and J. Vlissides, Design
Patterns: Elements of Reusable Object-Oriented Soft-
Andersson, A., T. Hagerup, S. Nilsson and R. Raman,
ware, Addison-Wesley Publishing Company, Read-
“Sorting in Linear Time?”, Proceedings of the 27th
ing, MA, 1995.
Annual ACM Symposium on the Theory of Comput-
Goodrich, M., and R. Tamassia, Data Structures and Algo-
ing, 1995.
rithms in Java, Second Edition, John Wiley & Sons,
Arnold, K., and J. Gosling, The Java Programming Lan- Inc., New York, 2001.
guage, Addison-Wesley Publishing Company, Read- Gries, D., Science of Programming, Springer-Verlag, New
ing, MA, 1996. York, 1981.
Bailey, D. A., Data Structures in Java for the Princi- Guibas, L., and R. Sedgewick, “A Diochromatic Frame-
pled Programmer, Second Edition, The McGraw- work for Balanced Trees”, Proceedings of the 19th
Hill Companies, Inc., Burr Ridge, IL, 2003. Annual IEEE Symposium on Foundations of Com-
Bayer, R. “Symmetric Binary B-trees: Data Structure and puter Science, 1978: 8–21.
Maintenance Algorithms”, Acta Informatica, 1(4), Habibi, M., Java Regular Expressions: Taming the
1972: 290–306. java.util.regex Engine, Apress, Berkeley, CA, 2004.
Bentley, J. L. and M. D. McIlroy, “Engineering a Heileman, G. L., Data Structures, Algorithms and Object-
Sort Function,” Software— Practice and Experience , Oriented Programming, The McGraw-Hill Compa-
23(11), November 1993: 1249–1265. nies, Inc., New York, 1996.
Bloch, J., Effective Java Programming Language Guide, Hoare, C. A. R., “Quicksort,” Computer Journal , 5(4),
Addison-Wesley, Boston, 2001. April 1962: 10–15.
Collins, W. J., Data Structures and the Standard Template Huffman, D. A., “A Model for the Construction of Min-
Library, McGraw-Hill, New York, NY, 2003. imum Redundancy Codes,” Proceedings of the IRE ,
Cormen, T., C. Leierson and R. Rivest, Introduction 40, 1952: 1098–1101.
to Algorithms, Second Edition, McGraw-Hill, New Knuth, D. E., The Art of Computer Programming, Vol-
York, NY, 2002. ume 1: “Fundamental Algorithms,” Second Edi-
Dale, N. “If You Were Lost on a Desert Island, What tion, Addison-Wesley Publishing Company, Read-
One ADT Would You Like to Have with You?”, ing, MA, 1973.
Proceedings of the Twenty-First SIGCSE Technical Knuth, D. E., The Art of Computer Programming, Vol-
Symposium, 22(1), March 1991: 139–142. ume 2: “Seminumerical Algorithms,” Second Edi-
Dijkstra, E. W., “A Note on Two Problems in Connex- tion, Addison-Wesley Publishing Company, Read-
ion with Graphs”, Numerische Mathematik 1, 1959: ing, MA, 1973.
269–271. Knuth, D. E., The Art of Computer Programming, Vol-
Dijkstra, E. W., A Discipline of Programming, Prentice- ume 3: “Sorting and Searching,” Addison-Wesley
Hall, Inc., Englewood Cliffs, New Jersey, 1976. Publishing Company, Reading, MA, 1973.
725
726 REFERENCES
Kruse, R. L., Data Structures and Program Design, Riel, A. J., Object-Oriented Design Heuristics, Addison-
Prentice-Hall, Inc., Englewood Cliffs, New Jersey, Wesley Publishing Company, Reading, MA, 1996.
1987. Roberts, S., Thinking Recursively, John Wiley & Sons,
Lewis, J., and W. Loftus, Java Software Solutions: Foun- Inc., New York, 1986.
dations of Program Design, Second Edition, Addison Sahni, S., Data Structures, Algorithms, and Applications
Wesley Longman, Inc., Reading, MA, 2000. in Java, The McGraw-Hill Companies, Inc., Burr
McIlroy, M., “A Killer Adversary for Quicksort,” Ridge, IL, 2000.
Software— Practice and Experience 29(0), 1999: Schaffer, R., and R. Sedgewick, “The Analysis of Heap-
1–4. sort,” Journal of Algorithms 14, 1993: 76–100.
Meyer, B., Object-oriented Software Construction, Shaffer, C., A Practical Introduction to Data Structures
Prentice-Hall International, London, 1988. and Algorithm Analysis, Prentice-Hall, Inc., Upper
Newhall, T., and L. Meeden, “A Comprehensive Project Saddle River, New Jersey, 1998.
for CS2: Combining Key Data Structures and Algo- Simmons, G. J. (Editor), Contemporary Cryptology: The
rithms into an Integrated Web Browser and Search Science of Information Integrity, IEEE Press, New
Engine,” Proceedings of the 33rd SIGCSE Technical York, NY, 1992.
Symposium on Computer Science Education, March Wallace, S. P., Programming Web Graphics, O’Reilly &
2002: 386–290. Associates, Sebastopol, CA, 1999.
Noonan, R. E., “An Object-Oriented View of Back- Weiss, M. A., Data Structures and Problem Solving Using
tracking,” Proceedings of the 31st SIGCSE Technical Java, Second Edition, Addison Wesley Longman,
Symposium on Computer Science Education, March Inc., Reading, MA, 2002.
2000: 362–366. Whitehead, A. N., and B. Russell, Principia Mathematica,
Pfleeger S. L., Software Engineering: Theory and Prac- Cambridge University Press, Cambridge, England,
tice, Prentice-Hall, Inc., Upper Saddle River, New 1910 (Volume 1), 1912 (Volume 2), 1913 (Volume
Jersey, 1998. 3).
Pohl, I., and C. McDowell, Java by Dissection, Addison Williams, J. W., “Algorithm 232: Heapsort”, Communi-
Wesley Longman, Inc., Reading, MA, 2000. cations of the ACM 7(6), 1964: 347–348.
Prim, R. C., “Shortest Connection Networks and Some Wirth, N., Algorithms + Data Structures = Programs,
Generalizations”, Bell System Technical Journal 36, Prentice-Hall, Inc., Englewood Cliffs, New Jersey,
1957: 1389–1401. 1976.
Rawlins, G. J., Compared to What? An Introduction to Zwillinger, Daniel, CRC Standard Mathematical Tables
the Analysis of Algorithms, Computer Science Press, and Formulae, Thirty-First Edition, Chemical Rub-
New York, NY, 1992. ber Company, Cleveland, OH, 2002.
INDEX
727
728 INDEX