java ™ Third Edition S O F T WA R E S T R U C T U R E S Designing and Using Data Structures This page intentionally left blank java ™ Third Edition S O F T WA R E S T R U C T U R E S Designing and Using Data Structures JOHN LEWIS V i r g i n i a Te c h JOSEPH CHASE Radford University Addison-Wesley New York Boston San Francisco London Toronto Sydney Tokyo Singapore Madrid Mexico City Munich Paris Cape Town Hong Kong Montreal Editor-in-Chief Editorial Assistant Managing Editor Production Supervisor Marketing Manager Marketing Coordinator Senior Manufacturing Buyer Online Product Manager Art Director Cover Design Project Management, Composition, and Illustrations Project Coordinator, Nesbitt Graphics, Inc. Project Manager, Nesbitt Graphics, Inc. Text Design, Nesbitt Graphics, Inc. Cover Image Michael Hirsch Stephanie Sellinger Jeffrey Holcomb Heather McNally Erin Davis Kathryn Ferranti Carol Melville Bethany Tidd Linda Knowles Elena Sidorova Nesbitt Graphics, Inc. Harry Druding Kathy Smith Jerilyn Bockorick, Alisha Webber Steve Cole/Getty Images Access the latest information about Addison-Wesley Computer Science titles from our World Wide Web site: http://www.pearsonhighered.com/cs. Many of the designations used by manufacturers and sellers to distinguish their products are claimed as trademarks. Where those designations appear in this book, and Addison-Wesley was aware of a trademark claim, the designations have been printed in initial caps or all caps. The programs and applications presented in this book have been included for their instructional value. They have been tested with care, but are not guaranteed for any particular purpose. The publisher does not offer any warranties or representations, nor does it accept any liabilities with respect to the programs or applications. Library of Congress Cataloging-in-Publication Data Lewis, John, 1963Java software structures : designing and using data structures / John Lewis, Joseph Chase. -- 3rd ed. p. cm. Includes bibliographical references and index. ISBN 978-0-13-607858-6 (alk. paper) 1. Java (Computer program language) 2. Data structures (Computer science) 3. Computer software--Development. I. Chase, Joseph. II. Title. QA76.73.J38L493 2009 005.13'3--dc22 2009000302 Copyright © 2010 Pearson Education, Inc. All rights reserved. No part of this publication may be reproduced, stored in a retrieval system, or transmitted, in any form or by any means, electronic, mechanical, photocopying, recording, or otherwise, without the prior written permission of the publisher. Printed in the United States of America. For information on obtaining permission for use of material in this work, please submit a written request to Pearson Education, Inc., Rights and Contracts Department, 501 Boylston Street, Suite 900, Boston, MA 02116, fax your request to (617)671-3447, or e-mail at http://www.pearsoned.com/legal/permissions.htm. Addison Wesley is an imprint of ISBN-13: 978-013-607858-6 ISBN-10: 0-13-607858-3 www.pearsonhighered.com 1 2 3 4 5 6 7 8 10–EB–13 12 11 10 09 To my wife Sharon and my kids: Justin, Kayla, Nathan, and Samantha –J. L. To my loving wife Melissa for her support and encouragement and to our families, friends, colleagues, and students who have provided so much support and inspiration through the years. –J. C. This page intentionally left blank Preface This book is designed to serve as a text for a course on data structures and algorithms. This course is typically referred to as the CS2 course because it is often taken as the second course in a computing curriculum. We have designed this book to embrace the tenets of Computing Curricula 2001 (CC2001). Pedagogically, this book follows the style and approach of the leading CS1 book Java Software Solutions: Foundations of Program Design, by John Lewis and William Loftus. Our book uses many of the highly regarded features of that book, such as the Key Concept boxes and complete code examples. Together, these two books support a solid and consistent approach to either a two-course or three-course introductory sequence for computing students. That said, this book does not assume that students have used Java Software Solutions in a previous course. Material that might be presented in either course (such as recursion or sorting) is presented in this book as well. We also include strong reference material providing an overview of object-oriented concepts and how they are realized in Java. We understand the crucial role that the data structures and algorithms course plays in a curriculum and we think this book serves the needs of that course well. The Third Edition We have made some key modifications in this third edition to enhance its pedagogy. The most important change is a fundamental reorganization of material that is designed to create a cleaner flow of topics. Instead of having an early, large chapter to review object-oriented concepts, we’ve included that material as an appendix for reference. Then we review concepts as needed and appropriate in the context of the implementation strategies discussed throughout the book and cite the appropriate reference material. This not only links the topics in a timely fashion but also demonstrates the usefulness of particular language constructs. We’ve expanded the discussion of Analysis of Algorithms, and given it its own chapter. The discussion, however, stays at an appropriately moderate level. Our strategy is to motivate the concepts involved in the analysis of algorithms, laying a solid foundation, rather than get embroiled in too much formality. Another key organizational change is that the introduction to collections uses a stack as the primary example. In previous editions of this book we went out of vii viii PREFA C E our way to introduce collections in an abstract way that separated it from the core data structures, using examples such as a bag or set collection. This new approach capitalizes on the fact that a stack is conceptually about as straightforward as it gets. Using it as a first example enhances the understanding of collections as a whole. The previous edition of the book had several chapters that focused on larger case studies that made use of collections to solve non-trivial problems. While many instructors found these useful, they also seemed to interrupt the flow of coverage of core topics. Therefore we have taken the case study chapters out of the book and put them on the web as supplementary resources. We encourage all instructors to download and use these resources as they see fit. Finally, for this edition we’ve reviewed and improved the discussions throughout the book. We’ve expanded the discussion of graphs and reversed the order of the graphs and hashing chapters to make a cleaner flow. And we’ve added a chapter that specifically covers sets and maps. We think these modifications build upon the strong pedagogy established by previous editions and give instructors more opportunity and flexibility to cover topics as they choose. Our Approach Books of this type vary greatly in their overall approach. Our approach is founded on a few important principles that we fervently embraced. First, we present the various collections explored in the book in a consistent manner. Second, we emphasize the importance of sound software design techniques. Third, we organized the book to support and reinforce the big picture: the study of data structures and algorithms. Let’s examine these principles further. Consistent Presentation When exploring a particular type of collection, we carefully address each of the following issues in order: 1. Concept: We discuss the collection conceptually, establishing the services it provides (its interface). 2. Use: We explore examples that illustrate how the particular nature of the collection, no matter how it’s implemented, can be useful when solving problems. 3. Implementation: We explore various implementation options for the collection. 4. Analysis: We compare and contrast the implementations. P REFA C E The Java Collections API is included in the discussion as appropriate. If there is support for a particular collection type in the API, we discuss it and its implementation. Thus we embrace the API, but are not completely tied to it. And we are not hesitant to point out its shortcomings. The analysis is kept at a high level. We establish the concept of Big-Oh notation in Chapter 2 and use it throughout the book, but the analysis is more intuitive than it is mathematical. Sound Program Design Throughout the book, we keep sound software engineering practices a high priority. Our design of collection implementations and the programs that use them follow consistent and appropriate standards. Of primary importance is the separation of a collection’s interface from its underlying implementation. The services that a collection provides are always formally defined in a Java interface. The interface name is used as the type designation of the collection whenever appropriate to reinforce the collection as an abstraction. In addition to practicing solid design principles, we stress them in the discussion throughout the text. We attempt to teach both by example and by continual reinforcement. Clean Organization The contents of the book have been carefully organized to minimize distracting tangents and to reinforce the overall purpose of the book. The organization supports the book in its role as a pedagogical exploration of data structures and algorithms as well as its role as a valuable reference. The book can be divided into numerous parts: Part I consists of the first two chapters and provides an introduction to the concept of a collection and analysis of algorithms. Part II includes the next four chapters, which cover introductory and underlying issues that affect all aspects of data structures and algorithms as well as linear collections (stacks, queues, and lists). Part III covers the concepts of recursion, sorting, and searching. Part IV covers the nonlinear collections (trees, heaps, hashing, and graphs). Each type of collection, with the exception of trees, is covered in its own chapter. Trees are covered in a series of chapters that explore their various aspects and purposes. Chapter Breakdown Chapter 1 (Introduction) discusses various aspects of software quality and provides an overview of software development issues. It is designed to establish the ix x PREFA C E appropriate mindset before embarking on the details of data structure and algorithm design. Chapter 2 (Analysis of Algorithms) lays the foundation for determining the efficiency of an algorithm and explains the important criteria that allow a developer to compare one algorithm to another in proper ways. Our emphasis in this chapter is understanding the important concepts more than getting mired in heavy math or formality. Chapter 3 (Collections) establishes the concept of a collection, stressing the need to separate the interface from the implementation. It also conceptually introduces a stack, then explores an array-based implementation of a stack. Chapter 4 (Linked Structures) discusses the use of references to create linked data structures. It explores the basic issues regarding the management of linked lists, and then defines an alternative implementation of a stack (introduced in Chapter 3) using an underlying linked data structure. Chapter 5 (Queues) explores the concept and implementation of a first-in, firstout queue. Radix sort is discussed as an example of using queues effectively. The implementation options covered include an underlying linked list as well as both fixed and circular arrays. Chapter 6 (Lists) covers three types of lists: ordered, unordered, and indexed. These three types of lists are compared and contrasted, with discussion of the operations that they share and those that are unique to each type. Inheritance is used appropriately in the design of the various types of lists, which are implemented using both array-based and linked representations. Chapter 7 (Recursion) is a general introduction to the concept of recursion and how recursive solutions can be elegant. It explores the implementation details of recursion and discusses the basic idea of analyzing recursive algorithms. Chapter 8 (Sorting and Searching) discusses the linear and binary search algorithms, as well as the algorithms for several sorts: selection sort, insertion sort, bubble sort, quick sort, and merge sort. Programming issues related to searching and sorting, such as using the Comparable interface as the basis of comparing objects, are stressed in this chapter. Searching and sorting that are based in particular data structures (such as heap sort) are covered in the appropriate chapter later in the book. Chapter 9 (Trees) provides an overview of trees, establishing key terminology and concepts. It discusses various implementation approaches and uses a binary tree to represent and evaluate an arithmetic expression. Chapter 10 (Binary Search Trees) builds off of the basic concepts established in Chapter 9 to define a classic binary search tree. A linked implementation of a binary search tree is examined, followed by a discussion of how the balance in the P REFA C E tree nodes is key to its performance. That leads to exploring AVL and red/black implementations of binary search trees. Chapter 11 (Priority Queues and Heaps) explores the concept, use, and implementations of heaps and specifically their relationship to priority queues. A heap sort is used as an example of its usefulness as well. Both linked and array-based implementations are explored. Chapter 12 (Multi-way Search Trees) is a natural extension of the discussion of the previous chapters. The concepts of 2-3 trees, 2-4 trees, and general B-trees are examined and implementation options are discussed. Chapter 13 (Graphs) explores the concept of undirected and directed graphs and establishes important terminology. It examines several common graph algorithms and discusses implementation options, including adjacency matrices. Chapter 14 (Hashing) covers the concept of hashing and related issues, such as hash functions and collisions. Various Java Collections API options for hashing are discussed. Chapter 15 (Sets and Maps) explores these two types of collections and their importance to the Java Collections API. Appendix A (UML) provides an introduction to the Unified Modeling Language as a reference. UML is the de facto standard notation for representing object-oriented systems. Appendix B (Object-Oriented Design) is a reference for anyone needing a review of fundamental object-oriented concepts and how they are accomplished in Java. Included are the concepts of abstraction, classes, encapsulation, inheritance, and polymorphism, as well as many related Java language constructs such as interfaces. Supplements The following supplements are available to all readers of this book at www.aw .com/cssupport. ■ Source Code for all programs presented in the book ■ Full case studies of programs that illustrate concepts from the text, including a Black Jack Game, a Calculator, a Family Tree Program, and a Web Crawler The following instructor supplements are only available to qualified instructors at Pearson Education’s Instructor Resource Center, http://www .pearsonhighered.com/irc. Please visit the Web site, contact your local Pearson Education Sales Representative, or send an e-mail to [email protected], for information about how to access them. xi xii PREFA C E ■ Solutions for selected exercises and programming projects in the book ■ Test Bank, containing questions that can be used for exams ■ PowerPoint® Slides for the presentation of the book content Acknowledgements First and most importantly we want to thank our students for whom this book is written and without whom it never could have been. Your feedback helps us become better educators and writers. Please continue to keep us on our toes. We would like to thank all of the reviewers listed below who took the time to share their insight on the content and presentation of the material in this book and its previous editions. Your input was invaluable. Mary P. Boelk, Robert Burton, Gerald Cohen, Robert Cohen, Jack Davis, Bob Holloway, Nisar Hundewale, Chung Lee, Mark C. Lewis, Mark J. Llewellyn, Ronald Marsh, Eli C. Minkoff, Ned Okie, Manuel A. Perez-Quinones, Moshe Rosenfeld Salam Salloum, Don Slater, Ashish Soni, Carola Wenk, Marquette University Brigham Young University St. Joseph’s College University of Massachusetts–Boston Radford University University of Wisconsin–Madison Georgia State University California State Polytechnic University Trinity University University of Central Florida University of North Dakota Bates College; University of Maine–Augusta Radford University Virginia Tech University of Washington California State Polytechnic University–Pomona Carnegie Mellon University University of Southern California University of Texas–San Antonio The folks at Addison-Wesley have gone to great lengths to support and develop this book along with us. It is a true team effort. Editor-in-Chief Michael Hirsch and his assistant Stephanie Sellinger have always been there to help. Marketing Manager Erin Davis, her assistant Kathryn Ferranti, and the entire Addison-Wesley sales force work tirelessly to make sure that instructors understand the goals and benefits of the book. Heather McNally flawlessly handled the production of the book, and Elena Sidorova is to be credited for the wonderful cover design. They are supported by Kathy Smith and Harry Druding at Nesbitt Graphics. Carol Melville always finds a way to get us time on press so P REFA C E that our book makes it into your hands in time to use it in class. Thank you all very much for all your hard work and dedication to this book. We’d be remiss if we didn’t acknowledge the wonderful contributions of the ACM Special Interest Group on Computer Science Education. Its publications and conferences are crucial to anyone who takes the pedagogy of computing seriously. If you’re not part of this group, you’re missing out. Finally, we want to thank our families, who support and encourage us in whatever projects we find ourselves diving into. Ultimately, you are the reason we do what we do. xiii This page intentionally left blank Contents Preface Chapter 1 Introduction 1.1 Software Quality Correctness Reliability Robustness Usability Maintainability Reusability Portability Efficiency Quality Issues 1.2 Data Structures A Physical Example Containers as Objects Chapter 2 Analysis of Algorithms 1 2 3 3 4 4 5 5 6 6 6 7 7 10 13 2.1 Algorithm Efficiency 14 2.2 Growth Functions and Big-OH Notation 15 2.3 Comparing Growth Functions 17 2.4 Determining Time Complexity Analyzing Loop Execution Nested Loops Method Calls 19 19 20 21 Chapter 3 Collections 3.1 Introduction to Collections Abstract Data Types The Java Collections API 27 28 29 31 xv xvi CONTENTS 3.2 A Stack Collection 31 3.3 Crucial OO Concepts Inheritance Class Hierarchies The Object Class Polymorphism References and Class Hierarchies Generics 33 34 36 37 38 38 40 3.4 A Stack ADT Interfaces 41 41 3.5 Using Stacks: Evaluating Postfix Expressions 44 3.6 Exceptions Exception Messages The try Statement Exception Propagation 51 52 53 54 3.7 Implementing a Stack: With Arrays Managing Capacity 55 56 3.8 The ArrayStack Class The Constructors The push Operation The pop Operation The peek Operation Other Operations 57 58 59 61 62 63 Chapter 4 Linked Structures 71 4.1 References as Links 72 4.2 Managing Linked Lists Accessing Elements Inserting Nodes Deleting Nodes Sentinel Nodes 74 74 75 76 77 4.3 Elements Without Links Doubly Linked Lists 78 78 4.4 Implementing a Stack: With Links The LinkedStack Class 79 79 CONTENTS The push Operation The pop Operation Other Operations 83 85 86 4.5 Using Stacks: Traversing a Maze 86 4.6 Implementing Stacks: The java.util.Stack Class Unique Operations Inheritance and Implementation 93 93 94 Chapter 5 Queues 99 5.1 A Queue ADT 100 5.2 Using Queues: Code Keys 103 5.3 Using Queues: Ticket Counter Simulation 107 5.4 Implementing Queues: With Links The enque Operation The dequeue Operation Other Operations 112 114 115 117 5.5 Implementing Queues: With Arrays The enqueue Operation The dequeue Operation Other Operations 117 123 124 125 Chapter 6 Lists 131 6.1 A List ADT Iterators Adding Elements to a List Interfaces and Polymorphism 132 134 135 137 6.2 Using Ordered Lists: Tournament Maker 140 6.3 Using Indexed Lists: The Josephus Problem 150 6.4 Implementing Lists: With Arrays The remove Operation The contains Operation The iterator Operation The add Operation for an Ordered List 152 155 157 158 158 xvii xviii CONTENTS Operations Particular to Unordered Lists 161 The addAfter Operation for an Unordered List 162 6.5 6.6 Implementing Lists: With Links The remove Operation Doubly Linked Lists The iterator Operation 163 163 165 168 Lists in the Java Collections API 171 172 172 172 173 173 176 Cloneable Serializable RandomAccess Java.util.Vector Java.util.ArrayList Java.util.LinkedList Chapter 7 Recursion 185 7.1 Recursive Thinking Infinite Recursion Recursion in Math 186 186 187 7.2 Recursive Programming Recursion versus Iteration Direct versus Indirect Recursion 188 190 191 7.3 Using Recursion Traversing a Maze The Towers of Hanoi 192 192 197 7.4 Analyzing Recursive Algorithms 201 Chapter 8 Sorting and Searching 209 8.1 Searching Static Methods Generic Methods Linear Search Binary Search Comparing Search Algorithms 210 211 211 212 213 216 8.2 Sorting Selection Sort Insertion Sort 217 220 222 CONTENTS 8.3 Bubble Sort Quick Sort Merge Sort 224 226 229 Radix Sort 231 Chapter 9 Trees 241 9.1 Trees Tree Classifications 242 243 9.2 Strategies for Implementing Trees Computational Strategy for Array Implementation of Trees Simulated Link Strategy for Array Implementation of Trees Analtsis of Trees 245 9.3 Tree Traversals Preorder Traversal Inorder Traversal Postorder Traversal Level-Order Traversal 248 248 249 249 250 9.4 A Binary Tree ADT 251 9.5 Using Binary Trees: Expression Trees 255 9.6 Implementing Binary Trees with Links The find Method The iteratorInOrder Method 262 269 270 9.7 Implementing Binary Trees with Arrays The find Method The iteratorInOrder Method 271 273 274 Chapter 10 Binary Search Trees 245 246 247 281 10.1 A Binary Search Tree 282 10.2 Implementing Binary Search Trees: With Links The addElement Operation The removeElement Operation 284 286 288 xix xx CONTENTS 10.3 10.4 The removeAllOccurrences Operation The removeMin Operation 291 292 Implementing Binary Search Trees: With Arrays The addElement Operation The removeElement Operation The removeAllOccurrences Operation The removeMin Operation 294 295 296 302 303 Using Binary Search Trees: Implementing Ordered Lists Analysis of the BinarySearchTreeList Implementation 304 308 10.5 Balanced Binary Search Trees Right Rotation Left Rotation Rightleft Rotation Leftright Rotation 309 310 310 311 311 10.6 Implementing Binary Search Trees: AVL Trees Right Rotation in an AVL Tree Left Rotation in an AVL Tree Rightleft Rotation in an AVL Tree Leftright Rotation in an AVL Tree 312 313 315 315 315 Implementing Binary Search Trees: Red/Black Trees Insertion into a Red/Black Tree Element Removal from a Red/Black Tree 315 316 319 Implementing Binary Search Trees: The Java Collections API 321 A Philosophical Quandary 325 10.7 10.8 10.9 Chapter 11 Priority Queues and Heaps 333 11.1 A Heap The addElement Operation The removeMin Operation The findMin Operation 334 334 337 338 11.2 Using Heaps: Priority Queues 339 CONTENTS 11.3 Implementing Heaps: With Links The addElement Operation The removeMin Operation The findMin Operation 343 343 346 349 11.4 Implementing Heaps: With Arrays The addElement Operation The removeMin Operation The findMin Operation 350 350 352 353 11.5 Using Heaps: Heap Sort 354 Chapter 12 Multi-way Search Trees 361 12.1 Combining Tree Concepts 362 12.2 2-3 Trees Inserting Elements into a 2-3 Tree Removing Elements from a 2-3 Tree 362 362 365 12.3 2-4 Trees 369 12.4 B-Trees B*-trees B+-trees Analysis of B-trees 369 371 372 372 12.5 Implementation Strategies for B-Trees 373 Chapter 13 Graphs 377 13.1 Undirected Graphs 378 13.2 Directed Graphs 380 13.3 Networks 381 13.4 Common Graph Algorithms Traversals Testing for Connectivity Minimum Spanning Trees Determining the Shortest Path 382 383 387 388 391 13.5 Strategies for Implementing Graphs Adjacency Lists Adjacency Matrices 392 392 393 xxi xxii CONTENTS 13.6 Implementing Undirected Graphs with an Adjacency Matrix The addEdge Method The addVertex Method The extendCapacity Method Other Methods Chapter 14 Hashing 395 399 400 401 401 407 14.1 Hashing 408 14.2 Hashing Functions The Division Method The Folding Method The Mid-Square Method The Radix Transformation Method The Digit Analysis Method The Length-Dependent Method Hashing Functions in the Java Language 410 410 411 411 412 412 412 413 14.3 Resolving Collisions Chaining Open Addressing 413 413 416 14.4 Deleting Elements from a Hash Table Deleting from a Chained Implementation Deleting from an Open Addressing Implementation 419 420 Hash Tables in the Java Collections API The Hashtable Class The HashSet Class The HashMap Class The IdentityHashMap Class The WeakHashMap Class LinkedHashSet and LinkedHashMap 421 422 424 424 424 425 428 14.5 Chapter 15 Sets and Maps 420 435 15.1 A Set Collection 436 15.2 Using a Set: Bingo 439 CONTENTS 15.3 Implementing a Set: With Arrays The add Operation The addAll Operation The removeRandom Operation The remove Operation The union Operation The contains Operation The equals Operation Other Operations UML Description 443 445 447 448 449 450 451 452 453 453 15.4 Implementing a Set: With Links The add Operation The removeRandom Operation The remove Operation Other Operations 455 456 457 458 459 15.5 Maps and the Java Collections API 459 Appendix A UML 467 The Unified Modeling Language (UML) UML Class Diagrams UML Relationships Appendix B Object-Oriented Design 468 468 469 475 B.1 Overview of Object-Orientation 476 B.2 Using Objects Abstraction Creating Objects 476 477 478 B.3 Class Libraries and Packages The import Declaration 480 480 B.4 State and Behavior 481 B.5 Classes Instance Data 482 485 B.6 Encapsulation Visibility Modifiers Local Data 486 486 488 xxiii xxiv CONTENTS Index B.7 Constructors 488 B.8 Method Overloading 489 B.9 References Revisited The null Reference The this Reference Aliases Garbage Collection Passing Objects as Parameters 490 490 491 493 494 495 B.10 The static Modifier Static Variables Static Methods 495 495 496 B.11 Wrapper Classes 497 B.12 Interfaces The Comparable Interface The Iterator Interface 498 499 500 B.13 Inheritance Derived Classes The protected Modifier The super Reference Overriding Methods 500 501 503 503 504 B.14 Class Hierarchies The Object Class Abstract Classes Interface Hierarchies 504 505 506 508 B.15 Polymorphism References and Class Hierarchies Polymorphism via Inheritance Polymorphism via Interfaces 508 509 510 512 B.16 Generic Types 514 B.17 Exceptions Exception Messages The try Statement Exception Propagation The Exception Class Hierarchy 515 515 516 517 517 527 1 Introduction O ur exploration of data structures begins with an overview of the underlying issues surrounding the quality of CHAPTER OBJECTIVES ■ Identify various aspects of software quality ■ Motivate the need for data structures based upon quality issues ■ Introduce the basic concept of a data structure ■ Introduce several elementary data structures software systems. It is important for us to understand that it is necessary for systems to work as specified, but simply working is not sufficient. We must also develop quality systems. This chapter discusses a variety of issues related to software quality and data structures, and it establishes some terminology that is crucial to our exploration of data structures and software design. 1 2 C HA PT ER 1 Introduction 1.1 Software Quality Imagine a scenario where you are approaching a bridge that has recently been built over a large river. As you approach, you see a sign informing you that the bridge was designed and built by local construction workers and that engineers were not involved in the project. Would you continue across the bridge? Would it make a difference if the sign informed you that the bridge was designed by engineers and built by construction workers? The word “engineer” in this context refers to an individual who has been educated in the history, theory, method, and practice of the engineering discipline. This definition includes fields of study such as electrical engineering, mechanical engineering, and chemical engineering. Software engineering is the study of the techniques and theory that underlie the development of high-quality software. When the term “software engineering” was first coined in the 1970s, it was an aspiration—a goal set out by leaders in the industry who realized that much of the software being created was of poor quality. They wanted developers to move away from the simplistic idea of writing programs and toward the disciplined idea of engineering software. To engineer software we must first realize that this term is more than just a title and that it, in fact, represents a completely different attitude. Many arguments have been started over the question of whether software engineering has reached the state of a true engineering discipline. We will leave that argument for software engineering courses. For our purposes, it is sufficient to understand that as software developers we share a common history, we are constrained by common theory, and we must understand current methods and practices in order to work together. Ultimately, we want to satisfy the client, the person or organization who pays for the software to be developed, as well as the final users of the system, which may include the client, depending on the situation. The goals of software engineering are much the same as those for other engineering disciplines: ■ Solve the right problem ■ Deliver a solution on time and within budget ■ Deliver a high-quality solution ■ Accomplish these things in an ethical manner (see www.acm.org/about/ code-of-ethics) Sir Isaac Newton is credited with the quote “If I have seen further it is by standing on the shoulders of giants.” As modern software developers, we stand upon the shoulders of the giants that founded and developed our field. To truly stand upon their shoulders, we must understand, among other things, the fundamentals of quality software systems and the organization, storage, and retrieval of data. These are the building blocks upon which modern software development is built. 1.1 Software Quality Quality Characteristic Description Correctness The degree to which software adheres to its specific requirements. Reliability The frequency and criticality of software failure. Robustness The degree to which erroneous situations are handled gracefully. Usability Maintainability Reusability Portability Efficiency The ease with which users can learn and execute tasks within the software. The ease with which changes can be made to the software. The ease with which software components can be reused in the development of other software systems. The ease with which software components can be used in multiple computer environments. The degree to which the software fulfills its purpose without wasting resources. F I G U R E 1 . 1 Aspects of software quality To maximize the quality of our software, we must first realize that quality means different things to different people. And there are a variety of quality characteristics to consider. Figure 1.1 lists several aspects of high-quality software. Correctness The concept of correctness goes back to our original goal to develop the appropriate solution. At each step along the way of developing a program, we want to make sure that we are addressing the problem as defined by the requirements specification and that we are providing an accurate solution to that problem. Almost all other aspects of quality are meaningless if the software doesn’t solve the right problem. Reliability KEY CON CEPT Reliable software seldom fails and, If you have ever attempted to access your bank account electronically when it does, it minimizes the effects and been unable to do so, or if you have ever lost all of your work beof that failure. cause of a failure of the software or hardware you were using, you are already familiar with the concept of reliability. A software failure can be defined as any unacceptable behavior that occurs within permissible operating conditions. We can compute measures of reliability, such as the mean time between failures or the number of operations or tasks between failures. 3 4 C HA PT ER 1 Introduction In some situations, reliability is an issue of life and death. In the early 1980s, a piece of medical equipment called the Therac-25 was designed to deliver a dose of radiation according to the settings made by a technician on a special keyboard. An error existed in the software that controlled the device and when the technician made a very specific adjustment to the values on the keyboard, the internal settings of the device were changed drastically and a lethal dose of radiation was issued. The error occurred so infrequently that several people died before the source of the problem was determined. In other cases, reliability repercussions are financial. On a particular day in November, 1998, the entire AT&T network infrastructure in the eastern United States failed, causing major interruption in communications capabilities. The problem was eventually traced back to a specific software error. That one failure cost millions of dollars in lost revenue to the companies affected. Robustness Reliability is related to how robust a system is. A robust system handles problems gracefully. For example, if a particular input field is designed to handle numeric data, what happens when alphabetic information is entered? The program could be allowed to terminate abnormally because of the resulting error. However, a more robust solution would be to design the system to acknowledge and handle the situation transparently or with an appropriate error message. Developing a thoroughly robust system may or may not be worth the development cost. In some cases, it may be perfectly acceptable for a program to abnormally terminate if very unusual conditions occur. On the other hand, if adding such protections is not excessively costly, it is simply considered to be good development practice. Furthermore, well-defined system requirements should carefully spell out the situations in which robust error handling is required. Usability To be effective, a software system must be truly usable. If a system is too difficult to use, it doesn’t matter if it provides wonderful functionality. Within the discipline of computer science there is a field of study called HumanComputer Interaction (HCI) that focuses on the analysis and design of user interfaces of software systems. The interaction between the user and system must be well designed, including such things as help options, meaningful messages, consistent layout, appropriate use of color, error prevention, and error recovery. 1.1 Maintainability Software Quality KEY CON CEPT Software developers must maintain their software. That is, they Software systems must be carefully designed, written, and documented must make changes to software in order to fix errors, to enhance to support the work of developers, the functionality of the system, or simply to keep up with evolving maintainers, and users. requirements. A useful software system may need to be maintained for many years after its original development. The software engineers who perform maintenance tasks are often not the same ones as those who originally developed the software. Thus, it is important that a software system be well structured, well written, and well documented in order to maximize its maintainability. Large software systems are rarely written by a single individual or even a small group of developers. Instead, large teams, often working from widely distributed locations, work together to develop systems. For this reason, communication among developers is critical. Therefore, creating maintainable software is beneficial for the long term as well as for the initial development effort. Reusability Suppose that you are a contractor involved in the construction of an office building. It is possible that you might design and build each door in the building from scratch. This would require a great deal of engineering and construction effort, not to mention money. Another option is to use pre-engineered, prefabricated doors for the doorways in the building. This approach represents a great savings of time and money because you can rely on a proven design that has been used many times before. You can be confident that it has been thoroughly tested and that you know its capabilities. However, this does not exclude the possibility that a few doors in the building will be custom engineered and custom built to fit a specific need. When developing a software system, it often makes sense to use pre-existing software components if they fit the needs of the developer and the client. Why reinvent the wheel? Pre-existing components can range in scope from the entire system, to entire subsystems, to individual classes and methods. They may come from part of another system developed earlier or from libraries of components that are created to support the development of future systems. Some pre-existing components are referred to as Commercial Off-The-Shelf (COTS) products. Pre-existing components are often reliable because they have been tested in other systems. Using pre-existing components can reduce the development effort. However, reuse comes at a price. The developer must take the time to investigate potential components to find the right one. Often the component must be modified or extended to fit the criteria of the new system. Thus, it is helpful if the component is truly reusable. That is, software should be written and documented so that it can 5 6 C HA PT ER 1 Introduction be easily incorporated into new systems and easily modified or extended to accommodate new requirements. Portability Software that is easily portable, can be moved from one computing environment to another with little or no effort. Software developed using a particular operating system and underlying central processing unit (CPU) may not run well or at all in another environment. One obvious problem is a program that has been compiled into a particular CPU’s machine language. Because each type of CPU has its own machine language, porting it to another machine would require another, translated version. Differences in the various translations may cause the “same” program on two types of machines to behave differently. Using the Java programming language addresses this issue because Java sourcecode is compiled into bytecode, which is a low-level language that is not the machine language for any particular CPU. Bytecode runs on a Java Virtual Machine (JVM), which is software that interprets the bytecode and executes it. Therefore, at least theoretically, any system that has a JVM can execute any Java program. Efficiency The last software quality characteristic listed in Figure 1.1 is efficiency. Software systems should make efficient use of the resources allocated to them. Two key resources are CPU time and memory. User demands on computers and their software have risen steadily ever since computers were K E Y CO N C E PT first created. Software must always make the best use of its reSoftware must make efficient use of sources in order to meet those demands. The efficiency of individresources such as CPU time and memory. ual algorithms is an important part of this issue and is discussed in more detail in the next chapter and throughout the book. Quality Issues To a certain extent, quality is in the eye of the beholder. That is, some quality characteristics are more important to certain people than to others. We must consider the needs of the various stakeholders, the people affected one way or another by the project. For example, the end user certainly K E Y CO N C E PT wants to maximize reliability, usability, and efficiency, but doesn’t Quality characteristics must be necessarily care about the software’s maintainability or reusability. prioritized, and then maximized to The client wants to make sure the user is satisfied, but is also worthe extent possible. ried about the overall cost. The developers and maintainers want the internal system quality to be high. 1.2 Note also that some quality characteristics are in competition with each other. For example, to make a program more efficient, we may choose to use a complex algorithm that is difficult to understand and therefore hard to maintain. These types of trade-offs require us to carefully prioritize the issues related to a particular project and, within those boundaries, maximize all quality characteristics as much as possible. If we decide that we must use the more complex algorithm for efficiency, we can also document the code especially well to assist with future maintenance tasks. Although all of these quality characteristics are important, for our exploration of data structures in this book, we will focus upon reliability, robustness, reusability and efficiency. In the creation of data structures, we are not creating applications or end-user systems but rather reusable components that may be used in a variety of systems. Thus, usability is not an issue since there will not be a user interface component of our data structures. By implementing in Java and adhering to Javadoc standards, we also address the issues of portability and maintainability. 1.2 Data Structures Why spend so much time talking about software engineering and software quality in a text that focuses on data structures and their algorithms? Well, as you begin to develop more complex programs, it’s important to develop a more mature outlook on the process. As we discussed at the beginning of this chapter, the goal should be to engineer software, not just write code. The data structures we examine in this book lay the foundation for complex software that must be carefully designed. Let’s consider an example that will illustrate the need for and the various approaches to data structures. A Physical Example Imagine that you must design the process for how to handle shipping containers being unloaded from cargo ships at a dock. In a perfect scenario, the trains and trucks that will haul these containers to their destinations will be waiting as the ship is unloaded and thus there would not be a need to store the containers at all. However, such timing is unlikely, and would not necessarily provide the most efficient result for the trucks and the trains since they would have to sit and wait while each ship was unloaded. Instead, as each shipping container is unloaded from a ship, it is moved to a storage location where it will be held until it is loaded on either a truck or a train. Our goal should be to make both the unloading of containers and the retrieval of containers for transportation as efficient as possible. This will mean minimizing the amount of searching required in either process and minimizing the number of times a container is moved. Data Structures 7 8 C HA PT ER 1 Introduction Before we go any further, let’s examine the initial assumptions underlying this problem. First, our shipping containers are generic. By that we mean that they are all the same shape, same size, and can hold the same volume and types of materials. Second, each shipping container has a unique identification number. This number is the key that determines the final destination of each container and whether it is to be shipped by truck or train. Third, the dock workers handling the containers do not need to know, nor can they know, what is inside of each container. Given these assumptions as a starting point, how might we design our storage process for these containers? One possibility might be to simply lay out a very large array indexed by the identification number of the container. Keep in mind, however, the identification number is unique, not just for a single ship but for all ships and all shipping containers. That means that we would need to layout an array of storage at each dock large enough to handle all of the shipping containers in the world. This would also mean that at any given point in time, the vast majority of the units in these physical arrays would be empty. This would appear to be a terrible waste of space and very inefficient. We will explore issues surrounding the efficiency of algorithms in the next chapter. We could try this same solution but allow the array to collapse and expand like an ArrayList as containers are removed or added. However, given the physical nature of this array, such a solution may involve having to move containers multiple times as other containers with lower ID numbers are added or removed. Again, such a solution would be very inefficient. What if we could estimate the maximum number of shipping containers that we would be storing on our dock at any given point in time? This would create the possibility of creating an array of that maximum size and then simply placing each container into the next available position in the array as it is unloaded from the ship. This would be relatively efficient for unloading purposes since the dock workers would simply keep track of which locations in the array were empty and place each new container in an empty location. However, using this approach, what would happen when a truck driver arrived looking for a particular container? Since the array is not ordered by container ID, the driver would have to search for the container in the array. In the worst case scenario, the container they are seeking would be the last one in the array causing them to search the entire array. Of course, the average case would be that each driver would have to search half of the array. Again, this seems less efficient than it could be. Before we try a new design, let’s reconsider what we know about each container. Of course, we have the container ID. But from that, we also have the destination of each container. What if instead of an abitrary ordering of containers in a onedimensional array, we create a two-dimensional array where the first dimension is the destination? Thus we can implement our previous solution as the second dimension within each destination. Unloading a container is still quite simple. The dock 1.2 Data Structures workers would simply take the container to the array for its destination and place it in the first available empty slot. Then our truck drivers would not have to search the entire dock for their shipping container, only that portion, the array within the array, that is bound for their destination. This solution, which is beginning to behave like a data structure called a hash table (discussed in Chapter 13), is an improvement but still requires searching at least a portion of the storage. The first part of this solution, organizing the first dimension by destination, seems like a good idea. However, using a simple unordered array for the second dimension still does not accomplish our goal. There is one additional piece of information that we have for each container that we have yet to examine—the order that it has been unloaded from the ship. Let’s consider an example for a moment of the components of an oil rig being shipped in multiple containers. Does the order of those containers matter? Yes. The crew constructing the oil rig must receive and install the foundation and base components before being able to construct the top of the rig. Now the order the containers are removed from the ship and the order they are placed in storage is important. To this point, we have been considering only the problem of unloading, storing, and shipping storage containers. Now we begin to see that this problem exists within a larger context including how the ship was loaded at its point of origin. Three possibilities exist for how the ship was loaded: containers that are order dependent were loaded in the order they are needed, containers that are order dependent were loaded in the reverse order they are needed, or containers that are order dependent were loaded without regard to order but with order included as part of the information associated with the container ID. Let’s consider each of these cases separately. Keep in mind that the process of unloading the ship will reverse the order of the loading process onto the ship. This behavior is very much like that of a stack, data structure, which we will discuss further in Chapters 3 and 4. If the ship was loaded in the order the components are needed, then the unloading process will reverse the order. Thus our storage and retrieval process must reverse the order again in order to get the containers back into the correct order. This would be accomplished by taking the containers to the array for their destination and storing them in the order they were unloaded. Then the trucker retrieving these containers would simply start with the last one stored. Finally, with this solution we have a storage and retrieval process that involves no searching at all and no extra handling of containers. What if the containers were loaded onto the ship in the reverse orKEY CON CEPT der they are needed? In that case, unloading the ship will bring the A stack can be used to reverse the containers off in the order they will be needed and our storage and order of a set of data. retrieval process must preserve that order. This would be accomplished by taking the containers to the array for their destination, storing them in the order they were unloaded, and then having the truckers start with the first container stored rather than the last. Again, this solution does not 9 10 C HA PT ER 1 Introduction involve any searching or extra handling of containers. This behavior is that of a queue data structure, which will be discussed in detail in Chapter 5. Both of our previous solutions, while very efficient, have been dependent upon the order that the containers were placed aboard ship. A queue preserves the order of its What do we do if the containers were not placed aboard in any pardata. ticular order? In that case, we will need to order the destination array by the priority order of the containers. Rather than trying to exhaust this example, let’s just say that there are several data structures that might accomplish this purpose including ordered lists (Chapter 6), priority queues and heaps (Chapter 11), and hash tables (Chapter 13). We also have the additional issue of how best to organize the file that relates container IDs to the other information about each container. Solutions to this problem might include binary search trees (Chapter 10) and multi-way search trees (Chapter 12). K E Y CO N C E PT Containers as Objects Our shipping container example illustrates another issue surrounding data structures beyond our discussion of how to store the containers. It also illustrates the issue of what the containers store. Early in our discussion, we made the assumption that all shipping containers have the same shape and same size, and can hold the same volume and type of material. While the first two assumptions must remain true in order for containers to be stored and shipped interchangeably, the latter assumptions may not be true. For example, consider the shipment of materials that must remain refrigerated. It may be useful to develop refrigerated shipping containers for this purpose. Similarly some products require very strict humidity control and might require a container with additional environmental controls. Others might be designed for hazardous material and may be lined and/or double walled for protection. Once we develop multiple types of containers, then our containers are no longer generic. We can then think of shipping containers as a hierarchy much like a class hierarchy in object-oriented programming. Thus assigning material to a shipping container becomes analogous to assigning an object to a reference. With both shipping containers and objects, not all assignments are valid. We will discuss issues of type compatibility, inheritance, polymorphism, and creating generic data structures in Chapter 3. Summary of Key Concepts Summary of Key Concepts ■ Reliable software seldom fails and, when it does, it minimizes the effects of that failure. ■ Software systems must be carefully designed, written, and documented to support the work of developers, maintainers, and users. ■ Software must make efficient use of resources such as CPU time and memory. ■ Quality characteristics must be prioritized, and then maximized to the extent possible. ■ A stack can be used to reverse the order of a set of data. ■ A queue preserves the order of its data. Self-Review Questions SR 1.1 What is the difference between software engineering and programming? SR 1.2 Name several software quality characteristics. SR 1.3 What is the relationship between reliability and robustness? SR 1.4 Describe the benefits of developing software that is maintainable. SR 1.5 How does the Java programming language address the quality characteristic of portability? SR 1.6 What is the principle difference in behavior between a stack and a queue? SR 1.7 List two common data structures that might be used to order a set of data. Exercises EX 1.1 Compare and contrast software engineering with other engineering disciplines. EX 1.2 Give a specific example that illustrates each of the software quality characteristics listed in Figure 1.1. EX 1.3 Provide an example, and describe the implications, of a trade-off that might be necessary between quality characteristics in the development of a software system. 11 12 C HA PT ER 1 Introduction Answers to Self-Review Questions SRA 1.1 Software engineering is concerned with the larger goals of system design and development, not just the writing of code. Programmers mature into software engineers as they begin to understand the issues related to the development of high-quality software and adopt the appropriate practices. SRA 1.2 Software quality characteristics include: correctness, reliability, robustness, usability, maintainability, reusability, portability, and efficiency. SRA 1.3 Reliability is concerned with how often and under what circumstances a software system fails, and robustness is concerned with what happens when a software system fails. SRA 1.4 Software that is well structured, well designed, and well documented is much easier to maintain. These same characteristics also provide benefit for distributed development. SRA 1.5 The Java programming language addresses this issue by compiling into bytecode, which is a low-level language that is not the machine language for any particular CPU. Bytecode runs on a Java Virtual Machine (JVM), which is software that interprets the bytecode and executes it. Therefore, at least theoretically, any system that has a JVM can execute any Java program. SRA 1.6 A stack reverses order whereas a queue preserves order. SRA 1.7 Common data structures that can be used to order a set of data include ordered lists, heaps, and hash tables. 2 Analysis of Algorithms I t is important that we understand the concepts surround- ing the efficiency of algorithms before we begin building CHAPTER OBJECTIVES ■ Discuss the goals of software development with respect to efficiency ■ Introduce the concept of algorithm analysis ■ Explore the concept of asymptotic complexity ■ Compare various growth functions data structures. A data structure built correctly and with an eye toward efficient use of both the CPU and memory is one that can be reused effectively in many different applications. However, a data structure that is not built efficiently is similar to using a damaged original as the master from which to make copies. 13 14 C HA PT ER 2 Analysis of Algorithms 2.1 Algorithm Efficiency One of the quality characteristics discussed in section 1.1 is the efficient use of resources. One of the most important resources is CPU time. The efficiency of an algorithm we use to accomplish a particular task is a major factor that determines how fast a program executes. Although the techniques that we will discuss here may also be used to analyze an algorithm relative to the amount of memory it uses, we will focus our discussion on the efficient use of processing time. K E Y CO N C E PT Algorithm analysis is a fundamental computer science topic. The analysis of algorithms is a fundamental computer science topic and involves a variety of techniques and concepts. It is a primary theme that we return to throughout this book. This chapter introduces the issues related to algorithm analysis and lays the groundwork for using analysis techniques. Let’s start with an everyday example: washing dishes by hand. If we assume that washing a dish takes 30 seconds and drying a dish takes an additional 30 seconds, then we can see quite easily that it would take n minutes to wash and dry n dishes. This computation could be expressed as follows: Time (n dishes) = n * (30 seconds wash time + 30 seconds dry time) = 60n seconds or, written more formally: f(x) = 30x + 30x f(x) = 60x On the other hand, suppose we were careless while washing the dishes and splashed too much water around. Suppose each time we washed a dish, we had to dry not only that dish but also all of the dishes we had washed before that one. It would still take 30 seconds to wash each dish, but now it would take 30 seconds to dry the last dish (once), 2 * 30 or 60 seconds to dry the second-to-last dish (twice), 3 * 30 or 90 seconds to dry the third-to-last dish (three times), and so on. This computation could be expressed as follows: n Time (n dishes) = n * (30 seconds wash time) + a (i * 30) i=1 n Using the formula for an arithmetic series g 1 i = n(n + 1)/2 then the function becomes Time (n dishes) = 30n + 30n(n + 1)/2 = 15n2 + 45n seconds If there were 30 dishes to wash, the first approach would take 30 minutes, whereas the second (careless) approach would take 247.5 minutes. The more dishes 2.2 Growth Functions and Big-OH Notation we wash the worse that discrepancy becomes. For example, if there were 300 dishes to wash, the first approach would take 300 minutes or 5 hours, whereas the second approach would take 908,315 minutes or roughly 15,000 hours! 2.2 Growth Functions and Big-OH Notation For every algorithm we want to analyze, we need to define the size of the problem. For our dishwashing example, the size of the problem is the number of dishes to be washed and dried. We also must determine the value that represents efficient use of time or space. For time considerations, we often pick an appropriate processing step that we’d like to minimize, such as our goal to minimize the number of times a dish has to be washed and dried. The overall amount of time spent on the task is directly related to how many times we have to perform that task. The algorithm’s efficiency can be defined in terms of the problem size and the processing step. Consider an algorithm that sorts a list of numbers into increasing order. One natural way to express the size of the problem would be the number of values to be sorted. The processing step we are trying to optimize could be expressed as the number of comparisons we have to make for the algorithm to put the values in order. The more comparisons we make, the more CPU time is used. KEY CON CEPT A growth function shows time or space utilization relative to the problem size. A growth function shows the relationship between the size of the problem (n) and the value we hope to optimize. This function represents the time complexity or space complexity of the algorithm. The growth function for our second dishwashing algorithm is t(n) = 15n2 + 45n However, it is not typically necessary to know the exact growth function for an algorithm. Instead, we are mainly interested in the asymptotic complexity of an algorithm. That is, we want to focus on the general nature of the function as n increases. This characteristic is based on the dominant term of the expression—the term that increases most quickly as n increases. As n gets very large, the value of the dishwashing growth function is dominated by the n2 term because the n2 term grows much faster than the n term. The constants, in this case 15 and 45, and the secondary term, in this case 45n, quickly become irrelevant as n increases. That is to say, the value of n2 dominates the growth in the value of the expression. The table in Figure 2.1 shows how the two terms and the value of the expression grow. As you can see from the table, as n gets larger, the 15n2 term dominates the value of the expression. It is important to note that the 45n term is larger for very small values of n. Saying that a term is the dominant term as n gets large does not mean that it is larger than the other terms for all values of n. 15 16 C HA PT ER 2 Analysis of Algorithms Number of dishes (n) 15n 2 45n 15n 2 ⴙ 45n 1 2 5 10 100 1,000 10,000 100,000 1,000,000 10,000,000 15 60 375 1,500 150,000 15,000,000 1,500,000,000 150,000,000,000 15,000,000,000,000 1,500,000,000,000,000 45 90 225 450 4,500 45,000 450,000 4,500,000 45,000,000 450,000,000 60 150 600 1,950 154,500 15,045,000 1,500,450,000 150,004,500,000 15,000,045,000,000 1,500,000,450,000,000 F I G U R E 2 . 1 Comparison of terms in growth function The asymptotic complexity is called the order of the algorithm. Thus, our second dishwashing algorithm is said to have order n2 time complexity, written O(n2). Our first, more efficient dishwashing example, with growth function t(n) = 60(n) would have order n, written O(n). Thus the reason for the difference between our O(n) original algorithm and our O(n2) sloppy algorithm is the fact each dish will have to be dried multiple times. This notation is referred to as O() or Big-Oh notation. A growth function that executes in constant time regardless of the size of the problem is said to have O(1). In general, we are only concerned with executable statements in a program or algorithm in determining its growth function and efficiency. Keep in mind, however, that some declarations may include initializations and some of these may be complex enough to factor into the efficiency of an algorithm. As an example, assignment statements and if statements that are only executed once regardless of the size of the problem are O(1). The order of an algorithm is found by Therefore, it does not matter how many of those you string together; it eliminating constants and all but the is still O(1). Loops and method calls may result in higher order growth dominant term in the algorithm’s growth function. functions because they may result in a statement or series of statements being executed more than once based on the size of the problem. We will discuss these separately in later sections of this chapter. Figure 2.2 shows several growth functions and their asymptotic complexity. K E Y CO N C E PT K E Y CO N C E PT The order of an algorithm provides an upper bound to the algorithm’s growth function. More formally, saying that the growth function t(n) = 15n2 + 45n is O(n2) means that there exists a constant m and some value of n (n0), such that t(n) … m*n2 for all n 7 n0. Another way of stating this is that the order of an algorithm provides an upper bound to its growth function. It is also important to note that there are other related notations such as omega (Ω) which refers to a function that provides a lower 2.3 Comparing Growth Functions Growth Function Order Label t(n) = 17 t(n) = 3log n t(n) = 20n – 4 t(n) = 12n log n + 100n t(n) = 3n2 + 5n – 2 t(n) = 8n3 + 3n2 t(n) = 2n + 18n2 + 3n O (1) O (log n) O (n) O (n log n) O(n2) O(n3) O(2n) constant logarithmic linear n log n quadratic cubic exponential F I G U R E 2 . 2 Some growth functions and their asymptotic complexity bound and theta (Θ) which refers to a function that provides both an upper and lower bound. We will focus our discussion on order. Because the order of the function is the key factor, the other terms and constants are often not even mentioned. All algorithms within a given order are considered to be generally equivalent in terms of efficiency. For example, while two algorithms to accomplish the same task may have different growth functions, if they are both O(n2) then they are considered to be roughly equivalent with respect to efficiency. 2.3 Comparing Growth Functions One might assume that, with the advances in the speed of processors and the availability of large amounts of inexpensive memory, algorithm analysis would no longer be necessary. However, nothing could be farther from the truth. Processor speed and memory cannot make up for the differences in efficiency of algorithms. Keep in mind that in our previous discussion we have been eliminating constants as irrelevant when discussing the order of an algorithm. Increasing processor speed simply adds a constant to the growth function. When possible, finding a more efficient algorithm is a better solution than finding a faster processor. Another way of looking at the effect of algorithm complexity was proposed by Aho, Hopcroft, and Ullman (1974). If a system can currently handle a problem of size n in a given time period, what happens to the allowable size of the problem if we increase the speed of the processor tenfold? As shown in Figure 2.3, the linear case is relatively simple. Algorithm A, with a linear time complexity of n, is indeed improved by a factor of 10, meaning that this algorithm can process 10 times the data in the same amount of time given a tenfold speed up of the processor. However, algorithm B, with a time complexity of n2, is only improved by a factor of 3.16. Why do we not get the full tenfold increase in problem size? Because the complexity of algorithm B is n2 our effective speedup is only the square root of 10 or 3.16. 17 18 C HA PT ER 2 Analysis of Algorithms Algorithm Time Complexity Max Problem Size Before Speedup Max Problem Size After Speedup A B C D n n2 n3 2n s1 s2 s3 s4 10s1 3.16s2 2.15s3 s4 + 3.3 F I G U R E 2 . 3 Increase in problem size with a tenfold increase in processor speed Similarly, algorithm C, with complexity n3, is only improved by a factor of 2.15 or the cube root of 10. For algorithms with If the algorithm is inefficient, a faster exponential complexity like algorithm D, in which the size variable is processor will not help in the long in the exponent of the complexity term, the situation is far worse. In run. this case the speed up is log2n or in this case, 3.3. Note this is not a factor of 3, but the original problem size plus 3. In the grand scheme of things, if an algorithm is inefficient, speeding up the processor will not help. K E Y C O N C E PT Figure 2.4 illustrates various growth functions graphically for relatively small values of n. Note that when n is small, there is little difference between the algo500 400 Time 300 200 log n n n log n n2 n3 2n 100 0 1 5 10 15 20 25 Input Size (N ) F I G U R E 2 . 4 Comparison of typical growth functions for small values of n 2.4 Determining Time Complexity rithms. That is, if you can guarantee a very small problem size (5 or less), it doesn’t really matter which algorithm is used. However, notice that in Figure 2.5, as n gets very large, the differences between the growth functions become obvious. 2.4 Determining Time Complexity Analyzing Loop Execution To determine the order of an algorithm, we have to determine how often a particular statement or set of statements gets executed. Therefore, we often have to determine how many times the body of a loop is executed. To analyze loop execution, first determine the order of the body of the loop, and then multiply that by the number of times the loop will execute relative to n. Keep in mind that n represents the problem size. KEY CON CEPT Analyzing algorithm complexity often requires analyzing the execution of loops. 200,000 Time 150,000 100,000 log n n n log n n2 n3 2n 50,000 0 1 100 200 300 400 Input Size (N ) F I G U R E 2 . 5 Comparison of typical growth functions for large values of n 500 19 20 C HA PT ER 2 Analysis of Algorithms Assuming that the body of a loop is O(1), then a loop such as this: for (int count = 0; count < n; count++) { /* some sequence of O(1) steps */ } would have O(n) time complexity. This is due to the fact that the body of the loop has O(1) complexity but is executed n times by the loop structure. In general, if a loop structure steps through n items in a linear fashion and the body of the loop is O(1), then the loop is O(n). Even in a case where the loop is designed to skip some number of elements, as long as the progression of elements to skip is linear, the loop is still O(n). For example, if the preceding loop skipped every other number (e.g. count += 2), the growth function of the loop would be n/2, but since constants don’t affect the asymptotic complexity, the order is still O(n). Let’s look at another example. If the progression of the loop is logarithmic such as the following: count = 1 while (count < n) { count *= 2; /* some sequence of O(1) steps */ } K E Y C O N C E PT The time complexity of a loop is found by multiplying the complexity of the body of the loop by how many times the loop will execute. then the loop is said to be O(log n). Note that when we use a logarithm in an algorithm complexity, we almost always mean log base 2. This can be explicitly written as O(log2n). Since each time through the loop the value of count is multiplied by 2, the number of times the loop is executed is log2n. Nested Loops A slightly more interesting scenario arises when loops are nested. In this case, we must multiply the complexity of the outer loop by the complexity of the inner loop to find the resulting complexity. For example, the following nested loops: for (int count = 0; count < n; count++) { for (int count2 = 0; count2 < n; count2++) { /* some sequence of O(1) steps */ } } 2.4 Determining Time Complexity would have complexity O(n2). The body of the inner loop is O(1) and the inner loop will execute n times. This means the inner loop is O(n). Multiplying this result by the number of times the outer loop will execute (n) results in O(n2). KEY CON CEPT The analysis of nested loops must take into account both the inner and outer loops. What is the complexity of the following nested loop? for (int count = 0; count < n; count++) { for (int count2 = count; count2 < n; count2++) { /* some sequence of O(1) steps */ } } In this case, the inner loop index is initialized to the current value of the index for the outer loop. The outer loop executes n times. The inner loop executes n times the first time, n–1 times the second time, etc. However, remember that we are only interested in the dominant term, not in constants or any lesser terms. If the progression is linear, regardless of whether some elements are skipped, the order is still O(n). Thus the resulting complexity for this code is O(n2). Method Calls Let’s suppose that we have the following segment of code: for (int count = 0; count < n; count++) { printsum (count); } We know from our previous discussion that we find the order of the loop by multiplying the order of the body of the loop by the number of times the loop will execute. In this case, however, the body of the loop is a method call. Therefore, we must first determine the order of the method before we can determine the order of the code segment. Let’s suppose that the purpose of the method is to print the sum of the integers from 1 to n each time it is called. We might be tempted to create a brute force method such as the following: public void printsum(int count) { int sum = 0; for (int I = 1; I < count; I++) sum += I; System.out.println (sum); } 21 22 C HA PT ER 2 Analysis of Algorithms What is the time complexity of this printsum method? Keep in mind that only executable statements contribute to the time complexity so in this case, all of the executable statements are O(1) except for the loop. The loop on the other hand is O(n) and thus the method itself is O(n). Now to compute the time complexity of the original loop that called the method, we simply multiply the complexity of the method, which is the body of the loop, by the number of times the loop will execute. Our result, then, is O(n2) using this implementation of the printsum method. However, if you recall, we know from our earlier discussion that we do not have to use a loop to calculate the sum of the numbers from 1 to n. In fact, we know that n the g 1i = n(n + 1)/2. Now let’s rewrite our printsum method and see what happens to our time complexity: public void printsum(int count) { sum = count*(count+1)/2; System.out.println (sum); } Now the time complexity of the printsum method is made up of an assignment statement which is O(1) and a print statement which is also O(1). The result of this change is that the time complexity of the printsum method is now O(1) meaning that the loop that calls this method now goes from being O(n2) to O(n). We know from our our earlier discussion and from Figure 2.5 that this is a very significant improvement. Once again we see that there is a difference between delivering correct results and doing so efficiently. What if the body of a method is made up of multiple method calls and loops? Consider the following code using our printsum method above: public void sample(int n) { printsum(n); /* this method call is O(1) */ for (int count = 0; count < n; count++) /* this loop is O(n) */ printsum (count); for (int count = 0; count < n; count++) /* this loop is O(n2) */ for (int count2 = 0; count2 < n; count2++) System.out.println (count, count2); } The initial call to the printsum method with the parameter temp is O(1) since the method is O(1). The for loop containing the call to the printsum method with the parameter count is O(n) since the method is O(1) and the loop executes 2.4 Determining Time Complexity n times. The nested loops are O(n2) since the inner loop will execute n times each time the outer loop executes and the outer loop will also execute n times. The entire method is then O(n2) since only the dominant term matters. More formally, the growth function for the method sample is given by: f(x) = 1 + n + n2 Then given that we eliminate constants and all but the dominant term, the time complexity is O(n2). There is one additional issue to deal with when analyzing the time complexity of method calls and that is recursion, the situation when a method calls itself. We will save that discussion for Chapter 7. 23 24 C HA PT ER 2 Analysis of Algorithms Summary of Key Concepts ■ Software must make efficient use of resources such as CPU time and memory. ■ Algorithm analysis is a fundamental computer science topic. ■ A growth function shows time or space utilization relative to the problem size. ■ The order of an algorithm is found by eliminating constants and all but the dominant term in the algorithm’s growth function. ■ The order of an algorithm provides an upper bound to the algorithm’s growth function. ■ If the algorithm is inefficient, a faster processor will not help in the long run. ■ Analyzing algorithm complexity often requires analyzing the execution of loops. ■ The time complexity of a loop is found by multiplying the complexity of the body of the loop by how many times the loop will execute. ■ The analysis of nested loops must take into account both the inner and outer loops. Self-Review Questions SR 2.1 What is the difference between the growth function of an algorithm and the order of that algorithm? SR 2.2 Why does speeding up the CPU not necessarily speed up the process by the same amount? SR 2.3 How do we use the growth function of an algorithm to determine its order? SR 2.4 How do we determine the time complexity of a loop? SR 2.5 How do we determine the time complexity of a method call? Exercises EX 2.1 What is the order of the following growth functions? a. 10n2 + 100n + 1000 b. 10n3 – 7 c. 2n + 100n3 d. n2 log n Answers to Self Review Questions EX 2.2 Arrange the growth functions of the previous exercise in ascending order of efficiency for n=10 and again for n=1,000,000. EX 2.3 Write the code necessary to find the largest element in an unsorted array of integers. What is the time complexity of this algorithm? EX 2.4 Determine the growth function and order of the following code fragment: for (int count=0; count < n; count++) { for (int count2=0; count2 < n; count2=count2+2) { System.out.println(count, count2); } } EX 2.5 Determine the growth function and order of the following code fragment: for (int count=0; count < n; count++) { for (int count2=0; count2 < n; count2=count2*2) { System.out.println(count, count2); } } EX 2.6 The table in Figure 2.1 shows how the terms of the growth function for our dishwashing example relate to one another as n grows. Write a program that will create such a table for any given growth function. Answers to Self-Review Questions SRA 2.1 The growth function of an algorithm represents the exact relationship between the problem size and the time complexity of the solution. The order of the algorithm is the asymptotic time complexity. As the size of the problem grows, the complexity of the algorithm approaches the asymptotic complexity. SRA 2.2 Linear speedup only occurs if the algorithm has constant order, O(1), or linear order, O(n). As the complexity of the algorithm grows, faster processors have significantly less impact. SRA 2.3 The order of an algorithm is found by eliminating constants and all but the dominant term from the algorithm’s growth function. 25 26 C HA PT ER 2 Analysis of Algorithms SRA 2.4 The time complexity of a loop is found by multiplying the time complexity of the body of the loop times the number of times the loop will execute. SRA 2.5 The time complexity of a method call is found by determining the time complexity of the method and then substituting that for the method call. References Aho, A. V., J. E. Hopcroft, and J. D. Ullman. The Design and Analysis of Computer Algorithms. Reading, Mass.: Addison-Wesley, 1974. 3 Collections T his chapter begins our exploration of collections and the underlying data structures used to implement them. It lays CHAPTER OBJECTIVES ■ Define the concepts and terminology related to collections ■ Explore the basic structure of the Java Collections API ■ Discuss the abstract design of collections ■ Explore issues surrounding collections including inheritance, polymorphism, generics, and interfaces ■ Define a stack collection ■ Use a stack collection to solve a problem ■ Examine an array implementation of a stack the groundwork for the study of collections by carefully defining the issues and goals related to their design. This chapter also introduces a collection called a stack and uses it to exemplify the issues related to the design, implementation, and use of collections. 27 28 C HA PT ER 3 Collections 3.1 Introduction to Collections A collection is an object that gathers and organizes other objects. It defines the specific ways in which those objects, which are called elements of the collection, can be accessed and managed. The user of a collection, which is usually another class or object in the software system, must interact with the collection only in the prescribed ways. KE Y C O N C E PT A collection is an object that gathers and organizes other objects. Over the past 50 years, several specific types of collections have been defined by software developers and researchers. Each type of collection lends itself to solving particular kinds of problems. A large portion of this book is devoted to exploring these classic collections. K E Y C O N C E PT Elements in a collection are typically organized by the order of their addition to the collection or by some inherent relationship among the elements. Collections can be separated into two broad categories: linear and nonlinear. As the name implies, a linear collection is one in which the elements of the collection are organized in a straight line. A nonlinear collection is one in which the elements are organized in something other than a straight line, such as a hierarchy or a network. For that matter, a nonlinear collection may not have any organization at all. Figure 3.1 shows a linear and a nonlinear collection. It usually doesn’t matter whether the elements in a linear collection are depicted horizontally or vertically. The organization of the elements in a collection, relative to each other, is usually determined by one of two things: ■ The order in which they were added to the collection ■ Some inherent relationship among the elements themselves F I G U R E 3 . 1 A linear and a nonlinear collection 3.1 Introduction to Collections For example, one linear collection may always add new elements to one end of the line, so the order of the elements is determined by the order in which they are added. Another linear collection may be kept in sorted order based on some characteristic of the elements. For example, a list of people may be kept in alphabetical order based on the characters that make up their name. The specific organization of the elements in a nonlinear collection can be determined in either of these two ways as well. Abstract Data Types An abstraction hides certain details at certain times. Dealing with an abstraction is easier than dealing with too many details at one time. In fact, we couldn’t get through a day without relying on abstractions. For example, we couldn’t possibly drive a car if we had to worry about all the details that make the car work: the spark plugs, the pistons, the transmission, and so on. Instead, we can focus on the interface to the car: the steering wheel, the pedals, and a few other controls. These controls are an abstraction, hiding the underlying details and allowing us to control an otherwise very complicated machine. A collection, like any well-designed object, is an abstraction. A collection defines the interface operations through which the user can manage the objects in the collection, such as adding and removing elements. The user interacts with the collection through this interface, as depicted in Figure 3.2. However, the details of how a collection is implemented to fulfill KEY CON CEPT that definition are another issue altogether. A class that impleA collection is an abstraction where ments the collection’s interface must fulfill the conceptual definithe details of the implementation are hidden. tion of the collection, but it can do so in many ways. Interface Class that uses the collection Class that implements the collection F I G U R E 3 . 2 A well-defined interface masks the implementation of the collection 29 30 C HA PT ER 3 Collections Abstraction is another important software engineering concept. In large software systems, it is virtually impossible for any one person to grasp all of the details of the system at once. Instead, the system is divided into abstract subsystems such that the purpose of and the interactions among those subsystems can be specified. Subsystems may then be assigned to different developers or groups of developers that will develop the subsystem to meet its specification. An object is the perfect mechanism for creating a collection because, if it is designed correctly, the internal workings of an object are encapsulated from the rest of the system. In almost all cases, the instance variables defined in a class should be declared with private visibility. Therefore, only the methods of that class can access and modify them. The only interaction a user has with an object should be through its public methods, which represent the services that the object provides. As we progress through our exploration of collections, we will always stress the idea of separating the interface from the implementation. Therefore, for every collection that we examine, we should consider the following: ■ How does the collection operate, conceptually? ■ How do we formally define the interface to the collection? ■ What kinds of problems does the collection help us solve? ■ In which various ways might we implement the collection? ■ What are the benefits and costs of each implementation? Before we continue, let’s carefully define some other terms related to the exploration of collections. A data type is a group of values and the operations defined on those values. The primitive data types defined in Java are the primary examples. For example, the integer data type defines a set of numeric values and the operations (addition, subtraction, etc.) that can be used on them. An abstract data type (ADT) is a data type whose values and operations are not inherently defined within a programming language. It is abstract only in that the details of its implementation must be defined and should be hidden from the user. A collection, therefore, is an abstract data type. A data structure is the collection of programming constructs used to implement a collection. For example, a collection might be implemented using a fixedsize structure such as an array. One interesting artifact of these definitions and our design decision to separate the interface from the K E Y C O N C E PT implementation (i.e., the collection from the data structure that imA data structure is the underlying programming construct used to implements it) is that we may, and often do, end up with a linear data plement a collection. structure, such as an array, being used to implement a nonlinear collection, such as a tree. Historically, the terms ADT and data structure have been used in various ways. We carefully define them here to avoid any confusion, and will use them 3.2 A Stack Collection consistently. Throughout this book we will examine various data structures and how they can be used to implement various collections. The Java Collections API The Java programming language is accompanied by a very large library of classes that can be used to support the development of software. Parts of the library are organized into application programming interfaces (APIs). The Java Collections API is a set of classes that represent a few specific types of collections, implemented in various ways. You might ask why we should learn how to design and implement collections if a set of collections has already been provided for us. There are several reasons. First, the Java Collections API provides only a subset of the collections you may want to use. Second, the classes that are provided may not implement the collections in the ways you desire. Third, and perhaps most important, the study of software development requires a deep understanding of the issues involved in the design of collections and the data structures used to implement them. As we explore various types of collections, we will also examine the appropriate classes of the Java Collections API. In each case, we will analyze the various implementations that we develop and compare them to the approach used by the classes in the standard library. 3.2 A Stack Collection Let’s look at an example of a collection. A stack is a linear collection KEY CON CEPT whose elements are added and removed from the same end. We say Stack elements are processed in a that a stack is processed in a last in, first out (LIFO) manner. That is, LIFO manner—the last element in is the last element to be put on a stack will be the first one that gets rethe first element out. moved. Said another way, the elements of a stack are removed in the reverse order of their placement on it. In fact, one of the principal uses of a stack in computing is to reverse the order of something (e.g., an undo operation). The processing of a stack is shown in Figure 3.3. Usually a stack is depicted vertically, and we refer to the end to which elements are added and from which they are removed as the top of the stack. Recall from our earlier discussions that we define an abstract data type (ADT) by identifying a specific set of operations that establishes the valid ways in which we can manage the elements stored in the data structure. We always want to use this concept to formally define the operations for a collection and work within 31 32 C HA PT ER 3 Collections Removing an element Adding an element top of stack F I G U R E 3 . 3 A conceptual view of a stack the functionality it provides. That way, we can cleanly separate the interface to the collection from any particular implementation technique used to create it. The operations for a stack ADT are listed in Figure 3.4. In stack terminology, we push an element onto a stack and we pop an element off a stack. We can also peek at the top element of a stack, examining it or using it as needed, without actually removing it from the collection. There are also the general operations that allow us to determine if the stack is empty and, if not empty, how many elements it contains. KE Y CO N C E PT A programmer should choose the structure that is appropriate for the type of data management needed. Sometimes there are variations on the naming conventions for the operations on a collection. For a stack, the use of the terms push and pop is relatively standard. The peek operation is sometimes referred to as top. Operation Description push pop peek isEmpty size Adds an element to the top of the stack. Removes an element from the top of the stack. Examines the element at the top of the stack. Determines if the stack is empty. Determines the number of elements on the stack. F I G U R E 3 . 4 The operations on a stack 3.3 D E S I G N Crucial OO Concepts F O C U S In the design of the stack ADT, we see the separation between the role of the stack and the role of the application that is using the stack. Notice that any implementation of this stack ADT is expected to throw an exception if a pop or peek operation is requested on an empty stack. The role of the collection is not to determine how such an exception is handled but merely to report it back to the application using the stack. Similarly, the concept of a full stack does not exist in the stack ADT. Thus, it is the role of the stack collection to manage its own storage to eliminate the possibility of being full. Keep in mind that the definition of a collection is not universal. You will find variations in the operations defined for specific data structures from one book to another. We’ve been very careful in this book to define the operations on each collection so that they are consistent with its purpose. For example, note that none of the stack operations in Figure 3.4 allow us to reach down into the stack to modify, remove, or reorganize the elements in the stack. That is the very nature of a stack—all activity occurs at one end. If we discover that, to solve a particular problem, we need to access the elements in the middle or at the bottom of the collection, then a stack is not the appropriate data structure to use. We do provide a toString operation for the collection. This is not a classic operation defined for a stack, and it could be argued that this operation violates the prescribed behavior of a stack. However, it provides a convenient means to traverse and display the stack’s contents without allowing modification of the stack and this is quite useful for debugging purposes. 3.3 Crucial OO Concepts Now let’s consider what we will store in our stack. One possibility would be to simply recreate our stack data structure each time we need it and create it to store the specific object type for that application. For example, if we needed a stack of strings, we would simply copy and paste our stack code and change the object type to String. While copy, paste, and modify is technically a form of reuse, this brute force type of reuse is not our goal. Reuse, in its purest form, should mean that we create a collection that is written once, compiled into byte code once, and will then handle any objects we choose to store in it safely, efficiently, and effectively. To accomplish these goals, we must take type compatibility and type checking into account. Type compatibility is the concept of whether a particular 33 34 C HA PT ER 3 Collections assignment of an object to a reference is legal. For example, the following assignment is not legal because you cannot assign a reference declared to be of type String to point to an object of type Integer. String x = new Integer(10); Java provides compile-time type checking that will flag this invalid assignment. A second possibility of what to store in our collection would be to take advantage of the concepts of inheritance and polymorphism to create a collection that could store objects of any class. To explore this possibility, we must first explore the concept of inheritance. Inheritance KE Y CO N C E PT Inheritance is the process of deriving a new class from an existing one. K E Y C O N C E PT One purpose of inheritance is to reuse existing software. Inheritance is the process in which a new class is derived from an existing one. The new class automatically contains some or all of the variables and methods in the original class. Then, to tailor the class as needed, the programmer can add new variables and methods to the derived class, or modify the inherited ones. In general, creating new classes via inheritance is faster, easier, and cheaper than writing them from scratch. At the heart of inheritance is the idea of software reuse. By using existing software components to create new ones, we capitalize on all of the effort that went into the design, implementation, and testing of the existing software. Keep in mind that the word class comes from the idea of classifying groups of objects with similar characteristics. Classification schemes often use levels of classes that relate to one another. For example, all mammals share certain characteristics: they are warm-blooded, have hair, and bear live offspring. Now consider a subset of mammals, such as horses. All horses are mammals, and have all the characteristics of mammals. But they also have unique features that make them different from other mammals. If we map this idea into software terms, a class called Mammal would have certain variables and methods that describe the state and behavior of mammals. A Horse class could be derived from the existing Mammal class, automatically inheriting the variables and methods contained in Mammal. The Horse class can refer to the inherited variables and methods as if they had been declared locally in that class. New variables and methods can then be added to the derived class, to distinguish a horse from other mammals. Inheritance nicely models many situations found in the natural world. The original class that is used to derive a new one is called the parent class, superclass, or base class. The derived class is called a child class, or subclass. Java 3.3 Crucial OO Concepts uses the reserved word extends to indicate that a new class is being derived from an existing class. KEY CON CEPT The derivation process should establish a specific kind of relationship between two classes: an is-a relationship. This type of relationship means that the derived class should be a more specific version of the original. For example, a horse is a mammal. Not all mammals are horses, but all horses are mammals. KEY CON CEPT Let’s look at an example. The following class can be used to define a book: 35 Inherited variables and methods can be used in the derived class as if they had been declared locally. Inheritance creates an is-a relationship between all parent and child classes. class Book { protected int numPages; protected void pages() { System.out.println ("Number of pages: " + numPages); } } To derive a child class that is based on the Book class, we use the reserved word extends in the header of the child class. For example, a Dictionary class can be derived from Book as follows: class Dictionary extends Book { private int numDefs; public void info() { System.out.println ("Number of definitions: " + numDefs); System.out.println ("Definitions per page: " + numDefs/numPages); } } By saying that the Dictionary class extends the Book class, the Dictionary class automatically inherits the numPages variable and the pages method. Note that the info method uses the numPages variable explicitly. Inheritance is a one-way street. The Book class cannot use variables or methods that are declared explicitly in the Dictionary class. For instance, if we created an object from the Book class, it could not be used to invoke the info method. This restriction makes sense, because a child class is a more specific version of the parent. A dictionary has pages, because all books have pages; however, although a dictionary has definitions, not all books do. 36 C HA PT ER 3 Collections Throughout this book, we will use the Unified Modeling Language (UML) to represent the design of our systems. Appendix A provides an introduction to the UML. Inheritance relationships are represented in UML class diagrams using an arrow with an open arrowhead pointing from the child class to the parent class. Class Hierarchies A child class derived from one parent can be the parent of its own child class. Furthermore, multiple classes can be derived from a single parent. Therefore, inheritance relationships often develop into class hierarchies. The UML class diagram in Figure 3.5 shows a class hierarchy that incorporates the inheritance relationship between classes Mammal and Horse. There is no limit to the number of children a class can have, or to the number of levels to which a class hierarchy can extend. Two children of the same parent are called siblings. Although siblings share the characteristics passed on by their common parent, they are not related by inheritance, because one is not used to derive the other. K E Y C O N C E PT The child of one class can be the parent of one or more other classes, creating a class hierarchy. In class hierarchies, common features should be kept as high in the hierarchy as reasonably possible. That way, the only characteristics explicitly established in a child class are those that make the class distinct from its parent and from its siblings. This approach maximizes the ability to reuse classes. It also facilitates maintenance activities, because when K E Y C O N C E PT changes are made to the parent, they are automatically reflected in Common features should be located as high in a class hierarchy as is reasonthe descendants. Always remember to maintain the is-a relationship able, minimizing maintenance efforts. when building class hierarchies. Animal Reptile Snake Bird Lizard Parrot Mammal Horse Bat F I G U R E 3 . 5 A UML class diagram showing a class hierarchy 3.3 Crucial OO Concepts The inheritance mechanism is transitive. That is, a parent passes along a trait to a child class, that child class passes it along to its children, and so on. An inherited feature might have originated in the immediate parent, or possibly from several levels higher in a more distant ancestor class. There is no single best hierarchy organization for all situations. The decisions made when designing a class hierarchy restrict and guide more detailed design decisions and implementation options, and they must be made carefully. The Object Class In Java, all classes are derived ultimately from the Object class. If a class definition doesn’t use the extends clause to derive itself explicitly from another class, then that class is automatically derived from the Object class by default. Therefore, the following two class definitions are equivalent: class Thing { // whatever } and class Thing extends Object { // whatever } Because all classes are derived from Object, any public method of Object can be invoked through any object created in any Java program. The Object class is defined in the java.lang package of the standard class library. The toString method, for instance, is defined in the Object class, so the toString method can be called on any object. When a println method is called with an object parameter, toString is called to determine what to print. KEY CON CEPT All Java classes are derived, directly or indirectly, from the Object class. The definition for toString that is provided by the Object class returns a string containing the object’s class name followed by a numeric value that is unique for that object. Usually, we override the Object version of toString to fit our own needs. The String class has overridden the toString method so that it returns its stored string value. The equals method of the Object class is also useful. Its purpose is to determine if two objects are equal. The definition of the equals method provided by the Object class returns true if the two object references actually refer to the same object (that is, if they are aliases). Classes often override the inherited 37 38 C HA PT ER 3 Collections K E Y C O N C E PT The toString and equals methods are defined in the Object class and therefore are inherited by every class in every Java program. definition of the equals method in favor of a more appropriate definition. For instance, the String class overrides equals so that it returns true only if both strings contain the same characters in the same order. A more complete discussion of inheritance is provided in Appendix B. Polymorphism This leads us to the concept of polymorphism. The term polymorphism can be defined as “having many forms.” A polymorphic reference is a reference variable that can refer to different types of objects at different points in time. The specific method invoked through a polymorphic reference can change from one invocation to the next. Consider the following line of code: obj.doIt(); KE Y C O N C E PT A polymorphic reference can refer to different types of objects over time. If the reference obj is polymorphic, it can refer to different types of objects at different times. If that line of code is in a loop or in a method that is called more than once, that line of code might call a different version of the doIt method each time it is invoked. At some point, the commitment is made to execute certain code to carry out a method invocation. This commitment is referred to as binding a method invocation to a method definition. In most situations, the binding of a method invocation to a method definition can occur at compile time. For polymorphic references, however, the decision cannot be made until run time. The method definition that is used is based on the object that is being referred to by the reference variable at that moment. This deferred commitment is called late binding or dynamic binding. It is less efficient than binding at compile time because the decision has to be made during the execution of the program. This overhead is generally acceptable in light of the flexibility that a polymorphic reference provides. There are two ways to create a polymorphic reference in Java: using inheritance and using interfaces. The following sections describe creating polymorphism via inheritance. We will revisit the discussion of polymorphism via interfaces in Chapter 6. References and Class Hierarchies In Java, a reference that is declared to refer to an object of a particular class also can be used to refer to an object of any class related to it by inheritance. For example, if the class Mammal is used to derive the class Horse, then a Mammal 3.3 Crucial OO Concepts 39 reference can be used to refer to an object of class Horse. This ability is shown in the code segment below: Mammal pet; Horse secretariat = new Horse(); pet = secretariat; // a valid assignment The reverse operation, assigning the Mammal object to a Horse reference, is also valid, but requires an explicit cast. Assigning a reference in this direction is generally less useful and more likely to cause problems, because although a horse has all the functionality of a mammal (because a horse is-a mammal), the reverse is not necessarily true. KEY CON CEPT A reference variable can refer to any object created from any class related to it by inheritance. This relationship works throughout a class hierarchy. If the Mammal class were derived from a class called Animal, then the following assignment would also be valid: Animal creature = new Horse(); The reference variable creature, as defined in the previous section, can be polymorphic, because at any point in time it could refer to an Animal object, a Mammal object, or a Horse object. Suppose that all three of these classes have a method called move and that it is implemented in a different way in each class (because the child class overrode the definition it inherited). The following invocation calls the move method, but the particular version of the method it calls is determined at run time: creature.move(); At the point when this line is executed, if creature currently refers to an Animal object, the move method of the Animal class is invoked. Likewise, if creature currently refers to a Mammal or Horse object, the Mammal or Horse version of move is invoked, respectively. Again, a more complete discussion of polymorphism and all of its implications is included in Appendix B. KEY CON CEPT A polymorphic reference uses the type of the object, not the type of the reference, to determine which version of a method to invoke. Carrying this to the extreme, an Object reference can be used to refer to any object, because ultimately all classes are descendants of the Object class. An ArrayList, for example, uses polymorphism in that it is designed to hold Object references. That’s why an ArrayList can be used to store any kind of object. In fact, a particular ArrayList can be used to hold several different types of objects at one time, because, in essence, they are all Object objects. The result of this discussion would seem to be that we could simply store Object references in our stack and take advantage of polymorphism via inheritance to create a collection that can store any type of objects. However, this possible solution creates some unexpected consequences. Because in this chapter we focus on implementing 40 C HA PT ER 3 Collections a stack with an array, let’s examine what can happen when dealing with polymorphic references and arrays. Consider our classes represented in Figure 3.5. Since Animal is a superclass of all of the other classes in this diagram, an assignment such as the following is allowable: Animal creature = new Bird(); However, this also means that the following assignments will compile as well: Animal[] creatures = new Mammal[]; creatures[1] = new Reptile(); Note that by definition, creatures[1] should be both a Mammal and an Animal, but not a Reptile. This code will compile but will generate a java.lang.ArrayStoreException at run time. Thus, because using the Object class will not provide us with compile-time type checking, we should look for a better solution. Generics Beginning with Java 5.0, Java enables us to define a class based on a generic type. That is, we can define a class so that it stores, operates on, and manages objects whose type is not specified until the class is instantiated. Generics are an integral part of our discussions of collections and their underlying implementations throughout the rest of this book. Let’s assume we need to define a class called Box that stores and manages other objects. As we discussed, using polymorphism, we could simply define Box so that internally it stores references to the Object class. Then, any type of object could be stored inside a box. In fact, multiple types of unrelated objects could be stored in Box. We lose a lot of control with that level of flexibility in our code. A better approach is to define the Box class to store a generic type T. (We can use any identifier we want for the generic type, but using T has become a convention.) The header of the class contains a reference to the type in angle brackets. For example: class Box<T> { // declarations and code that manage objects of type T } Then, when a Box is needed, it is instantiated with a specific class used in place of T. For example, if we wanted a Box of Widget objects, we could use the following declaration: Box<Widget> box1 = new Box<Widget>; 3.4 A Stack ADT 41 The type of the box1 variable is Box<Widget>. In essence, for the box1 object, the Box class replaces T with Widget. Now suppose we wanted a Box in which to store Gadget objects; we could make the following declaration: Box<Gadget> box2 = new Box<Gadget>; For box2, the Box class essentially replaces T with Gadget. So, although the box1 and box2 objects are both boxes, they have different types because the generic type is taken into account. This is a safer implementation, because at this point we cannot use box1 to store gadgets (or anything else for that matter), nor could we use box2 to store widgets. A generic type such as T cannot be instantiated. It is merely a placeholder to allow us to define the class that will manage a specific type of object that is established when the class is instantiated. Given that we now have a mechanism using generic types for creating a collection that can be used to store any type of object safely and effectively, let’s continue on with our discussion of the stack collection. 3.4 A Stack ADT Interfaces To facilitate the separation of the interface operations from the methods that implement them, we can define a Java interface structure for a collection. A Java interface provides a formal mechanism for defining the set of operations for any collection. KEY CON CEPT A Java interface defines a set of abstract methods and is useful in separating the concept of an abstract data type from its implementation. Recall that a Java interface defines a set of abstract methods, specifying each method’s signature but not its body. A class that implements an interface provides definitions for the methods defined in the interface. The interface name can be used as the type of a reference, which can be assigned any object of any class that implements the interface. Listing 3.1 defines a Java interface for a stack collection. We name a collection interface using the collection name followed by the abbreviation ADT (for abstract data type). Thus, StackADT.java contains the interface for a stack collection. It is defined as part of the jss2 package, which contains all of the collection classes and interfaces presented in this book. Note that the stack interface is defined as StackADT<T>, operating on a generic type T. In the methods of the interface, the type of various parameters and return values is often expressed using the generic type T. When this interface is implemented, it will be based on a type that is substituted for T. KEY CON CEPT By using the interface name as a return type, the interface doesn’t commit the method to the use of any particular class that implements a stack. Later in this chapter we examine a class that implements this interface. In the next chapter, we will examine an alternative implementation. For 42 C HA PT ER 3 L I S T I N G Collections 3 . 1 /** * @author Lewis and Chase * * Defines the interface to a stack data structure. */ package jss2; public interface StackADT<T> { /** Adds one element to the top of this stack. * @param element element to be pushed onto stack */ public void push (T element); /** Removes and returns the top element from this stack. * @return T element removed from the top of the stack */ public T pop(); /** Returns without removing the top element of this stack. * @return T element on top of the stack */ public T peek(); /** Returns true if this stack contains no elements. * @return boolean whether or not this stack is empty */ public boolean isEmpty(); /** Returns the number of elements in this stack. * @return int number of elements in this stack */ public int size(); /** Returns a string representation of this stack. * @return String representation of this stack */ public String toString(); } 3.4 <<interface>> StackADT push() pop() peek() isEmpty() size() toString() F I G U R E 3 . 6 The StackADT interface in UML now, our abstract understanding of how a stack operates allows us to explore situations in which stacks help us solve particular problems. Each time we introduce an interface, a class, or a system in this text, we will accompany that description with the UML description of that interface, class, or system. This should help you become accustomed to reading UML descriptions and creating them for other classes and systems. Figure 3.6 illustrates the UML description of the StackADT interface. Note that UML provides flexibility in describing the methods associated with a class or interface. In this case, we have chosen to identify each of the methods as public (+), but we have not listed the parameters for each. Stacks are used quite frequently in the computing world. For example, the undo operation in a word processor is usually implemented using a stack. As we make changes to a document (add data, delete data, make format changes, etc.), the word processor keeps track of each operation by pushing some representation of it onto a stack. If we choose to undo an operation, the word processing software pops the most recently performed operation off the stack and reverses it. If we choose to undo again (undoing the second-to-last operation we performed), D E S I G N F O C U S Undo operations are often implemented using a special type of stack called a drop-out stack. The basic operations on a drop-out stack are the same as those for a stack (i.e., push, pop, and peek). The only difference is that a drop-out stack has a limit to the number of elements it will hold and, once that limit is reached, the element on the bottom of the stack drops off the stack when a new element is pushed on. The development of a drop-out stack is left as an exercise. A Stack ADT 43 44 C HA PT ER 3 Collections another element is popped from the stack. In most word processors, many operations can be reversed in this manner. The following section explores in detail an example of using a stack to solve a problem. 3.5 Using Stacks: Evaluating Postfix Expressions Traditionally, arithmetic expressions are written in infix notation, meaning that the operator is placed between its operands in the form <operand> <operator> <operand> such as in the expression 4 + 5 When evaluating an infix expression, we rely on precedence rules to determine the order of operator evaluation. For example, the expression 4 + 5 * 2 evaluates to 14 rather than 18 because of the precedence rule that says in the absence of parentheses, multiplication evaluates before addition. In a postfix expression, the operator comes after its two operands. Therefore, a postfix expression takes the form <operand> <operand> <operator> For example, the postfix expression 6 9 - is equivalent to the infix expression 6 - 9 A postfix expression is generally easier to evaluate than an infix expression because precedence rules and parentheses do not have to be taken into account. The order of the values and operators in the expression are sufficient to determine the result. Programming language compilers and run-time environments often use postfix expressions in their internal calculations for this reason. The process of evaluating a postfix expression can be stated in one simple rule: Scanning from left to right, apply each operation to the two operands immediately preceding it and replace the operator with the result. At the end we are left with the final value of the expression. Consider the infix expression we looked at earlier: 4 + 5 * 2 3.5 Using Stacks: Evaluating Postfix Expressions In postfix notation, this expression would be written 4 5 2 * + Let’s use our evaluation rule to determine the final value of this expression. We scan from the left until we encounter the multiplication (*) operator. We apply this operator to the two operands immediately preceding it (5 and 2) and replace it with the result (10), leaving us with 4 10 + Continuing our scan from left to right, we immediately encounter the plus (+) operator. Applying this operator to the two operands immediately preceding it (4 and 10) yields 14, which is the final value of the expression. Let’s look at a slightly more complicated example. Consider the following infix expression: (3 * 4 - (2 + 5)) * 4 / 2 The equivalent postfix expression is 3 4 * 2 5 + - 4 * 2 / Applying our evaluation rule results in: then then then then 12 2 5 + - 4 * 2 / 12 7 - 4 * 2 / 5 4 * 2 / 20 2 / 10 Now let’s think about the design of a program that will evaluate a postfix expression. The evaluation rule relies on being able to retrieve the previous two operands whenever we encounter an operator. Furthermore, a large postfix expression will have many operators and operands to manage. It turns out that a stack is the perfect collection to use in this case. The operations provided by a stack coincide nicely with the process of evaluating a postfix expression. KEY CON CEPT A stack is the ideal data structure to use when evaluating a postfix expression. The algorithm for evaluating a postfix expression using a stack can be expressed as follows: Scan the expression from left to right, identifying each token (operator or operand) in turn. If it is an operand, push it onto the stack. If it is an operator, pop the top two elements off of the stack, apply the operation to them, and push the result onto the stack. When we reach the end of the expression, the element remaining on the stack is the result of the expression. If at any point we attempt to pop two elements off of the stack but there are not two elements on the stack, then our postfix expression was not properly formed. Similarly, if we reach the end of the expression and more than one element remains on the stack, then our expression was not well formed. Figure 3.7 depicts the use of a stack to evaluate a postfix expression. 45 46 C HA PT ER 3 Collections 5 top top 1 –3 4 top 7 6 top –12 –12 –12 7 7 7 7 4 -3 * 1 5 + / top –2 7 top –14 * F I G U R E 3 . 7 Using a stack to evaluate a postfix expression The program in Listing 3.2 evaluates multiple postfix expressions entered by the user. It uses the PostfixEvaluator class shown in Listing 3.3. To keep things simple, this program assumes that the operands to the expression are integers and are literal values (not variables). When executed, the program repeatedly accepts and evaluates postfix expressions until the user chooses not to. L I S T I N G 3 . 2 /** * @author Lewis and Chase * * Demonstrates the use of a stack to evaluate postfix expressions. */ import java.util.Scanner; public class Postfix { /** * Reads and evaluates multiple postfix expressions. */ public static void main (String[] args) { String expression, again; int result; try { Scanner in = new Scanner(System.in); 3.5 L I S T I N G 3 . 2 Using Stacks: Evaluating Postfix Expressions continued do { PostfixEvaluator evaluator = new PostfixEvaluator(); System.out.println ("Enter a valid postfix expression: "); expression = in.nextLine(); result = evaluator.evaluate (expression); System.out.println(); System.out.println ("That expression equals " + result); System.out.print ("Evaluate another expression [Y/N]? "); again = in.nextLine(); System.out.println(); } while (again.equalsIgnoreCase("y")); } catch (Exception IOException) { System.out.println(“Input exception reported"); } } } L I S T I N G 3 . 3 /** * PostfixEvaluator.java Authors: Lewis/Chase * * Represents an integer evaluator of postfix expressions. Assumes * the operands are constants. */ import jss2.ArrayStack; import java.util.StringTokenizer; public class PostfixEvaluator { /** constant for addition symbol */ private final char ADD = '+'; /** constant for subtraction symbol */ private final char SUBTRACT = '-'; /** constant for multiplication symbol */ 47 48 C HA PT ER 3 L I S T I N G Collections 3 . 3 continued private final char MULTIPLY = '*'; /** constant for division symbol */ private final char DIVIDE = '/'; /** the stack */ private ArrayStack<Integer> stack; /** * Sets up this evaluator by creating a new stack. */ public PostfixEvaluator() { stack = new ArrayStack<Integer>(); } /** * Evaluates the specified postfix expression. If an operand is * encountered, it is pushed onto the stack. If an operator is * encountered, two operands are popped, the operation is * evaluated, and the result is pushed onto the stack. * @param expr String representation of a postfix expression * @return int value of the given expression */ public int evaluate (String expr) { int op1, op2, result = 0; String token; StringTokenizer tokenizer = new StringTokenizer (expr); while (tokenizer.hasMoreTokens()) { token = tokenizer.nextToken(); if (isOperator(token)) { op2 = (stack.pop()).intValue(); op1 = (stack.pop()).intValue(); result = evalSingleOp (token.charAt(0), op1, op2); stack.push (new Integer(result)); } else stack.push (new Integer(Integer.parseInt(token))); } return result; } 3.5 L I S T I N G 3 . 3 Using Stacks: Evaluating Postfix Expressions continued /** * Determines if the specified token is an operator. * @param token String representing a single token * @return boolean true if token is operator */ private boolean isOperator (String token) { return ( token.equals("+") || token.equals("-") || token.equals("*") || token.equals("/") ); } /** * Performs integer evaluation on a single expression consisting of * the specified operator and operands. * @param operation operation to be performed * @param op1 the first operand * @param op2 the second operand * @return int value of the expression */ private int evalSingleOp (char operation, int op1, int op2) { int result = 0; switch (operation) { case ADD: result = op1 + op2; break; case SUBTRACT: result = op1 - op2; break; case MULTIPLY: result = op1 * op2; break; case DIVIDE: result = op1 / op2; } return result; } } 49 50 C HA PT ER 3 Collections ArrayStack <<interface>> StackADT top : int stack : Object[] DEFAULT_CAPACITY push() pop() peek() isEmpty() size() toString() push() pop() peek() isEmpty() size() toString - expandCapacity() PostfixEvaluator Postfix letter number main() toString() F I G U R E 3 . 8 UML class diagram for the postfix expression evaluation program The evaluate method performs the evaluation algorithm described earlier, supported by the isOperator and evalSingleOp methods. Note that in the evaluate method, only operands are pushed onto the stack. Operators are used as they are encountered and are never put on the stack. This is consistent with the evaluation algorithm we discussed. An operand is put on the stack as an Integer object, instead of as an int primitive value, because the stack data structure is designed to store objects. When an operator is encountered, the most recent two operands are popped off of the stack. Note that the first operand popped is actually the second operand in the expression, and the second operand popped is the first operand in the expression. This order doesn’t matter in the cases of addition and multiplication, but it certainly matters for subtraction and division. Note also that the postfix expression program assumes that the postfix expression entered is valid, meaning that it contains a properly organized set of operators and operands. A postfix expression is invalid if either (1) two operands are not available on the stack when an operator is encountered or (2) there is more than one value on the stack when the tokens in the expression are exhausted. Either situation indicates that there was something wrong 3.6 with the format of the expression, and both can be caught by examining the state of the stack at the appropriate point in the program. We will discuss how we might deal with these situations and other exceptional cases in the next section. Perhaps the most important aspect of this program is the use of the class that defined the stack collection. At this point, we don’t know how the stack was implemented. We simply trusted the class to do its job. In this example, we used the class ArrayStack, but we could have used any class that implemented a stack as long as it performed the stack operations (defined by the StackADT interface) as expected. From the point of view of evaluating postfix expressions, the manner in which the stack is implemented is largely irrelevant. Figure 3.8 shows a UML class diagram for the postfix expression evaluation program. 3.6 Exceptions One concept that we will explore with each of the collections we discuss is that of exceptional behavior. What action should the collection take in the exceptional case? There are some such cases that are inherent in the collection itself. For example, in the case of a stack, what should happen if an attempt is made to pop an element from an empty stack? In this case, it does not matter what data structure is being used to implement the collection; the exception will still apply. Some such cases are artifacts of the data structure being used to implement the collection. For example, if we are using an array to implement a stack, what should happen if an attempt is made to push an element onto the stack but the array is full? Let’s take a moment to explore this concept further. Problems that arise in a Java program may generate exceptions or errors. An exception is an object that defines an unusual or erroneous situation. An exception is thrown by a program or the run-time environment, and it can be caught and handled appropriately if desired. An error is similar to an exception, except that an error generally represents an unrecoverable situation, and it should not be caught. Java has a predefined set of exceptions and errors that may occur during the execution of a program. In our postfix evaluation example, there were several potential exceptional situations. For example: ■ If the stack were full on a push ■ If the stack were empty on a pop ■ If the stack held more than one value at the completion of the evaluation Let’s consider each of these separately. The possibility that the stack might be full on a push is an issue for the underlying data structure, not the collection. Conceptually speaking, there is no such thing as a full stack. Now we know that Exceptions 51 52 C HA PT ER 3 Collections this is not reality and that all data structures will eventually reach a limit. However, even when this physical limit is reached, the stack is not full; only the data structure that implements the stack is full. We will discuss strategies for handling this situation as we implement our stack in the next section. What if the stack is empty on a pop? This is an exceptional case that has to do with the problem and not the underlying data structure. In our postfix evaluation example, if we attempt to pop two operands and there are not two operands available on the stack, our postfix expression was not properly formed. This is a perfect case where the collection needs to report the exception and the application then must interpret that exception in context. The third case is equally interesting. What if the stack holds more than one value at the completion of the evaluation? From the perErrors and exceptions represent unspective of the stack collection, this is not an exception. However, usual or invalid processing. from the perspective of the application, this is a problem that means once again that the postfix expression was not well formed. Because it will not generate an exception from the collection, this is a condition for which the application must test. K E Y CO N C E PT A program can be designed to process an exception in one of three ways: ■ Not handle the exception at all. ■ Handle the exception where it occurs. ■ Handle the exception at another point in the program. We explore each of these approaches in the following sections. Exception Messages If an exception is not handled at all by the program, the program will terminate (abnormally) and produce a message that describes what exception occurred and where in the program it was produced. The information associated with an exception is often helpful in tracking down the cause of a problem. Let’s look at the output of an exception. An ArithmeticException is thrown when an invalid arithmetic operation is attempted, such as dividing by zero. When that exception is thrown, if there is no code in the program to handle the exception explicitly, the program terminates and prints a message similar to the following: Exception in thread "main" java.lang.ArithmeticException: / by zero at Zero.main (Zero.java:17) The first line of the exception output indicates which exception was thrown and provides some information about why it was thrown. The remaining line or lines are the call stack trace, which indicates where the exception occurred. In this case, there 3.6 Exceptions is only one line in the call stack trace, but there may be several, depending on where the exception originated in the program. The first line of the trace indicates the method, file, and line number where the exception occurred. The other lines in the trace, if present, indicate the methods that were called to get to the method that produced the exception. In this program, there is only one method, and it produced the exception; therefore, there is only one line in the trace. The call stack trace information is also available by calling methods of the exception object that is being thrown. The method getMessage returns a string explaining the reason the exception was thrown. The method printStackTrace prints the call stack trace. KEY CON CEPT The messages printed by a thrown exception indicate the nature of the problem and provide a method call stack trace. The try Statement Let’s now examine how we catch and handle an exception when it is thrown. A try statement consists of a try block followed by one or more catch clauses. The try block is a group of statements that may throw an exception. A catch clause defines how a particular kind of exception is handled. A try block can have several catch clauses associated with it, each dealing with a particular kind of exception. A catch clause is sometimes called an exception handler. Here is the general format of a try statement: try { // } catch { // } catch { // } statements in the try block (IOException exception) statements that handle the I/O problem (NumberFormatException exception) statements that handle the number format problem When a try statement is executed, the statements in the try block are executed. If no exception is thrown during the execution of the try block, processing continues with the statement following the try statement (after all of the catch clauses). This situation is the normal execution flow and should occur most of the time. KEY CON CEPT Each catch clause on a try statement handles a particular kind of exception that may be thrown within the try block. If an exception is thrown at any point during the execution of the try block, control is immediately transferred to the appropriate exception handler if it is 53 54 C HA PT ER 3 Collections present. That is, control transfers to the first catch clause whose specified exception corresponds to the class of the exception that was thrown. After executing the statements in the catch clause, control is transferred to the statement after the entire try statement. Exception Propagation If an exception is not caught and handled where it occurs, control is immediately returned to the method that invoked the method that produced the exception. We can design our software so that the exception is caught and handled at this outer level. If it isn’t caught there, control returns to the method that called it. This process is called propagating the exception. K E Y C ON C E PT If an exception is not caught and handled where it occurs, it is propagated to the calling method. K E Y C ON C E PT A programmer must carefully consider how exceptions should be handled, if at all, and at what level. Exception propagation continues until the exception is caught and handled, or until it is propagated out of the main method, which terminates the program and produces an exception message. To catch an exception at an outer level, the method that produces the exception must be invoked inside a try block that has an appropriate catch clause to handle it. A programmer must pick the most appropriate level at which to catch and handle an exception. There is no single best answer. It depends on the situation and the design of the system. Sometimes the right approach will be not to catch an exception at all and let the program terminate. The manner in which exceptions are used is important to the definition of a software system. Exceptions could be thrown in many situations in a collection. Usually it’s best to throw exceptions whenever an invalid operation is attempted. For example, in the case of a stack, we will throw an exception whenever the user attempts to pop an element from an empty stack. The user then has the choice of checking the situation beforehand to avoid the exception: if (! theStack.isEmpty()) element = theStack.pop(); Or the user can use a try-catch statement to handle the situation when it does occur: try { element = theStack.pop() } catch (EmptyCollectionException exception) { System.out.println ("No elements available."); } 3.7 Implementing a Stack: WIth Arrays As we explore particular implementation techniques for a collection, we will also discuss the appropriate use of exceptions. 3.7 Implementing a Stack: With Arrays So far in our discussion of a stack collection we have described its basic conceptual nature and the operations that allow the user to interact with it. In software engineering terms, we would say that we have done the analysis for a stack collection. We have also used a stack, without knowing the details of how it was implemented, to solve a particular problem. Now let’s turn our attention to the implementation details. There are various ways to implement a class that represents a stack. In this section, we examine an implementation strategy that uses an array to store the objects contained in the stack. In the next chapter, we examine a second technique for implementing a stack. To explore this implementation, we must recall several key characteristics of Java arrays. The elements stored in an array are indexed from 0 to n–1, where n is the total number of cells in the array. An array is an object, which is instantiated separately from the objects it holds. And when we talk about an array of objects, we are actually talking about an array of references to objects, as pictured in Figure 3.9. 0 1 2 3 4 5 6 F I G U R E 3 . 9 An array of object references KEY CON CEPT The implementation of the collection operations should not affect the way users interact with the collection. 55 56 C HA PT ER 3 Collections Keep in mind the separation between the collection and the underlying data structure used to implement it. Our goal is to design an efficient implementation that provides the functionality of every operation defined in the stack abstract data type. The array is just a convenient data structure in which to store the objects. Managing Capacity When an array object is created, it is allocated a specific number of cells into which elements can be stored. For example, the following instantiation creates an array that can store 500 elements, indexed from 0 to 499: Object[] collection = Object[500]; The number of cells in an array is called its capacity. This value is stored in the length constant of the array. The capacity of an array cannot be changed once the array has been created. When using an array to implement a collection, we have to deal with the situation in which all cells of the array are being used to store elements. That is, because we are using a fixed-size data structure, at some point the data structure may become “full.” However, just because the data structure is full, should that mean that the collection is full? A crucial question in the design of a collection is what to do in the case in which a new element is added to a full data structure. Three basic options exist: ■ We could implement operations that add an element to the collection such that they throw an exception if the data structure is full. ■ We could implement the add operations to return a status indicator that can be checked by the user to see if the add operation was successful. ■ We could automatically expand the capacity of the underlying data structure whenever necessary so that, essentially, it would never become full. K E Y CO N C E PT How we handle exceptional conditions determines whether the collection or the user of the collection controls the particular behavior. In the first two cases, the user of the collection must be aware that the collection could get full and take steps to deal with it when needed. For these solutions we would also provide extra operations that allow the user to check to see if the collection is full and to expand the capacity of the data structure as desired. The advantage of these approaches is that it gives the user more control over the capacity. However, given our goal to separate the interface from the implementation, the third option is attractive. The capacity of the underlying data structure is an implementation detail that, in general, should be hidden from the user. Furthermore, the capacity issue is particular to this implementation. Other techniques used to 3.8 The ArrayStack Class implement the collection, such as the one we explore in the next chapter, are not restricted by a fixed capacity and therefore never have to deal with this issue. In the solutions presented in this book, we opt to implement fixed data structure solutions by automatically expanding the capacity of the underlying data structure. Occasionally, other options are explored as programming projects. 3.8 The ArrayStack Class In the Java Collections API framework, class names indicate both the underlying data structure and the collection. We follow that naming convention in this book. Thus, we define a class called ArrayStack to represent a stack with an underlying array-based implementation. To be more precise, we define a class called ArrayStack<T> that represents an array-based implementation of a stack collection that stores objects of generic type T. When we instantiate an ArrayStack object, we specify what the generic type T represents. An array implementation of a stack can be designed by making the following four assumptions: the array is an array of object references (type determined when the stack is instantiated), the bottom of the stack is always at index 0 of the array, the elements of the stack are stored in order and contiguously in the array, and there is an integer variable top that stores the index of the array immediately following the top element in the stack. KEY CON CEPT For efficiency, an array-based stack implementation keeps the bottom of the stack at index 0. Figure 3.10 illustrates this configuration for a stack that currently contains the elements A, B, C, and D, assuming that they have been pushed on in that order. To simplify the figure, the elements are shown in the array itself rather than as objects referenced from the array. Note that the variable top represents both the next cell into which a pushed element should be stored as well as the count of the number of elements currently in the stack. 0 1 2 3 A B C D 4 top 5 6 7 ... 4 F IG URE 3 .1 0 An array implementation of a stack 57 58 C HA PT ER 3 Collections In this implementation, the bottom of the stack is always held at index 0 of the array, and the stack grows and shrinks at the higher indexes. This is considerably more efficient than if the stack were reversed within the array. Consider the processing that would be necessary if the top of the stack were kept at index 0. From these assumptions, we can determine that our class will need a constant to store the default capacity, a variable to keep track of the top of the stack, and a variable for the array to store the stack. This results in the following class header. Note that our ArrayStack class will be part of the jss2 package and will make use of a package called jss2.exceptions. /** * @author Lewis and Chase * * Represents an array implementation of a stack. */ package jss2; import jss2.exceptions.*; public class ArrayStack<T> implements StackADT<T> { /** * constant to represent the default capacity of the array */ private final int DEFAULT_CAPACITY = 100; /** * int that represents both the number of elements and the next * available position in the array */ private int top; /** * array of generic elements to represent the stack */ private T[] stack; The Constructors Our class will have two constructors, one to use the default capacity and the other to use a specified capacity. 3.8 The ArrayStack Class /** * Creates an empty stack using the default capacity. */ public ArrayStack() { top = 0; stack = (T[])(new Object[DEFAULT_CAPACITY]); } /** * Creates an empty stack using the specified capacity. * @param initialCapacity represents the specified capacity */ public ArrayStack (int initialCapacity) { top = 0; stack = (T[])(new Object[initialCapacity]); } Just to refresh our memory, this is an excellent example of method overloading (i.e., two methods with the same name that differ only in the parameter list). From our previous discussion of generics we recall that you cannot instantiate a generic type. This also means that you cannot instantiate an array of a generic type. This results in an interesting line of code in both of our constructors: stack = (T[])(new Object[DEFAULT_CAPACITY]); Note that in this line, we are instantiating an array of Objects and then casting it as an array of our generic type. This will create a compile-time warning for an unchecked type conversion because the Java compiler cannot guarantee the type safety of this cast. As we have seen from our earlier discussion, it is worth this warning to gain the flexibility and safety of generics. The push Operation To push an element onto the stack, we simply insert it in the next available position in the array as specified by the variable top. Before doing so, however, we must determine if the array has reached its capacity and expand it if necessary. After storing the value, we must update the value of top so that it continues to represent the number of elements in the stack. Implementing these steps results in the following code: 59 60 C HA PT ER 3 Collections /** * Adds the specified element to the top of this stack, expanding * the capacity of the stack array if necessary. * @param element generic element to be pushed onto stack */ public void push (T element) { if (size() == stack.length) expandCapacity(); stack[top] = element; top++; } The expandCapacity method is implemented to double the size of the array as needed. Of course, since an array cannot be resized once it is instantiated, this method simply creates a new larger array and then copies the contents of the old array into the new one. It serves as a support method of the class and can therefore be implemented with private visibility. /** * Creates a * twice the */ private void { T[] larger new array to store the contents of this stack with capacity of the old one. expandCapacity() = (T[])(new Object[stack.length*2]); for (int index=0; index < stack.length; index++) larger[index] = stack[index]; stack = larger; } Figure 3.11 illustrates the result of pushing an element E onto the stack that was depicted in Figure 3.10. The push operation for the array implementation of a stack consists of the following steps: ■ Make sure that the array is not full. ■ Set the reference in position top of the array to the object being added to the stack. ■ Increment the values of top and count. 3.8 0 1 2 3 4 A B C D E top 5 6 7 The ArrayStack Class ... 5 FIG URE 3 .1 1 The stack after pushing element E Each of these steps is O(1). Thus the operation is O(1). We might wonder about the time complexity of the expandCapacity method and the impact it might have on the analysis of the push method. This method does contain a linear for loop and, intuitively, we would call that O(n). However, given how seldom the expandCapacity method is called relative to the number of times push may be called, we can amortize that complexity across all instances of push. The pop Operation The pop operation removes and returns the element at the top of the stack. For an array implementation, that means returning the element at index top ᎑ 1. Before attempting to return an element, however, we must ensure that there is at least one element in the stack to return. The array-based version of the pop operation can be implemented as follows: /** * Removes the element at the top of this stack and returns a * reference to it. Throws an EmptyCollectionException if the stack * is empty. * @return T element removed from top of stack * @throws EmptyCollectionException if a pop is attempted on empty stack */ public T pop() throws EmptyCollectionException { if (isEmpty()) throw new EmptyCollectionException("Stack"); top--; T result = stack[top]; stack[top] = null; return result; } 61 62 C HA PT ER 3 Collections 0 1 2 3 A B C D 4 top 5 6 7 ... 4 FIG URE 3 .1 2 The stack after popping the top element If the stack is empty when the pop method is called, an EmptyCollection Exception is thrown. Otherwise, the value of top is decremented and the element stored at that location is stored into a temporary variable so that it can be returned. That cell in the array is then set to null. Note that the value of top ends up with the appropriate value relative to the now smaller stack. Figure 3.12 illustrates the results of a pop operation on the stack from Figure 3.10, which brings it back to its earlier state (identical to Figure 3.10). The pop operation for the array implementation consists of the following steps: ■ Make sure the stack is not empty. ■ Decrement the top counter. ■ Set a temporary reference equal to the element in stack[top]. ■ Set stack[top] equal to null. ■ Return the temporary reference. All of these steps are also O(1). Thus, the pop operation for the array implementation has time complexity O(1). The peek Operation The peek operation returns a reference to the element at the top of the stack without removing it from the array. For an array implementation, that means returning a reference to the element at position top-1. This one step is O(1) and thus the peek operation is O(1) as well. 3.8 The ArrayStack Class /** * Returns a reference to the element at the top of this stack. * The element is not removed from the stack. Throws an * EmptyCollectionException if the stack is empty. * @return T element on top of stack * @throws EmptyCollectionException if a peek is attempted on empty stack */ public T peek() throws EmptyCollectionException { if (isEmpty()) throw new EmptyCollectionException("Stack"); return stack[top-1]; } Other Operations The isEmpty, size, and toString operations and their analysis are left as programming projects exercises, respectively. 63 64 C HA PT ER 3 Collections Summary of Key Concepts ■ A collection is an object that gathers and organizes other objects. ■ Elements in a collection are typically organized by the order of their addition to the collection or by some inherent relationship among the elements. ■ A collection is an abstraction where the details of the implementation are hidden. ■ A data structure is the underlying programming construct used to implement a collection. ■ Stack elements are processed in a LIFO manner—the last element in is the first element out. ■ A programmer should choose the structure that is appropriate for the type of data management needed. ■ Inheritance is the process of deriving a new class from an existing one. ■ One purpose of inheritance is to reuse existing software. ■ Inherited variables and methods can be used in the derived class as if they had been declared locally. ■ Inheritance creates an is-a relationship between all parent and child classes. ■ The child of one class can be the parent of one or more other classes, creating a class hierarchy. ■ Common features should be located as high in a class hierarchy as is reasonable, minimizing maintenance efforts. ■ All Java classes are derived, directly or indirectly, from the Object class. ■ The toString and equals methods are defined in the Object class and therefore are inherited by every class in every Java program. ■ A polymorphic reference can refer to different types of objects over time. ■ A reference variable can refer to any object created from any class related to it by inheritance. ■ A polymorphic reference uses the type of the object, not the type of the reference, to determine which version of a method to invoke. ■ A Java interface defines a set of abstract methods and is useful in separating the concept of an abstract data type from its implementation. ■ By using the interface name as a return type, the interface doesn’t commit the method to the use of any particular class that implements a stack. ■ A stack is the ideal data structure to use when evaluating a postfix expression. Summary of Key Concepts ■ Errors and exceptions represent unusual or invalid processing. ■ The messages printed by a thrown exception indicate the nature of the problem and provide a method call stack trace. ■ Each catch clause on a try statement handles a particular kind of exception that may be thrown within the try block. ■ If an exception is not caught and handled where it occurs, it is propagated to the calling method. ■ A programmer must carefully consider how exceptions should be handled, if at all, and at what level. ■ The implementation of the collection operations should not affect the way users interact with the collection. ■ How we handle exceptional conditions determines whether the collection or the user of the collection controls the particular behavior. ■ For efficiency, an array-based stack implementation keeps the bottom of the stack at index 0. Self-Review Questions SR 3.1 What is a collection? SR 3.2 What is a data type? SR 3.3 What is an abstract data type? SR 3.4 What is a data structure? SR 3.5 What is abstraction and what advantage does it provide? SR 3.6 Why is a class an excellent representation of an abstract data type? SR 3.7 What is the characteristic behavior of a stack? SR 3.8 What are the five basic operations on a stack? SR 3.9 What are some of the other operations that might be implemented for a stack? SR 3.10 Define inheritance. SR 3.11 Define polymorphism. SR 3.12 Given the example in Figure 3.5, list the subclasses of Mammal. SR 3.13 Given the example in Figure 3.5, will the following code compile? Animal creature = new Parrot(); 65 66 C HA PT ER 3 Collections SR 3.14 Given the example in Figure 3.5, will the following code compile? Horse creature = new Mammal(); SR 3.15 What is the purpose of Generics in the Java language? SR 3.16 What is the advantage of postfix notation? Exercises EX 3.1 Compare and contrast data types, abstract data types, and data structures. EX 3.2 List the collections in the Java Collections API and mark the ones that are covered in this text. EX 3.3 Define the concept of abstraction and explain why it is important in software development. EX 3.4 Hand trace a stack X through the following operations: X.push(new Integer(4)); X.push(new Integer(3)); Integer Y = X.pop(); X.push(new Integer(7)); X.push(new Integer(2)); X.push(new Integer(5)); X.push(new Integer(9)); Integer Y = X.pop(); X.push(new Integer(3)); X.push(new Integer(9)); EX 3.5 Given the resulting stack X from the previous exercise, what would be the result of each of the following? a. Y = X.peek(); b. Y = X.pop(); Z = X.peek(); c. Y = X.pop(); Z = X.peek(); EX 3.6 What should be the time complexity of the isEmpty(), size(), and toString() methods? EX 3.7 Show how the undo operation in a word processor can be supported by the use of a stack. Give specific examples and draw the contents of the stack after various actions are taken. Programming Projects EX 3.8 In the postfix expression evaluation example, the two most recent operands are popped when an operator is encountered so that the subexpression can be evaluated. The first operand popped is treated as the second operand in the subexpression, and the second operand popped is the first. Give and explain an example that demonstrates the importance of this aspect of the solution. EX 3.9 Draw an example using the five integers (12, 23, 1, 45, 9) of how a stack could be used to reverse the order (9, 45, 1, 23, 12) of these elements. EX 3.10 Explain what would happen to the algorithms and the time complexity of an array implementation of the stack if the top of the stack were at position 0. Programming Projects PP 3.1 Complete the implementation of the ArrayStack class presented in this chapter. Specifically, complete the implementations of the peek, isEmpty, size, and toString methods. PP 3.2 Design and implement an application that reads a sentence from the user and prints the sentence with the characters of each word backwards. Use a stack to reverse the characters of each word. PP 3.3 Modify the solution to the postfix expression evaluation problem so that it checks for the validity of the expression that is entered by the user. Issue an appropriate error message when an erroneous situation is encountered. PP 3.4 The array implementation in this chapter keeps the top variable pointing to the next array position above the actual top of the stack. Rewrite the array implementation such that stack[top] is the actual top of the stack. PP 3.5 There is a data structure called a drop-out stack that behaves like a stack in every respect except that if the stack size is n, when the n+1 element is pushed, the first element is lost. Implement a dropout stack using an array. (Hint: a circular array implementation would make sense.) PP 3.6 Implement an integer adder using three stacks. PP 3.7 Implement an infix-to-postfix translator using stacks. 67 68 C HA PT ER 3 Collections PP 3.8 Implement a class called reverse that uses a stack to output a set of elements input by the user in reverse order. PP 3.9 Create a graphical application that provides a button for push and pop from a stack, a text field to accept a string as input for push, and a text area to display the contents of the stack after each operation. Answers to Self-Review Questions SRA 3.1 A collection is an object that gathers and organizes other objects. SRA 3.2 A data type is a set of values and operations on those values defined within a programming language. SRA 3.3 An abstract data type is a data type that is not defined within the programming language and must be defined by the programmer. SRA 3.4 A data structure is the set of objects necessary to implement an abstract data type. SRA 3.5 Abstraction is the concept of hiding the underlying implementation of operations and data storage in order to simplify the use of a collection. SRA 3.6 Classes naturally provide abstraction since only those methods that provide services to other classes have public visibility. SRA 3.7 A stack is a last in, first out (LIFO) structure. SRA 3.8 The operations are: push—adds an element to the end of the stack pop—removes an element from the front of the stack peek—returns a reference to the element at the front of the stack isEmpty—returns true if the stack is empty, returns false otherwise size—returns the number of elements in the stack SRA 3.9 makeEmpty(), destroy(), full() SRA 3.10 Inheritance is the process in which a new class is derived from an existing one. The new class automatically contains some or all of the variables and methods in the original class. Then, to tailor the class as needed, the programmer can add new variables and methods to the derived class, or modify the inherited ones. SRA 3.11 The term polymorphism can be defined as “having many forms.” A polymorphic reference is a reference variable that can refer to Answers to Self-Review Questions different types of objects at different points in time. The specific method invoked through a polymorphic reference can change from one invocation to the next. SRA 3.12 The subclasses of Mammal are Horse and Bat. SRA 3.13 Yes, a reference variable of a parent class or any superclass may hold a reference to one of its descendants. SRA 3.14 No, a reference variable for a child or subclass may not hold a reference to a parent or superclass. To make this assignment, you would have to explicitly cast the parent class into the child class (Horse creature = (Horse)(new Mammal()); SRA 3.15 Beginning with Java 5.0, Java enables us to define a class based on a generic type. That is, we can define a class so that it stores, operates on, and manages objects whose type is not specified until the class is instantiated. This allows for the creation of structures that can manipulate “generic” elements and still provide type checking. SRA 3.16 Postfix notation avoids the need for precedence rules that are required to evaluate infix expressions. 69 This page intentionally left blank 4 Linked Structures T his chapter explores a technique for creating data struc- tures using references to create links between objects. CHAPTER OBJECTIVES ■ Describe the use of references to create linked structures ■ Compare linked structures to array-based structures ■ Explore the techniques for managing a linked list ■ Discuss the need for a separate node object to form linked structures ■ Implement a stack collection using a linked list Linked structures are fundamental in the development of software, especially the design and implementation of collections. This approach has both advantages and disadvantages when compared to a solution using arrays. 71 72 C HA PT ER 4 Linked Structures 4.1 KE Y C O N C E PT Object reference variables can be used to create linked structures. References as Links In Chapter 3, we discussed the concept of collections and explored one collection in particular: a stack. We defined the operations on a stack collection and designed an implementation using an underlying array-based data structure. In this chapter, we explore an entirely different approach to designing a data structure. A linked structure is a data structure that uses object reference variables to create links between objects. Linked structures are the primary alternative to an arraybased implementation of a collection. After discussing various issues involved in linked structures, we will define a new implementation of a stack collection that uses an underlying linked data structure and demonstrate its use. Recall that an object reference variable holds the address of an object, indicating where the object is stored in memory. The following declaration creates a variable called obj that is only large enough to hold the numeric address of an object: Object obj; Usually the specific address that an object reference variable holds is irrelevant. That is, while it is important to be able to use the reference variable to access an object, the specific location in memory where it is stored is unimportant. Therefore, instead of showing addresses, we usually depict a reference variable as a name that “points to” an object, as shown in Figure 4.1. A reference variable, used in this context, is sometimes called a pointer. Consider the situation in which a class defines as instance data a reference to another object of the same class. For example, suppose we have a class named Person that contains a person’s name, address, and other relevant information. Now suppose that in addition to this data, the Person class also contains a reference variable to another Person object: public class Person { private String name; private String address; private Person next; // a link to another Person object // whatever else } Using only this one class, a linked structure can be created. One Person object contains a link to a second Person object. This second object also contains a reference to a Person, which contains another, and so on. This type of object is sometimes called self-referential. 4.1 References as Links obj F I G U R E 4 . 1 An object reference variable pointing to an object This kind of relationship forms the basis of a linked list, which is a linked structure in which one object refers to the next, creating a linear ordering of the objects in the list. A linked list is depicted in Figure 4.2. Often the objects stored in a linked list are referred to generically as the nodes of the list. Note that a separate reference variable is needed to indicate the first node in the list. The list is terminated in a node whose next reference is null. KEY CON CEPT A linked list is composed of objects that each point to the next object in the list. A linked list is only one kind of linked structure. If a class is set up to have multiple references to objects, a more complex structure can be created, such as the one depicted in Figure 4.3. The way in which the links are managed dictates the specific organization of the structure. front F I G U R E 4 . 2 A linked list entry F I G U R E 4 . 3 A complex linked structure 73 74 C HA PT ER 4 Linked Structures K E Y C O N C E PT A linked list dynamically grows as needed and essentially has no capacity limitations. For now, we will focus on the details of a linked list. Many of these techniques apply to more complicated linked structures as well. Unlike an array, which has a fixed size, a linked list has no upper bound on its capacity other than the limitations of memory in the computer. A linked list is considered to be a dynamic structure because its size grows and shrinks as needed to accommodate the number of elements stored. In Java, all objects are created dynamically from an area of memory called the system heap, or free store. The next section explores some of the primary ways in which a linked list is managed. 4.2 Managing Linked Lists Keep in mind that our goal is to use linked lists and other linked structures to create collections. Because the principal purpose of a collection is to be able to add, remove, and access elements, we must first examine how to accomplish these fundamental operations using links. No matter what a linked list is used to store, there are a few basic techniques involved in managing the nodes in the list. Specifically, elements in the list are accessed, elements are inserted into a list, and elements are removed from the list. Accessing Elements Special care must be taken when dealing with the first node in the list so that the reference to the entire list is maintained appropriately. When using linked lists, we maintain a pointer to the first element in the list. To access other elements, we must access the first one and then follow the next pointer from that one to the next one and so on. Consider our previous example of a person class containing the attributes name, address, and next. If we wanted to find the fourth person in the list, and assuming that we had a variable first of type Person that pointed to the first person in the list and that the list contained at least four nodes, we might use the following code: Person current = first; for (int i = 0, i < 3, i++) current = current.next; After executing this code, current will point to the fourth person in the list. Notice that it is very important to create a new reference variable, in this case current, and then start by setting that reference variable to point to the first element 4.2 Managing Linked Lists in the list. Consider what would happen if we used the first pointer in the loop instead of current. Once we moved the first pointer to point to the second element in the list, we would no longer have a pointer to the first element and would not be able to access it. Keep in mind that with a linked list, the only way to access the elements in the list is to start with the first element and progress through the list. Of course, a more likely scenario would be that we would need to search our list for a particular person. Assuming that the Person class overrides the equals method such that it returns true if the given String matches the name stored for that person, then the following code will search the list for Tom Jones: String searchstring = "Tom Jones"; Person current = first; while ((not(current.equals(searchstring)) && (current.next != null)) current = current.next; Note that this loop will terminate when the string is found or when the end of the list is encountered. Now that we have seen how to access elements in a linked list, let’s consider how to insert elements into a list. Inserting Nodes A node may be inserted into a linked list at any location: at the front of the list, among the interior nodes in the middle of the list, or at the end of the list. Adding a node to the front of the list requires resetting the reference to the entire list, as shown in Figure 4.4. First, the next reference of the added node is set to point to the current first node in the list. Second, the reference to the front of the list is reset to point to the newly added node. node 2 front 1 F I G U R E 4 . 4 Inserting a node at the front of a linked list KEY CON CEPT The order in which references are changed is crucial to maintaining a linked list. 75 76 C HA PT ER 4 Linked Structures front current 2 1 node F I G U R E 4 . 5 Inserting a node in the middle of a linked list Note that difficulties would arise if these steps were reversed. If we were to reset the front reference first, we would lose the only reference to the existing list and it could not be retrieved. Inserting a node into the middle of a list requires some additional processing. First, we have to find the node in the list that will immediately precede the new node being inserted. Unlike an array, in which we can access elements using subscripts, a linked list requires that we use a separate reference to move through the nodes of the list until we find the one we want. This type of reference is often called current, because it indicates the current node in the list that is being examined. Initially, current is set to point to the first node in the list. Then a loop is used to move the current reference along the list of nodes until the desired node is found. Once it is found, the new node can be inserted, as shown in Figure 4.5. First, the next reference of the new node is set to point to the node following the one to which current refers. Then, the next reference of the current node is reset to point to the new node. Once again, the order of these steps is important. This process will work wherever the node is to be inserted along the list, including making it the new second node in the list or making it the last node in the list. If the new node is inserted immediately after the first node in the list, then current and front will refer to the same (first) node. If the new node is inserted at the end of the list, the next reference of the new node is set to null. The only special case occurs when the new node is inserted as the first node in the list. Deleting Nodes KE Y C O N C E PT Dealing with the first node in a linked list often requires special handling. Any node in the list can be deleted. We must maintain the integrity of the list no matter which node is deleted. As with the process of inserting a node, dealing with the first node in the list represents a special case. 4.2 Managing Linked Lists front F I G U R E 4 . 6 Deleting the first node in a linked list To delete the first node in a linked list, we reset the reference to the front of the list so that it points to the current second node in the list. This process is shown in Figure 4.6. If the deleted node is needed elsewhere, a separate reference to it must be set up before resetting the front reference. To delete a node from the interior of the list, we must first find the node in front of the node that is to be deleted. This processing often requires the use of two references: one to find the node to be deleted and another to keep track of the node immediately preceding that one. Thus, they are often called current and previous, as shown in Figure 4.7. Once these nodes have been found, the next reference of the previous node is reset to point to the node pointed to by the next reference of the current node. The deleted node can then be used as needed. Sentinel Nodes Thus far, we have described insertion into and deletion from a list as having two cases: the case when dealing with the first node and the KEY CON CEPT case when dealing with any other node. It is possible to eliminate Implementing a list with a sentinel the special case involving the first node by introducing a sentinel node or dummy node as the first node eliminates the special cases node or dummy node at the front of the list. A sentinel node serves dealing with the first node. as a false first node and doesn’t actually represent an element in the list. By using a sentinel node, all insertions and deletions will fall under the second case and the implementations will not have as many special situations to consider. front previous current F I G U R E 4 . 7 Deleting an interior node from a linked list 77 78 C HA PT ER 4 Linked Structures 4.3 Elements Without Links Now that we have explored some of the techniques needed to manage the nodes of a linked list, we can turn our attention to using a linked list as an alternative implementation approach for a collection. However, to do so we need to carefully examine one other key aspect of linked lists. We must separate the details of the linked list structure from the elements that the list stores. KE Y CO N C E PT Objects that are stored in a collection should not contain any implementation details of the underlying data structure. Earlier in this chapter we discussed the idea of a Person class that contains, among its other data, a link to another Person object. The flaw in this approach is that the self-referential Person class must be designed so that it “knows” it may become a node in a linked list of Person objects. This assumption is impractical, and it violates our goal of separating the implementation details from the parts of the system that use the collection. The solution to this problem is to define a separate node class that serves to link the elements together. A node class is fairly simple, containing only two important references: one to the next node in the linked list and another to the element that is being stored in the list. This approach is depicted in Figure 4.8. The linked list of nodes can still be managed using the techniques discussed in the previous section. The only additional aspect is that the actual elements stored in the list are accessed using a separate reference in the node objects. Doubly Linked Lists An alternative implementation for linked structures is the concept of a doubly linked list, as illustrated in Figure 4.9. In a doubly linked list, two references are maintained: one to point to the first node in the list and another to point to the last node in the list. Each node in the list stores both a reference to the next element and a reference to the previous one. If we were to use sentinel nodes with a doubly linked list, we would place sentinel nodes on both ends of the list. We discuss doubly linked lists further in Chapter 6. front F I G U R E 4 . 8 Using separate node objects to store and link elements Implementing a Stack: With Links rear of list front of list 4.4 F I G U R E 4 . 9 A doubly linked list 4.4 Implementing a Stack: With Links Let’s use a linked list to implement a stack collection, which was defined in Chapter 3. Note that we are not changing the way in which a stack works. Its conceptual nature remains the same, as does the set of operations defined for it. We are merely changing the underlying data structure used to implement it. The purpose of the stack, and the solutions it helps us to create, also remains the same. The postfix expression evaluation example from Chapter 3 used the ArrayStack<T> class, but any valid implementation of a stack could be used instead. Once we create the LinkedStack<T> class to define an alternative implementation, it could be substituted into the postfix expression solution without having to change anything but the class name. That is the beauty of abstraction. KEY CON CEPT Any implementation of a collection can be used to solve a problem as long as it validly implements the appropriate operations. In the following discussion, we show and discuss the methods that are important to understanding the linked list implementation of a stack. Some of the stack operations are left as programming projects. The LinkedStack Class The LinkedStack<T> class implements the StackADT<T> interface, just as the ArrayStack<T> class from Chapter 3 does. Both provide the operations defined for a stack collection. Because we are using a linked-list approach, there is no array in which we store the elements of the collection. Instead, we need only a single reference to the first node in the list. We will also maintain a count of the number of elements in the list. The header and class-level data of the LinkedStack<T> class is therefore: 79 80 C HA PT ER 4 Linked Structures /** * @author Lewis and Chase * * Represents a linked implementation of a stack. */ package jss2; import jss2.exceptions.*; import java.util.Iterator; public class LinkedStack<T> implements StackADT<T> { /** indicates number of elements stored */ private int count; /** pointer to top of stack */ private LinearNode<T> top; The LinearNode<T> class serves as the node class, containing a reference to the next LinearNode<T> in the list and a reference to the element stored in that node. Each node stores a generic type that is determined when the node is instantiated. In our LinkedStack<T> implementation, we simply use the same type for the node as used to define the stack. The LinearNode<T> class also contains methods to set and get the element values. The LinearNode<T> class is shown in Listing 4.1. Note that the LinearNode<T> class is not tied to the implementation of a stack collection. It can be used in any linear linked-list implementation of a collection. We will use it for other collections as needed. Using the LinearNode<T> class and maintaining a count of elements in the collection creates the implementation strategy depicted in Figure 4.10. KE Y CO N C E PT A linked implementation of a stack adds and removes elements from one end of the linked list. The constructor of the LinkedStack<T> class sets the count of elements to zero and sets the front of the list, represented by the variable top, to null. Note that because a linked-list implementation does not have to worry about capacity limitations, there is no need to create a second constructor as we did in the ArrayStack<T> class of Chapter 3. /** * Creates an empty stack. */ public LinkedStack() { count = 0; top = null; } 4.4 L I S T I N G Implementing a Stack: With Links 4 . 1 /** * @author Lewis and Chase * * Represents a node in a linked list. */ package jss2; public class LinearNode<T> { /** reference to next node in list */ private LinearNode<T> next; /** element stored at this node */ private T element; /** * Creates an empty node. */ public LinearNode() { next = null; element = null; } /** * Creates a node storing the specified element. * @param elem element to be stored */ public LinearNode (T elem) { next = null; element = elem; } /** * Returns the node that follows this one. * @return LinearNode<T> reference to next node */ public LinearNode<T> getNext() { return next; } 81 82 C HA PT ER 4 L I S T I N G Linked Structures 4 . 1 continued /** * Sets the node that follows this one. * @param node node to follow this one */ public void setNext (LinearNode<T> node) { next = node; } /** * Returns the element stored in this node. * @return T element stored at this node */ public T getElement() { return element; } /** * Sets the element stored in this node. * @param elem element to be stored at this node */ public void setElement (T elem) { element = elem; } } count: 6 top FIG URE 4 .1 0 A linked implementation of a stack collection 4.4 Implementing a Stack: With Links Because the nature of a stack is to only allow elements to be added or removed from one end, we will only need to operate on one end of our linked list. We could choose to push the first element into the first position in the linked list, the second element into the second position, etc. This would mean that the top of the stack would always be at the tail end of the list. However, if we consider the efficiency of this strategy, we realize that this would mean that we would have to traverse the entire list on every push and every pop operation. Instead, we can choose to operate on the front of the list making the front of the list the top of the stack. In this way, we do not have to traverse the list for either the push or pop operations. Figure 4.11 illustrates this configuration for a stack containing four elements, A, B, C, and D, that have been pushed onto the stack in that order. Let’s explore the implementation of the stack operations for the LinkedStack class. The push Operation Every time a new element is pushed onto the stack, a new LinearNode object must be created to store it in the linked list. To position the newly created node at the top of the stack, we must set its next reference to the current top of the stack, and reset the top reference to point to the new node. We must also increment the count variable. top D C count B A 4 F IG URE 4 .1 1 A linked implementation of a stack 83 84 C HA PT ER 4 Linked Structures Implementing these steps results in the following code: /** * Adds the specified element to the top of this stack. * @param element element to be pushed on stack */ public void push (T element) { LinearNode<T> temp = new LinearNode<T> (element); temp.setNext(top); top = temp; count++; } Figure 4.12 shows the result of pushing the element E onto the stack depicted in Figure 4.11. The push operation for the linked implementation of a stack consists of the following steps: ■ Create a new node containing a reference to the object to be placed on the stack. ■ Set the next reference of the new node to point to the current top of the stack (which will be null if the stack is empty). ■ Set the top reference to point to the new node. ■ Increment the count of elements in the stack. All of these steps have time complexity O(1) because they require only one processing step regardless of the number of elements already in the stack. Each of top E D count C B A 5 FIG URE 4 .1 2 The stack after pushing element E 4.4 Implementing a Stack: With Links these steps would have to be accomplished once for each of the elements to be pushed. Thus, using this method, the push operation would be O(1). The pop Operation The pop operation is implemented by returning a reference to the element currently stored at the top of the stack and adjusting the top reference to the new top of the stack. Before attempting to return any element, however, we must first ensure that there is at least one element to return. This operation can be implemented as follows: /** * Removes the element at the top of this stack and returns a * reference to it. Throws an EmptyCollectionException if the stack * is empty. * @return T element from top of stack * @throws EmptyStackException on pop from empty stack */ public T pop() throws EmptyCollectionException { if (isEmpty()) throw new EmptyCollectionException(“Stack”); T result = top.getElement(); top = top.getNext(); count--; return result; } If the stack is empty, as determined by the isEmpty method, an EmptyCollectionException is thrown. If there is at least one element to pop, it is stored in a temporary variable so that it can be returned. Then the reference to the top of the stack is set to the next element in the list, which is now the new top of the stack. The count of elements is decremented as well. Figure 4.13 illustrates the result of a pop operation on the stack from Figure 4.12. Notice that this figure is identical to our original configuration in Figure 4.11. This illustrates the fact that the pop operation is the inverse of the push operation. The pop operation for the linked implementation consists of the following steps: ■ Make sure the stack is not empty. ■ Set a temporary reference equal to the element on top of the stack. 85 86 C HA PT ER 4 Linked Structures top D C count B A 4 FIG URE 4 .1 3 The stack after a pop operation ■ Set the top reference equal to the next reference of the node at the top of the stack. ■ Decrement the count of elements in the stack. ■ Return the element pointed to by the temporary reference. As with our previous examples, each of these operations consists of a single comparison or a simple assignment and is therefore O(1). Thus, the pop operation for the linked implementation is O(1). Other Operations Using a linked implementation, the peek operation is implemented by returning a reference to the element pointed to by the node pointed to by the top pointer. The isEmpty operation returns true if the count of elements is 0, and false otherwise. The size operation simply returns the count of elements in the stack. The toString operation can be implemented by simply traversing the linked list. These operations are left as programming projects. 4.5 Using Stacks: Traversing a Maze Another classic use of a stack data structure is to keep track of alternatives in maze traversal or other similar algorithms that involve trial and error. Suppose 4.5 Using Stacks: Traversing a Maze that we build a grid as a two-dimensional array of ints where each number represents either a path (1) or a wall (0) in a maze: private int [][] grid = { {1,1,1,0,1,1,0,0,0,1,1,1,1}, {1,0,0,1,1,0,1,1,1,1,0,0,1}, {1,1,1,1,1,0,1,0,1,0,1,0,0}, {0,0,0,0,1,1,1,0,1,0,1,1,1}, {1,1,1,0,1,1,1,0,1,0,1,1,1}, {1,0,1,0,0,0,0,1,1,1,0,0,1}, {1,0,1,1,1,1,1,1,0,1,1,1,1}, {1,0,0,0,0,0,0,0,0,0,0,0,0}, {1,1,1,1,1,1,1,1,1,1,1,1,1} }; Our goal is to start in the top-left corner of this grid and traverse to the bottomright corner of this grid, traversing only positions that are marked as a path. Valid moves will be those that are within the bounds of the grid and are to cells in the grid marked with a 1. We will mark our path as we go by changing the 1’s to 3’s, and we will push only valid moves onto the stack. Starting in the top-left corner, we have two valid moves: down and right. We push these moves onto the stack, pop the top move off of the stack (right), and then move to that location. This means that we moved right one position: {3,3,1,0,1,1,0,0,0,1,1,1,1} {1,0,0,1,1,0,1,1,1,1,0,0,1} {1,1,1,1,1,0,1,0,1,0,1,0,0} {0,0,0,0,1,1,1,0,1,0,1,1,1} {1,1,1,0,1,1,1,0,1,0,1,1,1} {1,0,1,0,0,0,0,1,1,1,0,0,1} {1,0,1,1,1,1,1,1,0,1,1,1,1} {1,0,0,0,0,0,0,0,0,0,0,0,0} {1,1,1,1,1,1,1,1,1,1,1,1,1} We now have only one valid move. We push that move onto the stack, pop the top element off of the stack (right), and then move to that location. Again we moved right one position: {3,3,3,0,1,1,0,0,0,1,1,1,1} {1,0,0,1,1,0,1,1,1,1,0,0,1} {1,1,1,1,1,0,1,0,1,0,1,0,0} {0,0,0,0,1,1,1,0,1,0,1,1,1} {1,1,1,0,1,1,1,0,1,0,1,1,1} {1,0,1,0,0,0,0,1,1,1,0,0,1} {1,0,1,1,1,1,1,1,0,1,1,1,1} {1,0,0,0,0,0,0,0,0,0,0,0,0} {1,1,1,1,1,1,1,1,1,1,1,1,1} 87 88 C HA PT ER 4 Linked Structures From this position, we do not have any valid moves. At this point, however, our stack is not empty. Keep in mind that we still have a valid move on the stack left from the first position. We pop the next (and currently last) element off of the stack (down from the first position). We move to that position, push the valid move(s) from that position onto the stack, and continue processing. Using a stack in this way is actually simulating recursion, a process whereby a method calls itself either directly or indirectly. Recursion, which we will discuss in greater detail in Chapter 7, uses the concept of a program stack. A program stack (or run-time stack) is used to keep track of methods that are invoked. Every time a method is called, an activation record that represents the invocation is created and pushed onto the program stack. Therefore, the elements on the stack represent the series of method invocations that occurred to reach a particular point in an executing program. For example, when the main method of a program is called, an activation record for it is created and pushed onto the program stack. When main calls another method (say m2), an activation record for m2 is created and pushed onto the stack. If m2 calls method m3, then an activation record for m3 is created and pushed onto the stack. When method m3 terminates, its activation record is popped off of the stack and control returns to the calling method (m2), which is now on the top of the stack. If an exception occurs during the execution of a Java program, the programmer can examine the call stack trace to see what method the problem occurred within and what method calls were made to arrive at that point. K E Y C O N C E PT Recursive processing can be simulated using a stack to keep track of the appropriate data. An activation record contains various administrative data to help manage the execution of the program. It also contains a copy of the method’s data (local variables and parameters) for that invocation of the method. Because of the relationship between stacks and recursion, we can always rewrite a recursive program into a nonrecursive program that uses a stack. Instead of using recursion to keep track of the data, we can create our own stack to do so. Listings 4.2 and 4.3 illustrate the Maze and MazeSearch classes that implement our stack-based solution to traversing a maze. We will revisit this same example in our discussion of recursion in Chapter 7. This solution uses a class called Position to encapsulate the coordinates of a position within the maze. The traverse method loops, popping the top position off of the stack, marking it as tried, and then testing to see if we are done. If we are not done, then all of the valid moves from this position are pushed onto the stack and the loop continues. A private method called pushNewPos has been created to handle the task of putting the valid moves from the current position onto the stack: 4.5 Using Stacks: Traversing a Maze private StackADT<Position> push_new_pos(int x, int y, StackADT<Position> stack) { Position npos = new Position(); npos.setx(x); npos.sety(y); if (valid(npos.getx(),npos.gety())) stack.push(npos); return stack; } L I S T I N G 4 . 2 /** * @author Lewis and Chase * * Represents a maze of characters. The goal is to get from the * top left corner to the bottom right, following a path of 1’s. */ import jss2.*; public class Maze { /** * constant to represent tried paths */ private final int TRIED = 3; /** * constant to represent the final path */ private final int PATH = 7; /** * two dimensional array representing the grid */ private int [][] grid = { {1,1,1,0,1,1,0,0,0,1,1,1,1}, {1,0,0,1,1,0,1,1,1,1,0,0,1}, {1,1,1,1,1,0,1,0,1,0,1,0,0}, {0,0,0,0,1,1,1,0,1,0,1,1,1}, {1,1,1,0,1,1,1,0,1,0,1,1,1}, {1,0,1,0,0,0,0,1,1,1,0,0,1}, {1,0,1,1,1,1,1,1,0,1,1,1,1}, {1,0,0,0,0,0,0,0,0,0,0,0,0}, {1,1,1,1,1,1,1,1,1,1,1,1,1} }; 89 90 C HA PT ER 4 L I S T I N G Linked Structures 4 . 2 continued /** * push a new attempted move onto the stack * @param x represents x coordinate * @param y represents y coordinate * @param stack the working stack of moves within the grid * @return StackADT<Position> stack of moves within the grid */ private StackADT<Position> push_new_pos(int x, int y, StackADT<Position> stack) { Position npos = new Position(); npos.setx(x); npos.sety(y); if (valid(npos.getx(),npos.gety())) stack.push(npos); return stack; } /** * Attempts to iteratively traverse the maze. It inserts special * characters indicating locations that have been tried and that * eventually become part of the solution. This method uses a * stack to keep track of the possible moves that could be made. * @return boolean returns true if the maze is successfully traversed */ public boolean traverse () { boolean done = false; Position pos = new Position(); Object dispose; StackADT<Position> stack = new LinkedStack<Position> (); stack.push(pos); while (!(done)) { pos = stack.pop(); grid[pos.getx()][pos.gety()] = TRIED; // this cell has been tried if (pos.getx() == grid.length-1 && pos.gety() == grid[0].length-1) done = true; // the maze is solved else { stack = push_new_pos(pos.getx(),pos.gety() - 1, stack); stack = push_new_pos(pos.getx(),pos.gety() + 1, stack); 4.5 L I S T I N G 4 . 2 Using Stacks: Traversing a Maze continued stack = push_new_pos(pos.getx() - 1,pos.gety(), stack); stack = push_new_pos(pos.getx() + 1,pos.gety(), stack); } } return done; } /** * Determines if a specific location is valid. * @param row int representing y coordinate * @param column int representing x coordinate * @return boolean true if the given coordinate is a valid move */ private boolean valid (int row, int column) { boolean result = false; /** Check if cell is in the bounds of the matrix */ if (row >= 0 && row < grid.length && column >= 0 && column < grid[row].length) /** Check if cell is not blocked and not previously tried */ if (grid[row][column] == 1) result = true; return result; } /** * Returns the maze as a string. * @return String representation of the maze grid */ public String toString () { String result = "\n"; for (int row=0; row < grid.length; row++) { for (int column=0; column < grid[row].length; column++) 91 92 C HA PT ER 4 L I S T I N G Linked Structures 4 . 2 continued result += grid[row][column] + ""; result += "\n"; } return result; } } L I S T I N G 4 . 3 /** * @author Lewis and Chase * * Demonstrates a simulation of recursion using a stack. */ public class MazeSearch { /** * Creates a new maze, prints its original form, attempts to * solve it, and prints out its final form. * @param args array of Strings */ public static void main (String[] args) { Maze labyrinth = new Maze(); System.out.println (labyrinth); if (labyrinth.traverse ()) System.out.println (“The maze was successfully traversed!”); else System.out.println (“There is no possible path.”); System.out.println (labyrinth); } } The UML description for the maze problem is left as an exercise. 4.6 Implementing Stacks: The java.util.Stack Class 4.6 Implementing Stacks: The java.util.Stack Class The class java.util.Stack is an implementation of a stack provided in the Java Collections API framework. This implementation provides either the same or similar operations to the ones that we have been discussing: ■ The push operation accepts a parameter item that is a reference to an object to be placed on the stack. ■ The pop operation removes the object on top of the stack and returns a reference to it. ■ The peek operation returns a reference to the object on top of the stack. ■ The empty operation behaves the same as the isEmpty operation that we have been discussing. ■ The size operation returns the number of elements in the stack. The java.util.Stack class is derived from the Vector class and uses its inherited capabilities to store the elements in the stack. Because this implementation is built upon a vector, it exhibits the characteristics of both a vector and a stack. This implementation keeps track of the top of the stack using an index similar to the array implementation and thus does not require the additional overhead of storing a next reference in each node. Further, like the linked implementation, the java.util.Stack implementation allocates additional space only as needed. Unique Operations The java.util.Stack class provides an additional operation called search. Given an object to search for, the search operation returns the distance from the top of the stack to the first occurrence of that object on the stack. If the object is found at the top of the stack, the search method returns KEY CON CEPT the value 1. If the object is not found on the stack, search returns The java.util.Stack class is derived from Vector, which gives a the value ᎑ 1. stack inappropriate operations. Unfortunately, because the java.util.Stack implementation is derived from the Vector class, quite a number of other operations are inherited from the Vector class that are available for use. In some cases, these additional capabilities violate the basic assumptions of a stack. Most software engineers consider this a bad use of inheritance. Because a stack is not everything a vector is (conceptually), the Stack class should not be derived from the Vector class. Well-disciplined developers can, of course, limit themselves to only those operations appropriate to a stack. 93 94 C HA PT ER 4 Linked Structures Inheritance and Implementation The class java.util.Stack is an extension of the class java.util.Vector, which is an extension of java.util.AbstractList, which is an extension of java.util.AbstractCollection, which is an extension of java.lang. Object. The java.util.Stack class implements the cloneable, collection, list, and serializable interfaces. These relationships are illustrated in the UML diagram of Figure 4.14. java.lang.Object java.util.AbstractCollection java.util.AbstractList java.util.Vector <<interface>> Cloneable java.util.Stack push () pop() peek() isEmpty() size() search () FIG URE 4 .1 4 <<interface>> Collection <<interface>> List <<interface>> Serializable A UML description of the java.util.Stack class Self-Review Questions Summary of Key Concepts ■ Object reference variables can be used to create linked structures. ■ A linked list is composed of objects that each point to the next object in the list. ■ A linked list dynamically grows as needed and essentially has no capacity limitations. ■ The order in which references are changed is crucial to maintaining a linked list. ■ Dealing with the first node in a linked list often requires special handling. ■ Implementing a list with a sentinel node or dummy node as the first node eliminates the special cases dealing with the first node. ■ Objects that are stored in a collection should not contain any implementation details of the underlying data structure. ■ Any implementation of a collection can be used to solve a problem as long as it validly implements the appropriate operations. ■ A linked implementation of a stack adds and removes elements from one end of the linked list. ■ Recursive processing can be simulated using a stack to keep track of the appropriate data. ■ The java.util.Stack class is derived from Vector, which gives a stack inappropriate operations. Self-Review Questions SR 4.1 How do object references help us define data structures? SR 4.2 Compare and contrast a linked list and an array. SR 4.3 What special case exists when managing linked lists? SR 4.4 Why should a linked list node be separate from the element stored on the list? SR 4.5 What do the LinkedStack<T> and ArrayStack<T> classes have in common? SR 4.6 What would be the time complexity of the push operation if we chose to push at the end of the list instead of the front? SR 4.7 What is the difference between a doubly linked list and a singly linked list? 95 96 C HA PT ER 4 Linked Structures SR 4.8 What impact would the use of sentinel nodes or dummy nodes have upon a doubly linked list implementation? SR 4.9 What are the advantages to using a linked implementation as opposed to an array implementation? SR 4.10 What are the advantages to using an array implementation as opposed to a linked implementation? SR 4.11 What are the advantages of the java.util.Stack implementation of a stack? SR 4.12 What is the potential problem with the java.util.Stack implementation? SR 4.13 What is the advantage of postfix notation? Exercises EX 4.1 Explain what will happen if the steps depicted in Figure 4.4 are reversed. EX 4.2 Explain what will happen if the steps depicted in Figure 4.5 are reversed. EX 4.3 Draw a UML diagram showing the relationships among the classes involved in the linked list implementation of a set. EX 4.4 Write an algorithm for the add method that will add at the end of the list instead of the beginning. What is the time complexity of this algorithm? EX 4.5 Modify the algorithm from the previous exercise so that it makes use of a rear reference. How does this affect the time complexity of this and the other operations? EX 4.6 Discuss the effect on all the operations if there were not a count variable in the implementation. EX 4.7 Discuss the impact (and draw an example) of using a sentinel node or dummy node at the head of the list. EX 4.8 Draw the UML class diagram for the iterative maze solver example from this chapter. Programming Projects PP 4.1 Complete the implementation of the LinkedStack<T> class by providing the definitions for the size, isEmpty, and toString methods. Answers to Self-Review Questions PP 4.2 Modify the postfix program from Chapter 3 so that it uses the LinkedStack<T> class instead of the ArrayStack<T> class. PP 4.3 Create a new version of the LinkedStack<T> class that makes use of a dummy record at the head of the list. PP 4.4 Create a simple graphical application that will allow a user to perform push, pop, and peek operations on a stack and display the resulting stack (using toString) in a text area. PP 4.5 Design and implement an application that reads a sentence from the user and prints the sentence with the characters of each word backwards. Use a stack to reverse the characters of each word. PP 4.6 Complete the solution to the iterative maze solver so that your solution marks the successful path. PP 4.7 The linked implementation in this chapter uses a count variable to keep track of the number of elements in the stack. Rewrite the linked implementation without a count variable. PP 4.8 There is a data structure called a drop-out stack that behaves like a stack in every respect except that if the stack size is n, when the n+1 element is pushed, the first element is lost. Implement a dropout stack using links. Answers to Self-Review Questions SRA 4.1 An object reference can be used as a link from one object to another. A group of linked objects can form a data structure, such as a linked list, on which a collection can be based. SRA 4.2 A linked list has no capacity limitations, while an array does. However, arrays provide direct access to elements using indexes, whereas a linked list must be traversed one element at a time to reach a particular point in the list. SRA 4.3 The primary special case in linked list processing occurs when dealing with the first element in the list. A special reference variable is maintained that specifies the first element in the list. If that element is deleted, or a new element is added in front of it, the front reference must be carefully maintained. SRA 4.4 It is unreasonable to assume that every object that we may want to put in a collection can be designed to cooperate with the collection implementation. Furthermore, the implementation details are supposed to be kept distinct from the user of the collection, including the elements the user chooses to add to the collection. 97 98 C HA PT ER 4 Linked Structures SRA 4.5 Both the LinkedStack<T> and ArrayStack<T> classes implement the StackADT<T> interface. This means that they both represent a stack collection, providing the necessary operations needed to use a stack. Though they both have distinct approaches to managing the collection, they are functionally interchangeable from the user’s point of view. SRA 4.6 To push at the end of the list, we would have to traverse the list to reach the last element. This traversal would cause the time complexity to be O(n). An alternative would be to modify the solution to add a rear reference that always pointed to the last element in the list. This would help the time complexity for add but would have consequences if we try to remove the last element. SRA 4.7 A singly linked list maintains a reference to the first element in the list and then a next reference from each node to the following node in the list. A doubly linked list maintains two references: front and rear. Each node in the doubly linked list stores both a next and a previous reference. SRA 4.8 It would take two dummy records in a doubly linked list, one at the front and one at the rear, to eliminate the special cases when dealing with the first and last node. SRA 4.9 A linked implementation allocates space only as it is needed and has a theoretical limit of the size of the hardware. SRA 4.10 An array implementation uses less space per object since it only has to store the object and not an extra pointer. However, the array implementation will allocate much more space than it needs initially. SRA 4.11 Because the java.util.Stack implementation is an extension of the Vector class, it can keep track of the positions of elements in the stack using an index and thus does not require each node to store an additional pointer. This implementation also allocates space only as it is needed, like the linked implementation. SRA 4.12 The java.util.Stack implementation is an extension of the Vector class and thus inherits a large number of operations that violate the basic assumptions of a stack. SRA 4.13 Postfix notation avoids the need for precedence rules that are required to evaluate infix expressions. 5 Queues A queue is another collection with which we are inher- ently familiar. A queue is a waiting line, such as a line of customers waiting in a bank for their opportunity to talk to CHAPTER OBJECTIVES ■ Examine queue processing ■ Define a queue abstract data type ■ Demonstrate how a queue can be used to solve problems ■ Examine various queue implementations ■ Compare queue implementations a teller. In fact, in many countries the word queue is used habitually in this way. In such countries, a person might say “join the queue” rather than “get in line.” Other examples of queues include a checkout line at the grocery store or cars waiting at a stoplight. In any queue, an item enters on one end and leaves from the other. Queues have a variety of uses in computer algorithms. 99 100 C HA PT ER 5 Queues 5.1 A Queue ADT A queue is a linear collection whose elements are added on one end and removed from the other. Therefore, we say that queue elements are processed in a first in, first out (FIFO) manner. Elements are removed from a queue in the same order in which they are placed on the queue. This is consistent with the general concept of a waiting line. When a customer arrives at a bank, he or she begins waiting at the K E Y CO N C E PT end of the line. When a teller becomes available, the customer at the Queue elements are processed in a beginning of the line leaves the line to receive service. Eventually FIFO manner—the first element in is the first element out. every customer that started out at the end of the line moves to the front of the line and exits. For any given set of people, the first person to get in line is the first person to leave it. The processing of a queue is pictured in Figure 5.1. Usually a queue is depicted horizontally. One end is established as the front of the queue and the other as the rear of the queue. Elements go onto the rear of the queue and come off of the front. Sometimes the front of the queue is called the head and the rear of the queue the tail. Compare and contrast the processing of a queue to the LIFO (last in, first out) processing of a stack, which was discussed in Chapters 3 and 4. In a stack, the processing occurs at only one end of the collection. In a queue, processing occurs at both ends. The operations defined for a queue ADT are listed in Figure 5.2. The term enqueue is used to refer to the process of adding a new element to the end of a queue. Likewise, dequeue refers to removing the element at the front of a queue. The first operation allows the user to examine the element at the front of the queue without removing it from the collection. front of queue rear of queue Remember that naming conventions are not universal for collection operations. Sometimes enqueue is simply called add or insert. The dequeue operation is Adding an element Removing an element F I G U R E 5 . 1 A conceptual view of a queue 5.1 Operation Description enqueue dequeue first isEmpty size toString Adds an element to the rear of the queue. Removes an element from the front of the queue. Examines the element at the front of the queue. Determines if the queue is empty. Determines the number of elements on the queue. Returns a string representation of the queue. F I G U R E 5 . 2 The operations on a queue sometimes called remove or serve. The first operation is sometimes called front. Note that there is a general similarity between the operations of a queue and those of a stack. The enqueue, dequeue, and first operations correspond to the stack operations push, pop, and peek. Similar to a stack, there are no operations that allow the user to “reach into” the middle of a queue and reorganize or remove elements. If that type of processing is required, perhaps the appropriate collection to use is a list of some kind, such as those discussed in the next chapter. As we did with stacks, we define a generic QueueADT interface that represents the queue operations, separating the general purpose of the operations from the variety of ways they could be implemented. A Java version of the QueueADT interface is shown in Listing 5.1, and its UML description is shown in Figure 5.3. Note that in addition to the standard queue operations, we have also included a toString method, as we did with our stack collection. It is included for convenience and is not generally considered a classic operation on a queue. <<interface>> QueueADT enqueue() dequeue() first() isEmpty() size() toString() F I G U R E 5 . 3 The QueueADT interface in UML A Queue ADT 101 102 C HA PT ER 5 L I S T I N G Queues 5 . 1 /** * QueueADT defines the interface to a queue collection. * * @author Dr. Lewis * @author Dr. Chase * @version 1.0, 8/12/08 */ package jss2; public interface QueueADT<T> { /** * Adds one element to the rear of this queue. * * @param element the element to be added to the rear of this queue */ public void enqueue (T element); /** * Removes and returns the element at the front of this queue. * * @return the element at the front of this queue */ public T dequeue(); /** * Returns without removing the element at the front of this queue. * * @return the first element in this queue */ public T first(); /** * Returns true if this queue contains no elements. * * @return true if this queue is empty */ public boolean isEmpty(); /** * Returns the number of elements in this queue. * * @return the integer representation of the size of this queue */ 5.2 L I S T I N G 5 . 1 Using Queues: Code Keys continued public int size(); /** * Returns a string representation of this queue. * * @return the string representation of this queue */ public String toString(); } Queues have a wide variety of application within computing. Whereas the principle purpose of a stack is to reverse order, the principle purpose of a queue is to preserve order. Before exploring various ways to implement a queue, let’s examine some ways in which a queue can be used to solve problems. 5.2 Using Queues: Code Keys A Caesar cipher is a simple approach to encoding messages by shifting each letter in a message along the alphabet by a constant amount k. For example, if k equals 3, then in an encoded message, each letter is shifted three characters forward: a is replaced with d, b with e, c with f, and so on. The end of the alphabet wraps back around to the beginning. Thus, w is replaced with z, x with a, y with b, and z with c. To decode the message, each letter is shifted the same number of characters backwards. Therefore, if k equals 3, the encoded message vlpsolflwb iroorzv frpsohalwb would be decoded into simplicity follows complexity Julius Caesar actually used this type of cipher in some of his secret government correspondence (hence the name). Unfortunately, the Caesar cipher is fairly easy to break. There are only 26 possibilities for shifting the characters, and the code can be broken by trying various key values until one works. An improvement can be made to this encoding technique if we use a repeating key. Instead of shifting each character by a constant amount, we can shift each character by a different amount using a list of key values. If the message is longer 103 104 C HA PT ER 5 Queues Encoded Message: n o v a n j g h l m u u r x l v Key: 3 1 7 4 2 5 3 1 7 4 2 5 3 1 7 4 Decoded Message: k n o w l e d g e i s p o w e r F I G U R E 5 . 4 An encoded message using a repeating key than the list of key values, we just start using the key over again from the beginning. For example, if the key values are 3 1 7 4 2 5 then the first character is shifted by three, the second character by one, the third character by seven, etc. After shifting the sixth character by five, we start using the key over again. The seventh character is shifted by three, the eighth by one, etc. K E Y CO N C E PT Figure 5.4 shows the message “knowledge is power” encoded using this repeating key. Note that this encryption approach encodes the same letter into different characters, depending on where it occurs in the message and thus which key value is used to encode it. Conversely, the same character in the encoded message is decoded into different characters. A queue is a convenient collection for storing a repeating code key. L I S T I N G The program in Listing 5.2 uses a repeating key to encode and decode a message. The key of integer values is stored in a queue. After a key value is used, it is put back on the end of the queue so that the 5 . 2 /** * Codes demonstrates the use of queues to encrypt and decrypt messages. * * @author Dr. Lewis * @author Dr. Chase * @version 1.0, 08/12/08 */ import jss2.CircularArrayQueue; public class Codes { /** * Encode and decode a message using a key of values stored in * a queue. */ 5.2 L I S T I N G 5 . 2 Using Queues: Code Keys continued public static void main (String[] args) { int[] key = {5, 12, -3, 8, -9, 4, 10}; Integer keyValue; String encoded = "", decoded = ""; String message = "All programmers are playwrights and all " + "computers are lousy actors."; CircularArrayQueue<Integer> keyQueue1 = new CircularArrayQueue<Integer>(); CircularArrayQueue<Integer> keyQueue2 = new CircularArrayQueue<Integer>(); /** load key queue */ for (int scan=0; scan < key.length; scan++) { keyQueue1.enqueue (new Integer(key[scan])); keyQueue2.enqueue (new Integer(key[scan])); } // encode message for (int scan=0; scan < message.length(); scan++) { keyValue = keyQueue1.dequeue(); encoded += (char) ((int)message.charAt(scan) + keyValue.intValue()); keyQueue1.enqueue (keyValue); } System.out.println ("Encoded Message:\n" + encoded + "\n"); // decode message for (int scan=0; scan < encoded.length(); scan++) { keyValue = keyQueue2.dequeue(); decoded += (char) ((int)encoded.charAt(scan) - keyValue.intValue()); keyQueue2.enqueue (keyValue); } System.out.println ("Decoded Message:\n" + decoded); } } 105 106 C HA PT ER 5 Queues LinkedQueue <<interface>> QueueADT front rear enqueue() dequeue() first() isEmpty() size() toString enqueue() dequeue() first() isEmpty() size() toString Codes main(String[] args) F I G U R E 5 . 5 UML description of the Codes program key continually repeats as needed for long messages. The key in this example uses both positive and negative values. Figure 5.5 illustrates the UML description of the Codes class. This program actually uses two copies of the key stored in two separate queues. The idea is that the person encoding the message has one copy of the key, and the person decoding the message has another. Two copies of the key are helpful in this program as well because the decoding process needs to match up the first character of the message with the first value in the key. Also, note that this program doesn’t bother to wrap around the end of the alphabet. It encodes any character in the Unicode character set by shifting it to some other position in the character set. Therefore, we can encode any character, including uppercase letters, lowercase letters, and punctuation. Even spaces get encoded. Using a queue to store the key makes it easy to repeat the key by putting each key value back onto the queue as soon as it is used. The nature of a queue keeps the key values in the proper order, and we don’t have to worry about reaching the end of the key and starting over. 5.3 5.3 Using Queues: Ticket Counter Simulation Using Queues: Ticket Counter Simulation Let’s look at another example using queues. Consider the situation in which you are waiting in line to purchase tickets at a movie theatre. In general, the more cashiers there are, the faster the line moves. The theatre manager wants to keep his customers happy, but he doesn’t want to employ any more cashiers than necessary. Suppose the manager wants to keep the total time needed by KEY CON CEPT a customer to less than seven minutes. Being able to simulate the efSimulations are often implemented fect of adding more cashiers during peak business hours allows the using queues to represent waiting manager to plan more effectively. And, as we’ve discussed, a queue lines. is the perfect collection for representing a waiting line. Our simulated ticket counter will use the following assumptions: ■ There is only one line and it is first come first served (a queue). ■ Customers arrive on average every 15 seconds. ■ If there is a cashier available, processing begins immediately upon arrival. ■ Processing a customer request takes on average two minutes (120 seconds) from the time the customer reaches a cashier. First we can create a Customer class, as shown in Listing 5.3. A Customer object keeps track of the time the customer arrives and the time the customer departs after purchasing a ticket. The total time spent by the customer is therefore the departure time minus the arrival time. To keep things simple, our simulation L I S T I N G 5 . 3 /** * Customer represents a waiting customer. * * @author Dr. Lewis * @author Dr. Chase * @version 1.0, 08/12/08 */ public class Customer { private int arrivalTime, departureTime; /** * Creates a new customer with the specified arrival time. * * @param arrives the integer representation of the arrival time */ 107 108 C HA PT ER 5 L I S T I N G Queues 5 . 3 continued public Customer (int arrives) { arrivalTime = arrives; departureTime = 0; } /** * Returns the arrival time of this customer. * * @return the integer representation of the arrival time */ public int getArrivalTime() { return arrivalTime; } /** * Sets the departure time for this customer. * * @param departs the integer representation of the departure time */ public void setDepartureTime (int departs) { departureTime = departs; } /** * Returns the departure time of this customer. * * @return the integer representation of the departure time */ public int getDepartureTime() { return departureTime; } /** * Computes and returns the total time spent by this customer. * * @return the integer representation of the total customer time */ 5.3 L I S T I N G 5 . 3 Using Queues: Ticket Counter Simulation 109 continued public int totalTime() { return departureTime - arrivalTime; } } will measure time in elapsed seconds, so a time value can be stored as a single integer. Our simulation will begin at time 0. Our simulation will create a queue of customers, then see how long it takes to process those customers if there is only one cashier. Then we will process the same queue of customers with two cashiers. Then we will do it again with three cashiers. We continue this process for up to ten cashiers. At the end we compare the average time it takes to process a customer. Because of our assumption that customers arrive every 15 seconds (on average), we can preload a queue with customers. We will process 100 customers in this simulation. The program shown in Listing 5.4 conducts our simulation. The outer loop determines how many cashiers are used in each pass of the simulation. For each pass, the customers are taken from the queue in turn and processed by a cashier. The total elapsed time is tracked, and at the end of each pass the average time is computed. Figure 5.6 shows the UML description of the TicketCounter and Customer classes. L I S T I N G 5 . 4 /** * TicketCounter demonstrates the use of a queue for simulating a waiting line. * * @author Dr. Lewis * @author Dr. Chase * @version 1.0, 08/12/08 */ import jss2.*; public class TicketCounter { 110 C HA PT ER 5 L I S T I N G Queues 5 . 4 continued final static int PROCESS = 120; final static int MAX_CASHIERS = 10; final static int NUM_CUSTOMERS = 100; public static void main ( String[] args) { Customer customer; LinkedQueue<Customer> customerQueue = new LinkedQueue<Customer>(); int[] cashierTime = new int[MAX_CASHIERS]; int totalTime, averageTime, departs; // process the simulation for various number of cashiers for (int cashiers=0; cashiers < MAX_CASHIERS; cashiers++) { // set each cashiers time to zero initially for (int count=0; count < cashiers; count++) cashierTime[count] = 0; // load customer queue for (int count=1; count <= NUM_CUSTOMERS; count++) customerQueue.enqueue(new Customer(count*15)); totalTime = 0; // process all customers in the queue while (!(customerQueue.isEmpty())) { for (int count=0; count <= cashiers; count++) { if (!(customerQueue.isEmpty())) { customer = customerQueue.dequeue(); if (customer.getArrivalTime() > cashierTime[count]) departs = customer.getArrivalTime() + PROCESS; else departs = cashierTime[count] + PROCESS; customer.setDepartureTime (departs); cashierTime[count] = departs; totalTime += customer.totalTime(); 5.3 L I S T I N G 5 . 4 Using Queues: Ticket Counter Simulation continued } } } // output results for this simulation averageTime = totalTime / NUM_CUSTOMERS; System.out.println ("Number of cashiers: " + (cashiers+1)); System.out.println ("Average time: " + averageTime + "\n"); } } } LinkedQueue <<interface>> QueueADT front rear enqueue() dequeue() first() isEmpty() size() toString TicketCounter PROCESS MAX_CASHIERS NUM_CUSTOMERS main(String[] args) enqueue() dequeue() first() isEmpty() size() toString Customer arrivalTime, departureTime Customer(int arrives) getArrivalTime() setDepartureTime() getDepartureTime() totalTime() F I G U R E 5 . 6 UML description of the TicketCounter program 111 112 C HA PT ER 5 Queues Number of Cashiers: Average Time (sec): 1 2 3 5317 2325 1332 4 5 6 7 8 9 840 547 355 219 120 120 10 120 F I G U R E 5 . 7 The results of the ticket counter simulation The results of the simulation are shown in Figure 5.7. Note that with eight cashiers, the customers do not wait at all. The time of 120 seconds reflects only the time it takes to walk up and purchase the ticket. Increasing the number of cashiers to nine or ten or more will not improve the situation. Since the manager has decided he wants to keep the total average time to less than seven minutes (420 seconds), the simulation tells him that he should have six cashiers. 5.4 Implementing Queues: With Links Because a queue is a linear collection, we can implement a queue as a linked list of LinearNode objects, as we did with stacks. The primary difference is that we will have to operate on both ends of the list. Therefore, in addition to a reference (called front) pointing to the first element in the list, we will also keep track of a second reference (called rear) that points to the last element in the list. We will also use an integer variable called count to keep track of the number of elements in the queue. Does it make a difference to which end of the list we add or enqueue elements and from which end of the list we remove or deA linked implementation of a queue queue elements? If our linked list is singly linked, meaning that each is facilitated by references to the first node has only a pointer to the node behind it in the list, then yes, it and last elements of the linked list. does make a difference. In the case of the enqueue operation, it will not matter whether we add new elements to the front or the rear of the list. The processing steps will be very similar. If we add to the front of the list then we would set the next pointer of the new node to point to the front of the list and set the front variable to point to the new node. If we add to the rear of the list then we would set the next pointer of the node at the rear of the list to point to the new node and then set the rear of the list to point to the new node. In both cases, all of these processing steps are O(1), and therefore the time complexity of the enqueue operation would be O(1). K E Y CO N C E PT The difference between our two choices, adding to the front or the rear of the list, occurs with the dequeue operation. If we enqueue at the rear of the list and dequeue from the front of the list, then to dequeue we simply set a temporary 5.4 front Implementing Queues: With Links rear A B count C D 4 F I G U R E 5 . 8 A linked implementation of a queue variable to point to the element at the front of the list and then set the front variable to the value of the next pointer of the first node. Both processing steps are O(1) and therefore the operation would be O(1). However, if we enqueue at the front of the list and therefore dequeue at the rear of the list, our processing steps become more interesting. In order to dequeue from the rear of the list we must set a temporary variable to point to the element at the rear of the list and then set the rear pointer to point to the node before the current rear. Unfortunately, in a singly linked list, we cannot get to this node without traversing the list. Therefore if we chose to enqueue at the front and dequeue at the rear, the dequeue operation would be O(n) instead of O(1) as it is with our other choice. Thus, we choose to enqueue at the rear and dequeue at the front of our singly linked list. Keep in mind that a doubly linked list would solve the problem of having to traverse the list and thus it would not matter which end was which in a doubly linked implementation. Figure 5.8 depicts this strategy for implementing a queue. It shows a queue that has had the elements A, B, C, and D added to the queue or enqueued, in that order. Remember that Figure 5.8 depicts the general case. We always have to be careful to accurately maintain our references in special cases. For an empty queue, the front and rear references are both null and the count is zero. If there is exactly one element in the queue, both the front and rear references point to the same object. If we were using a singly linked implementation with a sentinel node and the queue was empty, then both front and rear would point to the sentinel node. Let’s explore the implementation of the queue operations using this linked list approach. The header, class-level data, and constructors for our linked implementation of a queue are provided for context: 113 114 C HA PT ER 5 Queues /** * LinkedQueue represents a linked implementation of a queue. * * @author Dr. Lewis * @author Dr. Chase * @version 1.0, 08/12/08 */ package jss2; import jss2.exceptions.*; public class LinkedQueue<T> implements QueueADT<T> { private int count; private LinearNode<T> front, rear; /** * Creates an empty queue. */ public LinkedQueue() { count = 0; front = rear = null; } The enqueue Operation The enqueue operation requires that we put the new element on the rear of the list. In the general case, that means setting the next reference of the current last element to the new one, and resetting the rear reference to the new last element. However, if the queue is currently empty, the front reference must also be set to the new (and only) element. This operation can be implemented as follows: /** * Adds the specified element to the rear of this queue. * * @param element the element to be added to the rear of this queue */ public void enqueue (T element) { 5.4 Implementing Queues: With Links LinearNode<T> node = new LinearNode<T> (element); if (isEmpty()) front = node; else rear.setNext (node); rear = node; count++; } Note that the next reference of the new node need not be explicitly set in this method because it has already been set to null in the constructor for the LinearNode class. The rear reference is set to the new node in either case, and the count is incremented. Implementing the queue operations with sentinel nodes is left as an exercise. As we discussed earlier, this operation is O(1). Figure 5.9 shows the queue from Figure 5.8 after element E has been added. The dequeue Operation The first issue to address when implementing the dequeue operation is to ensure that there is at least one element to return. If not, an EmptyCollectionException is thrown. As we did with our stack collection in Chapters 3 and 4, it makes sense to employ a generic EmptyCollectionException to which we can pass a parameter front rear A B C count D 5 F I G U R E 5 . 9 The queue after adding element E E 115 116 C HA PT ER 5 Queues specifying which collection we are dealing with. If there is at least one element in the queue, the first one in the list is returned and the front reference is updated: /** * Removes the element at the front of this queue and returns a * reference to it. Throws an EmptyCollectionException if the * queue is empty. * * @return the element at the front of this queue * @throws EmptyCollectionException if an empty collection exception occurs */ public T dequeue() throws EmptyCollectionException { if (isEmpty()) throw new EmptyCollectionException ("queue"); T result = front.getElement(); front = front.getNext(); count--; if (isEmpty()) rear = null; return result; } K E Y CO N C E PT The enqueue and dequeue operations work on opposite ends of the collection. For the dequeue operation, we must consider the situation in which we are returning the only element in the queue. If, after removing the front element, the queue is now empty, then the rear reference is set to null. Note that in this case, the front will be null D E S I G N F O C U S The same goals of reuse apply to exceptions as they do to other classes. The EmptyCollectionException class is a good example of this. It is an example of an exceptional case that will be the same for any collection that we create (e.g., attempting to perform an operation on the collection that cannot be performed if the collection is empty). Thus, creating a single exception with a parameter that allows us to designate which collection has thrown the exception is an excellent example of designing for reuse. 5.5 front Implementing Queues: With Arrays rear B C D count E 4 FI GURE 5 .1 0 The queue after a dequeue operation because it was set equal to the next reference of the last element in the list. Again, as we discussed earlier, the dequeue operation for our implementation is O(1). Figure 5.10 shows the result of a dequeue operation on the queue from Figure 5.9. The element A at the front of the list is removed and returned to the user. Note that, unlike the pop and push operations on a stack, the dequeue operation is not the inverse of enqueue. That is, Figure 5.10 is not identical to our original configuration shown in Figure 5.8, because the enqueue and dequeue operations are working on opposite ends of the collection. Other Operations The remaining operations in the linked queue implementation are fairly straightforward and are similar to those in the stack collection. The first operation is implemented by returning a reference to the element at the front of the queue. The isEmpty operation returns true if the count of elements is 0, and false otherwise. The size operation simply returns the count of elements in the queue. Finally, the toString operation returns a string made up of the toString results of each individual element. These operations are left as programming projects. 5.5 Implementing Queues: With Arrays One array-based strategy for implementing a queue is to fix one end of the queue (say, the front) at index 0 of the array. The elements are stored contiguously in the array. Figure 5.11 depicts a queue stored in this manner, assuming elements A, B, C, and D have been added to the queue in that order. 117 118 C HA PT ER 5 Queues 0 1 2 3 A B C D rear 4 5 6 7 ... 4 FIG URE 5 .1 1 An array implementation of a queue KE Y CO N C E PT Because queue operations modify both ends of the collection, fixing one end at index 0 requires that elements be shifted. The integer variable rear is used to indicate the next open cell in the array. Note that it also represents the number of elements in the queue. This strategy assumes that the first element in the queue is always stored at index 0 of the array. Because queue processing affects both ends of the collection, this strategy will require that we shift the elements whenever an element is removed from the queue. This required shifting of elements would make the dequeue operation O(n). Like our discussion of the complexity of our singly linked list implementation above, making a poor choice in our array implementation could lead to less than optimal efficiency. K E Y CO N C E PT The shifting of elements in a noncircular array implementation creates an O(n) complexity. Would it make a difference if we fixed the rear of the queue at index 0 of the array instead of the front? Keep in mind that when we enqueue an element onto the queue, we do so at the rear of the queue. This would mean that each enqueue operation would result in shifting all of the elements in the queue up one position in the array making the enqueue operation O(n). D E S I G N F O C U S It is important to note that this fixed array implementation strategy, which was very effective in our implementation of a stack, is not nearly as efficient for a queue. This is an important example of matching the data structure used to implement a collection with the collection itself. The fixed array strategy was efficient for a stack because all of the activity (adding and removing elements) was on one end of the collection and thus on one end of the array. With a queue, now that we are operating on both ends of the collection and order does matter, the fixed array implementation is much less efficient. 5.5 Implementing Queues: With Arrays The key is to not fix either end. As elements are dequeued, the front of the queue will move further into the array. As elements are enqueued, the rear of the queue will also move further into the array. The challenge comes when the rear of the queue reaches the end of the array. Enlarging the array at this point is not a practical solution, and does not make use of the now empty space in the lower indexes of the array. KEY CON CEPT Treating arrays as circular eliminates the need to shift elements in an array queue implementation. To make this solution work, we will use a circular array to implement the queue, defined in a class called CircularArrayQueue. A circular array is not a new construct—it is just a way to think about the array used to store the collection. Conceptually, the array is used as a circle, whose last index is followed by the first index. A circular array storing a queue is shown in Figure 5.12. Two integer values are used to represent the front and rear of the queue. These values change as elements are added and removed. Note that the value of front represents the location where the first element in the queue is stored, and the value of rear represents the next available slot in the array (not where the last element is stored). Using rear in this manner is consistent with our other array implementation. Note, however, that the value of rear no longer represents the N 0 N-1 1 2 3 . . . 4 5 9 6 8 front 3 rear 7 7 count 4 F I GU RE 5 .1 2 A circular array implementation of a queue 119 120 C HA PT ER 5 Queues 99 0 1 98 2 97 3 . . . 4 5 9 6 8 front 98 rear 7 2 count 4 FIG URE 5 .1 3 A queue straddling the end of a circular array number of elements in the queue. We will use a separate integer value to keep a count of the elements. When the rear of the queue reaches the end of the array, it “wraps around” to the front of the array. The elements of the queue can therefore straddle the end of the array, as shown in Figure 5.13, which assumes the array can store 100 elements. Using this strategy, once an element has been added to the queue, it stays in one location in the array until it is removed with a dequeue operation. No elements need to be shifted as elements are added or removed. This approach requires, however, that we carefully manage the values of front and rear. Let’s look at another example. Figure 5.14 shows a circular array (drawn linearly) with a capacity of ten elements. Initially it is shown after elements A through H have been enqueued. It is then shown after the first four elements 5.5 Implementing Queues: With Arrays 0 1 2 3 4 5 6 7 A B C D E F G H front 0 1 2 3 front 0 1 K L 2 front rear 0 4 5 6 7 E F G H rear 4 3 4 count 8 count 8 8 8 8 5 6 7 8 E F G H I 2 count 9 4 4 rear 9 9 J 8 FI GU R E 5.1 4 Changes in a circular array implementation of a queue (A through D) have been dequeued. Finally, it is shown after elements I, J, K, and L have been enqueued, which causes the queue to wrap around the end of the array. The header, class-level data, and constructors for our circular array implementation of a queue are provided for context: 121 122 C HA PT ER 5 Queues /** * CircularArrayQueue represents an array implementation of a queue in * which the indexes for the front and rear of the queue circle back to 0 * when they reach the end of the array. * * @author Dr. Lewis * @author Dr. Chase * @version 1.0 08/12/08 */ package jss2; import jss2.exceptions.*; import java.util.Iterator; public class CircularArrayQueue<T> implements QueueADT<T> { private final int DEFAULT_CAPACITY = 100; private int front, rear, count; private T[] queue; /** * Creates an empty queue using the default capacity. */ public CircularArrayQueue() { front = rear = count = 0; queue = (T[]) (new Object[DEFAULT_CAPACITY]); } /** * Creates an empty queue using the specified capacity. * * @param initialCapacity the integer representation of the initial * size of the circular array queue */ public CircularArrayQueue (int initialCapacity) { front = rear = count = 0; queue = ( (T[])(new Object[initialCapacity]) ); } 5.5 Implementing Queues: With Arrays The enqueue Operation In general, after an element is enqueued, the value of rear is incremented. But when an enqueue operation fills the last cell of the array (at the largest index), the value of rear must be set to 0, indicating that the next element should be stored at index 0. The appropriate update to the value of rear can be accomplished in one calculation by using the remainder operator (%). Recall that the remainder operator returns the remainder after dividing the first operand by the second. Therefore, if queue is the name of the array storing the queue, the following line of code will update the value of rear appropriately: rear = (rear+1) % queue.length; Let’s try this calculation, assuming we have an array of size 10. If rear is currently 5, it will be set to 6%10, or 6. If rear is currently 9, it will be set to 10%10 or 0. Try this calculation using various situations to see that it works no matter how big the array is. Given this strategy, the enqueue operation can be implemented as follows: /** * Adds the specified element to the rear of this queue, expanding * the capacity of the queue array if necessary. * * @param element the element to add to the rear of the queue */ public void enqueue (T element) { if (size() == queue.length) expandCapacity(); queue[rear] = element; rear = (rear+1) % queue.length; count++; } Note that this implementation strategy can still allow the array to reach capacity. As with any array-based implementation, all cells in the array may become filled. This implies that the rear of the queue has “caught up” to the front of the queue. To add another element, the array would have to be enlarged. Keep in mind, however, that the elements of the existing array must be copied into the new array in their proper order in the queue, not necessarily the order in which they 123 124 C HA PT ER 5 Queues appear in the current array. This makes the expandCapacity method slightly more complex than the one we used for stacks: /** * Creates a new array to store the contents of this queue with * twice the capacity of the old one. */ public void expandCapacity() { T[] larger = (T[])(new Object[queue.length *2]); for(int scan=0; scan < count; scan++) { larger[scan] = queue[front]; front=(front+1) % queue.length; } front = 0; rear = count; queue = larger; } The dequeue Operation Likewise, after an element is dequeued, the value of front is incremented. After enough dequeue operations, the value of front will reach the last index of the array. After removing the element at the largest index, the value of front must be set to 0 instead of being incremented. The same calculation we used to set the value of rear in the enqueue operation can be used to set the value of front in the dequeue operation: /** * Removes the element at the front of this queue and returns a * reference to it. Throws an EmptyCollectionException if the * queue is empty. * * @return the reference to the element at the front * of the queue that was removed * @throws EmptyCollectionException if an empty collections exception occurs */ 5.5 Implementing Queues: With Arrays public T dequeue() throws EmptyCollectionException { if (isEmpty()) throw new EmptyCollectionException (“queue”); T result = queue[front]; queue[front] = null; front = (front+1) % queue.length; count--; return result; } Other Operations Operations such as toString become a bit more complicated using this approach because the elements are not stored starting at index 0 and may wrap around the end of the array. These methods have to take the current situation into account. All of the other operations for a circular array queue are left as programming projects. 125 126 C HA PT ER 5 Queues Summary of Key Concepts ■ Queue elements are processed in a FIFO manner—the first element in is the first element out. ■ A queue is a convenient collection for storing a repeating code key. ■ Simulations are often implemented using queues to represent waiting lines. ■ A linked implementation of a queue is facilitated by references to the first and last elements of the linked list. ■ The enqueue and dequeue operations work on opposite ends of the collection. ■ Because queue operations modify both ends of the collection, fixing one end at index 0 requires that elements be shifted. ■ The shifting of elements in a noncircular array implementation creates an O(n) complexity. ■ Treating arrays as circular eliminates the need to shift elements in an array queue implementation. Self-Review Questions SR 5.1 What is the difference between a queue and a stack? SR 5.2 What are the five basic operations on a queue? SR 5.3 What are some of the other operations that might be implemented for a queue? SR 5.4 Is it possible for the front and rear references in a linked implementation to be equal? SR 5.5 Is it possible for the front and rear references in a circular array implementation to be equal? SR 5.6 Which implementation has the worst time complexity? SR 5.7 Which implementation has the worst space complexity? Exercises EX 5.1 Hand trace a queue X through the following operations: X.enqueue(new Integer(4)); X.enqueue(new Integer(1)); Exercises Object Y = X.dequeue(); X.enqueue(new Integer(8)); X.enqueue(new Integer(2)); X.enqueue(new Integer(5)); X.enqueue(new Integer(3)); Object Y = X.dequeue(); X.enqueue(new Integer(4)); X.enqueue(new Integer(9)); EX 5.2 Given the resulting queue X from Exercise 5.1, what would be the result of each of the following? a. X.front(); b. Y = X.dequeue(); X.front(); c. Y = X.dequeue(); d. X.front(); EX 5.3 What would be the time complexity of the size operation for each of the implementations if there were not a count variable? EX 5.4 Under what circumstances could the front and rear references be equal for each of the implementations? EX 5.5 Hand trace the ticket counter problem for 22 customers and 4 cashiers. Graph the total process time for each person. What can you surmise from these results? EX 5.6 Compare and contrast the enqueue method of the LinkedQueue class to the push method of the LinkedStack class from Chapter 4. EX 5.7 Describe two different ways the isEmpty method of the LinkedQueue class could be implemented. EX 5.8 Name five everyday examples of a queue other than those discussed in this chapter. EX 5.9 Explain why the array implementation of a stack does not require elements to be shifted but the noncircular array implementation of a queue does. EX 5.10 Suppose the count variable was not used in the CircularArrayQueue class. Explain how you could use the values of front and rear to compute the number of elements in the list. 127 128 C HA PT ER 5 Queues Programming Projects PP 5.1 Complete the implementation of the LinkedQueue class presented in this chapter. Specifically, complete the implementations of the first, isEmpty, size, and toString methods. PP 5.2 Complete the implementation of the CircularArrayQueue class described in this chapter, including all methods. PP 5.3 Write a version of the CircularArrayQueue class that grows the list in the opposite direction from which the version described in this chapter grows the list. PP 5.4 All of the implementations in this chapter use a count variable to keep track of the number of elements in the queue. Rewrite the linked implementation without a count variable. PP 5.5 All of the implementations in this chapter use a count variable to keep track of the number of elements in the queue. Rewrite the circular array implementation without a count variable. PP 5.6 A data structure called a deque (pronounced like “deck”) is closely related to a queue. The name deque stands for doubleended queue. The difference between the two is that with a deque, you can insert or remove from either end of the queue. Implement a deque using arrays. PP 5.7 Implement the deque from Programming Project 5.6 using links. (Hint: Each node will need a next and a previous reference.) PP 5.8 Create a graphical application that provides buttons for enqueue and dequeue from a queue, a text field to accept a string as input for enqueue, and a text area to display the contents of the queue after each operation. PP 5.9 Create a system using a stack and a queue to test whether a given string is a palindrome (i.e., the characters read the same forward or backward). PP 5.10 Create a system to simulate vehicles at an intersection. Assume that there is one lane going in each of four directions, with stoplights facing each direction. Vary the arrival average of vehicles in each direction and the frequency of the light changes to view the “behavior” of the intersection. Answers to Self-Review Questions Answers to Self-Review Questions SRA 5.1 A queue is a first in, first out (FIFO) collection, whereas a stack is a last in, first out (LIFO) collection. SRA 5.2 The basic queue operations are: enqueue—adds an element to the end of the queue dequeue—removes an element from the front of the queue first—returns a reference to the element at the front of the queue isEmpty—returns true if the queue is empty, returns false other- wise SRA 5.3 makeEmpty(), destroy(), full() SRA 5.4 Yes, it happens when the queue is empty (both front and rear are null) and when there is only one element on the queue. SRA 5.5 Yes, it can happen under two circumstances: when the queue is empty, and when the queue is full. SRA 5.6 The noncircular array implementation with an O(n) dequeue or enqueue operation has the worst time complexity. SRA 5.7 Both of the array implementations waste space for unfilled elements in the array. The linked implementation uses more space per element stored. 129 This page intentionally left blank 6 Lists T he concept of a list is inherently familiar to us. We make “to-do” lists, lists of items to buy at the grocery store, CHAPTER OBJECTIVES ■ Examine list processing and various ordering techniques items in a list or we may keep them in alphabetical order. ■ Define a list abstract data type For other lists we may keep the items in a particular order ■ Introduce the concept of an iterator ■ Examine polymorphism via interfaces ■ Demonstrate how a list can be used to solve problems ■ Examine various list implementations ■ Compare list implementations and lists of friends to invite to a party. We may number the that simply makes the most sense to us. This chapter explores the concept of a list collection and some ways they can be managed. 131 132 C HA PT ER 6 Lists 6.1 A List ADT There are three types of list collections: ■ Ordered lists, whose elements are ordered by some inherent characteristic of the elements ■ Unordered lists, whose elements have no inherent order but are ordered by their placement in the list ■ Indexed lists, whose elements can be referenced using a numeric index The elements of an ordered list have an inherent relationship defining their order. K E Y CO N C E PT The elements of an unordered list are kept in whatever order the client chooses. The placement of elements in an unordered list is not based on any inherent characteristic of the elements. Don’t let the name mislead you. The elements in an unordered list are kept in a particular order, but that order is not based on the elements themselves. The client using the list determines the order of the elements. Figure 6.2 shows a conceptual view of an unordered list. A new element can be put on the front or rear of the list, or it can be inserted after a particular element already in the list. Adding an element rear of list K E Y C O N C E PT 54 List collections can be categorized as ordered, unordered, and indexed. front of list KE Y C O N C E PT An ordered list is based on some particular characteristic of the elements in the list. For example, you may keep a list of people ordered alphabetically by name, or you may keep an inventory list ordered by part number. The list is sorted based on some key value. Any element added to an ordered list has a proper location in the list, given its key value and the key values of the elements already in the list. Figure 6.1 shows a conceptual view of an ordered list, in which the elements are ordered by an integer key value. Adding a value to the list involves finding the new element’s proper, sorted position among the existing elements. 81 73 57 49 42 33 25 12 F I G U R E 6 . 1 A conceptual view of an ordered list 6.1 A List ADT rear of list front of list Adding an element Adding an element Adding an element F I G U R E 6 . 2 A conceptual view of an unordered list An indexed list is similar to an unordered list in that there is no inherent relationship among the elements that determines their order in the list. The client using the list determines the order of the elements. However, in addition, each element can be referenced by a numeric index that begins at 0 at the front of the list and continues contiguously until the end of the list. Figure 6.3 shows a conceptual view of an indexed list. A new element can be inserted into the list at any position, including at the front or rear of the list. Every time a change occurs in the list, the indexes are adjusted to stay in order and contiguous. Note the primary difference between an indexed list and an array: An indexed list keeps its indexes contiguous. If an element is removed, the positions of other elements “collapse” to eliminate the gap. When an element is inserted, the indexes of other elements are shifted to make room. The Java Collections API implements three different varieties of indexed lists, and we will explore these further KEY CON CEPT An indexed list maintains a contiguous numeric index range for its elements. rear of list front of list Adding an element Adding an element Adding an element 0 1 2 3 4 5 6 7 F I G U R E 6 . 3 A conceptual view of an indexed list 133 134 C HA PT ER 6 Lists Operation Description removeFirst removeLast remove first last contains isEmpty size Removes the first element from the list. Removes the last element from the list. Removes a particular element from the list. Examines the element at the front of the list. Examines the element at the rear of the list. Determines if the list contains a particular element. Determines if the list is empty. Determines the number of elements on the list. F I G U R E 6 . 4 The common operations on a list in Section 6.6. We will focus the majority of our discussion for now on ordered and unordered lists. Keep in mind that these are conceptual views of lists. As with any collection, they can be implemented in many ways. The implementations of these lists don’t even have to keep the elements in the order that their conceptual view indicates, though that may be easiest. K E Y CO N C E PT Many common operations can be defined for all list types. The differences between them stem from how elements are added. There is a set of operations that is common to both ordered and unordered lists. These common operations are shown in Figure 6.4. They include operations to remove and examine elements, as well as classic operations such as isEmpty and size. The contains operation is also supported by both list types, which allows the user to determine if a list contains a particular element. Note that for the first time, we are examining a collection that conceptually allows us to access elements in the middle of the list. With stacks, we were only able to operate on one end, the top, of the collection. With queues, we could add to one end and remove from the other. With lists, we are able to add, remove, or view any element in the list. For this reason, lists include an iterator. Iterators K E Y CO N C E PT An iterator is an object that provides a means to iterate over a collection. An iterator is an object that provides the means to iterate over a collection. That is, it provides methods that allow the user to acquire and use each element in a collection in turn. Most collections provide one or more ways to iterate over their elements. In the case of the ListADT interface, we define a method called iterator that returns an Iterator object. 6.1 The Iterator interface is defined in the Java standard class library. The two primary abstract methods defined in the Iterator interface are: ■ hasNext, which returns true if there are more elements in the iteration ■ next, which returns the next element in the iteration The iterator method of the ListADT interface returns an object that implements this interface. The user can then interact with that object, using the hasNext and next methods, to access the elements in the list. Note that there is no assumption about the order in which an Iterator object delivers the elements from the collection. In the case of a list, there is a linear order to the elements, so the iterator would likely follow that order. In other cases, an iterator may follow a different order that makes sense for that collection. Another issue surrounding the use of iterators is what happens if the collection is modified while the iterator is in use. Most of the collections in the Java Collections API are implemented to be fail-fast. This simply means that they will, or should, throw an exception if the collection is modified while the iterator is in use. However, the documentation regarding these collections is very explicit that this behavior cannot be guaranteed. We will illustrate a variety of alternative possibilities for iterator construction throughout the examples in the book. These possibilities include creating iterators that allow concurrent modification and reflect those changes in the iteration, and creating iterators that iterate over a snapshot of the collection for which concurrent modifications have no impact. By including an Iterator method in our collection we also make the collection Iterable or in other words, we implement the Iterable interface. This means that, like we do with arrays, we can use the iterator version of a for loop: for each . . . Adding Elements to a List The differences between ordered and unordered lists generally center on how elements are added to the list. In an ordered list, we need only specify the new element to add. Its position in the list is based on its key value. This operation is shown in Figure 6.5. Operation Description add Adds an element to the list. F I G U R E 6 . 5 The operation particular to an ordered list A List ADT 135 136 C HA PT ER 6 Lists Operation Description addToFront addToRear addAfter Adds an element to the front of the list. Adds an element to the rear of the list. Adds an element after a particular element already in the list. F I G U R E 6 . 6 The operations particular to an unordered list An unordered list supports three variations of the add operation. Elements can be added to the front or rear of the list, or after a particular element that is already in the list. These operations are shown in Figure 6.6. D E S I G N F O C U S Is it possible that a list could be both an ordered list and an indexed list? Possible perhaps but not very meaningful. If a list were both ordered and indexed, what would happen if a client application attempted to add an element at a particular index or change an element at a particular index such that it is not in the proper order? Which rule would have precedence, index position or order? Conceptually, the operations particular to an indexed list make use of its ability to reference elements by their index. A new element can be inserted into the list at a particular index, or it can be added to the rear of the list without specifying an index at all. Note that if an element is inserted or removed, the elements at higher indexes are either shifted up to make room or shifted down to close the gap. Alternatively, the element at a particular index can be set, which overwrites the element currently at that index and therefore does not cause other elements to shift. In addition, as we will explore later in this chapter, indexed lists provide operations to retrieve an element at a particular index and to determine the index of an element in the list. We can capitalize on the fact that both ordered lists and unordered lists share a common set of operations. These operations need to be defined only once. Therefore, we will define three list interfaces: one with the common operations and two with the operations particular to each list type. Inheritance can be used with interfaces just as it can with classes. The interfaces of the particular list types extend the common list definition. This relationship among the interfaces is shown in Figure 6.7. 6.1 A List ADT <<interface>> ListADT removeFirst() removeLast() remove (Object) first() last() isEmpty() size() iterator() toString() <<interface>> OrderedListADT add (Comparable) <<interface>> UnorderedListADT addToFront (Object) addToRear (Object) addAfter (element : Object, target : Object) F I G U R E 6 . 7 The various list interfaces When interfaces are inherited, the child interface contains all abstract methods defined in the parent. Therefore, any class implementing a child interface must implement all methods from both the parent and the child. KEY CON CEPT Interfaces can be used to derive other interfaces. The child interface contains all abstract methods of the parent. Interfaces and Polymorphism Up to this point, we have been able to simply store a generic type <T> in our stack and queue collections. The generic type <T> is then replaced with a specific type at the time a collection is instantiated. We have not placed any restrictions on what types could be stored in our collections. However, in dealing with ordered 137 138 C HA PT ER 6 Lists lists, we must place a restriction on the type so that only classes that implement the Comparable interface can be stored in an ordered list. This is necessary so that we can use the classes own compareTo method to order the elements in the list. This is sometimes referred to as the natural ordering of the elements. In Chapter 3, we discussed the concept of polymorphism via inheritance. Let’s examine how interfaces can be used to create polymorphism as well. As we have seen, a class name is used to declare the type of an object reference variable. Similarly, an interface name can be used as the type of a reference variable as well. An interface reference variable can be used to refer to any object of any class that implements that interface. Suppose we declare an interface called Speaker as follows: public interface Speaker { public void speak(); public void announce (String str); } The interface name, Speaker, can now be used to declare an object reference variable: K E Y CO N C E PT An interface name can be used to declare an object reference variable. An interface reference can refer to any object of any class that implements the interface. Speaker current; The reference variable current can be used to refer to any object of any class that implements the Speaker interface. For example, if we define a class called Philosopher such that it implements the Speaker interface, we could then assign a Philosopher object to a Speaker reference: current = new Philosopher(); This assignment is valid because a Philosopher is, in fact, a Speaker. The flexibility of an interface reference allows us to create polymorphic references. As we saw in Chapter 3, by using inheritance, we can create a polymorphic reference that can refer to any one of a set of objects related by inheritance. Using interfaces, we can create similar polymorphic references, except that the objects being referenced are related by implementing the same interface instead of being related by inheritance. K E Y CO N C E PT Interfaces allow us to make polymorphic references in which the method that is invoked is based on the particular object being referenced at the time. For example, if we create a class called Dog that also implements the Speaker interface, it too could be assigned to a Speaker reference variable. The same reference, in fact, could at one point refer to a Philosopher object, and then later refer to a Dog object. The following lines of code illustrate this: 6.1 Speaker guest; guest = new Philosopher(); guest.speak(); guest = new Dog(); guest.speak(); In this code, the first time the speak method is called, it invokes the speak method defined in the Philosopher class. The second time it is called, it invokes the speak method of the Dog class. As with polymorphic references via inheritance, it is not the type of the reference that determines which method gets invoked, but rather the type of the object that the reference points to at the moment of invocation. Note that when we are using an interface reference variable, we can invoke only the methods defined in the interface, even if the object it refers to has other methods to which it can respond. For example, suppose the Philosopher class also defined a public method called pontificate. The second line of the following code would generate a compiler error, even though the object can in fact respond to the pontificate method: Speaker special = new Philosopher(); special.pontificate(); // generates a compiler error The problem is that the compiler can determine only that the object is a Speaker, and therefore can guarantee only that the object can respond to the speak and announce methods. Because the reference variable special could refer to a Dog object (which cannot pontificate), it does not allow the reference. If we know that in a particular situation such an invocation is valid, we can cast the object into the appropriate reference so that the compiler will accept it: ((Philosopher) special).pontificate(); Similar to polymorphic references based on inheritance, an interface name can be used as the type of a method parameter. In such situations, any object of any class that implements the interface can be passed into the method. For example, the following method takes a Speaker object as a parameter. Therefore, both a Dog object and a Philosopher object can be passed into it in separate invocations. public void sayIt (Speaker current) { current.speak(); } Given this discussion, could we simply make the reference type of all of the elements in our ordered list Comparable? This would mean that any object of any class that implements the Comparable interface could be stored in any instance of A List ADT 139 140 C HA PT ER 6 Lists our ordered list collection. However, simply having objects that have implemented the Comparable interface does not mean that those objects can necessarily be compared to each other, only that they can be compared to other objects of the same type. This is certainly not what we intended. The Comparable interface is generic so what if we tried the following as a generic type for our collection: <T extends Comparable<T>> This still causes a problem. Assume for a moment that both Philosopher and Dog are subclasses of a class Mammal and that the Mammal class implements the Comparable<Mammal> interface. This results in both philosophers and dogs being comparable to other mammals but not exclusively to members of their own class. In other words, using <T extends Comparable<T>> would not allow us to create an ordered list of philosophers because philosophers are comparable to mammals, not just to other philosophers. A more comprehensive solution, though not necessarily a more intellectually satisfying solution is to write the generic type as: <T extends Comparable<? super T>> This will include both the case where T implements Comparable<T> and the case where some superclass of T implements Comparable. The implementers of the Java language chose a different alternative for the types for the ordered collections in the Java Collections API. In these cases, the type is simply implemented as a generic type <T>, and the methods that add elements to the collection will throw a ClassCastException if the element being added cannot be compared to the elements in the collection. We will adopt that same approach for our ordered list collection. Listings 6.1 through 6.3 show the Java interfaces corresponding to the UML diagram in Figure 6.7. Before exploring how these various kinds of lists can be implemented, let’s first see how they might be used. 6.2 Using Ordered Lists: Tournament Maker Sporting tournaments, such as the NCAA basketball tournament or a championship tournament at a local bowling alley, are often organized or seeded by the number of wins achieved during the regular season. Ordered lists can be used to help organize the tournament play. An ordered list can be used to store teams ordered by number of wins. To form the match-ups for the first round of the 6.2 L I S T I N G Using Ordered Lists: Tournament Maker 6 . 1 /** * ListADT defines the interface to a general list collection. Specific * types of lists will extend this interface to complete the * set of necessary operations. * @author Dr. Lewis * @author Dr. Chase * @version 1.0, 08/13/08 */ package jss2; import java.util.Iterator; public interface ListADT<T> extends Iterable<T> { /** * Removes and returns the first element from this list. * * @return the first element from this list */ public T removeFirst (); /** * Removes and returns the last element from this list. * * @return the last element from this list */ public T removeLast (); /** * Removes and returns the specified element from this list. * * @param element the element to be removed from the list */ public T remove (T element); /** * Returns a reference to the first element in this list. * * @return a reference to the first element in this list */ public T first (); 141 142 C HA PT ER 6 L I S T I N G Lists 6 . 1 continued /** * Returns a reference to the last element in this list. * * @return a reference to the last element in this list *// public T last (); /** * Returns true if this list contains the specified target element. * * @param target the target that is being sought in the list * @return true if the list contains this element */ public boolean contains (T target); /** * Returns true if this list contains no elements. * * @return true if this list contains no elements */ public boolean isEmpty(); /** * Returns the number of elements in this list. * * @return the integer representation of number of elements in this list */ public int size(); /** * Returns an iterator for the elements in this list. * * @return an iterator over the elements in this list */ public Iterator<T> iterator(); /** * Returns a string representation of this list. * * @return a string representation of this list */ public String toString(); } 6.2 L I S T I N G Using Ordered Lists: Tournament Maker 6 . 2 /** * OrderedListADT defines the interface to an ordered list collection. Only * comparable elements are stored, kept in the order determined by * the inherent relationship among the elements. * @author Dr. Lewis * @author Dr. Chase * @version 1.0, 08/13/08 */ package jss2; public interface OrderedListADT<T> extends ListADT<T> { /** * Adds the specified element to this list at the proper location * * @param element the element to be added to this list */ public void add (T element); } L I S T I N G 6 . 3 /** * UnorderedListADT defines the interface to an unordered list collection. * Elements are stored in any order the user desires. * * @author Dr. Lewis * @author Dr. Chase * @version 1.0, 08/13/08 */ package jss2; public interface UnorderedListADT<T> extends ListADT<T> { /** * Adds the specified element to the front of this list. * 143 144 C HA PT ER 6 L I S T I N G Lists 6 . 3 continued * @param element the element to be added to the front of this list */ public void addToFront (T element); /** * Adds the specified element to the rear of this list. * * @param element the element to be added to the rear of this list */ public void addToRear (T element); /** * Adds the specified element after the specified target. * * @param element the element to be added after the target * @param target the target is the item that the element will be added * after */ public void addAfter (T element, T target); } K E Y CO N C E PT An ordered list is a convenient collection to use when creating a tournament schedule. tournament, teams can be selected from the front and back of the list in pairs. For example, consider the eight bowling teams listed in Figure 6.8. This table indicates the number of wins each team achieved during the regular season. Team Name Scorecards Gutterballs KingPins PinDoctors Spares Splits Tenpins Woodsplitters Wins 10 9 8 7 5 4 3 2 F I G U R E 6 . 8 Bowling league team names and number of wins 6.2 Using Ordered Lists: Tournament Maker Game 1 (Scorecards vs Woodsplitters) Game 2 (GutterBalls vs Tenpins) Game 3 (KingPins vs Splits) Game 5 (winner of Game 1 vs winner of Game 4) Game 6 (winner of Game 2 vs winner of Game 3) Game 7 (winner of Game 5 vs winner of Game 6) Game 4 (PinDoctors vs Spares) F I G U R E 6 . 9 Sample tournament layout for a bowling league tournament To create the first-round tournament matches, the teams would be stored in a list ordered by the number of wins. The first team on the list (the team with the best record) is removed from the list and matched up with the last team on the list (the team with the worst record) to form the first game of the tournament. The process is repeated, matching up the team with the next best record with the team with the next worst record to form the second game. This process continues until the list is empty. Interestingly, the same process would be used to form the second-round match-ups, only for the second round, the teams would be ordered by game number from the first round. For the third round, the teams would be ordered by game number from the second round. This process would continue with half as many games per round until only one game was left. Thus, from our example in Figure 6.8, we would end up with the tournament as laid out in Figure 6.9. Creating a program to select the first-round tournament match-ups requires that we first create a class to represent the information we wish to store about the teams. This Team class needs to store both the name of the team and the number of wins. The Team class also needs to provide us with some sort of comparison operation. For this purpose, the Team class will implement the Comparable interface, thus providing a compareTo method. This method will return -1 if the first team has fewer wins than the second team, 0 if the two teams have the same number of wins, and 1 if the first team has more wins than the second team. As we will discuss in more detail in the next section, any class that we intend to store in our ordered list collection must implement the Comparable interface. Figure 6.10 illustrates the UML relationships among the classes used to solve this problem. Listing 6.4 shows the Tournament class, Listing 6.5 shows the Team class, and Listing 6.6 illustrates the TournamentMaker class. 145 146 C HA PT ER 6 Lists OrderedListADT Team teamname wins Team() compareTo() toString() ArrayOrderedList ArrayList TournamentMaker make() get_next_token Tournament main(String[] args) ListADT FIG URE 6 .1 0 UML description of the Tournament class L I S T I N G 6 . 4 /** * Tournament is a driver for a program that demonstrates first round of * tournament team match-ups using an ordered list. * * @author Dr. Lewis * @author Dr. Chase * @version 1.0, 08/12/08 */ import java.io.*; public class Tournament { 6.2 L I S T I N G 6 . 4 Using Ordered Lists: Tournament Maker continued /** * Determines and prints the tournament organization. */ public static void main (String[] args ) throws IOException { TournamentMaker temp = new TournamentMaker(); temp.make(); } } L I S T I N G 6 . 5 /** * Team represents a team. * * @author Dr. Lewis * @author Dr. Chase * @version 1.0, 08/12/08 */ import java.util.*; class Team implements Comparable<Team> { public String teamName; private int wins; /** * Sets up this team with the specified information. * * @param name the string representation of the name of this team * @param numWins the integer representation of the number of wins for this * team */ public Team (String name, int numWins) { teamName = name; wins = numWins; } 147 148 C HA PT ER 6 L I S T I N G Lists 6 . 5 continued /** * Returns the name of the given team. * * @return the string representation of the name of this team */ public String getName () { return teamName; } /** * Compares number of wins of this team to another team. Returns * -1, 0, or 1 for less than, equal to, or greater than. * * @param other the team to compare wins against this team * @return the integer representation of the result of this comparison, * valid values are -1. 0, 1, for less than, equal to, and * greater than. */ public int compareTo (Team other) { if (this.wins < other.wins) return -1; else if (this.wins == other.wins) return 0; else return 1; } /** * Returns the name of the team. * * @return the string representation of the name of this team */ public String toString() { return teamName; } } 6.2 L I S T I N G Using Ordered Lists: Tournament Maker 6 . 6 /** * TournamentMaker demonstrates first round of tournament team match-ups * using an ordered list. * * @author Dr. Lewis * @author Dr. Chase * @version 1.0, 08/12/08 */ import import import import import jss2.*; jss2.exceptions.*; java.util.Scanner; java.io.*; java.lang.*; public class TournamentMaker { /** * Determines and prints the tournament organization. * * @throws IOException if an IO exception is encountered */ public void make ( ) throws IOException { ArrayOrderedList<Team> tournament = new ArrayOrderedList<Team>(); String team1, team2, teamName; int numWins, numTeams = 0; double checkInput=-1; Scanner in = new Scanner(System.in); System.out.println("Tournament Maker\n"); while (((numTeams % 2) != 0) || (numTeams == 2) || (checkInput!=0)) { System.out.println ("Enter the number of teams (must be an even \n" + "number valid for a single elimination tournament):"); numTeams = in.nextInt(); in.nextLine(); // advance beyond new line char /** checks if numTeams is valid for single elimination tournament */ checkInput = (Math.log(numTeams)/Math.log(2)) % 1; } 149 150 C HA PT ER 6 L I S T I N G Lists 6 . 6 continued System.out.println ("\nEnter " + numTeams + " team names and number of wins."); System.out.println("Teams may be entered in any order."); for (int count=1; count <= numTeams; count++) { System.out.println("Enter team name: "); teamName = in.nextLine(); System.out.println("Enter number of wins: "); numWins = in.nextInt(); in.nextLine(); // advance beyond new line char tournament.add(new Team(teamName, numWins)); } System.out.println("\nThe first round matchups are: "); for (int count=1; count <=(numTeams/2); count++) { team1 = (tournament.removeFirst()).getName(); team2 = (tournament.removeLast()).getName(); System.out.println ("Game " + count + " is " + team1 + " against " + team2); System.out.println ("with the winner to play the winner of game " + (((numTeams/2)+1) - count) + "\n"); } } } 6.3 Using Indexed Lists: The Josephus Problem Flavius Josephus was a Jewish historian of the first century. Legend has it that he was one of a group of 41 Jewish rebels who decided to kill themselves rather than surrender to the Romans, who had them trapped. They decided to form a circle and to kill every third person until no one was left. Josephus, not wanting to die, calculated where he needed to stand so that he would be the last one K E Y CO N C E PT alive and thus would not have to die. Thus was born a class of probThe Josephus problem is a classic computing problem that is approprilems referred to as the Josephus problem. These problems involve findately solved with indexed lists. ing the order of events when events in a list are not taken in order, but rather they are taken every ith element in a cycle until none remains. For example, suppose that we have a list of seven elements numbered from 1 to 7: 1 2 3 4 5 6 7 If we were to remove every third element from the list, the first element to be removed would be number 3, leaving the list: 1 2 4 5 6 7 6.3 Using Indexed Lists: The Josephus Problem The next element to be removed would be number 6, leaving the list: 1 2 4 5 7 The elements are thought of as being in a continuous cycle, so that when we reach the end of the list, we continue counting at the beginning. Therefore, the next element to be removed would be number 2, leaving the list: 1 4 5 7 The next element to be removed would be number 7, leaving the list: 1 4 5 The next element to be removed would be number 5, leaving the list: 1 4 The next to last element to be removed would be number 1, leaving the number 4 as the last element on the list. Listing 6.7 illustrates a generic implementation of the Josephus problem, allowing the user to input the number of items in the list and the gap between elements. Note that the original list is placed in an indexed list. Each element is then L I S T I N G 6 . 7 /** * Josephus * * @author Dr. Lewis * @author Dr. Chase * @version 1.0, 08/13/08 */ import java.util.ArrayList; import java.util.Scanner; public class Josephus { /** * Continue around the circle eliminating every nth soldier * until all of the soldiers have been eliminated. */ public static void main (String[] args) { int numPeople, gap, newGap, counter; ArrayList<Integer> list = new ArrayList<Integer>(); Scanner in = new Scanner(System.in); // get the initial number of soldiers System.out.println("Enter the number of soldiers: "); numPeople = in.nextInt(); 151 152 C HA PT ER 6 L I S T I N G Lists 6 . 7 continued in.nextLine(); // get the gap between soldiers System.out.println("Enter the gap between soldiers: "); gap = in.nextInt(); // load the initial list of soldiers for (int count=1; count <= numPeople; count++) { list.add(new Integer(count)); } counter = gap - 1; newGap = gap; System.out.println("The order is: "); // Treating the list as circular, remove every nth element // until the list is empty while (!(list.isEmpty())) { System.out.println(list.remove(counter)); numPeople = numPeople - 1; if (numPeople > 0) counter = (counter + gap - 1) % numPeople; } } } removed from the list one at a time by computing the next index position in the list to be removed. The one complication in this process is the computation of the next index position to be removed. This is particularly interesting since the list collapses on itself as elements are removed. For example, the element number 6 from our previous example should be the second element removed from the list. However, once element 3 has been removed from the list, element 6 is no longer in its original position. Instead of being at index position 5 in the list, it is now at index position 4. Figure 6.11 illustrates the UML for the Josephus program. Notice that we have chosen to use the ArrayList implementation from the Java Collections API, which is actually an indexed list implementation. 6.4 Implementing Lists: With Arrays An array-based implementation of a list could fix one end of the list at index 0 and shift elements as needed. This is similar to our array-based implementation of 6.4 Implementing Lists: With Arrays java.util.AbstractList java.util.ArrayList java.util.AbstractCollection Josephus main(String[] args) java.util.Object F I GURE 6 .1 1 UML description of the Josephus program a stack from Chapter 3 and to the first array-based implementation of a queue we discussed in Chapter 5. We dismissed that implementation as too inefficient for our queue implementation because of the need to shift elements in the array either on enqueue or dequeue. However, with lists, we will now insert elements into and remove elements from the middle of the list and thus shifting of elements cannot be avoided. We could still use a circular array approach as we did with our arraybased queue implementation, but that will not eliminate the need to shift elements when adding or removing elements. That approach is left as a programming project. Figure 6.12 shows an array implementation of a list with the front of the list fixed at index 0. The integer variable rear represents the number of elements in the list and the next available slot for adding an element to the rear of the list. 0 1 2 3 A B C D rear 4 5 6 7 4 FIG URE 6 .1 2 An array implementation of a list ... 153 154 C HA PT ER 6 Lists Note that Figure 6.12 applies to both ordered and unordered lists. First we will explore the common operations. The header and class-level data of the ArrayList class are listed here to provide context: /** * ArrayList represents an array implementation of a list. The front of * the list is kept at array index 0. This class will be extended * to create a specific kind of list. * * @author Dr. Lewis * @author Dr. Chase * @version 1.0, 08/13/08 */ package jss2; import jss2.exceptions.*; import java.util.Iterator; public class ArrayList<T> implements ListADT<T>, Iterable<T> { protected final int DEFAULT_CAPACITY = 100; private final int NOT_FOUND = -1; protected int rear; protected T[] list; /** * Creates an empty list using the default capacity. */ public ArrayList() { rear = 0; list = (T[])(new Object[DEFAULT_CAPACITY]); } /** * Creates an empty list using the specified capacity. * * @param initialCapacity the integer value of the size of the array list */ public ArrayList (int initialCapacity) { rear = 0; list = (T[])(new Object[initialCapacity]); } 6.4 Implementing Lists: With Arrays The remove Operation This variation of the remove operation requires that we search for the element passed in as a parameter and remove it from the list if it is found. Then, elements at higher indexes in the array are shifted down in the list to fill in the gap. Consider what happens if the element to be removed is the first element in the list. In this case, there is a single comparison to find the element followed by n-1 shifts to shift the elements down to fill the gap. On the opposite extreme, what happens if the element to be removed is the last element in the list? In this case, we would require n comparisons to find the element and none of the remaining elements would need to be shifted. As it turns out, this implementation of the remove operation will always require exactly n comparisons and shifts and thus the operation is O(n). Note that if we were to use a circular array implementation, it would only improve the performance of the special case when the element to be removed is the first element. This operation can be implemented as follows: /** * Removes and returns the specified * * @param element * * @return * @throws ElementNotFoundException */ public T remove (T element) { T result; int index = find (element); element. the element to be removed and returned from the list the removed element if an element not found exception occurs if (index == NOT_FOUND) throw new ElementNotFoundException ("list"); result = list[index]; rear--; // shift the appropriate elements for (int scan=index; scan < rear; scan++) list[scan] = list[scan+1]; list[rear] = null; return result; } 155 156 C HA PT ER 6 Lists The remove method makes use of a method called find, which finds the element in question, if it exists in the list, and returns its index. The find method returns a constant called NOT_FOUND if the element is not in the list. The NOT_FOUND constant is equal to -1 and is defined in the ArrayList class. If the element is not found, a NoSuchElementException is generated. If it is found, the elements at higher indexes are shifted down, the rear value is updated, and the element is returned. The find method supports the implementation of a public operation on the list, rather than defining a new operation. Therefore, the find method is declared with private visibility. The find method can be implemented as follows: /** * Returns the array index of the specified element, or the * constant NOT_FOUND if it is not found. * * @param target the element that the list will be searched for * @return the integer index into the array containing the target * element, or the NOT_FOUND constant */ private int find (T target) { int scan = 0, result = NOT_FOUND; boolean found = false; if (! isEmpty()) while (! found && scan < rear) if (target.equals(list[scan])) found = true; else scan++; if (found) result = scan; return result; } Note that the find method relies on the equals method to determine if the target has been found. It’s possible that the object passed into the method is an exact copy of the element being sought. In fact, it may be an alias of the element in the list. However, if the parameter is a separate object, it may not contain all aspects of the element being sought. Only the key characteristics on which the equals method is based are important. 6.4 Implementing Lists: With Arrays The logic of the find method could have been incorporated into the remove method, though it would have made the remove method somewhat complicated. When appropriate, such support methods should be defined to keep each method readable. Furthermore, in this case, the find support method is useful in implementing the contains operation, as we will now explore. D E S I G N F O C U S The overriding of the equals method and the implementation of the Comparable interface are excellent examples of the power of object-oriented design. We can create implementations of collections that can handle classes of objects that have not yet been designed as long as those objects provide a definition of equality and/or a method of comparison between objects of the class. D E S I G N F O C U S Separating out private methods such as the find method in the ArrayList class provides multiple benefits. First, it simplifies the definition of the already complex remove method. Second, it allows us to use the find method to implement the contains operation as well as the addAfter method for an ArrayUnorderedList. Notice that the find method does not throw an ElementNotFound exception. It simply returns a value (-1), signifying that the element was not found. In this way, the calling routine can decide how to handle the fact that the element was not found. In the remove method, that means throwing an exception. In the contains method, that means returning false. The contains Operation The purpose of the contains operation is to determine if a particular element is currently contained in the list. As we discussed, we can use the find support method to create a fairly straightforward implementation: /** * Returns true if this list contains the specified element. * * @param target the element that the list is searched for * @return true if the target is in the list, false if otherwise */ public boolean contains (T target) { return (find(target) != NOT_FOUND); } 157 158 C HA PT ER 6 Lists If the target element is not found, the contains method returns false. If it is found, it returns true. A carefully constructed return statement ensures the proper return value. Because this method is performing a linear search of our list, our worst case will be that the element we are searching for is not in the list. This case would require n comparisons. We would expect this method to require, on average, n/2 comparisons, which results in the operation being O(n). The iterator Operation We have emphasized the idea thus far that we should reuse code whenever possible and design our solutions such that we can reuse them. The iterator operation is an excellent example of this philosophy. It would be possible to create an iterator method specifically for the array implementation of a list. However, instead we have created a general ArrayIterator class that will work with any array-based implementation of any collection. The iterator method for the array implementation of a list creates an instance of the ArrayIterator class. Listing 6.8 shows the ArrayIterator class. /** * Returns an iterator for the elements currently in this list. * * @return an iterator for the elements in this list */ public Iterator<T> iterator() { return new ArrayIterator<T> (list, rear); } The remaining common list operations are left as programming projects. Let’s turn our attention now to the operations that are particular to a specific type of list. The add Operation for an Ordered List The add operation is the only way an element can be added to an ordered list. No location is specified in the call because the elements themselves determine their order. Very much like the remove operation, the add operation will require a combination of comparisons and shifts: comparisons to find the correct location in the list 6.4 L I S T I N G Implementing Lists: With Arrays 6 . 8 /** * ArrayIterator represents an iterator over the elements of an array. * * @author Dr. Lewis * @author Dr. Chase * @version 1.0, 08/12/08 */ package jss2; import java.util.*; public class ArrayIterator<T> implements Iterator<T> { private int count; // the number of elements in the collection private int current; // the current position in the iteration private T[] items; /** * Sets up this iterator using the specified items. * * @param collection the collection to create the iterator for * @param size the size of the collection */ public ArrayIterator (T[] collection, int size) { items = collection; count = size; current = 0; } /** * Returns true if this iterator has at least one more element * to deliver in the iteration. * * @return true if this iterator has at least one more element to deliver * in the iteration */ public boolean hasNext() { return (current < count); } 159 160 C HA PT ER 6 L I S T I N G Lists 6 . 8 continued /** * Returns the next element in the iteration. If there are no * more elements in this iteration, a NoSuchElementException is * thrown. * * @return the next element in the iteration * @throws NoSuchElementException if an element not found exception occurs */ public T next() { if (! hasNext()) throw new NoSuchElementException(); current++; return items[current - 1]; } /** * The remove operation is not supported in this collection. * * @throws UnsupportedOperationException if an unsupported operation * exception occurs */ public void remove() throws UnsupportedOperationException { throw new UnsupportedOperationException(); } } and then shifts to open a position for the new element. Looking at the two extremes, if the element to be added to the list belongs at the front of the list, that will require one comparison and then the other n ᎑ 1 elements in the list will need to be shifted. If the element to be added belongs at the rear of the list, then this will require n comparisons and none of the other elements in the list will need to be shifted. Like the remove operation, the add operation will require n comparisons and shifts each time it is executed and thus the operation is O(n). The add operation can be implemented as follows: 6.4 Implementing Lists: With Arrays /** * Adds the specified Comparable element to this list, keeping * the elements in sorted order. * * @param element the element to be added to this list */ public void add (T element) { if (size() == list.length) expandCapacity(); Comparable<T> temp = (Comparable<T>)element; int scan = 0; while (scan < rear && temp.compareTo(list[scan]) > 0) scan++; for (int scan2=rear; scan2 > scan; scan2--) list[scan2] = list[scan2-1]; list[scan] = element; rear++; } Note that only Comparable objects can be stored in an ordered list. If an attempt is made to add a non-Comparable object to an ArrayOrderedList, a ClassCastException will result. Recall that the Comparable interface defines the compareTo method that returns a negative, zero, or positive integer value if the executing object is less than, equal to, or greater than the parameter, respectively. KEY CON CEPT Only Comparable objects can be stored in an ordered list. The unordered and indexed versions of a list do not require that the elements they store be Comparable. It is a testament to object-oriented programming that the various classes that implement these list variations can exist in harmony despite these differences. Operations Particular to Unordered Lists The addToFront and addToRear operations are similar to operations from other collections and therefore left as programming projects. Keep in mind that the 161 162 C HA PT ER 6 Lists addToFront operation must shift the current elements in the list first to make room at index 0 for the new element. Thus we know that the addToFront opera- tion will be O(n) because it requires n-1 elements to be shifted. Like the push operation on a stack, the addToRear operation will be O(1). The addAfter Operation for an Unordered List The addAfter operation accepts two parameters: one that represents the element to be added and one that represents the target element that determines the placement of the new element. The addAfter method must first find the target element, shift the elements at higher indexes to make room, and then insert the new element after it. Very much like the remove operation and the add operation for ordered lists, the addAfter method will require a combination of n comparisons and shifts and will be O(n). /** * Adds the specified element after the specified target element. * Throws an ElementNotFoundException if the target is not found. * * @param element the element to be added after the target element * @param target the target that the element is to be added after */ public void addAfter (T element, T target) { if (size() == list.length) expandCapacity(); int scan = 0; while (scan < rear && !target.equals(list[scan])) scan++; if (scan == rear) throw new ElementNotFoundException ("list"); scan++; for (int scan2=rear; scan2 > scan; scan2--) list[scan2] = list[scan2-1]; list[scan] = element; rear++; } 6.5 6.5 Implementing Lists: With Links Implementing Lists: With Links As we have seen with other collections, the use of a linked list is often another convenient way to implement a linear collection. The common operations that apply for ordered and unordered lists, as well as the particular operations for the both types, can be implemented with techniques similar to the ones that we have used before. We will examine a couple of the more interesting operations but will leave most of these as programming projects. The class header, class-level data, and constructor for our LinkedList class are provided for context. /** * LinkedList represents a linked implementation of a list. * * @author Dr. Lewis * @author Dr. Chase * @version 1.0, 08/13/08 */ package jss2; import jss2.exceptions.*; import java.util.*; public class LinkedList<T> implements ListADT<T>, Iterable<T> { protected int count; protected LinearNode<T> head, tail; /** * Creates an empty list. */ public LinkedList() { count = 0; head = tail = null; } The remove Operation The remove operation is part of the LinkedList class shared by both implementations: unordered and ordered lists. The remove operation consists of making sure that the list is not empty, finding the element to be removed, and then handling one 163 164 C HA PT ER 6 Lists of four cases: the element to be removed is the only element in the list, the element to be removed is the first element in the list, the element to be removed is the last element in the list, or the element to be removed is in the middle of the list. In all cases, the count is decremented by one. Unlike the remove operation for the array version, the linked version does not require elements to be shifted to close the gap. However, given that the worst case still requires n comparisons to determine that the target element is not in the list, the remove operation is still O(n). An implementation of the remove operation is shown below. /** * Removes the first instance of the specified element from this * list and returns a reference to it. Throws an EmptyListException * if the list is empty. Throws a NoSuchElementException if the * specified element is not found in the list. * * @param targetElement the element to be removed from the list * @return a reference to the removed element * @throws EmptyCollectionException if an empty collection exception occurs */ public T remove (T targetElement) throws EmptyCollectionException, ElementNotFoundException { if (isEmpty()) throw new EmptyCollectionException ("List"); boolean found = false; LinearNode<T> previous = null; LinearNode<T> current = head; while (current != null && !found) if (targetElement.equals (current.getElement())) found = true; else { previous = current; current = current.getNext(); } if (!found) throw new ElementNotFoundException ("List"); if (size() == 1) head = tail = null; else if (current.equals (head)) head = current.getNext(); 6.5 Implementing Lists: With Links else if (current.equals (tail)) { tail = previous; tail.setNext(null); } else previous.setNext(current.getNext()); count--; return current.getElement(); } Doubly Linked Lists Note how much code in this method is devoted to finding the target element and keeping track of a current and a previous reference. This seems like a missed opportunity to reuse code since we already have a find method in the LinkedList class. What if this list were doubly linked, meaning that each node stores a reference to the next element as well as to the previous element? Would this make the remove operation simpler? First, we would need a DoubleNode class, as shown in Listing 6.9. L I S T I N G 6 . 9 /** * DoubleNode represents a node in a doubly linked list. * * @author Dr. Lewis * @author Dr. Chase * @author Davis * @version 1.0, 08/13/08 */ package jss2; public class DoubleNode<E> { private DoubleNode<E> next; private E element; private DoubleNode<E> previous; 165 166 C HA PT ER 6 L I S T I N G Lists 6 . 9 continued /** * Creates an empty node. */ public DoubleNode() { next = null; element = null; previous = null; } /** * Creates a node storing the specified element. * * @param elem the element to be stored into the new node */ public DoubleNode (E elem) { next = null; element = elem; previous = null; } /** * Returns the node that follows this one. * * @return the node that follows the current one */ public DoubleNode<E> getNext() { return next; } /** * Returns the node that precedes this one. * * @return the node that precedes the current one */ public DoubleNode<E> getPrevious() { return previous; } 6.5 L I S T I N G 6 . 9 Implementing Lists: With Links continued /** * Sets the node that follows this one. * * @param dnode the node to be set as the one to follow the current one */ public void setNext (DoubleNode<E> dnode) { next = dnode; } /** * Sets the node that precedes this one. * * @param dnode the node to be set as the one to precede the current one */ public void setPrevious (DoubleNode<E> dnode) { previous = dnode; } /** * Returns the element stored in this node. * * @return the element stored in this node */ public E getElement() { return element; } /** * Sets the element stored in this node. * * @param elem the element to be stored in this node */ public void setElement (E elem) { element = elem; } } 167 168 C HA PT ER 6 Lists /** * Removes and returns the specified element. * * @param element the element to be removed and returned * from the list * @return the element that has been removed from * the list * @throws ElementNotFoundException if an element not found exception occurs */ public T remove (T element) { T result; DoubleNode<T> nodeptr = find (element); if (nodeptr == null) throw new ElementNotFoundException ("list"); result = nodeptr.getElement(); // check to see if front or rear if (nodeptr == front) result = this.removeFirst(); else if (nodeptr == rear) result = this.removeLast(); else { nodeptr.getNext().setPrevious(nodeptr.getPrevious()); nodeptr.getPrevious().setNext(nodeptr.getNext()); count-; } return result; } The remove operation can now be implemented much more elegantly using a doubly linked list. Note that we can now use the find operation to locate the target, and we no longer need to keep track of a previous reference. In this example, we also use the removeFirst and removeLast operations to handle the special cases associated with removing either the first or last element. The iterator Operation The iterator method simply returns a new LinkedIterator object: /** * Returns an iterator for the elements currently in this list. * 6.5 Implementing Lists: With Links * @return an iterator over the elements of this list */ public Iterator<T> iterator() { return new LinkedIterator<T>(head, count); } Like the ArrayIterator<T> class discussed earlier, the LinkedIterator<T> class is written so that it can be used with multiple collections. It stores the contents of the linked list and the count of elements, as shown in Listing 6.10. L I S T I N G 6 . 1 0 /** * LinkedIterator represents an iterator for a linked list of linear nodes. * * @author Dr. Lewis * @author Dr. Chase * @version 1.0, 08/13/08 */ package jss2; import jss2.exceptions.*; import java.util.*; public class LinkedIterator<T> implements Iterator<T> { private int count; // the number of elements in the collection private LinearNode<T> current; // the current position /** * Sets up this iterator using the specified items. * * @param collection the collection the iterator will move over * @param size the integer size of the collection */ public LinkedIterator (LinearNode<T> collection, int size) { current = collection; count = size; } 169 170 C HA PT ER 6 L I S T I N G Lists 6 . 1 0 continued /** * Returns true if this iterator has at least one more element * to deliver in the iteration. * * @return true if this iterator has a least one more element to deliver * in the iteration */ public boolean hasNext() { return (current!= null); } /** * Returns the next element in the iteration. If there are no * more elements in this iteration, a NoSuchElementException is * thrown. * * @return the next element in the iteration * @throws NoSuchElementException if a no such element exception occurs */ public T next() { if (! hasNext()) throw new NoSuchElementException(); T result = current.getElement(); current = current.getNext(); return result; } /** * The remove operation is not supported. * * @throws UnsupportedOperationException if an unsupported operation * exception occurs */ public void remove() throws UnsupportedOperationException { throw new UnsupportedOperationException(); } } Lists in the Java Collections API rear of list front of list 6.6 FIG URE 6 .1 3 A doubly linked list The LinkedIterator constructor sets up a reference that is designed to move across the list of elements in response to calls to the next method. The iteration is complete when current becomes null, which is the condition returned by the hasNext method. As in the case of the ArrayIterator<T> class from earlier in this chapter, the remove method is left unsupported. Figure 6.13 illustrates the structure of a doubly linked list. The implementations of the other operations for both linked and doubly linked lists are left as exercises. 6.6 Lists in the Java Collections API The Java Collections API provides three separate implementations for an indexed list: Vector, ArrayList and LinkedList. All three of these classes extend the abstract class java.util.AbstractList, which implements the java.util.List interface. These are part of the Java class library and are distinct from the interfaces and classes we have discussed so far in this chapter. The java.util.AbstractList class is an extension of the java.util.AbstractCollection class, which implements the java.util.Collection interface. All of the list implementations provided in the Java Collections API framework are indexed lists, even though the class names do not identify them as such. All three of these list implementations share some common attributes. First, they all implement the java.util.List interface. The precise details of the abstract methods in this interface are available online but suffice to say, there are many more methods than those in our simple implementation from earlier in the chapter. Keep in mind that the Java language is a very rich, industrial-strength language, not necessarily designed for education. Thus, as we have discussed previously, many of the collections within the Java Collections API do not precisely conform to the conceptual definition of their respective collections. They often provide a larger variety of operations than the conceptual collection prescribes. 171 172 C HA PT ER 6 Lists In addition to the Collection and List interfaces, all three varieties of lists implement the Cloneable and the Serializable interfaces as well. Cloneable Implementing the Cloneable interface does not necessarily require any additional methods. The interface does not contain a clone method or any other abstract methods. This interface simply indicates to the Object.clone() method that this class allows the method to make a field-for-field copy of its instances. Typically, classes that implement the Cloneable interface override the Object.clone() method with a public method of their own. Serializable Although serializability in general can provide a number of advantages, the typical use in our context is that a class that implements the Serializable interface can be decomposed into a byte stream and then written to memory or to disk using the ObjectOutputStream class of the java.io package. This byte stream can then be reconstructed into the original object using the ObjectInputStream class, also in the java.io package. Why is this important? Consider that when you create a class in Java source code (e.g., someclass.java), you are not creating data but simply creating a blueprint or structure for data. To this point, our data has typically been hard coded into our programs, input by the user as the program executes, or read from a file as a stream of text and then parsed into the appropriate fields of objects. This means that while the classes that we have been creating have been persistent, the instances of those classes have not. The instances simply cease to exist when the program terminates. Creating classes that implement the Serializable interface allows instances of those classes to be written to memory or to disk and then reconstructed at a later date without the developer having to rebuild them from text. Like the Cloneable interface, there are not any methods associated with the Serializable interface. This interface is simply a marker notifying the run-time environment that objects of this class are to be serialized. RandomAccess Both the Vector class and the ArrayList class also implement the RandomAccess interface. Like both the Cloneable interface and the Serializable interface, there are not any methods associated with the RandomAccess interface. This interface is simply a marker indicating that K E Y C ON C E PT these classes provide fast random access. Both Vector and The Java Collections API contains three ArrayList are array-based implementations and as we have implementations of an indexed list. seen with our own implementations earlier in this chapter, this gives us O(1) access to elements in the list. 6.6 Lists in the Java Collections API Keep in mind that operations other than simply accessing elements may be more complex. In fact we know that insertion and removal of elements will be O(n) based on the need to shift elements. Java.util.Vector The Vector implementation of an indexed list is an array-based implementation. Thus, many of the issues discussed in the array implementations of stacks, queues, unordered lists, and ordered lists apply here as well. For example, using an array implementation of a list, an add operation that specifies an index in the middle of the list will require all of the elements above that position in the list to be shifted one position higher in the list. Likewise, a remove operation that removes an element from the middle of the list will require all of the elements above that position in the list to be shifted one position lower in the list. The Vector class implements the Serializable, Cloneable, Iterable<E>, Collection<E>, Deque<E>, List<E>, and Queue<E> interfaces. Figure 6.14 illustrates the public and protected methods available for the Vector class. The Vector class also inherits quite a number of additional methods from its superclasses. Java.util.ArrayList The ArrayList implementation of an indexed list is, as its name implies, an arraybased implementation. Thus, just as with the implementation of the Vector class, many of the issues discussed in the array implementations of stacks, queues, unordered lists, and ordered lists apply here as well. For example, using an array implementation of a list, an add operation that specifies an index in the middle of the list will require all of the elements above that position in the list to be shifted one position higher in the list. Likewise, a remove operation that removes an element from the middle of the list will require all of the elements above that position in the list to be shifted one position lower in the list. Like the Vector class, the ArrayList implementation is resizable, meaning that if adding the next element would overflow the ArrayList, the underlying array is automatically resized. To manage the size of the array, both classes contain two additional operations: ensureCapacity increases the size of the array to the specified size if it is not already that large or larger, and trimToSize trims the array to the actual current size of the list. The ArrayList implementation is very similar to the implementation of a Vector. However, the Vector operations are synchronized, and ArrayList operations are not. Figure 6.15 illustrates the public and protected methods available for the ArrayList class. The ArrayList class also inherits quite a number of additional methods from its superclasses. The ArrayList class implements the Serializable, Coneable, Iterable<E>, Collection<E>, List<E>, and RandomAccess interfaces. 173 174 C HA PT ER 6 Lists Method Signature Return Type Description add(E e) boolean Appends the specified element to the end of this Vectorvector. add(int index, E element) void Inserts the specified element at the specified position in this vector. addAll(Collection <? extends E > c) boolean Appends all of the elements in the specified collection to the end of this vector, in the order that they are returned by the specified collection’s iterator. addAll(int index, Collection<? extends E > c) boolean Inserts all of the elements in the specified collection into this vector at the specified position. addElement(E obj) void Adds the specified component to the end of this vector, increasing its size by one. capacity() int Returns the current capacity of this vector. clear() void Removes all of the elements from this vector. clone() Object Returns a clone of this vector. contains(Object o) boolean Returns true if this vector contains the specified element. containsAll (Collection<?> c) boolean Returns true if this vector contains all of the elements in the specified collection. copyInto(Object[] anArray) void Copies the components of this vector into the specified array. elementAt(int index) E Returns the component at the specified index. elements() Enumeration<E> Returns an enumeration of the components of this vector. ensureCapacity(int minCapacity) void Increases the capacity of this vector, if necessary, to ensure that it can hold at least the number of components specified by the minimum capacity argument. equals(Object o) boolean Compares the specified object with this vector for equality. firstElement() E Returns the first component (the item at index 0) of this vector. get(int index) E Returns the element at the specified position in this vector. hashCode() int Returns the hash code value for this vector. indexOf(Object o) int Returns the index of the first occurrence of the specified element in this vector, or -1 if this vector does not contain the element. F I GU R E 6.1 4 Public and protected methods of the java.util.Vector class 6.6 Lists in the Java Collections API 175 Method Signature Return Type Description indexOf(Object o, int index) int Returns the index of the first occurrence of the specified element in this vector, searching forwards from index, or returns -1 if the element is not found. insertElementAt (E obj, int index) void Inserts the specified object as a component in this vector at the specified index. isEmpty() boolean Tests if this vector has no components. lastElement() E Returns the last component of the vector. lastIndexOf(Object o) int Returns the index of the last occurrence of the specified element in this vector, or -1 if this vector does not contain the element. lastIndexOf(Object o, int index) int Returns the index of the last occurrence of the specified element in this vector, searching backwards from index, or returns -1 if the element is not found. remove(int index) E Removes the element at the specified position in this vector. remove(Object o) boolean Removes the first occurrence of the specified element in this vector. If the vector does not contain the element, it is unchanged. removeAll (Collection<?> c) boolean Removes from this vector all of its elements that are contained in the specified collection. removeAllElements() void Removes all components from this vector and sets its size to zero. removeElement(Object obj) boolean Removes the first (lowest-indexed) occurrence of the argument from this vector. removeElementAt(int index) void Deletes the component at the specified index. removeRange(int fromIndex, int toIndex) void Removes from this list all of the elements whose index is between fromIndex, inclusive and toIndex, exclusive. retainAll (Collection<?> c) boolean Retains only the elements in this vector that are contained in the specified collection. set(int index, E element) E Replaces the element at the specified position in this vector with the specified element. setElementAt(E obj, int index) void Sets the component at the specified index of this vector to be the specified object. setSize(int newSize) void Sets the size of this vector. size() int Returns the number of components in this vector. FIG URE 6 . 1 4 Continued 176 C HA PT ER 6 Lists Method Signature Return Type Description subList(int fromIndex, int toIndex) List<E> Returns a view of the portion of this List between fromIndex, inclusive, and toIndex, exclusive. toArray() Object[] Returns an array containing all of the elements in this vector in the correct order. toArray(T[] a) <T> T[] Returns an array containing all of the elements in this vector in the correct order; the runtime type of the returned array is that of the specified array. toString() String Returns a string representation of this vector, containing the String representation of each element. trimToSize() void Trims the capacity of this vector to be the vector’s current size. FIG URE 6 .1 4 Continued Like arrays and the Vector implementation, one advantage of the ArrayList implementation is the ability to access any element in the list in equal time. However, the penalty for that access is the added cost of shifting remaining elements either as part of an insertion into the list or a deletion from the list. Java.util.LinkedList The LinkedList implementation of an indexed list is, as the name implies, a linked implementation. Thus, many of the issues discussed in the linked implementations of stacks, queues, unordered lists, and ordered lists apply here as well. For example, using a linked implementation of a list requires us to traverse the list to access a particular element instead of going directly to it by index. For this reason, the LinkedList class does not implement the RandomAccess interface because it does not provide constant or near constant time access to its elements. In addition to the interfaces discussed previously that all of the Java Collection API implementations of a list implement, the LinkedList class also implements both the Queue interface and the Deque interface (pronounced deck and meaning double-ended queue). As we discussed earlier, this is an artifact of the fact that Java is an industrial language and was not necessarily designed for education. As we will see, this use of the LinkedList class to implement a queue creates a collection that will allow actions inappropriate for a queue. The LinkedList class implements the Serializable, Cloneable, Iterable<E>, Collection<E>, Deque<E>, List<E>, and Queue<E> interfaces. Figure 6.16 illustrates the public and protected methods available for the LinkedList class. The LinkedList class also inherits quite a number of additional methods from its superclasses. 6.6 Lists in the Java Collections API 177 Method Signature Return Type Description add(E e) boolean Appends the specified element to the end of this list. add(int index, E element) void Inserts the specified element at the specified position in this list. addAll(Collection<? extends E > c) boolean addAll(int index, Collection<? extends E > c) boolean Appends all of the elements in the specified collection to the end of this list, in the order that they are returned by the specified collection’s iterator. Inserts all of the elements in the specified collection into this list, starting at the specified position. clear() void Removes all of the elements from this list. clone() Object Returns a shallow copy of this ArrayList instance. contains(Object o) boolean Returns true if this list contains the specified element. ensureCapacity(int minCapacity) void Increases the capacity of this ArrayList instance, if necessary, to ensure that it can hold at least the number of elements specified by the minimum capacity argument. get(int index) E Returns the element at the specified position in this list. indexOf(Object o) int Returns the index of the first occurrence of the specified element in this list, or -1 if this list does not contain the element. isEmpty() boolean Returns true if this list contains no elements. lastIndexOf(Object o) int Returns the index of the last occurrence of the specified element in this list, or -1 if this list does not contain the element. remove(int index) E Removes the element at the specified position in this list. remove(Object o) boolean Removes the first occurrence of the specified element from this list, if it is present. removeRange(int fromIndex, int toIndex) void Removes from this list all of the elements whose index is between fromIndex, inclusive, and toIndex, exclusive. set(int index, E element) E Replaces the element at the specified position in this list with the specified element. size() int Returns the number of elements in this list. toArray() Object[] Returns an array containing all of the elements in this list in proper sequence (from first to last element). toArray(T[] a) <T> T[] Returns an array containing all of the elements in this list in proper sequence (from first to last element); the runtime type of the returned array is that of the specified array. trimToSize() void Trims the capacity of this ArrayList instance to be the list’s current size. F I GU RE 6 .1 5 Public and protected methods of the java.util.ArrayList class 178 C HA PT ER 6 Lists Method Signature Return Type Description add(E e) boolean Appends the specified element to the end of this list. add(int index, E element) void Inserts the specified element at the specified position in this list. addAll(Collection<? extends E > c) boolean Appends all of the elements in the specified collection to the end of this list, in the order that they are returned by the specified collection’s iterator. addAll(int index, Collection<? extends E > c) addFirst(E e) boolean Inserts all of the elements in the specified collection into this list, starting at the specified position. void Inserts the specified element at the beginning of the list. addLast(E e) void Appends the specified element at the end of the list. clear() void Removes all of the elements from this list. clone() Object Returns a shallow copy of this ArrayList instance. contains(Object o) boolean Returns true if this list contains the specified element. descendingIterator() Iterator<E> Returns an iterator over the elements in this deque in reverse sequential order. element() E Retrieves, but does not remove, the head (first element) of this list. get(int index) E Returns the element at the specified position in this list. getFirst() E Returns the first element in this list. getLast() E Returns the last element in this list. indexOf(Object o) int Returns the index of the first occurrence of the specified element in this list, or -1 if this list does not contain the element. lastIndexOf(Object o) int Returns the index of the last occurrence of the specified element in this list, or -1 if this list does not contain the element. listIterator(int index) ListIterator<E> Returns a list-iterator of the elements in this list (in proper sequence), starting at the specified position in the list. offer(E e) boolean offerFirst(E e) boolean Adds the specified element as the tail (last element) of the list. Inserts the specified element at the front of this list. offerLast(E e) peek() boolean E Inserts the specified element at the end of this list. Retrieves, but does not remove, the first element in the list. FI GU R E 6. 16 Public and protected methods of the java.util.LinkedList class 6.6 Lists in the Java Collections API 179 Method Signature Return Type Description peekFirst() E Retrieves, but does not remove, the first element in the list or returns null if the list is empty. peekLast() E Retrieves, but does not remove, the last element in the list or returns null if the list is empty. poll() E Retrieves and removes the head (first element) of this list. pollFirst() E Retrieves and removes the first element of this list or returns null if the list is empty. pollLast() E Retrieves and removes the last element of this list or returns null if the list is empty. pop() E Pops an element from the stack represented by this list. push(E e) void remove() E Pushes an element onto the stack represented by this list. Retrieves and removes the head (first element) of this list. remove(int index) E Removes the element at the specified position in this list. remove(Object o) boolean Removes the first occurrence of the specified element from this list, if it is present. removeFirst() E Removes and returns the first element from this list. removeFirstOccurrence (Object o) boolean Removes the first occurrence of the specified element from this list. removeLast() E Removes and returns the last element from this list. removeLastOccurrence (Object o) boolean Removes the last occurrence of the specified element from this list. set(int index, E element) E Replaces the element at the specified position in this list with the specified element. size() int Returns the number of elements in this list. toArray() Object[] Returns an array containing all of the elements in this list in proper sequence (from first to last element). toArray(T[] a) <T> T[] Returns an array containing all of the elements in this list in proper sequence (from first to last element). The runtime type of the returned array is that of the specified array. FIG URE 6 . 1 6 Continued 180 C HA PT ER 6 Lists Summary of Key Concepts ■ List collections can be categorized as ordered, unordered, and indexed. ■ The elements of an ordered list have an inherent relationship defining their order. ■ The elements of an unordered list are kept in whatever order the client chooses. ■ An indexed list maintains a contiguous numeric index range for its elements. ■ Many common operations can be defined for all list types. The differences between them stem from how elements are added. ■ An iterator is an object that provides a means to iterate over a collection. ■ Interfaces can be used to derive other interfaces. The child interface contains all abstract methods of the parent. ■ An interface name can be used to declare an object reference variable. An interface reference can refer to any object of any class that implements the interface. ■ Interfaces allow us to make polymorphic references in which the method that is invoked is based on the particular object being referenced at the time. ■ An ordered list is a convenient collection to use when creating a tournament schedule. ■ The Josephus problem is a classic computing problem that is appropriately solved with indexed lists. ■ Only Comparable objects can be stored in an ordered list. ■ The Java Collections API contains three implementations of an indexed list. Self-Review Questions SR 6.1 What is the difference between an indexed list, an ordered list, and an unordered list? SR 6.2 What are the basic methods of accessing an indexed list? SR 6.3 What are the additional operations required of implementations that are part of the Java Collections API framework? SR 6.4 What are the trade-offs in space complexity between an ArrayList and a LinkedList? SR 6.5 What are the trade-offs in time complexity between an ArrayList and a LinkedList? Exercises SR 6.6 What is the time complexity of the contains operation and the find operation for both implementations? SR 6.7 What effect would it have if the LinkedList implementation were to use a singly linked list instead of a doubly linked list? SR 6.8 Why is the time to increase the capacity of the array on an add operation considered negligible for the ArrayList implementation? SR 6.9 What is an iterator and why is it useful for ADTs? SR 6.10 Why was an iterator not appropriate for stacks and queues but is appropriate for lists? SR 6.11 Why is a circular array implementation not as attractive as an implementation of a list as it was for a queue? Exercises EX 6.1 Hand trace an ordered list X through the following operations: X.add(new Integer(4)); X.add(new Integer(7)); Object Y = X.first(); X.add(new Integer(3)); X.add(new Integer(2)); X.add(new Integer(5)); Object Y = X.removeLast(); Object Y = X.remove(new Integer(7)); X.add(new Integer(9)); EX 6.2 Given the resulting list X from Exercise 6.1, what would be the result of each of the following? a. X.last(); b. z = X.contains(new Integer(3)); X.first(); c. Y = X.remove(new Integer(2)); X.first(); EX 6.3 EX 6.4 What would be the time complexity of the size operation for each of the implementations if there were not a count variable? In the array implementation, under what circumstances could the head and tail references be equal? EX 6.5 In the linked implementation, under what circumstances could the head and tail references be equal? 181 182 C HA PT ER 6 Lists EX 6.6 If there were not a count variable in the array implementation, how could you determine whether or not the list was empty? EX 6.7 If there were not a count variable in the array implementation, how could you determine whether or not the list was full? Programming Projects PP 6.1 Implement a stack using a LinkedList. PP 6.2 Implement a stack using an ArrayList. PP 6.3 Implement a queue using a LinkedList. PP 6.4 Implement a queue using an ArrayList. PP 6.5 Implement the Josephus problem using a queue, and compare the performance of that algorithm to the ArrayList implementation from this chapter. PP 6.6 Implement an OrderedList using a LinkedList. PP 6.7 Implement an OrderedList using an ArrayList. PP 6.8 Complete the implementation of the ArrayList class. PP 6.9 Complete the implementation of the ArrayOrderedList class. PP 6.10 Complete the implementation of the ArrayUnorderedList class. PP 6.11 Write an implementation of the LinkedList class. PP 6.12 Write an implementation of the LinkedOrderedList class. PP 6.13 Write an implementation of the LinkedUnorderedList class. PP 6.14 Create an implementation of a doubly linked DoubleOrderedList class. You will need to create a DoubleNode class, a DoubleList class, and a DoubleIterator class. PP 6.15 Create a graphical application that provides a button for add and remove from an ordered list, a text field to accept a string as input for add, and a text area to display the contents of the list after each operation. PP 6.16 Create a graphical application that provides a button for addToFront, addToRear, addAfter, and remove from an un- ordered list. Your application must provide a text field to accept a string as input for any of the add operations. The user should be able to select the element to be added after, and select the element to be removed. Answers to Self-Review Questions PP 6.17 Modify the TournamentMaker program from this chapter so that the user may select some number of teams to receive a bye in the first round. For example, in a ten team league, the user may specify that the top two teams would automatically advance to the second round to play the winner of the contest between teams 6 and 7, and 5 and 8, respectively. Answers to Self-Review Questions SRA 6.1 An indexed list is a collection of objects with no inherent order that are ordered by index value. An ordered list is a collection of objects ordered by value. An unordered list is a collection of objects with no inherent order. SRA 6.2 Access to the list is accomplished in one of three ways: by accessing a particular index position in the list, by accessing the ends of the list, or by accessing an object in the list by value. SRA 6.3 All Java Collections API framework classes implement the Collections interface, the Serializable interface, and the Cloneable interface. SRA 6.4 The linked implementation requires more space per object to be inserted in the list simply because of the space allocated for the references. Keep in mind that the LinkedList class is actually a doubly linked list, thus requiring twice as much space for references. The ArrayList class is more efficient at managing space than the array-based implementations we have discussed previously. This is due to the fact that ArrayList collections are resizable, and thus can dynamically allocate space as needed. Therefore, there need not be a large amount of wasted space allocated all at once. Rather, the list can grow as needed. SRA 6.5 The major difference between the two is access to a particular index position of the list. The ArrayList implementation can access any element of the list in equal time if the index value is known. The LinkedList implementation requires the list to be traversed from one end or the other to reach a particular index position. SRA 6.6 The contains and find operations for both implementations will be O(n) because they are simply linear searches. SRA 6.7 This would change the time complexity for the addToRear and removeLast operations because they would now require traversal of the list. 183 184 C HA PT ER 6 Lists SRA 6.8 Averaged over the total number of insertions into the list, the time to enlarge the array has little effect on the total time. SRA 6.9 An iterator is an object that provides a means of stepping through the elements of a collection one at a time. SRA 6.10 Conceptually, stacks and queues should not allow access to elements in the middle of the list. Stacks only allow access to the top and queues only allow the addition of elements on one end and removal of elements on the other. An iterator would violate the conceptual definition of these collections. Lists, on the other hand, allow access throughout and are perfectly suited for an iterator. SRA 6.11 The circular array implementation of a queue improved the efficiency of the dequeue operation from O(n) to O(1) because it eliminated the need to shift elements in the array. That is not the case for a list because we can add or remove elements anywhere in the list, not just at the front or the rear. 7 Recursion R ecursion is a powerful programming technique that provides elegant solutions to certain problems. It is particu- CHAPTER OBJECTIVES ■ Explain the underlying concepts of recursion ■ Examine recursive methods and unravel their processing steps ■ Define infinite recursion and discuss ways to avoid it ■ Explain when recursion should and should not be used ■ Demonstrate the use of recursion to solve problems larly helpful in the implementation of various data structures and in the process of searching and sorting data. This chapter provides an introduction to recursive processing. It contains an explanation of the basic concepts underlying recursion and then explores the use of recursion in programming. 185 186 C HA PT ER 7 Recursion 7.1 Recursive Thinking K E Y C O N C E PT Recursion is a programming technique in which a method calls itself. A key to being able to program recursively is to be able to think recursively. We know that one method can call another method to help it accomplish its goal. Similarly, a method can also call itself to help accomplish its goal. Recursion is a programming technique in which a method calls itself to fulfill its overall purpose. Before we get into the details of how we use recursion in a program, we need to explore the general concept of recursion first. The ability to think recursively is essential to being able to use recursion as a programming technique. In general, recursion is the process of defining something in terms of itself. For example, consider the following definition of the word decoration: decoration: n. any ornament or adornment used to decorate something The word decorate is used to define the word decoration. You may recall your grade-school teacher telling you to avoid such recursive definitions when explaining the meaning of a word. However, in many situations, recursion is an appropriate way to express an idea or definition. For example, suppose we want to formally define a list of one or more numbers, separated by commas. Such a list can be defined recursively either as a number or as a number followed by a comma followed by a list. This definition can be expressed as follows: A list is a: number or a: number comma list This recursive definition of a list defines each of the following lists of numbers: 24, 88, 40, 37 96, 43 14, 64, 21, 69, 32, 93, 47, 81, 28, 45, 81, 52, 69 70 No matter how long a list is, the recursive definition describes it. A list of one element, such as in the last example, is defined completely by the first (nonrecursive) part of the definition. For any list longer than one element, the recursive part of the definition (the part that refers to itself) is used as many times as necessary, until the last element is reached. The last element in the list is always defined by the nonrecursive part of this definition. Figure 7.1 shows how one particular list of numbers corresponds to the recursive definition of list. Infinite Recursion Note that this definition of a list contains one option that is recursive, and one option that is not. The part of the definition that is not recursive is called the base case. If all options had a recursive component, then the recursion would 7.1 LIST: number comma 24 , Recursive Thinking 187 LIST 88, 40, 37 number comma 88 , LIST 40, 37 number comma 40 , LIST 37 number 37 F I G U R E 7 . 1 Tracing the recursive definition of a list never end. For example, if the definition of a list were simply “a number followed by a comma followed by a list,” then no list could ever end. This problem is called infinite recursion. It is similar to an infinite loop, except that the “loop” occurs in the definition itself. KEY CON CEPT Any recursive definition must have a nonrecursive part, called the base case, which permits the recursion to eventually end. As in the infinite loop problem, a programmer must be careful to design algorithms so that they avoid infinite recursion. Any recursive definition must have a base case that does not result in a recursive option. The base case of the list definition is a single number that is not followed by anything. In other words, when the last number in the list is reached, the base case option terminates the recursive path. Recursion in Math Let’s look at an example of recursion in mathematics. The value referred to as N! (which is pronounced N factorial) is defined for any positive integer N as the product of all integers between 1 and N inclusive. Therefore: 3! = 3*2*1 = 6 and 5! = 5*4*3*2*1 = 120. Mathematical formulas are often expressed recursively. The definition of N! can be expressed recursively as: 1! = 1 N! = N * (N-1)! for N > 1 188 C HA PT ER 7 Recursion KE Y CO N C E PT Mathematical problems and formulas are often expressed recursively. The base case of this definition is 1!, which is defined to be 1. All other values of N! (for N > 1) are defined recursively as N times the value (N–1)!. The recursion is that the factorial function is defined in terms of the factorial function. Using this definition, 50! is equal to 50 * 49!. And 49! is equal to 49 * 48!. And 48! is equal to 48 * 47!. This process continues until we get to the base case of 1. Because N! is defined only for positive integers, this definition is complete and will always conclude with the base case. The next section describes how recursion is accomplished in programs. 7.2 Recursive Programming Let’s use a simple mathematical operation to demonstrate the concepts of recursive programming. Consider the process of summing the values between 1 and N inclusive, where N is any positive integer. The sum of the values from 1 to N can be expressed as N plus the sum of the values from 1 to N–1. That sum can be expressed similarly, as shown in Figure 7.2. K E Y C O N C E PT Each recursive call to a method creates new local variables and parameters. For example, the sum of the values between 1 and 20 is equal to 20 plus the sum of the values between 1 and 19. Continuing this approach, the sum of the values between 1 and 19 is equal to 19 plus the sum of the values between 1 and 18. This may sound like a strange way to think about this problem, but it is a straightforward example that can be used to demonstrate how recursion is programmed. In Java, as in many other programming languages, a method can call itself. Each call to the method creates a new environment in which to work. That is, all N Σ i=1 N –1 i = N + Σ N –2 i = N + N–1 + i=1 i Σ i=1 N –3 = N + N –1 + N –2 + Σi i=1 = N + N –1 + N –2 + . . . + 2 + 1 F I G U R E 7 . 2 The sum of the numbers 1 through N, defined recursively 7.2 Recursive Programming 189 local variables and parameters are newly defined with their own unique data space every time the method is called. Each parameter is given an initial value based on the new call. Each time a method terminates, processing returns to the method that called it (which may be an earlier invocation of the same method). These rules are no different from those governing any “regular” method invocation. A recursive solution to the summation problem is defined by the following recursive method called sum: // This method returns the sum of 1 to num public int sum (int num) { int result; if (num == 1) result = 1; else result = num + sum (num-1); return result; } Note that this method essentially embodies our recursive definition that the sum of the numbers between 1 and N is equal to N plus the sum of the numbers between 1 and N–1. The sum method is recursive because sum calls itself. The parameter passed to sum is decremented each time sum is called, until it reaches the base case of 1. Recursive methods usually contain an if-else statement, with one of the branches representing the base case. KEY CON CEPT Suppose the main method calls sum, passing it an initial value of 1, which is stored in the parameter num. Because num is equal to 1, the result of 1 is returned to main, and no recursion occurs. A careful trace of recursive processing can provide insight into the way it is used to solve a problem. Now let’s trace the execution of the sum method when it is passed an initial value of 2. Because num does not equal 1, sum is called again with an argument of num-1, or 1. This is a new call to the method sum, with a new parameter num and a new local variable result. Because this num is equal to 1 in this invocation, the result of 1 is returned without further recursive calls. Control returns to the first version of sum that was invoked. The return value of 1 is added to the initial value of num in that call to sum, which is 2. Therefore, result is assigned the value 3, which is returned to the main method. The method called from main correctly calculates the sum of the integers from 1 to 2, and returns the result of 3. The base case in the summation example is when N equals 1, at which point no further recursive calls are made. The recursion begins to fold back into the earlier versions of the sum method, returning the appropriate value each time. Each return value contributes to the computation of the sum at the higher level. Without the base case, infinite recursion would result. Each call to a method requires additional 190 C HA PT ER 7 Recursion main result = 4 + sum(3) sum(4) sum result = 3 + sum(2) sum(3) sum result = 2 + sum(1) sum(2) result = 1 sum sum(1) sum F I G U R E 7 . 3 Recursive calls to the sum method memory space; therefore, infinite recursion often results in a run-time error indicating that memory has been exhausted. Trace the sum function with different initial values of num until this processing becomes familiar. Figure 7.3 illustrates the recursive calls when main invokes sum to determine the sum of the integers from 1 to 4. Each box represents a copy of the method as it is invoked, indicating the allocation of space to store the formal parameters and any local variables. Invocations are shown as solid lines, and returns are shown as dotted lines. The return value result is shown at each step. The recursive path is followed completely until the base case is reached; then the calls begin to return their result up through the chain. Recursion versus Iteration Of course, there is a nonrecursive solution to the summation problem we just explored. One way to compute the sum of the numbers between 1 and num inclusive in an iterative manner is as follows: sum = 0; for (int number = 1; number <= num; number++) sum += number; This solution is certainly more straightforward than the recursive version. We used the summation problem to demonstrate recursion because it is a simple problem to understand, not because you would use recursion to solve it under 7.2 normal conditions. Recursion has the overhead of multiple method invocations and, in this case, presents a more complicated solution than its iterative counterpart. Recursive Programming KEY CON CEPT Recursion is the most elegant and appropriate way to solve some problems, but for others it is less intuitive than an iterative solution. A programmer must learn when to use recursion and when not to use it. Determining which approach is best is another important software engineering decision that depends on the problem being solved. All problems can be solved in an iterative manner, but in some cases the iterative version is much more complicated. Recursion, for some problems, allows us to create relatively short, elegant programs. Direct versus Indirect Recursion Direct recursion occurs when a method invokes itself, such as when sum calls sum. Indirect recursion occurs when a method invokes another method, eventually resulting in the original method being invoked again. For example, if method m1 invokes method m2, and m2 invokes method m1, we can say that m1 is indirectly recursive. The amount of indirection could be several levels deep, as when m1 invokes m2, which invokes m3, which invokes m4, which invokes m1. Figure 7.4 depicts a situation with indirect recursion. Method invocations are shown with solid lines, and returns are shown with dotted lines. The entire invocation path is followed, and then the recursion unravels following the return path. m1 m2 m3 m1 m2 m3 m1 m2 F I G U R E 7 . 4 Indirect recursion m3 191 192 C HA PT ER 7 Recursion Indirect recursion requires all of the same attention to base cases that direct recursion requires. Furthermore, indirect recursion can be more difficult to trace because of the intervening method calls. Therefore, extra care is warranted when designing or evaluating indirectly recursive methods. Ensure that the indirection is truly necessary and clearly explained in documentation. 7.3 Using Recursion The following sections describe problems that we then solve using a recursive technique. For each one, we examine exactly how recursion plays a role in the solution and how a base case is used to terminate the recursion. As you explore these examples, consider how complicated a nonrecursive solution for each problem would be. Traversing a Maze As we discussed in Chapter 4, solving a maze involves a great deal of trial and error: following a path, backtracking when you cannot go farther, and trying other, untried options. Such activities often are handled nicely using recursion. In Chapter 4, we solved this problem iteratively using a stack to keep track of our potential moves. However, we can also solve this problem recursively by using the run-time stack to keep track of our progress. The program shown in Listing 7.1 creates a Maze object and attempts to traverse it. The Maze class, shown in Listing 7.2, uses a two-dimensional array of integers to represent the maze. The goal is to move from the top-left corner (the entry point) to the bottom-right corner (the exit point). Initially, a 1 indicates a clear path, and a 0 indicates a blocked path. As the maze is solved, these array elements are changed to other values to indicate attempted paths and, ultimately, a successful path through the maze if one exists. Figure 7.5 shows the UML illustration of this solution. The only valid moves through the maze are in the four primary directions: down, right, up, and left. No diagonal moves are allowed. In this example, the Maze MazeSearch uses mainString[]args TRIED, PATH grid traverse(int row, int column) valid(int row, int column) toString() F I G U R E 7 . 5 UML description of the Maze and MazeSearch classes 7.3 L I S T I N G Using Recursion 7 . 1 /** * MazeSearch demonstrates recursion. * * @author Dr. Chase * @author Dr. Lewis * @version 1.0, 8/18/08 */ public class MazeSearch { /** * Creates a new maze, prints its original form, attempts to * solve it, and prints out its final form. */ public static void main (String[] args) { Maze labyrinth = new Maze(); System.out.println (labyrinth); if (labyrinth.traverse (0, 0)) System.out.println ("The maze was successfully traversed!"); else System.out.println ("There is no possible path."); System.out.println (labyrinth); } } maze is 8 rows by 13 columns, although the code is designed to handle a maze of any size. Let’s think this through recursively. The maze can be traversed successfully if it can be traversed successfully from position (0, 0). Therefore, the maze can be traversed successfully if it can be traversed successfully from any positions adjacent to (0, 0), namely position (1, 0), position (0, 1), position (–1, 0), or position (0, –1). Picking a potential next step, say (1, 0), we find ourselves in the same type of situation we did before. To successfully traverse the maze from the new current position, we must successfully traverse it from an adjacent position. At any point, some of the adjacent positions may be invalid, may be blocked, or may represent a possible successful path. We continue this process recursively. If the base case, position (7, 12), is reached, the maze has been traversed successfully. 193 194 C HA PT ER 7 L I S T I N G Recursion 7 . 2 /** * Maze represents a maze of characters. The goal is to get from the * top left corner to the bottom right, following a path of 1’s. * * @author Dr. Chase * @author Dr. Lewis * @version 1.0, 8/18/08 */ public class Maze { private final int TRIED = 3; private final int PATH = 7; private int[][] grid = { {1,1,1,0,1,1,0,0,0,1,1,1,1}, {1,0,1,1,1,0,1,1,1,1,0,0,1}, {0,0,0,0,1,0,1,0,1,0,1,0,0}, {1,1,1,0,1,1,1,0,1,0,1,1,1}, {1,0,1,0,0,0,0,1,1,1,0,0,1}, {1,0,1,1,1,1,1,1,0,1,1,1,1}, {1,0,0,0,0,0,0,0,0,0,0,0,0}, {1,1,1,1,1,1,1,1,1,1,1,1,1}}; /** * Attempts to recursively traverse the maze. Inserts special * characters indicating locations that have been tried and that * eventually become part of the solution. * * @param row the integer value of the row * @param column the integer value of the column * @return true if the maze has been solved */ public boolean traverse (int row, int column) { boolean done = false; if (valid (row, column)) { grid[row][column] = TRIED; // this cell has been tried if (row == grid.length-1 && column == grid[0].length-1) done = true; // the maze is solved else { 7.3 L I S T I N G 7 . 2 Using Recursion continued done = traverse (row+1, column); if (!done) done = traverse (row, column+1); if (!done) done = traverse (row-1, column); if (!done) done = traverse (row, column-1); // down // right // up // left } if (done) // this location is part of the final path grid[row][column] = PATH; } return done; } /** * Determines if a specific location is valid. * * @param row the column to be checked * @param column the column to be checked * @return true if the location is valid */ private boolean valid (int row, int column) { boolean result = false; // check if cell is in the bounds of the matrix if (row >= 0 && row < grid.length && column >= 0 && column < grid[row].length) // check if cell is not blocked and not previously tried if (grid[row][column] == 1) result = true; return result; } /** * Returns the maze as a string. * * @return a string representation of the maze */ public String toString () { String result = "\n"; 195 196 C HA PT ER 7 L I S T I N G Recursion 7 . 2 continued for (int row=0; row < grid.length; row++) { for (int column=0; column < grid[row].length; column++) result += grid[row][column] + ""; result += "\n"; } return result; } } The recursive method in the Maze class is called traverse. It returns a boolean value that indicates whether a solution was found. First the method determines if a move to the specified row and column is valid. A move is considered valid if it stays within the grid boundaries and if the grid contains a 1 in that location, indicating that a move in that direction is not blocked. The initial call to traverse passes in the upper-left location (0, 0). If the move is valid, the grid entry is changed from a 1 to a 3, marking this location as visited so that later we don’t retrace our steps. Then the traverse method determines if the maze has been completed by having reached the bottomright location. Therefore, there are actually three possibilities of the base case for this problem that will terminate any particular recursive path: ■ An invalid move because the move is out of bounds or blocked ■ An invalid move because the move has been tried before ■ A move that arrives at the final location If the current location is not the bottom-right corner, we search for a solution in each of the primary directions, if necessary. First, we look down by recursively calling the traverse method and passing in the new location. The logic of the traverse method starts all over again using this new position. A solution either is ultimately found by first attempting to move down from the current location, or it is not found. If it’s not found, we try moving right. If that fails, we try moving up. Finally, if no other direction has yielded a correct path, we try moving left. If no direction from the current location yields a correct solution, then there is no path from this location, and traverse returns false. If the very first invocation of the traverse method returns false, then there is no possible path through this maze. 7.3 Using Recursion If a solution is found from the current location, then the grid entry is changed to a 7. The first 7 is placed in the bottom-right corner. The next 7 is placed in the location that led to the bottom-right corner, and so on until the final 7 is placed in the upper-left corner. Therefore, when the final maze is printed, 0 still indicates a blocked path, 1 indicates an open path that was never tried, 3 indicates a path that was tried but failed to yield a correct solution, and 7 indicates a part of the final solution of the maze. Note that there are several opportunities for recursion in each call to the traverse method. Any or all of them might be followed, depending on the maze configuration. Although there may be many paths through the maze, the recursion terminates when a path is found. Carefully trace the execution of this code while following the maze array to see how the recursion solves the problem. Then consider the difficulty of producing a nonrecursive solution. The Towers of Hanoi The Towers of Hanoi puzzle was invented in the 1880s by Edouard Lucas, a French mathematician. It has become a favorite among computer scientists because its solution is an excellent demonstration of recursive elegance. The puzzle consists of three upright pegs (towers) and a set of disks with holes in the middle so that they slide onto the pegs. Each disk has a different diameter. Initially, all of the disks are stacked on one peg in order of size such that the largest disk is on the bottom, as shown in Figure 7.6. The goal of the puzzle is to move all of the disks from their original (first) peg to the destination (third) peg. We can use the “extra” peg as a temporary place to put disks, but we must obey the following three rules: ■ We can move only one disk at a time. ■ We cannot place a larger disk on top of a smaller disk. ■ All disks must be on some peg except for the disk in transit between pegs. These rules imply that we must move smaller disks “out of the way” in order to move a larger disk from one peg to another. Figure 7.7 shows the step-by-step F I G U R E 7 . 6 The Towers of Hanoi puzzle 197 198 C HA PT ER 7 Recursion Original Configuration Fourth Move First Move Fifth Move Second Move Sixth Move Third Move Seventh and Last Move F I G U R E 7 . 7 A solution to the three-disk Towers of Hanoi puzzle solution for the Towers of Hanoi puzzle using three disks. To move all three disks from the first peg to the third peg, we first have to get to the point where the smaller two disks are out of the way on the second peg so that the largest disk can be moved from the first peg to the third peg. The first three moves shown in Figure 7.7 can be thought of as “moving the smaller disks out of the way.” The fourth move puts the largest disk in its final place. The last three moves put the smaller disks in their final place on top of the largest one. Let’s use this idea to form a general strategy. To move a stack of N disks from the original peg to the destination peg: ■ Move the topmost N–1 disks from the original peg to the extra peg. ■ Move the largest disk from the original peg to the destination peg. ■ Move the N–1 disks from the extra peg to the destination peg. This strategy lends itself nicely to a recursive solution. The step to move the N–1 disks out of the way is the same problem all over again: moving a stack of disks. For this subtask, though, there is one less disk, and our destination peg is what we were originally calling the extra peg. An analogous situation occurs after 7.3 L I S T I N G Using Recursion 7 . 3 /** * SolveTowers demonstrates recursion. * * @author Dr. Lewis * @author Dr. Chase * @version 1.0, 8/18/08 */ public class SolveTowers { /** * Creates a TowersOfHanoi puzzle and solves it. */ public static void main (String[] args) { TowersOfHanoi towers = new TowersOfHanoi (4); towers.solve(); } } we have moved the largest disk, and we have to move the original N–1 disks again. The base case for this problem occurs when we want to move a “stack” that consists of only one disk. That step can be accomplished directly and without recursion. The program in Listing 7.3 creates a TowersOfHanoi object and invokes its solve method. The output is a step-by-step list of instructions that describes how the disks should be moved to solve the puzzle. This example uses four disks, which is specified by a parameter to the TowersOfHanoi constructor. The TowersOfHanoi class, shown in Listing 7.4, uses the solve method to make an initial call to moveTower, the recursive method. The initial call indicates that all of the disks should be moved from peg 1 to peg 3, using peg 2 as the extra position. The moveTower method first considers the base case (a “stack” of one disk). When that occurs, it calls the moveOneDisk method, which prints a single line describing that particular move. If the stack contains more than one disk, we call moveTower again to get the N–1 disks out of the way, then move the largest disk, then move the N–1 disks to their final destination with yet another call to moveTower. 199 200 C HA PT ER 7 L I S T I N G Recursion 7 . 4 /** * TowersOfHanoi represents the classic Towers of Hanoi puzzle. * * @author Dr. Lewis * @author Dr. Chase * @version 1.0, 8/18/08 */ public class TowersOfHanoi { private int totalDisks; /** * Sets up the puzzle with the specified number of disks. * * @param disks the number of disks to start the towers puzzle with */ public TowersOfHanoi (int disks) { totalDisks = disks; } /** * Performs the initial call to moveTower to solve the puzzle. * Moves the disks from tower 1 to tower 3 using tower 2. */ public void solve() { moveTower (totalDisks, 1, 3, 2); } /** * Moves the specified number of disks from one tower to another * by moving a subtower of n-1 disks out of the way, moving one * disk, then moving the subtower back. Base case of 1 disk. * * @param numDisks the number of disks to move * @param start the starting tower * @param end the ending tower * @param temp the temporary tower */ 7.4 L I S T I N G 7 . 4 Analyzing Recursive Algorithms 201 continued private void moveTower (int numDisks, int start, int end, int temp) { if (numDisks == 1) moveOneDisk (start, end); else { moveTower (numDisks-1, start, temp, end); moveOneDisk (start, end); moveTower (numDisks-1, temp, end, start); } } /** * Prints instructions to move one disk from the specified start * tower to the specified end tower. * * @param start the starting tower * @param end the ending tower */ private void moveOneDisk (int start, int end) { System.out.println ("Move one disk from " + start + " to " + end); } } Note that the parameters to moveTower describing the pegs are switched around as needed to move the partial stacks. This code follows our general strategy and uses the moveTower method to move all partial stacks. Trace the code carefully for a stack of three disks to understand the processing. Figure 7.8 shows the UML diagram for this problem. 7.4 Analyzing Recursive Algorithms In Chapter 2, we explored the concept of analyzing an algorithm to determine its complexity (usually its time complexity) and expressed it in terms of a growth function. The growth function gave us the order of the algorithm, which can be used to compare it to other algorithms that accomplish the same task. KEY CON CEPT The order of a recursive algorithm can be determined using techniques similar to those used in analyzing iterative processing. 202 C HA PT ER 7 Recursion TowersOfHanoi SolveTowers uses main(String[] args) totalDisks TowersOfHanoi(int disks) solve() moveTower(int numDisks, int start, int end, int temp) moveOneDisk(int start, int end) F I G U R E 7 . 8 UML description of the SolveTowers and TowersOfHanoi classes When analyzing a loop, we determined the order of the body of the loop and multiplied it by the number of times the loop was executed. Analyzing a recursive algorithm uses similar thinking. Determining the order of a recursive algorithm is a matter of determining the order of the recursion (the number of times the recursive definition is followed) and multiplying that by the order of the body of the recursive method. Consider the recursive method presented in Section 7.2 that computes the sum of the integers from 1 to some positive value. We reprint it here for convenience: // This method returns the sum of 1 to num public int sum (int num) { int result; if (num == 1) result = 1; else result = num + sum (num-1); return result; } The size of this problem is naturally expressed as the number of values to be summed. Because we are summing the integers from 1 to num, the number of values to be summed is num. The operation of interest is the act of adding two values together. The body of the recursive method performs one addition operation, and therefore is O(1). Each time the recursive method is invoked, the value of num is decreased by 1. Therefore, the recursive method is called num times, so the order of the recursion is O(n). Thus, because the body is O(1) and the recursion is O(n), the order of the entire algorithm is O(n). We will see that in some algorithms the recursive step operates on half as much data as the previous call, thus creating an order of recursion of O(log n). If the 7.4 Analyzing Recursive Algorithms 203 body of the method is O(1), then the whole algorithm is O(log n). If the body of the method is O(n), then the whole algorithm is O(n log n). Now consider the Towers of Hanoi puzzle. The size of the puzzle is naturally the number of disks, and the processing operation of interest is the step of moving one disk from one peg to another. Each call to the recursive method moveTower results in one disk being moved. Unfortunately, except for the base case, each recursive call results in calling itself twice more, and each call operates on a stack of disks that is only one less than the stack that is passed in as the parameter. Thus, calling moveTower with 1 disk results in 1 disk being moved, calling moveTower with 2 disks results in 3 disks being moved, calling moveTower with 3 disks results in 7 disks being moved, calling moveTower with 4 disks results in 15 disks being moved, etc. Looking at it another way, if f(n) is the growth function for this problem, then: f(n) = 1 when n is equal to 1 for n 7 1, f(n) = 2*(f(n-1) + 1) = 2n -1 Contrary to its short and elegant implementation, the solution to the Towers of Hanoi puzzle is terribly inefficient. To solve the puzzle with a stack of n disks, we have to make 2n – 1 individual disk moves. Therefore, the Towers of Hanoi algorithm is O(2n). As we discussed in Chapter 2, this order is an example of exponential complexity. As the number of disks increases, the number of required moves increases exponentially. KEY CON CEPT The Towers of Hanoi solution has exponential complexity, which is very inefficient. Yet the implementation of the solution is remarkably short and elegant. Legend has it that priests of Brahma are working on this puzzle in a temple at the center of the world. They are using 64 gold disks, moving them between pegs of pure diamond. The downside is that when the priests finish the puzzle, the world will end. The upside is that even if they move one disk every second of every day, it will take them over 584 billion years to complete it. That’s with a puzzle of only 64 disks! It is certainly an indication of just how intractable exponential algorithm complexity is. 204 C HA PT ER 7 Recursion Summary of Key Concepts ■ Recursion is a programming technique in which a method calls itself. A key to being able to program recursively is to be able to think recursively. ■ Any recursive definition must have a nonrecursive part, called the base case, which permits the recursion to eventually end. ■ Mathematical problems and formulas are often expressed recursively. ■ Each recursive call to a method creates new local variables and parameters. ■ A careful trace of recursive processing can provide insight into the way it is used to solve a problem. ■ Recursion is the most elegant and appropriate way to solve some problems, but for others it is less intuitive than an iterative solution. ■ The order of a recursive algorithm can be determined using techniques similar to those used in analyzing iterative processing. ■ The Towers of Hanoi solution has exponential complexity, which is very inefficient. Yet the implementation of the solution is incredibly short and elegant. Self-Review Questions SR 7.1 What is recursion? SR 7.2 What is infinite recursion? SR 7.3 When is a base case needed for recursive processing? SR 7.4 Is recursion necessary? SR 7.5 When should recursion be avoided? SR 7.6 What is indirect recursion? SR 7.7 Explain the general approach to solving the Towers of Hanoi puzzle. How does it relate to recursion? Exercises EX 7.1 Write a recursive definition of a valid Java identifier. EX 7.2 Write a recursive definition of xy (x raised to the power y), where x and y are integers and y > 0. EX 7.3 Write a recursive definition of i * j (integer multiplication), where i > 0. Define the multiplication process in terms of integer addition. For example, 4 * 7 is equal to 7 added to itself 4 times. Programming Projects EX 7.4 Write a recursive definition of the Fibonacci numbers, a sequence of integers, each of which is the sum of the previous two numbers. The first two numbers in the sequence are 0 and 1. Explain why you would not normally use recursion to solve this problem. EX 7.5 Modify the method that calculates the sum of the integers between 1 and N shown in this chapter. Have the new version match the following recursive definition: The sum of 1 to N is the sum of 1 to (N/2) plus the sum of (N/2 + 1) to N. Trace your solution using an N of 7. EX 7.6 Write a recursive method that returns the value of N! (N factorial) using the definition given in this chapter. Explain why you would not normally use recursion to solve this problem. EX 7.7 Write a recursive method to reverse a string. Explain why you would not normally use recursion to solve this problem. EX 7.8 Design or generate a new maze for the MazeSearch program in this chapter, and rerun the program. Explain the processing in terms of your new maze, giving examples of a path that was tried but failed, a path that was never tried, and the ultimate solution. EX 7.9 Annotate the lines of output of the SolveTowers program in this chapter to show the recursive steps. EX 7.10 Produce a chart showing the number of moves required to solve the Towers of Hanoi puzzle using the following number of disks: 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 20, and 25. EX 7.11 Determine and explain the order of your solution to Exercise 7.4. EX 7.12 Determine and explain the order of your solution to Exercise 7.5. EX 7.13 Determine and explain the order of your solution to Exercise 7.6. EX 7.14 Determine the order of the recursive maze solution presented in this chapter. Programming Projects PP 7.1 Design and implement a program that implements Euclid’s algorithm for finding the greatest common divisor of two positive integers. The greatest common divisor is the largest integer that divides both values without producing a remainder. In a class called DivisorCalc, define a static method called gcd that accepts two integers, num1 and num2. Create a driver to test your implementation. The recursive algorithm is defined as follows: 205 206 C HA PT ER 7 Recursion gcd (num1, num2) is num2 if num2 <= num1 and num2 divides num1 gcd (num1, num2) is gcd (num2, num1) if num1 < num2 gcd (num1, num2) is gcd (num2, num1%num2) otherwise PP 7.2 Modify the Maze class so that it prints out the path of the final solution as it is discovered, without storing it. PP 7.3 Design and implement a program that traverses a 3D maze. PP 7.4 Design and implement a recursive program that solves the Nonattacking Queens problem. That is, write a program to determine how eight queens can be positioned on an eight-by-eight chessboard so that none of them is in the same row, column, or diagonal as any other queen. There are no other chess pieces on the board. PP 7.5 In the language of an alien race, all words take the form of Blurbs. A Blurb is a Whoozit followed by one or more Whatzits. A Whoozit is the character ‘x’ followed by zero or more ‘y’s. A Whatzit is a ‘q’ followed by either a ‘z’ or a ‘d’, followed by a Whoozit. Design and implement a recursive program that generates random Blurbs in this alien language. PP 7.6 Design and implement a recursive program to determine if a string is a valid Blurb as defined in the previous project description. PP 7.7 Design and implement a recursive program to determine and print the Nth line of Pascal’s Triangle, as shown below. Each interior value is the sum of the two values above it. (Hint: Use an array to store the values on each line.) 1 1 1 1 1 1 1 1 1 PP 7.8 8 3 5 7 6 15 1 4 10 20 35 56 1 3 10 21 28 2 4 6 1 5 15 35 70 1 1 6 21 56 1 7 28 1 8 1 Design and implement a graphic version of the Towers of Hanoi puzzle. Allow the user to set the number of disks used in the puzzle. The user should be able to interact with the puzzle in two main ways. The user can move the disks from one peg to another using Answers to Self-Review Questions the mouse, in which case the program should ensure that each move is legal. The user can also watch a solution take place as an animation, with pause/resume buttons. Permit the user to control the speed of the animation. Answers to Self-Review Questions SRA 7.1 Recursion is a programming technique in which a method calls itself, solving a smaller version of the problem each time, until the terminating condition is reached. SRA 7.2 Infinite recursion occurs when there is no base case that serves as a terminating condition, or when the base case is improperly specified. The recursive path is followed forever. In a recursive program, infinite recursion will often result in an error that indicates that available memory has been exhausted. SRA 7.3 A base case is always required to terminate recursion and begin the process of returning through the calling hierarchy. Without the base case, infinite recursion results. SRA 7.4 Recursion is not necessary. Every recursive algorithm can be written in an iterative manner. However, some problem solutions are much more elegant and straightforward when written recursively. SRA 7.5 Avoid recursion when the iterative solution is simpler and more easily understood and programmed. Recursion has the overhead of multiple method calls and is not always intuitive. SRA 7.6 Indirect recursion occurs when a method calls another method, which calls another method, and so on until one of the called methods invokes the original. Indirect recursion is usually more difficult to trace than direct recursion, in which a method calls itself. SRA 7.7 The Towers of Hanoi puzzle of N disks is solved by moving N–1 disks out of the way onto an extra peg, moving the largest disk to its destination, then moving the N–1 disks from the extra peg to the destination. This solution is inherently recursive because, to move the substack of N–1 disks, we can use the same process. 207 This page intentionally left blank 8 Sorting and Searching T wo common tasks in the world of software develop- ment are searching for a particular element within a group CHAPTER OBJECTIVES ■ Examine the linear search and binary search algorithms There are a variety of algorithms that can be used to accom- ■ Examine several sort algorithms plish these tasks, and the differences between them are ■ Discuss the complexity of these algorithms and sorting a group of elements into a particular order. worth exploring carefully. These topics go hand in hand with our study of collections and data structures. 209 210 C HA PT ER 8 Sorting and Searching 8.1 K E Y CO N C E PT Searching is the process of finding a designated target within a group of items or determining that it doesn’t exist. K E Y CO N C E PT Searching Searching is the process of finding a designated target element within a group of items, or determining that the target does not exist within the group. The group of items to be searched is sometimes called the search pool. This section examines two common approaches to searching: a linear search and a binary search. Later in this book, other search techniques are presented that use the characteristics of particular data structures to facilitate the search process. Our goal is to perform the search as efficiently as possible. In terms of algorithm analysis, we want to minimize the number of comparisons we have to make to find the target. In general, the more items there are in the search pool, the more comparisons it will take to find the target. Thus, the size of the problem is defined by the number of items in the search pool. An efficient search minimizes the number of comparisons made. To be able to search for an object, we must be able to compare one object to another. Our implementations of these algorithms search an array of Comparable objects. Therefore, the elements involved must implement the Comparable interface and be comparable to each other. We might attempt to accomplish this restriction in the header for the SortingandSearching class in which all of our sorting and searching methods are located by doing something like: public class SortingandSearching<T extends Comparable<? super T>> The net effect of this generic declaration is that we can instantiate the SortingandSearching class with any class that implements the Comparable interface. Recall that the Comparable interface contains one method, compareTo, which is designed to return an integer that is less than zero, equal to zero, or greater than zero (respectively) if the object is less than, equal to, or greater than the object to which it is being compared. Therefore, any class that implements the Comparable interface defines the relative order of any two objects of that class. Declaring the SortingandSearching in this manner, however, will cause us to have to instantiate the class any time we want to use one of the search or sort methods. This is awkward at best for a class that contains nothing but service methods. A better solution would be to declare all of the methods as static and generic. Let’s first remind ourselves about the concept of static methods, and then we will explore generic static methods. 8.1 Searching Static Methods A static method (also called a class method) can be invoked through the class name (all the methods of the Math class are static methods, for example). You don’t have to instantiate an object of the class to invoke a static method. For example, the sqrt method is called through the Math class as follows: System.out.println ("Square root of 27: " + Math.sqrt(27)); A method is made static by using the static modifier in the method declaration. As we have seen, the main method of a Java program must be declared with the static modifier; this is so that main can be executed by the interpreter without instantiating an object from KEY CON CEPT the class that contains main. A method is made static by using the static modifier in the method Because static methods do not operate in the context of a pardeclaration. ticular object, they cannot reference instance variables, which exist only in an instance of a class. The compiler will issue an error if a static method attempts to use a nonstatic variable. A static method can, however, reference static variables, because static variables exist independent of specific objects. Therefore, the main method can access only static or local variables. The methods in the Math class perform basic computations based on values passed as parameters. There is no object state to maintain in these situations; therefore, there is no good reason to force us to create an object in order to request these services. Generic Methods Similar to what we have done in creating generic classes, we can also create generic methods. To create a generic method, we simply insert a generic declaration in the header of the method immediately preceding the return type. public static <T extends Comparable<? super T>> boolean linearSearch (T[] data, int min, int max, T target) It makes sense that the generic declaration has to come before the return type so that the generic type could be used as part of the return type. Now that we can create a generic static method, we do not need to instantiate the SortingandSearching class each time we need one of the methods. Instead, we can simply invoke the static method using the class name and including our type to replace the generic type. For example, an invocation of the linearSearch method to search an array of Strings might look like this: SortingandSearching.linearSearch(targetarray, min, max, target); 211 212 C HA PT ER 8 Sorting and Searching start F I G U R E 8 . 1 A linear search Note that it is not necessary to specify the type to replace the generic type. The compiler will infer the type from the arguments provided. Thus for this line of code, the compiler will replace the generic type T with whatever the element type is for targetarray and the type of target. Linear Search If the search pool is organized into a list of some kind, one straightforward way to perform the search is to start at the beginning of the list and compare each value in turn to the target element. Eventually, we will either find the target or come to the end of the list and conclude that the target doesn’t exist in the group. This approach is called a linear search because it begins at one end and scans the search pool in a linear manner. This process is depicted in Figure 8.1. The following method implements a linear search. It accepts the array of elements to be searched, the beginning and ending index for the search, and the target value sought. The method returns a boolean value that indicates whether or not the target element was found. /** * Searches the specified array of objects using a linear search * algorithm. * * @param data the array to be sorted * @param min the integer representation of the minimum value * @param max the integer representation of the maximum value * @param target the element being searched for * @return true if the desired element is found */ public static <T extends Comparable<? super T>> boolean linearSearch (T[] data, int min, int max, T target) 8.1 Searching { int index = min; boolean found = false; while (!found && index <= max) { if (data[index].compareTo(target) == 0) found = true; index++; } return found; } The while loop steps through the elements of the array, terminating when either the element is found or the end of the array is reached. The boolean variable found is initialized to false and is changed to true only if the target element is located. Variations on this implementation could return the element found in the array if it is found and return a null reference if it is not found. Alternatively, an exception could be thrown if the target element is not found. The linearSearch method could be incorporated into any class. Our version of this method is defined as part of a class containing methods that provide various searching capabilities. The linear search algorithm is fairly easy to understand, though it is not particularly efficient. Note that a linear search does not require the elements in the search pool to be in any particular order within the array. The only criterion is that we must be able to examine them one at a time in turn. The binary search algorithm, described next, improves on the efficiency of the search process, but it only works if the search pool is ordered. Binary Search If the group of items in the search pool is sorted, then our approach to searching can be much more efficient than that of a linear search. A binary search algorithm eliminates large parts of the search pool with each comparison by capitalizing on the fact that the search pool is in sorted order. KEY CON CEPT A binary search capitalizes on the fact that the search pool is sorted. 213 214 C HA PT ER 8 Sorting and Searching start F I G U R E 8 . 2 A binary search Instead of starting the search at one end or the other, a binary search begins in the middle of the sorted list. If the target element is not found at that middle element, then the search continues. And because the list is sorted, we know that if the target is in the list, it will be on one side of the array or the other, depending on whether the target is less than or greater than the middle element. Thus, because the list is sorted, we eliminate half of the search pool with one carefully chosen comparison. The remaining half of the search pool represents the viable candidates in which the target element may yet be found. The search continues in this same manner, examining the middle element of the viable candidates, eliminating half of them. Each comparison reduces the viable candidates by half until eventually the target element is found or there are no more viable candidates, which means the target element is not in the search pool. The process of a binary search is depicted in Figure 8.2. Let’s look at an example. Consider the following sorted list of integers: 10 12 18 22 31 34 40 46 59 67 69 72 80 84 98 Suppose we were trying to determine if the number 67 is in the list. Initially, the target could be anywhere in the list (all items in the search pool are viable candidates). The binary search approach begins by examining the middle element, in this case 46. That element is not our target, so we must continue searching. But since we know that the list is sorted, we know that if 67 is in the list, it must be in the second half of the data, because all data items to the left of the middle have values of 46 or less. This leaves the following viable candidates to search (shown in bold): 10 12 18 22 31 34 40 46 59 67 69 72 80 84 98 Continuing the same approach, we examine the middle value of the viable candidates (72). Again, this is not our target value, so we must continue the search. This time we can eliminate all values higher than 72, leaving: 10 12 18 22 31 34 40 46 59 67 69 72 80 84 98 8.1 Note that, in only two comparisons, we have reduced the viable candidates from 15 items down to 3 items. Employing the same approach again, we select the middle element, 67, and find the element we are seeking. If it had not been our target, we would have continued with this process until we either found the value or eliminated all possible data. Searching 215 KEY CON CEPT A binary search eliminates half of the viable candidates with each comparison. With each comparison, a binary search eliminates approximately half of the remaining data to be searched (it also eliminates the middle element as well). That is, a binary search eliminates half of the data with the first comparison, another quarter of the data with the second comparison, another eighth of the data with the third comparison, and so on. The following method implements a binary search. Like the linearSearch method, it accepts an array of Comparable objects to be searched as well as the target value. It also takes integer values representing the minimum index and maximum index that define the portion of the array to search (the viable candidates). /** * Searches the specified array of objects using a binary search * algorithm. * * @param data the array to be sorted * @param min the integer representation of the minimum value * @param max the integer representation of the maximum value * @param target the element being searched for * @return true if the desired element is found */ public static <T extends Comparable<? super T>> boolean binarySearch (T[] data, int min, int max, T target) { boolean found = false; int midpoint = (min + max) / 2; // determine the midpoint if (data[midpoint].compareTo(target) == 0) found = true; else if (data[midpoint].compareTo(target) > 0) { if (min <= midpoint - 1) found = binarySearch(data, min, midpoint - 1, target); } 216 C HA PT ER 8 Sorting and Searching else if (midpoint + 1 <= max) found = binarySearch(data, midpoint + 1, max, target); return found; } Note that the binarySearch method is implemented recursively. If the target element is not found, and there is more data to search, the method calls itself, passing parameters that shrink the size of viable candidates within the array. The min and max indexes are used to determine if there is still more data to search. That is, if the reduced search area does not contain at least one element, the method does not call itself, and a value of false is returned. At any point in this process, we may have an even number of values to search, and therefore two “middle” values. As far as the algorithm is concerned, the midpoint used could be either of the two middle values as long as the same choice is made consistently. In this implementation of the binary search, the calculation that determines the midpoint index discards any fractional part, and therefore picks the first of the two middle values. Comparing Search Algorithms For a linear search, the best case occurs when the target element happens to be the first item we examine in the group. The worst case occurs when the target is not in the group, and we have to examine every element before we determine that it isn’t present. The expected case is that we would have to search half of the list before we find the element. That is, if there are n elements in the search pool, on average we would have to examine n/2 elements before finding the one for which we were searching. Therefore, the linear search algorithm has a linear time complexity of O(n). Because the elements are searched one at a time in turn, the complexity is linear— in direct proportion to the number of elements to be searched. A binary search, on the other hand, is generally much faster. Because we can eliminate half of the remaining data with each comparison, we can find the element much more quickly. The best case is that we find the target in one comparison— that is, the target element happens to be at the midpoint of the array. The worst case occurs if the element is not present in the list, in which case we have to make approximately log2n comparisons before we eliminate all of the data. Thus, the expected case for finding an element that is in the search pool is approximately (log2n)/2 comparisons. 8.2 Therefore, a binary search is a logarithmic algorithm and has a time complexity of O(log2n). Compared to a linear search, a binary search is much faster for large values of n. Sorting 217 KEY CON CEPT A binary search has logarithmic complexity, making it very efficient for a large search pool. The question might be asked, if a logarithmic search is more efficient than a linear search, why would we ever use a linear search? First, a linear search is generally simpler than a binary search, and it is therefore easier to program and debug. Second, a linear search does not require the additional overhead of sorting the search list. There is a trade-off between the effort to keep the search pool sorted and the efficiency of the search. For small problems, there is little practical difference between the two types of algorithms. However, as n gets larger, the binary search becomes increasingly attractive. Suppose a given set of data contains one million elements. In a linear search, we would have to examine each of the one million elements to determine that a particular target element is not in the group. In a binary search, we could make that conclusion in roughly 20 comparisons. 8.2 Sorting Sorting is the process of arranging a group of items into a defined order, either ascending or descending, based on some criteria. For example, you may want to alphabetize a list of names or put a list of survey results into descending numeric order. KEY CON CEPT Sorting is the process of arranging a list of items into a defined order based on some criteria. Many sort algorithms have been developed and critiqued over the years. In fact, sorting is considered to be a classic area of study in computer science. Similar to search algorithms, sort algorithms generally are divided into two categories based on efficiency: sequential sorts, which typically use a pair of nested loops and require roughly n2 comparisons to sort n elements, and logarithmic sorts, which typically require roughly nlog2n comparisons to sort n elements. As with the search algorithms, when n is small, there is little practical difference between the two categories of algorithms. In this chapter, we examine three sequential sorts—selection sort, insertion sort, and bubble sort—and two logarithmic sorts—quick sort and merge sort. Other search techniques are examined elsewhere in the book based on particular data structures. Before we dive into particular sort algorithms, let’s look at a general sorting problem to solve. The SortPhoneList program, shown in Listing 8.1, creates an array of Contact objects, sorts those objects, and then prints the sorted list. In this implementation, the Contact objects are sorted using a call to the selectionSort method, which we examine later in this chapter. However, any sorting method described in this chapter could be used instead to achieve the same results. 218 C HA PT ER 8 L I S T I N G Sorting and Searching 8 . 1 /** * SortPhoneList driver for testing an object selection sort. * * @author Dr. Chase * @author Dr. Lewis * @version 1.0, 8/18/08 */ public class SortPhoneList { /** * Creates an array of Contact objects, sorts them, then prints * them. */ public static void main (String[] args) { Contact[] friends = new Contact[7]; friends[0] friends[1] friends[2] friends[3] friends[4] friends[5] friends[6] = = = = = = = new new new new new new new Contact Contact Contact Contact Contact Contact Contact ("John", "Smith", "610-555-7384"); ("Sarah", "Barnes", "215-555-3827"); ("Mark", "Riley", "733-555-2969"); ("Laura", "Getz", "663-555-3984"); ("Larry", "Smith", "464-555-3489"); ("Frank", "Phelps", "322-555-2284"); ("Marsha", "Grant", "243-555-2837"); SortingAndSearching.selectionSort(friends); for (int index = 0; index < friends.length; index++) System.out.println (friends[index]); } } Each Contact object represents a person with a last name, a first name, and a phone number. The Contact class is shown in Listing 8.2. The UML description of these classes is left as an exercise. The Contact class implements the Comparable interface and therefore provides a definition of the compareTo method. In this case, the contacts are 8.2 L I S T I N G 8 . 2 /** * Contact represents a phone contact. * * @author Dr. Chase * @author Dr. Lewis * @version 1.0, 8/18/08 */ public class Contact implements Comparable { private String firstName, lastName, phone; /** * Sets up this contact with the specified information. * * @param first a string representation of a first name * @param last a string representation of a last name * @param telephone a string representation of a phone number */ public Contact (String first, String last, String telephone) { firstName = first; lastName = last; phone = telephone; } /** * Returns a description of this contact as a string. * * @return a string representation of this contact */ public String toString () { return lastName + ", " + firstName + "\t" + phone; } /** * Uses both last and first names to determine lexical ordering. * * @param other the contact to be compared to this contact * @return the integer result of the comparison */ Sorting 219 220 C HA PT ER 8 L I S T I N G Sorting and Searching 8 . 2 continued public int compareTo (Object other) { int result; if (lastName.equals(((Contact)other).lastName)) result = firstName.compareTo(((Contact)other).firstName); else result = lastName.compareTo(((Contact)other).lastName); return result; } } sorted by last name; if two contacts have the same last name, their first names are used. Now let’s examine several sort algorithms and their implementations. Any of these could be used to put the Contact objects into sorted order. Selection Sort K E Y C O N C E PT The selection sort algorithm sorts a list of values by repetitively putting a particular value into its final, sorted, position. In other words, for each position in the list, the algorithm selects the value that should go in that position and puts it there. The selection sort algorithm sorts a list of values by repetitively putting a particular value into its final, sorted, position. The general strategy of the selection sort algorithm is as follows: Scan the entire list to find the smallest value. Exchange that value with the value in the first position of the list. Scan the rest of the list (all but the first value) to find the smallest value, and then exchange it with the value in the second position of the list. Scan the rest of the list (all but the first two values) to find the smallest value, and then exchange it with the value in the third position of the list. Continue this process for each position in the list. When complete, the list is sorted. The selection sort process is illustrated in Figure 8.3. The following method defines an implementation of the selection sort algorithm. It accepts an array of objects as a parameter. When it returns to the calling method, the elements within the array are sorted. 8.2 3 9 6 1 2 1 9 6 3 2 1 2 6 3 9 1 2 3 6 9 1 2 3 6 9 Sorting Scan right starting with 3. 1 is the smallest. Exchange 1 and 3. Scan right starting with 9. 2 is the smallest. Exchange 9 and 2. Scan right starting with 6. 3 is the smallest. Exchange 6 and 3. Scan right starting with 6. 6 is the smallest. Exchange 6 and 6. F I G U R E 8 . 3 Illustration of selection sort processing /** * Sorts the specified array of integers using the selection * sort algorithm. * * @param data the array to be sorted */ public static <T extends Comparable<? super T>> void selectionSort (T[] data) { int min; T temp; for (int index = 0; index < data.length-1; index++) { min = index; for (int scan = index+1; scan < data.length; scan++) if (data[scan].compareTo(data[min])<0) min = scan; // Swap the values 221 222 C HA PT ER 8 Sorting and Searching temp = data[min]; data[min] = data[index]; data[index] = temp; } } The implementation of the selectionSort method uses two loops to sort an array. The outer loop controls the position in the array where the next smallest value will be stored. The inner loop finds the smallest value in the rest of the list by scanning all positions greater than or equal to the index specified by the outer loop. When the smallest value is determined, it is exchanged with the value stored at index. This exchange is accomplished by three assignment statements using an extra variable called temp. This type of exchange is called swapping. Note that because this algorithm finds the smallest value during each iteration, the result is an array sorted in ascending order (i.e., smallest to largest). The algorithm can easily be changed to put values in descending order by finding the largest value each time. Insertion Sort K E Y CO N C E PT The insertion sort algorithm sorts a list of values by repetitively inserting a particular value into a subset of the list that has already been sorted. One at a time, each unsorted element is inserted at the appropriate position in that sorted subset until the entire list is in order. The general strategy of the insertion sort algorithm is as follows: Sort the first two values in the list relative to each other by exchanging them if necessary. Insert the list’s third value into the appropriate position relative to the first two (sorted) values. Then insert the fourth value into its proper position relative to the first three values in the list. Each time an insertion is made, the number of values in the sorted subset increases by one. Continue this process until all values in the list are completely sorted. The insertion process requires that the other values in the array shift to make room for the inserted element. Figure 8.4 illustrates the insertion sort process. The insertion sort algorithm sorts a list of values by repetitively inserting a particular value into a subset of the list that has already been sorted. The following method implements an insertion sort: /** * Sorts the specified array of objects using an insertion * sort algorithm. * 8.2 Sorting * @param data the array to be sorted */ public static <T extends Comparable<? super T>> void insertionSort (T[] data) { for (int index = 1; index < data.length; index++) { T key = data[index]; int position = index; // Shift larger values to the right while (position > 0 && data[position-1].compareTo(key) > 0) { data[position] = data[position-1]; position-; } data[position] = key; } } Similar to the selection sort implementation, the insertionSort method uses two loops to sort an array of objects. In the insertion sort, however, the outer loop controls the index in the array of the next value to be inserted. The inner loop compares the current insert value with values stored at lower indexes (which make up a 3 9 6 1 2 3 9 6 1 2 3 6 9 1 2 1 3 6 9 2 3 is sorted. Shift nothing. Insert 9. 3 and 9 are sorted. Shift 9 to the right. Insert 6. 3, 6, and 9 are sorted. Shift 9, 6, and 3 to the right. Insert 1. 1, 3, 6, and 9 are sorted. Shift 9, 6, and 3 to the right. Insert 2. F I G U R E 8 . 4 Illustration of insertion sort processing 223 224 C HA PT ER 8 Sorting and Searching sorted subset of the entire list). If the current insert value is less than the value at position, then that value is shifted to the right. Shifting continues until the proper position is opened to accept the insert value. Each iteration of the outer loop adds one more value to the sorted subset of the list, until the entire list is sorted. Bubble Sort A bubble sort is another sequential sort algorithm that uses two nested loops. It sorts values by repeatedly comparing neighboring elements in the list and swapping their position if they are not in order relative to each other. K E Y C O N C E PT The bubble sort algorithm sorts a list by repeatedly comparing neighboring elements and swapping them if necessary. The general strategy of the bubble sort algorithm is as follows: Scan through the list comparing adjacent elements and exchange them if they are not in relative order. This has the effect of “bubbling” the largest value to the last position in the list, which is its appropriate position in the final sorted list. Then scan through the list again, bubbling up the second-to-last value. This process continues until all elements have been bubbled into their correct positions. Each pass through the bubble sort algorithm moves the largest value to its final position. A pass may also reposition other elements as well. For example, if we started with the list: 9 6 8 12 3 1 7 we would first compare 9 and 6 and, finding them not in the correct order, swap them, yielding: 6 9 8 12 3 1 7 Then we would compare 9 to 8 and, again, finding them not in the correct order, swap them, yielding: 6 8 9 12 3 1 7 Then we would compare 9 to 12. Since they are in the correct order, we don’t swap them. Instead, we move to the next pair of values to compare. That is, we then compare 12 to 3. Because they are not in order, we swap them, yielding: 6 8 9 3 12 1 7 We then compare 12 to 1 and swap them, yielding: 6 8 9 3 1 12 7 We then compare 12 to 7 and swap them, yielding: 6 8 9 3 1 7 12 8.2 Sorting This completes one pass through the data to be sorted. After this first pass, the largest value in the list (12) is in its correct position, but we cannot be sure about any of the other numbers. Each subsequent pass through the data guarantees that one more element is put into the correct position. Thus we make n-1 passes through the data, because if n-1 elements are in the correct, sorted positions, the nth item must also be in the correct location. An implementation of the bubble sort algorithm is shown in the following method: /** * Sorts the specified array of objects using a bubble sort * algorithm. * * @param data the array to be sorted */ public static <T extends Comparable<? super T>> void bubbleSort (T[] data) { int position, scan; T temp; for (position = data.length - 1; position >= 0; position--) { for (scan = 0; scan <= position - 1; scan++) { if (data[scan].compareTo(data[scan+1]) > 0) { // Swap the values temp = data[scan]; data[scan] = data[scan + 1]; data[scan + 1] = temp; } } } } The outer for loop in the bubbleSort method represents the n-1 passes through the data. The inner for loop scans through the data, performs the pairwise comparisons of the neighboring data, and swaps them if necessary. Note that the outer loop also has the effect of decreasing the position that represents the maximum index to examine in the inner loop. That is, after the first pass, 225 226 C HA PT ER 8 Sorting and Searching which puts the last value in its correct position, there is no need to consider that value in future passes through the data. After the second pass, we can forget about the last two, and so on. Thus the inner loop examines one less value on each pass. Quick Sort The sort algorithms we have discussed thus far in this chapter (selection sort, insertion sort, and bubble sort) are relatively simple, but they are inefficient sequential sorts that use a pair of nested loops and require roughly n2 comparisons to sort a list of n elements. Now we can turn our attention to more efficient sorts that lend themselves to a recursive implementation. The quick sort algorithm sorts a list by partitioning the list using an arbitrarily chosen partition element and then recursively sorting K E Y C O N C E PT The quick sort algorithm sorts a list the sublists on either side of the partition element. The general stratby partitioning the list and then reegy of the quick sort algorithm is as follows: First, choose one elecursively sorting the two partitions. ment of the list to act as a partition element. Next, partition the list so that all elements less than the partition element are to the left of that element and all elements greater than the partition element are to the right. Finally, apply this quick sort strategy (recursively) to both partitions. If the data is truly random, the choice of the partition element is arbitrary. We will use the element in the middle of the section we want to partition. For efficiency reasons, it would be nice if the partition element divided the list roughly in half, but the algorithm will work no matter what element is chosen as the partition. Let’s look at an example of creating a partition. If we started with the following list: 305 65 7 90 120 110 8 we would choose 90 as our partition element. We would then rearrange the list, swapping the elements that are less than 90 to the left side and those that are greater than 90 to the right side, yielding: 8 65 7 90 120 110 305 We would then apply the quick sort algorithm separately to both partitions. This process continues until a partition contains only one element, which is inherently sorted. Thus, after the algorithm is applied recursively to either side, the entire list is sorted. Once the initial partition element is determined and placed, it is never considered or moved again. The following method implements the quick sort algorithm. It accepts an array of objects to sort and the minimum and maximum index values used for a particular call to the method. For the initial call to the method, the values of min and max would encompass the entire set of elements to be sorted. 8.2 /** * Sorts the specified array of objects using the quick sort * algorithm. * * @param data the array to be sorted * @param min the integer representation of the minimum value * @param max the integer representation of the maximum value */ public static <T extends Comparable<? super T>> void quickSort (T[] data, int min, int max) { int indexofpartition; if (max - min > 0) { // Create partitions indexofpartition = findPartition(data, min, max); // Sort the left side quickSort(data, min, indexofpartition - 1); // Sort the right side quickSort(data, indexofpartition + 1, max); } } The quickSort method relies heavily on the findPartition method, which it calls initially to divide the sort area into two partitions. The findPartition method returns the index of the partition value. Then the quickSort method is called twice (recursively) to sort the two partitions. The base case of the recursion, represented by the if statement in the quickSort method, is a list of one element or less, which is already inherently sorted. The findPartition method is shown below: /** * Used by the quick sort algorithm to find the partition. * * @param data the array to be sorted Sorting 227 228 C HA PT ER 8 Sorting and Searching * @param min * @param max */ the integer representation of the minimum value the integer representation of the maximum value private static <T extends Comparable<? super T>> int findPartition (T[] data, int min, int max) { int left, right; T temp, partitionelement; int middle = (min + max)/2; partitionelement = data[middle]; // use middle element as partition left = min; right = max; while (left<right) { // search for an element that is > the partitionelement while (data[left].compareTo(partitionelement) <=0 && left < right) left++; // search for an element that is < the partitionelement while (data[right].compareTo(partitionelement) > 0) right--; // swap the elements if (left<right) { temp = data[left]; data[left] = data[right]; data[right] = temp; } } // move partition element to partition index temp = data[min]; data[min] = data[right]; data[right] = temp; return right; } 8.2 Sorting 229 The two inner while loops of the findPartition method are used to find elements to swap that are in the wrong partitions. The first loop scans from left to right looking for an element that is greater than the partition element. The second loop scans from right to left looking for an element that is less than the partition element. When these two elements are found, they are swapped. This process continues until the right and left indexes meet in the “middle” of the list. The location where they meet also indicates where the partition element (which isn’t moved from its initial location until the end) will ultimately reside. What happens if we get a poor partition element? If the partition element is near the smallest or the largest element in the list, then we effectively waste a pass through the data. One way to ensure a better partition element is to choose the middle of three elements. For example, the algorithm could check the first, middle, and last elements in the list and choose the middle value as the partition element. This approach is left as a programming project. Merge Sort The merge sort algorithm, another recursive sort algorithm, sorts a list by recursively dividing the list in half until each sublist has one element and then recombining these sublists in order. The general strategy of the merge sort algorithm is as follows: Begin by dividing the list in two roughly equal parts and then recursively calling itself with each of those lists. Continue the recursive decomposition of the list until the base case of the recursion is reached, where the list is divided into lists of length one, which are by definition sorted. Then, as control passes back up the recursive calling structure, the algorithm merges the two sorted sublists resulting from the two recursive calls into one sorted list. KEY CON CEPT The merge sort algorithm sorts a list by recursively dividing the list in half until each sublist has one element and then merging these sublists into the sorted order. For example, if we started with the initial list from our example in the previous section, the recursive decomposition portion of the algorithm would yield the results shown in Figure 8.5. 305 305 65 305 305 7 90 120 7 110 90 65 65 65 7 90 7 90 8 120 110 8 120 120 110 110 F I G U R E 8 . 5 The decomposition of merge sort 8 8 230 C HA PT ER 8 Sorting and Searching The merge portion of the algorithm would then recombine the list as shown in Figure 8.6. 305 65 305 7 7 65 90 7 65 120 90 110 120 305 8 8 7 8 65 305 110 120 8 90 110 110 120 90 F I G U R E 8 . 6 The merge portion of the merge sort algorithm An implementation of the merge sort algorithm is shown below: /** * Sorts the specified array of objects using the merge sort * algorithm. * * @param data the array to be sorted * @param min the integer representation of the minimum value * @param max the integer representation of the maximum value */ public static <T extends Comparable<? super T>> void mergeSort (T[] data, int min, int max) { T[] temp; int index1, left, right; // return on list of length one if (min==max) return; // find the length and the midpoint of the list int size = max - min + 1; int pivot = (min + max) / 2; temp = (T[])(new Comparable[size]); 8.3 Radix Sort mergeSort(data, min, pivot); // sort left half of list mergeSort(data, pivot + 1, max); // sort right half of list // copy sorted data into workspace for (index1 = 0; index1 < size; index1++) temp[index1] = data[min + index1]; // merge the two sorted lists left = 0; right = pivot - min + 1; for (index1 = 0; index1 < size; index1++) { if (right <= max - min) if (left <= pivot - min) if (temp[left].compareTo(temp[right]) > 0) data[index1 + min] = temp[right++]; else data[index1 + min] = temp[left++]; else data[index1 + min] = temp[right++]; else data[index1 + min] = temp[left++]; } } 8.3 Radix Sort To this point, all of the sorting techniques we have discussed have involved comparing elements within the list to each other. As we have seen, the best of these comparison-based sorts is O(nlogn). What if there was a way to sort elements without comparing them directly to each other. It might then be possible to build a more efficient sorting algorithm. We can find such a KEY CON CEPT A radix sort is inherently based on technique by revisiting our discussion of queues from Chapter 5. queue processing. A sort is based on some particular value, called the sort key. For example, a set of people might be sorted by their last name. A radix sort, rather than comparing items by sort key, is based on the structure of the sort key. Separate queues are created for each possible value of each digit or character of the sort key. The number of queues, or the number of possible values, is called the radix. For example, if we were sorting strings made up of lowercase alphabetic 231 232 C HA PT ER 8 Sorting and Searching characters, the radix would be 26. We would use 26 separate queues, one for each possible character. If we were sorting decimal numbers, then the radix would be ten, one for each digit 0 through 9. Let’s look at an example that uses a radix sort to put ten three-digit numbers in order. To keep things manageable, we will restrict the digits of these numbers to 0 through 5, which means we will need only six queues. Each three-digit number to be sorted has a 1s position (right digit), a 10s position (middle digit), and a 100s position (left digit). The radix sort will make three passes through the values, one for each digit position. On the first pass, each number is put on the queue corresponding to its 1s digit. On the second pass, each number is put on the queue corresponding to its 10s digit. And finally, on the third pass, each number is put on the queue corresponding to its 100s digit. Originally, the numbers are loaded into the queues from the original list. On the second pass, the numbers are taken from the queues in a particular order. They are retrieved from the digit 0 queue first, and then the digit 1 queue, etc. For each queue, the numbers are processed in the order in which they come off the queue. This processing order is crucial to the operation of a radix sort. Likewise, on the third pass, the numbers are again taken from the queues in the same way. When the numbers are pulled off of the queues after the third pass, they will be completely sorted. Figure 8.7 shows the processing of a radix sort for ten three-digit numbers. The number 442 is taken from the original list and put onto the queue corresponding to Digit 1s Position 10s Position 100s Position front 0 420 1 2 front 250 503 341 102 3 312 442 143 503 4 Original List: 325 145 442 503 102 312 325 145 143 442 143 420 341 325 312 442 420 250 312 145 250 341 102 250 341 145 5 front 503 325 102 F I G U R E 8 . 7 A radix sort of ten three-digit numbers 420 143 8.3 Radix Sort digit 2. Then 503 is put onto the queue corresponding to digit 3. Then 312 is put onto the queue corresponding to digit 2 (following 442). This continues for all values, resulting in the set of queues for the 1s position. Assume, as we begin the second pass, that we have a fresh set of six empty digit queues. In actuality, the queues can be used over again if processed carefully. To begin the second pass, the numbers are taken from the 0 digit queue first. The number 250 is put onto the queue for digit 5, and then 420 is put onto the queue for digit 2. Then we can move to the next queue, taking 341 and putting it onto the queue for digit 4. This continues until all numbers have been taken off of the 1s position queues, resulting in the set of queues for the 10s position. For the third pass, the process is repeated. First, 102 is put onto the queue for digit 1, then 503 is put onto the queue for digit 5, then 312 is put onto the queue for digit 3. This continues until we have the final set of digit queues for the 100s position. These numbers are now in sorted order if taken off of each queue in turn. Let’s now look at a program that implements the radix sort. For this example, we will sort four-digit numbers, and we won’t restrict the digits used in those numbers. Listing 8.3 shows the RadixSort class, which contains a single main L I S T I N G 8 . 3 /** * RadixSort driver demonstrates the use of queues in the execution of a radix * sort. * * @author Dr. Chase * @author Dr. Lewis * @version 1.0, 8/18/08 */ import jss2.ArrayQueue; public class RadixSort { /** * Performs a radix sort on a set of numeric values. **/ public static void main (String[] args) { int[] list = {7843, 4568, 8765, 6543, 7865, 4532, 9987, 3241, 6589, 6622, 1211}; 233 234 C HA PT ER 8 L I S T I N G Sorting and Searching 8 . 3 continued String temp; Integer numObj; int digit, num; ArrayQueue<Integer>[] digitQueues = (ArrayQueue<Integer>[]) (new ArrayQueue[10]); for (int digitVal = 0; digitVal <= 9; digitVal++) digitQueues[digitVal] = new ArrayQueue<Integer>(); // sort the list for (int position=0; position <= 3; position++) { for (int scan=0; scan < list.length; scan++) { temp = String.valueOf (list[scan]); digit = Character.digit (temp.charAt(3-position), 10); digitQueues[digit].enqueue (new Integer(list[scan])); } // gather numbers back into list num = 0; for (int digitVal = 0; digitVal <= 9; digitVal++) { while (!(digitQueues[digitVal].isEmpty())) { numObj = digitQueues[digitVal].dequeue(); list[num] = numObj.intValue(); num++; } } } // output the sorted list for (int scan=0; scan < list.length; scan++) System.out.println (list[scan]); } } 8.3 ArrayQueue DefaultCapacity rear queue enqueue() dequeue() first() isEmpty() size() toString() - expandCapacity() <<interface>> QueueADT enqueue() dequeue() first() isEmpty() size() toString RadixSort main(String[] args) F I G U R E 8 . 8 UML description of the RadixSort program method. Using an array of ten queue objects (one for each digit 0 through 9), this method carries out the processing steps of a radix sort. Figure 8.8 shows the UML description of the RadixSort class. Notice that in the RadixSort program, we cannot create an array of queues to hold integers because of the restriction that prevents the instantiation of arrays of generic types. Instead, we create an array of CircularArrayQueues and then cast that as an array of CircularArrayQueue<Integer>. In the RadixSort program, the numbers are originally stored in an array called list. After each pass, the numbers are pulled off of the queues and stored back into the list array in the proper order. This allows the program to reuse the original array of ten queues for each pass of the sort. The concept of a radix sort can be applied to any type of data as long as the sort key can be dissected into well-defined positions. Note that unlike the sorts we discussed earlier in this chapter, it’s not reasonable to create a generic radix sort for any object, because dissecting the key values is an integral part of the process. Radix Sort 235 236 C HA PT ER 8 Sorting and Searching So what is the time complexity of a radix sort? In this case, there is not any comparison or swapping of elements. Elements are simply removed from a queue and placed in another one on each pass. For any given radix, the number of passes through the data is a constant based on the number of characters in the key; let’s call it c. Then the time complexity of the algorithm is simply c*n. Keep in mind, from our discussion in Chapter 2, that we ignore constants when computing the time complexity of an algorithm. Thus the radix sort algorithm is O(n). So why not use radix sort for all of our sorting? First, each radix sort algorithm has to be designed specifically for the key of a given problem. Second, for keys where the number of digits in the key (c) and the number of elements in the list (n) are very close together, the actual time complexity of the radix sort algorithm mimics n2 instead of n. In addition, we also need to keep in mind that there is another constant that impacts space complexity and that is the radix, or the number of possible values for each position or character in the key. Imagine, for example, trying to implement a radix sort for a key that allows any character from the Unicode character set. Because this set has more than 100,000 characters, we would need that many queues! Summary of Key Concepts Summary of Key Concepts ■ Searching is the process of finding a designated target within a group of items or determining that it doesn’t exist. ■ An efficient search minimizes the number of comparisons made. ■ A method is made static by using the static modifier in the method declaration. ■ A binary search capitalizes on the fact that the search pool is sorted. ■ A binary search eliminates half of the viable candidates with each comparison. ■ A binary search has logarithmic complexity, making it very efficient for a large search pool. ■ Sorting is the process of arranging a list of items into a defined order based on some criteria. ■ The selection sort algorithm sorts a list of values by repetitively putting a particular value into its final, sorted, position. ■ The insertion sort algorithm sorts a list of values by repetitively inserting a particular value into a subset of the list that has already been sorted. ■ The bubble sort algorithm sorts a list by repeatedly comparing neighboring elements and swapping them if necessary. ■ The quick sort algorithm sorts a list by partitioning the list and then recursively sorting the two partitions. ■ The merge sort algorithm sorts a list by recursively dividing the list in half until each sublist has one element and then merging these sublists into the sorted order. ■ A radix sort is inherently based on queue processing. Self-Review Questions SR 8.1 When would a linear search be preferable to a logarithmic search? SR 8.2 Which searching method requires that the list be sorted? SR 8.3 When would a sequential sort be preferable to a recursive sort? SR 8.4 The insertion sort algorithm sorts using what technique? SR 8.5 The bubble sort algorithm sorts using what technique? SR 8.6 The selection sort algorithm sorts using what technique? SR 8.7 The quick sort algorithm sorts using what technique? SR 8.8 The merge sort algorithm sorts using what technique? 237 238 C HA PT ER 8 Sorting and Searching SR 8.9 How many queues would it take to use a radix sort to sort names stored as all lowercase? Exercises EX 8.1 Compare and contrast the linearSearch and binarySearch algorithms by searching for the numbers 45 and 54 in the following list (3, 8, 12, 34, 54, 84, 91, 110). EX 8.2 Using the list from Exercise 8.1, construct a table showing the number of comparisons required to sort that list for each of the sort algorithms (selection sort, insertion sort, bubble sort, quick sort, and merge sort). EX 8.3 Using the same list from Exercise 8.1, what happens to the number of comparisons for each of the sort algorithms if the list is already sorted? EX 8.4 Given the following list: 90 8 7 56 123 235 9 1 653 Show a trace of execution for: a. selection sort b. insertion sort c. bubble sort d. quick sort e. merge sort EX 8.5 Given the resulting sorted list from Exercise 8.4, show a trace of execution for a binary search, searching for the number 235. EX 8.6 Draw the UML description of the SortPhoneList example. EX 8.7 Hand trace a radix sort for the following list of five-digit student ID numbers, assuming that each digit must be between 1 and 5: 13224 32131 54355 12123 22331 21212 33333 54312 Answers to Self-Review Questions EX 8.8 What is the time complexity of a radix sort? Programming Projects PP 8.1 The bubble sort algorithm shown in this chapter is less efficient than it could be. If a pass is made through the list without exchanging any elements, this means that the list is sorted and there is no reason to continue. Modify this algorithm so that it will stop as soon as it recognizes that the list is sorted. Do not use a break statement! PP 8.2 There is a variation of the bubble sort algorithm called a gap sort that, rather than comparing neighboring elements each time through the list, compares elements that are some number (i) positions apart, where i is an integer less than n. For example, the first element would be compared to the (i + 1) element, the second element would be compared to the (i + 2) element, the nth element would be compared to the (n - i) element, etc. A single iteration is completed when all of the elements that can be compared, have been compared. On the next iteration, i is reduced by some number greater than 1 and the process continues until i is less than 1. Implement a gap sort. PP 8.3 Modify the sorts listed in the chapter (selection sort, insertion sort, bubble sort, quick sort, and merge sort) by adding code to each to tally the total number of comparisons and total execution time of each algorithm. Execute the sort algorithms against the same list, recording information for the total number of comparisons and total execution time for each algorithm. Try several different lists, including at least one that is already in sorted order. PP 8.4 Modify the quick sort method to choose the partition element using the middle of three technique described in the chapter. Run this new version against the old version for several sets of data and compare the total execution time. Answers to Self-Review Questions SRA 8.1 A linear search would be preferable for relatively small, unsorted lists, and in languages where recursion is not supported. SRA 8.2 Binary search. SRA 8.3 A sequential sort would be preferable for relatively small data sets and in languages where recursion is not supported. 239 240 C HA PT ER 8 Sorting and Searching SRA 8.4 The insertion sort algorithm sorts a list of values by repetitively inserting a particular value into a subset of the list that has already been sorted. SRA 8.5 The bubble sort algorithm sorts a list by repeatedly comparing neighboring elements in the list and swapping their position if they are not already in order. SRA 8.6 The selection sort algorithm, which is an O(n2) sort algorithm, sorts a list of values by repetitively putting a particular value into its final, sorted, position. SRA 8.7 The quick sort algorithm sorts a list by partitioning the list using an arbitrarily chosen partition element and then recursively sorting the sublists on either side of the partition element. SRA 8.8 The merge sort algorithm sorts a list by recursively dividing the list in half until each sublist has one element and then recombining these sublists in order. SRA 8.9 It would require 27 queues, one for each of the 26 letters in the alphabet and 1 to store the whole list before, during, and after sorting. 9 Trees T his chapter begins our exploration of nonlinear collec- tions and data structures. We discuss the use and implementation of trees, define the terms associated with trees, analyze CHAPTER OBJECTIVES ■ Define trees as data structures ■ Define the terms associated with trees ■ Discuss the possible implementations of trees ■ Analyze tree implementations of collections ■ Discuss methods for traversing trees ■ Examine a binary tree example possible tree implementations, and look at examples of implementing and using trees. 241 242 C HA PT ER 9 Trees 9.1 K E Y CO N C E PT Trees The collections we have examined to this point in the book (stacks, queues, and lists) are all linear data structures, meaning their elements are arranged in order one after another. A tree is a nonlinear structure in which elements are organized into a hierarchy. This section describes trees in general and establishes some important terminology. Conceptually, a tree is composed of a set of nodes in which elements are stored and edges that connect one node to another. Each node is at a particular level in the tree hierarchy. The root of the tree is the only node at the top level of the tree. There is only one root node in a tree. Figure 9.1 shows a tree that helps to illustrate these terms. A tree is a nonlinear structure whose elements are organized into a hierarchy. The nodes at lower levels of the tree are the children of nodes at the previous level. In Figure 9.1, the nodes labeled B, C, D, and E are the children of A. Nodes F and G are the children of B. A node can have only one parent, but a node may have multiple children. Nodes that have the same parent are called siblings. Thus, nodes H, I, and J are siblings because they are all children of D. K E Y C O N C E PT Trees are described by a large set of related terms. The root node is the only node in a tree that does not have a parent. A node that does not have any children is called a leaf. A node that is not the root and has at least one child is called an internal node. Note that the tree analogy is upside-down. Our trees “grow” from the root at the top of the tree to the leaves toward the bottom of the tree. The root is the entry point into a tree structure. We can follow a path through the tree from parent to child. For example, the path from node A to N in Figure 9.1 is A, D, I, N. A node is the ancestor of another node if it is above it on the path root internal node A B F C G L M D E H I J N O P F I G U R E 9 . 1 Tree terminology K leaf 9.1 Level 0 A B D F C E G 1 2 3 F I G U R E 9 . 2 Path length and level from the root. Thus the root is the ultimate ancestor of all nodes in a tree. Nodes that can be reached by following a path from a particular node are the descendants of that node. The level of a node is also the length of the path from the root to the node. This path length is determined by counting the number of edges that must be followed to get from the root to the node. The root is considered to be level 0, the children of the root are at level 1, the grandchildren of the root are at level 2, and so on. Path length and level are depicted in Figure 9.2. The height of a tree is the length of the longest path from the root to a leaf. Thus the height or order of the tree in Figure 9.2 is 3, because the path length from the root to leaves F and G is 3. The path length from the root to leaf C is 1. Tree Classifications Trees can be classified in many ways. The most important criterion is the maximum number of children any node in the tree may have. This value is sometimes referred to as the order of the tree. A tree that has no limit to the number of children a node may have is called a general tree. A tree that limits each node to no more than n children is referred to as an n-ary tree. One n-ary tree is of particular importance. A tree in which nodes may have at most two children is called a binary tree. This type of tree is helpful in many situations. Much of our exploration of trees will focus on binary trees. Another way to classify a tree is whether it is balanced or not. There are many definitions of balance depending upon the algorithms being used. We will explore some of these algorithms in the next chapter. Roughly speaking, a tree is considered to be balanced if all of the leaves of the tree are on the same level or at least within one level of each other. Thus, the tree shown on the left in Figure 9.3 is Trees 243 244 C HA PT ER 9 Trees balanced unbalanced F I G U R E 9 . 3 Balanced and unbalanced trees balanced, while the one on the right is not. A balanced n-ary tree with m elements will have a height of lognm. Thus a balanced binary tree with n nodes will have a height of log2n. The concept of a complete tree is related to the balance of a tree. A tree is considered complete if it is balanced and all of the leaves at the bottom level are on the left side of the tree. While this is a seemingly arbitrary concept, this definition has implications for how the tree is stored in certain implementations. Another way to define this concept is that a complete binary tree has 2k nodes at every level k except the last, where the nodes must be leftmost. Another related concept is the notion of a full tree. An n-ary tree is considered full if all the leaves of the tree are at the same level and every node is either a leaf or has exactly n children. The balanced tree from Figure 9.3 would not be considered complete while of the 3-ary (or tertiary) trees shown in Figure 9.4, the trees shown in parts (a) and (c)–are complete. Only the third tree (c) in Figure 9.4 is full. a b F I G U R E 9 . 4 Some complete trees c 9.2 9.2 Strategies for Implementing Trees 245 Strategies for Implementing Trees Let’s examine some general strategies for implementing trees. The most obvious implementation of a tree is a linked structure. Each node could be defined as a TreeNode class, similar to what we did with the LinearNode class for linked lists. Each node would contain a pointer to the element to be stored in that node as well as pointers for each of the possible children of the node. Depending on the implementation, it may also be useful for each node to store a pointer to its parent. This use of pointers is similar to the concept of a doubly linked list where each node points not only to the next node in the list but to the previous one as well. Because a tree is a nonlinear structure, it may not seem reasonable to attempt to implement it using an underlying linear structure such as an array. However, sometimes that approach is useful. The strategies for array implementations of a tree may be less obvious. There are two principle approaches: a computational strategy and a simulated link strategy. Computational Strategy for Array Implementation of Trees For certain types of trees, specifically binary trees, a computational strategy can be used for storing a tree using an array. One possible strategy is as follows: For any element stored in position n of the array, that element’s left child will be stored in position (2 * n + 1) and that element’s right child will be stored in position (2 * (n + 1)). This strategy is very effective and can be managed in terms of capacity in much the same way that we managed capacity for the array implementations of lists, queues, and stacks. However, despite KEY CON CEPT the conceptual elegance of this solution, it is not without drawbacks. One possible computational strategy For example, if the tree that we are storing is not complete or is only places the left child of element n at position (2 * n + 1) and the right relatively complete, we may be wasting large amounts of memory alchild at position (2 * (n + 1)). located in the array for positions of the tree that do not contain data. The computational strategy is illustrated in Figure 9.5. Simulated Link Strategy for Array Implementation of Trees A second possible array implementation of trees is modeled after the way operating systems manage memory. Instead of assigning elements of the tree to array positions by location in the tree, array positions are allocated contiguously on a first-come, first-served basis. Each element of the array will be a node class similar to the TreeNode class that we discussed earlier. However, instead of storing object reference variables as pointers to its children (and perhaps its parent), each node 246 C HA PT ER 9 Trees A B D C E element A B C D E position 0 1 2 3 4 F 5 6 7 F F I G U R E 9 . 5 Computational strategy for array implementation of trees would store the array index of each child (and perhaps its parent). This approach allows elements to be stored contiguously in the array so that space is not wasted. However, this approach increases the overhead for deleting elements in the tree, because it requires either that remaining elements be shifted to maintain contiguity or that a freelist be maintained. This strategy is illustrated in Figure 9.6. The order of the elements in the array is determined simply by their entry order into the tree. In this case, the entry order is assumed to have been A, C, B, E, D, F. This same strategy may also be used when tree structures need to be stored directly on disk using a direct I/O approach. In this case, rather than using an array index as a pointer, each node will store the relative position in the file of its children so that an offset can be calculated given the base address of the file. K E Y C O N C E PT The simulated link strategy allows array positions to be allocated contiguously regardless of the completeness of the tree. A B D C E A 2 C 1 B 4 E 3 D F 5 F F I G U R E 9 . 6 Simulated link strategy for array implementation of trees 9.2 D E S I G N Strategies for Implementing Trees 247 F O C U S When does it begin to make sense to define an ADT for a collection? At this point, we have defined many of the terms for a tree and we have a general understanding of how a tree might be used, but are we ready to define an ADT? Not really. Trees, in the general sense, are more of an abstract data structure than a collection, so attempting to define an ADT for a general tree likely won’t be very useful. Instead, we will wait until we have specified more details about the type of the tree and its use before we attempt to define an interface. Analysis of Trees As we discussed earlier, trees are a useful and efficient way to implement other collections. Let’s consider an ordered list as an example. In our analysis of list implementations in Chapter 6, we described the find operation as expected case n/2 or O(n). However, if we were to implement an orKEY CON CEPT dered list using a balanced binary search tree—a binary tree with the In general, a balanced n-ary tree with added property that the left child is always less than the parent, m elements will have height lognm. which is always less than or equal to the right child—then we could improve the efficiency of the find operation to O(log n). We will discuss binary search trees in much greater detail in Chapter 10. This increased efficiency is due to the fact that the height of such a tree will always be log2n, where n is the number of elements in the tree. This is very similar to our discussion of the binary search in Chapter 8. In fact, for any balanced nary tree with m elements, the tree’s height will be lognm. With the added ordering property of a binary search tree, you are guaranteed to, at worst, search one path from the root to a leaf and that path can be no longer than lognm. D E S I G N F O C U S If trees provide more efficient implementations than linear structures, why would we ever use linear structures? There is an overhead associated with trees in terms of maintaining the structure and order of the tree that may not be present in other structures; thus there is a trade-off between this overhead and the size of the problem. With a relatively small n, the difference between the analysis of tree implementations and that of linear structures is not particularly significant relative to the overhead involved in the tree. However, as n increases, the efficiency of a tree becomes more attractive. 248 C HA PT ER 9 Trees 9.3 Tree Traversals Because a tree is a nonlinear structure, the concept of traversing a tree is generally more interesting than the concept of traversing a linear structure. There are four basic methods for traversing a tree: ■ Preorder traversal, which is accomplished by visiting each node, followed by its children, starting with the root ■ Inorder traversal, which is accomplished by visiting the left child of the node, then the node, then any remaining nodes, starting with the root ■ Postorder traversal, which is accomplished by visiting the children, then the node, starting with the root ■ Level-order traversal, which is accomplished by visiting all of the nodes at each level, one level at a time, starting with the root KE Y CO N C E PT There are four basic methods for traversing a tree: preorder, inorder, postorder, and level-order. Each of these definitions applies to all trees. However, as an example, let us examine how each of these definitions would apply to a binary tree (i.e., a tree in which each node has at most two children). Preorder Traversal Given the tree shown in Figure 9.7, a preorder traversal would produce the sequence A, B, D, E, C. The definition stated previously says that preorder traversal is accomplished by visiting each node, followed by its children, starting with the root. So, starting with the root, we visit the root, giving us A. Next we traverse to the first child of the root, which is the node containing B. We then use the same algorithm by first visiting the current node, yielding B, and then visiting its children. Next we traverse to the first child of B, which is the node containing D. We then use the same algorithm again by first visiting the current node, yielding D, and then visiting its children. Only this time, there are no children. We then traverse to any other children of B. This yields E, and because E has no children, A B D C E F I G U R E 9 . 7 A complete tree 9.3 Tree Traversals 249 we then traverse to any other children of A. This brings us to the node containing C, where we again use the same algorithm, first visiting the node, yielding C, and then visiting any children. Because there are no children of C and no more children of A, the traversal is complete. Stated in pseudocode for a binary tree, the algorithm for a preorder traversal is Visit node Traverse(left child) Traverse(right child) KEY CON CEPT Preorder traversal means visit the node, then the left child, then the right child. Inorder Traversal Given the tree shown in Figure 9.7, an inorder traversal would produce the sequence D, B, E, A, C. As defined earlier, inorder traversal is accomplished by visiting the left child of the node, then the node, then any remaining nodes, starting with the root. So, starting with the root, we traverse to the left child of the root, the node containing B. We then use the same algorithm again and traverse to the left child of B, the node containing D. Note that we have not yet visited any nodes. Using the same algorithm again, we attempt to traverse to the left child of D. Because there is not one, we then visit the current node, yielding D. Continuing the same algorithm, we attempt to traverse to any remaining children of D. Because there are no children, we then visit the previous node, yielding B. We then attempt to traverse to any remaining children of B. This brings us to the node containing E. Because E does not have a left child, we visit the node, yielding E. Because E has no right child, we then visit the previous node, yielding A. We then traverse to any remaining children of A, which takes us to the node containing C. Using the same algorithm, we then attempt to traverse to the left child of C. Because there is not one, we then visit the current node, yielding C. We then attempt to traverse to any remaining children of C. Because there are no children, we return to the previous node, which happens to be the root. Because there are no more children of the root, the traversal is complete. Stated in pseudocode for a binary tree, the algorithm for an inorder traversal is Traverse(left child) Visit node Traverse(right child) KEY CON CEPT Inorder traversal means visit the left child, then the node, then the right child. Postorder Traversal Given the tree shown in Figure 9.7, a postorder traversal would produce the sequence D, E, B, C, A. As previously defined, postorder traversal is accomplished by visiting the children, then the node, starting with the root. So, starting from 250 C HA PT ER 9 Trees the root, we traverse to the left child, the node containing B. Repeating that process, we traverse to the left child again, the node containing D. Because that node does not have any children, we then visit that node, yielding D. Returning to the previous node, we visit the right child, the node containing E. Because this node does not have any children, we visit the node, yielding E, and then return to the previous node and visit it, yielding B. Returning to the previous node, in this case the root, we find that it has a right child, so we traverse to the right child, the node containing C. Because this node does not have any children, we visit it, yielding C. Returning to the previous node (the root), we find KE Y C O N C E PT that it has no remaining children, so we visit it, yielding A, and the Postorder traversal means visit the traversal is complete. left child, then the right child, then the node. Stated in pseudocode for a binary tree, the algorithm for a postorder traversal is Traverse(left child) Traverse(right child) Visit node Level-Order Traversal Given the tree shown in Figure 9.7, a level-order traversal would produce the sequence A, B, C, D, E. As defined earlier, a level-order traversal is accomplished by visiting all of the nodes at each level, one level at a time, starting with the root. Using this definition, we first visit the root, yielding A. Next we visit the left child of the root, yielding B, then the right child of the root, yielding C, and then the children of B, yielding D and E. Stated in pseudocode for a binary tree, an algorithm for a level-order traversal is Create a queue called nodes Create an unordered list called results Enqueue the root onto the nodes queue While the nodes queue is not empty { Dequeue the first element from the queue If that element is not null Add that element to the rear of the results list Enqueue the children of the element on the nodes queue Else Add null on the result list } Return an iterator for the result list This algorithm for a level-order traversal is only one of many possible solutions. However, it does have some interesting properties. First, note that we are 9.4 A Binary Tree ADT 251 using collections, namely a queue and a list, to solve a problem within another collection, namely a binary tree. Second, recall that in our earlier discussions of iterators, we talked about their behavior with respect to the collection if the collection is modified while the iterator is in use. In this case, using a list to store the elements in the proper order and then returning an iterator KEY CON CEPT over the list, this iterator behaves like a snap-shot of the binary tree Level-order traversal means visit the nodes at each level, one level at a and is not affected by any concurrent modifications. This can be time, starting with the root. both a positive and negative attribute depending upon how the iterator is used. 9.4 A Binary Tree ADT Let’s take a look at a simple binary tree implementation using links. In Section 9.5, we will consider an example using this implementation. As we discussed earlier in this chapter, it is difficult to abstract an interface for all trees. However, once we have narrowed our focus to binary trees, the task becomes more reasonable. One possible set of operations for a binary tree ADT is listed in Figure 9.8. Keep in mind that the definition of a collection is not universal. You will find variations in the operations defined for specific collections from one book to another. We have been very careful in this book to define the operations on each collection so that they are consistent with its purpose. Notice that in all of the operations listed, there are no operations to add elements to or remove elements from the tree. This is because until we specify the Operation Description getRoot Returns a reference to the root of the binary tree isEmpty Determines if the tree is empty size Returns the number of elements in the tree contains Determines if the specified target is in the tree find Returns a reference to the specified target element if it is found toString Returns a string representation of the tree iteratorInOrder Returns an iterator for an inorder traversal of the tree iteratorPreOrder Returns an iterator for a preorder traversal of the tree iteratorPostOrder Returns an iterator for a postorder traversal of the tree iteratorLevelOrder Returns an iterator for a level-order traversal of the tree F I G U R E 9 . 8 The operations on a binary tree 252 C HA PT ER 9 Trees purpose and organization of the binary tree, there is no way to know how or more specifically where to add an element to the tree. Similarly, any operation to remove one or more elements from the tree may violate the purpose or structure of the tree as well. As with adding an element, we do not yet have enough information to know how to remove an element. When we were dealing with stacks in Chapters 3 and 4, we could think about the concept of removing an element from a stack, and it was easy to conceptualize the state of the stack after the removal of the element. The same can be said of queues, because we could remove an element from only one end of the linear structures. Even with lists, where we could remove an element from the middle of the linear structure, it was easy to conceptualize the state of the resulting list. With a tree, however, upon removing an element, we have many issues to handle that will affect the state of the tree. What happens to the children and other descendants of the element that is removed? Where does the child pointer of the element’s parent now point? What if the element we are removing is the root? As we will see in our example using expression trees later in this chapter, there will be applications of trees where there is no concept of the removal of an element from the tree. Once we have specified more detail about the use of the tree, we may then decide that a removeElement method is appropriate. An excellent example of this is binary search trees, as we will see in Chapter 10. Listing 9.1 shows the BinaryTreeADT interface. Figure 9.9 shows the UML description for the BinaryTreeADT interface. <<interface>> BinaryTreeADT getRoot() toString() isEmpty() size() contains() find() iteratorInOrder() iteratorPreOrder() iteratorPostOrder() iteratorLevelOrder() F I G U R E 9 . 9 UML description of the BinaryTreeADT interface 9.4 L I S T I N G A Binary Tree ADT 9 . 1 /** * BinaryTreeADT defines the interface to a binary tree data structure. * * @author Dr. Lewis * @author Dr. Chase * @version 1.0, 8/19/08 */ package jss2; import java.util.Iterator; public interface BinaryTreeADT<T> { /** * Returns a reference to the root element * * @return a reference to the root */ public T getRoot (); /** * Returns true if this binary tree is empty and false otherwise. * * @return true if this binary tree is empty */ public boolean isEmpty(); /** * Returns the number of elements in this binary tree. * * @return the integer number of elements in this tree */ public int size(); /** * Returns true if the binary tree contains an element that matches * the specified element and false otherwise. * * @param targetElement the element being sought in the tree * @return true if the tree contains the target element */ public boolean contains (T targetElement); 253 254 C HA PT ER 9 L I S T I N G Trees 9 . 1 continued /** * Returns a reference to the specified element if it is found in * this binary tree. Throws an exception if the specified element * is not found. * * @param targetElement the element being sought in the tree * @return a reference to the specified element */ public T find (T targetElement); /** * Returns the string representation of the binary tree. * * @return a string representation of the binary tree */ public String toString(); /** * Performs an inorder traversal on this binary tree by calling an * overloaded, recursive inorder method that starts with the root. * * @return an iterator over the elements of this binary tree */ public Iterator<T> iteratorInOrder(); /** * Performs a preorder traversal on this binary tree by calling an * overloaded, recursive preorder method that starts with the root. * * @return an iterator over the elements of this binary tree */ public Iterator<T> iteratorPreOrder(); /** * Performs a postorder traversal on this binary tree by calling an * overloaded, recursive postorder method that starts with the root. * * @return an iterator over the elements of this binary tree */ public Iterator<T> iteratorPostOrder(); 9.5 L I S T I N G 9 . 1 Using Binary Trees: Expression Trees continued /** * Performs a level-order traversal on the binary tree, using a queue. * * @return an iterator over the elements of this binary tree */ public Iterator<T> iteratorLevelOrder(); } 9.5 Using Binary Trees: Expression Trees In Chapter 3, we used a stack algorithm to evaluate postfix expressions. In this section, we modify that algorithm to construct an expression tree using an ExpressionTree class that extends our definition of a binary tree. Figure 9.10 illustrates the concept of an expression tree. Notice that the root and all of the internal nodes of an expression tree contain operations and that all of the leaves contain operands. An expression tree is evaluated from the bottom up. In this case, the (5–3) term is evaluated first, yielding 2. That result is then multiplied by 4, yielding 8. Finally, the result of that term is added to 9, yielding 17. Listing 9.2 illustrates our ExpressionTree class. This class extends the LinkedBinaryTree class, providing a new constructor that will combine expression trees to make a new tree and providing an evaluate method to recursively evaluate an expression tree once it has been constructed. Note that this class could have also been written to extend the ArrayBinaryTree class, but many of + * – 5 9 4 3 (5 – 3) * 4 + 9 FIG URE 9 .1 0 An example expression tree 255 256 C HA PT ER 9 Trees the operations would have been significantly different. For example, our constructor that so elegantly combines a new element and two existing trees to form a new tree in constant O(1) time would require the merging of two arrays in the array implementation resulting in at best O(n) time complexity. This does not mean that the linked implementation is always better; it simply means that it fits this particular problem better than the array implementation. L I S T I N G 9 . 2 /** * ExpressionTree represents an expression tree of operators and operands. * * @author Dr. Lewis * @author Dr. Chase * @version 1.0, 8/19/08 */ package jss2; public class ExpressionTree extends LinkedBinaryTree<ExpressionTreeObj> { /** * Creates an empty expression tree. */ public ExpressionTree() { super(); } /** * Constructs a expression tree from the two specified expression * trees. * * @param element the expression tree for the center * @param leftSubtree the expression tree for the left subtree * @param rightSubtree the expression tree for the right subtree */ public ExpressionTree (ExpressionTreeObj element, ExpressionTree leftSubtree, ExpressionTree rightSubtree) { root = new BinaryTreeNode<T> (element); count = 1; 9.5 L I S T I N G 9 . 2 Using Binary Trees: Expression Trees continued if (leftSubtree != null) { count = count + leftSubtree.size(); root.left = leftSubtree.root; } else root.left = null; if (rightSubtree !=null) { count = count + rightSubtree.size(); root.right = rightSubtree.root; } else root.right = null; } /** * Evaluates the expression tree by calling the recursive * evaluateNode method. * * @return the integer evaluation of the tree */ public int evaluateTree() { return evaluateNode(root); } /** * Recursively evaluates each node of the tree. * * @param root the root of the tree to be evaluated * @return the integer evaluation of the tree */ public int evaluateNode(BinaryTreeNode root) { int result, operand1, operand2; ExpressionTreeObj temp; if (root==null) result = 0; else { 257 258 C HA PT ER 9 L I S T I N G Trees 9 . 2 continued temp = (ExpressionTreeObj)root.element; if (temp.isOperator()) { operand1 = evaluateNode(root.left); operand2 = evaluateNode(root.right); result = computeTerm(temp.getOperator(), operand1, operand2); } else result = temp.getValue(); } return result; } /** * Evaluates a term consisting of an operator and two operands. * * @param operator the operator for the expression * @param operand1 the first operand for the expression * @param operand2 the second operand for the expression */ private static int computeTerm(char operator, int operand1, int operand2) { int result=0; if (operator == '+') result = operand1 + operand2; else if (operator == result = operand1 else if (operator == result = operand1 else result = operand1 return result; } } '-') - operand2; '*') * operand2; / operand2; 9.5 Using Binary Trees: Expression Trees The evaluateTree method calls the recursive evaluateNode method. The evaluateNode method returns the value if the node contains a number, or it returns the result of the operation using the value of the left and right subtrees if the node contains an operation. The ExpressionTree class uses the ExpressionTreeObj class as the element to store at each node of the tree. The ExpressionTreeObj class allows us to keep track of whether the element is a number or an operator and which operator or what value is stored there. The ExpressionTreeObj class is illustrated in Listing 9.3. L I S T I N G 9 . 3 /** * ExpressionTreeObj represents an element in an expression tree. * * @author Dr. Lewis * @author Dr. Chase * @version 1.0, 8/19/08 */ package jss2; public class ExpressionTreeObj { private int termType; private char operator; private int value; /** * Creates a new expression tree object with the specified data. * * @param type the integer type of the expression * @param op the operand for the expression * @param val the value for the expression */ public ExpressionTreeObj (int type, char op, int val) { termType = type; operator = op; value = val; } 259 260 C HA PT ER 9 L I S T I N G Trees 9 . 3 continued /** * Returns true if this object is an operator and false otherwise. * * @return true if this object is an operator */ public boolean isOperator() { return (termType == 1); } /** * Returns the operator of this expression tree object. * * @return the character representation of the operator */ public char getOperator() { return operator; } /** * Returns the value of this expression tree object. * * @return the value of this expression tree object */ public int getValue() { return value; } } The Postfix and PostfixEvaluator classes are a modification of our solution from Chapter 3. This solution allows the user to enter a postfix expression from the keyboard. As each term is entered, if it is an operand, a new ExpressionTreeObj is created with the given value and then an ExpressionTree is constructed using that element as the root and with no children. The new ExpressionTree is then pushed onto a stack. If the term entered is an operator, the top two ExpressionTrees on the stack are popped off, a new ExpressionTreeObj is created with the given operator value, and a new ExpressionTree is created with this operator as the root and the two ExpressionTrees popped off of the stack as the left and right subtrees. Figure 9.11 illustrates this process for the expression tree from Figure 9.10. Note that the top of the expression tree stack is on the right. 9.5 Using Binary Trees: Expression Trees Input in Postfix: 5 3 – 4 * 9 + Token Processing Steps 5 push(new ExpressionTree(5, null, null) 3 push(new ExpressionTree(3, null, null) – op2 = pop op1 = pop push(new ExpressionTree(–, op1, op2) 4 Expression Tree Stack (top at right) 5 5 – 5 3 4 – push(new ExpressionTree(4, null, null) 5 * 3 3 * op2 = pop op1 = pop push(new ExpressionTree(*, op1, op2) – 4 5 9 * push(new ExpressionTree(9, null, null) 9 – 4 5 + 3 3 op2 = pop op1 = pop push(new ExpressionTree(+, op1, op2) + * – 5 9 4 3 F I GU R E 9. 11 Building an Expression Tree from a postfix expression 261 262 C HA PT ER 9 L I S T I N G Trees 9 . 4 /** * Postfix2 uses the PostfixEvaluator class to solve a postfix expression * * @author Dr. Lewis * @author Dr. Chase * @version 1.0, 8/19/08 */ public class Postfix2 { /** * Uses the PostfixEvaluator class to solve a postfix expression. */ public static void main (String[] args) { PostfixEvaluator2 temp = new PostfixEvaluator2(); temp.solve(); } } The Postfix class is shown in Listing 9.4, and the PostfixEvaluator class is shown in Listing 9.5. The UML description of the Postfix class is shown in Figure 9.12. 9.6 Implementing Binary Trees with Links We will examine how some of these methods might be implemented using a linked implementation, while others will be left as exercises. The LinkedBinaryTree class implementing the BinaryTreeADT interface will need to keep track of the node that is at the root of the tree and the number of elements on the tree. The LinkedBinaryTree instance data could be declared as protected int count; protected BinaryTreeNode<T> root; 9.6 Implementing Binary Trees with Links BinaryTreeNode element left right LinkedQueue BinaryTreeNode() numChildren() BinaryTree <<interface>> BinaryTreeADT root count getRoot() toString() isEmpty() size() contains() find() iteratorInOrder() iteratorPreOrder() iteratorPostOrder() iteratorLevelOrder() BinaryTree() BinaryTree(Object element) BinaryTree(Object element, BinaryTreeleftsubtree, rightsubtree) removeLeftsubtree() removeRightsubtree() removeAllElements() isEmpty() size() contains() find() iteratorInOrder() iteratorPreOrder() iteratorPostOrder() iteratorLevelOrder() ExpressionTree ExpressionTreeObj 0..* termtype operator value ExpressionTreeObj() isOperator() getOperator() getValue() 1 ExpressionTree() evaluateTree() evaluateNode() computeTerm() 1 PostfixEvaluator LinkedStack Postfix getOperand() getNextToken() main() 1 F I GURE 9 .1 2 UML description of the Postfix example 263 264 C HA PT ER 9 L I S T I N G Trees 9 . 5 /** * PostfixEvaluator2: this modification of our stack example uses a pair of * stacks to create an expression tree from a VALID postfix integer expression * and then uses a recursive method from the ExpressionTree class to * evaluate the tree. * * @author Dr. Lewis * @author Dr. Chase * @version 1.0, 8/19/08 */ import jss2.*; import jss2.exceptions.*; import java.util.StringTokenizer; import java.util.Iterator; import java.io.*; public class PostfixEvaluator2 { /** * Retrieves and returns the next operand off of this tree stack. * * @param treeStack the tree stack from which the operand will be returned * @return the next operand off of this tree stack */ private ExpressionTree getOperand(LinkedStack<ExpressionTree> treeStack) { ExpressionTree temp; temp = treeStack.pop(); return temp; } /** * Retrieves and returns the next token, either an operator or * operand from the user. * * @return the next string token */ private String getNextToken() { String tempToken = "0", inString; StringTokenizer tokenizer; 9.6 L I S T I N G 9 . 5 Implementing Binary Trees with Links 265 continued try { BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); inString = in.readLine(); tokenizer = new StringTokenizer(inString); tempToken = (tokenizer.nextToken()); } catch (Exception IOException) { System.out.println("An input/output exception has occurred"); } return tempToken; } /** * Prompts the user for a valid post-fix expression, converts it to * an expression tree using a two stack method, then calls a * recursive method to evaluate the expression. */ public void solve () { ExpressionTree operand1, operand2; char operator; String tempToken; LinkedStack<ExpressionTree> treeStack = new LinkedStack<ExpressionTree>(); System.out.println("Enter a valid post-fix expression one token " + "at a time pressing the enter key after each token"); System.out.println("Enter an integer, an operator (+,-,*,/)" + "then ! to evaluate"); tempToken = getNextToken(); operator = tempToken.charAt(0); while (!(operator == '!')) { if ((operator == '+') || (operator == '-') || (operator == '*') || (operator == '/')) { 266 C HA PT ER 9 L I S T I N G Trees 9 . 5 continued operand1 = getOperand(treeStack); operand2 = getOperand(treeStack); treeStack.push(new ExpressionTree (new ExpressionTreeObj(1,operator,0), operand2, operand1)); } else { treeStack.push(new ExpressionTree (new ExpressionTreeObj (2,' ', Integer.parseInt(tempToken)), null, null)); } tempToken = getNextToken(); operator = tempToken.charAt(0); } System.out.print("The result is "); System.out.println(((ExpressionTree)treeStack.peek()).evaluateTree()); } } The constructors for the LinkedBinaryTree class should handle two cases: We want to create a binary tree with nothing in it, and we want to create a binary tree with a single element but no children. Neither of these possibilities should violate any specific organization of a binary tree. With these goals in mind, the LinkedBinaryTree class might have the following constructors. Note that each of the constructors must account for both the root and count attributes. /** * Creates an empty binary tree. */ public LinkedBinaryTree() { count = 0; root = null; } 9.6 Implementing Binary Trees with Links 267 /** * Creates a binary tree with the specified element as its root. * * @param element the element that will become the root of the new binary * tree */ public LinkedBinaryTree (T element) { count = 1; root = new BinaryTreeNode<T> (element); } Note that both the instance data and the constructors use an additional class called BinaryTreeNode. As discussed earlier, this class keeps track of the element stored at each location as well as pointers to the left and right subtree or children of each node. In this particular implementation, we chose not to include a pointer back to the parent of each node. Listing 9.6 shows the BinaryTreeNode class. The BinaryTreeNode class also includes a recursive method to return the number of children of the given node. There are a variety of other possibilities for implementation of a tree node or binary tree node class. For example, methods could be included to test whether L I S T I N G 9 . 6 /** * BinaryTreeNode represents a node in a binary tree with a left and * right child. * * @author Dr. Lewis * @author Dr. Chase * @version 1.0, 8/19/08 */ package jss2; public class BinaryTreeNode<T> { protected T element; protected BinaryTreeNode<T> left, right; 268 C HA PT ER 9 L I S T I N G Trees 9 . 6 continued /** * Creates a new tree node with the specified data. * * @param obj the element that will become a part of the new tree node */ BinaryTreeNode (T obj) { element = obj; left = null; right = null; } /** * Returns the number of non-null children of this node. * This method may be able to be written more efficiently. * * @return the integer number of non-null children of this node */ public int numChildren() { int children = 0; if (left != null) children = 1 + left.numChildren(); if (right != null) children = children + 1 + right.numChildren(); return children; } } the node is a leaf (i.e., does not have any children), to test whether the node is an internal node (i.e., has at least one child), to test the depth of the node from the root, or to calculate the height of the left and right subtrees. Another alternative would be to use polymorphism such that, rather than testing a node to see if it has data or has children, we would create various implementations, such as an emptyTreeNode, an innerTreeNode, and a leafTreeNode, that would distinguish the various possibilities. 9.6 Implementing Binary Trees with Links The find Method As with our earlier collections, our find method traverses the tree using the equals method of the class stored in the tree to determine equality. This puts the definition of equality under the control of the class being stored in the tree. The find method throws an exception if the target element is not found. Many methods associated with trees may be written either recursively or iteratively. Often, when written recursively, these methods require the use of a private support method because the signature and/or the behavior of the first call and each successive call may not be the same. The find method in our simple implementation is an excellent example of this strategy. We have chosen to use a recursive findAgain method. We know that the first call to find will start at the root of the tree, and if that instance of the find method completes without finding the target, we need to throw an exception. The private findAgain method allows us to distinguish between this first instance of the find method and each successive call. /** * Returns a reference to the specified target element if it is * found in this binary tree. Throws a NoSuchElementException if * the specified target element is not found in the binary tree. * * @param targetElement the element being sought in this tree * @return a reference to the specified target * @throws ElementNotFoundException if an element not found exception occurs */ public T find(T targetElement) throws ElementNotFoundException { BinaryTreeNode<T> current = findAgain(targetElement, root); if( current == null ) throw new ElementNotFoundException("binary tree"); return (current.element); } /** * Returns a reference to the specified target element if it is * found in this binary tree. * 269 270 C HA PT ER 9 Trees * @param targetElement the element being sought in this tree * @param next the element to begin searching from */ private BinaryTreeNode<T> findAgain(T targetElement, BinaryTreeNode<T> next) { if (next == null) return null; if (next.element.equals(targetElement)) return next; BinaryTreeNode<T> temp = findAgain(targetElement, next.left); if (temp == null) temp = findAgain(targetElement, next.right); return temp; } As seen in earlier examples, the contains method can make use of the find method. Our implementation of this is left as a programming project. The iteratorInOrder Method Another interesting operation is the iteratorInOrder method. The task is to create an Iterator object that will allow a user class to step through the elements of the tree in an inorder traversal. The solution to this problem provides another example of using one collection to build another. We simply traverse the tree using a definition of “visit” from earlier pseudocode that adds the contents of the node onto an unordered list. We then return the list iterator as the result of the iterator method for our tree. This approach is possible because of the linear nature of an unordered list and the way that we implemented the iterator method for a list. The iterator method for a list returns a LinkedIterator that starts with the element at the front of the list and steps through the list in a linear fashion. It is important to understand that this behavior is not a requirement for an iterator associated with a list. It is simply an artifact of the way that we chose to implement the iterator method for a list and the LinkedIterator class. Like the find operation, we use a private helper method in our recursion. 9.7 Implementing Binary Trees with Arrays 271 /** * Performs an inorder traversal on this binary tree by calling an * overloaded, recursive inorder method that starts with * the root. * * @return an in order iterator over this binary tree */ public Iterator<T> iteratorInOrder() { ArrayUnorderedList<T> tempList = new ArrayUnorderedList<T>(); inorder (root, tempList); return tempList.iterator(); } /** * Performs a recursive inorder traversal. * * @param node the node to be used as the root for this traversal * @param tempList the temporary list for use in this traversal */ protected void inorder (BinaryTreeNode<T> node, ArrayUnorderedList<T> tempList) { if (node != null) { inorder (node.left, tempList); tempList.addToRear(node.element); inorder (node.right, tempList); } } The other iterator operations are similar and are left as exercises. 9.7 Implementing Binary Trees with Arrays Now let us consider how some of these methods might be implemented using an array implementation. Again, many of the methods will be left as exercises. The ArrayBinaryTree class implementing the BinaryTreeADT interface will use the computational method we described earlier. Thus the left child of a node stored at position n will be stored in position (2 * n + 1), and that element’s right child will be stored in position (2 * (n + 1)). KEY CON CEPT In the computational strategy to implement a tree with an array, the children of node n are stored at 2n + 1 and 2(n + 1), respectively. 272 C HA PT ER 9 Trees The ArrayBinaryTree instance data could be declared as protected int count; protected T[] tree; Like the constructors for the LinkedBinaryTree class, the constructors for the ArrayBinaryTree class should handle two cases: We want to create a binary tree with nothing in it, and we want to create a binary tree with a single element but no children. Neither of these possibilities should violate any specific organization of a binary tree. /** * Creates an empty binary tree. */ public ArrayBinaryTree() { count = 0; tree = (T[]) new Object[capacity]; } /** * Creates a binary tree with the specified element as its root. * * @param element the element which will become the root of the new tree */ public ArrayBinaryTree (T element) { count = 1; tree = (T[]) new Object[capacity]; tree[0] = element; } Note that unlike our linked implementation, there is no need to use the BinaryTreeNode class. We can simply store the elements directly in the array. However, we will need a method to expand the capacity of the array just as we did with our earlier array implementations. /** * Private method to expand capacity if full. */ 9.7 Implementing Binary Trees with Arrays 273 protected void expandCapacity() { T[] temp = (T[]) new Object[tree.length * 2]; for (int ct=0; ct < tree.length; ct++) temp[ct] = tree[ct]; tree = temp; } The find Method As with our earlier collections, our find method traverses the tree using the equals method of the class stored in the tree to determine equality. This puts the definition of equality under the control of the class being stored in the tree. The find method throws an exception if the target element is not found. Because we are using an array implementation of a tree, one seemingly brute force solution to finding an element in the tree would be to simply use a linear search of the array. Keep in mind that we do not know anything about the ordering of the elements in our tree. Therefore, the element we are searching for may be in any location of our array. We could attempt to use a recursive solution like the one we used for the linked implementation. However, given our array implementation, we do not need to incur the overhead of a recursive algorithm. We will, instead, use a simple, O(n), linear search of the array. Keep in mind that as we extend our tree implementations in later chapters to add order, we will be able to improve upon the time complexity of this operation. /** * Returns a reference to the specified target element if it is * found in this binary tree. Throws a NoSuchElementException if * the specified target element is not found in the binary tree. * * @param targetElement the element being sought in the tree * @return true if the element is in the tree * @throws ElementNotFoundException if an element not found exception occurs */ 274 C HA PT ER 9 Trees public T find (T targetElement) throws ElementNotFoundException { T temp=null; boolean found = false; for (int ct=0; ct<count && !found; ct++) if (targetElement.equals(tree[ct])) { found = true; temp = tree[ct]; } if (!found) throw new ElementNotFoundException("binary tree"); return temp; } The contains method, as we did in earlier examples, can make use of the find method and is left as a programming project. The iteratorInOrder Method Another interesting operation is the iteratorInOrder method. As with the linked implementation, the task is to create an iterator object that will allow a user class to step through the elements of the tree in an inorder traversal. The solution to this problem provides another example of using one collection to build another. It is identical to the method we used in the linked implementation. We simply traverse the tree using a definition of “visit” from earlier pseudocode that adds the contents of the node onto an unordered list. We then return the list iterator as the result of the iterator method for our tree. Like the find operation, we use a private helper method in our recursion. /** * Performs an inorder traversal on this binary tree by calling an * overloaded, recursive inorder method that starts with * the root. * * @return an iterator over the binary tree */ 9.7 Implementing Binary Trees with Arrays public Iterator<T> iteratorInOrder() { ArrayUnorderedList<T> templist = new ArrayUnorderedList<T>(); inorder (0, templist); return templist.iterator(); } /** * Performs a recursive inorder traversal. * * @param node the node used in the traversal * @param templist the temporary list used in the traversal */ protected void inorder (int node, ArrayUnorderedList<T> templist) { if (node < tree.length) if (tree[node] != null) { inorder (node*2+1, templist); templist.addToRear(tree[node]); inorder ((node+1)*2, templist); } } The other iterator operations are similar and are left as exercises. 275 276 C HA PT ER 9 Trees Summary of Key Concepts ■ A tree is a nonlinear structure whose elements are organized into a hierarchy. ■ Trees are described by a large set of related terms. ■ The simulated link strategy allows array positions to be allocated contiguously regardless of the completeness of the tree. ■ In general, a balanced n-ary tree with m elements will have height lognm. ■ There are four basic methods for traversing a tree: preorder, inorder, postorder, and level-order. ■ Preorder traversal means visit the node, then the left child, then the right child. ■ Inorder traversal means visit the left child, then the node, then the right child. ■ Postorder traversal means visit the left child, then the right child, then the node. ■ Level-order traversal means visit the nodes at each level, one level at a time, starting with the root. ■ In the computational strategy to implement a tree with an array, the children of node n are stored at 2n + 1 and 2(n + 1), respectively. Self-Review Questions SR 9.1 What is a tree? SR 9.2 What is a node? SR 9.3 What is the root of the tree? SR 9.4 What is a leaf? SR 9.5 What is an internal node? SR 9.6 Define the height of a tree. SR 9.7 Define the level of a node. SR 9.8 What are the advantages and disadvantages of the computational strategy? SR 9.9 What are the advantages and disadvantages of the simulated link strategy? SR 9.10 What attributes should be stored in the TreeNode class? SR 9.11 Which method of traversing a tree would result in a sorted list for a binary search tree? Programming Projects SR 9.12 We used a list to implement the iterator methods for a binary tree. What must be true for this strategy to be successful? Exercises EX 9.1 Develop a pseudocode algorithm for a level-order traversal of a binary tree. EX 9.2 Draw either a matrilineage (following your mother’s lineage) or a patrilineage (following your father’s lineage) diagram for a couple of generations. Develop a pseudocode algorithm for inserting a person into their proper place in the tree. EX 9.3 Develop a pseudocode algorithm to build an expression tree from a prefix expression. EX 9.4 Develop a pseudocode algorithm to build an expression tree from an infix expression. EX 9.5 Calculate the time complexity of the find method. EX 9.6 Calculate the time complexity of the iteratorInOrder method. EX 9.7 Develop a pseudocode algorithm for the size method assuming that there is not a count variable. EX 9.8 Develop a pseudocode algorithm for the isEmpty operation assuming that there is not a count variable. EX 9.9 Draw an expression tree for the expression (9 + 4) * 5 + (4 - (6 - 3)). Programming Projects PP 9.1 Complete the implementation of the getRoot and toString operations of a binary tree. PP 9.2 Complete the implementation of the size and isEmpty operations of a binary tree, assuming that there is not a count variable. PP 9.3 Create boolean methods for our BinaryTreeNode class to determine if the node is a leaf or an internal node. PP 9.4 Create a method called depth that will return an int representing the level or depth of the given node from the root. PP 9.5 Complete the implementation of the contains method for a binary tree. 277 278 C HA PT ER 9 Trees PP 9.6 Implement the contains method for a binary tree without using the find operation. PP 9.7 Complete the implementation of the iterator methods for a binary tree. PP 9.8 Implement the iterator methods for a binary tree without using a list. PP 9.9 Modify the ExpressionTree class to create a method called draw that will graphically depict the expression tree. PP 9.10 We use postfix notation in the example in this chapter because it eliminates the need to parse an infix expression by precedence rules and parentheses. Some infix expressions do not need parentheses to modify precedence. Implement a method for the ExpressionTree class that will determine if an integer expression would require parentheses if it were written in infix notation. PP 9.11 Create an array-based implementation of a binary tree using the computational strategy. PP 9.12 Create an array-based implementation of a binary tree using the simulated link strategy. Answers to Self-Review Questions SRA 9.1 A tree is a nonlinear structure defined by the concept that each node in the tree, other than the first node or root node, has exactly one parent. SRA 9.2 Node refers to a location in the tree where an element is stored. SRA 9.3 Root refers to the node at the base of the tree or the one node in the tree that does not have a parent. SRA 9.4 A leaf is a node that does not have any children. SRA 9.5 An internal node is any non-root node that has at least one child. SRA 9.6 The height of the tree is the length of the longest path from the root to a leaf. SRA 9.7 The level of a node is measured by the number of links that must be followed to reach that node from the root. SRA 9.8 The computational strategy does not have to store links from parent to child because that relationship is fixed by position. Answers to Self-Review Questions However, this strategy may lead to substantial wasted space for trees that are not balanced and/or not complete. SRA 9.9 The simulated link strategy stores array index values as pointers between parent and child and allows the data to be stored contiguously no matter how balanced and/or complete the tree. However, this strategy increases the overhead in terms of maintaining a freelist or shifting elements in the array. SRA 9.10 The TreeNode class must store a pointer to the element stored in that position as well as pointers to each of the children of that node. The class may also contain a pointer to the parent of the node. SRA 9.11 Inorder traversal of a binary search tree would result in a sorted list in ascending order. SRA 9.12 For this strategy to be successful, the iterator for a list must return the elements in the order in which they were added. For this particular implementation of a list, we know this is indeed the case. 279 This page intentionally left blank 10 Binary Search Trees I n this chapter, we will explore the concept of binary search trees and options for their implementation. We will CHAPTER OBJECTIVES ■ Define a binary search tree abstract data structure ■ Demonstrate how a binary search tree can be used to solve problems ■ Examine various binary search tree implementations ■ Compare binary search tree implementations examine algorithms for adding and removing elements from binary search trees and for maintaining balanced binary search trees. We will discuss the analysis of these implementations and also explore various uses of binary search trees. 281 282 C HA PT ER 10 Binary Search Trees 10.1 A Binary Search Tree A binary search tree is a binary tree with the added property that, for each node, the left child is less than the parent, which is less than or equal to the right child. As we discussed in Chapter 9, it is very difficult to abstract a KE Y CO N C E PT set of operations for a tree without knowing what type of tree it is A binary search tree is a binary tree and its intended purpose. With the added ordering property that with the added property that the left must be maintained, we can now extend our definition to include the child is less than the parent, which is operations on a binary search tree listed in Figure 10.1. less than or equal to the right child. K E Y CO N C E PT The definition of a binary search tree is an extension of the definition of a binary tree. L I S T I N G We must keep in mind that the definition of a binary search tree is an extension of the definition of a binary tree discussed in the last chapter. Thus, these operations are in addition to the ones defined for a binary tree. Keep in mind that at this point we are simply discussing binary search trees, but as we will see shortly, the interface for a balanced binary search tree will be the same. Listing 10.1 and Figure 10.2 describe a BinarySearchTreeADT. 1 0 . 1 /** * BinarySearchTreeADT defines the interface to a binary search tree. * * @author Dr. Lewis * @author Dr. Chase * @version 1.0, 8/19/08 */ package jss2; public interface BinarySearchTreeADT<T> extends BinaryTreeADT<T> { /** * Adds the specified element to the proper location in this tree. * * @param element the element to be added to this tree */ public void addElement (T element); 10.1 L I S T I N G 1 0 . 1 A Binary Search Trees continued /** * Removes and returns the specified element from this tree. * * @param targetElement the element to be removed from this tree * @return the element removed from this tree */ public T removeElement (T targetElement); /** * Removes all occurrences of the specified element from this tree. * * @param targetElement the element that the list will have all instances * of it removed */ public void removeAllOccurrences (T targetElement); /** * Removes and returns the smallest element from this tree. * * @return the smallest element from this tree. */ public T removeMin(); /** * Removes and returns the largest element from this tree. * * @return the largest element from this tree */ public T removeMax(); /** * Returns a reference to the smallest element in this tree. * * @return a reference to the smallest element in this tree */ public T findMin(); 283 284 C HA PT ER 10 L I S T I N G Binary Search Trees 1 0 . 1 continued /** * Returns a reference to the largest element in this tree. * * @return a reference to the largest element in this tree */ public T findMax(); } 10.2 Implementing Binary Search Trees: With Links In Chapter 9, we introduced a simple implementation of a LinkedBinaryTree class using a BinaryTreeNode class to represent each node of the tree. Each BinaryTreeNode object maintains a reference to the element stored at that node as well as references to each of the node’s children. We can simply extend that definition with a LinkedBinarySearchTree class implementing the KE Y CO N C E PT BinarySearchTreeADT interface. Because we are extending the Each BinaryTreeNode object mainLinkedBinaryTree class from Chapter 9, all of the methods we distains a reference to the element cussed are still supported, including the various traversals. stored at that node as well as references to each of the node’s children. Our LinkedBinarySearchTree class offers two constructors: one to create an empty LinkedBinarySearchTree and the other to Operation Description addElement removeElement removeAllOccurrences removeMin removeMax findMin findMax Add an element to the tree. Remove an element from the tree. Remove all occurrences of element from the tree. Remove the minimum element in the tree. Remove the maximum element in the tree. Returns a reference to the minimum element in the tree. Returns a reference to the maximum element in the tree. FIG URE 1 0 .1 The operations on a binary search tree 10.2 Implementing Binary Search Trees: With Links <<interface>> BinaryTreeADT <<interface>> BinarySearchTreeADT addElement() removeElement() removeAllOccurrences() removeMin() removeMax() findMin() findMax() getRoot() toString() isEmpty() size() contains() find() iteratorInOrder() iteratorPreOrder() iteratorPostOrder() iteratorLevelOrder() F I GU R E 1 0 .2 UML description of the BinarySearchTreeADT create a LinkedBinarySearchTree with a particular element at the root. Both of these constructors simply refer to the equivalent constructors of the super class (i.e., the LinkedBinaryTree class). /** * Creates an empty binary search tree. */ public LinkedBinarySearchTree() { super(); } /** * Creates a binary search with the specified element as its root. * * @param element the element that will be the root of the new binary * search tree */ public LinkedBinarySearchTree (T element) { super (element); } 285 286 C HA PT ER 10 Binary Search Trees Add 5 Add 7 5 5 Add 3 Add 4 5 7 3 5 7 3 7 4 FIG URE 1 0 .3 Adding elements to a binary search tree The addElement Operation The addElement method adds a given element to an appropriate location in the tree, given its value. If the element is not Comparable, the method throws a ClassCastException. If the tree is empty, the new element becomes the root. If the tree is not empty, the new element is compared to the element at the root. If it is less than the element stored at the root and the left child of the root is null, then the new element becomes the left child of the root. If the new element is less than the element stored at the root and the left child of the root is not null, then we traverse to the left child of the root and compare again. If the new element is greater than or equal to the element stored at the root and the right child of the root is null, then the new element becomes the right child of the root. If the new element is greater than or equal to the element stored at the root and the right child of the root is not null, then we traverse to the right child of the root and compare again. Figure 10.3 illustrates this process of adding elements to a binary search tree. Note that we have chosen to implement this method iteratively. However, this method could have very naturally and elegantly been implemented recursively. Creating the recursive implementation is left as a programming project. D E S I G N F O C U S Once we have a definition of the type of tree that we wish to construct and how it is to be used, we have the ability to define an interface and implementations. In Chapter 9, we defined a binary tree that enabled us to define a very basic set of operations. Now that we have limited our scope to a binary search tree, we can fill in more details of the interface and the implementation. Determining the level at which to build interface descriptions and determining the boundaries between parent and child classes are design choices . . . and not always easy design choices. 10.2 Implementing Binary Search Trees: With Links /** * Adds the specified object to the binary search tree in the * appropriate position according to its key value. Note that * equal elements are added to the right. * * @param element the element to be added to the binary search tree */ public void addElement (T element) { BinaryTreeNode<T> temp = new BinaryTreeNode<T> (element); Comparable<T> comparableElement = (Comparable<T>)element; if (isEmpty()) root = temp; else { BinaryTreeNode<T> current = root; boolean added = false; while (!added) { if (comparableElement.compareTo(current.element) < 0) { if (current.left == null) { current.left = temp; added = true; } else current = current.left; } else { if (current.right == null) { current.right = temp; added = true; } else current = current.right; } } } count++; } 287 288 C HA PT ER 10 Binary Search Trees The removeElement Operation The removeElement method removes a given Comparable element from a binary search tree or throws an ElementNotFoundException if the given target is not found in the tree. Unlike our earlier study of linear structures, we cannot simply remove the node by making the reference point KE Y CO N C E PT around the node to be removed. Instead, another node will have to In removing an element from a binary search tree, another node must be promoted to replace the one being removed. The protected be promoted to replace the node method replacement returns a reference to a node that will replace being removed. the one specified for removal. There are three cases for selecting the replacement node: ■ If the node has no children, replacement returns null. ■ If the node has only one child, replacement returns that child. ■ If the node to be removed has two children, replacement returns the inorder successor of the node to be removed (because equal elements are placed to the right). /** * Removes the first element that matches the specified target * element from the binary search tree and returns a reference to * it. Throws a ElementNotFoundException if the specified target * element is not found in the binary search tree. * * @param targetElement the element being sought in the binary * search tree * @throws ElementNotFoundException if an element not found exception occurs */ public T removeElement (T targetElement) throws ElementNotFoundException { T result = null; if (!isEmpty()) { if (((Comparable)targetElement).equals(root.element)) { result = root.element; root = replacement (root); count--; } 10.2 Implementing Binary Search Trees: With Links 289 else { BinaryTreeNode<T> current, parent = root; boolean found = false; if (((Comparable)targetElement).compareTo(root.element) < 0) current = root.left; else current = root.right; while (current != null && !found) { if (targetElement.equals(current.element)) { found = true; count--; result = current.element; if (current == parent.left) { parent.left = replacement (current); } else { parent.right = replacement (current); } } else { parent = current; if (((Comparable)targetElement).compareTo(current.element) < 0) current = current.left; else current = current.right; } } // end while if (!found) throw new ElementNotFoundException("binary search tree"); } } // end outer if return result; } 290 C HA PT ER 10 Binary Search Trees Initial tree Remove 3 10 5 3 10 15 7 13 5 13 10 15 7 Remove 10 Remove 5 7 13 15 7 15 13 FIG URE 1 0 .4 Removing elements from a binary tree The following code illustrates the replacement method. Figure 10.4 further illustrates the process of removing elements from a binary search tree. /** * Returns a reference to a node that will replace the one * specified for removal. In the case where the removed node has * two children, the inorder successor is used as its replacement. * * @param node the node to be removed * @return a reference to the replacing node */ protected BinaryTreeNode<T> replacement (BinaryTreeNode<T> node) { BinaryTreeNode<T> result = null; if ((node.left == null)&&(node.right==null)) result = null; else if ((node.left != null)&&(node.right==null)) result = node.left; else if ((node.left == null)&&(node.right != null)) result = node.right; else { BinaryTreeNode<T> current = node.right; BinaryTreeNode<T> parent = node; 10.2 Implementing Binary Search Trees: With Links while (current.left != null) { parent = current; current = current.left; } if (node.right == current) current.left = node.left; else { parent.left = current.right; current.right = node.right; current.left = node.left; } result = current; } return result; } The removeAllOccurrences Operation The removeAllOccurrences method removes all occurrences of a given element from a binary search tree and throws an ElementNotFoundException if the given element is not found in the tree. This method also throws a ClassCastException if the element given is not Comparable. This method makes use of the removeElement method by calling it once, guaranteeing that the exception will be thrown if there is not at least one occurrence of the element in the tree. The removeElement method is then called again as long as the tree contains the target element. /** * Removes elements that match the specified target element from * the binary search tree. Throws a ElementNotFoundException if * the specified target element is not found in this tree. * * @param targetElement the element being sought in the binary * search tree * @throws ElementNotFoundException if an element not found exception occurs */ public void removeAllOccurrences (T targetElement) throws ElementNotFoundException 291 292 C HA PT ER 10 Binary Search Trees { removeElement(targetElement); try { while (contains( (T) targetElement)) removeElement(targetElement); } catch (Exception ElementNotFoundException) { } } The removeMin Operation There are three possible cases for the location of the minimum element in a binary search tree: ■ If the root has no left child, then the root is the minimum element and the right child of the root becomes the new root. ■ If the leftmost node of the tree is a leaf, then it is the minimum element and we simply set its parent’s left child reference to null. ■ If the leftmost node of the tree is an internal node, then we set its parent’s left child reference to point to the right child of the node to be removed. KE Y CO N C E PT The leftmost node in a binary search tree will contain the minimum element whereas the rightmost node will contain the maximum element. Figure 10.5 illustrates these possibilities. Given these possibilities, the code for the removeMin operation is relatively straightforward. /** * Removes the node with the least value from the binary search * tree and returns a reference to its element. Throws an * EmptyBinarySearchTreeException if this tree is empty. * * @return a reference to the node with the least * value * @throws EmptyCollectionException if an empty collection exception occurs */ 10.2 Implementing Binary Search Trees: With Links public T removeMin() throws EmptyCollectionException { T result = null; if (isEmpty()) throw new EmptyCollectionException ("binary search tree"); else { if (root.left == null) { result = root.element; root = root.right; } else { BinaryTreeNode<T> parent = root; BinaryTreeNode<T> current = root.left; while (current.left != null) { parent = current; current = current.left; } result = current.element; parent.left = current.right; } count--; } return result; } The removeMax, findMin, and findMax operations are left as exercises. Initial tree RemoveMin RemoveMin 10 10 10 5 3 15 7 13 5 15 7 13 7 15 13 F I GU R E 10. 5 Removing the minimum element from a binary search tree 293 294 C HA PT ER 10 Binary Search Trees 10.3 Implementing Binary Search Trees: With Arrays In Chapter 9, we introduced a simple implementation of an ArrayBinaryTree class. Each element was stored in the tree using a computational strategy to maintain the relationship between parents and children in the tree (e.g., left child at position 2n + 1 and right child at position 2(n + 1)). We can simply extend that definition with an ArrayBinarySearchTree class implementing the BinarySearchTreeADT interface. Because we are extending the ArrayBinary Tree class from Chapter 9, all of the methods we discussed are still supported, including the various traversals. Our ArrayBinarySearchTree class offers two constructors: one to create an empty ArrayBinarySearchTree and the other to create an ArrayBinary SearchTree with a particular element at the root. Both of these constructors simply refer to the equivalent constructors of the super class (i.e., the ArrayBinary Tree class). Note that we are also keeping track of the height of the tree and the maximum index used in the array. /** * Creates an empty binary search tree. */ public ArrayBinarySearchTree() { super(); height = 0; maxIndex = -1; } /** * Creates a binary search with the specified element as its * root. * * @param element the element that will become the root of the new tree */ public ArrayBinarySearchTree (T element) { super(element); height = 1; maxIndex = 0; } 10 . 3 Implementing Binary Search Trees: With Arrays The addElement Operation The addElement method adds a given element to an appropriate location in the tree, given its value. If the element is not Comparable, the method throws a ClassCastException. If the tree is empty, the new element becomes the root. If the tree is not empty, the new element is compared to the element at the root. If it is less than the element stored at the root and the left child of the root is null, then the new element becomes the left child of the root. If the new element is less than the element stored at the root and the left child of the root is not null, then we traverse to the left child of the root and compare again. If the new element is greater than or equal to the element stored at the root and the right child of the root is null, then the new element becomes the right child of the root. If the new element is greater than or equal to the element stored at the root and the right child of the root is not null, then we traverse to the right child of the root and compare again. Again, we have chosen to implement this method iteratively. Creating the recursive implementation is left as a programming project. /** * Adds the specified object to this binary search tree in the * appropriate position according to its key value. Note that * equal elements are added to the right. Also note that the * index of the left child of the current index can be found by * doubling the current index and adding 1. Finding the index * of the right child can be calculated by doubling the current * index and adding 2. * * @param element the element to be added to the search tree */ public void addElement (T element) { if (tree.length < maxIndex*2+3) expandCapacity(); Comparable<T> tempElement = (Comparable<T>)element; if (isEmpty()) { tree[0] = element; maxIndex = 0; } else { boolean added = false; int currentIndex = 0; 295 296 C HA PT ER 10 Binary Search Trees while (!added) { if (tempElement.compareTo((tree[currentIndex]) ) < 0) { // go left if (tree[currentIndex*2+1] == null) { tree[currentIndex*2+1] = element; added = true; if (currentIndex*2+1 > maxIndex) maxIndex = currentIndex*2+1; } else currentIndex = currentIndex*2+1; } else { // go right if (tree[currentIndex*2+2] == null) { tree[currentIndex*2+2] = element; added = true; if (currentIndex*2+2 > maxIndex) maxIndex = currentIndex*2+2; } else currentIndex = currentIndex*2+2; } } } height = (int)(Math.log(maxIndex + 1) / Math.log(2)) + 1; count++; } The removeElement Operation The removeElement method removes a given Comparable element from a binary search tree or throws an ElementNotFoundException if the given target is not found in the tree. Like we did with the linked implementation, we must reorder the tree to maintain its structure after the removal. As we did with the linked implementation, we will use a protected method to replace the element that is being 10 . 3 Implementing Binary Search Trees: With Arrays 297 removed. We will also use a protected method that will find the array index of the given element if it is in the tree. /** * Removes the first element that matches the specified target * element from this binary search tree and returns a reference to * it. Throws an ElementNotFoundException if the specified target * element is not found in the binary search tree. * * @param targetElement the element to be removed from the tree * @return a reference to the removed element * @throws ElementNotFoundException if an element not found exception occurs */ public T removeElement (T targetElement) throws ElementNotFoundException { T result = null; boolean found = false; if (isEmpty()) throw new ElementNotFoundException("binary search tree"); Comparable<T> tempElement = (Comparable<T>)targetElement; int targetIndex = findIndex (tempElement, 0); result = tree[targetIndex] ; replace(targetIndex); count--; int temp = maxIndex; maxIndex = -1; for (int i = 0; i <= temp; i++) { if (tree[i] != null) maxIndex = i; } height = (int)(Math.log(maxIndex + 1) / Math.log(2)) + 1; return result; } The following code illustrates the method to find the index of a given element if it is in the tree. 298 C HA PT ER 10 Binary Search Trees /** * Returns the index of the specified target element if it is * found in this binary tree. Throws an ElementNotFoundException if * the specified target element is not found in the binary tree. * * @param targetElement the element being sought in the tree * @return true if the element is in the tree * @throws ElementNotFoundException if an element not found exception occurs */ protected int findIndex (Comparable<T> targetElement, int n) throws ElementNotFoundException { int result = 0; if (n > tree.length) throw new ElementNotFoundException("binary if (tree[n] == null) throw new ElementNotFoundException("binary if (targetElement.compareTo(tree[n]) == 0) result = n; else if (targetElement.compareTo(tree[n]) > 0) result = findIndex (targetElement, (2 * else result = findIndex (targetElement, (2 * search tree"); search tree"); (n + 1))); n + 1)); return result; } The method to replace the element being removed is far more interesting in the array implementation than it was in the linked implementation. Keep in mind, in this implementation it is not just a matter of finding the replacement element and moving it, a significant portion of the array, from the point of the deletion forward, will have to be reordered to maintain our computational strategy. We have chosen to use a series of unordered lists to support this operation. /** * Removes the node specified for removal and shifts the tree * array accordingly. * * @param targetIndex the node to be removed */ 10 . 3 Implementing Binary Search Trees: With Arrays 299 protected void replace (int targetIndex) { int currentIndex, parentIndex, temp, oldIndex, newIndex; ArrayUnorderedList<Integer> oldlist = new ArrayUnorderedList<Integer>(); ArrayUnorderedList<Integer> newlist = new ArrayUnorderedList<Integer>(); ArrayUnorderedList<Integer> templist = new ArrayUnorderedList<Integer>(); Iterator<Integer> oldIt, newIt; // if target node has no children if ((targetIndex*2+1 >= tree.length) || (targetIndex*2+2 >= tree.length)) tree[targetIndex] = null; // if target node has no children else if ((tree[targetIndex*2+1] == null) && (tree[targetIndex*2+2] == null)) tree[targetIndex] = null; // if target node only has a left child else if ((tree[targetIndex*2+1] != null) && (tree[targetIndex*2+2] == null)) { // fill newlist with indices of nodes that will replace // the corresponding indices in oldlist currentIndex = targetIndex*2+1; templist.addToRear(new Integer(currentIndex)); while (!templist.isEmpty()) { currentIndex = ((Integer)templist.removeFirst()).intValue(); newlist.addToRear(new Integer(currentIndex)); if ((currentIndex*2+2) <= (Math.pow(2,height)-2)) { templist.addToRear(new Integer(currentIndex*2+1)); templist.addToRear(new Integer(currentIndex*2+2)); } } // fill oldlist currentIndex = targetIndex; templist.addToRear(new Integer(currentIndex)); while (!templist.isEmpty()) { currentIndex = ((Integer)templist.removeFirst()).intValue(); oldlist.addToRear(new Integer(currentIndex)); if ((currentIndex*2+2) <= (Math.pow(2,height)-2)) 300 C HA PT ER 10 Binary Search Trees { templist.addToRear(new Integer(currentIndex*2+1)); templist.addToRear(new Integer(currentIndex*2+2)); } } // do replacement oldIt = oldlist.iterator(); newIt = newlist.iterator(); while (newIt.hasNext()) { oldIndex = oldIt.next(); newIndex = newIt.next(); tree[oldIndex] = tree[newIndex]; tree[newIndex] = null; } } // if target node only has a right child else if ((tree[targetIndex*2+1] == null) && (tree[targetIndex*2+2] != null)) { // fill newlist with indices of nodes that will replace // the corresponding indices in oldlist currentIndex = targetIndex*2+2; templist.addToRear(new Integer(currentIndex)); while (!templist.isEmpty()) { currentIndex = ((Integer)templist.removeFirst()).intValue(); newlist.addToRear(new Integer(currentIndex)); if ((currentIndex*2+2) <= (Math.pow(2,height)-2)) { templist.addToRear(new Integer(currentIndex*2+1)); templist.addToRear(new Integer(currentIndex*2+2)); } } // fill oldlist currentIndex = targetIndex; templist.addToRear(new Integer(currentIndex)); while (!templist.isEmpty()) { currentIndex = ((Integer)templist.removeFirst()).intValue(); oldlist.addToRear(new Integer(currentIndex)); if ((currentIndex*2+2) <= (Math.pow(2,height)-2)) 10 . 3 Implementing Binary Search Trees: With Arrays { templist.addToRear(new Integer(currentIndex*2+1)); templist.addToRear(new Integer(currentIndex*2+2)); } } // do replacement oldIt = oldlist.iterator(); newIt = newlist.iterator(); while (newIt.hasNext()) { oldIndex = oldIt.next(); newIndex = newIt.next(); tree[oldIndex] = tree[newIndex]; tree[newIndex] = null; } } // if target node has two children else { currentIndex = targetIndex*2+2; while (tree[currentIndex*2+1] != null) currentIndex = currentIndex*2+1; tree[targetIndex] = tree[currentIndex]; // the index of the root of the subtree to be replaced int currentRoot = currentIndex; // if currentIndex has a right child if (tree[currentRoot*2+2] != null) { // fill newlist with indices of nodes that will replace // the corresponding indices in oldlist currentIndex = currentRoot*2+2; templist.addToRear(new Integer(currentIndex)); while (!templist.isEmpty()) { currentIndex = ((Integer)templist.removeFirst()).intValue(); newlist.addToRear(new Integer(currentIndex)); if ((currentIndex*2+2) <= (Math.pow(2,height)-2)) 301 302 C HA PT ER 10 Binary Search Trees { templist.addToRear(new Integer(currentIndex*2+1)); templist.addToRear(new Integer(currentIndex*2+2)); } } // fill oldlist currentIndex = currentRoot; templist.addToRear(new Integer(currentIndex)); while (!templist.isEmpty()) { currentIndex = ((Integer)templist.removeFirst()).intValue(); oldlist.addToRear(new Integer(currentIndex)); if ((currentIndex*2+2) <= (Math.pow(2,height)-2)) { templist.addToRear(new Integer(currentIndex*2+1)); templist.addToRear(new Integer(currentIndex*2+2)); } } // do replacement oldIt = oldlist.iterator(); newIt = newlist.iterator(); while (newIt.hasNext()) { oldIndex = oldIt.next(); newIndex = newIt.next(); tree[oldIndex] = tree[newIndex]; tree[newIndex] = null; } } else tree[currentRoot] = null; } } The removeAllOccurrences Operation The removeAllOccurrences method removes all occurrences of a given element from a binary search tree and throws an ElementNotFoundException if the given element is not found in the tree. This method also throws a ClassCastException if the element given is not Comparable. This method makes use of the removeElement method by calling it once, guaranteeing that the 10 . 3 Implementing Binary Search Trees: With Arrays exception will be thrown if there is not at least one occurrence of the element in the tree. The removeElement method is then called again as long as the tree contains the target element. Note that the code for this method is identical to that of the linked implementation. /** * Removes elements that match the specified target element from * the binary search tree. Throws a ElementNotFoundException if * the specified target element is not found in this tree. * * @param targetElement the element being sought in the binary * search tree * @throws ElementNotFoundException if an element not found exception occurs */ public void removeAllOccurrences (T targetElement) throws ElementNotFoundException { removeElement(targetElement); try { while (contains( (T) targetElement)) removeElement(targetElement); } catch (Exception ElementNotFoundException) { } } The removeMin Operation There are three possible cases for the location of the minimum element in a binary search tree: ■ If the root has no left child, then the root is the minimum element and the right child of the root becomes the new root. ■ If the leftmost node of the tree is a leaf, then it is the minimum element, and we simply set its parent’s left child reference to null. ■ If the leftmost node of the tree is an internal node, then we set its parent’s left child reference to point to the right child of the node to be removed. Like the removeElement method, the removeMin method makes use of the protected replace method to replace the element being removed from the tree. 303 304 C HA PT ER 10 Binary Search Trees /** * Removes the node with the least value from this binary search * tree and returns a reference to its element. Throws an * EmptyBinarySearchTreeException if the binary search tree is * empty. * * @return a reference to the node with the least value in this tree * @throws EmptyCollectionException if an empty collection exception occurs */ public T removeMin() throws EmptyCollectionException { T result = null; if (isEmpty()) throw new EmptyCollectionException ("binary search tree"); else { int currentIndex = 1; int previousIndex = 0; while (tree[currentIndex] != null && currentIndex <= tree.length) { previousIndex = currentIndex; currentIndex = currentIndex * 2 + 1; } result = tree[previousIndex] ; replace(previousIndex); } count--; return result; } The removeMax, findMin, and findMax operations are left as exercises. 10.4 Using Binary Search Trees: Implementing Ordered Lists As we discussed in Chapter 9, one of the uses of trees is to provide efficient implementations of other collections. The OrderedList collection from Chapter 6 provides an excellent example. Figure 10.6 reminds us of the common operations for lists, and Figure 10.7 reminds us of the operations particular to an ordered list. 1 0 .4 Using Binary Search Trees: Implementing Ordered Lists Operation Description removeFirst removeLast remove first last contains isEmpty size Removes the first element from the list. Removes the last element from the list. Removes a particular element from the list. Examines the element at the front of the list. Examines the element at the rear of the list. Determines if the list contains a particular element. Determines if the list is empty. Determines the number of elements on the list. 305 FIG URE 1 0 .6 The common operations on a list Using a binary search tree, we can create an implementation called Binary SearchTreeList that is a more efficient implementation than those we discussed in Chapter 6. For simplicity, we have implemented both the ListADT and the OrderedListADT interfaces with the BinarySearchTreeList class, KEY CON CEPT as shown in Listing 10.2. For some of the methods, the same method One of the uses of trees is to provide from either the LinkedBinaryTree or LinkedBinarySearchTree efficient implementations of other collections. class will suffice. This is the case for the contains, isEmpty, and size operations. For the rest of the operations, there is a one-to-one correspondence between methods of the LinkedBinaryTree or LinkedBinarySearchTree classes and the required methods for an ordered list. Thus, each of these methods is implemented by simply calling the associated method for a LinkedBinarySearchTree. This is the case for the add, removeFirst, removeLast, remove, first, last, and iterator methods. L I S T I N G 1 0 . 2 /** * BinarySearchTreeList represents an ordered list implemented using a binary * search tree. * * @author Dr. Lewis * @author Dr. Chase * @version 1.0, 8/19/08 */ package jss2; import jss2.exceptions.*; import java.util.Iterator; 306 C HA PT ER 10 L I S T I N G Binary Search Trees 1 0 . 2 continued public class BinarySearchTreeList<T> extends LinkedBinarySearchTree<T> implements ListADT<T>, OrderedListADT<T>, Iterable<T> { /** * Creates an empty BinarySearchTreeList. */ public BinarySearchTreeList() { super(); } /** * Adds the given element to this list. * * @param element the element to be added to this list */ public void add (T element) { addElement(element); } /** * Removes and returns the first element from this list. * * @return the first element in this list */ public T removeFirst () { return removeMin(); } /** * Removes and returns the last element from this list. * * @return the last element from this list */ public T removeLast () { return removeMax(); } 1 0 .4 L I S T I N G 1 0 . 2 Using Binary Search Trees: Implementing Ordered Lists continued /** * Removes and returns the specified element from this list. * * @param element the element being sought in this list * @return the element from the list that matches the target */ public T remove (T element) { return removeElement(element); } /** * Returns a reference to the first element on this list. * * @return a reference to the first element in this list */ public T first () { return findMin(); } /** * Returns a reference to the last element on this list. * * @return a reference to the last element in this list */ public T last () { return findMax(); } /** * Returns an iterator for the list. * * @return an iterator over the elements in this list */ public Iterator<T> iterator() { return iteratorInOrder(); } } 307 308 C HA PT ER 10 Binary Search Trees Operation Description add Adds an element to the list. FIG URE 1 0 .7 The operation particular to an ordered list Analysis of the BinarySearchTreeList Implementation We will assume that the LinkedBinarySearchTree implementation used in the BinarySearchTreeList implementation is a balanced binary search tree with the added property that the maximum depth of any node is log2(n), where n is the number of elements stored in the tree. This is a tremendously important assumption, as we will see over the next several sections. With that assumption, Figure 10.8 shows a comparison of the order of each operation for a singly linked implementation of an ordered list and our BinarySearchTreeList implementation. Note that given our assumption of a balanced binary search tree, both the add and remove operations could cause the tree to need to be rebalanced, which, depending on the algorithm used, could affect the analysis. It is also important to note that although some operations are more efficient in the tree implementation, such as removeLast, last, and contains, others, such as removeFirst and first, are less efficient when implemented using a tree. Operation removeFirst removeLast remove first last contains isEmpty size add LinkedList O(1) O(n) O(n) O(1) O(n) O(n) O(1) O(1) O(n) BinarySearchTreeList O(log n) O(log n) O(log n)* O(log n) O(log n) O(log n) O(1) O(1) O(log n)* *both the add and remove operations may cause the tree to become unbalanced FIG URE 1 0 .8 Analysis of linked list and binary search tree implementations of an ordered list 10.5 10.5 Balanced Binary Search Trees 309 Balanced Binary Search Trees Why is our balance assumption important? What would happen to our analysis if the tree were not balanced? As an example, let’s assume that we read the following list of integers from a file and added them to a binary search tree: 3 5 9 12 18 20 Figure 10.9 shows the resulting binary search tree. This resulting binary tree, referred to as a degenerate tree, looks more like a linked list, and in fact is less efficient than a linked list because of the additional overhead associated with each node. If this is the tree we are manipulating, then our analysis from the previous section will look far worse. For example, without our balance assumption, the addElement operation would have worst case time complexity of O(n) instead of O(log n) because of the possibility that the root is the smallest element in the tree and the element we are inserting might be the largest element. KEY CON CEPT If a binary search tree is not balanced, it may be less efficient than a linear structure. Our goal instead is to keep the maximum path length in the tree at or near log2n. There are a variety of algorithms available for balancing or maintaining balance in a tree. There are brute-force methods, which are not elegant or efficient, but get the job done. For example, we could write an inorder traversal of the tree to an array and then use a recursive method (much like binary search) to insert the middle element of the array as the root, and then build balanced left and right subtrees. Though such an approach would work, there are more elegant solutions, such as AVL trees and red/black trees, which we examine later in this chapter. However, before we move on to these techniques, we need to understand some additional terminology that is common to many balancing techniques. The methods 3 5 9 12 18 20 FIG URE 1 0 .9 A degenerate binary tree 310 C HA PT ER 10 Binary Search Trees described here will work for any subtree of a binary search tree as well. For those subtrees, we simply replace the reference to root with the reference to the root of the subtree. Right Rotation Figure 10.10 shows a binary search tree that is not balanced and the processing steps necessary to rebalance it. The maximum path length in this tree is 3 and the minimum path length is 1. With only 6 elements in the tree, the maximum path length should be log26 or 2. To get this tree into balance, we need to ■ Make the left child element of the root the new root element. ■ Make the former root element the right child element of the new root. ■ Make the right child of what was the left child of the former root the new left child of the former root. This is referred to as a right rotation and is often referred to as a right rotation of the left child around the parent. The last image in Figure 10.10 shows the same tree after a right rotation. The same kind of rotation can be done at any level of the tree. This single rotation to the right will solve the imbalance if the imbalance is caused by a long path length in the left subtree of the left child of the root. Left Rotation Figure 10.11 shows another binary search tree that is not balanced. Again, the maximum path length in this tree is 3 and the minimum path length is 1. Initial tree Step A 7 5 3 15 10 7 7 13 5 Step C Step B 13 5 15 3 10 7 13 15 3 13 5 3 10 10 FIG URE 1 0 .1 0 Unbalanced tree and balanced tree after a right rotation 15 10.5 Initial tree Step A Step B 10 10 5 3 10 7 13 5 13 15 7 Step C 10 13 5 15 3 Balanced Binary Search Trees 15 3 13 5 3 7 7 FI GU R E 10.1 1 Unbalanced tree and balanced tree after a left rotation However, this time the larger path length is in the right subtree of the right child of the root. To get this tree into balance, we need to ■ Make the right child element of the root the new root element. ■ Make the former root element the left child element of the new root. ■ Make the left child of what was the right child of the former root the new right child of the former root. This is referred to as a left rotation and is often stated as a left rotation of the right child around the parent. Figure 10.11 shows the same tree through the processing steps of a left rotation. The same kind of rotation can be done at any level of the tree. This single rotation to the left will solve the imbalance if the imbalance is caused by a longer path length in the right subtree of the right child of the root. Rightleft Rotation Unfortunately, not all imbalances can be solved by single rotations. If the imbalance is caused by a long path length in the left subtree of the right child of the root, we must first perform a right rotation of the left child of the right child of the root around the right child of the root, and then perform a left rotation of the resulting right child of the root around the root. Figure 10.12 illustrates this process. Leftright Rotation Similarly, if the imbalance is caused by a long path length in the right subtree of the left child of the root, we must first perform a left rotation of the right child of the left child of the root around the left child of the root, and then perform a right 15 311 312 C HA PT ER 10 Binary Search Trees Initial tree Right Rotation 5 3 Left Rotation 5 3 13 10 10 5 10 7 15 13 7 3 13 7 15 15 FIG URE 1 0 . 1 2 A rightleft rotation rotation of the resulting left child of the root around the root. Figure 10.13 illustrates this process. 10.6 Implementing Binary Search Trees: AVL Trees We have been discussing a generic method for balancing a tree where the maximum path length from the root must be no more than log2n and the minimum path length from the root must be no less than log2n–1. Adel’son-Vel’skii and Landis developed a method called AVL trees that is a variation on this theme. For each node in the tree, we will keep track of the height of the left and right subtrees. For any node in the tree, if the balance factor, or the difference in the heights of its subtrees (height of the right subtree minus the height of the left subtree), is greater Initial tree 13 13 7 15 5 3 Right Rotation Left Rotation 7 5 10 7 5 15 10 3 3 FIG URE 1 0 . 1 3 A leftright rotation 13 10 15 10.6 Implementing Binary Search Trees: AVL Trees than 1 or less than –1, then the subtree with that node as the root needs to be rebalanced. There are only two ways that a tree, or any subtree of a tree, can become unbalanced: through the insertion of a node or through the deletion of a node. Thus, each time one of these operations is performed, the balance factors must be updated and the balance of the tree must be checked starting at the point of insertion or removal of a node and working up toward the root of the tree. Because of this need to work back up the tree, AVL trees are often best implemented by including a parent reference in each node. In the diagrams that follow, all edges are represented as a single bidirectional line. The cases for rotation that we discussed in the last section apply here as well, and by using this method, we can easily identify when to use each. KEY CON CEPT The height of the right subtree minus the height of the left subtree is called the balance factor of a node. KEY CON CEPT There are only two ways that a tree, or any subtree of a tree, can become unbalanced: through the insertion of a node or through the deletion of a node. Right Rotation in an AVL Tree If the balance factor of a node is –2, this means that the node’s left subtree has a path that is too long. We then check the balance factor of the left child of the original node. If the balance factor of the left child is –1, this means that the long path is in the left subtree of the left child and therefore a simple right rotation of the left child around the original node will rebalance the tree. Figure 10.14 shows Initial tree After insertion 7 (–1) 5 (0) 3 (0) Right Rotation 7 (–2) 9 (0) 6 (0) 5 (–1) 3 (–1) 5 (0) 9 (0) 6 (0) 3 (–1) 1 (0) 1 (0) New node FIG URE 1 0 .1 4 A right rotation in an AVL tree 7 (0) 6 (0) 313 9 (0) 314 C HA PT ER 10 Binary Search Trees how an insertion of a node could cause an imbalance and how a right rotation would resolve it. Note that we are representing both the values stored at each node and the balance factors, with the balance factors shown in parentheses. Initial tree After removal 10 (1) 5 (–1) 3 (0) 10 (2) 15 (–1) 13 (–1) 5 (0) 17 (0) 13 (–1) 11 (0) Node to be removed 17 (0) 11 (0) Left Rotation Right Rotation 13 (0) 10 (2) 5 (0) 15 (–1) 10 (0) 13 (1) 11 (0) 15 (1) 5 (0) 15 (1) 11 (0) 17 (0) FIG URE 1 0 .1 5 A rightleft rotation in an AVL tree 17 (0) 1 0 .7 Implementing Binary Search Trees: Red/Black Trees Left Rotation in an AVL Tree If the balance factor of a node is +2, this means that the node’s right subtree has a path that is too long. We then check the balance factor of the right child of the original node. If the balance factor of the right child is +1, this means that the long path is in the right subtree of the right child and therefore a simple left rotation of the right child around the original node will rebalance the tree. Rightleft Rotation in an AVL Tree If the balance factor of a node is +2, this means that the node’s right subtree has a path that is too long. We then check the balance factor of the right child of the original node. If the balance factor of the right child is –1, this means that the long path is in the left subtree of the right child and therefore a rightleft double rotation will rebalance the tree. This is accomplished by first performing a right rotation of the left child of the right child of the original node around the right child of the original node, and then performing a left rotation of the right child of the original node around the original node. Figure 10.15 shows how the removal of an element from the tree could cause an imbalance and how a rightleft rotation would resolve it. Again, note that we are representing both the values stored at each node and the balance factors, with the balance factors shown in parentheses. Leftright Rotation in an AVL Tree If the balance factor of a node is –2, this means that the node’s left subtree has a path that is too long. We then check the balance factor of the left child of the original node. If the balance factor of the left child is +1, this means that the long path is in the right subtree of the left child and therefore a leftright double rotation will rebalance the tree. This is accomplished by first performing a left rotation of the right child of the left child of the original node around the left child of the original node, and then performing a right rotation of the left child of the original node around the original node. 10.7 Implementing Binary Search Trees: Red/Black Trees Another alternative to the implementation of binary search trees is the concept of a red/black tree, developed by Bayer and extended by Guibas and Sedgewick. A red/black tree is a balanced binary search tree in which we will store a color with 315 316 C HA PT ER 10 Binary Search Trees 5 13 7 5 15 13 3 10 7 10 5 15 3 13 10 15 7 3 FIG URE 1 0 . 1 6 Valid red/black trees each node (either red or black, usually implemented as a boolean value with false being equivalent to red). The following rules govern the color of a node: ■ The root is black. ■ All children of a red node are black. ■ Every path from the root to a leaf contains the same number of black nodes. Figure 10.16 shows three valid red/black trees (the lighter-shade nodes are “red”). Notice that the balance restriction on a red/black tree is somewhat less strict than that for AVL trees or for our earlier theoretical discussion. However, finding an element in both implementations is still an O(log n) operation. Because no red node can have a red child, then, at most, half KE Y CO N C E PT of the nodes in a path could be red nodes and at least half of the The balance restriction on a red/black tree is somewhat less strict nodes in a path are black. From this we can argue that the maximum than that for AVL trees. height of a red/black tree is roughly 2*log n and thus the traversal of the longest path is still order log n. As with AVL trees, the only time we need to be concerned about balance is after an insertion or removal of an element in the tree. Unlike AVL trees, the two are handled quite separately. Insertion into a Red/Black Tree Insertion into a red/black tree will progress much as it did in our earlier addElement method. However, we will always begin by setting the color of the new element to red. Once the new element has been inserted, we will then rebalance the tree as needed and change the color of elements as needed to maintain 1 0 .7 Implementing Binary Search Trees: Red/Black Trees the properties of a red/black tree. As a last step, we will always set the color of the root of the tree to black. For purposes of our discussion, we will simply refer to the color of a node as node.color. However, it may be more elegant in an actual implementation to create a method to return the color of a node. The rebalancing (and recoloring) process after insertion is an iterative (or recursive) one starting at the point of insertion and working up the tree toward the root. Therefore, like AVL trees, red/black trees are best implemented by including a parent reference in each node. The termination conditions for this process are (current == root), where current is the node we are currently processing, or (current.parent.color == black) (i.e., the color of the parent of the current node is black). The first condition terminates the process because we will always set the root color to black, and the root is included in all paths and therefore cannot violate the rule that each path have the same number of black elements. The second condition terminates the process because the node pointed to by current will always be a red node. This means that if the parent of the current node is black, then all of the rules are met as well since a red node does not affect the number of black nodes in a path and because we are working from the point of insertion up, we will have already balanced the subtree under the current node. In each iteration of the rebalancing process, we will focus on the color of the sibling of the parent of the current node. Keep in mind that there are two possibilities for the parent of the current node: current.parent could be a left child or a right child. Assuming that the parent of current is a right child, we can get the color information by using current.parent.parent.left.color, but for purposes of our discussion, we will use the terms parentsleftsibling.color and parentsrightsibling.color. It is also important to keep in mind that the color of a null element is considered to be black. In the case where the parent of current is a right child, there are two cases, either (parentsleftsibling.color == red) or (parentsleftsibling.color == black). Keep in mind that in either case, we are describing processing steps that are occurring inside of a loop with the termination conditions described earlier. Figure 10.17 shows a red/black tree after insertion with this first case (parentsleftsibling.color==red). The processing steps in this case are ■ Set the color of current’s parent to black. Set the color of parentsleftsibling to black. Set the color of current’s grandparent to red. ■ Set current to point to the grandparent of current. ■ ■ In Figure 10.17, we inserted 8 into our tree. Keep in mind that current points to our new node and current.color is set to red. Following the processing steps, we set the parent of current to black, we set the left sibling of the parent of current to black, and we set the grandparent of current to red. We then set 317 318 C HA PT ER 10 Binary Search Trees Initial tree After insertion 7 7 After rebalancing and recoloring the tree 7 current 4 10 4 10 4 8 10 8 current FIG URE 1 0 . 1 7 Red/black tree after insertion current to point to the grandparent. Because the grandparent is the root, the loop terminates. Finally, we set the root of the tree to black. However, if (parentsleftsibling.color == black), then we first need to check to see if current is a left or right child. If current is a left child, then we must set current equal to its parent and then rotate current.left to the right (around current) before continuing. Once this is accomplished, the processing steps are the same as if current were a right child to begin with: ■ Set the color of current’s parent to black. ■ Set the color of current’s grandparent to red. ■ If current’s grandparent does not equal null, then rotate current’s parent to the left around current’s grandparent. In the case where the parent of current is a left child, there are two cases, either (parentsrightsibling.color == red) or (parentsrightsibling. color == black). Keep in mind that in either case, we are describing processing steps that are occurring inside of a loop with the termination conditions described earlier. Figure 10.18 shows a red/black tree after insertion in this case (parentsrightsibling.color==red). The processing steps in this case are ■ Set the color of current’s parent to black. ■ Set the color of parentsrightsibling to black. ■ Set the color of current’s grandparent to red. ■ Set current to point to the grandparent of current. In Figure 10.18. we inserted 5 into our tree, setting current to point to the new node and setting current.color to red. Again, following our processing steps, we set the parent of current to black, we set the right sibling of the parent 1 0 .7 After rebalancing and recoloring the tree After insertion Initial tree 15 15 7 4 Implementing Binary Search Trees: Red/Black Trees 20 10 7 20 10 4 15 current 7 10 4 5 20 5 current FIG URE 1 0 .1 8 Red/black tree after insertion of current to black, and we set the grandparent of current to red. We then set current to point to its grandparent. Because the parent of the new current is black, our loop terminates. Lastly, we set the color of the root to black. If (parentsrightsibling.color == black), then we first need to check to see if current is a left or right child. If current is a right child, then we must set current equal to current.parent and then rotate current.right to the left (around current) before continuing. Once this is accomplished, the processing steps are the same as if current were a left child to begin with: ■ Set the color of current’s parent to black. ■ Set the color of current’s grandparent to red. ■ If current’s grandparent does not equal null, then rotate current’s parent to the right around current’s grandparent. As you can see, the cases, depending on whether or not current’s parent is a left or right child, are symmetrical. Element Removal from a Red/Black Tree As with insertion, the removeElement operation behaves much as it did before, only with the additional step of rebalancing (and recoloring) the tree. This rebalancing (and recoloring) process after removal of an element is an iterative one starting at the point of removal and working up the tree toward the root. Therefore, as stated earlier, red/black trees are often best implemented by including a parent reference in each node. The termination conditions for this process 319 320 C HA PT ER 10 Binary Search Trees are (current == root), where current is the node we are currently processing, or (current.color == red). As with the cases for insertion, the cases for removal are symmetrical depending upon whether current is a left or right child. We only examine the case where current is a right child. The other cases are easily derived by simply substituting left for right and right for left in the following cases. In insertion, we were most concerned with the color of the sibling of the parent of the current node. For removal, we will focus on the color of the sibling of current. We could reference this color using current.parent.left.color, but we will simply refer to it as sibling.color. We will also look at the color of the children of the sibling. It is important to note that the default for color is black. Therefore, if at any time we are attempting to get the color of a null object, the result will be black. Figure 10.19 shows a red/black tree after the removal of an element. Intermediate step Initial tree sibling 15 7 15 20 4 7 10 4 Element to be removed 12 5 current 12 Final tree 7 7 15 4 sibling 10 5 Intermediate step 5 20 10 12 4 20 12 5 10 current FIG URE 1 0 . 1 9 Red/black tree after removal 15 1 0 .8 Implementing Binary Search Trees: The Java Collections API If the sibling’s color is red, then before we do anything else, we must complete the following processing steps: ■ Set the color of the sibling to black. Set the color of current’s parent to red. Rotate the sibling right around current’s parent. ■ Set the sibling equal to the left child of current’s parent. ■ ■ Next, our processing continues regardless of whether the original sibling was red or black. Now our processing is divided into one of two cases based upon the color of the children of the sibling. If both children of the sibling are black (or null), then we do the following: ■ Set the color of the sibling to red. ■ Set current equal to current’s parent. If the children of the sibling are not both black, then we check to see if the left child of the sibling is black. If it is, we must complete the following steps before continuing: ■ Set the color of the sibling’s right child to black. Set the color of the sibling to red. Rotate the sibling’s right child left around the sibling. ■ Set the sibling equal to the left child of current’s parent. ■ ■ Then to complete the process when both of the sibling’s children are not black, we must: ■ Set the color of the sibling to the color of current’s parent. Set the color of current’s parent to black. Set the color of the sibling’s left child to black. Rotate the sibling right around current’s parent. ■ Set current equal to the root. ■ ■ ■ Once the loop terminates, we must always then remove the node and set its parent’s child reference to null. 10.8 Implementing Binary Search Trees: The Java Collections API The Java Collections API provides two implementations of balanced binary search trees: TreeSet and TreeMap. Both use a red/black tree implementation approach. In order to understand these implementations, we must first discuss the difference between a set and a map in the Java Collections API. 321 322 C HA PT ER 10 Binary Search Trees In the terminology of the Java Collections API, all of the collections that we have discussed thus far could be considThe Java Collections API provides two ered sets (except that sets do not allow duplicates), because implementations of balanced binary the data or element stored in each collection contains all of the search trees, TreeSet and TreeMap, data associated with that object. For example, if we were creboth of which use a red/black tree ating an ordered list of employees, ordered by name, then we implementation. would have created an employee object that contained all of the data for each employee, including the name and a compareTo method to test the name, and we would have used our operations for an ordered list to add those employees into the list. K E Y C ON C E PT D E S I G N F O C U S Java provides thorough and efficient implementations of binary search trees with the TreeSet and TreeMap classes. Why, then, do we spend time learning how to build such collections and learning other methods for balancing trees such as AVL trees? Languages come and go. Simply because Java is a popular language at the moment does not mean that it will continue to be or that developers will not be asked to use other languages, either newer or older languages, many of which do not provide tree implementations. Secondly, the principles involved in this discussion are perhaps more important than the implementations themselves. Understanding that a collection can be built to provide order, balance, and efficiency and the principles involved in how and why those things are done are important lessons. However, in this same scenario, if we wanted to create an ordered list that is a map, we would have created a class to represent the name of each employee and a reference that would point to a second class that contains all of the rest of the employee data. We would have then used our ordered list operations to load the first class into our list, whereas the objects of the second class could exist anywhere in memory. The first class in this case is sometimes referred to as the key, whereas the second class is often referred to as the data. In this way, as we manipulate elements of the list, we are only dealing with the key, the name, and the reference, which is a much smaller segment of memory than if we were manipulating all of the data associated with an employee. We also have the advantage that the same employee data could be referenced by multiple maps without having to make multiple copies. Thus, if for one application we wanted to represent employees in a stack collection and for another application we needed to represent employees as an ordered list, we could load keys into a stack and load matching keys into an ordered list while only having one instance of the actual data. Like any situation dealing with aliases (i.e., multiple references 1 0 .8 Implementing Binary Search Trees: The Java Collections API 323 to the same object), we must be careful that changes to an object through one reference affect the object referenced by all of the other references because there is only one instance of the object. Figures 10.20 and 10.21 show the operations for a TreeSet and TreeMap, respectively. Note that these implementations use (and allow the use of) a Operation Description Constructs a new, empty set, sorted according to the elements’ natural order. Constructs a new set containing the elements in the TreeSet(Collection c) specified collection, sorted according to the elements’ natural order. TreeSet(Comparator c) Constructs a new, empty set, sorted according to the given comparator. Constructs a new set containing the same elements as TreeSet(SortedSet s) the given sorted set, sorted according to the same ordering. Adds the specified element to this set if it is not boolean add(Object o) already present. Adds all of the elements in the specified collection boolean addAll(Collection c) to this set. void clear() Removes all of the elements from this set. Returns a shallow copy of this TreeSet instance. Object clone() Returns the comparator used to order this sorted set, Comparator comparator() or null if this TreeSet uses its elements’ natural ordering. Returns true if this set contains the specified element. boolean contains(Object o) Returns the first (lowest) element currently in Object first() this sorted set. SortedSet headSet(Object toElement) Returns a view of the portion of this set whose elements are strictly less than toElement. boolean isEmpty() Returns true if this set contains no elements. Iterator iterator() Returns an iterator over the elements in this set. Returns the last (highest) element currently in Object last() this sorted set. boolean remove(Object o) Removes the given element from this set if it is present. int size() Returns the number of elements in this set (its cardinality). SortedSet subSet(Object fromElement, Returns a view of the portion of this set whose elements range from fromElement, inclusive, Object toElement) to toElement, exclusive. Returns a view of the portion of this set whose elements SortedSet tailSet(Object are greater than or equal to fromElement. fromElement) TreeSet() FIG URE 1 0 .2 0 Operations on a TreeSet 324 C HA PT ER 10 Operation Binary Search Trees Description Constructs a new, empty map, sorted according to the keys’ natural order. Constructs a new, empty map, sorted according to the TreeMap(Comparator c) given comparator. Constructs a new map containing the same mappings TreeMap(Map m) as the given map, sorted according to the keys’ natural order. Constructs a new map containing the same mappings TreeMap(SortedMap m) as the given SortedMap, sorted according to the same ordering. void clear() Removes all mappings from this TreeMap. Object clone() Returns a shallow copy of this TreeMap instance. Returns the comparator used to order this map, or null Comparator comparator() if this map uses its keys’ natural order. Returns true if this map contains a mapping for the boolean containsKey(Object key) specified key. boolean containsValue(Object value) Returns true if this map maps one or more keys to the specified value. Set entrySet() Returns a set view of the mappings contained in this map. Object firstKey() Returns the first (lowest) key currently in this sorted map. Object get(Object key) Returns the value to which this map maps the specified key. Returns a view of the portion of this map whose keys SortedMap headMap(Object toKey) are strictly less than toKey. Set keySet() Returns a set view of the keys contained in this map. Object lastKey() Returns the last (highest) key currently in this sorted map. Object put(Object key, Object value) Associates the specified value with the specified key in this map. Copies all of the mappings from the specified map void putAll(Map map) to this map. Removes the mapping for this key from this Object remove(Object key) TreeMap if present. Returns the number of key-value mappings in this map. int size() Returns a view of the portion of this map whose keys SortedMap subMap(Object fromKey, range from fromKey, inclusive, to toKey, exclusive. Object toKey) Returns a view of the portion of this map whose keys are SortedMap tailMap(Object fromKey) greater than or equal to fromKey. Returns a collection view of the values contained Collection values() in this map. TreeMap() FIG URE 1 0 .2 1 Operations on a TreeMap 10.9 A Philosophical Quandary Comparator instead of using Comparable as we did in our earlier implementations. The Comparator interface describes a method, compare, that, like compareTo, returns –1, 0, or 1, representing less than, equal to, or greater than. However, unlike compareTo, compare takes two arguments and does not need to be implemented within the class to be stored in the collection. We will discuss the general concepts of sets and maps further in Chapter 15 along with the Java Collections API implementations of these collections. 10.9 A Philosophical Quandary Early in Chapter 3, we stated that our naming convention would follow that of the Java Collections API where a class implementing a collection would be given the name of the data structure used and the collection it implements (e.g., ArrayStack, LinkedList, ArrayQueue). Given that naming convention and what we have just learned about the Java Collections API implementations of trees (e.g., TreeSet and TreeMap), it would appear that we have a philosophical quandary. Is a tree a data structure or a collection? Earlier in this chapter, we presented our implementation of both an ArrayBinarySearchTree and a LinkedBinarySearchTree. This would suggest that a binary search tree is a collection. However, we also presented the BinarySearchTreeList class, which would suggest that a binary search tree is a data structure. Figure 10.22 illustrates how the BinarySearchTreeList class was built upon the LinkedBinary SearchTree class which was built upon the LinkedBinaryTree class and that class was built using references as links. F I GU R E 1 0 .2 2 Using collections to implement other collections 325 326 C HA PT ER 10 Binary Search Trees So which of these represent data structures and which represents collections? Working from the top down, there is no question that an ordered list is a collection no matter how it is implemented. Similarly, even though it is often used to provide efficient implementations of other collections, a binary search tree fits our earlier definition of a collection by virtue of being an object that gathers and organizes other objects and by defining the specific ways in which those objects, which are called elements of the collection, can be accessed and managed. However, saying that a binary search tree is a collection does not mean that we cannot use it, or any other collection, to provide an implementation of other collections. It simply means that our naming convention can become unwieldy. What about a binary tree? Keep in mind that without specifying any additional detail about the organization of a binary tree, we cannot create methods to add or remove elements from the tree. Our examples of using a binary tree thus far have been expression trees from Chapter 9 and binary search trees in this chapter. Although expression trees may come closer to a specific application, both expression trees and binary search trees could easily be considered collections. However, it is difficult to argue that a binary tree is in and of itself a collection, given that we cannot add or remove anything from it. Though it is certainly open for debate, perhaps the best categorization for a binary tree is as an abstract data structure. Self-Review Questions Summary of Key Concepts ■ A binary search tree is a binary tree with the added property that the left child is less than the parent, which is less than or equal to the right child. ■ The definition of a binary search tree is an extension of the definition of a binary tree. ■ Each BinaryTreeNode object maintains a reference to the element stored at that node as well as references to each of the node’s children. ■ In removing an element from a binary search tree, another node must be promoted to replace the node being removed. ■ The leftmost node in a binary search tree will contain the minimum element, whereas the rightmost node will contain the maximum element. ■ One of the uses of trees is to provide efficient implementations of other collections. ■ If a binary search tree is not balanced, it may be less efficient than a linear structure. ■ The height of the right subtree minus the height of the left subtree is called the balance factor of a node. ■ There are only two ways that a tree, or any subtree of a tree, can become unbalanced: through the insertion of a node or through the deletion of a node. ■ The balance restriction on a red/black tree is somewhat less strict than that for AVL trees. However, in both cases, the find operation is order log n. ■ The Java Collections API provides two implementations of balanced binary search trees, TreeSet and TreeMap, both of which use a red/black tree implementation. Self-Review Questions SR 10.1 What is the difference between a binary tree and a binary search tree? SR 10.2 Why are we able to specify addElement and removeElement operations for a binary search tree but we were unable to do so for a binary tree? SR 10.3 Assuming that the tree is balanced, what is the time complexity (order) of the addElement operation? SR 10.4 Without the balance assumption, what is the time complexity (order) of the addElement operation? 327 328 C HA PT ER 10 Binary Search Trees SR 10.5 As stated in this chapter, a degenerate tree might actually be less efficient than a linked list. Why? SR 10.6 Our removeElement operation uses the inorder successor as the replacement for a node with two children. What would be another reasonable choice for the replacement? SR 10.7 The removeAllOccurrences operation uses both the contains and removeElement operations. What is the resulting time complexity (order) for this operation? SR 10.8 RemoveFirst and first were O(1) operations for our earlier im- plementation of an ordered list. Why are they less efficient for our BinarySearchTreeOrderedList? SR 10.9 Why does the BinarySearchTreeOrderedList class have to define the iterator method? Why can’t it just rely on the iterator method of its parent class like it does for size and isEmpty? SR 10.10 What is the time complexity of the addElement operation after modifying to implement an AVL tree? SR 10.11 What imbalance is fixed by a single right rotation? SR 10.12 What imbalance is fixed by a leftright rotation? SR 10.13 What is the balance factor of an AVL tree node? SR 10.14 In our discussion of the process for rebalancing an AVL tree, we never discussed the possibility of the balance factor of a node being either +2 or –2 and the balance factor of one of its children being either +2 or –2. Why not? SR 10.15 We noted that the balance restriction for a red/black tree is less strict than that of an AVL tree and yet we still claim that traversing the longest path in a red/black tree is still O(log n). Why? SR 10.16 What is the difference between a TreeSet and a TreeMap? Exercises EX 10.1 Draw the binary search tree that results from adding the following integers (34 45 3 87 65 32 1 12 17). Assume our simple implementation with no balancing mechanism. EX 10.2 Starting with the resulting tree from Exercise 10.1, draw the tree that results from removing (45 12 1), again using our simple implementation with no balancing mechanism. Programming Projects EX 10.3 Repeat Exercise 10.1, this time assuming an AVL tree. Include the balance factors in your drawing. EX 10.4 Repeat Exercise 10.2, this time assuming an AVL tree and using the result of Exercise 10.3 as a starting point. Include the balance factors in your drawing. EX 10.5 Repeat Exercise 10.1, this time assuming a red/black tree. Label each node with its color. EX 10.6 Repeat Exercise 10.2, this time assuming a red/black tree and using the result of Exercise 10.5 as a starting point. Label each node with its color. EX 10.7 Starting with an empty red/black tree, draw the tree after insertion and before rebalancing, and after rebalancing (if necessary) for the following series of inserts and removals: AddElement(40); AddElement(25): AddElement(10); AddElement(5); AddElement(1); AddElement(45); AddElement(50); RemoveElement(40); RemoveElement(25); EX 10.8 Repeat Exercise 10.7, this time with an AVL tree. Programming Projects PP 10.1 The ArrayBinarySearchTree class is currently using the find and contains methods of the ArrayBinaryTree class. Implement these methods for the ArrayBinarySearchTree class so that they will be more efficient by making use of the ordering property of a binary search tree. PP 10.2 The LinkedBinarySearchTree class is currently using the find and contains methods of the LinkedBinaryTree class. Implement these methods for the LinkedBinarySearchTree class so that they will be more efficient by making use of the ordering property of a binary search tree. PP 10.3 Implement the removeMax, findMin, and findMax operations for our linked binary search tree implementation. PP 10.4 Implement the removeMax, findMin, and findMax operations for our array binary search tree implementation. 329 330 C HA PT ER 10 Binary Search Trees PP 10.5 Implement a balance tree method for the linked implementation using the brute-force method described in Section 10.4. PP 10.6 Implement a balance tree method for the array implementation using the brute-force method described in Section 10.4. PP 10.7 Develop an array implementation of a binary search tree built upon an array implementation of a binary tree by using the simulated link strategy. Each element of the array will need to maintain both a reference to the data element stored there and the array positions of the left and right child. You also need to maintain a list of available array positions where elements have been removed, in order to reuse those positions. PP 10.8 Modify the binary search tree implementation to make it an AVL tree. PP 10.9 Modify the binary search tree implementation to make it a red/black tree. PP 10.10 Modify the add operation for the linked implementation of a binary search tree to use a recursive algorithm. PP 10.11 Modify the add operation for the array implementation of a binary search tree to use a recursive algorithm. Answers to Self-Review Questions SRA 10.1 A binary search tree has the added ordering property that the left child of any node is less than the node, and the node is less than or equal to its right child. SRA 10.2 With the added ordering property of a binary search tree, we are now able to define what the state of the tree should be after an add or remove. We were unable to define that state for a binary tree. SRA 10.3 If the tree is balanced, finding the insertion point for the new element will take at worst log n steps, and since inserting the element is simply a matter of setting the value of one reference, the operation is O(log n). SRA 10.4 Without the balance assumption, the worst case would be a degenerate tree, which is effectively a linked list. Therefore, the addElement operation would be O(n). SRA 10.5 A degenerate tree will waste space with unused references, and many of the algorithms will check for null references before following the degenerate path, thus adding steps that the linked list implementation does not have. Answers to Self-Review Questions SRA 10.6 The best choice is the inorder successor since we are placing equal values to the right. SRA 10.7 With our balance assumption, the contains operation uses the find operation, which will be rewritten in the BinarySearchTree class to take advantage of the ordering property and will be O(log n). The removeElement operation is O(log n). The while loop will iterate some constant (k) number of times depending on how many times the given element occurs within the tree. The worst case would be that all n elements of the tree are the element to be removed, which would make the tree degenerate, and in which case the complexity would be n*2*n or O(n2). However, the expected case would be some small constant (0<=k<n) occurrences of the element in a balanced tree, which would result in a complexity of k*2*log n or O(log n). SRA 10.8 In our earlier linked implementation of an ordered list, we had a reference that kept track of the first element in the list, which made it quite simple to remove it or return it. With a binary search tree, we have to traverse to get to the leftmost element before knowing that we have the first element in the ordered list. SRA 10.9 Remember that the iterators for a binary tree are all followed by which traversal order to use. That is why the iterator method for the BinarySearchTreeOrderedList class calls the iteratorInOrder method of the BinaryTree class. SRA 10.10 Keep in mind that an addElement method only affects one path of the tree, which in a balanced AVL tree has a maximum length of log n. As we have discussed previously, finding the position to insert and setting the reference is O(log n). We then have to progress back up the same path, updating the balance factors of each node (if necessary) and rotating if necessary. Updating the balance factors is an O(1) step and rotation is also an O(1) step. Each of these will at most have to be done log n times. Therefore, addElement has time complexity 2*log n or O(log n). SRA 10.11 A single right rotation will fix the imbalance if the long path is in the left subtree of the left child of the root. SRA 10.12 A leftright rotation will fix the imbalance if the long path is in the right subtree of the left child of the root. SRA 10.13 The balance factor of an AVL tree node is the height of the right subtree minus the height of the left subtree. 331 332 C HA PT ER 10 Binary Search Trees SRA 10.14 Rebalancing an AVL tree is done after either an insertion or a deletion and it is done starting at the affected node and working up along a single path to the root. As we progress upward, we update the balance factors and rotate if necessary. We will never encounter a situation where both a child and a parent have balance factors of +/–2 because we would have already fixed the child before we ever reached the parent. SRA 10.15 Since no red node can have a red child, then at most half of the nodes in a path could be red nodes and at least half of the nodes in a path are black. From this we can argue that the maximum height of a red/black tree is roughly 2*log n and thus the traversal of the longest path is O(log n). SRA 10.16 Both are red/black tree implementations of a binary search tree. The difference is that in a Set, all of the data are stored with an element, and with a TreeMap, a separate key is created and stored in the collection while the data are stored separately. References Adel’son-Vel’skii, G. M., and E. M. Landis. “An Algorithm for the Organization of Information.” Soviet Mathematics 3 (1962): 1259–1263. Bayer, R. “Symmetric Binary B-trees: Data Structure and Maintenance Algorithms.” Acta Informatica (1972): 290–306. Collins, W. J. Data Structures and the Java Collections Framework. New York: McGraw-Hill, 2002. Cormen, T., C. Leierson, and R. Rivest. Introduction to Algorithms. New York: McGraw-Hill, 1992. Guibas, L., and R. Sedgewick. “A Diochromatic Framework for Balanced Trees.” Proceedings of the 19th Annual IEEE Symposium on Foundations of Computer Science (1978): 8–21. 11 Priority Queues and Heaps I n this chapter, we will look at another ordered extension of binary trees. We will examine heaps, including both CHAPTER OBJECTIVES ■ Define a heap abstract data structure ■ Demonstrate how a heap can be used to solve problems ■ Examine various heap implementations ■ Compare heap implementations linked and array implementations, and the algorithms for adding and removing elements from a heap. We will also examine some uses for heaps including their principal use, priority queues. 333 334 C HA PT ER 11 Priority Queues and Heaps 11.1 A Heap A heap is a binary tree with two added properties: ■ It is a complete tree, as described in Chapter 9. ■ For each node, the node is less than or equal to both the left child and the right child. This definition describes a minheap. A heap can also be a maxheap, in which the node is greater than or equal to its children. We will focus our discussion in this chapter on minheaps. All of the same processes work for maxheaps by reversing the comparisons. K E Y CO N C E PT A minheap is a complete binary tree in which each node is less than or equal to both of its children. K E Y CO N C E PT A minheap stores its smallest element at the root of the binary tree, and both children of the root of a minheap are also minheaps. Figure 11.1 describes the operations on a heap. The definition of a heap is an extension of a binary tree and thus inherits all of those operations as well. Note that since our implementation of a binary tree did not have any operations to add or remove elements from the tree, there are not any operations that would violate the properties of a heap. Listing 11.1 shows the interface definition for a heap. Figure 11.2 shows the UML description of the HeapADT. Simply put, a minheap will always store its smallest element at the root of the binary tree, and both children of the root of a minheap are also minheaps. Figure 11.3 illustrates two valid minheaps with the same data. Let’s look at the basic operations on a heap and examine generic algorithms for each. The addElement Operation The addElement method adds a given element to the appropriate location in the heap, maintaining both the completeness property and the ordering property of the heap. This method throws a ClassCastException if the given element is not Comparable. A binary tree is considered complete if it is balanced, meaning all of the leaves are at level h or h–1, where h is log2n and n is the number of elements in Operation Description addElement removeMin findMin Adds the given element to the heap. Removes the minimum element in the heap. Returns a reference to the minimum element in the heap. FIG URE 11 . 1 The operations on a heap 11.1 L I S T I N G A Heap 1 1 . 1 /** * HeapADT defines the interface to a heap. * * @author Dr. Lewis * @author Dr. Chase * @version 1.0, 9/9/2008 */ package jss2; public interface HeapADT<T> extends BinaryTreeADT<T> { /** * Adds the specified object to this heap. * * @param obj the element to be added to this heap */ public void addElement (T obj); /** * Removes element with the lowest value from this heap. * * @return the element with the lowest value from this heap */ public T removeMin(); /** * Returns a reference to the element with the lowest value in * this heap. * * @return a reference to the element with the lowest value in this heap */ public T findMin(); } KEY CON CEPT the tree, and all of the leaves at level h are on the left side of the tree. Because a heap is a complete tree, there is only one correct location for the insertion of a new node, and that is either the next open position from the left at level h or the first position on the left at level h + 1 if level h is full. Figure 11.4 illustrates these two possibilities. The addElement method adds a given Comparable element to the appropriate location in the heap, maintaining both the completeness property and the ordering property of the heap. 335 336 C HA PT ER 11 Priority Queues and Heaps <<interface>> BinaryTreeADT <<interface>> HeapADT getRoot() toString() isEmpty() size() contains() find() iteratorInOrder() iteratorPreOrder() iteratorPostOrder() iteratorLevelOrder() addElement() removeMin() findMin() FIG URE 1 1 .2 UML description of the HeapADT KE Y CO N C E PT Once we have located the new node in the proper position, we then must account for the ordering property. To do this, we simply compare the new value to its parent value and swap the values if the new node is less than its parent. We continue this process up the tree until the new value either is greater than its parent or is in the root of the heap. Figure 11.5 illustrates this process for inserting a new element into a heap. Typically, in heap implementations, we keep track of the position of the last node or, more precisely, the last leaf in the tree. After an addElement operation, the last node is set to the node that was inserted. Because a heap is a complete tree, there is only one correct location for the insertion of a new node, and that is either the next open position from the left at level h or the first position on the left at level h + 1 if level h is full. 3 3 5 8 4 7 9 7 8 4 9 5 FIG URE 1 1 .3 Two minheaps containing the same data 11.1 3 337 3 5 8 A Heap 4 7 7 9 4 8 9 5 6 Next insertion point FIG URE 1 1 .4 Insertion points for a heap The removeMin Operation KEY CON CEPT Typically, in heap implementations, The removeMin method removes the minimum element from the we keep track of the position of the minheap and returns it. Because the minimum element is stored in last node or, more precisely, the last leaf in the tree. the root of a minheap, we need to return the element stored at the root and replace it with another element in the heap. As with the addElement operation, to maintain the completeness of the tree, there is only one valid element to replace the root, and that is the element stored in the last leaf in the tree. This last leaf will be the rightmost leaf at level h 3 5 8 3 4 7 9 insert 2 5 8 3 4 7 9 5 2 8 2 2 7 9 5 4 FIG URE 1 1 .5 Insertion and reordering in a heap 8 3 7 9 4 338 C HA PT ER 11 Priority Queues and Heaps 4 5 5 9 7 8 2 2 3 5 3 8 7 9 4 3 8 FIG URE 1 1 .6 Examples of the last leaf in a heap K E Y CO N C E PT To maintain the completeness of the tree, there is only one valid element to replace the root, and that is the element stored in the last leaf in the tree. of the tree. Figure 11.6 illustrates this concept of the last leaf under a variety of circumstances. Once the element stored in the last leaf has been moved to the root, the heap will then have to be reordered to maintain the heap’s ordering property. This is accomplished by comparing the new root element to the smaller of its children and then swapping them if the child is smaller. This process is repeated on down the tree until the element either is in a leaf or is less than both of its children. Figure 11.7 illustrates the process of removing the minimum element and then reordering the tree. The findMin Operation The findMin method returns a reference to the smallest element in the minheap. Because that element is always stored in the root of the tree, this method is simply implemented by returning the element stored in the root. Before reordering Initial Heap Element to be removed 8 9 3 4 5 7 After reordering 9 5 8 4 5 4 7 8 Replacement FIG URE 1 1 .7 Removal and reordering in a heap 9 7 11.2 11.2 Using Heaps: Priority Queues 339 Using Heaps: Priority Queues A priority queue is a collection that follows two ordering rules. First, items with higher priority go first. Second, items with the same priority use a first in, first out method to determine their ordering. Priority queues have a variety of applications (e.g., task scheduling in an operating system, traffic scheduling on a network, and even job scheduling at your local auto mechanic). A priority queue could be implemented using a list of queues where each queue represents items of a given priority. Another solution to this problem is to use a minheap. Sorting the heap by priority accomplishes the first ordering (higher priority items go first). However, the first in, first out ordering of items with the same priority is something we will have to manipulate. The solution is to create a PriorityQueueNode object that stores the element to KEY CON CEPT be placed on the queue, the priority of the element, and the order in Though not a queue at all, a minheap which elements are placed on the queue. Then, we simply define the provides an efficient implementation of a priority queue. compareTo method for the PriorityQueueNode class to compare priorities first and then compare order if there is a tie. Listing 11.2 shows the PriorityQueueNode class, and Listing 11.3 shows the PriorityQueue class. The UML description of the PriorityQueue class is left as an exercise. L I S T I N G 1 1 . 2 /** * PriorityQueueNode represents a node in a priority queue containing a * comparable object, order, and a priority value. * * @author Dr. Lewis * @author Dr. Chase * @version 1.0, 8/19/08 */ public class PriorityQueueNode<T> implements Comparable<PriorityQueueNode> { private static int nextorder = 0; private int priority; private int order; private T element; /** * Creates a new PriorityQueueNode with the specified data. * 340 C HA PT ER 11 L I S T I N G Priority Queues and Heaps 1 1 . 2 continued * @param obj the element of the new priority queue node * @param prio the integer priority of the new queue node */ public PriorityQueueNode (T obj, int prio) { element = obj; priority = prio; order = nextorder; nextorder++; } /** * Returns the element in this node. * * @return the element contained within this node */ public T getElement() { return element; } /** * Returns the priority value for this node. * * @return the integer priority for this node */ public int getPriority() { return priority; } /** * Returns the order for this node. * * @return the integer order for this node */ public int getOrder() { return order; } /** * Returns a string representation for this node. * */ 11.2 L I S T I N G 1 1 . 2 Using Heaps: Priority Queues continued public String toString() { String temp = (element.toString() + priority + order); return temp; } /** * Returns the 1 if the current node has higher priority than * the given node and -1 otherwise. * * @param obj the node to compare to this node * @return the integer result of the comparison of the obj node and * this one */ public int compareTo(PriorityQueueNode obj) { int result; PriorityQueueNode<T> temp = obj; if (priority > temp.getPriority()) result = 1; else if (priority < temp.getPriority()) result = -1; else if (order > temp.getOrder()) result = 1; else result = -1; return result; } } L I S T I N G 1 1 . 3 /** * PriorityQueue demonstrates a priority queue using a Heap. * * @author Dr. Lewis * @author Dr. Chase 341 342 C HA PT ER 11 L I S T I N G Priority Queues and Heaps 1 1 . 3 continued * @version 1.0, 8/19/08 */ import jss2.*; public class PriorityQueue<T> extends ArrayHeap<PriorityQueueNode<T>> { /** * Creates an empty priority queue. */ public PriorityQueue() { super(); } /** * Adds the given element to this PriorityQueue. * * @param object the element to be added to the priority queue * @param priority the integer priority of the element to be added */ public void addElement (T object, int priority) { PriorityQueueNode<T> node = new PriorityQueueNode<T> (object, priority); super.addElement(node); } /** * Removes the next highest priority element from this priority * queue and returns a reference to it. * * @return a reference to the next highest priority element in this queue */ public T removeNext() { PriorityQueueNode<T> temp = (PriorityQueueNode<T>)super.removeMin(); return temp.getElement(); } } 11.3 11.3 Implementing Heaps: With Links 343 Implementing Heaps: With Links All of our implementations of trees thus far have been illustrated using links. Thus it is natural to extend that discussion to a linked implementation of a heap. Because of the requirement that we be able to traverse up the tree after an insertion, it is necessary for the nodes in a heap to store a pointer to their parent. Because our BinaryTreeNode class did not have a parent pointer, we start our linked implementation by creating a HeapNode class that extends our BinaryTreeNode class and adds a parent pointer. Listing 11.4 shows the HeapNode class. KEY CON CEPT Because of the requirement that we be able to traverse up the tree after an insertion, it is necessary for the nodes in a heap to store a pointer to their parent. The additional instance data for a linked implementation will consist of a single reference to a HeapNode called lastNode so that we can keep track of the last leaf in the heap: public HeapNode lastNode; The addElement Operation The addElement method must accomplish three tasks: add the new node at the appropriate location, reorder the heap to maintain the ordering property, and then reset the lastNode pointer to point to the new last node. L I S T I N G 1 1 . 4 /** * HeapNode creates a binary tree node with a parent pointer for use * in heaps. * * @author Dr. Lewis * @author Dr. Chase * @version 1.0, 9/9/2008 */ package jss2; public class HeapNode<T> extends BinaryTreeNode<T> { protected HeapNode<T> parent; /** * Creates a new heap node with the specified data. * * @param obj the data to be contained within the new heap nodes */ 344 C HA PT ER 11 L I S T I N G Priority Queues and Heaps 1 1 . 4 continued HeapNode (T obj) { super(obj); parent = null; } } /** * Adds the specified element to this heap in the appropriate * position according to its key value. Note that equal elements * are added to the right. * * @param obj the element to be added to this heap */ public void addElement (T obj) { HeapNode<T> node = new HeapNode<T>(obj); if (root == null) root=node; else { HeapNode<T> next_parent = getNextParentAdd(); if (next_parent.left == null) next_parent.left = node; else next_parent.right = node; node.parent = next_parent; } lastNode = node; count++; if (count>1) heapifyAdd(); } 11.3 Implementing Heaps: With Links This method also uses two private methods: getNextParentAdd, which returns a reference to the node that will be the parent of the node to be inserted, and heapifyAdd, which accomplishes any necessary reordering of the heap starting with the new leaf and working up toward the root. Both of those methods are shown below. /** * Returns the node that will be the parent of the new node * * @return the node that will be a parent of the new node */ private HeapNode<T> getNextParentAdd() { HeapNode<T> result = lastNode; while ((result != root) && (result.parent.left != result)) result = result.parent; if (result != root) if (result.parent.right == null) result = result.parent; else { result = (HeapNode<T>)result.parent.right; while (result.left != null) result = (HeapNode<T>)result.left; } else while (result.left != null) result = (HeapNode<T>)result.left; return result; } /** * Reorders this heap after adding a node. */ private void heapifyAdd() { T temp; HeapNode<T> next = lastNode; 345 346 C HA PT ER 11 Priority Queues and Heaps temp = next.element; while ((next != root) && (((Comparable)temp).compareTo (next.parent.element) < 0)) { next.element = next.parent.element; next = next.parent; } next.element = temp; } In this linked implementation, the first step in the process of adding an element is to determine the parent of the node to be inserted. Because, in the worst case, this involves traversing from the bottom-right node of the heap up to the root and then down to the bottom-left node of the heap, this step has time complexity 2 ⫻ log n. The next step is to insert the new node. Because this involves only simple assignment statements, this step has constant time complexity (O(1)). The last step is to reorder the path from the inserted leaf to the root if necessary. This process involves at most log n comparisons because that is the length of the path. Thus the addElement operation for the linked implementation has time complexity 2 ⫻ log n + 1 + log n or O(log n). Note that the heapifyAdd method does not perform a full swap of parent and child as it moves up the heap. Instead, it simply shifts parent elements down until a proper insertion point is found and then assigns the new value into that location. This does not actually improve the O() of the algorithm as it would be O(log n) even if we were performing full swaps. However, it does improve the efficiency since it reduces the number of assignments performed at each level of the heap. The removeMin Operation The removeMin method must accomplish three tasks: replace the element stored in the root with the element stored in the last node, reorder the heap if necessary, and return the original root element. Like the addElement method, the removeMin method uses two additional methods: getNewLastNode, which returns a reference to the node that will be the new last node, and heapifyRemove, which will accomplish any necessary reordering of the tree starting from the root down. All three of these methods are shown below. 11.3 Implementing Heaps: With Links /** * Remove the element with the lowest value in this heap and * returns a reference to it. Throws an EmptyCollectionException * if the heap is empty. * * @return the element with the lowest value in * this heap * @throws EmptyCollectionException if an empty collection exception occurs */ public T removeMin() throws EmptyCollectionException { if (isEmpty()) throw new EmptyCollectionException ("Empty Heap"); T minElement = root.element; if (count == 1) { root = null; lastNode = null; } else { HeapNode<T> next_last = getNewLastNode(); if (lastNode.parent.left == lastNode) lastNode.parent.left = null; else lastNode.parent.right = null; root.element = lastNode.element; lastNode = next_last; heapifyRemove(); } count--; return minElement; } 347 348 C HA PT ER 11 Priority Queues and Heaps /** * Returns the node that will be the new last node after a remove. * * @return the node that will be the new last node after a remove */ private HeapNode<T> getNewLastNode() { HeapNode<T> result = lastNode; while ((result != root) && (result.parent.left == result)) result = result.parent; if (result != root) result = (HeapNode<T>)result.parent.left; while (result.right != null) result = (HeapNode<T>)result.right; return result; } /** * Reorders this heap after removing the root element. */ private void heapifyRemove() { T temp; HeapNode<T> node = (HeapNode<T>)root; HeapNode<T> left = (HeapNode<T>)node.left; HeapNode<T> right = (HeapNode<T>)node.right; HeapNode<T> next; if ((left == null) && (right == null)) next = null; else if (left == null) next = right; else if (right == null) next = left; else if (((Comparable)left.element).compareTo(right.element) < 0) next = left; 11.3 Implementing Heaps: With Links else next = right; temp = node.element; while ((next != null) && (((Comparable)next.element).compareTo (temp) < 0)) { node.element = next.element; node = next; left = (HeapNode<T>)node.left; right = (HeapNode<T>)node.right; if ((left == null) && (right == null)) next = null; else if (left == null) next = right; else if (right == null) next = left; else if (((Comparable)left.element).compareTo(right.element) < 0) next = left; else next = right; } node.element = temp; } The removeMin method for the linked implementation must remove the root element and replace it with the element from the last node. Because this is simply assignment statements, this step has time complexity 1. Next, this method must reorder the heap if necessary from the root down to a leaf. Because the maximum path length from the root to a leaf is log n, this step has time complexity log n. Finally, we must determine the new last node. Like the process for determining the next parent node for the addElement method, the worst case is that we must traverse from a leaf through the root and down to another leaf. Thus the time complexity of this step is 2*log n. The resulting time complexity of the removeMin operation is 2*log n + log n + 1 or O(log n). The findMin Operation The findMin method simply returns a reference to the element stored at the root of the heap and therefore is O(1). 349 350 C HA PT ER 11 Priority Queues and Heaps 11.4 Implementing Heaps: With Arrays An array implementation of a heap may provide a simpler alternative than our linked implementation. Many of the intricacies of the linked implementation relate to the need to traverse up and down the tree to determine the last leaf of the tree or to determine the parent of the next node to KE Y CO N C E PT insert. Many of those difficulties do not exist in the array implemenIn an array implementation of a binary tree, the root of the tree is in tation because we are able to determine the last node in the tree by position 0, and for each node n, n’s looking at the last element stored in the array. left child is in position 2n + 1 and n’s right child is in position 2(n + 1). As we discussed in Chapter 9, a simple array implementation of a binary tree can be created using the notion that the root of the tree is in position 0, and for each node n, n’s left child will be in position 2n+1 of the array and n’s right child will be in position 2(n + 1) of the array. Of course, the inverse is also true. For any node n other than the root, n’s parent is in position (n – 1)/2. Because of our ability to calculate the location of both parent and child, unlike the linked implementation, the array implementation does not require the creation of a HeapNode class. The UML description of the array implementation of a heap is left as an exercise. The addElement Operation The addElement method for the array implementation must accomplish three tasks: add the new node at the appropriate location, reorder the heap to maintain the ordering property, and increment the count by one. Of course, as with all of our array implementations, the method must first check for available space and expand the capacity of the array if necessary. Like the linked implementation, the addElement operation of the array implementation uses a private method called heapifyAdd to reorder the heap if necessary. /** * Adds the specified element to this heap in the appropriate * position according to its key value. Note that equal elements * are added to the right. * * @param obj the element to be added to this heap */ public void addElement (T obj) { 11.4 Implementing Heaps: With Arrays 351 if (count==tree.length) expandCapacity(); tree[count] =obj; count++; if (count>1) heapifyAdd(); } /** * Reorders this heap to maintain the ordering property after * adding a node. */ private void heapifyAdd() { T temp; int next = count - 1; temp = tree[next]; while ((next != 0) && (((Comparable)temp).compareTo (tree[(next-1)/2]) < 0)) { tree[next] = tree[(next-1)/2]; next = (next-1)/2; } tree[next] = temp; } Unlike the linked implementation, the array implementation does not require the first step of determining the parent of the new node. However, both of the other steps are the same as those for the linked implementation. Thus the time complexity for the addElement operation for the array implementation is 1 + log n or O(log n). Granted, the two implementations have the same Order(), but the array implementation is more efficient. KEY CON CEPT The addElement operation for both the linked and array implementations is O(log n). 352 C HA PT ER 11 Priority Queues and Heaps The removeMin Operation The removeMin method must accomplish three tasks: replace the element stored in the root with the element stored in the last element, reorder the heap if necessary, and return the original root element. In the case of the array implementation, we know the last element of the heap is stored in position count⫺1 of the array. We then use a private method heapifyRemove to reorder the heap as necessary. /** * Remove the element with the lowest value in this heap and * returns a reference to it. Throws an EmptyHeapException if * the heap is empty. * * @return a reference to the element with the * lowest value in this head * @throws EmptyCollection Exception if an empty collection exception occurs */ public T removeMin() throws EmptyCollectionException { if (isEmpty()) throw new EmptyCollectionException ("Empty Heap"); T minElement = tree[0]; tree[0] = tree[count-1]; heapifyRemove(); count--; return minElement; } /** * Reorders this heap to maintain the ordering property. */ private void heapifyRemove() { T temp; int node = 0; int left = 1; int right = 2; int next; 11.4 Implementing Heaps: With Arrays 353 if ((tree[left] == null) && (tree[right] == null)) next = count; else if (tree[left] == null) next = right; else if (tree[right] == null) next = left; else if (((Comparable)tree[left]).compareTo(tree[right]) < 0) next = left; else next = right; temp = tree[node]; while ((next < count) && (((Comparable)tree[next]).compareTo (temp) < 0)) { tree[node] = tree[next]; node = next; left = 2*node+1; right = 2*(node+1); if ((tree[left] == null) && (tree[right] == null)) next = count; else if (tree[left] == null) next = right; else if (tree[right] == null) next = left; else if (((Comparable)tree[left]).compareTo(tree[right]) < 0) next = left; else next = right; } tree[node] = temp; } Like the addElement method, the array implementation of the removeMin operation looks just like the linked implementation except that it does not have to determine the new last node. Thus the resulting time complexity is log n + 1 or O(log n). KEY CON CEPT The removeMin operation for both the linked and array implementations is O(log n). The findMin Operation Like the linked implementation, the findMin method simply returns a reference to the element stored at the root of the heap or position 0 of the array and therefore is O(1). 354 C HA PT ER 11 Priority Queues and Heaps 11.5 Using Heaps: Heap Sort In Chapter 8, we introduced a variety of sorting techniques, some of which were sequential sorts (bubble sort, selection sort, and insertion sort) and some of which were logarithmic sorts (merge sort and quick sort). In that chapter, we also introduced a queue-based sort called a radix sort. Given the ordering property of a heap, it is natural to think of using a heap to sort a list of numbers. The process is quite simple. Simply add each of the elements of the list to a heap and then remove them one at a time from the root. In the case of a minheap, the result will be the list in ascending order. In the case of a maxheap, the result will be the list in descending order. Because both the add and remove operations are O(log n), it might be tempting to conclude that a heap sort is also O(log n). However, keep in mind that those operations are O(log n) to add or remove a single element in a list of n elements. Insertion into a heap is O(log n) for any given node, and thus would be O(n log n) for n nodes. Removal is also O(log n) for a single node and thus O(n log n) for n nodes. With the heap sort algorithm, we are performing both operations, addElement KE Y CO N C E PT and removeMin, n times, once for each of the elements in the list. The heapSort method consists of Therefore, the resulting time complexity is 2 ⫻ n ⫻ log n or O(n log n). adding each of the elements of the list to a heap and then removing them one at a time. KE Y CO N C E PT Heap sort is O(n log n). L I S T I N G It is also possible to “build” a heap in place using the array to be sorted. Because we know the relative position of each parent and child in the heap, we can simply start with the first non-leaf node in the array, compare it to its children, and swap if necessary. We then work backward in the array until we reach the root. Because, at most, this will require us to make two comparisons for each non-leaf node, this approach is O(n) to build the heap. However, using this approach, removing each element from the heap and maintaining the properties of the heap would still be O(n log n). Thus, even though this approach is slightly more efficient, roughly 2 ⫻ n + n log n, it is still O(n log n). The implementation of this approach is left as an exercise. The heapSort method could be added to our class of search and sort methods described in Chapter 8. Listing 11.5 illustrates how it might be created as a standalone class. 1 1 . 5 /** * HeapSort sorts a given array of Comparable objects using a heap. * * @author Dr. Lewis * @author Dr. Chase * @version 1.0, 8/19/08 */ 11.5 L I S T I N G 1 1 . 5 Using Heaps: Heap Sort continued package jss2; public class HeapSort<T> { /** * @param data the data to be added to the heapsort * @param min the integer minimum value * @param max the integer maximum value */ public void HeapSort(T[] data, int min, int max) { ArrayHeap<T> temp = new ArrayHeap<T>(); /** copy the array into a heap */ for (int ct = min; ct <= max; ct++) temp.addElement(data[ct]); /** place the sorted elements back into the array */ int count = min; while (!(temp.isEmpty())) { data[count] = temp.removeMin(); count++; } } } 355 356 C HA PT ER 11 Priority Queues and Heaps Summary of Key Concepts ■ A minheap is a complete binary tree in which each node is less than or equal to both the left child and the right child. ■ A minheap stores its smallest element at the root of the binary tree, and both children of the root of a minheap are also minheaps. ■ The addElement method adds a given Comparable element to the appropriate location in the heap, maintaining both the completeness property and the ordering property of the heap. ■ Because a heap is a complete tree, there is only one correct location for the insertion of a new node, and that is either the next open position from the left at level h or the first position on the left at level h + 1 if level h is full. ■ Typically, in heap implementations, we keep track of the position of the last node or, more precisely, the last leaf in the tree. ■ To maintain the completeness of the tree, there is only one valid element to replace the root, and that is the element stored in the last leaf in the tree. ■ Though not a queue at all, a minheap provides an efficient implementation of a priority queue. ■ Because of the requirement that we be able to traverse up the tree after an insertion, it is necessary for the nodes in a heap to store a pointer to their parent. ■ In an array implementation of a binary tree, the root of the tree is in position 0, and for each node n, n’s left child is in position 2n + 1 and n’s right child is in position 2(n + 1). ■ The addElement operation for both the linked and array implementations is O(log n). ■ The removeMin operation for both the linked and array implementations is O(log n). ■ The heapSort method consists of adding each of the elements of the list to a heap and then removing them one at a time. ■ Heap sort is O(n log n). Self-Review Questions SR 11.1 What is the difference between a heap (a minheap) and a binary search tree? SR 11.2 What is the difference between a minheap and a maxheap? Exercises SR 11.3 What does it mean for a heap to be complete? SR 11.4 Does a heap ever have to be rebalanced? SR 11.5 The addElement operation for the linked implementation must determine the parent of the next node to be inserted. Why? SR 11.6 Why does the addElement operation for the array implementation not have to determine the parent of the next node to be inserted? SR 11.7 The removeMin operation for both implementations replaces the element at the root with the element in the last leaf of the heap. Why is this the proper replacement? SR 11.8 What is the time complexity of the addElement operation? SR 11.9 What is the time complexity of the removeMin operation? SR 11.10 What is the time complexity of heap sort? Exercises EX 11.1 Draw the heap that results from adding the following integers (34 45 3 87 65 32 1 12 17). EX 11.2 Starting with the resulting tree from Exercise 11.1, draw the tree that results from performing a removeMin operation. EX 11.3 Starting with an empty minheap, draw the heap after each of the following operations: addElement(40); addElement(25): removeMin(); addElement(10); removeMin(); addElement(5); addElement(1); removeMin(); addElement(45); addElement(50); EX 11.4 Repeat Exercise 11.3, this time with maxheap. EX 11.5 Draw the UML description for the PriorityQueue class described in this chapter. EX 11.6 Draw the UML description for the array implementation of heap described in this chapter. 357 358 C HA PT ER 11 Priority Queues and Heaps Programming Projects PP 11.1 Implement a queue using a heap. Keep in mind that a queue is a first in, first out structure. Thus the comparison in the heap will have to be according to order entry into the queue. PP 11.2 Implement a stack using a heap. Keep in mind that a stack is a last in, first out structure. Thus the comparison in the heap will have to be according to order entry into the queue. PP 11.3 Implement a maxheap using an array implementation. PP 11.4 Implement a maxheap using a linked implementation. PP 11.5 It is possible to make the heap sort algorithm more efficient by writing a method that will order the entire list at once instead of adding the elements one at a time. Implement such a method, and rewrite the heap sort algorithm to make use of it. PP 11.6 Use a heap to implement a simulator for a process scheduling system. In this system, jobs will be read from a file consisting of the job id (a six-character string), the length of the job (an int representing seconds), and the priority of the job (an int where the higher the number the higher the priority). Each job will also be assigned an arrival number (an int representing the order of its arrival). The simulation should output the job id, the priority, the length of the job, and the completion time (relative to a simulation start time of 0). PP 11.7 Create a birthday reminder system using a minheap such that the ordering on the heap is done each day according to days remaining until the individual’s birthday. Keep in mind that when a birthday passes, the heap must be reordered. PP 11.8 In Section 11.2, we described a more efficient heap sort algorithm that would build the heap within the existing array. Implement this more efficient heap sort algorithm. Answers to Self-Review Questions SRA 11.1 A binary search tree has the ordering property that the left child of any node is less than the node, and the node is less than or equal to its right child. A minheap is complete and has the ordering property that the node is less than both of its children. SRA 11.2 A minheap has the ordering property that the node is less than both of its children. A maxheap has the ordering property that the node is greater than both of its children. Answers to Self-Review Questions SRA 11.3 A heap is considered complete if it is balanced, meaning all of the leaves are at level h or h – 1, where h is log2n and n is the number of elements in the tree, and all of the leaves at level h are on the left side of the tree. SRA 11.4 No. By definition, a complete heap is balanced and the algorithms for add and remove maintain that balance. SRA 11.5 The addElement operation must determine the parent of the node to be inserted so that a child pointer of that node can be set to the new node. SRA 11.6 The addElement operation for the array implementation does not have to determine the parent of the new node because the new element is inserted in position count of the array and its parent is determined by position in the array. SRA 11.7 To maintain the completeness of the tree, the only valid replacement for the element at the root is the element at the last leaf. Then the heap must be reordered as necessary to maintain the ordering property. SRA 11.8 For both implementations, the addElement operation is O(log n). However, despite having the same order, the array implementation is somewhat more efficient because it does not have to determine the parent of the node to be inserted. SRA 11.9 For both implementations, the removeMin operation is O(log n). However, despite having the same order, the array implementation is somewhat more efficient because it does not have to determine the new last leaf. SRA 11.10 The heap sort algorithm is O(n log n). 359 This page intentionally left blank 12 Multi-way Search Trees W hen we first introduced the concept of efficiency of algorithms, we said that we were interested in issues such as processing time and memory. In this chapter, we explore CHAPTER OBJECTIVES ■ Examine 2-3 and 2-4 trees ■ Introduce the generic concept of a B-tree ■ Examine some specialized implementations of B-trees multi-way trees that were specifically designed with a concern for the use of space and the effect that a particular use of space could have on the total processing time for an algorithm. 361 362 C HA PT ER 12 Multi-way Search Trees 12.1 Combining Tree Concepts In Chapter 9, we established the difference between a general tree, which has a varying number of children per node, and a binary tree, which has at most two children per node. Then in Chapter 10, we discussed the concept of a search tree, which has a specific ordering relationship among the elements in the nodes to allow efficient searching for a target value. In particular, we focused on binary search trees. Now we can combine these concepts and exK E Y CO N C E PT tend them further. A multi-way search tree can have In a multi-way search tree, each node might have more than two child nodes, and, because it is a search tree, there is a specific ordering relationship among the elements. Furthermore, a single node in a multi-way search tree may store more than one element. more than two children per node and can store more than one element in each node. This chapter examines three specific forms of a multi-way search tree: ■ 2-3 trees ■ 2-4 trees ■ B-trees 12.2 2-3 Trees A 2-3 tree is a multi-way search tree in which each node has two children (referred to as a 2-node) or three children (referred to as a 3-node). A 2-node contains one element and, like a binary search tree, the left subtree contains elements that are less than that element and the right subtree contains elements that are greater than or equal to that element. However, unlike a binary search tree, a 2-node can have either no children or two children—it cannot have just one child. A 3-node contains two elements, one designated as the smaller element and one designated as the larger element. A 3-node has either no children or three children. If a 3-node has children, the left subtree contains elements that are less than the smaller element and the right subtree contains elements that are greater than or equal to the larger element. The middle subtree contains elements that are greater than or equal to the smaller element and less than K E Y CO N C E PT the larger element. A 2-3 tree contains nodes that contain either one or two elements and have either zero, two, or three children. All of the leaves of a 2-3 tree are on the same level. Figure 12.1 illustrates a valid 2-3 tree. Inserting Elements into a 2-3 Tree Similar to a binary search tree, all insertions into a 2-3 tree occur at the leaves of the tree. That is, the tree is searched to determine where the new element will go; 12.2 45 30 22 60 82 51 55 35 40 75 87 FIG URE 1 2 .1 A 2-3 tree then it is inserted. Unlike a binary tree, however, the process of inserting an element into a 2-3 tree can have a ripple effect on the structure of the rest of the tree. Inserting an element into a 2-3 tree has three cases. The first, and simplest, case is that the tree is empty. In this case, a new node is created containing the new element, and this node is designated as the root of the tree. The second case occurs when we want to insert a new element at a leaf that is a 2-node. That is, we traverse the tree to the appropriate leaf (which may also be the root) and find that the leaf is a 2-node (containing only one element). In this case, the new element is added to the 2-node, making it a 3-node. Note that the new element may be less than or greater than the existing element. Figure 12.2 illustrates this case by inserting the value 27 into the tree pictured in Figure 12.1. The leaf node containing 22 is a 2-node, therefore 27 is inserted into that node, making it a 3-node. Note that neither the number of nodes in the tree nor the height of the tree changed because of this insertion. The third insertion situation occurs when we want to insert a new element at a leaf that is a 3-node (containing two elements). In this case, because the 3-node cannot hold any more elements, it is split, and the middle element is moved up a level in the tree. The middle element that moves up a level could be either of the two elements that already existed in the 3-node, or it could be the new element being inserted. It depends on the relationship among those three elements. 45 45 30 22 30 60 82 35 40 51 55 75 87 22 27 60 82 35 40 initial tree result FIG URE 1 2 .2 Inserting 27 51 55 75 87 2-3 Trees 363 364 C HA PT ER 12 Multi-way Search Trees 45 45 30 22 27 60 82 35 40 51 55 75 60 82 30 35 87 22 27 32 40 initial tree 51 55 75 87 result FIGU RE 1 2 . 3 Inserting 32 Figure 12.3 shows the result of inserting the element 32 into the tree from Figure 12.2. Searching the tree, we reach the 3-node that contains the elements 35 and 40. That node is split, and the middle element (35) is moved up to join its parent node. Thus the internal node that contains 30 becomes a 3-node that contains both 30 and 35. Note that the act of splitting a 3-node results in two 2-nodes at the leaf level. In this example, we are left with one 2-node that contains 32 and another 2-node that contains 40. Now consider the situation in which we must split a 3-node whose parent is already a 3-node. The middle element that is promoted causes the parent to split, moving an element up yet another level in the tree. Figure 12.4 shows the effect of inserting the element 57 into the tree from Figure 12.3. Searching the tree, we reach the 3-node leaf that contains 51 and 55. This node is split, causing the middle element 55 to move up a level. But that node is already a 3-node, containing the values 60 and 82, so we split that node as well, promoting the element 60, which joins the 2-node containing 45 at the root. Therefore, inserting an element into a 2-3 tree can cause a ripple effect that changes several nodes in the tree. 45 60 45 60 82 30 35 22 27 32 40 51 55 75 30 35 87 22 27 32 initial tree 55 40 51 82 57 result FIG URE 1 2 . 4 Inserting 57 75 87 12.2 2-3 Trees 365 45 45 60 30 30 35 22 27 32 55 40 51 82 57 75 60 25 87 22 35 27 55 40 32 initial tree 51 82 57 75 87 result FIG URE 1 2 .5 Inserting 25 If this effect propagates all the way to the root of the entire tree, a new 2-node root is created. For example, inserting the element 25 into the tree from Figure 12.4 results in the tree depicted in Figure 12.5. The 3-node containing 22 and 27 is split, promoting 25. This causes the 3-node containing 30 and 35 to split, promoting 30. This causes the 3-node containing 45 and 60 (which happens to be the root of the entire tree) to split, creating a new 2-node root that contains 45. Note that when the root of the tree splits, the height of the tree increases by one. The insertion strategy for a 2-3 tree keeps all of the leaves at the same level. KEY CON CEPT If the propagation effect of a 2-3 tree insertion causes the root to split, the tree increases in height. Removing Elements from a 2-3 Tree Removal of elements from a 2-3 tree is also made up of three cases. The first case is that the element to be removed is in a leaf that is a 3-node. In this case, removal is simply a matter of removing the element from the node. Figure 12.6 illustrates 45 45 30 22 30 60 82 35 40 51 55 initial tree 75 87 22 60 82 35 40 55 result FIG URE 1 2 .6 Removal from a 2-3 tree (case 1) 75 87 366 C HA PT ER 12 Multi-way Search Trees 45 45 30 22 35 60 82 35 40 51 55 initial tree 75 87 30 60 82 40 51 55 75 87 result FIG URE 1 2 .7 Removal from a 2-3 tree (case 2.1) this process by removing the element 51 from the tree we began with in Figure 12.1. Note that the properties of a 2-3 tree are maintained. The second case is that the element to be removed is in a leaf that is a 2-node. This condition is called underflow and creates a situation in which we must rotate the tree and/or reduce the tree’s height in order to maintain the properties of the 2-3 tree. This situation can be broken down into four subordinate cases that we will refer to as cases 2.1, 2.2, 2.3, and 2.4. Figure 12.7 illustrates case 2.1 and shows what happens if we remove the element 22 from our initial tree shown in Figure 12.1. In this case, because the parent node has a right child that is a 3-node, we can maintain the properties of a 2-3 tree by rotating the smaller element of the 3-node around the parent. The same process will work if the element being removed from a 2-node leaf is the right child and the left child is a 3-node. What happens if we now remove the element 30 from the resulting tree in Figure 12.7? We can no longer maintain the properties of a 2-3 tree through a local rotation. Keep in mind, a node in a 2-3 tree cannot have just one child. Because the leftmost child of the right child of the root is a 3-node, we can rotate the smaller element of that node around the root to maintain the properties of a 2-3 tree. This process is illustrated in Figure 12.8 and represents case 2.2. Notice that the element 51 moves to the root, the element 45 becomes the larger element in a 3-node leaf, and then the smaller element of that leaf is rotated around its parent. Once element 51 was moved to the root and element 45 was moved to a 3-node leaf, we were back in the same situation as case 2.1. Given the resulting 2-3 tree in Figure 12.8, what happens if we now remove element 55? None of the leaves of this tree are 3-nodes. Thus, rotation from a leaf, even from a distance, is no longer an option. However, because the parent node is a 3-node, all that is required to maintain the properties of a 2-3 node is to change this 3-node to a 2-node by rotating the smaller element (60) into what will now be the left child of the node. Figure 12.9 illustrates case 2.3. 12.2 45 51 35 30 35 60 82 51 55 40 75 87 60 82 40 45 30 initial tree 55 75 87 intermediate step 51 40 35 60 82 45 55 75 87 result FIG URE 1 2 .8 Removal from a 2-3 tree (case 2.2) If we then remove element 60 (using case 1), the resulting tree contains nothing but 2-nodes. Now, if we remove another element, perhaps element 45, rotation is no longer an option. We must instead reduce the height of the tree in order to maintain the properties of a 2-3 tree. This is case 2.4. To accomplish this, we simply combine each of the leaves with their parent and siblings in order. If any of these combinations contains more than two elements, we split it into two 2-nodes and promote or propagate the middle element. Figure 12.10 illustrates this process for reducing the height of the tree. The third case is that the element to be removed is in an internal node. As we did with binary search trees, we can simply replace the element to be removed 51 51 40 30 40 60 82 45 55 initial tree 75 87 35 82 60 75 45 result FIG URE 1 2 .9 Removal from a 2-3 tree (case 2.3) 87 2-3 Trees 367 368 C HA PT ER 12 Multi-way Search Trees 51 51 82 40 35 40 82 35 45 75 75 87 87 result initial tree FIG URE 1 2 .1 0 Removal from a 2-3 tree (case 2.4) with its inorder successor. In a 2-3 tree, the inorder successor of an internal element will always be a leaf, which, if it is a 2-node, will bring us back to our first case, and if it is a 3-node, requires no further action. Figure 12.11 illustrates these possibilities by removing the element 30 from our original tree from Figure 12.1 and then by removing the element 60 from the resulting tree. 45 30 22 35 60 82 35 40 51 55 75 87 22 40 51 55 after removing 30 45 45 75 82 40 60 82 initial tree 35 22 45 51 55 after removing 60 ? 22 87 55 82 35 87 75 40 51 after rotation FIG URE 1 2 .1 1 Removal from a 2-3 tree (case 3) 75 87 12.4 12.3 B-Trees 369 2-4 Trees KEY CON CEPT A 2-4 tree is similar to a 2-3 tree, adding the characteristic that a A 2-4 tree expands on the concept node can contain three elements. Expanding on the same principles of a 2-3 tree to include the use of as a 2-3 tree, a 4-node contains three elements and has either no chil4-nodes. dren or four children. The same ordering property applies: the left child will be less than the leftmost element of a node, which will be less than or equal to the second child of the node, which will be less than the second element of the node, which will be less than or equal to the third child of the node, which will be less than the third element of the node, which will be less than or equal to the fourth child of the node. The same cases for insertion and removal of elements apply, with 2-nodes and 3-nodes behaving similarly on insertion and 3-nodes and 4-nodes behaving similarly on removal. Figure 12.12 illustrates a series of insertions into a 2-4 tree. Figure 12.13 illustrates a series of removals from a 2-4 tree. 12.4 B-Trees KEY CON CEPT Both 2-3 and 2-4 trees are examples of a larger class of multi-way search trees called B-trees. We refer to the maximum number of children of each node as the order of the B-tree. Thus, 2-3 trees are order 3 B-trees, and 2-4 trees are order 4 B-trees. 25 3 14 55 3 14 insert 25 25 25 60 3 14 22 40 55 99 insert 22, 99 3 14 22 25 3 14 55 insert 3, 55, 14 40 55 A B-tree extends the concept of 2-3 and 2-4 trees so that nodes can have an arbitrary maximum number of elements. 40 55 insert 40 17 25 60 99 3 14 insert 60 FIG URE 1 2 .1 2 Insertions into a 2-4 tree 22 40 55 insert 17 99 370 C HA PT ER 12 Multi-way Search Trees 17 25 60 3 14 40 55 22 17 25 60 99 14 22 25 60 55 55 99 14 22 remove 3, 40 initial tree 14 25 60 55 99 remove 17 60 14 55 99 remove 22 55 60 99 remove 14, 99 remove 25 FIG URE 1 2 .13 Removals from a 2-4 tree B-trees of order m have the following properties: ■ The root has at least two subtrees unless it is a leaf. ■ Each non-root internal node n holds k–1 elements and k children where < m/2 = … k … m. ■ Each leaf n holds k-1 elements where < m/2 = … k … m. ■ All leaves are on the same level. Figure 12.14 illustrates a B-tree of order 6. K E Y CO N C E PT Access to secondary storage is very slow relative to access to primary storage, which is motivation to use structures such as B-trees. The reasoning behind the creation and use of B-trees is an interesting study of the effects of algorithm and data structure design. To understand this reasoning, we must understand the context of most all of the collections we have discussed thus far. Our assumption has always been that we were dealing with a collection in primary memory. However, what if the data set that we are manipulating is too 5 12 22 35 55 1 3 4 7 8 11 13 16 17 21 25 28 31 32 33 FIG URE 1 2 .1 4 A B-tree of order 6 40 43 60 75 80 12.4 large for primary memory? In that case, our data structure would be paged in and out of memory from disk or some other secondary storage device. An interesting thing happens to time complexity once a secondary storage device is involved. No longer is the time to access an element of the collection simply a function of how many comparisons are needed to find the element. Now we must also consider the access time of the secondary storage device and how many separate accesses we will make to that device. In the case of a disk, this access time consists of seek time (the time it takes to position the read-write head over the appropriate track on the disk), rotational delay (the time it takes to spin the disk to the correct sector), and the transfer time (the time it takes to transfer a block of memory from the disk into primary memory). Adding this “physical” complexity to the access time for a collection can be very costly. Access to secondary storage devices is very slow relative to access to primary storage. Given this added time complexity, it makes sense to develop a structure that minimizes the number of times the secondary storage device must be accessed. A B-tree can be just such a structure. B-trees are typically tuned so that the size of a node is the same as the size of a block on secondary storage. In this way, we get the maximum amount of data for each disk access. Because B-trees can have many more elements per node than a binary tree, they are much flatter structures than binary trees. This reduces the number of nodes and/or blocks that must be accessed, thus improving performance. We have already demonstrated the processes of insertion and removal of elements for 2-3 and 2-4 trees, both of which are B-trees. The process for any order m B-tree is similar. Let’s now briefly examine some interesting variations of B-trees that were designed to solve specific problems. B*-trees One of the potential problems with a B-tree is that although we are attempting to minimize access to secondary storage, we have actually created a data structure that may be half empty. To minimize this problem, B*-trees were developed. B*trees have all of the same properties as B-trees except that, instead of each node having k children where < m/2 = … k … m, in a B*-tree, each node has k children where < (2m–1)/3 = … k … m. This means that each non-root node is at least twothirds full. This is accomplished by delaying splitting of nodes by rebalancing across siblings. Once siblings are full, instead of splitting one node into two, creating two half-full nodes, we split two nodes into three, creating three two-thirds full nodes. B-Trees 371 372 C HA PT ER 12 Multi-way Search Trees B+-trees Another potential problem with B-trees is sequential access. As with any tree, we can use an inorder traversal to look at the elements of the tree sequentially. However, this means that we are no longer taking advantage of the blocking structure of secondary storage. In fact, we have made it much worse, because now we will access each block containing an internal node many separate times as we pass through it during the traversal. B+-trees provide a solution to this problem. In a B-tree, each element appears only once in the tree, regardless of whether it appears in an internal node or in a leaf. In a B+-tree, each element appears in a leaf, regardless of whether it appears in an internal node. Elements appearing in an internal node will be listed again as the inorder successor (which is a leaf) of their position in the internal node. Additionally, each leaf node will maintain a pointer to the following leaf node. In this way, a B+-tree provides indexed access through the B-tree structure and sequential access through a linked list of leaves. Figure 12.15 illustrates this strategy. Analysis of B-trees With balanced binary search trees, we were able to say that searching for an element in the tree was O(log2n). This is because, at worst, we had to search a single path from the root to a leaf in the tree and, at worst, the length of that path would be log2n. Analysis of B-trees is similar. At worst, searching a B-tree, we will have to search a single path from the root to a leaf and, at worst, that path length will be logmn, where m is the order of the B-tree and n is the number of elements in the tree. However, finding the appropriate node is only part of the search. The other part of the search is finding the appropriate path from each node and then finding the target element in a given node. Because there are up to m–1 elements per node, it may take up to m–1 comparisons per node to find the appropriate path and/or to find the appropriate element. Thus, the analysis of a search of a B-tree yields O((m–1)logmn). Because for any given implementation, m is a constant, we can say that searching a B-tree is O(log n). 5 12 22 35 55 1 3 4 5 7 8 12 16 17 21 22 28 31 32 FIG URE 1 2 .1 5 A B+-tree of order 6 35 40 43 55 60 80 12.5 Implementation Strategies for B-Trees 373 The analysis of insertion into and deletion from a B-tree is similar and is left as an exercise. 12.5 Implementation Strategies for B-Trees We have already discussed insertion of elements into B-trees, reKEY CON CEPT moval of elements from B-trees, and the balancing mechanisms necArrays may provide a better solution essary to maintain the properties of a B-tree. What remains is to disboth within a B-tree node and for cuss strategies for storing B-trees. Keep in mind that the B-tree collecting B-tree nodes because they structure was developed specifically to address the issue of a collecare effective in both primary memory and secondary storage. tion that must move in and out of primary memory from secondary storage. If we attempt to use object reference variables to create a linked implementation, we are actually storing a primary memory address for an object. Once that object is moved back to secondary storage, that address is no longer valid. Therefore, if interaction with secondary memory is part of your motivation to use a B-tree, then an array implementation may be a better solution. A solution is to think of each node as a pair of arrays. The first array would be an array of m–1 elements and the second array would be an array of m children. Next, if we think of the tree itself as one large array of nodes, then the elements stored in the array of children in each node would simply be integer indexes into this array of nodes. In primary memory, this strategy works because, using an array, as long as we know the index position of the element within the array, it does not matter to us where the array is loaded in primary memory. For secondary memory, this same strategy works because, given that each node is of fixed length, the address in memory of any given node is given by: The base address of the file + (index of the node – 1) * length of a node. The array implementations of 2-3, 2-4, and larger B-trees are left as a programming project. 374 C HA PT ER 12 Multi-way Search Trees Summary of Key Concepts ■ A multi-way search tree can have more than two children per node and can store more than one element in each node. ■ A 2-3 tree contains nodes that contain either one or two elements and have zero, two, or three children. ■ Inserting an element into a 2-3 tree can have a ripple effect up the tree. ■ If the propagation effect of a 2-3 tree insertion causes the root to split, the tree increases in height. ■ A 2-4 tree expands on the concept of a 2-3 tree to include the use of 4-nodes. ■ A B-tree extends the concept of 2-3 and 2-4 trees so that nodes can have an arbitrary maximum number of elements. ■ Access to secondary storage is very slow relative to access to primary storage, which is motivation to use structures such as B-trees. ■ Arrays may provide a better solution both within a B-tree node and for collecting B-tree nodes because they are effective in both primary memory and secondary storage. Self-Review Questions SR 12.1 Describe the nodes in a 2-3 tree. SR 12.2 When does a node in a 2-3 tree split? SR 12.3 How can splitting a node in a 2-3 tree affect the rest of the tree? SR 12.4 Describe the process of deleting an element from a 2-3 tree. SR 12.5 Describe the nodes in a 2-4 tree. SR 12.6 How do insertions and deletions in a 2-4 tree compare to insertions and deletions in a 2-3 tree? SR 12.7 When is rotation no longer an option for rebalancing a 2-3 tree after a deletion? Exercises EX 12.1 Draw the 2-3 tree that results from adding the following elements into an initially empty tree: 34 45 3 87 65 32 1 12 17 EX 12.2 Using the resulting tree from Exercise 12.1, draw the resulting tree after removing each of the following elements: 3 87 12 17 45 Answers to Self-Review Questions EX 12.3 Repeat Exercise 12.1 using a 2-4 tree. EX 12.4 Repeat Exercise 12.2 using the resulting 2-4 tree from Exercise 12.3. EX 12.5 Draw the order 8 B-tree that results from adding the following elements into an initially empty tree: 34 45 3 87 65 32 1 12 17 33 55 23 67 15 39 11 19 47 EX 12.6 Draw the B-tree that results from removing the following from the resulting tree from Exercise 12.5: 1 12 17 33 55 23 19 47 EX 12.7 Describe the complexity (order) of insertion into a B-tree. EX 12.8 Describe the complexity (order) of deletion from a B-tree. Programming Projects PP 12.1 Create an implementation of a 2-3 tree using the array strategy discussed in Section 12.5. PP 12.2 Create an implementation of a 2-3 tree using a linked strategy. PP 12.3 Create an implementation of a 2-4 tree using the array strategy discussed in Section 12.5. PP 12.4 Create an implementation of a 2-4 tree using a linked strategy. PP 12.5 Create an implementation of an order 7 B-tree using the array strategy discussed in Section 12.5. PP 12.6 Create an implementation of an order 9 B+-tree using the array strategy discussed in Section 12.5. PP 12.7 Create an implementation of an order 11 B*-tree using the array strategy discussed in Section 12.5. PP 12.8 Implement a graphical system to manage employees using an employee id, employee name, and years of service. The system should use an order 7 B-tree to store employees, and it must provide the ability to add and remove employees. After each operation, your system must update a sorted list of employees sorted by name on the screen. Answers to Self-Review Questions SRA 12.1 A 2-3 tree node can have either one element or two, and can have no children, two children, or three children. If it has one element, then it is a 2-node and has either no children or two children. If it 375 376 C HA PT ER 12 Multi-way Search Trees has two elements, then it is a 3-node and has either no children or three children. SRA 12.2 A 2-3 tree node splits when it has three elements. The smallest element becomes a 2-node, the largest element becomes a 2-node, and the middle element is promoted or propagated to the parent node. SRA 12.3 If the split and resulting propagation forces the root node to split, then it will increase the height of the tree. SRA 12.4 Deletion from a 2-3 tree falls into one of three cases. Case 1, deletion of an element from a 3-node leaf, means simply removing the element and has no impact on the rest of the tree. Case 2, deletion of an element from a 2-node leaf, results in one of four cases. Case 2.1, deletion of an element from a 2-node that has a 3-node sibling, is resolved by rotating either the inorder predecessor or inorder successor of the parent, depending upon whether the 3-node is a left child or a right child, around the parent. Case 2.2, deletion of an element from a 2-node when there is a 3-node leaf elsewhere in the tree, is resolved by rotating an element out of that 3-node and propagating that rotation until a sibling of the node being deleted becomes a 3-node; then this case becomes case 2.1. Case 2.3, deletion of a 2-node where there is a 3-node internal node, can be resolved through rotation as well. Case 2.4, deletion of a 2-node when there are no 3-nodes in the tree, is resolved by reducing the height of the tree. SRA 12.5 Nodes in a 2-4 tree are exactly like those of a 2-3 tree except that 2-4 trees also allow 4-nodes, or nodes containing three elements and having four children. SRA 12.6 Insertions and deletions in a 2-4 tree are exactly like those of a 2-3 tree except that splits occur when there are four elements instead of three as in a 2-3 tree. SRA 12.7 If all of the nodes in a 2-3 tree are 2-nodes, then rotation is not an option for rebalancing. References Bayer, R. “Symmetric Binary B-trees: Data Structure and Maintenance Algorithms.” Acta Informatica (1972): 290–306. Comer, D. “The Ubiquitous B-Tree.” Computing Surveys 11(1979): 121–137. Wedeking, H. “On the Selection of Access Paths in a Data Base System.” In Data Base Management, edited by J. W. Klimbie and K. L. Koffeman, 385–397. Amsterdam: North-Holland, 1974. 13 Graphs I n Chapter 9, we introduced the concept of a tree, a non- linear structure defined by the concept that each node in the tree, other than the root node, has exactly one parent. If we were to violate that premise and allow each node in the tree to be connected to a variety of other nodes with no notion of parent or child, the result would be the concept of a graph, which we explore in this chapter. Graphs and graph CHAPTER OBJECTIVES ■ Define undirected graphs ■ Define directed graphs ■ Define weighted graphs or networks ■ Explore common graph algorithms theory make up entire subdisciplines of both mathematics and computer science. In this chapter, we introduce the basic concepts of graphs and their implementation. 377 378 C HA PT ER 13 Graphs 13.1 Undirected Graphs Like trees, a graph is made up of nodes and the connections between those nodes. In graph terminology, we refer to the nodes as vertices and refer to the connections among them as edges. Vertices are typically referenced by a name or a label. For example, we might label vertices A, B, C, and D. Edges are referenced by a pairing of the vertices that they connect. For example, we KE Y CO N C E PT An undirected graph is a graph might have an edge (A, B), which means there is an edge from vertex where the pairings representing the A to vertex B. edges are unordered. An undirected graph is a graph where the pairings representing the edges are unordered. Thus, listing an edge as (A, B) means that there is a connection between A and B that can be traversed in either direction. In an undirected graph, listing an edge as (A, B) means exactly the same thing as listing the edge as (B, A). Figure 13.1 illustrates the following undirected graph: Vertices: A, B, C, D Edges: (A, B), (A, C), (B, C), (B, D), (C, D) K E Y CO N C E PT Two vertices in a graph are adjacent if there is an edge connecting them. KE Y CO N C E PT Two vertices in a graph are adjacent if there is an edge connecting them. For example, in the graph of Figure 13.1, vertices A and B are adjacent while vertices A and D are not. Adjacent vertices are sometimes referred to as neighbors. An edge of a graph that connects a vertex to itself is called a self-loop or a sling and is represented by listing the vertex twice. For example, listing an edge (A, A) would mean that there is a sling connecting A to itself. An undirected graph is considered complete if it has the maximum number of edges connecting vertices. For the first vertex, it requires (n–1) edges to connect it to the other vertices. For the second vertex, it requires only (n–2) edges because it is already connected to the first vertex. For the third vertex, it requires (n–3) edges. This sequence continues until the final vertex requires no additional edges because all the other vertices An undirected graph is considered complete if it has the maximum number of edges connecting vertices. A B D C FIG URE 1 3 . 1 An example undirected graph 13.1 Undirected Graphs have already been connected to it. Remember from Chapter 2 that the summation from 1 to n is: n a i = n(n + 1)/2 1 Thus, in this case, since we are only summing from 1 to (n–1), the resulting summation is: n-1 a i = n(n - 1)/2 1 This means that for any undirected graph with n vertices, it would require n(n–1)/2 edges to make the graph complete. This, of course, assumes that none of those edges are slings. A path is a sequence of edges that connect two vertices in a graph. For example, in our graph from Figure 13.1, A, B, D is a path from A to D. Notice that each sequential pair, (A, B) and then (B, D), is an edge. A path in an undirected graph is bi-directional. For example, A, B, D is the path KEY CON CEPT from A to D, but because the edges are undirected, the inverse, D, B, A path is a sequence of edges that A, is also the path from D to A. The length of a path is the number connects two vertices in a graph. of edges in the path (or the number of vertices – 1). So for our previous example, the path length is 2. Notice that this definition of path length is identical to the definition that we used in discussing trees. In fact, trees are a special case of graphs. KEY CON CEPT An undirected graph is considered connected if for any two vertices in the graph, there is a path between them. Our graph from Figure 13.1 is connected. The same graph with a minor modification is not connected, as illustrated in Figure 13.2. Vertices: A, B, C, D Edges: (A, B), (A, C), (B, C) A cycle is a path in which the first and last vertices are the same and none of the edges are repeated. A cycle is a path in which the first and last vertices are the same and none of the edges are repeated. In Figure 13.2, we would say that the path A, B, C, A is a cycle. A graph that has no cycles is called acyclic. Earlier we mentioned the A B D C F I GU R E 13 .2 An example undirected graph that is not connected 379 380 C HA PT ER 13 Graphs relationship between graphs and trees. Now that we have introduced these definitions, we can formalize that relationship. An undirected tree is a connected, acyclic, undirected graph with one element designated as the root. KE Y CO N C E PT An undirected tree is a connected, acyclic, undirected graph with one element designated as the root. 13.2 Directed Graphs A directed graph, sometimes referred to as a digraph, is a graph where the edges are ordered pairs of vertices. This means that the edges (A, B) and (B, A) are separate, directional edges in a directed graph. In our previous example, we had the following description for an undirected graph: K E Y C O N C E PT Vertices: A, B, C, D Edges: (A, B), (A, C), (B, C), (B, D), (C, D) Figure 13.3 shows what happens if we interpret this earlier description as a directed graph. We represent each of the edges now with the direction of traversal specified by the ordering of the vertices. For example, the edge (A, B) allows traversal from A to B but not the other direction. Our previous definitions change slightly for directed graphs. For example, a path in a directed graph is a sequence of directed edges that connects two vertices in a graph. In our undirected graph, we listed the path A, B, D, as the path from A to D, and that is still true in our directed interpretation of the graph description. However, paths in a directed graph are not bi-directional, so the inverse is no longer true: D, B, A is not a valid path from D to A unless we were to add directional edges (D, B) and (B, A). A directed graph, sometimes referred as a digraph, is a graph where the edges are ordered pairs of vertices. KE Y CO N C E PT Our definition for a connected directed graph sounds the same as it did for undirected graphs. A directed graph is connected if for any two vertices in the graph, there is a path between them. However, keep in mind that our definition of path is different. Look at the two graphs shown in Figure 13.4. The first one is connected. The second one, how- A path in a directed graph is a sequence of directed edges that connects two vertices in a graph. A B C D FIG URE 1 3. 3 An example directed graph 13.3 1 2 3 1 2 3 6 5 4 6 5 4 connected Networks 381 unconnected FI GU R E 13. 4 An example of connected and unconnected directed graphs ever, is not connected because there is no path from any other vertex to vertex 1. If a directed graph has no cycles, it is possible to arrange the vertices such that vertex A precedes vertex B if an edge exists from A to B. The order of vertices resulting from this arrangement is called topological order and is very useful for examples such as course prerequisites. As we discussed earlier, trees are graphs. In fact, most of our previous work with trees actually focused on directed trees. A directed tree is a directed graph that has an element designated as the root and has the following properties: ■ There are no connections from other vertices to the root. ■ Every non-root element has exactly one connection to it. ■ There is a path from the root to every other vertex. 13.3 Networks A network, or a weighted graph, is a graph with weights or costs associated with each edge. Figure 13.5 shows an undirected network of the connections and the airfares between cities. This weighted graph or network could then be used to determine the cheapest path from one city to another. The weight of a path in a weighted graph is the sum of the weights of the edges in the KEY CON CEPT path. A network, or a weighted graph, is a graph with weights or costs associated Networks may be either undirected or directed depending upon with each edge. the need. Take our airfare example from Figure 13.5. What if the airfare to fly from New York to Boston is one price but the airfare to fly from Boston to New York is a different price? This would be an excellent application of a directed network, as illustrated in Figure 13.6. For networks, we represent each edge with a triple including the starting vertex, ending vertex, and the weight. Keep in mind, for undirected networks, the starting and ending vertices could be swapped with no impact. However, for directed 382 C HA PT ER 13 Graphs Boston 120 219 New York Philadelphia 225 320 Roanoke FIG URE 1 3 . 5 An undirected network Boston 140 219 120 199 205 Philadelphia New York 225 240 320 Roanoke FIG URE 1 3 . 6 A directed network networks, a triple must be included for every directional connection. For example, the network of Figure 13.6 would be represented as follows: Vertices: Boston, New York, Philadelphia, Roanoke Edges: (Boston, New York, 120), (Boston, Philadelphia, 199), (New York, Boston, 140), (New York, Philadelphia, 225), (New York, Roanoke, 320), (Philadelphia, Boston, 219), (Philadelphia, New York, 205), (Roanoke, New York, 240) 13.4 Common Graph Algorithms There are a number of common graph algorithms that may apply to undirected graphs, directed graphs, and/or networks. These include various traversal algorithms similar to what we explored with trees, as well as algorithms for 13.4 Common Graph Algorithms 383 finding the shortest path, algorithms for finding the least costly path in a network, and algorithms to answer simple questions about the graph such as whether or not the graph is connected or what the shortest path is between two vertices. Traversals In our discussion of trees in Chapter 9, we defined four types of traversals and then implemented them as iterators: preorder traversal, inorder traversal, postorder traversal, and level-order traversal. Because we know that a tree is a graph, we know that for certain types of graphs these traversals would still apply. Generally, however, we divide graph traversal into two categories: a breadth-first traversal, which behaves very much like the level-order traversal of a tree, and a depth-first traversal, which behaves very much like the preorder traversal of a tree. One difference here is that there is not a root node. Thus our traversal may start at any vertex in the graph. We can construct a breadth-first traversal for a graph using a queue and an unordered list. We will use the queue (traversal-queue) to manage the traversal and the unordered list (result-list) to build our result. The first step is to enqueue the starting vertex into the traversal-queue and mark the starting vertex as visited. We then begin a loop that will continue until the traversal-queue is empty. Within this loop, we will take the first vertex off of the traversal-queue and add that vertex to the rear of the result-list. Next, we will enqueue each of the vertices that are adjacent to the current one, and have not already been marked as visited, into the traversal-queue, mark each of them as visited, and then repeat the loop. We simply repeat this process for each of the visited vertices until the traversal-queue is empty, meaning we can no longer reach any new vertices. The result-list now contains the vertices in breadth-first order from the given starting point. Very similar logic can be used to construct a breadth-first iterator. The iteratorBFS shows an iterative algorithm for this traversal for an array implementation of a graph. The determination of vertices that are adjacent to the current one depends upon the implementation we choose to represent edges in a graph. This particular method assumes an implementation using an adjacency matrix. We will discuss this further in Section 13.5. A depth-first traversal for a graph can be constructed using virtually the same logic by simply replacing the traversal-queue with a traversal-stack. One other difference in the algorithm, however, is that we do not want to mark a vertex as visited until it has been added to the result-list. The iteratorDFS method illustrates this algorithm for an array implementation of a graph. KEY CON CEPT The only difference between a depthfirst traversal of a graph and a breadth-first traversal is the use of a stack instead of a queue to manage the traversal. 384 C HA PT ER 13 Graphs /** * Returns an iterator that performs a breadth-first search * traversal starting at the given index. * * @param startIndex the index to begin the search from * @return an iterator that performs a breadth-first traversal */ public Iterator<T> iteratorBFS(int startIndex) { Integer x; LinkedQueue<Integer> traversalQueue = new LinkedQueue<Integer>(); ArrayUnorderedList<T> resultList = new ArrayUnorderedList<T>(); if (!indexIsValid(startIndex)) return resultList.iterator(); boolean[] visited = new boolean[numVertices]; for (int i = 0; i < numVertices; i++) visited[i] = false; traversalQueue.enqueue(new Integer(startIndex)); visited[startIndex] = true; while (!traversalQueue.isEmpty()) { x = traversalQueue.dequeue(); resultList.addToRear(vertices[x.intValue()]); /** Find all vertices adjacent to x that have not been visited and queue them up */ for (int i = 0; i < numVertices; i++) { if (adjMatrix[x.intValue()][i] && !visited[i]) { traversalQueue.enqueue(new Integer(i)); visited[i] = true; } } } return resultList.iterator(); } 13.4 Common Graph Algorithms /** * Returns an iterator that performs a depth-first search * traversal starting at the given index. * * @param startIndex the index to begin the search traversal from * @return an iterator that performs a depth-first traversal */ public Iterator<T> iteratorDFS(int startIndex) { Integer x; boolean found; LinkedStack<Integer> traversalStack = new LinkedStack<Integer>(); ArrayUnorderedList<T> resultList = new ArrayUnorderedList<T>(); boolean[] visited = new boolean[numVertices]; if (!indexIsValid(startIndex)) return resultList.iterator(); for (int i = 0; i < numVertices; i++) visited[i] = false; traversalStack.push(new Integer(startIndex)); resultList.addToRear(vertices[startIndex]); visited[startIndex] = true; while (!traversalStack.isEmpty()) { x = traversalStack.peek(); found = false; /** Find a vertex adjacent to x that has not been visited and push it on the stack */ for (int i = 0; (i < numVertices) && !found; i++) { if (adjMatrix[x.intValue()][i] && !visited[i]) { traversalStack.push(new Integer(i)); resultList.addToRear(vertices[i]); visited[i] = true; found = true; } } if (!found && !traversalStack.isEmpty()) traversalStack.pop(); } return resultList.iterator(); } 385 386 C HA PT ER 13 Graphs 1 2 3 4 5 7 8 6 9 FIG URE 1 3 . 7 A traversal example Let’s look at an example. Figure 13.7 shows a sample undirected graph where each vertex is labeled with an integer. For a breadth-first traversal starting from vertex 9, we do the following: 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. Add 9 to the traversal-queue and mark it as visited. Dequeue 9 from the traversal-queue. Add 9 on the result-list. Add 6, 7, and 8 to the traversal-queue, marking each of them as visited. Dequeue 6 from the traversal-queue. Add 6 on the result-list. Add 3 and 4 to the traversal-queue, marking them both as visited. Dequeue 7 from the traversal-queue and add it to the result-list. Add 5 to the traversal-queue, marking it as visited. Dequeue 8 from the traversal-queue and add it to the result-list. (We do not add any new vertices to the traversal-queue because there are no neighbors of 8 that have not already been visited.) Dequeue 3 from the traversal-queue and add it to the result-list. Add 1 to the traversal-queue, marking it as visited. Dequeue 4 from the traversal-queue and add it to the result-list. Add 2 to the traversal-queue, marking it as visited. Dequeue 5 from the traversal-queue and add it to the result-list. (Because there are no unvisited neighbors, we continue without adding anything to the traversal-queue.) Dequeue 1 from the traversal-queue and add it to the result-list. (Because there are no unvisited neighbors, we continue without adding anything to the traversal-queue.) Dequeue 2 from the traversal-queue and add it to the result-list. 13.4 Common Graph Algorithms 387 Thus, the result-list now contains the breadth-first order starting at vertex 9: 9, 6, 7, 8, 3, 4, 5, 1, and 2. Try tracing a depth-first search on the same graph from Figure 13.7. Of course, both of these algorithms could be expressed recursively. For example, the following algorithm recursively defines a depth-first search: DepthFirstSearch(node x) { visit(x) result-list.addToRear(x) for each node y adjacent to x if y not visited DepthFirstSearch(y) } Testing for Connectivity In our earlier discussion, we defined a graph as connected if for any two vertices in the graph, there is a path between them. This definition holds true for both undirected and directed graphs. Given the algorithm we just discussed, there is a simple solution to the question of whether or not a KEY CON CEPT graph is connected: The graph is connected if and only if for each A graph is connected if and only if vertex v in a graph containing n vertices, the size of the result of a the number of vertices in the breadth-first traversal is the same as breadth-first traversal starting at v is n. the number of vertices in the graph Let’s look at the example undirected graphs in Figure 13.8. We stated regardless of the starting vertex. earlier that the graph on the left is connected and that the graph on the right is not. Let’s confirm that by following our algorithm. Figure 13.9 shows the breadth-first traversals for the graph on the left using each of the vertices as a starting point. As you can see, all of the traversals yield n = 4 vertices, thus the graph is connected. Figure 13.10 shows the breadth-first traversals for the graph on the right using each of the vertices as a starting point. Notice that not only do none of the traversals contain n = 4 vertices, but the one starting at vertex D has only the one vertex. Thus the graph is not connected. A B A B D C D C F IG URE 1 3 .8 Connectivity in an undirected graph 388 C HA PT ER 13 Graphs Starting Vertex Breadth-First Traversal A A, B, C, D B B, A, D, C C C, B, A, D D D, B, A, C FIG URE 1 3 .9 Breadth-first traversal for a connected undirected graph Starting Vertex Breadth-First Traversal A A, B, C B B, A, C C C, B, A D D FIG URE 1 3 .1 0 Breadth-first traversal for an unconnected undirected graph Minimum Spanning Trees A spanning tree is a tree that includes all of the vertices of a graph and some, but possibly not all, of the edges. Because trees are also graphs, for some graphs, the graph itself will be a spanning tree, and thus the only spanning tree for that graph will include all of the edges. Figure 13.11 shows a KE Y CO N C E PT spanning tree for our graph from Figure 13.7. A spanning tree is a tree that includes all of the vertices of a graph and some, but possibly not all, of the edges. KE Y CO N C E PT A minimum spanning tree is a spanning tree where the sum of the weights of the edges is less than or equal to the sum of the weights for any other spanning tree for the same graph. One interesting application of spanning trees is to find a minimum spanning tree for a weighted graph. A minimum spanning tree is a spanning tree where the sum of the weights of the edges is less than or equal to the sum of the weights for any other spanning tree for the same graph. The algorithm for developing a minimum spanning tree was developed by Prim (1957) and is quite elegant. As we discussed earlier, each edge is represented by a triple including the starting vertex, ending vertex, and the weight. We then pick an arbitrary starting vertex (it does not matter which one) and add it to our minimum spanning tree (MST). Next we add all of the edges that 13.4 Common Graph Algorithms 1 2 3 4 5 7 8 6 9 FIG URE 1 3 .1 1 A spanning tree include our starting vertex to a minheap ordered by weight. Keep in mind that if we are dealing with a directed network, we will only add edges that start at the given vertex. Next we remove the minimum edge from the minheap and add the edge and the new vertex to our MST. Next we add to our minheap all of the edges that include this new vertex and whose other vertex is not already in our MST. We continue this process until either our MST includes all of the vertices in our original graph or the minheap is empty. Figure 13.12 shows a weighted network and its associated minimum spanning tree. The getMST method illustrates this algorithm. 12 1 1 2 3 2 6 8 3 4 5 6 8 4 5 11 1 1 3 Network 3 Minimum Spanning Tree FIG URE 1 3 .1 2 A minimum spanning tree 389 390 C HA PT ER 13 Graphs /** * Returns a minimum spanning tree of the network. * * @return a minimum spanning tree of the network */ public Network mstNetwork() { int x, y; int index; double weight; int[] edge = new int[2]; Heap<Double> minHeap = new Heap<Double>(); Network<T> resultGraph = new Network<T>(); if (isEmpty() || !isConnected()) return resultGraph; resultGraph.adjMatrix = new double[numVertices][numVertices]; for (int i = 0; i < numVertices; i++) for (int j = 0; j < numVertices; j++) resultGraph.adjMatrix[i][j] = Double.POSITIVE_INFINITY; resultGraph.vertices = (T[])(new Object[numVertices]); boolean[] visited = new boolean[numVertices]; for (int i = 0; i < numVertices; i++) visited[i] = false; edge[0] = 0; resultGraph.vertices[0] = this.vertices[0]; resultGraph.numVertices++; visited[0] = true; /** Add all edges, which are adjacent to the starting vertex, to the heap */ for (int i = 0; i < numVertices; i++) minHeap.addElement(new Double(adjMatrix[0][i])); while ((resultGraph.size() < this.size()) && !minHeap.isEmpty()) { /** Get the edge with the smallest weight that has exactly one vertex already in the resultGraph */ do { 13.4 Common Graph Algorithms weight = (minHeap.removeMin()).doubleValue(); edge = getEdgeWithWeightOf(weight, visited); } while (!indexIsValid(edge[0]) || !indexIsValid(edge[1])); x = edge[0]; y = edge[1]; if (!visited[x]) index = x; else index = y; /** Add the new edge and vertex to the resultGraph */ resultGraph.vertices[index] = this.vertices[index]; visited[index] = true; resultGraph.numVertices++; resultGraph.adjMatrix[x][y] = this.adjMatrix[x][y]; resultGraph.adjMatrix[y][x] = this.adjMatrix[y][x]; /** Add all edges, that are adjacent to the newly added vertex, to the heap */ for (int i = 0; i < numVertices; i++) { if (!visited[i] && (this.adjMatrix[i][index] < Double.POSITIVE_INFINITY)) { edge[0] = index; edge[1] = i; minHeap.addElement(new Double(adjMatrix[index][i])); } } } return resultGraph; } Determining the Shortest Path There are two possibilities for determining the “shortest” path in a graph. The first, and perhaps simplest, possibility is to determine the literal shortest path between a starting vertex and a target vertex, meaning the least number of edges between the two vertices. This turns out to be a simple variation of our earlier breadth-first traversal algorithm. To convert this algorithm to find the shortest path, we simply store two additional pieces of information for each vertex during our traversal: the path length 391 392 C HA PT ER 13 Graphs from the starting vertex to this vertex, and the vertex that is the predecessor of this vertex in that path. Then we modify our loop to terminate when we reach our target vertex. The path length for the shortest path is simply the path length to the predecessor of the target + 1, and if we wish to output the vertices along the shortest path, we can simply backtrack along the chain of predecessors. The second possibility for determining the shortest path is to look for the cheapest path in a weighted graph. Dijkstra (1959) developed an algorithm for this possibility that is similar to our previous algorithm. However, instead of using a queue of vertices that causes us to progress through the graph in the order we encounter vertices, we use a minheap or a priority queue storing vertex and weight pairs based upon total weight (the sum of the weights from the starting vertex to this vertex) so that we always traverse through the graph following the cheapest path first. For each vertex, we must store the label of the vertex, the weight of the cheapest path (thus far) to that vertex from our starting point, and the predecessor of that vertex along that path. On the minheap, we will store vertex and weight pairs for each possible path that we have encountered but not yet traversed. As we remove a vertex, weight pair from the minheap, if we encounter a vertex with a weight less than the one already stored with the vertex, we update the cost. 13.5 Strategies for Implementing Graphs Let us begin our discussion of implementation strategies by examining what operations would need to be available for a graph. Of course, we would need to be able to add and remove vertices, and add and remove edges from the graph. There will need to be traversals (perhaps breadth first and depth first) beginning with a particular vertex, and these might be implemented as iterators, as we did for binary trees. Other operations like size, isEmpty, toString, and find will be useful as well. In addition to these, operations to determine the shortest path from a particular vertex to a particular target vertex, to determine the adjacency of two vertices, to construct a minimum spanning tree, and to test for connectivity would all likely need to be implemented. Whatever storage mechanism we use for vertices must allow us to mark vertices as visited during traversals and other algorithms. This can be accomplished by simply adding a Boolean variable to the class representing the vertices. Adjacency Lists Because trees are graphs, perhaps the best introduction to how we might implement graphs is to consider the discussions and examples that we have already seen concerning the implementation of trees. One might immediately think of using a 13.5 Strategies for Implementing Graphs set of nodes where each node contains an element and n–1 links to other nodes. When we used this strategy with trees, the number of connections from any given node was limited by the order of the tree (e.g., a maximum of two directed edges starting at any particular node in a binary tree). Because of this limitation, we were able to specify, for example, that a binary node had a left and a right child pointer. Even if the binary node was a leaf, the pointer still existed. It was simply set to null. In the case of a graph node, because each node could have up to n–1 edges connecting it to other nodes, it would be better to use a dynamic structure such as a linked list to store the edges within each node. This list is called an adjacency list. In the case of a network or weighted graph, each edge would be stored as a triple including the weight. In the case of an undirected graph, an edge (A, B) would appear in the adjacency list of both vertex A and vertex B. Adjacency Matrices Keep in mind that we must somehow efficiently (both in terms of space and access time) store both vertices and edges. Because vertices are just elements, we can use any of our collections to store the vertices. In fact, we often talk about a “set of vertices,” the term set implying an implementation strategy. However, another solution for storing edges is motivated by our use of array implementations of trees, but instead of using a one-dimensional array, we will use a two-dimensional array that we call an adjacency matrix. In an adjacency matrix, each position of the two-dimensional array represents an intersection between two vertices in the graph. Each of these intersections is represented by a Boolean value indicating whether or not the two vertices are connected. Figure 13.13 shows the undirected graph that we began with at the beginning of this chapter. Figure 13.14 shows the adjacency matrix for this graph. For any position (row, column) in the matrix, that position is true if and only if the edge (vrow, vcolumn) is in the graph. Because edges in an undirected graph are bi-directional, if (A, B) is an edge in the graph, then (B, A) is also in the graph. A B D C FIG URE 1 3 .1 3 An undirected graph 393 394 C HA PT ER 13 Graphs A B C D A F T T F B T F T T C T T F F D F T F F FIG URE 1 3 .1 4 An adjacency matrix for an undirected graph Notice that this matrix is symmetrical—that is, each side of the diagonal is a mirror image of the other. The reason for this is that we are representing an undirected graph. For undirected graphs, it may not be necessary to represent the entire matrix but simply one side or the other of the diagonal. However, for directed graphs, because all of the edges are directional, the result can be quite different. Figure 13.15 shows a directed graph, and Figure 13.16 shows the adjacency matrix for this graph. Adjacency matrices may also be used with networks or weighted graphs by simply storing an object at each position of the matrix to represent the weight of the edge. Positions in the matrix where edges do not exist would simply be set to null. A B D C FIG UR E 1 3 . 1 5 A directed graph A B C D A F T T F B F F T T C F F F F D F F F F FIG URE 1 3 .1 6 An adjacency matrix for a directed graph 1 3 .6 13.6 Implementing Undirected Graphs with an Adjacency Matrix Implementing Undirected Graphs with an Adjacency Matrix Like the other collections we have discussed, the first step in implementing a graph is to determine its interface. Listing 13.1 illustrates the GraphADT interface. Listing 13.2 illustrates the NetworkADT interface that extends the GraphADT interface. Note that our interfaces include methods to add and remove vertices, add and remove edges, iterators for both breadth-first and depth-first traversals, methods to determine the shortest path between two vertices and to determine if the graph is connected, and our usual collection of methods to determine the size of the collection, determine if it is empty, and return a string representation of it. L I S T I N G 1 3 . 1 /** * GraphADT defines the interface to a graph data structure. * * @author Dr. Chase * @author Dr. Lewis * @version 1.0, 9/17/2008 */ package jss2; import java.util.Iterator; public interface GraphADT<T> { /** * Adds a vertex to this graph, associating object with vertex. * * @param vertex the vertex to be added to this graph */ public void addVertex (T vertex); /** * Removes a single vertex with the given value from this graph. * * @param vertex the vertex to be removed from this graph */ public void removeVertex (T vertex); /** * Inserts an edge between two vertices of this graph. 395 396 C HA PT ER 13 L I S T I N G Graphs 1 3 . 1 continued * * @param vertex1 the first vertex * @param vertex2 the second vertex */ public void addEdge (T vertex1, T vertex2); /** * Removes an edge between two vertices of this graph. * * @param vertex1 the first vertex * @param vertex2 the second vertex */ public void removeEdge (T vertex1, T vertex2); /** * Returns a breadth first iterator starting with the given vertex. * * @param startVertex the starting vertex * @return a breadth first iterator beginning at the given * vertex */ public Iterator iteratorBFS(T startVertex); /** * Returns a depth first iterator starting with the given vertex. * * @param startVertex the starting vertex * @return a depth first iterator starting at the given vertex */ public Iterator iteratorDFS(T startVertex); /** * Returns an iterator that contains the shortest path between * the two vertices. * * @param startVertex the starting vertex * @param targetVertex the ending vertex * @return an iterator that contains the shortest path * between the two vertices */ public Iterator iteratorShortestPath(T startVertex, T targetVertex); 1 3 .6 L I S T I N G 1 3 . 1 Implementing Undirected Graphs with an Adjacency Matrix continued /** * Returns true if this graph is empty, false otherwise. * * @return true if this graph is empty */ public boolean isEmpty(); /** * Returns true if this graph is connected, false otherwise. * * @return true if this graph is connected */ public boolean isConnected(); /** * Returns the number of vertices in this graph. * * @return the integer number of vertices in this graph */ public int size(); /** * Returns a string representation of the adjacency matrix. * * @return a string representation of the adjacency matrix */ public String toString(); } L I S T I N G 1 3 . 2 /** * NetworkADT defines the interface to a network. * * @author Dr. Lewis * @author Dr. Chase * @version 1.0, 9/17/2008 */ 397 398 C HA PT ER 13 L I S T I N G Graphs 1 3 . 2 continued package jss2; import java.util.Iterator; public interface NetworkADT<T> extends GraphADT<T> { /** * Inserts an edge between two vertices of this graph. * * @param vertex1 the first vertex * @param vertex2 the second vertex * @param weight the weight */ public void addEdge (T vertex1, T vertex2, double weight); /** * Returns the weight of the shortest path in this network. * * @param vertex1 the first vertex * @param vertex2 the second vertex * @return the weight of the shortest path in this network */ public double shortestPathWeight(T vertex1, T vertex2); } Of course, this interface could be implemented a variety of ways, but we will focus our discussion on an adjacency matrix implementation. The other implementations of undirected graphs and networks as well as the implementations of directed graphs and networks are left as programming projects. The header and instance data for our implementation are presented to provide context. Note that the adjacency matrix is represented by a two-dimensional Boolean array. /** * Graph represents an adjacency matrix implementation of a graph. * * @author Dr. Lewis * @author Dr. Chase * @version 1.0, 9/16/2008 */ 1 3 .6 Implementing Undirected Graphs with an Adjacency Matrix package jss2; import jss2.exceptions.*; import java.util.*; public class { protected protected protected protected Graph<T> implements GraphADT<T> final int DEFAULT_CAPACITY = 10; int numVertices; // number of vertices in the graph boolean[][] adjMatrix; // adjacency matrix T[] vertices; // values of vertices Our constructor simply initializes the number of vertices to zero, constructs the adjacency matrix, and sets up an array of generic objects (T[]) to represent the vertices. /** * Creates an empty graph. */ public Graph() { numVertices = 0; this.adjMatrix = new boolean[DEFAULT_CAPACITY][DEFAULT_CAPACITY]; this.vertices = (T[])(new Object[DEFAULT_CAPACITY]); } The addEdge Method Once we have established our list of vertices and our adjacency matrix, adding an edge is simply a matter of setting the appropriate locations in the adjacency matrix to true. Our addEdge method uses the getIndex method to locate the proper indices and calls a different version of the addEdge method to make the assignments if the indices are valid. /** * Inserts an edge between two vertices of the graph. * * @param vertex1 the first vertex * @param vertex2 the second vertex */ 399 400 C HA PT ER 13 Graphs public void addEdge (T vertex1, T vertex2) { addEdge (getIndex(vertex1), getIndex(vertex2)); } /** * Inserts an edge between two vertices of the graph. * * @param index1 the first index * @param index2 the second index */ public void addEdge (int index1, int index2) { if (indexIsValid(index1) && indexIsValid(index2)) { adjMatrix[index1][index2] = true; adjMatrix[index2][index1] = true; } } The addVertex Method Adding a vertex to the graph involves adding the vertex in the next available position in the array and setting all of the appropriate locations in the adjacency matrix to false. /** * Adds a vertex to the graph, expanding the capacity of the graph * if necessary. It also associates an object with the vertex. * * @param vertex the vertex to add to the graph */ public void addVertex (T vertex) { if (numVertices == vertices.length) expandCapacity(); vertices[numVertices] = vertex; for (int i = 0; i <= numVertices; i++) 1 3 .6 Implementing Undirected Graphs with an Adjacency Matrix { adjMatrix[numVertices][i] = false; adjMatrix[i][numVertices] = false; } numVertices++; } The expandCapacity Method The expandCapacity method for our adjacency matrix implementation of a graph is more interesting than the similar method in our other array implementations. It is no longer just a case of expanding one array and copying the contents. Keep in mind that for our graph, we must not only expand the array of vertices and copy the existing vertices into the new array; we must also expand the capacity of the adjacency list and copy the old contents into the new list. /** * Creates new arrays to store the contents of the graph with * twice the capacity. */ protected void expandCapacity() { T[] largerVertices = (T[])(new Object[vertices.length*2]); boolean[][] largerAdjMatrix = new boolean[vertices.length*2][vertices.length*2]; for (int i = 0; i < numVertices; i++) { for (int j = 0; j < numVertices; j++) { largerAdjMatrix[i][j] = adjMatrix[i][j]; } largerVertices[i] = vertices[i]; } vertices = largerVertices; adjMatrix = largerAdjMatrix; } Other Methods The remaining methods for our graph implementation are left as programming projects. 401 402 C HA PT ER 13 Graphs Summary of Key Concepts ■ An undirected graph is a graph where the pairings representing the edges are unordered. ■ Two vertices in a graph are adjacent if there is an edge connecting them. ■ An undirected graph is considered complete if it has the maximum number of edges connecting vertices. ■ A path is a sequence of edges that connects two vertices in a graph. ■ A cycle is a path in which the first and last vertices are the same and none of the edges are repeated. ■ An undirected tree is a connected, acyclic, undirected graph with one element designated as the root. ■ A directed graph, sometimes referred as a digraph, is a graph where the edges are ordered pairs of vertices. ■ A path in a directed graph is a sequence of directed edges that connects two vertices in a graph. ■ A network, or a weighted graph, is a graph with weights or costs associated with each edge. ■ The only difference between a depth-first traversal of a graph and a breadthfirst traversal is the use of a stack instead of a queue to manage the traversal. ■ A graph is connected if and only if the number of vertices in the breadth-first traversal is the same as the number of vertices in the graph regardless of the starting vertex. ■ A spanning tree is a tree that includes all of the vertices of a graph and some, but possibly not all, of the edges. ■ A minimum spanning tree is a spanning tree where the sum of the weights of the edges is less than or equal to the sum of the weights for any other spanning tree for the same graph. Self-Review Questions SR 13.1 What is the difference between a graph and a tree? SR 13.2 What is an undirected graph? SR 13.3 What is a directed graph? SR 13.4 What does it mean to say that a graph is complete? Programming Projects SR 13.5 What is the maximum number of edges for an undirected graph and the maximum number of edges for a directed graph? SR 13.6 What is the definition of path and the definition of a cycle? SR 13.7 What is the difference between a network and a graph? SR 13.8 What is a spanning tree? A minimum spanning tree? Exercises EX 13.1 Draw the undirected graph that is represented by the following: vertices: 1, 2, 3, 4, 5, 6, 7 edges: (1, 2), (1, 4), (2, 3), (2, 4), (3, 7), (4, 7), (4, 6), (5, 6), (5, 7), (6, 7) EX 13.2 Is the graph from Exercise 13.1 connected? Is it complete? EX 13.3 List all of the cycles in the graph from Exercise 13.1. EX 13.4 Draw a spanning tree for the graph of Exercise 13.1. EX 13.5 Using the same data from Exercise 13.1, draw the resulting directed graph. EX 13.6 Is the directed graph of Exercise 13.5 connected? Is it complete? EX 13.7 List all of the cycles in the graph of Exercise 13.5. EX 13.8 Draw a spanning tree for the graph of Exercise 13.5. EX 13.9 Consider the weighted graph shown in Figure 13.10. List all of the possible paths from vertex 2 to vertex 3 along with the total weight of each path. Programming Projects PP 13.1 Implement an undirected graph using an adjacency list. Keep in mind that you must store both vertices and edges. Your implementation must implement the GraphADT interface. PP 13.2 Repeat Programming Project 13.1 for a directed graph. PP 13.3 Complete the implementation of a graph using an adjacency matrix that was presented in this chapter. PP 13.4 Extend the adjacency matrix implementation presented in this chapter to create an implementation of a weighted graph or network. 403 404 C HA PT ER 13 Graphs PP 13.5 Extend the adjacency matrix implementation presented in this chapter to create a directed graph. PP 13.6 Extend your implementation from Programming Project 13.1 to create a weighted, undirected graph. PP 13.7 Create a limited airline scheduling system that will allow a user to enter city to city connections and their prices. Your system should then allow a user to enter two cities and should return the shortest path and the cheapest path between the two cities. Your system should report if there is no connection between two cities. Assume an undirected network. PP 13.8 Repeat Programming Project 13.7 assuming a directed network. PP 13.9 Create a simple graphical application that will produce a textual representation of the shortest path and the cheapest path between two vertices in a network. PP 13.10 Create a network routing system that, given the point-to-point connections in the network and the costs of utilizing each, will produce cheapest-path connections from each point to each point in the network, pointing out any disconnected locations. Answers to Self-Review Questions SRA 13.1 A graph is the more general concept without the restriction that each node have one and only one parent except for the root, which does not have a parent. In the case of a graph, there is no root, and each vertex can be connected to up to n–1 other vertices. SRA 13.2 An undirected graph is a graph where the pairings representing the edges are unordered. SRA 13.3 A directed graph, sometimes referred as a digraph, is a graph where the edges are ordered pairs of vertices. SRA 13.4 A graph is considered complete if it has the maximum number of edges connecting vertices. SRA 13.5 The maximum number of edges for an undirected graph is n(n–1)/2. For a directed graph, it is n(n–1). SRA 13.6 A path is a sequence of edges that connects two vertices in a graph. A cycle is a path in which the first and last vertices are the same and none of the edges are repeated. References SRA 13.7 A network is a graph, either directed or undirected, with weights or costs associated with each edge. SRA 13.8 A spanning tree is a tree that includes all of the vertices of a graph and some, but possibly not all, of the edges. A minimum spanning tree is a spanning tree where the sum of the weights of the edges is less than or equal to the sum of the weights for any other spanning tree for the same graph. References Collins, W. J. Data Structures: An Object-Oriented Approach. Reading, Mass.: Addison-Wesley, 1992. Dijkstra, E. W. “A Note on Two Problems in Connection with Graphs.” Numerische Mathematik 1 (1959): 269–271. Drosdek, A. Data Structures and Algorithms in Java. Pacific Grove, Cal.: Brooks/Cole, 2001. Prim, R. C. “Shortest Connection Networks and Some Generalizations.” Bell System Technical Journal 36 (1957): 1389–1401. 405 This page intentionally left blank 14 Hashing I n Chapter 10, we discussed the idea that a binary search tree is, in effect, an efficient implementation of a set or a map. In this chapter, we examine hashing, an approach to CHAPTER OBJECTIVES ■ Define hashing ■ Examine various hashing functions ■ Examine the problem of collisions in hash tables ■ Explore the Java Collections API implementations of hashing implementing a set or map collection that can be even more efficient than binary search trees. 407 408 C HA PT ER 14 Hashing 14.1 A Hashing In all of our discussions of the implementations of collections, we have proceeded with one of two assumptions about the order of elements in a collection: ■ Order is determined by the order in which elements are added to and/or removed from our collection, as in the case of stacks, queues, unordered lists, and indexed lists. ■ Order is determined by comparing the values of the elements (or some key component of the elements) to be stored in the collection, as in the case of ordered lists and binary search trees. In this chapter, we will explore the concept of hashing, which means that the order—and, more specifically, the location of an item within the collection—is determined by some function of the value of the element to be stored, or some function of a key value of the element to be stored. In hashing, elements are stored in a hash table, with their location in the table deK E Y CO N C E PT termined by a hashing function. Each location in the table may be reIn hashing, elements are stored in ferred to as a cell or a bucket. We will discuss hashing functions a hash table, with their location in the table determined by a hashing further in Section 14.2. We will discuss implementation strategies function. and algorithms, and we will leave the implementations as programming projects. Consider a simple example where we create an array that will hold 26 elements. Wishing to store names in our array, we create a hashing function that equates each name to the position in the array associated with the first letter of the name (e.g., a first letter of A would be mapped to position 0 of the array, a first letter of D would be mapped to position 3 of the array, and so on). Figure 14.1 illustrates this scenario after several names have been added. Notice that unlike our earlier implementations of collections, using a hashing approach results in the access time to a particular eleK E Y CO N C E PT ment being independent of the number of elements in the table. This The situation where two elements or keys map to the same location in the means that all of the operations on an element of a hash table should table is called a collision. be O(1). This is the result of no longer having to do comparisons to find a particular element or to locate the appropriate position for a given element. Using hashing, we simply calculate where a particular element should be. K E Y CO N C E PT A hashing function that maps each element to a unique position in the table is said to be a perfect hashing function. However, this efficiency is only fully realized if each element maps to a unique position in the table. Consider our example from Figure 14.1. What will happen if we attempt to store the name “Ann” and the name “Andrew”? This situation, where two elements or keys map to the same location in the table, is called a collision. We will discuss how to resolve collisions in Section 14.3. 14.1 Ann Doug Elizabeth Hal Mary Tim Walter Young FIG URE 1 4 .1 A simple hashing example A hashing function that maps each element to a unique position in the table is said to be a perfect hashing function. Although it is possible in some situations to develop a perfect hashing function, a hashing function that does a good job of distributing the elements among the table positions will still result in constant time (O(1)) access to elements in the table and an improvement over our earlier algorithms that were either O(n) in the case of our linear approaches or O(log n) in the case of search trees. Another issue surrounding hashing is the question of how large the table should be. If the data set is of known size and a perfect hashing function can be used, then we simply make the table the same size as the data set. If a perfect hashing function is not available or practical but the size of the data set is known, a good rule of thumb is to make the table 150 percent the size of the data set. A Hashing 409 410 C HA PT ER 14 Hashing The third case is very common and far more interesting. What if we do not know the size of the data set? In this case, we depend on dynamic resizing. Dynamic resizing of a hash table involves creating a new hash table that is larger than, perhaps even twice as large as, the original, inserting all of the elements of the original table into the new table, and then discarding the original table. Deciding when to resize is also an interesting question. One possibility is to use the same method we used with our earlier array implementations and simply expand the table when it is full. However, it is the nature of hash tables that their performance seriously degrades as they become full. A better approach is to use a load factor. The load factor of a hash table is the percentage occupancy of the table at which the table will be resized. For example, if the load factor were set to 0.50, then the table would be resized each time it reached 50 percent capacity. 14.2 K E Y CO N C E PT Extraction involves using only a part of the element’s value or key to compute the location at which to store the element. Hashing Functions Although perfect hashing functions are possible if the data set is known, we do not need the hashing function to be perfect to get good performance from the hash table. Our goal is simply to develop a function that does a reasonably good job of distributing our elements in the table such that we avoid collisions. A reasonably good hashing function will still result in constant time access (O(1)) to our data set. There are a variety of approaches to developing a hashing function for a particular data set. The method that we used in our example in the previous section is called extraction. Extraction involves using only a part of the element’s value or key to compute the location at which to store the element. In our previous example, we simply extracted the first letter of a string and computed its value relative to the letter A. Other examples of extraction would be to store phone numbers according to the last four digits or to store information about cars according to the first three characters of the license plate. The Division Method Creating a hashing function by division simply means we will use the remainder of the key divided by some positive integer p as the index for the given element. This function could be defined as follows: Hashcode(key) ⫽ Math.abs(key)%p This function will yield a result in the range from 0 to p–1. If we use our table size as p, we then have an index that maps directly to a location in the table. 14.2 Hashing Functions 411 Using a prime number p as the table size and the divisor helps provide a better distribution of keys to locations in the table. For example, if our key value is 79 and our table size is 43, the division method would result in an index value of 36. The division method is very effective when dealing with an unknown set of key values. The Folding Method In the folding method, the key is divided into parts that are then combined or folded together to create an index into the table. This is done by first dividing the key into parts where each of the parts of the key will be the same length as the desired index, except possibly the last one. In the shift folding method, these parts are then added together to create the index. For example, KEY CON CEPT if our key is the Social Security number 987-65-4321, we might diIn the shift folding method, the parts of the key are added together to crevide this into three parts, 987, 654, and 321. Adding these together ate the index. yields 1962. Assuming we are looking for a three-digit key, at this point we could use either division or extraction to get our index. A second possibility is boundary folding. There are a number of variations on this approach. However, generally, they involve reversing some of the parts of the key before adding. One variation on this approach is to imagine that the parts of the key are written side by side on a piece of paper and that the piece of paper is folded along the boundaries of the parts of the key. In this way, if we begin with the same key, 987-65-4321, we first divide it into parts, 987, 654, and 321. We then reverse every other part of the key, yielding 987, 456, and 321. Adding these together yields 1764 and once again we can proceed with either extraction or division to get our index. Other variations on folding use different algorithms to determine which parts of the key to reverse. Folding may also be a useful method for building a hashing function for a key that is a string. One approach to this is to divide the string into substrings the same length (in bytes) as the desired index and then combine these strings using an exclusive-or function. This is also a useful way to convert a string to a number so that other methods, such as division, may be applied to strings. The Mid-Square Method In the mid-square method, the key is multiplied by itself, and then the extraction method is used to extract the appropriate number of digits from the middle of the squared result to serve as an index. The same “middle” digits must be chosen each time, to provide consistency. For example, if our key is 4321, we would multiply the key by itself, yielding 18671041. Assuming that we need a three-digit 412 C HA PT ER 14 Hashing key, we might extract 671 or 710, depending upon how we construct our algorithm. It is also possible to extract bits instead of digits and then construct the index from the extracted bits. The mid-square method may also be effectively used with strings by manipulating the binary representations of the characters in the string. The Radix Transformation Method In the radix transformation method, the key is transformed into another numeric base. For example, if our key is 23 in base 10, we might convert it to 32 in base 7. We then use the division method and divide the converted key by the table size and use the remainder as our index. Continuing our previous example, if our table size is 17, we would compute the function: Hashcode(23) = Math.abs(32)%17 = 15 The Digit Analysis Method In the digit analysis method, the index is formed by extracting, and then manipulating, specific digits from the key. For example, if our key is 1234567, we might select the digits in positions 2 through 4, yielding 234, and then manipulate them to form our index. This manipulation can take many forms, including simply reversing the digits (yielding 432), performing a circular shift to the right (yielding 423), performing a circular shift to the left (yielding 342), swapping each pair of digits (yielding 324), or any number of other possibilities, including the methods we have already discussed. The goal is simply to provide a function that does a reasonable job of distributing keys to locations in the table. The Length-Dependent Method In the length-dependent method, the key and the length of the key are combined in some way to form either the index itself or an intermediate value that is then used with one of our other methods to form the index. For example, if our key is 8765, we might multiply the first two digits by the length K E Y CO N C E PT and then divide by the last digit, yielding 69. If our table size is 43, The length-dependent method and the mid-square method may also be we would then use the division method, resulting in an index of 26. effectively used with strings by manipulating the binary representations of the characters in the string. The length-dependent method may also be effectively used with strings by manipulating the binary representations of the characters in the string. 14.3 Resolving Collisions 413 Hashing Functions in the Java Language The java.lang.Object class defines a method called hashcode that returns an integer based on the memory location of the object. This is generally not very useful. Classes that are derived from Object often override the inherited definition of hashcode to provide their own version. For exKEY CON CEPT ample, the String and Integer classes define their own hashcode Although Java provides a hashcode methods. These more specific hashcode functions can be very effecmethod for all objects, it is often preferable to define a specific hashtive for hashing. By having the hashcode method defined in the ing function for any particular class. Object class, all Java objects can be hashed. However, it is also possible, and often preferable, to define your own hashcode method for any class that you intend to store in a hash table. 14.3 Resolving Collisions If we are able to develop a perfect hashing function for a particular data set, then we do not need to concern ourselves with collisions, the situation where more than one element or key map to the same location in the table. However, when a perfect hashing function is not possible or practical, there are a number of ways to handle collisions. Similarly, if we are able to develop a perfect hashing function for a particular data set, then we do not need to concern ourselves with the size of the table. In this case, we will simply make the table the exact size of the data set. Otherwise, if the size of the data set is known, it is generally a good idea to set the initial size of the table to about 150 percent of the expected element count. If the size of the data set is not known, then dynamic resizing of the table becomes an issue. Chaining The chaining method for handling collisions simply treats the hash table conceptually as a table of collections rather than as a table of individual cells. Thus each cell is a pointer to the collection associated with that location in the table. Usually this internal collection is either an unordered list or an ordered list. Figure 14.2 illustrates this conceptual approach. KEY CON CEPT The chaining method for handling collisions simply treats the hash table conceptually as a table of collections rather than as a table of individual cells. Chaining can be implemented in a variety of ways. One approach would be to make the array holding the table larger than the number of cells in the table and use the extra space as an overflow area to store the linked lists associated with each table location. In this method, each position in the array could store both an element (or a key) and the array index of the next 414 C HA PT ER 14 Hashing FIG URE 1 4 .2 The chaining method of collision handling element in its list. The first element mapped to a particular location in the table would actually be stored in that location. The next element mapped to that location would be stored in a free location in this overflow area, and the array index of this second element would be stored with the first element in the table. If a third element is mapped to the same location, the third element would also be stored in this overflow area and the index of third element would be stored with the second element. Figure 14.3 illustrates this strategy. Note that, using this method, the table itself can never be full. However, if the table is implemented as an array, the array can become full, requiring a decision on whether to throw an exception or simply expand capacity. In our earlier collections, we chose to expand the capacity of the array. In this case, expanding the capacity of the array but leaving the embedded table the original size would have disastrous effects on efficiency. A more complete solution is to expand the array and expand the embedded table within the array. This will, however, require that all of the elements in the table be rehashed using the new table size. We will discuss the dynamic resizing of hash tables further in Section 14.5. Using this method, the worst case is that our hashing function will not do a good job of distributing elements to locations in the table so that we end up with one linked list of n elements, or a small number of linked lists with roughly n/k elements each, where k is some relatively small constant. In this case, hash tables become O(n) for both insertions and searches. Thus you can see how important it is to develop a good hashing function. 14.3 Resolving Collisions Overflow area FIG URE 1 4 .3 Chaining using an overflow area A second method for implementing chaining is to use links. In this method, each cell or bucket in the hash table would be something like the LinearNode class used in earlier chapters to construct linked lists. In this way, as a second element is mapped to a particular bucket, we simply create a new LinearNode, set the next reference of the existing node to point to the new node, set the element reference of the new node to the element being inserted, and set the next reference of the new node to null. The result is an implementation model that looks exactly like the conceptual model shown in Figure 14.2. A third method for implementing chaining is to literally make each position in the table a pointer to a collection. In this way, we could represent each position in the table with a list or perhaps even a more efficient collection (e.g., a balanced binary search tree), and this would improve our worst case. Keep in mind, however, that if our hashing function is doing a good job of distributing elements to locations in the table, this approach may incur a great deal of overhead while accomplishing very little improvement. 415 416 C HA PT ER 14 Hashing Open Addressing The open addressing method for handling collisions looks for another open position in the table other than the one to which the element is originally hashed. There are a variety of methods to find another available location in the table. We will examine three of these methods: linear probing, quadratic probing, and double hashing. The simplest of these methods is linear probing. In linear probing, if an element hashes to position p and position p is already occupied, K E Y CO N C E PT we simply try position (p+1)%s, where s is the size of the table. If The open addressing method for position (p+1)%s is already occupied, we try position (p+2)%s, and handling collisions looks for another open position in the table other than so on until either we find an open position or we find ourselves back the one to which the element is at the original position. If we find an open position, we insert the new originally hashed. element. What to do if we do not find an open position is a design decision when creating a hash table. As we have discussed previously, one possibility is to throw an exception if the table is full. A second possibility is to expand the capacity of the table and rehash the existing entries. The problem with linear probing is that it tends to create clusters of filled positions within the table, and these clusters then affect the performance of insertions and searches. Figure 14.4 illustrates the linear probing method and the creation of a cluster using our earlier hashing function of extracting the first character of the string. In this example, Ann was entered, followed by Andrew. Because Ann already occupied position 0 of the array, Andrew was placed in position 1. Later, Bob was entered. Because Andrew already occupied position 1, Bob was placed in the next open position, which was position 2. Doug and Elizabeth were already in the table by the time Betty arrived, thus Betty could not be placed in position 1, 2, 3, or 4 and was placed in the next open position, position 5. After Barbara, Hal, and Bill were added, we find that there is now a nine-location cluster at the front of the table, which will continue to grow as more names are added. Thus we see that linear probing may not be the best approach. A second form of the open addressing method is quadratic probing. Using quadratic probing, instead of using a linear approach, once we have a collision, we follow a formula such as newhashcode(x) = hashcode(x) + (-1)i - 1((i + 1)>2)2 for i in the range 1 to s–1 where s is the table size. The result of this formula is the search sequence p, p ⫹ 1, p ⫺ 1, p ⫹ 4, p ⫺ 4, p ⫹ 9, p ⫺ 9, . . . . Of course, this new hash code is then put through the division method to keep it within the table range. As with linear probing, the same possibility exists that we will eventually get back to the original hash code without having found an open position in which to insert. This “full” condition can be 14.3 Resolving Collisions Ann Andrew Bob Doug Elizabeth Betty Primary Cluster Barbara Hal Bill Mary Tim Walter Young FI GURE 1 4 .4 Open addressing using linear probing handled in all of the same ways that we described for chaining and linear probing. The benefit of the quadratic probing method is that it does not have as strong a tendency toward clustering as does linear probing. Figure 14.5 illustrates quadratic probing for the same key set and hashing function that we used in Figure 14.4. Notice that after the same data has been entered, we still have a cluster at the front of the table. However, this cluster occupies only six buckets instead of the nine-bucket cluster created by linear probing. A third form of the open addressing method is double hashing. Using the double hashing method, we will resolve collisions by providing a secondary hashing function to be used when the primary hashing function results in a collision. For example, if a key x hashes to a position p that is already occupied, then the next position p⬘ that we will try will be p’ ⫽ p ⫹ secondaryhashcode(x) 417 418 C HA PT ER 14 Hashing Ann Andrew Bob Doug Elizabeth Barbara Hal Bill Mary Tim Walter Betty Young FIG URE 1 4 .5 Open addressing using quadratic probing If this new position is also occupied, then we look to position p” ⫽ p ⫹ 2 * secondaryhashcode(x) We continue searching this way, of course using the division method to maintain our index within the bounds of the table, until an open position is found. This method, while somewhat more costly because of the introduction of an additional function, tends to further reduce clustering beyond the improvement gained by quadratic probing. Figure 14.6 illustrates this approach, again using the same key set and hashing function from our previous examples. For this example, the secondary hashing function is the length of the string. Notice that with the same data, we no longer have a cluster at the front of the table. However, we have 14.4 Deleting Elements from a Hash Table Ann Bob Doug Elizabeth Bill Andrew Hal Barbara Betty Mary Tim Walter Young FI GURE 1 4 .6 Open addressing using double hashing developed a six-bucket cluster from Doug through Barbara. The advantage of double hashing, however, is that even after a cluster has been created, it will tend to grow more slowly than it would if we were using linear probing or even quadratic probing. 14.4 Deleting Elements from a Hash Table Thus far, our discussion has centered on the efficiency of insertion of and searching for elements in a hash table. What happens if we remove an element from a hash table? The answer to this question depends upon which implementation we have chosen. 419 420 C HA PT ER 14 Hashing Deleting from a Chained Implementation If we have chosen to implement our hash table using a chained implementation and an array with an overflow area, then removing an element falls into one of five cases: Case 1 The element we are attempting to remove is the only one mapped to the particular location in the table. In this case, we simply remove the element by setting the table position to null. Case 2 The element we are attempting to remove is stored in the table (not in the overflow area) but has an index into the overflow area for the next element at the same position. In this case, we replace the element and the next index value in the table with the element and next index value of the array position pointed to by the element to be removed. We then also must set the position in the overflow area to null and add it back to whatever mechanism we are using to maintain a list of free positions. Case 3 The element we are attempting to remove is at the end of the list of elements stored at that location in the table. In this case, we set its position in the overflow area to null, and we set the next index value of the previous element in the list to null as well. We then also must set the position in the overflow area to null and add it back to whatever mechanism we are using to maintain a list of free positions. Case 4 The element we are attempting to remove is in the middle of the list of elements stored at that location in the table. In this case, we set its position in the overflow area to null, and we set the next index value of the previous element in the list to the next index value of the element being removed. We then also must add it back to whatever mechanism we are using to maintain a list of free positions. Case 5 The element we are attempting to remove is not in the list. In this case, we throw an ElementNotFoundException. If we have chosen to implement our hash table using a chained implementation where each element in the table is a collection, then we simply remove the target element from the collection. Deleting from an Open Addressing Implementation If we have chosen to implement our hash table using an open addressing implementation, then deletion creates more of a challenge. Consider the example in Figure 14.7. Notice that elements “Ann,” “Andrew,” and “Amy” all mapped to the same location in the table and the collision was resolved using linear probing. What happens if we now remove “Andrew”? If we then search for “Amy” we will not find that element because the search will find “Ann” and then follow the linear probing rule to look in the next position, find it null, and return an exception. 14.5 Hash Tables in the Java Collections API Ann Bob Andrew Doug Elizabeth Bill Amy Hal Barbara Betty Mary Tim Walter Young FIG URE 1 4 .7 Open addressing and deletion The solution to this problem is to mark items as deleted but not actually remove them from the table until some future point when the deleted element is overwritten by a new inserted element or the entire table is rehashed, either because it is being expanded or because we have reached some predetermined threshold for the percentage of deleted records in the table. This means that we will need to add a boolean flag to each node in the table and modify all of our algorithms to test and/or manipulate that flag. 14.5 Hash Tables in the Java Collections API The Java Collections API provides seven implementations of hashing: Hashtable, HashMap, HashSet, IdentityHashMap, LinkedHashSet, LinkedHashMap, and WeakHashMap. To understand these different solutions we must first remind 421 422 C HA PT ER 14 Hashing ourselves of the distinction between a set and a map in the Java Collections API as well as some of our other pertinent definitions. K E Y CO N C E PT The load factor is the maximum percentage occupancy allowed in the hash table before it is resized. A set is a collection of objects where in order to find an object, we must have an exact copy of the object for which we are looking. A map, on the other hand, is a collection that stores key-value pairs so that, given the key, we can find the associated value. Another definition that will be useful to us as we explore the Java Collections API implementations of hashing is that of a load factor. The load factor, as stated earlier, is the maximum percentage occupancy allowed in the hash table before it is resized. For the implementations that we are going to discuss here, the default is 0.75. Thus, using this default, when one of these implementations becomes 75 percent full, a new hash table is created that is twice the size of the current one, and then all of the elements from the current table are inserted into the new table. The load factor of these implementations can be altered when the table is created. All of these implementations rely on the hashcode method of the object being stored to return an integer. This integer is then processed using the division method (using the table size) to produce an index within the bounds of the table. As stated earlier, the best practice is to define your own hashcode method for any class that you intend to store in a hash table. Let’s look at each of these implementations. The Hashtable Class The Hashtable implementation of hashing is the oldest of the implementations in the Java Collections API. In fact, it predates the Collections API and was modified in version 1.2 to implement the Map interface so that it would become a part of the Collections API. Unlike the newer Java Collections implementations, Hashtable is synchronized. Figure 14.8 shows the operations for the Hashtable class. Creation of a Hashtable requires two parameters: initial capacity (with a default of 11) and load factor (with a default of 0.75). Capacity refers to the number of cells or locations in the initial table. Load factor is, as we described earlier, the maximum percentage occupancy allowed in the hash table before it is resized. Hashtable uses the chaining method for resolving collisions. The Hashtable class is a legacy class that will be most useful if you are connecting to legacy code or require synchronization. Otherwise, it is preferable to use the HashMap class. 14.5 Return Value Hash Tables in the Java Collections API Method Description Hashtable() Constructs a new, empty hash table with a default initial capacity (11) and load factor, which is 0.75. Constructs a new, empty hash table with the specified initial capacity and default load factor, which is 0.75. Constructs a new, empty hash table with the specified initial capacity and the specified load factor. Hashtable(int initialCapacity) Hashtable(int initialCapacity, float loadFactor) Hashtable (Map t) 423 Constructs a new hash table with the same mappings as the given Map. clear() Clears this hash table so that it contains no keys. Creates a shallow copy of this hash table. Tests if some key maps into the specified value in this hash table. boolean containsKey(Object key) Tests if the specified object is a key in this hash table. boolean Returns true if this hash table maps one or more containsValue keys to this value. (Object value) Enumeration elements() Returns an enumeration of the values in this hash table. Set Returns a Set view of the entries contained in entrySet() this hash table. Compares the specified Object with this Map for equality, equals(Object o) boolean as per the definition in the Map interface. Returns the value to which the specified key is mapped Object get(Object key) in this hash table. int Returns the hash code value for this Map as per the hashCode() definition in the Map interface. isEmpty() Tests if this hash table maps no keys to values. boolean Returns an enumeration of the keys in this hash table. Enumeration keys() keysSet() Returns a Set view of the keys contained in this hash table. Set Maps the specified key to the specified value in Object put(Object key this hash table. Object value) Copies all of the mappings from the specified Map to void putAll(Map t) this hash table. These mappings will replace any mappings that this hash table had for any of the keys currently in the specified Map. Increases the capacity of and internally reorganizes this protected rehash() hash table, in order to accommodate and access void its entries more efficiently. void Object boolean clone() contains(Object value) FIG URE 1 4 .8 Operations on the Hashtable class 424 C HA PT ER 14 Hashing Object remove(Object key) int String size() toString() Collection values() Removes the key (and its corresponding value) from this hash table. Returns the number of keys in this hash table. Returns a string representation of this hash table object in the form of a set of entries, enclosed in braces and separated by the ASCII characters comma and space. Returns a Collection view of the values contained in this hash table. FIG URE 1 4 .8 Continued The HashSet Class The HashSet class implements the Set interface using a hash table. The HashSet class, like most of the Java Collections API implementations of hashing, uses chaining to resolve collisions (each table position effectively being a linked list). The HashSet implementation does not guarantee the order of the set on iteration and does not guarantee that the order will remain constant over time. This is because the iterator simply steps through the table in order. Because the hashing function will somewhat randomly distribute the elements to table positions, order cannot be guaranteed. Further, if the table is expanded, all of the elements are rehashed relative to the new table size, and the order may change. Like the Hashtable class, the HashSet class also requires two parameters: initial capacity and load factor. The default for the load factor is the same as it is for Hashtable (0.75). The default for initial capacity is currently unspecified (originally it was 101). Figure 14.9 shows the operations for the HashSet class. The HashSet class is not synchronized and permits null values. The HashMap Class The HashMap class implements the Map interface using a hash table. The HashMap class also uses a chaining method to resolve collisions. Like the HashSet class, the HashMap class is not synchronized and allows null values. Also like the previous implementations, the default load factor is 0.75. Like the HashSet class, the current default initial capacity is unspecified though it was also originally 101. Figure 14.10 shows the operations on the HashMap class. The IdentityHashMap Class The IdentityHashMap class implements the Map interface using a hash table. The difference between this and the HashMap class is that the IdentityHashMap class 14.5 Return Value Hash Tables in the Java Collections API Method Description HashSet() Constructs a new, empty set; the backing HashMap instance has the default capacity and load factor, which is 0.75. Constructs a new set containing the elements in the specified collection. Constructs a new, empty set; the backing HashMap instance has the specified initial capacity and default load factor, which is 0.75. Constructs a new, empty set; the backing HashMap instance has the specified initial capacity and the specified load factor. Adds the specified element to this set if it is not already present. Removes all of the elements from this set. Returns a shallow copy of this HashSet instance: the elements themselves are not cloned. Returns true if this set contains the specified element. Returns true if this set contains no elements. Returns an iterator over the elements in this set. Removes the given element from this set if it is present. Returns the number of elements in this set (its cardinality). HashSet(Collection c) HashSet(int initialCapacity) boolean HashSet(int initial Capacity, float loadFactor) add(Object o) void clear() Object clone() boolean boolean iterator() boolean int contains(Object o) isEmpty() iterator() remove(Object o) size() FIG URE 1 4 .9 Operations on the HashSet class uses reference-equality instead of object-equality when comparing both keys and values. This is the difference between using key1⫽⫽key2 and using key1.equals(key2). This class has one parameter: expected maximum size. This is the maximum number of key-value pairs that the table is expected to hold. If the table exceeds this maximum, then the table size will be increased and the table entries rehashed. Figure 14.11 shows the operations on the IdentityHashMap class. The WeakHashMap Class The WeakHashMap class implements the Map interface using a hash table. This class is specifically designed with weak keys so that an entry in a WeakHashMap will automatically be removed when its key is no longer in use. In other words, if 425 426 C HA PT ER 14 Return Value Hashing Method Description HashMap() Constructs a new, empty map with a default capacity and load factor, which is 0.75. Constructs a new, empty map with the specified initial capacity and default load factor, which is 0.75. Constructs a new, empty map with the specified initial capacity and the specified load factor. HashMap(int initial Capacity) HashMap(int initial Capacity, float loadFactor) Constructs a new map with the same mappings as the given map. clear() Removes all mappings from this map. clone() Returns a shallow copy of this HashMap instance: the keys and values themselves are not cloned. containsKey(Object key) Returns true if this map contains a mapping for the specified key. Returns true if this map maps one or more keys to the containsValue specified value. (Object value) Returns a collection view of the mappings contained entrySet() in this map. Returns the value to which this map maps the get(Object key) specified key. Returns true if this map contains no key-value mappings. isEmpty() keySet() Returns a set view of the keys contained in this map. put(Object key, Associates the specified value with the specified key Object value) in this map. Copies all of the mappings from the specified map putAll(Map t) to this one. remove(Object key) Removes the mapping for this key from this map if present. size() Returns the number of key-value mappings in this map. Returns a collection view of the values contained values() in this map. HashMap(Map t) void Object boolean boolean set Object boolean Set Object void Object int Collection FIG URE 1 4 .1 0 Operations on the HashMap class the use of the key in a mapping in the WeakHashMap is the only remaining use of the key, the garbage collector will collect it anyway. The WeakHashMap class allows both null values and null keys, and has the same tuning parameters as the HashMap class: initial capacity and load factor. Figure 14.12 shows the operations on the WeakHashMap class. 14.5 Return Value Method Hash Tables in the Java Collections API 427 Description Set Constructs a new, empty identity hash map with a default expected maximum size (21). IdentityHashMap(int Constructs a new, empty map with the specified expected expectedMaxSize) maximum size. IdentityHashMap(Map m) Constructs a new identity hash map containing the key-value mappings in the specified map. clear() Removes all mappings from this map. Returns a shallow copy of this identity hash map: clone() the keys and values themselves are not cloned. containsKey(Object key) Tests whether the specified object reference is a key in this identity hash map. containsValue Tests whether the specified object reference is a value (Object value) in this identity hash map. entrySet() Returns a set view of the mappings contained in this map. boolean Object equals(Object o) get(Object key) int boolean hashCode() isEmpty() Set keySet() Object put(Object key, Object value) void putAll(Map t) Object int remove(Object key) size() Collection values() IdentityHashMap() void Object boolean boolean Compares the specified object with this map for equality. Returns the value to which the specified key is mapped in this identity hash map, or null if the map contains no mapping for this key. Returns the hash code value for this map. Returns true if this identity hash map contains no key-value mappings. Returns an identity-based set view of the keys contained in this map. Associates the specified value with the specified key in this identity hash map. Copies all of the mappings from the specified map to this map. These mappings will replace any mappings that this map had for any of the keys currently in the specified map. Removes the mapping for this key from this map if present. Returns the number of key-value mappings in this identity hash map. Returns a collection view of the values contained in this map. FIG URE 1 4 .1 1 Operations on the IdentityHashMap class 428 C HA PT ER 14 Return Value Hashing Method Description Constructs a new, empty WeakHashMap with the default initial capacity and the default load factor, which is 0.75. WeakHashMap(int Constructs a new, empty WeakHashMap with the given initialCapacity) initial capacity and the default load factor, which is 0.75. WeakHashMap(int Constructs a new, empty WeakHashMap with the given initial Capacity, float initial capacity and the given load factor. loadFactor) WeakHashMap(Map t) Constructs a new WeakHashMap with the same mappings as the specified map. clear() Removes all mappings from this map. containsKey(Object key) Returns true if this map contains a mapping for the specified key. entrySet() Returns a set view of the mappings in this map. get(Object key) Returns the value to which this map maps the specified key. isEmpty() Returns true if this map contains no key-value mappings. keySet() Returns a set view of the keys contained in this map. put(Object key, Associates the specified value with the specified key Object value) in this map. Copies all of the mappings from the specified map putAll(Map t) to this map. These mappings will replace any mappings that this map had for any of the keys currently in the specified map. Removes the mapping for the given key from this map, remove(Object key) if present. size() Returns the number of key-value mappings in this map. values() Returns a collection view of the values contained in this map. WeakHashMap() void boolean Set Object boolean Set Object void Object int Collection FIG URE 1 4 .1 2 Operations on the WeakHashMap class LinkedHashSet and LinkedHashMap The two remaining hashing implementations are extensions of previous classes. The LinkedHashSet class extends the HashSet class, and the LinkedHashMap class extends the HashMap class. Both of them are designed to solve the problem of iterator order. These implementations maintain a doubly linked list running through the entries to maintain the insertion order of the elements. Thus the iterator order for these implementations is the order in which the elements were inserted. Figure 14.13 shows the additional operations for the LinkedHashSet class. Figure 14.14 shows the additional operations for the LinkedHashMap class. 14.5 Return Value Hash Tables in the Java Collections API 429 Method Description LinkedHashSet() Constructs a new, empty linked hash set with the default initial capacity (16) and load factor (0.75). Constructs a new linked hash set with the same elements as the specified collection. Constructs a new, empty linked hash set with the specified initial capacity and the default load factor (0.75). Constructs a new, empty linked hash set with the specified initial capacity and load factor. LinkedHashSet (Collection c) LinkedHashSet (int initialCapacity) LinkedHashSet(int initialCapacity, float loadFactor) FIG URE 1 4 .1 3 Additional operations on the LinkedHashSet class Return Value Method Description LinkedHashMap() Constructs an empty insertion-ordered LinkedHashMap instance with a default capacity (16) and load factor (0.75). Constructs an empty insertion-ordered LinkedHashMap instance with the specified initial capacity and a default load factor (0.75). Constructs an empty insertion-ordered LinkedHashMap instance with the specified initial capacity and load factor. LinkedHashMap (int initialCapacity) LinkedHashMap (int initialCapacity, float loadFactor) LinkedHashMap (int initialCapacity, float loadFactor, boolean accessOrder) LinkedHashMap(Map m) void boolean clear() containsValue (Object value) Object protected boolean get(Object key) removeEldestEntry (Map.Entry eldest) Constructs an empty LinkedHashMap instance with the specified initial capacity, load factor, and ordering mode. Constructs an insertion-ordered LinkedHashMap instance with the same mappings as the specified map. Removes all mappings from this map. Returns true if this map maps one or more keys to the specified value. Returns the value to which this map maps the specified key. Returns true if this map should remove its eldest entry. FIG URE 1 4 .1 4 Additional operations on the LinkedHashMap class 430 C HA PT ER 14 Hashing Summary of Key Concepts ■ In hashing, elements are stored in a hash table, with their location in the table determined by a hashing function. ■ The situation where two elements or keys map to the same location in the table is called a collision. ■ A hashing function that maps each element to a unique position in the table is said to be a perfect hashing function. ■ Extraction involves using only a part of the element’s value or key to compute the location at which to store the element. ■ The division method is very effective when dealing with an unknown set of key values. ■ In the shift folding method, the parts of the key are added together to create the index. ■ The length-dependent method and the mid-square method may also be effectively used with strings by manipulating the binary representations of the characters in the string. ■ Although Java provides a hashcode method for all objects, it is often preferable to define a specific hashing function for any particular class. ■ The chaining method for handling collisions simply treats the hash table conceptually as a table of collections rather than as a table of individual cells. ■ The open addressing method for handling collisions looks for another open position in the table other than the one to which the element is originally hashed. ■ The load factor is the maximum percentage occupancy allowed in the hash table before it is resized. Self-Review Questions SR 14.1 What is the difference between a hash table and the other collections we have discussed? SR 14.2 What is a collision in a hash table? SR 14.3 What is a perfect hashing function? SR 14.4 What is our goal for a hashing function? SR 14.5 What is the consequence of not having a good hashing function? SR 14.6 What is the extraction method? Exercises SR 14.7 What is the division method? SR 14.8 What is the shift folding method? SR 14.9 What is the boundary folding method? SR 14.10 What is the mid-square method? SR 14.11 What is the radix transformation method? SR 14.12 What is the digit analysis method? SR 14.13 What is the length-dependent method? SR 14.14 What is chaining? SR 14.15 What is open addressing? SR 14.16 What are linear probing, quadratic probing, and double hashing? SR 14.17 Why is deletion from an open addressing implementation a problem? SR 14.18 What is the load factor, and how does it affect table size? Exercises EX 14.1 Draw the hash table that results from adding the following integers (34 45 3 87 65 32 1 12 17) to a hash table of size 11 using the division method and linked chaining. EX 14.2 Draw the hash table from Exercise 14.1 using a hash table of size 11 using array chaining with a total array size of 20. EX 14.3 Draw the hash table from Exercise 14.1 using a table size of 17 and open addressing using linear probing. EX 14.4 Draw the hash table from Exercise 14.1 using a table size of 17 and open addressing using quadratic probing. EX 14.5 Draw the hash table from Exercise 14.1 using a table size of 17 and double hashing using extraction of the first digit as the secondary hashing function. EX 14.6 Draw the hash table that results from adding the following integers (1983, 2312, 6543, 2134, 3498, 7654, 1234, 5678, 6789) to a hash table using shift folding of the first two digits with the last two digits. Use a table size of 13. EX 14.7 Draw the hash table from Exercise 14.6 using boundary folding. EX 14.8 Draw a UML diagram that shows how all of the various implementations of hashing within the Java Collections API are constructed. 431 432 C HA PT ER 14 Hashing Programming Projects PP 14.1 Implement the hash table illustrated in Figure 14.1 using the array version of chaining. PP 14.2 Implement the hash table illustrated in Figure 14.1 using the linked version of chaining. PP 14.3 Implement the hash table illustrated in Figure 14.1 using open addressing with linear probing. PP 14.4 Implement a dynamically resizable hash table to store people’s names and Social Security numbers. Use the extraction method with division using the last four digits of the Social Security number. Use an initial table size of 31 and a load factor of 0.80. Use open addressing with double hashing using an extraction method on the first three digits of the Social Security number. PP 14.5 Implement the problem from Programming Project 14.4 using linked chaining. PP 14.6 Implement the problem from Programming Project 14.4 using the HashMap class of the Java Collections API. PP 14.7 Create a new implementation of the bag collection called HashtableBag using a hash table. PP 14.8 Implement the problem from Programming Project 14.4 using shift folding with the Social Security number divided into three equal three-digit parts. PP 14.9 Create a graphical system that will allow a user to add and remove employees where each employee has an employee id (six-digit number), employee name, and years of service. Use the hashcode method of the Integer class as your hashing function and use one of the Java Collections API implementations of hashing. PP 14.10 Complete Programming Project 14.9 using your own hashcode function. Use extraction of the first three digits of the employee id as the hashing function and use one of the Java Collections API implementations of hashing. PP 14.11 Complete Programming Project 14.9 using your own hashcode function and your own implementation of a hash table. PP 14.12 Create a system that will allow a user to add and remove vehicles from an inventory system. Vehicles will be represented by license number (an eight-character string), make, model, and color. Use your own array-based implementation of a hash table using chaining. Answers to Self-Review Questions PP 14.13 Complete Programming Project 14.12 using a linked implementation with open addressing and double hashing. Answers to Self-Review Questions SRA 14.1 Elements are placed into a hash table at an index produced by a function of the value of the element or a key of the element. This is unique from other collections where the position/location of an element in the collection is determined either by comparison with the other values in the collection or by the order in which the elements were added or removed from the collection. SRA 14.2 The situation where two elements or keys map to the same location in the table is called a collision. SRA 14.3 A hashing function that maps each element to a unique position in the table is said to be a perfect hashing function. SRA 14.4 We need a hashing function that will do a good job of distributing elements into positions in the table. SRA 14.5 If we do not have a good hashing function, the result will be too many elements mapped to the same location in the table. This will result in poor performance. SRA 14.6 Extraction involves using only a part of the element’s value or key to compute the location at which to store the element. SRA 14.7 The division method involves dividing the key by some positive integer p (usually the table size and usually prime) and then using the remainder as the index. SRA 14.8 Shift folding involves dividing the key into parts (usually the same length as the desired index) and then adding the parts. Extraction or division is then used to get an index within the bounds of the table. SRA 14.9 Like shift folding, boundary folding involves dividing the key into parts (usually the same length as the desired index). However, some of the parts are then reversed before adding. One example is to imagine that the parts are written side by side on a piece of paper, which is then folded on the boundaries between parts. In this way, every other part is reversed. SRA 14.10 The mid-square method involves multiplying the key by itself and then extracting some number of digits or bytes from the middle of the result. Division can then be used to guarantee an index within the bounds of the table. 433 434 C HA PT ER 14 Hashing SRA 14.11 The radix transformation method is a variation on the division method where the key is first converted to another numeric base and then divided by the table size with the remainder used as the index. SRA 14.12 In the digit analysis method, the index is formed by extracting, and then manipulating, specific digits from the key. SRA 14.13 In the length-dependent method, the key and the length of the key are combined in some way to form either the index itself or an intermediate value that is then used with one of our other methods to form the index. SRA 14.14 The chaining method for handling collisions simply treats the hash table conceptually as a table of collections rather than as a table of individual cells. Thus each cell is a pointer to the collection associated with that location in the table. Usually this internal collection is either an unordered list or an ordered list. SRA 14.15 The open addressing method for handling collisions looks for another open position in the table other than the one to which the element is originally hashed. SRA 14.16 Linear probing, quadratic probing, and double hashing are methods for determining the next table position to try if the original hash causes a collision. SRA 14.17 Because of the way that a path is formed in open addressing, deleting an element from the middle of that path can cause elements beyond that on the path to be unreachable. SRA 14.18 The load factor is the maximum percentage occupancy allowed in the hash table before it is resized. Once the load factor has been reached, a new table is created that is twice the size of the current table, and then all of the elements in the current table are inserted into the new table. 15 Sets and Maps T his chapter wraps up our discussion of collections by in- troducing both set and map collections. These are the collections used in the Java Collections API to represent many CHAPTER OBJECTIVES ■ Define set and map collections ■ Use a set collection to solve a problem ■ Examine an array implementation of a set ■ Examine a linked implementation of a set ■ Explore Java Collections API implementations of sets and maps of the collections presented in this text. We will introduce our own implementations and also discuss the Java Collections API implementations. 435 436 C HA PT ER 15 Sets and Maps FIG URE 1 5 .1 The conceptual view of a set collection 15.1 A Set Collection A set can be defined as a collection of elements with no duplicates. For our current purposes, we will assume that there is no particular positional relationship among the elements of the set. Conceptually it is similar to a bag or box into which elements are placed. Figure 15.1 depicts a set collection holding its otherwise unorganized elements. K E Y CO N C E PT A set is a nonlinear collection in which there is essentially no inherent organization to the elements in the collection. A set is a nonlinear collection. There is essentially no organization to the elements in the collection at all. The elements in a set have no inherent relationship to each other, and there is no significance to the order in which they have been added to the set. The operations we define for a set collection are listed in Figure 15.2. Like our other collections, a set has operations that allow the user to Operation Description add addAll removeRandom remove union contains equals isEmpty size iterator toString Adds an element to the set. Adds the elements of one set to another. Removes an element at random from the set. Removes a particular element from the set. Combines the elements of two sets to create a third. Determines if a particular element is in the set. Determines if two sets contain the same elements. Determines if the set is empty. Determines the number of elements in the set. Provides an iterator for the set. Provides a string representation of the set. FIG URE 1 5 .2 The operations on a set collection 15.1 A Set Collection <<interface>> SetADT<T> add() addAll() removeRandom() remove() union() contains() equals() isEmpty() size() iterator() toString() FI GU RE 1 5 .3 UML description of the SetADT<T> interface add and remove elements. Some operations such as isEmpty and size are common to almost all collections as well. A set collection is somewhat unique in that it incorporates an element of randomness. The very nature of a set collection lends itself to being able to pick an element out of the set at random. Listing 15.1 defines a Java interface for a set collection. Figure 15.3 illustrates the UML description of the SetADT interface. L I S T I N G 1 5 . 1 /** * SetADT defines the interface to a set collection. * * @author Dr. Lewis * @author Dr. Chase * @version 1.0, 9/21/2008 */ package jss2; import java.util.Iterator; public interface SetADT<T> { /** * Adds one element to this set, ignoring duplicates. * * @param element the element to be added to this set */ public void add (T element); 437 438 C HA PT ER 15 L I S T I N G Sets and Maps 1 5 . 1 continued /** * Removes and returns a random element from this set. * * @return a random element from this set */ public T removeRandom (); /** * Removes and returns the specified element from this set. * * @param element the element to be removed from this list * @return the element just removed from this list */ public T remove (T element); /** * Returns the union of this set and the parameter * * @param set the set to be unioned with this set * @return a set that is the union of this set and the parameter */ public SetADT<T> union (SetADT<T> set); /** * Returns true if this set contains the parameter * * @param target the element being sought in this set * @return true if this set contains the parameter */ public boolean contains (T target); /** * Returns true if this set and the parameter contain exactly * the same elements * * @param set the set to be compared with this set * @return true if this set and the parameter contain exactly * the same elements */ public boolean equals (SetADT<T> set); /** * Returns true if this set contains no elements * 15.2 L I S T I N G 1 5 . 1 Using a Set: Bingo continued * @return true if this set contains no elements */ public boolean isEmpty(); /** * Returns the number of elements in this set * * @return the integer number of elements in this set */ public int size(); /** * Returns an iterator for the elements in this set * * @return an iterator for the elements in this set */ public Iterator<T> iterator(); /** * Returns a string representation of this set * * @return a string representation of this set */ public String toString(); } Note that because there is no inherent order in a set, the order of the elements returned by the iterator method will be arbitrary. 15.2 Using a Set: Bingo We can use the game called bingo to demonstrate the use of a set collection. In bingo, numbers are chosen at random from a limited set, usually 1 to 75. The numbers in the range 1 to 15 are associated with the letter B, 16 to 30 with the letter I, 31 to 45 with the letter N, 46 to 60 with the letter G, and 61 to 75 with the letter O. The person managing the game (the “caller”) selects a number randomly, and then announces the letter and the number. The caller then sets aside that number so that it cannot be used again in that game. All of the players then mark any squares on their card that match the letter and number called. Once any 439 440 C HA PT ER 15 Sets and Maps B I N G O 9 25 34 48 69 15 19 31 59 74 2 28 FREE 52 62 7 16 41 58 70 4 20 38 47 64 FIGU RE 1 5 . 4 A bingo card player has five squares in a row marked (vertically, horizontally, or diagonally), they announce “Bingo!” and claim their prize. Figure 15.4 shows a sample bingo card. A set is perfectly suited for assisting the caller in selecting randomly from the possible numbers. To solve this problem, we would simply need to create an object for each of the possible numbers and add them to a set. Then, each time the caller needs to select a number, we would call the removeRandom method. Listing 15.2 shows the BingoBall class needed to represent each possible selection. The program in Listing 15.3 adds the 75 bingo balls to the set and then selects some of them randomly to illustrate the task. In the Bingo program, the set is represented as an object of type ArraySet. More specifically, it is an object of type ArraySet<BingoBall>, a set that stores BingoBall objects. We explore the implementation of the ArraySet class in the next section. Figure 15.5 shows the relationship between the Bingo and BingoBall classes illustrated in UML. Bingo BingoBall uses letter number main() BingoBall() toString() FIG URE 1 5 .5 UML description of the Bingo and BingoBall classes 15.2 Li st i n g Using a Set: Bingo 1 5 .2 /** * BingoBall represents a ball used in a Bingo game. * * @author Dr. Lewis * @author Dr. Chase * @version 1.0, 9/21/2008 */ public class BingoBall { private char letter; private int number; /** * Sets up this Bingo ball with the specified number and the * appropriate letter. * * @param num the number to be applied to the new bingo ball */ public BingoBall (int num) { number = num; if (num <= 15) letter = 'B'; else if (num <= 30) letter = 'I'; else if (num <= 45) letter = 'N'; else if (num <= 60) letter = 'G'; else letter = 'O'; } /** * Returns a string representation of this bingo ball. * * @return a string representation of the bingo ball */ 441 442 C HA PT ER 15 L I S T I N G Sets and Maps 1 5 . 2 continued public String toString () { return (letter + " " + number); } } L I S T I N G 1 5 . 3 /** * Bingo demonstrates the use of a set collection. * * @author Dr. Lewis * @author Dr. Chase * @version 1.0, 9/21/2008 */ import jss2.ArraySet; public class Bingo { /** * Creates all 75 Bingo balls and stores them in a set. Then * pulls several balls from the set at random and prints them. */ public static void main (String[] args) { final int NUM_BALLS = 75, NUM_PULLS = 10; ArraySet<BingoBall> bingoSet = new ArraySet<BingoBall>(); BingoBall ball; for (int num = 1; num <= NUM_BALLS; num++) { ball = new BingoBall (num); bingoSet.add (ball); } System.out.println ("Size: " + bingoSet.size()); System.out.println (); 15.3 L I S T I N G 1 5 . 3 Implementing a Set: With Arrays 443 continued for (int num = 1; num <= NUM_PULLS; num++) { ball = bingoSet.removeRandom(); System.out.println (ball); } } } 15.3 Implementing a Set: With Arrays So far in our discussion of a set collection we have described its basic conceptual nature and the operations that allow the user to interact with it. In software engineering terms, we would say that we have done the analysis for a set collection. We have also used a set, without knowing the details of how it was implemented, to solve a particular problem. Now let’s turn our atKEY CON CEPT tention to the implementation details. There are various ways to imThe implementation of the collection plement a class that represents a set. In this section, we examine an operations should not affect the way users interact with the collection. implementation strategy that uses an array to store the objects contained in the set. In the next section, we examine a linked implementation of a set. The key instance data for the ArraySet<T> class includes the array that holds the contents of the set and the integer variable count that keeps track of the number of elements in the collection. We also define a Random object to support the drawing of a random element from the set, and a constant, DEFAULT_CAPACITY, to define a default capacity. We create another constant, NOT_FOUND, to assist in the operations that search for particular elements. The data associated with the ArraySet<T> class is declared as follows: /** * ArraySet represents an array implementation of a set. * * @author Dr. Lewis * @author Dr. Chase * @version 1.0, 9/21/2008 */ 444 C HA PT ER 15 Sets and Maps package jss2; import jss2.exceptions.*; import java.util.*; public class ArraySet<T> implements SetADT<T>, Iterable<T> { private static Random rand = new Random(); private final int DEFAULT_CAPACITY = 100; private final int NOT_FOUND = -1; private int count; // the current number of elements in the set private T[] contents; Note that the Random object is declared as a static variable and is instantiated in its declaration (rather than in a constructor). Because it is static, the Random object is shared among all instances of the ArraySet<T> class. This strategy avoids the problem of creating two sets that have random-number generators using the same seed value. Like we did with the stack implementation, the value of the variable count actually represents two related pieces of information. First, it represents the number of elements that are currently stored in the set collection. Second, because Java array indexes start at zero, it also represents the next open slot into which a new element can be stored in the array. On one hand the value of count represents the abstract state of the collection, and on the other it helps us with the internal implementation of that collection. In this implementation, the elements contained in the set are gathered contiguously at one end of the array. This strategy simplifies various aspects of the operations, though it does require operations that remove elements to “fill in the gaps” created in the elements. Figure 15.6 depicts the use of an array to store the elements of a set. count 0 1 2 3 4 5 6 7 8 elements are kept contiguous FIG URE 1 5 .6 An array implementation of a set collection 9 15.3 Implementing a Set: With Arrays 445 The following constructor is defined for the ArraySet<T> class to set up an initially empty set. The value of count is set to zero, and the array that will store the elements of the set is instantiated. This constructor uses a default value for the initial capacity of the contents array. /** * Creates an empty set using the default capacity. */ public ArraySet() { count = 0; contents = (T[])(new Object[DEFAULT_CAPACITY]); } We also provide a second constructor that accepts a single integer parameter representing the initial capacity of the contents array. In a particular situation, the user may know approximately how many elements will be stored in a set and can specify that value from the beginning. This overloaded constructor can be defined as follows: /** * Creates an empty set using the specified capacity. * * @param initialCapacity the initial capacity for the array set */ public ArraySet (int initialCapacity) { count = 0; contents = (T[])(new Object[initialCapacity]); } As we design the implementation of each operation, we must consider any situations that may exist that would require any special processing. For example, if the collection is empty, an element cannot be removed from it. Likewise, because we are dealing with a fixed capacity, we must consider the situation in which the underlying data structure is full. KEY CON CEPT In the Java Collections API and throughout this text, class names indicate both the underlying data structure and the collection. The add Operation The purpose of the add method is to incorporate into the set collection the T object that it accepts as a parameter. For our array implementation, that means storing 446 C HA PT ER 15 Sets and Maps the T object in an empty slot in the array. The instance variable count represents the next empty space in the array, so we can simply store the new element at that location. However, the add operation for a fixed-capacity structure must take into account the situation in which the array is filled. As with our previous array implementations, our solution is to automatically expand the capacity of the array when this situation arises. The following method implements the add operation for the ArraySet<T> class: /** * Adds the specified element to the set if it is not already * present. Expands the capacity of the set array if necessary. * * @param element the element to be added to the set array */ public void add (T element) { if (!(contains(element))) { if (size() == contents.length) expandCapacity(); contents[count] = element; count++; } } First, the add method uses the contains method to determine if a duplicate value already exists in the array. If this is the case, the method has no effect on the collection. The add method then uses the size method to determine the number of elements currently in the collection. If this value equals the total number of cells in the array, indicated by the length constant, then the expandCapacity method is called. Regardless of whether or not the capacity is expanded, the element is then stored in the array and the number of elements in the set collection is incremented. Note that after the add method finishes, the value of the count variable continues to represent both the number of elements in the set and the next open slot in the array. Instead of calling the size method, the add method could have examined the value of the count variable to determine if the capacity of the array needed to be expanded. The value of count is, after all, exactly what the size method returns. However, in situations like this in which there is a method to play a particular 15.3 Implementing a Set: With Arrays role (determine the size), we are better off using this method. If the design is later changed to determine the size of the set in a different way, the add method would still work without a problem. The expandCapacity method increases the size of the array that is storing the elements of the set. More precisely, it creates a second array that is twice the size of the one currently storing the contents of the set, copies all of the current references into the new array, then resets the contents instance variable to refer to the larger array. The expandCapacity method is implemented as follows: /** * Creates a * twice the */ private void { T[] larger new array to store the contents of the set with capacity of the old one. expandCapacity() = (T[])(new Object[contents.length*2]); for (int index=0; index < contents.length; index++) larger[index] = contents[index]; contents = larger; } Note that the expandCapacity method is declared with private visibility. It is designed as a support method, not as a service provided for the user of the set collection. Also note that the expandCapacity method doubles the size of the contents array. It could have tripled the size, or simply added ten more cells, or even just one. The amount of the increase determines how soon we will have to increase the size again. We don’t want to have to call the expandCapacity method too often, because it copies the entire contents of the collection from one array to another. We also don’t want to have too much unused space in the array, even though this is probably the less serious offense. There is some mathematical analysis that could be done to determine the most effective size increase, but at this point we will simply make reasonable choices. Like our other array implementations, the cost of expanding the capacity of the array is amortized over all of the calls to the add method. Thus, because the add method consists only of simple assignment statements, it is O(1). The addAll Operation The purpose of the addAll method is to incorporate all of the objects from one set, which it accepts as a parameter, into the set collection. For our array implementation, 447 448 C HA PT ER 15 Sets and Maps this means that we can use our iterator method to step through the contents of one set and use our add method to add those elements to the current set. One advantage of using the add method in this way is that the add method already checks capacity and expands the array if necessary. Because we have already determined that our add method is O(1), we need only concern ourselves with the loop using the iterator. This loop will execute n times, once for each element in the given set. Thus our addAll operation is O(n). The following method implements the addAll operation for the ArraySet<T> class: /** * Adds the contents of the parameter to this set. * * @param set the collection to be added to this set */ public void addAll (SetADT<T> set) { Iterator<T> scan = set.iterator(); while (scan.hasNext()) add (scan.next()); } The removeRandom Operation The removeRandom operation must choose an element from the collection at random, remove that element from the collection, and return it to the calling method. This operation relies on the static Random object called rand that is defined at the class level. The only special case for this operation is when an attempt is made to remove an element from an empty set. If the set collection is empty, this method throws an EmptyCollectionException. This processing is consistent with our philosophy of using exceptions. The removeRandom method of the ArraySet<T> class is written as follows: /** * Removes a random element from the set and returns it. Throws * an EmptyCollectionException if the set is empty. * 15.3 Implementing a Set: With Arrays * @return a random element from the set * @throws EmptyCollectionException if an empty set exception occurs */ public T removeRandom() throws EmptyCollectionException { if (isEmpty()) throw new EmptyCollectionException("Stack"); int choice = rand.nextInt(count); T result = contents[choice]; contents[choice] = contents[count-1]; contents[count-1] = null; count--; // fill the gap return result; } The nextInt method of the Random class is used to determine a pseudorandom value in the range from 0 to count-1. This range represents the indices of all elements currently stored in the array. Once the random element is chosen, it is stored in the local variable called result, which is returned to the calling method when this method is complete. Recall that this implementation of the set collection keeps all elements in the set stored contiguously at one end of the contents array. Because this method removes one of the elements, we must “fill the gap” in some way. We could use a loop to shift all of the elements down one, but that is unnecessary. Because there is no ordering implied by the array, we can simply take the last element in the list (at index count-1) and put it in the cell of the removed element, which requires no looping. Thus our removeRandom method is O(1). Were we to alter the algorithm to shift elements down instead, our method would be O(n). The remove Operation The remove operation removes the specified element from the set and returns it. This method will throw an EmptyCollectionException if the set is empty and a NoSuchElementException if the target element is not in the set. 449 450 C HA PT ER 15 Sets and Maps /** * Removes the specified element from the set and returns it. * Throws an EmptyCollectionException if the set is empty and a * NoSuchElementException if the target is not in the set. * * @param target the element being sought in the set * @return the element specified by the target * @throws EmptyCollectionException if an empty set exception occurs * @throws NoSuchElementException if a no such element exception occurs */ public T remove (T target) throws EmptyCollectionException, NoSuchElementException { int search = NOT_FOUND; if (isEmpty()) throw new EmptyCollectionException("Stack"); for (int index=0; index < count && search == NOT_FOUND; index++) if (contents[index].equals(target)) search = index; if (search == NOT_FOUND) throw new NoSuchElementException(); T result = contents[search]; contents[search] = contents[count-1]; contents[count-1] = null; count--; return result; } With our array implementation, the remove operation is simply a matter of searching the array for the target element, removing it, and replacing it with the element stored at count-1, or the last element stored in the array. Because the elements of a set are not stored in any particular order, there is no need to shift more than the one element. We then decrement the count. Because of the need to do a linear search of the array, this method is O(n). The union Operation The union operation returns a new set that is the union of this set and the parameter; i.e., a new set that contains all of the elements from both sets. Again, we can use 15.3 Implementing a Set: With Arrays our existing operations. We use our constructor to create a new set and then step through our array and use the add method to add each element of our current set to the new set. Next, we create an iterator for the set passed as a parameter, step through each element of that set, and add each of them to the new set. Because there is no inherent order in a set, it does not matter which set’s contents we add first. There are several interesting design possibilities with this operation. First, because the method is returning a new set that is the combination of this set and another set, one could argue that the method should simply be a static method accepting two sets as input parameters. For consistency, we have chosen not to use that solution. A second possibility is to use the addAll method (i.e., both.addAll(this) followed by both.addAll(set)). However, we have deliberately chosen to use a for loop and an iterator in this implementation to demonstrate an important concept. Because the process occurs “inside” one set, we have access to its private instance data and thus can use a for loop to traverse the array. However, for the set passed as a parameter, we use an iterator to access its elements. Because of the two linear loops, this method is O(n + m) where n and m are the sizes of the two sets. /** * Returns a new set that is the union of this set and the * parameter. * * @param set the set that is to be unioned with this set * @return a new set that is the union of this set and * the parameter */ public SetADT<T> union (SetADT<T> set) { ArraySet<T> both = new ArraySet<T>(); for (int index = 0; index < count; index++) both.add (contents[index]); Iterator<T> scan = set.iterator(); while (scan.hasNext()) both.add (scan.next()); return both; } The contains Operation The contains operation returns true if this set contains the specified target element. As with the remove operation, because of our array implementation, this 451 452 C HA PT ER 15 Sets and Maps operation becomes a simple search of an array to locate a particular element and is O(n). /** * Returns true if this set contains the specified target * element. * * @param target the element being sought within this set * @return true if the set contains the target element */ public boolean contains (T target) { int search = NOT_FOUND; for (int index=0; index < count && search == NOT_FOUND; index++) if (contents[index].equals(target)) search = index; return (search != NOT_FOUND); } The equals Operation The equals operation will return true if the current set contains exactly the same elements as the set passed as a parameter. If the two sets are of different sizes, then there is no reason to continue the comparison. However, if the two sets are the same size, we create a deep copy of each set and then use an iterator to step through the elements of the set passed as a parameter and use the contains method to confirm that each of those elements is also in the current set. As we find elements in both sets, we remove them from the copies, being careful not to affect the original sets. If both of the copies are empty at the end of the process, then the sets are indeed equal. Notice that we iterate over the original set passed as a parameter while removing matching elements from the copies. This avoids any problems associated with modifying a set while using the associated iterator. Because we are iterating over one of the sets, this operation is O(n). /** * Returns true if this set contains exactly the same elements * as the parameter. * * @param set the set to be compared with this set 15.3 Implementing a Set: With Arrays * @return true if the parameter set and this set contain * exactly the same elements */ public boolean equals (SetADT<T> set) { boolean result = false; ArraySet<T> temp1 = new ArraySet<T>(); ArraySet<T> temp2 = new ArraySet<T>(); T obj; if (size() == set.size()) { temp1.addAll(this); temp2.addAll(set); Iterator<T> scan = set.iterator(); while (scan.hasNext()) { obj = scan.next(); if (temp1.contains(obj)) { temp1.remove(obj); temp2.remove(obj); } } result = (temp1.isEmpty() && temp2.isEmpty()); } return result; } Other Operations The iterator, toString, size and isEmpty methods can be implemented using the same strategies used in earlier implmentations. These methods are left as programming projects. UML Description Now that we have all of our classes defined, it is possible to see a UML representation of the entire class diagram for our Bingo example, as illustrated in Figure 15.7. 453 454 C HA PT ER 15 Sets and Maps ArrayIterator count current items hasNext() next() ArraySet rand DEFAULT_CAPACITY NOT_FOUND count contents uses <<interface>> SetADT add() addAll() removeRandom() remove() union() contains() equals() isEmpty() size() iterator() toString add() addAll() removeRandom() remove() union() contains() equals() isEmpty() size() iterator() toString stores bingo balls in Bingo BingoBall letter number uses main BingoBall() toString() FIG URE 1 5 .7 UML description of the bingo system 15.4 15.4 Implementing a Set: With Links Implementing a Set: With Links The LinkedSet<T> class implements the SetADT<T> interface. Because we are using a linked list approach, we need only a single reference to the first node in the list. We will also maintain a count of the number of elements in the list. Finally, we need a Random object to support the removeRandom operation. We can also reuse our LinearNode class that we used with stacks, queues, and lists. The classlevel data of the LinkedSet<T> class is therefore: /** * LinkedSet represents a linked implementation of a set. * * @author Dr. Chase * @author Dr. Lewis * @version 1.0, 9/21/2008 */ package jss2; import jss2.exceptions.*; import java.util.*; public class LinkedSet<T> implements SetADT<T>, Iterable<T> { private static Random rand = new Random(); private int count; // the current number of elements in the set private LinearNode<T> contents; Using the LinearNode<T> class and maintaining a count of elements in the collection creates the implementation strategy depicted in Figure 15.8. count: 6 contents F I GURE 1 5 .8 A linked implementation of a set collection 455 456 C HA PT ER 15 Sets and Maps The constructor of the LinkedSet<T> class sets the count of elements to zero and sets the front of the list, represented by the variable contents, to null. /** * Creates an empty set. */ public LinkedSet() { count = 0; contents = null; } The add Operation The add method incorporates the element passed as a parameter into the collection. Because there is no inherent order among the elements of a set, we can simply add the new element to the front of the list after verifying that there are no duplicates: /** * Adds the specified element to this set if it is not already * present. * * @param element the element to be added to this set */ public void add (T element) { if (!(contains(element))) { LinearNode<T> node = new LinearNode<T> (element); node.setNext(contents); contents = node; count++; } } The add method creates a new LinearNode<T> object, using its constructor to store the element. Then the new node’s next reference is set to the first node in the list, the reference to the first node is reset to point to the newly created one, and the count is incremented. Like the add method for the array implementation, this method consists only of simple assignment statements and is O(1). 15.4 Implementing a Set: With Links The removeRandom Operation The removeRandom method demonstrates how a linked list solution is sometimes more complicated than its array-based counterpart. In the ArraySet<T> class, the removeRandom method simply chose a random index into the array and returned the corresponding element. In this version of removeRandom, we must traverse the list, counting the elements until we get to the one that has been randomly selected for removal: /** * Removes and returns a random element from this set. Throws * an EmptyCollectionException if the set contains no elements. * * @return a random element from this set * @throws EmptyCollectionException if an empty set exception occurs */ public T removeRandom() throws EmptyCollectionException { LinearNode<T> previous, current; T result = null; if (isEmpty()) throw new EmptyCollectionException("Set"); int choice = rand.nextInt(count) + 1; if (choice == 1) { result = contents.getElement(); contents = contents.getNext(); } else { previous = contents; for (int skip=2; skip < choice; skip++) previous = previous.getNext(); current = previous.getNext(); result = current.getElement(); previous.setNext(current.getNext()); } count--; return result; } 457 458 C HA PT ER 15 Sets and Maps Like the ArraySet<T> version, this method throws an EmptyCollection Exception if there are no elements in the set. If there is at least one, a random number is chosen in the proper range. If the first element is chosen for removal, that situation is handled separately to maintain the reference to the front of the list. Note that unlike the ArraySet<T> version, we cannot simply access the element indexed by our random number. Instead we must traverse the list resulting in a time complexity of O(n). The remove Operation The remove method follows somewhat similar logic to the removeRandom method, except that it is looking for a particular element to remove. If the first element matches the target element, it is removed. Otherwise, previous and current references are used to traverse the list to the appropriate point. /** * Removes and returns the specified element from this set. * Throws an EmptyCollectionException if the set is empty and a * NoSuchElementException if the target is not in the set. * * @param target the element being sought in this set * @return the element just removed from this set * @throws EmptyCollectionException if an empty set exception occurs * @throws NoSuchElementException if a no such element exception occurs */ public T remove (T target) throws EmptyCollectionException, NoSuchElementException { boolean found = false; LinearNode<T> previous, current; T result = null; if (isEmpty()) throw new EmptyCollectionException("Set"); if (contents.getElement().equals(target)) { result = contents.getElement(); contents = contents.getNext(); } else { previous = contents; current = contents.getNext(); 15.5 Maps and the Java Collections API for (int look=0; look < count && !found; look++) if (current.getElement().equals(target)) found = true; else { previous = current; current = current.getNext(); } if (!found) throw new NoSuchElementException(); result = current.getElement(); previous.setNext(current.getNext()); } count--; return result; } There is always the possibility that the target element will not be found in the collection. In that case, a NoSuchElementException is thrown. If the exception is not thrown, the element found is stored so that it can be returned at the end of the method. Similarly to the removeRandom method, this method must traverse the list to find the element to be removed resulting in a time complexity of O(n). Other Operations The remaining methods are similar to their counterparts in the ArraySet<T> class from the previous section, and are left as programming projects. 15.5 Maps and the Java Collections API Maps are very similar to sets with the exception that the elements to be stored in the collection are separated into a key element and the associated data. Then only the key, and a reference to its associated data, are stored in the collection. This provides a couple of potential advantages. First, the data can then be part of multiple collections without duplication. Second, the collections themselves can be much smaller given that they only contain keys. In the terminology of the Java Collections API, all of the collections that we have discussed thus far could be considered sets (except that sets do not allow duplicates), because the data or element 459 460 C HA PT ER 15 Sets and Maps KE Y CO N C E PT The difference between a set and map is that a set stores the key and the data together while a map separates the key from the data and only stores the key and a reference to the data. stored in each collection contains all of the data associated with that object. For example, if we were creating an ordered list of employees, ordered by name, then we would have created an employee object that contained all of the data for each employee, including the name and a compareTo method to test the name, and we would have used our operations for an ordered list to add those employees into the list. Modifying our set implementations to create map implementations is left as a programming project. However, in this same scenario, if we wanted to create an ordered list that is a map, we would have created a class to represent the name of each employee and a reference that would point to a second class that contains all of the rest of the employee data. We would have then used our ordered list operations to load the first class into our list, while the objects of the second class could exist anywhere in memory. The first class in this case is referred to as the key, whereas the second class is referred to as the data. In this way, as we manipulate elements of the list, we are only dealing with the key, the name, and the reference, which is a much smaller segment of memory than if we were manipulating all of the data associated with an employee. We also have the advantage that the same employee data could be referenced by multiple maps without having to make multiple copies. Thus, if for one application we wanted to represent employees in a stack collection while for another application we needed to represent employees as an ordered list, we could load keys into a stack and load matching keys into an ordered list while only having one instance of the actual data. Like any situation dealing with aliases (i.e., multiple references to the same object), we must be careful that changes to an object through one reference affect the object referenced by all of the other references because there is only one instance of the object. In Chapter 10, we discussed the Java Collections API implementations of TreeSet and TreeMap. In Chapter 14, we discussed the Java Collections API im- plementations of sets and maps using hashing. Note that these implementations of a set violate the algebraic notion of a set without any specific ordering to its elements. In addition to these, there are a number of other classes that implement either the java.util.Set or java.util.Map interfaces. So why, in many cases, would the creators of the Java language use sets and maps in place of the traditional data structures we have discussed in this text. Simply put, Java was not designed specifically for education. Instead, it was designed to solve industrial problems quickly and efficiently. Therefore many of the Java KE Y CO N C E PT solutions can be described as expedient rather than elegant. In the Java Collections API, sets and maps are interfaces with a wide variety of implementations. Of course, the opposite question is equally important. If the creators of Java have chosen to use sets and maps in place of many of the traditional data structures presented in this text, why have we 15.5 Maps and the Java Collections API spent so much time talking about traditional data structures? Although there are many good answers to this question, there is one answer that supersedes all others. Although Java is a good language, it is unlikely that it is the only language you will encounter in your career. Languages have come and gone many times in the short history of our profession, but the foundational principles of data structures have remained the same. It is important that we all learn these foundational principles so that we do not have to repeat the failures of the past. 461 462 C HA PT ER 15 Sets and Maps Summary of Key Concepts ■ A set is a nonlinear collection in which there is essentially no inherent organization to the elements in the collection. ■ The implementation of the collection operations should not affect the way users interact with the collection. ■ In the Java Collections API and throughout this text, class names indicate both the underlying data structure and the collection. ■ The difference between a set and map is that a set stores the key and the data together while a map separates the key from the data and only stores the key and a reference to the data. ■ In the Java Collections API, sets and maps are interfaces with a wide variety of implementations. Self-Review Questions SR 15.1 What is a set? SR 15.2 What would the time complexity be for the size operation if there were not a count variable? SR 15.3 What would the time complexity be for the add operation if there were not a count variable? SR 15.4 What do the LinkedSet<T> and ArraySet<T> classes have in common? SR 15.5 What would be the time complexity of the add operation if we chose to add at the end of the list instead of the front? SR 15.6 What is the difference between a set and a map? SR 15.7 What are the potential advantages of a map over a set? Exercises EX 15.1 Define the concept of a set. List additional operations that might be considered for a set. EX 15.2 List each occurrence of one method in the ArraySet<T> class calling on another method from the same class. Why is this good programming practice? Programming Projects EX 15.3 Write an algorithm for the add method that would place each new element in position 0 of the array. What would the time complexity be for this algorithm? EX 15.4 A bag is a very similar construct to a set except that duplicates are allowed in a bag. What changes would have to be made to our methods to create an implementation of a bag? EX 15.5 Draw a UML diagram showing the relationships among the classes involved in the linked list implementation of a set. EX 15.6 Write an algorithm for the add method that will add at the end of the list instead of the beginning. What is the time complexity of this algorithm? EX 15.7 Modify the algorithm from the previous exercise so that it makes use of a rear reference. How does this affect the time complexity of this and the other operations? EX 15.8 Discuss the effect on all the operations if there were not a count variable in the implementation. EX 15.9 Discuss the impact (and draw an example) of using a dummy record at the head of the list. Programming Projects PP 15.1 Complete the implementation of the ArraySet<T> class by providing the definitions for the size, isEmpty, iterator and toString methods. PP 15.2 Complete the implementation of the LinkedSet<T> class by providing the definitions for the size, isEmpty, addAll, union, contains, equals, iterator, and toString methods. PP 15.3 Modify the ArraySet<T> class such that it puts the user in control of the set’s capacity. Eliminate the automatic expansion of the array. The revised class should throw a FullCollectionException when an element is added to a full set. Add a method called isFull that returns true if the set is full. And add a method that the user can call to expand the capacity by a particular number of cells. PP 15.4 An additional operation that might be implemented for a set is difference. This operation would take a set as a parameter and subtract the contents of that set from the current set if they exist in the current set. The result would be returned in a new set. 463 464 C HA PT ER 15 Sets and Maps Implement this operation for an array implementation of a set. Be careful to consider possible exceptional situations. PP 15.5 PP 15.6 Implement the difference operation from the previous project for a linked implementation of a set. Another operation that might be implemented for a set is intersection. This operation would take a set as a parameter and would return a set containing those elements that exist in both sets. Implement this operation for an array implementation of a set. PP 15.7 Implement the intersection operation from the previous project for a linked implementation of a set. PP 15.8 Another operation that might be implemented for a set is count. This operation would take an element as a parameter and return the number of copies of that element in the set. Implement this operation for an array implementation of a set. Be careful to consider possible exceptional situations. PP 15.9 Implement the count operation from the previous project for a linked implementation of a set. PP 15.10 A bag is a very similar construct to a set except that duplicates are allowed in a bag. Implement a bag collection by creating both a BagADT<T> interface and an ArrayBag<T> class. Include the additional operations described in the earlier projects. PP 15.11 Implement a bag collection by creating both a BagADT<T> interface and a LinkedBag<T> class. Include the additional operations described in the earlier projects. PP 15.12 Create a new version of the LinkedSet<T> class that makes use of a dummy record at the head of the list. PP 15.13 Create a simple graphical application that will allow a user to perform add, remove, and removeRandom operations on a set and display the resulting set (using toString) in a text area. PP 15.14 Use the array implementation of a set presented in this chapter as a guide to create an array implementation of a map. PP 15.15 Use the linked implementation of a set presented in this chapter as a guide to create a linked implementation of a map. Answers to Self-Review Questions SRA 15.1 A set is a collection in which there is no particular order or relationship among the elements in the collection. Answers to Self-Review Questions SRA 15.2 Without a count variable, the most likely solution would be to traverse the array using a while loop, counting as you go, until you encounter the first null element of the array. Thus, this operation would be O(n). SRA 15.3 Without a count variable, the most likely solution would be to traverse the array using a while loop until you encounter the first null element of the array. The new element would then be added into this position. Thus, this operation would be O(n). SRA 15.4 Both the LinkedSet<T> and ArraySet<T> classes implement the SetADT<T> interface. This means that they both represent a set collection, providing the necessary operations needed to use a set. Though they both have distinct approaches to managing the collection, they are functionally interchangeable from the user’s point of view. SRA 15.5 To add at the end of the list, we would have to traverse the list to reach the last element. This traversal would cause the time complexity to be O(n). An alternative would be to modify the solution to add a rear reference that always pointed to the last element in the list. This would help the time complexity for add but would have consequences if we try to remove the last element. SRA 15.6 A set contains all of the data associated with the elements that are stored within it whereas a map contains only a key and a reference to the rest of the data that is stored elsewhere. SRA 15.7 First, the data can then be part of multiple collections without duplication. Second, the collections themselves can be much smaller given that they only contain keys. 465 This page intentionally left blank Appendix UML A 467 468 APPENDI X A UML The Unified Modeling Language (UML) Software engineering deals with the analysis, synthesis, and communication of ideas in the development of software systems. In order to facilitate the methods and practices necessary to accomplish these goals, software engineers have developed a wide variety of notations to capture and communicate information. Although numerous notations are available, only a few have become popular, one of which in particular has become a de facto standard in the industry. The Unified Modeling Language (UML) was developed in the mid-1990s, but is actually the synthesis of three separate and longThe Unified Modeling Language (UML) provides a notation with which standing design notations, each popular in its own right. We use we can capture and illustrate proUML notation throughout this book to illustrate program designs, gram designs. and this section describes the key aspects of UML diagrams. Keep in mind that UML is language-independent. It uses generic terms and contains some features that are not relevant to the Java programming language. We focus on aspects of UML that are particularly appropriate for its use in this book. KE Y C O N C E PT UML is an object-oriented modeling language. It provides a convenient way to represent the relationships among classes and objects in a software system. We provide an overview of UML here, and use it throughout the book. The details of the underlying object-oriented concepts are discussed in Appendix B. UML Class Diagrams A UML class diagram describes the classes in the system, the static relationships among them, the attributes and operations associated with a class, and the constraints on the connections among objects. The terms “attribute” and “operation” are generic object-oriented terms. An attribute is any class-level data including variables and constants. An operation is essentially equivalent to a method. A class is represented in UML by a rectangle, usually divided into three sections containing the class name, its attributes, and its operations. Figure A.1 LibraryItem title callNumber checkout() return() F I G U R E A . 1 LibraryItem class diagram AP P END I X A UML 469 illustrates a class named LibraryItem. There are two attributes associated with the class, title and callNumber, and there are two operations associated with the class, checkout and return. In the notation for a class, the attributes and operations are optional. Therefore, a class may be represented by a single rectangle containing only the class name, if desired. We can include the attributes and/or operations whenever they help convey important information in the diagram. If attributes or operations are included, then both sections are shown (though not necessarily filled) to make it clear which is which. There are many additional pieces of information that can be included in the UML class notation. An annotation bracketed using < and > is called a stereotype in UML terminology. The <abstract> stereotype or the <interface> stereotype could be added above the name to indicate that it is representing an abstract class or an interface. The visibility of a class is assumed to be public by default, though nonpublic classes can be identified using a property string in curly braces, such as {private}. Attributes listed in a class can also provide several pieces of additional information. The full syntax for showing an attribute is visibility name : type = default-value The visibility may be spelled out as public, protected, or private, or you may use the symbols ⫹ to represent public visibility, # for protected visibility, or - for private visibility. For example, we might have listed the title of a LibraryItem as - title : String indicating that the attribute title is a private variable of type String. A default value is not provided in this case. Also, the stereotype <final> may be added to an attribute to indicate that it is a constant. Similarly, the full syntax for an operation is visibility name (parameter-list) : return-type {property-string} As with the syntax for attributes, all of the items other than the name are optional. The visibility modifiers are the same as they are for attributes. The parameter-list can include the name and type of each parameter, separated by a colon. The return-type is the type of the value returned from the operation. KEY CON CEPT Various kinds of relationships can be represented in a UML class diagram. UML Relationships There are several kinds of relationships among classes that UML diagrams can represent. Usually they are shown as lines or arrows connecting one class to 470 APPENDI X A UML LibraryItem title callNumber checkout() return() Book author publisher Video producer studio F I G U R E A . 2 A UML class diagram showing inheritance relationships another. Specific types of lines and arrowheads have specific meaning in UML. KE Y C O N C E PT One type of relationship shown between two classes in a UML diagram is an inheritance relationship. Figure A.2 shows two classes that are derived from the LibraryItem class. Inheritance is shown using an arrow with an open arrowhead pointing from the child class to the parent class. This example shows that both the Book class and the Video class inherit all of the attributes and operations of LibraryItem, but they also extend that definition with attributes of their own. Note that in this example, neither subclass has any additional operations other than those provided in the parent class. The inheritance relationship is indicative of one class being derived from or being a child of the other class. KE Y C O N C E PT Another relationship shown in a UML diagram is an association, which represents relationships between instances (objects) of the classes. An association is indicated by a solid line between the two classes involved and can be annotated with the cardinality of the relationship on either side. For example, Figure A.3 shows an association between a LibraryCustomer and a LibraryItem. The cardinality of 0..* means “zero or more,” in this case indicating that any given library customer may check out 0 or more items, and that any given library item may be The association relationship represents relationships between instances of classes. AP P END I X A LibraryCustomer name address UML 471 LibraryItem 0..* 0..* register() deregister() title callNumber checkout() return() F I G U R E A . 3 A UML class diagram showing an association checked out by multiple customers. The cardinality of an association may indicate other relationships, such as an exact number or a specific range. For example, if a customer is allowed to check out no more than five items, the cardinality could have been indicated by 0..5. A third type of relationship between classes is the concept of aggregation. This is the situation in which one class is essentially made up, at least in part, of other classes. For example, we can extend our library example to show a CourseMaterials class that is made up of books, course notes, and videos, as shown in Figure A.4. Aggregation is shown by using an open diamond on the aggregate end of the relationship. A fourth type of relationship that we may wish to represent is the concept of implementation. This relationship occurs between an interface and any class that implements that interface. Figure A.5 shows an interface called Copyrighted that contains two abstract CourseMaterials courseTitle courseNumber Book author publisher Video producer studio CourseNotes author format F I G U R E A . 4 One class shown as an aggregate of other classes KEY CON CEPT The aggregation relationship represents one class being made up of other classes. KEY CON CEPT The implementation relationship represents a class implementing an interface. 472 APPENDI X A UML <<interface>> Copyrighted Book author publisher setCopyright() getCopyright() F I G U R E A . 5 A UML diagram showing a class implementing an interface methods. The dotted arrow with the open arrowhead indicates that the Book class implements the Copyrighted interface. KE Y C O N C E PT The uses relationship represents one class using another. A fifth type of relationship between classes is the concept of one class using another. Examples of this concept include such things as an instructor using a chalkboard, a driver using a car, or a library customer using a computer. Figure A.6 illustrates this relationship, showing that a LibraryCustomer might use a Computer. The uses relationship is indicated by a dotted line with an open arrowhead that is usually annotated with the nature of the relationship. LibraryCustomer name address register() deregister() Computer searches online catalog location ipAddress logon() logoff() F I G U R E A . 6 One class indicating its use of another Summary of Key Concepts Summary of Key Concepts ■ The Unified Modeling Language (UML) provides a notation with which we can capture and illustrate program designs. ■ Various kinds of relationships can be represented in a UML class diagram. ■ The inheritance relationship is indicative of one class being derived from or being a child of the other class. ■ The association relationship represents relationships between instances of classes. ■ The aggregation relationship represents one class being made up of other classes. ■ The implementation relationship represents a class implementing an interface. ■ The uses relationship represents one class using another. Self-Review Questions SR A.1 What does a UML class diagram represent? SR A.2 What are the different types of relationships represented in a class diagram? Exercises EX A.1 Create a UML class diagram for the organization of a university, where the university is made up of colleges, which are made up of departments, which contain faculty and students. EX A.2 Complete the UML class description for a library system outlined in this chapter. EX A.3 List and illustrate an example of each of the relationships discussed in this chapter. Answers to Self-Review Questions SRA A.1 A class diagram describes the types of objects or classes in the system, the static relationships among them, the attributes and operations of a class, and the constraints on the connections among objects. SRA A.2 Relationships shown in a UML class diagram include subtypes or extensions, associations, aggregates, and the implementation of interfaces. 473 This page intentionally left blank Appendix Object-Oriented Design B 475 476 APPENDI X B Object-Oriented Design B.1 Overview of Object-Orientation Java is an object-oriented language. As the name implies, an object is a fundamental entity in a Java program. In addition to objects, a Java program also manages primitive data. Primitive data includes common, fundamental values such as numbers and characters. An object usually represents something more specialized or complex, such as a bank account. An object often contains primitive values and is in part defined by them. For example, an object that represents a bank account might contain the account balance, which is stored as a primitive numeric value. An object is defined by a class, which can be thought of as the data type of the object. The operations that can be performed on the object are defined by the methods in the class. Once a class has been defined, multiple objects can be created from that class. For example, once we define a class to represent the concept of a bank account, we can create multiple objects that represent specific, individual bank accounts. Each bank account object would keep track of its own balance. This is an example of encapsulation, meaning that each object protects and manages its own information. The methods defined in the bank account class would allow us to perform operations on individual bank account objects. For instance, we might withdraw money from a particular account. We can think of these operations as services that the object performs. The act of invoking a method on an object is sometimes referred to as sending a message to the object, requesting that the service be performed. Classes can be created from other classes using inheritance. That is, the definition of one class can be based on another class that already exists. Inheritance is a form of software reuse, capitalizing on the similarities between various kinds of classes that we may want to create. One class can be used to derive several new classes. Derived classes can then be used to derive even more classes. This creates a hierarchy of classes, where characteristics defined in one class are inherited by its children, which in turn pass them on to their children, and so on. For example, we might create a hierarchy of classes that represent various types of accounts. Common characteristics are defined in high-level classes, and specific differences are defined in derived classes. Classes, objects, encapsulation, and inheritance are the primary ideas that make up the world of object-oriented software. They are depicted in Figure B.1 and are explored in more detail throughout this appendix. B.2 Using Objects The following println statement illustrates the process of using an object for the services it provides: System.out.println ("Whatever you are, be a good one."); B.2 A class defines a concept Multiple encapsulated objects can be created from one class Bank Account John’s Bank Account Balance: $5,257 Classes can be organized into inheritance hierarchies Bill’s Bank Account Balance: $1,245,069 Account Mary’s Bank Account Balance: $16,833 Charge Account Bank Account Savings Account Checking Account F I G U R E B . 1 Various aspects of object-oriented software The System.out object represents an output device or file, which by default is the monitor screen. To be more precise, the object’s name is out, and it is stored in the System class. The println method represents a service that the System.out object performs for us. Whenever we request it, the object will print a string of characters to the screen. We can say that we send the println message to the System.out object to request that some text be printed. Abstraction An object is an abstraction, meaning that the precise details of how it works are irrelevant from the point of view of the user of the object. We don’t really need to know how the println method prints characters to the screen, as long as we can count on it to do its job. Of course, there are times when it is helpful to understand such information, but it is not necessary in order to use the object. Sometimes it is important to hide or ignore certain details. A human being is capable of mentally managing around seven (plus or minus two) pieces of information Using Objects 477 478 APPENDI X B Object-Oriented Design in short-term memory. Beyond that, we start to lose track of some of the pieces. However, if we group pieces of information together, then those pieces can be managed as one “chunk” in our minds. We don’t actively deal with all of the details in the chunk, but we can still manage it as a single entity. Therefore, we can deal with large quantities of information by organizing it into chunks. An object is a construct that organizes information and allows us to hide the details inside. An object is therefore a wonderful abstraction. We use abstractions every day. Think about a car for a moment. You don’t necessarily need to know how a four-cycle combustion engine works in order to drive a car. You just need to know some basic operations: how to turn it on, how to put it in gear, how to make it move with the pedals and steering wheel, and how to stop it. These operations define the way a person interacts with the car. They mask the details of what is happening inside the car that allow it to function. When you are driving a car, you are not usually thinking about the spark plugs igniting the gasoline that drives the piston that turns the crankshaft that turns the axle that turns the wheels. If we had to worry about all of these underlying details, we would never be able to operate something as complicated as a car. Initially, all cars had manual transmissions. The driver had to understand and deal with the details of changing gears with the stick shift. Eventually, automatic transmissions were developed, and the driver no longer had to worry about shifting gears. Those details were hidden by raising the level of abstraction. Of course, someone has to deal with the details. The car manufacturer has to know the details in order to design and build the car in the first place. A car mechanic relies on the fact that most people don’t have the expertise or tools necessary to fix a car when it breaks. KE Y CO N C E PT An abstraction hides details. A good abstraction hides the right details at the right time so that we can manage complexity. The level of abstraction must be appropriate for each situation. Some people prefer to drive a manual transmission car. A race car driver, for instance, needs to control the shifting manually for optimum performance. Likewise, someone has to create the code for the objects we use. Later in this chapter, we explore how to define objects by creating classes. For now, we can create and use objects from classes that have been defined for us already. Abstraction makes that possible. Creating Objects A Java variable can hold either a primitive value or a reference to an object. Like variables that hold primitive types, a variable that serves as an object reference must be declared. A class is used to define an object, and the class name can be thought of as the type of an object. The declarations of object references have a similar structure to the declarations of primitive variables. B.2 Using Objects 479 The following declaration creates a reference to a String object: String name; That declaration is like the declaration of an integer, in that the type is followed by the variable name we want to use. However, no String object actually exists yet. To create an object, we use the new operator: name = new String ("James Gosling"); The act of creating an object by using the new operator is called instantiation. An object is said to be an instance of a particular class. After the new operator creates the object, a constructor is invoked to help set it up initially. A constructor has the same name as the class, and is similar to a method. In this example, the parameter to the constructor is a string literal that specifies the characters that the String object will hold. The acts of declaring the object reference variable and creating the object itself can be combined into one step by initializing the variable in the declaration, just as we do with primitive types: KEY CON CEPT The new operator returns a reference to a newly created object. String name = new String ("James Gosling"); After an object has been instantiated, we use the dot operator to access its methods. The dot operator is appended directly after the object reference, followed by the method being invoked. For example, to invoke the length method defined in the String class, we use the dot operator on the name reference variable: count = name.length(); An object reference variable (such as name) actually stores the address where the object is stored in memory. However, we don’t usually care about the actual address value. We just want to access the object, wherever it is. Even though they are not primitive types, strings are so fundamental and frequently used that Java defines string literals delimited by double quotation marks, as we have seen in various examples. This is a shortcut notation. Whenever a string literal appears, a String object is created. Therefore, the following declaration is valid: String name = "James Gosling"; That is, for String objects, the explicit use of the new operator, and the call to the constructor can be eliminated. In most cases, this simplified syntax for strings is used. 480 APPENDI X B Object-Oriented Design B.3 Class Libraries and Packages A class library is a set of classes that supports the development of programs. A compiler often comes with a class library. Class libraries can also be obtained separately through third-party vendors. The classes in a class library contain methods that are often valuable to a programmer because of the special functionality they offer. In fact, programmers often become dependent on the methods in a class library and begin to think of them as part of the language. But, technically, they are not in the language definition. The String class, for instance, is not an inherent part of the Java language. It is part of the Java standard class library that can be found in any Java development environment. The classes that make up the library were created by employees at Sun Microsystems, the company that created the Java language. K E Y CO N C E PT The Java standard class library is a useful set of classes that anyone can use when writing Java programs. K E Y CO N C E PT A package is a Java language element used to group related classes under a common name. The class library is made up of several clusters of related classes, which are sometimes called Java APIs. API stands for application programmer interface. For example, we may refer to the Java Database API when we are talking about the set of classes that helps us to write programs that interact with a database. Another example of an API is the Java Swing API, which refers to a set of classes that defines special graphical components used in a graphical user interface. Sometimes the entire standard library is referred to generically as the Java API. The classes of the Java standard class library are also grouped into packages, which, like the APIs, let us group related classes by one name. Each class is part of a particular package. The String class and the System class, for example, are both part of the java.lang package. The package organization is more fundamental and language-based than the API names. Though there is a general correspondence between package and API names, the groups of classes that make up a given API might cross packages. We primarily refer to classes in terms of their package organization in this text. The import Declaration The classes of the package java.lang are automatically available for use when writing a program. To use classes from any other package, however, we must either fully qualify the reference or use an import declaration. When you want to use a class from a class library in a program, you could use its fully qualified name, including the package name, every time it is referenced. For example, every time you want to refer to the Random class that is defined in the java.util package, you can write java.util.Random. However, completely B.4 State and Behavior specifying the package and class name every time it is needed quickly becomes tiring. Java provides the import declaration to simplify these references. The import declaration identifies the packages and classes that will be used in a program, so that the fully qualified name is not necessary with each reference. The following is an example of an import declaration: import java.util.Random; This declaration asserts that the Random class of the java.util package may be used in the program. Once this import declaration is made, it is sufficient to use the simple name Random when referring to that class in the program. Another form of the import declaration uses an asterisk (*) to indicate that any class inside the package might be used in the program. Therefore, the declaration import java.util.*; allows all classes in the java.util package to be referenced in the program without the explicit package name. If only one class of a particular package will be used in a program, it is usually better to name the class specifically in the import declaration. However, if two or more classes will be used, the * notation is fine. Once a class is imported, it is as if its code has been brought into the program. The code is not actually moved, but that is the effect. The classes of the java.lang package are automatically imported because they are fundamental and can be thought of as basic extensions to the language. Therefore, any class in the java.lang package, such as String, can be used without an explicit import declaration. It is as if all programs automatically contain the following declaration: import java.lang.*; B.4 State and Behavior Think about objects in the world around you. How would you describe them? Let’s use a ball as an example. A ball has particular characteristics such as its diameter, color, and elasticity. Formally, we say the properties that describe an object, called attributes, define the object’s state of being. We also describe a ball by what it does, such as the fact that it can be thrown, bounced, or rolled. These activities define the object’s behavior. All objects have a state and a set of behaviors. We can represent these characteristics in software objects as well. The values of an object’s variables describe the object’s state, and the methods that can be invoked using the object define the object’s behaviors. 481 482 APPENDI X B Object-Oriented Design KE Y CO N C E PT Each object has a state and a set of behaviors. The values of an object’s variables define its state. The methods to which an object responds define its behaviors. Consider a computer game that uses a ball. The ball could be represented as an object. It could have variables to store its size and location, and methods that draw it on the screen and calculate how it moves when thrown, bounced, or rolled. The variables and methods defined in the ball object establish the state and behavior that are relevant to the ball’s use in the computerized ball game. Each object has its own state. Each ball object has a particular location, for instance, which typically is different from the location of all other balls. Behaviors, though, tend to apply to all objects of a particular type. For instance, in general, any ball can be thrown, bounced, or rolled. The act of rolling a ball is generally the same for all balls. The state of an object and that object’s behaviors work together. How high a ball bounces depends on its elasticity. The action is the same, but the specific result depends on that particular object’s state. An object’s behavior often modifies its state. For example, when a ball is rolled, its location changes. Any object can be described in terms of its state and behavior. Let’s consider another example. In software that is used to manage a university, a student could be represented as an object. The collection of all such objects represents the entire student body at the university. Each student has a state. That is, each student object contains the variables that store information about a particular student, such as name, address, major, courses taken, grades, and grade point average. A student object also has behaviors. For example, the class of the student object may contain a method to add a new course. Although software objects often represent tangible items, they don’t have to. For example, an error message can be an object, with its state being the text of the message, and behaviors including the process of issuing (perhaps printing) the error. A common mistake made by new programmers to the world of object-orientation is to limit the possibilities to tangible entities. B.5 Classes An object is defined by a class. A class is the model, pattern, or blueprint from which an object is created. Consider the blueprint created by an architect when designing a house. The blueprint defines the important characteristics of the house: walls, windows, doors, electrical outlets, and so forth. Once the blueprint is created, several houses can be built using it. In one sense, the houses built from the blueprint are different. They are in different locations, have different addresses, contain different furniture, and different people live in them. Yet, in many ways they are the “same” house. The layout of the rooms and other crucial characteristics are the same in each. To create a different house, we would need a different blueprint. B.5 int x, y, diameter; character type; double elasticity; Classes 483 Data declarations Method declarations F I G U R E B . 2 The members of a class: data and method declarations A class is a blueprint of an object. But a class is not an object any more than a blueprint is a house. In general, no space to store data values is reserved in a class. To allocate space to store data values, we have to instantiate one or more objects from the class (static data is the exception to this rule KEY CON CEPT and is discussed later in this chapter). Each object is an instance of a A class is a blueprint for an object; it class. Each object has space for its own data, which is why each object reserves no memory space for data. Each object has its own data space, can have its own state. and thus its own state. A class contains the declarations of the data that will be stored in each instantiated object, and the declarations of the methods that can be invoked using an object. Collectively these are called the members of the class. See Figure B.2. Consider the class shown in Listing B.1, called Coin, that represents a coin that can be flipped and that at any point in time shows a face of either heads or tails. In the Coin class, we have two integer constants, HEADS and TAILS, and one integer variable, face. The rest of the Coin class is composed of the Coin constructor and three regular methods: flip, isHeads, and toString. Constructors are special methods that have the same name as the class. The Coin constructor gets called when the new operator is used to create a new instance of the Coin class. The rest of the methods in the Coin class define the various services provided by Coin objects. A class we define can be used in multiple programs. This is no different from using the String class in whatever program we need it. When designing a class, it is always good to look to the future to try to give the class behaviors that may be beneficial in other programs, not just fit the specific purpose for which you are creating it at the moment. 484 APPENDI X B L I S T I N G Object-Oriented Design B . 1 /** * Coin represents a coin with two sides that can be flipped. * * @author Lewis * @author Chase * @version 1.0, 8/18/08 */ public class Coin { private final int HEADS = 0; private final int TAILS = 1; private int face; /** * Sets up the coin by flipping it initially. */ public Coin () { flip(); } /** * Flips the coin by randomly choosing a face value. */ public void flip () { face = (int) (Math.random() * 2); } /** * Returns true if the current face of the coin is heads. * * @return true if the face is heads */ public boolean isHeads () { return (face == HEADS); } B.5 L I S T I N G B . 1 Classes 485 continued /** * Returns the current face of the coin as a string. * * @return the string representation of the current face value of this coin */ public String toString() { String faceName; if (face == HEADS) faceName = "Heads"; else faceName = "Tails"; return faceName; } } Instance Data KEY CON CEPT Note that in the Coin class, the constants HEADS and TAILS and The scope of a variable, which deterthe variable face are declared inside the class, but not inside any mines where it can be referenced, demethod. The location at which a variable is declared defines its pends on where it is declared. scope, which is the area within a program in which that variable can be referenced. By being declared at the class level (not within a method), these variables and constants can be referenced in any method of the class. Attributes declared at the class level are also called instance data, because memory space for the data is reserved for each instance of the class that is created. Each Coin object, for example, has its own face variable with its own data space. Therefore, at any point in time two Coin objects can have their own states: one can be showing heads and the other can be showing tails, perhaps. Java automatically initializes any variables declared at the class level. For example, all variables of numeric types such as int and double are initialized to zero. However, despite the fact that the language performs this automatic initialization, it is good practice to initialize variables explicitly (usually in a constructor) so that anyone reading the code will clearly understand the intent. 486 APPENDI X B Object-Oriented Design B.6 Encapsulation We can think about an object in one of two ways. The view we take depends on what we are trying to accomplish at the moment. First, when we are designing and implementing an object, we need to think about the details of how an object works. That is, we have to design the class; we have to define the variables that will be held in the object and define the methods that make the object useful. However, when we are designing a solution to a larger problem, we have to think in terms of how the objects in the program interact. At that level, we have to think only about the services that an object provides, not about the details of how those services are provided. As we discussed earlier in this chapter, an object provides a level of abstraction that allows us to focus on the larger picture when we need to. KE Y CO N C E PT Objects should be encapsulated. The rest of a program should interact with an object only through a welldefined interface. This abstraction works only if we are careful to respect its boundaries. An object should be self-governing, which means that the variables contained in an object should be modified only within the object. Only the methods within an object should have access to the variables in that object. We should make it difficult, if not impossible, for code outside of a class to “reach in” and change the value of a variable that is declared inside the class. The object-oriented term for this characteristic is encapsulation. An object should be encapsulated from the rest of the system. It should interact with other parts of a program only through the specific set of methods that define the services provided by that object. These methods define the interface between that object and the program that uses it. The code that uses an object, sometimes called the client of an object, should not be allowed to access variables directly. The client should interact with the object’s methods, which in turn interact on behalf of the client with the data encapsulated within the object. Visibility Modifiers In Java, we accomplish object encapsulation using modifiers. A modifier is a Java reserved word that is used to specify particular characteristics of a programming language construct. For example, the final modifier is used to declare a constant. Java has several modifiers that can be used in various ways. Some modifiers can be used together, but some combinations are invalid. Some Java modifiers are called visibility modifiers because they control access to the members of a class. The reserved words public and private are visibility modifiers that can be applied to the variables and methods of a class. If a member of a class has public visibility, then it can be directly referenced from outside of the object. If a B.6 Encapsulation 487 member of a class has private visibility, it can be used anywhere inside the class definition but cannot be referenced externally. A third visibility modifier, protected, is relevant only in the context of inheritance, which is discussed later in this chapter. Public variables violate encapsulation. They allow code external to the class in which the data is defined to reach in and access or modify the value of the data. Therefore, instance data should be defined with private visibility. Data that is declared as private can be accessed only by the methods of the class, which makes the objects created from that class self-governing. Which visibility we apply to a method depends on the purpose of that method. Methods that provide services to the client of the class must be declared with public visibility so that they can be invoked by the client. These methods are sometimes referred to as service methods. A private method cannot be KEY CON CEPT invoked from outside the class. The only purpose of a private method Instance variables should be declared is to help the other methods of the class do their job. Therefore, priwith private visibility to promote encapsulation. vate methods are sometimes referred to as support methods. The table in Figure B.3 summarizes the effects of public and private visibility on both variables and methods. Note that a client can still access or modify private data by invoking service methods that change the data. A class must provide service methods for valid client operations. The code of those methods must be carefully designed to permit only appropriate access and valid changes. Giving constants public visibility is generally considered acceptable. Although their values can be accessed directly, they cannot be changed because they were declared using the final modifier. Keep in mind that encapsulation means that data values should not be able to be changed directly by another part of the code. Because constants, by definition, cannot be changed, the encapsulation issue is largely moot. public private Variables Violate encapsulation Enforce encapsulation Methods Provide services to clients Support other methods in the class F I G U R E B . 3 The effects of public and private visibility 488 APPENDI X B Object-Oriented Design UML diagrams reflect the visibility of a class member with special notations. A member with public visibility is preceded by a plus sign (+), and a member with private visibility is preceded by a minus sign (⫺). Local Data As we defined earlier, the scope of a variable or constant is the part of a program in which a valid reference to that variable can be made. A variable can be declared inside a method, making it local data as opposed to instance data. K E Y CO N C E PT Recall that instance data is declared in a class but not inside any parA variable declared in a method is ticular method. Local data has scope limited to only the method in local to that method and cannot be which it is declared. Any reference to local data of one method in any used outside of it. other method would cause the compiler to issue an error message. A local variable simply does not exist outside of the method in which it is declared. Instance data, declared at the class level, has a scope of the entire class. Any method of the class can refer to it. Because local data and instance data operate at different levels of scope, it’s possible to declare a local variable inside a method by using the same name as an instance variable declared at the class level. Referring to that name in the method will reference the local version of the variable. This naming practice obviously has the potential to confuse anyone reading the code, so it should be avoided. The formal parameter names in a method header serve as local data for that method. They don’t exist until the method is called, and cease to exist when the method is exited. B.7 Constructors A constructor is similar to a method that is invoked when an object is instantiated. When we define a class, we usually define a constructor to help us set up the class. In particular, we often use a constructor to initialize the variables associated with each object. A constructor differs from a regular method in two ways. First, the name of a constructor is the same name as the class. Therefore, the name of the constructor in the Coin class is Coin, and the name of the constructor in the Account class is Account. Second, a constructor cannot return a value and does not have a return type specified in the method header. K E Y C O N C E PT A common mistake made by programmers is to put a void return type on a constructor. As far as the compiler is concerned, putting any return type on a constructor, even void, turns it into a regular method that happens to have the same name as the class. As such, it cannot be invoked as a constructor. This leads to error messages that are sometimes difficult to decipher. A constructor cannot have any return type, even void. B.8 Method Overloading A constructor is generally used to initialize the newly instantiated object. We don’t have to define a constructor for every class. Each class has a default constructor that takes no parameters and is used if we don’t provide our own. This default constructor generally has no effect on the newly created object. B.8 Method Overloading When a method is invoked, the flow of control transfers to the code that defines the method. After the method has been executed, control returns to the location of the call and processing continues. Often the method name is sufficient to indicate which method is being called by a specific invocation. But in Java, as in other object-oriented languages, you can use the same method name with different parameter lists for multiple methods. This technique is called method overloading. It is useful when you need to perform similar methods on different types of data. The compiler must still be able to associate each invocation to a specific method declaration. If the method name for two or more methods is the same, then additional information is used to uniquely identify the version that is being invoked. In Java, a method name can be used for multiple methods as long as the number of parameters, the types of those parameters, or the order of the types of parameters is distinct. A method’s name along with the number, type, KEY CON CEPT and order of its parameters is called the method’s signature. The comThe versions of an overloaded piler uses the complete method signature to bind a method invocamethod are distinguished by their signatures. The number, type, or tion to the appropriate definition. order of their parameters must be The compiler must be able to examine a method invocation, indistinct. cluding the parameter list, to determine which specific method is being invoked. If you attempt to specify two method names with the same signature, the compiler will issue an appropriate error message and will not create an executable program. There can be no ambiguity. Note that the return type of a method is not part of the method signature. That is, two overloaded methods cannot differ only by their return type. The reason is that the value returned by a method can be ignored by the invocation. The compiler would not be able to distinguish which version of an overloaded method is being referenced in such situations. The println method is an example of a method that is overloaded several times, each accepting a single type. Here is a partial list of its various signatures: > println > println > println > println >println (String s) (int i) (double d) (char c) (boolean b) 489 490 APPENDI X B Object-Oriented Design The following two lines of code actually invoke different methods that have the same name: System.out.println ("The total is: "); System.out.println (count); The first line invokes the println that accepts a string, and the second line, assuming count is an integer variable, invokes the version of println that accepts an integer. We often use a println statement that prints several distinct types, such as: System.out.println ("The total is: " + count); In this case, the plus sign is the string concatenation operator. First, the value in the variable count is converted to a string representation. Then the two strings are concatenated into one longer string, and the definition of println that accepts a single string is invoked. Constructors are primary candidates for overloading. By providing multiple versions of a constructor, we provide several ways to set up an object. B.9 References Revisited In previous examples, we have declared object reference variables through which we access particular objects. Let’s examine this relationship in more detail. K E Y CO N C E PT An object reference variable stores the address of an object. An object reference variable and an object are two separate things. Remember that the declaration of the reference variable and the creation of the object that it refers to are separate steps. We often declare the reference variable and create an object for it to refer to on the same line, but keep in mind that we don’t have to do so. In fact, in many cases, we won’t want to. The reference variable holds the address of an object even though the address never is disclosed to us. When we use the dot operator to invoke an object’s method, we are actually using the address in the reference variable to locate the representation of the object in memory, look up the appropriate method, and invoke it. The null Reference A reference variable that does not currently point to an object is called a null reference. When a reference variable is initially declared as an instance variable, it is a null reference. If we try to follow a null reference, a NullPointerException is thrown, indicating that there is no object to reference. For example, consider the following situation: B.9 References Revisited 491 class NameIsNull { String name; // not initialized, therefore null void printName() { System.out.println (name.length()); // causes an exception } } The declaration of the instance variable name asserts it to be a reference to a String object, but it doesn’t create any String object for it to refer to. The variable name, therefore, contains a null reference. When the method attempts to invoke the length method of the object to which name refers, an exception is thrown because no object exists to execute the method. Note that this situation can arise only in the case of instance variables. Suppose, for instance, the following two lines of code were in a method: String name; System.out.println (name.length()); In this case, the variable name is local to whatever method it is declared in. The compiler would complain that we were using the name variable before it had been initialized. In the case of instance variables, however, the compiler can’t determine whether a variable had been initialized or not. Therefore, the danger of attempting to follow a null reference is a problem. The identifier null is a reserved word in Java and represents a null reference. We can explicitly set a reference to null to ensure that it doesn’t point to any object. We can also use it to check whether a particular reference currently points to an object. For example, we could have used the following code in the printName method to keep us from following a null reference: KEY CON CEPT The reserved word null represents a reference that does not point to a valid object. if (name == null) System.out.println ("Invalid Name"); else System.out.println (name.length()); The this Reference Another special reference for Java objects is called the this reference. The word this is a reserved word in Java. It allows an object to refer to itself. As we have discussed, a method is always invoked through a particular object or class. Inside that method, the this reference can be used to refer to the currently executing object. 492 APPENDI X B Object-Oriented Design For example, in the ChessPiece class, there could be a method called move, which could contain the following line: K E Y CO N C E PT The this reference always refers to the currently executing object. if (this.position == piece2.position) result = false; In this situation, the this reference is being used to clarify which position is being referenced. The this reference refers to the object through which the method was invoked. So when the following line is used to invoke the method, the this reference refers to bishop1: bishop1.move(); But when another object is used to invoke the method, the this reference refers to it. Therefore, when the following invocation is used, the this reference in the move method refers to bishop2: bishop2.move(); The this reference can also be used to distinguish the parameters of a constructor from their corresponding instance variables with the same names. For example, the constructor of a class called Account could be defined as follows: public Account (String owner, long account, double initial) { name = owner; acctNumber = account; balance = initial; } In this constructor, we deliberately came up with different names for the parameters to distinguish them from the instance variables name, acctNumber, and balance. This distinction is arbitrary. The constructor could have been written as follows using the this reference: public Account (String name, long acctNumber, double balance) { this.name = name; this.acctNumber = acctNumber; this.balance = balance; } In this version of the constructor, the this reference specifically refers to the instance variables of the object. The variables on the right-hand side of the assignment statements refer to the formal parameters. This approach eliminates the need to come up with different yet equivalent names. This situation sometimes occurs in other methods, but comes up often in constructors. B.9 References Revisited 493 Aliases Because an object reference variable stores an address, programmers must be careful when managing objects. In particular, the semantics of an assignment statement for objects must be carefully understood. First, let’s review the concept of assignment for primitive types. Consider the following declarations of primitive data: int num1 = 5; int num2 = 12; In the following assignment statement, a copy of the value that is stored in num1 is stored in num2: num2 = num1; The original value of 12 in num2 is overwritten by the value 5. The variables num1 and num2 still refer to different locations in memory, and both of those locations now contain the value 5. Now consider the following object declarations: ChessPiece bishop1 = new ChessPiece(); ChessPiece bishop2 = new ChessPiece(); Initially, the references bishop1 and bishop2 refer to two different ChessPiece objects. The following assignment statement copies the value in bishop1 into bishop2: bishop2 = bishop1; The key issue is that when an assignment like this is made, the address stored in bishop1 is copied into bishop2. Originally the two references referred to different objects. After the assignment, both bishop1 and bishop2 contain the same address, and therefore refer to the same object. The bishop1 and bishop2 references are now aliases of each other, because they are two names that refer to the same object. All references to the object that was originally referenced by bishop2 are now gone; that object cannot be used again in the program. KEY CON CEPT Several references can refer to the same object. These references are aliases of each other. One important implication of aliases is that when we use one reference to change the state of the object, it is also changed for the other, because there is really only one object. If you change the state of bishop1, for instance, you change the state of bishop2, because they both refer to the same object. Aliases can produce undesirable effects unless they are managed carefully. Another important aspect of references is the way they affect how we determine if two objects are equal. The == operator that we use for primitive data can be used with object references, but it returns true only if the two references being compared 494 APPENDI X B Object-Oriented Design are aliases of each other. It does not “look inside” the objects to see if they contain the same data. KE Y CO N C E PT The == operator compares object references for equality, returning true if the references are aliases of each other. That is, the following expression is true only if bishop1 and bishop2 currently refer to the same object: bishop1 == bishop2 A method called equals is defined for all objects, but unless we replace it with a specific definition when we write a class, it has the same semantics as the == operator. That is, the equals method returns a boolean value that, by default, will be true if the two objects being compared are aliases of each other. The equals method is invoked through one object, and takes the other one as a parameter. Therefore, the expression bishop1.equals(bishop2) K E Y CO N C E PT The equals method can be defined to determine equality between objects in any way we consider appropriate. returns true if both references refer to the same object. However, we could define the equals method in the ChessPiece class to define equality for ChessPiece objects any way we would like. That is, we could define the equals method to return true under whatever conditions we think are appropriate to mean that one ChessPiece is equal to another. The equals method has been given an appropriate definition in the String class. When comparing two String objects, the equals method returns true only if both strings contain the same characters. A common mistake is to use the == operator to compare strings, which compares the references for equality, when most of the time we want to compare the characters in the strings for equality. The equals method is discussed in more detail later in this chapter. Garbage Collection All interaction with an object occurs through a reference variable, so we can use an object only if we have a reference to it. When all references to an object are lost (perhaps by reassignment), that object can no longer participate in the program. The program can no longer invoke its methods or use its variables. At this point the object is called garbage because it serves no useful purpose. K E Y CO N C E PT If an object has no references to it, a program cannot use it. Java performs automatic garbage collection by periodically reclaiming the memory space occupied by these objects. Java performs automatic garbage collection. When the last reference to an object is lost, the object becomes a candidate for garbage collection. Occasionally, the Java run time executes a method that “collects” all of the objects marked for garbage collection and returns their allocated memory to the system for future use. The programmer does not have to worry about explicitly returning memory that has become garbage. B. 1 0 The static Modifier If there is an activity that a programmer wants to accomplish in conjunction with the object being destroyed, the programmer can define a method called finalize in the object’s class. The finalize method takes no parameters and has a void return type. It will be executed by the Java run time after the object is marked for garbage collection and before it is actually destroyed. The finalize method is not often used because the garbage collector performs most normal cleanup operations. However, it is useful for performing activities that the garbage collector does not address, such as closing files. Passing Objects as Parameters Another important issue related to object references comes up when we want to pass an object to a method. Java passes all parameters to a method by value. That is, the current value of the actual parameter (in the invocation) is copied into the formal parameter in the method header. Essentially, parameter passing is like an assignment statement, assigning to the formal parameter a copy of the value stored in the actual parameter. This issue must be considered when making changes to a formal parameter inside a method. The formal parameter is a separate copy of the value that is passed in, so any changes made to it have no effect on the actual parameter. After control returns to the calling method, the actual parameter will have the same value as it did before the method was called. However, when we pass an object to a method, we are actually passing a reference to that object. The value that gets copied is the address of the object. Therefore, the formal parameter and the actual parameter become aliases of each other. If we change the state of the object through the formal parameter reference inside the method, we are changing the object referenced by the actual parameter, because they refer to the same object. On the other KEY CON CEPT hand, if we change the formal parameter reference itself (to make it When an object is passed to a point to a new object, for instance), we have not changed the fact method, the actual and formal parameters become aliases of each that the actual parameter still refers to the original object. other. B.10 The static Modifier We have seen how visibility modifiers allow us to specify the encapsulation characteristics of variables and methods in a class. Java has several other modifiers that determine other characteristics. For example, the static modifier associates a variable or method with its class rather than with an object of the class. Static Variables So far, we have seen two categories of variables: local variables, which are declared inside a method, and instance variables, which are declared in a class but not inside 495 496 APPENDI X B Object-Oriented Design a method. The term instance variable is used because an instance variable is accessed through a particular instance (an object) of a class. In general, each object has distinct memory space for each variable, so that each object can have a distinct value for that variable. Another kind of variable, called a static variable or class variable, is shared among all instances of a class. There is only one copy of a static variable for all objects of a class. Therefore, changing the value of a static variable in one object changes it for all of the others. The reserved word static is used as a modifier to declare a static variable: private static int count = 0; K E Y CO N C E PT A static variable is shared among all instances of a class. Memory space for a static variable is established when the class that contains it is referenced for the first time in a program. A local variable declared within a method cannot be static. Constants, which are declared using the final modifier, are also often declared using the static modifier as well. Because the value of constants cannot be changed, there might as well be only one copy of the value across all objects of the class. Static Methods A static method (also called a class method) can be invoked through the class name (all the methods of the Math class are static methods, for example). You don’t have to instantiate an object of the class to invoke a static method. For example, the sqrt method is called through the Math class as follows: System.out.println ("Square root of 27: " + Math.sqrt(27)); K E Y CO N C E PT A method is made static by using the static modifier in the method declaration. A method is made static by using the static modifier in the method declaration. As we have seen, the main method of a Java program must be declared with the static modifier; this is so that main can be executed by the interpreter without instantiating an object from the class that contains main. Because static methods do not operate in the context of a particular object, they cannot reference instance variables, which exist only in an instance of a class. The compiler will issue an error if a static method attempts to use a nonstatic variable. A static method can, however, reference static variables, because static variables exist independent of specific objects. Therefore, the main method can access only static or local variables. The methods in the Math class perform basic computations based on values passed as parameters. There is no object state to maintain in these situations; therefore, there is no good reason to force us to create an object in order to request these services. B. 1 1 B.11 Wrapper Classes 497 Wrapper Classes In some object-oriented programming languages, everything is represented using classes and the objects that are instantiated from them. In Java there are primitive types (such as int, double, char, and boolean) in addition to classes and objects. Having two categories of data to manage (primitive values and object references) can present a challenge in some circumstances. For example, we might create an object that serves as a collection to hold various types of other objects. But in a specific situation we want the collection to hold simple integer values. In these cases we need to “wrap” a primitive type into a class so that it can be treated as an object. A wrapper class represents a particular primitive type. For instance, the Integer class represents a simple integer value. An object created from the Integer class stores a single int value. The constructors of the wrapper classes accept the primitive value to store. For example: Integer ageObj = new Integer(45); Once this declaration and instantiation are performed, the ageObj object effectively represents the integer 45 as an object. It can be used wherever an object is called for in a program instead of a primitive type. For each primitive type in Java there exists a corresponding wrapper class in the Java class library. All wrapper classes are defined in the java.lang package. There is even a wrapper class that represents the type void. However, unlike the other wrapper classes, the Void class cannot be instantiated. It simply represents the concept of a void reference. KEY CON CEPT A wrapper class represents a primitive value so that it can be treated as an object. The wrapper classes also provide various methods related to the management of the associated primitive type. For example, the Integer class contains methods that return the int value stored in the object, and that convert the stored value to other primitive types. Wrapper classes also contain static methods that can be invoked independent of any instantiated object. For example, the Integer class contains a static method called parseInt to convert an integer that is stored in a String to its corresponding int value. If the String object str holds the string “987”, then the following line of code converts and stores the integer value 987 into the int variable num: num = Integer.parseInt(str); The Java wrapper classes often contain static constants that are helpful as well. For example, the Integer class contains two constants, MIN_VALUE and MAX_VALUE, which hold the smallest and largest int values, respectively. The other wrapper classes contain similar constants for their types. 498 APPENDI X B Object-Oriented Design B.12 K E Y C O N C E PT Interfaces We have used the term “interface” to mean the public methods through which we can interact with an object. That definition is consistent with our use of it in this section, but now we are going to formalize this concept using a particular language construct in Java. A Java interface is a collection of constants and abstract methods. An abstract method is a method that does not have an implementation. That is, there is no body of code defined for an abstract method. The header of the method, including its parameter list, is simply followed by a semicolon. An interface cannot be instantiated. An interface is a collection of abstract methods. It cannot be instantiated. The following interface, called Complexity, contains two abstract methods, setComplexity and getComplexity: interface Complexity { void setComplexity (int complexity); int getComplexity (); } An abstract method can be preceded by the reserved word abstract, though in interfaces it usually is not. Methods in interfaces have public visibility by default. KE Y CO N C E PT A class implements an interface, which formally defines a set of methods used to interact with objects of that class. A class implements an interface by providing method implementations for each of the abstract methods defined in the interface. A class that implements an interface uses the reserved word implements followed by the interface name in the class header. If a class asserts that it implements a particular interface, it must provide a definition for all methods in the interface. The compiler will produce errors if any of the methods in the interface is not given a definition in the class. For example, a class called Question could be defined to represent a question that a teacher may ask on a test. If the Question class implements the Complexity interface, it must explicitly say so in the header and must define both methods from the Complexity interface: class Questions implements Complexity { int difficulty; // whatever else void setComplexity (int complexity) { difficulty = complexity; } B. 1 2 int getComplexity () { return difficulty; } } Multiple classes can implement the same interface, providing alternative definitions for the methods. For example, we could implement a class called Task that also implements the Complexity interface. In it we could choose to manage the complexity of a task in a different way (though it would still have to implement all the methods of the interface). A class can implement more than one interface. In these cases, the class must provide an implementation for all methods in all interfaces listed. To show that a class implements multiple interfaces, they are listed in the implements clause, separated by commas. For example: class ManyThings implements interface1, interface2, interface3 { // all methods of all interfaces } In addition to, or instead of, abstract methods, an interface can also contain constants, defined using the final modifier. When a class implements an interface, it gains access to all of the constants defined in it. This mechanism allows multiple classes to share a set of constants that are defined in a single location. The Comparable Interface The Java standard class library contains interfaces as well as classes. The Comparable interface, for example, is defined in the java.lang package. It contains only one method, compareTo, which takes an object as a parameter and returns an integer. The intention of this interface is to provide a common mechanism for comparing one object to another. One object calls the method and passes another as a parameter: if (obj1.compareTo(obj2) < 0) System.out.println ("obj1 is less than obj2"); As specified by the documentation for the interface, the integer that is returned from the compareTo method should be negative if obj1 is less than obj2, 0 if they are equal, and positive if obj1 is greater than obj2. It is up to the designer of each class to decide what it means for one object of that class to be less than, equal to, or greater than another. Interfaces 499 500 APPENDI X B Object-Oriented Design The String class contains a compareTo method that operates in this manner. Now we can clarify that the String class has this method because it implements the Comparable interface. The String class implementation of this method bases the comparison on the lexicographic ordering defined by the Unicode character set. The Iterator Interface The Iterator interface is another interface defined as part of the Java standard class library. It is used by classes that represent a collection of objects, providing a means to move through the collection one object at a time. The two primary methods in the Iterator interface are hasNext, which returns a boolean result, and next, which returns an object. Neither of these methods takes any parameters. The hasNext method returns true if there are items left to process, and next returns the next object. It is up to the designer of the class that implements the Iterator interface to decide the order in which objects will be delivered by the next method. We should note that, according to the spirit of the interface, the next method does not remove the object from the underlying collection; it simply returns a reference to it. The Iterator interface also has a method called remove, which takes no parameters and has a void return type. A call to the remove method removes the object that was most recently returned by the next method from the underlying collection. The Iterator interface is an improved version of an older interface called Enumeration, which is still part of the Java standard class library. The Enumeration interface does not have a remove method. Generally, the Iterator interface is the preferred choice between the two. B.13 Inheritance A class establishes the characteristics and behaviors of an object, but reserves no memory space for variables (unless those variables are declared as static). Classes are the plan, and objects are the embodiment of that plan. Many houses can be created from the same blueprint. They are essentially the same house in different locations with different people living in them. But suppose you want a house that is similar to another, but with some different or additional features. You want to start with the same basic blueprint but modify it to suit your needs and desires. Many housing developments are created this way. The houses in the development have the same core layout, but they can have unique features. For instance, they might all be split-level homes with the same bedroom, kitchen, and living-room configuration, but some have a fireplace or B. 1 3 Inheritance 501 full basement whereas others do not, and some have an attached garage instead of a carport. It’s likely that the housing developer commissioned a master architect to create a single blueprint to establish the basic design of all houses in the development, then a series of new blueprints that include variations designed to appeal to different buyers. The act of creating the series of blueprints was simplified because they all begin with the same underlying structure, while the variations give them unique characteristics that may be very important to the prospective owners. Creating a new blueprint that is based on an existing blueprint is analogous to the object-oriented concept of inheritance, which allows a software designer to define a new class in terms of an existing one. It is a powerful software development technique and a defining characteristic of object-oriented programming. Derived Classes Inheritance is the process in which a new class is derived from an existing one. The new class automatically contains some or all of the variables and methods in the original class. Then, to tailor the class as needed, the programmer can add new variables and methods to the derived class, or modify the inherited ones. In general, creating new classes via inheritance is faster, easier, and cheaper than writing them from scratch. At the heart of inheritance is the idea of software reuse. By using existing software components to create new ones, we capitalize on all of the effort that went into the design, implementation, and testing of the existing software. KEY CON CEPT Inheritance is the process of deriving a new class from an existing one. KEY CON CEPT One purpose of inheritance is to reuse existing software. Keep in mind that the word class comes from the idea of classifying groups of objects with similar characteristics. Classification schemes often use levels of classes that relate to one another. For example, all mammals share certain characteristics: they are warm-blooded, have hair, and bear live offspring. Now consider a subset of mammals, such as horses. All horses are mammals and have all the characteristics of mammals. But they also have unique features that make them different from other mammals. If we map this idea into software terms, an existing class called Mammal would have certain variables and methods that describe the state and behavior of mammals. A Horse class could be derived from the existing Mammal class, automatically inheriting the variables and methods contained in Mammal. The Horse class can refer to the inherited variables and methods as if they had been declared locally in that class. New variables and methods can then be added to the derived class, to distinguish a horse from other mammals. Inheritance nicely models many situations found in the natural world. 502 APPENDI X B Object-Oriented Design K E Y CO N C E PT Inherited variables and methods can be used in the derived class as if they had been declared locally. K E Y CO N C E PT Inheritance creates an is-a relationship between all parent and child classes. The original class that is used to derive a new one is called the parent class, superclass, or base class. The derived class is called a child class, or subclass. Java uses the reserved word extends to indicate that a new class is being derived from an existing class. The derivation process should establish a specific kind of relationship between two classes: an is-a relationship. This type of relationship means that the derived class should be a more specific version of the original. For example, a horse is a mammal. Not all mammals are horses, but all horses are mammals. Let’s look at an example. The following class can be used to define a book: class Book { protected int numPages; protected void pages() { System.out.println ("Number of pages: " + numPages); } } To derive a child class that is based on the Book class, we use the reserved word extends in the header of the child class. For example, a Dictionary class can be derived from Book as follows: class Dictionary extends Book { private int numDefs; public void info() { System.out.println ("Number of definitions: " + numDefs); System.out.println ("Definitions per page: " + numDefs/numPages); } } By saying that the Dictionary class extends the Book class, the Dictionary class automatically inherits the numPages variable and the pages method. Note that the info method uses the numPages variable explicitly. Inheritance is a one-way street. The Book class cannot use variables or methods that are declared explicitly in the Dictionary class. For instance, if we created an object from the Book class, it could not be used to invoke the info method. This restriction makes sense, because a child class is a more specific version of the parent. B. 1 3 Inheritance 503 A dictionary has pages, because all books have pages; but although a dictionary has definitions, not all books do. Inheritance relationships are represented in UML class diagrams using an arrow with an open arrowhead pointing from the child class to the parent class. The protected Modifier Not all variables and methods are inherited in a derivation. The visibility modifiers used to declare the members of a class determine which ones are inherited and which are not. Specifically, the child class inherits variables and methods that are declared public, and it does not inherit those that are declared private. However, if we declare a variable with public visibility so that a derived class can inherit it, we violate the principle of encapsulation. Therefore, Java provides a third visibility modifier: protected. When a variable or method is declared with protected visibility, a derived class will inherit it, retaining some of its encapsulation properties. The encapsulation with protected visibility is not as tight as it would be if the variable or method were declared private, but it is better than if it were declared public. Specifically, a variable or method declared with protected visibilKEY CON CEPT ity may be accessed by any class in the same package. Visibility modifiers determine which Each inherited variable or method retains the effect of its original visibility modifier. For example, if a method is public in the parent, it is public in the child. variables and methods are inherited. Protected visibility provides the best possible encapsulation that permits inheritance. Constructors are not inherited in a derived class, even though they have public visibility. This is an exception to the rule about public members being inherited. Constructors are special methods that are used to set up a particular type of object, so it wouldn’t make sense for a class called Dictionary to have a constructor called Book. The super Reference The reserved word super can be used in a class to refer to its parent class. Using the super reference, we can access a parent’s members, even if they aren’t inherited. Like the this reference, what the word super refers to depends on the class in which it is used. However, unlike the this reference, which refers to a particular instance of a class, super is a general reference to the members of the parent class. One use of the super reference is to invoke a parent’s constructor. If the following invocation is performed at the beginning of a constructor, the parent’s constructor is invoked, passing any appropriate parameters: super (x, y, z); A child’s constructor is responsible for calling its parent’s constructor. Generally, the first line of a constructor should use the super reference call to a constructor of 504 APPENDI X B Object-Oriented Design K E Y C O N C E PT A parent’s constructor can be invoked using the super reference. the parent class. If no such call exists, Java will automatically make a call to super() at the beginning of the constructor. This rule ensures that a parent class initializes its variables before the child class constructor begins to execute. Using the super reference to invoke a parent’s constructor can be done only in the child’s constructor and, if included, must be the first line of the constructor. The super reference can also be used to reference other variables and methods defined in the parent’s class. Overriding Methods When a child class defines a method with the same name and signature as a method in the parent, we say that the child’s version overrides the parent’s version in favor of its own. The need for overriding occurs often in inheritance situations. KE Y CO N C E PT A child class can override (redefine) the parent’s definition of an inherited method. The object that is used to invoke a method determines which version of the method is actually executed. If it is an object of the parent type, the parent’s version of the method is invoked. If it is an object of the child type, the child’s version is invoked. This flexibility allows two objects that are related by inheritance to use the same naming conventions for methods that accomplish the same general task in different ways. A method can be defined with the final modifier. A child class cannot override a final method. This technique is used to ensure that a derived class uses a particular definition for a method. The concept of method overriding is important to several issues related to inheritance. These issues are explored in later sections of this chapter. B.14 Class Hierarchies A child class derived from one parent can be the parent of its own child class. Furthermore, multiple classes can be derived from a single parent. Therefore, inheritance relationships often develop into class hierarchies. The UML class diagram in Figure B.4 shows a class hierarchy that incorporates the inheritance relationship between classes Mammal and Horse. KE Y C O N C E PT The child of one class can be the parent of one or more other classes, creating a class hierarchy. There is no limit to the number of children a class can have, or to the number of levels to which a class hierarchy can extend. Two children of the same parent are called siblings. Although siblings share the characteristics passed on by their common parent, they are not related by inheritance, because one is not used to derive the other. In class hierarchies, common features should be kept as high in the hierarchy as reasonably possible. That way, the only characteristics B. 1 4 Class Hierarchies 505 Animal Reptile Snake Bird Lizard Parrot Mammal Horse Bat F I G U R E B . 4 A UML class diagram showing a class hierarchy explicitly established in a child class are those that make the class distinct from its parent and from its siblings. This approach maximizes the ability to reuse classes. It also facilitates maintenance activities, because when changes are made to the parent, they are automatically reflected in the descendants. Always remember to maintain the is-a relationship when building class hierarchies. KEY CON CEPT Common features should be located as high in a class hierarchy as is reasonable, minimizing maintenance efforts. The inheritance mechanism is transitive. That is, a parent passes along a trait to a child class, that child class passes it along to its children, and so on. An inherited feature might have originated in the immediate parent, or possibly from several levels higher in a more distant ancestor class. There is no single best hierarchy organization for all situations. The decisions made when designing a class hierarchy restrict and guide more detailed design decisions and implementation options, and they must be made carefully. The Object Class In Java, all classes are derived ultimately from the Object class. If a class definition doesn’t use the extends clause to derive itself explicitly from another class, then that class is automatically derived from the Object class by default. Therefore, the following two class definitions are equivalent: class Thing { // whatever } 506 APPENDI X B Object-Oriented Design and class Thing extends Object { // whatever } Because all classes are derived from Object, any public method of Object can be invoked through any object created in any Java program. The Object class is defined in the java.lang package of the standard class library. K E Y CO N C E PT All Java classes are derived, directly or indirectly, from the Object class. The toString method, for instance, is defined in the Object class, so the toString method can be called on any object. When a println method is called with an object parameter, toString is called to determine what to print. The definition for toString that is provided by the Object class returns a string containing the object’s class name followed by a numeric value that is unique for that object. Usually, we override the Object version of toString to fit our own needs. The String class has overridden the toString method so that it returns its stored string value. KE Y CO N C E PT The toString and equals methods are defined in the Object class and therefore are inherited by every class in every Java program. The equals method of the Object class is also useful. Its purpose is to determine if two objects are equal. The definition of the equals method provided by the Object class returns true if the two object references actually refer to the same object (that is, if they are aliases). Classes often override the inherited definition of the equals method in favor of a more appropriate definition. For instance, the String class overrides equals so that it returns true only if both strings contain the same characters in the same order. Abstract Classes An abstract class represents a generic concept in a class hierarchy. An abstract class cannot be instantiated and usually contains one or more abstract methods, which have no definition. In this sense, an abstract class is similar to an interface. Unlike interfaces, however, an abstract class can contain methods that are not abstract, and can contain data declarations other than constants. A class is declared as abstract by including the abstract modifier in the class header. Any class that contains one or more abstract methods must be declared as abstract. In abstract classes (unlike interfaces), the abstract modifier must be applied to each abstract method. A class declared as abstract does not have to contain abstract methods. Abstract classes serve as placeholders in a class hierarchy. As the name implies, an abstract class represents an abstract entity that is usually insufficiently defined to B. 1 4 Class Hierarchies 507 Vehicle Car Boat Plane F I G U R E B . 5 A Vehicle class hierarchy be useful by itself. Instead, an abstract class may contain a partial description that is inherited by all of its descendants in the class hierarchy. Its children, which are more specific, fill in the gaps. KEY CON CEPT An abstract class cannot be instanti- ated. It represents a concept on Consider the class hierarchy shown in Figure B.5. The Vehicle which other classes can build their class at the top of the hierarchy may be too generic for a particular apdefinitions. plication. Therefore, we may choose to implement it as an abstract class. Concepts that apply to all vehicles can be represented in the Vehicle class and are inherited by its descendants. That way, each of its descendants doesn’t have to define the same concept redundantly, and perhaps inconsistently. For example, we may say that all vehicles have a particular speed. Therefore, we declare a speed variable in the Vehicle class, and all specific vehicles below it in the hierarchy automatically have that variable via inheritance. Any change we make to the representation of the speed of a vehicle is automatically reflected in all descendant classes. Similarly, we may declare an abstract method called fuelConsumption, whose purpose is to calculate how quickly fuel is being consumed by a particular vehicle. The details of the fuelConsumption method must be defined by each type of vehicle, but the Vehicle class establishes that all vehicles consume fuel and provides a consistent way to compute that value. Some concepts don’t apply to all vehicles, so we wouldn’t represent those concepts at the Vehicle level. For instance, we wouldn’t include a variable called numberOfWheels in the Vehicle class, because not all vehicles have wheels. The child classes for which wheels are appropriate can add that concept at the appropriate level in the hierarchy. There are no restrictions as to where in a class hierarchy an abstract class can be defined. Usually they are located at the upper levels of a class hierarchy. However, it is possible to derive an abstract class from a nonabstract parent. Usually, a child of an abstract class will provide a specific definition for an abstract method inherited from its parent. Note that this is just a specific case of overriding a method, giving a different definition than the one the parent provides. If a 508 APPENDI X B Object-Oriented Design K E Y C O N C E PT A class derived from an abstract parent must override all of its parent’s abstract methods, or the derived class will also be considered abstract. child of an abstract class does not give a definition for every abstract method that it inherits from its parent, then the child class is also considered to be abstract. Note that it would be a contradiction for an abstract method to be modified as final or static. Because a final method cannot be overridden in subclasses, an abstract final method would have no way of being given a definition in subclasses. A static method can be invoked using the class name without declaring an object of the class. Because abstract methods have no implementation, an abstract static method would make no sense. Choosing which classes and methods to make abstract is an important part of the design process. Such choices should be made only after careful consideration. By using abstract classes wisely, we can create flexible, extensible software designs. Interface Hierarchies The concept of inheritance can be applied to interfaces as well as classes. That is, one interface can be derived from another interface. These relationships can form an interface hierarchy, which is similar to a class hierarchy. Inheritance relationships between interfaces are shown in UML using the same connection (an arrow with an open arrowhead) as they are with classes. K E Y CO N C E PT Inheritance can be applied to interfaces, so that one interface can be derived from another interface. When a parent interface is used to derive a child interface, the child inherits all abstract methods and constants of the parent. Any class that implements the child interface must implement all of the methods. There are no restrictions on the inheritance between interfaces, as there are with protected and private members of a class, because all members of an interface are public. Class hierarchies and interface hierarchies do not overlap. That is, an interface cannot be used to derive a class, and a class cannot be used to derive an interface. A class and an interface interact only when a class is designed to implement a particular interface. B.15 Polymorphism Usually, the type of a reference variable matches the class of the object it refers to exactly. That is, if we declare a reference as follows: ChessPiece bishop; the bishop reference is used to refer to an object created by instantiating the ChessPiece class. However, the relationship between a reference variable and the object it refers to is more flexible than that. B. 1 5 The term polymorphism can be defined as “having many forms.” A polymorphic reference is a reference variable that can refer to different types of objects at different points in time. The specific method invoked through a polymorphic reference can change from one invocation to the next. Polymorphism 509 KEY CON CEPT A polymorphic reference can refer to different types of objects over time. Consider the following line of code: obj.doIt(); If the reference obj is polymorphic, it can refer to different types of objects at different times. If that line of code is in a loop or in a method that is called more than once, that line of code might call a different version of the doIt method each time it is invoked. At some point, the commitment is made to execute certain code to carry out a method invocation. This commitment is referred to as binding a method invocation to a method definition. In most situations, the binding of a method invocation to a method definition can occur at compile time. For polymorphic references, however, the decision cannot be made until run time. The method definition that is used is based on the object that is being referred to by the reference variable at that moment. This deferred commitment is called late binding or dynamic binding. It is less efficient than binding at compile time because the decision has to be made during the execution of the program. This overhead is generally acceptable in light of the flexibility that a polymorphic reference provides. There are two ways to create a polymorphic reference in Java: using inheritance and using interfaces. The following sections describe these approaches. References and Class Hierarchies In Java, a reference that is declared to refer to an object of a particular class also can be used to refer to an object of any class related to it by inheritance. For example, if the class Mammal is used to derive the class Horse, then a Mammal reference can be used to refer to an object of class Horse. This ability is shown in the code segment below: Mammal pet; Horse secretariat = new Horse(); pet = secretariat; // a valid assignment The reverse operation, assigning the Mammal object to a Horse reference, is also valid, but requires an explicit cast. Assigning a reference in this direction is generally less useful and more likely to cause problems, because although a horse has all the functionality of a mammal (because a horse is-a mammal), the reverse is not necessarily true. KEY CON CEPT A reference variable can refer to any object created from any class related to it by inheritance. 510 APPENDI X B Object-Oriented Design This relationship works throughout a class hierarchy. If the Mammal class were derived from a class called Animal, then the following assignment would also be valid: Animal creature = new Horse(); Carrying this to the extreme, an Object reference can be used to refer to any object, because ultimately all classes are descendants of the Object class. An ArrayList, for example, uses polymorphism in that it is designed to hold Object references. That’s why an ArrayList can be used to store any kind of object. In fact, a particular ArrayList can be used to hold several different types of objects at one time, because, in essence, they are all Object objects. Polymorphism via Inheritance The reference variable creature, as defined in the previous section, can be polymorphic, because at any point in time it could refer to an Animal object, a Mammal object, or a Horse object. Suppose that all three of these classes have a method called move and that it is implemented in a different way in each class (because the child class overrode the definition it inherited). The following invocation calls the move method, but the particular version of the method it calls is determined at run time: creature.move(); At the point when this line is executed, if creature currently refers to an Animal object, the move method of the Animal class is invoked. Likewise, if creature currently refers to a Mammal or Horse object, the Mammal or Horse version of move is invoked, respectively. KE Y CO N C E PT A polymorphic reference uses the type of the object, not the type of the reference, to determine which version of a method to invoke. Of course, because Animal and Mammal represent general concepts, they may be defined as abstract classes. This situation does not eliminate the ability to have polymorphic references. Suppose the move method in the Mammal class is abstract, and is given unique definitions in the Horse, Dog, and Whale classes (all derived from Mammal). A Mammal reference variable can be used to refer to any objects created from any of the Horse, Dog, and Whale classes, and can be used to execute the move method on any of them. Let’s consider another situation. The class hierarchy shown in Figure B.6 contains classes that represent various types of employees that might work at a particular company. Polymorphism could be used in this situation to pay various employees in different ways. One list of employees (of whatever type) could be paid using a single loop that invokes each employee’s pay method. But the pay method that is invoked each time will depend on the specific type of employee that is executing the pay method during that iteration of the loop. B. 1 5 Polymorphism Firm main (args : String[]) : void StaffMember Staff name : String address : String phone : String staffList : StaffMember[] payday() : void toString() : String pay() : double Volunteer Employee socialSecurityNumber : String payRate : double pay() : double toString() : String pay() : double Executive Hourly bonus : double hoursWorked : int awardBonus (execBonus : double) : void pay() : double addHours (moreHours : int) : void pay() : double toString() : String F I G U R E B . 6 A class hierarchy of employees 511 512 APPENDI X B Object-Oriented Design This is a classic example of polymorphism—allowing different types of objects to handle a similar operation in different ways. Polymorphism via Interfaces As we have seen, a class name is used to declare the type of an object reference variable. Similarly, an interface name can be used as the type of a reference variable as well. An interface reference variable can be used to refer to any object of any class that implements that interface. Suppose we declare an interface called Speaker as follows: public interface Speaker { public void speak(); public void announce (String str); } The interface name, Speaker, can now be used to declare an object reference variable: KE Y C O N C E PT An interface name can be used to declare an object reference variable. An interface reference can refer to any object of any class that implements the interface. Speaker current; The reference variable current can be used to refer to any object of any class that implements the Speaker interface. For example, if we define a class called Philosopher such that it implements the Speaker interface, we could then assign a Philosopher object to a Speaker reference: current = new Philosopher(); This assignment is valid because a Philosopher is, in fact, a Speaker. K E Y CO N C E PT Interfaces allow us to make polymorphic references, in which the method that is invoked is based on the particular object being referenced at the time. The flexibility of an interface reference allows us to create polymorphic references. As we saw earlier in this chapter, by using inheritance, we can create a polymorphic reference that can refer to any one of a set of objects related by inheritance. Using interfaces, we can create similar polymorphic references, except that the objects being referenced are related by implementing the same interface instead of being related by inheritance. For example, if we create a class called Dog that also implements the Speaker interface, it too could be assigned to a Speaker reference variable. The same reference, in fact, could at one point refer to a Philosopher object and then later refer to a Dog object. The following lines of code illustrate this: B. 1 5 Speaker guest; guest = new Philosopher(); guest.speak(); guest = new Dog(); guest.speak(); In this code, the first time the speak method is called, it invokes the speak method defined in the Philosopher class. The second time it is called, it invokes the speak method of the Dog class. As with polymorphic references via inheritance, it is not the type of the reference that determines which method gets invoked, but rather it is the type of the object that the reference points to at the moment of invocation. Note that when we are using an interface reference variable, we can invoke only the methods defined in the interface, even if the object it refers to has other methods to which it can respond. For example, suppose the Philosopher class also defined a public method called pontificate. The second line of the following code would generate a compiler error, even though the object can in fact respond to the pontificate method: Speaker special = new Philosopher(); special.pontificate(); // generates a compiler error The problem is that the compiler can determine only that the object is a Speaker, and therefore can guarantee only that the object can respond to the speak and announce methods. Because the reference variable special could refer to a Dog object (which cannot pontificate), it does not allow the refer- ence. If we know in a particular situation that such an invocation is valid, we can cast the object into the appropriate reference so that the compiler will accept it: ((Philosopher) special).pontificate(); Similar to polymorphic references based on inheritance, an interface name can be used as the type of a method parameter. In such situations, any object of any class that implements the interface can be passed into the method. For example, the following method takes a Speaker object as a parameter. Therefore, both a Dog object and a Philosopher object can be passed into it in separate invocations. public void sayIt (Speaker current) { current.speak(); } Polymorphism 513 514 APPENDI X B Object-Oriented Design B.16 Generic Types Java enables us to define a class based on a generic type. That is, we can define a class so that it stores, operates on, and manages objects whose type is not specified until the class is instantiated. Generics are an integral part of our discussions of collections and their underlying implementations throughK E Y C O N C E PT out the rest of this book. Errors and exceptions represent unusual or invalid processing. Let’s assume we need to define a class called Box that stores and manages other objects. Using polymorphism, we could simply define Box so that internally it stores references to the Object class. Then, any type of object could be stored inside a box. In fact, multiple types of unrelated objects could be stored in Box. We lose a lot of control with that level of flexibility in our code. A better approach is to define the Box class to store a generic type T. (We can use any identifier we want for the generic type, though using T has become a convention.) The header of the class contains a reference to the type in angle brackets. For example: class Box<T> { // declarations and code that manage objects of type T } Then, when a Box is needed, it is instantiated with a specific class used in place of T. For example, if we wanted a Box of Widget objects, we could use the following declaration: Box<Widget> box1 = new Box<Widget>; The type of the box1 variable is Box<Widget>. In essence, for the box1 object, the Box class replaces T with Widget. Now suppose we wanted a Box in which to store Gadget objects; we could make the following declaration: Box<Gadget> box2 = new Box<Gadget>; For box2, the Box class essentially replaces T with Gadget. So, although the box1 and box2 objects are both boxes, they have different types because the generic type is taken into account. This is a safer implementation, because at this point we cannot use box1 to store gadgets (or anything else for that matter), nor could we use box2 to store widgets. A generic type such as T cannot be instantiated. It is merely a placeholder to allow us to define the class that will manage a specific type of object that is established when the class is instantiated. B. 1 7 B.17 Exceptions Exceptions Problems that arise in a Java program may generate exceptions or errors. An exception is an object that defines an unusual or erroneous situation. An exception is thrown by a program or the runtime environment, and it can be caught and handled appropriately if desired. An error is similar KEY CON CEPT to an exception, except that an error generally represents an unreErrors and exceptions represent uncoverable situation, and it should not be caught. Java has a predeusual or invalid processing. fined set of exceptions and errors that may occur during the execution of a program. A program can be designed to process an exception in one of three ways: ■ Not handle the exception at all. ■ Handle the exception where it occurs. ■ Handle the exception at another point in the program. We explore each of these approaches in the following sections. Exception Messages If an exception is not handled at all by the program, the program will terminate (abnormally) and produce a message that describes what exception occurred and where in the program it was produced. The information associated with an exception is often helpful in tracking down the cause of a problem. Let’s look at the output of an exception. An ArithmeticException is thrown when an invalid arithmetic operation is attempted, such as dividing by zero. When that exception is thrown, if there is no code in the program to handle the exception explicitly, the program terminates and prints a message similar to the following: Exception in thread "main" java.lang.ArithmeticException: / by zero at Zero.main (Zero.java:17) The first line of the exception output indicates which exception was thrown and provides some information about why it was thrown. The remaining line or lines are the call stack trace, which indicates where the exception occurred. In this case, there is only one line in the call stack trace, but in other cases there may be several, depending on where the exception originated in the program. KEY CON CEPT The messages printed by a thrown exception indicate the nature of the problem and provide a method call stack trace. 515 516 APPENDI X B Object-Oriented Design The first line of the trace indicates the method, file, and line number where the exception occurred. The other lines in the trace, if present, indicate the methods that were called to get to the method that produced the exception. In this program, there is only one method, and it produced the exception; therefore, there is only one line in the trace. The call stack trace information is also available by calling methods of the exception object that is being thrown. The method getMessage returns a string explaining the reason the exception was thrown. The method printStackTrace prints the call stack trace. The try Statement Let’s now examine how we catch and handle an exception when it is thrown. A try statement consists of a try block followed by one or more catch clauses. The try block is a group of statements that may throw an exception. A catch clause defines how a particular kind of exception is handled. A try block can have several catch clauses associated with it, each dealing with a particular kind of exception. A catch clause is sometimes called an exception handler. Here is the general format of a try statement: try { // } catch { // } catch { // } KE Y CO N C E PT statements in the try block (IOException exception) statements that handle the I/O problem (NumberFormatException exception) statements that handle the number format problem When a try statement is executed, the statements in the try block are executed. If no exception is thrown during the execution of the try block, processing continues with the statement following the try statement (after all of the catch clauses). This situation is the normal execution flow and should occur most of the time. Each catch clause on a try statement handles a particular kind of exception that may be thrown within the try block. If an exception is thrown at any point during the execution of the try block, control is immediately transferred to the appropriate ex- ception handler if it is present. That is, control transfers to the first catch clause whose specified exception corresponds to the class of B. 1 7 Exceptions 517 the exception that was thrown. After the statements in the catch clause are executed, control transfers to the statement after the entire try statement. Exception Propagation If an exception is not caught and handled where it occurs, control is immediately returned to the method that invoked the method that produced the exception. We can design our software so that the exception is caught and handled at this outer level. If it isn’t caught there, control returns to the method that called it. This process is called propagating the exception. Exception propagation continues until the exception is caught and handled, or until it is propagated out of the main method, which terminates the program and produces an exception message. To catch an exKEY CON CEPT ception at an outer level, the method that produces the exception If an exception is not caught and must be invoked inside a try block that has an appropriate catch handled where it occurs, it is propagated to the calling method. clause to handle it. A programmer must pick the most appropriate level at which to catch and handle an exception. There is no single best answer. It depends on the situation and the design of the system. Sometimes the right approach will be not to catch an exception at all and let the program terminate. KEY CON CEPT The Exception Class Hierarchy A programmer must carefully consider how exceptions should be handled, if at all, and at what level. The classes that define various exceptions are related by inheritance, creating a class hierarchy that is shown in part in Figure B.7. The Throwable class is the parent of both the Error class and the Exception class. Many types of exceptions are derived from the Exception class, and these classes also have many children. Though these high-level classes are defined in the java.lang package, many child classes that define specific exceptions are part of several other packages. Inheritance relationships can span package boundaries. We can define our own exceptions by deriving a new class from Exception or one of its descendants. The class we choose as the parent depends on KEY CON CEPT what situation or condition the new exception represents. After creating the class that defines the exception, an object of that type can be created as needed. The throw statement is used to throw the exception. For example: throw new MyException(); A new exception is defined by deriving a new class from the Exception class or one of its descendants. 518 APPENDI X B Object-Oriented Design Object Throwable Error Exception RunTimeException Linkage Error ThreadDeath ArithmeticException VirtualMachineError IndexOutOfBoundsException AWTError NullPointerException IllegalAccessException NoSuchMethodException ClassNotFoundException F I G U R E B . 7 Part of the Error and Exception class hierarchy Summary of Key Concepts Summary of Key Concepts ■ An abstraction hides details. A good abstraction hides the right details at the right time so that we can manage complexity. ■ The new operator returns a reference to a newly created object. ■ The Java standard class library is a useful set of classes that anyone can use when writing Java programs. ■ A package is a Java language element used to group related classes under a common name. ■ Each object has a state and a set of behaviors. The values of an object’s variables define its state. The methods to which an object responds define its behaviors. ■ A class is a blueprint for an object; it reserves no memory space for data. Each object has its own data space, and thus its own state. ■ The scope of a variable, which determines where it can be referenced, depends on where it is declared. ■ Objects should be encapsulated. The rest of a program should interact with an object only through a well-defined interface. ■ Instance variables should be declared with private visibility to promote encapsulation. ■ A variable declared in a method is local to that method and cannot be used outside of it. ■ A constructor cannot have any return type, even void. ■ The versions of an overloaded method are distinguished by their signatures. The number, type, or order of their parameters must be distinct. ■ An object reference variable stores the address of an object. ■ The reserved word null represents a reference that does not point to a valid object. ■ The this reference always refers to the currently executing object. ■ Several references can refer to the same object. These references are aliases of each other. ■ The == operator compares object references for equality, returning true if the references are aliases of each other. ■ The equals method can be defined to determine equality between objects in any way we consider appropriate. 519 520 APPENDI X B Object-Oriented Design ■ If an object has no references to it, a program cannot use it. Java performs automatic garbage collection by periodically reclaiming the memory space occupied by these objects. ■ When an object is passed to a method, the actual and formal parameters become aliases of each other. ■ A static variable is shared among all instances of a class. ■ A method is made static by using the static modifier in the method declaration. ■ A wrapper class represents a primitive value so that it can be treated as an object. ■ An interface is a collection of abstract methods. It cannot be instantiated. ■ A class implements an interface, which formally defines a set of methods used to interact with objects of that class. ■ Inheritance is the process of deriving a new class from an existing one. ■ One purpose of inheritance is to reuse existing software. ■ Inherited variables and methods can be used in the derived class as if they had been declared locally. ■ Inheritance creates an is-a relationship between all parent and child classes. ■ Visibility modifiers determine which variables and methods are inherited. Protected visibility provides the best possible encapsulation that permits inheritance. ■ A parent’s constructor can be invoked using the super reference. ■ A child class can override (redefine) the parent’s definition of an inherited method. ■ The child of one class can be the parent of one or more other classes, creating a class hierarchy. ■ Common features should be located as high in a class hierarchy as is reasonable, minimizing maintenance efforts. ■ All Java classes are derived, directly or indirectly, from the Object class. ■ The toString and equals methods are defined in the Object class and therefore are inherited by every class in every Java program. ■ An abstract class cannot be instantiated. It represents a concept on which other classes can build their definitions. ■ A class derived from an abstract parent must override all of its parent’s abstract methods, or the derived class will also be considered abstract. Self-Review Questions ■ Inheritance can be applied to interfaces, so that one interface can be derived from another interface. ■ A polymorphic reference can refer to different types of objects over time. ■ A reference variable can refer to any object created from any class related to it by inheritance. ■ A polymorphic reference uses the type of the object, not the type of the reference, to determine which version of a method to invoke. ■ An interface name can be used to declare an object reference variable. An interface reference can refer to any object of any class that implements the interface. ■ Interfaces allow us to make polymorphic references, in which the method that is invoked is based on the particular object being referenced at the time. ■ Errors and exceptions represent unusual or invalid processing. ■ The messages printed by a thrown exception indicate the nature of the problem and provide a method call stack trace. ■ Each catch clause on a try statement handles a particular kind of exception that may be thrown within the try block. ■ If an exception is not caught and handled where it occurs, it is propagated to the calling method. ■ A programmer must carefully consider how exceptions should be handled, if at all, and at what level. ■ A new exception is defined by deriving a new class from the Exception class or one of its descendants. Self-Review Questions SR B.1 What is the difference between an object and a class? SR B.2 Objects should be self-governing. Explain. SR B.3 Describe each of the following: a. public method b. private method c. public variable d. private variable SR B.4 What are constructors used for? How are they defined? SR B.5 How are overloaded methods distinguished from each other? SR B.6 What is an aggregate object? 521 522 APPENDI X B Object-Oriented Design SR B.7 What is the difference between a static variable and an instance variable? SR B.8 What is the difference between a class and an interface? SR B.9 Describe the relationship between a parent class and a child class. SR B.10 What relationship should every class derivation represent? SR B.11 What is the significance of the Object class? SR B.12 What is polymorphism? SR B.13 How is overriding related to polymorphism? SR B.14 How can polymorphism be accomplished using interfaces? Exercises EX B.1 Identify the following as a class, object, or method: > > > > EX B.2 superman breakChain SuperHero saveLife Identify the following as a class, object, or method: > > > > > Beverage pepsi drink refill coke EX B.3 Explain why a static method cannot refer to an instance variable. EX B.4 Can a class implement two interfaces that each contains the same method signature? Explain. EX B.5 Describe the relationship between a parent class and a child class. EX B.6 Draw and annotate a class hierarchy that represents various types of faculty at a university. Show what characteristics would be represented in the various classes of the hierarchy. Explain how polymorphism could play a role in the process of assigning courses to each faculty member. Programming Projects PP B.1 Design and implement a class called Sphere that contains instance data that represents the sphere’s diameter. Define the Sphere constructor to accept and initialize the diameter, and include getter and setter methods for the diameter. Include methods that calculate and Programming Projects return the volume and surface area of the sphere (see Programming Project 3.2 for the formulas). Include a toString method that returns a one-line description of the sphere. Create a driver class called MultiSphere, whose main method instantiates and updates several Sphere objects. PP B.2 Design and implement a class called Dog that contains instance data that represents the dog’s name and age. Define the Dog constructor to accept and initialize instance data. Include getter and setter methods for the name and age. Include a method to compute and return the age of the dog in “person years” (seven times the dogs age). Include a toString method that returns a one-line description of the dog. Create a driver class called Kennel, whose main method instantiates and updates several Dog objects. PP B.3 Design and implement a class called Box that contains instance data that represents the height, width, and depth of the box. Also include a boolean variable called full as instance data that represents if the box is full or not. Define the Box constructor to accept and initialize the height, width, and depth of the box. Each newly created Box is empty (the constructor should initialize full to false). Include getter and setter methods for all instance data. Include a toString method that returns a one-line description of the box. Create a driver class called BoxTest, whose main method instantiates and updates several Box objects. PP B.4 Design and implement a class called Book that contains instance data for the title, author, publisher, and copyright date. Define the Book constructor to accept and initialize this data. Include setter and getter methods for all instance data. Include a toString method that returns a nicely formatted, multi-line description of the book. Create a driver class called Bookshelf, whose main method instantiates and updates several Book objects. PP B.5 Design and implement a class called Flight that represents an airline flight. It should contain instance data that represents the airline name, flight number, and the flight’s origin and destination cities. Define the Flight constructor to accept and initialize all instance data. Include getter and setter methods for all instance data. Include a toString method that returns a one-line description of the flight. Create a driver class called FlightTest, whose main method instantiates and updates several Flight objects. PP B.6 Design a Java interface called Priority that includes two methods: setPriority and getPriority. The interface should define a way to establish numeric priority among a set of objects. Design 523 524 APPENDI X B Object-Oriented Design and implement a class called Task that represents a task (such as on a to-do list) that implements the Priority interface. Create a driver class to exercise some Task objects. PP B.7 Design a Java interface called Lockable that includes the following methods: setKey, lock, unlock, and locked. The setKey, lock, and unlock methods take an integer parameter that represents the key. The setKey method establishes the key. The lock and unlock methods lock and unlock the object, but only if the key passed in is correct. The locked method returns a boolean that indicates whether or not the object is locked. A Lockable object represents an object whose regular methods are protected: if the object is locked, the methods cannot be invoked; if it is unlocked, they can be invoked. Redesign and implement a version of the Coin class from Chapter 5 so that it is Lockable. PP B.8 Design and implement a set of classes that define the employees of a hospital: doctor, nurse, administrator, surgeon, receptionist, janitor, and so on. Include methods in each class that are named according to the services provided by that person and that print an appropriate message. Create a main driver class to instantiate and exercise several of the classes. PP B.9 Design and implement a set of classes that define various types of reading material: books, novels, magazines, technical journals, textbooks, and so on. Include data values that describe various attributes of the material, such as the number of pages and the names of the primary characters. Include methods that are named appropriately for each class and that print an appropriate message. Create a main driver class to instantiate and exercise several of the classes. PP B.10 Design and implement a set of classes that keeps track of demographic information about a set of people, such as age, nationality, occupation, income, and so on. Design each class to focus on a particular aspect of data collection. Create a main driver class to instantiate and exercise several of the classes. PP B.11 Design and implement a program that creates an exception class called StringTooLongException, designed to be thrown when a string is discovered that has too many characters in it. In the main driver of the program, read strings from the user until the user enters “DONE”. If a string is entered that has too many characters (say 20), throw the exception. Allow the thrown exception to terminate the program. Answers to Self-Review Questions PP B.12 Modify the solution to Programming Project 10.1 such that it catches and handles the exception if it is thrown. Handle the exception by printing an appropriate message, and then continue processing more strings. Answers to Self-Review Questions SRA B.1 A class is the implementation of the blueprint for an object. An object is a specific instance of a class. SRA B.2 Objects should be self-governing meaning that only the methods of a particular object should be able to access or modify the objects variables. SRA B.3 Describe each of the following: a. public method—is a method within a class that has public visibility and may be called by a method of any other class that has declared a variable of the first class. b. private method—is a method that has private visibility and may only be accessed by methods within the class. c. public variable—is a variable within a class that has public visibility and may be accessed by any method of any class that has declared a variable of the first class. d. private variable—is a variable within a class that has private visibility and may only be accessed by methods within the class. SRA B.4 A constructor is the method that is called in the creation of an instance of a class. The constructor will typically initialize object attributes. Multiple constructors may be provided for various initialization strategies (e.g., no parameters for a default initialization or one or more parameters for more specific initializations). SRA B.5 Overloaded methods are distinguished from each other by their signatures including the number and type of the parameters. SRA B.6 An aggregate object is an object that is made up of other objects. SRA B.7 A static variable is shared among all instances of a class where as an instance variable is unique to a particular instance. SRA B.8 A class provides implementations for all of its methods (unless it is an abstract class) where as an interface simply provides the headings for each method. SRA B.9 The relationship between a child class and its parent class is called an is-a relationship. For example if class B is derived from class A, 525 526 APPENDI X B Object-Oriented Design then B is an instance of A with whatever additional information and methods that are provided in B. SRA B.10 Is-a. SRA B.11 The java.lang.Object class is the root of the class hierarchy for the Java language. This means that all classes in Java are ultimately derived from the Object class. SRA B.12 Polymorphism means having many forms. In object-oriented programming, we refer to an object reference as polymorphic if it can refer to objects of multiple classes. SRA B.13 Overriding is related to polymorphism because a parent class reference can point to objects of any of its descendant classes. One or more of these classes may have overriden methods from the parent class causing the behavior of such a method to be dependent upon the type of the object referenced by the call. SRA B.14 Polymorphism can be accomplished using interfaces because reference variables can be created using the interface type. Then those references can point to objects of any class that implements the interface. Index Symbols and Numbers Θ (Theta) notation, 17 Ω (Omega) notation, 16–17 2-3 Trees, 362–368 inserting elements into, 362–365 overview of, 362 removing elements from, 365–368 2-4 Trees, 369 2-node, 362–363 3-node, 362–363 4-node, 369 A Abstract classes, 506–508 Abstract data structures, 325–326 abstract data types. see ADTs (abstract data types) Abstract methods, 498 Abstraction collections design and, 29–31 objects and, 477–478 Activation record, 88 Acyclic graphs, 379–380 add method array implementation of sets, 445–447 elements to list, 135–137 elements to ordered lists, 158–161 link implementation of sets, 456 OrderedListADT, 137, 143 addAfter method, unordered lists, 136–137, 144, 162 addAll method, 447–448 AddEdge method, graphs, 399–400 addElement method array implementation of binary search trees, 295–296 array implementation of heaps, 350–351 heaps and, 334–337 link implementation of binary search trees, 286–287 link implementation of heaps, 343–346 addToFront method, unordered lists, 136–137, 144, 161–162 addToRear method, unordered lists, 136–137, 144, 161–162 AddVertex method, graphs, 400–401 Adjacency lists, 392–393 Adjacency matrices addVertex method and, 400–401 expandCapacity method and, 401 graph implementation strategy, 393–394 implementing undirected graphs with, 395–399 Adjacent vertices, graphs, 378 ADTs (abstract data types) BinarySearchTreeADT, 251–255 BinaryTreeADT, 251–255 defined, 30–31 designing stacks, 32–33 ListADT, 134–135 OrderedListADT, 137 QueueADT, 100–103 StackADT, 41–44 UnorderedListADT, 143–144 Aggregation relationships, UML, 471 Algorithms, 13–26 balancing binary search trees, 309 comparing growth functions, 17–19 comparison of search, 216–217 determining time complexity, 19–23 efficiency of, 14–15 growth functions and BigOh notation and, 15–17 recursive, 201–203 search. see searching sort. see sorting Algorithms, graph connectivity testing, 387–388 overview of, 382–383 shortest path, 391–392 spanning trees, 388–391 traversals, 383–387 Aliases, 493–494 Ancestors, tree, 242–243 APIs (application programming interfaces) defined, 31 Java APIs, 480 ArrayBinarySearchTree class, 294 ArrayIterator class, 158–160 ArrayList class defined, 171 implementing indexed lists, 173, 176 implementing lists with arrays, 154–157 RandomAccess interface, 172–173 Arrays B-trees and, 373 527 528 INDEX Arrays (continued) indexed lists compared with, 133–134 managing capacity of, 56–57 physical example of, 8–9 Arrays, implementing binary search trees addElement method, 295–296 overview of, 294 removeAllOccurrences method, 302–303 removeElement method, 296–302 removeMin method, 303–304 Arrays, implementing heaps addElement method, 350–351 findMin method, 353 overview of, 350 removeMin method, 352–353 Arrays, implementing lists, 152–162 add method for ordered lists, 158–161 contains method, 157–158 iterator method, 158 operations for unordered lists, 161–162 overview of, 152–154 remove method, 155–157 Arrays, implementing queues, 117–125 dequeue method, 124–125 enqueue method, 123–124 other operations, 125 overview of, 117–122 Arrays, implementing sets add method, 445–447 addAll method, 447–448 contains method, 451–452 equals method, 452–453 remove method, 449–450 removeRandom method, 448–449 union method, 450–451 Arrays, implementing stacks, 55–57 Arrays, implementing trees, 245–246 ArraySet class add method, 445–447 addAll method, 447–448 Bingo example, 440 contains method, 451–452 equals method, 452–453 overview of, 443–445 remove method, 449–450 removeRandom method, 448–449 union method, 450–451 ArrayStack class, 57–63 constructors, 58–59 overview of, 57–58 peek method, 62–63 pop method, 61–62 push method, 59–61 Association relationships, UML, 470–471 Asymptotic complexity, of algorithm, 15–17 Attributes instance data and, 485 object state and, 481 UML class diagrams and, 468–469 AVL trees balancing binary search trees with, 313–315 overview of, 312–313 red/black trees compared with, 317 B B*-trees, 371 B+-trees, 372 Balance factor, AVL trees, 312 Balanced binary search trees. see also AVL trees; Red/black trees left rotation for balancing, 310–311 leftright rotation for balancing, 311–312 overview of, 309–310 right rotation, 310 rightleft rotation for balancing, 311 Balanced trees, 243–244 Base case recursion and, 186–187 in recursive programming, 189 Base classes. see also Parent classes derived classes, 502 Behaviors, object, 481 Big-Oh notation, 16 Binary search, 213–216 Binary search trees, 281–332 as abstract data structure, 325–326 addElement method with arrays, 295–296 addElement method with links, 286–287 AVL trees, 312–313 balancing, 309–310 balancing with AVL trees, 313–315 BinarySearchTreeList implementation, 308 defined, 247 implementing with arrays, 294 implementing with links, 284–285 insertion into red/black trees, 316–319 Java Collections API for implementing, 321–325 left rotation for balancing, 310–311 leftright rotation for balancing, 311–312 ordered lists and, 304–308 overview of, 281–284 Q & A/exercises, 327–332 red/black trees, 315–316 removeAllOccurrences method with arrays, 302–303 removeAllOccurrences method with links, 291–292 removeElement method in red/black trees, 319–321 removeElement method with arrays, 296–302 INDEX removeElement method with links, 288–291 removeMin method with arrays, 303–304 removeMin method with links, 292–293 review, 327 right rotation, 310 rightleft rotation for balancing, 311 Binary trees. see also Heaps binary search trees compared with, 282 BinaryTreeADT, 251–255 complete trees, 334–335 defined, 243 implementing with arrays, 271–275 implementing with links, 262–271 using binary trees - expression trees, 255–262 BinarySearchTreeADT overview of, 282–284 UML description of, 285 BinarySearchTreeList implementation of, 305–308 trees: data structure or collection?, 325–326 BinaryTreeADT, 251–255 Binding method invocation to method definition, 38 overview of, 509 Bingo example sets used for, 439–443 UML representation of, 454 Boundary folding functions, hashing, 411 Breadth-first traversal, 383, 388 Brute-force methods, for balancing binary search trees, 309 B-trees, 369–374 analysis of, 372–373 B*-trees, 371 B+-trees, 372 implementation strategies for, 373 overview of, 369–371 Q & A/exercises, 374–376 review, 374 Bubble sort, 224–226 Buckets, hashing, 408 Bytecode, for portable software, 6 C Caesar cipher, 103 Call stack trace, 52–53, 88 Capacity, managing array, 56–57 Cardinality, of UML relationships, 470 catch clause, try statements, 53–54 Cells, hashing, 408 Chaining method deleting elements from chained implementation, 420 resolving collisions, 413–415 Child classes class hierarchies and, 36–37 defined, 34 derived classes, 502 is-a relationships, 35 Children, of tree nodes defining, 242 tree classifications based on, 243 Circular arrays, queue implementation with, 119–126 CircularArrayQueue class, 119–126 Class diagrams, UML, 468–469 Class hierarchies abstract classes, 506–508 exceptions, 517–518 interface hierarchies, 508 Object class, 505–506 overview of, 36–37, 504–505 references and, 38–40, 509–510 Class libraries, 480 Class methods. see Static methods Class relationships, UML, 469–472 Classes abstract, 506–508 data, 322, 460 default constructors, 489 defined, 34 derived, 37–38, 501–503 generic, 40–41 import declaration and, 480–481 inheritance and, 34–36 instance data, 485 interfaces, 498–499 is-a relationships, 502 members of, 483 objects as instance of, 479 overview of, 482–483 Classifications, tree, 243–244 Clients of objects, 486 software development and, 2 Cloneable interface, 172 Code keys, using queues with, 103–106 Codes class, 106 Collections, 27–69 abstract design of, 29–31 ArrayStack class, 57–63 class hierarchies and, 36–37 evaluating postfix expressions using stacks, 44–51 exception handling, 51–55 generic, 40–41 implementing stacks with arrays, 55–57 implementing using trees. see trees inheritance and, 34–36 introduction to, 28–29 maps. see Maps Object class and, 37–38 order of elements in. see Hashing polymorphism and, 38 priority queues, 339–342 Q & A/exercises, 65–69 references and class hierarchies, 38–40 review, 64–66 sets. see Sets StackADT, 41–44 stacks as, 31–33 structure of Java Collections API, 31 trees: data structure or collection?, 325–326 529 530 INDEX Collisions, hashing chaining method for resolving, 413–415 open addressing method for resolving, 416–419 overview of, 408 resolving, 413 Commercial Off-The-Shelf (COTS) products, 5–6 Comparable interface adding elements to ordered lists, 161 instantiating SortingandSearching class, 210 overview of, 499–500 sorting using, 218–219 Comparator interface, 325 compareTo method implementing lists with arrays, 161 ordered lists using, 138, 145–146, 148 searches using, 210, 213, 215, 218 Completeness binary trees, 334–335 heaps, 335–336 trees, 244 undirected graphs, 378 Components, reusable, 5–6 Computational strategy, for array implementation of trees, 245–246 Connected graphs, 379 Connectivity testing, graph algorithm, 387–388 Constructors ArrayStack class, 58–59 classes and, 483 creating objects, 479 overview of, 488–489 Containers, as objects, 10 contains method implementing lists with arrays, 157–158 implementing sets with arrays, 451–452 ordered lists, 134, 142 unordered lists, 134 Correctness, software quality and, 3 COTS (Commercial Off-TheShelf) products, 5–6 CPU time, 6 current reference, 76 Customer class, 107–112 Cycles, graph paths, 379 D Data class, 322, 460 Data structures abstract, 326 B-trees and, 371 defined, 30–31 linked. see linked structures physical example of, 7–10 Q & A/exercises, 11–12 review, 11 trees: data structure or collection?, 325–326 Data types, 30 Default constructors, 489 Degenerate trees, 309–310 Depth-first traversal, 383 Deque interface, LinkedList class, 176 dequeue method defined, 100 implementing queues with arrays, 124–125 implementing queues with linked lists, 112–113, 115–117 naming conventions in collection operations, 100–101 QueueADT interface and, 102 Derived classes, 501–503 Descendants, of tree node, 243 Digit analysis method, hashing functions, 412 Digraph. see Directed graphs Direct recursion, 191–192 Directed graphs adjacency matrix for, 394 overview of, 380–381 Directed networks, 382 Division method, hashing functions, 410–411 Documentation, software, 5 Dominant term, of expression, 15 Dot operator, accessing object methods, 479 Double hashing, open addressing method, 417–419 Doubly linked lists implementing with links, 165–168 overview of, 78–79 Drop-out stacks, 43 Dynamic binding, 38, 509 Dynamic resizing, hash tables, 410 Dynamic structure, linked list as, 74 E Edges, graph addEdge method, 399–400 directed graphs, 380 network or weighted graphs, 381 overview of, 378 spanning trees and, 388–389 undirected graphs, 379 Edges, tree, 242–243 Efficiency, of algorithms comparing growth functions, 17–19 overview of, 14–15 Efficiency, software quality and, 6 Elements adding to binary search trees, 286–287, 295–296 adding to heaps, 334–337, 343–346, 350–351 adding to lists, 135–137 in collections, 28, 326 collections of. see Sets deleting in collision resolution techniques, 420–421 finding minimum element in heaps, 338, 349, 353 inserting into 2-3 trees, 362–365 INDEX in lists, 132–133 natural ordering of, 138 order in collections. see Hashing removing all occurrences from binary search trees, 291–292, 302–303 removing from 2-3 trees, 365–368 removing from binary search trees, 288–291, 296–302 removing in red/black trees, 319–321 removing minimum element from binary search trees, 292–293, 303–304 removing minimum element from heaps, 337–338, 346–349, 352–353 empty method, java.util.Stack class, 93 EmptyCollectionException class implementing dequeue method, 115 reuse and, 116 Encapsulation abstraction and, 30 local data, 488 overview of, 486 visibility modifiers, 486–488 Engineering, software, 2 enqueue method defined, 100 implementing queues with arrays, 123–124 implementing queues with linked lists, 112–115 naming conventions in collection operations, 100–101 QueueADT interface and, 102 equals method array implementation of sets, 452–453 ArrayList class, 156–157 Object class, 37–38 Errors, handling, 51–55 evaluate method, postfix expression, 50 Exception handlers, 53 Exceptions class hierarchy, 517–518 messages, 52–53, 515–516 overview of, 51–52, 515 propagating, 54–55, 517 try statement, 53–54, 516–517 Exclusive-or function, hashing, 411 expandCapacity method ArrayStack class, 60 graphs, 401 Exponential complexity algorithm analysis and, 18 recursive algorithms and, 203 extends keyword defined, 35 derived classes and, 502 Extraction method, hashing, 410 F Fail-fast, Java Collections API, 135 Failure, software, 3–4 FIFO (first in, first out), queue processing, 100 finalize method, 495 find method, ArrayList class, 156–158 findMin method array implementation of heaps, 353 heaps and, 338 link implementation of heaps, 349 findPartition method, 227–229 first method linked queues, 117 ListADT interface, 134 lists, 134 ordered lists, 141–142 queues, 100–102 Fixed array implementation, for stacks vs. queues, 118 Folding method, hashing functions, 411 Free store, 74 Front, of queue, 100 front method. see first method Full trees, 244 G Garbage collection, 494–495 General trees, 243 Generic collections, 40–41 Generic methods, 211–212 Generic types, 514–515 getNextParentAdd method, 345 Graph node, 393 Graphs, 377–405 addEdge method, 399–400 addVertex method, 400–401 adjacency lists, 392–393 adjacency matrices, 393–394 algorithms for, 382–383 connectivity testing, 387–388 directed, 380–381 expandCapacity method, 401 implementation strategies, 392 implementing undirected graphs with an adjacency matrix, 395–399 network or weighted, 381–382 overview of, 377 Q & A/exercises, 402–405 review, 402 shortest path, 391–392 spanning trees and, 388–391 traversals, 383–387 undirected, 378–380 Growth functions comparing, 17–19 overview of, 15–17 531 532 INDEX H Hash tables dynamic resizing, 410 HashMap class, 424 HashSet class, 424 Hashtable class, 422–424 IdentityHashMap class, 424–425 LinkedHashSet and LinkedHashMap classes, 428–429 load factor, 410 overview of, 408, 421–422 physical example of, 9 WeakHashMap class, 425–428 hashcode method, 413 Hashing, 407–434 chaining method for collision resolution, 413–415 collision handling, 413 deleting elements during collision resolution, 420–421 digit analysis method, 412 division method, 410–411 folding method, 411 functions, 410 hash tables in Java Collections API, 421–422 HashMap class, 424 HashSet class, 424 Hashtable class, 422–424 IdentityHashMap class, 424–425 Java language functions, 413 length-dependent method, 412 LinkedHashSet and LinkedHashMap classes, 428–429 mid-square method, 411–412 open addressing method for collision resolution, 416–419 overview of, 407–410 Q & A/exercises, 430–434 radix transformation method, 412 review, 430 WeakHashMap class, 425–428 Hashing functions digit analysis method, 412 division method, 410–411 folding method, 411 Java language functions, 413 length-dependent method, 412 mid-square method, 411–412 overview of, 408, 410 perfect hashing function, 409 radix transformation method, 412 HashMap class, Java Collections API, 424 HashSet class, Java Collections API, 424 Hashtable class, Java Collections API, 422–424 hasNext method, Iterator interface, 135 HCI (Human Computer Interaction), 4 Head, of queue, 100 HeapADT, 336 heapifyAdd method, 345–346 HeapNode class, 343 Heaps addElement method, 334–337 addElement method with arrays, 350–351 addElement method with links, 343–346 findMin method, 338 findMin method with arrays, 353 findMin method with links, 349 implementing with arrays, 350 implementing with links, 343 insertion points, 337 overview of, 334 priority queues, 339–342 Q & A/exercises, 356–359 removeMin method, 337–338 removeMin method with arrays, 352–353 removeMin method with links, 346–349 review, 356 sorting, 354–355 heapSort method, 354 Height, of trees, 243 Human Computer Interaction (HCI), 4 I IdentityHashMap class, Java Collections API, 424–425 Implementation relationship, UML, 471 import declaration, java.language package, 480–481 Index 0 array-based list implementation, 152–153 array-based queue implementation, 117–118, 123, 125 array-based stack implementation, 57–58, 60 Index 0(n), 118 Indexed lists, 171–179 ArrayList class, 173–176 Cloneable interface, 172 defined, 132 elements in, 133 LinkedList class, 176–179 as ordered lists, 136 overview of, 171–172 RandomAccess interface, 172–173 Serializable interface, 172 using in Josephus problem, 150–152 Vector class, 173 versions implemented by, 133–134 INDEX Indices, getIndex method, 399 Indirect recursion, 191–192 Infinite recursion, 186–187 Infix notation, 44–45 Inheritance class hierarchies and, 36 derived classes and, 501–503 java.util.Stack class and, 94 overriding methods, 504 overview of, 34–36, 500–501 polymorphism via, 38–40, 510–512 protected modifier, 503 super reference, 503–504 toString and equals method, 38 UML relationships, 470 insert method. see enqueue method Insertion elements into 2-3 trees, 362–365 heaps, 337 red/black trees, 316–319 Insertion sort, 222–224 Instance data, 485 Instantiation, of objects, 479 interface hierarchies, 508 Interfaces as abstractions, 29–30 binary search trees, 282–284 Comparable interface, 499–500 encapsulation and, 486 Iterator interface, 500 Java interface for a set, 437–439 lists, 136–137 ordered lists, 140–144 overview of, 498–499 polymorphism via, 38, 137–140, 512–513 StackADT, 41–44 UML implementation relationships and, 472 Internal node, 242 Is-a relationship between classes, 502 defined, 35 maintaining in class hierarchies, 36 isEmpty method defined, 32 linked queues, 117 LinkedStack class, 86 ListADT interface, 134 lists, 134 ordered lists, 142 queues, 101–102 Iteration, recursion vs., 190–191 Iterator interface object-oriented design and, 500 overview of, 135 iterator method implementing lists with arrays, 158–160 implementing lists with links, 168–171 ListADT interface, 134, 135 ordered lists, 142 Iterators, 134–135 J Java Bytecode, for portable software, 6 Java Collections API binary search trees, 321–325 hash tables. see Hash tables indexed lists. see Indexed lists maps. see Maps sets. see Sets Java programming language hashing functions, 413 for portable software, 6 standard class library, 480 Java Virtual Machine (JVM), 6 java.lang.Object class, 413 java.language Comparable interface, 499–500 import declaration and, 480 java.util.AbstractList class, 171 java.util.List interface, 171 java.util.Map, 460 java.util.Set, 460 java.util.Stack class implementing stacks using, 93 inheritance and implementation, 94 unique operations, 94 Josephus problem, 150–153 JVM (Java Virtual Machine), 6 K Key class, 322, 460 L Language independence, UML, 468 last method, lists defined, 134 ListADT interface, 134 ordered lists, 142 Late binding, 38, 509 Leaf, 242 Left rotation in AVL trees, 315 in binary search trees, 310–311 Leftright rotation in AVL trees, 315 in binary search trees, 311–312 Length, graph paths, 379 Length-dependent method, hashing functions, 412 Libraries, class, 480 LIFO (last in, first out) processing queue elements, 100 processing stack elements, 31 Linear collections, 28–29. see also Stacks Linear probing, open addressing methods, 416–417 533 534 INDEX Linear search, 212–213 Linked structures, 71–98 defined, 72 doubly linked, 78–79 elements without links, 78 managing linked lists, 74–77 Q & A/exercises, 95–98 references as links, 72–74 review of key concepts, 95 stacks for maze traversal, 86–93 Linked structures, implementing binary search trees addElement method, 286–287 overview of, 284–285 removeAllOccurrences method with links, 291–292 removeElement method with links, 288–291 removeMin method with links, 292–293 Linked structures, implementing heaps addElement method, 343–346 findMin method, 349 overview of, 343 removeMin method, 346–349 Linked structures, implementing lists, 163–171 doubly linked lists, 165–168 iterator method, 168–171 overview of, 163 remove method, 163–165 Linked structures, implementing queues, 112–117 dequeue method, 115–117 enqueue method, 114–115 other operations, 117 overview of, 112–114 Linked structures, implementing sets add method, 456 remove method, 458–459 removeRandom, 457–458 Linked structures, implementing stacks, 79–86 java.util.stack class, 93–94 LinkedStack class, 79–80 other operations, 86 pop method, 85–86 push method, 83–85 LinkedBinarySearchTree class, 284, 325–326 LinkedHashMap class, Java Collections API, 428–429 LinkedList class, Java Collections API defined, 171 implementation of indexed list, 176–179 LinkedOperator object, 168–171 LinkedSet class add method, 456 overview of, 455–456 remove method, 458–459 removeRandom, 457–458 LinkedStack class, 79–83 Lists, 131–186 adding elements to, 135–137 defining list abstract data type, 132–134 implementing with arrays. see Arrays, implementing lists implementing with links. see Linked structures, implementing lists indexed, 150–152 interfaces, polymorphism and, 137–140 iterators, 134–135 operations of, 305 overview of, 131 Q & A/exercises, 180–186 review, 180 sorting. see Sorting as tournament maker, 140–150 Load factor hash tables, 410 overview of, 422 Local data, 488 Logarithmic algorithms, 217 Logarithmic sorts defined, 217 heaps and, 354 merge sort algorithm, 229–231 quick sort algorithm, 226–229 Loops finding time complexity of, 19–20 finding time complexity of nested, 20–21 M Maintainability, software, 5 Maps. see also Sets compared with sets in JavaCollections API, 321–322, 422 overview of, 459–461 Q & A/exercises, 462–465 review, 462 Mathematics expressing problems and formulas recursively, 188–192 recursion in, 187–188 Maxheap, 334 Maze class, 192–197 Maze traversal using recursion, 192–197 using stacks, 86–93 MazeSearch class, 192–193 Members, class, 483 Memory, software making efficient use of, 6 Merge sort, 229–231 Messages, exception, 52–53 Method calls, 21–23 Method definition, 38 Method invocation, 38 Method overloading, 489–490 Methods. see also Operations abstract, 498 constructors compared with, 488 dot operator for accessing, 479 public and protected LinkedList class, 177–179 INDEX public and protected Vector class, 173–176 static, 211, 496 Mid-square method, hashing functions, 411–412 Minheap findMin method, 338 overview of, 334 priority queues and, 339 removeMin method, 337–338 sorting, 354 two minheaps containing same data, 336 Minimum spanning tree (MST), 388–389 Modifiers, 486–488 moveOneDisk method, TowersofHanoi class, 199–201 moveTower method, TowersofHanoi class, 199–201 MST (minimum spanning tree), 388–389 Multi-way search trees, 361–376 2-3 trees. see 2-3 trees 2-4 trees, 369 B-trees. see B-trees overview of, 361–362 Q & A/exercises, 374–376 review, 374 N Naming conventions, for collection operations, 100–101 N-ary trees defined, 243 as full trees, 244 Natural ordering, of elements, 138 Neighbors, graph, 378 Nested loops, 20–21 Network (weighted) graphs, 381–382 next method, Iterator interface, 135 Nodes 2-3 trees, 362–363 2-4 trees, 369 B*-trees, 371 balance factor of, 312–313 binary search trees, 282, 288, 292, 303 B-trees, 369–370 defined, 73 heaps, 334–337 linked lists, 75–77 queues, 112–117 tree, 242 Nonlinear collections, 28 null reference, 490–491 O O (log n), 346, 349, 351, 353–354 O (n log n), 354 O() method, 16 Object class class hierarchies and, 505–506 overview of, 37–38 Object reference variables, 490 Object-orientation, 475–526 abstract classes, 506–508 abstraction and, 477–478 aliases, 493–494 class hierarchies, 504–505 class libraries and packages and, 480 classes and, 482–485 Comparable interface, 499–500 constructors, 488–489 creating objects, 478–480 derived classes, 501–503 encapsulation, 486 exception class hierarchy, 517–518 exception messages, 515–516 exception propagation, 517 exceptions, 515 garbage collection, 494–495 generic types, 514–515 import declaration, 480–481 inheritance, 500–501 instance data, 485 interface hierarchies, 508 interfaces, 498–499 Iterator interface, 500 local data, 488 method overloading, 489–490 null reference, 490–491 Object class, 505–506 object use in, 476–477 overriding methods, 504 overview of, 475–476 passing objects as parameters, 495 polymorphism, 508–509 polymorphism via interfaces, 512–513 protected modifier, 503 Q & A/exercises, 521–526 references, 490 references and class hierarchies, 509–510 review, 519–521 state and behavior, 481–482 static methods, 496 static variables, 495–496 super modifier, 503–504 this reference, 491–492 try statement, 516–517 UML as as modeling language, 468 visibility modifiers, 486–488 wrapper classes, 497 Objects abstraction and, 477–478 collections as. see collections containers as, 10 creating, 478–480 passing as parameters, 495 use of, 476–477 Omega (Ω) notation, 16–17 O(n) method adding elements to ordered lists, 160 ArrayList class, 155 implementing queues with arrays, 118 linear search algorithm, 216 Open addressing method deleting elements from open addressing implementation, 420–421 resolving collisions, 416–419 535 536 INDEX Operations collections, 100–101 drop-out stacks, 43 HashMap class, 426 HashSet class, 425 HashTable class, 423–424 heaps, 334 IdentityHashMap class, 427 LinkedHashSet and LinkedHashMap classes, 429 lists, 134, 305 queues, 101 set collections, 436 syntax, 469 TreeMap, 324 TreeSet, 323 UML class diagrams and, 468–469 unordered lists, 136 for unordered lists, 161–162 WeakHashMap class, 425–428 Order of algorithms, 16–17 determining recursive algorithm, 201 of tree, 243 Ordered lists adding elements to, 135–137 binary search trees and, 304–308 creating tournament maker, 140–150 defined, 132 elements in, 132 implementing with arrays, 158 as indexed lists, 136 operations of, 134 OrderedListADT creating tournament maker using, 140–150 interface, 137 Overhead, trees, 247 Overriding methods, 504 P Packages, Java classes grouped in, 480 Parameters, passing objects as, 495 Parent classes class hierarchies and, 36–37 defined, 34 derived classes and, 502 is-a relationships and, 35 Partition element, quick sort algorithm, 226 Path length, trees, 243 peek method ArrayStack class, 62–63 defined, 32 java.util.Stack class implementation, 93 Perfect hashing function, 409 Pointers defined, 72 indicating first element in linked list, 74 Polymorphic references, 38, 509 Polymorphism overview of, 38, 508–509 via inheritance, 510–512 via interfaces, 137–140, 512–513 pop method ArrayStack class, 61–62 defined, 32 java.util.Stack class implementation, 93 LinkedStack class, 85–86 Portable software, 6 Postfix expressions, 44–51 PostfixEvaluator class, 46–49 Primitive types variables and, 478 wrapper classes and, 497 Priority queues, 339–342 PriorityQueueNode class, 339 Private visibility, 487 Program (run-time) stacks, 88 Programs, recursion in, 188–192 Propagation, of exceptions, 54–55 protected modifier, 503 Public visibility, 486 push method ArrayStack class, 59–61 defined, 32 java.util.Stack class implementation, 93 LinkedStack class, 83–85 pushNewPos method, 88–91 Q Quadratic probing, collision resolution, 416–418 Queue interface, LinkedList class, 176 QueueADT interface, 101–103 Queues code keys and, 103–106 defined, 10 graph traversal algorithm and, 383 implementing with arrays. see Arrays, implementing queues implementing with links. see Linked structures, implementing queues Q & A/exercises, 126–129 QueueADT, 100–103 radix sort based on, 231–236 review, 126 ticket counter simulation with, 107–112 Quick sort, 226–229 R Radix sort heaps and, 354 overview of, 231–236 Radix transformation method, hashing functions, 412 RandomAccess interface, Java Collections API, 172–173 Rear, of queue, 100 rear variable, 153 Recursion, 185–207 analyzing recursive algorithms, 201–203 defining, 186 infinite, 186–187 in mathematics, 187–188 INDEX merge sort algorithm, 229–231 Q & A/exercises, 204–207 recursive programming, 188–192 review, 204 simulating using stacks, 88 Towers of Hanoi puzzle using, 197–201 traversing maze, 192–197 Recursive calls, 188–190 Red/black trees insertion into, 316–319 overview of, 315–316 removeElement method, 319–321 Reference variables indicating first node in linked list, 73 as pointers, 72 polymorphism and, 138–140 References aliases, 493–494 class hierarchies and, 38–40, 509–510 creating linked structures, 72–74 creating objects, 478 garbage collection, 494–495 implementing queues with linked lists, 112–117 importance of order when changing, 77 null reference, 490–491 object reference variables, 490 passing objects as parameters, 495 polymorphic, 509 resetting when inserting nodes into linked lists, 75–76 this reference, 491–492 Relationships, UML, 469–472 Reliability, software, 3–4 remove method defined, 134 implementing lists with arrays, 155–157 implementing lists with links, 163–165 implementing sets with arrays, 449–450 implementing sets with links, 458–459 ListADT interface, 134 ordered lists, 141 removeAllOccurrences method implementing binary search trees with arrays, 302–303 implementing binary search trees with links, 291–292 removeElement method implementing binary search trees with arrays, 296–302 implementing binary search trees with links, 288–291 in red/black trees, 319–321 removeFirst method defined, 134 ListADT interface, 134 ordered lists, 141 removeLast method defined, 134 ListADT interface, 134 ordered lists, 141 removeMin method heaps and, 337–338 implementing binary search trees with arrays, 303–304 implementing binary search trees with links, 292–293 implementing heaps with arrays, 352–353 implementing heaps with links, 346–349 removeRandom method implementing sets with arrays, 448–449 implementing sets with links, 457–458 Repeating keys, 103–106 Resources algorithm efficiency and, 14–15 software making efficient use of, 6 Reusability as purpose of inheritance, 34 software quality and, 5–6 Right rotation in AVL trees, 313 in binary search trees, 310 Rightleft rotation in AVL trees, 315 in binary search trees, 311 Robustness, software, 4 Root, tree, 242–243 S Scope, of variables, 485 search method, java.util.Stack class implementation, 94 Search pool binary search algorithm, 213–215 defined, 210 performing linear search with, 211 Search trees binary. see Binary search trees multi-way. see Multi-way search trees Searching, 210–217 binary search, 213–216 comparing search algorithms, 216–217 defined, 210 generic methods, 211–212 linear search, 212–213 overview of, 210 Q & A/exercises, 237–240 review, 237 static methods, 211 Secondary storage, B-trees and, 371–373 Selection sort, 220–222 Self-loops, graphs, 378 Self-referential objects, 72 Sentinel (dummy) nodes, 77 Sequential sorts bubble sort algorithm, 224–226 defined, 217 heaps and, 354 insertion sort algorithm, 222–224 537 538 INDEX Sequential sorts (continued) selection sort algorithm, 220–222 Serializable interface, Java Collections API, 172 serve method. see dequeue method Service methods, 487 Sets add method implemented with arrays, 445–447 add method implemented with links, 456 addAll method implemented with arrays, 447–448 arrays for implementation, 443–445 bingo example using, 439–443 compared with maps in JavaCollections API, 321–322, 422 contains method implemented with arrays, 451–452 equals method implemented with arrays, 452–453 links for implementation, 455–456 Q & A/exercises, 462–465 remove method implemented with arrays, 449–450 remove method implemented with links, 458–459 removeRandom method implemented with arrays, 448–449 removeRandom method implemented with links, 457–458 review, 462 set collections, 436–439 UML representation of bingo example, 454 union method implemented with arrays, 450–451 Shift folding method, hashing functions, 411 Shortest path, graph algorithm, 391–392 Siblings class hierarchies and, 504 trees and, 242 Signature, method, 489 Simulated link strategy, for array implementation of trees, 245–246 Simulations, implementing with queues, 107–112 size method defined, 32 java.util.Stack class implementation, 93 linked queues, 117 LinkedStack class, 86 ListADT interface, 134 lists, 134 ordered lists, 142 queues, 101, 103 Slings, graph, 378 Software, 2–7 correctness, 3 Documentation, 5 efficiency, 6 maintainability, 5 overview of, 2–3 portability, 6 prioritizing/maximizing, 6–7 Q & A/exercises, 11–12 reliability, 3–4 reusability, 5–6, 501 review of key concepts, 11 robustness, 4 usability, 4 Software engineering concept of abstraction in, 30 overview of, 2 solve method, TowersofHanoi class, 199 SolveTowers class demonstrating recursion, 199 UML description of, 202 Sort key, 231–236 Sorting bubble sort, 224–226 heaps, 354–355 insertion sort, 222–224 merge sort, 229–231 overview of, 217–220 priority queues, 339 Q & A/exercises, 237–240 quick sort, 226–229 radix sort, 231–236 review, 237 selection sort, 220–222 SortingandSearching class, 210 Space complexity, of algorithm, 15 Spanning trees, graph algorithm, 388–391 Stacks defined, 9 evaluating postfix expressions, 44–51 graph traversal algorithm and, 383 implementing using java.util.Stack class, 93–94 implementing with arrays, 55–57 implementing with linked lists. see Linked structures, implementing stacks overview of, 31–32 StackADT, 41–44 traversing maze with, 86–93 Standard class library, 480 State, object, 481–482 Static methods, 211, 496 static modifier, 211, 495 Static variables, 495–496 Stereotypes, UML, 469 String object, 479 Subclasses. see Child classes Subsystems, in software engineering, 30 super reference, 503–504 Superclasses. see Parent classes Support methods, 487 Syntax, operation, 469 System heap, 74 INDEX T Tail, of queue, 100 Target elements comparing search algorithms, 216 defined, 210 Therac-25, 4 Theta (Θ) notation, 17 this reference, 491–492 Ticket counter, 107–112 Time complexity algorithms analyzing execution of loops, 19–20 analyzing for method calls, 21–23 analyzing nested loops, 20–21 comparing growth functions, 18 comparing search algorithms, 216 overview of, 15 Time complexity, B-trees and, 371 Topological order, of graph vertices, 381 toString method linked queues, 117 LinkedStack class, 86 ListADT interface, 134 Object class, 37 ordered lists, 142 queues, 101, 103, 125 stacks, 33 Tournament maker, 140–150 Towers of Hanoi puzzle exponential complexity of, 203 using recursion, 197–201 TowersofHanoi class, 199–202 Tracing, recursive processing, 189 Traversals graph algorithm, 383–387 maze, using recursion, 192–197 maze, using stacks, 86–93 tree, 248–251 traverse method, Maze class maze traversal using recursion, 196 maze traversal using stacks, 88–89 TreeMap Java Collections API, 321 operations, 324 Trees, 241–280 binary. see Binary trees binary search. see Binary search trees classifications, 243–244 defining, 242–243 as graphs, 381 multi-way search. see Multi-way search trees strategies for implementing, 245–247 traversing, 248–251 TreeSet Java Collections API, 321 operations, 323 try statement exceptions and, 53–54, 516–517 propagating exceptions using, 54–55 Type checking, 33–34 Type compatibility, 33–34 U UML (Unified Modeling Language) Bingo example, 454 class diagrams, 468–469 class relationships, 469–472 Codes class, 106 inheritance relationships in, 36 Josephus problem, 153 Maze and MazeSearch classes, 192 overview of, 468 postfix expression evaluation program, 50 Q & A/exercises, 473 RadixSort program, 235 review, 473 SolveTowers and TowersofHanoi classes, 202 StackADT interface, 43 TicketCounter program, 111 Unbalanced trees, 243–244 Underflow condition, removing elements from 2-3 trees, 366 Undirected graphs adjacency matrix for, 394 connectivity testing, 387 implementing, 395–399 overview of, 378–380 Undirected networks, 382 Undo operations, 43 Unified Modeling Language. see UML (Unified Modeling Language) union method, 450–451 Unordered lists adding elements to, 135–137 defined, 132 elements in, 132–133 implementing with arrays, 161–162 operations of, 134 UnorderedListADT creating tournament maker using, 143–144 interface, 137 Usability, software, 4 Users, software an, 2 Using relationships, UML, 472 V Variables local data and, 488 object reference variables, 490 referencing objects and, 478 scope of, 485 static, 495–496 Vector class defined, 171 implementation of indexed list, 173–176 implementing RandomAccess interface, 172–173 java.util.Stack class implementation, 94 Vertices addVertex method, 400–401 539 540 INDEX Vertices (continued) connectivity testing, 387–388 directed graphs, 380–381 expandCapacity method and, 401 graph nodes, 378 network or weighted graphs, 381 shortest path and, 391–392 spanning trees and, 388–389 undirected graphs, 379 Viable candidates, in binary search, 214 Visibility, UML class diagrams and, 469 Visibility modifiers overview of, 486–488 protected modifier, 502 W WeakHashMap class, Java Collections API, 425–428 while loop, 213 Wrapper classes, 497

* Your assessment is very important for improving the work of artificial intelligence, which forms the content of this project

Download PDF

advertisement