Algorithms,.Robert.sedgewick,.Kevin.wayne,.4e 2

Download as pdf or txt
Download as pdf or txt
You are on page 1of 9

1.

3 BAGS, QUEUES, AND STACKS

Several fundamental data types involve collections of objects. Specifically, the set
of values is a collection of objects, and the operations revolve around adding, remov-
ing, or examining objects in the collection. In this section, we consider three such data
types, known as the bag, the queue, and the stack. They differ in the specification of
which object is to be removed or examined next.
Bags, queues, and stacks are fundamental and broadly useful. We use them in imple-
mentations throughout the book. Beyond this direct applicability, the client and imple-
mentation code in this section serves as an introduction to our general approach to the
development of data structures and algorithms.
One goal of this section is to emphasize the idea that the way in which we represent
the objects in the collection directly impacts the efficiency of the various operations.
For collections, we design data structures for representing the collection of objects that
can support efficient implementation of the requisite operations.
A second goal of this section is to introduce generics and iteration, basic Java con-
structs that substantially simplify client code. These are advanced programming-lan-
guage mechanisms that are not necessarily essential to the understanding of algorithms,
but their use allows us to develop client code (and implementations of algorithms) that
is more clear, compact, and elegant than would otherwise be possible.
A third goal of this section is to introduce and show the importance of linked data
structures. In particular, a classic data structure known as the linked list enables im-
plementation of bags, queues, and stacks that achieve efficiencies not otherwise pos-
sible. Understanding linked lists is a key first step to the study of algorithms and data
structures.
For each of the three types, we consider APIs and sample client programs, then
look at possible representations of the data type values and implementations of the
data-type operations. This scenario repeats (with more complicated data structures)
throughout this book. The implementations here are models of implementations later
in the book and worthy of careful study.

120
1.3 ■ Bags, Queues, and Stacks 121

APIs As usual, we begin our discussion of abstract data types for collections by de-
fining their APIs, shown below. Each contains a no-argument constructor, a method to
add an item to the collection, a method to test whether the collection is empty, and a
method that returns the size of the collection. Stack and Queue each have a method to
remove a particular item from the collection. Beyond these basics, these APIs reflect two
Java features that we will describe on the next few pages: generics and iterable collections.

Bag

public class Bag<Item> implements Iterable<Item>

Bag() create an empty bag


void add(Item item) add an item
boolean isEmpty() is the bag empty?
int size() number of items in the bag

FIFO queue

public class Queue<Item> implements Iterable<Item>

Queue() create an empty queue


void enqueue(Item item) add an item
Item dequeue() remove the least recently added item
boolean isEmpty() is the queue empty?
int size() number of items in the queue

Pushdown (LIFO) stack

public class Stack<Item> implements Iterable<Item>

Stack() create an empty stack


void push(Item item) add an item
Item pop() remove the most recently added item
boolean isEmpty() is the stack empty?
int size() number of items in the stack

APIs for fundamental generic iterable collections


122 CHAPTER 1 ■ Fundamentals

Generics. An essential characteristic of collection ADTs is that we should be able to use


them for any type of data. A specific Java mechanism known as generics, also known
as parameterized types, enables this capability. The impact of generics on the program-
ming language is sufficiently deep that they are not found in many languages (including
early versions of Java), but our use of them in the present context involves just a small
bit of extra Java syntax and is easy to understand. The notation <Item> after the class
name in each of our APIs defines the name Item as a type parameter, a symbolic place-
holder for some concrete type to be used by the client. You can read Stack<Item> as
“stack of items.” When implementing Stack, we do not know the concrete type of Item,
but a client can use our stack for any type of data, including one defined long after we
develop our implementation. The client code provides a concrete type when the stack
is created: we can replace Item with the name of any reference data type (consistently,
everywhere it appears). This provides exactly the capability that we need. For example,
you can write code such as
Stack<String> stack = new Stack<String>();
stack.push("Test");
...
String next = stack.pop();
to use a stack for String objects and code such as
Queue<Date> queue = new Queue<Date>();
queue.enqueue(new Date(12, 31, 1999));
...
Date next = queue.dequeue();
to use a queue for Date objects. If you try to add a Date (or data of any other type than
String) to stack or a String (or data of any other type than Date) to queue, you will
get a compile-time error. Without generics, we would have to define (and implement)
different APIs for each type of data we might need to collect; with generics, we can use
one API (and one implementation) for all types of data, even types that are imple-
mented in the future. As you will soon see, generic types lead to clear client code that is
easy to understand and debug, so we use them throughout this book.
Autoboxing. Type parameters have to be instantiated as reference types, so Java has
special mechanisms to allow generic code to be used with primitive types. Recall that
Java’s wrapper types are reference types that correspond to primitive types: Boolean,
Byte, Character, Double, Float, Integer, Long, and Short correspond to boolean,
byte, char, double, float, int, long, and short, respectively. Java automatically con-
verts between these reference types and the corresponding primitive types—in assign-
ments, method arguments, and arithmetic/logic expressions. In the present context,
1.3 ■ Bags, Queues, and Stacks 123

this conversion is helpful because it enables us to use generics with primitive types, as
in the following code:
Stack<Integer> stack = new Stack<Integer>();
stack.push(17); // auto-boxing (int -> Integer)
int i = stack.pop(); // auto-unboxing (Integer -> int)

Automatically casting a primitive type to a wrapper type is known as autoboxing, and


automatically casting a wrapper type to a primitive type is known as auto-unboxing.
In this example, Java automatically casts (autoboxes) the primitive value 17 to be of
type Integer when we pass it to the push() method. The pop() method returns an
Integer, which Java casts (auto-unboxes) to an int before assigning it to the variable i.

Iterable collections. For many applications, the client’s requirement is just to process
each of the items in some way, or to iterate through the items in the collection. This
paradigm is so important that it has achieved first-class status in Java and many other
modern languages (the programming language itself has specific mechanisms to sup-
port it, not just the libraries). With it, we can write clear and compact code that is free
from dependence on the details of a collection’s implementation. For example, suppose
that a client maintains a collection of transactions in a Queue, as follows:
Queue<Transaction> collection = new Queue<Transaction>();

If the collection is iterable, the client can print a transaction list with a single statement:
for (Transaction t : collection)
{ StdOut.println(t); }

This construct is known as the foreach statement: you can read the for statement as for
each transaction t in the collection, execute the following block of code. This client code
does not need to know anything about the representation or the implementation of the
collection; it just wants to process each of the items in the collection. The same for loop
would work with a Bag of transactions or any other iterable collection. We could hardly
imagine client code that is more clear and compact. As you will see, supporting this
capability requires extra effort in the implementation, but this effort is well worthwhile.

It is interesting to note that the only differences between the APIs for Stack and
Queue are their names and the names of the methods. This observation highlights the
idea that we cannot easily specify all of the characteristics of a data type in a list of
method signatures. In this case, the true specification has to do with the English-lan-
guage descriptions that specify the rules by which an item is chosen to be removed (or
to be processed next in the foreach statement). Differences in these rules are profound,
part of the API, and certainly of critical importance in developing client code.
124 CHAPTER 1 ■ Fundamentals

Bags. A bag is a collection where removing items is not supported—its purpose is to


provide clients with the ability to collect items and then to iterate through the collected
items (the client can also test if a bag is empty and find its number of items). The order
of iteration is unspecified and should be immaterial to the client. To appreciate the con-
cept, consider the idea of an avid marble collector, who might put marbles in a bag, one
at a time, and periodically process all the marbles to look
for one having some particular characteristic. With our a bag of
marbles
Bag API, a client can add items to a bag and process them
all with a foreach statement whenever needed. Such a cli-
ent could use a stack or a queue, but one way to emphasize
that the order in which items are processed is immaterial
is to use a Bag. The class Stats at right illustrates a typi-
cal Bag client. The task is simply to compute the average
add( )
and the sample standard deviation of the double values
on standard input. If there are N numbers on standard in-
put, their average is computed by adding the numbers and
dividing by N; their sample standard deviation is comput-
ed by adding the squares of the difference between each
number and the average, dividing by N–1, and taking the
square root. The order in which the numbers are consid- add( )

ered is not relevant for either of these calculations, so we


save them in a Bag and use the foreach construct to com-
pute each sum. Note : It is possible to compute the standard
deviation without saving all the numbers (as we did for the
average in Accumulator—see Exercise 1.2.18). Keeping
the all numbers in a Bag is required for more complicated for (Marble m : bag)
statistics.

process each marble m


(in any order)
Operations on a bag
1.3 ■ Bags, Queues, and Stacks 125

typical Bag client public class Stats


{
public static void main(String[] args)
{
Bag<Double> numbers = new Bag<Double>();

while (!StdIn.isEmpty())
numbers.add(StdIn.readDouble());
int N = numbers.size();

double sum = 0.0;


for (double x : numbers)
sum += x;
double mean = sum/N;

sum = 0.0;
for (double x : numbers)
sum += (x - mean)*(x - mean);
double std = Math.sqrt(sum/(N-1));

StdOut.printf("Mean: %.2f\n", mean);


StdOut.printf("Std dev: %.2f\n", std);

}
}

application % java Stats


100
99
101
120
98
107
109
81
101
90

Mean: 100.60
Std dev: 10.51
126 CHAPTER 1 ■ Fundamentals

FIFO queues. A FIFO queue (or just a queue) is a collection that is based on the first-
in-first-out (FIFO) policy. The policy of doing tasks in the same order that they arrive
is one that we encounter frequently in everyday life:
server
queue of customers from people waiting in line at a theater, to cars wait-
ing in line at a toll booth, to tasks waiting to be ser-
0 1 2 viced by an application on your computer. One bed-
rock principle of any service policy is the perception
new arrival
at the end of fairness. The first idea that comes to mind when
enqueue most people think about fairness is that whoever
3 0 1 2 3
has been waiting the longest should be served first.
new arrival That is precisely the FIFO discipline. Queues are a
at the end
natural model for many everyday phenomena, and
enqueue
0 1 2 3 4
they play a central role in numerous applications.
4
When a client iterates through the items in a queue
first in line with the foreach construct, the items are processed
leaves queue
dequeue in the order they were added to the queue. A typi-
0 0 1 2 3 4 cal reason to use a queue in an application is to save
items in a collection while at the same time preserv-
next in line
leaves queue ing their relative order : they come out in the same
dequeue order in which they were put in. For example, the
1 1 2 3 4 client below is a possible implementation of the
readDoubles() static method from our In class.
A typical FIFO queue The problem that this method solves for the client
is that the client can get numbers from a file into an
array without knowing the file size ahead of time. We enqueue the numbers from the file,
use the size() method from Queue to find the size needed for the array, create the ar-
ray, and then dequeue the num-
bers to move them to the array. public static int[] readInts(String name)
{
A queue is appropriate because In in = new In(name);
it puts the numbers into the ar- Queue<Integer> q = new Queue<Integer>();
ray in the order in which they while (!in.isEmpty())
q.enqueue(in.readInt());
appear in the file (we might use
int N = q.size();
a Bag if that order is immateri-
int[] a = new int[N];
al). This code uses autoboxing for (int i = 0; i < N; i++)
and auto-unboxing to convert a[i] = q.dequeue();
return a;
between the client’s double }
primitive type and and the
queue’s Double wrapper type. Sample Queue client
1.3 ■ Bags, Queues, and Stacks 127

Pushdown stacks. A pushdown stack (or just a stack) is a stack of


documents
a collection that is based on the last-in-first-out (LIFO)
policy. When you keep your mail in a pile on your desk,
you are using a stack. You pile pieces of new mail on the
top when they arrive and take each piece of mail from new (gray) one
goes on top
the top when you are ready to read it. People do not push( )
process as many papers as they did in the past, but the
same organizing principle underlies several of the ap-
plications that you use regularly on your computer. For new (black) one
example, many people organize their email as a stack— push( )
goes on top
they push messages on the top when they are received
and pop them from the top when they read them, with
most recently received first (last in, first out). The ad-
vantage of this strategy is that we see interesting email as remove the
black one
soon as possible; the disadvantage is that some old email = pop() from the top
might never get read if we never empty the stack. You
have likely encountered another common example of a
stack when surfing the web. When you click a hyperlink, remove the
your browser displays the new page (and pushes onto a = pop()
gray one
from the top
stack). You can keep clicking on hyperlinks to visit new
pages, but you can always revisit the previous page by
clicking the back button (popping it from the stack).
The LIFO policy offered by a stack provides just the be- Operations on a pushdown stack
havior that you expect. When a client iterates through
the items in a stack with the foreach construct, the items
are processed in the reverse of the order in
which they were added. A typical reason to
use a stack iterator in an application is to save public class Reverse
{
items in a collection while at the same time public static void main(String[] args)
reversing their relative order . For example, {
the client Reverse at right reverses the or- Stack<Integer> stack;
stack = new Stack<Integer>();
der of the integers on standard input, again while (!StdIn.isEmpty())
without having to know ahead of time how stack.push(StdIn.readInt());
many there are. The importance of stacks in for (int i : stack)
computing is fundamental and profound, StdOut.println(i);
}
as indicated in the detailed example that we }
consider next.
Sample Stack client
128 CHAPTER 1 ■ Fundamentals

Arithmetic expression evaluation. As another example of a stack client, we consider


a classic example that also demonstrates the utility of generics. Some of the first pro-
grams that we considered in Section 1.1 involved computing the value of arithmetic
expressions like this one:
( 1 + ( ( 2 + 3 ) * ( 4 * 5 ) ) )

If you multiply 4 by 5, add 3 to 2, multiply the result, and then add 1, you get the value
101. But how does the Java system do this calculation? Without going into the details of
how the Java system is built, we can address the essential ideas by writing a Java program
that can take a string as input (the expression) and produce the number represented by
the expression as output. For simplicity, we begin with the following explicit recursive
definition: an arithmetic expression is either a number, or a left parenthesis followed by
an arithmetic expression followed by an operator followed by another arithmetic ex-
pression followed by a right parenthesis. For simplicity, this definition is for fully paren-
thesized arithmetic expressions, which specify precisely which operators apply to which
operands—you are a bit more familiar with expressions such as 1 + 2 * 3, where we
often rely on precedence rules instead of parentheses. The same basic mechanisms that
we consider can handle precedence rules, but we avoid that complication. For speci-
ficity, we support the familiar binary operators *, +, -, and /, as well as a square-root
operator sqrt that takes just one argument. We could easily allow more operators and
more kinds of operators to embrace a large class of familiar mathematical expressions,
involving trigonometric, exponential, and logarithmic functions. Our focus is on un-
derstanding how to interpret the string of parentheses, operators, and numbers to en-
able performing in the proper order the low-level arithmetic operations that are avail-
able on any computer. Precisely how can we convert an arithmetic expression—a string
of characters—to the value that it represents? A remarkably simple algorithm that was
developed by E. W. Dijkstra in the 1960s uses two stacks (one for operands and one for
operators) to do this job. An expression consists of parentheses, operators, and oper-
ands (numbers). Proceeding from left to right and taking these entities one at a time,
we manipulate the stacks according to four possible cases, as follows:
■ Push operands onto the operand stack.

■ Push operators onto the operator stack.

■ Ignore left parentheses.

■ On encountering a right parenthesis, pop an operator, pop the requisite number

of operands, and push onto the operand stack the result of applying that opera-
tor to those operands.
After the final right parenthesis has been processed, there is one value on the stack,
which is the value of the expression. This method may seem mysterious at first, but it

You might also like

pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy