in-class exercise (everyone together): what is the runtime (big O) of
empty(), push(), and pop()?
what is the runtime (big O) of these same methods in the array stack?
Other stack implementations, using a java Vector or List
any extensible data structure that has access at one end can be
used to implement a stack
this includes java Vectors and java Lists
new data is added to or removed from the end of the Vector or
ArrayList in O(1) time (when the array doesn't have to grow)
in the code in the book (p. 164), note that the data is
stored in an object of type List<E>, that is created
as an ArrayList<E>: this is an example of polymorphism,
and makes it easy to switch to a different kind of list
new data is added to or removed from the front of the Linked List,
in O(1) time
Stack Applications: Palindromes
a palindrome is a string that is the same when read backwards
or forwards: "radar", "level", "racecar"
there are many algorithms for recognizing palindromes, and most
are equivalent
one such algorithm uses a stack:
the characters of the string are pushed onto the stack, one by one
as they are removed from the stack in LIFO order, they are removed
in reverse order
they can be compared to the characters in the string
if the characters from the stack match the characters from the string,
the string is a palindrome
Stack Applications: Matching Parentheses
balanced parenteses: "(a (b c) [d (e)] f g)"
unbalanced parenteses: "(a (b c {d (e)) f g]"
algorithm to check for balanced parentheses:
when encountering an open parenthesis, put it on the stack
when encountering a closed parenthesis, remove the matching
one from the top of the stack, or declare an error (if the top
of stack does not match, or if the stack was empty)
at the end of the string, should have an empty stack
if the stack is not empty at the end of the string, the parentheses
are not balanced
infix expressions
2 + 3 * 4 has the value?
some operators (*, /, %) have higher precedence than other
operators (+, -)
operators with the same precedence are evaluated in left-to-right order
these expression can be evaluated using two stacks:
when an operand is read, it is pushed onto the operand stack
operators are:
pushed onto the operator stack if the new operator has higher
precedence than the old operator
otherwise, the top of the operator stack is popped and evaluated with
the top two elements of the operand stack, the result is pushed onto the
operand stack, and the new operator is pushed onto the operator stack
at the end of the expression, operators are popped off and evaluated
(popping the operands and pushing the results) until the operator stack is empty
at this point, the operand stack should have exactly one number in it
more interesting with more precedence levels, e.g. ^ (exponentiation)
the Java compiler must recognize (parse) Java expressions
and either:
evaluate any constant-valued (sub-) expressions, or
emit code to evaluate the expression at run-time
parenthesized infix expressions
(2 + 3) * 4 has the value?
when reading a left parenthesis, push it onto the operator stack
when reading a right parenthesis, behave as at the end of the
expression, until the matching left parenthesis is popped from the
operator stack
everything else is the same as the previous algorithm
integer operators
addition (+), subtraction (-), multiplication (*) work as expected
remainder (%), also known as modulo, returns the remainder from the
division: 3 % 2 gives 1, 127 % 100 gives 27
multiplication, division, and modulo have higher precedence than
addition and subtraction, and so are evaluated first: 3 + 54 * 17
is 3 + (54 * 17)
with equal-precedence operators, the expression is evaluated from
left to right:
99 - 3 - 33 / 11 / 3 is ((99 - 3) - ((33 / 11) / 3))
other kinds of expressions
in a prefix expression, the operator comes before the operands:
/ + 3 * 7 4 2 means (3 + 7 * 4) / 2
in a postfix expression, the operator comes after the operands:
4 2 / 3 + 1 * means (4 / 2 + 3) * 1
in an infix expression, the operator comes in-between the operands
only infix expressions need:
precedence
parentheses (to override precedence)
in prefix and postfix expression, the position indicates which operands
are used with which operators
converting from one notation to the other can benefit from
using a stack or recursion
computing in prefix or postfix is easy when using a stack
algorithm for postfix computation
read the next input (next character) of the string
if the character is an operand, push it on the stack
if the character is an operator, pop the top two elements
off the stack, apply the corresponding operation
(the operands must be in the correct order!), and push the result back
on the stack
if the string is empty:
if the stack has one element, that element is the result
if the stack has 0 or multiple elements, the expression is
malformed
in-class exercise: use the above algorithm to
evaluate the following expressions:
9 7 /
1 2 * 3 * 4 *
3 4 * 1 2 + -
StringBuilder
it is easy to concatenate strings
however, it is not particularly efficient: it involves copying all the
characters of the original and the new string
a
string builder
is like an array list, but for strings: it is a data structure
that efficiently supports growable (extensible) strings
a StringBuffer is similar, but will work correctly in multi-threaded
programs
queues
a stack is a Last-In, First-Out (LIFO) data structure
a First-In, First-Out (FIFO) data structure is known as a queue
the word "queue" (pronounced the same as the letter "Q") is
used in the UK for what in the US is known
as a "line", e.g. at a supermarket or a bank or a movie theater
the first person in the queue will be the first one served
queues are commonly used with computers:
documents to be printed are queued
packets to be sent on a network are queued
priority queues allow high-priority items to move to the head
of a line, so are not strictly FIFO, whereas
compute the average waiting time over many simulations
compute the worst case waiting time over many simulations
compute the average worst case waiting time for each simulation
recognizing palindromes:
queue is FIFO, stack is LIFO
push each character onto the stack, offer each character to the queue
then remove one character at a time from both data structures
if it is a palindrome, the characters will be the same
print queue: print jobs in the order submitted
traversing data structures
queue implementation strategies
similar to stack
linked list implementation, or
array implementation
as for other collections, we don't need to know the type of the
elements, only that they are objects, so a generic implementation
is fine
array implementation of queues
like stack: store all the elements in an array
when inserting a new element (offer), just like stack,
add the element at the end of the queue
two choices when removing an new element from the front of
the list (poll, remove):
copy all subsequent elements down by one index, so the first element
is still at the beginning of the array, or
keep track of where the head of the queue is, with another index
variable
for example, removing the front element (a) from this queue:
can be done by copying:
or by keeping track of where the first element is:
in-class exercise: what are the advantages and disadvantages of these
two strategies?
in-place array implementation
two integer variables, one the index of the head of the queue
(front), the other the index of the tail of the queue (end)
so valid elements are found from
array[front] through array[end - 1]
queue contents get ever higher in the array, while lower-numbered
indices will no longer contain valid data
eventually, even if the queue only has a few values,
we run out of indices to put new data into
solution: once we reach the end of the array, put the data
beginning at index 0 again
the modulo operation can be used to make this simple:
index = (index + 1) % QUEUE_SIZE;
for example, if queue size is 15 and index = 14,
14 + 1 = 15,
15 % 15 = 0
so the new value of the index is 0
the number of elements in the array is
(end - front + QUEUE_SIZE) % QUEUE_SIZE,
but it is easier to simply maintain a size field
this is an example of adding elements 's' and 't' to a queue:
implementation of the method offer
if there is room,
insert at end
increment end modulo MAX_SIZE
offer calls the private method
full to make sure room is available
if no room, simply returns false
implementation in textbook (p. 323) doubles the size of the array:
what is the runtime of this method?
amortized runtime analysis
so far, we have only considered worst-case runtime in our big-O
analysis
consider the offer method:
if there is room, it takes O(1) (constant time)
if more room is needed, it takes O(n) (linear time)
how long does it take on average?
assume we just doubled the size of the array, to size 2n
so for the next n calls to offer, the calls
will take constant time
then, on the following calls to offer, it will take
time n
the total time for n calls is O(n)
so the average time per calls must be O(1)
this is known as the amortized runtime: sometimes the
call is expensive, but this cost is amortized across a large (enough)
number of inexpensive calls, so the average is low
the hard part is guaranteeing that there will be all those
inexpensive calls
for example, assume that instead of doubling the array size, the
array size increases by a constant, say 10 new elements
then, O(n) time is spent for every 10 calls
on average, the time is O(n/10), which is still O(n) or linear
no constant is large enough to amortize the linear cost
so the array size has to at least double to give O(1) amortized
time
implementation of the method poll
if there is at least one element,
element is taken from front
increment front modulo MAX_SIZE
if there is no element, simply returns null
peek is even simpler: no increment, no size change
what is the runtime of this method?
what is the runtime of the empty method?
data structure traversal
in a linked list, each node has a link to at most one other node
in a doubly-linked list, each node has a link to at most two other nodes
there are more general data structures in which nodes can have links
to multiple other nodes
these data structures go by different names, including trees
and graphs
in some cases, we need to have a program start at one node and visit
all the other nodes in the data structure: tree traversal or
graph traversal
in general, I can start from a given node and put all the nodes it
is connected to into a queue
then, I repeatedly remove one node from the queue, visit it, and put
into the queue all the nodes it is connected to
if I keep doing this, staying away from nodes that have already been
visited, I will eventually visit all connected nodes
this is called breadth-first traversal:
visually arranging the first node at the top
the nodes it is connected to right below it,
the nodes they are connected to right below them,
nodes are visited in left-to-right and top-to-bottom order
depth-first traversal
for a traversal, I could push the nodes in a stack instead of a queue
then, the node I will visit next is the node I put on the stack most
recently
that means visiting every node connected to the most recently visited
node, before visiting any node that was pushed onto the stack earlier
this is called depth-first traversal because the traversal
tends to go top-to-bottom, then climb back up and explore the side branches