Steven S. Skiena
Stacks and Queues
The first data structures we will study this semester will be lists which have the property that the order in which the items are used is determined by the order they arrive.
Queues might seem fairer, which is why lines at stores are organized as queues instead of stacks, but both have important applications in programs as a data structure.
Operations on Stacks
The terminology associated with stacks comes from the spring loaded plate containers common in dining halls.
When a new plate is washed it is pushed on the stack.
When someone is hungry, a clean plate is popped off the stack.
A stack is an appropriate data structure for this task since the plates don't care about when they are used!
Maintaining Procedure Calls
Stacks are used to maintain the return points when Modula-3 procedures call other procedures which call other procedures ...
Jacob and Esau
In the biblical story, Jacob and Esau were twin brothers where Esau was born first and thus inherited Issac's birthright. However, Jacob got Esau to give it away for a bowl of soup, and so Jacob went to become a patriarch of Israel.
But why was Jacob justified in so tricking his brother???
Rashi, a famous 11th century Jewish commentator, explained the problem by saying Jacob was conceived first, then Esau second, and Jacob could not get around the narrow tube to assume his rightful place first in line!
Abstract Operations on a Stack
Defining these abstract operations lets us build a stack module to use and reuse without knowing the details of the implementation.
The easiest implementation uses an array with an index variable to represent the top of the stack.
An alternative implementation, using linked lists is sometimes better, for it can't ever overflow. Note that we can change the implementations without the rest of the program knowing!
Declarations for a stack
INTERFACE Stack; (*14.07.94 RM, LB*) (* Stack of integer elements *) TYPE ET = INTEGER; (*element type*) PROCEDURE Push(elem : ET); (*adds element to top of stack*) PROCEDURE Pop(): ET; (*removes and returns top element*) PROCEDURE Empty(): BOOLEAN; (*returns true if stack is empty*) PROCEDURE Full(): BOOLEAN; (*returns true if stack is full*) END Stack.
MODULE Stack; (*14.07.94 RM, LB*) (* Implementation of an integer stack *) CONST Max = 8; (*maximum number of elements on stack*) TYPE S = RECORD info: ARRAY [1 .. Max] OF ET; top: CARDINAL := 0; (*initialize stack to empty*) END; (*S*) VAR stack: S; (*instance of stack*) PROCEDURE Push(elem:ET) = (*adds element to top of stack*) BEGIN INC(stack.top); stack.info[stack.top]:= elem END Push; PROCEDURE Pop(): ET = (*removes and returns top element*) BEGIN DEC(stack.top); RETURN stack.info[stack.top + 1] END Pop; PROCEDURE Empty(): BOOLEAN = (*returns true if stack is empty*) BEGIN RETURN stack.top = 0 END Empty; PROCEDURE Full(): BOOLEAN = (*returns true if stack is full*) BEGIN RETURN stack.top = Max END Full; BEGIN END Stack.
Using the Stack Type
MODULE StackUser EXPORTS Main; (*14.02.95. LB*) (* Example client of the integer stack *) FROM Stack IMPORT Push, Pop, Empty, Full; FROM SIO IMPORT Error, GetInt, PutInt, PutText, Nl; <* FATAL Error *> (*suppress warning*) BEGIN PutText("Stack User. Please enter numbers:\n"); WHILE NOT Full() DO Push(GetInt()) (*add entered number to stack*) END; WHILE NOT Empty() DO PutInt(Pop()) (*remove number from stack and return it*) END; Nl(); END StackUser.
Queues are more difficult to implement than stacks, because action happens at both ends.
The easiest implementation uses an array, adds elements at one end, and moves all elements when something is taken off the queue.
It is very wasteful moving all the elements on each DEQUEUE. Can we do better?
More Efficient Queues
Suppose that we maintaining pointers to the first (head) and last (tail) elements in the array/queue?
Note that there is no reason to explicitly clear previously unused cells.
Now both ENQUEUE and DEQUEUE are fast, but they are wasteful of space. We need a array bigger than the total number of ENQUEUEs, instead of the maximum number of items stored at a particular time.
Circular queues let us reuse empty space!
Note that the pointer to the front of the list is now behind the back pointer!
When the queue is full, the two pointers point to neighboring elements.
There are lots of possible ways to adjust the pointers for circular queues. All are tricky!
How do you distinguish full from empty queues, since their pointer positions might be identical? The easiest way to distinguish full from empty is with a counter of how many elements are in the queue.
FIFO Queue Interface
INTERFACE Fifo; (*14.07.94 RM, LB*) (* A queue of text elements *) TYPE ET = TEXT; (*element type*) PROCEDURE Enqueue(elem:ET); (*adds element to end*) PROCEDURE Dequeue(): ET; (*removes and returns first element*) PROCEDURE Empty(): BOOLEAN; (*returns true if queue is empty*) PROCEDURE Full(): BOOLEAN; (*returns true if queue is full*) END Fifo.
Priority Queue Implementation
MODULE Fifo; (*14.07.94 RM, LB*) (* Implementation of a fifo queue of text elements *) CONST Max = 8; (*Maximum number of elements in FIFO queue*) TYPE Fifo = RECORD info: ARRAY [0 .. Max - 1] OF ET; in, out, n: CARDINAL := 0; END; (*Fifo*) VAR w: Fifo; (*contains a FIFO queue*) PROCEDURE Enqueue(elem:ET) = (*adds element to end*) BEGIN w.info[w.in]:= elem; (*stores new element*) w.in:= (w.in + 1) MOD Max; (*increments in-pointer in ring*) INC(w.n); (*increments number of stored elements*) END Enqueue; PROCEDURE Dequeue(): ET = (*removes and returns first element*) VAR e: ET; BEGIN e:= w.info[w.out]; (*removes oldest element*) w.out:= (w.out + 1) MOD Max; (*increments out-pointer in ring*) DEC(w.n); (*decrements number of stored elements*) RETURN e; (*returns the read element*) END Dequeue;
PROCEDURE Empty(): BOOLEAN = (*returns true if queue is empty*) BEGIN RETURN w.n = 0; END Empty; PROCEDURE Full(): BOOLEAN = (*returns true if queue is full*) BEGIN RETURN w.n = Max END Full; BEGIN END Fifo.
MODULE FifoUser EXPORTS Main; (*14.07.94. LB*) (* Example client of the text queue. *) FROM Fifo IMPORT Enqueue, Dequeue, Empty, Full; (* operations of the queue *) FROM SIO IMPORT Error, GetText, PutText, Nl; <* FATAL Error *> (*supress warning*) BEGIN PutText("FIFO User. Please enter texts:\n"); WHILE NOT Full() DO Enqueue(GetText()) END; WHILE NOT Empty() DO PutText(Dequeue() & " ") END; Nl(); END FifoUser.
Double-ended queues - These are data structures which support both push and pop and enqueue/dequeue operations.
Priority Queues(heaps) - Supports insertions and ``remove minimum'' operations which useful in simulations to maintain a queue of time events.
We will discuss simulations in a future class.