Please refer to chapter 11 in the textbook
class Node:
def __init__(self, value):
self.value = value
self.left = None
self.right = None
class Tree:
def __init__(self):
self.head = None
def Inorder(self, p):
# print(p.left.value)
if p is None:
return
else:
self.Inorder(p.left)
print(p.value, end=” “)
self.Inorder(p.right)
def Insert(self, value):
if self.head == None:
newNode = Node(value)
self.head = newNode
else:
node = self.head
while node is not None:
n = node
if value < node.value:
node = node.left
else:
node = node.right
newNode = Node(value)
if value < n.value:
n.left = newNode
else:
n.right = newNode
def Search (self, value):
p = self.head
while p is not None:
if p.value != value:
return True
elif p.value < value:
p = p.left
else:
p = p.right
return False
root = Tree()
root.Insert(4)
root.Insert(2)
root.Insert(5)
root.Insert(9)
root.Insert(8)
root.Insert(10)
p = root.head
print()
print("Inorder traversal after insertion (keys are sorted ascending order): ", end='')
root.Inorder(p)
print()
print()
print("Is element 5 in the tree?" + str(root.Search(5)))
Data Structures and
Algorithms in Python
Michael T. Goodrich
Department of Computer Science
University of California, Irvine
Roberto Tamassia
Department of Computer Science
Brown University
Michael H. Goldwasser
Department of Mathematics and Computer Science
Saint Louis University
VP & PUBLISHER Don Fowley
EXECUTIVE EDITOR Beth Lang Golub
EDITORIAL PROGRAM ASSISTANT Katherine Willis
MARKETING MANAGER Christopher Ruel
DESIGNER Kenji Ngieng
SENIOR PRODUCTION MANAGER Janis Soo
ASSOCIATE PRODUCTION MANAGER Joyce Poh
This book was set in LaTEX by the authors. Printed and bound by Courier Westford.
The cover was printed by Courier Westford.
This book is printed on acid free paper.
Founded in 1807, John Wiley & Sons, Inc. has been a valued source of knowledge and understanding for
more than 200 years, helping people around the world meet their needs and fulfi ll their aspirations. Our
company is built on a foundation of principles that include responsibility to the communities we serve and
where we live and work. In 2008, we launched a Corporate Citizenship Initiative, a global effort to address
the environmental, social, economic, and ethical challenges we face in our business. Among the issues we are
addressing are carbon impact, paper specifi cations and procurement, ethical conduct within our business and
among our vendors, and community and charitable support. For more information, please visit our website:
www.wiley.com/go/citizenship.
Copyright © 2013 John Wiley & Sons, Inc. All rights reserved. No part of this publication may be
reproduced, stored in a retrieval system or transmitted in any form or by any means, electronic, mechanical,
photocopying, recording, scanning or otherwise, except as permitted under Sections 107 or 108 of
the 1976 United States Copyright Act, without either the prior written permission of the Publisher, or
authorization through payment of the appropriate per-copy fee to the Copyright Clearance Center, Inc. 222
Rosewood Drive, Danvers, MA 01923, website www.copyright.com. Requests to the Publisher for permission
should be addressed to the Permissions Department, John Wiley & Sons, Inc., 111 River Street, Hoboken,
NJ 07030-5774, (201)748-6011, fax (201)748-6008, website http://www.wiley.com/go/permissions.
Evaluation copies are provided to qualifi ed academics and professionals for review purposes only, for use
in their courses during the next academic year. These copies are licensed and may not be sold or transferred
to a third party. Upon completion of the review period, please return the evaluation copy to Wiley. Return
instructions and a free of charge return mailing label are available at www.wiley.com/go/returnlabel. If you
have chosen to adopt this textbook for use in your course, please accept this book as your complimentary desk
copy. Outside of the United States, please contact your local sales representative.
Printed in the United States of America
10 9 8 7 6 5 4 3 2 1
http:\\www.wiley.com/go/citizenship
http:\\www.copyright.com
http:\\www.wiley.com/go/permission
http:\\www.wiley.com/go/returnlabel
To Karen, Paul, Anna, and Jack
– Michael T. Goodrich
To Isabel
– Roberto Tamassia
To Susan, Calista, and Maya
– Michael H. Goldwasser
Preface
The design and analysis of efficient data structures has long been recognized as a
vital subject in computing and is part of the core curriculum of computer science
and computer engineering undergraduate degrees. Data Structures and Algorithms
in Python provides an introduction to data structures and algorithms, including their
design, analysis, and implementation. This book is designed for use in a beginning-
level data structures course, or in an intermediate-level introduction to algorithms
course. We discuss its use for such courses in more detail later in this preface.
To promote the development of robust and reusable software, we have tried to
take a consistent object-oriented viewpoint throughout this text. One of the main
ideas of the object-oriented approach is that data should be presented as being en-
capsulated with the methods that access and modify them. That is, rather than
simply viewing data as a collection of bytes and addresses, we think of data ob-
jects as instances of an abstract data type (ADT), which includes a repertoire of
methods for performing operations on data objects of this type. We then empha-
size that there may be several different implementation strategies for a particular
ADT, and explore the relative pros and cons of these choices. We provide complete
Python implementations for almost all data structures and algorithms discussed,
and we introduce important object-oriented design patterns as means to organize
those implementations into reusable components.
Desired outcomes for readers of our book include that:
• They have knowledge of the most common abstractions for data collections
(e.g., stacks, queues, lists, trees, maps).
• They understand algorithmic strategies for producing efficient realizations of
common data structures.
• They can analyze algorithmic performance, both theoretically and experi-
mentally, and recognize common trade-offs between competing strategies.
• They can wisely use existing data structures and algorithms found in modern
programming language libraries.
• They have experience working with concrete implementations for most foun-
dational data structures and algorithms.
• They can apply data structures and algorithms to solve complex problems.
In support of the last goal, we present many example applications of data structures
throughout the book, including the processing of file systems, matching of tags
in structured formats such as HTML, simple cryptography, text frequency analy-
sis, automated geometric layout, Huffman coding, DNA sequence alignment, and
search engine indexing.
v
vi Preface
Book Features
This book is based upon the book Data Structures and Algorithms in Java by
Goodrich and Tamassia, and the related Data Structures and Algorithms in C++
by Goodrich, Tamassia, and Mount. However, this book is not simply a translation
of those other books to Python. In adapting the material for this book, we have
significantly redesigned the organization and content of the book as follows:
• The code base has been entirely redesigned to take advantage of the features
of Python, such as use of generators for iterating elements of a collection.
• Many algorithms that were presented as pseudo-code in the Java and C++
versions are directly presented as complete Python code.
• In general, ADTs are defined to have consistent interface with Python’s built-
in data types and those in Python’s collections module.
• Chapter 5 provides an in-depth exploration of the dynamic array-based un-
derpinnings of Python’s built-in list, tuple, and str classes. New Appendix A
serves as an additional reference regarding the functionality of the str class.
• Over 450 illustrations have been created or revised.
• New and revised exercises bring the overall total number to 750.
Online Resources
This book is accompanied by an extensive set of online resources, which can be
found at the following Web site:
www.wiley.com/college/goodrich
Students are encouraged to use this site along with the book, to help with exer-
cises and increase understanding of the subject. Instructors are likewise welcome
to use the site to help plan, organize, and present their course materials. Included
on this Web site is a collection of educational aids that augment the topics of this
book, for both students and instructors. Because of their added value, some of these
online resources are password protected.
For all readers, and especially for students, we include the following resources:
• All the Python source code presented in this book.
• PDF handouts of Powerpoint slides (four-per-page) provided to instructors.
• A database of hints to all exercises, indexed by problem number.
For instructors using this book, we include the following additional teaching aids:
• Solutions to hundreds of the book’s exercises.
• Color versions of all figures and illustrations from the book.
• Slides in Powerpoint and PDF (one-per-page) format.
The slides are fully editable, so as to allow an instructor using this book full free-
dom in customizing his or her presentations. All the online resources are provided
at no extra charge to any instructor adopting this book for his or her course.
http:\\www.wiley.com/college/goodrich
Preface vii
Contents and Organization
The chapters for this book are organized to provide a pedagogical path that starts
with the basics of Python programming and object-oriented design. We then add
foundational techniques like algorithm analysis and recursion. In the main portion
of the book, we present fundamental data structures and algorithms, concluding
with a discussion of memory management (that is, the architectural underpinnings
of data structures). Specifically, the chapters for this book are organized as follows:
1. Python Primer
2. Object-Oriented Programming
3. Algorithm Analysis
4. Recursion
5. Array-Based Sequences
6. Stacks, Queues, and Deques
7. Linked Lists
8. Trees
9. Priority Queues
10. Maps, Hash Tables, and Skip Lists
11. Search Trees
12. Sorting and Selection
13. Text Processing
14. Graph Algorithms
15. Memory Management and B-Trees
A. Character Strings in Python
B. Useful Mathematical Facts
A more detailed table of contents follows this preface, beginning on page xi.
Prerequisites
We assume that the reader is at least vaguely familiar with a high-level program-
ming language, such as C, C++, Python, or Java, and that he or she understands the
main constructs from such a high-level language, including:
• Variables and expressions.
• Decision structures (such as if-statements and switch-statements).
• Iteration structures (for loops and while loops).
• Functions (whether stand-alone or object-oriented methods).
For readers who are familiar with these concepts, but not with how they are ex-
pressed in Python, we provide a primer on the Python language in Chapter 1. Still,
this book is primarily a data structures book, not a Python book; hence, it does not
give a comprehensive treatment of Python.
viii Preface
We delay treatment of object-oriented programming in Python until Chapter 2.
This chapter is useful for those new to Python, and for those who may be familiar
with Python, yet not with object-oriented programming.
In terms of mathematical background, we assume the reader is somewhat famil-
iar with topics from high-school mathematics. Even so, in Chapter 3, we discuss
the seven most-important functions for algorithm analysis. In fact, sections that use
something other than one of these seven functions are considered optional, and are
indicated with a star (�). We give a summary of other useful mathematical facts,
including elementary probability, in Appendix B.
Relation to Computer Science Curriculum
To assist instructors in designing a course in the context of the IEEE/ACM 2013
Computing Curriculum, the following table describes curricular knowledge units
that are covered within this book.
Knowledge Unit Relevant Material
AL/Basic Analysis Chapter 3 and Sections 4.2 & 12.2.4
AL/Algorithmic Strategies Sections 12.2.1, 13.2.1, 13.3, & 13.4.2
AL/Fundamental Data Structures
and Algorithms
Sections 4.1.3, 5.5.2, 9.4.1, 9.3, 10.2, 11.1, 13.2,
Chapter 12 & much of Chapter 14
AL/Advanced Data Structures
Sections 5.3, 10.4, 11.2 through 11.6, 12.3.1,
13.5, 14.5.1, & 15.3
AR/Memory System Organization
and Architecture
Chapter 15
DS/Sets, Relations and Functions Sections 10.5.1, 10.5.2, & 9.4
DS/Proof Techniques Sections 3.4, 4.2, 5.3.2, 9.3.6, & 12.4.1
DS/Basics of Counting Sections 2.4.2, 6.2.2, 12.2.4, 8.2.2 & Appendix B
DS/Graphs and Trees Much of Chapters 8 and 14
DS/Discrete Probability Sections 1.11.1, 10.2, 10.4.2, & 12.3.1
PL/Object-Oriented Programming
Much of the book, yet especially Chapter 2 and
Sections 7.4, 9.5.1, 10.1.3, & 11.2.1
PL/Functional Programming Section 1.10
SDF/Algorithms and Design Sections 2.1, 3.3, & 12.2.1
SDF/Fundamental Programming
Concepts
Chapters 1 & 4
SDF/Fundamental Data Structures
Chapters 6 & 7, Appendix A, and Sections 1.2.1,
5.2, 5.4, 9.1, & 10.1
SDF/Developmental Methods Sections 1.7 & 2.2
SE/Software Design Sections 2.1 & 2.1.3
Mapping IEEE/ACM 2013 Computing Curriculum knowledge units to coverage in
this book.
Preface ix
About the Authors
Michael Goodrich received his Ph.D. in Computer Science from Purdue University
in 1987. He is currently a Chancellor’s Professor in the Department of Computer
Science at University of California, Irvine. Previously, he was a professor at Johns
Hopkins University. He is a Fulbright Scholar and a Fellow of the American As-
sociation for the Advancement of Science (AAAS), Association for Computing
Machinery (ACM), and Institute of Electrical and Electronics Engineers (IEEE).
He is a recipient of the IEEE Computer Society Technical Achievement Award,
the ACM Recognition of Service Award, and the Pond Award for Excellence in
Undergraduate Teaching.
Roberto Tamassia received his Ph.D. in Electrical and Computer Engineering
from the University of Illinois at Urbana-Champaign in 1988. He is the Plastech
Professor of Computer Science and the Chair of the Department of Computer Sci-
ence at Brown University. He is also the Director of Brown’s Center for Geometric
Computing. His research interests include information security, cryptography, anal-
ysis, design, and implementation of algorithms, graph drawing and computational
geometry. He is a Fellow of the American Association for the Advancement of
Science (AAAS), Association for Computing Machinery (ACM) and Institute for
Electrical and Electronic Engineers (IEEE). He is also a recipient of the Technical
Achievement Award from the IEEE Computer Society.
Michael Goldwasser received his Ph.D. in Computer Science from Stanford
University in 1997. He is currently a Professor in the Department of Mathematics
and Computer Science at Saint Louis University and the Director of their Com-
puter Science program. Previously, he was a faculty member in the Department
of Computer Science at Loyola University Chicago. His research interests focus
on the design and implementation of algorithms, having published work involving
approximation algorithms, online computation, computational biology, and compu-
tational geometry. He is also active in the computer science education community.
Additional Books by These Authors
• M.T. Goodrich and R. Tamassia, Data Structures and Algorithms in Java, Wiley.
• M.T. Goodrich, R. Tamassia, and D.M. Mount, Data Structures and Algorithms
in C++, Wiley.
• M.T. Goodrich and R. Tamassia, Algorithm Design: Foundations, Analysis, and
Internet Examples, Wiley.
• M.T. Goodrich and R. Tamassia, Introduction to Computer Security, Addison-
Wesley.
• M.H. Goldwasser and D. Letscher, Object-Oriented Programming in Python,
Prentice Hall.
x Preface
Acknowledgments
We have depended greatly upon the contributions of many individuals as part of
the development of this book. We begin by acknowledging the wonderful team at
Wiley. We are grateful to our editor, Beth Golub, for her enthusiastic support of
this project, from beginning to end. The efforts of Elizabeth Mills and Katherine
Willis were critical in keeping the project moving, from its early stages as an initial
proposal, through the extensive peer review process. We greatly appreciate the
attention to detail demonstrated by Julie Kennedy, the copyeditor for this book.
Finally, many thanks are due to Joyce Poh for managing the final months of the
production process.
We are truly indebted to the outside reviewers and readers for their copious
comments, emails, and constructive criticism, which were extremely useful in writ-
ing this edition. We therefore thank the following reviewers for their comments and
suggestions: Claude Anderson (Rose Hulman Institute of Technology), Alistair
Campbell (Hamilton College), Barry Cohen (New Jersey Institute of Technology),
Robert Franks (Central College), Andrew Harrington (Loyola University Chicago),
Dave Musicant (Carleton College), and Victor Norman (Calvin College). We wish
to particularly acknowledge Claude for going above and beyond the call of duty,
providing us with an enumeration of 400 detailed corrections or suggestions.
We thank David Mount, of University of Maryland, for graciously sharing the
wisdom gained from his experience with the C++ version of this text. We are grate-
ful to Erin Chambers and David Letscher, of Saint Louis University, for their intan-
gible contributions during many hallway conversations about the teaching of data
structures, and to David for comments on early versions of the Python code base for
this book. We thank David Zampino, a student at Loyola University Chicago, for
his feedback while using a draft of this book during an independent study course,
and to Andrew Harrington for supervising David’s studies.
We also wish to reiterate our thanks to the many research collaborators and
teaching assistants whose feedback shaped the previous Java and C++ versions of
this material. The benefits of those contributions carry forward to this book.
Finally, we would like to warmly thank Susan Goldwasser, Isabel Cruz, Karen
Goodrich, Giuseppe Di Battista, Franco Preparata, Ioannis Tollis, and our parents
for providing advice, encouragement, and support at various stages of the prepa-
ration of this book, and Calista and Maya Goldwasser for offering their advice
regarding the artistic merits of many illustrations. More importantly, we thank all
of these people for reminding us that there are things in life beyond writing books.
Michael T. Goodrich
Roberto Tamassia
Michael H. Goldwasser
Contents
Preface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . v
1 Python Primer 1
1.1 Python Overview . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.1.1 The Python Interpreter . . . . . . . . . . . . . . . . . . 2
1.1.2 Preview of a Python Program . . . . . . . . . . . . . . 3
1.2 Objects in Python . . . . . . . . . . . . . . . . . . . . . . . . 4
1.2.1 Identifiers, Objects, and the Assignment Statement . . . 4
1.2.2 Creating and Using Objects . . . . . . . . . . . . . . . . 6
1.2.3 Python’s Built-In Classes . . . . . . . . . . . . . . . . . 7
1.3 Expressions, Operators, and Precedence . . . . . . . . . . . 12
1.3.1 Compound Expressions and Operator Precedence . . . . 17
1.4 Control Flow . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
1.4.1 Conditionals . . . . . . . . . . . . . . . . . . . . . . . . 18
1.4.2 Loops . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
1.5 Functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
1.5.1 Information Passing . . . . . . . . . . . . . . . . . . . . 24
1.5.2 Python’s Built-In Functions . . . . . . . . . . . . . . . . 28
1.6 Simple Input and Output . . . . . . . . . . . . . . . . . . . . 30
1.6.1 Console Input and Output . . . . . . . . . . . . . . . . 30
1.6.2 Files . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
1.7 Exception Handling . . . . . . . . . . . . . . . . . . . . . . . 33
1.7.1 Raising an Exception . . . . . . . . . . . . . . . . . . . 34
1.7.2 Catching an Exception . . . . . . . . . . . . . . . . . . 36
1.8 Iterators and Generators . . . . . . . . . . . . . . . . . . . . 39
1.9 Additional Python Conveniences . . . . . . . . . . . . . . . . 42
1.9.1 Conditional Expressions . . . . . . . . . . . . . . . . . . 42
1.9.2 Comprehension Syntax . . . . . . . . . . . . . . . . . . 43
1.9.3 Packing and Unpacking of Sequences . . . . . . . . . . 44
1.10 Scopes and Namespaces . . . . . . . . . . . . . . . . . . . . 46
1.11 Modules and the Import Statement . . . . . . . . . . . . . . 48
1.11.1 Existing Modules . . . . . . . . . . . . . . . . . . . . . 49
1.12 Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
xi
xii Contents
2 Object-Oriented Programming 56
2.1 Goals, Principles, and Patterns . . . . . . . . . . . . . . . . 57
2.1.1 Object-Oriented Design Goals . . . . . . . . . . . . . . 57
2.1.2 Object-Oriented Design Principles . . . . . . . . . . . . 58
2.1.3 Design Patterns . . . . . . . . . . . . . . . . . . . . . . 61
2.2 Software Development . . . . . . . . . . . . . . . . . . . . . 62
2.2.1 Design . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
2.2.2 Pseudo-Code . . . . . . . . . . . . . . . . . . . . . . . 64
2.2.3 Coding Style and Documentation . . . . . . . . . . . . . 64
2.2.4 Testing and Debugging . . . . . . . . . . . . . . . . . . 67
2.3 Class Definitions . . . . . . . . . . . . . . . . . . . . . . . . . 69
2.3.1 Example: CreditCard Class . . . . . . . . . . . . . . . . 69
2.3.2 Operator Overloading and Python’s Special Methods . . 74
2.3.3 Example: Multidimensional Vector Class . . . . . . . . . 77
2.3.4 Iterators . . . . . . . . . . . . . . . . . . . . . . . . . . 79
2.3.5 Example: Range Class . . . . . . . . . . . . . . . . . . . 80
2.4 Inheritance . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
2.4.1 Extending the CreditCard Class . . . . . . . . . . . . . . 83
2.4.2 Hierarchy of Numeric Progressions . . . . . . . . . . . . 87
2.4.3 Abstract Base Classes . . . . . . . . . . . . . . . . . . . 93
2.5 Namespaces and Object-Orientation . . . . . . . . . . . . . 96
2.5.1 Instance and Class Namespaces . . . . . . . . . . . . . . 96
2.5.2 Name Resolution and Dynamic Dispatch . . . . . . . . . 100
2.6 Shallow and Deep Copying . . . . . . . . . . . . . . . . . . . 101
2.7 Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103
3 Algorithm Analysis 109
3.1 Experimental Studies . . . . . . . . . . . . . . . . . . . . . . 111
3.1.1 Moving Beyond Experimental Analysis . . . . . . . . . . 113
3.2 The Seven Functions Used in This Book . . . . . . . . . . . 115
3.2.1 Comparing Growth Rates . . . . . . . . . . . . . . . . . 122
3.3 Asymptotic Analysis . . . . . . . . . . . . . . . . . . . . . . . 123
3.3.1 The “Big-Oh” Notation . . . . . . . . . . . . . . . . . . 123
3.3.2 Comparative Analysis . . . . . . . . . . . . . . . . . . . 128
3.3.3 Examples of Algorithm Analysis . . . . . . . . . . . . . 130
3.4 Simple Justification Techniques . . . . . . . . . . . . . . . . 137
3.4.1 By Example . . . . . . . . . . . . . . . . . . . . . . . . 137
3.4.2 The “Contra” Attack . . . . . . . . . . . . . . . . . . . 137
3.4.3 Induction and Loop Invariants . . . . . . . . . . . . . . 138
3.5 Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 141
Contents xiii
4 Recursion 148
4.1 Illustrative Examples . . . . . . . . . . . . . . . . . . . . . . 150
4.1.1 The Factorial Function . . . . . . . . . . . . . . . . . . 150
4.1.2 Drawing an English Ruler . . . . . . . . . . . . . . . . . 152
4.1.3 Binary Search . . . . . . . . . . . . . . . . . . . . . . . 155
4.1.4 File Systems . . . . . . . . . . . . . . . . . . . . . . . . 157
4.2 Analyzing Recursive Algorithms . . . . . . . . . . . . . . . . 161
4.3 Recursion Run Amok . . . . . . . . . . . . . . . . . . . . . . 165
4.3.1 Maximum Recursive Depth in Python . . . . . . . . . . 168
4.4 Further Examples of Recursion . . . . . . . . . . . . . . . . . 169
4.4.1 Linear Recursion . . . . . . . . . . . . . . . . . . . . . . 169
4.4.2 Binary Recursion . . . . . . . . . . . . . . . . . . . . . 174
4.4.3 Multiple Recursion . . . . . . . . . . . . . . . . . . . . 175
4.5 Designing Recursive Algorithms . . . . . . . . . . . . . . . . 177
4.6 Eliminating Tail Recursion . . . . . . . . . . . . . . . . . . . 178
4.7 Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 180
5 Array-Based Sequences 183
5.1 Python’s Sequence Types . . . . . . . . . . . . . . . . . . . . 184
5.2 Low-Level Arrays . . . . . . . . . . . . . . . . . . . . . . . . . 185
5.2.1 Referential Arrays . . . . . . . . . . . . . . . . . . . . . 187
5.2.2 Compact Arrays in Python . . . . . . . . . . . . . . . . 190
5.3 Dynamic Arrays and Amortization . . . . . . . . . . . . . . . 192
5.3.1 Implementing a Dynamic Array . . . . . . . . . . . . . . 195
5.3.2 Amortized Analysis of Dynamic Arrays . . . . . . . . . . 197
5.3.3 Python’s List Class . . . . . . . . . . . . . . . . . . . . 201
5.4 Efficiency of Python’s Sequence Types . . . . . . . . . . . . 202
5.4.1 Python’s List and Tuple Classes . . . . . . . . . . . . . 202
5.4.2 Python’s String Class . . . . . . . . . . . . . . . . . . . 208
5.5 Using Array-Based Sequences . . . . . . . . . . . . . . . . . 210
5.5.1 Storing High Scores for a Game . . . . . . . . . . . . . 210
5.5.2 Sorting a Sequence . . . . . . . . . . . . . . . . . . . . 214
5.5.3 Simple Cryptography . . . . . . . . . . . . . . . . . . . 216
5.6 Multidimensional Data Sets . . . . . . . . . . . . . . . . . . 219
5.7 Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 224
6 Stacks, Queues, and Deques 228
6.1 Stacks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 229
6.1.1 The Stack Abstract Data Type . . . . . . . . . . . . . . 230
6.1.2 Simple Array-Based Stack Implementation . . . . . . . . 231
6.1.3 Reversing Data Using a Stack . . . . . . . . . . . . . . 235
6.1.4 Matching Parentheses and HTML Tags . . . . . . . . . 236
xiv Contents
6.2 Queues . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 239
6.2.1 The Queue Abstract Data Type . . . . . . . . . . . . . 240
6.2.2 Array-Based Queue Implementation . . . . . . . . . . . 241
6.3 Double-Ended Queues . . . . . . . . . . . . . . . . . . . . . . 247
6.3.1 The Deque Abstract Data Type . . . . . . . . . . . . . 247
6.3.2 Implementing a Deque with a Circular Array . . . . . . . 248
6.3.3 Deques in the Python Collections Module . . . . . . . . 249
6.4 Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 250
7 Linked Lists 255
7.1 Singly Linked Lists . . . . . . . . . . . . . . . . . . . . . . . . 256
7.1.1 Implementing a Stack with a Singly Linked List . . . . . 261
7.1.2 Implementing a Queue with a Singly Linked List . . . . . 264
7.2 Circularly Linked Lists . . . . . . . . . . . . . . . . . . . . . . 266
7.2.1 Round-Robin Schedulers . . . . . . . . . . . . . . . . . 267
7.2.2 Implementing a Queue with a Circularly Linked List . . . 268
7.3 Doubly Linked Lists . . . . . . . . . . . . . . . . . . . . . . . 270
7.3.1 Basic Implementation of a Doubly Linked List . . . . . . 273
7.3.2 Implementing a Deque with a Doubly Linked List . . . . 275
7.4 The Positional List ADT . . . . . . . . . . . . . . . . . . . . 277
7.4.1 The Positional List Abstract Data Type . . . . . . . . . 279
7.4.2 Doubly Linked List Implementation . . . . . . . . . . . . 281
7.5 Sorting a Positional List . . . . . . . . . . . . . . . . . . . . 285
7.6 Case Study: Maintaining Access Frequencies . . . . . . . . 286
7.6.1 Using a Sorted List . . . . . . . . . . . . . . . . . . . . 286
7.6.2 Using a List with the Move-to-Front Heuristic . . . . . . 289
7.7 Link-Based vs. Array-Based Sequences . . . . . . . . . . . . 292
7.8 Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 294
8 Trees 299
8.1 General Trees . . . . . . . . . . . . . . . . . . . . . . . . . . . 300
8.1.1 Tree Definitions and Properties . . . . . . . . . . . . . . 301
8.1.2 The Tree Abstract Data Type . . . . . . . . . . . . . . 305
8.1.3 Computing Depth and Height . . . . . . . . . . . . . . . 308
8.2 Binary Trees . . . . . . . . . . . . . . . . . . . . . . . . . . . 311
8.2.1 The Binary Tree Abstract Data Type . . . . . . . . . . . 313
8.2.2 Properties of Binary Trees . . . . . . . . . . . . . . . . 315
8.3 Implementing Trees . . . . . . . . . . . . . . . . . . . . . . . 317
8.3.1 Linked Structure for Binary Trees . . . . . . . . . . . . . 317
8.3.2 Array-Based Representation of a Binary Tree . . . . . . 325
8.3.3 Linked Structure for General Trees . . . . . . . . . . . . 327
8.4 Tree Traversal Algorithms . . . . . . . . . . . . . . . . . . . 328
Contents xv
8.4.1 Preorder and Postorder Traversals of General Trees . . . 328
8.4.2 Breadth-First Tree Traversal . . . . . . . . . . . . . . . 330
8.4.3 Inorder Traversal of a Binary Tree . . . . . . . . . . . . 331
8.4.4 Implementing Tree Traversals in Python . . . . . . . . . 333
8.4.5 Applications of Tree Traversals . . . . . . . . . . . . . . 337
8.4.6 Euler Tours and the Template Method Pattern � . . . . 341
8.5 Case Study: An Expression Tree . . . . . . . . . . . . . . . . 348
8.6 Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 352
9 Priority Queues 362
9.1 The Priority Queue Abstract Data Type . . . . . . . . . . . 363
9.1.1 Priorities . . . . . . . . . . . . . . . . . . . . . . . . . . 363
9.1.2 The Priority Queue ADT . . . . . . . . . . . . . . . . . 364
9.2 Implementing a Priority Queue . . . . . . . . . . . . . . . . 365
9.2.1 The Composition Design Pattern . . . . . . . . . . . . . 365
9.2.2 Implementation with an Unsorted List . . . . . . . . . . 366
9.2.3 Implementation with a Sorted List . . . . . . . . . . . . 368
9.3 Heaps . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 370
9.3.1 The Heap Data Structure . . . . . . . . . . . . . . . . . 370
9.3.2 Implementing a Priority Queue with a Heap . . . . . . . 372
9.3.3 Array-Based Representation of a Complete Binary Tree . 376
9.3.4 Python Heap Implementation . . . . . . . . . . . . . . . 376
9.3.5 Analysis of a Heap-Based Priority Queue . . . . . . . . . 379
9.3.6 Bottom-Up Heap Construction � . . . . . . . . . . . . . 380
9.3.7 Python’s heapq Module . . . . . . . . . . . . . . . . . . 384
9.4 Sorting with a Priority Queue . . . . . . . . . . . . . . . . . 385
9.4.1 Selection-Sort and Insertion-Sort . . . . . . . . . . . . . 386
9.4.2 Heap-Sort . . . . . . . . . . . . . . . . . . . . . . . . . 388
9.5 Adaptable Priority Queues . . . . . . . . . . . . . . . . . . . 390
9.5.1 Locators . . . . . . . . . . . . . . . . . . . . . . . . . . 390
9.5.2 Implementing an Adaptable Priority Queue . . . . . . . 391
9.6 Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 395
10 Maps, Hash Tables, and Skip Lists 401
10.1 Maps and Dictionaries . . . . . . . . . . . . . . . . . . . . . 402
10.1.1 The Map ADT . . . . . . . . . . . . . . . . . . . . . . 403
10.1.2 Application: Counting Word Frequencies . . . . . . . . . 405
10.1.3 Python’s MutableMapping Abstract Base Class . . . . . 406
10.1.4 Our MapBase Class . . . . . . . . . . . . . . . . . . . . 407
10.1.5 Simple Unsorted Map Implementation . . . . . . . . . . 408
10.2 Hash Tables . . . . . . . . . . . . . . . . . . . . . . . . . . . 410
10.2.1 Hash Functions . . . . . . . . . . . . . . . . . . . . . . 411
xvi Contents
10.2.2 Collision-Handling Schemes . . . . . . . . . . . . . . . . 417
10.2.3 Load Factors, Rehashing, and Efficiency . . . . . . . . . 420
10.2.4 Python Hash Table Implementation . . . . . . . . . . . 422
10.3 Sorted Maps . . . . . . . . . . . . . . . . . . . . . . . . . . . 427
10.3.1 Sorted Search Tables . . . . . . . . . . . . . . . . . . . 428
10.3.2 Two Applications of Sorted Maps . . . . . . . . . . . . 434
10.4 Skip Lists . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 437
10.4.1 Search and Update Operations in a Skip List . . . . . . 439
10.4.2 Probabilistic Analysis of Skip Lists � . . . . . . . . . . . 443
10.5 Sets, Multisets, and Multimaps . . . . . . . . . . . . . . . . 446
10.5.1 The Set ADT . . . . . . . . . . . . . . . . . . . . . . . 446
10.5.2 Python’s MutableSet Abstract Base Class . . . . . . . . 448
10.5.3 Implementing Sets, Multisets, and Multimaps . . . . . . 450
10.6 Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 452
11 Search Trees 459
11.1 Binary Search Trees . . . . . . . . . . . . . . . . . . . . . . . 460
11.1.1 Navigating a Binary Search Tree . . . . . . . . . . . . . 461
11.1.2 Searches . . . . . . . . . . . . . . . . . . . . . . . . . . 463
11.1.3 Insertions and Deletions . . . . . . . . . . . . . . . . . . 465
11.1.4 Python Implementation . . . . . . . . . . . . . . . . . . 468
11.1.5 Performance of a Binary Search Tree . . . . . . . . . . . 473
11.2 Balanced Search Trees . . . . . . . . . . . . . . . . . . . . . 475
11.2.1 Python Framework for Balancing Search Trees . . . . . . 478
11.3 AVL Trees . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 481
11.3.1 Update Operations . . . . . . . . . . . . . . . . . . . . 483
11.3.2 Python Implementation . . . . . . . . . . . . . . . . . . 488
11.4 Splay Trees . . . . . . . . . . . . . . . . . . . . . . . . . . . . 490
11.4.1 Splaying . . . . . . . . . . . . . . . . . . . . . . . . . . 490
11.4.2 When to Splay . . . . . . . . . . . . . . . . . . . . . . . 494
11.4.3 Python Implementation . . . . . . . . . . . . . . . . . . 496
11.4.4 Amortized Analysis of Splaying � . . . . . . . . . . . . 497
11.5 (2,4) Trees . . . . . . . . . . . . . . . . . . . . . . . . . . . . 502
11.5.1 Multiway Search Trees . . . . . . . . . . . . . . . . . . 502
11.5.2 (2,4)-Tree Operations . . . . . . . . . . . . . . . . . . . 505
11.6 Red-Black Trees . . . . . . . . . . . . . . . . . . . . . . . . . 512
11.6.1 Red-Black Tree Operations . . . . . . . . . . . . . . . . 514
11.6.2 Python Implementation . . . . . . . . . . . . . . . . . . 525
11.7 Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 528
Contents xvii
12 Sorting and Selection 536
12.1 Why Study Sorting Algorithms? . . . . . . . . . . . . . . . . 537
12.2 Merge-Sort . . . . . . . . . . . . . . . . . . . . . . . . . . . . 538
12.2.1 Divide-and-Conquer . . . . . . . . . . . . . . . . . . . . 538
12.2.2 Array-Based Implementation of Merge-Sort . . . . . . . 543
12.2.3 The Running Time of Merge-Sort . . . . . . . . . . . . 544
12.2.4 Merge-Sort and Recurrence Equations � . . . . . . . . . 546
12.2.5 Alternative Implementations of Merge-Sort . . . . . . . 547
12.3 Quick-Sort . . . . . . . . . . . . . . . . . . . . . . . . . . . . 550
12.3.1 Randomized Quick-Sort . . . . . . . . . . . . . . . . . . 557
12.3.2 Additional Optimizations for Quick-Sort . . . . . . . . . 559
12.4 Studying Sorting through an Algorithmic Lens . . . . . . . 562
12.4.1 Lower Bound for Sorting . . . . . . . . . . . . . . . . . 562
12.4.2 Linear-Time Sorting: Bucket-Sort and Radix-Sort . . . . 564
12.5 Comparing Sorting Algorithms . . . . . . . . . . . . . . . . . 567
12.6 Python’s Built-In Sorting Functions . . . . . . . . . . . . . . 569
12.6.1 Sorting According to a Key Function . . . . . . . . . . . 569
12.7 Selection . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 571
12.7.1 Prune-and-Search . . . . . . . . . . . . . . . . . . . . . 571
12.7.2 Randomized Quick-Select . . . . . . . . . . . . . . . . . 572
12.7.3 Analyzing Randomized Quick-Select . . . . . . . . . . . 573
12.8 Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 574
13 Text Processing 581
13.1 Abundance of Digitized Text . . . . . . . . . . . . . . . . . . 582
13.1.1 Notations for Strings and the Python str Class . . . . . . 583
13.2 Pattern-Matching Algorithms . . . . . . . . . . . . . . . . . 584
13.2.1 Brute Force . . . . . . . . . . . . . . . . . . . . . . . . 584
13.2.2 The Boyer-Moore Algorithm . . . . . . . . . . . . . . . 586
13.2.3 The Knuth-Morris-Pratt Algorithm . . . . . . . . . . . . 590
13.3 Dynamic Programming . . . . . . . . . . . . . . . . . . . . . 594
13.3.1 Matrix Chain-Product . . . . . . . . . . . . . . . . . . . 594
13.3.2 DNA and Text Sequence Alignment . . . . . . . . . . . 597
13.4 Text Compression and the Greedy Method . . . . . . . . . 601
13.4.1 The Huffman Coding Algorithm . . . . . . . . . . . . . 602
13.4.2 The Greedy Method . . . . . . . . . . . . . . . . . . . . 603
13.5 Tries . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 604
13.5.1 Standard Tries . . . . . . . . . . . . . . . . . . . . . . . 604
13.5.2 Compressed Tries . . . . . . . . . . . . . . . . . . . . . 608
13.5.3 Suffix Tries . . . . . . . . . . . . . . . . . . . . . . . . 610
13.5.4 Search Engine Indexing . . . . . . . . . . . . . . . . . . 612
xviii Contents
13.6 Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 613
14 Graph Algorithms 619
14.1 Graphs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 620
14.1.1 The Graph ADT . . . . . . . . . . . . . . . . . . . . . . 626
14.2 Data Structures for Graphs . . . . . . . . . . . . . . . . . . . 627
14.2.1 Edge List Structure . . . . . . . . . . . . . . . . . . . . 628
14.2.2 Adjacency List Structure . . . . . . . . . . . . . . . . . 630
14.2.3 Adjacency Map Structure . . . . . . . . . . . . . . . . . 632
14.2.4 Adjacency Matrix Structure . . . . . . . . . . . . . . . . 633
14.2.5 Python Implementation . . . . . . . . . . . . . . . . . . 634
14.3 Graph Traversals . . . . . . . . . . . . . . . . . . . . . . . . . 638
14.3.1 Depth-First Search . . . . . . . . . . . . . . . . . . . . 639
14.3.2 DFS Implementation and Extensions . . . . . . . . . . . 644
14.3.3 Breadth-First Search . . . . . . . . . . . . . . . . . . . 648
14.4 Transitive Closure . . . . . . . . . . . . . . . . . . . . . . . . 651
14.5 Directed Acyclic Graphs . . . . . . . . . . . . . . . . . . . . 655
14.5.1 Topological Ordering . . . . . . . . . . . . . . . . . . . 655
14.6 Shortest Paths . . . . . . . . . . . . . . . . . . . . . . . . . . 659
14.6.1 Weighted Graphs . . . . . . . . . . . . . . . . . . . . . 659
14.6.2 Dijkstra’s Algorithm . . . . . . . . . . . . . . . . . . . . 661
14.7 Minimum Spanning Trees . . . . . . . . . . . . . . . . . . . . 670
14.7.1 Prim-Jarńık Algorithm . . . . . . . . . . . . . . . . . . 672
14.7.2 Kruskal’s Algorithm . . . . . . . . . . . . . . . . . . . . 676
14.7.3 Disjoint Partitions and Union-Find Structures . . . . . . 681
14.8 Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 686
15 Memory Management and B-Trees 697
15.1 Memory Management . . . . . . . . . . . . . . . . . . . . . . 698
15.1.1 Memory Allocation . . . . . . . . . . . . . . . . . . . . 699
15.1.2 Garbage Collection . . . . . . . . . . . . . . . . . . . . 700
15.1.3 Additional Memory Used by the Python Interpreter . . . 703
15.2 Memory Hierarchies and Caching . . . . . . . . . . . . . . . 705
15.2.1 Memory Systems . . . . . . . . . . . . . . . . . . . . . 705
15.2.2 Caching Strategies . . . . . . . . . . . . . . . . . . . . 706
15.3 External Searching and B-Trees . . . . . . . . . . . . . . . . 711
15.3.1 (a,b) Trees . . . . . . . . . . . . . . . . . . . . . . . . . 712
15.3.2 B-Trees . . . . . . . . . . . . . . . . . . . . . . . . . . 714
15.4 External-Memory Sorting . . . . . . . . . . . . . . . . . . . . 715
15.4.1 Multiway Merging . . . . . . . . . . . . . . . . . . . . . 716
15.5 Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 717
Contents xix
A Character Strings in Python 721
B Useful Mathematical Facts 725
Bibliography 732
Index 737
Chapter
1 Python Primer
Contents
1.1 Python Overview . . . . . . . . . . . . . . . . . . . . . . . . 2
1.1.1 The Python Interpreter . . . . . . . . . . . . . . . . . . . 2
1.1.2 Preview of a Python Program . . . . . . . . . . . . . . . 3
1.2 Objects in Python . . . . . . . . . . . . . . . . . . . . . . . 4
1.2.1 Identifiers, Objects, and the Assignment Statement . . . . 4
1.2.2 Creating and Using Objects . . . . . . . . . . . . . . . . . 6
1.2.3 Python’s Built-In Classes . . . . . . . . . . . . . . . . . . 7
1.3 Expressions, Operators, and Precedence . . . . . . . . . . . 12
1.3.1 Compound Expressions and Operator Precedence . . . . . 17
1.4 Control Flow . . . . . . . . . . . . . . . . . . . . . . . . . . 18
1.4.1 Conditionals . . . . . . . . . . . . . . . . . . . . . . . . . 18
1.4.2 Loops . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
1.5 Functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
1.5.1 Information Passing . . . . . . . . . . . . . . . . . . . . . 24
1.5.2 Python’s Built-In Functions . . . . . . . . . . . . . . . . . 28
1.6 Simple Input and Output . . . . . . . . . . . . . . . . . . . 30
1.6.1 Console Input and Output . . . . . . . . . . . . . . . . . 30
1.6.2 Files . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
1.7 Exception Handling . . . . . . . . . . . . . . . . . . . . . . 33
1.7.1 Raising an Exception . . . . . . . . . . . . . . . . . . . . 34
1.7.2 Catching an Exception . . . . . . . . . . . . . . . . . . . 36
1.8 Iterators and Generators . . . . . . . . . . . . . . . . . . . 39
1.9 Additional Python Conveniences . . . . . . . . . . . . . . . 42
1.9.1 Conditional Expressions . . . . . . . . . . . . . . . . . . . 42
1.9.2 Comprehension Syntax . . . . . . . . . . . . . . . . . . . 43
1.9.3 Packing and Unpacking of Sequences . . . . . . . . . . . 44
1.10 Scopes and Namespaces . . . . . . . . . . . . . . . . . . . 46
1.11 Modules and the Import Statement . . . . . . . . . . . . . 48
1.11.1 Existing Modules . . . . . . . . . . . . . . . . . . . . . . 49
1.12 Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
2 Chapter 1. Python Primer
1.1 Python Overview
Building data structures and algorithms requires that we communicate detailed in-
structions to a computer. An excellent way to perform such communications is
using a high-level computer language, such as Python. The Python programming
language was originally developed by Guido van Rossum in the early 1990s, and
has since become a prominently used language in industry and education. The sec-
ond major version of the language, Python 2, was released in 2000, and the third
major version, Python 3, released in 2008. We note that there are significant in-
compatibilities between Python 2 and Python 3. This book is based on Python 3
(more specifically, Python 3.1 or later). The latest version of the language is freely
available at www.python.org, along with documentation and tutorials.
In this chapter, we provide an overview of the Python programming language,
and we continue this discussion in the next chapter, focusing on object-oriented
principles. We assume that readers of this book have prior programming experi-
ence, although not necessarily using Python. This book does not provide a com-
plete description of the Python language (there are numerous language references
for that purpose), but it does introduce all aspects of the language that are used in
code fragments later in this book.
1.1.1 The Python Interpreter
Python is formally an interpreted language. Commands are executed through a
piece of software known as the Python interpreter. The interpreter receives a com-
mand, evaluates that command, and reports the result of the command. While the
interpreter can be used interactively (especially when debugging), a programmer
typically defines a series of commands in advance and saves those commands in a
plain text file known as source code or a script. For Python, source code is conven-
tionally stored in a file named with the .py suffix (e.g., demo.py).
On most operating systems, the Python interpreter can be started by typing
python from the command line. By default, the interpreter starts in interactive
mode with a clean workspace. Commands from a predefined script saved in a
file (e.g., demo.py) are executed by invoking the interpreter with the filename as
an argument (e.g., python demo.py), or using an additional -i flag in order to
execute a script and then enter interactive mode (e.g., python -i demo.py).
Many integrated development environments (IDEs) provide richer software
development platforms for Python, including one named IDLE that is included
with the standard Python distribution. IDLE provides an embedded text-editor with
support for displaying and editing Python code, and a basic debugger, allowing
step-by-step execution of a program while examining key variable values.
1.1. Python Overview 3
1.1.2 Preview of a Python Program
As a simple introduction, Code Fragment 1.1 presents a Python program that com-
putes the grade-point average (GPA) for a student based on letter grades that are
entered by a user. Many of the techniques demonstrated in this example will be
discussed in the remainder of this chapter. At this point, we draw attention to a few
high-level issues, for readers who are new to Python as a programming language.
Python’s syntax relies heavily on the use of whitespace. Individual statements
are typically concluded with a newline character, although a command can extend
to another line, either with a concluding backslash character (\), or if an opening
delimiter has not yet been closed, such as the { character in defining value map.
Whitespace is also key in delimiting the bodies of control structures in Python.
Specifically, a block of code is indented to designate it as the body of a control
structure, and nested control structures use increasing amounts of indentation. In
Code Fragment 1.1, the body of the while loop consists of the subsequent 8 lines,
including a nested conditional structure.
Comments are annotations provided for human readers, yet ignored by the
Python interpreter. The primary syntax for comments in Python is based on use
of the # character, which designates the remainder of the line as a comment.
print( Welcome to the GPA calculator. )
print( Please enter all your letter grades, one per line. )
print( Enter a blank line to designate the end. )
# map from letter grade to point value
points = { A+ :4.0, A :4.0, A- :3.67, B+ :3.33, B :3.0, B- :2.67,
C+ :2.33, C :2.0, C :1.67, D+ :1.33, D :1.0, F :0.0}
num courses = 0
total points = 0
done = False
while not done:
grade = input( ) # read line from user
if grade == : # empty line was entered
done = True
elif grade not in points: # unrecognized grade entered
print(“Unknown grade {0} being ignored”.format(grade))
else:
num courses += 1
total points += points[grade]
if num courses > 0: # avoid division by zero
print( Your GPA is {0:.3} .format(total points / num courses))
Code Fragment 1.1: A Python program that computes a grade-point average (GPA).
4 Chapter 1. Python Primer
1.2 Objects in Python
Python is an object-oriented language and classes form the basis for all data types.
In this section, we describe key aspects of Python’s object model, and we intro-
duce Python’s built-in classes, such as the int class for integers, the float class
for floating-point values, and the str class for character strings. A more thorough
presentation of object-orientation is the focus of Chapter 2.
1.2.1 Identifiers, Objects, and the Assignment Statement
The most important of all Python commands is an assignment statement, such as
temperature = 98.6
This command establishes temperature as an identifier (also known as a name),
and then associates it with the object expressed on the right-hand side of the equal
sign, in this case a floating-point object with value 98.6. We portray the outcome
of this assignment in Figure 1.1.
float
98.6
temperature
Figure 1.1: The identifier temperature references an instance of the float class
having value 98.6.
Identifiers
Identifiers in Python are case-sensitive, so temperature and Temperature are dis-
tinct names. Identifiers can be composed of almost any combination of letters,
numerals, and underscore characters (or more general Unicode characters). The
primary restrictions are that an identifier cannot begin with a numeral (thus 9lives
is an illegal name), and that there are 33 specially reserved words that cannot be
used as identifiers, as shown in Table 1.1.
Reserved Words
False as continue else from in not return yield
None assert def except global is or try
True break del finally if lambda pass while
and class elif for import nonlocal raise with
Table 1.1: A listing of the reserved words in Python. These names cannot be used
as identifiers.
1.2. Objects in Python 5
For readers familiar with other programming languages, the semantics of a
Python identifier is most similar to a reference variable in Java or a pointer variable
in C++. Each identifier is implicitly associated with the memory address of the
object to which it refers. A Python identifier may be assigned to a special object
named None, serving a similar purpose to a null reference in Java or C++.
Unlike Java and C++, Python is a dynamically typed language, as there is no
advance declaration associating an identifier with a particular data type. An iden-
tifier can be associated with any type of object, and it can later be reassigned to
another object of the same (or different) type. Although an identifier has no de-
clared type, the object to which it refers has a definite type. In our first example,
the characters 98.6 are recognized as a floating-point literal, and thus the identifier
temperature is associated with an instance of the float class having that value.
A programmer can establish an alias by assigning a second identifier to an
existing object. Continuing with our earlier example, Figure 1.2 portrays the result
of a subsequent assignment, original = temperature.
float
98.6
originaltemperature
Figure 1.2: Identifiers temperature and original are aliases for the same object.
Once an alias has been established, either name can be used to access the under-
lying object. If that object supports behaviors that affect its state, changes enacted
through one alias will be apparent when using the other alias (because they refer to
the same object). However, if one of the names is reassigned to a new value using
a subsequent assignment statement, that does not affect the aliased object, rather it
breaks the alias. Continuing with our concrete example, we consider the command:
temperature = temperature + 5.0
The execution of this command begins with the evaluation of the expression on the
right-hand side of the = operator. That expression, temperature + 5.0, is eval-
uated based on the existing binding of the name temperature, and so the result
has value 103.6, that is, 98.6 + 5.0. That result is stored as a new floating-point
instance, and only then is the name on the left-hand side of the assignment state-
ment, temperature, (re)assigned to the result. The subsequent configuration is dia-
grammed in Figure 1.3. Of particular note, this last command had no effect on the
value of the existing float instance that identifier original continues to reference.
98.6
float
103.6
temperature original
float
Figure 1.3: The temperature identifier has been assigned to a new value, while
original continues to refer to the previously existing value.
6 Chapter 1. Python Primer
1.2.2 Creating and Using Objects
Instantiation
The process of creating a new instance of a class is known as instantiation. In
general, the syntax for instantiating an object is to invoke the constructor of a class.
For example, if there were a class named Widget, we could create an instance of
that class using a syntax such as w = Widget( ), assuming that the constructor does
not require any parameters. If the constructor does require parameters, we might
use a syntax such as Widget(a, b, c) to construct a new instance.
Many of Python’s built-in classes (discussed in Section 1.2.3) support what is
known as a literal form for designating new instances. For example, the command
temperature = 98.6 results in the creation of a new instance of the float class; the
term 98.6 in that expression is a literal form. We discuss further cases of Python
literals in the coming section.
From a programmer’s perspective, yet another way to indirectly create a new
instance of a class is to call a function that creates and returns such an instance. For
example, Python has a built-in function named sorted (see Section 1.5.2) that takes
a sequence of comparable elements as a parameter and returns a new instance of
the list class containing those elements in sorted order.
Calling Methods
Python supports traditional functions (see Section 1.5) that are invoked with a syn-
tax such as sorted(data), in which case data is a parameter sent to the function.
Python’s classes may also define one or more methods (also known as member
functions), which are invoked on a specific instance of a class using the dot (“.”)
operator. For example, Python’s list class has a method named sort that can be
invoked with a syntax such as data.sort( ). This particular method rearranges the
contents of the list so that they are sorted.
The expression to the left of the dot identifies the object upon which the method
is invoked. Often, this will be an identifier (e.g., data), but we can use the dot op-
erator to invoke a method upon the immediate result of some other operation. For
example, if response identifies a string instance (we will discuss strings later in this
section), the syntax response.lower( ).startswith( y ) first evaluates the method
call, response.lower( ), which itself returns a new string instance, and then the
startswith( y ) method is called on that intermediate string.
When using a method of a class, it is important to understand its behavior.
Some methods return information about the state of an object, but do not change
that state. These are known as accessors. Other methods, such as the sort method
of the list class, do change the state of an object. These methods are known as
mutators or update methods.
1.2. Objects in Python 7
1.2.3 Python’s Built-In Classes
Table 1.2 provides a summary of commonly used, built-in classes in Python; we
take particular note of which classes are mutable and which are immutable. A class
is immutable if each object of that class has a fixed value upon instantiation that
cannot subsequently be changed. For example, the float class is immutable. Once
an instance has been created, its value cannot be changed (although an identifier
referencing that object can be reassigned to a different value).
Class Description Immutable?
bool Boolean value �
int integer (arbitrary magnitude) �
float floating-point number �
list mutable sequence of objects
tuple immutable sequence of objects �
str character string �
set unordered set of distinct objects
frozenset immutable form of set class �
dict associative mapping (aka dictionary)
Table 1.2: Commonly used built-in classes for Python
In this section, we provide an introduction to these classes, discussing their
purpose and presenting several means for creating instances of the classes. Literal
forms (such as 98.6) exist for most of the built-in classes, and all of the classes
support a traditional constructor form that creates instances that are based upon
one or more existing values. Operators supported by these classes are described in
Section 1.3. More detailed information about these classes can be found in later
chapters as follows: lists and tuples (Chapter 5); strings (Chapters 5 and 13, and
Appendix A); sets and dictionaries (Chapter 10).
The bool Class
The bool class is used to manipulate logical (Boolean) values, and the only two
instances of that class are expressed as the literals True and False. The default
constructor, bool( ), returns False, but there is no reason to use that syntax rather
than the more direct literal form. Python allows the creation of a Boolean value
from a nonboolean type using the syntax bool(foo) for value foo. The interpretation
depends upon the type of the parameter. Numbers evaluate to False if zero, and
True if nonzero. Sequences and other container types, such as strings and lists,
evaluate to False if empty and True if nonempty. An important application of this
interpretation is the use of a nonboolean value as a condition in a control structure.
8 Chapter 1. Python Primer
The int Class
The int and float classes are the primary numeric types in Python. The int class is
designed to represent integer values with arbitrary magnitude. Unlike Java and
C++, which support different integral types with different precisions (e.g., int,
short, long), Python automatically chooses the internal representation for an in-
teger based upon the magnitude of its value. Typical literals for integers include 0,
137, and −23. In some contexts, it is convenient to express an integral value using
binary, octal, or hexadecimal. That can be done by using a prefix of the number 0
and then a character to describe the base. Example of such literals are respectively
0b1011, 0o52, and 0x7f.
The integer constructor, int( ), returns value 0 by default. But this constructor
can be used to construct an integer value based upon an existing value of another
type. For example, if f represents a floating-point value, the syntax int(f) produces
the truncated value of f. For example, both int(3.14) and int(3.99) produce the
value 3, while int(−3.9) produces the value −3. The constructor can also be used
to parse a string that is presumed to represent an integral value (such as one en-
tered by a user). If s represents a string, then int(s) produces the integral value
that string represents. For example, the expression int( 137 ) produces the inte-
ger value 137. If an invalid string is given as a parameter, as in int( hello ), a
ValueError is raised (see Section 1.7 for discussion of Python’s exceptions). By de-
fault, the string must use base 10. If conversion from a different base is desired, that
base can be indicated as a second, optional, parameter. For example, the expression
int( 7f , 16) evaluates to the integer 127.
The float Class
The float class is the sole floating-point type in Python, using a fixed-precision
representation. Its precision is more akin to a double in Java or C++, rather than
those languages’ float type. We have already discussed a typical literal form, 98.6.
We note that the floating-point equivalent of an integral number can be expressed
directly as 2.0. Technically, the trailing zero is optional, so some programmers
might use the expression 2. to designate this floating-point literal. One other form
of literal for floating-point values uses scientific notation. For example, the literal
6.022e23 represents the mathematical value 6.022 × 1023 .
The constructor form of float( ) returns 0.0. When given a parameter, the con-
structor attempts to return the equivalent floating-point value. For example, the call
float(2) returns the floating-point value 2.0. If the parameter to the constructor is
a string, as with float( 3.14 ), it attempts to parse that string as a floating-point
value, raising a ValueError as an exception.
1.2. Objects in Python 9
Sequence Types: The list, tuple, and str Classes
The list, tuple, and str classes are sequence types in Python, representing a col-
lection of values in which the order is significant. The list class is the most general,
representing a sequence of arbitrary objects (akin to an “array” in other languages).
The tuple class is an immutable version of the list class, benefiting from a stream-
lined internal representation. The str class is specially designed for representing
an immutable sequence of text characters. We note that Python does not have a
separate class for characters; they are just strings with length one.
The list Class
A list instance stores a sequence of objects. A list is a referential structure, as it
technically stores a sequence of references to its elements (see Figure 1.4). El-
ements of a list may be arbitrary objects (including the None object). Lists are
array-based sequences and are zero-indexed, thus a list of length n has elements
indexed from 0 to n − 1 inclusive. Lists are perhaps the most used container type in
Python and they will be extremely central to our study of data structures and algo-
rithms. They have many valuable behaviors, including the ability to dynamically
expand and contract their capacities as needed. In this chapter, we will discuss only
the most basic properties of lists. We revisit the inner working of all of Python’s
sequence types as the focus of Chapter 5.
Python uses the characters [ ] as delimiters for a list literal, with [ ] itself being
an empty list. As another example, [ red , green , blue ] is a list containing
three string instances. The contents of a list literal need not be expressed as literals;
if identifiers a and b have been established, then syntax [a, b] is legitimate.
The list( ) constructor produces an empty list by default. However, the construc-
tor will accept any parameter that is of an iterable type. We will discuss iteration
further in Section 1.8, but examples of iterable types include all of the standard con-
tainer types (e.g., strings, list, tuples, sets, dictionaries). For example, the syntax
list( hello ) produces a list of individual characters, [ h , e , l , l , o ].
Because an existing list is itself iterable, the syntax backup = list(data) can be
used to construct a new list instance referencing the same contents as the original.
3 4 5 6 70 1 2 1098
primes:
13 19 23 29 317532 11 17
Figure 1.4: Python’s internal representation of a list of integers, instantiated as
prime = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31]. The implicit indices of the ele-
ments are shown below each entry.
10 Chapter 1. Python Primer
The tuple Class
The tuple class provides an immutable version of a sequence, and therefore its
instances have an internal representation that may be more streamlined than that of
a list. While Python uses the [ ] characters to delimit a list, parentheses delimit a
tuple, with ( ) being an empty tuple. There is one important subtlety. To express
a tuple of length one as a literal, a comma must be placed after the element, but
within the parentheses. For example, (17,) is a one-element tuple. The reason for
this requirement is that, without the trailing comma, the expression (17) is viewed
as a simple parenthesized numeric expression.
The str Class
Python’s str class is specifically designed to efficiently represent an immutable
sequence of characters, based upon the Unicode international character set. Strings
have a more compact internal representation than the referential lists and tuples, as
portrayed in Figure 1.5.
0
AS M P L E
3 4 51 2
Figure 1.5: A Python string, which is an indexed sequence of characters.
String literals can be enclosed in single quotes, as in hello , or double
quotes, as in “hello”. This choice is convenient, especially when using an-
other of the quotation characters as an actual character in the sequence, as in
“Don t worry”. Alternatively, the quote delimiter can be designated using a
backslash as a so-called escape character, as in Don\ t worry . Because the
backslash has this purpose, the backslash must itself be escaped to occur as a natu-
ral character of the string literal, as in C:\\Python\\ , for a string that would be
displayed as C:\Python\. Other commonly escaped characters are \n for newline
and \t for tab. Unicode characters can be included, such as 20\u20AC for the
string 20 .
Python also supports using the delimiter or “”” to begin and end a string
literal. The advantage of such triple-quoted strings is that newline characters can
be embedded naturally (rather than escaped as \n). This can greatly improve the
readability of long, multiline strings in source code. For example, at the beginning
of Code Fragment 1.1, rather than use separate print statements for each line of
introductory output, we can use a single print statement, as follows:
print(”””Welcome to the GPA calculator.
Please enter all your letter grades, one per line.
Enter a blank line to designate the end.”””)
1.2. Objects in Python 11
The set and frozenset Classes
Python’s set class represents the mathematical notion of a set, namely a collection
of elements, without duplicates, and without an inherent order to those elements.
The major advantage of using a set, as opposed to a list, is that it has a highly
optimized method for checking whether a specific element is contained in the set.
This is based on a data structure known as a hash table (which will be the primary
topic of Chapter 10). However, there are two important restrictions due to the
algorithmic underpinnings. The first is that the set does not maintain the elements
in any particular order. The second is that only instances of immutable types can be
added to a Python set. Therefore, objects such as integers, floating-point numbers,
and character strings are eligible to be elements of a set. It is possible to maintain a
set of tuples, but not a set of lists or a set of sets, as lists and sets are mutable. The
frozenset class is an immutable form of the set type, so it is legal to have a set of
frozensets.
Python uses curly braces { and } as delimiters for a set, for example, as {17}
or { red , green , blue }. The exception to this rule is that { } does not
represent an empty set; for historical reasons, it represents an empty dictionary
(see next paragraph). Instead, the constructor syntax set( ) produces an empty set.
If an iterable parameter is sent to the constructor, then the set of distinct elements
is produced. For example, set( hello ) produces { h , e , l , o }.
The dict Class
Python’s dict class represents a dictionary, or mapping, from a set of distinct keys
to associated values. For example, a dictionary might map from unique student ID
numbers, to larger student records (such as the student’s name, address, and course
grades). Python implements a dict using an almost identical approach to that of a
set, but with storage of the associated values.
A dictionary literal also uses curly braces, and because dictionaries were intro-
duced in Python prior to sets, the literal form { } produces an empty dictionary.
A nonempty dictionary is expressed using a comma-separated series of key:value
pairs. For example, the dictionary { ga : Irish , de : German } maps
ga to Irish and de to German .
The constructor for the dict class accepts an existing mapping as a parameter,
in which case it creates a new dictionary with identical associations as the existing
one. Alternatively, the constructor accepts a sequence of key-value pairs as a pa-
rameter, as in dict(pairs) with pairs = [( ga , Irish ), ( de , German )].
12 Chapter 1. Python Primer
1.3 Expressions, Operators, and Precedence
In the previous section, we demonstrated how names can be used to identify ex-
isting objects, and how literals and constructors can be used to create instances of
built-in classes. Existing values can be combined into larger syntactic expressions
using a variety of special symbols and keywords known as operators. The seman-
tics of an operator depends upon the type of its operands. For example, when a
and b are numbers, the syntax a + b indicates addition, while if a and b are strings,
the operator indicates concatenation. In this section, we describe Python’s opera-
tors in various contexts of the built-in types.
We continue, in Section 1.3.1, by discussing compound expressions, such as
a + b c, which rely on the evaluation of two or more operations. The order
in which the operations of a compound expression are evaluated can affect the
overall value of the expression. For this reason, Python defines a specific order of
precedence for evaluating operators, and it allows a programmer to override this
order by using explicit parentheses to group subexpressions.
Logical Operators
Python supports the following keyword operators for Boolean values:
not unary negation
and conditional and
or conditional or
The and and or operators short-circuit, in that they do not evaluate the second
operand if the result can be determined based on the value of the first operand.
This feature is useful when constructing Boolean expressions in which we first test
that a certain condition holds (such as a reference not being None), and then test a
condition that could have otherwise generated an error condition had the prior test
not succeeded.
Equality Operators
Python supports the following operators to test two notions of equality:
is same identity
is not different identity
== equivalent
!= not equivalent
The expression a is b evaluates to True, precisely when identifiers a and b are
aliases for the same object. The expression a == b tests a more general notion of
equivalence. If identifiers a and b refer to the same object, then a == b should also
evaluate to True. Yet a == b also evaluates to True when the identifiers refer to
1.3. Expressions, Operators, and Precedence 13
different objects that happen to have values that are deemed equivalent. The precise
notion of equivalence depends on the data type. For example, two strings are con-
sidered equivalent if they match character for character. Two sets are equivalent if
they have the same contents, irrespective of order. In most programming situations,
the equivalence tests == and != are the appropriate operators; use of is and is not
should be reserved for situations in which it is necessary to detect true aliasing.
Comparison Operators
Data types may define a natural order via the following operators:
< less than
<= less than or equal to
> greater than
>= greater than or equal to
These operators have expected behavior for numeric types, and are defined lexi-
cographically, and case-sensitively, for strings. An exception is raised if operands
have incomparable types, as with 5 < hello .
Arithmetic Operators
Python supports the following arithmetic operators:
+ addition
− subtraction
multiplication
/ true division
// integer division
% the modulo operator
The use of addition, subtraction, and multiplication is straightforward, noting that if
both operands have type int, then the result is an int as well; if one or both operands
have type float, the result will be a float.
Python takes more care in its treatment of division. We first consider the case
in which both operands have type int, for example, the quantity 27 divided by
4. In mathematical notation, 27 ÷ 4 = 6 34 = 6.75. In Python, the / operator
designates true division, returning the floating-point result of the computation.
Thus, 27 / 4 results in the float value 6.75. Python supports the pair of opera-
tors // and % to perform the integral calculations, with expression 27 // 4 evalu-
ating to int value 6 (the mathematical floor of the quotient), and expression 27 % 4
evaluating to int value 3, the remainder of the integer division. We note that lan-
guages such as C, C++, and Java do not support the // operator; instead, the / op-
erator returns the truncated quotient when both operands have integral type, and the
result of true division when at least one operand has a floating-point type.
14 Chapter 1. Python Primer
Python carefully extends the semantics of // and % to cases where one or both
operands are negative. For the sake of notation, let us assume that variables n
and m represent respectively the dividend and divisor of a quotient nm , and that
q = n // m and r = n % m. Python guarantees that q m + r will equal n. We
already saw an example of this identity with positive operands, as 6 ∗ 4 + 3 = 27.
When the divisor m is positive, Python further guarantees that 0 ≤ r < m. As
a consequence, we find that −27 // 4 evaluates to −7 and −27 % 4 evaluates
to 1, as (−7) ∗ 4 + 1 = −27. When the divisor is negative, Python guarantees that
m < r ≤ 0. As an example, 27 // −4 is −7 and 27 % −4 is −1, satisfying the
identity 27 = (−7) ∗ (−4) + (−1).
The conventions for the // and % operators are even extended to floating-
point operands, with the expression q = n // m being the integral floor of the
quotient, and r = n % m being the “remainder” to ensure that q m + r equals
n. For example, 8.2 // 3.14 evaluates to 2.0 and 8.2 % 3.14 evaluates to 1.92, as
2.0 ∗ 3.14 + 1.92 = 8.2.
Bitwise Operators
Python provides the following bitwise operators for integers:
∼ bitwise complement (prefix unary operator)
& bitwise and
| bitwise or
ˆ bitwise exclusive-or
<< shift bits left, filling in with zeros
>> shift bits right, filling in with sign bit
Sequence Operators
Each of Python’s built-in sequence types (str, tuple, and list) support the following
operator syntaxes:
s[j] element at index j
s[start:stop] slice including indices [start,stop)
s[start:stop:step] slice including indices start, start + step,
start + 2 step, . . . , up to but not equalling or stop
s + t concatenation of sequences
k s shorthand for s + s + s + … (k times)
val in s containment check
val not in s non-containment check
Python relies on zero-indexing of sequences, thus a sequence of length n has ele-
ments indexed from 0 to n − 1 inclusive. Python also supports the use of negative
indices, which denote a distance from the end of the sequence; index −1 denotes
the last element, index −2 the second to last, and so on. Python uses a slicing
1.3. Expressions, Operators, and Precedence 15
notation to describe subsequences of a sequence. Slices are described as half-open
intervals, with a start index that is included, and a stop index that is excluded. For
example, the syntax data[3:8] denotes a subsequence including the five indices:
3, 4, 5, 6, 7. An optional “step” value, possibly negative, can be indicated as a third
parameter of the slice. If a start index or stop index is omitted in the slicing nota-
tion, it is presumed to designate the respective extreme of the original sequence.
Because lists are mutable, the syntax s[j] = val can be used to replace an ele-
ment at a given index. Lists also support a syntax, del s[j], that removes the desig-
nated element from the list. Slice notation can also be used to replace or delete a
sublist.
The notation val in s can be used for any of the sequences to see if there is an
element equivalent to val in the sequence. For strings, this syntax can be used to
check for a single character or for a larger substring, as with amp in example .
All sequences define comparison operations based on lexicographic order, per-
forming an element by element comparison until the first difference is found. For
example, [5, 6, 9] < [5, 7] because of the entries at index 1. Therefore, the follow-
ing operations are supported by sequence types:
s == t equivalent (element by element)
s != t not equivalent
s < t lexicographically less than
s <= t lexicographically less than or equal to
s > t lexicographically greater than
s >= t lexicographically greater than or equal to
Operators for Sets and Dictionaries
Sets and frozensets support the following operators:
key in s containment check
key not in s non-containment check
s1 == s2 s1 is equivalent to s2
s1 != s2 s1 is not equivalent to s2
s1 <= s2 s1 is subset of s2
s1 < s2 s1 is proper subset of s2
s1 >= s2 s1 is superset of s2
s1 > s2 s1 is proper superset of s2
s1 | s2 the union of s1 and s2
s1 & s2 the intersection of s1 and s2
s1 − s2 the set of elements in s1 but not s2
s1 ˆ s2 the set of elements in precisely one of s1 or s2
Note well that sets do not guarantee a particular order of their elements, so the
comparison operators, such as <, are not lexicographic; rather, they are based on
the mathematical notion of a subset. As a result, the comparison operators define
16 Chapter 1. Python Primer
a partial order, but not a total order, as disjoint sets are neither “less than,” “equal
to,” or “greater than” each other. Sets also support many fundamental behaviors
through named methods (e.g., add, remove); we will explore their functionality
more fully in Chapter 10.
Dictionaries, like sets, do not maintain a well-defined order on their elements.
Furthermore, the concept of a subset is not typically meaningful for dictionaries, so
the dict class does not support operators such as <. Dictionaries support the notion
of equivalence, with d1 == d2 if the two dictionaries contain the same set of key-
value pairs. The most widely used behavior of dictionaries is accessing a value
associated with a particular key k with the indexing syntax, d[k]. The supported
operators are as follows:
d[key] value associated with given key
d[key] = value set (or reset) the value associated with given key
del d[key] remove key and its associated value from dictionary
key in d containment check
key not in d non-containment check
d1 == d2 d1 is equivalent to d2
d1 != d2 d1 is not equivalent to d2
Dictionaries also support many useful behaviors through named methods, which
we explore more fully in Chapter 10.
Extended Assignment Operators
Python supports an extended assignment operator for most binary operators, for
example, allowing a syntax such as count += 5. By default, this is a shorthand for
the more verbose count = count + 5. For an immutable type, such as a number or
a string, one should not presume that this syntax changes the value of the existing
object, but instead that it will reassign the identifier to a newly constructed value.
(See discussion of Figure 1.3.) However, it is possible for a type to redefine such
semantics to mutate the object, as the list class does for the += operator.
alpha = [1, 2, 3]
beta = alpha # an alias for alpha
beta += [4, 5] # extends the original list with two more elements
beta = beta + [6, 7] # reassigns beta to a new list [1, 2, 3, 4, 5, 6, 7]
print(alpha) # will be [1, 2, 3, 4, 5]
This example demonstrates the subtle difference between the list semantics for the
syntax beta += foo versus beta = beta + foo.
1.3. Expressions, Operators, and Precedence 17
1.3.1 Compound Expressions and Operator Precedence
Programming languages must have clear rules for the order in which compound
expressions, such as 5 + 2 3, are evaluated. The formal order of precedence
for operators in Python is given in Table 1.3. Operators in a category with higher
precedence will be evaluated before those with lower precedence, unless the expres-
sion is otherwise parenthesized. Therefore, we see that Python gives precedence to
multiplication over addition, and therefore evaluates the expression 5 + 2 3 as
5 + (2 3), with value 11, but the parenthesized expression (5 + 2) 3 evalu-
ates to value 21. Operators within a category are typically evaluated from left to
right, thus 5 − 2 + 3 has value 6. Exceptions to this rule include that unary oper-
ators and exponentiation are evaluated from right to left.
Python allows a chained assignment, such as x = y = 0, to assign multiple
identifiers to the rightmost value. Python also allows the chaining of comparison
operators. For example, the expression 1 <= x + y <= 10 is evaluated as the
compound (1 <= x + y) and (x + y <= 10), but without computing the inter-
mediate value x + y twice.
Operator Precedence
Type Symbols
1 member access expr.member
2
function/method calls expr(...)
container subscripts/slices expr[...]
3 exponentiation
4 unary operators +expr, −expr, ˜expr
5 multiplication, division , /, //, %
6 addition, subtraction +, −
7 bitwise shifting <<, >>
8 bitwise-and &
9 bitwise-xor ˆ
10 bitwise-or |
11
comparisons is, is not, ==, !=, <, <=, >, >=
containment in, not in
12 logical-not not expr
13 logical-and and
14 logical-or or
15 conditional val1 if cond else val2
16 assignments =, +=, −=, =, etc.
Table 1.3: Operator precedence in Python, with categories ordered from highest
precedence to lowest precedence. When stated, we use expr to denote a literal,
identifier, or result of a previously evaluated expression. All operators without
explicit mention of expr are binary operators, with syntax expr1 operator expr2.
18 Chapter 1. Python Primer
1.4 Control Flow
In this section, we review Python’s most fundamental control structures: condi-
tional statements and loops. Common to all control structures is the syntax used
in Python for defining blocks of code. The colon character is used to delimit the
beginning of a block of code that acts as a body for a control structure. If the body
can be stated as a single executable statement, it can technically placed on the same
line, to the right of the colon. However, a body is more typically typeset as an
indented block starting on the line following the colon. Python relies on the inden-
tation level to designate the extent of that block of code, or any nested blocks of
code within. The same principles will be applied when designating the body of a
function (see Section 1.5), and the body of a class (see Section 2.3).
1.4.1 Conditionals
Conditional constructs (also known as if statements) provide a way to execute a
chosen block of code based on the run-time evaluation of one or more Boolean
expressions. In Python, the most general form of a conditional is written as follows:
if first condition:
first body
elif second condition:
second body
elif third condition:
third body
else:
fourth body
Each condition is a Boolean expression, and each body contains one or more com-
mands that are to be executed conditionally. If the first condition succeeds, the first
body will be executed; no other conditions or bodies are evaluated in that case.
If the first condition fails, then the process continues in similar manner with the
evaluation of the second condition. The execution of this overall construct will
cause precisely one of the bodies to be executed. There may be any number of
elif clauses (including zero), and the final else clause is optional. As described on
page 7, nonboolean types may be evaluated as Booleans with intuitive meanings.
For example, if response is a string that was entered by a user, and we want to
condition a behavior on this being a nonempty string, we may write
if response:
as a shorthand for the equivalent,
if response != :
1.4. Control Flow 19
As a simple example, a robot controller might have the following logic:
if door is closed:
open door( )
advance( )
Notice that the final command, advance( ), is not indented and therefore not part of
the conditional body. It will be executed unconditionally (although after opening a
closed door).
We may nest one control structure within another, relying on indentation to
make clear the extent of the various bodies. Revisiting our robot example, here is a
more complex control that accounts for unlocking a closed door.
if door is closed:
if door is locked:
unlock door( )
open door( )
advance( )
The logic expressed by this example can be diagrammed as a traditional flowchart,
as portrayed in Figure 1.6.
open door( )
False
door is closed
advance( )
door is locked
unlock door( )
TrueFalse
True
Figure 1.6: A flowchart describing the logic of nested conditional statements.
20 Chapter 1. Python Primer
1.4.2 Loops
Python offers two distinct looping constructs. A while loop allows general repeti-
tion based upon the repeated testing of a Boolean condition. A for loop provides
convenient iteration of values from a defined series (such as characters of a string,
elements of a list, or numbers within a given range). We discuss both forms in this
section.
While Loops
The syntax for a while loop in Python is as follows:
while condition:
body
As with an if statement, condition can be an arbitrary Boolean expression, and
body can be an arbitrary block of code (including nested control structures). The
execution of a while loop begins with a test of the Boolean condition. If that condi-
tion evaluates to True, the body of the loop is performed. After each execution of
the body, the loop condition is retested, and if it evaluates to True, another iteration
of the body is performed. When the conditional test evaluates to False (assuming
it ever does), the loop is exited and the flow of control continues just beyond the
body of the loop.
As an example, here is a loop that advances an index through a sequence of
characters until finding an entry with value X or reaching the end of the sequence.
j = 0
while j < len(data) and data[j] != X :
j += 1
The len function, which we will introduce in Section 1.5.2, returns the length of a
sequence such as a list or string. The correctness of this loop relies on the short-
circuiting behavior of the and operator, as described on page 12. We intention-
ally test j < len(data) to ensure that j is a valid index, prior to accessing element
data[j]. Had we written that compound condition with the opposite order, the eval-
uation of data[j] would eventually raise an IndexError when X is not found. (See
Section 1.7 for discussion of exceptions.)
As written, when this loop terminates, variable j’s value will be the index of
the leftmost occurrence of X , if found, or otherwise the length of the sequence
(which is recognizable as an invalid index to indicate failure of the search). It is
worth noting that this code behaves correctly, even in the special case when the list
is empty, as the condition j < len(data) will initially fail and the body of the loop
will never be executed.
1.4. Control Flow 21
For Loops
Python’s for-loop syntax is a more convenient alternative to a while loop when
iterating through a series of elements. The for-loop syntax can be used on any
type of iterable structure, such as a list, tuple str, set, dict, or file (we will discuss
iterators more formally in Section 1.8). Its general syntax appears as follows.
for element in iterable:
body # body may refer to element as an identifier
For readers familiar with Java, the semantics of Python’s for loop is similar to the
“for each” loop style introduced in Java 1.5.
As an instructive example of such a loop, we consider the task of computing
the sum of a list of numbers. (Admittedly, Python has a built-in function, sum, for
this purpose.) We perform the calculation with a for loop as follows, assuming that
data identifies the list:
total = 0
for val in data:
total += val # note use of the loop variable, val
The loop body executes once for each element of the data sequence, with the iden-
tifier, val, from the for-loop syntax assigned at the beginning of each pass to a
respective element. It is worth noting that val is treated as a standard identifier. If
the element of the original data happens to be mutable, the val identifier can be
used to invoke its methods. But a reassignment of identifier val to a new value has
no affect on the original data, nor on the next iteration of the loop.
As a second classic example, we consider the task of finding the maximum
value in a list of elements (again, admitting that Python’s built-in max function
already provides this support). If we can assume that the list, data, has at least one
element, we could implement this task as follows:
biggest = data[0] # as we assume nonempty list
for val in data:
if val > biggest:
biggest = val
Although we could accomplish both of the above tasks with a while loop, the
for-loop syntax had an advantage of simplicity, as there is no need to manage an
explicit index into the list nor to author a Boolean loop condition. Furthermore, we
can use a for loop in cases for which a while loop does not apply, such as when
iterating through a collection, such as a set, that does not support any direct form
of indexing.
22 Chapter 1. Python Primer
Index-Based For Loops
The simplicity of a standard for loop over the elements of a list is wonderful; how-
ever, one limitation of that form is that we do not know where an element resides
within the sequence. In some applications, we need knowledge of the index of an
element within the sequence. For example, suppose that we want to know where
the maximum element in a list resides.
Rather than directly looping over the elements of the list in that case, we prefer
to loop over all possible indices of the list. For this purpose, Python provides
a built-in class named range that generates integer sequences. (We will discuss
generators in Section 1.8.) In simplest form, the syntax range(n) generates the
series of n values from 0 to n − 1. Conveniently, these are precisely the series of
valid indices into a sequence of length n. Therefore, a standard Python idiom for
looping through the series of indices of a data sequence uses a syntax,
for j in range(len(data)):
In this case, identifier j is not an element of the data—it is an integer. But the
expression data[j] can be used to retrieve the respective element. For example, we
can find the index of the maximum element of a list as follows:
big index = 0
for j in range(len(data)):
if data[j] > data[big index]:
big index = j
Break and Continue Statements
Python supports a break statement that immediately terminate a while or for loop
when executed within its body. More formally, if applied within nested control
structures, it causes the termination of the most immediately enclosing loop. As
a typical example, here is code that determines whether a target value occurs in a
data set:
found = False
for item in data:
if item == target:
found = True
break
Python also supports a continue statement that causes the current iteration of a
loop body to stop, but with subsequent passes of the loop proceeding as expected.
We recommend that the break and continue statements be used sparingly. Yet,
there are situations in which these commands can be effectively used to avoid in-
troducing overly complex logical conditions.
1.5. Functions 23
1.5 Functions
In this section, we explore the creation of and use of functions in Python. As we
did in Section 1.2.2, we draw a distinction between functions and methods. We
use the general term function to describe a traditional, stateless function that is in-
voked without the context of a particular class or an instance of that class, such as
sorted(data). We use the more specific term method to describe a member function
that is invoked upon a specific object using an object-oriented message passing syn-
tax, such as data.sort( ). In this section, we only consider pure functions; methods
will be explored with more general object-oriented principles in Chapter 2.
We begin with an example to demonstrate the syntax for defining functions in
Python. The following function counts the number of occurrences of a given target
value within any form of iterable data set.
def count(data, target):
n = 0
for item in data:
if item == target: # found a match
n += 1
return n
The first line, beginning with the keyword def, serves as the function’s signature.
This establishes a new identifier as the name of the function (count, in this exam-
ple), and it establishes the number of parameters that it expects, as well as names
identifying those parameters (data and target, in this example). Unlike Java and
C++, Python is a dynamically typed language, and therefore a Python signature
does not designate the types of those parameters, nor the type (if any) of a return
value. Those expectations should be stated in the function’s documentation (see
Section 2.2.3) and can be enforced within the body of the function, but misuse of a
function will only be detected at run-time.
The remainder of the function definition is known as the body of the func-
tion. As is the case with control structures in Python, the body of a function is
typically expressed as an indented block of code. Each time a function is called,
Python creates a dedicated activation record that stores information relevant to the
current call. This activation record includes what is known as a namespace (see
Section 1.10) to manage all identifiers that have local scope within the current call.
The namespace includes the function’s parameters and any other identifiers that are
defined locally within the body of the function. An identifier in the local scope
of the function caller has no relation to any identifier with the same name in the
caller’s scope (although identifiers in different scopes may be aliases to the same
object). In our first example, the identifier n has scope that is local to the function
call, as does the identifier item, which is established as the loop variable.
24 Chapter 1. Python Primer
Return Statement
A return statement is used within the body of a function to indicate that the func-
tion should immediately cease execution, and that an expressed value should be
returned to the caller. If a return statement is executed without an explicit argu-
ment, the None value is automatically returned. Likewise, None will be returned if
the flow of control ever reaches the end of a function body without having executed
a return statement. Often, a return statement will be the final command within the
body of the function, as was the case in our earlier example of a count function.
However, there can be multiple return statements in the same function, with con-
ditional logic controlling which such command is executed, if any. As a further
example, consider the following function that tests if a value exists in a sequence.
def contains(data, target):
for item in target:
if item == target: # found a match
return True
return False
If the conditional within the loop body is ever satisfied, the return True statement is
executed and the function immediately ends, with True designating that the target
value was found. Conversely, if the for loop reaches its conclusion without ever
finding the match, the final return False statement will be executed.
1.5.1 Information Passing
To be a successful programmer, one must have clear understanding of the mech-
anism in which a programming language passes information to and from a func-
tion. In the context of a function signature, the identifiers used to describe the
expected parameters are known as formal parameters, and the objects sent by the
caller when invoking the function are the actual parameters. Parameter passing
in Python follows the semantics of the standard assignment statement. When a
function is invoked, each identifier that serves as a formal parameter is assigned, in
the function’s local scope, to the respective actual parameter that is provided by the
caller of the function.
For example, consider the following call to our count function from page 23:
prizes = count(grades, A )
Just before the function body is executed, the actual parameters, grades and A ,
are implicitly assigned to the formal parameters, data and target, as follows:
data = grades
target = A
1.5. Functions 25
These assignment statements establish identifier data as an alias for grades and
target as a name for the string literal A . (See Figure 1.7.)
…
str
A
data targetgrades
list
Figure 1.7: A portrayal of parameter passing in Python, for the function call
count(grades, A ). Identifiers data and target are formal parameters defined
within the local scope of the count function.
The communication of a return value from the function back to the caller is
similarly implemented as an assignment. Therefore, with our sample invocation of
prizes = count(grades, A ), the identifier prizes in the caller’s scope is assigned
to the object that is identified as n in the return statement within our function body.
An advantage to Python’s mechanism for passing information to and from a
function is that objects are not copied. This ensures that the invocation of a function
is efficient, even in a case where a parameter or return value is a complex object.
Mutable Parameters
Python’s parameter passing model has additional implications when a parameter is
a mutable object. Because the formal parameter is an alias for the actual parameter,
the body of the function may interact with the object in ways that change its state.
Considering again our sample invocation of the count function, if the body of the
function executes the command data.append( F ), the new entry is added to the
end of the list identified as data within the function, which is one and the same as
the list known to the caller as grades. As an aside, we note that reassigning a new
value to a formal parameter with a function body, such as by setting data = [ ],
does not alter the actual parameter; such a reassignment simply breaks the alias.
Our hypothetical example of a count method that appends a new element to a
list lacks common sense. There is no reason to expect such a behavior, and it would
be quite a poor design to have such an unexpected effect on the parameter. There
are, however, many legitimate cases in which a function may be designed (and
clearly documented) to modify the state of a parameter. As a concrete example,
we present the following implementation of a method named scale that’s primary
purpose is to multiply all entries of a numeric data set by a given factor.
def scale(data, factor):
for j in range(len(data)):
data[j] = factor
26 Chapter 1. Python Primer
Default Parameter Values
Python provides means for functions to support more than one possible calling
signature. Such a function is said to be polymorphic (which is Greek for “many
forms”). Most notably, functions can declare one or more default values for pa-
rameters, thereby allowing the caller to invoke a function with varying numbers of
actual parameters. As an artificial example, if a function is declared with signature
def foo(a, b=15, c=27):
there are three parameters, the last two of which offer default values. A caller is
welcome to send three actual parameters, as in foo(4, 12, 8), in which case the de-
fault values are not used. If, on the other hand, the caller only sends one parameter,
foo(4), the function will execute with parameters values a=4, b=15, c=27. If a
caller sends two parameters, they are assumed to be the first two, with the third be-
ing the default. Thus, foo(8, 20) executes with a=8, b=20, c=27. However, it is
illegal to define a function with a signature such as bar(a, b=15, c) with b having
a default value, yet not the subsequent c; if a default parameter value is present for
one parameter, it must be present for all further parameters.
As a more motivating example for the use of a default parameter, we revisit
the task of computing a student’s GPA (see Code Fragment 1.1). Rather than as-
sume direct input and output with the console, we prefer to design a function that
computes and returns a GPA. Our original implementation uses a fixed mapping
from each letter grade (such as a B−) to a corresponding point value (such as
2.67). While that point system is somewhat common, it may not agree with the
system used by all schools. (For example, some may assign an A+ grade a value
higher than 4.0.) Therefore, we design a compute gpa function, given in Code
Fragment 1.2, which allows the caller to specify a custom mapping from grades to
values, while offering the standard point system as a default.
def compute gpa(grades, points={ A+ :4.0, A :4.0, A- :3.67, B+ :3.33,
B :3.0, B- :2.67, C+ :2.33, C :2.0,
C :1.67, D+ :1.33, D :1.0, F :0.0}):
num courses = 0
total points = 0
for g in grades:
if g in points: # a recognizable grade
num courses += 1
total points += points[g]
return total points / num courses
Code Fragment 1.2: A function that computes a student’s GPA with a point value
system that can be customized as an optional parameter.
1.5. Functions 27
As an additional example of an interesting polymorphic function, we consider
Python’s support for range. (Technically, this is a constructor for the range class,
but for the sake of this discussion, we can treat it as a pure function.) Three calling
syntaxes are supported. The one-parameter form, range(n), generates a sequence of
integers from 0 up to but not including n. A two-parameter form, range(start,stop)
generates integers from start up to, but not including, stop. A three-parameter
form, range(start, stop, step), generates a similar range as range(start, stop), but
with increments of size step rather than 1.
This combination of forms seems to violate the rules for default parameters.
In particular, when a single parameter is sent, as in range(n), it serves as the stop
value (which is the second parameter); the value of start is effectively 0 in that
case. However, this effect can be achieved with some sleight of hand, as follows:
def range(start, stop=None, step=1):
if stop is None:
stop = start
start = 0
…
From a technical perspective, when range(n) is invoked, the actual parameter n will
be assigned to formal parameter start. Within the body, if only one parameter is
received, the start and stop values are reassigned to provide the desired semantics.
Keyword Parameters
The traditional mechanism for matching the actual parameters sent by a caller, to
the formal parameters declared by the function signature is based on the concept
of positional arguments. For example, with signature foo(a=10, b=20, c=30),
parameters sent by the caller are matched, in the given order, to the formal param-
eters. An invocation of foo(5) indicates that a=5, while b and c are assigned their
default values.
Python supports an alternate mechanism for sending a parameter to a function
known as a keyword argument. A keyword argument is specified by explicitly
assigning an actual parameter to a formal parameter by name. For example, with
the above definition of function foo, a call foo(c=5) will invoke the function with
parameters a=10, b=20, c=5.
A function’s author can require that certain parameters be sent only through the
keyword-argument syntax. We never place such a restriction in our own function
definitions, but we will see several important uses of keyword-only parameters in
Python’s standard libraries. As an example, the built-in max function accepts a
keyword parameter, coincidentally named key, that can be used to vary the notion
of “maximum” that is used.
28 Chapter 1. Python Primer
By default, max operates based upon the natural order of elements according
to the < operator for that type. But the maximum can be computed by comparing
some other aspect of the elements. This is done by providing an auxiliary function
that converts a natural element to some other value for the sake of comparison.
For example, if we are interested in finding a numeric value with magnitude that is
maximal (i.e., considering −35 to be larger than +20), we can use the calling syn-
tax max(a, b, key=abs). In this case, the built-in abs function is itself sent as the
value associated with the keyword parameter key. (Functions are first-class objects
in Python; see Section 1.10.) When max is called in this way, it will compare abs(a)
to abs(b), rather than a to b. The motivation for the keyword syntax as an alternate
to positional arguments is important in the case of max. This function is polymor-
phic in the number of arguments, allowing a call such as max(a,b,c,d); therefore,
it is not possible to designate a key function as a traditional positional element.
Sorting functions in Python also support a similar key parameter for indicating a
nonstandard order. (We explore this further in Section 9.4 and in Section 12.6.1,
when discussing sorting algorithms).
1.5.2 Python’s Built-In Functions
Table 1.4 provides an overview of common functions that are automatically avail-
able in Python, including the previously discussed abs, max, and range. When
choosing names for the parameters, we use identifiers x, y, z for arbitrary numeric
types, k for an integer, and a, b, and c for arbitrary comparable types. We use
the identifier, iterable, to represent an instance of any iterable type (e.g., str, list,
tuple, set, dict); we will discuss iterators and iterable data types in Section 1.8.
A sequence represents a more narrow category of indexable classes, including str,
list, and tuple, but neither set nor dict. Most of the entries in Table 1.4 can be
categorized according to their functionality as follows:
Input/Output: print, input, and open will be more fully explained in Section 1.6.
Character Encoding: ord and chr relate characters and their integer code points.
For example, ord( A ) is 65 and chr(65) is A .
Mathematics: abs, divmod, pow, round, and sum provide common mathematical
functionality; an additional math module will be introduced in Section 1.11.
Ordering: max and min apply to any data type that supports a notion of compar-
ison, or to any collection of such values. Likewise, sorted can be used to produce
an ordered list of elements drawn from any existing collection.
Collections/Iterations: range generates a new sequence of numbers; len reports
the length of any existing collection; functions reversed, all, any, and map oper-
ate on arbitrary iterations as well; iter and next provide a general framework for
iteration through elements of a collection, and are discussed in Section 1.8.
1.5. Functions 29
Common Built-In Functions
Calling Syntax Description
abs(x) Return the absolute value of a number.
all(iterable) Return True if bool(e) is True for each element e.
any(iterable) Return True if bool(e) is True for at least one element e.
chr(integer) Return a one-character string with the given Unicode code point.
divmod(x, y) Return (x // y, x % y) as tuple, if x and y are integers.
hash(obj) Return an integer hash value for the object (see Chapter 10).
id(obj) Return the unique integer serving as an “identity” for the object.
input(prompt) Return a string from standard input; the prompt is optional.
isinstance(obj, cls) Determine if obj is an instance of the class (or a subclass).
iter(iterable) Return a new iterator object for the parameter (see Section 1.8).
len(iterable) Return the number of elements in the given iteration.
map(f, iter1, iter2, ...)
Return an iterator yielding the result of function calls f(e1, e2, ...)
for respective elements e1 ∈ iter1, e2 ∈ iter2, ...
max(iterable) Return the largest element of the given iteration.
max(a, b, c, ...) Return the largest of the arguments.
min(iterable) Return the smallest element of the given iteration.
min(a, b, c, ...) Return the smallest of the arguments.
next(iterator) Return the next element reported by the iterator (see Section 1.8).
open(filename, mode) Open a file with the given name and access mode.
ord(char) Return the Unicode code point of the given character.
pow(x, y)
Return the value xy (as an integer if x and y are integers);
equivalent to x y.
pow(x, y, z) Return the value (xy mod z) as an integer.
print(obj1, obj2, ...) Print the arguments, with separating spaces and trailing newline.
range(stop) Construct an iteration of values 0, 1, . . . , stop − 1.
range(start, stop) Construct an iteration of values start, start + 1, . . . , stop − 1.
range(start, stop, step) Construct an iteration of values start, start + step, start + 2 step, . . .
reversed(sequence) Return an iteration of the sequence in reverse.
round(x) Return the nearest int value (a tie is broken toward the even value).
round(x, k) Return the value rounded to the nearest 10−k (return-type matches x).
sorted(iterable) Return a list containing elements of the iterable in sorted order.
sum(iterable) Return the sum of the elements in the iterable (must be numeric).
type(obj) Return the class to which the instance obj belongs.
Table 1.4: Commonly used built-in function in Python.
30 Chapter 1. Python Primer
1.6 Simple Input and Output
In this section, we address the basics of input and output in Python, describing stan-
dard input and output through the user console, and Python’s support for reading
and writing text files.
1.6.1 Console Input and Output
The print Function
The built-in function, print, is used to generate standard output to the console.
In its simplest form, it prints an arbitrary sequence of arguments, separated by
spaces, and followed by a trailing newline character. For example, the command
print( maroon , 5) outputs the string maroon 5\n . Note that arguments need
not be string instances. A nonstring argument x will be displayed as str(x). Without
any arguments, the command print( ) outputs a single newline character.
The print function can be customized through the use of the following keyword
parameters (see Section 1.5 for a discussion of keyword parameters):
• By default, the print function inserts a separating space into the output be-
tween each pair of arguments. The separator can be customized by providing
a desired separating string as a keyword parameter, sep. For example, colon-
separated output can be produced as print(a, b, c, sep= : ). The separating
string need not be a single character; it can be a longer string, and it can be
the empty string, sep= , causing successive arguments to be directly con-
catenated.
• By default, a trailing newline is output after the final argument. An alterna-
tive trailing string can be designated using a keyword parameter, end. Des-
ignating the empty string end= suppresses all trailing characters.
• By default, the print function sends its output to the standard console. How-
ever, output can be directed to a file by indicating an output file stream (see
Section 1.6.2) using file as a keyword parameter.
The input Function
The primary means for acquiring information from the user console is a built-in
function named input. This function displays a prompt, if given as an optional pa-
rameter, and then waits until the user enters some sequence of characters followed
by the return key. The formal return value of the function is the string of characters
that were entered strictly before the return key (i.e., no newline character exists in
the returned string).
1.6. Simple Input and Output 31
When reading a numeric value from the user, a programmer must use the input
function to get the string of characters, and then use the int or float syntax to
construct the numeric value that character string represents. That is, if a call to
response = input( ) reports that the user entered the characters, 2013 , the syntax
int(response) could be used to produce the integer value 2013. It is quite common
to combine these operations with a syntax such as
year = int(input( In what year were you born? ))
if we assume that the user will enter an appropriate response. (In Section 1.7 we
discuss error handling in such a situation.)
Because input returns a string as its result, use of that function can be combined
with the existing functionality of the string class, as described in Appendix A. For
example, if the user enters multiple pieces of information on the same line, it is
common to call the split method on the result, as in
reply = input( Enter x and y, separated by spaces: )
pieces = reply.split( ) # returns a list of strings, as separated by spaces
x = float(pieces[0])
y = float(pieces[1])
A Sample Program
Here is a simple, but complete, program that demonstrates the use of the input
and print functions. The tools for formatting the final output is discussed in Ap-
pendix A.
age = int(input( Enter your age in years: ))
max heart rate = 206.9 − (0.67 age) # as per Med Sci Sports Exerc.
target = 0.65 max heart rate
print( Your target fat-burning heart rate is , target)
1.6.2 Files
Files are typically accessed in Python beginning with a call to a built-in function,
named open, that returns a proxy for interactions with the underlying file. For
example, the command, fp = open( sample.txt ), attempts to open a file named
sample.txt, returning a proxy that allows read-only access to the text file.
The open function accepts an optional second parameter that determines the
access mode. The default mode is r for reading. Other common modes are w
for writing to the file (causing any existing file with that name to be overwritten),
or a for appending to the end of an existing file. Although we focus on use of
text files, it is possible to work with binary files, using access modes such as rb
or wb .
32 Chapter 1. Python Primer
When processing a file, the proxy maintains a current position within the file as
an offset from the beginning, measured in number of bytes. When opening a file
with mode r or w , the position is initially 0; if opened in append mode, a ,
the position is initially at the end of the file. The syntax fp.close( ) closes the file
associated with proxy fp, ensuring that any written contents are saved. A summary
of methods for reading and writing a file is given in Table 1.5
Calling Syntax Description
fp.read( ) Return the (remaining) contents of a readable file as a string.
fp.read(k) Return the next k bytes of a readable file as a string.
fp.readline( ) Return (remainder of ) the current line of a readable file as a string.
fp.readlines( ) Return all (remaining) lines of a readable file as a list of strings.
for line in fp: Iterate all (remaining) lines of a readable file.
fp.seek(k) Change the current position to be at the kth byte of the file.
fp.tell( ) Return the current position, measured as byte-offset from the start.
fp.write(string) Write given string at current position of the writable file.
fp.writelines(seq)
Write each of the strings of the given sequence at the current
position of the writable file. This command does not insert
any newlines, beyond those that are embedded in the strings.
print(..., file=fp) Redirect output of print function to the file.
Table 1.5: Behaviors for interacting with a text file via a file proxy (named fp).
Reading from a File
The most basic command for reading via a proxy is the read method. When invoked
on file proxy fp, as fp.read(k), the command returns a string representing the next k
bytes of the file, starting at the current position. Without a parameter, the syntax
fp.read( ) returns the remaining contents of the file in entirety. For convenience,
files can be read a line at a time, using the readline method to read one line, or
the readlines method to return a list of all remaining lines. Files also support the
for-loop syntax, with iteration being line by line (e.g., for line in fp:).
Writing to a File
When a file proxy is writable, for example, if created with access mode w or
a , text can be written using methods write or writelines. For example, if we de-
fine fp = open( results.txt , w ), the syntax fp.write( Hello World.\n )
writes a single line to the file with the given string. Note well that write does not
explicitly add a trailing newline, so desired newline characters must be embedded
directly in the string parameter. Recall that the output of the print method can be
redirected to a file using a keyword parameter, as described in Section 1.6.
1.7. Exception Handling 33
1.7 Exception Handling
Exceptions are unexpected events that occur during the execution of a program.
An exception might result from a logical error or an unanticipated situation. In
Python, exceptions (also known as errors) are objects that are raised (or thrown) by
code that encounters an unexpected circumstance. The Python interpreter can also
raise an exception should it encounter an unexpected condition, like running out of
memory. A raised error may be caught by a surrounding context that “handles” the
exception in an appropriate fashion. If uncaught, an exception causes the interpreter
to stop executing the program and to report an appropriate message to the console.
In this section, we examine the most common error types in Python, the mechanism
for catching and handling errors that have been raised, and the syntax for raising
errors from within user-defined blocks of code.
Common Exception Types
Python includes a rich hierarchy of exception classes that designate various cate-
gories of errors; Table 1.6 shows many of those classes. The Exception class serves
as a base class for most other error types. An instance of the various subclasses
encodes details about a problem that has occurred. Several of these errors may be
raised in exceptional cases by behaviors introduced in this chapter. For example,
use of an undefined identifier in an expression causes a NameError, and errant use
of the dot notation, as in foo.bar( ), will generate an AttributeError if object foo
does not support a member named bar.
Class Description
Exception A base class for most error types
AttributeError Raised by syntax obj.foo, if obj has no member named foo
EOFError Raised if “end of file” reached for console or file input
IOError Raised upon failure of I/O operation (e.g., opening file)
IndexError Raised if index to sequence is out of bounds
KeyError Raised if nonexistent key requested for set or dictionary
KeyboardInterrupt Raised if user types ctrl-C while program is executing
NameError Raised if nonexistent identifier used
StopIteration Raised by next(iterator) if no element; see Section 1.8
TypeError Raised when wrong type of parameter is sent to a function
ValueError Raised when parameter has invalid value (e.g., sqrt(−5))
ZeroDivisionError Raised when any division operator used with 0 as divisor
Table 1.6: Common exception classes in Python
34 Chapter 1. Python Primer
Sending the wrong number, type, or value of parameters to a function is another
common cause for an exception. For example, a call to abs( hello ) will raise a
TypeError because the parameter is not numeric, and a call to abs(3, 5) will raise
a TypeError because one parameter is expected. A ValueError is typically raised
when the correct number and type of parameters are sent, but a value is illegitimate
for the context of the function. For example, the int constructor accepts a string,
as with int( 137 ), but a ValueError is raised if that string does not represent an
integer, as with int( 3.14 ) or int( hello ).
Python’s sequence types (e.g., list, tuple, and str) raise an IndexError when
syntax such as data[k] is used with an integer k that is not a valid index for the given
sequence (as described in Section 1.2.3). Sets and dictionaries raise a KeyError
when an attempt is made to access a nonexistent element.
1.7.1 Raising an Exception
An exception is thrown by executing the raise statement, with an appropriate in-
stance of an exception class as an argument that designates the problem. For exam-
ple, if a function for computing a square root is sent a negative value as a parameter,
it can raise an exception with the command:
raise ValueError( x cannot be negative )
This syntax raises a newly created instance of the ValueError class, with the error
message serving as a parameter to the constructor. If this exception is not caught
within the body of the function, the execution of the function immediately ceases
and the exception is propagated to the calling context (and possibly beyond).
When checking the validity of parameters sent to a function, it is customary
to first verify that a parameter is of an appropriate type, and then to verify that it
has an appropriate value. For example, the sqrt function in Python’s math library
performs error-checking that might be implemented as follows:
def sqrt(x):
if not isinstance(x, (int, float)):
raise TypeError( x must be numeric )
elif x < 0:
raise ValueError( x cannot be negative )
# do the real work here...
Checking the type of an object can be performed at run-time using the built-in
function, isinstance. In simplest form, isinstance(obj, cls) returns True if object,
obj, is an instance of class, cls, or any subclass of that type. In the above example, a
more general form is used with a tuple of allowable types indicated with the second
parameter. After confirming that the parameter is numeric, the function enforces
an expectation that the number be nonnegative, raising a ValueError otherwise.
1.7. Exception Handling 35
How much error-checking to perform within a function is a matter of debate.
Checking the type and value of each parameter demands additional execution time
and, if taken to an extreme, seems counter to the nature of Python. Consider the
built-in sum function, which computes a sum of a collection of numbers. An im-
plementation with rigorous error-checking might be written as follows:
def sum(values):
if not isinstance(values, collections.Iterable):
raise TypeError( parameter must be an iterable type )
total = 0
for v in values:
if not isinstance(v, (int, float)):
raise TypeError( elements must be numeric )
total = total+ v
return total
The abstract base class, collections.Iterable, includes all of Python’s iterable con-
tainers types that guarantee support for the for-loop syntax (e.g., list, tuple, set);
we discuss iterables in Section 1.8, and the use of modules, such as collections, in
Section 1.11. Within the body of the for loop, each element is verified as numeric
before being added to the total. A far more direct and clear implementation of this
function can be written as follows:
def sum(values):
total = 0
for v in values:
total = total + v
return total
Interestingly, this simple implementation performs exactly like Python’s built-in
version of the function. Even without the explicit checks, appropriate exceptions
are raised naturally by the code. In particular, if values is not an iterable type, the
attempt to use the for-loop syntax raises a TypeError reporting that the object is not
iterable. In the case when a user sends an iterable type that includes a nonnumer-
ical element, such as sum([3.14, oops ]), a TypeError is naturally raised by the
evaluation of expression total + v. The error message
unsupported operand type(s) for +: ’float’ and ’str’
should be sufficiently informative to the caller. Perhaps slightly less obvious is the
error that results from sum([ alpha , beta ]). It will technically report a failed
attempt to add an int and str, due to the initial evaluation of total + alpha ,
when total has been initialized to 0.
In the remainder of this book, we tend to favor the simpler implementations
in the interest of clean presentation, performing minimal error-checking in most
situations.
36 Chapter 1. Python Primer
1.7.2 Catching an Exception
There are several philosophies regarding how to cope with possible exceptional
cases when writing code. For example, if a division x/y is to be computed, there
is clear risk that a ZeroDivisionError will be raised when variable y has value 0. In
an ideal situation, the logic of the program may dictate that y has a nonzero value,
thereby removing the concern for error. However, for more complex code, or in
a case where the value of y depends on some external input to the program, there
remains some possibility of an error.
One philosophy for managing exceptional cases is to “look before you leap.”
The goal is to entirely avoid the possibility of an exception being raised through
the use of a proactive conditional test. Revisiting our division example, we might
avoid the offending situation by writing:
if y != 0:
ratio = x / y
else:
... do something else ...
A second philosophy, often embraced by Python programmers, is that “it is
easier to ask for forgiveness than it is to get permission.” This quote is attributed
to Grace Hopper, an early pioneer in computer science. The sentiment is that we
need not spend extra execution time safeguarding against every possible excep-
tional case, as long as there is a mechanism for coping with a problem after it
arises. In Python, this philosophy is implemented using a try-except control struc-
ture. Revising our first example, the division operation can be guarded as follows:
try:
ratio = x / y
except ZeroDivisionError:
... do something else ...
In this structure, the “try” block is the primary code to be executed. Although it
is a single command in this example, it can more generally be a larger block of
indented code. Following the try-block are one or more “except” cases, each with
an identified error type and an indented block of code that should be executed if the
designated error is raised within the try-block.
The relative advantage of using a try-except structure is that the non-exceptional
case runs efficiently, without extraneous checks for the exceptional condition. How-
ever, handling the exceptional case requires slightly more time when using a try-
except structure than with a standard conditional statement. For this reason, the
try-except clause is best used when there is reason to believe that the exceptional
case is relatively unlikely, or when it is prohibitively expensive to proactively eval-
uate a condition to avoid the exception.
1.7. Exception Handling 37
Exception handling is particularly useful when working with user input, or
when reading from or writing to files, because such interactions are inherently less
predictable. In Section 1.6.2, we suggest the syntax, fp = open( sample.txt ),
for opening a file with read access. That command may raise an IOError for a vari-
ety of reasons, such as a non-existent file, or lack of sufficient privilege for opening
a file. It is significantly easier to attempt the command and catch the resulting error
than it is to accurately predict whether the command will succeed.
We continue by demonstrating a few other forms of the try-except syntax. Ex-
ceptions are objects that can be examined when caught. To do so, an identifier must
be established with a syntax as follows:
try:
fp = open( sample.txt )
except IOError as e:
print( Unable to open the file: , e)
In this case, the name, e, denotes the instance of the exception that was thrown, and
printing it causes a detailed error message to be displayed (e.g., “file not found”).
A try-statement may handle more than one type of exception. For example,
consider the following command from Section 1.6.1:
age = int(input( Enter your age in years: ))
This command could fail for a variety of reasons. The call to input will raise an
EOFError if the console input fails. If the call to input completes successfully, the
int constructor raises a ValueError if the user has not entered characters represent-
ing a valid integer. If we want to handle two or more types of errors in the same
way, we can use a single except-statement, as in the following example:
age = −1 # an initially invalid choice
while age <= 0:
try:
age = int(input( Enter your age in years: ))
if age <= 0:
print( Your age must be positive )
except (ValueError, EOFError):
print( Invalid response )
We use the tuple, (ValueError, EOFError), to designate the types of errors that we
wish to catch with the except-clause. In this implementation, we catch either error,
print a response, and continue with another pass of the enclosing while loop. We
note that when an error is raised within the try-block, the remainder of that body
is immediately skipped. In this example, if the exception arises within the call to
input, or the subsequent call to the int constructor, the assignment to age never
occurs, nor the message about needing a positive value. Because the value of age
38 Chapter 1. Python Primer
will be unchanged, the while loop will continue. If we preferred to have the while
loop continue without printing the Invalid response message, we could have
written the exception-clause as
except (ValueError, EOFError):
pass
The keyword, pass, is a statement that does nothing, yet it can serve syntactically
as a body of a control structure. In this way, we quietly catch the exception, thereby
allowing the surrounding while loop to continue.
In order to provide different responses to different types of errors, we may use
two or more except-clauses as part of a try-structure. In our previous example, an
EOFError suggests a more insurmountable error than simply an errant value being
entered. In that case, we might wish to provide a more specific error message, or
perhaps to allow the exception to interrupt the loop and be propagated to a higher
context. We could implement such behavior as follows:
age = −1 # an initially invalid choice
while age <= 0:
try:
age = int(input( Enter your age in years: ))
if age <= 0:
print( Your age must be positive )
except ValueError:
print( That is an invalid age specification )
except EOFError:
print( There was an unexpected error reading input. )
raise # let s re-raise this exception
In this implementation, we have separate except-clauses for the ValueError and
EOFError cases. The body of the clause for handling an EOFError relies on another
technique in Python. It uses the raise statement without any subsequent argument,
to re-raise the same exception that is currently being handled. This allows us to
provide our own response to the exception, and then to interrupt the while loop and
propagate the exception upward.
In closing, we note two additional features of try-except structures in Python.
It is permissible to have a final except-clause without any identified error types,
using syntax except:, to catch any other exceptions that occurred. However, this
technique should be used sparingly, as it is difficult to suggest how to handle an
error of an unknown type. A try-statement can have a finally clause, with a body of
code that will always be executed in the standard or exceptional cases, even when
an uncaught or re-raised exception occurs. That block is typically used for critical
cleanup work, such as closing an open file.
1.8. Iterators and Generators 39
1.8 Iterators and Generators
In Section 1.4.2, we introduced the for-loop syntax beginning as:
for element in iterable:
and we noted that there are many types of objects in Python that qualify as being
iterable. Basic container types, such as list, tuple, and set, qualify as iterable types.
Furthermore, a string can produce an iteration of its characters, a dictionary can
produce an iteration of its keys, and a file can produce an iteration of its lines. User-
defined types may also support iteration. In Python, the mechanism for iteration is
based upon the following conventions:
• An iterator is an object that manages an iteration through a series of values. If
variable, i, identifies an iterator object, then each call to the built-in function,
next(i), produces a subsequent element from the underlying series, with a
StopIteration exception raised to indicate that there are no further elements.
• An iterable is an object, obj, that produces an iterator via the syntax iter(obj).
By these definitions, an instance of a list is an iterable, but not itself an iterator.
With data = [1, 2, 4, 8], it is not legal to call next(data). However, an iterator
object can be produced with syntax, i = iter(data), and then each subsequent call
to next(i) will return an element of that list. The for-loop syntax in Python simply
automates this process, creating an iterator for the give iterable, and then repeatedly
calling for the next element until catching the StopIteration exception.
More generally, it is possible to create multiple iterators based upon the same
iterable object, with each iterator maintaining its own state of progress. However,
iterators typically maintain their state with indirect reference back to the original
collection of elements. For example, calling iter(data) on a list instance produces
an instance of the list iterator class. That iterator does not store its own copy of the
list of elements. Instead, it maintains a current index into the original list, represent-
ing the next element to be reported. Therefore, if the contents of the original list
are modified after the iterator is constructed, but before the iteration is complete,
the iterator will be reporting the updated contents of the list.
Python also supports functions and classes that produce an implicit iterable se-
ries of values, that is, without constructing a data structure to store all of its values
at once. For example, the call range(1000000) does not return a list of numbers; it
returns a range object that is iterable. This object generates the million values one
at a time, and only as needed. Such a lazy evaluation technique has great advan-
tage. In the case of range, it allows a loop of the form, for j in range(1000000):,
to execute without setting aside memory for storing one million values. Also, if
such a loop were to be interrupted in some fashion, no time will have been spent
computing unused values of the range.
40 Chapter 1. Python Primer
We see lazy evaluation used in many of Python’s libraries. For example, the
dictionary class supports methods keys( ), values( ), and items( ), which respec-
tively produce a “view” of all keys, values, or (key,value) pairs within a dictionary.
None of these methods produces an explicit list of results. Instead, the views that
are produced are iterable objects based upon the actual contents of the dictionary.
An explicit list of values from such an iteration can be immediately constructed by
calling the list class constructor with the iteration as a parameter. For example, the
syntax list(range(1000)) produces a list instance with values from 0 to 999, while
the syntax list(d.values( )) produces a list that has elements based upon the current
values of dictionary d. We can similarly construct a tuple or set instance based
upon a given iterable.
Generators
In Section 2.3.4, we will explain how to define a class whose instances serve as
iterators. However, the most convenient technique for creating iterators in Python
is through the use of generators. A generator is implemented with a syntax that
is very similar to a function, but instead of returning values, a yield statement is
executed to indicate each element of the series. As an example, consider the goal
of determining all factors of a positive integer. For example, the number 100 has
factors 1, 2, 4, 5, 10, 20, 25, 50, 100. A traditional function might produce and
return a list containing all factors, implemented as:
def factors(n): # traditional function that computes factors
results = [ ] # store factors in a new list
for k in range(1,n+1):
if n % k == 0: # divides evenly, thus k is a factor
results.append(k) # add k to the list of factors
return results # return the entire list
In contrast, an implementation of a generator for computing those factors could be
implemented as follows:
def factors(n): # generator that computes factors
for k in range(1,n+1):
if n % k == 0: # divides evenly, thus k is a factor
yield k # yield this factor as next result
Notice use of the keyword yield rather than return to indicate a result. This indi-
cates to Python that we are defining a generator, rather than a traditional function. It
is illegal to combine yield and return statements in the same implementation, other
than a zero-argument return statement to cause a generator to end its execution. If
a programmer writes a loop such as for factor in factors(100):, an instance of our
generator is created. For each iteration of the loop, Python executes our procedure
1.8. Iterators and Generators 41
until a yield statement indicates the next value. At that point, the procedure is tem-
porarily interrupted, only to be resumed when another value is requested. When
the flow of control naturally reaches the end of our procedure (or a zero-argument
return statement), a StopIteration exception is automatically raised. Although this
particular example uses a single yield statement in the source code, a generator can
rely on multiple yield statements in different constructs, with the generated series
determined by the natural flow of control. For example, we can greatly improve
the efficiency of our generator for computing factors of a number, n, by only test-
ing values up to the square root of that number, while reporting the factor n//k
that is associated with each k (unless n//k equals k). We might implement such a
generator as follows:
def factors(n): # generator that computes factors
k = 1
while k k < n: # while k < sqrt(n)
if n % k == 0:
yield k
yield n // k
k += 1
if k k == n: # special case if n is perfect square
yield k
We should note that this generator differs from our first version in that the factors
are not generated in strictly increasing order. For example, factors(100) generates
the series 1, 100, 2, 50, 4, 25, 5, 20, 10.
In closing, we wish to emphasize the benefits of lazy evaluation when using a
generator rather than a traditional function. The results are only computed if re-
quested, and the entire series need not reside in memory at one time. In fact, a
generator can effectively produce an infinite series of values. As an example, the
Fibonacci numbers form a classic mathematical sequence, starting with value 0,
then value 1, and then each subsequent value being the sum of the two preceding
values. Hence, the Fibonacci series begins as: 0, 1, 1, 2, 3, 5, 8, 13, . . .. The follow-
ing generator produces this infinite series.
def fibonacci( ):
a = 0
b = 1
while True: # keep going...
yield a # report value, a, during this pass
future = a + b
a = b # this will be next value reported
b = future # and subsequently this
42 Chapter 1. Python Primer
1.9 Additional Python Conveniences
In this section, we introduce several features of Python that are particularly conve-
nient for writing clean, concise code. Each of these syntaxes provide functionality
that could otherwise be accomplished using functionality that we have introduced
earlier in this chapter. However, at times, the new syntax is a more clear and direct
expression of the logic.
1.9.1 Conditional Expressions
Python supports a conditional expression syntax that can replace a simple control
structure. The general syntax is an expression of the form:
expr1 if condition else expr2
This compound expression evaluates to expr1 if the condition is true, and otherwise
evaluates to expr2. For those familiar with Java or C++, this is equivalent to the
syntax, condition ? expr1 : expr2, in those languages.
As an example, consider the goal of sending the absolute value of a variable, n,
to a function (and without relying on the built-in abs function, for the sake of ex-
ample). Using a traditional control structure, we might accomplish this as follows:
if n >= 0:
param = n
else:
param = −n
result = foo(param) # call the function
With the conditional expression syntax, we can directly assign a value to variable,
param, as follows:
param = n if n >= 0 else −n # pick the appropriate value
result = foo(param) # call the function
In fact, there is no need to assign the compound expression to a variable. A condi-
tional expression can itself serve as a parameter to the function, written as follows:
result = foo(n if n >= 0 else −n)
Sometimes, the mere shortening of source code is advantageous because it
avoids the distraction of a more cumbersome control structure. However, we rec-
ommend that a conditional expression be used only when it improves the readability
of the source code, and when the first of the two options is the more “natural” case,
given its prominence in the syntax. (We prefer to view the alternative value as more
exceptional.)
1.9. Additional Python Conveniences 43
1.9.2 Comprehension Syntax
A very common programming task is to produce one series of values based upon
the processing of another series. Often, this task can be accomplished quite simply
in Python using what is known as a comprehension syntax. We begin by demon-
strating list comprehension, as this was the first form to be supported by Python.
Its general form is as follows:
[ expression for value in iterable if condition ]
We note that both expression and condition may depend on value, and that the
if-clause is optional. The evaluation of the comprehension is logically equivalent
to the following traditional control structure for computing a resulting list:
result = [ ]
for value in iterable:
if condition:
result.append(expression)
As a concrete example, a list of the squares of the numbers from 1 to n, that is
[1, 4, 9, 16, 25, . . . , n2], can be created by traditional means as follows:
squares = [ ]
for k in range(1, n+1):
squares.append(k k)
With list comprehension, this logic is expressed as follows:
squares = [k k for k in range(1, n+1)]
As a second example, Section 1.8 introduced the goal of producing a list of factors
for an integer n. That task is accomplished with the following list comprehension:
factors = [k for k in range(1,n+1) if n % k == 0]
Python supports similar comprehension syntaxes that respectively produce a
set, generator, or dictionary. We compare those syntaxes using our example for
producing the squares of numbers.
[ k k for k in range(1, n+1) ] list comprehension
{ k k for k in range(1, n+1) } set comprehension
( k k for k in range(1, n+1) ) generator comprehension
{ k : k k for k in range(1, n+1) } dictionary comprehension
The generator syntax is particularly attractive when results do not need to be stored
in memory. For example, to compute the sum of the first n squares, the genera-
tor syntax, total = sum(k k for k in range(1, n+1)), is preferred to the use of an
explicitly instantiated list comprehension as the parameter.
44 Chapter 1. Python Primer
1.9.3 Packing and Unpacking of Sequences
Python provides two additional conveniences involving the treatment of tuples and
other sequence types. The first is rather cosmetic. If a series of comma-separated
expressions are given in a larger context, they will be treated as a single tuple, even
if no enclosing parentheses are provided. For example, the assignment
data = 2, 4, 6, 8
results in identifier, data, being assigned to the tuple (2, 4, 6, 8). This behavior
is called automatic packing of a tuple. One common use of packing in Python is
when returning multiple values from a function. If the body of a function executes
the command,
return x, y
it will be formally returning a single object that is the tuple (x, y).
As a dual to the packing behavior, Python can automatically unpack a se-
quence, allowing one to assign a series of individual identifiers to the elements
of sequence. As an example, we can write
a, b, c, d = range(7, 11)
which has the effect of assigning a=7, b=8, c=9, and d=10, as those are the four
values in the sequence returned by the call to range. For this syntax, the right-hand
side expression can be any iterable type, as long as the number of variables on the
left-hand side is the same as the number of elements in the iteration.
This technique can be used to unpack tuples returned by a function. For exam-
ple, the built-in function, divmod(a, b), returns the pair of values (a // b, a % b)
associated with an integer division. Although the caller can consider the return
value to be a single tuple, it is possible to write
quotient, remainder = divmod(a, b)
to separately identify the two entries of the returned tuple. This syntax can also be
used in the context of a for loop, when iterating over a sequence of iterables, as in
for x, y in [ (7, 2), (5, 8), (6, 4) ]:
In this example, there will be three iterations of the loop. During the first pass, x=7
and y=2, and so on. This style of loop is quite commonly used to iterate through
key-value pairs that are returned by the items( ) method of the dict class, as in:
for k, v in mapping.items( ):
1.9. Additional Python Conveniences 45
Simultaneous Assignments
The combination of automatic packing and unpacking forms a technique known
as simultaneous assignment, whereby we explicitly assign a series of values to a
series of identifiers, using a syntax:
x, y, z = 6, 2, 5
In effect, the right-hand side of this assignment is automatically packed into a tuple,
and then automatically unpacked with its elements assigned to the three identifiers
on the left-hand side.
When using a simultaneous assignment, all of the expressions are evaluated
on the right-hand side before any of the assignments are made to the left-hand
variables. This is significant, as it provides a convenient means for swapping the
values associated with two variables:
j, k = k, j
With this command, j will be assigned to the old value of k, and k will be assigned
to the old value of j. Without simultaneous assignment, a swap typically requires
more delicate use of a temporary variable, such as
temp = j
j = k
k = temp
With the simultaneous assignment, the unnamed tuple representing the packed val-
ues on the right-hand side implicitly serves as the temporary variable when per-
forming such a swap.
The use of simultaneous assignments can greatly simplify the presentation of
code. As an example, we reconsider the generator on page 41 that produces the
Fibonacci series. The original code requires separate initialization of variables a
and b to begin the series. Within each pass of the loop, the goal was to reassign a
and b, respectively, to the values of b and a+b. At the time, we accomplished this
with brief use of a third variable. With simultaneous assignments, that generator
can be implemented more directly as follows:
def fibonacci( ):
a, b = 0, 1
while True:
yield a
a, b = b, a+b
46 Chapter 1. Python Primer
1.10 Scopes and Namespaces
When computing a sum with the syntax x + y in Python, the names x and y must
have been previously associated with objects that serve as values; a NameError
will be raised if no such definitions are found. The process of determining the
value associated with an identifier is known as name resolution.
Whenever an identifier is assigned to a value, that definition is made with a
specific scope. Top-level assignments are typically made in what is known as global
scope. Assignments made within the body of a function typically have scope that is
local to that function call. Therefore, an assignment, x = 5, within a function has
no effect on the identifier, x, in the broader scope.
Each distinct scope in Python is represented using an abstraction known as a
namespace. A namespace manages all identifiers that are currently defined in a
given scope. Figure 1.8 portrays two namespaces, one being that of a caller to our
count function from Section 1.5, and the other being the local namespace during
the execution of that function.
A-
str
A
str
CS
float
3.56
int
2
item
datagrades
major
gpa target
n
list
str
B+
str
A-
str
Figure 1.8: A portrayal of the two namespaces associated with a user’s call
count(grades, A ), as defined in Section 1.5. The left namespace is the caller’s
and the right namespace represents the local scope of the function.
Python implements a namespace with its own dictionary that maps each iden-
tifying string (e.g., n ) to its associated value. Python provides several ways to
examine a given namespace. The function, dir, reports the names of the identifiers
in a given namespace (i.e., the keys of the dictionary), while the function, vars,
returns the full dictionary. By default, calls to dir( ) and vars( ) report on the most
locally enclosing namespace in which they are executed.
1.10. Scopes and Namespaces 47
When an identifier is indicated in a command, Python searches a series of
namespaces in the process of name resolution. First, the most locally enclosing
scope is searched for a given name. If not found there, the next outer scope is
searched, and so on. We will continue our examination of namespaces, in Sec-
tion 2.5, when discussing Python’s treatment of object-orientation. We will see
that each object has its own namespace to store its attributes, and that classes each
have a namespace as well.
First-Class Objects
In the terminology of programming languages, first-class objects are instances of
a type that can be assigned to an identifier, passed as a parameter, or returned by
a function. All of the data types we introduced in Section 1.2.3, such as int and
list, are clearly first-class types in Python. In Python, functions and classes are also
treated as first-class objects. For example, we could write the following:
scream = print # assign name ’scream’ to the function denoted as ’print’
scream( Hello ) # call that function
In this case, we have not created a new function, we have simply defined scream
as an alias for the existing print function. While there is little motivation for pre-
cisely this example, it demonstrates the mechanism that is used by Python to al-
low one function to be passed as a parameter to another. On page 28, we noted
that the built-in function, max, accepts an optional keyword parameter to specify
a non-default order when computing a maximum. For example, a caller can use
the syntax, max(a, b, key=abs), to determine which value has the larger absolute
value. Within the body of that function, the formal parameter, key, is an identifier
that will be assigned to the actual parameter, abs.
In terms of namespaces, an assignment such as scream = print, introduces the
identifier, scream, into the current namespace, with its value being the object that
represents the built-in function, print. The same mechanism is applied when a user-
defined function is declared. For example, our count function from Section 1.5
beings with the following syntax:
def count(data, target):
…
Such a declaration introduces the identifier, count, into the current namespace,
with the value being a function instance representing its implementation. In similar
fashion, the name of a newly defined class is associated with a representation of
that class as its value. (Class definitions will be introduced in the next chapter.)
48 Chapter 1. Python Primer
1.11 Modules and the Import Statement
We have already introduced many functions (e.g., max) and classes (e.g., list)
that are defined within Python’s built-in namespace. Depending on the version of
Python, there are approximately 130–150 definitions that were deemed significant
enough to be included in that built-in namespace.
Beyond the built-in definitions, the standard Python distribution includes per-
haps tens of thousands of other values, functions, and classes that are organized in
additional libraries, known as modules, that can be imported from within a pro-
gram. As an example, we consider the math module. While the built-in namespace
includes a few mathematical functions (e.g., abs, min, max, round), many more
are relegated to the math module (e.g., sin, cos, sqrt). That module also defines
approximate values for the mathematical constants, pi and e.
Python’s import statement loads definitions from a module into the current
namespace. One form of an import statement uses a syntax such as the following:
from math import pi, sqrt
This command adds both pi and sqrt, as defined in the math module, into the cur-
rent namespace, allowing direct use of the identifier, pi, or a call of the function,
sqrt(2). If there are many definitions from the same module to be imported, an
asterisk may be used as a wild card, as in, from math import , but this form
should be used sparingly. The danger is that some of the names defined in the mod-
ule may conflict with names already in the current namespace (or being imported
from another module), and the import causes the new definitions to replace existing
ones.
Another approach that can be used to access many definitions from the same
module is to import the module itself, using a syntax such as:
import math
Formally, this adds the identifier, math, to the current namespace, with the module
as its value. (Modules are also first-class objects in Python.) Once imported, in-
dividual definitions from the module can be accessed using a fully-qualified name,
such as math.pi or math.sqrt(2).
Creating a New Module
To create a new module, one simply has to put the relevant definitions in a file
named with a .py suffix. Those definitions can be imported from any other .py
file within the same project directory. For example, if we were to put the definition
of our count function (see Section 1.5) into a file named utility.py, we could
import that function using the syntax, from utility import count.
1.11. Modules and the Import Statement 49
It is worth noting that top-level commands with the module source code are
executed when the module is first imported, almost as if the module were its own
script. There is a special construct for embedding commands within the module
that will be executed if the module is directly invoked as a script, but not when
the module is imported from another script. Such commands should be placed in a
body of a conditional statement of the following form,
if name == __main__ :
Using our hypothetical utility.py module as an example, such commands will
be executed if the interpreter is started with a command python utility.py, but
not when the utility module is imported into another context. This approach is often
used to embed what are known as unit tests within the module; we will discuss unit
testing further in Section 2.2.4.
1.11.1 Existing Modules
Table 1.7 provides a summary of a few available modules that are relevant to a
study of data structures. We have already discussed the math module briefly. In the
remainder of this section, we highlight another module that is particularly important
for some of the data structures and algorithms that we will study later in this book.
Existing Modules
Module Name Description
array Provides compact array storage for primitive types.
collections
Defines additional data structures and abstract base classes
involving collections of objects.
copy Defines general functions for making copies of objects.
heapq Provides heap-based priority queue functions (see Section 9.3.7).
math Defines common mathematical constants and functions.
os Provides support for interactions with the operating system.
random Provides random number generation.
re Provides support for processing regular expressions.
sys Provides additional level of interaction with the Python interpreter.
time Provides support for measuring time, or delaying a program.
Table 1.7: Some existing Python modules relevant to data structures and algorithms.
Pseudo-Random Number Generation
Python’s random module provides the ability to generate pseudo-random numbers,
that is, numbers that are statistically random (but not necessarily truly random).
A pseudo-random number generator uses a deterministic formula to generate the
50 Chapter 1. Python Primer
next number in a sequence based upon one or more past numbers that it has gen-
erated. Indeed, a simple yet popular pseudo-random number generator chooses its
next number based solely on the most recently chosen number and some additional
parameters using the following formula.
next = (a*current + b) % n;
where a, b, and n are appropriately chosen integers. Python uses a more advanced
technique known as a Mersenne twister. It turns out that the sequences generated
by these techniques can be proven to be statistically uniform, which is usually
good enough for most applications requiring random numbers, such as games. For
applications, such as computer security settings, where one needs unpredictable
random sequences, this kind of formula should not be used. Instead, one should
ideally sample from a source that is actually random, such as radio static coming
from outer space.
Since the next number in a pseudo-random generator is determined by the pre-
vious number(s), such a generator always needs a place to start, which is called its
seed. The sequence of numbers generated for a given seed will always be the same.
One common trick to get a different sequence each time a program is run is to use
a seed that will be different for each run. For example, we could use some timed
input from a user or the current system time in milliseconds.
Python’s random module provides support for pseudo-random number gener-
ation by defining a Random class; instances of that class serve as generators with
independent state. This allows different aspects of a program to rely on their own
pseudo-random number generator, so that calls to one generator do not affect the
sequence of numbers produced by another. For convenience, all of the methods
supported by the Random class are also supported as stand-alone functions of the
random module (essentially using a single generator instance for all top-level calls).
Syntax Description
seed(hashable)
Initializes the pseudo-random number generator
based upon the hash value of the parameter
random( )
Returns a pseudo-random floating-point
value in the interval [0.0, 1.0).
randint(a,b)
Returns a pseudo-random integer
in the closed interval [a, b].
randrange(start, stop, step)
Returns a pseudo-random integer in the standard
Python range indicated by the parameters.
choice(seq)
Returns an element of the given sequence
chosen pseudo-randomly.
shuffle(seq)
Reorders the elements of the given
sequence pseudo-randomly.
Table 1.8: Methods supported by instances of the Random class, and as top-level
functions of the random module.
1.12. Exercises 51
1.12 Exercises
For help with exercises, please visit the site, www.wiley.com/college/goodrich.
Reinforcement
R-1.1 Write a short Python function, is multiple(n, m), that takes two integer
values and returns True if n is a multiple of m, that is, n = mi for some
integer i, and False otherwise.
R-1.2 Write a short Python function, is even(k), that takes an integer value and
returns True if k is even, and False otherwise. However, your function
cannot use the multiplication, modulo, or division operators.
R-1.3 Write a short Python function, minmax(data), that takes a sequence of
one or more numbers, and returns the smallest and largest numbers, in the
form of a tuple of length two. Do not use the built-in functions min or
max in implementing your solution.
R-1.4 Write a short Python function that takes a positive integer n and returns
the sum of the squares of all the positive integers smaller than n.
R-1.5 Give a single command that computes the sum from Exercise R-1.4, rely-
ing on Python’s comprehension syntax and the built-in sum function.
R-1.6 Write a short Python function that takes a positive integer n and returns
the sum of the squares of all the odd positive integers smaller than n.
R-1.7 Give a single command that computes the sum from Exercise R-1.6, rely-
ing on Python’s comprehension syntax and the built-in sum function.
R-1.8 Python allows negative integers to be used as indices into a sequence,
such as a string. If string s has length n, and expression s[k] is used for in-
dex −n ≤ k < 0, what is the equivalent index j ≥ 0 such that s[j] references
the same element?
R-1.9 What parameters should be sent to the range constructor, to produce a
range with values 50, 60, 70, 80?
R-1.10 What parameters should be sent to the range constructor, to produce a
range with values 8, 6, 4, 2, 0, −2, −4, −6, −8?
R-1.11 Demonstrate how to use Python’s list comprehension syntax to produce
the list [1, 2, 4, 8, 16, 32, 64, 128, 256].
R-1.12 Python’s random module includes a function choice(data) that returns a
random element from a non-empty sequence. The random module in-
cludes a more basic function randrange, with parameterization similar to
the built-in range function, that return a random choice from the given
range. Using only the randrange function, implement your own version
of the choice function.
http:\\www.wiley.com/college/goodrich
52 Chapter 1. Python Primer
Creativity
C-1.13 Write a pseudo-code description of a function that reverses a list of n
integers, so that the numbers are listed in the opposite order than they
were before, and compare this method to an equivalent Python function
for doing the same thing.
C-1.14 Write a short Python function that takes a sequence of integer values and
determines if there is a distinct pair of numbers in the sequence whose
product is odd.
C-1.15 Write a Python function that takes a sequence of numbers and determines
if all the numbers are different from each other (that is, they are distinct).
C-1.16 In our implementation of the scale function (page 25), the body of the loop
executes the command data[j] = factor. We have discussed that numeric
types are immutable, and that use of the = operator in this context causes
the creation of a new instance (not the mutation of an existing instance).
How is it still possible, then, that our implementation of scale changes the
actual parameter sent by the caller?
C-1.17 Had we implemented the scale function (page 25) as follows, does it work
properly?
def scale(data, factor):
for val in data:
val = factor
Explain why or why not.
C-1.18 Demonstrate how to use Python’s list comprehension syntax to produce
the list [0, 2, 6, 12, 20, 30, 42, 56, 72, 90].
C-1.19 Demonstrate how to use Python’s list comprehension syntax to produce
the list [ a , b , c , ..., z ], but without having to type all 26 such
characters literally.
C-1.20 Python’s random module includes a function shuffle(data) that accepts a
list of elements and randomly reorders the elements so that each possi-
ble order occurs with equal probability. The random module includes a
more basic function randint(a, b) that returns a uniformly random integer
from a to b (including both endpoints). Using only the randint function,
implement your own version of the shuffle function.
C-1.21 Write a Python program that repeatedly reads lines from standard input
until an EOFError is raised, and then outputs those lines in reverse order
(a user can indicate end of input by typing ctrl-D).
1.12. Exercises 53
C-1.22 Write a short Python program that takes two arrays a and b of length n
storing int values, and returns the dot product of a and b. That is, it returns
an array c of length n such that c[i] = a[i] · b[i], for i = 0, . . . , n − 1.
C-1.23 Give an example of a Python code fragment that attempts to write an ele-
ment to a list based on an index that may be out of bounds. If that index
is out of bounds, the program should catch the exception that results, and
print the following error message:
“Don’t try buffer overflow attacks in Python!”
C-1.24 Write a short Python function that counts the number of vowels in a given
character string.
C-1.25 Write a short Python function that takes a string s, representing a sentence,
and returns a copy of the string with all punctuation removed. For exam-
ple, if given the string "Let s try, Mike.", this function would return
"Lets try Mike".
C-1.26 Write a short program that takes as input three integers, a, b, and c, from
the console and determines if they can be used in a correct arithmetic
formula (in the given order), like “a + b = c,” “a = b − c,” or “a ∗ b = c.”
C-1.27 In Section 1.8, we provided three different implementations of a generator
that computes factors of a given integer. The third of those implementa-
tions, from page 41, was the most efficient, but we noted that it did not
yield the factors in increasing order. Modify the generator so that it reports
factors in increasing order, while maintaining its general performance ad-
vantages.
C-1.28 The p-norm of a vector v = (v1, v2, . . . , vn) in n-dimensional space is de-
fined as
‖v‖ = p
√
vp1 + v
p
2 + ··· + v
p
n .
For the special case of p = 2, this results in the traditional Euclidean
norm, which represents the length of the vector. For example, the Eu-
clidean norm of a two-dimensional vector with coordinates (4, 3) has a
Euclidean norm of
√
42 + 32 =
√
16 + 9 =
√
25 = 5. Give an implemen-
tation of a function named norm such that norm(v, p) returns the p-norm
value of v and norm(v) returns the Euclidean norm of v. You may assume
that v is a list of numbers.
54 Chapter 1. Python Primer
Projects
P-1.29 Write a Python program that outputs all possible strings formed by using
the characters c , a , t , d , o , and g exactly once.
P-1.30 Write a Python program that can take a positive integer greater than 2 as
input and write out the number of times one must repeatedly divide this
number by 2 before getting a value less than 2.
P-1.31 Write a Python program that can “make change.” Your program should
take two numbers as input, one that is a monetary amount charged and the
other that is a monetary amount given. It should then return the number
of each kind of bill and coin to give back as change for the difference
between the amount given and the amount charged. The values assigned
to the bills and coins can be based on the monetary system of any current
or former government. Try to design your program so that it returns as
few bills and coins as possible.
P-1.32 Write a Python program that can simulate a simple calculator, using the
console as the exclusive input and output device. That is, each input to the
calculator, be it a number, like 12.34 or 1034, or an operator, like + or =,
can be done on a separate line. After each such input, you should output
to the Python console what would be displayed on your calculator.
P-1.33 Write a Python program that simulates a handheld calculator. Your pro-
gram should process input from the Python console representing buttons
that are “pushed,” and then output the contents of the screen after each op-
eration is performed. Minimally, your calculator should be able to process
the basic arithmetic operations and a reset/clear operation.
P-1.34 A common punishment for school children is to write out a sentence mul-
tiple times. Write a Python stand-alone program that will write out the
following sentence one hundred times: “I will never spam my friends
again.” Your program should number each of the sentences and it should
make eight different random-looking typos.
P-1.35 The birthday paradox says that the probability that two people in a room
will have the same birthday is more than half, provided n, the number of
people in the room, is more than 23. This property is not really a paradox,
but many people find it surprising. Design a Python program that can test
this paradox by a series of experiments on randomly generated birthdays,
which test this paradox for n = 5, 10, 15, 20, . . . , 100.
P-1.36 Write a Python program that inputs a list of words, separated by white-
space, and outputs how many times each word appears in the list. You
need not worry about efficiency at this point, however, as this topic is
something that will be addressed later in this book.
Chapter Notes 55
Chapter Notes
The official Python Web site (http://www.python.org) has a wealth of information, in-
cluding a tutorial and full documentation of the built-in functions, classes, and standard
modules. The Python interpreter is itself a useful reference, as the interactive command
help(foo) provides documentation for any function, class, or module that foo identifies.
Books providing an introduction to programming in Python include titles authored by
Campbell et al. [22], Cedar [25], Dawson [32], Goldwasser and Letscher [43], Lutz [72],
Perkovic [82], and Zelle [105]. More complete reference books on Python include titles by
Beazley [12], and Summerfield [91].
Chapter
2 Object-Oriented Programming
Contents
2.1 Goals, Principles, and Patterns . . . . . . . . . . . . . . . . 57
2.1.1 Object-Oriented Design Goals . . . . . . . . . . . . . . . 57
2.1.2 Object-Oriented Design Principles . . . . . . . . . . . . . 58
2.1.3 Design Patterns . . . . . . . . . . . . . . . . . . . . . . . 61
2.2 Software Development . . . . . . . . . . . . . . . . . . . . 62
2.2.1 Design . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
2.2.2 Pseudo-Code . . . . . . . . . . . . . . . . . . . . . . . . 64
2.2.3 Coding Style and Documentation . . . . . . . . . . . . . . 64
2.2.4 Testing and Debugging . . . . . . . . . . . . . . . . . . . 67
2.3 Class Definitions . . . . . . . . . . . . . . . . . . . . . . . . 69
2.3.1 Example: CreditCard Class . . . . . . . . . . . . . . . . . 69
2.3.2 Operator Overloading and Python’s Special Methods . . . 74
2.3.3 Example: Multidimensional Vector Class . . . . . . . . . . 77
2.3.4 Iterators . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
2.3.5 Example: Range Class . . . . . . . . . . . . . . . . . . . . 80
2.4 Inheritance . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
2.4.1 Extending the CreditCard Class . . . . . . . . . . . . . . . 83
2.4.2 Hierarchy of Numeric Progressions . . . . . . . . . . . . . 87
2.4.3 Abstract Base Classes . . . . . . . . . . . . . . . . . . . . 93
2.5 Namespaces and Object-Orientation . . . . . . . . . . . . . 96
2.5.1 Instance and Class Namespaces . . . . . . . . . . . . . . . 96
2.5.2 Name Resolution and Dynamic Dispatch . . . . . . . . . . 100
2.6 Shallow and Deep Copying . . . . . . . . . . . . . . . . . . 101
2.7 Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103
2.1. Goals, Principles, and Patterns 57
2.1 Goals, Principles, and Patterns
As the name implies, the main “actors” in the object-oriented paradigm are called
objects. Each object is an instance of a class. Each class presents to the outside
world a concise and consistent view of the objects that are instances of this class,
without going into too much unnecessary detail or giving others access to the inner
workings of the objects. The class definition typically specifies instance variables,
also known as data members, that the object contains, as well as the methods, also
known as member functions, that the object can execute. This view of computing
is intended to fulfill several goals and incorporate several design principles, which
we discuss in this chapter.
2.1.1 Object-Oriented Design Goals
Software implementations should achieve robustness, adaptability, and reusabil-
ity. (See Figure 2.1.)
Robustness Adaptability Reusability
Figure 2.1: Goals of object-oriented design.
Robustness
Every good programmer wants to develop software that is correct, which means that
a program produces the right output for all the anticipated inputs in the program’s
application. In addition, we want software to be robust, that is, capable of handling
unexpected inputs that are not explicitly defined for its application. For example,
if a program is expecting a positive integer (perhaps representing the price of an
item) and instead is given a negative integer, then the program should be able to
recover gracefully from this error. More importantly, in life-critical applications,
where a software error can lead to injury or loss of life, software that is not robust
could be deadly. This point was driven home in the late 1980s in accidents involv-
ing Therac-25, a radiation-therapy machine, which severely overdosed six patients
between 1985 and 1987, some of whom died from complications resulting from
their radiation overdose. All six accidents were traced to software errors.
58 Chapter 2. Object-Oriented Programming
Adaptability
Modern software applications, such as Web browsers and Internet search engines,
typically involve large programs that are used for many years. Software, there-
fore, needs to be able to evolve over time in response to changing conditions in its
environment. Thus, another important goal of quality software is that it achieves
adaptability (also called evolvability). Related to this concept is portability, which
is the ability of software to run with minimal change on different hardware and
operating system platforms. An advantage of writing software in Python is the
portability provided by the language itself.
Reusability
Going hand in hand with adaptability is the desire that software be reusable, that
is, the same code should be usable as a component of different systems in various
applications. Developing quality software can be an expensive enterprise, and its
cost can be offset somewhat if the software is designed in a way that makes it easily
reusable in future applications. Such reuse should be done with care, however, for
one of the major sources of software errors in the Therac-25 came from inappropri-
ate reuse of Therac-20 software (which was not object-oriented and not designed
for the hardware platform used with the Therac-25).
2.1.2 Object-Oriented Design Principles
Chief among the principles of the object-oriented approach, which are intended to
facilitate the goals outlined above, are the following (see Figure 2.2):
• Modularity
• Abstraction
• Encapsulation
Modularity Abstraction Encapsulation
Figure 2.2: Principles of object-oriented design.
2.1. Goals, Principles, and Patterns 59
Modularity
Modern software systems typically consist of several different components that
must interact correctly in order for the entire system to work properly. Keeping
these interactions straight requires that these different components be well orga-
nized. Modularity refers to an organizing principle in which different components
of a software system are divided into separate functional units.
As a real-world analogy, a house or apartment can be viewed as consisting of
several interacting units: electrical, heating and cooling, plumbing, and structural.
Rather than viewing these systems as one giant jumble of wires, vents, pipes, and
boards, the organized architect designing a house or apartment will view them as
separate modules that interact in well-defined ways. In so doing, he or she is using
modularity to bring a clarity of thought that provides a natural way of organizing
functions into distinct manageable units.
In like manner, using modularity in a software system can also provide a pow-
erful organizing framework that brings clarity to an implementation. In Python,
we have already seen that a module is a collection of closely related functions and
classes that are defined together in a single file of source code. Python’s standard
libraries include, for example, the math module, which provides definitions for key
mathematical constants and functions, and the os module, which provides support
for interacting with the operating system.
The use of modularity helps support the goals listed in Section 2.1.1. Robust-
ness is greatly increased because it is easier to test and debug separate components
before they are integrated into a larger software system. Furthermore, bugs that per-
sist in a complete system might be traced to a particular component, which can be
fixed in relative isolation. The structure imposed by modularity also helps enable
software reusability. If software modules are written in a general way, the modules
can be reused when related need arises in other contexts. This is particularly rel-
evant in a study of data structures, which can typically be designed with sufficient
abstraction and generality to be reused in many applications.
Abstraction
The notion of abstraction is to distill a complicated system down to its most funda-
mental parts. Typically, describing the parts of a system involves naming them and
explaining their functionality. Applying the abstraction paradigm to the design of
data structures gives rise to abstract data types (ADTs). An ADT is a mathematical
model of a data structure that specifies the type of data stored, the operations sup-
ported on them, and the types of parameters of the operations. An ADT specifies
what each operation does, but not how it does it. We will typically refer to the
collective set of behaviors supported by an ADT as its public interface.
60 Chapter 2. Object-Oriented Programming
As a programming language, Python provides a great deal of latitude in regard
to the specification of an interface. Python has a tradition of treating abstractions
implicitly using a mechanism known as duck typing. As an interpreted and dy-
namically typed language, there is no “compile time” checking of data types in
Python, and no formal requirement for declarations of abstract base classes. In-
stead programmers assume that an object supports a set of known behaviors, with
the interpreter raising a run-time error if those assumptions fail. The description
of this as “duck typing” comes from an adage attributed to poet James Whitcomb
Riley, stating that “when I see a bird that walks like a duck and swims like a duck
and quacks like a duck, I call that bird a duck.”
More formally, Python supports abstract data types using a mechanism known
as an abstract base class (ABC). An abstract base class cannot be instantiated
(i.e., you cannot directly create an instance of that class), but it defines one or more
common methods that all implementations of the abstraction must have. An ABC
is realized by one or more concrete classes that inherit from the abstract base class
while providing implementations for those method declared by the ABC. Python’s
abc module provides formal support for ABCs, although we omit such declarations
for simplicity. We will make use of several existing abstract base classes coming
from Python’s collections module, which includes definitions for several common
data structure ADTs, and concrete implementations of some of those abstractions.
Encapsulation
Another important principle of object-oriented design is encapsulation. Different
components of a software system should not reveal the internal details of their
respective implementations. One of the main advantages of encapsulation is that it
gives one programmer freedom to implement the details of a component, without
concern that other programmers will be writing code that intricately depends on
those internal decisions. The only constraint on the programmer of a component
is to maintain the public interface for the component, as other programmers will
be writing code that depends on that interface. Encapsulation yields robustness
and adaptability, for it allows the implementation details of parts of a program to
change without adversely affecting other parts, thereby making it easier to fix bugs
or add new functionality with relatively local changes to a component.
Throughout this book, we will adhere to the principle of encapsulation, making
clear which aspects of a data structure are assumed to be public and which are
assumed to be internal details. With that said, Python provides only loose support
for encapsulation. By convention, names of members of a class (both data members
and member functions) that start with a single underscore character (e.g., secret)
are assumed to be nonpublic and should not be relied upon. Those conventions
are reinforced by the intentional omission of those members from automatically
generated documentation.
2.1. Goals, Principles, and Patterns 61
2.1.3 Design Patterns
Object-oriented design facilitates reusable, robust, and adaptable software. De-
signing good code takes more than simply understanding object-oriented method-
ologies, however. It requires the effective use of object-oriented design techniques.
Computing researchers and practitioners have developed a variety of organiza-
tional concepts and methodologies for designing quality object-oriented software
that is concise, correct, and reusable. Of special relevance to this book is the con-
cept of a design pattern, which describes a solution to a “typical” software design
problem. A pattern provides a general template for a solution that can be applied in
many different situations. It describes the main elements of a solution in an abstract
way that can be specialized for a specific problem at hand. It consists of a name,
which identifies the pattern; a context, which describes the scenarios for which this
pattern can be applied; a template, which describes how the pattern is applied; and
a result, which describes and analyzes what the pattern produces.
We present several design patterns in this book, and we show how they can be
consistently applied to implementations of data structures and algorithms. These
design patterns fall into two groups—patterns for solving algorithm design prob-
lems and patterns for solving software engineering problems. The algorithm design
patterns we discuss include the following:
• Recursion (Chapter 4)
• Amortization (Sections 5.3 and 11.4)
• Divide-and-conquer (Section 12.2.1)
• Prune-and-search, also known as decrease-and-conquer (Section 12.7.1)
• Brute force (Section 13.2.1)
• Dynamic programming (Section 13.3).
• The greedy method (Sections 13.4.2, 14.6.2, and 14.7)
Likewise, the software engineering design patterns we discuss include:
• Iterator (Sections 1.8 and 2.3.4)
• Adapter (Section 6.1.2)
• Position (Sections 7.4 and 8.1.2)
• Composition (Sections 7.6.1, 9.2.1, and 10.1.4)
• Template method (Sections 2.4.3, 8.4.6, 10.1.3, 10.5.2, and 11.2.1)
• Locator (Section 9.5.1)
• Factory method (Section 11.2.1)
Rather than explain each of these concepts here, however, we introduce them
throughout the text as noted above. For each pattern, be it for algorithm engineering
or software engineering, we explain its general use and we illustrate it with at least
one concrete example.
62 Chapter 2. Object-Oriented Programming
2.2 Software Development
Traditional software development involves several phases. Three major steps are:
1. Design
2. Implementation
3. Testing and Debugging
In this section, we briefly discuss the role of these phases, and we introduce sev-
eral good practices for programming in Python, including coding style, naming
conventions, formal documentation, and unit testing.
2.2.1 Design
For object-oriented programming, the design step is perhaps the most important
phase in the process of developing software. For it is in the design step that we
decide how to divide the workings of our program into classes, we decide how
these classes will interact, what data each will store, and what actions each will
perform. Indeed, one of the main challenges that beginning programmers face is
deciding what classes to define to do the work of their program. While general
prescriptions are hard to come by, there are some rules of thumb that we can apply
when determining how to design our classes:
• Responsibilities: Divide the work into different actors, each with a different
responsibility. Try to describe responsibilities using action verbs. These
actors will form the classes for the program.
• Independence: Define the work for each class to be as independent from
other classes as possible. Subdivide responsibilities between classes so that
each class has autonomy over some aspect of the program. Give data (as in-
stance variables) to the class that has jurisdiction over the actions that require
access to this data.
• Behaviors: Define the behaviors for each class carefully and precisely, so
that the consequences of each action performed by a class will be well un-
derstood by other classes that interact with it. These behaviors will define
the methods that this class performs, and the set of behaviors for a class are
the interface to the class, as these form the means for other pieces of code to
interact with objects from the class.
Defining the classes, together with their instance variables and methods, are key
to the design of an object-oriented program. A good programmer will naturally
develop greater skill in performing these tasks over time, as experience teaches
him or her to notice patterns in the requirements of a program that match patterns
that he or she has seen before.
2.2. Software Development 63
A common tool for developing an initial high-level design for a project is the
use of CRC cards. Class-Responsibility-Collaborator (CRC) cards are simple in-
dex cards that subdivide the work required of a program. The main idea behind this
tool is to have each card represent a component, which will ultimately become a
class in the program. We write the name of each component on the top of an index
card. On the left-hand side of the card, we begin writing the responsibilities for
this component. On the right-hand side, we list the collaborators for this compo-
nent, that is, the other components that this component will have to interact with to
perform its duties.
The design process iterates through an action/actor cycle, where we first iden-
tify an action (that is, a responsibility), and we then determine an actor (that is, a
component) that is best suited to perform that action. The design is complete when
we have assigned all actions to actors. In using index cards for this process (rather
than larger pieces of paper), we are relying on the fact that each component should
have a small set of responsibilities and collaborators. Enforcing this rule helps keep
the individual classes manageable.
As the design takes form, a standard approach to explain and document the
design is the use of UML (Unified Modeling Language) diagrams to express the
organization of a program. UML diagrams are a standard visual notation to express
object-oriented software designs. Several computer-aided tools are available to
build UML diagrams. One type of UML figure is known as a class diagram. An
example of such a diagram is given in Figure 2.3, for a class that represents a
consumer credit card. The diagram has three portions, with the first designating
the name of the class, the second designating the recommended instance variables,
and the third designating the recommended methods of the class. In Section 2.2.3,
we discuss our naming conventions, and in Section 2.3.1, we provide a complete
implementation of a Python CreditCard class based on this design.
Class:
Fields:
Behaviors:
make payment(amount)
customer
account
get customer( )
get bank( )
bank
get account( )
balance
limit
get balance( )
get limit( )
CreditCard
charge(price)
Figure 2.3: Class diagram for a proposed CreditCard class.
64 Chapter 2. Object-Oriented Programming
2.2.2 Pseudo-Code
As an intermediate step before the implementation of a design, programmers are
often asked to describe algorithms in a way that is intended for human eyes only.
Such descriptions are called pseudo-code. Pseudo-code is not a computer program,
but is more structured than usual prose. It is a mixture of natural language and
high-level programming constructs that describe the main ideas behind a generic
implementation of a data structure or algorithm. Because pseudo-code is designed
for a human reader, not a computer, we can communicate high-level ideas, without
being burdened with low-level implementation details. At the same time, we should
not gloss over important steps. Like many forms of human communication, finding
the right balance is an important skill that is refined through practice.
In this book, we rely on a pseudo-code style that we hope will be evident to
Python programmers, yet with a mix of mathematical notations and English prose.
For example, we might use the phrase “indicate an error” rather than a formal raise
statement. Following conventions of Python, we rely on indentation to indicate
the extent of control structures and on an indexing notation in which entries of a
sequence A with length n are indexed from A[0] to A[n − 1]. However, we choose
to enclose comments within curly braces { like these } in our pseudo-code, rather
than using Python’s # character.
2.2.3 Coding Style and Documentation
Programs should be made easy to read and understand. Good programmers should
therefore be mindful of their coding style, and develop a style that communicates
the important aspects of a program’s design for both humans and computers. Con-
ventions for coding style tend to vary between different programming communities.
The official Style Guide for Python Code is available online at
http://www.python.org/dev/peps/pep-0008/
The main principles that we adopt are as follows:
• Python code blocks are typically indented by 4 spaces. However, to avoid
having our code fragments overrun the book’s margins, we use 2 spaces for
each level of indentation. It is strongly recommended that tabs be avoided, as
tabs are displayed with differing widths across systems, and tabs and spaces
are not viewed as identical by the Python interpreter. Many Python-aware
editors will automatically replace tabs with an appropriate number of spaces.
2.2. Software Development 65
• Use meaningful names for identifiers. Try to choose names that can be read
aloud, and choose names that reflect the action, responsibility, or data each
identifier is naming.
◦ Classes (other than Python’s built-in classes) should have a name that
serves as a singular noun, and should be capitalized (e.g., Date rather
than date or Dates). When multiple words are concatenated to form a
class name, they should follow the so-called “CamelCase” convention
in which the first letter of each word is capitalized (e.g., CreditCard).
◦ Functions, including member functions of a class, should be lowercase.
If multiple words are combined, they should be separated by under-
scores (e.g., make payment). The name of a function should typically
be a verb that describes its affect. However, if the only purpose of the
function is to return a value, the function name may be a noun that
describes the value (e.g., sqrt rather than calculate sqrt).
◦ Names that identify an individual object (e.g., a parameter, instance
variable, or local variable) should be a lowercase noun (e.g., price).
Occasionally, we stray from this rule when using a single uppercase
letter to designate the name of a data structures (such as tree T).
◦ Identifiers that represent a value considered to be a constant are tradi-
tionally identified using all capital letters and with underscores to sep-
arate words (e.g., MAX SIZE).
Recall from our discussion of encapsulation that identifiers in any context
that begin with a single leading underscore (e.g., secret) are intended to
suggest that they are only for “internal” use to a class or module, and not part
of a public interface.
• Use comments that add meaning to a program and explain ambiguous or
confusing constructs. In-line comments are good for quick explanations;
they are indicated in Python following the # character, as in
if n % 2 == 1: # n is odd
Multiline block comments are good for explaining more complex code sec-
tions. In Python, these are technically multiline string literals, typically de-
limited with triple quotes (”””), which have no effect when executed. In the
next section, we discuss the use of block comments for documentation.
66 Chapter 2. Object-Oriented Programming
Documentation
Python provides integrated support for embedding formal documentation directly
in source code using a mechanism known as a docstring. Formally, any string literal
that appears as the first statement within the body of a module, class, or function
(including a member function of a class) will be considered to be a docstring. By
convention, those string literals should be delimited within triple quotes (”””). As
an example, our version of the scale function from page 25 could be documented
as follows:
def scale(data, factor):
”””Multiply all entries of numeric data list by the given factor.”””
for j in range(len(data)):
data[j] = factor
It is common to use the triple-quoted string delimiter for a docstring, even when
the string fits on a single line, as in the above example. More detailed docstrings
should begin with a single line that summarizes the purpose, followed by a blank
line, and then further details. For example, we might more clearly document the
scale function as follows:
def scale(data, factor):
”””Multiply all entries of numeric data list by the given factor.
data an instance of any mutable sequence type (such as a list)
containing numeric elements
factor a number that serves as the multiplicative factor for scaling
”””
for j in range(len(data)):
data[j] = factor
A docstring is stored as a field of the module, function, or class in which it
is declared. It serves as documentation and can be retrieved in a variety of ways.
For example, the command help(x), within the Python interpreter, produces the
documentation associated with the identified object x. An external tool named
pydoc is distributed with Python and can be used to generate formal documentation
as text or as a Web page. Guidelines for authoring useful docstrings are available
at:
http://www.python.org/dev/peps/pep-0257/
In this book, we will try to present docstrings when space allows. Omitted
docstrings can be found in the online version of our source code.
2.2. Software Development 67
2.2.4 Testing and Debugging
Testing is the process of experimentally checking the correctness of a program,
while debugging is the process of tracking the execution of a program and discov-
ering the errors in it. Testing and debugging are often the most time-consuming
activity in the development of a program.
Testing
A careful testing plan is an essential part of writing a program. While verifying the
correctness of a program over all possible inputs is usually infeasible, we should
aim at executing the program on a representative subset of inputs. At the very
minimum, we should make sure that every method of a class is tested at least once
(method coverage). Even better, each code statement in the program should be
executed at least once (statement coverage).
Programs often tend to fail on special cases of the input. Such cases need to be
carefully identified and tested. For example, when testing a method that sorts (that
is, puts in order) a sequence of integers, we should consider the following inputs:
• The sequence has zero length (no elements).
• The sequence has one element.
• All the elements of the sequence are the same.
• The sequence is already sorted.
• The sequence is reverse sorted.
In addition to special inputs to the program, we should also consider special
conditions for the structures used by the program. For example, if we use a Python
list to store data, we should make sure that boundary cases, such as inserting or
removing at the beginning or end of the list, are properly handled.
While it is essential to use handcrafted test suites, it is also advantageous to
run the program on a large collection of randomly generated inputs. The random
module in Python provides several means for generating random numbers, or for
randomizing the order of collections.
The dependencies among the classes and functions of a program induce a hi-
erarchy. Namely, a component A is above a component B in the hierarchy if A
depends upon B, such as when function A calls function B, or function A relies on
a parameter that is an instance of class B. There are two main testing strategies,
top-down and bottom-up, which differ in the order in which components are tested.
Top-down testing proceeds from the top to the bottom of the program hierarchy.
It is typically used in conjunction with stubbing, a boot-strapping technique that
replaces a lower-level component with a stub, a replacement for the component
that simulates the functionality of the original. For example, if function A calls
function B to get the first line of a file, when testing A we can replace B with a stub
that returns a fixed string.
68 Chapter 2. Object-Oriented Programming
Bottom-up testing proceeds from lower-level components to higher-level com-
ponents. For example, bottom-level functions, which do not invoke other functions,
are tested first, followed by functions that call only bottom-level functions, and so
on. Similarly a class that does not depend upon any other classes can be tested
before another class that depends on the former. This form of testing is usually
described as unit testing, as the functionality of a specific component is tested in
isolation of the larger software project. If used properly, this strategy better isolates
the cause of errors to the component being tested, as lower-level components upon
which it relies should have already been thoroughly tested.
Python provides several forms of support for automated testing. When func-
tions or classes are defined in a module, testing for that module can be embedded
in the same file. The mechanism for doing so was described in Section 1.11. Code
that is shielded in a conditional construct of the form
if name == __main__ :
# perform tests...
will be executed when Python is invoked directly on that module, but not when the
module is imported for use in a larger software project. It is common to put tests
in such a construct to test the functionality of the functions and classes specifically
defined in that module.
More robust support for automation of unit testing is provided by Python’s
unittest module. This framework allows the grouping of individual test cases into
larger test suites, and provides support for executing those suites, and reporting or
analyzing the results of those tests. As software is maintained, the act of regression
testing is used, whereby all previous tests are re-executed to ensure that changes to
the software do not introduce new bugs in previously tested components.
Debugging
The simplest debugging technique consists of using print statements to track the
values of variables during the execution of the program. A problem with this ap-
proach is that eventually the print statements need to be removed or commented
out, so they are not executed when the software is finally released.
A better approach is to run the program within a debugger, which is a special-
ized environment for controlling and monitoring the execution of a program. The
basic functionality provided by a debugger is the insertion of breakpoints within
the code. When the program is executed within the debugger, it stops at each
breakpoint. While the program is stopped, the current value of variables can be
inspected.
The standard Python distribution includes a module named pdb, which provides
debugging support directly within the interpreter. Most IDEs for Python, such as
IDLE, provide debugging environments with graphical user interfaces.
2.3. Class Definitions 69
2.3 Class Definitions
A class serves as the primary means for abstraction in object-oriented program-
ming. In Python, every piece of data is represented as an instance of some class.
A class provides a set of behaviors in the form of member functions (also known
as methods), with implementations that are common to all instances of that class.
A class also serves as a blueprint for its instances, effectively determining the way
that state information for each instance is represented in the form of attributes (also
known as fields, instance variables, or data members).
2.3.1 Example: CreditCard Class
As a first example, we provide an implementation of a CreditCard class based on
the design we introduced in Figure 2.3 of Section 2.2.1. The instances defined by
the CreditCard class provide a simple model for traditional credit cards. They have
identifying information about the customer, bank, account number, credit limit, and
current balance. The class restricts charges that would cause a card’s balance to go
over its spending limit, but it does not charge interest or late payments (we revisit
such themes in Section 2.4.1).
Our code begins in Code Fragment 2.1 and continues in Code Fragment 2.2.
The construct begins with the keyword, class, followed by the name of the class, a
colon, and then an indented block of code that serves as the body of the class. The
body includes definitions for all methods of the class. These methods are defined as
functions, using techniques introduced in Section 1.5, yet with a special parameter,
named self, that serves to identify the particular instance upon which a member is
invoked.
The self Identifier
In Python, the self identifier plays a key role. In the context of the CreditCard
class, there can presumably be many different CreditCard instances, and each must
maintain its own balance, its own credit limit, and so on. Therefore, each instance
stores its own instance variables to reflect its current state.
Syntactically, self identifies the instance upon which a method is invoked. For
example, assume that a user of our class has a variable, my card, that identifies
an instance of the CreditCard class. When the user calls my card.get balance( ),
identifier self, within the definition of the get balance method, refers to the card
known as my card by the caller. The expression, self. balance refers to an instance
variable, named balance, stored as part of that particular credit card’s state.
70 Chapter 2. Object-Oriented Programming
1 class CreditCard:
2 ”””A consumer credit card.”””
3
4 def init (self, customer, bank, acnt, limit):
5 ”””Create a new credit card instance.
6
7 The initial balance is zero.
8
9 customer the name of the customer (e.g., John Bowman )
10 bank the name of the bank (e.g., California Savings )
11 acnt the acount identifier (e.g., 5391 0375 9387 5309 )
12 limit credit limit (measured in dollars)
13 ”””
14 self. customer = customer
15 self. bank = bank
16 self. account = acnt
17 self. limit = limit
18 self. balance = 0
19
20 def get customer(self):
21 ”””Return name of the customer.”””
22 return self. customer
23
24 def get bank(self):
25 ”””Return the bank s name.”””
26 return self. bank
27
28 def get account(self):
29 ”””Return the card identifying number (typically stored as a string).”””
30 return self. account
31
32 def get limit(self):
33 ”””Return current credit limit.”””
34 return self. limit
35
36 def get balance(self):
37 ”””Return current balance.”””
38 return self. balance
Code Fragment 2.1: The beginning of the CreditCard class definition (continued in
Code Fragment 2.2).
2.3. Class Definitions 71
39 def charge(self, price):
40 ”””Charge given price to the card, assuming sufficient credit limit.
41
42 Return True if charge was processed; False if charge was denied.
43 ”””
44 if price + self. balance > self. limit: # if charge would exceed limit,
45 return False # cannot accept charge
46 else:
47 self. balance += price
48 return True
49
50 def make payment(self, amount):
51 ”””Process customer payment that reduces balance.”””
52 self. balance −= amount
Code Fragment 2.2: The conclusion of the CreditCard class definition (continued
from Code Fragment 2.1). These methods are indented within the class definition.
We draw attention to the difference between the method signature as declared
within the class versus that used by a caller. For example, from a user’s perspec-
tive we have seen that the get balance method takes zero parameters, yet within
the class definition, self is an explicit parameter. Likewise, the charge method is
declared within the class having two parameters (self and price), even though this
method is called with one parameter, for example, as my card.charge(200). The
interpretter automatically binds the instance upon which the method is invoked to
the self parameter.
The Constructor
A user can create an instance of the CreditCard class using a syntax as:
cc = CreditCard( John Doe, 1st Bank , 5391 0375 9387 5309 , 1000)
Internally, this results in a call to the specially named init method that serves
as the constructor of the class. Its primary responsibility is to establish the state of
a newly created credit card object with appropriate instance variables. In the case
of the CreditCard class, each object maintains five instance variables, which we
name: customer, bank, account, limit, and balance. The initial values for the
first four of those five are provided as explicit parameters that are sent by the user
when instantiating the credit card, and assigned within the body of the construc-
tor. For example, the command, self. customer = customer, assigns the instance
variable self. customer to the parameter customer; note that because customer is
unqualified on the right-hand side, it refers to the parameter in the local namespace.
72 Chapter 2. Object-Oriented Programming
Encapsulation
By the conventions described in Section 2.2.3, a single leading underscore in the
name of a data member, such as balance, implies that it is intended as nonpublic.
Users of a class should not directly access such members.
As a general rule, we will treat all data members as nonpublic. This allows
us to better enforce a consistent state for all instances. We can provide accessors,
such as get balance, to provide a user of our class read-only access to a trait. If
we wish to allow the user to change the state, we can provide appropriate update
methods. In the context of data structures, encapsulating the internal representation
allows us greater flexibility to redesign the way a class works, perhaps to improve
the efficiency of the structure.
Additional Methods
The most interesting behaviors in our class are charge and make payment. The
charge function typically adds the given price to the credit card balance, to reflect
a purchase of said price by the customer. However, before accepting the charge,
our implementation verifies that the new purchase would not cause the balance to
exceed the credit limit. The make payment charge reflects the customer sending
payment to the bank for the given amount, thereby reducing the balance on the
card. We note that in the command, self. balance −= amount, the expression
self. balance is qualified with the self identifier because it represents an instance
variable of the card, while the unqualified amount represents the local parameter.
Error Checking
Our implementation of the CreditCard class is not particularly robust. First, we
note that we did not explicitly check the types of the parameters to charge and
make payment, nor any of the parameters to the constructor. If a user were to make
a call such as visa.charge( candy ), our code would presumably crash when at-
tempting to add that parameter to the current balance. If this class were to be widely
used in a library, we might use more rigorous techniques to raise a TypeError when
facing such misuse (see Section 1.7).
Beyond the obvious type errors, our implementation may be susceptible to log-
ical errors. For example, if a user were allowed to charge a negative price, such
as visa.charge(−300), that would serve to lower the customer’s balance. This pro-
vides a loophole for lowering a balance without making a payment. Of course,
this might be considered valid usage if modeling the credit received when a cus-
tomer returns merchandise to a store. We will explore some such issues with the
CreditCard class in the end-of-chapter exercises.
2.3. Class Definitions 73
Testing the Class
In Code Fragment 2.3, we demonstrate some basic usage of the CreditCard class,
inserting three cards into a list named wallet. We use loops to make some charges
and payments, and use various accessors to print results to the console.
These tests are enclosed within a conditional, if name == __main__ :,
so that they can be embedded in the source code with the class definition. Using
the terminology of Section 2.2.4, these tests provide method coverage, as each of
the methods is called at least once, but it does not provide statement coverage, as
there is never a case in which a charge is rejected due to the credit limit. This
is not a particular advanced from of testing as the output of the given tests must
be manually audited in order to determine whether the class behaved as expected.
Python has tools for more formal testing (see discussion of the unittest module
in Section 2.2.4), so that resulting values can be automatically compared to the
predicted outcomes, with output generated only when an error is detected.
53 if name == __main__ :
54 wallet = [ ]
55 wallet.append(CreditCard( John Bowman , California Savings ,
56 5391 0375 9387 5309 , 2500) )
57 wallet.append(CreditCard( John Bowman , California Federal ,
58 3485 0399 3395 1954 , 3500) )
59 wallet.append(CreditCard( John Bowman , California Finance ,
60 5391 0375 9387 5309 , 5000) )
61
62 for val in range(1, 17):
63 wallet[0].charge(val)
64 wallet[1].charge(2 val)
65 wallet[2].charge(3 val)
66
67 for c in range(3):
68 print( Customer = , wallet[c].get customer( ))
69 print( Bank = , wallet[c].get bank( ))
70 print( Account = , wallet[c].get account( ))
71 print( Limit = , wallet[c].get limit( ))
72 print( Balance = , wallet[c].get balance( ))
73 while wallet[c].get balance( ) > 100:
74 wallet[c].make payment(100)
75 print( New balance = , wallet[c].get balance( ))
76 print( )
Code Fragment 2.3: Testing the CreditCard class.
74 Chapter 2. Object-Oriented Programming
2.3.2 Operator Overloading and Python’s Special Methods
Python’s built-in classes provide natural semantics for many operators. For ex-
ample, the syntax a + b invokes addition for numeric types, yet concatenation for
sequence types. When defining a new class, we must consider whether a syntax
like a + b should be defined when a or b is an instance of that class.
By default, the + operator is undefined for a new class. However, the author
of a class may provide a definition using a technique known as operator overload-
ing. This is done by implementing a specially named method. In particular, the
+ operator is overloaded by implementing a method named add , which takes
the right-hand operand as a parameter and which returns the result of the expres-
sion. That is, the syntax, a + b, is converted to a method call on object a of the
form, a. add (b). Similar specially named methods exist for other operators.
Table 2.1 provides a comprehensive list of such methods.
When a binary operator is applied to two instances of different types, as in
3 love me , Python gives deference to the class of the left operand. In this
example, it would effectively check if the int class provides a sufficient definition
for how to multiply an instance by a string, via the mul method. However,
if that class does not implement such a behavior, Python checks the class defini-
tion for the right-hand operand, in the form of a special method named rmul
(i.e., “right multiply”). This provides a way for a new user-defined class to support
mixed operations that involve an instance of an existing class (given that the exist-
ing class would presumably not have defined a behavior involving this new class).
The distinction between mul and rmul also allows a class to define dif-
ferent semantics in cases, such as matrix multiplication, in which an operation is
noncommutative (that is, A x may differ from x A).
Non-Operator Overloads
In addition to traditional operator overloading, Python relies on specially named
methods to control the behavior of various other functionality, when applied to
user-defined classes. For example, the syntax, str(foo), is formally a call to the
constructor for the string class. Of course, if the parameter is an instance of a user-
defined class, the original authors of the string class could not have known how
that instance should be portrayed. So the string constructor calls a specially named
method, foo. str ( ), that must return an appropriate string representation.
Similar special methods are used to determine how to construct an int, float, or
bool based on a parameter from a user-defined class. The conversion to a Boolean
value is particularly important, because the syntax, if foo:, can be used even when
foo is not formally a Boolean value (see Section 1.4.1). For a user-defined class,
that condition is evaluated by the special method foo. bool ( ).
2.3. Class Definitions 75
Common Syntax Special Method Form
a + b a. add (b); alternatively b. radd (a)
a − b a. sub (b); alternatively b. rsub (a)
a b a. mul (b); alternatively b. rmul (a)
a / b a. truediv (b); alternatively b. rtruediv (a)
a // b a. floordiv (b); alternatively b. rfloordiv (a)
a % b a. mod (b); alternatively b. rmod (a)
a b a. pow (b); alternatively b. rpow (a)
a << b a. lshift (b); alternatively b. rlshift (a)
a >> b a. rshift (b); alternatively b. rrshift (a)
a & b a. and (b); alternatively b. rand (a)
a ˆ b a. xor (b); alternatively b. rxor (a)
a | b a. or (b); alternatively b. ror (a)
a += b a. iadd (b)
a −= b a. isub (b)
a = b a. imul (b)
. . . . . .
+a a. pos ( )
−a a. neg ( )
˜a a. invert ( )
abs(a) a. abs ( )
a < b a. lt (b)
a <= b a. le (b)
a > b a. gt (b)
a >= b a. ge (b)
a == b a. eq (b)
a != b a. ne (b)
v in a a. contains (v)
a[k] a. getitem (k)
a[k] = v a. setitem (k,v)
del a[k] a. delitem (k)
a(arg1, arg2, …) a. call (arg1, arg2, …)
len(a) a. len ( )
hash(a) a. hash ( )
iter(a) a. iter ( )
next(a) a. next ( )
bool(a) a. bool ( )
float(a) a. float ( )
int(a) a. int ( )
repr(a) a. repr ( )
reversed(a) a. reversed ( )
str(a) a. str ( )
Table 2.1: Overloaded operations, implemented with Python’s special methods.
76 Chapter 2. Object-Oriented Programming
Several other top-level functions rely on calling specially named methods. For
example, the standard way to determine the size of a container type is by calling
the top-level len function. Note well that the calling syntax, len(foo), is not the
traditional method-calling syntax with the dot operator. However, in the case of a
user-defined class, the top-level len function relies on a call to a specially named
len method of that class. That is, the call len(foo) is evaluated through a
method call, foo. len ( ). When developing data structures, we will routinely
define the len method to return a measure of the size of the structure.
Implied Methods
As a general rule, if a particular special method is not implemented in a user-defined
class, the standard syntax that relies upon that method will raise an exception. For
example, evaluating the expression, a + b, for instances of a user-defined class
without add or radd will raise an error.
However, there are some operators that have default definitions provided by
Python, in the absence of special methods, and there are some operators whose
definitions are derived from others. For example, the bool method, which
supports the syntax if foo:, has default semantics so that every object other than
None is evaluated as True. However, for container types, the len method is
typically defined to return the size of the container. If such a method exists, then
the evaluation of bool(foo) is interpreted by default to be True for instances with
nonzero length, and False for instances with zero length, allowing a syntax such as
if waitlist: to be used to test whether there are one or more entries in the waitlist.
In Section 2.3.4, we will discuss Python’s mechanism for providing iterators
for collections via the special method, iter . With that said, if a container class
provides implementations for both len and getitem , a default iteration is
provided automatically (using means we describe in Section 2.3.4). Furthermore,
once an iterator is defined, default functionality of contains is provided.
In Section 1.3 we drew attention to the distinction between expression a is b
and expression a == b, with the former evaluating whether identifiers a and b are
aliases for the same object, and the latter testing a notion of whether the two iden-
tifiers reference equivalent values. The notion of “equivalence” depends upon the
context of the class, and semantics is defined with the eq method. However, if
no implementation is given for eq , the syntax a == b is legal with semantics
of a is b, that is, an instance is equivalent to itself and no others.
We should caution that some natural implications are not automatically pro-
vided by Python. For example, the eq method supports syntax a == b, but
providing that method does not affect the evaluation of syntax a != b. (The ne
method should be provided, typically returning not (a == b) as a result.) Simi-
larly, providing a lt method supports syntax a < b, and indirectly b > a, but
providing both lt and eq does not imply semantics for a <= b.
2.3. Class Definitions 77
2.3.3 Example: Multidimensional Vector Class
To demonstrate the use of operator overloading via special methods, we provide
an implementation of a Vector class, representing the coordinates of a vector in a
multidimensional space. For example, in a three-dimensional space, we might wish
to represent a vector with coordinates 〈5,−2, 3〉. Although it might be tempting to
directly use a Python list to represent those coordinates, a list does not provide an
appropriate abstraction for a geometric vector. In particular, if using lists, the ex-
pression [5, −2, 3] + [1, 4, 2] results in the list [5, −2, 3, 1, 4, 2]. When working
with vectors, if u = 〈5,−2, 3〉 and v = 〈1, 4, 2〉, one would expect the expression,
u + v, to return a three-dimensional vector with coordinates 〈6, 2, 5〉.
We therefore define a Vector class, in Code Fragment 2.4, that provides a better
abstraction for the notion of a geometric vector. Internally, our vector relies upon
an instance of a list, named coords, as its storage mechanism. By keeping the
internal list encapsulated, we can enforce the desired public interface for instances
of our class. A demonstration of supported behaviors includes the following:
v = Vector(5) # construct five-dimensional <0, 0, 0, 0, 0>
v[1] = 23 # <0, 23, 0, 0, 0> (based on use of setitem )
v[−1] = 45 # <0, 23, 0, 0, 45> (also via setitem )
print(v[4]) # print 45 (via getitem )
u = v + v # <0, 46, 0, 0, 90> (via add )
print(u) # print <0, 46, 0, 0, 90>
total = 0
for entry in v: # implicit iteration via len and getitem
total += entry
We implement many of the behaviors by trivially invoking a similar behavior
on the underlying list of coordinates. However, our implementation of add
is customized. Assuming the two operands are vectors with the same length, this
method creates a new vector and sets the coordinates of the new vector to be equal
to the respective sum of the operands’ elements.
It is interesting to note that the class definition, as given in Code Fragment 2.4,
automatically supports the syntax u = v + [5, 3, 10, −2, 1], resulting in a new
vector that is the element-by-element “sum” of the first vector and the list in-
stance. This is a result of Python’s polymorphism. Literally, “polymorphism”
means “many forms.” Although it is tempting to think of the other parameter of
our add method as another Vector instance, we never declared it as such.
Within the body, the only behaviors we rely on for parameter other is that it sup-
ports len(other) and access to other[j]. Therefore, our code executes when the
right-hand operand is a list of numbers (with matching length).
78 Chapter 2. Object-Oriented Programming
1 class Vector:
2 ”””Represent a vector in a multidimensional space.”””
3
4 def init (self, d):
5 ”””Create d-dimensional vector of zeros.”””
6 self. coords = [0] d
7
8 def len (self):
9 ”””Return the dimension of the vector.”””
10 return len(self. coords)
11
12 def getitem (self, j):
13 ”””Return jth coordinate of vector.”””
14 return self. coords[j]
15
16 def setitem (self, j, val):
17 ”””Set jth coordinate of vector to given value.”””
18 self. coords[j] = val
19
20 def add (self, other):
21 ”””Return sum of two vectors.”””
22 if len(self) != len(other): # relies on len method
23 raise ValueError( dimensions must agree )
24 result = Vector(len(self)) # start with vector of zeros
25 for j in range(len(self)):
26 result[j] = self[j] + other[j]
27 return result
28
29 def eq (self, other):
30 ”””Return True if vector has same coordinates as other.”””
31 return self. coords == other. coords
32
33 def ne (self, other):
34 ”””Return True if vector differs from other.”””
35 return not self == other # rely on existing eq definition
36
37 def str (self):
38 ”””Produce string representation of vector.”””
39 return < + str(self. coords)[1:−1] + > # adapt list representation
Code Fragment 2.4: Definition of a simple Vector class.
2.3. Class Definitions 79
2.3.4 Iterators
Iteration is an important concept in the design of data structures. We introduced
Python’s mechanism for iteration in Section 1.8. In short, an iterator for a collec-
tion provides one key behavior: It supports a special method named next that
returns the next element of the collection, if any, or raises a StopIteration exception
to indicate that there are no further elements.
Fortunately, it is rare to have to directly implement an iterator class. Our pre-
ferred approach is the use of the generator syntax (also described in Section 1.8),
which automatically produces an iterator of yielded values.
Python also helps by providing an automatic iterator implementation for any
class that defines both len and getitem . To provide an instructive exam-
ple of a low-level iterator, Code Fragment 2.5 demonstrates just such an iterator
class that works on any collection that supports both len and getitem .
This class can be instantiated as SequenceIterator(data). It operates by keeping an
internal reference to the data sequence, as well as a current index into the sequence.
Each time next is called, the index is incremented, until reaching the end of
the sequence.
1 class SequenceIterator:
2 ”””An iterator for any of Python s sequence types.”””
3
4 def init (self, sequence):
5 ”””Create an iterator for the given sequence.”””
6 self. seq = sequence # keep a reference to the underlying data
7 self. k = −1 # will increment to 0 on first call to next
8
9 def next (self):
10 ”””Return the next element, or else raise StopIteration error.”””
11 self. k += 1 # advance to next index
12 if self. k < len(self. seq):
13 return(self. seq[self. k]) # return the data element
14 else:
15 raise StopIteration( ) # there are no more elements
16
17 def iter (self):
18 ”””By convention, an iterator must return itself as an iterator.”””
19 return self
Code Fragment 2.5: An iterator class for any sequence type.
80 Chapter 2. Object-Oriented Programming
2.3.5 Example: Range Class
As the final example for this section, we develop our own implementation of a
class that mimics Python’s built-in range class. Before introducing our class, we
discuss the history of the built-in version. Prior to Python 3 being released, range
was implemented as a function, and it returned a list instance with elements in
the specified range. For example, range(2, 10, 2) returned the list [2, 4, 6, 8].
However, a typical use of the function was to support a for-loop syntax, such as
for k in range(10000000). Unfortunately, this caused the instantiation and initial-
ization of a list with the range of numbers. That was an unnecessarily expensive
step, in terms of both time and memory usage.
The mechanism used to support ranges in Python 3 is entirely different (to be
fair, the “new” behavior existed in Python 2 under the name xrange). It uses a
strategy known as lazy evaluation. Rather than creating a new list instance, range
is a class that can effectively represent the desired range of elements without ever
storing them explicitly in memory. To better explore the built-in range class, we
recommend that you create an instance as r = range(8, 140, 5). The result is a
relatively lightweight object, an instance of the range class, that has only a few
behaviors. The syntax len(r) will report the number of elements that are in the
given range (27, in our example). A range also supports the getitem method,
so that syntax r[15] reports the sixteenth element in the range (as r[0] is the first
element). Because the class supports both len and getitem , it inherits
automatic support for iteration (see Section 2.3.4), which is why it is possible to
execute a for loop over a range.
At this point, we are ready to demonstrate our own version of such a class. Code
Fragment 2.6 provides a class we name Range (so as to clearly differentiate it from
built-in range). The biggest challenge in the implementation is properly computing
the number of elements that belong in the range, given the parameters sent by the
caller when constructing a range. By computing that value in the constructor, and
storing it as self. length, it becomes trivial to return it from the len method. To
properly implement a call to getitem (k), we simply take the starting value of
the range plus k times the step size (i.e., for k=0, we return the start value). There
are a few subtleties worth examining in the code:
• To properly support optional parameters, we rely on the technique described
on page 27, when discussing a functional version of range.
• We compute the number of elements in the range as
max(0, (stop − start + step − 1) // step)
It is worth testing this formula for both positive and negative step sizes.
• The getitem method properly supports negative indices by converting
an index −k to len(self)−k before computing the result.
2.3. Class Definitions 81
1 class Range:
2 ”””A class that mimic s the built-in range class.”””
3
4 def init (self, start, stop=None, step=1):
5 ”””Initialize a Range instance.
6
7 Semantics is similar to built-in range class.
8 ”””
9 if step == 0:
10 raise ValueError( step cannot be 0 )
11
12 if stop is None: # special case of range(n)
13 start, stop = 0, start # should be treated as if range(0,n)
14
15 # calculate the effective length once
16 self. length = max(0, (stop − start + step − 1) // step)
17
18 # need knowledge of start and step (but not stop) to support getitem
19 self. start = start
20 self. step = step
21
22 def len (self):
23 ”””Return number of entries in the range.”””
24 return self. length
25
26 def getitem (self, k):
27 ”””Return entry at index k (using standard interpretation if negative).”””
28 if k < 0:
29 k += len(self) # attempt to convert negative index
30
31 if not 0 <= k < self. length:
32 raise IndexError( index out of range )
33
34 return self. start + k self. step
Code Fragment 2.6: Our own implementation of a Range class.
82 Chapter 2. Object-Oriented Programming
2.4 Inheritance
A natural way to organize various structural components of a software package
is in a hierarchical fashion, with similar abstract definitions grouped together in
a level-by-level manner that goes from specific to more general as one traverses
up the hierarchy. An example of such a hierarchy is shown in Figure 2.4. Using
mathematical notations, the set of houses is a subset of the set of buildings, but a
superset of the set of ranches. The correspondence between levels is often referred
to as an “is a” relationship, as a house is a building, and a ranch is a house.
Building
Low-rise
Apartment
High-rise
Apartment
Two-story
House
Ranch Skyscraper
Commercial
Building
HouseApartment
Figure 2.4: An example of an “is a” hierarchy involving architectural buildings.
A hierarchical design is useful in software development, as common function-
ality can be grouped at the most general level, thereby promoting reuse of code,
while differentiated behaviors can be viewed as extensions of the general case, In
object-oriented programming, the mechanism for a modular and hierarchical orga-
nization is a technique known as inheritance. This allows a new class to be defined
based upon an existing class as the starting point. In object-oriented terminology,
the existing class is typically described as the base class, parent class, or super-
class, while the newly defined class is known as the subclass or child class.
There are two ways in which a subclass can differentiate itself from its su-
perclass. A subclass may specialize an existing behavior by providing a new im-
plementation that overrides an existing method. A subclass may also extend its
superclass by providing brand new methods.
2.4. Inheritance 83
Python’s Exception Hierarchy
Another example of a rich inheritance hierarchy is the organization of various ex-
ception types in Python. We introduced many of those classes in Section 1.7, but
did not discuss their relationship with each other. Figure 2.5 illustrates a (small)
portion of that hierarchy. The BaseException class is the root of the entire hierar-
chy, while the more specific Exception class includes most of the error types that
we have discussed. Programmers are welcome to define their own special exception
classes to denote errors that may occur in the context of their application. Those
user-defined exception types should be declared as subclasses of Exception.
ValueError
Exception KeyboardInterruptSystemExit
BaseException
IndexError KeyError ZeroDivisionError
LookupError ArithmeticError
Figure 2.5: A portion of Python’s hierarchy of exception types.
2.4.1 Extending the CreditCard Class
To demonstrate the mechanisms for inheritance in Python, we revisit the CreditCard
class of Section 2.3, implementing a subclass that, for lack of a better name, we
name PredatoryCreditCard. The new class will differ from the original in two
ways: (1) if an attempted charge is rejected because it would have exceeded the
credit limit, a $5 fee will be charged, and (2) there will be a mechanism for assess-
ing a monthly interest charge on the outstanding balance, based upon an Annual
Percentage Rate (APR) specified as a constructor parameter.
In accomplishing this goal, we demonstrate the techniques of specialization
and extension. To charge a fee for an invalid charge attempt, we override the
existing charge method, thereby specializing it to provide the new functionality
(although the new version takes advantage of a call to the overridden version). To
provide support for charging interest, we extend the class with a new method named
process month.
84 Chapter 2. Object-Oriented Programming
Class:
Fields:
Behaviors:
Class:
Fields:
Behaviors:
process month( )
apr
customer
account
get customer( )
get bank( )
bank
get account( )
balance
limit
get balance( )
get limit( )
charge(price)
make payment(amount)
PredatoryCreditCard
CreditCard
charge(price)
Figure 2.6: Diagram of an inheritance relationship.
Figure 2.6 provides an overview of our use of inheritance in designing the new
PredatoryCreditCard class, and Code Fragment 2.7 gives a complete Python im-
plementation of that class.
To indicate that the new class inherits from the existing CreditCard class, our
definition begins with the syntax, class PredatoryCreditCard(CreditCard). The
body of the new class provides three member functions: init , charge, and
process month. The init constructor serves a very similar role to the original
CreditCard constructor, except that for our new class, there is an extra parameter
to specify the annual percentage rate. The body of our new constructor relies upon
making a call to the inherited constructor to perform most of the initialization (in
fact, everything other than the recording of the percentage rate). The mechanism
for calling the inherited constructor relies on the syntax, super( ). Specifically, at
line 15 the command
super( ). init (customer, bank, acnt, limit)
calls the init method that was inherited from the CreditCard superclass. Note
well that this method only accepts four parameters. We record the APR value in a
new field named apr.
In similar fashion, our PredatoryCreditCard class provides a new implemen-
tation of the charge method that overrides the inherited method. Yet, our imple-
mentation of the new method relies on a call to the inherited method, with syntax
super( ).charge(price) at line 24. The return value of that call designates whether
2.4. Inheritance 85
1 class PredatoryCreditCard(CreditCard):
2 ”””An extension to CreditCard that compounds interest and fees.”””
3
4 def init (self, customer, bank, acnt, limit, apr):
5 ”””Create a new predatory credit card instance.
6
7 The initial balance is zero.
8
9 customer the name of the customer (e.g., John Bowman )
10 bank the name of the bank (e.g., California Savings )
11 acnt the acount identifier (e.g., 5391 0375 9387 5309 )
12 limit credit limit (measured in dollars)
13 apr annual percentage rate (e.g., 0.0825 for 8.25% APR)
14 ”””
15 super( ). init (customer, bank, acnt, limit) # call super constructor
16 self. apr = apr
17
18 def charge(self, price):
19 ”””Charge given price to the card, assuming sufficient credit limit.
20
21 Return True if charge was processed.
22 Return False and assess 5 fee if charge is denied.
23 ”””
24 success = super( ).charge(price) # call inherited method
25 if not success:
26 self. balance += 5 # assess penalty
27 return success # caller expects return value
28
29 def process month(self):
30 ”””Assess monthly interest on outstanding balance.”””
31 if self. balance > 0:
32 # if positive balance, convert APR to monthly multiplicative factor
33 monthly factor = pow(1 + self. apr, 1/12)
34 self. balance = monthly factor
Code Fragment 2.7: A subclass of CreditCard that assesses interest and fees.
86 Chapter 2. Object-Oriented Programming
the charge was successful. We examine that return value to decide whether to as-
sess a fee, and in turn we return that value to the caller of method, so that the new
version of charge has a similar outward interface as the original.
The process month method is a new behavior, so there is no inherited version
upon which to rely. In our model, this method should be invoked by the bank,
once each month, to add new interest charges to the customer’s balance. The most
challenging aspect in implementing this method is making sure we have working
knowledge of how an annual percentage rate translates to a monthly rate. We do
not simply divide the annual rate by twelve to get a monthly rate (that would be too
predatory, as it would result in a higher APR than advertised). The correct com-
putation is to take the twelfth-root of 1 + self. apr, and use that as a multiplica-
tive factor. For example, if the APR is 0.0825 (representing 8.25%), we compute
12
√
1.0825 ≈ 1.006628, and therefore charge 0.6628% interest per month. In this
way, each $100 of debt will amass $8.25 of compounded interest in a year.
Protected Members
Our PredatoryCreditCard subclass directly accesses the data member self. balance,
which was established by the parent CreditCard class. The underscored name, by
convention, suggests that this is a nonpublic member, so we might ask if it is okay
that we access it in this fashion. While general users of the class should not be
doing so, our subclass has a somewhat privileged relationship with the superclass.
Several object-oriented languages (e.g., Java, C++) draw a distinction for nonpub-
lic members, allowing declarations of protected or private access modes. Members
that are declared as protected are accessible to subclasses, but not to the general
public, while members that are declared as private are not accessible to either. In
this respect, we are using balance as if it were protected (but not private).
Python does not support formal access control, but names beginning with a sin-
gle underscore are conventionally akin to protected, while names beginning with a
double underscore (other than special methods) are akin to private. In choosing to
use protected data, we have created a dependency in that our PredatoryCreditCard
class might be compromised if the author of the CreditCard class were to change
the internal design. Note that we could have relied upon the public get balance( )
method to retrieve the current balance within the process month method. But the
current design of the CreditCard class does not afford an effective way for a sub-
class to change the balance, other than by direct manipulation of the data member.
It may be tempting to use charge to add fees or interest to the balance. However,
that method does not allow the balance to go above the customer’s credit limit,
even though a bank would presumably let interest compound beyond the credit
limit, if warranted. If we were to redesign the original CreditCard class, we might
add a nonpublic method, set balance, that could be used by subclasses to affect a
change without directly accessing the data member balance.
2.4. Inheritance 87
2.4.2 Hierarchy of Numeric Progressions
As a second example of the use of inheritance, we develop a hierarchy of classes for
iterating numeric progressions. A numeric progression is a sequence of numbers,
where each number depends on one or more of the previous numbers. For example,
an arithmetic progression determines the next number by adding a fixed constant
to the previous value, and a geometric progression determines the next number
by multiplying the previous value by a fixed constant. In general, a progression
requires a first value, and a way of identifying a new value based on one or more
previous values.
To maximize reusability of code, we develop a hierarchy of classes stemming
from a general base class that we name Progression (see Figure 2.7). Technically,
the Progression class produces the progression of whole numbers: 0, 1, 2, . . . .
However, this class is designed to serve as the base class for other progression types,
providing as much common functionality as possible, and thereby minimizing the
burden on the subclasses.
FibonacciProgression
Progression
ArithmeticProgression GeometricProgression
Figure 2.7: Our hierarchy of progression classes.
Our implementation of the basic Progression class is provided in Code Frag-
ment 2.8. The constructor for this class accepts a starting value for the progression
(0 by default), and initializes a data member, self. current, to that value.
The Progression class implements the conventions of a Python iterator (see
Section 2.3.4), namely the special next and iter methods. If a user of
the class creates a progression as seq = Progression( ), each call to next(seq) will
return a subsequent element of the progression sequence. It would also be possi-
ble to use a for-loop syntax, for value in seq:, although we note that our default
progression is defined as an infinite sequence.
To better separate the mechanics of the iterator convention from the core logic
of advancing the progression, our framework relies on a nonpublic method named
advance to update the value of the self. current field. In the default implementa-
tion, advance adds one to the current value, but our intent is that subclasses will
override advance to provide a different rule for computing the next entry.
For convenience, the Progression class also provides a utility method, named
print progression, that displays the next n values of the progression.
88 Chapter 2. Object-Oriented Programming
1 class Progression:
2 ”””Iterator producing a generic progression.
3
4 Default iterator produces the whole numbers 0, 1, 2, …
5 ”””
6
7 def init (self, start=0):
8 ”””Initialize current to the first value of the progression.”””
9 self. current = start
10
11 def advance(self):
12 ”””Update self. current to a new value.
13
14 This should be overridden by a subclass to customize progression.
15
16 By convention, if current is set to None, this designates the
17 end of a finite progression.
18 ”””
19 self. current += 1
20
21 def next (self):
22 ”””Return the next element, or else raise StopIteration error.”””
23 if self. current is None: # our convention to end a progression
24 raise StopIteration( )
25 else:
26 answer = self. current # record current value to return
27 self. advance( ) # advance to prepare for next time
28 return answer # return the answer
29
30 def iter (self):
31 ”””By convention, an iterator must return itself as an iterator.”””
32 return self
33
34 def print progression(self, n):
35 ”””Print next n values of the progression.”””
36 print( .join(str(next(self)) for j in range(n)))
Code Fragment 2.8: A general numeric progression class.
2.4. Inheritance 89
An Arithmetic Progression Class
Our first example of a specialized progression is an arithmetic progression. While
the default progression increases its value by one in each step, an arithmetic pro-
gression adds a fixed constant to one term of the progression to produce the next.
For example, using an increment of 4 for an arithmetic progression that starts at 0
results in the sequence 0, 4, 8, 12, . . . .
Code Fragment 2.9 presents our implementation of an ArithmeticProgression
class, which relies on Progression as its base class. The constructor for this new
class accepts both an increment value and a starting value as parameters, although
default values for each are provided. By our convention, ArithmeticProgression(4)
produces the sequence 0, 4, 8, 12, . . . , and ArithmeticProgression(4, 1) produces
the sequence 1, 5, 9, 13, . . . .
The body of the ArithmeticProgression constructor calls the super constructor
to initialize the current data member to the desired start value. Then it directly
establishes the new increment data member for the arithmetic progression. The
only remaining detail in our implementation is to override the advance method so
as to add the increment to the current value.
1 class ArithmeticProgression(Progression): # inherit from Progression
2 ”””Iterator producing an arithmetic progression.”””
3
4 def init (self, increment=1, start=0):
5 ”””Create a new arithmetic progression.
6
7 increment the fixed constant to add to each term (default 1)
8 start the first term of the progression (default 0)
9 ”””
10 super( ). init (start) # initialize base class
11 self. increment = increment
12
13 def advance(self): # override inherited version
14 ”””Update current value by adding the fixed increment.”””
15 self. current += self. increment
Code Fragment 2.9: A class that produces an arithmetic progression.
90 Chapter 2. Object-Oriented Programming
A Geometric Progression Class
Our second example of a specialized progression is a geometric progression, in
which each value is produced by multiplying the preceding value by a fixed con-
stant, known as the base of the geometric progression. The starting point of a ge-
ometric progression is traditionally 1, rather than 0, because multiplying 0 by any
factor results in 0. As an example, a geometric progression with base 2 proceeds as
1, 2, 4, 8, 16, . . . .
Code Fragment 2.10 presents our implementation of a GeometricProgression
class. The constructor uses 2 as a default base and 1 as a default starting value, but
either of those can be varied using optional parameters.
1 class GeometricProgression(Progression): # inherit from Progression
2 ”””Iterator producing a geometric progression.”””
3
4 def init (self, base=2, start=1):
5 ”””Create a new geometric progression.
6
7 base the fixed constant multiplied to each term (default 2)
8 start the first term of the progression (default 1)
9 ”””
10 super( ). init (start)
11 self. base = base
12
13 def advance(self): # override inherited version
14 ”””Update current value by multiplying it by the base value.”””
15 self. current = self. base
Code Fragment 2.10: A class that produces a geometric progression.
A Fibonacci Progression Class
As our final example, we demonstrate how to use our progression framework to
produce a Fibonacci progression. We originally discussed the Fibonacci series
on page 41 in the context of generators. Each value of a Fibonacci series is the
sum of the two most recent values. To begin the series, the first two values are
conventionally 0 and 1, leading to the Fibonacci series 0, 1, 1, 2, 3, 5, 8, . . . . More
generally, such a series can be generated from any two starting values. For example,
if we start with values 4 and 6, the series proceeds as 4, 6, 10, 16, 26, 42, . . . .
2.4. Inheritance 91
1 class FibonacciProgression(Progression):
2 ”””Iterator producing a generalized Fibonacci progression.”””
3
4 def init (self, first=0, second=1):
5 ”””Create a new fibonacci progression.
6
7 first the first term of the progression (default 0)
8 second the second term of the progression (default 1)
9 ”””
10 super( ). init (first) # start progression at first
11 self. prev = second − first # fictitious value preceding the first
12
13 def advance(self):
14 ”””Update current value by taking sum of previous two.”””
15 self. prev, self. current = self. current, self. prev + self. current
Code Fragment 2.11: A class that produces a Fibonacci progression.
We use our progression framework to define a new FibonacciProgression class,
as shown in Code Fragment 2.11. This class is markedly different from those for the
arithmetic and geometric progressions because we cannot determine the next value
of a Fibonacci series solely from the current one. We must maintain knowledge of
the two most recent values. The base Progression class already provides storage
of the most recent value as the current data member. Our FibonacciProgression
class introduces a new member, named prev, to store the value that proceeded the
current one.
With both previous values stored, the implementation of advance is relatively
straightforward. (We use a simultaneous assignment similar to that on page 45.)
However, the question arises as to how to initialize the previous value in the con-
structor. The desired first and second values are provided as parameters to the
constructor. The first should be stored as current so that it becomes the first
one that is reported. Looking ahead, once the first value is reported, we will do
an assignment to set the new current value (which will be the second value re-
ported), equal to the first value plus the “previous.” By initializing the previous
value to (second − first), the initial advancement will set the new current value to
first + (second − first) = second, as desired.
Testing Our Progressions
To complete our presentation, Code Fragment 2.12 provides a unit test for all of
our progression classes, and Code Fragment 2.13 shows the output of that test.
92 Chapter 2. Object-Oriented Programming
if name == __main__ :
print( Default progression: )
Progression( ).print progression(10)
print( Arithmetic progression with increment 5: )
ArithmeticProgression(5).print progression(10)
print( Arithmetic progression with increment 5 and start 2: )
ArithmeticProgression(5, 2).print progression(10)
print( Geometric progression with default base: )
GeometricProgression( ).print progression(10)
print( Geometric progression with base 3: )
GeometricProgression(3).print progression(10)
print( Fibonacci progression with default start values: )
FibonacciProgression( ).print progression(10)
print( Fibonacci progression with start values 4 and 6: )
FibonacciProgression(4, 6).print progression(10)
Code Fragment 2.12: Unit tests for our progression classes.
Default progression:
0 1 2 3 4 5 6 7 8 9
Arithmetic progression with increment 5:
0 5 10 15 20 25 30 35 40 45
Arithmetic progression with increment 5 and start 2:
2 7 12 17 22 27 32 37 42 47
Geometric progression with default base:
1 2 4 8 16 32 64 128 256 512
Geometric progression with base 3:
1 3 9 27 81 243 729 2187 6561 19683
Fibonacci progression with default start values:
0 1 1 2 3 5 8 13 21 34
Fibonacci progression with start values 4 and 6:
4 6 10 16 26 42 68 110 178 288
Code Fragment 2.13: Output of the unit tests from Code Fragment 2.12.
2.4. Inheritance 93
2.4.3 Abstract Base Classes
When defining a group of classes as part of an inheritance hierarchy, one technique
for avoiding repetition of code is to design a base class with common function-
ality that can be inherited by other classes that need it. As an example, the hi-
erarchy from Section 2.4.2 includes a Progression class, which serves as a base
class for three distinct subclasses: ArithmeticProgression, GeometricProgression,
and FibonacciProgression. Although it is possible to create an instance of the
Progression base class, there is little value in doing so because its behavior is sim-
ply a special case of an ArithmeticProgression with increment 1. The real purpose
of the Progression class was to centralize the implementations of behaviors that
other progressions needed, thereby streamlining the code that is relegated to those
subclasses.
In classic object-oriented terminology, we say a class is an abstract base class
if its only purpose is to serve as a base class through inheritance. More formally,
an abstract base class is one that cannot be directly instantiated, while a concrete
class is one that can be instantiated. By this definition, our Progression class is
technically concrete, although we essentially designed it as an abstract base class.
In statically typed languages such as Java and C++, an abstract base class serves
as a formal type that may guarantee one or more abstract methods. This provides
support for polymorphism, as a variable may have an abstract base class as its de-
clared type, even though it refers to an instance of a concrete subclass. Because
there are no declared types in Python, this kind of polymorphism can be accom-
plished without the need for a unifying abstract base class. For this reason, there
is not as strong a tradition of defining abstract base classes in Python, although
Python’s abc module provides support for defining a formal abstract base class.
Our reason for focusing on abstract base classes in our study of data structures
is that Python’s collections module provides several abstract base classes that assist
when defining custom data structures that share a common interface with some of
Python’s built-in data structures. These rely on an object-oriented software design
pattern known as the template method pattern. The template method pattern is
when an abstract base class provides concrete behaviors that rely upon calls to
other abstract behaviors. In that way, as soon as a subclass provides definitions for
the missing abstract behaviors, the inherited concrete behaviors are well defined.
As a tangible example, the collections.Sequence abstract base class defines be-
haviors common to Python’s list, str, and tuple classes, as sequences that sup-
port element access via an integer index. More so, the collections.Sequence class
provides concrete implementations of methods, count, index, and contains
that can be inherited by any class that provides concrete implementations of both
len and getitem . For the purpose of illustration, we provide a sample
implementation of such a Sequence abstract base class in Code Fragment 2.14.
94 Chapter 2. Object-Oriented Programming
1 from abc import ABCMeta, abstractmethod # need these definitions
2
3 class Sequence(metaclass=ABCMeta):
4 ”””Our own version of collections.Sequence abstract base class.”””
5
6 @abstractmethod
7 def len (self):
8 ”””Return the length of the sequence.”””
9
10 @abstractmethod
11 def getitem (self, j):
12 ”””Return the element at index j of the sequence.”””
13
14 def contains (self, val):
15 ”””Return True if val found in the sequence; False otherwise.”””
16 for j in range(len(self)):
17 if self[j] == val: # found match
18 return True
19 return False
20
21 def index(self, val):
22 ”””Return leftmost index at which val is found (or raise ValueError).”””
23 for j in range(len(self)):
24 if self[j] == val: # leftmost match
25 return j
26 raise ValueError( value not in sequence ) # never found a match
27
28 def count(self, val):
29 ”””Return the number of elements equal to given value.”””
30 k = 0
31 for j in range(len(self)):
32 if self[j] == val: # found a match
33 k += 1
34 return k
Code Fragment 2.14: An abstract base class akin to collections.Sequence.
This implementation relies on two advanced Python techniques. The first is that
we declare the ABCMeta class of the abc module as a metaclass of our Sequence
class. A metaclass is different from a superclass, in that it provides a template for
the class definition itself. Specifically, the ABCMeta declaration assures that the
constructor for the class raises an error.
2.4. Inheritance 95
The second advanced technique is the use of the @abstractmethod decorator
immediately before the len and getitem methods are declared. That de-
clares these two particular methods to be abstract, meaning that we do not provide
an implementation within our Sequence base class, but that we expect any concrete
subclasses to support those two methods. Python enforces this expectation, by dis-
allowing instantiation for any subclass that does not override the abstract methods
with concrete implementations.
The rest of the Sequence class definition provides tangible implementations for
other behaviors, under the assumption that the abstract len and getitem
methods will exist in a concrete subclass. If you carefully examine the source code,
the implementations of methods contains , index, and count do not rely on any
assumption about the self instances, other than that syntax len(self) and self[j] are
supported (by special methods len and getitem , respectively). Support
for iteration is automatic as well, as described in Section 2.3.4.
In the remainder of this book, we omit the formality of using the abc module.
If we need an “abstract” base class, we simply document the expectation that sub-
classes provide assumed functionality, without technical declaration of the methods
as abstract. But we will make use of the wonderful abstract base classes that are
defined within the collections module (such as Sequence). To use such a class, we
need only rely on standard inheritance techniques.
For example, our Range class, from Code Fragment 2.6 of Section 2.3.5, is an
example of a class that supports the len and getitem methods. But that
class does not support methods count or index. Had we originally declared it with
Sequence as a superclass, then it would also inherit the count and index methods.
The syntax for such a declaration would begin as:
class Range(collections.Sequence):
Finally, we emphasize that if a subclass provides its own implementation of
an inherited behaviors from a base class, the new definition overrides the inherited
one. This technique can be used when we have the ability to provide a more effi-
cient implementation for a behavior than is achieved by the generic approach. As
an example, the general implementation of contains for a sequence is based
on a loop used to search for the desired value. For our Range class, there is an
opportunity for a more efficient determination of containment. For example, it
is evident that the expression, 100000 in Range(0, 2000000, 100), should evalu-
ate to True, even without examining the individual elements of the range, because
the range starts with zero, has an increment of 100, and goes until 2 million; it
must include 100000, as that is a multiple of 100 that is between the start and
stop values. Exercise C-2.27 explores the goal of providing an implementation of
Range. contains that avoids the use of a (time-consuming) loop.
96 Chapter 2. Object-Oriented Programming
2.5 Namespaces and Object-Orientation
A namespace is an abstraction that manages all of the identifiers that are defined in
a particular scope, mapping each name to its associated value. In Python, functions,
classes, and modules are all first-class objects, and so the “value” associated with
an identifier in a namespace may in fact be a function, class, or module.
In Section 1.10 we explored Python’s use of namespaces to manage identifiers
that are defined with global scope, versus those defined within the local scope of
a function call. In this section, we discuss the important role of namespaces in
Python’s management of object-orientation.
2.5.1 Instance and Class Namespaces
We begin by exploring what is known as the instance namespace, which man-
ages attributes specific to an individual object. For example, each instance of our
CreditCard class maintains a distinct balance, a distinct account number, a distinct
credit limit, and so on (even though some instances may coincidentally have equiv-
alent balances, or equivalent credit limits). Each credit card will have a dedicated
instance namespace to manage such values.
There is a separate class namespace for each class that has been defined. This
namespace is used to manage members that are to be shared by all instances of
a class, or used without reference to any particular instance. For example, the
make payment method of the CreditCard class from Section 2.3 is not stored
independently by each instance of that class. That member function is stored
within the namespace of the CreditCard class. Based on our definition from Code
Fragments 2.1 and 2.2, the CreditCard class namespace includes the functions:
init , get customer, get bank, get account, get balance, get limit, charge,
and make payment. Our PredatoryCreditCard class has its own namespace, con-
taining the three methods we defined for that subclass: init , charge, and
process month.
Figure 2.8 provides a portrayal of three such namespaces: a class namespace
containing methods of the CreditCard class, another class namespace with meth-
ods of the PredatoryCreditCard class, and finally a single instance namespace for
a sample instance of the PredatoryCreditCard class. We note that there are two
different definitions of a function named charge, one in the CreditCard class, and
then the overriding method in the PredatoryCreditCard class. In similar fashion,
there are two distinct init implementations. However, process month is a
name that is only defined within the scope of the PredatoryCreditCard class. The
instance namespace includes all data members for the instance (including the apr
member that is established by the PredatoryCreditCard constructor).
2.5. Namespaces and Object-Orientation 97
get bank
get account
make payment
get balance
get limit
charge
initfunction
function
function
function
function
function
function
get customer
function
charge
init
function
function
process month
function
bank
account
balance
limit
apr
1234.56
2500
John Bowman
California Savings
5391 0375 9387 5309
customer
0.0825
(a) (b) (c)
Figure 2.8: Conceptual view of three namespaces: (a) the class namespace for
CreditCard; (b) the class namespace for PredatoryCreditCard; (c) the instance
namespace for a PredatoryCreditCard object.
How Entries Are Established in a Namespace
It is important to understand why a member such as balance resides in a credit
card’s instance namespace, while a member such as make payment resides in the
class namespace. The balance is established within the init method when a
new credit card instance is constructed. The original assignment uses the syntax,
self. balance = 0, where self is an identifier for the newly constructed instance.
The use of self as a qualifier for self. balance in such an assignment causes the
balance identifier to be added directly to the instance namespace.
When inheritance is used, there is still a single instance namespace per object.
For example, when an instance of the PredatoryCreditCard class is constructed,
the apr attribute as well as attributes such as balance and limit all reside in that
instance’s namespace, because all are assigned using a qualified syntax, such as
self. apr.
A class namespace includes all declarations that are made directly within the
body of the class definition. For example, our CreditCard class definition included
the following structure:
class CreditCard:
def make payment(self, amount):
…
Because the make payment function is declared within the scope of the CreditCard
class, that function becomes associated with the name make payment within the
CreditCard class namespace. Although member functions are the most typical
types of entries that are declared in a class namespace, we next discuss how other
types of data values, or even other classes can be declared within a class namespace.
98 Chapter 2. Object-Oriented Programming
Class Data Members
A class-level data member is often used when there is some value, such as a con-
stant, that is to be shared by all instances of a class. In such a case, it would
be unnecessarily wasteful to have each instance store that value in its instance
namespace. As an example, we revisit the PredatoryCreditCard introduced in Sec-
tion 2.4.1. That class assesses a $5 fee if an attempted charge is denied because
of the credit limit. Our choice of $5 for the fee was somewhat arbitrary, and our
coding style would be better if we used a named variable rather than embedding
the literal value in our code. Often, the amount of such a fee is determined by the
bank’s policy and does not vary for each customer. In that case, we could define
and use a class data member as follows:
class PredatoryCreditCard(CreditCard):
OVERLIMIT FEE = 5 # this is a class-level member
def charge(self, price):
success = super( ).charge(price)
if not success:
self. balance += PredatoryCreditCard.OVERLIMIT FEE
return success
The data member, OVERLIMIT FEE, is entered into the PredatoryCreditCard
class namespace because that assignment takes place within the immediate scope
of the class definition, and without any qualifying identifier.
Nested Classes
It is also possible to nest one class definition within the scope of another class.
This is a useful construct, which we will exploit several times in this book in the
implementation of data structures. This can be done by using a syntax such as
class A: # the outer class
class B: # the nested class
…
In this case, class B is the nested class. The identifier B is entered into the name-
space of class A associated with the newly defined class. We note that this technique
is unrelated to the concept of inheritance, as class B does not inherit from class A.
Nesting one class in the scope of another makes clear that the nested class
exists for support of the outer class. Furthermore, it can help reduce potential name
conflicts, because it allows for a similarly named class to exist in another context.
For example, we will later introduce a data structure known as a linked list and will
define a nested node class to store the individual components of the list. We will
also introduce a data structure known as a tree that depends upon its own nested
2.5. Namespaces and Object-Orientation 99
node class. These two structures rely on different node definitions, and by nesting
those within the respective container classes, we avoid ambiguity.
Another advantage of one class being nested as a member of another is that it
allows for a more advanced form of inheritance in which a subclass of the outer
class overrides the definition of its nested class. We will make use of that technique
in Section 11.2.1 when specializing the nodes of a tree structure.
Dictionaries and the slots Declaration
By default, Python represents each namespace with an instance of the built-in dict
class (see Section 1.2.3) that maps identifying names in that scope to the associated
objects. While a dictionary structure supports relatively efficient name lookups,
it requires additional memory usage beyond the raw data that it stores (we will
explore the data structure used to implement dictionaries in Chapter 10).
Python provides a more direct mechanism for representing instance namespaces
that avoids the use of an auxiliary dictionary. To use the streamlined representation
for all instances of a class, that class definition must provide a class-level member
named slots that is assigned to a fixed sequence of strings that serve as names
for instance variables. For example, with our CreditCard class, we would declare
the following:
class CreditCard:
slots = _customer , _bank , _account , _balance , _limit
In this example, the right-hand side of the assignment is technically a tuple (see
discussion of automatic packing of tuples in Section 1.9.3).
When inheritance is used, if the base class declares slots , a subclass must
also declare slots to avoid creation of instance dictionaries. The declaration
in the subclass should only include names of supplemental methods that are newly
introduced. For example, our PredatoryCreditCard declaration would include the
following declaration:
class PredatoryCreditCard(CreditCard):
slots = _apr # in addition to the inherited members
We could choose to use the slots declaration to streamline every class in
this book. However, we do not do so because such rigor would be atypical for
Python programs. With that said, there are a few classes in this book for which
we expect to have a large number of instances, each representing a lightweight
construct. For example, when discussing nested classes, we suggest linked lists
and trees as data structures that are often comprised of a large number of individual
nodes. To promote greater efficiency in memory usage, we will use an explicit
slots declaration in any nested classes for which we expect many instances.
100 Chapter 2. Object-Oriented Programming
2.5.2 Name Resolution and Dynamic Dispatch
In the previous section, we discussed various namespaces, and the mechanism for
establishing entries in those namespaces. In this section, we examine the process
that is used when retrieving a name in Python’s object-oriented framework. When
the dot operator syntax is used to access an existing member, such as obj.foo, the
Python interpreter begins a name resolution process, described as follows:
1. The instance namespace is searched; if the desired name is found, its associ-
ated value is used.
2. Otherwise the class namespace, for the class to which the instance belongs,
is searched; if the name is found, its associated value is used.
3. If the name was not found in the immediate class namespace, the search con-
tinues upward through the inheritance hierarchy, checking the class name-
space for each ancestor (commonly by checking the superclass class, then its
superclass class, and so on). The first time the name is found, its associate
value is used.
4. If the name has still not been found, an AttributeError is raised.
As a tangible example, let us assume that mycard identifies an instance of the
PredatoryCreditCard class. Consider the following possible usage patterns.
• mycard. balance (or equivalently, self. balance from within a method body):
the balance method is found within the instance namespace for mycard.
• mycard.process month( ): the search begins in the instance namespace, but
the name process month is not found in that namespace. As a result, the
PredatoryCreditCard class namespace is searched; in this case, the name is
found and that method is called.
• mycard.make payment(200): the search for the name, make payment, fails
in the instance namespace and in the PredatoryCreditCard namespace. The
name is resolved in the namespace for superclass CreditCard and thus the
inherited method is called.
• mycard.charge(50): the search for name charge fails in the instance name-
space. The next namespace checked is for the PredatoryCreditCard class,
because that is the true type of the instance. There is a definition for a charge
function in that class, and so that is the one that is called.
In the last case shown, notice that the existence of a charge function in the
PredatoryCreditCard class has the effect of overriding the version of that function
that exists in the CreditCard namespace. In traditional object-oriented terminol-
ogy, Python uses what is known as dynamic dispatch (or dynamic binding) to
determine, at run-time, which implementation of a function to call based upon the
type of the object upon which it is invoked. This is in contrast to some languages
that use static dispatching, making a compile-time decision as to which version of
a function to call, based upon the declared type of a variable.
2.6. Shallow and Deep Copying 101
2.6 Shallow and Deep Copying
In Chapter 1, we emphasized that an assignment statement foo = bar makes the
name foo an alias for the object identified as bar. In this section, we consider
the task of making a copy of an object, rather than an alias. This is necessary in
applications when we want to subsequently modify either the original or the copy
in an independent manner.
Consider a scenario in which we manage various lists of colors, with each color
represented by an instance of a presumed color class. We let identifier warmtones
denote an existing list of such colors (e.g., oranges, browns). In this application,
we wish to create a new list named palette, which is a copy of the warmtones list.
However, we want to subsequently be able to add additional colors to palette, or
to modify or remove some of the existing colors, without affecting the contents of
warmtones. If we were to execute the command
palette = warmtones
this creates an alias, as shown in Figure 2.9. No new list is created; instead, the
new identifier palette references the original list.
red
green
blue
color
52
163
169
list
warmtones
red
green
blue
color
43
124
249
palette
Figure 2.9: Two aliases for the same list of colors.
Unfortunately, this does not meet our desired criteria, because if we subsequently
add or remove colors from “palette,” we modify the list identified as warmtones.
We can instead create a new instance of the list class by using the syntax:
palette = list(warmtones)
In this case, we explicitly call the list constructor, sending the first list as a param-
eter. This causes a new list to be created, as shown in Figure 2.10; however, it is
what is known as a shallow copy. The new list is initialized so that its contents are
precisely the same as the original sequence. However, Python’s lists are referential
(see page 9 of Section 1.2.3), and so the new list represents a sequence of references
to the same elements as in the first.
102 Chapter 2. Object-Oriented Programming
red
green
blue
color
52
163
169
list list
warmtones palette
red
green
blue
color
43
124
249
Figure 2.10: A shallow copy of a list of colors.
This is a better situation than our first attempt, as we can legitimately add
or remove elements from palette without affecting warmtones. However, if we
edit a color instance from the palette list, we effectively change the contents of
warmtones. Although palette and warmtones are distinct lists, there remains indi-
rect aliasing, for example, with palette[0] and warmtones[0] as aliases for the same
color instance.
We prefer that palette be what is known as a deep copy of warmtones. In a
deep copy, the new copy references its own copies of those objects referenced by
the original version. (See Figure 2.11.)
blue
color
52
163
169
list
red
green
blue
color
43
124
249 red
green
blue
color
52
163
169
list
red
green
blue
color
43
124
249
warmtones palette
red
green
Figure 2.11: A deep copy of a list of colors.
Python’s copy Module
To create a deep copy, we could populate our list by explicitly making copies of
the original color instances, but this requires that we know how to make copies of
colors (rather than aliasing). Python provides a very convenient module, named
copy, that can produce both shallow copies and deep copies of arbitrary objects.
This module supports two functions: the copy function creates a shallow copy
of its argument, and the deepcopy function creates a deep copy of its argument.
After importing the module, we may create a deep copy for our example, as shown
in Figure 2.11, using the command:
palette = copy.deepcopy(warmtones)
2.7. Exercises 103
2.7 Exercises
For help with exercises, please visit the site, www.wiley.com/college/goodrich.
Reinforcement
R-2.1 Give three examples of life-critical software applications.
R-2.2 Give an example of a software application in which adaptability can mean
the difference between a prolonged lifetime of sales and bankruptcy.
R-2.3 Describe a component from a text-editor GUI and the methods that it en-
capsulates.
R-2.4 Write a Python class, Flower, that has three instance variables of type str,
int, and float, that respectively represent the name of the flower, its num-
ber of petals, and its price. Your class must include a constructor method
that initializes each variable to an appropriate value, and your class should
include methods for setting the value of each type, and retrieving the value
of each type.
R-2.5 Use the techniques of Section 1.7 to revise the charge and make payment
methods of the CreditCard class to ensure that the caller sends a number
as a parameter.
R-2.6 If the parameter to the make payment method of the CreditCard class
were a negative number, that would have the effect of raising the balance
on the account. Revise the implementation so that it raises a ValueError if
a negative value is sent.
R-2.7 The CreditCard class of Section 2.3 initializes the balance of a new ac-
count to zero. Modify that class so that a new account can be given a
nonzero balance using an optional fifth parameter to the constructor. The
four-parameter constructor syntax should continue to produce an account
with zero balance.
R-2.8 Modify the declaration of the first for loop in the CreditCard tests, from
Code Fragment 2.3, so that it will eventually cause exactly one of the three
credit cards to go over its credit limit. Which credit card is it?
R-2.9 Implement the sub method for the Vector class of Section 2.3.3, so
that the expression u−v returns a new vector instance representing the
difference between two vectors.
R-2.10 Implement the neg method for the Vector class of Section 2.3.3, so
that the expression −v returns a new vector instance whose coordinates
are all the negated values of the respective coordinates of v.
http:\\www.wiley.com/college/goodrich
104 Chapter 2. Object-Oriented Programming
R-2.11 In Section 2.3.3, we note that our Vector class supports a syntax such as
v = u + [5, 3, 10, −2, 1], in which the sum of a vector and list returns
a new vector. However, the syntax v = [5, 3, 10, −2, 1] + u is illegal.
Explain how the Vector class definition can be revised so that this syntax
generates a new vector.
R-2.12 Implement the mul method for the Vector class of Section 2.3.3, so
that the expression v 3 returns a new vector with coordinates that are 3
times the respective coordinates of v.
R-2.13 Exercise R-2.12 asks for an implementation of mul , for the Vector
class of Section 2.3.3, to provide support for the syntax v 3. Implement
the rmul method, to provide additional support for syntax 3 v.
R-2.14 Implement the mul method for the Vector class of Section 2.3.3, so
that the expression u v returns a scalar that represents the dot product of
the vectors, that is, ∑di=1 ui · vi.
R-2.15 The Vector class of Section 2.3.3 provides a constructor that takes an in-
teger d, and produces a d-dimensional vector with all coordinates equal to
0. Another convenient form for creating a new vector would be to send the
constructor a parameter that is some iterable type representing a sequence
of numbers, and to create a vector with dimension equal to the length of
that sequence and coordinates equal to the sequence values. For example,
Vector([4, 7, 5]) would produce a three-dimensional vector with coordi-
nates <4, 7, 5>. Modify the constructor so that either of these forms is
acceptable; that is, if a single integer is sent, it produces a vector of that
dimension with all zeros, but if a sequence of numbers is provided, it pro-
duces a vector with coordinates based on that sequence.
R-2.16 Our Range class, from Section 2.3.5, relies on the formula
max(0, (stop − start + step − 1) // step)
to compute the number of elements in the range. It is not immediately ev-
ident why this formula provides the correct calculation, even if assuming
a positive step size. Justify this formula, in your own words.
R-2.17 Draw a class inheritance diagram for the following set of classes:
• Class Goat extends object and adds an instance variable tail and
methods milk( ) and jump( ).
• Class Pig extends object and adds an instance variable nose and
methods eat(food) and wallow( ).
• Class Horse extends object and adds instance variables height and
color, and methods run( ) and jump( ).
• Class Racer extends Horse and adds a method race( ).
• Class Equestrian extends Horse, adding an instance variable weight
and methods trot( ) and is trained( ).
2.7. Exercises 105
R-2.18 Give a short fragment of Python code that uses the progression classes
from Section 2.4.2 to find the 8th value of a Fibonacci progression that
starts with 2 and 2 as its first two values.
R-2.19 When using the ArithmeticProgression class of Section 2.4.2 with an in-
crement of 128 and a start of 0, how many calls to next can we make
before we reach an integer of 263 or larger?
R-2.20 What are some potential efficiency disadvantages of having very deep in-
heritance trees, that is, a large set of classes, A, B, C, and so on, such that
B extends A, C extends B, D extends C, etc.?
R-2.21 What are some potential efficiency disadvantages of having very shallow
inheritance trees, that is, a large set of classes, A, B, C, and so on, such
that all of these classes extend a single class, Z?
R-2.22 The collections.Sequence abstract base class does not provide support for
comparing two sequences to each other. Modify our Sequence class from
Code Fragment 2.14 to include a definition for the eq method, so
that expression seq1 == seq2 will return True precisely when the two
sequences are element by element equivalent.
R-2.23 In similar spirit to the previous problem, augment the Sequence class with
method lt , to support lexicographic comparison seq1 < seq2.
Creativity
C-2.24 Suppose you are on the design team for a new e-book reader. What are the
primary classes and methods that the Python software for your reader will
need? You should include an inheritance diagram for this code, but you
do not need to write any actual code. Your software architecture should
at least include ways for customers to buy new books, view their list of
purchased books, and read their purchased books.
C-2.25 Exercise R-2.12 uses the mul method to support multiplying a Vector
by a number, while Exercise R-2.14 uses the mul method to support
computing a dot product of two vectors. Give a single implementation of
Vector. mul that uses run-time type checking to support both syntaxes
u v and u k, where u and v designate vector instances and k represents
a number.
C-2.26 The SequenceIterator class of Section 2.3.4 provides what is known as a
forward iterator. Implement a class named ReversedSequenceIterator that
serves as a reverse iterator for any Python sequence type. The first call to
next should return the last element of the sequence, the second call to next
should return the second-to-last element, and so forth.
106 Chapter 2. Object-Oriented Programming
C-2.27 In Section 2.3.5, we note that our version of the Range class has im-
plicit support for iteration, due to its explicit support of both len
and getitem . The class also receives implicit support of the Boolean
test, “k in r” for Range r. This test is evaluated based on a forward itera-
tion through the range, as evidenced by the relative quickness of the test
2 in Range(10000000) versus 9999999 in Range(10000000). Provide a
more efficient implementation of the contains method to determine
whether a particular value lies within a given range. The running time of
your method should be independent of the length of the range.
C-2.28 The PredatoryCreditCard class of Section 2.4.1 provides a process month
method that models the completion of a monthly cycle. Modify the class
so that once a customer has made ten calls to charge in the current month,
each additional call to that function results in an additional $1 surcharge.
C-2.29 Modify the PredatoryCreditCard class from Section 2.4.1 so that a cus-
tomer is assigned a minimum monthly payment, as a percentage of the
balance, and so that a late fee is assessed if the customer does not subse-
quently pay that minimum amount before the next monthly cycle.
C-2.30 At the close of Section 2.4.1, we suggest a model in which the CreditCard
class supports a nonpublic method, set balance(b), that could be used
by subclasses to affect a change to the balance, without directly accessing
the balance data member. Implement such a model, revising both the
CreditCard and PredatoryCreditCard classes accordingly.
C-2.31 Write a Python class that extends the Progression class so that each value
in the progression is the absolute value of the difference between the pre-
vious two values. You should include a constructor that accepts a pair of
numbers as the first two values, using 2 and 200 as the defaults.
C-2.32 Write a Python class that extends the Progression class so that each value
in the progression is the square root of the previous value. (Note that
you can no longer represent each value with an integer.) Your construc-
tor should accept an optional parameter specifying the start value, using
65, 536 as a default.
Projects
P-2.33 Write a Python program that inputs a polynomial in standard algebraic
notation and outputs the first derivative of that polynomial.
P-2.34 Write a Python program that inputs a document and then outputs a bar-
chart plot of the frequencies of each alphabet character that appears in
that document.
2.7. Exercises 107
P-2.35 Write a set of Python classes that can simulate an Internet application in
which one party, Alice, is periodically creating a set of packets that she
wants to send to Bob. An Internet process is continually checking if Alice
has any packets to send, and if so, it delivers them to Bob’s computer, and
Bob is periodically checking if his computer has a packet from Alice, and,
if so, he reads and deletes it.
P-2.36 Write a Python program to simulate an ecosystem containing two types
of creatures, bears and fish. The ecosystem consists of a river, which is
modeled as a relatively large list. Each element of the list should be a
Bear object, a Fish object, or None. In each time step, based on a random
process, each animal either attempts to move into an adjacent list location
or stay where it is. If two animals of the same type are about to collide in
the same cell, then they stay where they are, but they create a new instance
of that type of animal, which is placed in a random empty (i.e., previously
None) location in the list. If a bear and a fish collide, however, then the
fish dies (i.e., it disappears).
P-2.37 Write a simulator, as in the previous project, but add a Boolean gender
field and a floating-point strength field to each animal, using an Animal
class as a base class. If two animals of the same type try to collide, then
they only create a new instance of that type of animal if they are of differ-
ent genders. Otherwise, if two animals of the same type and gender try to
collide, then only the one of larger strength survives.
P-2.38 Write a Python program that simulates a system that supports the func-
tions of an e-book reader. You should include methods for users of your
system to “buy” new books, view their list of purchased books, and read
their purchased books. Your system should use actual books, which have
expired copyrights and are available on the Internet, to populate your set
of available books for users of your system to “purchase” and read.
P-2.39 Develop an inheritance hierarchy based upon a Polygon class that has
abstract methods area( ) and perimeter( ). Implement classes Triangle,
Quadrilateral, Pentagon, Hexagon, and Octagon that extend this base
class, with the obvious meanings for the area( ) and perimeter( ) methods.
Also implement classes, IsoscelesTriangle, EquilateralTriangle, Rectan-
gle, and Square, that have the appropriate inheritance relationships. Fi-
nally, write a simple program that allows users to create polygons of the
various types and input their geometric dimensions, and the program then
outputs their area and perimeter. For extra effort, allow users to input
polygons by specifying their vertex coordinates and be able to test if two
such polygons are similar.
108 Chapter 2. Object-Oriented Programming
Chapter Notes
For a broad overview of developments in computer science and engineering, we refer the
reader to The Computer Science and Engineering Handbook [96]. For more information
about the Therac-25 incident, please see the paper by Leveson and Turner [69].
The reader interested in studying object-oriented programming further, is referred to
the books by Booch [17], Budd [20], and Liskov and Guttag [71]. Liskov and Guttag
also provide a nice discussion of abstract data types, as does the survey paper by Cardelli
and Wegner [23] and the book chapter by Demurjian [33] in the The Computer Science
and Engineering Handbook [96]. Design patterns are described in the book by Gamma et
al. [41].
Books with specific focus on object-oriented programming in Python include those
by Goldwasser and Letscher [43] at the introductory level, and by Phillips [83] at a more
advanced level,
Chapter
3 Algorithm Analysis
Contents
3.1 Experimental Studies . . . . . . . . . . . . . . . . . . . . . 111
3.1.1 Moving Beyond Experimental Analysis . . . . . . . . . . . 113
3.2 The Seven Functions Used in This Book . . . . . . . . . . 115
3.2.1 Comparing Growth Rates . . . . . . . . . . . . . . . . . . 122
3.3 Asymptotic Analysis . . . . . . . . . . . . . . . . . . . . . . 123
3.3.1 The “Big-Oh” Notation . . . . . . . . . . . . . . . . . . . 123
3.3.2 Comparative Analysis . . . . . . . . . . . . . . . . . . . . 128
3.3.3 Examples of Algorithm Analysis . . . . . . . . . . . . . . 130
3.4 Simple Justification Techniques . . . . . . . . . . . . . . . 137
3.4.1 By Example . . . . . . . . . . . . . . . . . . . . . . . . . 137
3.4.2 The “Contra” Attack . . . . . . . . . . . . . . . . . . . . 137
3.4.3 Induction and Loop Invariants . . . . . . . . . . . . . . . 138
3.5 Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . 141
110 Chapter 3. Algorithm Analysis
In a classic story, the famous mathematician Archimedes was asked to deter-
mine if a golden crown commissioned by the king was indeed pure gold, and not
part silver, as an informant had claimed. Archimedes discovered a way to perform
this analysis while stepping into a bath. He noted that water spilled out of the bath
in proportion to the amount of him that went in. Realizing the implications of this
fact, he immediately got out of the bath and ran naked through the city shouting,
“Eureka, eureka!” for he had discovered an analysis tool (displacement), which,
when combined with a simple scale, could determine if the king’s new crown was
good or not. That is, Archimedes could dip the crown and an equal-weight amount
of gold into a bowl of water to see if they both displaced the same amount. This
discovery was unfortunate for the goldsmith, however, for when Archimedes did
his analysis, the crown displaced more water than an equal-weight lump of pure
gold, indicating that the crown was not, in fact, pure gold.
In this book, we are interested in the design of “good” data structures and algo-
rithms. Simply put, a data structure is a systematic way of organizing and access-
ing data, and an algorithm is a step-by-step procedure for performing some task in
a finite amount of time. These concepts are central to computing, but to be able to
classify some data structures and algorithms as “good,” we must have precise ways
of analyzing them.
The primary analysis tool we will use in this book involves characterizing the
running times of algorithms and data structure operations, with space usage also
being of interest. Running time is a natural measure of “goodness,” since time is a
precious resource—computer solutions should run as fast as possible. In general,
the running time of an algorithm or data structure operation increases with the input
size, although it may also vary for different inputs of the same size. Also, the run-
ning time is affected by the hardware environment (e.g., the processor, clock rate,
memory, disk) and software environment (e.g., the operating system, programming
language) in which the algorithm is implemented and executed. All other factors
being equal, the running time of the same algorithm on the same input data will be
smaller if the computer has, say, a much faster processor or if the implementation
is done in a program compiled into native machine code instead of an interpreted
implementation. We begin this chapter by discussing tools for performing exper-
imental studies, yet also limitations to the use of experiments as a primary means
for evaluating algorithm efficiency.
Focusing on running time as a primary measure of goodness requires that we be
able to use a few mathematical tools. In spite of the possible variations that come
from different environmental factors, we would like to focus on the relationship
between the running time of an algorithm and the size of its input. We are interested
in characterizing an algorithm’s running time as a function of the input size. But
what is the proper way of measuring it? In this chapter, we “roll up our sleeves”
and develop a mathematical way of analyzing algorithms.
3.1. Experimental Studies 111
3.1 Experimental Studies
If an algorithm has been implemented, we can study its running time by executing
it on various test inputs and recording the time spent during each execution. A
simple approach for doing this in Python is by using the time function of the time
module. This function reports the number of seconds, or fractions thereof, that have
elapsed since a benchmark time known as the epoch. The choice of the epoch is
not significant to our goal, as we can determine the elapsed time by recording the
time just before the algorithm and the time just after the algorithm, and computing
their difference, as follows:
from time import time
start time = time( ) # record the starting time
run algorithm
end time = time( ) # record the ending time
elapsed = end time − start time # compute the elapsed time
We will demonstrate use of this approach, in Chapter 5, to gather experimental data
on the efficiency of Python’s list class. An elapsed time measured in this fashion
is a decent reflection of the algorithm efficiency, but it is by no means perfect. The
time function measures relative to what is known as the “wall clock.” Because
many processes share use of a computer’s central processing unit (or CPU), the
elapsed time will depend on what other processes are running on the computer
when the test is performed. A fairer metric is the number of CPU cycles that are
used by the algorithm. This can be determined using the clock function of the time
module, but even this measure might not be consistent if repeating the identical
algorithm on the identical input, and its granularity will depend upon the computer
system. Python includes a more advanced module, named timeit, to help automate
such evaluations with repetition to account for such variance among trials.
Because we are interested in the general dependence of running time on the size
and structure of the input, we should perform independent experiments on many
different test inputs of various sizes. We can then visualize the results by plotting
the performance of each run of the algorithm as a point with x-coordinate equal to
the input size, n, and y-coordinate equal to the running time, t. Figure 3.1 displays
such hypothetical data. This visualization may provide some intuition regarding
the relationship between problem size and execution time for the algorithm. This
may lead to a statistical analysis that seeks to fit the best function of the input size
to the experimental data. To be meaningful, this analysis requires that we choose
good sample inputs and test enough of them to be able to make sound statistical
claims about the algorithm’s running time.
112 Chapter 3. Algorithm Analysis
R
u
n
n
in
g
T
im
e
(m
s)
100
300
400
500
10000
0
0 5000 15000
200
Input Size
Figure 3.1: Results of an experimental study on the running time of an algorithm.
A dot with coordinates (n,t) indicates that on an input of size n, the running time
of the algorithm was measured as t milliseconds (ms).
Challenges of Experimental Analysis
While experimental studies of running times are valuable, especially when fine-
tuning production-quality code, there are three major limitations to their use for
algorithm analysis:
• Experimental running times of two algorithms are difficult to directly com-
pare unless the experiments are performed in the same hardware and software
environments.
• Experiments can be done only on a limited set of test inputs; hence, they
leave out the running times of inputs not included in the experiment (and
these inputs may be important).
• An algorithm must be fully implemented in order to execute it to study its
running time experimentally.
This last requirement is the most serious drawback to the use of experimental stud-
ies. At early stages of design, when considering a choice of data structures or
algorithms, it would be foolish to spend a significant amount of time implementing
an approach that could easily be deemed inferior by a higher-level analysis.
3.1. Experimental Studies 113
3.1.1 Moving Beyond Experimental Analysis
Our goal is to develop an approach to analyzing the efficiency of algorithms that:
1. Allows us to evaluate the relative efficiency of any two algorithms in a way
that is independent of the hardware and software environment.
2. Is performed by studying a high-level description of the algorithm without
need for implementation.
3. Takes into account all possible inputs.
Counting Primitive Operations
To analyze the running time of an algorithm without performing experiments, we
perform an analysis directly on a high-level description of the algorithm (either in
the form of an actual code fragment, or language-independent pseudo-code). We
define a set of primitive operations such as the following:
• Assigning an identifier to an object
• Determining the object associated with an identifier
• Performing an arithmetic operation (for example, adding two numbers)
• Comparing two numbers
• Accessing a single element of a Python list by index
• Calling a function (excluding operations executed within the function)
• Returning from a function.
Formally, a primitive operation corresponds to a low-level instruction with an exe-
cution time that is constant. Ideally, this might be the type of basic operation that is
executed by the hardware, although many of our primitive operations may be trans-
lated to a small number of instructions. Instead of trying to determine the specific
execution time of each primitive operation, we will simply count how many prim-
itive operations are executed, and use this number t as a measure of the running
time of the algorithm.
This operation count will correlate to an actual running time in a specific com-
puter, for each primitive operation corresponds to a constant number of instructions,
and there are only a fixed number of primitive operations. The implicit assumption
in this approach is that the running times of different primitive operations will be
fairly similar. Thus, the number, t, of primitive operations an algorithm performs
will be proportional to the actual running time of that algorithm.
Measuring Operations as a Function of Input Size
To capture the order of growth of an algorithm’s running time, we will associate,
with each algorithm, a function f (n) that characterizes the number of primitive
operations that are performed as a function of the input size n. Section 3.2 will in-
troduce the seven most common functions that arise, and Section 3.3 will introduce
a mathematical framework for comparing functions to each other.
114 Chapter 3. Algorithm Analysis
Focusing on the Worst-Case Input
An algorithm may run faster on some inputs than it does on others of the same size.
Thus, we may wish to express the running time of an algorithm as the function of
the input size obtained by taking the average over all possible inputs of the same
size. Unfortunately, such an average-case analysis is typically quite challenging.
It requires us to define a probability distribution on the set of inputs, which is often
a difficult task. Figure 3.2 schematically shows how, depending on the input distri-
bution, the running time of an algorithm can be anywhere between the worst-case
time and the best-case time. For example, what if inputs are really only of types
“A” or “D”?
An average-case analysis usually requires that we calculate expected running
times based on a given input distribution, which usually involves sophisticated
probability theory. Therefore, for the remainder of this book, unless we specify
otherwise, we will characterize running times in terms of the worst case, as a func-
tion of the input size, n, of the algorithm.
Worst-case analysis is much easier than average-case analysis, as it requires
only the ability to identify the worst-case input, which is often simple. Also, this
approach typically leads to better algorithms. Making the standard of success for an
algorithm to perform well in the worst case necessarily requires that it will do well
on every input. That is, designing for the worst case leads to stronger algorithmic
“muscles,” much like a track star who always practices by running up an incline.
best-case time
B C D E F G
average-case time?
A
}
Input Instance
1 ms
2 ms
3 ms
4 ms
5 ms
R
u
n
n
in
g
T
im
e
(m
s)
worst-case time
Figure 3.2: The difference between best-case and worst-case time. Each bar repre-
sents the running time of some algorithm on a different possible input.
3.2. The Seven Functions Used in This Book 115
3.2 The Seven Functions Used in This Book
In this section, we briefly discuss the seven most important functions used in the
analysis of algorithms. We will use only these seven simple functions for almost
all the analysis we do in this book. In fact, a section that uses a function other
than one of these seven will be marked with a star (�) to indicate that it is optional.
In addition to these seven fundamental functions, Appendix B contains a list of
other useful mathematical facts that apply in the analysis of data structures and
algorithms.
The Constant Function
The simplest function we can think of is the constant function. This is the function,
f (n) = c,
for some fixed constant c, such as c = 5, c = 27, or c = 210. That is, for any
argument n, the constant function f (n) assigns the value c. In other words, it does
not matter what the value of n is; f (n) will always be equal to the constant value c.
Because we are most interested in integer functions, the most fundamental con-
stant function is g(n) = 1, and this is the typical constant function we use in this
book. Note that any other constant function, f (n) = c, can be written as a constant
c times g(n). That is, f (n) = cg(n) in this case.
As simple as it is, the constant function is useful in algorithm analysis, because
it characterizes the number of steps needed to do a basic operation on a computer,
like adding two numbers, assigning a value to some variable, or comparing two
numbers.
The Logarithm Function
One of the interesting and sometimes even surprising aspects of the analysis of
data structures and algorithms is the ubiquitous presence of the logarithm function,
f (n) = logb n, for some constant b > 1. This function is defined as follows:
x = logb n if and only if b
x = n.
By definition, logb 1 = 0. The value b is known as the base of the logarithm.
The most common base for the logarithm function in computer science is 2,
as computers store integers in binary, and because a common operation in many
algorithms is to repeatedly divide an input in half. In fact, this base is so common
that we will typically omit it from the notation when it is 2. That is, for us,
log n = log2 n.
116 Chapter 3. Algorithm Analysis
We note that most handheld calculators have a button marked LOG, but this is
typically for calculating the logarithm base-10, not base-two.
Computing the logarithm function exactly for any integer n involves the use
of calculus, but we can use an approximation that is good enough for our pur-
poses without calculus. In particular, we can easily compute the smallest integer
greater than or equal to logb n (its so-called ceiling,
logb n�). For positive integer,
n, this value is equal to the number of times we can divide n by b before we get
a number less than or equal to 1. For example, the evaluation of
log3 27� is 3,
because ((27/3)/3)/3 = 1. Likewise,
log4 64� is 3, because ((64/4)/4)/4 = 1,
and
log2 12� is 4, because (((12/2)/2)/2)/2 = 0.75 ≤ 1.
The following proposition describes several important identities that involve
logarithms for any base greater than 1.
Proposition 3.1 (Logarithm Rules): Given real numbers a > 0, b > 1, c > 0
and d > 1, we have:
1. logb(ac) = logb a + logb c
2. logb(a/c) = logb a − logb c
3. logb(a
c) = c logb a
4. logb a = logd a/ logd b
5. blogd a = alogd b
By convention, the unparenthesized notation log nc denotes the value log(nc).
We use a notational shorthand, logc n, to denote the quantity, (log n)c, in which the
result of the logarithm is raised to a power.
The above identities can be derived from converse rules for exponentiation that
we will present on page 121. We illustrate these identities with a few examples.
Example 3.2: We demonstrate below some interesting applications of the loga-
rithm rules from Proposition 3.1 (using the usual convention that the base of a
logarithm is 2 if it is omitted).
• log(2n) = log 2 + log n = 1 + log n, by rule 1
• log(n/2) = log n − log 2 = log n − 1, by rule 2
• log n3 = 3 log n, by rule 3
• log 2n = n log 2 = n · 1 = n, by rule 3
• log4 n = (log n)/ log 4 = (log n)/2, by rule 4
• 2log n = nlog 2 = n1 = n, by rule 5.
As a practical matter, we note that rule 4 gives us a way to compute the base-two
logarithm on a calculator that has a base-10 logarithm button, LOG, for
log2 n = LOG n / LOG 2.
3.2. The Seven Functions Used in This Book 117
The Linear Function
Another simple yet important function is the linear function,
f (n) = n.
That is, given an input value n, the linear function f assigns the value n itself.
This function arises in algorithm analysis any time we have to do a single basic
operation for each of n elements. For example, comparing a number x to each
element of a sequence of size n will require n comparisons. The linear function
also represents the best running time we can hope to achieve for any algorithm that
processes each of n objects that are not already in the computer’s memory, because
reading in the n objects already requires n operations.
The N-Log-N Function
The next function we discuss in this section is the n-log-n function,
f (n) = n log n,
that is, the function that assigns to an input n the value of n times the logarithm
base-two of n. This function grows a little more rapidly than the linear function and
a lot less rapidly than the quadratic function; therefore, we would greatly prefer an
algorithm with a running time that is proportional to n log n, than one with quadratic
running time. We will see several important algorithms that exhibit a running time
proportional to the n-log-n function. For example, the fastest possible algorithms
for sorting n arbitrary values require time proportional to n log n.
The Quadratic Function
Another function that appears often in algorithm analysis is the quadratic function,
f (n) = n2.
That is, given an input value n, the function f assigns the product of n with itself
(in other words, “n squared”).
The main reason why the quadratic function appears in the analysis of algo-
rithms is that there are many algorithms that have nested loops, where the inner
loop performs a linear number of operations and the outer loop is performed a
linear number of times. Thus, in such cases, the algorithm performs n · n = n2
operations.
118 Chapter 3. Algorithm Analysis
Nested Loops and the Quadratic Function
The quadratic function can also arise in the context of nested loops where the first
iteration of a loop uses one operation, the second uses two operations, the third uses
three operations, and so on. That is, the number of operations is
1 + 2 + 3 + ··· + (n − 2) + (n − 1) + n.
In other words, this is the total number of operations that will be performed by the
nested loop if the number of operations performed inside the loop increases by one
with each iteration of the outer loop. This quantity also has an interesting history.
In 1787, a German schoolteacher decided to keep his 9- and 10-year-old pupils
occupied by adding up the integers from 1 to 100. But almost immediately one
of the children claimed to have the answer! The teacher was suspicious, for the
student had only the answer on his slate. But the answer, 5050, was correct and the
student, Carl Gauss, grew up to be one of the greatest mathematicians of his time.
We presume that young Gauss used the following identity.
Proposition 3.3: For any integer n ≥ 1, we have:
1 + 2 + 3 + ···+ (n − 2) + (n − 1) + n = n(n + 1)
2
.
We give two “visual” justifications of Proposition 3.3 in Figure 3.3.
1 2 n
0
1
2
n
3
3
…
1 n/2
0
1
2
n
3
2
n+1
…
(a) (b)
Figure 3.3: Visual justifications of Proposition 3.3. Both illustrations visualize the
identity in terms of the total area covered by n unit-width rectangles with heights
1, 2, . . . , n. In (a), the rectangles are shown to cover a big triangle of area n2/2 (base
n and height n) plus n small triangles of area 1/2 each (base 1 and height 1). In
(b), which applies only when n is even, the rectangles are shown to cover a big
rectangle of base n/2 and height n + 1.
3.2. The Seven Functions Used in This Book 119
The lesson to be learned from Proposition 3.3 is that if we perform an algorithm
with nested loops such that the operations in the inner loop increase by one each
time, then the total number of operations is quadratic in the number of times, n,
we perform the outer loop. To be fair, the number of operations is n2/2 + n/2,
and so this is just over half the number of operations than an algorithm that uses n
operations each time the inner loop is performed. But the order of growth is still
quadratic in n.
The Cubic Function and Other Polynomials
Continuing our discussion of functions that are powers of the input, we consider
the cubic function,
f (n) = n3,
which assigns to an input value n the product of n with itself three times. This func-
tion appears less frequently in the context of algorithm analysis than the constant,
linear, and quadratic functions previously mentioned, but it does appear from time
to time.
Polynomials
Most of the functions we have listed so far can each be viewed as being part of a
larger class of functions, the polynomials. A polynomial function has the form,
f (n) = a0 + a1n + a2n
2 + a3n
3 + ··· + ad nd ,
where a0, a1, . . . , ad are constants, called the coefficients of the polynomial, and
ad �= 0. Integer d, which indicates the highest power in the polynomial, is called
the degree of the polynomial.
For example, the following functions are all polynomials:
• f (n) = 2 + 5n + n2
• f (n) = 1 + n3
• f (n) = 1
• f (n) = n
• f (n) = n2
Therefore, we could argue that this book presents just four important functions used
in algorithm analysis, but we will stick to saying that there are seven, since the con-
stant, linear, and quadratic functions are too important to be lumped in with other
polynomials. Running times that are polynomials with small degree are generally
better than polynomial running times with larger degree.
120 Chapter 3. Algorithm Analysis
Summations
A notation that appears again and again in the analysis of data structures and algo-
rithms is the summation, which is defined as follows:
b
∑
i=a
f (i) = f (a) + f (a + 1) + f (a + 2) + ··· + f (b),
where a and b are integers and a ≤ b. Summations arise in data structure and algo-
rithm analysis because the running times of loops naturally give rise to summations.
Using a summation, we can rewrite the formula of Proposition 3.3 as
n
∑
i=1
i =
n(n + 1)
2
.
Likewise, we can write a polynomial f (n) of degree d with coefficients a0, . . . , ad as
f (n) =
d
∑
i=0
ain
i.
Thus, the summation notation gives us a shorthand way of expressing sums of in-
creasing terms that have a regular structure.
The Exponential Function
Another function used in the analysis of algorithms is the exponential function,
f (n) = bn,
where b is a positive constant, called the base, and the argument n is the exponent.
That is, function f (n) assigns to the input argument n the value obtained by mul-
tiplying the base b by itself n times. As was the case with the logarithm function,
the most common base for the exponential function in algorithm analysis is b = 2.
For example, an integer word containing n bits can represent all the nonnegative
integers less than 2n. If we have a loop that starts by performing one operation
and then doubles the number of operations performed with each iteration, then the
number of operations performed in the nth iteration is 2n.
We sometimes have other exponents besides n, however; hence, it is useful
for us to know a few handy rules for working with exponents. In particular, the
following exponent rules are quite helpful.
3.2. The Seven Functions Used in This Book 121
Proposition 3.4 (Exponent Rules): Given positive integers a, b, and c, we have
1. (ba)c = bac
2. babc = ba+c
3. ba/bc = ba−c
For example, we have the following:
• 256 = 162 = (24)2 = 24·2 = 28 = 256 (Exponent Rule 1)
• 243 = 35 = 32+3 = 3233 = 9 · 27 = 243 (Exponent Rule 2)
• 16 = 1024/64 = 210/26 = 210−6 = 24 = 16 (Exponent Rule 3)
We can extend the exponential function to exponents that are fractions or real
numbers and to negative exponents, as follows. Given a positive integer k, we de-
fine b1/k to be kth root of b, that is, the number r such that rk = b. For example,
251/2 = 5, since 52 = 25. Likewise, 271/3 = 3 and 161/4 = 2. This approach al-
lows us to define any power whose exponent can be expressed as a fraction, for
ba/c = (ba)1/c, by Exponent Rule 1. For example, 93/2 = (93)1/2 = 7291/2 = 27.
Thus, ba/c is really just the cth root of the integral exponent ba.
We can further extend the exponential function to define bx for any real number
x, by computing a series of numbers of the form ba/c for fractions a/c that get pro-
gressively closer and closer to x. Any real number x can be approximated arbitrarily
closely by a fraction a/c; hence, we can use the fraction a/c as the exponent of b
to get arbitrarily close to bx. For example, the number 2π is well defined. Finally,
given a negative exponent d, we define bd = 1/b−d , which corresponds to applying
Exponent Rule 3 with a = 0 and c = −d. For example, 2−3 = 1/23 = 1/8.
Geometric Sums
Suppose we have a loop for which each iteration takes a multiplicative factor longer
than the previous one. This loop can be analyzed using the following proposition.
Proposition 3.5: For any integer n ≥ 0 and any real number a such that a > 0 and
a �= 1, consider the summation
n
∑
i=0
ai = 1 + a + a2 + ··· + an
(remembering that a0 = 1 if a > 0). This summation is equal to
an+1 − 1
a − 1 .
Summations as shown in Proposition 3.5 are called geometric summations, be-
cause each term is geometrically larger than the previous one if a > 1. For example,
everyone working in computing should know that
1 + 2 + 4 + 8 + ···+ 2n−1 = 2n − 1,
for this is the largest integer that can be represented in binary notation using n bits.
122 Chapter 3. Algorithm Analysis
3.2.1 Comparing Growth Rates
To sum up, Table 3.1 shows, in order, each of the seven common functions used in
algorithm analysis.
constant logarithm linear n-log-n quadratic cubic exponential
1 log n n n log n n2 n3 an
Table 3.1: Classes of functions. Here we assume that a > 1 is a constant.
Ideally, we would like data structure operations to run in times proportional
to the constant or logarithm function, and we would like our algorithms to run in
linear or n-log-n time. Algorithms with quadratic or cubic running times are less
practical, and algorithms with exponential running times are infeasible for all but
the smallest sized inputs. Plots of the seven functions are shown in Figure 3.4.
f(
n
)
107106
n
105104103102
Linear
Exponential
Constant
Logarithmic
N-Log-N
Quadratic
Cubic
101510141013101210111010109108101
100
104
108
1012
1016
1020
1028
1032
1036
1040
1044
100
1024
Figure 3.4: Growth rates for the seven fundamental functions used in algorithm
analysis. We use base a = 2 for the exponential function. The functions are plotted
on a log-log chart, to compare the growth rates primarily as slopes. Even so, the
exponential function grows too fast to display all its values on the chart.
The Ceiling and Floor Functions
One additional comment concerning the functions above is in order. When dis-
cussing logarithms, we noted that the value is generally not an integer, yet the
running time of an algorithm is usually expressed by means of an integer quantity,
such as the number of operations performed. Thus, the analysis of an algorithm
may sometimes involve the use of the floor function and ceiling function, which
are defined respectively as follows:
• �x� = the largest integer less than or equal to x.
•
x� = the smallest integer greater than or equal to x.
3.3. Asymptotic Analysis 123
3.3 Asymptotic Analysis
In algorithm analysis, we focus on the growth rate of the running time as a function
of the input size n, taking a “big-picture” approach. For example, it is often enough
just to know that the running time of an algorithm grows proportionally to n.
We analyze algorithms using a mathematical notation for functions that disre-
gards constant factors. Namely, we characterize the running times of algorithms
by using functions that map the size of the input, n, to values that correspond to
the main factor that determines the growth rate in terms of n. This approach re-
flects that each basic step in a pseudo-code description or a high-level language
implementation may correspond to a small number of primitive operations. Thus,
we can perform an analysis of an algorithm by estimating the number of primitive
operations executed up to a constant factor, rather than getting bogged down in
language-specific or hardware-specific analysis of the exact number of operations
that execute on the computer.
As a tangible example, we revisit the goal of finding the largest element of a
Python list; we first used this example when introducing for loops on page 21 of
Section 1.4.2. Code Fragment 3.1 presents a function named find max for this task.
1 def find max(data):
2 ”””Return the maximum element from a nonempty Python list.”””
3 biggest = data[0] # The initial value to beat
4 for val in data: # For each value:
5 if val > biggest # if it is greater than the best so far,
6 biggest = val # we have found a new best (so far)
7 return biggest # When loop ends, biggest is the max
Code Fragment 3.1: A function that returns the maximum value of a Python list.
This is a classic example of an algorithm with a running time that grows pro-
portional to n, as the loop executes once for each data element, with some fixed
number of primitive operations executing for each pass. In the remainder of this
section, we provide a framework to formalize this claim.
3.3.1 The “Big-Oh” Notation
Let f (n) and g(n) be functions mapping positive integers to positive real numbers.
We say that f (n) is O(g(n)) if there is a real constant c > 0 and an integer constant
n0 ≥ 1 such that
f (n) ≤ cg(n), for n ≥ n0.
This definition is often referred to as the “big-Oh” notation, for it is sometimes pro-
nounced as “ f (n) is big-Oh of g(n).” Figure 3.5 illustrates the general definition.
124 Chapter 3. Algorithm Analysis
Input Size
R
un
ni
ng
T
im
e
cg(n)
f(n)
n0
Figure 3.5: Illustrating the “big-Oh” notation. The function f (n) is O(g(n)), since
f (n) ≤ c · g(n) when n ≥ n0.
Example 3.6: The function 8n + 5 is O(n).
Justification: By the big-Oh definition, we need to find a real constant c > 0 and
an integer constant n0 ≥ 1 such that 8n + 5 ≤ cn for every integer n ≥ n0. It is easy
to see that a possible choice is c = 9 and n0 = 5. Indeed, this is one of infinitely
many choices available because there is a trade-off between c and n0. For example,
we could rely on constants c = 13 and n0 = 1.
The big-Oh notation allows us to say that a function f (n) is “less than or equal
to” another function g(n) up to a constant factor and in the asymptotic sense as n
grows toward infinity. This ability comes from the fact that the definition uses “≤”
to compare f (n) to a g(n) times a constant, c, for the asymptotic cases when n ≥ n0.
However, it is considered poor taste to say “ f (n) ≤ O(g(n)),” since the big-Oh
already denotes the “less-than-or-equal-to” concept. Likewise, although common,
it is not fully correct to say “ f (n) = O(g(n)),” with the usual understanding of the
“=” relation, because there is no way to make sense of the symmetric statement,
“O(g(n)) = f (n).” It is best to say,
“ f (n) is O(g(n)).”
Alternatively, we can say “ f (n) is order of g(n).” For the more mathematically
inclined, it is also correct to say, “ f (n) ∈ O(g(n)),” for the big-Oh notation, techni-
cally speaking, denotes a whole collection of functions. In this book, we will stick
to presenting big-Oh statements as “ f (n) is O(g(n)).” Even with this interpretation,
there is considerable freedom in how we can use arithmetic operations with the big-
Oh notation, and with this freedom comes a certain amount of responsibility.
3.3. Asymptotic Analysis 125
Characterizing Running Times Using the Big-Oh Notation
The big-Oh notation is used widely to characterize running times and space bounds
in terms of some parameter n, which varies from problem to problem, but is always
defined as a chosen measure of the “size” of the problem. For example, if we
are interested in finding the largest element in a sequence, as with the find max
algorithm, we should let n denote the number of elements in that collection. Using
the big-Oh notation, we can write the following mathematically precise statement
on the running time of algorithm find max (Code Fragment 3.1) for any computer.
Proposition 3.7: The algorithm, find max, for computing the maximum element
of a list of n numbers, runs in O(n) time.
Justification: The initialization before the loop begins requires only a constant
number of primitive operations. Each iteration of the loop also requires only a con-
stant number of primitive operations, and the loop executes n times. Therefore,
we account for the number of primitive operations being c′ + c′′ · n for appropriate
constants c′ and c′′ that reflect, respectively, the work performed during initializa-
tion and the loop body. Because each primitive operation runs in constant time, we
have that the running time of algorithm find max on an input of size n is at most a
constant times n; that is, we conclude that the running time of algorithm find max
is O(n).
Some Properties of the Big-Oh Notation
The big-Oh notation allows us to ignore constant factors and lower-order terms and
focus on the main components of a function that affect its growth.
Example 3.8: 5n4 + 3n3 + 2n2 + 4n + 1 is O(n4).
Justification: Note that 5n4 + 3n3 + 2n2 + 4n + 1 ≤ (5 + 3 + 2 + 4 + 1)n4 = cn4,
for c = 15, when n ≥ n0 = 1.
In fact, we can characterize the growth rate of any polynomial function.
Proposition 3.9: If f (n) is a polynomial of degree d, that is,
f (n) = a0 + a1n + ··· + ad nd ,
and ad > 0, then f (n) is O(n
d ).
Justification: Note that, for n ≥ 1, we have 1 ≤ n ≤ n2 ≤ ··· ≤ nd ; hence,
a0 + a1n + a2n
2 + ··· + ad nd ≤ (|a0| + |a1| + |a2| + ··· + |ad|) nd .
We show that f (n) is O(nd ) by defining c = |a0| + |a1| + ··· + |ad| and n0 = 1.
126 Chapter 3. Algorithm Analysis
Thus, the highest-degree term in a polynomial is the term that determines the
asymptotic growth rate of that polynomial. We consider some additional properties
of the big-Oh notation in the exercises. Let us consider some further examples here,
focusing on combinations of the seven fundamental functions used in algorithm
design. We rely on the mathematical fact that log n ≤ n for n ≥ 1.
Example 3.10: 5n2 + 3n log n + 2n + 5 is O(n2).
Justification: 5n2 + 3n log n + 2n + 5 ≤ (5 + 3 + 2 + 5)n2 = cn2, for c = 15, when
n ≥ n0 = 1.
Example 3.11: 20n3 + 10n log n + 5 is O(n3).
Justification: 20n3 + 10n log n + 5 ≤ 35n3, for n ≥ 1.
Example 3.12: 3 log n + 2 is O(log n).
Justification: 3 log n + 2 ≤ 5 log n, for n ≥ 2. Note that log n is zero for n = 1.
That is why we use n ≥ n0 = 2 in this case.
Example 3.13: 2n+2 is O(2n).
Justification: 2n+2 = 2n · 22 = 4 · 2n ; hence, we can take c = 4 and n0 = 1 in this
case.
Example 3.14: 2n + 100 log n is O(n).
Justification: 2n + 100 log n ≤ 102n, for n ≥ n0 = 1; hence, we can take c = 102
in this case.
Characterizing Functions in Simplest Terms
In general, we should use the big-Oh notation to characterize a function as closely
as possible. While it is true that the function f (n) = 4n3 + 3n2 is O(n5) or even
O(n4), it is more accurate to say that f (n) is O(n3). Consider, by way of analogy,
a scenario where a hungry traveler driving along a long country road happens upon
a local farmer walking home from a market. If the traveler asks the farmer how
much longer he must drive before he can find some food, it may be truthful for the
farmer to say, “certainly no longer than 12 hours,” but it is much more accurate
(and helpful) for him to say, “you can find a market just a few minutes drive up this
road.” Thus, even with the big-Oh notation, we should strive as much as possible
to tell the whole truth.
It is also considered poor taste to include constant factors and lower-order terms
in the big-Oh notation. For example, it is not fashionable to say that the function
2n2 is O(4n2 + 6n log n), although this is completely correct. We should strive
instead to describe the function in the big-Oh in simplest terms.
3.3. Asymptotic Analysis 127
The seven functions listed in Section 3.2 are the most common functions used
in conjunction with the big-Oh notation to characterize the running times and space
usage of algorithms. Indeed, we typically use the names of these functions to refer
to the running times of the algorithms they characterize. So, for example, we would
say that an algorithm that runs in worst-case time 4n2 + n log n is a quadratic-time
algorithm, since it runs in O(n2) time. Likewise, an algorithm running in time at
most 5n + 20 log n + 4 would be called a linear-time algorithm.
Big-Omega
Just as the big-Oh notation provides an asymptotic way of saying that a function is
“less than or equal to” another function, the following notations provide an asymp-
totic way of saying that a function grows at a rate that is “greater than or equal to”
that of another.
Let f (n) and g(n) be functions mapping positive integers to positive real num-
bers. We say that f (n) is Ω(g(n)), pronounced “ f (n) is big-Omega of g(n),” if g(n)
is O( f (n)), that is, there is a real constant c > 0 and an integer constant n0 ≥ 1 such
that
f (n) ≥ cg(n), for n ≥ n0.
This definition allows us to say asymptotically that one function is greater than or
equal to another, up to a constant factor.
Example 3.15: 3n log n − 2n is Ω(n log n).
Justification: 3n log n − 2n = n log n + 2n(log n − 1) ≥ n log n for n ≥ 2; hence,
we can take c = 1 and n0 = 2 in this case.
Big-Theta
In addition, there is a notation that allows us to say that two functions grow at the
same rate, up to constant factors. We say that f (n) is Θ(g(n)), pronounced “ f (n)
is big-Theta of g(n),” if f (n) is O(g(n)) and f (n) is Ω(g(n)) , that is, there are real
constants c′ > 0 and c′′ > 0, and an integer constant n0 ≥ 1 such that
c′g(n) ≤ f (n) ≤ c′′g(n), for n ≥ n0.
Example 3.16: 3n log n + 4n + 5 log n is Θ(n log n).
Justification: 3n log n ≤ 3n log n + 4n + 5 log n ≤ (3 + 4 + 5)n log n for n ≥ 2.
128 Chapter 3. Algorithm Analysis
3.3.2 Comparative Analysis
Suppose two algorithms solving the same problem are available: an algorithm A,
which has a running time of O(n), and an algorithm B, which has a running time
of O(n2). Which algorithm is better? We know that n is O(n2), which implies that
algorithm A is asymptotically better than algorithm B, although for a small value
of n, B may have a lower running time than A.
We can use the big-Oh notation to order classes of functions by asymptotic
growth rate. Our seven functions are ordered by increasing growth rate in the fol-
lowing sequence, that is, if a function f (n) precedes a function g(n) in the sequence,
then f (n) is O(g(n)):
1, log n, n, n log n, n2, n3, 2n.
We illustrate the growth rates of the seven functions in Table 3.2. (See also
Figure 3.4 from Section 3.2.1.)
n log n n n log n n2 n3 2n
8 3 8 24 64 512 256
16 4 16 64 256 4, 096 65, 536
32 5 32 160 1, 024 32, 768 4, 294, 967, 296
64 6 64 384 4, 096 262, 144 1.84 × 1019
128 7 128 896 16, 384 2, 097, 152 3.40 × 1038
256 8 256 2, 048 65, 536 16, 777, 216 1.15 × 1077
512 9 512 4, 608 262, 144 134, 217, 728 1.34 × 10154
Table 3.2: Selected values of fundamental functions in algorithm analysis.
We further illustrate the importance of the asymptotic viewpoint in Table 3.3.
This table explores the maximum size allowed for an input instance that is pro-
cessed by an algorithm in 1 second, 1 minute, and 1 hour. It shows the importance
of good algorithm design, because an asymptotically slow algorithm is beaten in
the long run by an asymptotically faster algorithm, even if the constant factor for
the asymptotically faster algorithm is worse.
Running Maximum Problem Size (n)
Time (μs) 1 second 1 minute 1 hour
400n 2,500 150,000 9,000,000
2n2 707 5,477 42,426
2n 19 25 31
Table 3.3: Maximum size of a problem that can be solved in 1 second, 1 minute,
and 1 hour, for various running times measured in microseconds.
3.3. Asymptotic Analysis 129
The importance of good algorithm design goes beyond just what can be solved
effectively on a given computer, however. As shown in Table 3.4, even if we
achieve a dramatic speedup in hardware, we still cannot overcome the handicap
of an asymptotically slow algorithm. This table shows the new maximum problem
size achievable for any fixed amount of time, assuming algorithms with the given
running times are now run on a computer 256 times faster than the previous one.
Running Time New Maximum Problem Size
400n 256m
2n2 16m
2n m + 8
Table 3.4: Increase in the maximum size of a problem that can be solved in a fixed
amount of time, by using a computer that is 256 times faster than the previous one.
Each entry is a function of m, the previous maximum problem size.
Some Words of Caution
A few words of caution about asymptotic notation are in order at this point. First,
note that the use of the big-Oh and related notations can be somewhat misleading
should the constant factors they “hide” be very large. For example, while it is true
that the function 10100n is O(n), if this is the running time of an algorithm being
compared to one whose running time is 10n log n, we should prefer the O(n log n)-
time algorithm, even though the linear-time algorithm is asymptotically faster. This
preference is because the constant factor, 10100, which is called “one googol,” is
believed by many astronomers to be an upper bound on the number of atoms in
the observable universe. So we are unlikely to ever have a real-world problem that
has this number as its input size. Thus, even when using the big-Oh notation, we
should at least be somewhat mindful of the constant factors and lower-order terms
we are “hiding.”
The observation above raises the issue of what constitutes a “fast” algorithm.
Generally speaking, any algorithm running in O(n log n) time (with a reasonable
constant factor) should be considered efficient. Even an O(n2)-time function may
be fast enough in some contexts, that is, when n is small. But an algorithm running
in O(2n) time should almost never be considered efficient.
Exponential Running Times
There is a famous story about the inventor of the game of chess. He asked only that
his king pay him 1 grain of rice for the first square on the board, 2 grains for the
second, 4 grains for the third, 8 for the fourth, and so on. It is an interesting test of
programming skills to write a program to compute exactly the number of grains of
rice the king would have to pay.
130 Chapter 3. Algorithm Analysis
If we must draw a line between efficient and inefficient algorithms, therefore,
it is natural to make this distinction be that between those algorithms running in
polynomial time and those running in exponential time. That is, make the distinc-
tion between algorithms with a running time that is O(nc), for some constant c > 1,
and those with a running time that is O(b n), for some constant b > 1. Like so many
notions we have discussed in this section, this too should be taken with a “grain of
salt,” for an algorithm running in O(n100) time should probably not be considered
“efficient.” Even so, the distinction between polynomial-time and exponential-time
algorithms is considered a robust measure of tractability.
3.3.3 Examples of Algorithm Analysis
Now that we have the big-Oh notation for doing algorithm analysis, let us give some
examples by characterizing the running time of some simple algorithms using this
notation. Moreover, in keeping with our earlier promise, we illustrate below how
each of the seven functions given earlier in this chapter can be used to characterize
the running time of an example algorithm.
Rather than use pseudo-code in this section, we give complete Python imple-
mentations for our examples. We use Python’s list class as the natural representa-
tion for an “array” of values. In Chapter 5, we will fully explore the underpinnings
of Python’s list class, and the efficiency of the various behaviors that it supports. In
this section, we rely on just a few of its behaviors, discussing their efficiencies as
introduced.
Constant-Time Operations
Given an instance, named data, of the Python list class, a call to the function,
len(data), is evaluated in constant time. This is a very simple algorithm because
the list class maintains, for each list, an instance variable that records the current
length of the list. This allows it to immediately report that length, rather than take
time to iteratively count each of the elements in the list. Using asymptotic notation,
we say that this function runs in O(1) time; that is, the running time of this function
is independent of the length, n, of the list.
Another central behavior of Python’s list class is that it allows access to an arbi-
trary element of the list using syntax, data[j], for integer index j. Because Python’s
lists are implemented as array-based sequences, references to a list’s elements are
stored in a consecutive block of memory. The jth element of the list can be found,
not by iterating through the list one element at a time, but by validating the index,
and using it as an offset into the underlying array. In turn, computer hardware sup-
ports constant-time access to an element based on its memory address. Therefore,
we say that the expression data[j] is evaluated in O(1) time for a Python list.
3.3. Asymptotic Analysis 131
Revisiting the Problem of Finding the Maximum of a Sequence
For our next example, we revisit the find max algorithm, given in Code Frag-
ment 3.1 on page 123, for finding the largest value in a sequence. Proposition 3.7
on page 125 claimed an O(n) run-time for the find max algorithm. Consistent with
our earlier analysis of syntax data[0], the initialization uses O(1) time. The loop
executes n times, and within each iteration, it performs one comparison and possi-
bly one assignment statement (as well as maintenance of the loop variable). Finally,
we note that the mechanism for enacting a return statement in Python uses O(1)
time. Combining these steps, we have that the find max function runs in O(n) time.
Further Analysis of the Maximum-Finding Algorithm
A more interesting question about find max is how many times we might update
the current “biggest” value. In the worst case, if the data is given to us in increasing
order, the biggest value is reassigned n − 1 times. But what if the input is given
to us in random order, with all orders equally likely; what would be the expected
number of times we update the biggest value in this case? To answer this question,
note that we update the current biggest in an iteration of the loop only if the current
element is bigger than all the elements that precede it. If the sequence is given to
us in random order, the probability that the jth element is the largest of the first j
elements is 1/ j (assuming uniqueness). Hence, the expected number of times we
update the biggest (including initialization) is Hn = ∑
n
j=1 1/ j, which is known as
the nth Harmonic number. It turns out (see Proposition B.16) that Hn is O(log n).
Therefore, the expected number of times the biggest value is updated by find max
on a randomly ordered sequence is O(log n).
Prefix Averages
The next problem we consider is computing what are known as prefix averages
of a sequence of numbers. Namely, given a sequence S consisting of n num-
bers, we want to compute a sequence A such that A[ j] is the average of elements
S[0], . . . , S[ j], for j = 0, . . . , n − 1, that is,
A[ j] =
∑
j
i=0 S[i]
j + 1
.
Computing prefix averages has many applications in economics and statistics. For
example, given the year-by-year returns of a mutual fund, ordered from recent to
past, an investor will typically want to see the fund’s average annual returns for the
most recent year, the most recent three years, the most recent five years, and so on.
Likewise, given a stream of daily Web usage logs, a Web site manager may wish
to track average usage trends over various time periods. We analyze three different
implementations that solve this problem but with rather different running times.
132 Chapter 3. Algorithm Analysis
A Quadratic-Time Algorithm
Our first algorithm for computing prefix averages, named prefix average1, is shown
in Code Fragment 3.2. It computes every element of A separately, using an inner
loop to compute the partial sum.
1 def prefix average1(S):
2 ”””Return list such that, for all j, A[j] equals average of S[0], …, S[j].”””
3 n = len(S)
4 A = [0] n # create new list of n zeros
5 for j in range(n):
6 total = 0 # begin computing S[0] + … + S[j]
7 for i in range(j + 1):
8 total += S[i]
9 A[j] = total / (j+1) # record the average
10 return A
Code Fragment 3.2: Algorithm prefix average1.
In order to analyze the prefix average1 algorithm, we consider the various steps
that are executed.
• The statement, n = len(S), executes in constant time, as described at the
beginning of Section 3.3.3.
• The statement, A = [0] n, causes the creation and initialization of a Python
list with length n, and with all entries equal to zero. This uses a constant
number of primitive operations per element, and thus runs in O(n) time.
• There are two nested for loops, which are controlled, respectively, by coun-
ters j and i. The body of the outer loop, controlled by counter j, is ex-
ecuted n times, for j = 0, . . . , n − 1. Therefore, statements total = 0 and
A[j] = total / (j+1) are executed n times each. This implies that these two
statements, plus the management of counter j in the range, contribute a num-
ber of primitive operations proportional to n, that is, O(n) time.
• The body of the inner loop, which is controlled by counter i, is executed j + 1
times, depending on the current value of the outer loop counter j. Thus, state-
ment total += S[i], in the inner loop, is executed 1 + 2 + 3 + ··· + n times.
By recalling Proposition 3.3, we know that 1 + 2 + 3 + ··· + n = n(n + 1)/2,
which implies that the statement in the inner loop contributes O(n2) time.
A similar argument can be done for the primitive operations associated with
maintaining counter i, which also take O(n2) time.
The running time of implementation prefix average1 is given by the sum of three
terms. The first and the second terms are O(n), and the third term is O(n2). By a
simple application of Proposition 3.9, the running time of prefix average1 is O(n2).
3.3. Asymptotic Analysis 133
Our second implementation for computing prefix averages, prefix average2, is
presented in Code Fragment 3.3.
1 def prefix average2(S):
2 ”””Return list such that, for all j, A[j] equals average of S[0], …, S[j].”””
3 n = len(S)
4 A = [0] n # create new list of n zeros
5 for j in range(n):
6 A[j] = sum(S[0:j+1]) / (j+1) # record the average
7 return A
Code Fragment 3.3: Algorithm prefix average2.
This approach is essentially the same high-level algorithm as in prefix average1,
but we have replaced the inner loop by using the single expression sum(S[0:j+1])
to compute the partial sum, S[0] + ··· + S[ j]. While the use of that function greatly
simplifies the presentation of the algorithm, it is worth asking how it affects the
efficiency. Asymptotically, this implementation is no better. Even though the ex-
pression, sum(S[0:j+1]), seems like a single command, it is a function call and
an evaluation of that function takes O( j + 1) time in this context. Technically, the
computation of the slice, S[0:j+1], also uses O( j + 1) time, as it constructs a new
list instance for storage. So the running time of prefix average2 is still dominated
by a series of steps that take time proportional to 1+ 2 + 3 +··· + n, and thus O(n2).
A Linear-Time Algorithm
Our final algorithm, prefix averages3, is given in Code Fragment 3.4. Just as with
our first two algorithms, we are interested in computing, for each j, the prefix sum
S[0] + S[1] + ··· + S[ j], denoted as total in our code, so that we can then compute
the prefix average A[j] =total / (j + 1). However, there is a key difference that
results in much greater efficiency.
1 def prefix average3(S):
2 ”””Return list such that, for all j, A[j] equals average of S[0], …, S[j].”””
3 n = len(S)
4 A = [0] n # create new list of n zeros
5 total = 0 # compute prefix sum as S[0] + S[1] + …
6 for j in range(n):
7 total += S[j] # update prefix sum to include S[j]
8 A[j] = total / (j+1) # compute average based on current sum
9 return A
Code Fragment 3.4: Algorithm prefix average3.
134 Chapter 3. Algorithm Analysis
In our first two algorithms, the prefix sum is computed anew for each value of j.
That contributed O( j) time for each j, leading to the quadratic behavior. In algo-
rithm prefix average3, we maintain the current prefix sum dynamically, effectively
computing S[0] + S[1] + ··· + S[ j] as total + S[j], where value total is equal to the
sum S[0] + S[1] + ··· + S[ j − 1] computed by the previous pass of the loop over j.
The analysis of the running time of algorithm prefix average3 follows:
• Initializing variables n and total uses O(1) time.
• Initializing the list A uses O(n) time.
• There is a single for loop, which is controlled by counter j. The maintenance
of that counter by the range iterator contributes a total of O(n) time.
• The body of the loop is executed n times, for j = 0, . . . , n − 1. Thus, state-
ments total += S[j] and A[j] = total / (j+1) are executed n times each.
Since each of these statements uses O(1) time per iteration, their overall
contribution is O(n) time.
The running time of algorithm prefix average3 is given by the sum of the four
terms. The first is O(1) and the remaining three are O(n). By a simple application
of Proposition 3.9, the running time of prefix average3 is O(n), which is much
better than the quadratic time of algorithms prefix average1 and prefix average2.
Three-Way Set Disjointness
Suppose we are given three sequences of numbers, A, B, and C. We will assume
that no individual sequence contains duplicate values, but that there may be some
numbers that are in two or three of the sequences. The three-way set disjointness
problem is to determine if the intersection of the three sequences is empty, namely,
that there is no element x such that x ∈ A, x ∈ B, and x ∈ C. A simple Python
function to determine this property is given in Code Fragment 3.5.
1 def disjoint1(A, B, C):
2 ”””Return True if there is no element common to all three lists.”””
3 for a in A:
4 for b in B:
5 for c in C:
6 if a == b == c:
7 return False # we found a common value
8 return True # if we reach this, sets are disjoint
Code Fragment 3.5: Algorithm disjoint1 for testing three-way set disjointness.
This simple algorithm loops through each possible triple of values from the
three sets to see if those values are equivalent. If each of the original sets has size
n, then the worst-case running time of this function is O(n3).
3.3. Asymptotic Analysis 135
We can improve upon the asymptotic performance with a simple observation.
Once inside the body of the loop over B, if selected elements a and b do not match
each other, it is a waste of time to iterate through all values of C looking for a
matching triple. An improved solution to this problem, taking advantage of this
observation, is presented in Code Fragment 3.6.
1 def disjoint2(A, B, C):
2 ”””Return True if there is no element common to all three lists.”””
3 for a in A:
4 for b in B:
5 if a == b: # only check C if we found match from A and B
6 for c in C:
7 if a == c # (and thus a == b == c)
8 return False # we found a common value
9 return True # if we reach this, sets are disjoint
Code Fragment 3.6: Algorithm disjoint2 for testing three-way set disjointness.
In the improved version, it is not simply that we save time if we get lucky. We
claim that the worst-case running time for disjoint2 is O(n2). There are quadrat-
ically many pairs (a, b) to consider. However, if A and B are each sets of distinct
elements, there can be at most O(n) such pairs with a equal to b. Therefore, the
innermost loop, over C, executes at most n times.
To account for the overall running time, we examine the time spent executing
each line of code. The management of the for loop over A requires O(n) time.
The management of the for loop over B accounts for a total of O(n2) time, since
that loop is executed n different times. The test a == b is evaluated O(n2) times.
The rest of the time spent depends upon how many matching (a, b) pairs exist. As
we have noted, there are at most n such pairs, and so the management of the loop
over C, and the commands within the body of that loop, use at most O(n2) time.
By our standard application of Proposition 3.9, the total time spent is O(n2).
Element Uniqueness
A problem that is closely related to the three-way set disjointness problem is the
element uniqueness problem. In the former, we are given three collections and we
presumed that there were no duplicates within a single collection. In the element
uniqueness problem, we are given a single sequence S with n elements and asked
whether all elements of that collection are distinct from each other.
Our first solution to this problem uses a straightforward iterative algorithm.
The unique1 function, given in Code Fragment 3.7, solves the element uniqueness
problem by looping through all distinct pairs of indices j < k, checking if any of
136 Chapter 3. Algorithm Analysis
1 def unique1(S):
2 ”””Return True if there are no duplicate elements in sequence S.”””
3 for j in range(len(S)):
4 for k in range(j+1, len(S)):
5 if S[j] == S[k]:
6 return False # found duplicate pair
7 return True # if we reach this, elements were unique
Code Fragment 3.7: Algorithm unique1 for testing element uniqueness.
those pairs refer to elements that are equivalent to each other. It does this using two
nested for loops, such that the first iteration of the outer loop causes n − 1 iterations
of the inner loop, the second iteration of the outer loop causes n − 2 iterations of
the inner loop, and so on. Thus, the worst-case running time of this function is
proportional to
(n − 1) + (n − 2) + ··· + 2 + 1,
which we recognize as the familiar O(n2) summation from Proposition 3.3.
Using Sorting as a Problem-Solving Tool
An even better algorithm for the element uniqueness problem is based on using
sorting as a problem-solving tool. In this case, by sorting the sequence of elements,
we are guaranteed that any duplicate elements will be placed next to each other.
Thus, to determine if there are any duplicates, all we need to do is perform a sin-
gle pass over the sorted sequence, looking for consecutive duplicates. A Python
implementation of this algorithm is as follows:
1 def unique2(S):
2 ”””Return True if there are no duplicate elements in sequence S.”””
3 temp = sorted(S) # create a sorted copy of S
4 for j in range(1, len(temp)):
5 if S[j−1] == S[j]:
6 return False # found duplicate pair
7 return True # if we reach this, elements were unique
Code Fragment 3.8: Algorithm unique2 for testing element uniqueness.
The built-in function, sorted, as described in Section 1.5.2, produces a copy of
the original list with elements in sorted order. It guarantees a worst-case running
time of O(n log n); see Chapter 12 for a discussion of common sorting algorithms.
Once the data is sorted, the subsequent loop runs in O(n) time, and so the entire
unique2 algorithm runs in O(n log n) time.
3.4. Simple Justification Techniques 137
3.4 Simple Justification Techniques
Sometimes, we will want to make claims about an algorithm, such as showing that
it is correct or that it runs fast. In order to rigorously make such claims, we must
use mathematical language, and in order to back up such claims, we must justify or
prove our statements. Fortunately, there are several simple ways to do this.
3.4.1 By Example
Some claims are of the generic form, “There is an element x in a set S that has
property P.” To justify such a claim, we only need to produce a particular x in S
that has property P. Likewise, some hard-to-believe claims are of the generic form,
“Every element x in a set S has property P.” To justify that such a claim is false, we
only need to produce a particular x from S that does not have property P. Such an
instance is called a counterexample.
Example 3.17: Professor Amongus claims that every number of the form 2i − 1
is a prime, when i is an integer greater than 1. Professor Amongus is wrong.
Justification: To prove Professor Amongus is wrong, we find a counterexample.
Fortunately, we need not look too far, for 24 − 1 = 15 = 3 · 5.
3.4.2 The “Contra” Attack
Another set of justification techniques involves the use of the negative. The two
primary such methods are the use of the contrapositive and the contradiction. The
use of the contrapositive method is like looking through a negative mirror. To
justify the statement “if p is true, then q is true,” we establish that “if q is not true,
then p is not true” instead. Logically, these two statements are the same, but the
latter, which is called the contrapositive of the first, may be easier to think about.
Example 3.18: Let a and b be integers. If ab is even, then a is even or b is even.
Justification: To justify this claim, consider the contrapositive, “If a is odd and
b is odd, then ab is odd.” So, suppose a = 2 j + 1 and b = 2k + 1, for some integers
j and k. Then ab = 4 jk + 2 j + 2k + 1 = 2(2 jk + j + k) + 1; hence, ab is odd.
Besides showing a use of the contrapositive justification technique, the previous
example also contains an application of DeMorgan’s Law. This law helps us deal
with negations, for it states that the negation of a statement of the form “p or q” is
“not p and not q.” Likewise, it states that the negation of a statement of the form
“p and q” is “not p or not q.”
138 Chapter 3. Algorithm Analysis
Contradiction
Another negative justification technique is justification by contradiction, which
also often involves using DeMorgan’s Law. In applying the justification by con-
tradiction technique, we establish that a statement q is true by first supposing that
q is false and then showing that this assumption leads to a contradiction (such as
2 �= 2 or 1 > 3). By reaching such a contradiction, we show that no consistent sit-
uation exists with q being false, so q must be true. Of course, in order to reach this
conclusion, we must be sure our situation is consistent before we assume q is false.
Example 3.19: Let a and b be integers. If ab is odd, then a is odd and b is odd.
Justification: Let ab be odd. We wish to show that a is odd and b is odd. So,
with the hope of leading to a contradiction, let us assume the opposite, namely,
suppose a is even or b is even. In fact, without loss of generality, we can assume
that a is even (since the case for b is symmetric). Then a = 2 j for some integer
j. Hence, ab = (2 j)b = 2( jb), that is, ab is even. But this is a contradiction: ab
cannot simultaneously be odd and even. Therefore, a is odd and b is odd.
3.4.3 Induction and Loop Invariants
Most of the claims we make about a running time or a space bound involve an inte-
ger parameter n (usually denoting an intuitive notion of the “size” of the problem).
Moreover, most of these claims are equivalent to saying some statement q(n) is true
“for all n ≥ 1.” Since this is making a claim about an infinite set of numbers, we
cannot justify this exhaustively in a direct fashion.
Induction
We can often justify claims such as those above as true, however, by using the
technique of induction. This technique amounts to showing that, for any particular
n ≥ 1, there is a finite sequence of implications that starts with something known
to be true and ultimately leads to showing that q(n) is true. Specifically, we begin a
justification by induction by showing that q(n) is true for n = 1 (and possibly some
other values n = 2, 3, . . . , k, for some constant k). Then we justify that the inductive
“step” is true for n > k, namely, we show “if q( j) is true for all j < n, then q(n) is
true.” The combination of these two pieces completes the justification by induction.
3.4. Simple Justification Techniques 139
Proposition 3.20: Consider the Fibonacci function F(n), which is defined such
that F(1) = 1, F(2) = 2, and F(n) = F(n − 2) + F(n − 1) for n > 2. (See Sec-
tion 1.8.) We claim that F (n) < 2n.
Justification: We will show our claim is correct by induction.
Base cases: (n ≤ 2). F (1) = 1 < 2 = 21 and F(2) = 2 < 4 = 22.
Induction step: (n > 2). Suppose our claim is true for all n′ < n. Consider F(n).
Since n > 2, F(n) = F(n − 2) + F (n − 1). Moreover, since both n − 2 and n − 1 are
less than n, we can apply the inductive assumption (sometimes called the “inductive
hypothesis”) to imply that F(n) < 2n−2 + 2n−1, since
2n−2 + 2n−1 < 2n−1 + 2n−1 = 2 · 2n−1 = 2n.
Let us do another inductive argument, this time for a fact we have seen before.
Proposition 3.21: (which is the same as Proposition 3.3)
n
∑
i=1
i =
n(n + 1)
2
.
Justification: We will justify this equality by induction.
Base case: n = 1. Trivial, for 1 = n(n + 1)/2, if n = 1.
Induction step: n ≥ 2. Assume the claim is true for n′ < n. Consider n.
n
∑
i=1
i = n +
n−1
∑
i=1
i.
By the induction hypothesis, then
n
∑
i=1
i = n +
(n − 1)n
2
,
which we can simplify as
n +
(n − 1)n
2
=
2n + n2 − n
2
=
n2 + n
2
=
n(n + 1)
2
.
We may sometimes feel overwhelmed by the task of justifying something true
for all n ≥ 1. We should remember, however, the concreteness of the inductive tech-
nique. It shows that, for any particular n, there is a finite step-by-step sequence of
implications that starts with something true and leads to the truth about n. In short,
the inductive argument is a template for building a sequence of direct justifications.
140 Chapter 3. Algorithm Analysis
Loop Invariants
The final justification technique we discuss in this section is the loop invariant. To
prove some statement L about a loop is correct, define L in terms of a series of
smaller statements L0,L1, . . . ,Lk, where:
1. The initial claim, L0, is true before the loop begins.
2. If L j−1 is true before iteration j, then L j will be true after iteration j.
3. The final statement, Lk, implies the desired statement L to be true.
Let us give a simple example of using a loop-invariant argument to justify the
correctness of an algorithm. In particular, we use a loop invariant to justify that
the function, find (see Code Fragment 3.9), finds the smallest index at which ele-
ment val occurs in sequence S.
1 def find(S, val):
2 ”””Return index j such that S[j] == val, or -1 if no such element.”””
3 n = len(S)
4 j = 0
5 while j < n:
6 if S[j] == val:
7 return j # a match was found at index j
8 j += 1
9 return −1
Code Fragment 3.9: Algorithm for finding the first index at which a given element
occurs in a Python list.
To show that find is correct, we inductively define a series of statements, L j,
that lead to the correctness of our algorithm. Specifically, we claim the following
is true at the beginning of iteration j of the while loop:
L j: val is not equal to any of the first j elements of S.
This claim is true at the beginning of the first iteration of the loop, because j is 0
and there are no elements among the first 0 in S (this kind of a trivially true claim
is said to hold vacuously). In iteration j, we compare element val to element S[ j]
and return the index j if these two elements are equivalent, which is clearly correct
and completes the algorithm in this case. If the two elements val and S[ j] are not
equal, then we have found one more element not equal to val and we increment
the index j. Thus, the claim L j will be true for this new value of j; hence, it is
true at the beginning of the next iteration. If the while loop terminates without
ever returning an index in S, then we have j = n. That is, Ln is true—there are no
elements of S equal to val. Therefore, the algorithm correctly returns −1 to indicate
that val is not in S.
3.5. Exercises 141
3.5 Exercises
For help with exercises, please visit the site, www.wiley.com/college/goodrich.
Reinforcement
R-3.1 Graph the functions 8n, 4n log n, 2n2, n3, and 2n using a logarithmic scale
for the x- and y-axes; that is, if the function value f (n) is y, plot this as a
point with x-coordinate at log n and y-coordinate at log y.
R-3.2 The number of operations executed by algorithms A and B is 8n log n and
2n2, respectively. Determine n0 such that A is better than B for n ≥ n0.
R-3.3 The number of operations executed by algorithms A and B is 40n2 and
2n3, respectively. Determine n0 such that A is better than B for n ≥ n0.
R-3.4 Give an example of a function that is plotted the same on a log-log scale
as it is on a standard scale.
R-3.5 Explain why the plot of the function nc is a straight line with slope c on a
log-log scale.
R-3.6 What is the sum of all the even numbers from 0 to 2n, for any positive
integer n?
R-3.7 Show that the following two statements are equivalent:
(a) The running time of algorithm A is always O( f (n)).
(b) In the worst case, the running time of algorithm A is O( f (n)).
R-3.8 Order the following functions by asymptotic growth rate.
4n log n + 2n 210 2log n
3n + 100 log n 4n 2n
n2 + 10n n3 n log n
R-3.9 Show that if d(n) is O( f (n)), then ad(n) is O( f (n)), for any constant
a > 0.
R-3.10 Show that if d(n) is O( f (n)) and e(n) is O(g(n)), then the product d(n)e(n)
is O( f (n)g(n)).
R-3.11 Show that if d(n) is O( f (n)) and e(n) is O(g(n)), then d(n) + e(n) is
O( f (n) + g(n)).
R-3.12 Show that if d(n) is O( f (n)) and e(n) is O(g(n)), then d(n) − e(n) is not
necessarily O( f (n) − g(n)).
R-3.13 Show that if d(n) is O( f (n)) and f (n) is O(g(n)), then d(n) is O(g(n)).
R-3.14 Show that O(max{ f (n), g(n)}) = O( f (n) + g(n)).
http:\\www.wiley.com/college/goodrich
142 Chapter 3. Algorithm Analysis
R-3.15 Show that f (n) is O(g(n)) if and only if g(n) is Ω( f (n)).
R-3.16 Show that if p(n) is a polynomial in n, then log p(n) is O(log n).
R-3.17 Show that (n + 1)5 is O(n5).
R-3.18 Show that 2n+1 is O(2n).
R-3.19 Show that n is O(n log n).
R-3.20 Show that n2 is Ω(n log n).
R-3.21 Show that n log n is Ω(n).
R-3.22 Show that
f (n)� is O( f (n)), if f (n) is a positive nondecreasing function
that is always greater than 1.
R-3.23 Give a big-Oh characterization, in terms of n, of the running time of the
example1 function shown in Code Fragment 3.10.
R-3.24 Give a big-Oh characterization, in terms of n, of the running time of the
example2 function shown in Code Fragment 3.10.
R-3.25 Give a big-Oh characterization, in terms of n, of the running time of the
example3 function shown in Code Fragment 3.10.
R-3.26 Give a big-Oh characterization, in terms of n, of the running time of the
example4 function shown in Code Fragment 3.10.
R-3.27 Give a big-Oh characterization, in terms of n, of the running time of the
example5 function shown in Code Fragment 3.10.
R-3.28 For each function f (n) and time t in the following table, determine the
largest size n of a problem P that can be solved in time t if the algorithm
for solving P takes f (n) microseconds (one entry is already completed).
1 Second 1 Hour 1 Month 1 Century
log n ≈ 10300000
n
n log n
n2
2n
R-3.29 Algorithm A executes an O(log n)-time computation for each entry of an
n-element sequence. What is its worst-case running time?
R-3.30 Given an n-element sequence S, Algorithm B chooses log n elements in
S at random and executes an O(n)-time calculation for each. What is the
worst-case running time of Algorithm B?
R-3.31 Given an n-element sequence S of integers, Algorithm C executes an
O(n)-time computation for each even number in S, and an O(log n)-time
computation for each odd number in S. What are the best-case and worst-
case running times of Algorithm C?
3.5. Exercises 143
1 def example1(S):
2 ”””Return the sum of the elements in sequence S.”””
3 n = len(S)
4 total = 0
5 for j in range(n): # loop from 0 to n-1
6 total += S[j]
7 return total
8
9 def example2(S):
10 ”””Return the sum of the elements with even index in sequence S.”””
11 n = len(S)
12 total = 0
13 for j in range(0, n, 2): # note the increment of 2
14 total += S[j]
15 return total
16
17 def example3(S):
18 ”””Return the sum of the prefix sums of sequence S.”””
19 n = len(S)
20 total = 0
21 for j in range(n): # loop from 0 to n-1
22 for k in range(1+j): # loop from 0 to j
23 total += S[k]
24 return total
25
26 def example4(S):
27 ”””Return the sum of the prefix sums of sequence S.”””
28 n = len(S)
29 prefix = 0
30 total = 0
31 for j in range(n):
32 prefix += S[j]
33 total += prefix
34 return total
35
36 def example5(A, B): # assume that A and B have equal length
37 ”””Return the number of elements in B equal to the sum of prefix sums in A.”””
38 n = len(A)
39 count = 0
40 for i in range(n): # loop from 0 to n-1
41 total = 0
42 for j in range(n): # loop from 0 to n-1
43 for k in range(1+j): # loop from 0 to j
44 total += A[k]
45 if B[i] == total:
46 count += 1
47 return count
Code Fragment 3.10: Some sample algorithms for analysis.
144 Chapter 3. Algorithm Analysis
R-3.32 Given an n-element sequence S, Algorithm D calls Algorithm E on each
element S[i]. Algorithm E runs in O(i) time when it is called on element
S[i]. What is the worst-case running time of Algorithm D?
R-3.33 Al and Bob are arguing about their algorithms. Al claims his O(n log n)-
time method is always faster than Bob’s O(n2)-time method. To settle the
issue, they perform a set of experiments. To Al’s dismay, they find that if
n < 100, the O(n2)-time algorithm runs faster, and only when n ≥ 100 is
the O(n log n)-time one better. Explain how this is possible.
R-3.34 There is a well-known city (which will go nameless here) whose inhabi-
tants have the reputation of enjoying a meal only if that meal is the best
they have ever experienced in their life. Otherwise, they hate it. Assum-
ing meal quality is distributed uniformly across a person’s life, describe
the expected number of times inhabitants of this city are happy with their
meals?
Creativity
C-3.35 Assuming it is possible to sort n numbers in O(n log n) time, show that it
is possible to solve the three-way set disjointness problem in O(n log n)
time.
C-3.36 Describe an efficient algorithm for finding the ten largest elements in a
sequence of size n. What is the running time of your algorithm?
C-3.37 Give an example of a positive function f (n) such that f (n) is neither O(n)
nor Ω(n).
C-3.38 Show that ∑ni=1 i
2 is O(n3).
C-3.39 Show that ∑ni=1 i/2
i < 2. (Hint: Try to bound this sum term by term with
a geometric progression.)
C-3.40 Show that logb f (n) is Θ(log f (n)) if b > 1 is a constant.
C-3.41 Describe an algorithm for finding both the minimum and maximum of n
numbers using fewer than 3n/2 comparisons. (Hint: First, construct a
group of candidate minimums and a group of candidate maximums.)
C-3.42 Bob built a Web site and gave the URL only to his n friends, which he
numbered from 1 to n. He told friend number i that he/she can visit the
Web site at most i times. Now Bob has a counter, C, keeping track of the
total number of visits to the site (but not the identities of who visits). What
is the minimum value for C such that Bob can know that one of his friends
has visited his/her maximum allowed number of times?
C-3.43 Draw a visual justification of Proposition 3.3 analogous to that of Fig-
ure 3.3(b) for the case when n is odd.
3.5. Exercises 145
C-3.44 Communication security is extremely important in computer networks,
and one way many network protocols achieve security is to encrypt mes-
sages. Typical cryptographic schemes for the secure transmission of mes-
sages over such networks are based on the fact that no efficient algorithms
are known for factoring large integers. Hence, if we can represent a secret
message by a large prime number p, we can transmit, over the network,
the number r = p · q, where q > p is another large prime number that acts
as the encryption key. An eavesdropper who obtains the transmitted num-
ber r on the network would have to factor r in order to figure out the secret
message p.
Using factoring to figure out a message is very difficult without knowing
the encryption key q. To understand why, consider the following naive
factoring algorithm:
for p in range(2,r):
if r % p == 0: # if p divides r
return The secret message is p!
a. Suppose that the eavesdropper uses the above algorithm and has a
computer that can carry out in 1 microsecond (1 millionth of a sec-
ond) a division between two integers of up to 100 bits each. Give an
estimate of the time that it will take in the worst case to decipher the
secret message p if the transmitted message r has 100 bits.
b. What is the worst-case time complexity of the above algorithm?
Since the input to the algorithm is just one large number r, assume
that the input size n is the number of bytes needed to store r, that is,
n = �(log2 r)/8� + 1, and that each division takes time O(n).
C-3.45 A sequence S contains n − 1 unique integers in the range [0, n − 1], that
is, there is one number from this range that is not in S. Design an O(n)-
time algorithm for finding that number. You are only allowed to use O(1)
additional space besides the sequence S itself.
C-3.46 Al says he can prove that all sheep in a flock are the same color:
Base case: One sheep. It is clearly the same color as itself.
Induction step: A flock of n sheep. Take a sheep, a, out. The remaining
n − 1 are all the same color by induction. Now put sheep a back in and
take out a different sheep, b. By induction, the n − 1 sheep (now with a)
are all the same color. Therefore, all the sheep in the flock are the same
color. What is wrong with Al’s “justification”?
C-3.47 Let S be a set of n lines in the plane such that no two are parallel and
no three meet in the same point. Show, by induction, that the lines in S
determine Θ(n2) intersection points.
146 Chapter 3. Algorithm Analysis
C-3.48 Consider the following “justification” that the Fibonacci function, F(n)
(see Proposition 3.20) is O(n):
Base case (n ≤ 2): F(1) = 1 and F(2) = 2.
Induction step (n > 2): Assume claim true for n′ < n. Consider n. F(n) =
F(n − 2) + F (n − 1). By induction, F(n − 2) is O(n − 2) and F(n − 1) is
O(n − 1). Then, F(n) is O((n − 2) + (n − 1)), by the identity presented in
Exercise R-3.11. Therefore, F(n) is O(n).
What is wrong with this “justification”?
C-3.49 Consider the Fibonacci function, F (n) (see Proposition 3.20). Show by
induction that F (n) is Ω((3/2)n).
C-3.50 Let p(x) be a polynomial of degree n, that is, p(x) = ∑ni=0 aix
i.
(a) Describe a simple O(n2)-time algorithm for computing p(x).
(b) Describe an O(n log n)-time algorithm for computing p(x), based upon
a more efficient calculation of xi.
(c) Now consider a rewriting of p(x) as
p(x) = a0 + x(a1 + x(a2 + x(a3 + ··· + x(an−1 + xan)··· ))),
which is known as Horner’s method. Using the big-Oh notation, charac-
terize the number of arithmetic operations this method executes.
C-3.51 Show that the summation ∑ni=1 log i is O(n log n).
C-3.52 Show that the summation ∑ni=1 log i is Ω(n log n).
C-3.53 An evil king has n bottles of wine, and a spy has just poisoned one of
them. Unfortunately, they do not know which one it is. The poison is very
deadly; just one drop diluted even a billion to one will still kill. Even so,
it takes a full month for the poison to take effect. Design a scheme for
determining exactly which one of the wine bottles was poisoned in just
one month’s time while expending O(log n) taste testers.
C-3.54 A sequence S contains n integers taken from the interval [0, 4n], with repe-
titions allowed. Describe an efficient algorithm for determining an integer
value k that occurs the most often in S. What is the running time of your
algorithm?
Projects
P-3.55 Perform an experimental analysis of the three algorithms prefix average1,
prefix average2, and prefix average3, from Section 3.3.3. Visualize their
running times as a function of the input size with a log-log chart.
P-3.56 Perform an experimental analysis that compares the relative running times
of the functions shown in Code Fragment 3.10.
Chapter Notes 147
P-3.57 Perform experimental analysis to test the hypothesis that Python’s sorted
method runs in O(n log n) time on average.
P-3.58 For each of the three algorithms, unique1, unique2, and unique3, which
solve the element uniqueness problem, perform an experimental analysis
to determine the largest value of n such that the given algorithm runs in
one minute or less.
Chapter Notes
The big-Oh notation has prompted several comments about its proper use [19, 49, 63].
Knuth [64, 63] defines it using the notation f (n) = O(g(n)), but says this “equality” is only
“one way.” We have chosen to take a more standard view of equality and view the big-Oh
notation as a set, following Brassard [19]. The reader interested in studying average-case
analysis is referred to the book chapter by Vitter and Flajolet [101]. For some additional
mathematical tools, please refer to Appendix B.
Chapter
4 Recursion
Contents
4.1 Illustrative Examples . . . . . . . . . . . . . . . . . . . . . . 150
4.1.1 The Factorial Function . . . . . . . . . . . . . . . . . . . 150
4.1.2 Drawing an English Ruler . . . . . . . . . . . . . . . . . . 152
4.1.3 Binary Search . . . . . . . . . . . . . . . . . . . . . . . . 155
4.1.4 File Systems . . . . . . . . . . . . . . . . . . . . . . . . . 157
4.2 Analyzing Recursive Algorithms . . . . . . . . . . . . . . . 161
4.3 Recursion Run Amok . . . . . . . . . . . . . . . . . . . . . 165
4.3.1 Maximum Recursive Depth in Python . . . . . . . . . . . 168
4.4 Further Examples of Recursion . . . . . . . . . . . . . . . . 169
4.4.1 Linear Recursion . . . . . . . . . . . . . . . . . . . . . . . 169
4.4.2 Binary Recursion . . . . . . . . . . . . . . . . . . . . . . 174
4.4.3 Multiple Recursion . . . . . . . . . . . . . . . . . . . . . 175
4.5 Designing Recursive Algorithms . . . . . . . . . . . . . . . 177
4.6 Eliminating Tail Recursion . . . . . . . . . . . . . . . . . . 178
4.7 Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . 180
149
One way to describe repetition within a computer program is the use of loops,
such as Python’s while-loop and for-loop constructs described in Section 1.4.2. An
entirely different way to achieve repetition is through a process known as recursion.
Recursion is a technique by which a function makes one or more calls to itself
during execution, or by which a data structure relies upon smaller instances of
the very same type of structure in its representation. There are many examples of
recursion in art and nature. For example, fractal patterns are naturally recursive. A
physical example of recursion used in art is in the Russian Matryoshka dolls. Each
doll is either made of solid wood, or is hollow and contains another Matryoshka
doll inside it.
In computing, recursion provides an elegant and powerful alternative for per-
forming repetitive tasks. In fact, a few programming languages (e.g., Scheme,
Smalltalk) do not explicitly support looping constructs and instead rely directly
on recursion to express repetition. Most modern programming languages support
functional recursion using the identical mechanism that is used to support tradi-
tional forms of function calls. When one invocation of the function make a recur-
sive call, that invocation is suspended until the recursive call completes.
Recursion is an important technique in the study of data structures and algo-
rithms. We will use it prominently in several later chapters of this book (most
notably, Chapters 8 and 12). In this chapter, we begin with the following four il-
lustrative examples of the use of recursion, providing a Python implementation for
each.
• The factorial function (commonly denoted as n!) is a classic mathematical
function that has a natural recursive definition.
• An English ruler has a recursive pattern that is a simple example of a fractal
structure.
• Binary search is among the most important computer algorithms. It allows
us to efficiently locate a desired value in a data set with upwards of billions
of entries.
• The file system for a computer has a recursive structure in which directories
can be nested arbitrarily deeply within other directories. Recursive algo-
rithms are widely used to explore and manage these file systems.
We then describe how to perform a formal analysis of the running time of a
recursive algorithm and we discuss some potential pitfalls when defining recur-
sions. In the balance of the chapter, we provide many more examples of recursive
algorithms, organized to highlight some common forms of design.
150 Chapter 4. Recursion
4.1 Illustrative Examples
4.1.1 The Factorial Function
To demonstrate the mechanics of recursion, we begin with a simple mathematical
example of computing the value of the factorial function. The factorial of a posi-
tive integer n, denoted n!, is defined as the product of the integers from 1 to n. If
n = 0, then n! is defined as 1 by convention. More formally, for any integer n ≥ 0,
n! =
{
1 if n = 0
n · (n − 1) · (n − 2)··· 3 · 2 · 1 if n ≥ 1.
For example, 5! = 5 · 4 · 3 · 2 · 1 = 120. The factorial function is important because
it is known to equal the number of ways in which n distinct items can be arranged
into a sequence, that is, the number of permutations of n items. For example, the
three characters a, b, and c can be arranged in 3! = 3 · 2 · 1 = 6 ways: abc, acb,
bac, bca, cab, and cba.
There is a natural recursive definition for the factorial function. To see this,
observe that 5! = 5 · (4 · 3 · 2 · 1) = 5 · 4!. More generally, for a positive integer n,
we can define n! to be n · (n − 1)!. This recursive definition can be formalized as
n! =
{
1 if n = 0
n · (n − 1)! if n ≥ 1.
This definition is typical of many recursive definitions. First, it contains one
or more base cases, which are defined nonrecursively in terms of fixed quantities.
In this case, n = 0 is the base case. It also contains one or more recursive cases,
which are defined by appealing to the definition of the function being defined.
A Recursive Implementation of the Factorial Function
Recursion is not just a mathematical notation; we can use recursion to design a
Python implementation of a factorial function, as shown in Code Fragment 4.1.
1 def factorial(n):
2 if n == 0:
3 return 1
4 else:
5 return n factorial(n−1)
Code Fragment 4.1: A recursive implementation of the factorial function.
4.1. Illustrative Examples 151
This function does not use any explicit loops. Repetition is provided by the
repeated recursive invocations of the function. There is no circularity in this defini-
tion, because each time the function is invoked, its argument is smaller by one, and
when a base case is reached, no further recursive calls are made.
We illustrate the execution of a recursive function using a recursion trace. Each
entry of the trace corresponds to a recursive call. Each new recursive function
call is indicated by a downward arrow to a new invocation. When the function
returns, an arrow showing this return is drawn and the return value may be indicated
alongside this arrow. An example of such a trace for the factorial function is shown
in Figure 4.1.
return 4 6 = 24
factorial(4)
factorial(1)
factorial(0)
factorial(3)
factorial(2)
return 1
return 1 1 = 1
return 2 1 = 2
return 3 2 = 6
Figure 4.1: A recursion trace for the call factorial(5).
A recursion trace closely mirrors the programming language’s execution of the
recursion. In Python, each time a function (recursive or otherwise) is called, a struc-
ture known as an activation record or frame is created to store information about
the progress of that invocation of the function. This activation record includes a
namespace for storing the function call’s parameters and local variables (see Sec-
tion 1.10 for a discussion of namespaces), and information about which command
in the body of the function is currently executing.
When the execution of a function leads to a nested function call, the execu-
tion of the former call is suspended and its activation record stores the place in the
source code at which the flow of control should continue upon return of the nested
call. This process is used both in the standard case of one function calling a dif-
ferent function, or in the recursive case in which a function invokes itself. The key
point is that there is a different activation record for each active call.
152 Chapter 4. Recursion
4.1.2 Drawing an English Ruler
In the case of computing a factorial, there is no compelling reason for preferring
recursion over a direct iteration with a loop. As a more complex example of the
use of recursion, consider how to draw the markings of a typical English ruler. For
each inch, we place a tick with a numeric label. We denote the length of the tick
designating a whole inch as the major tick length. Between the marks for whole
inches, the ruler contains a series of minor ticks, placed at intervals of 1/2 inch,
1/4 inch, and so on. As the size of the interval decreases by half, the tick length
decreases by one. Figure 4.2 demonstrates several such rulers with varying major
tick lengths (although not drawn to scale).
---- 0 ----- 0 --- 0
- - -
-- -- --
- - -
--- --- --- 1
- - -
-- -- --
- - -
---- 1 ---- --- 2
- - -
-- -- --
- - -
--- --- --- 3
- -
-- --
- -
---- 2 ----- 1
(a) (b) (c)
Figure 4.2: Three sample outputs of an English ruler drawing: (a) a 2-inch ruler
with major tick length 4; (b) a 1-inch ruler with major tick length 5; (c) a 3-inch
ruler with major tick length 3.
A Recursive Approach to Ruler Drawing
The English ruler pattern is a simple example of a fractal, that is, a shape that has
a self-recursive structure at various levels of magnification. Consider the rule with
major tick length 5 shown in Figure 4.2(b). Ignoring the lines containing 0 and 1,
let us consider how to draw the sequence of ticks lying between these lines. The
central tick (at 1/2 inch) has length 4. Observe that the two patterns of ticks above
and below this central tick are identical, and each has a central tick of length 3.
4.1. Illustrative Examples 153
In general, an interval with a central tick length L ≥ 1 is composed of:
• An interval with a central tick length L − 1
• A single tick of length L
• An interval with a central tick length L − 1
Although it is possible to draw such a ruler using an iterative process (see Ex-
ercise P-4.25), the task is considerably easier to accomplish with recursion. Our
implementation consists of three functions, as shown in Code Fragment 4.2. The
main function, draw ruler, manages the construction of the entire ruler. Its argu-
ments specify the total number of inches in the ruler and the major tick length. The
utility function, draw line, draws a single tick with a specified number of dashes
(and an optional string label, that is printed after the tick).
The interesting work is done by the recursive draw interval function. This
function draws the sequence of minor ticks within some interval, based upon the
length of the interval’s central tick. We rely on the intuition shown at the top of this
page, and with a base case when L = 0 that draws nothing. For L ≥ 1, the first and
last steps are performed by recursively calling draw interval(L − 1). The middle
step is performed by calling the function draw line(L).
1 def draw line(tick length, tick label= ):
2 ”””Draw one line with given tick length (followed by optional label).”””
3 line = - tick length
4 if tick label:
5 line += + tick label
6 print(line)
7
8 def draw interval(center length):
9 ”””Draw tick interval based upon a central tick length.”””
10 if center length > 0: # stop when length drops to 0
11 draw interval(center length − 1) # recursively draw top ticks
12 draw line(center length) # draw center tick
13 draw interval(center length − 1) # recursively draw bottom ticks
14
15 def draw ruler(num inches, major length):
16 ”””Draw English ruler with given number of inches, major tick length.”””
17 draw line(major length, 0 ) # draw inch 0 line
18 for j in range(1, 1 + num inches):
19 draw interval(major length − 1) # draw interior ticks for inch
20 draw line(major length, str(j)) # draw inch j line and label
Code Fragment 4.2: A recursive implementation of a function that draws a ruler.
154 Chapter 4. Recursion
Illustrating Ruler Drawing Using a Recursion Trace
The execution of the recursive draw interval function can be visualized using a re-
cursion trace. The trace for draw interval is more complicated than in the factorial
example, however, because each instance makes two recursive calls. To illustrate
this, we will show the recursion trace in a form that is reminiscent of an outline for
a document. See Figure 4.3.
(previous pattern repeats)
draw interval(3)
draw interval(2)
draw interval(1)
draw interval(1)
draw interval(0)
draw line(1)
draw interval(0)
draw interval(0)
draw line(1)
draw interval(0)
draw line(3)
draw interval(2)
draw line(2)
Output
Figure 4.3: A partial recursion trace for the call draw interval(3). The second
pattern of calls for draw interval(2) is not shown, but it is identical to the first.
4.1. Illustrative Examples 155
4.1.3 Binary Search
In this section, we describe a classic recursive algorithm, binary search, that is used
to efficiently locate a target value within a sorted sequence of n elements. This is
among the most important of computer algorithms, and it is the reason that we so
often store data in sorted order (as in Figure 4.4).
37
50 1 2 3 4 6 7 8 9 10 11 12 13 14 15
92 4 5 7 8 12 14 17 19 22 25 27 28 33
Figure 4.4: Values stored in sorted order within an indexable sequence, such as a
Python list. The numbers at top are the indices.
When the sequence is unsorted, the standard approach to search for a target
value is to use a loop to examine every element, until either finding the target or
exhausting the data set. This is known as the sequential search algorithm. This
algorithm runs in O(n) time (i.e., linear time) since every element is inspected in
the worst case.
When the sequence is sorted and indexable, there is a much more efficient
algorithm. (For intuition, think about how you would accomplish this task by
hand!) For any index j, we know that all the values stored at indices 0, . . . , j − 1
are less than or equal to the value at index j, and all the values stored at indices
j + 1, . . . , n − 1 are greater than or equal to that at index j. This observation allows
us to quickly “home in” on a search target using a variant of the children’s game
“high-low.” We call an element of the sequence a candidate if, at the current stage
of the search, we cannot rule out that this item matches the target. The algorithm
maintains two parameters, low and high, such that all the candidate entries have
index at least low and at most high. Initially, low = 0 and high = n − 1. We then
compare the target value to the median candidate, that is, the item data[mid] with
index
mid = �(low + high)/2� .
We consider three cases:
• If the target equals data[mid], then we have found the item we are looking
for, and the search terminates successfully.
• If target < data[mid], then we recur on the first half of the sequence, that is,
on the interval of indices from low to mid − 1.
• If target > data[mid], then we recur on the second half of the sequence, that
is, on the interval of indices from mid + 1 to high.
An unsuccessful search occurs if low > high, as the interval [low, high] is empty.
156 Chapter 4. Recursion
This algorithm is known as binary search. We give a Python implementation
in Code Fragment 4.3, and an illustration of the execution of the algorithm in Fig-
ure 4.5. Whereas sequential search runs in O(n) time, the more efficient binary
search runs in O(log n) time. This is a significant improvement, given that if n
is one billion, log n is only 30. (We defer our formal analysis of binary search’s
running time to Proposition 4.2 in Section 4.2.)
1 def binary search(data, target, low, high):
2 ”””Return True if target is found in indicated portion of a Python list.
3
4 The search only considers the portion from data[low] to data[high] inclusive.
5 ”””
6 if low > high:
7 return False # interval is empty; no match
8 else:
9 mid = (low + high) // 2
10 if target == data[mid]: # found a match
11 return True
12 elif target < data[mid]:
13 # recur on the portion left of the middle
14 return binary search(data, target, low, mid − 1)
15 else:
16 # recur on the portion right of the middle
17 return binary search(data, target, mid + 1, high)
Code Fragment 4.3: An implementation of the binary search algorithm.
mid
high
highlow
low mid
low mid
low=mid=high
high
14 19 22 25 27 28 33 37
6 7 8 9 10 11 12 13 14 15
7542 98
92 4 5 7 8 12 14 17
37332827252219
92 4 5 7 8 12 14 17 19 22 25 27 28 33 37
19 22 25 27 28 33 37
50 1 2 3 4
171412
92 4 5 7 8 12 17
Figure 4.5: Example of a binary search for target value 22.
4.1. Illustrative Examples 157
4.1.4 File Systems
Modern operating systems define file-system directories (which are also sometimes
called “folders”) in a recursive way. Namely, a file system consists of a top-level
directory, and the contents of this directory consists of files and other directories,
which in turn can contain files and other directories, and so on. The operating
system allows directories to be nested arbitrarily deep (as long as there is enough
space in memory), although there must necessarily be some base directories that
contain only files, not further subdirectories. A representation of a portion of such
a file system is given in Figure 4.6.
/user/rt/courses/
cs016/ cs252/
programs/homeworks/ projects/
papers/ demos/
hw1 hw2 hw3 pr1 pr2 pr3
grades
marketbuylow sellhigh
grades
Figure 4.6: A portion of a file system demonstrating a nested organization.
Given the recursive nature of the file-system representation, it should not come
as a surprise that many common behaviors of an operating system, such as copying
a directory or deleting a directory, are implemented with recursive algorithms. In
this section, we consider one such algorithm: computing the total disk usage for all
files and directories nested within a particular directory.
For illustration, Figure 4.7 portrays the disk space being used by all entries in
our sample file system. We differentiate between the immediate disk space used by
each entry and the cumulative disk space used by that entry and all nested features.
For example, the cs016 directory uses only 2K of immediate space, but a total of
249K of cumulative space.
158 Chapter 4. Recursion
/user/rt/courses/
cs016/ cs252/
programs/homeworks/ projects/
papers/ demos/hw1
3K
hw2
2K
hw3
4K
pr1
57K
pr2
97K
pr3
74K
grades
8K
market
4786K
buylow
26K
sellhigh
55K
grades
3K
2K 1K
1K
1K1K1K
1K 1K
10K 229K 4870K
82K 4787K
5124K
249K 4874K
Figure 4.7: The same portion of a file system given in Figure 4.6, but with additional
annotations to describe the amount of disk space that is used. Within the icon for
each file or directory is the amount of space directly used by that artifact. Above
the icon for each directory is an indication of the cumulative disk space used by
that directory and all its (recursive) contents.
The cumulative disk space for an entry can be computed with a simple recursive
algorithm. It is equal to the immediate disk space used by the entry plus the sum
of the cumulative disk space usage of any entries that are stored directly within
the entry. For example, the cumulative disk space for cs016 is 249K because it
uses 2K itself, 8K cumulatively in grades, 10K cumulatively in homeworks, and
229K cumulatively in programs. Pseudo-code for this algorithm is given in Code
Fragment 4.4.
Algorithm DiskUsage(path):
Input: A string designating a path to a file-system entry
Output: The cumulative disk space used by that entry and any nested entries
total = size(path) {immediate disk space used by the entry}
if path represents a directory then
for each child entry stored within directory path do
total = total + DiskUsage(child) {recursive call}
return total
Code Fragment 4.4: An algorithm for computing the cumulative disk space usage
nested at a file-system entry. Function size returns the immediate disk space of an
entry.
4.1. Illustrative Examples 159
Python’s os Module
To provide a Python implementation of a recursive algorithm for computing disk
usage, we rely on Python’s os module, which provides robust tools for interacting
with the operating system during the execution of a program. This is an extensive
library, but we will only need the following four functions:
• os.path.getsize(path)
Return the immediate disk usage (measured in bytes) for the file or directory
that is identified by the string path (e.g., /user/rt/courses).
• os.path.isdir(path)
Return True if entry designated by string path is a directory; False otherwise.
• os.listdir(path)
Return a list of strings that are the names of all entries within a directory
designated by string path. In our sample file system, if the parameter is
/user/rt/courses, this returns the list [ cs016 , cs252 ].
• os.path.join(path, filename)
Compose the path string and filename string using an appropriate operating
system separator between the two (e.g., the / character for a Unix/Linux
system, and the \ character for Windows). Return the string that represents
the full path to the file.
Python Implementation
With use of the os module, we now convert the algorithm from Code Fragment 4.4
into the Python implementation of Code Fragment 4.5.
1 import os
2
3 def disk usage(path):
4 ”””Return the number of bytes used by a file/folder and any descendents.”””
5 total = os.path.getsize(path) # account for direct usage
6 if os.path.isdir(path): # if this is a directory,
7 for filename in os.listdir(path): # then for each child:
8 childpath = os.path.join(path, filename) # compose full path to child
9 total += disk usage(childpath) # add child’s usage to total
10
11 print ( {0:<7} .format(total), path) # descriptive output (optional)
12 return total # return the grand total
Code Fragment 4.5: A recursive function for reporting disk usage of a file system.
160 Chapter 4. Recursion
Recursion Trace
To produce a different form of a recursion trace, we have included an extraneous
print statement within our Python implementation (line 11 of Code Fragment 4.5).
The precise format of that output intentionally mirrors output that is produced by
a classic Unix/Linux utility named du (for “disk usage”). It reports the amount of
disk space used by a directory and all contents nested within, and can produce a
verbose report, as given in Figure 4.8.
Our implementation of the disk usage function produces an identical result,
when executed on the sample file system portrayed in Figure 4.7. During the ex-
ecution of the algorithm, exactly one recursive call is made for each entry in the
portion of the file system that is considered. Because the print statement is made
just before returning from a recursive call, the output shown in Figure 4.8 reflects
the order in which the recursive calls are completed. In particular, we begin and
end a recursive call for each entry that is nested below another entry, computing
the nested cumulative disk space before we can compute and report the cumulative
disk space for the containing entry. For example, we do not know the cumulative
total for entry /user/rt/courses/cs016 until after the recursive calls regarding
contained entries grades, homeworks, and programs complete.
8 /user/rt/courses/cs016/grades
3 /user/rt/courses/cs016/homeworks/hw1
2 /user/rt/courses/cs016/homeworks/hw2
4 /user/rt/courses/cs016/homeworks/hw3
10 /user/rt/courses/cs016/homeworks
57 /user/rt/courses/cs016/programs/pr1
97 /user/rt/courses/cs016/programs/pr2
74 /user/rt/courses/cs016/programs/pr3
229 /user/rt/courses/cs016/programs
249 /user/rt/courses/cs016
26 /user/rt/courses/cs252/projects/papers/buylow
55 /user/rt/courses/cs252/projects/papers/sellhigh
82 /user/rt/courses/cs252/projects/papers
4786 /user/rt/courses/cs252/projects/demos/market
4787 /user/rt/courses/cs252/projects/demos
4870 /user/rt/courses/cs252/projects
3 /user/rt/courses/cs252/grades
4874 /user/rt/courses/cs252
5124 /user/rt/courses/
Figure 4.8: A report of the disk usage for the file system shown in Figure 4.7,
as generated by the Unix/Linux utility du (with command-line options -ak), or
equivalently by our disk usage function from Code Fragment 4.5.
4.2. Analyzing Recursive Algorithms 161
4.2 Analyzing Recursive Algorithms
In Chapter 3, we introduced mathematical techniques for analyzing the efficiency
of an algorithm, based upon an estimate of the number of primitive operations that
are executed by the algorithm. We use notations such as big-Oh to summarize the
relationship between the number of operations and the input size for a problem. In
this section, we demonstrate how to perform this type of running-time analysis to
recursive algorithms.
With a recursive algorithm, we will account for each operation that is performed
based upon the particular activation of the function that manages the flow of control
at the time it is executed. Stated another way, for each invocation of the function,
we only account for the number of operations that are performed within the body of
that activation. We can then account for the overall number of operations that are
executed as part of the recursive algorithm by taking the sum, over all activations,
of the number of operations that take place during each individual activation. (As
an aside, this is also the way we analyze a nonrecursive function that calls other
functions from within its body.)
To demonstrate this style of analysis, we revisit the four recursive algorithms
presented in Sections 4.1.1 through 4.1.4: factorial computation, drawing an En-
glish ruler, binary search, and computation of the cumulative size of a file system.
In general, we may rely on the intuition afforded by a recursion trace in recogniz-
ing how many recursive activations occur, and how the parameterization of each
activation can be used to estimate the number of primitive operations that occur
within the body of that activation. However, each of these recursive algorithms has
a unique structure and form.
Computing Factorials
It is relatively easy to analyze the efficiency of our function for computing fac-
torials, as described in Section 4.1.1. A sample recursion trace for our factorial
function was given in Figure 4.1. To compute factorial(n), we see that there are a
total of n + 1 activations, as the parameter decreases from n in the first call, to n − 1
in the second call, and so on, until reaching the base case with parameter 0.
It is also clear, given an examination of the function body in Code Fragment 4.1,
that each individual activation of factorial executes a constant number of opera-
tions. Therefore, we conclude that the overall number of operations for computing
factorial(n) is O(n), as there are n + 1 activations, each of which accounts for O(1)
operations.
162 Chapter 4. Recursion
Drawing an English Ruler
In analyzing the English ruler application from Section 4.1.2, we consider the fun-
damental question of how many total lines of output are generated by an initial call
to draw interval(c), where c denotes the center length. This is a reasonable bench-
mark for the overall efficiency of the algorithm as each line of output is based upon
a call to the draw line utility, and each recursive call to draw interval with nonzero
parameter makes exactly one direct call to draw line.
Some intuition may be gained by examining the source code and the recur-
sion trace. We know that a call to draw interval(c) for c > 0 spawns two calls to
draw interval(c−1) and a single call to draw line. We will rely on this intuition to
prove the following claim.
Proposition 4.1: For c ≥ 0, a call to draw interval(c) results in precisely 2c − 1
lines of output.
Justification: We provide a formal proof of this claim by induction (see Sec-
tion 3.4.3). In fact, induction is a natural mathematical technique for proving the
correctness and efficiency of a recursive process. In the case of the ruler, we
note that an application of draw interval(0) generates no output, and that 20 − 1 =
1 − 1 = 0. This serves as a base case for our claim.
More generally, the number of lines printed by draw interval(c) is one more
than twice the number generated by a call to draw interval(c−1), as one center
line is printed between two such recursive calls. By induction, we have that the
number of lines is thus 1 + 2 · (2c−1 − 1) = 1 + 2c − 2 = 2c − 1.
This proof is indicative of a more mathematically rigorous tool, known as a
recurrence equation that can be used to analyze the running time of a recursive
algorithm. That technique is discussed in Section 12.2.4, in the context of recursive
sorting algorithms.
Performing a Binary Search
Considering the running time of the binary search algorithm, as presented in Sec-
tion 4.1.3, we observe that a constant number of primitive operations are executed
at each recursive call of method of a binary search. Hence, the running time is
proportional to the number of recursive calls performed. We will show that at most
�log n� + 1 recursive calls are made during a binary search of a sequence having n
elements, leading to the following claim.
Proposition 4.2: The binary search algorithm runs in O(log n) time for a sorted
sequence with n elements.
4.2. Analyzing Recursive Algorithms 163
Justification: To prove this claim, a crucial fact is that with each recursive call
the number of candidate entries still to be searched is given by the value
high − low + 1.
Moreover, the number of remaining candidates is reduced by at least one half with
each recursive call. Specifically, from the definition of mid, the number of remain-
ing candidates is either
(mid − 1) − low + 1 =
⌊
low + high
2
⌋
− low ≤ high − low + 1
2
or
high − (mid + 1) + 1 = high −
⌊
low + high
2
⌋
≤ high − low + 1
2
.
Initially, the number of candidates is n; after the first call in a binary search, it is at
most n/2; after the second call, it is at most n/4; and so on. In general, after the jth
call in a binary search, the number of candidate entries remaining is at most n/2 j .
In the worst case (an unsuccessful search), the recursive calls stop when there are no
more candidate entries. Hence, the maximum number of recursive calls performed,
is the smallest integer r such that
n
2r
< 1.
In other words (recalling that we omit a logarithm’s base when it is 2), r > log n.
Thus, we have r = �log n� + 1,
which implies that binary search runs in O(log n) time.
Computing Disk Space Usage
Our final recursive algorithm from Section 4.1 was that for computing the overall
disk space usage in a specified portion of a file system. To characterize the “prob-
lem size” for our analysis, we let n denote the number of file-system entries in the
portion of the file system that is considered. (For example, the file system portrayed
in Figure 4.6 has n = 19 entries.)
To characterize the cumulative time spent for an initial call to the disk usage
function, we must analyze the total number of recursive invocations that are made,
as well as the number of operations that are executed within those invocations.
We begin by showing that there are precisely n recursive invocations of the
function, in particular, one for each entry in the relevant portion of the file system.
Intuitively, this is because a call to disk usage for a particular entry e of the file
system is only made from within the for loop of Code Fragment 4.5 when process-
ing the entry for the unique directory that contains e, and that entry will only be
explored once.
164 Chapter 4. Recursion
To formalize this argument, we can define the nesting level of each entry such
that the entry on which we begin has nesting level 0, entries stored directly within
it have nesting level 1, entries stored within those entries have nesting level 2, and
so on. We can prove by induction that there is exactly one recursive invocation of
disk usage upon each entry at nesting level k. As a base case, when k = 0, the only
recursive invocation made is the initial one. As the inductive step, once we know
there is exactly one recursive invocation for each entry at nesting level k, we can
claim that there is exactly one invocation for each entry e at nesting level k, made
within the for loop for the entry at level k that contains e.
Having established that there is one recursive call for each entry of the file
system, we return to the question of the overall computation time for the algorithm.
It would be great if we could argue that we spend O(1) time in any single invocation
of the function, but that is not the case. While there are a constant number of
steps reflect in the call to os.path.getsize to compute the disk usage directly at that
entry, when the entry is a directory, the body of the disk usage function includes a
for loop that iterates over all entries that are contained within that directory. In the
worst case, it is possible that one entry includes n − 1 others.
Based on this reasoning, we could conclude that there are O(n) recursive calls,
each of which runs in O(n) time, leading to an overall running time that is O(n2).
While this upper bound is technically true, it is not a tight upper bound. Remark-
ably, we can prove the stronger bound that the recursive algorithm for disk usage
completes in O(n) time! The weaker bound was pessimistic because it assumed
a worst-case number of entries for each directory. While it is possible that some
directories contain a number of entries proportional to n, they cannot all contain
that many. To prove the stronger claim, we choose to consider the overall number
of iterations of the for loop across all recursive calls. We claim there are precisely
n − 1 such iteration of that loop overall. We base this claim on the fact that each
iteration of that loop makes a recursive call to disk usage, and yet we have already
concluded that there are a total of n calls to disk usage (including the original call).
We therefore conclude that there are O(n) recursive calls, each of which uses O(1)
time outside the loop, and that the overall number of operations due to the loop
is O(n). Summing all of these bounds, the overall number of operations is O(n).
The argument we have made is more advanced than with the earlier examples
of recursion. The idea that we can sometimes get a tighter bound on a series of
operations by considering the cumulative effect, rather than assuming that each
achieves a worst case is a technique called amortization; we will see a further
example of such analysis in Section 5.3. Furthermore, a file system is an implicit
example of a data structure known as a tree, and our disk usage algorithm is really
a manifestation of a more general algorithm known as a tree traversal. Trees will
be the focus of Chapter 8, and our argument about the O(n) running time of the
disk usage algorithm will be generalized for tree traversals in Section 8.4.
4.3. Recursion Run Amok 165
4.3 Recursion Run Amok
Although recursion is a very powerful tool, it can easily be misused in various ways.
In this section, we examine several problems in which a poorly implemented recur-
sion causes drastic inefficiency, and we discuss some strategies for recognizing and
avoid such pitfalls.
We begin by revisiting the element uniqueness problem, defined on page 135
of Section 3.3.3. We can use the following recursive formulation to determine if
all n elements of a sequence are unique. As a base case, when n = 1, the elements
are trivially unique. For n ≥ 2, the elements are unique if and only if the first n − 1
elements are unique, the last n − 1 items are unique, and the first and last elements
are different (as that is the only pair that was not already checked as a subcase). A
recursive implementation based on this idea is given in Code Fragment 4.6, named
unique3 (to differentiate it from unique1 and unique2 from Chapter 3).
1 def unique3(S, start, stop):
2 ”””Return True if there are no duplicate elements in slice S[start:stop].”””
3 if stop − start <= 1: return True # at most one item
4 elif not unique(S, start, stop−1): return False # first part has duplicate
5 elif not unique(S, start+1, stop): return False # second part has duplicate
6 else: return S[start] != S[stop−1] # do first and last differ?
Code Fragment 4.6: Recursive unique3 for testing element uniqueness.
Unfortunately, this is a terribly inefficient use of recursion. The nonrecursive
part of each call uses O(1) time, so the overall running time will be proportional to
the total number of recursive invocations. To analyze the problem, we let n denote
the number of entries under consideration, that is, let n= stop − start.
If n = 1, then the running time of unique3 is O(1), since there are no recursive
calls for this case. In the general case, the important observation is that a single call
to unique3 for a problem of size n may result in two recursive calls on problems of
size n − 1. Those two calls with size n − 1 could in turn result in four calls (two
each) with a range of size n − 2, and thus eight calls with size n − 3 and so on.
Thus, in the worst case, the total number of function calls is given by the geometric
summation
1 + 2 + 4 + ··· + 2n−1,
which is equal to 2n − 1 by Proposition 3.5. Thus, the running time of function
unique3 is O(2n). This is an incredibly inefficient function for solving the ele-
ment uniqueness problem. Its inefficiency comes not from the fact that it uses
recursion—it comes from the fact that it uses recursion poorly, which is something
we address in Exercise C-4.11.
166 Chapter 4. Recursion
An Inefficient Recursion for Computing Fibonacci Numbers
In Section 1.8, we introduced a process for generating the Fibonacci numbers,
which can be defined recursively as follows:
F0 = 0
F1 = 1
Fn = Fn−2 + Fn−1 for n > 1.
Ironically, a direct implementation based on this definition results in the function
bad fibonacci shown in Code Fragment 4.7, which computes the sequence of Fi-
bonacci numbers by making two recursive calls in each non-base case.
1 def bad fibonacci(n):
2 ”””Return the nth Fibonacci number.”””
3 if n <= 1:
4 return n
5 else:
6 return bad fibonacci(n−2) + bad fibonacci(n−1)
Code Fragment 4.7: Computing the nth Fibonacci number using binary recursion.
Unfortunately, such a direct implementation of the Fibonacci formula results
in a terribly inefficient function. Computing the nth Fibonacci number in this way
requires an exponential number of calls to the function. Specifically, let cn denote
the number of calls performed in the execution of bad fibonacci(n). Then, we have
the following values for the cn’s:
c0 = 1
c1 = 1
c2 = 1 + c0 + c1 = 1 + 1 + 1 = 3
c3 = 1 + c1 + c2 = 1 + 1 + 3 = 5
c4 = 1 + c2 + c3 = 1 + 3 + 5 = 9
c5 = 1 + c3 + c4 = 1 + 5 + 9 = 15
c6 = 1 + c4 + c5 = 1 + 9 + 15 = 25
c7 = 1 + c5 + c6 = 1 + 15 + 25 = 41
c8 = 1 + c6 + c7 = 1 + 25 + 41 = 67
If we follow the pattern forward, we see that the number of calls more than doubles
for each two consecutive indices. That is, c4 is more than twice c2, c5 is more than
twice c3, c6 is more than twice c4, and so on. Thus, cn > 2
n/2, which means that
bad fibonacci(n) makes a number of calls that is exponential in n.
4.3. Recursion Run Amok 167
An Efficient Recursion for Computing Fibonacci Numbers
We were tempted into using the bad recursion formulation because of the way the
nth Fibonacci number, Fn, depends on the two previous values, Fn−2 and Fn−1. But
notice that after computing Fn−2, the call to compute Fn−1 requires its own recursive
call to compute Fn−2, as it does not have knowledge of the value of Fn−2 that was
computed at the earlier level of recursion. That is duplicative work. Worse yet, both
of those calls will need to (re)compute the value of Fn−3, as will the computation
of Fn−1. This snowballing effect is what leads to the exponential running time of
bad recursion.
We can compute Fn much more efficiently using a recursion in which each invo-
cation makes only one recursive call. To do so, we need to redefine the expectations
of the function. Rather than having the function return a single value, which is the
nth Fibonacci number, we define a recursive function that returns a pair of con-
secutive Fibonacci numbers (Fn, Fn−1), using the convention F−1 = 0. Although
it seems to be a greater burden to report two consecutive Fibonacci numbers in-
stead of one, passing this extra information from one level of the recursion to the
next makes it much easier to continue the process. (It allows us to avoid having
to recompute the second value that was already known within the recursion.) An
implementation based on this strategy is given in Code Fragment 4.8.
1 def good fibonacci(n):
2 ”””Return pair of Fibonacci numbers, F(n) and F(n-1).”””
3 if n <= 1:
4 return (n,0)
5 else:
6 (a, b) = good fibonacci(n−1)
7 return (a+b, a)
Code Fragment 4.8: Computing the nth Fibonacci number using linear recursion.
In terms of efficiency, the difference between the bad recursion and the good
recursion for this problem is like night and day. The bad fibonacci function uses
exponential time. We claim that the execution of function good fibonacci(n) takes
O(n) time. Each recursive call to good fibonacci decreases the argument n by 1;
therefore, a recursion trace includes a series of n function calls. Because the nonre-
cursive work for each call uses constant time, the overall computation executes in
O(n) time.
168 Chapter 4. Recursion
4.3.1 Maximum Recursive Depth in Python
Another danger in the misuse of recursion is known as infinite recursion. If each
recursive call makes another recursive call, without ever reaching a base case, then
we have an infinite series of such calls. This is a fatal error. An infinite recursion
can quickly swamp computing resources, not only due to rapid use of the CPU,
but because each successive call creates an activation record requiring additional
memory. A blatant example of an ill-formed recursion is the following:
def fib(n):
return fib(n) # fib(n) equals fib(n)
However, there are far more subtle errors that can lead to an infinite recursion.
Revisiting our implementation of binary search in Code Fragment 4.3, in the final
case (line 17) we make a recursive call on the right portion of the sequence, in
particular going from index mid+1 to high. Had that line instead been written as
return binary search(data, target, mid, high) # note the use of mid
this could result in an infinite recursion. In particular, when searching a range of
two elements, it becomes possible to make a recursive call on the identical range.
A programmer should ensure that each recursive call is in some way progress-
ing toward a base case (for example, by having a parameter value that decreases
with each call). However, to combat against infinite recursions, the designers of
Python made an intentional decision to limit the overall number of function acti-
vations that can be simultaneously active. The precise value of this limit depends
upon the Python distribution, but a typical default value is 1000. If this limit is
reached, the Python interpreter raises a RuntimeError with a message, maximum
recursion depth exceeded.
For many legitimate applications of recursion, a limit of 1000 nested function
calls suffices. For example, our binary search function (Section 4.1.3) has O(log n)
recursive depth, and so for the default recursive limit to be reached, there would
need to be 21000 elements (far, far more than the estimated number of atoms in the
universe). However, in the next section we discuss several algorithms that have
recursive depth proportional to n. Python’s artificial limit on the recursive depth
could disrupt such otherwise legitimate computations.
Fortunately, the Python interpreter can be dynamically reconfigured to change
the default recursive limit. This is done through use of a module named sys, which
supports a getrecursionlimit function and a setrecursionlimit. Sample usage of
those functions is demonstrated as follows:
import sys
old = sys.getrecursionlimit( ) # perhaps 1000 is typical
sys.setrecursionlimit(1000000) # change to allow 1 million nested calls
4.4. Further Examples of Recursion 169
4.4 Further Examples of Recursion
In the remainder of this chapter, we provide additional examples of the use of re-
cursion. We organize our presentation by considering the maximum number of
recursive calls that may be started from within the body of a single activation.
• If a recursive call starts at most one other, we call this a linear recursion.
• If a recursive call may start two others, we call this a binary recursion.
• If a recursive call may start three or more others, this is multiple recursion.
4.4.1 Linear Recursion
If a recursive function is designed so that each invocation of the body makes at
most one new recursive call, this is know as linear recursion. Of the recursions we
have seen so far, the implementation of the factorial function (Section 4.1.1) and
the good fibonacci function (Section 4.3) are clear examples of linear recursion.
More interestingly, the binary search algorithm (Section 4.1.3) is also an example
of linear recursion, despite the “binary” terminology in the name. The code for
binary search (Code Fragment 4.3) includes a case analysis with two branches that
lead to recursive calls, but only one of those calls can be reached during a particular
execution of the body.
A consequence of the definition of linear recursion is that any recursion trace
will appear as a single sequence of calls, as we originally portrayed for the factorial
function in Figure 4.1 of Section 4.1.1. Note that the linear recursion terminol-
ogy reflects the structure of the recursion trace, not the asymptotic analysis of the
running time; for example, we have seen that binary search runs in O(log n) time.
Summing the Elements of a Sequence Recursively
Linear recursion can be a useful tool for processing a data sequence, such as a
Python list. Suppose, for example, that we want to compute the sum of a sequence,
S, of n integers. We can solve this summation problem using linear recursion by
observing that the sum of all n integers in S is trivially 0, if n = 0, and otherwise
that it is the sum of the first n − 1 integers in S plus the last element in S. (See
Figure 4.9.)
4 3 6 2 8 9 3 2 8 5 1 7 2 8 3
5
7
0 1 2 3 4 6 7 8 9 10 11 12 13 14 15
Figure 4.9: Computing the sum of a sequence recursively, by adding the last number
to the sum of the first n − 1.
170 Chapter 4. Recursion
A recursive algorithm for computing the sum of a sequence of numbers based
on this intuition is implemented in Code Fragment 4.9.
1 def linear sum(S, n):
2 ”””Return the sum of the first n numbers of sequence S.”””
3 if n == 0:
4 return 0
5 else:
6 return linear sum(S, n−1) + S[n−1]
Code Fragment 4.9: Summing the elements of a sequence using linear recursion.
A recursion trace of the linear sum function for a small example is given in
Figure 4.10. For an input of size n, the linear sum algorithm makes n + 1 function
calls. Hence, it will take O(n) time, because it spends a constant amount of time
performing the nonrecursive part of each call. Moreover, we can also see that the
memory space used by the algorithm (in addition to the sequence S) is also O(n), as
we use a constant amount of memory space for each of the n + 1 activation records
in the trace at the time we make the final recursive call (with n = 0).
return 15 + S[4] = 15 + 8 = 23
linear sum(S, 5)
linear sum(S, 4)
linear sum(S, 3)
linear sum(S, 2)
linear sum(S, 1)
linear sum(S, 0)
return 0
return 0 + S[0] = 0 + 4 = 4
return 4 + S[1] = 4 + 3 = 7
return 7 + S[2] = 7 + 6 = 13
return 13 + S[3] = 13 + 2 = 15
Figure 4.10: Recursion trace for an execution of linear sum(S, 5) with input pa-
rameter S = [4, 3, 6, 2, 8].
4.4. Further Examples of Recursion 171
Reversing a Sequence with Recursion
Next, let us consider the problem of reversing the n elements of a sequence, S, so
that the first element becomes the last, the second element becomes second to the
last, and so on. We can solve this problem using linear recursion, by observing that
the reversal of a sequence can be achieved by swapping the first and last elements
and then recursively reversing the remaining elements. We present an implemen-
tation of this algorithm in Code Fragment 4.10, using the convention that the first
time we call this algorithm we do so as reverse(S, 0, len(S)).
1 def reverse(S, start, stop):
2 ”””Reverse elements in implicit slice S[start:stop].”””
3 if start < stop − 1: # if at least 2 elements:
4 S[start], S[stop−1] = S[stop−1], S[start] # swap first and last
5 reverse(S, start+1, stop−1) # recur on rest
Code Fragment 4.10: Reversing the elements of a sequence using linear recursion.
Note that there are two implicit base case scenarios: When start == stop, the
implicit range is empty, and when start == stop−1, the implicit range has only
one element. In either of these cases, there is no need for action, as a sequence
with zero elements or one element is trivially equal to its reversal. When otherwise
invoking recursion, we are guaranteed to make progress towards a base case, as
the difference, stop−start, decreases by two with each call (see Figure 4.11). If n
is even, we will eventually reach the start == stop case, and if n is odd, we will
eventually reach the start == stop − 1 case.
The above argument implies that the recursive algorithm of Code Fragment 4.10
is guaranteed to terminate after a total of 1 +
⌊
n
2
⌋
recursive calls. Since each call
involves a constant amount of work, the entire process runs in O(n) time.
6
3 6 2 8 9 5
5 9 8 2 6 3 4
5 3 6 2 8 9 4
5 9 8 2 6 3 4
5 9 6 2 8 3 4
50 1 2 3 4
4
Figure 4.11: A trace of the recursion for reversing a sequence. The shaded portion
has yet to be reversed.
172 Chapter 4. Recursion
Recursive Algorithms for Computing Powers
As another interesting example of the use of linear recursion, we consider the prob-
lem of raising a number x to an arbitrary nonnegative integer, n. That is, we wish
to compute the power function, defined as power(x, n) = xn. (We use the name
“power” for this discussion, to differentiate from the built-in function pow that pro-
vides such functionality.) We will consider two different recursive formulations for
the problem that lead to algorithms with very different performance.
A trivial recursive definition follows from the fact that xn = x · xn−1 for n > 0.
power(x, n) =
{
1 if n = 0
x · power(x, n − 1) otherwise.
This definition leads to a recursive algorithm shown in Code Fragment 4.11.
1 def power(x, n):
2 ”””Compute the value x n for integer n.”””
3 if n == 0:
4 return 1
5 else:
6 return x power(x, n−1)
Code Fragment 4.11: Computing the power function using trivial recursion.
A recursive call to this version of power(x, n) runs in O(n) time. Its recursion
trace has structure very similar to that of the factorial function from Figure 4.1,
with the parameter decreasing by one with each call, and constant work performed
at each of n + 1 levels.
However, there is a much faster way to compute the power function using an
alternative definition that employs a squaring technique. Let k =
⌊
n
2
⌋
denote the
floor of the division (expressed as n // 2 in Python). We consider the expression(
xk
)2
. When n is even,
⌊
n
2
⌋
= n2 and therefore
(
xk
)2
=
(
x
n
2
)2
= xn. When n is odd,⌊
n
2
⌋
= n−12 and
(
xk
)2
= xn−1, and therefore xn = x ·
(
xk
)2
, just as 213 = 2 · 26 · 26.
This analysis leads to the following recursive definition:
power(x, n) =
⎧⎪⎨
⎪⎩
1 if n = 0
x ·
(
power
(
x,
⌊
n
2
⌋))2
if n > 0 is odd(
power
(
x,
⌊
n
2
⌋))2
if n > 0 is even
If we were to implement this recursion making two recursive calls to compute
power(x,
⌊
n
2
⌋
) · power(x,
⌊
n
2
⌋
), a trace of the recursion would demonstrate O(n)
calls. We can perform significantly fewer operations by computing power(x,
⌊
n
2
⌋
)
as a partial result, and then multiplying it by itself. An implementation based on
this recursive definition is given in Code Fragment 4.12.
4.4. Further Examples of Recursion 173
1 def power(x, n):
2 ”””Compute the value x n for integer n.”””
3 if n == 0:
4 return 1
5 else:
6 partial = power(x, n // 2) # rely on truncated division
7 result = partial partial
8 if n % 2 == 1: # if n odd, include extra factor of x
9 result = x
10 return result
Code Fragment 4.12: Computing the power function using repeated squaring.
To illustrate the execution of our improved algorithm, Figure 4.12 provides a
recursion trace of the computation power(2, 13).
return 64 64 2 = 8192
power(2, 13)
power(2, 6)
power(2, 3)
power(2, 1)
power(2, 0)
return 1
return 1 1 2 = 2
return 2 2 2 = 8
return 8 8 = 64
Figure 4.12: Recursion trace for an execution of power(2, 13).
To analyze the running time of the revised algorithm, we observe that the expo-
nent in each recursive call of function power(x,n) is at most half of the preceding
exponent. As we saw with the analysis of binary search, the number of times that
we can divide n in half before getting to one or less is O(log n). Therefore, our new
formulation of the power function results in O(log n) recursive calls. Each individ-
ual activation of the function uses O(1) operations (excluding the recursive calls),
and so the total number of operations for computing power(x,n) is O(log n). This
is a significant improvement over the original O(n)-time algorithm.
The improved version also provides significant saving in reducing the memory
usage. The first version has a recursive depth of O(n), and therefore O(n) activation
records are simultaneous stored in memory. Because the recursive depth of the
improved version is O(log n), its memory usages is O(log n) as well.
174 Chapter 4. Recursion
4.4.2 Binary Recursion
When a function makes two recursive calls, we say that it uses binary recursion.
We have already seen several examples of binary recursion, most notably when
drawing the English ruler (Section 4.1.2), or in the bad fibonacci function of Sec-
tion 4.3. As another application of binary recursion, let us revisit the problem of
summing the n elements of a sequence, S, of numbers. Computing the sum of one
or zero elements is trivial. With two or more elements, we can recursively com-
pute the sum of the first half, and the sum of the second half, and add these sums
together. Our implementation of such an algorithm, in Code Fragment 4.13, is
initially invoked as binary sum(A, 0, len(A)).
1 def binary sum(S, start, stop):
2 ”””Return the sum of the numbers in implicit slice S[start:stop].”””
3 if start >= stop: # zero elements in slice
4 return 0
5 elif start == stop−1: # one element in slice
6 return S[start]
7 else: # two or more elements in slice
8 mid = (start + stop) // 2
9 return binary sum(S, start, mid) + binary sum(S, mid, stop)
Code Fragment 4.13: Summing the elements of a sequence using binary recursion.
To analyze algorithm binary sum, we consider, for simplicity, the case where
n is a power of two. Figure 4.13 shows the recursion trace of an execution of
binary sum(0, 8). We label each box with the values of parameters start:stop
for that call. The size of the range is divided in half at each recursive call, and
so the depth of the recursion is 1 + log2 n. Therefore, binary sum uses O(log n)
amount of additional space, which is a big improvement over the O(n) space used
by the linear sum function of Code Fragment 4.9. However, the running time of
binary sum is O(n), as there are 2n− 1 function calls, each requiring constant time.
0:1 1:2 2:3 4:5 6:7 7:83:4 5:6
0:2 4:6 6:82:4
0:4 4:8
0:8
Figure 4.13: Recursion trace for the execution of binary sum(0, 8).
4.4. Further Examples of Recursion 175
4.4.3 Multiple Recursion
Generalizing from binary recursion, we define multiple recursion as a process in
which a function may make more than two recursive calls. Our recursion for an-
alyzing the disk space usage of a file system (see Section 4.1.4) is an example of
multiple recursion, because the number of recursive calls made during one invoca-
tion was equal to the number of entries within a given directory of the file system.
Another common application of multiple recursion is when we want to enumer-
ate various configurations in order to solve a combinatorial puzzle. For example,
the following are all instances of what are known as summation puzzles:
pot + pan = bib
dog + cat = pig
boy + girl = baby
To solve such a puzzle, we need to assign a unique digit (that is, 0, 1, . . . , 9) to each
letter in the equation, in order to make the equation true. Typically, we solve such
a puzzle by using our human observations of the particular puzzle we are trying
to solve to eliminate configurations (that is, possible partial assignments of digits
to letters) until we can work though the feasible configurations left, testing for the
correctness of each one.
If the number of possible configurations is not too large, however, we can use
a computer to simply enumerate all the possibilities and test each one, without
employing any human observations. In addition, such an algorithm can use multiple
recursion to work through the configurations in a systematic way. We show pseudo-
code for such an algorithm in Code Fragment 4.14. To keep the description general
enough to be used with other puzzles, the algorithm enumerates and tests all k-
length sequences without repetitions of the elements of a given universe U . We
build the sequences of k elements by the following steps:
1. Recursively generating the sequences of k − 1 elements
2. Appending to each such sequence an element not already contained in it.
Throughout the execution of the algorithm, we use a set U to keep track of the
elements not contained in the current sequence, so that an element e has not been
used yet if and only if e is in U .
Another way to look at the algorithm of Code Fragment 4.14 is that it enumer-
ates every possible size-k ordered subset of U , and tests each subset for being a
possible solution to our puzzle.
For summation puzzles, U = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9} and each position in the
sequence corresponds to a given letter. For example, the first position could stand
for b, the second for o, the third for y, and so on.
176 Chapter 4. Recursion
Algorithm PuzzleSolve(k,S,U):
Input: An integer k, sequence S, and set U
Output: An enumeration of all k-length extensions to S using elements in U
without repetitions
for each e in U do
Add e to the end of S
Remove e from U {e is now being used}
if k = = 1 then
Test whether S is a configuration that solves the puzzle
if S solves the puzzle then
return “Solution found: ” S
else
PuzzleSolve(k−1,S,U) {a recursive call}
Remove e from the end of S
Add e back to U {e is now considered as unused}
Code Fragment 4.14: Solving a combinatorial puzzle by enumerating and testing
all possible configurations.
In Figure 4.14, we show a recursion trace of a call to PuzzleSolve(3, S,U ),
where S is empty and U = {a, b, c}. During the execution, all the permutations
of the three characters are generated and tested. Note that the initial call makes
three recursive calls, each of which in turn makes two more. If we had executed
PuzzleSolve(3, S,U ) on a set U consisting of four elements, the initial call would
have made four recursive calls, each of which would have a trace looking like the
one in Figure 4.14.
initial call
PuzzleSolve(3, ( ), {a,b,c})
PuzzleSolve(2, b, {a,c}) PuzzleSolve(2, c, {a,b})
PuzzleSolve(1, ca, {b})
PuzzleSolve(2, a, {b,c})
PuzzleSolve(1, ab, {c}) PuzzleSolve(1, ba, {c})
PuzzleSolve(1, bc, {a})PuzzleSolve(1, ac, {b}) PuzzleSolve(1, cb, {a})
acb
abc bac cab
bca cba
Figure 4.14: Recursion trace for an execution of PuzzleSolve(3, S,U ), where S is
empty and U = {a, b, c}. This execution generates and tests all permutations of a, b,
and c. We show the permutations generated directly below their respective boxes.
4.5. Designing Recursive Algorithms 177
4.5 Designing Recursive Algorithms
In general, an algorithm that uses recursion typically has the following form:
• Test for base cases. We begin by testing for a set of base cases (there should
be at least one). These base cases should be defined so that every possible
chain of recursive calls will eventually reach a base case, and the handling of
each base case should not use recursion.
• Recur. If not a base case, we perform one or more recursive calls. This recur-
sive step may involve a test that decides which of several possible recursive
calls to make. We should define each possible recursive call so that it makes
progress towards a base case.
Parameterizing a Recursion
To design a recursive algorithm for a given problem, it is useful to think of the
different ways we might define subproblems that have the same general structure
as the original problem. If one has difficulty finding the repetitive structure needed
to design a recursive algorithm, it is sometimes useful to work out the problem on
a few concrete examples to see how the subproblems should be defined.
A successful recursive design sometimes requires that we redefine the original
problem to facilitate similar-looking subproblems. Often, this involved reparam-
eterizing the signature of the function. For example, when performing a binary
search in a sequence, a natural function signature for a caller would appear as
binary search(data, target). However, in Section 4.1.3, we defined our function
with calling signature binary search(data, target, low, high), using the additional
parameters to demarcate sublists as the recursion proceeds. This change in param-
eterization is critical for binary search. If we had insisted on the cleaner signature,
binary search(data, target), the only way to invoke a search on half the list would
have been to make a new list instance with only those elements to send as the first
parameter. However, making a copy of half the list would already take O(n) time,
negating the whole benefit of the binary search algorithm.
If we wished to provide a cleaner public interface to an algorithm like bi-
nary search, without bothering a user with the extra parameters, a standard tech-
nique is to make one function for public use with the cleaner interface, such as
binary search(data, target), and then having its body invoke a nonpublic utility
function having the desired recursive parameters.
You will see that we similarly reparameterized the recursion in several other ex-
amples of this chapter (e.g., reverse, linear sum, binary sum). We saw a different
approach to redefining a recursion in our good fibonacci implementation, by in-
tentionally strengthening the expectation of what is returned (in that case, returning
a pair of numbers rather than a single number).
178 Chapter 4. Recursion
4.6 Eliminating Tail Recursion
The main benefit of a recursive approach to algorithm design is that it allows us to
succinctly take advantage of a repetitive structure present in many problems. By
making our algorithm description exploit the repetitive structure in a recursive way,
we can often avoid complex case analyses and nested loops. This approach can
lead to more readable algorithm descriptions, while still being quite efficient.
However, the usefulness of recursion comes at a modest cost. In particular, the
Python interpreter must maintain activation records that keep track of the state of
each nested call. When computer memory is at a premium, it is useful in some
cases to be able to derive nonrecursive algorithms from recursive ones.
In general, we can use the stack data structure, which we will introduce in
Section 6.1, to convert a recursive algorithm into a nonrecursive algorithm by man-
aging the nesting of the recursive structure ourselves, rather than relying on the
interpreter to do so. Although this only shifts the memory usage from the inter-
preter to our stack, we may be able to reduce the memory usage by storing only the
minimal information necessary.
Even better, some forms of recursion can be eliminated without any use of
axillary memory. A notable such form is known as tail recursion. A recursion
is a tail recursion if any recursive call that is made from one context is the very
last operation in that context, with the return value of the recursive call (if any)
immediately returned by the enclosing recursion. By necessity, a tail recursion
must be a linear recursion (since there is no way to make a second recursive call if
you must immediately return the result of the first).
Of the recursive functions demonstrated in this chapter, the binary search func-
tion of Code Fragment 4.3 and the reverse function of Code Fragment 4.10 are
examples of tail recursion. Several others of our linear recursions are almost like
tail recursion, but not technically so. For example, our factorial function of Code
Fragment 4.1 is not a tail recursion. It concludes with the command:
return n factorial(n−1)
This is not a tail recursion because an additional multiplication is performed after
the recursive call is completed. For similar reasons, the linear sum function of
Code Fragment 4.9 and the good fibonacci function of Code Fragment 4.7 fail to
be tail recursions.
Any tail recursion can be reimplemented nonrecursively by enclosing the body
in a loop for repetition, and replacing a recursive call with new parameters by a
reassignment of the existing parameters to those values. As a tangible example, our
binary search function can be reimplemented as shown in Code Fragment 4.15.
We initialize variables low and high, just prior to our while loop, to represent the
full extent of the sequence. Then, during each pass of the loop, we either find
4.6. Eliminating Tail Recursion 179
1 def binary search iterative(data, target):
2 ”””Return True if target is found in the given Python list.”””
3 low = 0
4 high = len(data)−1
5 while low <= high:
6 mid = (low + high) // 2
7 if target == data[mid]: # found a match
8 return True
9 elif target < data[mid]:
10 high = mid − 1 # only consider values left of mid
11 else:
12 low = mid + 1 # only consider values right of mid
13 return False # loop ended without success
Code Fragment 4.15: A nonrecursive implementation of binary search.
the target, or we narrow the range of the candidate subsequence. Where we made
the recursive call binary search(data, target, low, mid −1) in the original version,
we simply replace high = mid − 1 in our new version and then continue to the
next iteration of the loop. Our original base case condition of low > high has
simply been replaced by the opposite loop condition while low <= high. In our
new implementation, we return False to designate a failed search if the while loop
ends (that is, without having ever returned True from within).
We can similarly develop a nonrecursive implementation (Code Fragment 4.16)
of the original recursive reverse method of Code Fragment 4.10.
1 def reverse iterative(S):
2 ”””Reverse elements in sequence S.”””
3 start, stop = 0, len(S)
4 while start < stop − 1:
5 S[start], S[stop−1] = S[stop−1], S[start] # swap first and last
6 start, stop = start + 1, stop − 1 # narrow the range
Code Fragment 4.16: Reversing the elements of a sequence using iteration.
In this new version, we update the values start and stop during each pass of the
loop, exiting once we reach the case of having one or less elements in that range.
Many other linear recursions can be expressed quite efficiently with iteration,
even if they were not formally tail recursions. For example, there are trivial non-
recursive implementations for computing factorials, summing elements of a se-
quence, or computing Fibonacci numbers efficiently. In fact, our implementation
of a Fibonacci generator, from Section 1.8, produces each subsequent value in O(1)
time, and thus takes O(n) time to generate the nth entry in the series.
180 Chapter 4. Recursion
4.7 Exercises
For help with exercises, please visit the site, www.wiley.com/college/goodrich.
Reinforcement
R-4.1 Describe a recursive algorithm for finding the maximum element in a se-
quence, S, of n elements. What is your running time and space usage?
R-4.2 Draw the recursion trace for the computation of power(2, 5), using the
traditional function implemented in Code Fragment 4.11.
R-4.3 Draw the recursion trace for the computation of power(2, 18), using the
repeated squaring algorithm, as implemented in Code Fragment 4.12.
R-4.4 Draw the recursion trace for the execution of function reverse(S, 0, 5)
(Code Fragment 4.10) on S = [4, 3, 6, 2, 6].
R-4.5 Draw the recursion trace for the execution of function PuzzleSolve(3, S,U )
(Code Fragment 4.14), where S is empty and U = {a, b, c, d}.
R-4.6 Describe a recursive function for computing the nth Harmonic number,
Hn = ∑
n
i=1 1/i.
R-4.7 Describe a recursive function for converting a string of digits into the in-
teger it represents. For example, 13531 represents the integer 13, 531.
R-4.8 Isabel has an interesting way of summing up the values in a sequence A of
n integers, where n is a power of two. She creates a new sequence B of half
the size of A and sets B[i] = A[2i] + A[2i + 1], for i = 0, 1, . . . , (n/2)− 1. If
B has size 1, then she outputs B[0]. Otherwise, she replaces A with B, and
repeats the process. What is the running time of her algorithm?
Creativity
C-4.9 Write a short recursive Python function that finds the minimum and max-
imum values in a sequence without using any loops.
C-4.10 Describe a recursive algorithm to compute the integer part of the base-two
logarithm of n using only addition and integer division.
C-4.11 Describe an efficient recursive function for solving the element unique-
ness problem, which runs in time that is at most O(n2) in the worst case
without using sorting.
C-4.12 Give a recursive algorithm to compute the product of two positive integers,
m and n, using only addition and subtraction.
http:\\www.wiley.com/college/goodrich
4.7. Exercises 181
C-4.13 In Section 4.2 we prove by induction that the number of lines printed by
a call to draw interval(c) is 2c − 1. Another interesting question is how
many dashes are printed during that process. Prove by induction that the
number of dashes printed by draw interval(c) is 2c+1 − c − 2.
C-4.14 In the Towers of Hanoi puzzle, we are given a platform with three pegs, a,
b, and c, sticking out of it. On peg a is a stack of n disks, each larger than
the next, so that the smallest is on the top and the largest is on the bottom.
The puzzle is to move all the disks from peg a to peg c, moving one disk
at a time, so that we never place a larger disk on top of a smaller one.
See Figure 4.15 for an example of the case n = 4. Describe a recursive
algorithm for solving the Towers of Hanoi puzzle for arbitrary n. (Hint:
Consider first the subproblem of moving all but the nth disk from peg a to
another peg using the third as “temporary storage.”)
Figure 4.15: An illustration of the Towers of Hanoi puzzle.
C-4.15 Write a recursive function that will output all the subsets of a set of n
elements (without repeating any subsets).
C-4.16 Write a short recursive Python function that takes a character string s and
outputs its reverse. For example, the reverse of pots&pans would be
snap&stop .
C-4.17 Write a short recursive Python function that determines if a string s is a
palindrome, that is, it is equal to its reverse. For example, racecar and
gohangasalamiimalasagnahog are palindromes.
C-4.18 Use recursion to write a Python function for determining if a string s has
more vowels than consonants.
C-4.19 Write a short recursive Python function that rearranges a sequence of in-
teger values so that all the even values appear before all the odd values.
C-4.20 Given an unsorted sequence, S, of integers and an integer k, describe a
recursive algorithm for rearranging the elements in S so that all elements
less than or equal to k come before any elements larger than k. What is
the running time of your algorithm on a sequence of n values?
182 Chapter 4. Recursion
C-4.21 Suppose you are given an n-element sequence, S, containing distinct in-
tegers that are listed in increasing order. Given a number k, describe a
recursive algorithm to find two integers in S that sum to k, if such a pair
exists. What is the running time of your algorithm?
C-4.22 Develop a nonrecursive implementation of the version of power from
Code Fragment 4.12 that uses repeated squaring.
Projects
P-4.23 Implement a recursive function with signature find(path, filename) that
reports all entries of the file system rooted at the given path having the
given file name.
P-4.24 Write a program for solving summation puzzles by enumerating and test-
ing all possible configurations. Using your program, solve the three puz-
zles given in Section 4.4.3.
P-4.25 Provide a nonrecursive implementation of the draw interval function for
the English ruler project of Section 4.1.2. There should be precisely 2c −1
lines of output if c represents the length of the center tick. If incrementing
a counter from 0 to 2c − 2, the number of dashes for each tick line should
be exactly one more than the number of consecutive 1’s at the end of the
binary representation of the counter.
P-4.26 Write a program that can solve instances of the Tower of Hanoi problem
(from Exercise C-4.14).
P-4.27 Python’s os module provides a function with signature walk(path) that
is a generator yielding the tuple (dirpath, dirnames, filenames) for each
subdirectory of the directory identified by string path, such that string
dirpath is the full path to the subdirectory, dirnames is a list of the names
of the subdirectories within dirpath, and filenames is a list of the names
of non-directory entries of dirpath. For example, when visiting the cs016
subdirectory of the file system shown in Figure 4.6, the walk would yield
( /user/rt/courses/cs016 , [ homeworks , programs ], [ grades ]).
Give your own implementation of such a walk function.
Chapter Notes
The use of recursion in programs belongs to the folkore of computer science (for example,
see the article of Dijkstra [36]). It is also at the heart of functional programming languages
(for example, see the classic book by Abelson, Sussman, and Sussman [1]). Interestingly,
binary search was first published in 1946, but was not published in a fully correct form
until 1962. For further discussions on lessons learned, please see papers by Bentley [14]
and Lesuisse [68].
Chapter
5 Array-Based Sequences
Contents
5.1 Python’s Sequence Types . . . . . . . . . . . . . . . . . . . 184
5.2 Low-Level Arrays . . . . . . . . . . . . . . . . . . . . . . . . 185
5.2.1 Referential Arrays . . . . . . . . . . . . . . . . . . . . . . 187
5.2.2 Compact Arrays in Python . . . . . . . . . . . . . . . . . 190
5.3 Dynamic Arrays and Amortization . . . . . . . . . . . . . . 192
5.3.1 Implementing a Dynamic Array . . . . . . . . . . . . . . . 195
5.3.2 Amortized Analysis of Dynamic Arrays . . . . . . . . . . . 197
5.3.3 Python’s List Class . . . . . . . . . . . . . . . . . . . . . 201
5.4 Efficiency of Python’s Sequence Types . . . . . . . . . . . 202
5.4.1 Python’s List and Tuple Classes . . . . . . . . . . . . . . 202
5.4.2 Python’s String Class . . . . . . . . . . . . . . . . . . . . 208
5.5 Using Array-Based Sequences . . . . . . . . . . . . . . . . 210
5.5.1 Storing High Scores for a Game . . . . . . . . . . . . . . 210
5.5.2 Sorting a Sequence . . . . . . . . . . . . . . . . . . . . . 214
5.5.3 Simple Cryptography . . . . . . . . . . . . . . . . . . . . 216
5.6 Multidimensional Data Sets . . . . . . . . . . . . . . . . . . 219
5.7 Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . 224
184 Chapter 5. Array-Based Sequences
5.1 Python’s Sequence Types
In this chapter, we explore Python’s various “sequence” classes, namely the built-
in list, tuple, and str classes. There is significant commonality between these
classes, most notably: each supports indexing to access an individual element of a
sequence, using a syntax such as seq[k], and each uses a low-level concept known
as an array to represent the sequence. However, there are significant differences in
the abstractions that these classes represent, and in the way that instances of these
classes are represented internally by Python. Because these classes are used so
widely in Python programs, and because they will become building blocks upon
which we will develop more complex data structures, it is imperative that we estab-
lish a clear understanding of both the public behavior and inner workings of these
classes.
Public Behaviors
A proper understanding of the outward semantics for a class is a necessity for a
good programmer. While the basic usage of lists, strings, and tuples may seem
straightforward, there are several important subtleties regarding the behaviors as-
sociated with these classes (such as what it means to make a copy of a sequence, or
to take a slice of a sequence). Having a misunderstanding of a behavior can easily
lead to inadvertent bugs in a program. Therefore, we establish an accurate men-
tal model for each of these classes. These images will help when exploring more
advanced usage, such as representing a multidimensional data set as a list of lists.
Implementation Details
A focus on the internal implementations of these classes seems to go against our
stated principles of object-oriented programming. In Section 2.1.2, we emphasized
the principle of encapsulation, noting that the user of a class need not know about
the internal details of the implementation. While it is true that one only needs to
understand the syntax and semantics of a class’s public interface in order to be able
to write legal and correct code that uses instances of the class, the efficiency of a
program depends greatly on the efficiency of the components upon which it relies.
Asymptotic and Experimental Analyses
In describing the efficiency of various operations for Python’s sequence classes,
we will rely on the formal asymptotic analysis notations established in Chapter 3.
We will also perform experimental analyses of the primary operations to provide
empirical evidence that is consistent with the more theoretical asymptotic analyses.
5.2. Low-Level Arrays 185
5.2 Low-Level Arrays
To accurately describe the way in which Python represents the sequence types,
we must first discuss aspects of the low-level computer architecture. The primary
memory of a computer is composed of bits of information, and those bits are typ-
ically grouped into larger units that depend upon the precise system architecture.
Such a typical unit is a byte, which is equivalent to 8 bits.
A computer system will have a huge number of bytes of memory, and to keep
track of what information is stored in what byte, the computer uses an abstraction
known as a memory address. In effect, each byte of memory is associated with a
unique number that serves as its address (more formally, the binary representation
of the number serves as the address). In this way, the computer system can refer
to the data in “byte #2150” versus the data in “byte #2157,” for example. Memory
addresses are typically coordinated with the physical layout of the memory system,
and so we often portray the numbers in sequential fashion. Figure 5.1 provides
such a diagram, with the designated memory address for each byte.
21
60
21
45
21
46
21
47
21
48
21
49
21
50
21
51
21
52
21
53
21
54
21
55
21
56
21
57
21
58
21
44
21
59
Figure 5.1: A representation of a portion of a computer’s memory, with individual
bytes labeled with consecutive memory addresses.
Despite the sequential nature of the numbering system, computer hardware is
designed, in theory, so that any byte of the main memory can be efficiently accessed
based upon its memory address. In this sense, we say that a computer’s main mem-
ory performs as random access memory (RAM). That is, it is just as easy to retrieve
byte #8675309 as it is to retrieve byte #309. (In practice, there are complicating
factors including the use of caches and external memory; we address some of those
issues in Chapter 15.) Using the notation for asymptotic analysis, we say that any
individual byte of memory can be stored or retrieved in O(1) time.
In general, a programming language keeps track of the association between
an identifier and the memory address in which the associated value is stored. For
example, identifier x might be associated with one value stored in memory, while y
is associated with another value stored in memory. A common programming task
is to keep track of a sequence of related objects. For example, we may want a video
game to keep track of the top ten scores for that game. Rather than use ten different
variables for this task, we would prefer to use a single name for the group and use
index numbers to refer to the high scores in that group.
186 Chapter 5. Array-Based Sequences
A group of related variables can be stored one after another in a contiguous
portion of the computer’s memory. We will denote such a representation as an
array. As a tangible example, a text string is stored as an ordered sequence of
individual characters. In Python, each character is represented using the Unicode
character set, and on most computing systems, Python internally represents each
Unicode character with 16 bits (i.e., 2 bytes). Therefore, a six-character string, such
as SAMPLE , would be stored in 12 consecutive bytes of memory, as diagrammed
in Figure 5.2.
0
M P L EAS
21
60
21
50
21
51
21
52
21
53
21
54
21
55
21
56
21
57
21
58
21
44
21
59
54321
21
45
21
46
21
47
21
48
21
49
Figure 5.2: A Python string embedded as an array of characters in the computer’s
memory. We assume that each Unicode character of the string requires two bytes
of memory. The numbers below the entries are indices into the string.
We describe this as an array of six characters, even though it requires 12 bytes
of memory. We will refer to each location within an array as a cell, and will use an
integer index to describe its location within the array, with cells numbered starting
with 0, 1, 2, and so on. For example, in Figure 5.2, the cell of the array with index 4
has contents L and is stored in bytes 2154 and 2155 of memory.
Each cell of an array must use the same number of bytes. This requirement is
what allows an arbitrary cell of the array to be accessed in constant time based on
its index. In particular, if one knows the memory address at which an array starts
(e.g., 2146 in Figure 5.2), the number of bytes per element (e.g., 2 for a Unicode
character), and a desired index within the array, the appropriate memory address
can be computed using the calculation, start + cellsize index. By this formula,
the cell at index 0 begins precisely at the start of the array, the cell at index 1 begins
precisely cellsize bytes beyond the start of the array, and so on. As an example,
cell 4 of Figure 5.2 begins at memory location 2146 + 2 · 4 = 2146 + 8 = 2154.
Of course, the arithmetic for calculating memory addresses within an array can
be handled automatically. Therefore, a programmer can envision a more typical
high-level abstraction of an array of characters as diagrammed in Figure 5.3.
0
AS M P L E
3 4 51 2
Figure 5.3: A higher-level abstraction for the string portrayed in Figure 5.2.
5.2. Low-Level Arrays 187
5.2.1 Referential Arrays
As another motivating example, assume that we want a medical information system
to keep track of the patients currently assigned to beds in a certain hospital. If we
assume that the hospital has 200 beds, and conveniently that those beds are num-
bered from 0 to 199, we might consider using an array-based structure to maintain
the names of the patients currently assigned to those beds. For example, in Python
we might use a list of names, such as:
[ Rene , Joseph , Janet , Jonas , Helen , Virginia , ... ]
To represent such a list with an array, Python must adhere to the requirement that
each cell of the array use the same number of bytes. Yet the elements are strings,
and strings naturally have different lengths. Python could attempt to reserve enough
space for each cell to hold the maximum length string (not just of currently stored
strings, but of any string we might ever want to store), but that would be wasteful.
Instead, Python represents a list or tuple instance using an internal storage
mechanism of an array of object references. At the lowest level, what is stored
is a consecutive sequence of memory addresses at which the elements of the se-
quence reside. A high-level diagram of such a list is shown in Figure 5.4.
0 31 2 54
Rene Virginia
Joseph Helen
JonasJanet
Figure 5.4: An array storing references to strings.
Although the relative size of the individual elements may vary, the number of
bits used to store the memory address of each element is fixed (e.g., 64-bits per
address). In this way, Python can support constant-time access to a list or tuple
element based on its index.
In Figure 5.4, we characterize a list of strings that are the names of the patients
in a hospital. It is more likely that a medical information system would manage
more comprehensive information on each patient, perhaps represented as an in-
stance of a Patient class. From the perspective of the list implementation, the same
principle applies: The list will simply keep a sequence of references to those ob-
jects. Note as well that a reference to the None object can be used as an element
of the list to represent an empty bed in the hospital.
188 Chapter 5. Array-Based Sequences
The fact that lists and tuples are referential structures is significant to the se-
mantics of these classes. A single list instance may include multiple references
to the same object as elements of the list, and it is possible for a single object to
be an element of two or more lists, as those lists simply store references back to
that object. As an example, when you compute a slice of a list, the result is a new
list instance, but that new list has references to the same elements that are in the
original list, as portrayed in Figure 5.5.
3 4 5 6 70 1 2
0 1 2
primes:
temp:
3 1152 1917137
Figure 5.5: The result of the command temp = primes[3:6].
When the elements of the list are immutable objects, as with the integer in-
stances in Figure 5.5, the fact that the two lists share elements is not that signifi-
cant, as neither of the lists can cause a change to the shared object. If, for example,
the command temp[2] = 15 were executed from this configuration, that does not
change the existing integer object; it changes the reference in cell 2 of the temp list
to reference a different object. The resulting configuration is shown in Figure 5.6.
3 4 5 6 70 1 2
0 1 2
primes:
temp:
131152
15
3 19177
Figure 5.6: The result of the command temp[2] = 15 upon the configuration por-
trayed in Figure 5.5.
The same semantics is demonstrated when making a new list as a copy of an
existing one, with a syntax such as backup = list(primes). This produces a new
list that is a shallow copy (see Section 2.6), in that it references the same elements
as in the first list. With immutable elements, this point is moot. If the contents of
the list were of a mutable type, a deep copy, meaning a new list with new elements,
can be produced by using the deepcopy function from the copy module.
5.2. Low-Level Arrays 189
As a more striking example, it is a common practice in Python to initialize an
array of integers using a syntax such as counters = [0] 8. This syntax produces
a list of length eight, with all eight elements being the value zero. Technically, all
eight cells of the list reference the same object, as portrayed in Figure 5.7.
4 5 6 70 1 2 3
counters:
0
Figure 5.7: The result of the command data = [0] 8.
At first glance, the extreme level of aliasing in this configuration may seem
alarming. However, we rely on the fact that the referenced integer is immutable.
Even a command such as counters[2] += 1 does not technically change the value
of the existing integer instance. This computes a new integer, with value 0 + 1, and
sets cell 2 to reference the newly computed value. The resulting configuration is
shown in Figure 5.8.
4 5 6 70 1 2 3
counters:
0
1
Figure 5.8: The result of command data[2] += 1 upon the list from Figure 5.7.
As a final manifestation of the referential nature of lists, we note that the extend
command is used to add all elements from one list to the end of another list. The
extended list does not receive copies of those elements, it receives references to
those elements. Figure 5.9 portrays the effect of a call to extend.
3 4 5 6 7 8 109210
0 1 2
primes:
extras:
29 317 193 1713112 5 23
Figure 5.9: The effect of command primes.extend(extras), shown in light gray.
190 Chapter 5. Array-Based Sequences
5.2.2 Compact Arrays in Python
In the introduction to this section, we emphasized that strings are represented using
an array of characters (not an array of references). We will refer to this more direct
representation as a compact array because the array is storing the bits that represent
the primary data (characters, in the case of strings).
0
AS M P L E
3 4 51 2
Compact arrays have several advantages over referential structures in terms
of computing performance. Most significantly, the overall memory usage will be
much lower for a compact structure because there is no overhead devoted to the
explicit storage of the sequence of memory references (in addition to the primary
data). That is, a referential structure will typically use 64-bits for the memory
address stored in the array, on top of whatever number of bits are used to represent
the object that is considered the element. Also, each Unicode character stored in
a compact array within a string typically requires 2 bytes. If each character were
stored independently as a one-character string, there would be significantly more
bytes used.
As another case study, suppose we wish to store a sequence of one million,
64-bit integers. In theory, we might hope to use only 64 million bits. However, we
estimate that a Python list will use four to five times as much memory. Each element
of the list will result in a 64-bit memory address being stored in the primary array,
and an int instance being stored elsewhere in memory. Python allows you to query
the actual number of bytes being used for the primary storage of any object. This
is done using the getsizeof function of the sys module. On our system, the size of
a typical int object requires 14 bytes of memory (well beyond the 4 bytes needed
for representing the actual 64-bit number). In all, the list will be using 18 bytes per
entry, rather than the 4 bytes that a compact list of integers would require.
Another important advantage to a compact structure for high-performance com-
puting is that the primary data are stored consecutively in memory. Note well that
this is not the case for a referential structure. That is, even though a list maintains
careful ordering of the sequence of memory addresses, where those elements reside
in memory is not determined by the list. Because of the workings of the cache and
memory hierarchies of computers, it is often advantageous to have data stored in
memory near other data that might be used in the same computations.
Despite the apparent inefficiencies of referential structures, we will generally
be content with the convenience of Python’s lists and tuples in this book. The only
place in which we consider alternatives will be in Chapter 15, which focuses on
the impact of memory usage on data structures and algorithms. Python provides
several means for creating compact arrays of various types.
5.2. Low-Level Arrays 191
Primary support for compact arrays is in a module named array. That module
defines a class, also named array, providing compact storage for arrays of primitive
data types. A portrayal of such an array of integers is shown in Figure 5.10.
3 4 5 6 70 1 2
1752 3 7 11 13 19
Figure 5.10: Integers stored compactly as elements of a Python array.
The public interface for the array class conforms mostly to that of a Python list.
However, the constructor for the array class requires a type code as a first parameter,
which is a character that designates the type of data that will be stored in the array.
As a tangible example, the type code, i , designates an array of (signed) integers,
typically represented using at least 16-bits each. We can declare the array shown in
Figure 5.10 as,
primes = array( i , [2, 3, 5, 7, 11, 13, 17, 19])
The type code allows the interpreter to determine precisely how many bits are
needed per element of the array. The type codes supported by the array module,
as shown in Table 5.1, are formally based upon the native data types used by the
C programming language (the language in which the the most widely used distri-
bution of Python is implemented). The precise number of bits for the C data types
is system-dependent, but typical ranges are shown in the table.
Code C Data Type Typical Number of Bytes
b signed char 1
B unsigned char 1
u Unicode char 2 or 4
h signed short int 2
H unsigned short int 2
i signed int 2 or 4
I unsigned int 2 or 4
l signed long int 4
L unsigned long int 4
f float 4
d float 8
Table 5.1: Type codes supported by the array module.
The array module does not provide support for making compact arrays of user-
defined data types. Compact arrays of such structures can be created with the lower-
level support of a module named ctypes. (See Section 5.3.1 for more discussion of
the ctypes module.)
192 Chapter 5. Array-Based Sequences
5.3 Dynamic Arrays and Amortization
When creating a low-level array in a computer system, the precise size of that array
must be explicitly declared in order for the system to properly allocate a consecutive
piece of memory for its storage. For example, Figure 5.11 displays an array of 12
bytes that might be stored in memory locations 2146 through 2157.
21
60
21
45
21
46
21
47
21
48
21
49
21
50
21
51
21
52
21
53
21
54
21
55
21
56
21
57
21
58
21
44
21
59
Figure 5.11: An array of 12 bytes allocated in memory locations 2146 through 2157.
Because the system might dedicate neighboring memory locations to store other
data, the capacity of an array cannot trivially be increased by expanding into sub-
sequent cells. In the context of representing a Python tuple or str instance, this
constraint is no problem. Instances of those classes are immutable, so the correct
size for an underlying array can be fixed when the object is instantiated.
Python’s list class presents a more interesting abstraction. Although a list has a
particular length when constructed, the class allows us to add elements to the list,
with no apparent limit on the overall capacity of the list. To provide this abstraction,
Python relies on an algorithmic sleight of hand known as a dynamic array.
The first key to providing the semantics of a dynamic array is that a list instance
maintains an underlying array that often has greater capacity than the current length
of the list. For example, while a user may have created a list with five elements,
the system may have reserved an underlying array capable of storing eight object
references (rather than only five). This extra capacity makes it easy to append a
new element to the list by using the next available cell of the array.
If a user continues to append elements to a list, any reserved capacity will
eventually be exhausted. In that case, the class requests a new, larger array from the
system, and initializes the new array so that its prefix matches that of the existing
smaller array. At that point in time, the old array is no longer needed, so it is
reclaimed by the system. Intuitively, this strategy is much like that of the hermit
crab, which moves into a larger shell when it outgrows its previous one.
We give empirical evidence that Python’s list class is based upon such a strat-
egy. The source code for our experiment is displayed in Code Fragment 5.1, and a
sample output of that program is given in Code Fragment 5.2. We rely on a func-
tion named getsizeof that is available from the sys module. This function reports
the number of bytes that are being used to store an object in Python. For a list, it
reports the number of bytes devoted to the array and other instance variables of the
list, but not any space devoted to elements referenced by the list.
5.3. Dynamic Arrays and Amortization 193
1 import sys # provides getsizeof function
2 data = [ ]
3 for k in range(n): # NOTE: must fix choice of n
4 a = len(data) # number of elements
5 b = sys.getsizeof(data) # actual size in bytes
6 print( Length: {0:3d}; Size in bytes: {1:4d} .format(a, b))
7 data.append(None) # increase length by one
Code Fragment 5.1: An experiment to explore the relationship between a list’s
length and its underlying size in Python.
Length: 0; Size in bytes : 72
Length: 1; Size in bytes : 104
Length: 2; Size in bytes : 104
Length: 3; Size in bytes : 104
Length: 4; Size in bytes : 104
Length: 5; Size in bytes : 136
Length: 6; Size in bytes : 136
Length: 7; Size in bytes : 136
Length: 8; Size in bytes : 136
Length: 9; Size in bytes : 200
Length: 10; Size in bytes : 200
Length: 11; Size in bytes : 200
Length: 12; Size in bytes : 200
Length: 13; Size in bytes : 200
Length: 14; Size in bytes : 200
Length: 15; Size in bytes : 200
Length: 16; Size in bytes : 200
Length: 17; Size in bytes : 272
Length: 18; Size in bytes : 272
Length: 19; Size in bytes : 272
Length: 20; Size in bytes : 272
Length: 21; Size in bytes : 272
Length: 22; Size in bytes : 272
Length: 23; Size in bytes : 272
Length: 24; Size in bytes : 272
Length: 25; Size in bytes : 272
Length: 26; Size in bytes : 352
Code Fragment 5.2: Sample output from the experiment of Code Fragment 5.1.
194 Chapter 5. Array-Based Sequences
In evaluating the results of the experiment, we draw attention to the first line of
output from Code Fragment 5.2. We see that an empty list instance already requires
a certain number of bytes of memory (72 on our system). In fact, each object in
Python maintains some state, for example, a reference to denote the class to which
it belongs. Although we cannot directly access private instance variables for a list,
we can speculate that in some form it maintains state information akin to:
n The number of actual elements currently stored in the list.
capacity The maximum number of elements that could be stored in the
currently allocated array.
A The reference to the currently allocated array (initially None).
As soon as the first element is inserted into the list, we detect a change in the
underlying size of the structure. In particular, we see the number of bytes jump
from 72 to 104, an increase of exactly 32 bytes. Our experiment was run on a
64-bit machine architecture, meaning that each memory address is a 64-bit number
(i.e., 8 bytes). We speculate that the increase of 32 bytes reflects the allocation of
an underlying array capable of storing four object references. This hypothesis is
consistent with the fact that we do not see any underlying change in the memory
usage after inserting the second, third, or fourth element into the list.
After the fifth element has been added to the list, we see the memory usage jump
from 104 bytes to 136 bytes. If we assume the original base usage of 72 bytes for
the list, the total of 136 suggests an additional 64 = 8×8 bytes that provide capacity
for up to eight object references. Again, this is consistent with the experiment, as
the memory usage does not increase again until the ninth insertion. At that point,
the 200 bytes can be viewed as the original 72 plus an additional 128-byte array to
store 16 object references. The 17th insertion pushes the overall memory usage to
272 = 72 + 200 = 72 + 25 × 8, hence enough to store up to 25 element references.
Because a list is a referential structure, the result of getsizeof for a list instance
only includes the size for representing its primary structure; it does not account for
memory used by the objects that are elements of the list. In our experiment, we
repeatedly append None to the list, because we do not care about the contents, but
we could append any type of object without affecting the number of bytes reported
by getsizeof(data).
If we were to continue such an experiment for further iterations, we might try
to discern the pattern for how large of an array Python creates each time the ca-
pacity of the previous array is exhausted (see Exercises R-5.2 and C-5.13). Before
exploring the precise sequence of capacities used by Python, we continue in this
section by describing a general approach for implementing dynamic arrays and for
performing an asymptotic analysis of their performance.
5.3. Dynamic Arrays and Amortization 195
5.3.1 Implementing a Dynamic Array
Although the Python list class provides a highly optimized implementation of dy-
namic arrays, upon which we rely for the remainder of this book, it is instructive to
see how such a class might be implemented.
The key is to provide means to grow the array A that stores the elements of a
list. Of course, we cannot actually grow that array, as its capacity is fixed. If an
element is appended to a list at a time when the underlying array is full, we perform
the following steps:
1. Allocate a new array B with larger capacity.
2. Set B[i] = A[i], for i = 0, . . . , n − 1, where n denotes current number of items.
3. Set A = B, that is, we henceforth use B as the array supporting the list.
4. Insert the new element in the new array.
An illustration of this process is shown in Figure 5.12.
B
A A
B A
(a) (b) (c)
Figure 5.12: An illustration of the three steps for “growing” a dynamic array: (a)
create new array B; (b) store elements of A in B; (c) reassign reference A to the new
array. Not shown is the future garbage collection of the old array, or the insertion
of the new element.
The remaining issue to consider is how large of a new array to create. A com-
monly used rule is for the new array to have twice the capacity of the existing array
that has been filled. In Section 5.3.2, we will provide a mathematical analysis to
justify such a choice.
In Code Fragment 5.3, we offer a concrete implementation of dynamic arrays
in Python. Our DynamicArray class is designed using ideas described in this sec-
tion. While consistent with the interface of a Python list class, we provide only
limited functionality in the form of an append method, and accessors len and
getitem . Support for creating low-level arrays is provided by a module named
ctypes. Because we will not typically use such a low-level structure in the remain-
der of this book, we omit a detailed explanation of the ctypes module. Instead,
we wrap the necessary command for declaring the raw array within a private util-
ity method make array. The hallmark expansion procedure is performed in our
nonpublic resize method.
196 Chapter 5. Array-Based Sequences
1 import ctypes # provides low-level arrays
2
3 class DynamicArray:
4 ”””A dynamic array class akin to a simplified Python list.”””
5
6 def init (self):
7 ”””Create an empty array.”””
8 self. n = 0 # count actual elements
9 self. capacity = 1 # default array capacity
10 self. A = self. make array(self. capacity) # low-level array
11
12 def len (self):
13 ”””Return number of elements stored in the array.”””
14 return self. n
15
16 def getitem (self, k):
17 ”””Return element at index k.”””
18 if not 0 <= k < self. n:
19 raise IndexError( invalid index )
20 return self. A[k] # retrieve from array
21
22 def append(self, obj):
23 ”””Add object to end of the array.”””
24 if self. n == self. capacity: # not enough room
25 self. resize(2 self. capacity) # so double capacity
26 self. A[self. n] = obj
27 self. n += 1
28
29 def resize(self, c): # nonpublic utitity
30 ”””Resize internal array to capacity c.”””
31 B = self. make array(c) # new (bigger) array
32 for k in range(self. n): # for each existing value
33 B[k] = self. A[k]
34 self. A = B # use the bigger array
35 self. capacity = c
36
37 def make array(self, c): # nonpublic utitity
38 ”””Return new array with capacity c.”””
39 return (c ctypes.py object)( ) # see ctypes documentation
Code Fragment 5.3: An implementation of a DynamicArray class, using a raw array
from the ctypes module as storage.
5.3. Dynamic Arrays and Amortization 197
5.3.2 Amortized Analysis of Dynamic Arrays
In this section, we perform a detailed analysis of the running time of operations on
dynamic arrays. We use the big-Omega notation introduced in Section 3.3.1 to give
an asymptotic lower bound on the running time of an algorithm or step within it.
The strategy of replacing an array with a new, larger array might at first seem
slow, because a single append operation may require Ω(n) time to perform, where
n is the current number of elements in the array. However, notice that by doubling
the capacity during an array replacement, our new array allows us to add n new
elements before the array must be replaced again. In this way, there are many
simple append operations for each expensive one (see Figure 5.13). This fact allows
us to show that performing a series of operations on an initially empty dynamic
array is efficient in terms of its total running time.
Using an algorithmic design pattern called amortization, we can show that per-
forming a sequence of such append operations on a dynamic array is actually quite
efficient. To perform an amortized analysis, we use an accounting technique where
we view the computer as a coin-operated appliance that requires the payment of
one cyber-dollar for a constant amount of computing time. When an operation
is executed, we should have enough cyber-dollars available in our current “bank
account” to pay for that operation’s running time. Thus, the total amount of cyber-
dollars spent for any computation will be proportional to the total time spent on that
computation. The beauty of using this analysis method is that we can overcharge
some operations in order to save up cyber-dollars to pay for others.
p
ri
m
it
iv
e
op
er
at
io
n
s
fo
r
an
a
p
p
en
d
current number of elements
1310 125 6 7 8 11 14 15 161 2 3 4 9
Figure 5.13: Running times of a series of append operations on a dynamic array.
198 Chapter 5. Array-Based Sequences
Proposition 5.1: Let S be a sequence implemented by means of a dynamic array
with initial capacity one, using the strategy of doubling the array size when full.
The total time to perform a series of n append operations in S, starting from S being
empty, is O(n).
Justification: Let us assume that one cyber-dollar is enough to pay for the execu-
tion of each append operation in S, excluding the time spent for growing the array.
Also, let us assume that growing the array from size k to size 2k requires k cyber-
dollars for the time spent initializing the new array. We shall charge each append
operation three cyber-dollars. Thus, we overcharge each append operation that does
not cause an overflow by two cyber-dollars. Think of the two cyber-dollars profited
in an insertion that does not grow the array as being “stored” with the cell in which
the element was inserted. An overflow occurs when the array S has 2i elements, for
some integer i ≥ 0, and the size of the array used by the array representing S is 2i.
Thus, doubling the size of the array will require 2i cyber-dollars. Fortunately, these
cyber-dollars can be found stored in cells 2i−1 through 2i − 1. (See Figure 5.14.)
Note that the previous overflow occurred when the number of elements became
larger than 2i−1 for the first time, and thus the cyber-dollars stored in cells 2i−1
through 2i − 1 have not yet been spent. Therefore, we have a valid amortization
scheme in which each operation is charged three cyber-dollars and all the comput-
ing time is paid for. That is, we can pay for the execution of n append operations
using 3n cyber-dollars. In other words, the amortized running time of each append
operation is O(1); hence, the total running time of n append operations is O(n).
(a)
0 2 4 5 6 731
$ $ $ $
$ $ $ $
(b)
0 2 4 5 6 7 8 9 113 10 12 13 14 151
$
$
Figure 5.14: Illustration of a series of append operations on a dynamic array: (a) an
8-cell array is full, with two cyber-dollars “stored” at cells 4 through 7; (b) an
append operation causes an overflow and a doubling of capacity. Copying the eight
old elements to the new array is paid for by the cyber-dollars already stored in the
table. Inserting the new element is paid for by one of the cyber-dollars charged to
the current append operation, and the two cyber-dollars profited are stored at cell 8.
5.3. Dynamic Arrays and Amortization 199
Geometric Increase in Capacity
Although the proof of Proposition 5.1 relies on the array being doubled each time
we expand, the O(1) amortized bound per operation can be proven for any geo-
metrically increasing progression of array sizes (see Section 2.4.2 for discussion of
geometric progressions). When choosing the geometric base, there exists a trade-
off between run-time efficiency and memory usage. With a base of 2 (i.e., doubling
the array), if the last insertion causes a resize event, the array essentially ends up
twice as large as it needs to be. If we instead increase the array by only 25% of
its current size (i.e., a geometric base of 1.25), we do not risk wasting as much
memory in the end, but there will be more intermediate resize events along the
way. Still it is possible to prove an O(1) amortized bound, using a constant factor
greater than the 3 cyber-dollars per operation used in the proof of Proposition 5.1
(see Exercise C-5.15). The key to the performance is that the amount of additional
space is proportional to the current size of the array.
Beware of Arithmetic Progression
To avoid reserving too much space at once, it might be tempting to implement a
dynamic array with a strategy in which a constant number of additional cells are
reserved each time an array is resized. Unfortunately, the overall performance of
such a strategy is significantly worse. At an extreme, an increase of only one cell
causes each append operation to resize the array, leading to a familiar 1 + 2 + 3 +
··· + n summation and Ω(n2) overall cost. Using increases of 2 or 3 at a time is
slightly better, as portrayed in Figure 5.13, but the overall cost remains quadratic.
p
ri
m
it
iv
e
op
er
at
io
n
s
fo
r
an
a
p
p
en
d
current number of elements
1310 125 6 7 8 11 14 15 161 2 3 4 9
p
ri
m
it
iv
e
op
er
at
io
n
s
fo
r
an
a
p
p
en
d
current number of elements
1310 125 6 7 8 11 14 15 161 2 3 4 9
(a) (b)
Figure 5.15: Running times of a series of append operations on a dynamic array
using arithmetic progression of sizes. (a) Assumes increase of 2 in size of the
array, while (b) assumes increase of 3.
200 Chapter 5. Array-Based Sequences
Using a fixed increment for each resize, and thus an arithmetic progression of
intermediate array sizes, results in an overall time that is quadratic in the number
of operations, as shown in the following proposition. Intuitively, even an increase
in 1000 cells per resize will become insignificant for large data sets.
Proposition 5.2: Performing a series of n append operations on an initially empty
dynamic array using a fixed increment with each resize takes Ω(n2) time.
Justification: Let c > 0 represent the fixed increment in capacity that is used for
each resize event. During the series of n append operations, time will have been
spent initializing arrays of size c, 2c, 3c, . . . , mc for m =
n/c�, and therefore, the
overall time would be proportional to c + 2c + 3c + ··· + mc. By Proposition 3.3,
this sum is
m
∑
i=1
ci = c ·
m
∑
i=1
i = c
m(m + 1)
2
≥ c
n
c (
n
c + 1)
2
≥ n
2
2c
.
Therefore, performing the n append operations takes Ω(n2) time.
A lesson to be learned from Propositions 5.1 and 5.2 is that a subtle difference in
an algorithm design can produce drastic differences in the asymptotic performance,
and that a careful analysis can provide important insights into the design of a data
structure.
Memory Usage and Shrinking an Array
Another consequence of the rule of a geometric increase in capacity when append-
ing to a dynamic array is that the final array size is guaranteed to be proportional to
the overall number of elements. That is, the data structure uses O(n) memory. This
is a very desirable property for a data structure.
If a container, such as a Python list, provides operations that cause the removal
of one or more elements, greater care must be taken to ensure that a dynamic array
guarantees O(n) memory usage. The risk is that repeated insertions may cause the
underlying array to grow arbitrarily large, and that there will no longer be a propor-
tional relationship between the actual number of elements and the array capacity
after many elements are removed.
A robust implementation of such a data structure will shrink the underlying
array, on occasion, while maintaining the O(1) amortized bound on individual op-
erations. However, care must be taken to ensure that the structure cannot rapidly
oscillate between growing and shrinking the underlying array, in which case the
amortized bound would not be achieved. In Exercise C-5.16, we explore a strategy
in which the array capacity is halved whenever the number of actual element falls
below one fourth of that capacity, thereby guaranteeing that the array capacity is at
most four times the number of elements; we explore the amortized analysis of such
a strategy in Exercises C-5.17 and C-5.18.
5.3. Dynamic Arrays and Amortization 201
5.3.3 Python’s List Class
The experiments of Code Fragment 5.1 and 5.2, at the beginning of Section 5.3,
provide empirical evidence that Python’s list class is using a form of dynamic arrays
for its storage. Yet, a careful examination of the intermediate array capacities (see
Exercises R-5.2 and C-5.13) suggests that Python is not using a pure geometric
progression, nor is it using an arithmetic progression.
With that said, it is clear that Python’s implementation of the append method
exhibits amortized constant-time behavior. We can demonstrate this fact experi-
mentally. A single append operation typically executes so quickly that it would be
difficult for us to accurately measure the time elapsed at that granularity, although
we should notice some of the more expensive operations in which a resize is per-
formed. We can get a more accurate measure of the amortized cost per operation
by performing a series of n append operations on an initially empty list and deter-
mining the average cost of each. A function to perform that experiment is given in
Code Fragment 5.4.
1 from time import time # import time function from time module
2 def compute average(n):
3 ”””Perform n appends to an empty list and return average time elapsed.”””
4 data = [ ]
5 start = time( ) # record the start time (in seconds)
6 for k in range(n):
7 data.append(None)
8 end = time( ) # record the end time (in seconds)
9 return (end − start) / n # compute average per operation
Code Fragment 5.4: Measuring the amortized cost of append for Python’s list class.
Technically, the time elapsed between the start and end includes the time to
manage the iteration of the for loop, in addition to the append calls. The empirical
results of the experiment, for increasingly large values of n, are shown in Table 5.2.
We see higher average cost for the smaller data sets, perhaps in part due to the over-
head of the loop range. There is also natural variance in measuring the amortized
cost in this way, because of the impact of the final resize event relative to n. Taken
as a whole, there seems clear evidence that the amortized time for each append is
independent of n.
n 100 1,000 10,000 100,000 1,000,000 10,000,000 100,000,000
μs 0.219 0.158 0.164 0.151 0.147 0.147 0.149
Table 5.2: Average running time of append, measured in microseconds, as observed
over a sequence of n calls, starting with an empty list.
202 Chapter 5. Array-Based Sequences
5.4 Efficiency of Python’s Sequence Types
In the previous section, we began to explore the underpinnings of Python’s list
class, in terms of implementation strategies and efficiency. We continue in this
section by examining the performance of all of Python’s sequence types.
5.4.1 Python’s List and Tuple Classes
The nonmutating behaviors of the list class are precisely those that are supported
by the tuple class. We note that tuples are typically more memory efficient than
lists because they are immutable; therefore, there is no need for an underlying
dynamic array with surplus capacity. We summarize the asymptotic efficiency of
the nonmutating behaviors of the list and tuple classes in Table 5.3. An explanation
of this analysis follows.
Operation Running Time
len(data) O(1)
data[j] O(1)
data.count(value) O(n)
data.index(value) O(k + 1)
value in data O(k + 1)
data1 == data2
O(k + 1)
(similarly !=, <, <=, >, >=)
data[j:k] O(k − j + 1)
data1 + data2 O(n1 + n2)
c data O(cn)
Table 5.3: Asymptotic performance of the nonmutating behaviors of the list and
tuple classes. Identifiers data, data1, and data2 designate instances of the list or
tuple class, and n, n1, and n2 their respective lengths. For the containment check
and index method, k represents the index of the leftmost occurrence (with k = n if
there is no occurrence). For comparisons between two sequences, we let k denote
the leftmost index at which they disagree or else k = min(n1, n2).
Constant-Time Operations
The length of an instance is returned in constant time because an instance explicitly
maintains such state information. The constant-time efficiency of syntax data[j] is
assured by the underlying access into an array.
5.4. Efficiency of Python’s Sequence Types 203
Searching for Occurrences of a Value
Each of the count, index, and contains methods proceed through iteration
of the sequence from left to right. In fact, Code Fragment 2.14 of Section 2.4.3
demonstrates how those behaviors might be implemented. Notably, the loop for
computing the count must proceed through the entire sequence, while the loops
for checking containment of an element or determining the index of an element
immediately exit once they find the leftmost occurrence of the desired value, if
one exists. So while count always examines the n elements of the sequence,
index and contains examine n elements in the worst case, but may be faster.
Empirical evidence can be found by setting data = list(range(10000000)) and
then comparing the relative efficiency of the test, 5 in data, relative to the test,
9999995 in data, or even the failed test, −5 in data.
Lexicographic Comparisons
Comparisons between two sequences are defined lexicographically. In the worst
case, evaluating such a condition requires an iteration taking time proportional
to the length of the shorter of the two sequences (because when one sequence
ends, the lexicographic result can be determined). However, in some cases the
result of the test can be evaluated more efficiently. For example, if evaluating
[7, 3, …] < [7, 5, ...], it is clear that the result is True without examining the re-
mainders of those lists, because the second element of the left operand is strictly
less than the second element of the right operand.
Creating New Instances
The final three behaviors in Table 5.3 are those that construct a new instance based
on one or more existing instances. In all cases, the running time depends on the
construction and initialization of the new result, and therefore the asymptotic be-
havior is proportional to the length of the result. Therefore, we find that slice
data[6000000:6000008] can be constructed almost immediately because it has only
eight elements, while slice data[6000000:7000000] has one million elements, and
thus is more time-consuming to create.
Mutating Behaviors
The efficiency of the mutating behaviors of the list class are described in Table 5.3.
The simplest of those behaviors has syntax data[j] = val, and is supported by the
special setitem method. This operation has worst-case O(1) running time be-
cause it simply replaces one element of a list with a new value. No other elements
are affected and the size of the underlying array does not change. The more inter-
esting behaviors to analyze are those that add or remove elements from the list.
204 Chapter 5. Array-Based Sequences
Operation Running Time
data[j] = val O(1)
data.append(value) O(1)∗
data.insert(k, value) O(n − k + 1)∗
data.pop( ) O(1)∗
data.pop(k)
O(n − k)∗
del data[k]
data.remove(value) O(n)∗
data1.extend(data2)
O(n2)
∗
data1 += data2
data.reverse( ) O(n)
data.sort( ) O(n log n)
∗amortized
Table 5.4: Asymptotic performance of the mutating behaviors of the list class. Iden-
tifiers data, data1, and data2 designate instances of the list class, and n, n1, and n2
their respective lengths.
Adding Elements to a List
In Section 5.3 we fully explored the append method. In the worst case, it requires
Ω(n) time because the underlying array is resized, but it uses O(1) time in the amor-
tized sense. Lists also support a method, with signature insert(k, value), that inserts
a given value into the list at index 0 ≤ k ≤ n while shifting all subsequent elements
back one slot to make room. For the purpose of illustration, Code Fragment 5.5 pro-
vides an implementation of that method, in the context of our DynamicArray class
introduced in Code Fragment 5.3. There are two complicating factors in analyzing
the efficiency of such an operation. First, we note that the addition of one element
may require a resizing of the dynamic array. That portion of the work requires Ω(n)
worst-case time but only O(1) amortized time, as per append. The other expense
for insert is the shifting of elements to make room for the new item. The time for
1 def insert(self, k, value):
2 ”””Insert value at index k, shifting subsequent values rightward.”””
3 # (for simplicity, we assume 0 <= k <= n in this verion)
4 if self. n == self. capacity: # not enough room
5 self. resize(2 self. capacity) # so double capacity
6 for j in range(self. n, k, −1): # shift rightmost first
7 self. A[j] = self. A[j−1]
8 self. A[k] = value # store newest element
9 self. n += 1
Code Fragment 5.5: Implementation of insert for our DynamicArray class.
5.4. Efficiency of Python’s Sequence Types 205
k210 n − 1
Figure 5.16: Creating room to insert a new element at index k of a dynamic array.
that process depends upon the index of the new element, and thus the number of
other elements that must be shifted. That loop copies the reference that had been
at index n − 1 to index n, then the reference that had been at index n − 2 to n − 1,
continuing until copying the reference that had been at index k to k + 1, as illus-
trated in Figure 5.16. Overall this leads to an amortized O(n − k + 1) performance
for inserting at index k.
When exploring the efficiency of Python’s append method in Section 5.3.3,
we performed an experiment that measured the average cost of repeated calls on
varying sizes of lists (see Code Fragment 5.4 and Table 5.2). We have repeated that
experiment with the insert method, trying three different access patterns:
• In the first case, we repeatedly insert at the beginning of a list,
for n in range(N):
data.insert(0, None)
• In a second case, we repeatedly insert near the middle of a list,
for n in range(N):
data.insert(n // 2, None)
• In a third case, we repeatedly insert at the end of the list,
for n in range(N):
data.insert(n, None)
The results of our experiment are given in Table 5.5, reporting the average time per
operation (not the total time for the entire loop). As expected, we see that inserting
at the beginning of a list is most expensive, requiring linear time per operation.
Inserting at the middle requires about half the time as inserting at the beginning,
yet is still Ω(n) time. Inserting at the end displays O(1) behavior, akin to append.
N
100 1,000 10,000 100,000 1,000,000
k = 0 0.482 0.765 4.014 36.643 351.590
k = n // 2 0.451 0.577 2.191 17.873 175.383
k = n 0.420 0.422 0.395 0.389 0.397
Table 5.5: Average running time of insert(k, val), measured in microseconds, as
observed over a sequence of N calls, starting with an empty list. We let n denote
the size of the current list (as opposed to the final list).
206 Chapter 5. Array-Based Sequences
Removing Elements from a List
Python’s list class offers several ways to remove an element from a list. A call to
pop( ) removes the last element from a list. This is most efficient, because all other
elements remain in their original location. This is effectively an O(1) operation,
but the bound is amortized because Python will occasionally shrink the underlying
dynamic array to conserve memory.
The parameterized version, pop(k), removes the element that is at index k < n
of a list, shifting all subsequent elements leftward to fill the gap that results from
the removal. The efficiency of this operation is O(n − k), as the amount of shifting
depends upon the choice of index k, as illustrated in Figure 5.17. Note well that this
implies that pop(0) is the most expensive call, using Ω(n) time. (see experiments
in Exercise R-5.8.)
k210 n − 1
Figure 5.17: Removing an element at index k of a dynamic array.
The list class offers another method, named remove, that allows the caller to
specify the value that should be removed (not the index at which it resides). For-
mally, it removes only the first occurrence of such a value from a list, or raises a
ValueError if no such value is found. An implementation of such behavior is given
in Code Fragment 5.6, again using our DynamicArray class for illustration.
Interestingly, there is no “efficient” case for remove; every call requires Ω(n)
time. One part of the process searches from the beginning until finding the value at
index k, while the rest iterates from k to the end in order to shift elements leftward.
This linear behavior can be observed experimentally (see Exercise C-5.24).
1 def remove(self, value):
2 ”””Remove first occurrence of value (or raise ValueError).”””
3 # note: we do not consider shrinking the dynamic array in this version
4 for k in range(self. n):
5 if self. A[k] == value: # found a match!
6 for j in range(k, self. n − 1): # shift others to fill gap
7 self. A[j] = self. A[j+1]
8 self. A[self. n − 1] = None # help garbage collection
9 self. n −= 1 # we have one less item
10 return # exit immediately
11 raise ValueError( value not found ) # only reached if no match
Code Fragment 5.6: Implementation of remove for our DynamicArray class.
5.4. Efficiency of Python’s Sequence Types 207
Extending a List
Python provides a method named extend that is used to add all elements of one list
to the end of a second list. In effect, a call to data.extend(other) produces the same
outcome as the code,
for element in other:
data.append(element)
In either case, the running time is proportional to the length of the other list, and
amortized because the underlying array for the first list may be resized to accom-
modate the additional elements.
In practice, the extend method is preferable to repeated calls to append because
the constant factors hidden in the asymptotic analysis are significantly smaller. The
greater efficiency of extend is threefold. First, there is always some advantage to
using an appropriate Python method, because those methods are often implemented
natively in a compiled language (rather than as interpreted Python code). Second,
there is less overhead to a single function call that accomplishes all the work, versus
many individual function calls. Finally, increased efficiency of extend comes from
the fact that the resulting size of the updated list can be calculated in advance. If the
second data set is quite large, there is some risk that the underlying dynamic array
might be resized multiple times when using repeated calls to append. With a single
call to extend, at most one resize operation will be performed. Exercise C-5.22
explores the relative efficiency of these two approaches experimentally.
Constructing New Lists
There are several syntaxes for constructing new lists. In almost all cases, the asymp-
totic efficiency of the behavior is linear in the length of the list that is created. How-
ever, as with the case in the preceding discussion of extend, there are significant
differences in the practical efficiency.
Section 1.9.2 introduces the topic of list comprehension, using an example
such as squares = [ k k for k in range(1, n+1) ] as a shorthand for
squares = [ ]
for k in range(1, n+1):
squares.append(k k)
Experiments should show that the list comprehension syntax is significantly faster
than building the list by repeatedly appending (see Exercise C-5.23).
Similarly, it is a common Python idiom to initialize a list of constant values
using the multiplication operator, as in [0] n to produce a list of length n with
all values equal to zero. Not only is this succinct for the programmer; it is more
efficient than building such a list incrementally.
208 Chapter 5. Array-Based Sequences
5.4.2 Python’s String Class
Strings are very important in Python. We introduced their use in Chapter 1, with
a discussion of various operator syntaxes in Section 1.3. A comprehensive sum-
mary of the named methods of the class is given in Tables A.1 through A.4 of
Appendix A. We will not formally analyze the efficiency of each of those behav-
iors in this section, but we do wish to comment on some notable issues. In general,
we let n denote the length of a string. For operations that rely on a second string as
a pattern, we let m denote the length of that pattern string.
The analysis for many behaviors is quite intuitive. For example, methods that
produce a new string (e.g., capitalize, center, strip) require time that is linear in
the length of the string that is produced. Many of the behaviors that test Boolean
conditions of a string (e.g., islower) take O(n) time, examining all n characters in the
worst case, but short circuiting as soon as the answer becomes evident (e.g., islower
can immediately return False if the first character is uppercased). The comparison
operators (e.g., ==, <) fall into this category as well.
Pattern Matching
Some of the most interesting behaviors, from an algorithmic point of view, are those
that in some way depend upon finding a string pattern within a larger string; this
goal is at the heart of methods such as contains , find, index, count, replace,
and split. String algorithms will be the topic of Chapter 13, and this particular
problem known as pattern matching will be the focus of Section 13.2. A naive im-
plementation runs in O(mn) time case, because we consider the n − m + 1 possible
starting indices for the pattern, and we spend O(m) time at each starting position,
checking if the pattern matches. However, in Section 13.2, we will develop an al-
gorithm for finding a pattern of length m within a longer string of length n in O(n)
time.
Composing Strings
Finally, we wish to comment on several approaches for composing large strings. As
an academic exercise, assume that we have a large string named document, and our
goal is to produce a new string, letters, that contains only the alphabetic characters
of the original string (e.g., with spaces, numbers, and punctuation removed). It may
be tempting to compose a result through repeated concatenation, as follows.
# WARNING: do not do this
letters = # start with empty string
for c in document:
if c.isalpha( ):
letters += c # concatenate alphabetic character
5.4. Efficiency of Python’s Sequence Types 209
While the preceding code fragment accomplishes the goal, it may be terribly
inefficient. Because strings are immutable, the command, letters += c, would
presumably compute the concatenation, letters + c, as a new string instance and
then reassign the identifier, letters, to that result. Constructing that new string
would require time proportional to its length. If the final result has n characters, the
series of concatenations would take time proportional to the familiar sum 1 + 2 +
3 + ··· + n, and therefore O(n2) time.
Inefficient code of this type is widespread in Python, perhaps because of the
somewhat natural appearance of the code, and mistaken presumptions about how
the += operator is evaluated with strings. Some later implementations of the
Python interpreter have developed an optimization to allow such code to complete
in linear time, but this is not guaranteed for all Python implementations. The op-
timization is as follows. The reason that a command, letters += c, causes a new
string instance to be created is that the original string must be left unchanged if
another variable in a program refers to that string. On the other hand, if Python
knew that there were no other references to the string in question, it could imple-
ment += more efficiently by directly mutating the string (as a dynamic array). As
it happens, the Python interpreter already maintains what are known as reference
counts for each object; this count is used in part to determine if an object can be
garbage collected. (See Section 15.1.2.) But in this context, it provides a means to
detect when no other references exist to a string, thereby allowing the optimization.
A more standard Python idiom to guarantee linear time composition of a string
is to use a temporary list to store individual pieces, and then to rely on the join
method of the str class to compose the final result. Using this technique with our
previous example would appear as follows:
temp = [ ] # start with empty list
for c in document:
if c.isalpha( ):
temp.append(c) # append alphabetic character
letters = .join(temp) # compose overall result
This approach is guaranteed to run in O(n) time. First, we note that the series of
up to n append calls will require a total of O(n) time, as per the definition of the
amortized cost of that operation. The final call to join also guarantees that it takes
time that is linear in the final length of the composed string.
As we discussed at the end of the previous section, we can further improve
the practical execution time by using a list comprehension syntax to build up the
temporary list, rather than by repeated calls to append. That solution appears as,
letters = .join([c for c in document if c.isalpha( )])
Better yet, we can entirely avoid the temporary list with a generator comprehension:
letters = .join(c for c in document if c.isalpha( ))
210 Chapter 5. Array-Based Sequences
5.5 Using Array-Based Sequences
5.5.1 Storing High Scores for a Game
The first application we study is storing a sequence of high score entries for a video
game. This is representative of many applications in which a sequence of objects
must be stored. We could just as easily have chosen to store records for patients in
a hospital or the names of players on a football team. Nevertheless, let us focus on
storing high score entries, which is a simple application that is already rich enough
to present some important data-structuring concepts.
To begin, we consider what information to include in an object representing a
high score entry. Obviously, one component to include is an integer representing
the score itself, which we identify as score. Another useful thing to include is
the name of the person earning this score, which we identify as name. We could
go on from here, adding fields representing the date the score was earned or game
statistics that led to that score. However, we omit such details to keep our example
simple. A Python class, GameEntry, representing a game entry, is given in Code
Fragment 5.7.
1 class GameEntry:
2 ”””Represents one entry of a list of high scores.”””
3
4 def init (self, name, score):
5 self. name = name
6 self. score = score
7
8 def get name(self):
9 return self. name
10
11 def get score(self):
12 return self. score
13
14 def str (self):
15 return ({0}, {1}) .format(self. name, self. score) # e.g., (Bob, 98)
Code Fragment 5.7: Python code for a simple GameEntry class. We include meth-
ods for returning the name and score for a game entry object, as well as a method
for returning a string representation of this entry.
5.5. Using Array-Based Sequences 211
A Class for High Scores
To maintain a sequence of high scores, we develop a class named Scoreboard. A
scoreboard is limited to a certain number of high scores that can be saved; once that
limit is reached, a new score only qualifies for the scoreboard if it is strictly higher
than the lowest “high score” on the board. The length of the desired scoreboard may
depend on the game, perhaps 10, 50, or 500. Since that limit may vary depending on
the game, we allow it to be specified as a parameter to our Scoreboard constructor.
Internally, we will use a Python list named board in order to manage the
GameEntry instances that represent the high scores. Since we expect the score-
board to eventually reach full capacity, we initialize the list to be large enough to
hold the maximum number of scores, but we initially set all entries to None. By
allocating the list with maximum capacity initially, it never needs to be resized. As
entries are added, we will maintain them from highest to lowest score, starting at
index 0 of the list. We illustrate a typical state of the data structure in Figure 5.18.
320 1 97 8654
660
Mike 1105 Paul 720 Rose 590
Rob 750 Anna Jack 510
Figure 5.18: An illustration of an ordered list of length ten, storing references to six
GameEntry objects in the cells from index 0 to 5, with the rest being None.
A complete Python implementation of the Scoreboard class is given in Code
Fragment 5.8. The constructor is rather simple. The command
self. board = [None] capacity
creates a list with the desired length, yet all entries equal to None. We maintain
an additional instance variable, n, that represents the number of actual entries
currently in our table. For convenience, our class supports the getitem method
to retrieve an entry at a given index with a syntax board[i] (or None if no such entry
exists), and we support a simple str method that returns a string representation
of the entire scoreboard, with one entry per line.
212 Chapter 5. Array-Based Sequences
1 class Scoreboard:
2 ”””Fixed-length sequence of high scores in nondecreasing order.”””
3
4 def init (self, capacity=10):
5 ”””Initialize scoreboard with given maximum capacity.
6
7 All entries are initially None.
8 ”””
9 self. board = [None] capacity # reserve space for future scores
10 self. n = 0 # number of actual entries
11
12 def getitem (self, k):
13 ”””Return entry at index k.”””
14 return self. board[k]
15
16 def str (self):
17 ”””Return string representation of the high score list.”””
18 return \n .join(str(self. board[j]) for j in range(self. n))
19
20 def add(self, entry):
21 ”””Consider adding entry to high scores.”””
22 score = entry.get score( )
23
24 # Does new entry qualify as a high score?
25 # answer is yes if board not full or score is higher than last entry
26 good = self. n < len(self. board) or score > self. board[−1].get score( )
27
28 if good:
29 if self. n < len(self. board): # no score drops from list
30 self. n += 1 # so overall number increases
31
32 # shift lower scores rightward to make room for new entry
33 j = self. n − 1
34 while j > 0 and self. board[j−1].get score( ) < score:
35 self. board[j] = self. board[j−1] # shift entry from j-1 to j
36 j −= 1 # and decrement j
37 self. board[j] = entry # when done, add new entry
Code Fragment 5.8: Python code for a Scoreboard class that maintains an ordered
series of scores as GameEntry objects.
5.5. Using Array-Based Sequences 213
Adding an Entry
The most interesting method of the Scoreboard class is add, which is responsible
for considering the addition of a new entry to the scoreboard. Keep in mind that
every entry will not necessarily qualify as a high score. If the board is not yet full,
any new entry will be retained. Once the board is full, a new entry is only retained
if it is strictly better than one of the other scores, in particular, the last entry of the
scoreboard, which is the lowest of the high scores.
When a new score is considered, we begin by determining whether it qualifies
as a high score. If so, we increase the count of active scores, n, unless the board
is already at full capacity. In that case, adding a new high score causes some other
entry to be dropped from the scoreboard, so the overall number of entries remains
the same.
To correctly place a new entry within the list, the final task is to shift any in-
ferior scores one spot lower (with the least score being dropped entirely when the
scoreboard is full). This process is quite similar to the implementation of the insert
method of the list class, as described on pages 204–205. In the context of our score-
board, there is no need to shift any None references that remain near the end of the
array, so the process can proceed as diagrammed in Figure 5.19.
0 1 98765432
Mike 1105
Rob 750
Paul 720 Rose 590
Anna 660 Jack 510
740Jill
Figure 5.19: Adding a new GameEntry for Jill to the scoreboard. In order to make
room for the new reference, we have to shift the references for game entries with
smaller scores than the new one to the right by one cell. Then we can insert the new
entry with index 2.
To implement the final stage, we begin by considering index j = self. n − 1,
which is the index at which the last GameEntry instance will reside, after complet-
ing the operation. Either j is the correct index for the newest entry, or one or more
immediately before it will have lesser scores. The while loop at line 34 checks the
compound condition, shifting references rightward and decrementing j, as long as
there is another entry at index j − 1 with a score less than the new score.
214 Chapter 5. Array-Based Sequences
5.5.2 Sorting a Sequence
In the previous subsection, we considered an application for which we added an ob-
ject to a sequence at a given position while shifting other elements so as to keep the
previous order intact. In this section, we use a similar technique to solve the sorting
problem, that is, starting with an unordered sequence of elements and rearranging
them into nondecreasing order.
The Insertion-Sort Algorithm
We study several sorting algorithms in this book, most of which are described in
Chapter 12. As a warm-up, in this section we describe a nice, simple sorting al-
gorithm known as insertion-sort. The algorithm proceeds as follows for an array-
based sequence. We start with the first element in the array. One element by itself
is already sorted. Then we consider the next element in the array. If it is smaller
than the first, we swap them. Next we consider the third element in the array. We
swap it leftward until it is in its proper order with the first two elements. We then
consider the fourth element, and swap it leftward until it is in the proper order with
the first three. We continue in this manner with the fifth element, the sixth, and so
on, until the whole array is sorted. We can express the insertion-sort algorithm in
pseudo-code, as shown in Code Fragment 5.9.
Algorithm InsertionSort(A):
Input: An array A of n comparable elements
Output: The array A with elements rearranged in nondecreasing order
for k from 1 to n − 1 do
Insert A[k] at its proper location within A[0], A[1], . . ., A[k].
Code Fragment 5.9: High-level description of the insertion-sort algorithm.
This is a simple, high-level description of insertion-sort. If we look back to
Code Fragment 5.8 of Section 5.5.1, we see that the task of inserting a new en-
try into the list of high scores is almost identical to the task of inserting a newly
considered element in insertion-sort (except that game scores were ordered from
high to low). We provide a Python implementation of insertion-sort in Code Frag-
ment 5.10, using an outer loop to consider each element in turn, and an inner
loop that moves a newly considered element to its proper location relative to the
(sorted) subarray of elements that are to its left. We illustrate an example run of the
insertion-sort algorithm in Figure 5.20.
The nested loops of insertion-sort lead to an O(n2) running time in the worst
case. The most work is done if the array is initially in reverse order. On the other
hand, if the initial array is nearly sorted or perfectly sorted, insertion-sort runs in
O(n) time because there are few or no iterations of the inner loop.
5.5. Using Array-Based Sequences 215
1 def insertion sort(A):
2 ”””Sort list of comparable elements into nondecreasing order.”””
3 for k in range(1, len(A)): # from 1 to n-1
4 cur = A[k] # current element to be inserted
5 j = k # find correct index j for current
6 while j > 0 and A[j−1] > cur: # element A[j-1] must be after current
7 A[j] = A[j−1]
8 j −= 1
9 A[j] = cur # cur is now in the right place
Code Fragment 5.10: Python code for performing insertion-sort on a list.
insert
insert
insert
0
0
0
0
0
0
0
00
0 0 0
Done!
0
C A E H G F
B C A E H G FD
B E H G FC D
A H G FB C D E
A FB C D E H
A G FB C D E H
E H G FDCBB E H G FDC
A FB C D E H
B C D E HG
BA
A B C D E G H A B
C
F
F
G
A
G
H
E
A
D
C D E H
HGFED
G
C
A
B D
no move
2 3 4 5 6 7
1 2 3 4 5 6 7
1 2 3 4 5 6 7
1 2 3 4 5 6 7
1 2 3 4 5 6 7
1 2 3 4 5 6 7
1 2 3 4 5 6 7
1 2 3 4 5 6 71 2 3 4 5 6 7
1 2 3 4 5 6 7 1 2 3 4 5 6 7 1 2 3 4 5 6 7
cur
1 2 3 4 5 6 7
move movemove
no move
no move
no move
no move
move no move
move move
1
Figure 5.20: Execution of the insertion-sort algorithm on an array of eight charac-
ters. Each row corresponds to an iteration of the outer loop, and each copy of the
sequence in a row corresponds to an iteration of the inner loop. The current element
that is being inserted is highlighted in the array, and shown as the cur value.
216 Chapter 5. Array-Based Sequences
5.5.3 Simple Cryptography
An interesting application of strings and lists is cryptography, the science of secret
messages and their applications. This field studies ways of performing encryp-
tion, which takes a message, called the plaintext, and converts it into a scrambled
message, called the ciphertext. Likewise, cryptography also studies corresponding
ways of performing decryption, which takes a ciphertext and turns it back into its
original plaintext.
Arguably the earliest encryption scheme is the Caesar cipher, which is named
after Julius Caesar, who used this scheme to protect important military messages.
(All of Caesar’s messages were written in Latin, of course, which already makes
them unreadable for most of us!) The Caesar cipher is a simple way to obscure a
message written in a language that forms words with an alphabet.
The Caesar cipher involves replacing each letter in a message with the letter that
is a certain number of letters after it in the alphabet. So, in an English message, we
might replace each A with D, each B with E, each C with F, and so on, if shifting by
three characters. We continue this approach all the way up to W, which is replaced
with Z. Then, we let the substitution pattern wrap around, so that we replace X
with A, Y with B, and Z with C.
Converting Between Strings and Character Lists
Given that strings are immutable, we cannot directly edit an instance to encrypt it.
Instead, our goal will be to generate a new string. A convenient technique for per-
forming string transformations is to create an equivalent list of characters, edit the
list, and then reassemble a (new) string based on the list. The first step can be per-
formed by sending the string as a parameter to the constructor of the list class. For
example, the expression list( bird ) produces the result [ b , i , r , d ].
Conversely, we can use a list of characters to build a string by invoking the join
method on an empty string, with the list of characters as the parameter. For exam-
ple, the call .join([ b , i , r , d ]) returns the string bird .
Using Characters as Array Indices
If we were to number our letters like array indices, so that A is 0, B is 1, C is 2,
and so on, then we can write the Caesar cipher with a rotation of r as a simple
formula: Replace each letter i with the letter (i + r) mod 26, where mod is the
modulo operator, which returns the remainder after performing an integer division.
This operator is denoted with % in Python, and it is exactly the operator we need
to easily perform the wrap around at the end of the alphabet. For 26 mod 26 is
0, 27 mod 26 is 1, and 28 mod 26 is 2. The decryption algorithm for the Caesar
cipher is just the opposite—we replace each letter with the one r places before it,
with wrap around (that is, letter i is replaced by letter (i − r) mod 26).
5.5. Using Array-Based Sequences 217
We can represent a replacement rule using another string to describe the trans-
lation. As a concrete example, suppose we are using a Caesar cipher with a three-
character rotation. We can precompute a string that represents the replacements
that should be used for each character from A to Z. For example, A should be re-
placed by D, B replaced by E, and so on. The 26 replacement characters in order
are DEFGHIJKLMNOPQRSTUVWXYZABC . We can subsequently use this translation
string as a guide to encrypt a message. The remaining challenge is how to quickly
locate the replacement for each character of the original message.
Fortunately, we can rely on the fact that characters are represented in Unicode
by integer code points, and the code points for the uppercase letters of the Latin
alphabet are consecutive (for simplicity, we restrict our encryption to uppercase
letters). Python supports functions that convert between integer code points and
one-character strings. Specifically, the function ord(c) takes a one-character string
as a parameter and returns the integer code point for that character. Conversely, the
function chr(j) takes an integer and returns its associated one-character string.
In order to find a replacement for a character in our Caesar cipher, we need to
map the characters A to Z to the respective numbers 0 to 25. The formula for
doing that conversion is j = ord(c) − ord( A ). As a sanity check, if character c
is A , we have that j = 0. When c is B , we will find that its ordinal value is pre-
cisely one more than that for A , so their difference is 1. In general, the integer j
that results from such a calculation can be used as an index into our precomputed
translation string, as illustrated in Figure 5.21.
10 2423222120191817161514131211 259876543210
M O P Q R S T U V W X Y Z A B CD E F G H I J K L N
Here is the
replacement for T
ord( T ) − ord( A )
84 − 65
19=
=
In Unicode
Using T as an index
encoder array
Figure 5.21: Illustrating the use of uppercase characters as indices, in this case to
perform the replacement rule for Caesar cipher encryption.
In Code Fragment 5.11, we develop a Python class for performing the Caesar
cipher with an arbitrary rotational shift, and demonstrate its use. When we run this
program (to perform a simple test), we get the following output.
Secret: WKH HDJOH LV LQ SODB; PHHW DW MRH’V.
Message: THE EAGLE IS IN PLAY; MEET AT JOE’S.
The constructor for the class builds the forward and backward translation strings for
the given rotation. With those in hand, the encryption and decryption algorithms
are essentially the same, and so we perform both by means of a nonpublic utility
method named transform.
218 Chapter 5. Array-Based Sequences
1 class CaesarCipher:
2 ”””Class for doing encryption and decryption using a Caesar cipher.”””
3
4 def init (self, shift):
5 ”””Construct Caesar cipher using given integer shift for rotation.”””
6 encoder = [None] 26 # temp array for encryption
7 decoder = [None] 26 # temp array for decryption
8 for k in range(26):
9 encoder[k] = chr((k + shift) % 26 + ord( A ))
10 decoder[k] = chr((k − shift) % 26 + ord( A ))
11 self. forward = .join(encoder) # will store as string
12 self. backward = .join(decoder) # since fixed
13
14 def encrypt(self, message):
15 ”””Return string representing encripted message.”””
16 return self. transform(message, self. forward)
17
18 def decrypt(self, secret):
19 ”””Return decrypted message given encrypted secret.”””
20 return self. transform(secret, self. backward)
21
22 def transform(self, original, code):
23 ”””Utility to perform transformation based on given code string.”””
24 msg = list(original)
25 for k in range(len(msg)):
26 if msg[k].isupper( ):
27 j = ord(msg[k]) − ord( A ) # index from 0 to 25
28 msg[k] = code[j] # replace this character
29 return .join(msg)
30
31 if name == __main__ :
32 cipher = CaesarCipher(3)
33 message = “THE EAGLE IS IN PLAY; MEET AT JOE S.”
34 coded = cipher.encrypt(message)
35 print( Secret: , coded)
36 answer = cipher.decrypt(coded)
37 print( Message: , answer)
Code Fragment 5.11: A complete Python class for the Caesar cipher.
5.6. Multidimensional Data Sets 219
5.6 Multidimensional Data Sets
Lists, tuples, and strings in Python are one-dimensional. We use a single index to
access each element of the sequence. Many computer applications involve mul-
tidimensional data sets. For example, computer graphics are often modeled in
either two or three dimensions. Geographic information may be naturally repre-
sented in two dimensions, medical imaging may provide three-dimensional scans
of a patient, and a company’s valuation is often based upon a high number of in-
dependent financial measures that can be modeled as multidimensional data. A
two-dimensional array is sometimes also called a matrix. We may use two indices,
say i and j, to refer to the cells in the matrix. The first index usually refers to a
row number and the second to a column number, and these are traditionally zero-
indexed in computer science. Figure 5.22 illustrates a two-dimensional data set
with integer values. This data might, for example, represent the number of stores
in various regions of Manhattan.
22 18 709 5 33 10 4 56 82 440
45 32 830 120 750 660 13 77 20 105
4 880 45 66 61 28 650 7 510 67
940 12 36 3 20 100 306 590 0 500
50 65 42 49 88 25 70 126 83 288
398 233 5 83 59 232 49 8 365 90
33 58 632 87 94 5 59 204 120 829
62 394 3 4 102 140 183 390 16 26
8
0
1
2
3
4
5
6
7
0 1 2 3 4 5 6 7 9
Figure 5.22: Illustration of a two-dimensional integer data set, which has 8 rows
and 10 columns. The rows and columns are zero-indexed. If this data set were
named stores, the value of stores[3][5] is 100 and the value of stores[6][2] is 632.
A common representation for a two-dimensional data set in Python is as a list
of lists. In particular, we can represent a two-dimensional array as a list of rows,
with each row itself being a list of values. For example, the two-dimensional data
22 18 709 5 33
45 32 830 120 750
4 880 45 66 61
might be stored in Python as follows.
data = [ [22, 18, 709, 5, 33], [45, 32, 830, 120, 750], [4, 880, 45, 66, 61] ]
An advantage of this representation is that we can naturally use a syntax such
as data[1][3] to represent the value that has row index 1 and column index 3, as
data[1], the second entry in the outer list, is itself a list, and thus indexable.
220 Chapter 5. Array-Based Sequences
Constructing a Multidimensional List
To quickly initialize a one-dimensional list, we generally rely on a syntax such as
data = [0] n to create a list of n zeros. On page 189, we emphasized that from
a technical perspective, this creates a list of length n with all entries referencing
the same integer instance, but that there was no meaningful consequence of such
aliasing because of the immutability of the int class in Python.
We have to be considerably more careful when creating a list of lists. If our
goal were to create the equivalent of a two-dimensional list of integers, with r rows
and c columns, and to initialize all values to zero, a flawed approach might be to
try the command
data = ([0] c) r # Warning: this is a mistake
While([0] c) is indeed a list of c zeros, multiplying that list by r unfortunately cre-
ates a single list with length r · c, just as [2,4,6] 2 results in list [2, 4, 6, 2, 4, 6].
A better, yet still flawed attempt is to make a list that contains the list of c zeros
as its only element, and then to multiply that list by r. That is, we could try the
command
data = [ [0] c ] r # Warning: still a mistake
This is much closer, as we actually do have a structure that is formally a list of lists.
The problem is that all r entries of the list known as data are references to the same
instance of a list of c zeros. Figure 5.23 provides a portrayal of such aliasing.
0
0
00 0 0 0 0
1 2 3 4 5
1 2
data:
Figure 5.23: A flawed representation of a 3× 6 data set as a list of lists, created with
the command data = [ [0] 6 ] 3. (For simplicity, we overlook the fact that the
values in the secondary list are referential.)
This is truly a problem. Setting an entry such as data[2][0] = 100 would change
the first entry of the secondary list to reference a new value, 100. Yet that cell of
the secondary list also represents the value data[0][0], because “row” data[0] and
“row” data[2] refer to the same secondary list.
5.6. Multidimensional Data Sets 221
0 0 0
0
00 0 0 0 0 000 0 00 00 00 0 0
31 2 2 3 4 5 1 2 4 5
1 2
data:
3 4 5 1
Figure 5.24: A valid representation of a 3× 6 data set as a list of lists. (For simplic-
ity, we overlook the fact that the values in the secondary lists are referential.)
To properly initialize a two-dimensional list, we must ensure that each cell of
the primary list refers to an independent instance of a secondary list. This can be
accomplished through the use of Python’s list comprehension syntax.
data = [ [0] c for j in range(r) ]
This command produces a valid configuration, similar to the one shown in Fig-
ure 5.24. By using list comprehension, the expression [0] c is reevaluated for
each pass of the embedded for loop. Therefore, we get r distinct secondary lists, as
desired. (We note that the variable j in that command is irrelevant; we simply need
a for loop that iterates r times.)
Two-Dimensional Arrays and Positional Games
Many computer games, be they strategy games, simulation games, or first-person
conflict games, involve objects that reside in a two-dimensional space. Software for
such positional games need a way of representing such a two-dimensional “board,”
and in Python the list of lists is a natural choice.
Tic-Tac-Toe
As most school children know, Tic-Tac-Toe is a game played in a three-by-three
board. Two players—X and O—alternate in placing their respective marks in the
cells of this board, starting with player X. If either player succeeds in getting three
of his or her marks in a row, column, or diagonal, then that player wins.
This is admittedly not a sophisticated positional game, and it’s not even that
much fun to play, since a good player O can always force a tie. Tic-Tac-Toe’s saving
grace is that it is a nice, simple example showing how two-dimensional arrays can
be used for positional games. Software for more sophisticated positional games,
such as checkers, chess, or the popular simulation games, are all based on the same
approach we illustrate here for using a two-dimensional array for Tic-Tac-Toe.
222 Chapter 5. Array-Based Sequences
Our representation of a 3 × 3 board will be a list of lists of characters, with
X or O designating a player’s move, or designating an empty space. For
example, the board configuration
XO O
O X
X
will be stored internally as
[ [ O , X , O ], [ , X , ], [ , O , X ] ]
We develop a complete Python class for maintaining a Tic-Tac-Toe board for
two players. That class will keep track of the moves and report a winner, but it
does not perform any strategy or allow someone to play Tic-Tac-Toe against the
computer. The details of such a program are beyond the scope of this chapter, but
it might nonetheless make a good course project (see Exercise P-8.68).
Before presenting the implementation of the class, we demonstrate its public
interface with a simple test in Code Fragment 5.12.
1 game = TicTacToe( )
2 # X moves: # O moves:
3 game.mark(1, 1); game.mark(0, 2)
4 game.mark(2, 2); game.mark(0, 0)
5 game.mark(0, 1); game.mark(2, 1)
6 game.mark(1, 2); game.mark(1, 0)
7 game.mark(2, 0)
8
9 print(game)
10 winner = game.winner( )
11 if winner is None:
12 print( Tie )
13 else:
14 print(winner, wins )
Code Fragment 5.12: A simple test for our Tic-Tac-Toe class.
The basic operations are that a new game instance represents an empty board,
that the mark(i,j) method adds a mark at the given position for the current player
(with the software managing the alternating of turns), and that the game board can
be printed and the winner determined. The complete source code for the TicTacToe
class is given in Code Fragment 5.13. Our mark method performs error checking
to make sure that valid indices are sent, that the position is not already occupied,
and that no further moves are made after someone wins the game.
5.6. Multidimensional Data Sets 223
1 class TicTacToe:
2 ”””Management of a Tic-Tac-Toe game (does not do strategy).”””
3
4 def init (self):
5 ”””Start a new game.”””
6 self. board = [ [ ] 3 for j in range(3) ]
7 self. player = X
8
9 def mark(self, i, j):
10 ”””Put an X or O mark at position (i,j) for next player s turn.”””
11 if not (0 <= i <= 2 and 0 <= j <= 2):
12 raise ValueError( Invalid board position )
13 if self. board[i][j] != :
14 raise ValueError( Board position occupied )
15 if self.winner( ) is not None:
16 raise ValueError( Game is already complete )
17 self. board[i][j] = self. player
18 if self. player == X :
19 self. player = O
20 else:
21 self. player = X
22
23 def is win(self, mark):
24 ”””Check whether the board configuration is a win for the given player.”””
25 board = self. board # local variable for shorthand
26 return (mark == board[0][0] == board[0][1] == board[0][2] or # row 0
27 mark == board[1][0] == board[1][1] == board[1][2] or # row 1
28 mark == board[2][0] == board[2][1] == board[2][2] or # row 2
29 mark == board[0][0] == board[1][0] == board[2][0] or # column 0
30 mark == board[0][1] == board[1][1] == board[2][1] or # column 1
31 mark == board[0][2] == board[1][2] == board[2][2] or # column 2
32 mark == board[0][0] == board[1][1] == board[2][2] or # diagonal
33 mark == board[0][2] == board[1][1] == board[2][0]) # rev diag
34
35 def winner(self):
36 ”””Return mark of winning player, or None to indicate a tie.”””
37 for mark in XO :
38 if self. is win(mark):
39 return mark
40 return None
41
42 def str (self):
43 ”””Return string representation of current game board.”””
44 rows = [ | .join(self. board[r]) for r in range(3)]
45 return \n-----\n .join(rows)
Code Fragment 5.13: A complete Python class for managing a Tic-Tac-Toe game.
224 Chapter 5. Array-Based Sequences
5.7 Exercises
For help with exercises, please visit the site, www.wiley.com/college/goodrich.
Reinforcement
R-5.1 Execute the experiment from Code Fragment 5.1 and compare the results
on your system to those we report in Code Fragment 5.2.
R-5.2 In Code Fragment 5.1, we perform an experiment to compare the length of
a Python list to its underlying memory usage. Determining the sequence
of array sizes requires a manual inspection of the output of that program.
Redesign the experiment so that the program outputs only those values of
k at which the existing capacity is exhausted. For example, on a system
consistent with the results of Code Fragment 5.2, your program should
output that the sequence of array capacities are 0, 4, 8, 16, 25, . . . .
R-5.3 Modify the experiment from Code Fragment 5.1 in order to demonstrate
that Python’s list class occasionally shrinks the size of its underlying array
when elements are popped from a list.
R-5.4 Our DynamicArray class, as given in Code Fragment 5.3, does not support
use of negative indices with getitem . Update that method to better
match the semantics of a Python list.
R-5.5 Redo the justification of Proposition 5.1 assuming that the the cost of
growing the array from size k to size 2k is 3k cyber-dollars. How much
should each append operation be charged to make the amortization work?
R-5.6 Our implementation of insert for the DynamicArray class, as given in
Code Fragment 5.5, has the following inefficiency. In the case when a re-
size occurs, the resize operation takes time to copy all the elements from
an old array to a new array, and then the subsequent loop in the body of
insert shifts many of those elements. Give an improved implementation
of the insert method, so that, in the case of a resize, the elements are
shifted into their final position during that operation, thereby avoiding the
subsequent shifting.
R-5.7 Let A be an array of size n ≥ 2 containing integers from 1 to n − 1, inclu-
sive, with exactly one repeated. Describe a fast algorithm for finding the
integer in A that is repeated.
R-5.8 Experimentally evaluate the efficiency of the pop method of Python’s list
class when using varying indices as a parameter, as we did for insert on
page 205. Report your results akin to Table 5.5.
http:\\www.wiley.com/college/goodrich
5.7. Exercises 225
R-5.9 Explain the changes that would have to be made to the program of Code
Fragment 5.11 so that it could perform the Caesar cipher for messages
that are written in an alphabet-based language other than English, such as
Greek, Russian, or Hebrew.
R-5.10 The constructor for the CaesarCipher class in Code Fragment 5.11 can
be implemented with a two-line body by building the forward and back-
ward strings using a combination of the join method and an appropriate
comprehension syntax. Give such an implementation.
R-5.11 Use standard control structures to compute the sum of all numbers in an
n × n data set, represented as a list of lists.
R-5.12 Describe how the built-in sum function can be combined with Python’s
comprehension syntax to compute the sum of all numbers in an n × n data
set, represented as a list of lists.
Creativity
C-5.13 In the experiment of Code Fragment 5.1, we begin with an empty list. If
data were initially constructed with nonempty length, does this affect the
sequence of values at which the underlying array is expanded? Perform
your own experiments, and comment on any relationship you see between
the initial length and the expansion sequence.
C-5.14 The shuffle method, supported by the random module, takes a Python
list and rearranges it so that every possible ordering is equally likely.
Implement your own version of such a function. You may rely on the
randrange(n) function of the random module, which returns a random
number between 0 and n − 1 inclusive.
C-5.15 Consider an implementation of a dynamic array, but instead of copying
the elements into an array of double the size (that is, from N to 2N) when
its capacity is reached, we copy the elements into an array with
N/4�
additional cells, going from capacity N to capacity N +
N/4�. Prove that
performing a sequence of n append operations still runs in O(n) time in
this case.
C-5.16 Implement a pop method for the DynamicArray class, given in Code Frag-
ment 5.3, that removes the last element of the array, and that shrinks the
capacity, N, of the array by half any time the number of elements in the
array goes below N/4.
C-5.17 Prove that when using a dynamic array that grows and shrinks as in the
previous exercise, the following series of 2n operations takes O(n) time:
n append operations on an initially empty array, followed by n pop oper-
ations.
226 Chapter 5. Array-Based Sequences
C-5.18 Give a formal proof that any sequence of n append or pop operations on
an initially empty dynamic array takes O(n) time, if using the strategy
described in Exercise C-5.16.
C-5.19 Consider a variant of Exercise C-5.16, in which an array of capacity N is
resized to capacity precisely that of the number of elements, any time the
number of elements in the array goes strictly below N/4. Give a formal
proof that any sequence of n append or pop operations on an initially
empty dynamic array takes O(n) time.
C-5.20 Consider a variant of Exercise C-5.16, in which an array of capacity N, is
resized to capacity precisely that of the number of elements, any time the
number of elements in the array goes strictly below N/2. Show that there
exists a sequence of n operations that requires Ω(n2) time to execute.
C-5.21 In Section 5.4.2, we described four different ways to compose a long
string: (1) repeated concatenation, (2) appending to a temporary list and
then joining, (3) using list comprehension with join, and (4) using genera-
tor comprehension with join. Develop an experiment to test the efficiency
of all four of these approaches and report your findings.
C-5.22 Develop an experiment to compare the relative efficiency of the extend
method of Python’s list class versus using repeated calls to append to
accomplish the equivalent task.
C-5.23 Based on the discussion of page 207, develop an experiment to compare
the efficiency of Python’s list comprehension syntax versus the construc-
tion of a list by means of repeated calls to append.
C-5.24 Perform experiments to evaluate the efficiency of the remove method of
Python’s list class, as we did for insert on page 205. Use known values so
that all removals occur either at the beginning, middle, or end of the list.
Report your results akin to Table 5.5.
C-5.25 The syntax data.remove(value) for Python list data removes only the first
occurrence of element value from the list. Give an implementation of a
function, with signature remove all(data, value), that removes all occur-
rences of value from the given list, such that the worst-case running time
of the function is O(n) on a list with n elements. Not that it is not efficient
enough in general to rely on repeated calls to remove.
C-5.26 Let B be an array of size n ≥ 6 containing integers from 1 to n − 5, inclu-
sive, with exactly five repeated. Describe a good algorithm for finding the
five integers in B that are repeated.
C-5.27 Given a Python list L of n positive integers, each represented with k =
log n� + 1 bits, describe an O(n)-time method for finding a k-bit integer
not in L.
C-5.28 Argue why any solution to the previous problem must run in Ω(n) time.
Chapter Notes 227
C-5.29 A useful operation in databases is the natural join. If we view a database
as a list of ordered pairs of objects, then the natural join of databases A
and B is the list of all ordered triples (x, y, z) such that the pair (x, y) is in
A and the pair (y, z) is in B. Describe and analyze an efficient algorithm
for computing the natural join of a list A of n pairs and a list B of m pairs.
C-5.30 When Bob wants to send Alice a message M on the Internet, he breaks M
into n data packets, numbers the packets consecutively, and injects them
into the network. When the packets arrive at Alice’s computer, they may
be out of order, so Alice must assemble the sequence of n packets in order
before she can be sure she has the entire message. Describe an efficient
scheme for Alice to do this, assuming that she knows the value of n. What
is the running time of this algorithm?
C-5.31 Describe a way to use recursion to add all the numbers in an n × n data
set, represented as a list of lists.
Projects
P-5.32 Write a Python function that takes two three-dimensional numeric data
sets and adds them componentwise.
P-5.33 Write a Python program for a matrix class that can add and multiply two-
dimensional arrays of numbers, assuming the dimensions agree appropri-
ately for the operation.
P-5.34 Write a program that can perform the Caesar cipher for English messages
that include both upper- and lowercase characters.
P-5.35 Implement a class, SubstitutionCipher, with a constructor that takes a
string with the 26 uppercase letters in an arbitrary order and uses that for
the forward mapping for encryption (akin to the self. forward string in
our CaesarCipher class of Code Fragment 5.11). You should derive the
backward mapping from the forward version.
P-5.36 Redesign the CaesarCipher class as a subclass of the SubstitutionCipher
from the previous problem.
P-5.37 Design a RandomCipher class as a subclass of the SubstitutionCipher
from Exercise P-5.35, so that each instance of the class relies on a random
permutation of letters for its mapping.
Chapter Notes
The fundamental data structures of arrays belong to the folklore of computer science. They
were first chronicled in the computer science literature by Knuth in his seminal book on
Fundamental Algorithms [64].
Chapter
6 Stacks, Queues, and Deques
Contents
6.1 Stacks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 229
6.1.1 The Stack Abstract Data Type . . . . . . . . . . . . . . . 230
6.1.2 Simple Array-Based Stack Implementation . . . . . . . . . 231
6.1.3 Reversing Data Using a Stack . . . . . . . . . . . . . . . 235
6.1.4 Matching Parentheses and HTML Tags . . . . . . . . . . 236
6.2 Queues . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 239
6.2.1 The Queue Abstract Data Type . . . . . . . . . . . . . . 240
6.2.2 Array-Based Queue Implementation . . . . . . . . . . . . 241
6.3 Double-Ended Queues . . . . . . . . . . . . . . . . . . . . . 247
6.3.1 The Deque Abstract Data Type . . . . . . . . . . . . . . 247
6.3.2 Implementing a Deque with a Circular Array . . . . . . . . 248
6.3.3 Deques in the Python Collections Module . . . . . . . . . 249
6.4 Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . 250
6.1. Stacks 229
6.1 Stacks
A stack is a collection of objects that are inserted and removed according to the
last-in, first-out (LIFO) principle. A user may insert objects into a stack at any
time, but may only access or remove the most recently inserted object that remains
(at the so-called “top” of the stack). The name “stack” is derived from the metaphor
of a stack of plates in a spring-loaded, cafeteria plate dispenser. In this case, the
fundamental operations involve the “pushing” and “popping” of plates on the stack.
When we need a new plate from the dispenser, we “pop” the top plate off the stack,
and when we add a plate, we “push” it down on the stack to become the new top
plate. Perhaps an even more amusing example is a PEZ ® candy dispenser, which
stores mint candies in a spring-loaded container that “pops” out the topmost candy
in the stack when the top of the dispenser is lifted (see Figure 6.1). Stacks are
a fundamental data structure. They are used in many applications, including the
following.
Example 6.1: Internet Web browsers store the addresses of recently visited sites
in a stack. Each time a user visits a new site, that site’s address is “pushed” onto the
stack of addresses. The browser then allows the user to “pop” back to previously
visited sites using the “back” button.
Example 6.2: Text editors usually provide an “undo” mechanism that cancels re-
cent editing operations and reverts to former states of a document. This undo oper-
ation can be accomplished by keeping text changes in a stack.
Figure 6.1: A schematic drawing of a PEZ ® dispenser; a physical implementation
of the stack ADT. (PEZ ® is a registered trademark of PEZ Candy, Inc.)
230 Chapter 6. Stacks, Queues, and Deques
6.1.1 The Stack Abstract Data Type
Stacks are the simplest of all data structures, yet they are also among the most
important. They are used in a host of different applications, and as a tool for many
more sophisticated data structures and algorithms. Formally, a stack is an abstract
data type (ADT) such that an instance S supports the following two methods:
S.push(e): Add element e to the top of stack S.
S.pop( ): Remove and return the top element from the stack S;
an error occurs if the stack is empty.
Additionally, let us define the following accessor methods for convenience:
S.top( ): Return a reference to the top element of stack S, without
removing it; an error occurs if the stack is empty.
S.is empty( ): Return True if stack S does not contain any elements.
len(S): Return the number of elements in stack S; in Python, we
implement this with the special method len .
By convention, we assume that a newly created stack is empty, and that there is no
a priori bound on the capacity of the stack. Elements added to the stack can have
arbitrary type.
Example 6.3: The following table shows a series of stack operations and their
effects on an initially empty stack S of integers.
Operation Return Value Stack Contents
S.push(5) – [5]
S.push(3) – [5, 3]
len(S) 2 [5, 3]
S.pop( ) 3 [5]
S.is empty( ) False [5]
S.pop( ) 5 [ ]
S.is empty( ) True [ ]
S.pop( ) “error” [ ]
S.push(7) – [7]
S.push(9) – [7, 9]
S.top( ) 9 [7, 9]
S.push(4) – [7, 9, 4]
len(S) 3 [7, 9, 4]
S.pop( ) 4 [7, 9]
S.push(6) – [7, 9, 6]
S.push(8) – [7, 9, 6, 8]
S.pop( ) 8 [7, 9, 6]
6.1. Stacks 231
6.1.2 Simple Array-Based Stack Implementation
We can implement a stack quite easily by storing its elements in a Python list. The
list class already supports adding an element to the end with the append method,
and removing the last element with the pop method, so it is natural to align the top
of the stack at the end of the list, as shown in Figure 6.2.
0
MB C D E F G K LA
1 2 top
Figure 6.2: Implementing a stack with a Python list, storing the top element in the
rightmost cell.
Although a programmer could directly use the list class in place of a formal
stack class, lists also include behaviors (e.g., adding or removing elements from
arbitrary positions) that would break the abstraction that the stack ADT represents.
Also, the terminology used by the list class does not precisely align with traditional
nomenclature for a stack ADT, in particular the distinction between append and
push. Instead, we demonstrate how to use a list for internal storage while providing
a public interface consistent with a stack.
The Adapter Pattern
The adapter design pattern applies to any context where we effectively want to
modify an existing class so that its methods match those of a related, but different,
class or interface. One general way to apply the adapter pattern is to define a new
class in such a way that it contains an instance of the existing class as a hidden
field, and then to implement each method of the new class using methods of this
hidden instance variable. By applying the adapter pattern in this way, we have
created a new class that performs some of the same functions as an existing class,
but repackaged in a more convenient way. In the context of the stack ADT, we can
adapt Python’s list class using the correspondences shown in Table 6.1.
Stack Method Realization with Python list
S.push(e) L.append(e)
S.pop( ) L.pop( )
S.top( ) L[−1]
S.is empty( ) len(L) == 0
len(S) len(L)
Table 6.1: Realization of a stack S as an adaptation of a Python list L.
232 Chapter 6. Stacks, Queues, and Deques
Implementing a Stack Using a Python List
We use the adapter design pattern to define an ArrayStack class that uses an un-
derlying Python list for storage. (We choose the name ArrayStack to emphasize
that the underlying storage is inherently array based.) One question that remains is
what our code should do if a user calls pop or top when the stack is empty. Our
ADT suggests that an error occurs, but we must decide what type of error. When
pop is called on an empty Python list, it formally raises an IndexError, as lists are
index-based sequences. That choice does not seem appropriate for a stack, since
there is no assumption of indices. Instead, we can define a new exception class that
is more appropriate. Code Fragment 6.1 defines such an Empty class as a trivial
subclass of the Python Exception class.
class Empty(Exception):
”””Error attempting to access an element from an empty container.”””
pass
Code Fragment 6.1: Definition for an Empty exception class.
The formal definition for our ArrayStack class is given in Code Fragment 6.2.
The constructor establishes the member self. data as an initially empty Python list,
for internal storage. The rest of the public stack behaviors are implemented, using
the corresponding adaptation that was outlined in Table 6.1.
Example Usage
Below, we present an example of the use of our ArrayStack class, mirroring the
operations at the beginning of Example 6.3 on page 230.
S = ArrayStack( ) # contents: [ ]
S.push(5) # contents: [5]
S.push(3) # contents: [5, 3]
print(len(S)) # contents: [5, 3]; outputs 2
print(S.pop( )) # contents: [5]; outputs 3
print(S.is empty( )) # contents: [5]; outputs False
print(S.pop( )) # contents: [ ]; outputs 5
print(S.is empty( )) # contents: [ ]; outputs True
S.push(7) # contents: [7]
S.push(9) # contents: [7, 9]
print(S.top( )) # contents: [7, 9]; outputs 9
S.push(4) # contents: [7, 9, 4]
print(len(S)) # contents: [7, 9, 4]; outputs 3
print(S.pop( )) # contents: [7, 9]; outputs 4
S.push(6) # contents: [7, 9, 6]
6.1. Stacks 233
1 class ArrayStack:
2 ”””LIFO Stack implementation using a Python list as underlying storage.”””
3
4 def init (self):
5 ”””Create an empty stack.”””
6 self. data = [ ] # nonpublic list instance
7
8 def len (self):
9 ”””Return the number of elements in the stack.”””
10 return len(self. data)
11
12 def is empty(self):
13 ”””Return True if the stack is empty.”””
14 return len(self. data) == 0
15
16 def push(self, e):
17 ”””Add element e to the top of the stack.”””
18 self. data.append(e) # new item stored at end of list
19
20 def top(self):
21 ”””Return (but do not remove) the element at the top of the stack.
22
23 Raise Empty exception if the stack is empty.
24 ”””
25 if self.is empty( ):
26 raise Empty( Stack is empty )
27 return self. data[−1] # the last item in the list
28
29 def pop(self):
30 ”””Remove and return the element from the top of the stack (i.e., LIFO).
31
32 Raise Empty exception if the stack is empty.
33 ”””
34 if self.is empty( ):
35 raise Empty( Stack is empty )
36 return self. data.pop( ) # remove last item from list
Code Fragment 6.2: Implementing a stack using a Python list as storage.
234 Chapter 6. Stacks, Queues, and Deques
Analyzing the Array-Based Stack Implementation
Table 6.2 shows the running times for our ArrayStack methods. The analysis di-
rectly mirrors the analysis of the list class given in Section 5.3. The implementa-
tions for top, is empty, and len use constant time in the worst case. The O(1) time
for push and pop are amortized bounds (see Section 5.3.2); a typical call to either
of these methods uses constant time, but there is occasionally an O(n)-time worst
case, where n is the current number of elements in the stack, when an operation
causes the list to resize its internal array. The space usage for a stack is O(n).
Operation Running Time
S.push(e) O(1)∗
S.pop( ) O(1)∗
S.top( ) O(1)
S.is empty( ) O(1)
len(S) O(1)
∗amortized
Table 6.2: Performance of our array-based stack implementation. The bounds for
push and pop are amortized due to similar bounds for the list class. The space
usage is O(n), where n is the current number of elements in the stack.
Avoiding Amortization by Reserving Capacity
In some contexts, there may be additional knowledge that suggests a maximum size
that a stack will reach. Our implementation of ArrayStack from Code Fragment 6.2
begins with an empty list and expands as needed. In the analysis of lists from
Section 5.4.1, we emphasized that it is more efficient in practice to construct a list
with initial length n than it is to start with an empty list and append n items (even
though both approaches run in O(n) time).
As an alternate model for a stack, we might wish for the constructor to accept
a parameter specifying the maximum capacity of a stack and to initialize the data
member to a list of that length. Implementing such a model requires significant
changes relative to Code Fragment 6.2. The size of the stack would no longer be
synonymous with the length of the list, and pushes and pops of the stack would not
require changing the length of the list. Instead, we suggest maintaining a separate
integer as an instance variable that denotes the current number of elements in the
stack. Details of such an implementation are left as Exercise C-6.17.
6.1. Stacks 235
6.1.3 Reversing Data Using a Stack
As a consequence of the LIFO protocol, a stack can be used as a general tool to
reverse a data sequence. For example, if the values 1, 2, and 3 are pushed onto a
stack in that order, they will be popped from the stack in the order 3, 2, and then 1.
This idea can be applied in a variety of settings. For example, we might wish
to print lines of a file in reverse order in order to display a data set in decreasing
order rather than increasing order. This can be accomplished by reading each line
and pushing it onto a stack, and then writing the lines in the order they are popped.
An implementation of such a process is given in Code Fragment 6.3.
1 def reverse file(filename):
2 ”””Overwrite given file with its contents line-by-line reversed.”””
3 S = ArrayStack( )
4 original = open(filename)
5 for line in original:
6 S.push(line.rstrip( \n )) # we will re-insert newlines when writing
7 original.close( )
8
9 # now we overwrite with contents in LIFO order
10 output = open(filename, w ) # reopening file overwrites original
11 while not S.is empty( ):
12 output.write(S.pop( ) + \n ) # re-insert newline characters
13 output.close( )
Code Fragment 6.3: A function that reverses the order of lines in a file.
One technical detail worth noting is that we intentionally strip trailing newlines
from lines as they are read, and then re-insert newlines after each line when writing
the resulting file. Our reason for doing this is to handle a special case in which the
original file does not have a trailing newline for the final line. If we exactly echoed
the lines read from the file in reverse order, then the original last line would be fol-
lowed (without newline) by the original second-to-last line. In our implementation,
we ensure that there will be a separating newline in the result.
The idea of using a stack to reverse a data set can be applied to other types of
sequences. For example, Exercise R-6.5 explores the use of a stack to provide yet
another solution for reversing the contents of a Python list (a recursive solution for
this goal was discussed in Section 4.4.1). A more challenging task is to reverse
the order in which elements are stored within a stack. If we were to move them
from one stack to another, they would be reversed, but if we were to then replace
them into the original stack, they would be reversed again, thereby reverting to their
original order. Exercise C-6.18 explores a solution for this task.
236 Chapter 6. Stacks, Queues, and Deques
6.1.4 Matching Parentheses and HTML Tags
In this subsection, we explore two related applications of stacks, both of which
involve testing for pairs of matching delimiters. In our first application, we consider
arithmetic expressions that may contain various pairs of grouping symbols, such as
• Parentheses: “(” and “)”
• Braces: “{” and “}”
• Brackets: “[” and “]”
Each opening symbol must match its corresponding closing symbol. For example, a
left bracket, “[,” must match a corresponding right bracket, “],” as in the expression
[(5+x)-(y+z)]. The following examples further illustrate this concept:
• Correct: ( )(( )){([( )])}
• Correct: ((( )(( )){([( )])}))
• Incorrect: )(( )){([( )])}
• Incorrect: ({[ ])}
• Incorrect: (
We leave the precise definition of a matching group of symbols to Exercise R-6.6.
An Algorithm for Matching Delimiters
An important task when processing arithmetic expressions is to make sure their
delimiting symbols match up correctly. Code Fragment 6.4 presents a Python im-
plementation of such an algorithm. A discussion of the code follows.
1 def is matched(expr):
2 ”””Return True if all delimiters are properly match; False otherwise.”””
3 lefty = ({[ # opening delimiters
4 righty = )}] # respective closing delims
5 S = ArrayStack( )
6 for c in expr:
7 if c in lefty:
8 S.push(c) # push left delimiter on stack
9 elif c in righty:
10 if S.is empty( ):
11 return False # nothing to match with
12 if righty.index(c) != lefty.index(S.pop( )):
13 return False # mismatched
14 return S.is empty( ) # were all symbols matched?
Code Fragment 6.4: Function for matching delimiters in an arithmetic expression.
6.1. Stacks 237
We assume the input is a sequence of characters, such as [(5+x)-(y+z)] .
We perform a left-to-right scan of the original sequence, using a stack S to facilitate
the matching of grouping symbols. Each time we encounter an opening symbol,
we push that symbol onto S, and each time we encounter a closing symbol, we pop
a symbol from the stack S (assuming S is not empty), and check that these two
symbols form a valid pair. If we reach the end of the expression and the stack is
empty, then the original expression was properly matched. Otherwise, there must
be an opening delimiter on the stack without a matching symbol.
If the length of the original expression is n, the algorithm will make at most
n calls to push and n calls to pop. Those calls run in a total of O(n) time, even con-
sidering the amortized nature of the O(1) time bound for those methods. Given that
our selection of possible delimiters, ({[, has constant size, auxiliary tests such as
c in lefty and righty.index(c) each run in O(1) time. Combining these operations,
the matching algorithm on a sequence of length n runs in O(n) time.
Matching Tags in a Markup Language
Another application of matching delimiters is in the validation of markup languages
such as HTML or XML. HTML is the standard format for hyperlinked documents
on the Internet and XML is an extensible markup language used for a variety of
structured data sets. We show a sample HTML document and a possible rendering
in Figure 6.3.
The Little Boat
The storm tossed the little
boat like a cheap sneaker in an
old washing machine. The three
drunken fishermen were used to
such treatment, of course, but
not the tree salesman, who even as
a stowaway now felt that he
had overpaid for the voyage.
- Will the salesman die?
- What color is the boat?
- And what about Naomi?
The Little Boat
The storm tossed the little boat
like a cheap sneaker in an
old washing machine. The three
drunken fishermen were used to
such treatment, of course, but not
the tree salesman, who even as
a stowaway now felt that he had
overpaid for the voyage.
1. Will the salesman die?
2. What color is the boat?
3. And what about Naomi?
(a) (b)
Figure 6.3: Illustrating HTML tags. (a) An HTML document; (b) its rendering.
238 Chapter 6. Stacks, Queues, and Deques
In an HTML document, portions of text are delimited by HTML tags. A simple
opening HTML tag has the form “
the form “
Figure 6.3(a), and the matching tag at the close of that document. Other
commonly used HTML tags that are used in this example include:
• body: document body
• h1: section header
• center: center justify
• p: paragraph
• ol: numbered (ordered) list
• li: list item
Ideally, an HTML document should have matching tags, although most browsers
tolerate a certain number of mismatching tags. In Code Fragment 6.5, we give a
Python function that matches tags in a string representing an HTML document. We
make a left-to-right pass through the raw string, using index j to track our progress
and the find method of the str class to locate the < and > characters that define
the tags. Opening tags are pushed onto the stack, and matched against closing tags
as they are popped from the stack, just as we did when matching delimiters in Code
Fragment 6.4. By similar analysis, this algorithm runs in O(n) time, where n is the
number of characters in the raw HTML source.
1 def is matched html(raw):
2 ”””Return True if all HTML tags are properly match; False otherwise.”””
3 S = ArrayStack( )
4 j = raw.find( < ) # find first ’<’ character (if any) 5 while j != −1: 6 k = raw.find( > , j+1) # find next ’>’ character
7 if k == −1:
8 return False # invalid tag
9 tag = raw[j+1:k] # strip away < >
10 if not tag.startswith( / ): # this is opening tag
11 S.push(tag)
12 else: # this is closing tag
13 if S.is empty( ):
14 return False # nothing to match with
15 if tag[1:] != S.pop( ):
16 return False # mismatched delimiter
17 j = raw.find( < , k+1) # find next ’<’ character (if any) 18 return S.is empty( ) # were all opening tags matched? Code Fragment 6.5: Function for testing if an HTML document has matching tags. 6.2. Queues 239 6.2 Queues Another fundamental data structure is the queue. It is a close “cousin” of the stack, as a queue is a collection of objects that are inserted and removed according to the first-in, first-out (FIFO) principle. That is, elements can be inserted at any time, but only the element that has been in the queue the longest can be next removed. We usually say that elements enter a queue at the back and are removed from the front. A metaphor for this terminology is a line of people waiting to get on an amusement park ride. People waiting for such a ride enter at the back of the line and get on the ride from the front of the line. There are many other applications of queues (see Figure 6.4). Stores, theaters, reservation centers, and other similar services typically process customer requests according to the FIFO principle. A queue would therefore be a logical choice for a data structure to handle calls to a customer service center, or a wait-list at a restaurant. FIFO queues are also used by many computing devices, such as a networked printer, or a Web server responding to requests. Tickets (a) Cal l C ent er Call Queue (b) Figure 6.4: Real-world examples of a first-in, first-out queue. (a) People waiting in line to purchase tickets; (b) phone calls being routed to a customer service center. 240 Chapter 6. Stacks, Queues, and Deques 6.2.1 The Queue Abstract Data Type Formally, the queue abstract data type defines a collection that keeps objects in a sequence, where element access and deletion are restricted to the first element in the queue, and element insertion is restricted to the back of the sequence. This restriction enforces the rule that items are inserted and deleted in a queue accord- ing to the first-in, first-out (FIFO) principle. The queue abstract data type (ADT) supports the following two fundamental methods for a queue Q: Q.enqueue(e): Add element e to the back of queue Q. Q.dequeue( ): Remove and return the first element from queue Q; an error occurs if the queue is empty. The queue ADT also includes the following supporting methods (with first being analogous to the stack’s top method): Q.first( ): Return a reference to the element at the front of queue Q, without removing it; an error occurs if the queue is empty. Q.is empty( ): Return True if queue Q does not contain any elements. len(Q): Return the number of elements in queue Q; in Python, we implement this with the special method len . By convention, we assume that a newly created queue is empty, and that there is no a priori bound on the capacity of the queue. Elements added to the queue can have arbitrary type. Example 6.4: The following table shows a series of queue operations and their effects on an initially empty queue Q of integers. Operation Return Value first ← Q ← last Q.enqueue(5) – [5] Q.enqueue(3) – [5, 3] len(Q) 2 [5, 3] Q.dequeue( ) 5 [3] Q.is empty( ) False [3] Q.dequeue( ) 3 [ ] Q.is empty( ) True [ ] Q.dequeue( ) “error” [ ] Q.enqueue(7) – [7] Q.enqueue(9) – [7, 9] Q.first( ) 7 [7, 9] Q.enqueue(4) – [7, 9, 4] len(Q) 3 [7, 9, 4] Q.dequeue( ) 7 [9, 4] 6.2. Queues 241 6.2.2 Array-Based Queue Implementation For the stack ADT, we created a very simple adapter class that used a Python list as the underlying storage. It may be very tempting to use a similar approach for supporting the queue ADT. We could enqueue element e by calling append(e) to add it to the end of the list. We could use the syntax pop(0), as opposed to pop( ), to intentionally remove the first element from the list when dequeuing. As easy as this would be to implement, it is tragically inefficient. As we dis- cussed in Section 5.4.1, when pop is called on a list with a non-default index, a loop is executed to shift all elements beyond the specified index to the left, so as to fill the hole in the sequence caused by the pop. Therefore, a call to pop(0) always causes the worst-case behavior of Θ(n) time. We can improve on the above strategy by avoiding the call to pop(0) entirely. We can replace the dequeued entry in the array with a reference to None, and main- tain an explicit variable f to store the index of the element that is currently at the front of the queue. Such an algorithm for dequeue would run in O(1) time. After several dequeue operations, this approach might lead to the configuration portrayed in Figure 6.5. 0 E F G K L M 1 2 f Figure 6.5: Allowing the front of the queue to drift away from index 0. Unfortunately, there remains a drawback to the revised approach. In the case of a stack, the length of the list was precisely equal to the size of the stack (even if the underlying array for the list was slightly larger). With the queue design that we are considering, the situation is worse. We can build a queue that has relatively few elements, yet which are stored in an arbitrarily large list. This occurs, for example, if we repeatedly enqueue a new element and then dequeue another (allowing the front to drift rightward). Over time, the size of the underlying list would grow to O(m) where m is the total number of enqueue operations since the creation of the queue, rather than the current number of elements in the queue. This design would have detrimental consequences in applications in which queues have relatively modest size, but which are used for long periods of time. For example, the wait-list for a restaurant might never have more than 30 entries at one time, but over the course of a day (or a week), the overall number of entries would be significantly larger. 242 Chapter 6. Stacks, Queues, and Deques Using an Array Circularly In developing a more robust queue implementation, we allow the front of the queue to drift rightward, and we allow the contents of the queue to “wrap around” the end of an underlying array. We assume that our underlying array has fixed length N that is greater that the actual number of elements in the queue. New elements are enqueued toward the “end” of the current queue, progressing from the front to index N − 1 and continuing at index 0, then 1. Figure 6.6 illustrates such a queue with first element E and last element M. 0 M F G HI J K L E 1 2 f N − 1 Figure 6.6: Modeling a queue with a circular array that wraps around the end. Implementing this circular view is not difficult. When we dequeue an element and want to “advance” the front index, we use the arithmetic f = (f + 1) % N. Re- call that the % operator in Python denotes the modulo operator, which is computed by taking the remainder after an integral division. For example, 14 divided by 3 has a quotient of 4 with remainder 2, that is, 143 = 4 2 3 . So in Python, 14 // 3 evaluates to the quotient 4, while 14 % 3 evaluates to the remainder 2. The modulo operator is ideal for treating an array circularly. As a concrete example, if we have a list of length 10, and a front index 7, we can advance the front by formally computing (7+1) % 10, which is simply 8, as 8 divided by 10 is 0 with a remainder of 8. Similarly, advancing index 8 results in index 9. But when we advance from index 9 (the last one in the array), we compute (9+1) % 10, which evaluates to index 0 (as 10 divided by 10 has a remainder of zero). A Python Queue Implementation A complete implementation of a queue ADT using a Python list in circular fashion is presented in Code Fragments 6.6 and 6.7. Internally, the queue class maintains the following three instance variables: data: is a reference to a list instance with a fixed capacity. size: is an integer representing the current number of elements stored in the queue (as opposed to the length of the data list). front: is an integer that represents the index within data of the first element of the queue (assuming the queue is not empty). We initially reserve a list of moderate size for storing data, although the queue formally has size zero. As a technicality, we initialize the front index to zero. When front or dequeue are called with no elements in the queue, we raise an instance of the Empty exception, defined in Code Fragment 6.1 for our stack. 6.2. Queues 243 1 class ArrayQueue: 2 ”””FIFO queue implementation using a Python list as underlying storage.””” 3 DEFAULT CAPACITY = 10 # moderate capacity for all new queues 4 5 def init (self): 6 ”””Create an empty queue.””” 7 self. data = [None] ArrayQueue.DEFAULT CAPACITY 8 self. size = 0 9 self. front = 0 10 11 def len (self): 12 ”””Return the number of elements in the queue.””” 13 return self. size 14 15 def is empty(self): 16 ”””Return True if the queue is empty.””” 17 return self. size == 0 18 19 def first(self): 20 ”””Return (but do not remove) the element at the front of the queue. 21 22 Raise Empty exception if the queue is empty. 23 ””” 24 if self.is empty( ): 25 raise Empty( Queue is empty ) 26 return self. data[self. front] 27 28 def dequeue(self): 29 ”””Remove and return the first element of the queue (i.e., FIFO). 30 31 Raise Empty exception if the queue is empty. 32 ””” 33 if self.is empty( ): 34 raise Empty( Queue is empty ) 35 answer = self. data[self. front] 36 self. data[self. front] = None # help garbage collection 37 self. front = (self. front + 1) % len(self. data) 38 self. size −= 1 39 return answer Code Fragment 6.6: Array-based implementation of a queue (continued in Code Fragment 6.7). 244 Chapter 6. Stacks, Queues, and Deques 40 def enqueue(self, e): 41 ”””Add an element to the back of queue.””” 42 if self. size == len(self. data): 43 self. resize(2 len(self.data)) # double the array size 44 avail = (self. front + self. size) % len(self. data) 45 self. data[avail] = e 46 self. size += 1 47 48 def resize(self, cap): # we assume cap >= len(self)
49 ”””Resize to a new list of capacity >= len(self).”””
50 old = self. data # keep track of existing list
51 self. data = [None] cap # allocate list with new capacity
52 walk = self. front
53 for k in range(self. size): # only consider existing elements
54 self. data[k] = old[walk] # intentionally shift indices
55 walk = (1 + walk) % len(old) # use old size as modulus
56 self. front = 0 # front has been realigned
Code Fragment 6.7: Array-based implementation of a queue (continued from Code
Fragment 6.6).
The implementation of len and is empty are trivial, given knowledge of
the size. The implementation of the front method is also simple, as the front
index tells us precisely where the desired element is located within the data list,
assuming that list is not empty.
Adding and Removing Elements
The goal of the enqueue method is to add a new element to the back of the queue.
We need to determine the proper index at which to place the new element. Although
we do not explicitly maintain an instance variable for the back of the queue, we
compute the location of the next opening based on the formula:
avail = (self. front + self. size) % len(self. data)
Note that we are using the size of the queue as it exists prior to the addition of the
new element. For example, consider a queue with capacity 10, current size 3, and
first element at index 5. The three elements of such a queue are stored at indices 5,
6, and 7. The new element should be placed at index (front + size) = 8. In a case
with wrap-around, the use of the modular arithmetic achieves the desired circular
semantics. For example, if our hypothetical queue had 3 elements with the first at
index 8, our computation of (8+3) % 10 evaluates to 1, which is perfect since the
three existing elements occupy indices 8, 9, and 0.
6.2. Queues 245
When the dequeue method is called, the current value of self. front designates
the index of the value that is to be removed and returned. We keep a local refer-
ence to the element that will be returned, setting answer = self. data[self. front]
just prior to removing the reference to that object from the list, with the assignment
self. data[self. front] = None. Our reason for the assignment to None relates to
Python’s mechanism for reclaiming unused space. Internally, Python maintains a
count of the number of references that exist to each object. If that count reaches
zero, the object is effectively inaccessible, thus the system may reclaim that mem-
ory for future use. (For more details, see Section 15.1.2.) Since we are no longer
responsible for storing a dequeued element, we remove the reference to it from our
list so as to reduce that element’s reference count.
The second significant responsibility of the dequeue method is to update the
value of front to reflect the removal of the element, and the presumed promotion
of the second element to become the new first. In most cases, we simply want
to increment the index by one, but because of the possibility of a wrap-around
configuration, we rely on modular arithmetic as originally described on page 242.
Resizing the Queue
When enqueue is called at a time when the size of the queue equals the size of the
underlying list, we rely on a standard technique of doubling the storage capacity of
the underlying list. In this way, our approach is similar to the one used when we
implemented a DynamicArray in Section 5.3.1.
However, more care is needed in the queue’s resize utility than was needed in
the corresponding method of the DynamicArray class. After creating a temporary
reference to the old list of values, we allocate a new list that is twice the size and
copy references from the old list to the new list. While transferring the contents, we
intentionally realign the front of the queue with index 0 in the new array, as shown
in Figure 6.7. This realignment is not purely cosmetic. Since the modular arith-
metic depends on the size of the array, our state would be flawed had we transferred
each element to its same index in the new array.
E G HI J K
E F G H I J K
F
f
1 2f = 0
Figure 6.7: Resizing the queue, while realigning the front element with index 0.
246 Chapter 6. Stacks, Queues, and Deques
Shrinking the Underlying Array
A desirable property of a queue implementation is to have its space usage be Θ(n)
where n is the current number of elements in the queue. Our ArrayQueue imple-
mentation, as given in Code Fragments 6.6 and 6.7, does not have this property.
It expands the underlying array when enqueue is called with the queue at full ca-
pacity, but the dequeue implementation never shrinks the underlying array. As a
consequence, the capacity of the underlying array is proportional to the maximum
number of elements that have ever been stored in the queue, not the current number
of elements.
We discussed this very issue on page 200, in the context of dynamic arrays, and
in subsequent Exercises C-5.16 through C-5.20 of that chapter. A robust approach
is to reduce the array to half of its current size, whenever the number of elements
stored in it falls below one fourth of its capacity. We can implement this strategy by
adding the following two lines of code in our dequeue method, just after reducing
self. size at line 38 of Code Fragment 6.6, to reflect the loss of an element.
if 0 < self. size < len(self. data) // 4:
self. resize(len(self. data) // 2)
Analyzing the Array-Based Queue Implementation
Table 6.3 describes the performance of our array-based implementation of the queue
ADT, assuming the improvement described above for occasionally shrinking the
size of the array. With the exception of the resize utility, all of the methods rely
on a constant number of statements involving arithmetic operations, comparisons,
and assignments. Therefore, each method runs in worst-case O(1) time, except
for enqueue and dequeue, which have amortized bounds of O(1) time, for reasons
similar to those given in Section 5.3.
Operation Running Time
Q.enqueue(e) O(1)∗
Q.dequeue( ) O(1)∗
Q.first( ) O(1)
Q.is empty( ) O(1)
len(Q) O(1)
∗amortized
Table 6.3: Performance of an array-based implementation of a queue. The bounds
for enqueue and dequeue are amortized due to the resizing of the array. The space
usage is O(n), where n is the current number of elements in the queue.
6.3. Double-Ended Queues 247
6.3 Double-Ended Queues
We next consider a queue-like data structure that supports insertion and deletion
at both the front and the back of the queue. Such a structure is called a double-
ended queue, or deque, which is usually pronounced “deck” to avoid confusion
with the dequeue method of the regular queue ADT, which is pronounced like the
abbreviation “D.Q.”
The deque abstract data type is more general than both the stack and the queue
ADTs. The extra generality can be useful in some applications. For example, we
described a restaurant using a queue to maintain a waitlist. Occassionally, the first
person might be removed from the queue only to find that a table was not available;
typically, the restaurant will re-insert the person at the first position in the queue. It
may also be that a customer at the end of the queue may grow impatient and leave
the restaurant. (We will need an even more general data structure if we want to
model customers leaving the queue from other positions.)
6.3.1 The Deque Abstract Data Type
To provide a symmetrical abstraction, the deque ADT is defined so that deque D
supports the following methods:
D.add first(e): Add element e to the front of deque D.
D.add last(e): Add element e to the back of deque D.
D.delete first( ): Remove and return the first element from deque D;
an error occurs if the deque is empty.
D.delete last( ): Remove and return the last element from deque D;
an error occurs if the deque is empty.
Additionally, the deque ADT will include the following accessors:
D.first( ): Return (but do not remove) the first element of deque D;
an error occurs if the deque is empty.
D.last( ): Return (but do not remove) the last element of deque D;
an error occurs if the deque is empty.
D.is empty( ): Return True if deque D does not contain any elements.
len(D): Return the number of elements in deque D; in Python,
we implement this with the special method len .
248 Chapter 6. Stacks, Queues, and Deques
Example 6.5: The following table shows a series of operations and their effects
on an initially empty deque D of integers.
Operation Return Value Deque
D.add last(5) – [5]
D.add first(3) – [3, 5]
D.add first(7) – [7, 3, 5]
D.first( ) 7 [7, 3, 5]
D.delete last( ) 5 [7, 3]
len(D) 2 [7, 3]
D.delete last( ) 3 [7]
D.delete last( ) 7 [ ]
D.add first(6) – [6]
D.last( ) 6 [6]
D.add first(8) – [8, 6]
D.is empty( ) False [8, 6]
D.last( ) 6 [8, 6]
6.3.2 Implementing a Deque with a Circular Array
We can implement the deque ADT in much the same way as the ArrayQueue class
provided in Code Fragments 6.6 and 6.7 of Section 6.2.2 (so much so that we leave
the details of an ArrayDeque implementation to Exercise P-6.32). We recommend
maintaining the same three instance variables: data, size, and front. Whenever
we need to know the index of the back of the deque, or the first available slot
beyond the back of the deque, we use modular arithmetic for the computation. For
example, our implementation of the last( ) method uses the index
back = (self. front + self. size − 1) % len(self. data)
Our implementation of the ArrayDeque.add last method is essentially the same
as that for ArrayQueue.enqueue, including the reliance on a resize utility. Like-
wise, the implementation of the ArrayDeque.delete first method is the same as
ArrayQueue.dequeue. Implementations of add first and delete last use similar
techniques. One subtlety is that a call to add first may need to wrap around the
beginning of the array, so we rely on modular arithmetic to circularly decrement
the index, as
self. front = (self. front − 1) % len(self. data) # cyclic shift
The efficiency of an ArrayDeque is similar to that of an ArrayQueue, with all
operations having O(1) running time, but with that bound being amortized for op-
erations that may change the size of the underlying list.
6.3. Double-Ended Queues 249
6.3.3 Deques in the Python Collections Module
An implementation of a deque class is available in Python’s standard collections
module. A summary of the most commonly used behaviors of the collections.deque
class is given in Table 6.4. It uses more asymmetric nomenclature than our ADT.
Our Deque ADT collections.deque Description
len(D) len(D) number of elements
D.add first( ) D.appendleft( ) add to beginning
D.add last( ) D.append( ) add to end
D.delete first( ) D.popleft( ) remove from beginning
D.delete last( ) D.pop( ) remove from end
D.first( ) D[0] access first element
D.last( ) D[−1] access last element
D[j] access arbitrary entry by index
D[j] = val modify arbitrary entry by index
D.clear( ) clear all contents
D.rotate(k) circularly shift rightward k steps
D.remove(e) remove first matching element
D.count(e) count number of matches for e
Table 6.4: Comparison of our deque ADT and the collections.deque class.
The collections.deque interface was chosen to be consistent with established
naming conventions of Python’s list class, for which append and pop are presumed
to act at the end of the list. Therefore, appendleft and popleft designate an opera-
tion at the beginning of the list. The library deque also mimics a list in that it is an
indexed sequence, allowing arbitrary access or modification using the D[j] syntax.
The library deque constructor also supports an optional maxlen parameter to
force a fixed-length deque. However, if a call to append at either end is invoked
when the deque is full, it does not throw an error; instead, it causes one element to
be dropped from the opposite side. That is, calling appendleft when the deque is
full causes an implicit pop from the right side to make room for the new element.
The current Python distribution implements collections.deque with a hybrid ap-
proach that uses aspects of circular arrays, but organized into blocks that are them-
selves organized in a doubly linked list (a data structure that we will introduce in
the next chapter). The deque class is formally documented to guarantee O(1)-time
operations at either end, but O(n)-time worst-case operations when using index
notation near the middle of the deque.
250 Chapter 6. Stacks, Queues, and Deques
6.4 Exercises
For help with exercises, please visit the site, www.wiley.com/college/goodrich.
Reinforcement
R-6.1 What values are returned during the following series of stack operations, if
executed upon an initially empty stack? push(5), push(3), pop(), push(2),
push(8), pop(), pop(), push(9), push(1), pop(), push(7), push(6), pop(),
pop(), push(4), pop(), pop().
R-6.2 Suppose an initially empty stack S has executed a total of 25 push opera-
tions, 12 top operations, and 10 pop operations, 3 of which raised Empty
errors that were caught and ignored. What is the current size of S?
R-6.3 Implement a function with signature transfer(S, T) that transfers all ele-
ments from stack S onto stack T, so that the element that starts at the top
of S is the first to be inserted onto T, and the element at the bottom of S
ends up at the top of T.
R-6.4 Give a recursive method for removing all the elements from a stack.
R-6.5 Implement a function that reverses a list of elements by pushing them onto
a stack in one order, and writing them back to the list in reversed order.
R-6.6 Give a precise and complete definition of the concept of matching for
grouping symbols in an arithmetic expression. Your definition may be
recursive.
R-6.7 What values are returned during the following sequence of queue opera-
tions, if executed on an initially empty queue? enqueue(5), enqueue(3),
dequeue(), enqueue(2), enqueue(8), dequeue(), dequeue(), enqueue(9),
enqueue(1), dequeue(), enqueue(7), enqueue(6), dequeue(), dequeue(),
enqueue(4), dequeue(), dequeue().
R-6.8 Suppose an initially empty queue Q has executed a total of 32 enqueue
operations, 10 first operations, and 15 dequeue operations, 5 of which
raised Empty errors that were caught and ignored. What is the current
size of Q?
R-6.9 Had the queue of the previous problem been an instance of ArrayQueue
that used an initial array of capacity 30, and had its size never been greater
than 30, what would be the final value of the front instance variable?
R-6.10 Consider what happens if the loop in the ArrayQueue. resize method at
lines 53–55 of Code Fragment 6.7 had been implemented as:
for k in range(self. size):
self. data[k] = old[k] # rather than old[walk]
Give a clear explanation of what could go wrong.
http:\\www.wiley.com/college/goodrich
6.4. Exercises 251
R-6.11 Give a simple adapter that implements our queue ADT while using a
collections.deque instance for storage.
R-6.12 What values are returned during the following sequence of deque ADT op-
erations, on initially empty deque? add first(4), add last(8), add last(9),
add first(5), back( ), delete first( ), delete last( ), add last(7), first( ),
last( ), add last(6), delete first( ), delete first( ).
R-6.13 Suppose you have a deque D containing the numbers (1, 2, 3, 4, 5, 6, 7, 8),
in this order. Suppose further that you have an initially empty queue Q.
Give a code fragment that uses only D and Q (and no other variables) and
results in D storing the elements in the order (1, 2, 3, 5, 4, 6, 7, 8).
R-6.14 Repeat the previous problem using the deque D and an initially empty
stack S.
Creativity
C-6.15 Suppose Alice has picked three distinct integers and placed them into a
stack S in random order. Write a short, straight-line piece of pseudo-code
(with no loops or recursion) that uses only one comparison and only one
variable x, yet that results in variable x storing the largest of Alice’s three
integers with probability 2/3. Argue why your method is correct.
C-6.16 Modify the ArrayStack implementation so that the stack’s capacity is lim-
ited to maxlen elements, where maxlen is an optional parameter to the
constructor (that defaults to None). If push is called when the stack is at
full capacity, throw a Full exception (defined similarly to Empty).
C-6.17 In the previous exercise, we assume that the underlying list is initially
empty. Redo that exercise, this time preallocating an underlying list with
length equal to the stack’s maximum capacity.
C-6.18 Show how to use the transfer function, described in Exercise R-6.3, and
two temporary stacks, to replace the contents of a given stack S with those
same elements, but in reversed order.
C-6.19 In Code Fragment 6.5 we assume that opening tags in HTML have form
to be expressed as part of an opening tag. The general form used is
a table can be given a border and additional padding by using an opening
tag of