- •Contents
- •Preface
- •Introduction to Computers, the Internet and the Web
- •1.3 Computer Organization
- •Languages
- •1.9 Java Class Libraries
- •1.12 The Internet and the World Wide Web
- •1.14 General Notes about Java and This Book
- •Sections
- •Introduction to Java Applications
- •2.4 Displaying Text in a Dialog Box
- •2.5 Another Java Application: Adding Integers
- •2.8 Decision Making: Equality and Relational Operators
- •Introduction to Java Applets
- •3.2 Sample Applets from the Java 2 Software Development Kit
- •3.3 A Simple Java Applet: Drawing a String
- •3.4 Two More Simple Applets: Drawing Strings and Lines
- •3.6 Viewing Applets in a Web Browser
- •3.7 Java Applet Internet and World Wide Web Resources
- •Repetition)
- •Class Attributes
- •5.8 Labeled break and continue Statements
- •5.9 Logical Operators
- •Methods
- •6.2 Program Modules in Java
- •6.7 Java API Packages
- •6.13 Example Using Recursion: The Fibonacci Series
- •6.16 Methods of Class JApplet
- •Class Operations
- •Arrays
- •7.6 Passing Arrays to Methods
- •7.8 Searching Arrays: Linear Search and Binary Search
- •Collaboration Among Objects
- •8.2 Implementing a Time Abstract Data Type with a Class
- •8.3 Class Scope
- •8.4 Controlling Access to Members
- •8.5 Creating Packages
- •8.7 Using Overloaded Constructors
- •8.9 Software Reusability
- •8.10 Final Instance Variables
- •Classes
- •8.16 Data Abstraction and Encapsulation
- •9.2 Superclasses and Subclasses
- •9.5 Constructors and Finalizers in Subclasses
- •Conversion
- •9.11 Type Fields and switch Statements
- •9.14 Abstract Superclasses and Concrete Classes
- •9.17 New Classes and Dynamic Binding
- •9.18 Case Study: Inheriting Interface and Implementation
- •9.19 Case Study: Creating and Using Interfaces
- •9.21 Notes on Inner Class Definitions
- •Strings and Characters
- •10.2 Fundamentals of Characters and Strings
- •10.21 Card Shuffling and Dealing Simulation
- •Handling
- •Graphics and Java2D
- •11.2 Graphics Contexts and Graphics Objects
- •11.5 Drawing Lines, Rectangles and Ovals
- •11.9 Java2D Shapes
- •12.12 Adapter Classes
- •Cases
- •13.3 Creating a Customized Subclass of JPanel
- •Applications
- •Controller
- •Exception Handling
- •14.6 Throwing an Exception
- •14.7 Catching an Exception
- •Multithreading
- •15.3 Thread States: Life Cycle of a Thread
- •15.4 Thread Priorities and Thread Scheduling
- •15.5 Thread Synchronization
- •15.9 Daemon Threads
- •Multithreading
- •Design Patterns
- •Files and Streams
- •16.2 Data Hierarchy
- •16.3 Files and Streams
- •Networking
- •17.2 Manipulating URIs
- •17.3 Reading a File on a Web Server
- •17.4 Establishing a Simple Server Using Stream Sockets
- •17.5 Establishing a Simple Client Using Stream Sockets
- •17.9 Security and the Network
- •18.2 Loading, Displaying and Scaling Images
- •18.3 Animating a Series of Images
- •18.5 Image Maps
- •18.6 Loading and Playing Audio Clips
- •18.7 Internet and World Wide Web Resources
- •Data Structures
- •19.4 Linked Lists
- •20.8 Bit Manipulation and the Bitwise Operators
- •Collections
- •21.8 Maps
- •21.9 Synchronization Wrappers
- •21.10 Unmodifiable Wrappers
- •22.2 Playing Media
- •22.3 Formatting and Saving Captured Media
- •22.5 Java Sound
- •22.8 Internet and World Wide Web Resources
- •Hexadecimal Numbers
19
Data Structures
Objectives
•To be able to form linked data structures using references, self-referential classes and recursion.
•To be able to create and manipulate dynamic data structures, such as linked lists, queues, stacks and binary trees.
•To understand various important applications of linked data structures.
•To understand how to create reusable data structures with classes, inheritance and composition.
Much that I bound, I could not free;
Much that I freed returned to me.
Lee Wilson Dodd
‘Will you walk a little faster?’ said a whiting to a snail, ‘There’s a porpoise close behind us, and he’s treading on my tail.’
Lewis Carroll
There is always room at the top.
Daniel Webster
Push on—keep moving.
Thomas Morton
I think that I shall never see
A poem lovely as a tree.
Joyce Kilmer
Chapter 19 |
Data Structures |
1095 |
Outline
19.1Introduction
19.2Self-Referential Classes
19.3Dynamic Memory Allocation
19.4Linked Lists
19.5Stacks
19.6Queues
19.7Trees
Summary • Terminology • Self-Review Exercises • Answers to Self-Review Exercises • Exercises • Special Section: Building Your Own Compiler
19.1 Introduction
We have studied such fixed-size data structures as singleand double-subscripted arrays. This chapter introduces dynamic data structures that grow and shrink at execution time. Linked lists are collections of data items “lined up in a row”—insertions and deletions can be made anywhere in a linked list. Stacks are important in compilers and operating systems; insertions and deletions are made only at one end of a stack—its top. Queues represent waiting lines; insertions are made at the back (also referred to as the tail) of a queue and deletions are made from the front (also referred to as the head) of a queue. Binary trees facilitate high-speed searching and sorting of data, eliminating of duplicate data items efficiently, representing file system directories, compiling expressions into machine language and many other interesting applications.
We will discuss each of the major types of data structures and implement programs that create and manipulate them. We use classes, inheritance and composition to create and package these data structures for reusability and maintainability. In Chapter 20, “Java Utilities Package and Bit Manipulation,” and Chapter 21, “Collections,” we discuss Java’s predefined classes that implement the data structures discussed in this chapter.
The chapter examples are practical programs that can be used in more advanced courses and in industrial applications. The exercises include a rich collection of useful applications.
We encourage you to attempt the major project described in the special section entitled Building Your Own Compiler. You have been using a Java compiler to translate your Java programs to bytecodes so that you could execute these programs on your computer. In this project, you will actually build your own compiler. It will read a file of statements written in a simple, yet powerful high-level language similar to early versions of the popular language Basic. Your compiler will translate these statements into a file of Simpletron Machine Language (SML) instructions—SML is the language you learned in the Chapter 7 special section, Building Your Own Computer. Your Simpletron Simulator program will then execute the SML program produced by your compiler! Implementing this project by using an object-oriented approach will give you a wonderful opportunity to exercise most of what you have learned in this book. The special section carefully walks you through the specifications of the high-level language and describes the algorithms you will need to con-
1096 |
Data Structures |
Chapter 19 |
vert each type of high-level language statement into machine language instructions. If you enjoy being challenged, you might attempt the many enhancements to both the compiler and the Simpletron Simulator suggested in the exercises.
19.2 Self-Referential Classes
A self-referential class contains an instance variable that refers to another object of the same class type. For example, the definition
class Node { private int data;
private Node nextNode;
public Node( int data ) |
{ |
/* constructor body */ } |
||
public void setData( int data ) { |
/* method body */ } |
|||
public int getData() |
{ |
/* method body */ |
} |
|
public |
void setNext( Node next ) { /* method body */ } |
|||
public |
Node getNext() |
{ |
/* method body */ |
} |
}
defines class Node. This type has two private instance variables—integer data and Node reference nextNode. Member nextNode references an object of type Node, an object of the same type as the one being declared here—hence, the term “self-referential class.” Member nextNode is a link—nextNode “links” an object of type Node to another object of the same type. Type Node also has five methods: a constructor that receives an integer to initialize data, a setData method to set the value data, a getData method to return the value of data, a setNext method to set the value of nextNode and a getNext method to return the value of member nextNode.
Programs can link self-referential objects together to form such useful data structures as lists, queues, stacks and trees. Figure 19.1 illustrates two self-referential objects linked together to form a list. A backslash—representing a null reference—is placed in the link member of the second self-referential object to indicate that the link does not refer to another object. The backslash is for illustration purposes; it does not correspond to the backslash character in Java. Normally, a null reference indicates the end of a data structure.
Common Programming Error 19.1
Not setting the link in the last node of a list to null is a logic error.
19.3 Dynamic Memory Allocation
Creating and maintaining dynamic data structures requires dynamic memory allocation— the ability for a program to obtain more memory space at execution time to hold new nodes and to release space no longer needed. As we have already learned, Java programs do not explicitly release dynamically allocated memory. Rather, Java performs automatic garbage collection on objects that are no longer referenced in a program.
The limit for dynamic memory allocation can be as large as the amount of available physical memory in the computer or the amount of available disk space in a virtual-memory system. Often, the limits are much smaller, because the computer’s available memory must be shared among many applications.
Chapter 19 |
Data Structures |
1097 |
15 10
Fig. 19.1 Two self-referential class objects linked together.
Operator new is essential to dynamic memory allocation. Operator new takes as an operand the type of the object being dynamically allocated and returns a reference to a newly created object of that type. For example, the statement
Node nodeToAdd = new Node( 10 );
allocates the appropriate amount of memory to store a Node object and places a reference to this object in nodeToAdd. If no memory is available, new throws an OutOfMemoryError. The 10 is the Node object’s data.
The following sections discuss lists, stacks, queues and trees that use dynamic memory allocation and self-referential classes to create dynamic data structures.
19.4 Linked Lists
A linked list is a linear collection (i.e., a sequence) of self-referential class objects, called nodes, connected by reference links—hence, the term “linked” list. A program accesses a linked list via a reference to the first node of the list. The program accesses each subsequent node via the link reference stored in the previous node. By convention, the link reference in the last node of a list is set to null to mark the end of the list. Data are stored in a linked list dynamically—the list creates each node as necessary. A node can contain data of any type, including objects of other classes. Stacks and queues are also linear data structures and, as we will see, are constrained versions of linked lists. Trees are nonlinear data structures.
Lists of data can be stored in arrays, but linked lists provide several advantages. A linked list is appropriate when the number of data elements to be represented in the data structure is unpredictable. Linked lists are dynamic, so the length of a list can increase or decrease as necessary. The size of a “conventional” Java array, however, cannot be altered, because the array size is fixed at the time the program creates the array. “Conventional” arrays can become full. Linked lists become full only when the system has insufficient memory to satisfy dynamic storage allocation requests. Package java.util contains class LinkedList for implementing and manipulating linked lists that grow and shrink during program execution. We discuss class LinkedList in Chapter 21, “Collections.”
Performance Tip 19.1
An array can be declared to contain more elements than the number of items expected, but this can waste memory. Linked lists can provide better memory utilization in these situations. Linked lists allow the program to adapt at runtime.
Performance Tip 19.2
Insertion into a linked list is fast—only two references have to be modified (after you have located the place to do the insertion). All existing nodes remain at their current locations in memory.
1098 |
Data Structures |
Chapter 19 |
Linked lists can be maintained in sorted order simply by inserting each new element at the proper point in the list (it does, of course, take time to locate the proper insertion point). Existing list elements do not need to be moved.
Linked list nodes normally are not stored contiguously in memory. Rather, they are logically contiguous. Figure 19.2 illustrates a linked list with several nodes. This diagram presents a singly-linked list—each node contains one reference to the next node in the list. Often, linked lists are implemented as doubly-linked lists—each node contains a reference to the next node in the list and a reference to the previous node in the list. Java’s LinkedList class (see Chapter 21) is a doubly-linked list implementation.
Performance Tip 19.3
Insertion and deletion in a sorted array can be time consuming—all the elements following the inserted or deleted element must be shifted appropriately.
Performance Tip 19.4
The elements of an array are stored contiguously in memory. This allows immediate access to any array element, because the address of any element can be calculated directly as its offset from the beginning of the array. Linked lists do not afford such immediate access to their elements—an element can be accessed only by traversing the list from the front.
Performance Tip 19.5
Using dynamic memory allocation (instead of arrays) for data structures that grow and shrink at execution time can save memory. Keep in mind, however, that references occupy space, and that dynamic memory allocation incurs the overhead of method calls.
The program of Fig. 19.3–Fig. 19.5 uses a List class to manipulate a list of miscellaneous object types. The main method of class ListTest (Fig. 19.5) creates a list of objects, inserts objects at the beginning of the list using method insertAtFront, inserts objects at the end of the list using method insertAtBack, deletes objects from the front of the list using method removeFromFront and deletes objects from the end of the list using method removeFromBack. After each insert and remove operation, ListTest calls List method print to display the current list contents. A detailed discussion of the program follows. If an attempt is made to remove an item from an empty list, an EmptyListException (Fig. 19.4) occurs.
The program of Fig. 19.3–Fig. 19.5 consists of four classes—ListNode (Fig. 19.3), List (Fig. 19.3), EmptyListException (Fig. 19.4) and ListTest (Fig. 19.5). The
List and ListNode classes are placed in package com.deitel.jhtp4.ch19, so they can be reused throughout this chapter. Encapsulated in each List object is a linked list of ListNode objects. The ListNode class (lines 6–38 of Fig. 19.3) consists of package-access members data and nextNode. ListNode member data can refer to any Object. ListNode member nextNode stores a reference to the next ListNode object in the linked list.
[Note: Many of the classes in this chapter are defined in the package com.deitel.jhtp4.ch19. Each such class should be compiled with the -d com- mand-line option to javac. When compiling the classes that are not in this package and when running the programs, be sure to use the -classpath option to javac and java, respectively.]
Chapter 19 |
Data Structures |
1099 |
firstNode |
lastNode |
||||||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
H D ... Q
Fig. 19.2 A graphical representation of a linked list.
1// Fig. 19.3: List.java
2 // Class ListNode and class List definitions
3 package com.deitel.jhtp4.ch19;
4
5 // class to represent one node in a list 6 class ListNode {
7
8 // package access members; List can access these directly
9Object data;
10 ListNode nextNode;
11
12// constructor to create a ListNode that refers to object
13ListNode( Object object )
14{
15this( object, null );
16}
17
18// constructor to create ListNode that refers to Object
19// and to next ListNode in List
20ListNode( Object object, ListNode node )
21{
22data = object;
23nextNode = node;
24}
25
26// return Object in this node
27Object getObject()
28{
29return data;
30}
31
32// get next node
33ListNode getNext()
34{
35return nextNode;
36}
37
38 } // end class ListNode
39
Fig. 19.3 Definitions of class ListNode and class List (part 1 of 4).
1100 |
Data Structures |
Chapter 19 |
40// class List definition
41public class List {
42private ListNode firstNode;
43private ListNode lastNode;
44private String name; // String like "list" used in printing
46// construct an empty List with a name
47public List( String string )
48{
49name = string;
50firstNode = lastNode = null;
51}
52
53// construct empty List with "list" as the name
54public List()
55{
56this( "list" );
57}
58
59// Insert Object at front of List. If List is empty,
60// firstNode and lastNode will refer to same object.
61// Otherwise, firstNode refers to new node.
62public synchronized void insertAtFront( Object insertItem )
63{
64if ( isEmpty() )
65 |
firstNode = lastNode = new ListNode( insertItem ); |
66 |
|
67 |
else |
68firstNode = new ListNode( insertItem, firstNode );
69}
70
71// Insert Object at end of List. If List is empty,
72// firstNode and lastNode will refer to same Object.
73// Otherwise, lastNode's nextNode refers to new node.
74public synchronized void insertAtBack( Object insertItem )
75{
76if ( isEmpty() )
77 |
firstNode = lastNode = new ListNode( insertItem ); |
78 |
|
79 |
else |
80 |
lastNode = lastNode.nextNode = |
81 |
new ListNode( insertItem ); |
82 |
} |
83 |
|
84// remove first node from List
85public synchronized Object removeFromFront()
86throws EmptyListException
87{
88Object removeItem = null;
89
90// throw exception if List is empty
91if ( isEmpty() )
92 throw new EmptyListException( name );
Fig. 19.3 Definitions of class ListNode and class List (part 2 of 4).
Chapter 19 |
Data Structures |
1101 |
93
94// retrieve data being removed
95removeItem = firstNode.data;
97// reset the firstNode and lastNode references
98if ( firstNode == lastNode )
99 |
firstNode = lastNode = null; |
100 |
|
101 |
else |
102 |
firstNode = firstNode.nextNode; |
103 |
|
104// return removed node data
105return removeItem;
106}
107
108// Remove last node from List
109public synchronized Object removeFromBack()
110throws EmptyListException
111{
112Object removeItem = null;
113
114// throw exception if List is empty
115if ( isEmpty() )
116 throw new EmptyListException( name );
117
118// retrieve data being removed
119removeItem = lastNode.data;
121// reset firstNode and lastNode references
122if ( firstNode == lastNode )
123 |
firstNode = lastNode = null; |
124 |
|
125 |
else { |
126 |
|
127 |
// locate new last node |
128 |
ListNode current = firstNode; |
129 |
|
130 |
// loop while current node does not refer to lastNode |
131 |
while ( current.nextNode != lastNode ) |
132 |
current = current.nextNode; |
133 |
|
134 |
// current is new lastNode |
135 |
lastNode = current; |
136current.nextNode = null;
137}
138
139// return removed node data
140return removeItem;
141}
142
143// return true if List is empty
144public synchronized boolean isEmpty()
145{
Fig. 19.3 Definitions of class ListNode and class List (part 3 of 4).
1102 |
Data Structures |
Chapter 19 |
146return firstNode == null;
147}
148
149// output List contents
150public synchronized void print()
151{
152if ( isEmpty() ) {
153 System.out.println( "Empty " + name );
154return;
155}
156
157 System.out.print( "The " + name + " is: " );
158
159 ListNode current = firstNode;
160
161// while not at end of list, output current node's data
162while ( current != null ) {
163 System.out.print( current.data.toString() + " " );
164current = current.nextNode;
165}
166
167System.out.println( "\n" );
168}
169
170 } // end class List
Fig. 19.3 Definitions of class ListNode and class List (part 4 of 4).
Class List contains private members firstNode (a reference to the first
ListNode in a List) and lastNode (a reference to the last ListNode in a List). The constructors (lines 47–51 and 54–57) initialize both references to null. The most important methods of class List are the synchronized methods insertAtFront (lines 62–69), insertAtBack (lines 74–82), removeFromFront (lines 85–106) and removeFromBack (lines 109–141). These methods are declared synchronized so List objects can be multithread safe when used in a multithreaded program. If one thread is modifying the contents of a List, no other thread can modify the same List object at the same time. Method isEmpty (lines 144–147) is a predicate method that determines whether the list is empty (i.e., the reference to the first node of the list is null). Predicate methods typically test a condition and do not modify the object on which they are called. If the list is empty, method isEmpty returns true; otherwise, it returns false. Method print (lines 150–168) displays the list’s contents. Both isEmpty and print are also synchronized methods.
1 // Fig. 19.4: EmptyListException.java
2 // Class EmptyListException definition
3 package com.deitel.jhtp4.ch19;
4
5 public class EmptyListException extends RuntimeException {
6
Fig. 19.4 Definition of class EmptyListException (part 1 of 2).
Chapter 19 |
Data Structures |
1103 |
7// initialize an EmptyListException
8public EmptyListException( String name )
9{
10super( "The " + name + " is empty" );
11}
12
13 } // end class EmptyListException
Fig. 19.4 Definition of class EmptyListException (part 2 of 2).
1 // Fig. 19.5: ListTest.java
2 // Class ListTest
3
4// Deitel packages
5import com.deitel.jhtp4.ch19.List;
6 import com.deitel.jhtp4.ch19.EmptyListException;
7
8 public class ListTest {
9
10// test class List
11public static void main( String args[] )
12{
13List list = new List(); // create the List container
15// create objects to store in List
16Boolean bool = Boolean.TRUE;
17Character character = new Character( '$' );
18Integer integer = new Integer( 34567 );
19String string = "hello";
20
21// use List insert methods
22list.insertAtFront( bool );
23list.print();
24list.insertAtFront( character );
25list.print();
26list.insertAtBack( integer );
27list.print();
28list.insertAtBack( string );
29list.print();
30
31// use List remove methods
32Object removedObject;
33
34// remove objects from list; print after each removal
35try {
36 |
removedObject = list.removeFromFront(); |
37 |
System.out.println( |
38 |
removedObject.toString() + " removed" ); |
39 |
list.print(); |
40 |
|
Fig. 19.5 Manipulating a linked list.
1104 |
Data Structures |
Chapter 19 |
|
|
|
41 |
removedObject = list.removeFromFront(); |
|
42 |
System.out.println( |
|
43 |
removedObject.toString() + " removed" ); |
|
44 |
list.print(); |
|
45 |
|
|
46 |
removedObject = list.removeFromBack(); |
|
47 |
System.out.println( |
|
48 |
removedObject.toString() + " removed" ); |
|
49 |
list.print(); |
|
50 |
|
|
51 |
removedObject = list.removeFromBack(); |
|
52 |
System.out.println( |
|
53 |
removedObject.toString() + " removed" ); |
|
54list.print();
55}
56
57// process exception if List is empty when attempt is
58// made to remove an item
59catch ( EmptyListException emptyListException ) {
60emptyListException.printStackTrace();
61}
62
63 } // end method main
64
65 } // end class ListTest
The list is: true
The list is: $ true
The list is: $ true 34567
The list is: $ true 34567 hello
$ removed
The list is: true 34567 hello
true removed
The list is: 34567 hello
hello removed
The list is: 34567
34567 removed Empty list
Fig. 19.5 Manipulating a linked list.
Over the next several pages, we discuss each of the methods of class List (Fig. 19.3) in detail. Method insertAtFront (lines 62–69 of Fig. 19.3) places a new node at the front of the list. The method consists of several steps (Fig. 19.6 illustrates the operation):
1. Call isEmpty to determine whether the list is empty (line 64).
Chapter 19 |
Data Structures |
1105 |
(a)firstNode
7 11
new ListNode
12
(b)firstNode
7 11
new ListNode 12
Fig. 19.6 The insertAtFront operation.
2.If the list is empty, both firstNode and lastNode are set to the ListNode allocated with new and initialized with insertItem (line 65). The ListNode constructor at lines 13–16 calls the ListNode constructor at lines 20–24 to set instance variable data to refer to the insertItem passed as an argument and to set reference nextNode to null.
3.If the list is not empty, the new node is “threaded” (not to be confused with multithreading) or “linked” into the list by setting firstNode to a new ListNode object and initializing that object with insertItem and firstNode (line 68). When the ListNode constructor (lines 20–24) executes, it sets instance variable data to refer to the insertItem passed as an argument and performs the insertion by setting the nextNode reference to the ListNode passed as an argument.
In Fig. 19.6, part (a) shows the list and the new node during the insertAtFront operation and before the program threads the new node into the list. The dotted arrows in part (b) illustrate step 3 of the insertAtFront operation that enables the node containing 12 to become the new list front.
Method insertAtBack (lines 74–82 of Fig. 19.3) places a new node at the back of the list. The method consists of several steps (Fig. 19.7 illustrates the operation):
1.Call isEmpty to determine whether the list is empty (line 76).
2.If the list is empty, both firstNode and lastNode are set to the ListNode allocated with new and initialized with insertItem (line 77). The ListNode constructor at lines 13–16 calls the ListNode constructor at lines 20–24 to set instance variable data to refer to the insertItem passed as an argument and to set reference nextNode to null.
3.If the list is not empty, the new node is threaded into the list by setting LastNode and lastNode.nextNode to the ListNode that was allocated with new and initialized with insertItem (lines 80–81). When the ListNode constructor (lines 13–16) executes, it sets instance variable data to refer to the insertItem passed as an argument and sets reference nextNode to null.
1106 |
Data Structures |
Chapter 19 |
(a) firstNode |
lastNode new ListNode |
|||||||||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
12 |
|
|
|
|
7 |
|
|
|
|
11 |
|
|
|
5 |
|
|
|||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|||||||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
||||
(b) firstNode |
|
|
|
lastNode |
new ListNode |
||||||||||||||||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
12 7 11 5
Fig. 19.7 A graphical representation of the insertAtBack operation.
In Fig. 19.7, part (a) shows the list and the new node during the insertAtBack operation and before the program threads the new node into the list. The dotted arrows in part (b) illustrate the steps of method insertAtBack that enable a new node to be added to the end of a list that is not empty.
Method removeFromFront (lines 85–106 of Fig. 19.3) removes the front node of the list and returns a reference to the removed data. The method throws an EmptyListException (lines 91–92) if the list is empty when the program calls this method. Otherwise, the method returns a reference to the removed data. The method consists of several steps (illustrated in Fig. 19.8):
1.Assign firstNode.data (the data being removed from the list) to reference removeItem (line 95).
2.If the objects to which firstNode and lastNode refer are equal (line 98), the list has only one element prior to the removal attempt. In this case, the method sets firstNode and lastNode to null (line 99) to “dethread” (remove) the node from the list (leaving the list empty).
3.If the list has more than one node prior to removal, then the method leaves reference lastNode as is and simply assigns firstNode.nextNode to reference firstNode (line 102). Thus, firstNode references the node that was the second node prior to the removeFromFront call.
4.Return the removeItem reference (line 105).
In Fig. 19.8, part (a) illustrates the list before the removal operation. Part (b) shows actual reference manipulations.
Chapter 19 |
Data Structures |
1107 |
(a) firstNode |
lastNode |
||||||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
12 |
|
|
|
|
7 |
|
|
|
|
11 |
|
|
|
|
5 |
|
|||
|
|
|
|
|
|
|
|
|
||||||||||||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|||
(b) firstNode |
|
|
|
|
|
|
|
|
|
lastNode |
||||||||||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
12 7 11 5
removeItem
Fig. 19.8 A graphical representation of the removeFromFront operation.
Method removeFromBack (lines 109–141 of Fig. 19.3) removes the last node of a list and returns a reference to the removed data. The method throws an EmptyListException (lines 94 and 95) if the list is empty when the program calls this method. The method consists of several steps (Fig. 19.9 illustrates the operation):
1.Assign lastNode.data (the data being removed from the list) to reference removeItem (line 119).
2.If the objects to which firstNode and lastNode refer are equal (line 122), the list has only one element prior to the removal attempt. In this case, the method sets firstNode and lastNode to null (line 123) to dethread (remove) that node from the list (leaving the list empty).
3.If the list has more than one node prior to removal, then create the ListNode reference current and assign it firstNode.
4.Now “walk the list” with current until it references the node before the last node. The while loop (lines 131–132) assigns current.nextNode to current as long as current.nextNode is not lastNode.
5.After locating the second-to-last node, assign current to lastNode (line 135) to dethread the last node from the list.
6.Set the current.nextNode to null (line 136) to terminate the list at the current node.
7.Return the removeItem reference (liner 140).
In Fig. 19.9, part (a) illustrates the list before the removal operation. Part (b) shows the actual reference manipulations.
1108 |
Data Structures |
Chapter 19 |
(a) firstNode |
lastNode |
||||||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
12 7 11 5
(b) firstNode |
current |
lastNode |
|||||||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
12 |
|
|
|
|
7 |
|
|
|
|
11 |
|
|
|
5 |
|
|
|
|
|
|
|
|
|
|
|
||||||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
removeItem
Fig. 19.9 A graphical representation of the removeFromBack operation.
Method print (lines 150–168) first determines whether the list is empty (lines 152– 155). If so, print displays a message indicating that the list is empty and returns control to the calling method. Otherwise, print outputs the data in the list. Line 159 creates ListNode reference current and initializes it with firstNode. While current is not null, there are more items in the list. therefore, line 163 outputs a String representation of current.data. Line 164 moves to the next node in the list by assigning the value of current.nextNode to current. This printing algorithm is identical for linked lists, stacks and queues.
19.5 Stacks
A stack is a constrained version of a linked list—new nodes can be added to a stack and removed from a stack only at the top. For this reason, a stack is referred to as a last-in, firstout (LIFO) data structure. The link member in the bottom (i.e., last) node of the stack is set to null to indicate the bottom of the stack.
Common Programming Error 19.2
Not setting the link in the bottom node of a stack to null is a common logic error.
The primary methods used to manipulate a stack are push and pop. Method push adds a new node to the top of the stack. Method pop removes a node from the top of the stack and returns the data from the popped node.
Stacks have many interesting applications. For example, when a program calls a method, the called method must know how to return to its caller, so the return address of the calling method is pushed onto the program execution stack. If a series of method calls
Chapter 19 |
Data Structures |
1109 |
occurs, the successive return addresses are pushed onto the stack in last-in, first-out order so that each method can return to its caller. Stacks support recursive method calls in the same manner as they do conventional nonrecursive method calls.
The program execution stack contains the memory for local variables on each invocation of a method during a program’s execution. When the method returns to its caller, the memory for that method’s local variables is popped off the stack and those variables are no longer known to the program.
Compilers use stacks to evaluate arithmetic expressions and generate machine language code to process the expressions. The exercises in this chapter explore several applications of stacks, including using them to develop a complete working compiler. The java.util package of the Java API contains class Stack for implementing and manipulating stacks that can grow and shrink during program execution. We will discuss class Stack in Chapter 20.
We take advantage of the close relationship between lists and stacks to implement a stack class by reusing a list class. We demonstrate two different forms of reusability. First, we implement the stack class by extending class List of Fig. 19.3. Then we implement an identically performing stack class through composition by including a List object as a private member of a stack class. The list, stack and queue data structures in this chapter are implemented to store Object references to encourage further reusability. Thus, any object type can be stored in a list, stack or queue.
The application of Fig. 19.10 and Fig. 19.11 creates a stack class by extending class List of Fig. 19.3. We want the stack to have methods push, pop, isEmpty and print. Essentially, these are the methods insertAtFront, removeFromFront, isEmpty and print of class List. Of course, class List contains other methods (such as insertAtBack and removeFromBack) that we would rather not make accessible through the public interface to the stack class. It is important to remember that all methods in the public interface of class List class also are public methods of the subclass StackInheritance (Fig. 19.10). When we implement the stack’s methods, we have each StackInheritance method call the appropriate List method—method push calls insertAtFront and method pop calls removeFromFront. Class StackInheritance is defined as part of package com.deitel.jhtp4.ch19 for reuse purposes. Note that StackInheritance does not import List, because both classes are in the same package.
1 // Fig. 19.10: StackInheritance.java
2// Derived from class List
3 package com.deitel.jhtp4.ch19;
4
5 public class StackInheritance extends List {
6
7// construct stack
8public StackInheritance()
9{
10super( "stack" );
11}
12
Fig. 19.10 Class StackInheritance extends class List (part 1 of 2).
1110 |
Data Structures |
Chapter 19 |
13// add object to stack
14public synchronized void push( Object object )
15{
16insertAtFront( object );
17}
18
19// remove object from stack
20public synchronized Object pop() throws EmptyListException
21{
22return removeFromFront();
23}
24
25 } // end class StackInheritance
Fig. 19.10 Class StackInheritance extends class List (part 2 of 2).
Class StackInheritanceTest’s method main (Fig. 19.11) uses class StackInheritance to instantiate a stack of Objects called stack. The program pushes onto the stack (lines 22, 24, 26 and 28) a Boolean object containing true, a Character object containing $, an Integer object containing 34567 and a String object containing hello, then popped off stack. Lines 37–42 pop the objects from the stack in an infinite while loop. When there are no objects left to pop, an method pop throws an EmptyListException, and the program displays the exception’s stack trace, which shows the methods on the program execution stack at the time the exception occurred. Note that the program uses method print (inherited from List) to output the contents of the stack.
1 // Fig. 19.11: StackInheritanceTest.java
2 // Class StackInheritanceTest
3
4// Deitel packages
5import com.deitel.jhtp4.ch19.StackInheritance;
6 import com.deitel.jhtp4.ch19.EmptyListException;
7
8 public class StackInheritanceTest {
9
10// test class StackInheritance
11public static void main( String args[] )
12{
13StackInheritance stack = new StackInheritance();
15// create objects to store in the stack
16Boolean bool = Boolean.TRUE;
17Character character = new Character( '$' );
18Integer integer = new Integer( 34567 );
19String string = "hello";
20
21// use push method
22stack.push( bool );
23stack.print();
24stack.push( character );
Fig. 19.11 A simple stack program (part 1 of 2).
Chapter 19 |
Data Structures |
1111 |
25stack.print();
26stack.push( integer );
27stack.print();
28stack.push( string );
29stack.print();
30
31// remove items from stack
32try {
33 |
|
34 |
// use pop method |
35 |
Object removedObject = null; |
36 |
|
37 |
while ( true ) { |
38 |
removedObject = stack.pop(); |
39 |
System.out.println( removedObject.toString() + |
40 |
" popped" ); |
41 |
stack.print(); |
42}
43}
44
45// catch exception if stack empty when item popped
46catch ( EmptyListException emptyListException ) {
47emptyListException.printStackTrace();
48}
49
50 } // end method main
51
52 } // end class StackInheritanceTest
The stack is: true
The stack is: $ true
The stack is: 34567 $ true
The stack is: hello 34567 $ true
hello popped
The stack is: 34567 $ true
34567 popped
The stack is: $ true
$ popped
The stack is: true
true popped Empty stack
com.deitel.jhtp4.ch19.EmptyListException: The stack is empty
at com.deitel.jhtp4.ch19.List.removeFromFront(List.java:92) at com.deitel.jhtp4.ch19.StackInheritance.pop(
StackInheritance.java:22)
at StackInheritanceTest.main(StackInheritanceTest.java:38)
Fig. 19.11 A simple stack program (part 2 of 2).
1112 |
Data Structures |
Chapter 19 |
Another way to implement a stack class is by reusing a list class through composition. The class in Fig. 19.12 uses a private object of class List (line 6) in the definition of class StackComposition. Composition enables us to hide the methods of class List that should not be in our stack’s public interface by providing public interface methods only to the required List methods. This technique of implementing each stack method as a call to a List method is called delegating—the stack method invoked delegates the call to the appropriate List method. In particular, StackComposition delegates calls to List methods insertAtFront, removeFromFront, isEmpty and print. In this example, we do not show class StackCompositionTest, because the only difference in this example is that we change the type of the stack from StackInheritance to StackComposition. If you execute the application from the code on the CD that accompanies this book, you will see that the output is identical.
1// Fig. 19.12: StackComposition.java
2 // Class StackComposition definition with composed List object
3 package com.deitel.jhtp4.ch19;
4
5 public class StackComposition {
6 private List stackList;
7
8// construct stack
9public StackComposition()
10{
11stackList = new List( "stack" );
12}
13
14// add object to stack
15public synchronized void push( Object object )
16{
17stackList.insertAtFront( object );
18}
19
20// remove object from stack
21public synchronized Object pop() throws EmptyListException
22{
23return stackList.removeFromFront();
24}
25
26// determine if stack is empty
27public synchronized boolean isEmpty()
28{
29return stackList.isEmpty();
30}
31
32// output stack contents
33public synchronized void print()
34{
35stackList.print();
36}
37
38 } // end class StackComposition
Fig. 19.12 A simple stack class using composition.
Chapter 19 |
Data Structures |
1113 |
19.6 Queues
Another common data structure is the queue. A queue is similar to a checkout line in a su- permarket—the cashier services the person at the beginning of the line first. Other customers enter the line only at the end and wait for service. Queue nodes are removed only from the head (or front) of the queue and are inserted only at the tail (or end) of the queue. For this reason, a queue is a first-in, first-out (FIFO) data structure. The insert and remove operations are known as enqueue and dequeue.
Queues have many applications in computer systems. Most computers have only a single processor, so only one application at a time can be serviced. Entries for the other applications are placed in a queue. The entry at the front of the queue is the next to receive service. Each entry gradually advances to the front of the queue as applications receive service.
Queues are also used to support print spooling. A multiuser environment may have only a single printer. Many users may send print jobs to the printer. Even when the printer is busy, other people may still send print jobs. These are “spooled” to disk (much as thread is wound onto a spool) where they wait in a queue until the printer becomes available.
Information packets also wait in queues in computer networks. Each time a packet arrives at a network node, it must be routed to the next node on the network along the path to the packet’s final destination. The routing node routes one packet at a time, so additional packets are enqueued until the router can route them.
A file server in a computer network handles file-access requests from many clients throughout the network. Servers have a limited capacity to service requests from clients. When that capacity is exceeded, client requests wait in queues.
The application of Fig. 19.13 and Fig. 19.14 creates a queue by extending class List (Fig. 19.3). We want class QueueInheritance (Fig. 19.13) to have methods enqueue and dequeue, isEmpty and print. Note that these are essentially the List methods insertAtBack and removeFromFront. Of course, class List contains other methods (i.e., insertAtFront and removeFromBack) that we would rather not make accessible through the public interface of the queue. Remember that all methods in the public interface of the List class are also public methods of the subclass QueueInheritance. In the queue’s implementation, each method of class QueueInheritance calls the appropriate List method—method enqueue calls insertAtBack and method dequeue calls removeFromFront. Class QueueInheritance is defined in package com.deitel.jhtp4.ch19 for reuse purposes.
Common Programming Error 19.3
Not setting the link in the last node of a queue to null is a common logic error.
1// Fig. 19.13: QueueInheritance.java
2 // Class QueueInheritance extends class List
3
4// Deitel packages
5 package com.deitel.jhtp4.ch19;
6
7 public class QueueInheritance extends List {
8
Fig. 19.13 Class QueueInheritance extends class List (part 1 of 2).
1114 |
Data Structures |
Chapter 19 |
9// construct queue
10public QueueInheritance()
11{
12super( "queue" );
13}
14
15// add object to queue
16public synchronized void enqueue( Object object )
17{
18insertAtBack( object );
19}
20
21// remove object from queue
22public synchronized Object dequeue() throws EmptyListException
23{
24return removeFromFront();
25}
26
27 } // end class QueueInheritance
Fig. 19.13 Class QueueInheritance extends class List (part 2 of 2).
Class QueueInheritanceTest method main (Fig. 19.14) uses class QueueInheritance to instantiate a queue of Objects called queue. Lines 22, 24, 26 and 28 enqueue a Boolean object containing true, a Character object containing $, an Integer object containing 34567 and a String object containing hello. Lines 37– 42 use an infinite while loop to dequeue the objects in first-in, first-out order. When there are no objects left to dequeue, method dequeue throws an EmptyListException, and the program displays the exception’s stack trace.
1 // Fig. 19.14: QueueInheritanceTest.java
2 // Class QueueInheritanceTest
3
4// Deitel packages
5import com.deitel.jhtp4.ch19.QueueInheritance;
6 import com.deitel.jhtp4.ch19.EmptyListException;
7
8 public class QueueInheritanceTest {
9
10// test class QueueInheritance
11public static void main( String args[] )
12{
13QueueInheritance queue = new QueueInheritance();
15// create objects to store in queue
16Boolean bool = Boolean.TRUE;
17Character character = new Character( '$' );
18Integer integer = new Integer( 34567 );
19String string = "hello";
Fig. 19.14 Processing a queue (part 1 of 3).
Chapter 19 |
Data Structures |
1115 |
20
21// use enqueue method
22queue.enqueue( bool );
23queue.print();
24queue.enqueue( character );
25queue.print();
26queue.enqueue( integer );
27queue.print();
28queue.enqueue( string );
29queue.print();
30
31// remove objects from queue
32try {
33 |
|
34 |
// use dequeue method |
35 |
Object removedObject = null; |
36 |
|
37 |
while ( true ) { |
38 |
removedObject = queue.dequeue(); |
39 |
System.out.println( removedObject.toString() + |
40 |
" dequeued" ); |
41 |
queue.print(); |
42}
43}
44
45// process exception if queue empty when item removed
46catch ( EmptyListException emptyListException ) {
47emptyListException.printStackTrace();
48}
49
50 } // end method main
51
52 } // end class QueueInheritanceTest
The queue is: true
The queue is: true $
The queue is: true $ 34567
The queue is: true $ 34567 hello
true dequeued
The queue is: $ 34567 hello
$ dequeued
The queue is: 34567 hello
34567 dequeued
The queue is: hello
hello dequeued Empty queue
(continued on next page)
Fig. 19.14 Processing a queue (part 2 of 3).
1116 |
Data Structures |
Chapter 19 |
(continued from previous page) com.deitel.jhtp4.ch19.EmptyListException: The queue is empty
at com.deitel.jhtp4.ch19.List.removeFromFront(List.java:92) at com.deitel.jhtp4.ch19.QueueInheritance.dequeue(
QueueInheritance.java:24)
at QueueInheritanceTest.main(QueueInheritanceTest.java:38)
Fig. 19.14 Processing a queue (part 3 of 3).
19.7 Trees
Linked lists, stacks and queues are linear data structures (i.e., sequences). A tree is a nonlinear, two-dimensional data structure with special properties. Tree nodes contain two or more links. This section discusses binary trees (Fig. 19.15)—trees whose nodes all contain two links (none, one or both of which may be null). The root node is the first node in a tree. Each link in the root node refers to a child. The left child is the first node in the left subtree, and the right child is the first node in the right subtree. The children of a specific node are called siblings. A node with no children is called a leaf node. Computer scientists normally draw trees from the root node down—exactly the opposite of the way most trees grow in nature.
Common Programming Error 19.4
Not setting to null the links in leaf nodes of a tree is a common logic error.
In our binary tree example, we create a special binary tree called a binary search tree. A binary search tree (with no duplicate node values) has the characteristic that the values in any left subtree are less than the value in the subtree’s parent node, and the values in any right subtree are greater than the value in the subtree’s parent node. Figure 19.16 illustrates a binary search tree with 12 integer values. Note that the shape of the binary search tree that corresponds to a set of data can vary, depending on the order in which the values are inserted into the tree.
B
|
A |
|
|
|
D |
|
|
|
|
|
|
|
|
C
Fig. 19.15 A graphical representation of a binary tree.
Chapter 19 |
Data Structures |
1117 |
|
|
|
47 |
|
|
|
25 |
|
77 |
|
11 |
43 |
65 |
93 |
7 |
17 |
31 44 |
68 |
|
|
|
|
|
|
|
|
|
|
|
Fig. 19.16 A binary search tree containing 12 values.
The application of Fig. 19.17 and Fig. 19.18 creates a binary search tree of integers and traverses it (i.e., walks through all its nodes) three ways—using recursive inorder, preorder and postorder traversals. The program generates 10 random numbers and inserts each into the tree. Class Tree is defined in package com.deitel.jhtp4.ch19 for reuse purposes.
1// Fig. 19.17: Tree.java
2 // Definition of class TreeNode and class Tree.
3
4// Deitel packages
5 package com.deitel.jhtp4.ch19;
6
7 // class TreeNode definition
8 class TreeNode {
9
10// package access members
11TreeNode leftNode;
12int data;
13TreeNode rightNode;
14
15// initialize data and make this a leaf node
16public TreeNode( int nodeData )
17{
18data = nodeData;
19leftNode = rightNode = null; // node has no children
20}
21
22// insert TreeNode into Tree that contains nodes;
23// ignore duplicate values
24public synchronized void insert( int insertValue )
25{
26// insert in left subtree
27if ( insertValue < data ) {
28 |
|
29 |
// insert new TreeNode |
30 |
if ( leftNode == null ) |
31 |
leftNode = new TreeNode( insertValue ); |
32 |
|
|
|
Fig. 19.17 |
Definitions of TreeNode and Tree for a binary search tree (part 1 of 4). |
1118 |
Data Structures |
Chapter 19 |
|
|
|
33 |
// continue traversing left subtree |
|
34 |
else |
|
35 |
leftNode.insert( insertValue ); |
|
36 |
} |
|
37 |
|
|
38// insert in right subtree
39else if ( insertValue > data ) {
41 |
// insert new TreeNode |
42 |
if ( rightNode == null ) |
43 |
rightNode = new TreeNode( insertValue ); |
44 |
|
45 |
// continue traversing right subtree |
46 |
else |
47 |
rightNode.insert( insertValue ); |
48 |
} |
49 |
|
50 |
} // end method insert |
51 |
|
52 |
} // end class TreeNode |
53 |
|
54// class Tree definition
55public class Tree {
56private TreeNode root;
58// construct an empty Tree of integers
59public Tree()
60{
61root = null;
62}
63
64// Insert a new node in the binary search tree.
65// If the root node is null, create the root node here.
66// Otherwise, call the insert method of class TreeNode.
67public synchronized void insertNode( int insertValue )
68{
69if ( root == null )
70 |
root = new TreeNode( insertValue ); |
71 |
|
72 |
else |
73root.insert( insertValue );
74}
75
76// begin preorder traversal
77public synchronized void preorderTraversal()
78{
79preorderHelper( root );
80}
81
82// recursive method to perform preorder traversal
83private void preorderHelper( TreeNode node )
84{
Fig. 19.17 Definitions of TreeNode and Tree for a binary search tree (part 2 of 4).
Chapter 19 |
Data Structures |
1119 |
85 if ( node == null )
86 return;
87
88// output node data
89System.out.print( node.data + " " );
91// traverse left subtree
92preorderHelper( node.leftNode );
94// traverse right subtree
95preorderHelper( node.rightNode );
96}
97
98// begin inorder traversal
99public synchronized void inorderTraversal()
100{
101inorderHelper( root );
102}
103
104// recursive method to perform inorder traversal
105private void inorderHelper( TreeNode node )
106{
107if ( node == null )
108 |
return; |
109 |
|
110// traverse left subtree
111inorderHelper( node.leftNode );
113// output node data
114System.out.print( node.data + " " );
116// traverse right subtree
117inorderHelper( node.rightNode );
118}
119
120// begin postorder traversal
121public synchronized void postorderTraversal()
122{
123postorderHelper( root );
124}
125
126// recursive method to perform postorder traversal
127private void postorderHelper( TreeNode node )
128{
129if ( node == null )
130 |
return; |
131 |
|
132// traverse left subtree
133postorderHelper( node.leftNode );
135// traverse right subtree
136postorderHelper( node.rightNode );
Fig. 19.17 Definitions of TreeNode and Tree for a binary search tree (part 3 of 4).
1120 |
Data Structures |
Chapter 19 |
138// output node data
139System.out.print( node.data + " " );
140}
141
142 } // end class Tree
Fig. 19.17 Definitions of TreeNode and Tree for a binary search tree (part 4 of 4).
Let us walk through the binary tree program. Method main of class TreeTest (Fig. 19.18) begins by instantiating an empty Tree object and storing it in reference tree (line 11). The program randomly generates 10 integers, each of which is inserted into the binary tree through a call to synchronized method insertNode (line 21). The program then performs preorder, inorder and postorder traversals (these will be explained shortly) of tree.
1// Fig. 19.18: TreeTest.java
2 // This program tests class Tree.
3 import com.deitel.jhtp4.ch19.Tree;
4
5 // Class TreeTest definition
6 public class TreeTest {
7
8// test class Tree
9public static void main( String args[] )
10{
11Tree tree = new Tree();
12int value;
13
14 System.out.println( "Inserting the following values: " );
15
16// insert 10 random integers from 0-99 in tree
17for ( int i = 1; i <= 10; i++ ) {
18 |
value = ( int ) ( |
Math.random() * 100 ); |
19 |
System.out.print( |
value + " " ); |
20 |
|
|
21tree.insertNode( value );
22}
23
24// perform preorder traveral of tree
25System.out.println ( "\n\nPreorder traversal" );
26tree.preorderTraversal();
27
28// perform inorder traveral of tree
29System.out.println ( "\n\nInorder traversal" );
30tree.inorderTraversal();
31
32// perform postorder traveral of tree
33System.out.println ( "\n\nPostorder traversal" );
34tree.postorderTraversal();
35System.out.println();
36}
Fig. 19.18 Creating and traversing a binary tree (part 1 of 2).
Chapter 19 |
Data Structures |
1121 |
37
38 } // end class TreeTest
Inserting the following values: 39 69 94 47 50 72 55 41 97 73
Preorder |
traversal |
|
|
|
39 69 |
47 |
41 50 55 |
94 |
72 73 97 |
Inorder traversal |
|
|
||
39 41 |
47 |
50 55 69 |
72 |
73 94 97 |
Postorder traversal |
|
|||
41 55 |
50 |
47 73 72 |
97 |
94 69 39 |
Fig. 19.18 Creating and traversing a binary tree (part 2 of 2).
Class Tree (lines 55–142 of Fig. 19.17) has as private data root—a reference to the root node of the tree. The class contains public method insertNode (lines 67–74) to insert a new node in the tree and public methods preorderTraversal (lines 77– 80), inorderTraversal (lines 99–102) and postorderTraversal (lines 121– 124) to begin traversals of the tree. Each of these methods calls a separate recursive utility method to perform the traversal operations on the internal representation of the tree. The Tree constructor (lines 59–62) initializes root to null to indicate that the tree is empty.
The Tree class’s synchronized method insertNode (lines 67–74) first determines if the tree is empty. If so, it allocates a new TreeNode, initializes the node with the integer being inserted in the tree and assigns the new node to the root reference (line 70). If the tree is not empty, insertNode calls TreeNode method insert (lines 24–52). This method uses recursion to determine the location for the new node in the tree and inserts the node at that location. A node can be inserted only as a leaf node in a binary search tree.
The TreeNode method insert compares the value to insert with the data value in the root node. If the insert value is less than the root node data, the program determines if the left subtree is empty (line 30). If so, line 31 allocates a new TreeNode, initializes it with the integer being inserted and assigns the new node to reference leftNode. Otherwise, line 35 recursively calls insert for the left subtree to insert the value into the left subtree. If the insert value is greater than the root node data, the program determines if the right subtree is empty (line 42). If so, line 43 allocates a new TreeNode, initializes it with the integer being inserted and assigns the new node to reference rightNode. Otherwise, line 47 recursively calls insert for the right subtree to insert the value in the right subtree.
Methods inorderTraversal, preorderTraversal and postorderTraversal call helper methods inorderHelper (lines 105–118), preorderHelper
(lines 83–96) and postorderHelper (lines 127–140), respectively, to traverse the tree (Fig. 19.19) and print the node values. The purpose of the helper methods in class Tree is to allow the programmer to start a traversal without the need to obtain a reference to the root node first, then call the recursive method with that reference. Methods inorderTraversal, preorderTraversal and postorderTraversal simply take the private root reference and pass it to the appropriate helper method to initiate a traversal of the tree.
1122 |
Data Structures |
Chapter 19 |
|
|
27 |
|
13 |
|
|
42 |
6 |
17 |
33 |
48 |
|
|
|
|
|
|
|
|
Fig. 19.19 A binary search tree.
Method inorderHelper (lines 105–118) defines the steps for an inorder traversal. Those steps are as follows:
1.Traverse the left subtree with a call to inorderHelper (line 111).
2.Process the value in the node (line 114).
3.Traverse the right subtree with a call to inorderHelper (line 117).
The inorder traversal does not process the value in a node until the values in that node’s left subtree are processed. The inorder traversal of the tree in Fig. 19.19 is
6 13 17 27 33 42 48
Note that the inorder traversal of a binary search tree prints the node values in ascending order. The process of creating a binary search tree actually sorts the data—and thus, this process is called the binary tree sort.
Method preorderHelper (lines 83–96) defines the steps for a preorder traversal. Those steps are as follows:
1.Process the value in the node (line 89).
2.Traverse the left subtree with a call to preorderHelper (line 92).
3.Traverse the right subtree with a call to preorderHelper (line 95).
The preorder traversal processes the value in each node as the node is visited. After processing the value in a given node, the preorder traversal processes the values in the left subtree, then the values in the right subtree. The preorder traversal of the tree in Fig. 19.19 is
27 13 6 17 42 33 48
Method postorderHelper (lines 127–140) defines the steps for a postorder traversal. Those steps are as follows:
1.Traverse the left subtree with a postorderHelper (line 133).
2.Traverse the right subtree with a postorderHelper (line 136).
3.Process the value in the node (line 139).
The postorder traversal processes the value in each node after the values of all that node’s children are processed. The postorderTraversal of the tree in Fig. 19.19 is
6 17 13 33 48 42 27
The binary search tree facilitates duplicate elimination. While building a tree, the insertion operation recognizes attempts to insert a duplicate value, because a duplicate follows
Chapter 19 |
Data Structures |
1123 |
the same “go left” or “go right” decisions on each comparison as the original value did. Thus, the insertion operation eventually compares the duplicate with a node containing the same value. At this point, the insertion operation might simply discard the duplicate value.
Searching a binary tree for a value that matches a key value is fast, especially for tightly packed trees. In a tightly packed tree, each level contains about twice as many elements as the previous level. Figure 19.19 is a tightly packed binary tree. A binary search tree with n elements has a minimum of log2n levels. Thus, at most log2n comparisons are required either to find a match or to determine that no match exists. Searching a (tightly packed) 1000-element binary search tree requires at most 10 comparisons, because 210 > 1000. Searching a (tightly packed) 1,000,000-element binary search tree requires at most 20 comparisons, because 220 > 1,000,000.
The chapter exercises present algorithms for several other binary tree operations, such as deleting an item from a binary tree, printing a binary tree in a two-dimensional tree format and performing a level-order traversal of a binary tree. The level-order traversal of a binary tree visits the nodes of the tree row-by-row, starting at the root node level. On each level of the tree, a level-order traversal visits the nodes from left to right. Other binary tree exercises include allowing a binary search tree to contain duplicate values, inserting string values in a binary tree and determining how many levels are contained in a binary tree.
SUMMARY
•Dynamic data structures can grow and shrink at execution time.
•Linked lists are collections of data items “lined up in a row”—insertions and deletions can be made anywhere in a linked list.
•Stacks are important in compilers and operating systems—insertions and deletions are made only at one end of a stack, its top.
•Queues represent waiting lines; insertions are made at the back (also referred to as the tail) of a queue and deletions are made from the front (also referred to as the head) of a queue.
•Binary trees facilitate high-speed searching and sorting of data, eliminating duplicate data items efficiently, representing file system directories and compiling expressions into machine language.
•A self-referential class contains a reference that refers to another object of the same class type. Self-referential objects can be linked together to form useful data structures such as lists, queues, stacks and trees.
•Creating and maintaining dynamic data structures requires dynamic memory allocation—the ability for a program to obtain more memory space at execution time to hold new nodes and to release space no longer needed.
•The limit for dynamic memory allocation can be as large as the available physical memory in the computer or the amount of available disk space in a virtual-memory system. Often, the limits are much smaller because the computer’s available memory must be shared among many users.
•Operator new takes as an operand the type of the object being dynamically allocated and returns a reference to a newly created object of that type. If no memory is available, new throws an OutOfMemoryError.
•A linked list is a linear collection (i.e., a sequence) of self-referential class objects, called nodes, connected by reference links.
•A linked list is accessed via a reference to the first node of the list. Each subsequent node is accessed via the link-reference member stored in the previous node.
1124 |
Data Structures |
Chapter 19 |
•By convention, the link reference in the last node of a list is set to null to mark the end of the list.
•A node can contain data of any type, including objects of other classes.
•Trees are nonlinear data structures.
•A linked list is appropriate when the number of data elements to be represented in the data structure is unpredictable. Linked lists are dynamic, so the length of a list can increase or decrease as necessary.
•The size of a “conventional” Java array cannot be altered—the size is fixed at creation time.
•Linked lists can be maintained in sorted order simply by inserting each new element at the proper point in the list.
•List nodes are normally not stored contiguously in memory. Rather, they are logically contiguous.
•Methods that manipulate the contents of a list should be declared synchronized so list objects can be multithread safe when used in a multithreaded program. If one thread is modifying the contents of a list, no other thread is allowed to modify the same list at the same time.
•A stack is a constrained version of a linked list—new nodes can be added to a stack and removed from a stack only at the top. A stack is referred to as a last-in, first-out (LIFO) data structure.
•The primary methods used to manipulate a stack are push and pop. Method push adds a new node to the top of the stack. Method pop removes a node from the top of the stack and returns the data object from the popped node.
•Stacks have many interesting applications. When a method call is made, the called method must know how to return to its caller, so the return address is pushed onto the program execution stack. If a series of method calls occurs, the successive return values are pushed onto the stack in last-in, first-out order so that each method can return to its caller.
•The program execution stack contains the space created for local variables on each invocation of a method. When the method returns to its caller, the space for that method’s local variables is popped off the stack, and those variables are no longer known to the program.
•Stacks are also used by compilers in the process of evaluating arithmetic expressions and generating machine language code to process the expressions.
•The technique of implementing each stack method as a call to a List method is called delegat- ing—the stack method invoked delegates the call to the appropriate List method.
•A queue is a constrained version of a list.
•A queue is similar to a checkout line in a supermarket—the first person in line is serviced first, and other customers enter the line only at the end and wait to be serviced.
•Queue nodes are removed only from the head of the queue and are inserted only at the tail of the queue. For this reason, a queue is referred to as a first-in, first-out (FIFO) data structure.
•The insert and remove operations for a queue are known as enqueue and dequeue.
•Queues have many applications in computer systems. Most computers have only a single processor, so only one user at a time can be serviced. Entries for the other users are placed in a queue. The entry at the front of the queue is the next to receive service. Each entry gradually advances to the front of the queue as users receive service.
•Queues are also used to support print spooling. A multiuser environment might have only a single printer. Many users may be generating outputs to be printed. If the printer is busy, other outputs may still be generated. These are “spooled” to disk (much as thread is wound onto a spool) where they wait in a queue until the printer becomes available.
•Information packets also wait in queues in computer networks. Each time a packet arrives at a network node, it must be routed to the next node on the network along the path to the packet’s final
Chapter 19 |
Data Structures |
1125 |
destination. The routing node routes one packet at a time, so additional packets are enqueued until the router can route them.
•A file server in a computer network handles file-access requests from many clients throughout the network. Servers have a limited capacity to service requests from clients. When that capacity is exceeded, client requests wait in queues.
•A tree is a nonlinear, two-dimensional data structure.
•Tree nodes contain two or more links.
•A binary tree is a tree whose nodes all contain two links. The root node is the first node in a tree.
•Each link in the root node refers to a child. The left child is the first node in the left subtree, and the right child is the first node in the right subtree.
•The children of a node are called siblings. A node with no children is called a leaf node.
•Computer scientists normally draw trees from the root node down.
•A binary search tree (with no duplicate node values) has the characteristic that the values in any left subtree are less than the value in its parent node, and the values in any right subtree are greater than the value in its parent node.
•A node can be inserted only as a leaf node in a binary search tree.
•An inorder traversal of a binary search tree processes the node values in ascending order.
•The process of creating a binary search tree actually sorts the data—and thus this process is called the binary tree sort.
•In a preorder traversal, the value in each node is processed as the node is visited. After the value in a given node is processed, the values in the left subtree are processed, then the values in the right subtree are processed.
•In a postorder traversal, the value in each node is processed after the values of its children.
•The binary search tree facilitates duplicate elimination. As the tree is created, attempts to insert a duplicate value are recognized because a duplicate follows the same “go left” or “go right” decisions on each comparison as the original value did. Thus, the duplicate eventually is compared with a node containing the same value. The duplicate value could simply be discarded at this point.
•Searching a binary tree for a value that matches a key value is also fast, especially for tightly packed trees. In a tightly packed tree, each level contains about twice as many elements as the pre-
vious level. So a binary search tree with n elements has a minimum of log2n levels, and thus at most log2n, comparisons would have to be made either to find a match or to determine that no match exists. Searching a (tightly packed) 1000-element binary search tree requires at most 10 comparisons, because 210 > 1000. Searching a (tightly packed) 1,000,000-element binary search tree requires at most 20 comparisons, because 220 > 1,000,000.
TERMINOLOGY
binary search tree |
enqueue |
binary tree |
FIFO (first-in, first-out) |
binary tree sort |
head of a queue |
child node |
inorder traversal of a binary tree |
children |
inserting a node |
delegating |
leaf node |
deleting a node |
left child |
dequeue |
left subtree |
duplicate elimination |
level-order traversal of a binary tree |
dynamic data structures |
LIFO (last-in, first-out) |
1126 |
Data Structures |
Chapter 19 |
linear data structure |
queue |
|
linked list |
|
recursive tree traversal algorithms |
node |
|
right child |
nonlinear data structure |
right subtree |
|
null reference |
root node |
|
OutOfMemoryError |
self-referential class |
|
parent node |
stack |
|
pop |
|
subtree |
postorder traversal of a binary tree |
tail of a queue |
|
predicate method |
top of a stack |
|
preorder traversal of a binary tree |
traversal |
|
program execution stack |
tree |
|
push |
|
visiting a node |
SELF-REVIEW EXERCISES
19.1Fill in the blanks in each of the following statements:
a) |
A self- |
|
|
|
class is used to form dynamic data structures that can grow and shrink |
|||||||||||||||||||||||||||
|
at execution time. |
|
|
|
|
|
|
|
|
|
|
|
|
|||||||||||||||||||
b) |
Operator |
|
|
|
|
dynamically allocates memory; this operator returns a reference to |
||||||||||||||||||||||||||
|
the allocated memory. |
|
|
|
|
|
|
|
|
|
|
|
|
|||||||||||||||||||
c) |
A |
|
|
is a constrained version of a linked list in which nodes can be inserted and |
||||||||||||||||||||||||||||
|
deleted only from the start of the list; this data structure returns node values in last-in, |
|||||||||||||||||||||||||||||||
|
first-out order. |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
||||||||
d) |
A method that does not alter a linked list, but simply looks at the list to determine whether |
|||||||||||||||||||||||||||||||
|
it is empty is referred to as a |
|
|
|
method. |
|
|
|
|
|
|
|
||||||||||||||||||||
e) |
A queue is referred to as a |
|
data structure because the first nodes inserted are |
|||||||||||||||||||||||||||||
|
the first nodes removed. |
|
|
|
|
|
|
|
|
|
|
|
|
|||||||||||||||||||
f) |
The reference to the next node in a linked list is referred to as a |
|
. |
|
||||||||||||||||||||||||||||
g) |
Automatically reclaiming dynamically allocated memory in Java is called |
|
|
. |
||||||||||||||||||||||||||||
h) |
A |
|
|
is a constrained version of a linked list in which nodes can be inserted only |
||||||||||||||||||||||||||||
|
at the end of the list and deleted only from the start of the list. |
|
|
|||||||||||||||||||||||||||||
i) |
A |
|
|
is a nonlinear, two-dimensional data structure that contains nodes with |
||||||||||||||||||||||||||||
|
two or more links. |
|
|
|
|
|
|
|
|
|
|
|
|
|||||||||||||||||||
j) |
A stack is referred to as a |
|
data structure because the last node inserted is the |
|||||||||||||||||||||||||||||
|
first node removed. |
|
|
|
|
|
|
|
|
|
|
|
|
|||||||||||||||||||
k) |
The nodes of a |
|
|
|
|
|
|
|
tree contain two link members. |
|
|
|||||||||||||||||||||
l) |
The first node of a tree is the |
|
|
|
node. |
|
|
|
|
|
|
|
||||||||||||||||||||
m) |
Each link in a tree node refers to a |
|
|
|
|
|
or |
|
|
of that node. |
|
|
||||||||||||||||||||
n) |
A tree node that has no children is called a |
|
|
node. |
|
|
||||||||||||||||||||||||||
o) |
The four traversal algorithms we mentioned in the text for binary search trees are |
|||||||||||||||||||||||||||||||
|
|
, |
|
|
|
, |
|
|
|
|
|
and |
. |
|
|
|
|
|
||||||||||||||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
19.2What are the differences between a linked list and a stack?
19.3What are the differences between a stack and a queue?
19.4Perhaps a more appropriate title for this chapter would have been “Reusable Data Structures.” Comment on how each of the following entities or concepts contributes to the reusability of data structures:
a)classes
b)inheritance
c)composition
Chapter 19 |
Data Structures |
1127 |
19.5 Manually provide the inorder, preorder and postorder traversals of the binary search tree of Fig. 19.20.
ANSWERS TO SELF-REVIEW EXERCISES
19.1 a) referential. b) new. c) stack. d) predicate. e) first-in, first-out (FIFO). f) link. g) garbage collection. h) queue. i) tree j) last-in, first-out (LIFO). k) binary. l) root. m) child or subtree. n) leaf. o) inorder, preorder, postorder, level order.
19.2It is possible to insert a node anywhere in a linked list and remove a node from anywhere in a linked list. Nodes in a stack may only be inserted at the top of the stack and removed from the top of a stack.
19.3A queue is a FIFO data structure that has references to both its head and its tail so that nodes may be inserted at the tail and deleted from the head. A stack is a LIFO data structure that has a single reference to the top of the stack where both insertion and deletion of nodes are performed.
19.4a) Classes allow us to instantiate as many data structure objects of a certain type (i.e., class)
as we wish.
b)Inheritance enables us to reuse code from a superclass in a subclass so that the derived class data structure is also a base-class data structure.
c)Composition enables us to reuse code by making a class object data structure a member of a composed class; if we make the class object a private member of the composed class, then the class object’s public methods are not available through the composed object’s interface.
19.5The inorder traversal is
11 18 19 28 32 40 44 49 69 71 72 83 92 97 99
The preorder traversal is
49 28 18 11 19 40 32 44 83 71 69 72 97 92 99
The postorder traversal is
11 19 18 32 44 40 28 69 72 71 92 99 97 83 49
EXERCISES
19.6 Write a program that concatenates two linked-list objects of characters. Class ListConcatenate should include a method concatenate that takes references to both list objects as arguments and concatenates the second list to the first list.
|
|
49 |
|
|
28 |
|
83 |
18 |
40 |
71 |
97 |
11 19 |
32 44 |
69 72 |
92 99 |
|
|
|
|
|
|
|
|
Fig. 19.20 A 15-node binary search tree.
1128 |
Data Structures |
Chapter 19 |
19.7Write a program that merges two ordered-list objects of integers into a single ordered list object of integers. Method merge of class ListMerge should receive references to each of the list objects to be merged and should return a reference to the merged list object.
19.8Write a program that inserts 25 random integers from 0 to 100 in order into a linked list object. The program should calculate the sum of the elements and the floating-point average of the elements.
19.9Write a program that creates a linked list object of 10 characters, then creates a second list object containing a copy of the first list, but in reverse order.
19.10Write a program that inputs a line of text and uses a stack object to print the words of the line in reverse order.
19.11Write a program that uses a stack to determine whether a string is a palindrome (i.e., the string is spelled identically backward and forward). The program should ignore spaces and punctuation.
19.12Stacks are used by compilers to help in the process of evaluating expressions and generating machine language code. In this and the next exercise, we investigate how compilers evaluate arithmetic expressions consisting only of constants, operators and parentheses.
Humans generally write expressions like 3 + 4 and 7 / 9 in which the operator (+ or / here) is written between its operands—this is called infix notation. Computers “prefer” postfix notation, in which the operator is written to the right of its two operands. The preceding infix expressions would appear in postfix notation as 3 4 + and 7 9 /, respectively.
To evaluate a complex infix expression, a compiler would first convert the expression to postfix notation and evaluate the postfix version of the expression. Each of these algorithms requires only a single left-to-right pass of the expression. Each algorithm uses a stack object in support of its operation, and in each algorithm, the stack is used for a different purpose.
In this exercise, you will write a Java version of the infix-to-postfix conversion algorithm. In the next exercise, you will write a Java version of the postfix expression evaluation algorithm. In a later exercise, you will discover that code you write in this exercise can help you implement a complete working compiler.
Write class InfixToPostfixConverter to convert an ordinary infix arithmetic expression (assume a valid expression is entered) with single-digit integers such as
(6 + 2) * 5 - 8 / 4
to a postfix expression. The postfix version of the preceding infix expression is (note that no parenthesis are needed)
6 2 + 5 * 8 4 / -
The program should read the expression into StringBuffer infix and use one of the stack classes implemented in this chapter to help create the postfix expression in StringBuffer postfix. The algorithm for creating a postfix expression is as follows:
a)Push a left parenthesis '(' on the stack.
b)Append a right parenthesis ')' to the end of infix.
c)While the stack is not empty, read infix from left to right and do the following: If the current character in infix is a digit, append it to postfix.
If the current character in infix is a left parenthesis, push it onto the stack. If the current character in infix is an operator:
Pop operators (if there are any) at the top of the stack while they have equal or higher precedence than the current operator, and append the popped operators to postfix.
Push the current character in infix onto the stack.
Chapter 19 |
Data Structures |
1129 |
If the current character in infix is a right parenthesis:
Pop operators from the top of the stack and append them to postfix until a left parenthesis is at the top of the stack.
Pop (and discard) the left parenthesis from the stack.
The following arithmetic operations are allowed in an expression: + addition
- subtraction
* multiplication / division
^ exponentiation % modulus
The stack should be maintained with stack nodes that each contain an instance variable and a reference to the next stack node. Some of the methods you may want to provide are as follows:
a)Method convertToPostfix, which converts the infix expression to postfix notation.
b)Method isOperator, which determines whether c is an operator.
c)Method precedence, which determines if the precedence of operator1 (from the infix expression) is less than, equal to or greater than the precedence of operator2 (from the stack). The method returns true if operator1 has lower precedence than operator2. Otherwise, false is returned.
d)Method stackTop (this should be added to the stack class), which returns the top value of the stack without popping the stack.
19.13Write class PostfixEvaluator, which evaluates a postfix expression (assume it is valid)
such as
6 2 + 5 * 8 4 / -
The program should read a postfix expression consisting of digits and operators into a StringBuffer. Using modified versions of the stack methods implemented earlier in this chapter, the program should scan the expression and evaluate it. The algorithm is as follows:
a)Append a right parenthesis (')') to the end of the postfix expression. When the rightparenthesis character is encountered, no further processing is necessary.
b)When the right-parenthesis character has not been encountered, read the expression from left to right.
If the current character is a digit do the following:
Push its integer value on the stack (the integer value of a digit character is its value in the computer’s character set minus the value of '0' in Unicode).
Otherwise, if the current character is an operator:
Pop the two top elements of the stack into variables x and y. Calculate y operator x.
Push the result of the calculation onto the stack.
c)When the right parenthesis is encountered in the expression, pop the top value of the stack. This is the result of the postfix expression.
[Note: In b) above (based on the sample expression at the beginning of this exercises), if the operator is '/', the top of the stack is 2 and the next element in the stack is 8, then pop 2 into x, pop 8 into y, evaluate 8 / 2 and push the result, 4, back on the stack. This note also applies to operator '-'.] The arithmetic operations allowed in an expression are:
+ addition
- subtraction
* multiplication / division
1130 |
Data Structures |
Chapter 19 |
^ exponentiation % modulus
The stack should be maintained with one of the stack classes introduced in this chapter. You may want to provide the following methods:
a)Method evaluatePostfixExpression, which evaluates the postfix expression.
b)Method calculate, which evaluates the expression op1 operator op2.
c)Method push, which pushes a value onto the stack.
d)Method pop, which pops a value off the stack.
e)Method isEmpty, which determines whether the stack is empty.
f)Method printStack, which prints the stack.
19.14Modify the postfix evaluator program of Exercise 19.13 so that it can process integer operands larger than 9.
19.15(Supermarket Simulation) Write a program that simulates a checkout line at a supermarket. The line is a queue object. Customers (i.e., customer objects) arrive in random integer intervals of from 1 to 4 minutes. Also, each customer is serviced in random integer intervals of from 1 to 4 minutes. Obviously, the rates need to be balanced. If the average arrival rate is larger than the average service rate, the queue will grow infinitely. Even with “balanced” rates, randomness can still cause long lines. Run the supermarket simulation for a 12-hour day (720 minutes), using the following algorithm:
a)Choose a random integer between 1 and 4 to determine the minute at which the first customer arrives.
b)At the first customer’s arrival time, do the following:
Determine customer’s service time (random integer from 1 to 4). Begin servicing the customer.
Schedule the arrival time of the next customer (random integer 1 to 4 added to the current time).
c)For each minute of the day, consider the following:
If the next customer arrives, proceed as follows: Say so.
Enqueue the customer.
Schedule the arrival time of the next customer.
If service was completed for the last customer, do the following: Say so.
Dequeue next customer to be serviced.
Determine customer’s service completion time (random integer from 1 to 4 added to the current time).
Now run your simulation for 720 minutes and answer each of the following:
a)What is the maximum number of customers in the queue at any time?
b)What is the longest wait any one customer experiences?
c)What happens if the arrival interval is changed from 1 to 4 minutes to 1 to 3 minutes?
19.16Modify the program of Fig. 19.17 and Fig. 19.18 to allow the binary tree to contain duplicates.
19.17Write a program based on the program of Fig. 19.17 and Fig. 19.18 that inputs a line of text, tokenizes the sentence into separate words (you might want to use the StreamTokenizer class from the java.io package), inserts the words in a binary search tree and prints the inorder, preorder and post-order traversals of the tree.
19.18In this chapter, we saw that duplicate elimination is straightforward when creating a binary search tree. Describe how you would perform duplicate elimination when using only a single-sub- scripted array. Compare the performance of array-based duplicate elimination with the performance of binary-search-tree-based duplicate elimination.
Chapter 19 |
Data Structures |
1131 |
19.19Write a method depth that receives a binary tree and determines how many levels it has.
19.20(Recursively Print a List Backwards) Write a method printListBackwards that recursively outputs the items in a linked list object in reverse order. Write a test program that creates a sorted list of integers and prints the list in reverse order.
19.21(Recursively Search a List) Write a method searchList that recursively searches a linked list object for a specified value. Method searchList should return a reference to the value if it is found; otherwise, null should be returned. Use your method in a test program that creates a list of integers. The program should prompt the user for a value to locate in the list.
19.22(Binary Tree Delete) In this exercise, we discuss deleting items from binary search trees. The deletion algorithm is not as straightforward as the insertion algorithm. There are three cases that are encountered when deleting an item—the item is contained in a leaf node (i.e., it has no children), the item is contained in a node that has one child or the item is contained in a node that has two children.
If the item to be deleted is contained in a leaf node, the node is deleted and the reference in the parent node is set to null.
If the item to be deleted is contained in a node with one child, the reference in the parent node is set to reference the child node and the node containing the data item is deleted. This causes the child node to take the place of the deleted node in the tree.
The last case is the most difficult. When a node with two children is deleted, another node in the tree must take its place. However, the reference in the parent node cannot simply be assigned to reference one of the children of the node to be deleted. In most cases, the resulting binary search tree would not adhere to the following characteristic of binary search trees (with no duplicate values):
The values in any left subtree are less than the value in the parent node, and the values in any right subtree are greater than the value in the parent node.
Which node is used as a replacement node to maintain this characteristic? It is either the node containing the largest value in the tree less than the value in the node being deleted, or the node containing the smallest value in the tree greater than the value in the node being deleted. Let us consider the node with the smaller value. In a binary search tree, the largest value less than a parent’s value is located in the left subtree of the parent node and is guaranteed to be contained in the rightmost node of the subtree. This node is located by walking down the left subtree to the right until the reference to the right child of the current node is null. We are now referencing the replacement node, which is either a leaf node or a node with one child to its left. If the replacement node is a leaf node, the steps to perform the deletion are as follows:
a)Store the reference to the node to be deleted in a temporary reference variable.
b)Set the reference in the parent of the node being deleted to reference the replacement node.
c)Set the reference in the parent of the replacement node to null.
d)Set the reference to the right subtree in the replacement node to reference the right subtree of the node to be deleted.
e)Set the reference to the left subtree in the replacement node to reference the left subtree of the node to be deleted.
The deletion steps for a replacement node with a left child are similar to those for a replacement node with no children, but the algorithm also must move the child into the replacement node’s position in the tree. If the replacement node is a node with a left child, the steps to perform the deletion are as follows:
a)Store the reference to the node to be deleted in a temporary reference variable.
b)Set the reference in the parent of the node being deleted to reference the replacement node.
c)Set the reference in the parent of the replacement node reference to the left child of the replacement node.
1132 |
Data Structures |
Chapter 19 |
d)Set the reference to the right subtree in the replacement node reference to the right subtree of the node to be deleted.
e)Set the reference to the left subtree in the replacement node to reference the left subtree of the node to be deleted.
Write method deleteNode, which takes as its argument the value to be deleted. Method deleteNode should locate in the tree the node containing the value to be deleted and use the algorithms discussed here to delete the node. If the value is not found in the tree, the method should print a message that indicates whether the value is deleted. Modify the program of Fig. 19.17 and Fig. 19.18 to use this method. After deleting an item, call the methods inorderTraversal, preorderTraversal and postorderTraversal to confirm that the delete operation was performed correctly.
19.23(Binary Tree Search) Write method binaryTreeSearch, which attempts to locate a specified value in a binary search tree object. The method should take as an argument a search key to be located. If the node containing the search key is found, the method should return a reference to that node; otherwise, the method should return a null reference.
19.24(Level-Order Binary Tree Traversal) The program of Fig. 19.17 and Fig. 19.18 illustrated three recursive methods of traversing a binary tree—inorder, preorder and postorder traversals. This exercise presents the level-order traversal of a binary tree, in which the node values are printed level- by-level, starting at the root node level. The nodes on each level are printed from left to right. The level-order traversal is not a recursive algorithm. It uses a queue object to control the output of the nodes. The algorithm is as follows:
a)Insert the root node in the queue.
b)While there are nodes left in the queue, do the following:
Get the next node in the queue.
Print the node’s value.
If the reference to the left child of the node is not null:
Insert the left child node in the queue.
If the reference to the right child of the node is not null:
Insert the right child node in the queue.
Write method levelOrder to perform a level-order traversal of a binary tree object. Modify the program of Fig. 19.17 and Fig. 19.18 to use this method. [Note: You will also need to use queueprocessing methods of Fig. 19.13 in this program.]
19.25 (Printing Trees) Write a recursive method outputTree to display a binary tree object on the screen. The method should output the tree row-by-row, with the top of the tree at the left of the screen and the bottom of the tree toward the right of the screen. Each row is output vertically. For example, the binary tree illustrated in Fig. 19.20 is output as shown in Fig. 19.21.
Note that the rightmost leaf node appears at the top of the output in the rightmost column and the root node appears at the left of the output. Each column of output starts five spaces to the right of the preceding column. Method outputTree should receive an argument totalSpaces representing the number of spaces preceding the value to be output. (This variable should start at zero so the root node is output at the left of the screen.) The method uses a modified inorder traversal to output the tree—it starts at the rightmost node in the tree and works back to the left. The algorithm is as follows:
While the reference to the current node is not null, perform the following: Recursively call outputTree with the right subtree of the current node and
totalSpaces + 5.
Use a for structure to count from 1 to totalSpaces and output spaces. Output the value in the current node.
Set the reference to the current node to refer to the left subtree of the current node. Increment totalSpaces by 5.
Chapter 19 |
Data Structures |
1133 |
99
97
92
83
72
71
69
49
44
40
32
28
19
18
11
Fig. 19.21 Sample output of recursive method outputTree.
SPECIAL SECTION: BUILDING YOUR OWN COMPILER
In Exercise 7.42 and Exercise 7.43, we introduced Simpletron Machine Language (SML), and you implemented a Simpletron computer simulator to execute programs written in SML. In this section, we build a compiler that converts programs written in a high-level programming language to SML. This section “ties” together the entire programming process. You will write programs in this new high-level language, compile these programs on the compiler you build and run the programs on the simulator you built in Exercise 7.43. You should make every effort to implement your compiler in an object-oriented manner.
19.26 (The Simple Language) Before we begin building the compiler, we discuss a simple, yet powerful high-level language similar to early versions of the popular language Basic. We call the language Simple. Every Simple statement consists of a line number and a Simple instruction. Line numbers must appear in ascending order. Each instruction begins with one of the following Simple commands: rem, input, let, print, goto, if/goto or end (see Fig. 19.22). All commands except end can be used repeatedly. Simple evaluates only integer expressions using the +, -, * and / operators. These operators have the same precedence as in Java. Parentheses can be used to change the order of evaluation of an expression.
Command |
Example statement |
Description |
|
|
|
|
|
rem |
50 |
rem this is a remark |
Any text following the command rem is for |
|
|
|
documentation purposes only and is ignored |
|
|
|
by the compiler. |
input |
30 |
input x |
Display a question mark to prompt the user to |
|
|
|
enter an integer. Read that integer from the |
|
|
|
keyboard and store the integer in x. |
let |
80 |
let u = 4 * (j - 56) |
Assign u the value of 4 * (j - 56). Note that |
|
|
|
an arbitrarily complex expression can appear |
|
|
|
to the right of the equal sign. |
Fig. 19.22 Simple commands (part 1 of 2).
1134 |
Data Structures |
Chapter 19 |
||
|
|
|
||
Command |
Example statement |
Description |
||
|
|
|
|
|
|
10 |
print w |
Display the value of w. |
|
goto |
|
70 |
goto 45 |
Transfer program control to line 45. |
if/goto |
35 |
if i == z goto 80 |
Compare i and z for equality and transfer |
|
|
|
|
|
program control to line 80 if the condition is |
|
|
|
|
true; otherwise, continue execution with the |
|
|
|
|
next statement. |
end |
|
99 |
end |
Terminate program execution. |
Fig. 19.22 Simple commands (part 2 of 2).
Our Simple compiler recognizes only lowercase letters. All characters in a Simple file should be lowercase. (Uppercase letters result in a syntax error unless they appear in a rem statement, in which case they are ignored.) A variable name is a single letter. Simple does not allow descriptive variable names, so variables should be explained in remarks to indicate their use in a program. Simple uses only integer variables. Simple does not have variable declarations—merely mentioning a variable name in a program causes the variable to be declared and initialized to zero. The syntax of Simple does not allow string manipulation (reading a string, writing a string, comparing strings etc.). If a string is encountered in a Simple program (after a command other than rem), the compiler generates a syntax error. The first version of our compiler assumes that Simple programs are entered correctly. Exercise 19.29 asks the reader to modify the compiler to perform syntax error checking.
Simple uses the conditional if/goto and unconditional goto statements to alter the flow of control during program execution. If the condition in the if/goto statement is true, control is transferred to a specific line of the program. The following relational and equality operators are valid in an if/goto statement: <, >, <=, >=, == or !=. The precedence of these operators is the same as in Java.
Let us now consider several programs that demonstrate Simple’s features. The first program (Fig. 19.23) reads two integers from the keyboard, stores the values in variables a and b and computes and prints their sum (stored in variable c).
The program of Fig. 19.24 determines and prints the larger of two integers. The integers are input from the keyboard and stored in s and t. The if/goto statement tests the condition s >= t. If the condition is true, control is transferred to line 90 and s is output; otherwise, t is output and control is transferred to the end statement in line 99, where the program terminates.
1 |
10 |
rem |
determine and print the sum of two integers |
2 |
15 |
rem |
|
3 |
20 |
rem |
input the two integers |
4 |
30 |
input |
a |
5 |
40 |
input b |
|
6 |
45 |
rem |
|
7 |
50 |
rem |
add integers and store result in c |
8 |
60 |
let c = a + b |
|
9 |
65 |
rem |
|
10 |
70 |
rem |
print the result |
11 |
80 |
c |
|
12 |
90 |
rem |
terminate program execution |
13 |
99 |
end |
|
|
|
|
|
Fig. 19.23 Simple program that determines the sum of two integers.
Chapter 19 |
Data Structures |
1135 |
Simple does not provide a repetition structure (such as Java’s for, while or do/while). However, Simple can simulate each of Java's repetition structures by using the if/goto and goto statements. Figure 19.25 uses a sentinel-controlled loop to calculate the squares of several integers. Each integer is input from the keyboard and stored in variable j. If the value entered is the sentinel value -9999, control is transferred to line 99, where the program terminates. Otherwise, k is assigned the square of j, k is output to the screen and control is passed to line 20, where the next integer is input.
Using the sample programs of Fig. 19.23–Fig. 19.25 as your guide, write a Simple program to accomplish each of the following:
a)Input three integers, determine their average and print the result.
b)Use a sentinel-controlled loop to input 10 integers and compute and print their sum.
c)Use a counter-controlled loop to input 7 integers, some positive and some negative, and compute and print their average.
d)Input a series of integers and determine and print the largest. The first integer input indicates how many numbers should be processed.
e)Input 10 integers and print the smallest.
f)Calculate and print the sum of the even integers from 2 to 30.
g)Calculate and print the product of the odd integers from 1 to 9.
1 |
10 |
rem |
determine and print the larger of two integers |
|
|
|
|
|
|
2 |
20 |
input |
s |
|
3 |
30 |
input t |
|
|
4 |
32 |
rem |
|
|
5 |
35 |
rem |
test if s >= t |
|
6 |
40 |
if s >= |
t goto 90 |
|
7 |
45 |
rem |
|
|
8 |
50 |
rem |
t |
is greater than s, so print t |
9 |
60 |
print t |
|
|
10 |
70 |
goto 99 |
|
|
11 |
75 |
rem |
|
|
12 |
80 |
rem |
s |
is greater than or equal to t, so print s |
13 |
90 |
print s |
|
|
14 |
99 |
end |
|
|
|
|
|||
Fig. 19.24 |
Simple program that finds the larger of two integers. |
|||
|
|
|
|
|
1 |
10 |
rem |
calculate the squares of several integers |
|
2 |
20 |
input j |
|
|
3 |
23 |
rem |
|
|
4 |
25 |
rem |
test for sentinel value |
|
5 |
30 |
if j == |
-9999 goto 99 |
|
6 |
33 |
rem |
|
|
7 |
35 |
rem |
calculate square of j and assign result to k |
|
8 |
40 |
let k |
= |
j * j |
9 |
50 |
print k |
|
|
10 |
53 |
rem |
|
|
11 |
55 |
rem |
loop to get next j |
|
12 |
60 |
goto 20 |
|
|
13 |
99 |
end |
|
|
|
|
|
|
|
Fig. 19.25 Calculate the squares of several integers.
1136 |
Data Structures |
Chapter 19 |
19.27 (Building A Compiler; Prerequisite: Complete Exercise 7.42, Exercise 7.43, Exercise 19.12, Exercise 19.13 and Exercise 19.26) Now that the Simple language has been presented (Exercise 19.26), we discuss how to build a Simple compiler. First, we consider the process by which a Simple program is converted to SML and executed by the Simpletron simulator (see Fig. 19.26). A file containing a Simple program is read by the compiler and converted to SML code. The SML code is output to a file on disk, in which SML instructions appear one per line. The SML file is then loaded into the Simpletron simulator, and the results are sent to a file on disk and to the screen. Note that the Simpletron program developed in Exercise 7.43 took its input from the keyboard. It must be modified to read from a file so it can run the programs produced by our compiler.
The Simple compiler performs two passes of the Simple program to convert it to SML. The first pass constructs a symbol table (object) in which every line number (object), variable name (object) and constant (object) of the Simple program is stored with its type and corresponding location in the final SML code (the symbol table is discussed in detail below). The first pass also produces the corresponding SML instruction object(s) for each of the Simple statements (object, etc.). If the Simple program contains statements that transfer control to a line later in the program, the first pass results in an SML program containing some “unfinished” instructions. The second pass of the compiler locates and completes the unfinished instructions and outputs the SML program to a file.
First Pass
The compiler begins by reading one statement of the Simple program into memory. The line must be separated into its individual tokens (i.e., “pieces” of a statement) for processing and compilation. (The StreamTokenizer class from the java.io package can be used.) Recall that every statement begins with a line number followed by a command. As the compiler breaks a statement into tokens, if the token is a line number, a variable or a constant, it is placed in the symbol table. A line number is placed in the symbol table only if it is the first token in a statement. The symbolTable object is an array of tableEntry objects representing each symbol in the program. There is no restriction on the number of symbols that can appear in the program. Therefore, the symbolTable for a particular program could be large. Make the symbolTable a 100-element array for now. You can increase or decrease its size once the program is working.
Each tableEntry object contains three members. Member symbol is an integer containing the Unicode representation of a variable (remember that variable names are single characters), a line number or a constant. Member type is one of the following characters indicating the symbol’s type: 'C' for constant, 'L' for line number or 'V' for variable. Member location contains the Simpletron memory location (00 to 99) to which the symbol refers. Simpletron memory is an array of 100 integers in which SML instructions and data are stored. For a line number, the location is the element in the Simpletron memory array at which the SML instructions for the Simple statement begin. For a variable or constant, the location is the element in the Simpletron memory array in which the variable or constant is stored. Variables and constants are allocated from the end of Simpletron’s memory backwards. The first variable or constant is stored at location 99, the next at location 98, etc.
The symbol table plays an integral part in converting Simple programs to SML. We learned in Chapter 7 that an SML instruction is a four-digit integer comprised of two parts—the operation code and the operand. The operation code is determined by commands in Simple. For example, the simple command input corresponds to SML operation code 10 (read), and the Simple command print corresponds to SML operation code 11 (write). The operand is a memory location containing the data on which the operation code performs its task (e.g., operation code 10 reads a value from the keyboard and stores it in the memory location specified by the operand). The compiler searches symbolTable to determine the Simpletron memory location for each symbol, so the corresponding location can be used to complete the SML instructions.
Chapter 19 |
Data Structures |
1137 |
Simple file |
compiler |
SML file |
Simpletron |
|
Simulator |
|||
|
|||
|
|
output to |
disk |
output to screen
Fig. 19.26 Writing, compiling and executing a Simple language program.
The compilation of each Simple statement is based on its command. For example, after the line number in a rem statement is inserted in the symbol table, the remainder of the statement is ignored by the compiler because a remark is for documentation purposes only. The input, print, goto and end statements correspond to the SML read, write, branch (to a specific location) and halt instructions. Statements containing these Simple commands are converted directly to SML. (Note: A goto statement may contain an unresolved reference if the specified line number refers to a statement further into the Simple program file; this is sometimes called a forward reference.)
When a goto statement is compiled with an unresolved reference, the SML instruction must be flagged to indicate that the second pass of the compiler must complete the instruction. The flags are stored in a 100-element array flags of type int in which each element is initialized to -1. If the memory location to which a line number in the Simple program refers is not yet known (i.e., it is not in the symbol table), the line number is stored in array flags in the element with the same subscript as the incomplete instruction. The operand of the incomplete instruction is set to 00 temporarily. For example, an unconditional branch instruction (making a forward reference) is left as +4000 until the second pass of the compiler. The second pass of the compiler will be described shortly.
Compilation of if/goto and let statements is more complicated than other statements—they are the only statements that produce more than one SML instruction. For an if/goto statement, the compiler produces code to test the condition and to branch to another line if necessary. The result of the branch could be an unresolved reference. Each of the relational and equality operators can be simulated by using SML’s branch zero and branch negative instructions (or possibly a combination of both).
For a let statement, the compiler produces code to evaluate an arbitrarily complex arithmetic expression consisting of integer variables and/or constants. Expressions should separate each operand and operator with spaces. Exercise 19.12 and Exercise 19.13 presented the infix-to-postfix conversion algorithm and the postfix evaluation algorithm used by compilers to evaluate expressions. Before proceeding with your compiler, you should complete each of these exercises. When a compiler encounters an expression, it converts the expression from infix notation to postfix notation, then evaluates the postfix expression.
How is it that the compiler produces the machine language to evaluate an expression containing variables? The postfix evaluation algorithm contains a “hook” where the compiler can generate SML instructions rather than actually evaluating the expression. To enable this “hook” in the compiler, the postfix evaluation algorithm must be modified to search the symbol table for each symbol it encounters (and possibly insert it), determine the symbol’s corresponding memory location and push the memory location on the stack (instead of the symbol). When an operator is encountered in the postfix expression, the two memory locations at the top of the stack are popped, and machine language for effecting the operation is produced by using the memory locations as operands. The result of each subexpression is stored in a temporary location in memory and pushed back onto the stack so the
1138 |
Data Structures |
Chapter 19 |
evaluation of the postfix expression can continue. When postfix evaluation is complete, the memory location containing the result is the only location left on the stack. This is popped, and SML instructions are generated to assign the result to the variable at the left of the let statement.
Second Pass
The second pass of the compiler performs two tasks: Resolve any unresolved references and output the SML code to a file. Resolution of references occurs as follows:
a)Search the flags array for an unresolved reference (i.e., an element with a value other than -1).
b)Locate the object in array symbolTable containing the symbol stored in the flags array (be sure that the type of the symbol is 'L' for line number).
c)Insert the memory location from member location into the instruction with the unresolved reference (remember that an instruction containing an unresolved reference has operand 00).
d)Repeat steps (a), (b) and (c) until the end of the flags array is reached.
After the resolution process is complete, the entire array containing the SML code is output to a disk file with one SML instruction per line. This file can be read by the Simpletron for execution (after the simulator is modified to read its input from a file). Compiling your first Simple program into an SML file and executing that file should give you a real sense of personal accomplishment.
A Complete Example
The following example illustrates complete conversion of a Simple program to SML as it will be performed by the Simple compiler. Consider a Simple program that inputs an integer and sums the values from 1 to that integer. The program and the SML instructions produced by the first pass of the Simple compiler are illustrated in Fig. 19.27. The symbol table constructed by the first pass is shown in Fig. 19.28. .
|
|
|
SML location |
|
|
Simple program |
and instruction |
Description |
|||
|
|
|
|||
5 rem sum 1 to x |
none |
rem ignored |
|||
10 |
input x |
00 |
+1099 |
read x into location 99 |
|
15 |
rem check y == x |
none |
rem ignored |
||
20 |
if y == x goto 60 |
01 |
+2098 |
load y (98) into accumulator |
|
|
|
|
02 |
+3199 |
sub x (99) from accumulator |
|
|
|
03 |
+4200 |
branch zero to unresolved location |
25 |
rem |
increment y |
none |
rem ignored |
|
30 |
let y = y + 1 |
04 |
+2098 |
load y into accumulator |
|
|
|
|
05 |
+3097 |
add 1 (97) to accumulator |
|
|
|
06 |
+2196 |
store in temporary location 96 |
|
|
|
07 |
+2096 |
load from temporary location 96 |
|
|
|
08 |
+2198 |
store accumulator in y |
35 |
rem |
add y to total |
none |
rem ignored |
Fig. 19.27 SML instructions produced after the compiler’s first pass (part 1 of 2).
Chapter 19 |
|
|
|
Data Structures |
1139 |
|
|
|
|
|
|
|
|
|
|
|
SML location |
|
|
|
Simple program |
and instruction |
Description |
|
|||
|
|
|
|
|
|
|
40 |
let t = t + y |
09 |
+2095 |
load t (95) into accumulator |
|
|
|
|
|
10 |
+3098 |
add y to accumulator |
|
|
|
|
11 |
+2194 |
store in temporary location 94 |
|
|
|
|
12 |
+2094 |
load from temporary location 94 |
|
|
|
|
13 |
+2195 |
store accumulator in t |
|
45 |
rem |
loop y |
none |
rem ignored |
|
|
50 |
goto 20 |
14 |
+4001 |
branch to location 01 |
|
|
55 |
rem |
output result |
none |
rem ignored |
|
|
60 |
print t |
15 |
+1195 |
output t to screen |
|
|
99 |
end |
|
16 |
+4300 |
terminate execution |
|
Fig. 19.27 SML instructions produced after the compiler’s first pass (part 2 of 2).
Symbol |
Type |
Location |
|
|
|
5 |
L |
00 |
10 |
L |
00 |
'x' |
V |
99 |
15 |
L |
01 |
20 |
L |
01 |
'y' |
V |
98 |
25 |
L |
04 |
30 |
L |
04 |
1 |
C |
97 |
35 |
L |
09 |
40 |
L |
09 |
't' |
V |
95 |
45 |
L |
14 |
50 |
L |
14 |
55 |
L |
15 |
60 |
L |
15 |
99 |
L |
16 |
Fig. 19.28 Symbol table for program of Fig. 19.27.
Most Simple statements convert directly to single SML instructions. The exceptions in this program are remarks, the if/goto statement in line 20 and the let statements. Remarks do not translate into machine language. However, the line number for a remark is placed in the symbol table in
1140 |
Data Structures |
Chapter 19 |
case the line number is referenced in a goto statement or an if/goto statement. Line 20 of the program specifies that, if the condition y == x is true, program control is transferred to line 60. Since line 60 appears later in the program, the first pass of the compiler has not as yet placed 60 in the symbol table. (Statement line numbers are placed in the symbol table only when they appear as the first token in a statement.) Therefore, it is not possible at this time to determine the operand of the SML branch zero instruction at location 03 in the array of SML instructions. The compiler places 60 in location 03 of the flags array to indicate that the second pass completes this instruction.
We must keep track of the next instruction location in the SML array because there is not a one- to-one correspondence between Simple statements and SML instructions. For example, the if/goto statement of line 20 compiles into three SML instructions. Each time an instruction is produced, we must increment the instruction counter to the next location in the SML array. Note that the size of Simpletron’s memory could present a problem for Simple programs with many statements, variables and constants. It is conceivable that the compiler will run out of memory. To test for this case, your program should contain a data counter to keep track of the location at which the next variable or constant will be stored in the SML array. If the value of the instruction counter is larger than the value of the data counter, the SML array is full. In this case, the compilation process should terminate, and the compiler should print an error message indicating that it ran out of memory during compilation. This serves to emphasize that, although the programmer is freed from the burdens of managing memory by the compiler, the compiler itself must carefully determine the placement of instructions and data in memory and must check for such errors as memory being exhausted during the compilation process.
A Step-by-Step View of the Compilation Process
Let us now walk through the compilation process for the Simple program in Fig. 19.27. The compiler reads the first line of the program
5 rem sum 1 to x
into memory. The first token in the statement (the line number) is determined using the StreamTokenizer class (see Chapter 10 for a discussion of Java’s string manipulation methods). The token returned by the StreamTokenizer is converted to an integer by using static method Integer.parseInt(), so the symbol 5 can be located in the symbol table. If the symbol is not found, it is inserted in the symbol table.
We are at the beginning of the program and this is the first line, and no symbols are in the table yet. Therefore, 5 is inserted into the symbol table as type L (line number) and assigned the first location in SML array (00). Although this line is a remark, a space in the symbol table is still allocated for the line number (in case it is referenced by a goto or an if/goto). No SML instruction is generated for a rem statement, so the instruction counter is not incremented.
10 input x
is tokenized next. The line number 10 is placed in the symbol table as type L and assigned the first location in the SML array (00 because a remark began the program, so the instruction counter is currently 00). The command input indicates that the next token is a variable (only a variable can appear in an input statement). input corresponds directly to an SML operation code; therefore, the compiler simply has to determine the location of x in the SML array. Symbol x is not found in the symbol table. So, it is inserted into the symbol table as the Unicode representation of x, given type V and assigned location 99 in the SML array (data storage begins at 99 and is allocated backwards). SML code can now be generated for this statement. Operation code 10 (the SML read operation code) is multiplied by 100, and the location of x (as determined in the symbol table) is added to complete the instruction. The instruction is then stored in the SML array at location 00. The instruction counter is incremented by one, because a single SML instruction was produced.
Chapter 19 |
Data Structures |
1141 |
The statement |
|
|
15 rem |
check y == x |
|
is tokenized next. The symbol table is searched for line number 15 (which is not found). The line number is inserted as type L and assigned the next location in the array, 01. (Remember that rem statements do not produce code, so the instruction counter is not incremented.)
The statement
20 if y == x goto 60
is tokenized next. Line number 20 is inserted in the symbol table and given type L at the next location in the SML array 01. The command if indicates that a condition is to be evaluated. The variable y is not found in the symbol table, so it is inserted and given the type V and the SML location 98. Next, SML instructions are generated to evaluate the condition. There is no direct equivalent in SML for the if/goto; it must be simulated by performing a calculation using x and y and branching according to the result. If y is equal to x, the result of subtracting x from y is zero, so the branch zero instruction can be used with the result of the calculation to simulate the if/goto statement. The first step requires that y be loaded (from SML location 98) into the accumulator. This produces the instruction 01 +2098. Next, x is subtracted from the accumulator. This produces the instruction 02 +3199. The value in the accumulator may be zero, positive or negative. The operator is ==, so we want to branch zero. First, the symbol table is searched for the branch location (60 in this case), which is not found. So, 60 is placed in the flags array at location 03, and the instruction 03 +4200 is generated. (We cannot add the branch location because we have not yet assigned a location to line 60 in the SML array.) The instruction counter is incremented to 04.
The compiler proceeds to the statement
25 rem increment y
The line number 25 is inserted in the symbol table as type L and assigned SML location 04. The instruction counter is not incremented.
When the statement
30 let y = y + 1
is tokenized, the line number 30 is inserted in the symbol table as type L and assigned SML location 04. Command let indicates that the line is an assignment statement. First, all the symbols on the line are inserted in the symbol table (if they are not already there). The integer 1 is added to the symbol table as type C and assigned SML location 97. Next, the right side of the assignment is converted from infix to postfix notation. Then the postfix expression (y 1 +) is evaluated. Symbol y is located in the symbol table, and its corresponding memory location is pushed onto the stack. Symbol 1 is also located in the symbol table, and its corresponding memory location is pushed onto the stack. When the operator + is encountered, the postfix evaluator pops the stack into the right operand of the operator and pops the stack again into the left operand of the operator, then produces the SML instructions
04 |
+2098 |
(load y) |
05 |
+3097 |
(add 1) |
The result of the expression is stored in a temporary location in memory (96) with instruction
06 +2196 (store temporary)
and the temporary location is pushed onto the stack. Now that the expression has been evaluated, the result must be stored in y (i.e., the variable on the left side of =). So, the temporary location is loaded into the accumulator and the accumulator is stored in y with the instructions
1142 |
Data Structures |
Chapter 19 |
||
|
07 |
+2096 |
(load temporary) |
|
|
08 |
+2198 |
(store y) |
|
The reader will immediately notice that SML instructions appear to be redundant. We will discuss this issue shortly.
When the statement
35 rem add y to total
is tokenized, line number 35 is inserted in the symbol table as type L and assigned location 09. The statement
40 let t = t + y
is similar to line 30. The variable t is inserted in the symbol table as type V and assigned SML location 95. The instructions follow the same logic and format as line 30, and the instructions 09 +2095, 10 +3098, 11 +2194, 12 +2094 and 13 +2195 are generated. Note that the result of t + y is assigned to temporary location 94 before being assigned to t (95). Once again, the reader will note that the instructions in memory locations 11 and 12 appear to be redundant. Again, we will discuss this shortly.
The statement
45 rem loop y
is a remark, so line 45 is added to the symbol table as type L and assigned SML location 14.
The statement
50 goto 20
transfers control to line 20. Line number 50 is inserted in the symbol table as type L and assigned SML location 14. The equivalent of goto in SML is the unconditional branch (40) instruction that transfers control to a specific SML location. The compiler searches the symbol table for line 20 and finds that it corresponds to SML location 01. The operation code (40) is multiplied by 100, and location 01 is added to it to produce the instruction 14 +4001.
The statement
55 rem output result
is a remark, so line 55 is inserted in the symbol table as type L and assigned SML location 15.
The statement
60 print t
is an output statement. Line number 60 is inserted in the symbol table as type L and assigned SML location 15. The equivalent of print in SML is operation code 11 (write). The location of t is determined from the symbol table and added to the result of the operation code multiplied by 100.
The statement
99 end
is the final line of the program. Line number 99 is stored in the symbol table as type L and assigned SML location 16. The end command produces the SML instruction +4300 (43 is halt in SML), which is written as the final instruction in the SML memory array.
This completes the first pass of the compiler. We now consider the second pass. The flags array is searched for values other than -1. Location 03 contains 60, so the compiler knows that instruction 03 is incomplete. The compiler completes the instruction by searching the symbol
Chapter 19 |
Data Structures |
1143 |
table for 60, determining its location and adding the location to the incomplete instruction. In this case, the search determines that line 60 corresponds to SML location 15, so the completed instruction 03 +4215 is produced, replacing 03 +4200. The Simple program has now been compiled successfully.
To build the compiler, you will have to perform each of the following tasks:
a)Modify the Simpletron simulator program you wrote in Exercise 7.43 to take its input from a file specified by the user (see Chapter 16). The simulator should output its results to a disk file in the same format as the screen output. Convert the simulator to be an ob- ject-oriented program. In particular, make each part of the hardware an object. Arrange the instruction types into a class hierarchy using inheritance. Then execute the program polymorphically simply by telling each instruction to execute itself with an executeInstruction message.
b)Modify the infix-to-postfix evaluation algorithm of Exercise 19.12 to process multidigit integer operands and single-letter variable name operands. (Hint: Class StreamTokenizer can be used to locate each constant and variable in an expression, and constants can be converted from strings to integers by using Integer class method parseInt.) [Note: The data representation of the postfix expression must be altered to support variable names and integer constants.]
c)Modify the postfix evaluation algorithm to process multidigit integer operands and variable name operands. Also, the algorithm should now implement the “hook” discussed earlier so that SML instructions are produced rather than directly evaluating the expression. (Hint: Class StreamTokenizer can be used to locate each constant and variable in an expression, and constants can be converted from strings to integers by using Integer class method parseInt.) [Note: The data representation of the postfix expression must be altered to support variable names and integer constants.]
d)Build the compiler. Incorporate parts b) and c) for evaluating expressions in let statements. Your program should contain a method that performs the first pass of the compiler and a method that performs the second pass of the compiler. Both methods can call other methods to accomplish their tasks. Make your compiler as object oriented as possible.
19.28(Optimizing the Simple Compiler) When a program is compiled and converted into SML, a set of instructions is generated. Certain combinations of instructions often repeat themselves, usually in triplets called productions. A production normally consists of three instructions, such as load, add and store. For example, Fig. 19.29 illustrates five of the SML instructions that were produced in the compilation of the program in Fig. 19.27. The first three instructions are the production that adds 1 to y. Note that instructions 06 and 07 store the accumulator value in temporary location 96, then load the value back into the accumulator so instruction 08 can store the value in location 98. Often a production is followed by a load instruction for the same location that was just stored. This code can be optimized by eliminating the store instruction and the subsequent load instruction that operate on the same memory location, thus enabling the Simpletron to execute the program faster. Figure 19.30 illustrates the optimized SML for the program of Fig. 19.27. Note that there are four fewer instructions in the optimized code—a memory-space savings of 25%.
1 |
04 |
+2098 |
(load) |
2 |
05 |
+3097 |
(add) |
3 |
06 |
+2196 |
(store) |
4 |
07 |
+2096 |
(load) |
5 |
08 |
+2198 |
(store) |
|
|
||
Fig. 19.29 |
Unoptimized code from the program of Fig. 19.25. |
1144 |
Data Structures |
|
|
Chapter 19 |
|
|
|
|
|
|
|
|
|
|
SML location |
|
|
Simple program |
and instruction |
Description |
|||
|
|
|
|
||
5 rem sum 1 to x |
|
none |
rem ignored |
||
10 |
input |
x |
00 |
+1099 |
read x into location 99 |
15 |
rem |
check y == x |
|
none |
rem ignored |
20 |
if y == x goto 60 |
01 |
+2098 |
load y (98) into accumulator |
|
|
|
|
02 |
+3199 |
sub x (99) from accumulator |
|
|
|
03 |
+4211 |
branch to location 11 if zero |
25 |
rem |
increment y |
|
none |
rem ignored |
30 |
let y = y + 1 |
04 |
+2098 |
load y into accumulator |
|
|
|
|
05 |
+3097 |
add 1 (97) to accumulator |
|
|
|
06 |
+2198 |
store accumulator in y (98) |
35 |
rem |
add y to total |
|
none |
rem ignored |
40 |
let t = t + y |
07 |
+2096 |
load t from location (96 ) |
|
|
|
|
08 |
+3098 |
add y (98) accumulator |
|
|
|
09 |
+2196 |
store accumulator in t (96) |
45 |
rem |
loop y |
|
none |
rem ignored |
50 |
goto 20 |
10 |
+4001 |
branch to location 01 |
|
55 |
rem |
output result |
|
none |
rem ignored |
60 |
print t |
11 |
+1196 |
output t (96) to screen |
|
99 |
end |
|
12 |
+4300 |
terminate execution |
Fig. 19.30 Optimized code for the program of Fig. 19.27.
19.29 (Modifications to the Simple Compiler) Perform the following modifications to the Simple compiler. Some of these modifications might also require modifications to the Simpletron simulator program written in Exercise 7.43.
a)Allow the modulus operator (%) to be used in let statements. Simpletron Machine Language must be modified to include a modulus instruction.
b)Allow exponentiation in a let statement using ^ as the exponentiation operator. Simpletron Machine Language must be modified to include an exponentiation instruction.
c)Allow the compiler to recognize uppercase and lowercase letters in Simple statements (e.g., 'A' is equivalent to 'a'). No modifications to the Simpletron simulator are required.
d)Allow input statements to read values for multiple variables such as input x, y. No modifications to the Simpletron simulator are required to perform this enhancement to the Simple compiler.
e)Allow the compiler to output multiple values from a single print statement, such as print a, b, c. No modifications to the Simpletron simulator are required to perform this enhancement.
f)Add syntax-checking capabilities to the compiler so error messages are output when syntax errors are encountered in a Simple program. No modifications to the Simpletron simulator are required.
Chapter 19 |
Data Structures |
1145 |
g)Allow arrays of integers. No modifications to the Simpletron simulator are required to perform this enhancement.
h)Allow subroutines specified by the Simple commands gosub and return. Command gosub passes program control to a subroutine and command return passes control back to the statement after the gosub. This is similar to a method call in Java. The same subroutine can be called from many gosub commands distributed throughout a program. No modifications to the Simpletron simulator are required.
i)Allow repetition structures of the form
for x = 2 to 10 step 2
Simple statements next
This for statement loops from 2 to 10 with an increment of 2. The next line marks the end of the body of the for line. No modifications to the Simpletron simulator are required.
j) Allow repetition structures of the form
for x = 2 to 10
Simple statements next
This for statement loops from 2 to 10 with a default increment of 1. No modifications to the Simpletron simulator are required.
k)Allow the compiler to process string input and output. This requires the Simpletron simulator to be modified to process and store string values. [Hint: Each Simpletron word (i.e., memory location) can be divided into two groups, each holding a two-digit integer. Each two-digit integer represents the Unicode decimal equivalent of a character. Add a machine-language instruction that will print a string beginning at a certain Simpletron memory location. The first half of the Simpletron word at that location is a count of the number of characters in the string (i.e., the length of the string). Each succeeding half word contains one Unicode character expressed as two decimal digits. The machine language instruction checks the length and prints the string by translating each two-digit number into its equivalent character.]
l)Allow the compiler to process floating-point values in addition to integers. The Simpletron Simulator must also be modified to process floating-point values.
19.30(A Simple Interpreter) An interpreter is a program that reads a high-level language program statement, determines the operation to be performed by the statement and executes the operation immediately. The high-level language program is not converted into machine language first. Interpreters execute more slowly than compilers do, because each statement encountered in the program being interpreted must first be deciphered at execution time. If statements are contained in a loop, the statements are deciphered each time they are encountered in the loop. Early versions of the Basic programming language were implemented as interpreters. Most Java programs are run interpretively.
Write an interpreter for the Simple language discussed in Exercise 19.26. The program should use the infix-to-postfix converter developed in Exercise 19.12 and the postfix evaluator developed in Exercise 19.13 to evaluate expressions in a let statement. The same restrictions placed on the Simple language in Exercise 19.26 should be adhered to in this program. Test the interpreter with the Simple programs written in Exercise 19.26. Compare the results of running these programs in the interpreter with the results of compiling the Simple programs and running them in the Simpletron simulator built in Exercise 7.43.
19.31 (Insert/Delete Anywhere in a Linked List) Our linked-list class allowed insertions and deletions at only the front and the back of the linked list. These capabilities were convenient for us when we used inheritance or composition to produce a stack class and a queue class with a minimal amount
1146 |
Data Structures |
Chapter 19 |
of code simply by reusing the list class. Linked lists are normally more general than those we provided. Modify the linked-list class we developed in this chapter to handle insertions and deletions anywhere in the list.
19.32 (Lists and Queues without Tail References) Our implementation of a linked list (Fig. 19.3) used both a firstNode and a lastNode. The lastNode was useful for the insertAtBack and removeFromBack methods of the List class. The insertAtBack method corresponds to the enqueue method of the Queue class.
Rewrite the List class so that it does not use a lastNode. Thus, any operations on the tail of a list must begin searching the list from the front. Does this affect our implementation of the Queue class (Fig. 19.13)?
19.33(Performance of Binary Tree Sorting and Searching) One problem with the binary tree sort is that the order in which the data is inserted affects the shape of the tree—for the same collection of data, different orderings can yield binary trees of dramatically different shapes. The performance of the binary tree sorting and searching algorithms is sensitive to the shape of the binary tree. What shape would a binary tree have if its data were inserted in increasing order? in decreasing order? What shape should the tree have to achieve maximal searching performance?
19.34(Indexed Lists) As presented in the text, linked lists must be searched sequentially. For large lists, this can result in poor performance. A common technique for improving list-searching performance is to create and maintain an index to the list. An index is a set of references to key places in the list. For example, an application that searches a large list of names could improve performance by creating an index with 26 entries—one for each letter of the alphabet. A search operation for a last name beginning with ‘Y’ would then first search the index to determine where the ‘Y’ entries begin, then “jump into” the list at that point and search linearly until the desired name is found. This would be much faster than searching the linked list from the beginning. Use the List class of Fig. 19.3 as the basis of an IndexedList class.
Write a program that demonstrates the operation of indexed lists. Be sure to include methods insertInIndexedList, searchIndexedList and deleteFromIndexedList.
20
Java Utilities Package
and
Bit Manipulation
Objectives
•To understand containers, such as classes Vector and Stack, and the Enumeration interface.
•To be able to create Hashtable objects and persistent hash tables called Properties objects.
•To understand random number generation with instances of class Random.
•To use bit manipulation and BitSet objects.
Nothing can have value without being an object of utility.
Karl Marx
I’ve been in Who’s Who, and I know what’s what, but this is the first time I ever made the dictionary.
Mae West
O! many a shaft at sent
Finds mark the archer little meant!
Sir Walter Scott
There was the Door to which I found no Key;
There was the Veil through which I might not see.
Edward FitzGerald, The Rubáiyát of Omar Khayyám, st. 32
“It's a poor sort of memory that only works backwards,” the Queen remarked.
Lewis Carroll [Charles Lutwidge Dodgson]
Not by age but by capacity is wisdom acquired.
Titus Maccius Plautus, Trinummus, act II, sc. ii, l.88
1148 |
Java Utilities Package and Bit Manipulation |
Chapter 20 |
Outline
20.1Introduction
20.2Vector Class and Enumeration Interface
20.3Stack Class
20.4Dictionary Class
20.5Hashtable Class
20.6Properties Class
20.7Random Class
20.8Bit Manipulation and the Bitwise Operators
20.9BitSet Class
Summary • Terminology • Self-Review Exercises • Answers to Self-Review Exercises • Exercises
20.1 Introduction
This chapter discusses several utility classes and interfaces in package java.util, including class Vector, interface Enumeration, class Stack, class Dictionary, class Hashtable, class Properties, class Random and class BitSet.
Programs use class Vector to create array-like objects that can grow and shrink dynamically as a program’s data storage requirements change. We consider interface Enumeration, which enables a program to iterate through the elements of a container such as a Vector.
Class Stack offers conventional stack operations push and pop, as well as others we did not consider in Chapter 19.
Class Dictionary is an abstract class that provides a framework for storing keyed data in tables and retrieving that data. The chapter explains the theory of “hashing,” a technique for rapidly storing and retrieving information from tables, and demonstrates the construction and manipulation of hash tables with Java’s Hashtable class. Also, the chapter considers class Properties, which provides support for persistent hash tables— hash tables that can be written to a file with an output stream and read from a file with an input stream.
Class Random provides a richer collection of random number capabilities than is available with static method random of class Math.
The chapter presents an extensive discussion of bit manipulation operators, followed by a discussion of class BitSet that enables the creation of bit-array-like objects for setting and getting individual bit values.
Chapter 21, Collections, introduces a framework for manipulating groups of objects called collections. Objects of type Vector, Stack and Hashtable are collections.
20.2 Vector Class and Enumeration Interface
In most programming languages, including Java, conventional arrays are fixed in size— they cannot grow or shrink in response to an application’s changing storage requirements. Java class Vector provides the capabilities of array-like data structures that can resize themselves dynamically.
Chapter 20 |
Java Utilities Package and Bit Manipulation |
1149 |
At any time, the Vector contains a certain number of elements less than or equal to its capacity. The capacity is the space that has been reserved for the Vector’s elements. If a Vector requires additional capacity, it grows by a capacity increment that you specify or by a default assumed by the system. If you do not specify a capacity increment, the system will double the size of the Vector each time additional capacity is needed.
Performance Tip 20.1
Inserting additional elements into a Vector whose current size is less than its capacity is a relatively fast operation.
Performance Tip 20.2
It is a relatively slow operation to insert an element into a Vector that needs to grow larger to accommodate the new element.
Performance Tip 20.3
The default capacity increment doubles the size of the Vector. This may seem a waste of storage, but it is actually an efficient way for many Vectors to grow quickly to be “about the right size.” This is much more efficient time-wise than growing the Vector each time by only as much space as it takes to hold a single element. This disadvantage is that the Vector might occupy more space than it requires.
Performance Tip 20.4
If storage is at a premium, use Vector method trimToSize to trim a Vector’s capacity to the Vector’s exact size. This optimizes a Vector’s use of storage. However, adding another element to the Vector will force the Vector to grow dynamically—trimming leaves no room for growth.
Vectors store references to Objects. Thus, a program can store references to any objects in a Vector. To store values of primitive data types in Vectors, use the typewrapper classes (e.g., Integer, Long, Float) from package java.lang to create objects containing the primitive data type values.
Figure 20.1 demonstrates class Vector and several of its methods. The program provides a JButton that enables the user to test each of the methods. The user can type a String into the provided JTextField, and then press a button to see what the method does. Each operation displays a message in a JLabel to indicate the results of the operation.
1// Fig. 20.1: VectorTest.java
2 // Testing the Vector class of the java.util package
3
4 // Java core packages
5 import java.util.*;
6import java.awt.*;
7 import java.awt.event.*;
8
9 // Java extension packages
10 import javax.swing.*;
11
12public class VectorTest extends JFrame {
13private JLabel statusLabel;
Fig. 20.1 Demonstrating class Vector of package java.util (part 1 of 6).
1150 |
Java Utilities Package and Bit Manipulation |
Chapter 20 |
14private Vector vector;
15private JTextField inputField;
17// set up GUI to test Vector methods
18public VectorTest()
19{
20super( "Vector Example" );
21
22Container container = getContentPane();
23container.setLayout( new FlowLayout() );
25statusLabel = new JLabel();
26vector = new Vector( 1 );
28 container.add( new JLabel( "Enter a string" ) );
29
30inputField = new JTextField( 10 );
31container.add( inputField );
32
33// button to add element to vector
34JButton addButton = new JButton( "Add" );
36 |
addButton.addActionListener( |
37 |
|
38 |
new ActionListener() { |
39 |
|
40 |
public void actionPerformed( ActionEvent event ) |
41 |
{ |
42 |
// add an element to vector |
43 |
vector.addElement( inputField.getText() ); |
44 |
statusLabel.setText( "Added to end: " + |
45 |
inputField.getText() ); |
46 |
inputField.setText( "" ); |
47 |
} |
48}
49); // end call to addActionListener
51 container.add( addButton );
52
53// button to remove element from vector
54JButton removeButton = new JButton( "Remove" );
56 |
removeButton.addActionListener( |
57 |
|
58 |
new ActionListener() { |
59 |
|
60 |
public void actionPerformed( ActionEvent event ) |
61 |
{ |
62 |
// remove element from vector |
63 |
if ( vector.removeElement( inputField.getText() ) ) |
64 |
statusLabel.setText( "Removed: " + |
65 |
inputField.getText() ); |
|
|
Fig. 20.1 Demonstrating class Vector of package java.util (part 2 of 6).
Chapter 20 |
Java Utilities Package and Bit Manipulation |
1151 |
|
|
|
66 |
else |
|
67 |
statusLabel.setText( inputField.getText() + |
|
68 |
" not in vector" ); |
|
69 |
} |
|
70}
71); // end call to addActionListener
73 container.add( removeButton );
74
75// button to get first element of vector
76JButton firstButton = new JButton( "First" );
78 |
firstButton.addActionListener( |
79 |
|
80 |
new ActionListener() { |
81 |
|
82 |
public void actionPerformed( ActionEvent event ) |
83 |
{ |
84 |
// return first element of vector |
85 |
try { |
86 |
statusLabel.setText( |
87 |
"First element: " + vector.firstElement() ); |
88 |
} |
89 |
|
90 |
// catch exception if Vector empty |
91 |
catch ( NoSuchElementException exception ) { |
92 |
statusLabel.setText( exception.toString() ); |
93 |
} |
94 |
} |
95}
96); // end call to addActionListener
98 container.add( firstButton );
99
100// button to get last element of vector
101JButton lastButton = new JButton( "Last" );
103 |
lastButton.addActionListener( |
104 |
|
105 |
new ActionListener() { |
106 |
|
107 |
public void actionPerformed( ActionEvent event ) |
108 |
{ |
109 |
// return last element of vector |
110 |
try { |
111 |
statusLabel.setText( |
112 |
"Last element: " + vector.lastElement() ); |
113 |
} |
114 |
|
115 |
// catch exception if Vector empty |
116 |
catch ( NoSuchElementException exception ) { |
117 |
statusLabel.setText( exception.toString() ); |
118 |
} |
Fig. 20.1 Demonstrating class Vector of package java.util (part 3 of 6).
1152 |
Java Utilities Package and Bit Manipulation |
Chapter 20 |
|
|
|
119 |
} |
|
120}
121); // end call to addActionListener
123 container.add( lastButton );
124
125// button to determine whether vector is empty
126JButton emptyButton = new JButton( "Is Empty?" );
128 |
emptyButton.addActionListener( |
129 |
|
130 |
new ActionListener() { |
131 |
|
132 |
public void actionPerformed( ActionEvent event ) |
133 |
{ |
134 |
// determine if Vector is empty |
135 |
statusLabel.setText( vector.isEmpty() ? |
136 |
"Vector is empty" : "Vector is not empty" ); |
137 |
} |
138}
139); // end call to addActionListener
141 container.add( emptyButton );
142
143// button to determine whether vector contains search key
144JButton containsButton = new JButton( "Contains" );
145 |
|
146 |
containsButton.addActionListener( |
147 |
|
148 |
new ActionListener() { |
149 |
|
150 |
public void actionPerformed( ActionEvent event ) |
151 |
{ |
152 |
String searchKey = inputField.getText(); |
153 |
|
154 |
// determine if Vector contains searchKey |
155 |
if ( vector.contains( searchKey ) ) |
156 |
statusLabel.setText( |
157 |
"Vector contains " + searchKey ); |
158 |
else |
159 |
statusLabel.setText( |
160 |
"Vector does not contain " + searchKey ); |
161 |
} |
162}
163); // end call to addActionListener
165 container.add( containsButton );
166
167// button to determine location of value in vector
168JButton locationButton = new JButton( "Location" );
170 |
locationButton.addActionListener( |
171 |
|
Fig. 20.1 Demonstrating class Vector of package java.util (part 4 of 6).
Chapter 20 |
Java Utilities Package and Bit Manipulation |
1153 |
|
|
|
172 |
new ActionListener() { |
|
173 |
|
|
174 |
public void actionPerformed( ActionEvent event ) |
|
175 |
{ |
|
176 |
// get location of an object in Vector |
|
177 |
statusLabel.setText( "Element is at location " + |
|
178 |
vector.indexOf( inputField.getText() ) ); |
|
179 |
} |
|
180}
181); // end call to addActionListener
183 container.add( locationButton );
184
185// button to trim vector size
186JButton trimButton = new JButton( "Trim" );
188 |
trimButton.addActionListener( |
189 |
|
190 |
new ActionListener() { |
191 |
|
192 |
public void actionPerformed( ActionEvent event ) |
193 |
{ |
194 |
// remove unoccupied elements to save memory |
195 |
vector.trimToSize(); |
196 |
statusLabel.setText( "Vector trimmed to size" ); |
197 |
} |
198}
199);
201 container.add( trimButton );
202
203// button to display vector size and capacity
204JButton statsButton = new JButton( "Statistics" );
206 |
statsButton.addActionListener( |
207 |
|
208 |
new ActionListener() { |
209 |
|
210 |
public void actionPerformed( ActionEvent event ) |
211 |
{ |
212 |
// get size and capacity of Vector |
213 |
statusLabel.setText( "Size = " + vector.size() + |
214 |
"; capacity = " + vector.capacity() ); |
215 |
} |
216}
217); // end call to addActionListener
219 container.add( statsButton );
220
221// button to display vector contents
222JButton displayButton = new JButton( "Display" );
224 displayButton.addActionListener(
Fig. 20.1 Demonstrating class Vector of package java.util (part 5 of 6).
1154 |
Java Utilities Package and Bit Manipulation |
Chapter 20 |
|
|
|
225 |
|
|
226 |
new ActionListener() { |
|
227 |
|
|
228 |
public void actionPerformed( ActionEvent event ) |
|
229 |
{ |
|
230 |
// use Enumeration to output Vector contents |
|
231 |
Enumeration enum = vector.elements(); |
|
232 |
StringBuffer buf = new StringBuffer(); |
|
233 |
|
|
234 |
while ( enum.hasMoreElements() ) |
|
235 |
buf.append( enum.nextElement() ).append( " " ); |
|
236 |
|
|
237 |
JOptionPane.showMessageDialog( null, |
|
238 |
buf.toString(), "Display", |
|
239 |
JOptionPane.PLAIN_MESSAGE ); |
|
240 |
} |
|
241}
242); // end call to addActionListener
244container.add( displayButton );
245container.add( statusLabel );
247setSize( 300, 200 );
248setVisible( true );
250 } // end VectorTest constructor
251
252// execute application
253public static void main( String args[] )
254{
255VectorTest application = new VectorTest();
257 application.setDefaultCloseOperation(
258JFrame.EXIT_ON_CLOSE );
259}
260
261 } // end class VectorTest
Fig. 20.1 Demonstrating class Vector of package java.util (part 6 of 6).
The application’s constructor creates a Vector (line 26) with an initial capacity of one element. This Vector will double in size each time it needs to grow to accommodate more elements. Class Vector provides three other constructors. The no-argument constructor creates an empty Vector with an initial capacity of 10 elements. The constructor that takes two
Chapter 20 |
Java Utilities Package and Bit Manipulation |
1155 |
arguments creates a Vector with an initial capacity specified by the first argument and a capacity increment specified by the second argument. Each time the Vector needs to grow, it will add space for the specified number of elements in the capacity increment. The constructor that takes a Collection allows creates a copy of a collection’s elements and stores them in the Vector. In Chapter 21, we discuss Collections.
Line 43 calls Vector method addElement to add its argument to the end of the Vector. If necessary, the Vector increases its capacity to accommodate the new element. Class Vector also provides method insertElementAt to insert an element at a specified position in the Vector and method setElementAt to set the element at a specific position in the Vector. Method insertElementAt makes room for the new element by shifting elements. Method setElementAt replaces the element at the specified position with its argument.
Line 63 calls Vector method removeElement to remove the first occurrence of its argument from the Vector. The method returns true if it finds the element in the Vector; otherwise, the method returns false. If the element is removed, all elements after that element in the Vector shift one position toward the beginning of the Vector to fill in the position of the removed element. Class Vector also provides method removeAllElements to remove every element from the Vector and method removeElementAt to remove the element at a specified index.
Line 87 calls Vector method firstElement to return a reference to the first element in the Vector. This method throws a NoSuchElementException if there are no elements currently in the Vector. Line 112 calls Vector method lastElement to return a reference to the last element in the Vector. This method throws a NoSuchElementException if there are no elements currently in the Vector.
Line 135 calls Vector method isEmpty to determine whether the Vector is empty. The method returns true if there are no elements in the Vector; otherwise, the method returns false.
Line 155 calls Vector method contains to determine whether the Vector contains the searchKey specified as an argument. Method contains returns true if searchKey is in the Vector; otherwise, the method returns false. Method contains uses Object method equals to determine whether the searchKey is equal to one of the Vector’s elements. Many classes override method equals to perform the comparisons in a manner specific to those classes. For example, class String defines equals to compare the individual characters in the two Strings being compared. If method equals is not overridden, the original version from class Object is used. This version performs comparisons using operator == to determine whether two references refer to the same object in memory.
Line 178 calls Vector method indexOf to determine the index of the first location in the Vector containing the argument. The method returns –1 if the argument is not found in the Vector. An overloaded version of this method takes a second argument specifying the index in the Vector at which the search should begin.
Performance Tip 20.5
Vector methods contains and indexOf perform linear searches of a Vector’s con- tents, which are inefficient for large Vectors. If a program frequently searches for elements in a collection, consider using a Hashtable (see Section 20.5) or one of the Java Collection API’s Map implementations (see Chapter 21).
1156 |
Java Utilities Package and Bit Manipulation |
Chapter 20 |
Line 195 calls Vector method trimToSize to reduce the capacity of the Vector to the current number of elements in the Vector (i.e., the Vector’s current size).
Lines 213–214 use Vector methods size and capacity to determine the number of elements currently in the Vector and the number of elements that can be stored in the Vector without allocating more memory, respectively.
Line 231 calls Vector method elements to return an Enumeration that enables the program to iterate through the Vector’s elements. An Enumeration provides two methods—hasMoreElements and nextElement. In line 234, method hasMoreElements returns true if there are more elements in the Vector. In line 235, method nextElement returns a reference to the next element in the Vector. If there are no more elements, method nextElement throws a NoSuchElementException.
For complete information on class Vector and its other methods, see the online Java API documentation.
20.3 Stack Class
In Chapter 19, Data Structures, we learned how to build such fundamental data structures as linked lists, stacks, queues and trees. In a world of software reuse, instead of building data structures as we need them, often we can take advantage of existing data structures. In this section, we investigate class Stack in the Java utilities package (java.util).
In Section 20.2, we discussed class Vector, which implements a dynamically resizable array. Class Stack extends class Vector to implement a stack data structure. As does Vector, class Stack stores references to Objects. To store primitive data types, use the appropriate type-wrapper class to create an object containing the primitive value (Boolean, Byte, Character, Short, Integer, Long, Float or Double). Figure 20.2 provides a GUI that enables the user to test each of the Stack methods.
1// Fig. 20.2: StackTest.java
2 // Testing the Stack class of the java.util package
3
4 // Java core packages
5import java.awt.*;
6 import java.awt.event.*;
7 import java.util.*;
8
9 // Java extension packages
10 import javax.swing.*;
11
12public class StackTest extends JFrame {
13private JLabel statusLabel;
14private JTextField inputField;
15private Stack stack;
16
17// create GUI to manipulate a Stack
18public StackTest()
19{
20super( "Stacks" );
21
Fig. 20.2 Demonstrating class Stack of package java.util (part 1 of 5).
Chapter 20 |
Java Utilities Package and Bit Manipulation |
1157 |
22 Container container = getContentPane();
23
24statusLabel = new JLabel();
25stack = new Stack();
26
27container.setLayout( new FlowLayout() );
28container.add( new JLabel( "Enter a string" ) );
29inputField = new JTextField( 10 );
30container.add( inputField );
31
32// button to place object on stack
33JButton pushButton = new JButton( "Push" );
35 |
pushButton.addActionListener( |
36 |
|
37 |
new ActionListener() { |
38 |
|
39 |
public void actionPerformed( ActionEvent event ) |
40 |
{ |
41 |
// put object on Stack |
42 |
statusLabel.setText( "Pushed: " + |
43 |
stack.push( inputField.getText() ) ); |
44 |
} |
45}
46);
48 container.add( pushButton );
49
50// button to remove top object on stack
51JButton popButton = new JButton( "Pop" );
53 |
popButton.addActionListener( |
54 |
|
55 |
new ActionListener() { |
56 |
|
57 |
public void actionPerformed( ActionEvent event ) |
58 |
{ |
59 |
// remove element from Stack |
60 |
try { |
61 |
statusLabel.setText( "Popped: " + stack.pop() ); |
62 |
} |
63 |
|
64 |
// process exception if Stack empty |
65 |
catch ( EmptyStackException exception ) { |
66 |
statusLabel.setText( exception.toString() ); |
67 |
} |
68 |
} |
69}
70);
72 container.add( popButton );
73
Fig. 20.2 Demonstrating class Stack of package java.util (part 2 of 5).
1158 |
Java Utilities Package and Bit Manipulation |
Chapter 20 |
74// button to look at top element of stack
75JButton peekButton = new JButton( "Peek" );
77 |
peekButton.addActionListener( |
78 |
|
79 |
new ActionListener() { |
80 |
|
81 |
public void actionPerformed( ActionEvent event ) |
82 |
{ |
83 |
// look at top object on Stack |
84 |
try { |
85 |
statusLabel.setText( "Top: " + stack.peek() ); |
86 |
} |
87 |
|
88 |
// process exception if Stack empty |
89 |
catch ( EmptyStackException exception ) { |
90 |
statusLabel.setText( exception.toString() ); |
91 |
} |
92 |
} |
93}
94);
96 container.add( peekButton );
97
98// button to determine whether stack is empty
99JButton emptyButton = new JButton( "Is Empty?" );
100
101 |
emptyButton.addActionListener( |
102 |
|
103 |
new ActionListener() { |
104 |
|
105 |
public void actionPerformed( ActionEvent event ) |
106 |
{ |
107 |
// determine if Stack is empty |
108 |
statusLabel.setText( stack.empty() ? |
109 |
"Stack is empty" : "Stack is not empty" ); |
110 |
} |
111}
112);
114 container.add( emptyButton );
115
116// button to determine whether search key is in stack
117JButton searchButton = new JButton( "Search" );
118 |
|
119 |
searchButton.addActionListener( |
120 |
|
121 |
new ActionListener() { |
122 |
|
123 |
public void actionPerformed( ActionEvent event ) |
124 |
{ |
125 |
// search Stack for specified object |
126 |
String searchKey = inputField.getText(); |
Fig. 20.2 Demonstrating class Stack of package java.util (part 3 of 5).
Chapter 20 |
Java Utilities Package and Bit Manipulation |
1159 |
|
|
|
127 |
int result = stack.search( searchKey ); |
|
128 |
|
|
129 |
if ( result == -1 ) |
|
130 |
statusLabel.setText( searchKey + " not found" ); |
|
131 |
else |
|
132 |
statusLabel.setText( searchKey + |
|
133 |
" found at element " + result ); |
|
134 |
} |
|
135}
136);
138 container.add( searchButton );
139
140// button to display stack contents
141JButton displayButton = new JButton( "Display" );
143 |
displayButton.addActionListener( |
144 |
|
145 |
new ActionListener() { |
146 |
|
147 |
public void actionPerformed( ActionEvent event ) |
148 |
{ |
149 |
// output Stack contents |
150 |
Enumeration enumeration = stack.elements(); |
151 |
StringBuffer buffer = new StringBuffer(); |
152 |
|
153 |
while ( enumeration.hasMoreElements() ) |
154 |
buffer.append( |
155 |
enumeration.nextElement() ).append( " " ); |
156 |
|
157 |
JOptionPane.showMessageDialog( null, |
158 |
buffer.toString(), "Display", |
159 |
JOptionPane.PLAIN_MESSAGE ); |
160 |
} |
161}
162);
164container.add( displayButton );
165container.add( statusLabel );
167setSize( 675, 100 );
168setVisible( true );
169}
170
171// execute application
172public static void main( String args[] )
173{
174StackTest application = new StackTest();
176 application.setDefaultCloseOperation(
177JFrame.EXIT_ON_CLOSE );
178}
179
Fig. 20.2 Demonstrating class Stack of package java.util (part 4 of 5).
1160 |
Java Utilities Package and Bit Manipulation |
Chapter 20 |
180 } // end class StackTest
Fig. 20.2 Demonstrating class Stack of package java.util (part 5 of 5).
Line 25 creates an empty Stack. Line 43 calls Stack method push to add its argument to the top of the stack. The method returns an Object reference to its argument.
Line 61 calls Stack method pop to remove the top element of the stack. The method returns an Object reference to the removed element. If there are no elements in the
Stack, method pop throws an EmptyStackException.
Line 85 calls Stack method peek to view the top element of the stack without removing the element. Method peek returns an Object reference to the element.
Line 108 calls Stack method empty to determine whether the stack is empty. If it is empty, the method returns true; otherwise, the method returns false.
Line 127 calls Stack method search to determine whether its argument is in the stack. If so, the method returns the position of the element in the stack. Note that the top element is position 1. If the element is not in the stack, –1 is returned.
The entire public interface of class Vector is actually part of class Stack, because Stack inherits from Vector. To prove this, our example provides a button to display the contents of the stack. This button invokes method elements to get an Enumeration of the stack; it then uses the Enumeration to walk through the stack elements.
Testing and Debugging Tip 20.1
Stack extends Vector, so the user may perform operations on Stack objects that are ordinarily not allowed on conventional stack data structures. This could "corrupt" the elements of the Stack and destroy the integrity of the Stack.
20.4 Dictionary Class
A Dictionary maps keys to values. When searching a Dictionary for a value, the program provides a key and the Dictionary returns the corresponding value. Dictionary is an abstract class. In particular, it is the superclass of class Hashtable, which we discuss in Section 20.5. Class Dictionary provides the public interface methods required to maintain a table of key–value pairs where the keys help store and retrieve the values in the table. Each key in the table is unique. The data structure is similar to a dictionary of words and definitions—the word is the key that is used to look up the definition (i.e., the value).
Dictionary method size returns the number of key–value pairs in a Dictionary object. Method isEmpty returns true if a Dictionary is empty, and false otherwise. Method keys returns an Enumeration that a program can use to iterate through a Dictionary’s keys. Method elements returns an Enumeration that a program can use to iterate through a Dictionary’s values. Method get returns the object that corresponds to a given key value. Method put puts an object associated with a given key into the table. Method remove removes an element corresponding to a given key and returns a reference to it.
Chapter 20 |
Java Utilities Package and Bit Manipulation |
1161 |
20.5 Hashtable Class
Object-oriented programming languages facilitate creating new data types. When a program creates objects of new or existing types, the program then needs to manage those objects efficiently. This includes storing and retrieving objects. Storing and retrieving information with arrays is efficient if some aspect of your data directly matches the key value and if those keys are unique and tightly packed. If you have 100 employees with 9- digit Social Security numbers and you want to store and retrieve employee data by using the Social Security number as a key, it would nominally require an array with 999,999,999 elements, because there are 999,999,999 unique 9-digit numbers. This is impractical for virtually all applications that use Social Security numbers as keys. If the program could have an array that large, the program could get high performance for both storing and retrieving employee records by simply using the Social Security number as the array index.
There are numerous applications that have this problem, namely, that either the keys are of the wrong type (i.e., not nonnegative integers), or they may be of the right type, but sparsely spread over a huge range.
What is needed is a high-speed scheme for converting keys such as Social Security numbers, inventory part numbers and the like into unique array subscripts. Then, when an application needs to store something, the scheme could convert the application key rapidly into a subscript and the record of information could be stored at that slot in the array. Retrieval is accomplished the same way: Once the application has a key for which it wants to retrieve the data record, the application simply applies the conversion to the key—this produces the array subscript where the data is stored and the data is retrieved.
The scheme we describe here is the basis of a technique called hashing. Why the name? When we convert a key into an array subscript, we literally scramble the bits, forming a kind of “mishmashed” number. The number actually has no real significance beyond its usefulness in storing and retrieving this particular number data record.
A glitch in the scheme occurs when collisions occur (i.e., when two different keys “hash into” the same cell (or element) in the array). We cannot store two different data records in the same space, so we need to find an alternative home for all records beyond the first that hash to a particular array subscript. There are many schemes for doing this. One is to “hash again” (i.e., to reapply the hashing transformation to the key to provide a next candidate cell in the array). The hashing process is designed to distribute the values throughout the table, so the assumption is that with just a few hashes an available cell will be found.
Another scheme uses one hash to locate the first candidate cell. If that cell is occupied, successive cells are searched linearly until an available cell is found. Retrieval works the same way: The key is hashed once to determine the initial location to check to see whether it contains the desired data. If it does, the search is finished. If it does not, successive cells are searched linearly until the desired data is found.
The most popular solution to hash table collisions is to have each cell of the table be a hash “bucket,” typically a linked list of all the key–value pairs that hash to that cell. This is the solution that Java’s Hashtable class (from package java.util) implements.
One factor that affects the performance of hashing schemes is called the load factor. This is the ratio of the number of occupied cells in the hash table to the size of the hash table. The closer this ratio gets to 1.0, the greater the chance of collisions.
1162 |
Java Utilities Package and Bit Manipulation |
Chapter 20 |
Performance Tip 20.6
The load factor in a hash table is a classic example of a space–time trade-off: By increasing the load factor, we get better memory utilization, but the program runs slower, due to increased hashing collisions. By decreasing the load factor, we get better program speed, because of reduced hashing collisions, but we get poorer memory utilization, because a larger portion of the hash table remains empty.
The complexity of programming hash tables properly is too much for most casual programmers. Computer science students study hashing schemes thoroughly in courses called “Data Structures” or “Algorithms.” Recognizing the value of hashing to most programmers, Java provides class Hashtable and some related features to enable programmers to use hashing without having to implement the messy details.
Actually, the preceding sentence is profoundly important in our study of object-ori- ented programming. As discussed in earlier chapters, classes encapsulate and hide complexity (i.e., implementation details) and offer user-friendly interfaces. Crafting classes to do this properly is one of the most valued skills in the field of object-oriented programming.
Figure 20.3 provides a GUI that enables you to test several Hashtable methods. Line 25 creates an empty Hashtable with a default capacity of 101 elements and a default load factor of .75. When the number of occupied slots in the Hashtable becomes more than the capacity times the load factor, the table grows larger. Class Hashtable also provides a constructor that takes one argument specifying the capacity and a constructor that takes two arguments, specifying the capacity and load factor, respectively.
1// Fig. 20.3: HashtableTest.java
2 // Demonstrates class Hashtable of the java.util package.
3
4 // Java core packages
5import java.awt.*;
6 import java.awt.event.*;
7 import java.util.*;
8
9 // Java extensions packages
10 import javax.swing.*;
11
12public class HashtableTest extends JFrame {
13private JLabel statusLabel;
14private Hashtable table;
15private JTextArea displayArea;
16private JTextField lastNameField;
17private JTextField firstNameField;
18
19// set up GUI to demonstrate Hashtable features
20public HashtableTest()
21{
22super( "Hashtable Example" );
23
24statusLabel = new JLabel();
25table = new Hashtable();
26displayArea = new JTextArea( 4, 20 );
Fig. 20.3 Demonstrating class Hashtable (part 1 of 6).
Chapter 20 |
Java Utilities Package and Bit Manipulation |
1163 |
27 displayArea.setEditable( false );
28
29 JPanel northSubPanel = new JPanel();
30
31northSubPanel.add( new JLabel( "First name" ) );
32firstNameField = new JTextField( 8 );
33northSubPanel.add( firstNameField );
34
35northSubPanel.add( new JLabel( "Last name (key)" ) );
36lastNameField = new JTextField( 8 );
37northSubPanel.add( lastNameField );
38
39JPanel northPanel = new JPanel();
40northPanel.setLayout( new BorderLayout() );
41northPanel.add( northSubPanel, BorderLayout.NORTH );
42northPanel.add( statusLabel, BorderLayout.SOUTH );
44JPanel southPanel = new JPanel();
45southPanel.setLayout( new GridLayout( 2, 5 ) );
46JButton putButton = new JButton( "Put" );
47 |
|
48 |
putButton.addActionListener( |
49 |
|
50 |
new ActionListener() { |
51 |
|
52 |
// add new key/value pair to hash table |
53 |
public void actionPerformed( ActionEvent event ) |
54 |
{ |
55 |
Employee employee = new Employee( |
56 |
firstNameField.getText(), |
57 |
lastNameField.getText() ); |
58 |
|
59 |
Object value = |
60 |
table.put( lastNameField.getText(), employee ); |
61 |
|
62 |
// first time this key was added |
63 |
if ( value == null ) |
64 |
statusLabel.setText( |
65 |
"Put: " + employee.toString() ); |
66 |
|
67 |
// replaced previous value for this key |
68 |
else |
69 |
statusLabel.setText( |
70 |
"Put: " + employee.toString() + |
71 |
"; Replaced: " + value.toString() ); |
72 |
} |
73}
74);
76 southPanel.add( putButton );
77
78// button to get value for specific key
79JButton getButton = new JButton( "Get" );
Fig. 20.3 Demonstrating class Hashtable (part 2 of 6).
1164 |
Java Utilities Package and Bit Manipulation |
Chapter 20 |
|
|
|
80 |
|
|
81 |
getButton.addActionListener( |
|
82 |
|
|
83 |
new ActionListener() { |
|
84 |
|
|
85 |
// get value for specific key |
|
86 |
public void actionPerformed( ActionEvent event ) |
|
87 |
{ |
|
88 |
Object value = table.get( lastNameField.getText() ); |
|
89 |
|
|
90 |
// value found for key |
|
91 |
if ( value != null ) |
|
92 |
statusLabel.setText( |
|
93 |
"Get: " + value.toString() ); |
|
94 |
|
|
95 |
// value not found for key |
|
96 |
else |
|
97 |
statusLabel.setText( |
|
98 |
"Get: " + lastNameField.getText() + |
|
99 |
" not in table" ); |
|
100 |
} |
|
101}
102);
104 southPanel.add( getButton );
105
106// button to remove key/value pair from table
107JButton removeButton = new JButton( "Remove" );
109 |
removeButton.addActionListener( |
110 |
|
111 |
new ActionListener() { |
112 |
|
113 |
// remove key/value pair |
114 |
public void actionPerformed( ActionEvent event ) |
115 |
{ |
116 |
Object value = |
117 |
table.remove( lastNameField.getText() ); |
118 |
|
119 |
// key found |
120 |
if ( value != null ) |
121 |
statusLabel.setText( "Remove: " + |
122 |
value.toString() ); |
123 |
|
124 |
// key not found |
125 |
else |
126 |
statusLabel.setText( "Remove: " + |
127 |
lastNameField.getText() + " not in table" ); |
128 |
} |
129}
130);
132 southPanel.add( removeButton );
Fig. 20.3 Demonstrating class Hashtable (part 3 of 6).
Chapter 20 |
Java Utilities Package and Bit Manipulation |
1165 |
133
134// button to detetmine whether hash table is empty
135JButton emptyButton = new JButton( "Empty" );
136 |
|
137 |
emptyButton.addActionListener( |
138 |
|
139 |
new ActionListener() { |
140 |
|
141 |
// determine whether hash table is empty |
142 |
public void actionPerformed( ActionEvent event ) |
143 |
{ |
144 |
statusLabel.setText( "Empty: " + table.isEmpty() ); |
145 |
} |
146}
147);
149 southPanel.add( emptyButton );
150
151// button to determine whether hash table contains key
152JButton containsKeyButton = new JButton( "Contains key" );
154 |
containsKeyButton.addActionListener( |
155 |
|
156 |
new ActionListener() { |
157 |
|
158 |
// determine whether hash table contains key |
159 |
public void actionPerformed( ActionEvent event ) |
160 |
{ |
161 |
statusLabel.setText( "Contains key: " + |
162 |
table.containsKey( lastNameField.getText() ) ); |
163 |
} |
164}
165);
167 southPanel.add( containsKeyButton );
168
169// button to clear all hash table contents
170JButton clearButton = new JButton( "Clear table" );
172 |
clearButton.addActionListener( |
173 |
|
174 |
new ActionListener() { |
175 |
|
176 |
// clear hash table contents |
177 |
public void actionPerformed( ActionEvent event ) |
178 |
{ |
179 |
table.clear(); |
180 |
statusLabel.setText( "Clear: Table is now empty" ); |
181 |
} |
182}
183);
185 southPanel.add( clearButton );
Fig. 20.3 Demonstrating class Hashtable (part 4 of 6).
1166 |
Java Utilities Package and Bit Manipulation |
Chapter 20 |
186
187// button to display hash table elements
188JButton listElementsButton = new JButton( "List objects" );
190 |
listElementsButton.addActionListener( |
191 |
|
192 |
new ActionListener() { |
193 |
|
194 |
// display hash table elements |
195 |
public void actionPerformed( ActionEvent event ) |
196 |
{ |
197 |
StringBuffer buffer = new StringBuffer(); |
198 |
|
199 |
for ( Enumeration enumeration = table.elements(); |
200 |
enumeration.hasMoreElements(); ) |
201 |
buffer.append( |
202 |
enumeration.nextElement() ).append( '\n' ); |
203 |
|
204 |
displayArea.setText( buffer.toString() ); |
205 |
} |
206}
207);
209 southPanel.add( listElementsButton );
210
211// button to display hash table keys
212JButton listKeysButton = new JButton( "List keys" );
214 |
listKeysButton.addActionListener( |
215 |
|
216 |
new ActionListener() { |
217 |
|
218 |
// display hash table KEYS |
219 |
public void actionPerformed( ActionEvent event ) |
220 |
{ |
221 |
StringBuffer buffer = new StringBuffer(); |
222 |
|
223 |
for ( Enumeration enumeration = table.keys(); |
224 |
enumeration.hasMoreElements(); ) |
225 |
buffer.append( |
226 |
enumeration.nextElement() ).append( '\n' ); |
227 |
|
228 |
JOptionPane.showMessageDialog( null, |
229 |
buffer.toString(), "Display", |
230 |
JOptionPane.PLAIN_MESSAGE ); |
231 |
} |
232}
233);
235 southPanel.add( listKeysButton );
236
237Container container = getContentPane();
238container.add( northPanel, BorderLayout.NORTH );
Fig. 20.3 Demonstrating class Hashtable (part 5 of 6).
Chapter 20 |
Java Utilities Package and Bit Manipulation |
1167 |
239 container.add( new JScrollPane( displayArea ),
240BorderLayout.CENTER );
241container.add( southPanel, BorderLayout.SOUTH );
243setSize( 540, 300 );
244setVisible( true );
245}
246
247// execute application
248public static void main( String args[] )
249{
250HashtableTest application = new HashtableTest();
252 application.setDefaultCloseOperation(
253JFrame.EXIT_ON_CLOSE );
254}
255
256 } // end class HashtableTest
257
258// Employee class to represent first and last name
259class Employee {
260private String first, last;
261
262// initialize an Employee
263public Employee( String firstName, String lastName )
264{
265first = firstName;
266last = lastName;
267}
268
269// convert Employee to String representation
270public String toString()
271{
272return first + " " + last;
273}
274
275 } // end class Employee
Fig. 20.3 Demonstrating class Hashtable (part 6 of 6).
1168 |
Java Utilities Package and Bit Manipulation |
Chapter 20 |
Lines 59–60 call Hashtable method put to add a key (the first argument) and a value (the second argument) into the Hashtable. Method put returns null if key has not been inserted in the Hashtable previously. Otherwise, method put returns the original value for that key in the Hashtable; this helps the program manage cases in which it intends to replace the value stored for a given key. If either the key or the value is null, a NullPointerException occurs.
Line 88 calls Hashtable method get to locate the value associated with the key specified as an argument. If the key is present in the table, get returns an Object reference to the corresponding value; otherwise, the method returns null.
Lines 116–117 call Hashtable method remove to remove a key–value pair from the table. The method returns a reference to the removed Object. If there is no value mapped to the specified key, the method returns null.
Line 144 calls Hashtable method isEmpty, which returns true if the Hashtable is empty; otherwise it returns false.
Line 162 calls Hashtable method containsKey to determine whether the key specified as an argument is in the Hashtable (i.e., a value is associated with that key). If so, the method returns true; otherwise, the method returns false. Class Hashtable also provides method contains to determine whether the Object specified as its argument is in the Hashtable.
Line 179 calls Hashtable method clear to empty the Hashtable contents. Line 199 calls Hashtable method elements to obtain an Enumeration of the values in the Hashtable. Line 223 calls Hashtable method keys to obtain an Enumeration of the keys in the Hashtable.
For more information on class Hashtable and its methods, see the online Java API documentation.
20.6 Properties Class
A Properties object is a persistent Hashtable object that normally stores key–value pairs of Strings—assuming that you use methods setProperty and getProperty to manipulate the table rather than Hashtable methods put and get. By persistent, we mean that the Hashtable object can be written to an output stream and directed to a file, and read back in through an input stream. In fact, most objects in Java can now be output and input with Java’s object serialization (see Chapter 16). The Properties class extends class Hashtable, so Properties objects have the methods we discussed in Fig. 20.3. The keys and values in a Properties object must be of type String. Class Properties provides some additional methods that are demonstrated in Fig. 20.4.
1// Fig. 20.4: PropertiesTest.java
2 // Demonstrates class Properties of the java.util package.
3
4 // Java core packages
5import java.awt.*;
6 import java.awt.event.*;
7import java.io.*;
8import java.util.*;
Fig. 20.4 Demonstrating class Properties (part 1 of 6).
Chapter 20 |
Java Utilities Package and Bit Manipulation |
1169 |
9
10// Java extension packages
11import javax.swing.*;
12
13public class PropertiesTest extends JFrame {
14private JLabel statusLabel;
15private Properties table;
16private JTextArea displayArea;
17private JTextField valueField, nameField;
19// set up GUI to test Properties table
20public PropertiesTest()
21{
22super( "Properties Test" );
23
24// create Properties table
25table = new Properties();
27 Container container = getContentPane();
28
29// set up NORTH of window's BorderLayout
30JPanel northSubPanel = new JPanel();
31
32northSubPanel.add( new JLabel( "Property value" ) );
33valueField = new JTextField( 10 );
34northSubPanel.add( valueField );
35
36northSubPanel.add( new JLabel( "Property name (key)" ) );
37nameField = new JTextField( 10 );
38northSubPanel.add( nameField );
39
40JPanel northPanel = new JPanel();
41northPanel.setLayout( new BorderLayout() );
42northPanel.add( northSubPanel, BorderLayout.NORTH );
44statusLabel = new JLabel();
45northPanel.add( statusLabel, BorderLayout.SOUTH );
47 container.add( northPanel, BorderLayout.NORTH );
48
49// set up CENTER of window's BorderLayout
50displayArea = new JTextArea( 4, 35 );
51container.add( new JScrollPane( displayArea ),
52 |
BorderLayout.CENTER ); |
53 |
|
54// set up SOUTH of window's BorderLayout
55JPanel southPanel = new JPanel();
56southPanel.setLayout( new GridLayout( 1, 5 ) );
58// button to put a name/value pair in Properties table
59JButton putButton = new JButton( "Put" );
60
Fig. 20.4 Demonstrating class Properties (part 2 of 6).
1170 |
Java Utilities Package and Bit Manipulation |
Chapter 20 |
|
|
|
61 |
putButton.addActionListener( |
|
62 |
|
|
63 |
new ActionListener() { |
|
64 |
|
|
65 |
// put name/value pair in Properties table |
|
66 |
public void actionPerformed( ActionEvent event ) |
|
67 |
{ |
|
68 |
Object value = table.setProperty( |
|
69 |
nameField.getText(), valueField.getText() ); |
|
70 |
|
|
71 |
if ( value == null ) |
|
72 |
showstatus( "Put: " + nameField.getText() + |
|
73 |
" " + valueField.getText() ); |
|
74 |
|
|
75 |
else |
|
76 |
showstatus( "Put: " + nameField.getText() + |
|
77 |
" " + valueField.getText() + |
|
78 |
"; Replaced: " + value.toString() ); |
|
79 |
|
|
80 |
listProperties(); |
|
81 |
} |
|
82}
83); // end call to addActionListener
85 southPanel.add( putButton );
86
87// button to empty contents of Properties table
88JButton clearButton = new JButton( "Clear" );
90 |
clearButton.addActionListener( |
91 |
|
92 |
new ActionListener() { |
93 |
|
94 |
// use method clear to empty table |
95 |
public void actionPerformed( ActionEvent event ) |
96 |
{ |
97 |
table.clear(); |
98 |
showstatus( "Table in memory cleared" ); |
99 |
listProperties(); |
100 |
} |
101}
102); // end call to addActionListener
104 southPanel.add( clearButton );
105
106// button to get value of a property
107JButton getPropertyButton = new JButton( "Get property" );
109 |
getPropertyButton.addActionListener( |
110 |
|
111 |
new ActionListener() { |
112 |
|
|
|
Fig. 20.4 Demonstrating class Properties (part 3 of 6).
Chapter 20 |
Java Utilities Package and Bit Manipulation |
1171 |
|
|
|
113 |
// use method getProperty to obtain a property value |
|
114 |
public void actionPerformed( ActionEvent event ) |
|
115 |
{ |
|
116 |
Object value = table.getProperty( |
|
117 |
nameField.getText() ); |
|
118 |
|
|
119 |
if ( value != null ) |
|
120 |
showstatus( "Get property: " + |
|
121 |
nameField.getText() + " " + |
|
122 |
value.toString() ); |
|
123 |
|
|
124 |
else |
|
125 |
showstatus( "Get: " + nameField.getText() + |
|
126 |
" not in table" ); |
|
127 |
|
|
128 |
listProperties(); |
|
129 |
} |
|
130}
131); // end call to addActionListener
133 southPanel.add( getPropertyButton );
134
135// button to contents of Properties table to file
136JButton saveButton = new JButton( "Save" );
137 |
|
138 |
saveButton.addActionListener( |
139 |
|
140 |
new ActionListener() { |
141 |
|
142 |
// use method save to place contents in file |
143 |
public void actionPerformed( ActionEvent event ) |
144 |
{ |
145 |
// save contents of table |
146 |
try { |
147 |
FileOutputStream output = |
148 |
new FileOutputStream( "props.dat" ); |
149 |
|
150 |
table.store( output, "Sample Properties" ); |
151 |
output.close(); |
152 |
|
153 |
listProperties(); |
154 |
} |
155 |
|
156 |
// process problems with file output |
157 |
catch( IOException ioException ) { |
158 |
ioException.printStackTrace(); |
159 |
} |
160 |
} |
161}
162); // end call to addActionListener
164 southPanel.add( saveButton );
165
Fig. 20.4 Demonstrating class Properties (part 4 of 6).
1172 |
Java Utilities Package and Bit Manipulation |
Chapter 20 |
166// button to load contents of Properties table from file
167JButton loadButton = new JButton( "Load" );
168 |
|
169 |
loadButton.addActionListener( |
170 |
|
171 |
new ActionListener() { |
172 |
|
173 |
// use method load to read contents from file |
174 |
public void actionPerformed( ActionEvent event ) |
175 |
{ |
176 |
// load contents of table |
177 |
try { |
178 |
FileInputStream input = |
179 |
new FileInputStream( "props.dat" ); |
180 |
|
181 |
table.load( input ); |
182 |
input.close(); |
183 |
listProperties(); |
184 |
} |
185 |
|
186 |
// process problems with file input |
187 |
catch( IOException ioException ) { |
188 |
ioException.printStackTrace(); |
189 |
} |
190 |
} |
191}
192); // end call to addActionListener
194 southPanel.add( loadButton );
195
196 container.add( southPanel, BorderLayout.SOUTH );
197
198setSize( 550, 225 );
199setVisible( true );
200}
201
202// output property values
203public void listProperties()
204{
205StringBuffer buffer = new StringBuffer();
206String name, value;
207
208 Enumeration enumeration = table.propertyNames();
209
210 while ( enumeration.hasMoreElements() ) {
211 name = enumeration.nextElement().toString();
212 value = table.getProperty( name );
213
214 buffer.append( name ).append( '\t' );
215buffer.append( value ).append( '\n' );
216}
217
218 displayArea.setText( buffer.toString() );
Fig. 20.4 Demonstrating class Properties (part 5 of 6).
Chapter 20 |
Java Utilities Package and Bit Manipulation |
1173 |
219 } // end method ListProperties
220
221// display String in statusLabel label
222public void showstatus( String s )
223{
224statusLabel.setText( s );
225}
226
227// execute application
228public static void main( String args[] )
229{
230PropertiesTest application = new PropertiesTest();
232 application.setDefaultCloseOperation(
233JFrame.EXIT_ON_CLOSE );
234}
235
236 } // end class PropertiesTest
Fig. 20.4 Demonstrating class Properties (part 6 of 6).
Line 25 uses the no-argument constructor to create an empty Properties table with no default properties. Class Properties also provides an overloaded constructor that receives a reference to a Properties object containing default property values.
Lines 68–69 call Properties method setProperty to store a value for the specified key. If the key does not exist in the table, setProperty returns null; otherwise, it returns the previous value for that key.
Lines 116–117 call Properties method getProperty to locate the value associated with the specified key. If the key is not found in this Properties object, getProperty uses the one in the default Properties object (if there is one). The process continues recursively until there are no more default Properties objects (remember that every Properties object can be initialized with a default Properties object), at which point getProperty returns null. An overloaded version of this method receives two arguments, the second of which is the default value to return if getProperty cannot locate the key.
Line 150 calls Properties method store to save the contents of the Properties object to the OutputStream object specified as the first argument (in this case, a
FileOutputStream). The String argument is a description of the Properties object. Class Properties also provides method list, which takes a PrintStream argument. This method is useful for displaying the set of properties.
1174 |
Java Utilities Package and Bit Manipulation |
Chapter 20 |
Testing and Debugging Tip 20.2
Use Properties method list to display the contents of a Properties object for debugging purposes.
Line 181 calls Properties method load to restore the contents of the Properties object from the InputStream specified as the first argument (in this case, a
FileInputStream).
Line 208 calls Properties method propertyNames to obtain an Enumeration of the property names. The value of each property can be determined by using method getProperty.
20.7 Random Class
We discussed random-number generation in Chapter 6, Methods, where we used Math class method random. Java provides extensive additional random number generation capabilities in class Random. We briefly walk through the API calls here.
A new random-number generator can be created by using
Random r = new Random();
This form uses the computer’s current time to seed its random-number generator differently during each constructor call and thus generates different sequences of random numbers for each Random object.
To create a pseudorandom-number generator with “repeatability,” use
Random r = new Random( seedValue );
The seedValue argument (type long) is used in the random number calculation to “seed” the random number generator. If the same seedValue is used every time, the Random object produces the same sequence of random numbers.
Testing and Debugging Tip 20.3
While a program is under development, use the form Random(seedValue) that produces a repeatable sequence of random numbers. If a bug occurs, fix the bug and test with the same seedValue; this allows you to reconstruct the exact same sequence of random numbers that caused the bug. Once the bugs have been removed, use the form Random(), which generates a new sequence of random numbers each time the program is run.
The call
r.setSeed( seedValue );
resets r’s seed value at any time. The calls
r.nextInt()
r.nextLong()
generate uniformly distributed random integers. You can use Math.abs to take the absolute value of the number produced by nextInt, thus giving a number in the range from zero through approximately 2 billion. Then use the % operator to scale the number. For example, to roll a six-sided die, if you scale with a 6, you will get a number in the range from