Transcript Lecture4MRM

Practicum 1:
- Persistent vs. destructive lists
- Java interfaces
15-211
Fundamental Data Structures and
Algorithms
Margaret Reid-Miller
January 22, 2004
Based on notes by Peter Lee
Reminders
 HW2 is out
due on Monday at 11:59pm
 Read
Chapter 17 (this should be review)
Integer lists in Java
public class List {
int head;
List tail;
public List(int n, List l) {
head = n;
tail = l;
}
}
 An integer list is either
an empty list, or
an integer paired with an integer list
Implementing length() and add()
public class List {
public static int length (List l) {
if (l==null) return 0;
else return 1 + length(l.tail);
}
public static List add (int n, List l) {
if (l==null)
return new List(n, null);
else
return new List(l.head, add(n, l.tail));
}
}
Using list objects
…
list1 = new List(3, null);
…
list2 = List.add(9, list1);
…
List.length(list2) …
…
Choice 1: Destructively modifying L
myList1
nil
myList2
Choice 2: Preserving the old list
myList1
nil
nil
myList2
Which choice does this implement?
public class ListOps {
public static int length (List l) {
if (l==null) return 0;
else return 1 + length(l.tail);
}
public static List add (int n, List l) {
if (l==null)
return new List(n, null);
else
return new List(l.head, add(n, l.tail));
}
}
Which choice is the right one?
 Should add() be destructive
(choice 1) or not (choice 2)?
 Does it matter? Why or why not?
Implementing reverse()
public static List reverse(List l) {
if (l==null)
return null;
else {
List r = reverse(l.tail);
return add(l.head, r);
}
}
How many list cells?
 Is this version of reverse()
destructive or not?
 How many list objects are there after
invoking reverse()?
[If you are a C/C++ hacker, be careful
– We aren’t flipping pointers here!]
Faster reverse?
 Question:
Can you write a version of reverse that
runs in just n steps?
 It should have the same behavior as the
reverse we have shown here, i.e., it should
not destroy the original list.
Faster reverse()
public static List reverse(List l) {
return rev(l, null);
}
private static List rev(List l,
List result) {
if (l==null)
return result;
else
return rev(l.tail,
new List(l.head, result));
}
Objects
 Java is an object-oriented
programming language.
 This means that it encourages
organizing a program’s functionality
around the idea of an “object”.
 But the code for Lists that we have
presented here is not really objectoriented…
Our List code is not object-oriented
public class List {
int head;
List tail;
Essentially a C/C++ struct
public List(int n,
Essentially C/C++ code
List l) {
head = n;
public class ListOps {
tail = l;
}
public static int length (List l)
}
{ … }
public static List add (int n, List l)
{ … }
public static List reverse (List l)
{ … }
}
The empty list is not an object!
…
list1 = new List(3,null);
list2 = null
…
… list1.length() …
… list2.length() …
…
Because null is not an object, method invocations
can fail.
Yuck!
Counters
 As a first example of object-oriented
programming, let’s define counters.
No, not these
kinds of
counters…
Counters
 Let’s define a counter to be an object
that maintains a count that can be
initialized and incremented.
1
0
1
inc
Counters
 Informally, a counting object
can be initialized,
can be incremented, and
can be read.
 We can specify these things in Java
by writing a Java interface.
The Countable interface
/** Interface for counting objects. */
public interface Countable {
Often an adjective
/** Increment the internal counter. */
public void inc();
/** Reset the internal counter to 0. */
public void resetCount();
/** Return the internal count. */
public int readCount();
}
Interfaces
 A Java interface is a kind of
specification for classes of objects.
In this example, the interface specifies
that any class of “countable” objects
must provide the inc(), resetCount(),
and readCount() methods.
A simple implementation
/** Simple counting objects. */
public class SimpleCounter implements Countable {
/** Initialization for new counters. */
public SimpleCounter () { resetCount(); }
/** Reset the counter to 0. */
public void resetCount () { count = 0; }
/** Increment the counter. */
public void inc () { count++; }
The constructor
method.
/** Return the current count. */
public int readCount () { return count; }
private int count;
}
Private instance variable.
Using SimpleCounter
/** A program that uses a SimpleCounters. */
public class Main {
public static void main (String args[]) {
int n = 99;
Countable c = new SimpleCounter();
Countable d = new SimpleCounter();
c.inc();
d.inc();
d.inc();
System.out.println (n + c.readCount() +
d.readCount());
}
}
Creation of new SimpleCounter objects.
Creating new objects
c
Countable c =
new SimpleCounter();
0
Countable d =
new SimpleCounter();
d
0
inc
inc
Our little program
 Files we have created for our little
program:
Countable.java
 The interface specification.
SimpleCounter.java
 An implementation of the interface.
Main.java
 A client program.
Why use interfaces?
 Strictly speaking, Java does not
require us to make use of an
interface for this program.
public class SimpleCounter implements Countable
{
…
}
Why use interfaces?
 Strictly speaking, Java does not
require us to make use of an
interface for this program.
public class SimpleCounter
{
…
}
Leaving out the implements declaration is OK
with Java.
Why use interfaces?
 Interfaces allow us to separate
interface from implementation.
 If properly designed, then clients
assume only the interface.
 Clients don’t have to change, even if
the underlying implementations
change.
read
write
setf
Interface vs implementation
>>(int)
close
open
29
read
write
setf
Interface vs implementation
O(n2)
>>(int)
close
open
30
Abstract data types
 When the allowable operations on a
type of data are controlled in this
manner, we say that we are using
abstract data types.
 In our first example, we have defined
the ADT of counters.
31
Abstract data types
 Abstract data types can make it
easier to…
…make use of pre-existing code.
…work with a team.
…maintain code in the long run.
…write down the results of your careful
thinking about the problem.
Another counting class
/** Counting objects that double every time. */
public class DoublingCounter implements Countable {
/** Initialization for new counters. */
public DoublingCounter () { resetCount(); }
/** Reset the counter to 1. */
public void resetCount () { count = 1; }
/** Increment the counter. */
public void inc () { count *= 2; }
/** Return the current count. */
public int readCount () { return count; }
private int count;
}
A digression
 Why the /** … */ comments?
 These are special documentation
comments that are used for public
classes and members of classes.
 When used properly, the javadoc
command will create web pages for
your code automatically.
 (See the hw1 code.)
Back to Lists
Lists of integers
public interface IntList {
/** @returns the length of the list */
public int length ();
/** Add n to the end of the list */
public IntList add (int n);
/** @returns a String representation */
public String toString ();
}
Inductive definitions
 An integer list is either
an empty list, or
an integer paired with an integer list
Empty lists
public class EmptyIntList implements IntList
{
public int length () { return 0; }
public IntList add (int n) {
return new IntListCell(n);
}
public String toString () {
return “”;
}
}
Non-empty lists
public class IntListCell implements IntList {
private int head;
private IntList tail;
public IntListCell (int n) {
head = n; tail = new EmptyIntList();
}
public IntListCell (int n, IntList tail) {
head = n; tail = tail;
}
public int length () {
return 1 + tail.length();
}
public IntList add (int n) {
return new IntListCell (head, tail.add(n));
}
}
How about the length method?
More inductive definitions
 The length of a list L is
0, if L is the empty list
Base case
1 + length of the tail of L, otherwise
Inductive case
Length
public class EmptyIntList implements IntList {
…
public int length() {
return 0; }
Base
…
}
public class IntListCell implements IntList {
…
public int length() {
return 1 + next.length(); }
…
}
case
Inductive case
Reverse
 The reversal of a list L is:
L, if L is empty
n appended to M, otherwise
 where n is the first element of L, and
 M is the reversal of the tail of L
Reverse
public class EmptyIntList implements IntList {
…
public IntList reverse() {
return this; }
…
}
public class IntListCell implements IntList {
…
public IntList reverse() {
IntList t = tail.reverse();
return t.add(head); }
…
}
The empty list is an object!
…
myList1 = new IntListCell(3);
myList2 = new EmptyIntList();
…
… myList1.length() …
… myList2.length() …
…
Since we use objects to represent empty lists,
method invocations are always OK.
Yay!
Constant factors
 “My computer is 4 times faster than
yours.”
 So what?
“Big-Oh” notation
T(N) = O(f(N))
“T(N) is order f(N)”
cf(N)
running time
T(N)
n0
N
“Big-Oh” notation
 Given a function T(N):
 T(N) = O(f(N)) if
there is a real constant c and integer
constant n0 such that T(N)  c.f(N) for
all N  n0.
c is called the constant factor.
Big-Oh
 When T(N) = O(f(N)), we are saying
that T(N) grows no faster than f(N).
I.e., f(N) describes an upper bound on
T(N).
 Put another way:
For “large enough” inputs, c.f(N) always
dominates T(N).
 Called the asymptotic behavior
Big-O characteristic
 If T(N) = c.f(N) then
T(N) = O(f(N))
Constant factors “don’t matter”
 Because of this, when T(N) =
O(c.g(N)), we usually drop the
constant and just say O(g(N))
Big-O characteristic
 Suppose T(N)= k, for some constant k
 Then T(N) = O(1)
 Why?
because c*1 > k, for some c
Big-O characteristic
 More interesting:
 Suppose T(N) = 20n3 + 10nlog n + 5
 Then T(N) = O(n3)
 Lower-order terms “don’t matter”
 Question:
 What constants c and n0 can be used to show that the
above is true?
 Answer: c=35, n0=1
From last time…
 We calculated this running time for
reverse()
 So, in big-Oh terms:
O(n2)
Big-O characteristic
 If T1(N) = O(f(N)) and T2(N) = O(g(N))
then
T1(N) + T2(N) = max(O(f(N)), O(g(N)).
The bigger task always dominates
eventually.
 Also:
T1(N)  T2(N) = O(f(N)  g(N)).
Some common functions
1200
1000
800
10N
100 log N
5 N^2
N^3
2^N
600
400
200
0
1
2
3
4
5
6
7
8
9
10
Big-Oh is imprecise
 Let T(N) = 100log N
 Then T(N) = O(log N)
 And T(N) = O(N2)
 And T(N) = O(N3)
 And T(N) = O(2N)
Tight bounds
 Because of this imprecision, we
normally try to find the “tightest”
bound on the number of steps.
 So, while it is true that reverse() is
O(2n), it is more useful to use the
tighter bound of O(n2).
Big-O characteristics
 logk(N) = O(N) for any constant k.
I.e, logarithms grow very slowly.
12
10
8
log N
N
6
4
2
0
1
2
3
4
5
6
7
8
9
10
Note
 There is a bit of a mismatch because
we are counting “steps”, which are
always whole numbers, but
logarithms are real numbers
 We will take the floor or ceiling of
any real numbers
Usually this is implicitly done
Stacks and Queues, Revisited
A Stack interface
public interface Stack {
public void push(Object x);
public void pop();
public Object top();
public boolean isEmpty();
public void makeEmpty();
}
Stacks are LIFO
Push operations:
e
d
c
b
a
Stacks are LIFO
Pop operation:
e
d
c
b
a
Last element
that was pushed
is the first to be
popped.
A Queue interface
public interface Queue {
public void enqueue(Object x);
public Object dequeue();
public boolean isEmpty();
public void makeEmpty();
}
Queues are FIFO
back
front
k
r
q
c
m
Queues are FIFO
Enqueue operation:
back
y
front
k
r
q
c
m
Queues are FIFO
Enqueue operation:
back
front
y
k
r
q
c
m
Queues are FIFO
Dequeue operation:
back
front
y
k
r
q
c
m
Implementing stacks, 1
Linked representation.
All operations constant
time, or O(1).
c
b
a
Implementing stacks, 2
 An alternative is to use an array-based
representation.
a
b
c
top
 What are some advantages and disadvantages of
an array-based representation?
Array representation of stacks
 But what to do when the array
overflows?
 Can we still get O(1) operations?
 We could have a linked list of arrays.
 Is there another way?
An idea
 Let’s try doubling the size of the
array every time it overflows.
 In more detail:
Start off with an array of size n.
When the array overflows, allocate a
new array of size 2n, copy all of the
previous n elements to it.
Set n=2n, and then continue by using
the new array.
A Big 15-211 Hint
 Whenever you see something
doubling, immediately think of
powers of 2:
20, 21, 22, 23, 24, …
Opportunity for induction…
Question 1
 In the worst case, what is the running
time of the push operation?
 I.e., how many copying operations might be
needed?
 Answer: O(n).
 If a push() operation causes an overflow, then
potentially all n elements on the stack must be
copied.
Question 2
 In the worst case, when starting with
an empty stack, what is the average
running time for all of the push
operations in any given sequence of
legal stack operations?
What contributes
 What contributes to the running
time?
The push itself
Possibility of creating a new array
Copying all of the elements to a new
array
Constant-time overhead
This is the overhead that we
have to worry about
Informal analysis
 Let’s try counting a sequence of push
operations, starting with array of size 0.
 push: 0 copies (create new array of size 20)
 push: 1 copy (into a new array of size 21)
 push: 2 copies (into a new array of size 22)
 push push: 4 copies (into a new array of size 23)
 push push push push: 8 copies (into a new array of size
24)
Informal analysis, cont’d
For 2n push operations,
a grand total of
2n-1+2n-2+2n-3…+20 =
 2n
copy operations are needed.
So, on average, one copy is needed
for every push operation.
Question 2, again
 In the worst case, when starting with
an empty stack, what is the average
running time for all of the push
operations in any given sequence of
legal stack operations?
 Answer: O(1)
Each push might require the creation of
a new array plus, on average, a copy.
Amortized running time
 We say that our array-based
implementation of stacks runs in
amortized constant time.
 The J2SE java.util.ArrayList class is
based on this representation.
A question to ponder…
 Question: How would you use an
implementation of the Stack
interface to implement the Queue
interface?
 What would be the running time of
the operations?
A queue from two stacks
Enqueue:
Dequeue:
j
a
i
b
h
c
g
d
f
e
What happens
when the stack
on the right
becomes empty?