Fundamental data types
Stack: examine the item most recently added (LIFO)
Queue: examine the item least recently added (FIFO)
Separate interface and implementation
For example: stack, queue, bag, priority queue, symbol table, union-find, ...
Benefits:
client can't know details of implementation
implementation can't know details of client needs
design: creates modular, reusable libraries
performance: use optimized implementation where it matters
a recursive data structure that is either empty (null
) or a reference to a node
having a reference to a (generic) item and a reference to a linked list
Warmup API: stack of strings data type
![]() |
![]() |
Warmup client: reverse sequence of strings from standard input
How to implement a stack with a linked list?
|
push("it") push("was") push("the") push("best") |
first
to first node in a singly-linked listfirst
first
// inner class private class Node { String item; Node next; }
public String pop() { String item = first.item; // save item to return first = first.next; // delete first node return item; // return saved item }
public void push(String item) { Node oldfirst = first; // save a link to the list first = new Node(); // create a new node for the beginning first.item = item; // set the instance variables in first.next = oldfirst; // the new node }
public class LinkedStackOfStrings { private Node first = null; private class Node { // private inner class String item; // (access modifiers for instance Node next; // variables don't matter) } public boolean isEmpty() { return first == null; } public void push(String item) { Node oldfirst = first; first = new Node(); first.item = item; first.next = oldfirst; } public String pop() { String item = first.item; first = first.next; return item; } }
Proposition: Every operation takes constant time in the worst case
Proposition: A stack with \(N\) items uses \(\sim 40 N\) bytes.
private class Node { String item; Node next; } |
![]() |
Remark: This accounts for the memory for the stack, but not the memory for String
s themselves, which the client owns.
How to implement a fixed-capacity stack with array?
|
push("it") push("was") push("the") push("best") push("times") |
s[]
to store N
items on stackpush()
: add new item at s[N]
pop()
: remove item from s[N-1]
Defect: Stack overflows when N
exceeds capacity! (stay tuned...)
public class FixedCapacityStackOfStrings { private String[] s; private int N = 0; public FixedCapacityStackOfStrings(int capacity) { s = new String[capacity]; // a cheat (stay tuned!) } public boolean isEmpty() { return N == 0; } public void push(String item) { s[N++] = item; // use to index into array, then increment N } public String pop() { return s[--N]; // decrement N, then use to index into array } }
Overflow and underflow
Null items: we allow null
items to be inserted
Loitering: holding a reference to an object when it is no longer needed
Following code allows for loitering.
public String pop() { return s[--N]; }
Following code avoids loitering, so garbage collector can reclaim memory for an object only if no outstanding references.
public String pop() { String item = s[--N]; s[N] = null; return item; }
Problem: requiring client to provide capacity does not implement API!
Q: How to grow and shrink array?
First try:
push()
: increase size of array s[]
by 1
.pop()
: decrease size of array s[]
by 1
.Too expensive!
Resizing by increasing size of array by 1
:
Challenge: Ensure that array resizing happens infrequently
Q: How to grow array?
A: If array is full, create a new array of twice the size, and copy items. (twice = repeated doubling)
public ResizingArrayStackOfStrings() { s = new String[1]; } public void push(String item) { if(N == s.length) resize(2 * s.length); s[N++] = item; } private void resize(int capacity) { String[] copy = new String[capacity]; for(int i = 0; i < N; i++) copy[i] = s[i]; s = copy; }
Array accesses to insert first \(N = 2^i\) items:
\[ \underbrace{N}_1 + \underbrace{(2+4+8+16+\ldots+N)}_k \sim 3N \]
Q: How to shrink array?
First try:
push()
: double size of s[]
when array is fullpop()
: halve size of array s[]
when array is one-half fullToo expensive in worst case
![]() ![]() ![]() ![]() |
push("to"); push("be"); push("or"); push("not"); push("be"); // N = 5 pop(); // N = 4 push("to"); // N = 5 pop(); // N = 4 |
Q: How to shrink array?
Efficient solution:
push()
: double size of array s[]
when array is fullpop()
: halve size of array s[]
when array is one-quarter fullpublic String pop() { String item = s[--N]; s[N] = null; if(N > 0 && N == s.length/4) resize(s.length/2); return item; }
Invariant: Array is between 25% and 100% full
Proposition: starting from an empty stack, any sequence of \(M\) push and pop operations takes time proportional to \(M\)
Order of growth of running time for resizing stack with \(N\) items:
operation | best | worst | amortized |
---|---|---|---|
construct |
\(1\) | \(1\) | \(1\) |
push |
\(1\) | \(N\) | \(1\) |
pop |
\(1\) | \(N\) | \(1\) |
size |
\(1\) | \(1\) | \(1\) |
\(N\) for push
and pop
because of doubling and halving operations.
Proposition: uses between \(\sim 8N\) and \(\sim 32N\) bytes to represent a stack with \(N\) items
public class ResizingArrayStackOfStrings { private String[] s; // 8 bytes * array size private int N = 0; // ... }
Remark: this accounts for the memory for the stack, but not the memory for strings themselves, which the client owns.
Tradeoffs: can implement a stack with either resizing array or linked list; client can use interchangeably. Which one is better?
Linked-list implementation
Resizing-array implementation
How to implement queue with linked list?
|
enqueue("it") enqueue("was") enqueue("the") enqueue("best") |
first
to first node in singly-linked listlast
to last nodefirst
last
public String dequeue() { String item = first.item; // save item to return first = first.next; // delete first node return item; // return saved item }
Remark: Identical code to linked-list stack pop
public String enqueue(String item) { Node oldlast = last; // save a link to the last node last = new Node(); // create a new node for end last.item = item; oldlast.next = last; // link new node to end of list }
public class LinkedQueueOfStrings { private Node first, last; private class Node { /* same as in LinkedStackOfStrings */ } public boolean isEmpty() { return first == null; } public void enqueue(String item) { Node oldlast = last; last = new Node(); last.item = item; last.next = null; if(isEmpty()) first = last; // handle special case else oldlast.next = last; // for empty queue } public String dequeue() { String item = first.item; first = first.next; if(isEmpty()) last = null; // handle special case... return item; } }
How to implement a fixed-capacity queue with an array?
D. Either B or C |
enqueue("it") enqueue("was") enqueue("the") enqueue("best") enqueue("times") |
q[]
to store items in queueenqueue
adds new item at q[tail]
dequeue
removes item from q[head]
head
and tail
modulo the capacity
Q: How to resize?
We implemented StackOfStrings
We also want StackOfURLs
, StackOfInts
, StackOfPancakes
, ...
Attempt 1: Implement a separate stack class for each type
Unfortunately, this was most reasonable approach in Java until 1.5!
We implemented StackOfStrings
We also want StackOfURLs
, StackOfInts
, StackOfPancakes
, ...
Attempt 2: Implement a stack with items of type Object
StackOfObjects s = new StackOfObjects(); Apple a = new Apple(); Banana b = new Banana(); s.push(a); s.push(b); a = (Apple) (s.pop()); // run-time error!
We implemented StackOfStrings
We also want StackOfURLs
, StackOfInts
, StackOfPancakes
, ...
Attempt 3: Java generics
Stack<Apple> s = new Stack<Apple>(); // ^^^^^ ^^^^^ type parameter Apple a = new Apple(); Banana b = new Banana(); s.push(a); s.push(b); // compile-time error a = s.pop();
Guiding principles: welcome compile-time errors; avoid run-time errors
public class LinkedStackOfStrings { private Node first = null; private class Node { // private inner class String item; // (access modifiers for instance Node next; // variables don't matter) } public boolean isEmpty() { return first == null; } public void push(String item) { Node oldfirst = first; first = new Node(); first.item = item; first.next = oldfirst; } public String pop() { String item = first.item; first = first.next; return item; } }
public class LinkedStack<Item> { private Node first = null; private class Node { // private inner class Item item; // (access modifiers for instance Node next; // variables don't matter) } public boolean isEmpty() { return first == null; } public void push(Item item) { Node oldfirst = first; first = new Node(); first.item = item; first.next = oldfirst; } public Item pop() { Item item = first.item; first = first.next; return item; } }
public class FixedCapacityStackOfStrings { private String[] s; private int N = 0; public FixedCapacityStackOfStrings(int capacity) { s = new String[capacity]; } public boolean isEmpty() { return N == 0; } public void push(String item) { s[N++] = item; } public String pop() { return s[--N]; } }
public class FixedCapacityStack<Item> { private Item[] s; private int N = 0; public FixedCapacityStackOfStrings(int capacity) { s = new Item[capacity]; // <- not allowed in Java :( } public boolean isEmpty() { return N == 0; } public void push(Item item) { s[N++] = item; } public Item pop() { return s[--N]; } }
Except line 6 (generic array creation) is not allowed in Java :(
public class FixedCapacityStack<Item> { private Item[] s; private int N = 0; public FixedCapacityStackOfStrings(int capacity) { s = (Item[]) new Object[capacity]; // ugly cast :( but works :| } public boolean isEmpty() { return N == 0; } public void push(Item item) { s[N++] = item; } public Item pop() { return s[--N]; } }
A (ugly) cast will "fix" the problem
May get a compiler warning
$ javac GenericArrays.java Note: GenericArrays.java uses unchecked or unsafe operations. Note: Recompile with -Xlint:unchecked for details. $ javac -Xlint:unchecked GenericArrays.java GenericArrays.java:6: warning: [unchecked] unchecked cast items = (Item[]) new Object[5]; ^ required: Item[] found: Object[] where Item is a type-variable: Item extends Object declared in class GenericArrays 1 warning
Q: Why does Java make me cast (or use reflection)?
Short answer: Backward compatibility.
Long answer: Need to learn about type erasure and covariant arrays (see this page for more details)
Note: creating a generic array where the generic type implements an interface requires using the interface, not Object
.
public class BrokenCollection<Item extends Comparable<Item>> { Item[] array; public Collection() { array = (Item[]) new Object[1]; // Runtime Exception :( } }
public class WorkingCollection<Item extends Comparable<Item>> { Item[] array; public Collection() { array = (Item[]) new Comparable[1]; // this works :) } }
Stack<String> llstack = new LinkedListStack<>(); Stack<String> astack = new ArrayStack<>(); // ^^^^^^^^ ^^ // only need to specify generic type once // these are still fine to do LinkedListStack<String> llstack2 = new LinkedListStack<>(); ArrayStack<String> astack2 = new ArrayStack<>(); // the following is error! Stack<String> stack = new Stack<>(); // ^^^^^ // cannot create instance of interface!
Q: What to do about primitive types? (int
, double
, boolean
, etc.)
Wrapper type
Integer
for int
, Double
for double
, Boolean
for boolean
, etc.Stack<Integer> s = new Stack<Integer>(); s.push(17); // s.push(Integer.valueOf(17)); int a = s.pop(); // int a = s.pop().intValue();
Bottom line: Client code can use generic stack for any type of data
Design challenge: Support iteration over stack items by client, without revealing the internal representation of the stack.
Java solution: Make stack implement the java.lang.Iterable
interface
Q: What is an Iterable
?
A: Has a method that returns an Iterator
.
Q: What is an Iterator
?
A: Has methods hasNext()
and next()
.
// java.lang.Iterable interface public interface Iterable<Item> { Iterator<Item> iterator(); } // java.util.Iterator interface public interface Iterator<Item> { boolean hasNext(); Item next(); void remove(); // optional; use at your own risk }
Q: Why make data structures Iterable
?
A: Java support elegant client code.
// "foreach" statement (shorthand) for(String s : stack) { StdOut.println(s); } // equivalent code (longhand) Iterator<String> i = stack.iterator(); while(i.hasNext()) { String s = i.next(); StdOut.println(s); }
Version 1: named Iterator
class for linked-list stack
import java.util.Iterator; // <- must import this! public class LinkedListStack<Item> implements Iterable<Item> { /* ... */ public Iterator<Item> iterator() { return new ListIterator(); } private class ListIterator implements Iterator<Item> { private Node<Item> current = first; public boolean hasNext() { return current != null; } public void remove() { /* not supported */ } public Item next() { Item item = current.item; current = current.next; return item; } } }
Version 2: unnamed Iterator
class for linked-list stack
import java.util.Iterator; // <- must import this! public class LinkedListStack<Item> implements Iterable<Item> { /* ... */ public Iterator<Item> iterator() { return new Iterator<Item>() { private Node<Item> current = first; public boolean hasNext() { return current != null; } public void remove() { /* not supported */ } public Item next() { Item item = current.item; current = current.next; return item; } }; } }
Version 1: named Iterator
class for array-based stack
import java.util.Iterator; // <- must import this! public class ArrayStack<Item> implements Iterable<Item> { /* ... */ public Iterator<Item> iterator() { return new ReverseArrayIterator(); } private class ReverseArrayIterator implements Iterator<Item> { private int i = N; public boolean hasNext() { return i > 0; } public void remove() { /* not supported */ } public Item next() { return s[--i]; } } }
Q: What if client modifies the data structure while iterating?
A: A fail-fast iterator throws a java.util.ConcurrentModificationException
.
// concurrent modification :( for(String s : stack) stack.push(s);
Q: How to detect?
A: Simple!
push()
and pop()
operations in Stack
Iterator
subclass upon creationnext()
and hasNext()
, the current count and saved count do not equal (client called push
or pop
), throw exceptionList interface: java.util.List
is API for a sequence of items
Implementations: java.util.ArrayList
uses resizing array; java.util.LinkedList
uses linked list.
Caveat: many operations available, but only some are efficient!
java.util.Stack
push()
, pop()
, and iterationjava.util.Vector
, which implements java.util.List
interface from previous slide, including get()
and remove()
Java 1.3 bug report (June 27, 2001)
“The iterator method on java.util.Stack iterates through a Stack from the bottom up. One would think that it should iterate as if it were popping off the top of the Stack.
”
Status (closed, will not fix)
“It was an incorrect design decision to have Stack extend Vector ("is-a" rather than "has-a"). We sympathize with the submitter but cannot fix this because of compatibility.
”
java.util.Stack
java.util.Queue
: An interface, not an implementation of a queue
Best practices: use implementations of Stack
, Queue
, and Bag
provided by algs4.jar
Generate random tile locations in \(N\)-by-\(N\) HexBoard
row
,col
at random; if already set, repeatjava.util.ArrayList
of \(N^2\) unset tile locations“Why is my program so slow?
”
Lesson: Don't use a library until you understand its API and implementation!
This course: Can't use a library until we've implemented it in class
How a compiler implements a function
Recursive function: Function that calls itself
Note: Can always use an explicit stack to remove recursion
Recursion example: greatest common divisor
static int gcd(int p, int q) { if(q == 0) return p; return gcd(q, p % q); }
gcd(216, 192) // stack: p=216, q=192 gcd(192, 24) // stack: 216, 192, p=192, q=24 gcd(24, 0) // stack: 216, 192, 192, 24, p=24, q=0 <- 24 // stack: 216, 192, 192, 24, ret=24 <- 24 // stack: 216, 192, ret=24 <- 24 // stack: ret=24
Goal: evaluate infix expressions
( 1 + ( ( 2 + 3 ) * ( 4 * 5 ) ) ) ^ operand ^ operator
Two-stack algorithm (E. W. Dijkstra)
Context: an interpreter!
public class Evaluate { public static void main(String[] args) { Stack<String> ops = new Stack<String>(); Stack<Double> vals = new Stack<Double>(); while(!StdIn.isEmpty()) { String s = StdIn.readString(); if (s.equals("(")) ; else if(s.equals("+")) ops.push(s); else if(s.equals("*")) ops.push(s); else if(s.equals(")")) { String op = ops.pop(); if (op.equals("+")) vals.push(vals.pop() + vals.pop()); else if(op.equals("*")) vals.push(vals.pop() * vals.pop()); } else vals.push(Double.parseDouble(s)); } StdOut.println(vals.pop()); } }
$ java Evaluate ( 1 + ( ( 2 + 3 ) * ( 4 * 5 ) ) ) 101.0
Q: Why correct?
A: When algorithm encounters an operator surrounded by two values within parentheses, it leaves the result on the value stack as if it was the original input.
input: ( 1 + ( ( 2 + 3 ) * ( 4 * 5 ) ) ) processing: ( 1 + ( ( 2 + 3 ) ... ( 1 + ( 5 * ( 4 * 5 ) ... ( 1 + ( 5 * 20 ) ... ( 1 + 100 ) output: 101
Extensions: More ops, precedence order, associativity
Observation 1: Dijkstra's two-stack algorithm computes the same value if the operator occurs after the two values
Observation 2: All the parentheses are redundant!
infix: ( 1 + ( ( 2 + 3 ) * ( 4 * 5 ) ) ) postfix: ( 1 ( ( 2 3 + ) ( 4 5 * ) * ) + ) no ()s: 1 2 3 + 4 5 * * + short: 1 2 3 + 4 5 * * +
Bottom line: Postfix or "reverse Polish" notation
Applications: Postscript, Forth, calculators, Java virtual machine, ...