Data Structures & Algorithms in Python - Com
Data Structures & Algorithms in Python - Com
John Canning
Alan Broder
Robert Lafore
John Canning
Alan Broder
Contents
1. Overview
2. Arrays
3. Simple Sorting
5. Linked Lists
6. Recursion
7. Advanced Sorting
8. Binary Trees
13. Heaps
14. Graphs
2. Arrays
The Array Visualization Tool
Using Python Lists to Implement the Array Class
The Ordered Array Visualization Tool
Python Code for an Ordered Array Class
Logarithms
Storing Objects
Big O Notation
Why Not Use Arrays for Everything?
Summary
Questions
Experiments
Programming Projects
3. Simple Sorting
How Would You Do It?
Bubble Sort
Selection Sort
nsertion Sort
Comparing the Simple Sorts
Summary
Questions
Experiments
Programming Projects
5. Linked Lists
Links
The Linked List Visualization Tool
A Simple Linked List
Linked List Efficiency
Abstract Data Types and Objects
Ordered Lists
Doubly Linked Lists
Circular Lists
terators
Summary
Questions
Experiments
Programming Projects
6. Recursion
Triangular Numbers
Factorials
Anagrams
A Recursive Binary Search
The Tower of Hanoi
Sorting with mergesort
Eliminating Recursion
Some Interesting Recursive Applications
Summary
Questions
Experiments
Programming Projects
7. Advanced Sorting
Shellsort
Partitioning
Quicksort
Degenerates to O(N2) Performance
Radix Sort
Timsort
Summary
Questions
Experiments
Programming Projects
8. Binary Trees
Why Use Binary Trees?
Tree Terminology
An Analogy
How Do Binary Search Trees Work?
Finding a Node
nserting a Node
Traversing the Tree
Finding Minimum and Maximum Key Values
Deleting a Node
The Efficiency of Binary Search Trees
Trees Represented as Arrays
Printing Trees
Duplicate Keys
The BinarySearchTreeTester.py Program
The Huffman Code
Summary
Questions
Experiments
Programming Projects
13. Heaps
ntroduction to Heaps
The Heap Visualization Tool
Python Code for Heaps
A Tree-Based Heap
Heapsort
Order Statistics
Summary
Questions
Experiments
Programming Projects
14. Graphs
ntroduction to Graphs
Traversal and Search
Minimum Spanning Trees
Topological Sorting
Connectivity in Directed Graphs
Summary
Questions
Experiments
Programming Projects
Structure
Each chapter presents a particular group of data structures and associated
algorithms. At the end of the chapters, we provide review questions
covering the key points in the chapter and sometimes relationships to
previous chapters. The answers for these can be found in Appendix C,
“Answers to Questions.” These questions are intended as a self-test for
readers, to ensure you understood all the material.
Many chapters suggest experiments for readers to try. These can be
individual thought experiments, team assignments, or exercises with the
software tools provided with the book. These are designed to apply the
knowledge just learned to some other area and help deepen your
understanding.
Programming projects are longer, more challenging programming exercises.
We provide a range of projects of different levels of difficulty. These
projects might be used in classroom settings as homework assignments.
Sample solutions to the programming projects are available to qualified
instructors from the publisher.
History
Mitchell Waite and Robert Lafore developed the first version of this book
and titled it Data Structures and Algorithms in Java. The first edition was
published in 1998, and the second edition, by Robert, came out in 2002.
John Canning and Alan Broder developed this version using Python due to
its popularity in education and commercial and noncommercial software
development. Java is widely used and an important language for computer
scientists to know. With many schools adopting Python as a first
programming language, the need for textbooks that introduce new concepts
in an already familiar language drove the development of this book. We
expanded the coverage of data structures and updated many of the
examples.
We’ve tried to make the learning process as painless as possible. We hope
this text makes the core, and frankly, the beauty of computer science
accessible to all. Beyond just understanding, we hope you find learning
these ideas fun. Enjoy yourself!
1. Overview
You have written some programs and learned enough to think that
programming is fun, or at least interesting. Some parts are easy, and some parts
are hard. You’d like to know more about how to make the process easier, get
past the hard parts, and conquer more complex tasks. You are starting to study
the heart of computer science, and that brings up many questions. This chapter
sets the stage for learning how to make programs that work properly and fast. It
explains a bunch of new terms and fills in background about the programming
language that we use in the examples.
In This Chapter
• What Are Data Structures and Algorithms?
• Overview of Data Structures
• Overview of Algorithms
• Some Definitions
• Programming in Python
• Object-Oriented Programming
Some Definitions
This section provides some definitions of key terms.
Database
We use the term database to refer to the complete collection of data that’s
being processed in a particular situation. Using the example of people
interested in tickets, the database could contain the phone numbers, the names,
the desired number of tickets, and the tickets awarded. This is a broader
definition than what’s meant by a relational database or object-oriented
database.
Record
Records group related data and are the units into which a database is divided.
They provide a format for storing information. In the ticket distribution
example, a record could contain a person’s name, a person’s phone number, a
desired number of tickets, and a number of awarded tickets. A record typically
includes all the information about some entity, in a situation in which there are
many such entities. A record might correspond to a user of a banking
application, a car part in an auto supply inventory, or a stored video in a
collection of videos.
Field
Records are usually divided into several fields. Each field holds a particular
kind of data. In the ticket distribution example, the fields could be as shown in
Figure 1-1.
Key
When searching for records or sorting them, one of the fields is called the key
(or search key or sort key). Search algorithms look for an exact match of the
key value to some target value and return the record containing it. The program
calling the search routine can then access all the fields in the record. For
example, in the ticket distribution system, you might search for a record by a
particular phone number and then look at the number of desired tickets in that
record. Another kind of search could use a different key. For example, you
could search for a record using the desired tickets as search key and look for
people who want three tickets. Note in this case that you could define the
search to return the first such record it finds or a collection of all records where
the desired number of tickets is three.
Programming in Python
Python is a programming language that debuted in 1991. It embraces object-
oriented programming and introduced syntax that made many common
operations very concise and elegant. One of the first things that programmers
new to Python notice is that certain whitespace is significant to the meaning of
the program. That means that when you edit Python programs, you should use
an editor that recognizes its syntax and helps you create the program as you
intend it to work. Many editors do this, and even editors that don’t recognize
the syntax by filename extension or the first few lines of text can often be
configured to use Python syntax for a particular file.
Interpreter
Python is an interpreted language, which means that even though there is a
compiler, you can execute programs and individual expressions and statements
by passing the text to an interpreter program. The compiler works by
translating the source code of a program into bytecode that is more easily read
by the machine and more efficient to process. Many Python programmers
never have to think about the compiler because the Python interpreter runs it
automatically, when appropriate.
Interpreted languages have the great benefit of allowing you to try out parts of
your code using an interactive command-line interpreter. There are often
multiple ways to start a Python interpreter, depending on how Python was
installed on the computer. If you use an Integrated Development Environment
(IDE) such as IDLE, which comes with most Python distributions, there is a
window that runs the command-line interpreter. The method for starting the
interpreter differs between IDEs. When IDLE is launched, it automatically
starts the command-line interpreter and calls it the Shell.
On computers that don’t have a Python IDE installed, you can still launch the
Python interpreter from a command-line interface (sometimes called a terminal
window, or shell, or console). In that command-line interface, type python and
then press the Return or Enter key. It should display the version of Python you
are using along with some other information, and then wait for you to type
some expression in Python. After reading the expression, the interpreter
decides if it’s complete, and if it is, computes the value of the expression and
prints it. The example in Listing 1-1 shows using the Python interpreter to
compute some math results.
$ python
Python 3.6.0 (default, Dec 23 2016, 13:19:00)
Type "help", "copyright", "credits" or "license" for more information.
>>> 2019 - 1991
28
>>> 2**32 - 1
4294967295
>>> 10**27 + 1
1000000000000000000000000001
>>> 10**27 + 1.001
1e+27
>>>
In Listing 1-1, we’ve colored the text that you type in blue italics. The first
dollar sign ($) is the prompt from command-line interpreter. The Python
interpreter prints out the rest of the text. The Python we use in this book is
version 3. If you see Python 2… on the first line, then you have an older version
of the Python interpreter. Try running python3 in the command-line interface
to see if Python version 3 is already installed on the computer. If not, either
upgrade the version of Python or find a different computer that has python3.
The differences between Python 2 and 3 can be subtle and difficult to
understand for new programmers, so it’s important to get the right version.
There are also differences between every minor release version of Python, for
example, between versions 3.8 and 3.9. Check the online documentation at
https://docs.python.org to find the changes.
The interpreter continues prompting for Python expressions, evaluating them,
and printing their values until you ask it to stop. The Python interpreter
prompts for expressions using >>>. If you want to terminate the interpreter and
you’re using an IDE, you typically quit the IDE application. For interpreters
launched in a command-line interface, you can press Ctrl-D or sometimes Ctrl-
C to exit the Python interpreter. In this book, we show all of the Python
examples being launched from a command line, with a command that starts
with $ python3.
In Listing 1-1, you can see that simple arithmetic expressions produce results
like other programming languages. What might be less obvious is that small
integers and very large integers (bigger than what fits in 32 or 64 bits of data)
can be calculated and used just like smaller integers. For example, look at the
result of the expression 10**27 + 1. Note that these big integers are not the
same as floating-point numbers. When adding integers and floating-point
numbers as in 10**27 + 1.0001, the big integer is converted to floating-point
representation. Because floating-point numbers only have enough precision for
a fixed number of decimal places, the result is rounded to 1e+27 or 1 × 1027.
Whitespace syntax is important even when using the Python interpreter
interactively. Nested expressions use indentation instead of a visible character
to enclose the expressions that are evaluated conditionally. For example,
Python if statements demarcate the then-expression and the else-expression
by indentation. In C++ and JavaScript, you could write
if (x / 2 == 1) {do_two(x)}
else {do_other(x)}
The curly braces enclose the two expressions. In Python, you would write
if x / 2 == 1:
do_two(x)
else:
do_other(x)
You must indent the two procedure call lines for the interpreter to recognize
their relation to the line before it. You must be consistent in the indentation,
using the same tabs or spaces, on each indented line for the interpreter to know
the nested expressions are at the same level. Think of the indentation changes
as replacements for the open curly brace and the close curly brace. When the
indent increases, it’s a left brace. When it decreases, it is a right brace.
When you enter the preceding expression interactively, the Python interpreter
prompts for additional lines with the ellipsis prompt (…). These prompts
continue until you enter an empty line to signal the end of the top-level
expression. The transcript looks like this, assuming that x is 3 and the
do_other() procedure prints a message:
>>> if x / 2 == 1:
... do_two(x)
... else:
... do_other(x)
...
Processing other value
>>>
Note, if you’ve only used Python 2 before, the preceding result might surprise
you, and you should read the details of the differences between the two
versions at https://docs.python.org. To get integer division in Python 3, use the
double slash (//) operator.
Python requires that the indentation of logical lines be the same if they are at
the same level of nesting. Logical lines are complete statements or expressions.
A logical line might span multiple lines of text, such as the previous if
statement. The next logical line to be executed after the if statement’s then or
else clause should start at the same indentation as the if statement does. The
deeper indentation indicates statements that are to be executed later (as in a
function definition), conditionally (as in an else clause), repeatedly (as in a
loop), or as parts of larger construct (as in a class definition). If you have long
expressions that you would prefer to split across multiple lines, they either
• Need to be inside parentheses or one of the other bracketed expression
types (lists, tuples, sets, or dictionaries), or
• Need to terminate with the backslash character (\) in all but the last line
of the expression
Inside of parentheses/brackets, the indentation can be whatever you like
because the closing parenthesis/bracket determines where the expression ends.
When the logical line containing the expression ends, the next logical line
should be at the same level of indentation as the one just finished. The
following example shows some unusual indentation to illustrate the idea:
>>> x = 9
>>> if (x %
... 2 == 0):
... if (x %
... 3 == 0):
... ’Divisible by 6’
... else:
... ’Divisible by 2’
... else:
... if (x %
... 3 == 0):
... ’Divisible by 3’
... else:
... ’Not divisble by 2 or 3’
...
’Divisible by 3’
The tests of divisibility in the example occur within parentheses and are split
across lines in an arbitrary way. Because the parentheses are balanced, the
Python interpreter knows where the if test expressions end and doesn’t
complain about the odd indentation. The nested if statements, however, must
have the same indentation to be recognized as being at equal levels within the
conditional tests. The else clauses must be at the same indentation as the
corresponding if statement for the interpreter to recognize their relationship. If
the first else clause is omitted as in the following example,
>>> if (x %
... 2 == 0):
... if (x %
... 3 == 0):
... ’Divisible by 6’
... else:
... if (x %
... 3 == 0):
... ’Divisible by 3’
... else:
... ’Not divisble by 2 or 3’
...
’Divisible by 3’
then the indentation makes clear that the first else clause now belongs to the
if (x % 2 == 0) and not the nested if (x % 3 == 0). If x is 4, then the
statement would evaluate to None because the else clause was omitted. The
mandatory indentation makes the structure clearer, and mixing in
unconventional indentation makes the program very hard to read!
Whitespace inside of strings is important and is preserved. Simple strings are
enclosed in single (‘) or double (“) quote characters. They cannot span lines but
may contain escaped whitespace such as newline (\n) or tab (\t) characters,
e.g.,
The interpreter reads the double-quoted string from the input and shows it in
printed representation form, essentially the same as the way it would be
entered in source code with the backslashes used to escape the special
whitespace. If that same double-quoted string is given to the print function, it
prints the embedded whitespace in output form. To create long strings with
many embedded newlines, you can enclose the string in triple quote characters
(either single or double quotes).
>>> """Python
... enforces readability
... using structured
... indentation.
... """
’Python\nenforces readability\nusing structured\nindentation.\n’
Long, multiline strings are especially useful as documentation strings in
function definitions.
You can add comments to the code by starting them with the pound symbol (#)
and continuing to the end of the line. Multiline comments must each have their
own pound symbol on the left. For example:
def within(x, lo, hi): # Check if x is within the [lo, hi] range
return lo <= x and x <= hi # Include hi in the range
We’ve added some color highlights to the comments and reserved words used
by Python like def, return, and and, to improve readability. We discuss the
meaning of those terms shortly. Note that comments are visible in the source
code files but not available in the runtime environment. The documentation
strings mentioned previously are attached to objects in the code, like function
definitions, and are available at runtime.
Dynamic Typing
The next most noticeable difference between Python and some other languages
is that it uses dynamic typing. That means that the data types of variables are
determined at runtime, not declared at compile time. In fact, Python doesn’t
require any variable declarations at all; simply assigning a value to variable
identifier creates the variable. You can change the value and type in later
assignments. For example,
>>> x = 2
>>> x
2
>>> x = 2.71828
>>> x
2.71828
>>> x = ’two’
>>> x
’two’
(x := 2) ** 2 + (y := 3) ** 2
Sequences
Arrays are different in Python than in other languages. The built-in data type
that “looks like” an array is called a list. Python’s list type is a hybrid of the
arrays and linked lists found in other languages. As with variables, the
elements of Python lists are dynamically typed, so they do not all have to be of
the same type. The maximum number of elements does not need to be declared
when creating a list, and lists can grow and shrink at runtime. What’s quite
different between Python lists and other linked list structures is that they can
be indexed like arrays to retrieve any element at any position. There is no data
type called array in the core of Python, but there is an array module that can
be imported. The array module allows for construction of arrays of fixed-
typed elements.
In this book, we make extensive use of the built-in list data type as if it were
an array. This is for convenience of syntax, and because the underlying
implementation of the list acts like arrays do in terms of indexed access to
elements. Please note, however, that we do not use all of the features that the
Python list type provides. The reason is that we want to show how fixed-type,
fixed-length arrays behave in all computer languages. For new programmers,
it’s better to use the simpler syntax that comes with the built-in list for
constructing arrays while learning how to manipulate their contents in an
algorithm.
Python’s built-in lists can be indexed using 0-relative indexes. For example:
>>> s = ’π = 3.14159’
>>> s
’π = 3.14159’
>>> len(s)
11
>>> π = 3.14159
>>> π
3.14159
In the preceding example, the string, s, contains the Greek letter π, which is
counted as one character by the len() function, whereas the Unicode character
takes two bytes of space. Unicode characters can also be used in variable
names in Python 3 as shown by using π as a variable name.
Python treats all the data types that can be indexed, such as lists, arrays, and
strings, as sequence data types. The sequence data types can be sliced to form
new sequences. Slicing means creating a subsequence of the data, which is
equivalent to getting a substring for strings. Slices are specified by a start and
end index, separated by a colon (:) character. Every element from the start
index up to, but not including, the end index is copied into the new sequence.
The start index defaults to 0, the beginning of the sequence, and the end index
defaults to the length of sequence. You can use negative numbers for both array
and slice indexes. Negative indices count backwards from the end of the
sequence; −1 means the last element, −2 means the second to last element, and
so on. Here are some examples with a string:
The preceding example shows two lists concatenated with the plus (+)
operator to form a longer list. Multiplying a string by an integer produces that
many copies of the string, concatenated together. The in operator is a Boolean
test that searches for an element in a sequence. It uses the == equality test to
determine whether the element matches. These operations work with all
sequence data types. This compact syntax hides some of the complexity of
stepping through each sequence element and doing some operation such as
equality testing or copying the value over to a new sequence.
>>> total = 0
>>> for x in [5, 4, 3, 2, 1]:
... total += x
...
>>> total
15
The for variable in sequence syntax is the basic loop construct (or iteration)
in Python. The nested expression is evaluated once for each value in the
sequence with the variable bound to the value. There is no need to explicitly
manipulate an index variable that points to the current element of the sequence;
that’s handled by the Python interpreter. One common mistake when trying to
enter this expression in the interactive interpreter is to forget the empty line
after the nested expression.
>>> total = 0
>>> for x in [5, 4, 3, 2, 1]:
... total += x
... total
File "<stdin>", line 3
total
^
SyntaxError: invalid syntax
The reason this is so common is that the empty line is needed only for the
interactive interpreter; the same Python expressions written in a file would not
report this as an error. The interactive interpreter, however, waits for the empty
line to signal the end of the for loop and begin evaluation of that full
expression. When the interpreter finds a new expression starting at the same
indent level as the for loop, it is dealing with two consecutive expressions and
does not allow it. The interpreter expects to read one expression, evaluate it,
and print the value, before starting to determine where the next expression
begins and ends.
In some circumstances, having an explicit index variable is important. In those
cases, there are a couple of convenient ways to perform the work. For example:
>>> height = [5, 4, 7, 2, 3]
>>> weightedsum = 0
>>> for i in range(len(height)):
... weightedsum += i * height[i]
...
>>> weightedsum
36
>>> for i, h in enumerate(height):
... weightedsum += i * h
...
>>> weightedsum
72
The example calculates a weighted sum where we multiply each value in the
height list by the index of that value. The range() function can be thought of
as a function that produces a list of integers starting at 0 and going up to, but
not equal to, its argument. By passing len(height) as an argument, range()
produces the list [0, 1, 2, 3, 4]. In the body of the first for loop, the
weightedsum variable is incremented by the product of the index, i, and the
value that i indexes in the height list. The second for loop repeats the same
calculation using a slightly more concise form called enumeration. The
enumerate() function can be thought of as taking a sequence as input and
producing a sequence of pairs. The first element of each pair is an index, and
the second is the corresponding value from its sequence argument. The second
for loop has two variables separated by a comma, i and h, instead of just one
in the previous loops. On each iteration of the enumerate loop, i is bound to
the index and h is bound to the corresponding value from height. Python
makes the common pattern of looping over a sequence very easy to write, both
with or without an index variable.
The range() and enumerate() functions actually create iterators, which are
complex data types that get called in each loop iteration to get the next value of
the sequence. It doesn’t actually produce a list in memory for the full sequence.
We discuss how iterators can be used to represent very long sequences without
taking up much memory in Chapter 5, “Linked Lists.”
Multivalued Assignment
The comma-separated list of variables can also be used in assignment
statements to perform multiple assignments with a single equal sign (=)
operator. This makes the most sense when all the values being assigned are
closely related, like coordinates of a vector. To illustrate:
>>> x, y, z = 3, 4, 5
>>> y
4
>>> (x, y, z) = [7, 8, 9]
>>> y
8
The sequences on both sides of the assignment operator must have the same
length; otherwise, an error occurs. The sequences can be of different types. The
second assignment here uses a tuple on the left and a list on the right side.
Python’s tuple data type is also a sequence data type very similar to a list, with
the distinction that its elements cannot be modified. In this case, the tuple on
the left must be a tuple of variable names. The comma is the operator that
creates the tuple of the x, y, and z variables. Each of them is bound to the
corresponding value in the list on the right. Looking back at the enumerate loop
in the previous example, the iterations of the loop are performing the
equivalent of
>>> i, h = (0, 5)
>>> weightedsum += i * h
>>> i, h = (1, 4)
>>> weightedsum += i * h
>>> i, h = (2, 7)
>>> weightedsum += i * h
>>> i, h = (3, 2)
>>> weightedsum += i * h
>>> i, h = (4, 3)
>>> weightedsum += i * h
The Python interpreter evaluates all the expressions on the right of the equal
sign, puts the results in a tuple, and then makes the assignments to the variables
on the left side from the tuple. The tuple holding the results is something like a
hidden temporary variable. The parentheses surrounding the tuples on both
sides of the equal sign operator are optional.
There is another kind of assignment statement that looks like the multivalued
assignment but is different. You use it to assign the same value to multiple
variables. For example:
Several variables are all assigned the same value. The overall assignment
statement, however, still evaluates as None.
Importing Modules
Functions can return multiple values in the form of tuples, too. You can decide
whether to store all these values as a single tuple, or in several variables, one
for each of the tuple’s elements, just like the multivalued assignment does. A
good example of this is splitting pathname components. The os module is in
Python’s standard library and provides many tools to operate on the underlying
operating system where Python is running. To access a module, you import it.
After it is imported, you refer to its contents via the module name, followed by
a period, and then the name of one of its functions or other definitions. For
example:
_, extension = os.path.splitext(’myfile.ext’)
Inside the body of the print subroutine, the objects parameter holds the list of
things to print. The printed output is the string representation of each object
value separated by a space character and terminated by a newline, by default.
Normally, the output string is sent to the standard output stream and can be
buffered. If, instead, you wanted to send the output to the standard error stream
separated by tab characters, you could write
In this example, the call to print has three positional parameters that all get
bound to the objects list. Note that the file and sep arguments are not in the
same position as their corresponding parameters.
List Comprehensions
Applying a calculation or function on each element of a list to produce a new
list is such a useful concept that Python has a special syntax for it. The concept
is called a list comprehension, and it is written as a little loop inside the
brackets used to create lists:
The for variable in sequence part is the same syntax as for procedural loops.
The expression at the beginning is the same as the loop body (except that it
can’t contain multiple statements; only a single expression is allowed). The
Python interpreter goes through the sequence getting each element and binding
the variable to that element. It evaluates the expression in the context of the
bound variable to compute a value. That computed value is put in the output
sequence in the same position as the element from the input sequence. Here are
two equivalent ways to produce a list of squares of the values in another list:
The first few lines in the example show a simple loop that appends the squares
to an initially empty list. The last line in the example collapses all those lines
(other than defining the values) into a single expression. The whitespace just
inside the square brackets is optional, but many programmers put it in to make
it clearer that this is a list comprehension instead of a list created by evaluating
a series of comma-separated expressions.
The list comprehension is a very compact syntax for describing the incredibly
useful concept of mapping. It hides all the implementation details of indices
and loop exit conditions while still making the essence of the operation very
clear. Mapping is used everywhere in programming. In many applications the
same operation needs to be applied over huge amounts of data and collected
into a similar structure to the input.
Let’s look at a couple more comprehensions to get the idea. To get the cube of
all the integers between 10 and 20, you can write
The loop can also have a filter condition. Only those elements satisfying the
condition are put in the output sequence. The filter expression goes at the end,
that is,
The filter expression should evaluate to True for those sequence elements you
wish to keep in the output. For example, let’s get the cube of all the integers
between 10 and 20 that are not multiples of 3:
These compact forms make it easy to describe the core operation while hiding
many of the looping details needed to implement basic operations.
Exceptions
Python allows programs to define what happens when particular exception
conditions occur. These exceptions aren’t just for errors; Python uses
exceptions to handle things like the end of iteration for loops, keyboard
interrupts, and timer expiration, which are all expected events. Your programs
can define new kinds of exceptions and ways to handle them if they occur.
Exception handling can be defined within try except statements. The basic
form is
try:
<statements>
except:
<statements>
The set of statements after the try: are executed from top to bottom, and if and
only if an exception occurs during that execution, the statements after the
except: are executed. If no exception occurs, then the next logical line after
the try except statement is executed.
Programs can define different handlers for different kinds of exceptions. For
example:
try:
long_running_function()
except KeyboardInterrupt:
print(’Keyboard interrupted long running function’)
except IndexError:
print(’Index out of range during long running function’)
Each of the except clauses can specify a type (class) of exception. Exceptions
of that type (or a subclass of the declared class) trigger the corresponding
statements to be executed. When no exception class is specified in an except
clause, as in the basic form example, any type of exception triggers the
execution of the clause.
When a program doesn’t specify an exception handler for the type of exception
that occurs, Python’s default exception handlers print out information about the
exception and where it occurred. There are also optional else and finally
clauses for try except statements, but we don’t use them in this book.
You can specify exceptions in your programs by using the raise statement,
which expects a single argument, an exception object, describing the condition.
The statement
raises a general exception with a short description. If the program has defined
an exception handler for the general Exception class at the time this exception
is raised, then it is used in place of Python’s default handler. Otherwise, Python
prints a stack trace and exits the program if you’re not running in the
interactive interpreter. We look more at exception objects in the next section.
Object-Oriented Programming
Object-oriented programming developed as a way to organize code for data
structures. The data managed in the structure is kept in an object. The object is
also called an instance of a class of objects. For example, in distributing
tickets for an event, you would want an object to hold the list of phone
numbers for people desiring tickets. The class of the object could be a queue
that makes it easier to implement a first-come, first-served distribution system
(as described in Chapter 4, “Stacks and Queues”). Object classes define
specific methods that implement operations on the object instances. For the
ticketing system, there needs to be a method to add a new phone number for
someone desiring tickets. There also needs to be methods to get the next phone
number to call and to record how many tickets are assigned to each person
(which might be stored in the same or different object). The methods are
common to all objects in the class and operate on the data specific to each
instance. If there were several events where tickets are being distributed, each
event would need its own queue instance. The method for adding a phone
number is common to each one of them and is inherited from the object class.
Python defines classes of objects with the class statement. These are
organized in a hierarchy so that classes can inherit the definitions in other
classes. The top of that hierarchy is Python’s base class, object. The nested
statements inside a class statement define the class; the def statements define
methods and assignment statements define class attributes. The first parameter
of each defined method should be self. The self parameter holds the object
instance, allowing methods to call other instance methods and reference
instance attributes. You reference an object instance’s methods or attributes by
appending the method or attribute name to the variable holding the object,
separated by a period as in object.method().
To define the constructor for object instances, there should be a def
__init__() statement inside the class definition. Like the other methods,
__init__() should accept self as its first parameter. The __init__() method
takes the empty instance and performs any needed initialization like creating
instance variables and setting their values (and doesn’t need to return self like
constructors do in other languages). The __init__() method is unusual in that
you will rarely see it called explicitly in a program (that is, in the form
variable.__init()). Python has several special methods and other constructs
that all have names beginning and ending with double underscores. We point
out a few of them as we use them in examples in the text.
The short example in Listing 1-2 illustrates the basic elements of object-
oriented programming in Python, along with some of its ability to handle fancy
math concepts.
class Power(object):
"""A class that computes a specific power of other numbers.
In other words, it raises numbers by a constant exponent.
"""
default_exponent = 2
print(’Power:’, Power)
print(’Power.default_exponent:’, Power.default_exponent)
square = Power()
root = Power(0.5)
print(’square: ’, square)
print(’square.of(3) =’, square.of(3))
print(’root.of(3) =’, root.of(3))
print(’root.of(-3) =’, root.of(-3))
real_root = RealPower(0.5)
print(’real_root.of(3) =’, real_root.of(3))
print(’real_root.of(-3) =’, real_root.of(-3))
print(’Done.’)
Listing 1-2 shows a file with two class definitions, three object instances, and
some print statements to show how the objects behave. The purpose of the first
class, Power, is to make objects that can be used to raise numbers to various
exponents. That is explained in the optional document string that follows the
class definition. Note that this is not a Python comment, which would have to
start with a pound symbol (#) on the left.
Each object instance is created with its own exponent, so you can create Power
objects to perform different power functions. The Power class has one class
attribute, default_exponent. This is used by the constructor for the class to
define what exponent to use if none is provided when an instance is created.
The constructor for the Power class simply stores the desired exponent as an
instance attribute. This is somewhat subtle because there are three distinct
kinds of storage: class attributes, instance attributes, and local variables in
the methods. The default_exponent attribute is defined by the assignment in
the top level of the class statement, so it is a class attribute and is shared
among all instances. The exponent parameter in the __init__() method of
Power is a local variable available only during the evaluation of the
constructor. It has a default value that is supplied from the class attribute.
When __init__() assigns a value to self.exponent, it creates an instance
attribute for self. The instance attribute is unique to the object instance being
created and is not shared with other objects of the class.
The Power class has one method called of that returns the result of raising a
number to the exponent defined at instance creation. We use it after creating
two instances, square and root, that can be called using square.of(3), for
example. To create the first instance, the program calls Power(2) and binds the
result to the variable square. This behavior might be somewhat unexpected
because there is no mention of __init__(). This is an example of Python’s use
of reserved names like __init__() to fill special roles such as object
constructors. When a class like Power is referenced using a function call syntax
—followed by a parenthesized list of arguments—Python builds a new instance
of the class and then invokes the __init__() method on it. The program in
Listing 1-2 makes two calls to Power() to make one object that produces
squares and one that produces square roots.
Underscores in Python names mean that the item being defined is either special
to the interpreter or to be treated as private, visible only in the block where it’s
defined. Python uses several special names like __init__ for customizing the
behavior of objects and programs. The special names start and end with double
underscores and enable the use of tools like using the class name as constructor
call.
In the example of Listing 1-2, both the default_exponent class attribute and
the exponent instance attribute are public because their names don’t start with
an underscore. If the name were changed to begin with an underscore, they are
expected to be accessed only by the class and its methods and not to be
accessed outside of the class or object. The restriction on access, however, is
only a convention; the Python interpreter does not enforce the privacy of any
attributes regardless of their name, nor does it provide other mechanisms to
enforce privacy. Python does have a mechanism to mangle the names of class
and object attributes that begin with a double underscore and end with at most
one underscore. The name mangling is designed to make class-specific
attributes that are not shared with subclasses, but they are still publicly
accessible through the mangled names.
You can run the program by giving the filename as an argument to the Python
interpreter program as follows (the colored text in blue italics is the text that
you type):
$ python3 Object_Oriented_Client.py
Power: <class ’__main__.Power’>
Power.default_exponent: 2
square: <__main__.Power object at 0x10715ad50>
square.of(3) = 9
root.of(3) = 1.7320508075688772
root.of(-3) = (1.0605752387249068e-16+1.7320508075688772j)
real_root.of(3) = 1.7320508075688772
Traceback (most recent call last):
File "01_code/Object_Oriented_Client.py", line 32, in <module>
print(’real_root.of(-3) =’, real_root.of(-3))
File "01_code/Object_Oriented_Client.py", line 20, in of
’Fractional powers of negative numbers are imaginary’)
ValueError: Fractional powers of negative numbers are imaginary
The transcript shows the output of the print statements. Power is a class, and
square is an object instance of that class. The square instance is created by the
call to Power() with no arguments provided, so the exponent defaults to 2. To
create a similar way to compute square roots, the object created by Power(0.5)
is assigned to root. The square of 3 printed by the program is the expected 9,
and the square root of 3 is the expected 1.732.
The next print statement is for the square root of −3, which may be a less
expected result. It returns a complex number where j stands for the square
root of −1. While that may be interesting for some engineering applications,
you might want a different behavior for other programs. The RealPower class is
a subclass of the Power class that raises an exception when raising negative
numbers to fractional powers.
In the class statement for RealPower, it is defined to inherit from the Power
class. That means that RealPower will have the same default_exponent
attribute and __init__ constructor as Power does. In the class definition for
RealPower, it replaces the of method with a new one that tests the values of the
exponent and the numeric argument. If they fall in the category that produces
imaginary numbers, it raises a ValueError exception.
The transcript shows how the Python interpreter handles exceptions. It prints a
traceback showing the function and method calls that were being evaluated
when the exception occurred. The traceback includes the line numbers and
copies of the lines from the input file. After encountering the exception, the
interpreter prints the traceback, quits, and does not evaluate the final print
statement in the file.
Summary
• Data can be arranged in the computer in different ways and using various
storage media. The data is organized and interpreted to represent
something.
• Algorithms are the procedures used to manipulate data.
• By coupling good data organization with appropriate algorithms, data
structures provide the fundamental building block of all programs.
• Examples of data structures are stacks, lists, queues, trees, and graphs.
• Data structures are often compared by how efficiently they perform
common operations.
• A database is a collection of many similar records, each of which
describes some entity.
• The records of a database are composed of fields, each of which has a
name and a value.
• A key field is used to search for and sort records.
• Data structures that act like databases support four core operations:
insertion, search, deletion, and traversal.
• Data structures are implemented as classes using object-oriented
programming.
• Python has rich support for object-oriented programming with classes to
implement data structures.
Questions
These questions are intended as a self-test for readers. The answers can be
found in Appendix C.
1. Data structures that allow programs to ______ a record, _______ for a
record, _______ a record, and _______ all the records are considered to
be databases in this book.
2. Data structures are
a. composed of names and values.
b. built with fields that have methods attached to them.
c. implemented with object classes.
d. records that don’t change in a database.
3. How can you tell if a def statement in Python defines a function or class
method?
4. What algorithms can help make searching more efficient?
5. What are constructors used for? What special name does Python use for
them?
6. What are some of the reasons for choosing one data structure over
another?
7. What is a key field used for?
8. Good data organization can help with the speed of an algorithm, but
what other benefits does it have?
9. For what purpose was object-oriented programming developed?
10. Which of the following are data structures used in programming?
a. traceback
b. heap
c. list comprehension
d. hash table
e. recipe
f. slices
g. binary tree
Experiments
Try the following experiments:
1-A Write a Python list comprehension that returns the individual characters
of a string that are not whitespace characters. Apply it to the string "4
and 20 blackbirds.\n”
1-B Take a deck of playing cards, pull out the 13 spade cards, set aside the
rest, and shuffle the spade cards. Devise an algorithm to sort them by
number under the constraints:
a. All the cards must be held in one hand. This is the “first” hand.
b. Initially, the shuffled cards are all stacked with faces in one direction
so that only one card is visible.
c. Initially, all the cards are held between the thumb and forefinger of
the first hand.
d. The visible card in the stack can be pulled out using the other hand
and placed in between any of the fingers of the first hand. It can only
be placed at the front or the back of the cards in the stack of cards
between those fingers.
e. The other hand can hold one card at a time and must place it
somewhere in the first hand before picking out another visible card
from one of the stacks.
f. The algorithm is done when all the cards are in sorted order in one
stack in the hand.
Compare the efficiency of your algorithm with that of classmates or friends.
2. Arrays
Arrays are the most commonly used data structure for many reasons. They are
straightforward to understand and match closely the underlying computer
hardware. Almost all CPUs make it very fast to access data at known offsets
from a base address. Almost every programming language supports them as
part of the core data structures. We study them first for their simplicity and
because many of the more complex data structures are built using them.
In This Chapter
• The Array Visualization Tool
• Using Python Lists to Implement the Array Class
• The Ordered Array Visualization Tool
• Binary Search
• Python Code for an Ordered Array Class
• Logarithms
• Storing Objects
• Big O Notation
• Why Not Use Arrays for Everything?
First, we look at the basics of how data is inserted, searched, and deleted from
arrays. Then, we look at how we can improve it by examining a special kind of
array, the ordered array, in which the data is stored in ascending (or
descending) key order. This arrangement makes possible a fast way of
searching for a data item: the binary search.
To improve a data structure’s performance requires a way of measuring
performance beyond just running it on sample data. Looking at examples of
how it handles particular kinds of data makes it easier to understand the
operations. We also take the first step to generalize the performance measure
by looking at linear and binary searches, and introducing Big O notation, the
most widely used measure of algorithm efficiency.
Suppose you’re coaching kids-league soccer, and you want to keep track of
which players are present at the practice field. What you need is an attendance-
monitoring program for your computer—a program that maintains a database
of the players who have shown up for practice. You can use a simple data
structure to hold this data. There are several actions you would like to be able
to perform:
• Insert a player into the data structure when the player arrives at the field.
• Check to see whether a particular player is present, by searching for the
player’s number in the structure.
• Delete a player from the data structure when that player leaves.
• List all the players present.
These four operations—insertion, searching, deletion, and enumeration
(traversal)—are the fundamental ones in most of the data storage structures
described in this book.
Figure 2-1 shows the initial array with 10 elements, 9 of which have data items
in them. You can think of these items as representing your players. Imagine
that each player has been issued a team shirt with the player’s number on the
back. That’s really helpful because you have just met most of these people and
haven’t learned all their names yet. To make things visually interesting, the
shirts come in a variety of colors. You can see each player’s number and shirt
color in the array. The height of the colored rectangle is proportional to the
number.
Searching
Imagine that you just arrived at the playing field to start coaching, your
assistant hands you the computer that’s tracking attendance, and a player’s
parent asks if the goalies can start their special drills. You know that players 2,
5, and 17 are the ones who play as goalies, but are they all here? Although
answering this question is trivial for a real coach with paper and pencil, let’s
look at the details needed to do it with the computer.
You want to search to see whether all the players are present. The array
operations let you search for one item at a time. In the visualization tool, you
can select the text entry box near the Search button, the hint disappears, and
you enter the number 2. The button becomes enabled, and you can select it to
start the search. The tool animates the search process, which starts off looking
like Figure 2-2.
Insertion
We didn’t find player 2 when asked before, but now that player has just
arrived. You need to record that player 2 is at practice, so it’s time to insert
them in the array. Type 2 in the text entry box and select Insert. A new colored
rectangle with 2 in it appears at the bottom and moves into position at the
empty cell indicated by the nItems pointer. When it’s in position, the nItems
pointer moves to the right by one. It may now point beyond the last cell of the
array.
The animation of the arrival of the new item takes a little time, but in terms of
what the computer has to do, only two steps were needed: writing the new
value in the array at the position indicated by nItems and then incrementing
nItems by 1. It doesn’t matter if there were two, three, or a hundred items
already in the array; inserting the value always takes two steps. That makes the
insertion operation quite different from the search operation and almost always
faster.
More precisely, the number of steps for insertion doesn’t depend on how many
items are in the array as long as it is not full. If all the cells are filled, putting a
value outside of the array is an error. The visualization tool won’t let that
happen and will produce an error message if you try. (In the history of
programming, however, quite a few programmers did not put that check in in
the code, leading to buffer overflows and security problems.)
The visualization tool lets you insert duplicate values (if there are available
cells). It’s up to you to avoid them, possibly by using the Search operation, if
you don’t want to allow it.
Deletion
Player 17 has to leave (he wants to start on the homework assignment due
tomorrow). To delete an item in the array, you must first find it. After you type
in the number of the item to be deleted, a process like the search operation
begins in the visualization tool. A j arrow appears starting at the leftmost cell
and steps to the right as it checks values in the cells. When it finds and circles
the value as shown in the top left of Figure 2-3, however, it does something
different.
Traversal
Arrays are simple to traverse. The data is already in a linear order as specified
by the index to the array elements. The index is set to 0, and if it’s less than the
current number of items in the array, then the array item at index 0 is
processed. In the Array visualization tool, the item is copied to an output box,
which is similar to printing it. The index is incremented until it equals the
current number of items in the array, at which point the traversal is complete.
Each item with an index less than nItems is processed exactly once. It’s very
easy to traverse the array in reverse order by decrementing the index too.
Creating an Array
As we noted in Chapter 1, Python lists are constructed by enclosing either a list
of values or by using a list comprehension (loop) in square brackets. The list
comprehension really builds a new list based on an existing list or sequence.
To allocate lists with large numbers of values, you use either an iterator like
range() inside a list comprehension or the multiplication operator. Here are
some examples:
integerArray = [1, 1, 2, 3, 5] # A list of 5 integers
charArray = [’a’ for j in range(1000)] # 1,000 letter ’a’ characters
booleArray = [False] * 32768 # 32,768 binary False values
Each of these assignment statements creates a list with specific initial values.
Python is dynamically typed, and the items of a list do not all have to be of the
same type. This is one of the core differences between Python lists and the
arrays in statically typed languages, where all items must be of the same type.
Knowing the type of the items means that the amount of memory needed to
represent each one is known, and the memory for the entire array can be
allocated. In the case of charArray in the preceding example, Python runs a
small loop 1,000 times to create the list. Although all three sample lists start
with items of the same type, they could later be changed to hold values of any
type, and their names would no longer be accurate descriptions of their
contents.
Data structures are typically created empty, and then later insertions, updates,
and deletions determine the exact contents. Primitive arrays are allocated with
a given maximum size, but their contents could be anything initially. When
you’re using an array, some other variables must track which of the array
elements have been initialized properly. This is typically managed by an
integer that stores the current number of initialized elements.
In this book, we refer to the storage location in the array as an element or as a
cell, and the value that is stored inside it as an item. In Python, you could write
the initialization of an array like this:
maxSize = 10000
myArray = [None] * maxSize
myArraySize = 0
This code allocates a list of 10,000 elements each initialized to the special None
value. The myArraySize variable is intended to hold the current number of
inserted items, which is 0 at first. You might think that Python’s built-in len()
function would be useful to determine the current size, but this is where the
implementation of an array using a Python list breaks down. The len()
function returns the allocated size, not how many values have been inserted in
myArray. In other words, len(myArray) would always be 10,000 (if you only
change individual element values). We will track the number of items in our
data structures using other variables such as nItems or myArraySize, in the
same manner that must be done with other programming languages.
Initialization
All the methods we’ve explored for creating lists in Python involve specifying
the initial value for the elements. In other languages, it’s easy to create or
allocate an array without specifying any initial value. As long as the array
elements have a known size and there is a known quantity of them, the memory
needed to hold the whole array can be allocated. Because computers and their
operating systems reuse memory that was released by other programs, the
element values in a newly allocated array could be anything. Programmers
must be careful not to write programs that use the array values before setting
them to some desired value because the uninitialized values can cause errors
and other unwanted behavior. Initializing Python list values to None or some
other known constant avoids that problem.
class Array(object):
The Array class has a constructor that initializes a fixed length list to hold the
array of items. The array items are stored in a private instance attribute, __a,
and the number of items stored in the array is kept in the public instance
attribute, nItems. The four methods define the four core operations.
Before we look at the implementation details, let’s use a program to test each
operation. A separate file, BadArrayClient.py, shown in Listing 2-2 uses the
Array class in the BadArray.py module. This program imports the class
definition, creates an Array called arr with a maxSize of 10, inserts 10 data
items (integers, strings, and floating-point numbers) in it, displays the contents
by traversing the Array, searches for a couple of items in it, tries to remove the
items with values 0 and 17, and then displays the remaining items.
import BadArray
maxSize = 10 # Max size of the Array
arr = BadArray.Array(maxSize) # Create an Array object
The results show that most of the methods work properly; this example
illustrates the use of the public instance attribute, nItems, to provide the
number of items in the Array. The Python traceback shows that there’s a
problem with the delete() method. The error is that the list index was out of
range. That means either k or k+1 was out of range in the line displayed in the
traceback. Going back to the code in BadArray.py, you can see that k lies in
the range of j up to but not including self.nItems. The index j can’t be out of
bounds because the method already accessed __a[j] and found that it matched
item in the line preceding the k loop. When k gets to be self.nItems − 1,
then k + 1 is self.nItems, and that is outside of the bounds of the maximum
size of the list initially allocated. So, we need to adjust the range that k takes
in the loop to move array items. Before fixing that, let’s look more at the
details of all the algorithms used.
Insertion
Inserting an item into the Array is easy; we already know the position where
the insertion should go because we have the number of current items that are
stored. Listing 2-1 shows that the item is placed in the internal list at the
self.nItems position. Afterward, the number of items attribute is increased so
that subsequent operations will know that that element is now filled. Note that
the method does not check whether the allocated space for the list is enough
to accommodate the new item.
Searching
The item variable holds the value being sought. The search method steps
through only those indices of the internal list within the current number of
items, comparing the item argument with each array item. If the loop variable
j passes the last occupied element with no match being found, the value isn’t in
the Array. Appropriate messages are displayed by the BadArrayClient.py
program: Search for 12 returns None or Search for 12.34 returns
12.34.
Deletion
Deletion begins with a search for the specified item. If found, all the items with
higher index values are moved down one element to fill in the hole in the list
left by the deleted item. The method decrements the instance’s nItems
attribute, but you’ve already seen an error happen before that point. Another
possibility to consider is what happens if the item isn’t found. In the
implementation of Listing 2-1, it returns False. Another approach would be to
raise an exception in that case.
Traversal
Traversing all the items is straightforward: We step through the Array,
accessing each one via the private instance variable __a[j] and apply the
(print) function to it.
Observations
In addition to the bug discovered, the BadArray.py module does not provide
methods for accessing or changing arbitrary items in the Array. That’s a
fundamental operation of arrays, so we need to include that. We will keep the
code simple and focus attention on operations that will be common across
many data structures.
The Array class demonstrates encapsulation (another aspect of object-oriented
programming) by providing the four methods and keeping the underlying data
stored in the __a list as private. Programs that use Array objects are able to
access the data only through those methods. The nItems attribute is public,
which makes it convenient for access by Array users. Being public, however,
opens that attribute to being manipulated by Array users, which could cause
errors to occur. We address these issues in the next version of the program.
import Array
maxSize = 10 # Max size of the array
arr = Array.Array(maxSize) # Create an array object
We now have a functional Array class that implements the four core methods
for a data storage object. The code shown in the Array visualization tool is that
of the Array class in Listing 2-3. Try using the visualization tool to search for
an item and follow the highlights in the code. You will see that it calls the
search() method, which calls the find() method. Those both show up
separated by a gray line. When the find() method finishes, its local variables
are erased, and its source code disappears, leaving the search() method to use
its result and try to get the item. The visualization does not show the execution
of the get() method but does show a message indicating whether the item was
found or not.
You can also try out the allocation of a new array. The “New” operation
allocates an array of a size you provide (if the cells fit on the screen). If you ask
for a large number of cells, it makes them smaller, and the numbers may be
hidden. The code shown for the New operation is that of the __init__()
constructor for the Array class. The Random Fill operation fills any empty
cells of the current array with random keys. The Delete Rightmost removes the
last item from the array. These aren’t methods in the basic Array class, but they
are helpful for the visualization.
Binary Search
The payoff for using an ordered array comes when you use a binary search.
You use this for the Search operation because it is much faster than a linear
search, especially for large arrays.
After comparing the value at mid (59) with the value you’re trying to find, the
algorithm determines that 55 must lie in the range to the left of mid. It moves
the hi arrow to be one less than mid to narrow the range and leaves lo
unchanged. Then it updates mid to be at the midpoint of the narrowed range.
The bottom of Figure 2-6 shows this second step of the process.
Each step reduces the range by about half. With the initial 10-element array, the
ranges go from 10 to 5, to 2, to 1 at the very most (in Figure 2-6, it goes from
10 to 4 to 2, before finding 55 at index 2). If mid happens to point at the goal
item, the search can stop. Otherwise, it will continue until the range collapses
to nothing.
Try a few searches to see how quickly the visualization tool finds values. Try a
search for a value not in the array to see what happens. With a 10-element
array, it will take at most four values for mid to determine whether the value is
present in the array.
What about larger arrays? Use the New operation to find out. Select the text
entry box, enter 35, and then select New. If there’s enough room in the tool
window, it will draw 35 empty cells of a new array. Fill them with random
values by selecting Random Fill. The cells show colored rectangles, but the
numbers disappear when the cells are too skinny. You can try a variation of the
Guess-a-Number game here by typing in a value, selecting Search, and seeing
whether it ended up in the array. If you succeed, the tool will add an oval to the
cell and provide a success message at the end. You can also select a colored
rectangle with your pointer, and it will fill in its value in the text entry area.
Can you figure out how many steps the binary search algorithm will take to
find a number based on the size of the array you’re searching? We return to this
question in the last section of this chapter.
The method begins by setting the lo and hi variables to the first and last
indices in the array. Setting these variables specifies the range for where the
item may be found. Then, within the while loop, the index, mid, is set to the
middle of this range.
If you’re lucky, mid may already be pointing to the desired item, so you first
check if self.__a[mid] == item is true. If it is, you’ve found the item, and
you return with its index, mid.
If mid does not point to the item being sought, then you need to figure out
which half of the range it falls in. You check whether the item is bigger than
the one at the midpoint by testing self.__a[mid] < item. If the item is
bigger, then you can shrink the search range by setting the lo boundary to be 1
above the midpoint. Note that setting lo to be the same as mid would mean
including that midpoint item in the remaining search. You don’t want to
include it because the comparison with the item at mid already showed that its
value is too low. Finally, if the midpoint item is neither equal to nor less than
the item being sought, it must be bigger. In this case, you can shrink the search
range by setting hi to be 1 below the midpoint. Figure 2-7 shows how the
range is altered in these two situations.
Figure 2-7 Dividing the range in a binary search
Each time through the loop you divide the range in half. Eventually, the range
becomes so small that it can’t be divided any more. You check for this in the
loop condition: if lo is greater than hi, the range has ceased to exist. (When lo
equals hi, the range is one and you need one more pass through the loop.) You
can’t continue the search without a valid range, but you haven’t found the
desired item, so you return lo, the last lower bound of the search range. This
might seem odd because you’re returning an index that doesn’t point to the
item being sought. It still could be useful, however, because it specifies where
an item with that value would be placed in the ordered array.
class OrderedArray(object):
def __init__(self, initialSize): # Constructor
self.__a = [None] * initialSize # The array stored as a list
self.__nItems = 0 # No items in array initially
class OrderedArray(object):
…
def find(self, item): # Find index at or just below
lo = 0 # item in ordered list
hi = self.__nItems-1 # Look between lo and hi
Listing 2-6 starts with the find() method that implements the binary search
algorithm. The search() method changes a little from that of the Array. It first
calls find() and verifies that the index returned is in bounds. If not, or if the
indexed item doesn’t match the sought item, it returns None. That means
searching for an item not in the array will return None without raising an
exception.
A check at the beginning of the insert() method determines whether the array
is full. This is done by comparing the length of the Python list, __a, to the
number of items currently in the array, __nItems. If __nItems is equal to (or
somehow, larger than) the size of the list, inserting another item will overflow
it, so the method raises an exception.
Otherwise, the insert() method calls find() to locate where the new item
goes. Then it uses a loop over the indices to the right of the insertion index to
move those items one cell to the right. The loop uses range(self.__nItems,
index, -1) to go backward through the indices from __nItems to index + 1.
The number of items to be moved could be all N of them if the new item is the
smallest. On average, it will move half the current items.
The delete() method calls find() to figure out the location of the item to be
deleted and whether it is in the array. If it does find the item, it also must move
half the current items to the left on average. If not, it can return False without
moving anything.
Like before, we use a separate client program to test the operations of the class
and the utility methods. The OrderedArrayClient.py program appears in
Listing 2-7.
Note that you can pass the arr variable directly to print and expect a
reasonable output because of the __str__() method. We also use a different
form of the import statement in OrderedArrayClient.py. By importing the
module using the “from module import *” syntax, the definitions it contains
are added in the same namespace as the client program, not in a new
namespace for the module. That means you can create the object using the
expression OrderedArray(maxSize) instead of
OrderedArray.OrderedArray(maxSize). The output of the program looks like
this:
$ python3 OrderedArrayClient.py
Array containing 11 items: [0, 0, 3, 12, 44, 44, 55, 77, 77, 99, 99]
Array after deletions has 7 items: [12, 44, 44, 55, 77, 77, 99]
find(44) returns 1
find(46) returns 3
find(77) returns 5
The last three print statements illustrate some particular cases of the binary
search with duplicate entries. The result of find(46) shows that even though
46 is not in arr, it should be inserted after the first three items to preserve
ordering. The find(44) finds the first occurrence of 44 at position 1. If
delete(44) were called at this point, it would delete the first of the 44s
currently in the array. By contrast, find(77) points at the second of the two 77s
in the array. The binary search stops after it finds the first matching item, which
could be any instance of an item that appears multiple times.
Logarithms
In this section we explain how you can use logarithms to calculate the number
of steps necessary in a binary search. If you’re a math fan, you can probably
skip this section. If thinking about math makes you nervous, give it a try, and
make sure to take a long, hard look at Table 2-3.
A binary search provides a significant speed increase over a linear search. In
the number-guessing game, with a range from 1 to 100, a maximum of seven
guesses is needed to identify any number using a binary search; just as in an
array of 100 records, a maximum of seven comparisons is needed to find a
record with a specified key value. How about other ranges? Table 2-3 shows
some representative ranges and the number of comparisons needed for a binary
search.
Table 2-3 Comparisons Needed in a Binary Search
Notice the differences between binary search times and linear search times. For
very small numbers of items, the difference isn’t dramatic. Searching 10 items
would take an average of 5 comparisons with a linear search (N/2) and a
maximum of 4 comparisons with a binary search. But the more items there are,
the bigger the difference. With 100 items, there are 50 comparisons in a linear
search, but only 7 in a binary search. For 1,000 items, the numbers are 500
versus 10, and for 1,000,000 items, they’re 500,000 versus 20. You can
conclude that for all but very small arrays, the binary search is greatly superior.
The Equation
You can verify the results of Table 2-3 by repeatedly dividing a range (from the
first column) in half until it’s too small to divide further. The number of
divisions this process requires is the number of comparisons shown in the
second column.
Repeatedly dividing the range by two is an algorithmic approach to finding the
number of comparisons. You might wonder if you could also find the number
using a simple equation. Of course, there is such an equation, and it’s worth
exploring here because it pops up from time to time in the study of data
structures. This formula involves logarithms. (Don’t panic yet.)
You have probably already experienced logarithms, without having recognized
them. Have you ever heard someone say “a six figure salary” or read about “a
deal worth eight figures”? Those simplified expressions tell you the
approximate amount of the salary or deal by telling you how many digits are
needed to write the number. The number of digits could be found by repeatedly
dividing the number by 10. When it’s less than 1, the number of divisions is the
number of digits.
The numbers in Table 2-3 leave out some interesting data. They don’t answer
such questions as “What is the exact size of the maximum range that can be
searched in five steps?” To solve this problem, you can create a similar table,
but one that starts at the beginning, with a range of one, and works up from
there by multiplying the range by two each time. Table 2-4 shows how this
looks for the first seven steps.
Table 2-4 Powers of Two
For the original problem with a range of 100, you can see that 6 steps don’t
produce a range quite big enough (64), whereas 7 steps cover it handily (128).
Thus, the 7 steps that are shown for 100 items in Table 2-3 are correct, as are
the 10 steps for a range of 1,000.
Doubling the range each time creates a series that’s the same as raising 2 to a
power, as shown in the third column of Table 2-4. We can express this power as
a formula. If s represents steps (the number of times you multiply by 2—that is,
the power to which 2 is raised) and r represents the range, then the equation is
r = 2s
If you know s, the number of steps, this tells you r, the range. For example, if s
is 6, the range is 26, or 64.
This equation says that the number of steps (comparisons) is equal to the
logarithm to the base 2 of the range. What’s a logarithm? The base 2 logarithm
of a number r is the number of times you must multiply 2 by itself to get r. In
Table 2-4, the step numbers in the first column, s, are equal to log2(r).
How do you find the logarithm of a number without doing a lot of dividing?
Most calculators and computer languages have a log function. For those that
don’t, sometimes it can be added as option, such as with Python’s math
module. It might only provide a function for log to the base 10, but you can
convert easily to base 2 by multiplying by 3.322. For example, log10(100) = 2,
so log2(100) = 2 times 3.322, or 6.644. Rounded up to the whole number 7, this
is what appears in the column to the right of 100 in Table 2-3.
In any case, the point here isn’t to calculate logarithms. It’s more important to
understand the relationship between a number and its logarithm. Look again at
Table 2-3, which compares the number of items and the number of steps
needed to find a particular item. Every time you multiply the number of items
(the range) by a factor of 10, you add only three or four steps (actually 3.322,
before rounding off to whole numbers) to the number needed to find a
particular item. This is true because, as a number grows larger, its logarithm
doesn’t grow nearly as fast. We compare this logarithmic growth rate with that
of other mathematical functions when we talk about Big O notation later in this
chapter.
Storing Objects
In the examples we’ve shown so far, we’ve stored single values in array data
structures such as integers, floating-point numbers, and strings. Storing such
simple values simplifies the program examples, but it’s not representative of
how you use data storage structures in the real world. Usually, the data you
want to store comprises many values or fields, usually called a record. For a
personnel record, you might store the family name, given name, birth date, first
working date, identification number, and so forth. For a fleet of vehicles, you
might store the type of vehicle, the name, the date it entered service, a license
tag, and so forth. In object-oriented programs, you want to store the objects
themselves in data structures. The objects can represent records.
When storing objects or records in ordered data structures, like the
OrderedArray class, you need to define the way the records are ordered by
specifying a key that can be used on all of them. Let’s look at how that changes
the implementation.
class OrderedRecordArray(object):
def __init__(self, initialSize, key=identity): # Constructor
self.__a = [None] * initialSize # The array stored as a list
self.__nItems = 0 # No items in array initially
self.__key = key # Key function gets record key
Listing 2-9 shows that the find() and search() methods change to take a key
as an argument, instead of the item or record used in the OrderedArray class.
This key is a value, not a function, and is used to compare with the keys
extracted from the records in the array. The find and search methods use the
internal __key function on the records to get the right value to compare with
the key being sought. The insert and delete method signatures don’t change
—they still operate on item records—but internally they change the way they
pass the appropriate key to find().
class OrderedRecordArray(object):
…
def find(self, key): # Find index at or just below key
lo = 0 # in ordered list
hi = self.__nItems-1 # Look between lo and hi
else:
hi = mid - 1 # No, but could be in lower half
# Insert 10 items
for rec in [(’a’, 3.1), (’b’, 7.5), (’c’, 6.0), (’d’, 3.1),
(’e’, 1.4), (’f’, -1.2), (’g’, 0.0), (’h’, 7.5),
(’i’, 7.5), (’j’, 6.0)]:
arr.insert(rec)
After putting 10 records in the array including some with duplicate keys, the
test program deletes a few records, showing the result of the deletion. It then
tries to find a few keys in the reduced array. The result of running the program
is
$ python3 OrderedRecordArrayClient.py
Array containing 10 items:
[(’f’, -1.2), (’g’, 0.0), (’e’, 1.4), (’d’, 3.1), (’a’, 3.1), (’j’,
6.0), (’c’, 6.0), (’i’, 7.5), (’h’, 7.5), (’b’, 7.5)]
Deleting (’c’, 6.0) returns False
Deleting (’g’, 0.0) returns True
Deleting (’g’, 0.0) returns False
Deleting (’b’, 7.5) returns False
Deleting (’i’, 7.5) returns True
Array after deletions has 8 items:
[(’f’, -1.2), (’e’, 1.4), (’d’, 3.1), (’a’, 3.1), (’j’, 6.0), (’c’,
6.0), (’h’, 7.5), (’b’, 7.5)]
find( 4.4 ) returns 4 and get( 4 ) returns (’j’, 6.0)
find( 6.0 ) returns 5 and get( 5 ) returns (’c’, 6.0)
find( 7.5 ) returns 6 and get( 6 ) returns (’h’, 7.5)
The program output shows that deleting the record (’c’, 6.0) fails. Why?
The next two deletions show that deleting (’g’, 0.0) succeeds the first time
but fails the second time because only one record has that key, 0.0. That’s what
is expected, but the next deletions are unexpected. The deletion of the record
(’b’, 7.5) fails, but the deletion of (’i’, 7.5) succeeds. What is going on?
The issue comes up because of the duplicate keys. The program inserts three
records that have the key 7.5. When the find() method runs, it uses binary
search to get the index to one of those records. The exact one it finds depends
on the sequence of values for the mid variable. You can see which one it finds
in the output of the find tests. Note that find(4.4) returns a valid index, 4,
and that points to the location where a record with that key should go. The
record at index 4 has the next higher key value, 6.0. When you call find(7.5)
on the final Array, it returns 6, which points to the (’h’, 7.5) record. That
isn’t equal to the(’b’, 7.5) record using Python’s == test. The delete()
method removes only items that pass the == test. You can also deduce that
find(7.5) did find the (’i’, 7.5) record on the earlier delete operation. This
example illustrates an important issue when duplicate keys are allowed in a
sorted data structure like OrderedRecordArray. One of the end-of-chapter
programming projects asks you to change the behavior of this class to correctly
delete records with duplicate keys.
Big O Notation
Which algorithms are faster than others? Everyone wants their results as soon
as possible, so you need to be able compare the different approaches. You can
certainly run experiments with each program on a particular computer and with
a particular set of data to see which is fastest. That capability is useful, but
when the computer changes or the data changes, you could get different results.
Computers generally get faster as better technologies are invented, and that
makes all algorithms run faster. The changes with the data, however, are harder
to predict. You’ve already seen that a binary search takes far fewer steps than a
linear search because the number of items to search increases. We’d like to be
able to extend that reasoning to help predict what will happen with other
algorithms.
People like to categorize things, especially by what they are capable of doing.
If you think about cutting grass, there are push lawn mowers, powered lawn
mowers, riding lawn mowers, and towed grass cutters. Each one of them is
good for different size jobs of grass cutting. Similarly for refrigeration, there
are personal refrigerators, household refrigerators, restaurant kitchen
refrigerators, walk-in refrigerators, and refrigerated warehouses for different
quantities of perishable items. In each case, choosing something too big or too
small for the job would be costly in time or money.
In computer science, a rough measure of performance called Big O notation is
used to describe algorithms. It’s primarily used to describe the speed of
algorithms but is also used to describe how much storage they need.
Algorithms with the same Big O speed are in the same category. The category
gives a rough idea of what amount of data they can process (or storage they
need). For example, the linear search Array class in Listing 2-3 should be
plenty fast for small jobs like keeping a list of contacts in a personal computer
or phone or watch. It would probably not be acceptable for the list of contacts
of a large corporation with tens of thousands of employees, and it certainly
would be too slow to manage all the contact information for a nation of tens of
millions of people. By using the OrderedRecordArray class in Listing 2-8, you
can get the benefit of binary search and drastically reduce the search time by
making it proportional to the logarithm of the number of items. Are there even
better algorithms? Big O notation helps answer that question.
As you saw earlier, the search time is proportional to the base 2 logarithm of N.
Actually, because any logarithm is related to any other logarithm by a constant
(for example, multiplying by 3.322 to go from base 2 to base 10), you can lump
this constant into K as well. Then you don’t need to specify the base:
T = K × log(N)
You might ask why deletion in ordered arrays isn’t shown as O(log N) + O(N)
or maybe O(log N + N) because it uses binary search to find the location of the
item to delete. The reason is that the O(N) part needed for shifting the items of
the array is so much larger than the O(log N) part that it really doesn’t matter
when N gets big. Big O notation is intended to describe how the algorithm
behaves for very large numbers of items.
Figure 2-8 graphs some Big O relationships between time (in number of steps)
and number of items, N. Based on this graph, you might rate the various Big O
values (very subjectively) like this:
• O(1) is excellent,
• O(log N) is good,
• O(N) is fair,
• O(N × log N) is poor, and
• O(N2) is bad.
O(N × log N) occurs in many kinds of sorting. O(N2) occurs in simple sorting
and in certain graph algorithms, all of which we look at later in this book.
Summary
• Arrays are sequential groupings of data elements. Each element can store
a value called an item.
• Each element of the array can be accessed by knowing the start of the
array and an integer index to the element.
• Object-oriented programs are used to implement data structures to
encapsulate the algorithms that manipulate the data.
• Data structures use private instance variables to restrict access to
important values of the structure that could cause errors if changed by
the calling program.
• Unordered arrays offer fast insertion but slow searching and deletion.
• A binary search can be applied to an ordered array.
• The logarithm to the base B of a number A is (roughly) the number of
times you can divide A by B before the result is less than 1.
• Linear searches require time proportional to the number of items in an
array.
• Binary searches require time proportional to the logarithm of the number
of items.
• Data structures usually store complex data types like records.
• A key must be defined to order complex data types.
• If duplicate items or keys are allowed in a data structure, the algorithms
should have a predictable behavior for how they are managed.
• Big O notation provides a convenient way to compare the speed of
algorithms.
• An algorithm that runs in O(1) time is the best, O(log N) is good, O(N) is
fair, and O(N2) is bad.
Questions
These questions are intended as a self-test for readers. Answers may be found
in Appendix C.
1. Aside from the insert, delete, search, and traverse methods common to
all “database” data structures, array data structures should have
___________ method(s).
2. When constructing a new instance of an Array (Listing 2-3):
a. the initial value for at least one of the array cells must be set.
b. the data type of all the array cells must be set.
c. the key for each data item must be set.
d. the maximum number of cells the array can hold must be set.
e. none of the above.
3. Why is it important to use private instance attributes like __nItems in
the definition of the array data structure?
4. Inserting an item into an unordered array
a. takes time proportional to the size of the array.
b. requires multiple comparisons.
c. requires shifting other items to make room.
d. takes the same time no matter how many items there are.
5. True or False: When you delete an item from an unordered array, in
most cases you shift other items to fill in the gap.
6. In an unordered array, allowing duplicates
a. increases times for all operations.
b. increases search times in some situations.
c. always increases insertion times.
d. sometimes decreases insertion times.
7. True or False: In an unordered array, it’s generally faster to find out an
item is not in the array than to find out it is.
8. Ordered arrays, compared with unordered arrays, are
a. much quicker at deletion.
b. quicker at insertion.
c. quicker to create.
d. quicker at searching.
9. Keys are used with arrays
a. to provide a single value for each array item that can be used to order
the items.
b. to decrypt the values stored in the array cell.
c. to decrease the insertion time in unordered arrays.
d. to allow complex data types to be stored as a single key value in the
array.
10. The OrderedArray.py (Listing 2-6) and OrderedRecordArray.py
(Listing 2-9) modules have both a find() and a search() method. How
are the two methods the same and how do they differ?
11. A logarithm is the inverse of _____________.
12. The base 10 logarithm of 1,000 is _____.
13. The maximum number of items that must be examined to complete a
binary search in an array of 200 items is
a. 200.
b. 8.
c. 1.
d. 13.
14. The base 2 logarithm of 64 is ______.
15. True or False: The base 2 logarithm of 100 is 2.
16. Big O notation tells
a. how the speed of an algorithm relates to the number of items.
b. the running time of an algorithm for a given size data structure.
c. the running time of an algorithm for a given number of items.
d. how the size of a data structure relates to the speed of one of its
algorithms.
17. O(1) means a process operates in _________ time.
18. Advantages of using arrays include
a. the variable size of array cells.
b. the variable length of the array over the lifetime of the data structure.
c. the O(1) access time to read or write an array cell.
d. the O(1) time to traverse all the items in the array.
e. all of the above.
19. A colleague asks for your comments on a data structure using an
unordered array without duplicates and binary search. Which of the
following comments makes sense?
a. Because the array can store any data type, a binary search won’t be
efficient.
b. Because the array is unordered, a binary search cannot guarantee
finding the item being sought.
c. Because binary search takes O(N) time, it would be better to use an
ordered array.
d. Because the array doesn’t have duplicates, binary search doesn’t
really have an advantage over the simpler linear search.
20. You’ve been asked to adapt some code that maintains a record about
each planet and their moons in a solar system like ours into a system
that will store a record about every planet and moon in every known
galaxy. The record structure will be a little larger for each planet to hold
some new attributes. It’s likely that the records will be added and
updated “randomly” as telescopes and other sensors point at different
parts of the universe over time, filling in some initial attributes of the
records, and then updating others during frequent observations. The
current code uses an unordered array for the records. Would you
recommend any changes? If so, why?
Experiments
Carrying out these experiments will help to provide insights into the topics
covered in the chapter. No programming is involved.
2-A Use the Array Visualization tool to insert, search for, and delete items.
Make sure you can predict what it’s going to do. Do this both with
duplicate values present and without.
2-B Make sure you can predict in advance what indices the Ordered Array
Visualization tool will select at each step for lo, mid, and hi when you
search for the lowest, second lowest, one above middle, and highest
values in the array.
2-C In the Ordered Array Visualization tool, create an array of 12 cells and
then use the Random Fill button to fill them with values. Use the Delete
Rightmost button to remove the five highest values. Then insert five of
the same value, somewhere in the middle of the array. Note the colors
that are assigned to inserted values and the order they were inserted.
Can you predict the order they will be deleted? Try deleting the value
you chose several times to see if your prediction is right.
Programming Projects
Writing programs to solve the Programming Projects helps to solidify your
understanding of the material and demonstrates how the chapter’s concepts are
applied. (As noted in the Introduction, qualified instructors may obtain
completed solutions to the Programming Projects on the publisher’s website.)
2.1 To the Array class in the Array.py program (Listing 2-3), add a method
called getMaxNum() that returns the value of the highest number in the
array, or None if the array has no numbers. You can use the expression
isinstance(x, (int, float)) to test for numbers. Add some code to
ArrayClient.py (Listing 2-4) to exercise this method. You should try it
on arrays containing a variety of data types and some that contain zeros
and some that contain no numbers.
2.2 Modify the method in Programming Project 2.1 so that the item with the
highest numeric value is not only returned by the method but also
removed from the array. Call the method deleteMaxNum().
2.3 The deleteMaxNum() method in Programming Project 2.2 suggests a
way to create an array of numbers sorted by numeric value. Implement
a sorting scheme that does not require modifying the Array class from
Project 2.2, but only the code in ArrayClient.py (Listing 2-4).
2.4 Write a removeDupes() method for the Array.py program (Listing 2-3)
that removes any duplicate entries in the array. That is, if three items
with the value ’bar’ appear in the array, removeDupes() should remove
two of them. Don’t worry about maintaining the order of the items. One
approach is to make a new, empty list, move items one at a time into it
after first checking that they are not already in the new list, and then set
the array to be the new list. Of course, the array size will be reduced if
any duplicate entries exist. Write some tests to show it works on arrays
with and without duplicate values.
2.5 Add a merge() method to the OrderedRecordArray class (Listing 2-8
and Listing 2-9) so that you can merge one ordered source array into
that object’s existing ordered array. The merge should occur only if both
objects’ key functions are identical. Your solution should create a new
list big enough to hold the contents of the current (self) list and the
merging array list. Write tests for your class implementation that creates
two arrays, inserts some random numbers into them, invokes merge() to
add the contents of one to the other, and displays the contents of the
resulting array. The source arrays may hold different numbers of data
items. Your algorithm needs to compare the keys of the source arrays,
picking the smallest one to copy to the destination. You also need to
handle the situation when one source array exhausts its contents before
the other. Note that, in Python, you can access a parameter’s private
attributes in a manner similar to using self. If the parameter arr is an
OrderedRecordArray object, you can access its number of items as
arr.__nItems.
2.6 Modify the OrderedRecordArray class (Listing 2-8 and Listing 2-9) so
that requests to delete records that have duplicate keys correctly find the
target records and delete them if present. Make sure you test the
program thoroughly so that regardless of the number of records with
duplicate keys or their order within the internal list, your modified
version finds the matching record if it exists and leaves the list
unchanged if it is not present.
2.7 Modify the OrderedRecordArray class (Listing 2-8 and Listing 2-9) so
that it stores the maximum size of the array. When an insertion would
go beyond the current maximum size, create a new list capable of
holding more data and copy the existing list contents into it. The new
size can be a fixed increment or a multiple of the current size. Test your
new class by inserting data in a way that forces the object to expand the
list several times and determine which is the best strategy—growing the
list by adding a fixed amount of storage each time it fills up, or
multiplying the list’s storage by a fixed multiple each time it fills up.
3. Simple Sorting
In This Chapter
• How Would You Do It?
• Bubble Sort
• Selection Sort
• Insertion Sort
• Comparing the Simple Sorts
As soon as you create a significant database, you’ll probably think of reasons
to sort the records in various ways. You need to arrange names in alphabetical
order, students by grade, customers by postal code, home sales by price, cities
in order of population, countries by land mass, stars by magnitude, and so on.
Sorting data may also be a preliminary step to searching it. As you saw in
Chapter 2, “Arrays,” a binary search, which can be applied only to sorted data,
is much faster than a linear search.
Because sorting is so important and potentially so time-consuming, it has been
the subject of extensive research in computer science, and some very
sophisticated methods have been developed. In this chapter we look at three of
the simpler algorithms: the bubble sort, the selection sort, and the insertion
sort. Each is demonstrated in a Visualization tool. In Chapter 6, “Recursion,”
and Chapter 7, “Advanced Sorting,” we return to look at more sophisticated
approaches including Shellsort and quicksort.
The techniques described in this chapter, while unsophisticated and
comparatively slow, are nevertheless worth examining. Besides being easier to
understand, they are actually better in some circumstances than the more
sophisticated algorithms. The insertion sort, for example, is preferable to
quicksort for small arrays and for almost-sorted arrays. In fact, an insertion sort
is commonly used in the last stage of a quicksort implementation.
The sample programs in this chapter build on the array classes developed in the
preceding chapter. The sorting algorithms are implemented as methods of the
Array class.
Be sure to try out the algorithm visualizations provided with this chapter. They
are very effective in explaining how the sorting algorithms work in
combination with the descriptions and static pictures from the text.
When you reach the first sorted player, start over at the left end of the line.
You continue this process until all the players are in order. Describing this
process is much harder than demonstrating it, so let’s watch its work in the
Simple Sorting Visualization tool.
Note
The Stop ( ) button stops an animation and allows you to start other operations. The array
contents, however, may be different than what they were at the start of the animation. There
may be missing items or extra copies of items depending on when the operation was
interrupted. In this way, the visualization mimics what happens in computer memory if
something stops the execution.
Python Code for a Bubble Sort
The bubble sort algorithm is pretty straightforward to explain, but we need to
look at how to write the program. Listing 3-1 shows the bubbleSort() method
of an Array class. It is almost the same as the Array class introduced in
Chapter 2, and the full SortArray module is shown later in this chapter in
Listing 3-4.
In the Array class, each element of the array is assumed to be a simple value
that can be compared with any of the other values for the purposes of ordering
them. We reintroduce a key function to handle ordering records later.
The bubbleSort() method has two loops. The inner loop handles stepping
through the array and swapping elements that are out of order. The outer loop
handles the decreasing length of the unsorted part of the array. The outer loop
sets the last variable to point initially at the last element of the array. That’s
done by using Python’s range function to start at __nItems-1, and step down
to 1 in increments of −1. The inner loop starts each time with the inner
variable set to 0 and increments by 1 to reach last-1. The elements at indices
greater than last are always completely sorted. After each pass of the inner
loop, the last variable can be reduced by 1 because the maximum element
between 0 and last bubbled up to the last position.
The inner loop body performs the test to compare the elements at the inner
and inner+1 indices. If the value at inner is larger than the one to its right, you
have to swap the two array elements (we’ll see how swap() works when we
look at all the SortArray code in a later section).
Invariants
In many algorithms there are conditions that remain unchanged as the
algorithm proceeds. These conditions are called invariants. Recognizing
invariants can be useful in understanding the algorithm. In certain situations
they may help in debugging; you can repeatedly check that the invariant is true,
and signal an error if it isn’t.
In the bubbleSort() method, the invariant is that the array elements to the
right of last are sorted. This remains true throughout the running of the
algorithm. On the first pass, nothing has been sorted yet, and there are no items
to the right of last because it starts on the rightmost element.
Thus, the algorithm makes about N2/2 comparisons (ignoring the −1, which
doesn’t make much difference, especially if N is large).
There are fewer swaps than there are comparisons because two bars are
swapped only if they need to be. If the data is random, a swap is necessary
about half the time, so there would be about N2/4 swaps. (In the worst case,
with the initial data inversely sorted, a swap is necessary with every
comparison.)
Both swaps and comparisons are proportional to N2. Because constants don’t
count in Big O notation, you can ignore the 2 and the 4 in the divisor and say
that the bubble sort runs in O(N2) time. This is slow, as you can verify by
running the Bubble Sort in the Simple Sorting Visualization tool with arrays of
20+ cells.
Whenever you see one loop nested within another, such as those in the bubble
sort and the other sorting algorithms in this chapter, you can suspect that an
algorithm runs in O(N2) time. The outer loop executes N times, and the inner
loop executes N (or perhaps N divided by some constant) times for each cycle
of the outer loop. This means you’re doing something approximately N×N or
N2 times.
Selection Sort
How can you improve the efficiency of the sorting operation? You know that
O(N2) is pretty bad. Can you get to O(N) or maybe even O(log N)? We look
next at a method called selection sort, which reduces the number of swaps.
That could be significant when sorting large records by a key. Copying entire
records rather than just integers could take much more time than comparing
two keys. In Python as in other languages, the computer probably just copies
pointers or references to record objects rather than the entire record, but it’s
important to understand which operations are happening the most times.
A Brief Description
What’s involved in the selection sort is making a pass through all the players
and picking (or selecting, hence the name of the sort) the shortest one. This
shortest player is then swapped with the player on the left end of the line, at
position 0. Now the leftmost player is sorted and doesn’t need to be moved
again. Notice that in this algorithm the sorted players accumulate on the left
(lower indices), whereas in the bubble sort they accumulated on the right.
The next time you pass down the row of players, you start at position 1, and,
finding the minimum, swap with position 1. This process continues until all the
players are sorted.
A More Detailed Description
Let’s start at the left end of the line of players. Record the leftmost player’s
height in your notebook and put a ball on the ground in front of this person.
Then compare the height of the next player to the right with the height in your
notebook. If this player is shorter, cross out the height of the first player and
record the second player’s height instead. Also move the ball, placing it in
front of this new “shortest” (for the time being) player. Continue down the row,
comparing each player with the minimum. Change the minimum value in your
notebook and move the ball whenever you find a shorter player. When you
reach the end of the line, the ball will be in front of the shortest player. For
example, the top of Figure 3-6 shows the ball being placed in front of the
fourth player from the left.
Figure 3-6 Selection sort on football players
Swap this shortest player with the player on the left end of the line. You’ve
now sorted one player. You’ve made N−1 comparisons, but only one swap.
On the next pass, you do exactly the same thing, except that you can skip the
player on the left because this player has already been sorted. Thus, the
algorithm starts the second pass at position 1, instead of 0. In the case of Figure
3-6, this second pass finds the shortest player at position 1, so no swap is
needed. With each succeeding pass, one more player is sorted and placed on
the left, and one fewer player needs to be considered when finding the new
minimum. The bottom of Figure 3-6 shows how this sort looks after the first
three passes.
Figure 3-7 The start of a selection sort in the Simple Sorting Visualization
tool
As inner moves right, each cell it points at is compared with the one at min. If
the value at min is lower, then the min arrow is moved to where inner is, just as
you moved the ball to lie in front of the shortest player.
When inner reaches the right end, the items at the outer and min arrows are
swapped. That means that one more cell has been sorted, so outer can be
moved one cell to the right. The next pass starts with min pointing to the new
position of outer and inner one cell to their right.
The outer arrow marks the position of the first unsorted item. All items from
outer to the right end are unsorted. Cells to the left of outer are fully sorted.
The sorting continues until outer reaches the last cell on the right. At that point,
the next possible comparison would have to be past the last cell, so the sorting
must stop.
The outer loop starts by setting the outer variable to point at the beginning of
the array (index 0) and proceeds right toward higher indices. The minimum
valued item is assumed to be the first one by setting min to be the same as
outer. The inner loop, with loop variable inner, begins at outer+1 and
likewise proceeds to the right.
At each new position of inner, the elements __a[inner] and __a[min] are
compared. If __a[inner] is smaller, then min is given the value of inner. At
the end of the inner loop, min points to the minimum value, and the array
elements pointed to by outer and min are swapped.
Invariant
In the selectionSort() method, the array elements with indices less than
outer are always sorted. This condition holds when outer reaches __nItems-
1, which means all but one item are sorted. At that point, you could try to run
the inner loop one more time, but it couldn’t change the min index from outer
because there is only one item left in the unsorted range. After doing nothing in
the inner loop, it would then swap the item at outer with itself—another do
nothing operation—and then outer would be incremented to __nItems.
Because that entire last pass does nothing, you can end when outer reaches
__nItems-1.
Insertion Sort
In most cases the insertion sort is the best of the elementary sorts described in
this chapter. It still executes in O(N2) time, but it’s about twice as fast as the
bubble sort and somewhat faster than the selection sort in normal situations.
It’s also not too complex, although it’s slightly more involved than the bubble
and selection sorts. It’s often used as the final stage of more sophisticated sorts,
such as quicksort.
Partial Sorting
You can use your handy ball to mark a place in the middle of the line. The
players to the left of this marker are partially sorted. This means that they are
sorted among themselves; each one is taller than the person to their left. The
players, however, aren’t necessarily in their final positions because they may
still need to be moved when previously unsorted players are inserted between
them.
Note that partial sorting did not take place in the bubble sort and selection sort.
In these algorithms, a group of data items was completely sorted at any given
time; in the insertion sort, one group of items is only partially sorted.
In the outer for loop, outer starts at 1 and moves right. It marks the leftmost
unsorted array element. In the inner while loop, inner starts at outer and
moves left to inner-1, until either temp is smaller than the array element there,
or it can’t go left any further. Each pass through the while loop shifts another
sorted item one space right.
class Array(object):
def __init__(self, initialSize): # Constructor
self.__a = [None] * initialSize # The array stored as a list
self.__nItems = 0 # No items in array initially
The swap() method swaps the values in two cells of the array. It ensures that
swaps happen only with items that have been inserted in the array and not with
allocated but uninitialized cells. Those tests may not be necessary with the
sorting methods in this module that already ensure proper indices, but they are
a good idea for a general-purpose routine.
We use a separate client program, SortArrayClient.py, to test this new
module and compare the performance of the different sorting methods. This
program uses some other Python modules and features to help in this process
and is shown in Listing 3-5.
Listing 3-5 The SortArrayClient.py program
arr = initArray()
print("Array containing", len(arr), "items:\n", arr)
arr.insertionSort()
print(’Sorted array contains:\n’, arr)
Looking at the output, you can see that the randrange() function provided a
broad variety of values for the array in a random order. The results of the
timing of the sort tests show the bubble sort taking at least twice as much time
as the selection and insertion sorts do. The final sorted version of the array
confirms that sorting works and shows that there are many duplicate elements
in the array.
Stability
Sometimes it matters what happens to data items that have equal keys when
sorting. The SortArrayClient.py test stored only integers in the array, and
equal integers are pretty much indistinguishable. If the array contained
complex records, however, it could be very important how records with the
same key are sorted. For example, you may have employee records arranged
alphabetically by family names. (That is, the family names were used as key
values in the sort.) Let’s say you want to sort the data by postal code too, but
you want all the items with the same postal code to continue to be sorted by
family names. This is called a secondary sort key. You want the sorting
algorithm to shift only what needs to be sorted by the current key and leave
everything else in its order after previous sorts using other keys. Some sorting
algorithms retain this secondary ordering; they’re said to be stable. Stable
sorting methods also minimize the number of swaps or copy operations.
Some of the algorithms in this chapter are stable. We have included an exercise
at the end of the chapter for you to decide which ones are. The answer is not
obvious from the output of their test programs, especially on simple integers.
You need to review the algorithms to see that swaps and copies only happen on
items that need to be moved to be in the right position for the final order. They
should also move items with equal keys the minimum number of array cells
necessary so that they remain in the same relative order in the final
arrangement.
Summary
• The sorting algorithms in this chapter all assume an array as a data
storage structure.
• Sorting involves comparing the keys of data items in the array and
moving the items (usually references to the items) around until they’re in
sorted order.
• All the algorithms in this chapter execute in O(N2) time. Nevertheless,
some can be substantially faster than others.
• An invariant is a condition that remains unchanged while an algorithm
runs.
• The bubble sort is the least efficient but the simplest sort.
• The insertion sort is the most commonly used of the O(N2) sorts
described in this chapter.
• The selection sort performs O(N2) comparisons and only O(N) swaps,
which can be important when swap time is much more significant than
comparison time.
• A sort is stable if the order of elements with the same key is retained.
• None of the sorts in this chapter require more than a single temporary
record variable, in addition to the original array.
Questions
These questions are intended as a self-test for readers. Answers may be found
in Appendix C.
1. Computer sorting algorithms are more limited than humans in that
a. the amount of data that computers can sort is much less than what
humans can.
b. humans can invent new sorting algorithms, whereas computers
cannot.
c. humans know what to sort, whereas computers need to be told.
d. computers can compare only two things at a time, whereas humans
can compare small groups.
2. The two basic operations in simple sorting are _________ items and
_________ them (or sometimes _________ them).
3. True or False: The bubble sort always ends up comparing every possible
pair of items in the initial array.
4. The bubble sort algorithm alternates between
a. comparing and swapping.
b. moving and copying.
c. moving and comparing.
d. copying and comparing.
5. True or False: If there are N items, the bubble sort makes exactly N×N
comparisons.
6. In the selection sort,
a. the largest keys accumulate on the left (low indices).
b. a minimum key is repeatedly discovered.
c. a number of items must be shifted to insert each item in its correctly
sorted position.
d. the sorted items accumulate on the right.
7. True or False: If, on a particular computing platform, swaps take much
longer than comparisons, the selection sort is about twice as fast as the
bubble sort for all values of N.
8. Ignoring the details of where the computer stores each piece of data,
what is a reasonable assumption about the ratio of the amounts of time
taken for a copy operation versus a swap operation?
9. What is the invariant in the selection sort?
10. In the insertion sort, the “marked player” described in the text
corresponds to which variable in the insertionSort() method?
a. inner
b. outer
c. temp
d. __a[outer]
11. In the insertion sort, the “partially sorted” group members are
a. the items that are already sorted but still need to be moved as a block.
b. the items that are in their final block position but may still need to be
sorted.
c. only partially sorted in order by their keys.
d. the items that are sorted among themselves, but items outside the
group may need to be inserted in the group.
12. Shifting a group of items left or right requires repeated __________.
13. In the insertion sort, after an item is inserted in the partially sorted
group, it
a. is never moved again.
b. is never shifted to the left.
c. is often moved out of this group.
d. finds that its group is steadily shrinking.
14. The invariant in the insertion sort is that ________.
15. Stability might refer to
a. items with secondary keys being excluded from a sort.
b. keeping cities sorted by increasing population within each state, in a
sort by state.
c. keeping the same given names matched with the same family names.
d. items keeping the same order of secondary keys without regard to
primary keys.
Experiments
Carrying out these experiments will help to provide insights into the topics
covered in the chapter. No programming is involved.
3-A The “Stability” section explains that stable sorting algorithms don’t
change the relative position within the array of items having equal
valued keys. For example, when you’re sorting an array containing
tuples by their first element such as
(elm, 1), (asp, 1), (elm, 2), (oak, 1)
the algorithm always produces
(asp, 1), (elm, 1), (elm, 2), (oak, 1)
and never
(asp, 1), (elm, 2), (elm, 1), (oak, 1)
Review each of the simple sorting algorithms in this chapter and
determine if they are always stable.
Determining whether a sorting algorithm is stable or not can be difficult.
Try walking through some sample inputs that include some equal-
valued keys to see when those items are moved. If you can think of even
one example where two items with equal keys are reordered from their
original relative order, then you have proven that the algorithm is
unstable. If you can’t think of an example where equal valued keys are
moved out of order, then the algorithm might be stable. To be sure, you
need to provide more proof. That’s usually done by finding invariant
conditions about a partial sequence of items as the algorithm progresses.
For example, if you can show that the output array after index i contains
only items in stable order, that condition stays valid at iteration j if it
held in iteration j − 1, and that i always ends at index 0, then the
algorithm must be stable.
Note that if you get stuck, we provide an answer for this exercise in
Appendix C.
3-B Sometimes the items in an array might have just a few distinct keys with
many copies. That creates a situation where there are many duplicates
of the same key. Use the Simple Sorting Visualization tool to create an
array with 15 cells and filled with only two distinct values, say 10 and
90. Try shuffling and sorting that kind of array with the three sorting
algorithms. Do any of them show advantages or disadvantages over the
others for this kind of data?
3-C In the selectionSort() method shown in Listing 3-2, the inner loop
makes a swap on every pass. It doesn’t need to swap them if the value
of the min and outer indices are the same. Would it make sense to add
an additional comparison of those indices and perform the swap only if
they are different? If so, under what conditions would that improve the
performance? If not, why not?
Programming Projects
Writing programs to solve the Programming Projects helps to solidify your
understanding of the material and demonstrates how the chapter’s concepts are
applied. (As noted in the Introduction, qualified instructors may obtain
completed solutions to the Programming Projects on the publisher’s website.)
3.1 In the bubbleSort() method (Listing 3-1) and the Visualization tool, the
inner index always goes from left to right, finding the largest item and
carrying it out on the right. Modify the bubbleSort() method so that
it’s bidirectional. This means the in index will first carry the largest
item from left to right as before, but when it reaches last, it will
reverse and carry the smallest item from right to left. You need two
outer indexes, one on the right (the old last) and another on the left.
3.2 Add a method called median() to the Array class in the SortArray.py
module (Listing 3-4). This method should return the median value in
the array. (Recall that in a group of numbers, half are larger than the
median and half are smaller.) Do it the easy way.
3.3 Add a method called deduplicate() to the Array class in the
SortArray.py module (Listing 3-4) that removes duplicates from a
previously sorted array without disrupting the order. You can use any of
the sort methods in your test program to sort the data. You can imagine
schemes in which all the items from the place where a duplicate was
discovered to the end of the array would be shifted down one space
every time a duplicate was discovered, but this would lead to slow
O(N2) time, at least when there were a lot of duplicates. In your
algorithm, make sure no item is moved more than once, no matter how
many duplicates there are. This will give you an algorithm with O(N)
time.
3.4 Another simple sort is the odd-even sort. The idea is to repeatedly make
two passes through the array. On the first pass, you look at all the pairs
of items, a[j] and a[j+1], where j is odd (j = 1, 3, 5, …). If their key
values are out of order, you swap them. On the second pass, you do the
same for all the even values (j = 0, 2, 4, …). You do these two passes
repeatedly until the array is sorted. Add an oddEvenSort() method to
the Array class in the SortArray.py module (Listing 3-4). Perform the
outer loop until no swaps occur to see how many passes are needed; one
pass includes both odd-pair and even-pair swapping. Make sure it works
for varying amounts of data and on good and bad initial orderings. After
testing how many passes are needed before no more swaps occur,
determine the maximum number of passes of the outer loop based on
the length of the input array.
The odd-even sort is actually useful in a multiprocessing environment,
where a separate processor can operate on each odd pair simultaneously
and then on each even pair. Because the odd pairs are independent of
each other, each pair can be checked—and swapped, if necessary—by a
different processor. This makes for a very fast sort.
3.5 Modify the insertionSort() method in SortArray.py (Listing 3-4) so
that it counts the number of copies and the number of item comparisons
it makes during a sort and displays the totals. You need to look at the
loop condition in the inner while loop and carefully count item
comparisons. Use this program to measure the number of copies and
comparisons for different amounts of inversely sorted data. Do the
results verify O(N2) efficiency? Do the same for almost-sorted data
(only a few items out of place). What can you deduce about the
efficiency of this algorithm for almost-sorted data?
3.6 Here’s an interesting way to remove items with duplicate keys from an
array. The insertion sort uses a loop-within-a-loop algorithm that
compares every item in the array with the partially sorted items so far.
One way to remove items with duplicate keys would be to modify the
algorithm in the Array class in the SortArray.py module (Listing 3-4)
so that it removes the duplicates as it sorts. Here’s one approach: When
a duplicate key is found, instead of copying the duplicate back into the
array cell at its sorted position, change the key for the marked item to be
a special value that is treated as lower than any other possible key. With
that low key value, it is automatically moved into place at the beginning
of the array. By keeping track of how many duplicates are found, you
know the end of duplicates and beginning of the remaining elements in
the array. When the outer loop ends, the algorithm would have to make
one more pass to shift the unique keys into the cells occupied by the
duplicates. Write an insertionSortAndDedupe() method that performs
this operation. Make sure to test that it works with all different kinds of
input array data.
4. Stacks and Queues
In This Chapter
• Different Structures for Different Use Cases
• Stacks
• Queues
• Priority Queues
• Parsing Arithmetic Expressions
This chapter examines three data storage structures widely used in all kinds of
applications: the stack, the queue, and the priority queue. We describe how
these structures differ from arrays, examining each one in turn. In the last
section, we look at an operation in which the stack plays a significant role:
parsing arithmetic expressions.
Restricted Access
In an array, any item can be accessed, either immediately—if its index number
is known—or by searching through a sequence of cells until it’s found. In the
data structures in this chapter, however, access is restricted: only one item can
be read or removed at a time.
The interface of these structures is designed to enforce this restricted access.
Access to other items is (in theory) not allowed.
More Abstract
Stacks, queues, and priority queues are more abstract structures than arrays and
many other data storage structures. They’re defined primarily by their interface
—the permissible operations that can be carried out on them. The underlying
mechanism used to implement them is typically not visible to their user.
The underlying mechanism for a stack, for example, can be an array, as shown
in this chapter, or it can be a linked list. The underlying mechanism for a
priority queue can be an array or a special kind of tree called a heap. When one
data structure is used to implement a more abstract one, we often say that it is a
lower-level data structure by picturing the more abstract structure being
layered on top of it. We return to the topic of one data structure being
implemented by another when we discuss abstract data types (ADTs) in
Chapter 5, “Linked Lists.”
Stacks
A stack data structure allows access to only one data item in the collection: the
last item inserted. If you remove this item, you can access the next-to-last item
inserted, and so on. Although that constraint seems to be quite limiting, this
behavior occurs in many programming situations. In this section we show how
a stack can be used to check whether parentheses, braces, and brackets are
balanced in a computer program source file. At the end of this chapter, a stack
plays a vital role in parsing (analyzing) arithmetic expressions such as 3×(4+5).
A stack is also a handy aid for algorithms applied to certain complex data
structures. In Chapter 8, “Binary Trees,” you will see it used to help traverse
the nodes of a tree. In Chapter 14, “Graphs,” you will apply it to searching the
vertices of a graph (a technique that can be used to find your way out of a
maze).
Nearly all computers use a stack-based architecture. When a function or
method is called, the return address for where to resume execution and the
function arguments are pushed (inserted) onto a stack in a particular order.
When the function returns, they’re popped off. The stack operations are often
accelerated by the computer hardware.
The idea of having all the function arguments pushed on a stack makes very
clear what the operands are for a particular function or operator. This scheme is
used in some older pocket calculators and formal languages such as PostScript
and PDF content streams. Instead of entering arithmetic expressions using
parentheses to group operands, you insert (or push) those values onto a stack.
The operator comes after the operands and pops them off, leaving the
(intermediate) result on the stack. You will learn more about this approach
when we discuss parsing arithmetic expressions in the last section in this
chapter.
Stack Size
Stacks come in all sizes and are typically capped in size so that they can be
allocated in a single block of memory. The application allocates some initial
size based on the maximum number of items expected to be stacked. If the
application tries to push items beyond what the stack can hold, then either the
stack needs to increase in size, or an exception must occur. The visualization
tool limits the size to what fits conveniently on the screen, but stacks in many
applications can have thousands or millions of cells.
class Stack(object):
def __init__(self, max): # Constructor
self.__stackList = [None] * max # The stack stored as a list
self.__top = -1 # No items initially
The SimpleStack.py implementation has just the basic features needed for a
stack. Like the Array class you saw in Chapter 2, the constructor allocates an
array of known size, called __stackList, to hold the items with the __top
pointer as an index to the topmost item in the stack. Unlike the Array class,
__top points to the topmost item and not the next array cell to be filled. Instead
of insert(), it has a push() method that puts a new item on top of the stack.
The pop() method returns the top item on the stack, clears the array cell that
held it, and decreases the stack size. The peek() method returns the top item
without decreasing the stack size.
In this simple implementation, there is very little error checking. It does
include isEmpty() and isFull() methods that return Boolean values
indicating whether the stack has no items or is at capacity. The peek() method
checks for an empty stack and returns the top value only if there is one. To
avoid errors, a client program would need to use isEmpty() before calling
pop(). The class also includes methods to measure the stack depth and a string
conversion method for convenience in displaying stack contents.
To exercise this class, you can use the SimpleStackClient.py program shown
in Listing 4-2.
stack = Stack(10)
The client creates a small stack, pushes some strings onto the stack, displays
the contents, then pops them off, printing them left to right separated by spaces.
The transcript of running the program shows the following:
$ python3 SimpleStackClient.py
After pushing 6 words on the stack, it contains:
[May, the, force, be, with, you]
Is stack full? False
Popping items off the stack:
you with be force the May
Notice how the program reverses the order of the data items. Because the last
item pushed is the first one popped, the you appears first in the output. Figure
4-3 shows how the data gets placed in the array cells of the stack and then
returned for push and pop operations. The array cells shown as empty in the
illustration still have the value None in them, but because only the values from
the bottom up through the __top pointer are occupied, the ones beyond __top
can be considered unfilled. The visualization tool uses the same pop() method
as in Listing 4-1, setting the popped cell to None.
Figure 4-3 Operation of the Stack class push() and pop() methods
Error Handling
There are different philosophies about how to handle stack errors. What should
happen if you try to push an item onto a stack that’s already full or pop an item
from a stack that’s empty?
In this implementation, the responsibility for avoiding or handling such errors
has been left up to the program using the class. That program should always
check to be sure the stack is not full before inserting an item, as in
if not stack.isFull():
stack.push(item)
else:
print("Can’t insert, stack is full")
The client program in Listing 4-2 checks for an empty stack before it calls
pop(). It does not, however, check for a full stack before calling push().
Alternatively, many stack classes check for these conditions internally, in the
push() and pop() methods. Typically, a stack class that discovers such
conditions either throws an exception, which can then be caught and processed
by the program using the class, or takes some predefined action, such as
returning None. By providing both the ability for users to query the internal
state and the possibility of raising exceptions when constraints are violated, the
data structure enables both proactive and reactive approaches.
The program makes use of Python’s input() function, which prints a prompt
string and then waits for a user to type a response string. The program
constructs a stack instance, gets the word to be reversed from the user, loops
over the letters in the word, and pushes them on the stack. Here the program
avoids pushing letters if the stack is already full. After all the letters are pushed
on the stack, the program creates the reversed version, starting with an empty
string and appending each character popped from the stack. The results look
like this:
$ python3 ReverseWord.py
Word to reverse: draw
The reverse of draw is ward
$ python3 ReverseWord.py
Word to reverse: racecar
The reverse of racecar is racecar
$ python3 ReverseWord.py
Word to reverse: bolton
The reverse of bolton is notlob
$ python3 ReverseWord.py
Word to reverse: A man a plan a canal Panama
The reverse of A man a plan a canal Panama is amanaP lanac a nalp a
nam A
The results show that single-word palindromes come out the same as the input.
If the input “word” has spaces, as in the last example, the spaces are treated
just as any other letter in the string.
Table 4-1 shows how the stack looks as each character is read from this string.
The entries in the second column show the stack contents, reading from the
bottom of the stack on the left to the top on the right.
Table 4-1 Stack Contents in Delimiter Matching
As the string is read, each opening delimiter is placed on the stack. Each
closing delimiter read from the input is matched with the opening delimiter
popped from the top of the stack. If they form a pair, all is well. Nondelimiter
characters are not inserted on the stack; they’re ignored.
This approach works because pairs of delimiters that are opened last should be
closed first. This matches the last-in, first-out property of the stack.
The program doesn’t define any functions; it processes one expression from the
user and exits. It creates a Stack object to hold the delimiters as they are found.
The errors variable is used to track the number of errors found in parsing the
expression. It loops over the characters in the expression using Python’s
enumerate() sequencer, which gets both the index and the value of the string
at that index. As it finds starting (or left) delimiters, it pushes them on the
stack, avoiding overflowing the stack. When it finds ending (or closing or
right) delimiters, it checks whether there is a matching delimiter on the top of
the stack. If not, it prints the error and continues. Some sample outputs are
$ python3 DelimiterChecker.py
Expression to check: a()
Delimiters balance in expression a()
$ python3 DelimiterChecker.py
Expression to check: a( b[4]d )
Delimiters balance in expression a( b[4]d )
$ python3 DelimiterChecker.py
Expression to check: a( b]4[d )
Error: ] at position 4 does not match left delimiter (
Error: ) at position 9 does not match left delimiter [
$ python3 DelimiterChecker.py
Expression to check: {{a( b]4[d )
Error: ] at position 6 does not match left delimiter (
Error: ) at position 11 does not match left delimiter [
Expression missing right delimiters for [{, {]
Efficiency of Stacks
Items can be both pushed and popped from the stack implemented in the Stack
class in constant O(1) time. That is, the time is not dependent on how many
items are in the stack and is therefore very quick. No comparisons or moves
within the stack are necessary.
Queues
In computer science, a queue is a data structure that is somewhat like a stack,
except that in a queue the first item inserted is the first to be removed (first-in,
first-out, or FIFO). In stacks, as you’ve seen, the last item inserted is the first to
be removed (LIFO). A queue models the way people wait for something, such
as tickets being sold at a window or a chance to greet the bride and groom at a
big wedding. The first person to arrive goes first, the next goes second, and so
forth. Americans call it a waiting line, whereas the British call it a queue. The
key aspect is that the first items to arrive are the first to be processed.
Queues are used as a programmer’s tool just like stacks are. They are found
everywhere in computer systems: the jobs waiting to run, the messages to be
passed over a network, the sequence of characters waiting to be printed on a
terminal. They’re used to model real-world situations such as people waiting in
line for tickets, airplanes waiting to take off, or students waiting to see whether
they get into a particular course. This ordering is sometimes called arrival
ordering because the time of arrival in the queue determines the order.
Various queues are quietly doing their job in your computer’s (or the
network’s) operating system. There’s a printer queue where print jobs wait for
the printer to be available. Queues also store user input events like keystrokes,
mouse clicks, touchscreen touches, and microphone inputs. They are really
important in multiprocessing systems so that each event can be processed in the
correct order even when the processor is busy doing something else when the
event occurs.
The two basic operations on the queue are called insert and remove. Insert
corresponds to a person inserting themself at the rear of a ticket line. When that
person makes their purchase, they remove themself from the front of the line.
The terms for insertion and removal in a stack are fairly standard; everyone
says push and pop. The terminology for queues is not quite as standardized.
Insert is also called put or add or enqueue, whereas remove may be called
delete or get or dequeue. The rear of the queue, where items are inserted, is
also called the back or tail or end. The front, where items are removed, may
also be called the head. In this book, we use the terms insert, remove, front, and
rear.
A Shifty Problem
In thinking about how to implement a queue using arrays, the first option
would be to handle inserts like a push on a stack. The first item goes at the last
position (first empty cell) of the array. Then when it’s time to remove an item
from the queue, you would take the first filled cell of the array. To avoid
hunting for the position of those cells, you could keep two indices, front and
rear, to track where the filled cells begin and end, as shown in Figure 4-4.
When Ken arrives, he’s placed in the cell indexed by rear. When you remove
the first item in the queue, you get Raj from the cell indexed by front.
Figure 4-4 Queue operations in a linear array
This operation works nicely because both insert and remove simply copy an
item and update an index pointer. It’s just as fast as the push and pop operations
of the stack and takes only one more variable to manage.
What happens when you get to the end of the array? If the rear index reaches
the end of the array, there’s no space to insert new items. That might be
acceptable because it’s no worse than when a stack runs out of room. If the
front index has moved past the beginning of the array, however, free cells
could be used for item storage. It seems wasteful not to take advantage of them.
One way to reclaim that unused storage space would be to shift all the items in
the array when an insertion would go past the end. Shifting is similar to what
happens with people standing in a line/queue; they all step forward as people
leave the front of the queue. In the array, you would move the item indexed by
front to cell 0 and move all the items up to rear the same number of cells;
then you would set front to 0 and rear to rear – front. Shifting items,
however, takes time, and doing so would make some insert operations take
O(N) time instead of O(1). Is there a way to avoid the shifts?
A Circular Queue
To avoid the problem of not being able to insert more items into a queue when
it’s not full, you let the front and rear pointers wrap around to the beginning
of the array. The result is a circular queue (sometimes called a ring buffer).
This is easy to visualize if you take a row of cells and bend them around in the
form of a circle so that the last cell and first cell are adjacent, as shown in
Figure 4-5. The array has N cells in it, and they are numbered 0, 1, 2, …, N−2,
N−1. When one of the pointers is at N−1 and needs to be incremented, you
simply set it to 0. You still need to be careful not to let the wraparound go too
far and start writing over cells that have valid items in them. To see how, let’s
look first at the Queue Visualization tool and then the code that implements it.
Figure 4-5 Operation of the Queue.insert() method on an empty queue
Keep inserting values until all the cells are filled. Note how the _rear index
wraps around from 9 to 0. When all the cells are filled, _rear is one less than
_front. That’s the same relationship as when the queue was empty, but now
the _nItems counter is 10, not 0.
class Queue(object):
def __init__(self, size): # Constructor
self.__maxSize = size # Size of [circular] array
self.__que = [None] * size # Queue stored as a list
self.__front = 1 # Empty Queue has front 1
self.__rear = 0 # after rear and
self.__nItems = 0 # No items in queue
The __front and __rear pointers point at the first and last items in the queue,
respectively. These and other attributes are named with double underscore
prefixes to indicate they are private. They should not be changed directly by
the object user.
When the queue is empty, where should __front and __rear point? We
typically set one of them to 0, and we choose to do that for __rear. If we also
set __front to be 0, we will have a problem inserting the first element. We set
__front to 1 initially, as shown in the empty queue of Figure 4-5, so that when
the first element is inserted and __rear is incremented, they both are 1. That’s
desirable because the first and last items in the queue are one and the same. So
__rear and __front are 1 for the first item, and __rear is increased for the
insertions that follow. That means the frontmost items in the queue are at lower
indices, and the rearmost are at higher indices, in general.
The insert() method adds a new item to the rear of the queue. It first checks
whether the queue is full. If it is, insert() raises an exception. This is the
preferred way to implement data structures: provide tests so that callers can
check the status in advance, but if they don’t, raise an exception for invalid
operations. Python’s most general-purpose Exception class is used here with a
custom reason string, “Queue overflow”. Many data structures define their
own exception classes so that they can be easily distinguished from exception
conditions like ValueError and IndexError.
You can avoid shifting items in the array during inserts by verifying that space
is available before incrementing the __rear pointer and placing the new item at
that empty cell of the array. The increment takes an extra step to handle the
circular array logic when the pointer would go beyond the maximum size of
the array by setting __rear back to zero. Finally, insert() increases the item
count to reflect the inserted item at the rear.
The remove() method is similar in operation but acts on the __front of the
queue. First, it checks whether the queue is empty and raises an “underflow”
exception if it is. Then it makes a copy of the first item, clears the array cell,
and increments the __front pointer to point at the next cell. The __front
pointer must wrap around, just like __rear did, returning to 0 when it gets to
the end of the array. The item count decreases because the front item was
removed from the array, and the copy of the item is returned.
The peek() method looks at the frontmost item of the queue. You could create
peekfront() and peekrear() methods to look at either end of the queue, but
it’s rare to need both in practice. The peek() method returns None when the
queue is empty, although it might be more consistent to use the same underflow
exception produced by remove().
The isEmpty() and isFull() methods are simple tests on the number of items
in the queue. Note that a slightly different Python syntax is used here. The
whole body of the method is a single return statement. In that case, Python
allows the statement to be placed after the colon ending the method signature.
The __len__() method also uses the shortened syntax. Note also that these
tests look at the __nItems value of the attribute rather than the __front and
__rear indices. That’s needed to distinguish the empty and full queues. We
look at how wrapping the indices around makes that choice harder in Figure 4-
9.
The last method for Queue is __str__(), which creates a string showing the
contents of the queue enclosed in brackets and separated by commas for
display. This method illustrates how circular array indices work. The beginning
of the string has the front of the queue, and the end of the string is the rearmost
item. The for loop uses the variable i to index all current items in the queue. A
separate variable, j, starts at the __front and increments toward the __rear
wrapping around if it passes the maximum size of the array.
Some simple tests of the Queue class are shown in Listing 4-6 and demonstrate
the basic operations. The program creates a queue and inserts some names in it.
The initial queue is empty, and Figure 4-5 shows how the first name, ’Don’, is
inserted. After that first insertion, both __front and __rear point at array cell 1
and the number of items is 1.
Listing 4-6 The QueueClient.py Program
queue = Queue(10)
The person names inserted into the queue keep advancing the __rear pointer
and increasing the __nItems count, as shown in Figure 4-7. After inserting all
the names, the QueueClient.py program uses the __str__() method
(implicitly) and prints the contents of the queue along with the status of
whether the queue is full or not. After that’s complete, the program removes
items one at a time from the queue until the queue is empty. The items are
printed separated by spaces (which is different from the way the __str__()
method displays them). The result looks like this:
$ python3 QueueClient.py
After inserting 6 persons on the queue, it contains:
[Don, Ken, Ivan, Raj, Amir, Adi]
Is queue full? False
Removing items from the queue:
Don Ken Ivan Raj Amir Adi
Figure 4-7 Inserting an item into a partially full queue
The printed result shows that items are deleted from the queue in the same
order they were inserted in the queue. The first step of the deletion process is
shown in Figure 4-8. When the first item, ’Don’, is deleted from the front of
the queue, the __front pointer is advanced to 2, and the number of items
decreases to 5. The __rear pointer stays the same.
Figure 4-8 Deleting an item from the queue
Let’s look at what happens when the queue wraps around the circular array. If
you delete only one name and then insert more names into the queue, you’ll
eventually get to the situation shown in Figure 4-9. The __rear pointer keeps
increasing with each insertion. After it gets to __maxSize – 1, the next
insertion forces __rear to point at cell 0.