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:
Warmup API: stack of strings data type
![]() |
![]() |
Warmup client: reverse sequence of strings from standard input
A. Can't be done efficiently with a singly-linked list.
B.
C.
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 \(\texttilde 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.
A. Can't be done efficiently with an array?
B.
C.
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+\ldots+N)}_k \texttilde 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
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 \(\texttilde 8N\) and \(\texttilde 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
A. Can't be done efficiently with a singly-linked list?
B.
C.
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.time first = first.next; if(isEmpty()) last = null; // handle special case... return item; } }
A. Can't be done efficiently with an array.
B.
C.
A. Can't be done efficiently with an array.
B.
C.
D. Either B or C
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
, StackOfVans
, ...
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
, StackOfVans
, ...
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
, StackOfVans
, ...
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 :( } 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.
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
import java.util.Iterator; // <- must import this! public class Stack<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
import java.util.Iterator; // <- must import this! public class Stack<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
public class Stack<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: only some operations 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 provided implementations of Stack
, Queue
, and Bag
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!
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!
infix expression (fully parenthesized)
infix expression (fully parenthesized)
infix expression (fully parenthesized)
infix expression (fully parenthesized)
infix expression (fully parenthesized)
infix expression (fully parenthesized)
infix expression (fully parenthesized)
infix expression (fully parenthesized)
infix expression (fully parenthesized)
infix expression (fully parenthesized)
infix expression (fully parenthesized)
infix expression (fully parenthesized)
infix expression (fully parenthesized)
infix expression (fully parenthesized)
infix expression (fully parenthesized)
infix expression (fully parenthesized)
infix expression (fully parenthesized)
infix expression (fully parenthesized)
infix expression (fully parenthesized)
infix expression (fully parenthesized)
infix expression (fully parenthesized)
infix expression (fully parenthesized)
infix expression (fully parenthesized)
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, ...