260 likes | 401 Views
Implementing Stacks & Queues. Outline. ADT Stacks Basic operations Examples of use Implementations Array-based and linked list-based ADT Queues Basic operations Examples of use Implementations Array-based and linked list-based Stack Applications Balanced Symbol Checker
E N D
Outline • ADT Stacks • Basic operations • Examples of use • Implementations • Array-based and linked list-based • ADT Queues • Basic operations • Examples of use • Implementations • Array-based and linked list-based • Stack Applications • Balanced Symbol Checker • Postfix Machines • Summary
Linear Data Structures • A collection of components that are arranged along one dimension, i.e in a straight line, or linearly. • Stack: a linear data structure where access is restricted to the most recently inserted item. • Queue: a linear data structure where access is restricted to the least recently inserted item. • Both of these abstract data types can be implemented at a lower level using a list: either an array or a linked list.
Stack • The last item added is pushed (added) to the stack. • The last item added can be popped (removed) from the stack. • The last item added can be topped (accessed) from the stack. • These operations all take constant time: O(1). A typical stack interface: void push(Thing newThing); void pop(); Thing top();
Stack Implementation: Array • A stack can be implemented as an array A and an integer top that records the index of the top of the stack. • For an empty stack, set topto -1. • When push(X) is called, increment top, and write X to A[top]. • When pop() is called, decrement top. • When top() is called, return A[top].
Push A Push B B top(1) A top(0) A top(-1) Example MyStack myStack = new MyStack(); myStack.push(A); myStack.push(B);
Array Doubling • When array-based stack is constructed, instantiate an array with a “default” size. • When the array underlying the stack is full (not the stack itself!), we can increase the array through array doubling. • Allocate a new array twice the size, and copy the old array to the first half of the new array: Thing[] newA = new Thing[oldA.length*2]; for(int ii=0; ii<oldA.length; ii++) newA[ii] = oldA[ii]; oldA = newA;
Running Time • Without array doubling, all stack operations take constant time – O(1). • With array doubling, push() may be O(N), but this happens quite rarely: array doubling due to data size N must be preceded by N/2push() non-doubling calls. Effectively, still constant time Amortization.
Stack Implementation: Array public class MyArrayStack<T> { private T[] array; private int topOfStack; private static final int DEFAULT_CAPACITY = 10; public MyArrayStack() … public boolean isEmpty() … public void makeEmpty() … public T top() … public void pop() … public T topAndPop() … public void push(T x) … private void doubleArray() … }
d c b a Stack Implementation: Linked List • First item in list = top of stack (if empty: null) • push(Thing x): • Create a new node containing x • Insert it as the first element • pop(): • Delete first item (i.e. move “top” to the second item) • top(): • Return the data of the first element topOfStack
Stack Implementation: Linked List public class MyLinkedListStack<T> { private ListNode<T> topOfStack; public MyLinkedListStack() … public boolean isEmpty() … public void makeEmpty() … public T top() … public void pop() … public T topAndPop() … public void push(T x) … }
Queue • Last item added is enqueued (added) to the back. • First item added is dequeued (removed) from the front. • First item added can be accessed: getFront. • These operations all take constant time – O(1). A typical queue interface: void enqueue(Thing newThing); void dequeue(); Thing getFront();
Y X X Y X Z Z Y back back back back Queue Implementation: Simple Idea • Store items in an array A • Maintain index: back • Front of queue = A[0] • Back of queue = A[back] • Enqueue is easy & fast: store at A[back], back++ • Dequeue is inefficient: A[1] to A[back] needs to be shifted (and back--) O(N) enqueue(X) enqueue(Y) enqueue(Z) dequeue()
Z X Y Z Y Z back back front front front back Queue Implementation: Better Idea • Add another index: front, which records the front of the queue • Dequeue is now done by incrementing front • Both enqueue and dequeue are now O(1). • But what happens if enqueue and dequeue array.length-1 items? enqueue(X) enqueue(Y) enqueue(Z) dequeue() dequeue()
Queue Implementation: “Circular” Array • After the array.length-1-th item is enqueued, the underlying array is full, even though the queue is not logically, it should be (almost?) empty. • Solution: wraparound • Re-use cells at beginning of array that are ‘empty’ due to dequeue. • When either front or back is incremented and points “outside array” (≥array.length), reset to 0.
front back back front back back front back front back front front Q T R Q P P Q R S T R S T R R Q T P S T S S front back Circular Example • Both front and back indexes “wraparound” the array. Think of the array as a circle…
Java Implementation • Fairly straightforward. Basically, maintain • Front • Back • Number of items in queue • When is the underlying array really full? • How do we do array doubling?
Queue Implementation: Array public class MyArrayQueue<T> { private T[] array; private int front,back,currentSize; private static final int DEFAULT_CAPACITY = 10; public MyArrayQueue() … public boolean isEmpty() … public void makeEmpty() … public T getFront() … public void dequeue() … public T getFrontAndDequeue() … public void enqueue(T x) … private void doubleQueue() … private int increment(int x) … }
a b c d Queue Implementation: Linked List • Maintain 2 node references: front & back • An empty queue: front = back = null. • enqueue(Thing X): • Create a new node N containing X • If queue empty: front = back = N • Else append N and update back • dequeue(): • Delete first item (referenced by front) • getFront(): • Return data of first element front back
Queue Implementation: Linked List public class MyLinkedListQueue<T> { private ListNode<T> front; private ListNode<T> back; public MyLinkedListQueue() … public boolean isEmpty() … public void makeEmpty() … public T getFront() … public void dequeue() … public T getFrontAndDequeue() … public void enqueue(T x) … }
Stack Application: Balanced Symbols • Java uses pairs of symbols to denote blocks: • ( and ) • { and } • [ and ] • An opening symbol must be matched with a closing symbol: • { [ ( ) ] } is OK • { ( [ ) ] } is NOT OK • A closing symbol, e.g. ‘)’, must match the most recently seen opening symbol, e.g. ‘(‘. • This can be done using a stack.
Balanced Symbol Checker Algorithm • Make an empty stack • Read symbols: • If symbol is an opening symbol, push onto stack. • If symbol is a closing symbol: • If stack is empty, report error! • Else, (top and) pop stack. If popped symbol is not a matching opening symbol, report error! • At the end, if the stack is not empty, report error!
Stack Application: Simple Calculator • Say we are given an arithmetic expression, and we are asked to evaluate it. • Typically, we use infix expressions, of the formoperand1 OPERATOR operand2 • Example: • 1 + 2 • 4 * 3 • To process infix expressions we need precedence and associativity: • 1 + 2 * 3 • 10 - 4 - 3 • 2 ^ 3 ^ 3 • 1 - 2 - 4 ^ 5 * 3 * 6 / 7 ^ 2 ^ 2 • We use brackets: (1-2)-((((4^5)*3)*6)/(7^(2^2)))
Postfix Notation • Typically, we use postfix expressions, of the formoperand1 operand2 OPERATOR • Example: • 1 2 + • 4 3 * • With postfix expressions, precedence is unambiguous: • 1 - 2 - 4 ^ 5 * 3 * 6 / 7 ^ 2 ^ 2, or • (1-2)-((((4^5)*3)*6)/(7^(2^2))), becomes • 1 2 - 4 5 ^ 3 * 6 * 7 2 2 ^ ^ / -
Postfix Machines • A postfix machine evaluates a postfix expression: • If an operand is seen, push it onto stack • If an operator is seen, pop appropriate number of operands, evaluate operator, push result onto stack • When the complete postfix expression is evaluated, the stack should contain exactly one item: the result.
Summary • Both versions, array and linked-list, run in O(1) • Linked-list implementation requires extra overhead due to next reference at each node • (Circular) array implementation of queues can be quite tricky • Array space doubling needs memory at least 3x size of actual data.