Introduction
Stacks
The conceptual picture of a stack is something like this:
_______ _________Think of a stack of newspapers, or trays in a cafeteria. The only item that can be taken out (or even seen) is the most recently added item; a stack is a Last-In-First-Out (LIFO) data structure.
/ \ / \
values in \/ / \/
| ----- | values out
| |
| ----- |
| |
| ----- |
| |
| |
----------
Here are the stack operations:
OPERATION | DESCRIPTION |
Stack() | (constructor) create an empty stack |
boolean empty() | return true iff the stack is empty |
int size() | return the number of items in the stack |
void push(Object ob) | add ob to the top of the stack |
Object pop() | remove and return the item from the top of the stack (error if the stack is empty) |
Object peek() | return the item that is on the top of the stack, but do not remove it (error if the stack is empty) |
Queues
The conceptual picture of a queue is something like this:
------------------Think of people standing in line. A queue is a First-In-First-Out (FIFO) data structure. Items can only be added at the rear of the queue, and the only item that can be removed is the one at the front of the queue.
values in ----> items in the queue ----> values out
------------------
^ ^
| |
this is the rear of this is the front of
the queue the queue
Here are the queue operations:
OPERATION | DESCRIPTION |
Queue() | (constructor) create an empty queue |
boolean empty() | return true iff the queue is empty |
int size() | return the number of items in the queue |
void enqueue(Object ob) | add ob to the rear of the queue |
Object dequeue() | remove and return the item from the front of the queue (error if the queue is empty) |
Implementing Stacks
Array Implementation
public class Stack {
// *** fields ***
private static final int INITSIZE = 10; // initial array size
private Object[] items; // the items in the stack
private int numItems; // the number of items in the stack
//*** methods ***
// constructor
public Stack() { ... }
// add items
public void push(Object ob) { ... }
// remove items
public Object pop() throws EmptyStackException { ... }
// other methods
public Object peek() throws EmptyStackException { ... }
public int size() { ... }
public boolean empty() { ... }
}
Write the Stack constructor.
The push method is like the version of the List add method that adds an object to the end of the list (because items are always pushed onto the top of the stack). Note that it is up to us as the designers of the Stack class to decide which end of the array corresponds to the top of the stack. We could choose always to add items at the beginning of the array, or always to add items at the end of the array. However, it is clearly not a good idea to add items at the beginning of the array since that requires moving all existing items; i.e., that choice would make push be O(N) (where N is the number of items in the stack). If we add items at the end of the array, then push is O(1), except when the array is full.
Here are before and after pictures, illustrating the effects of a call to push:
And here's the code for the push method:
public void push(Object ob) {
if (items.length == numItems) expandArray();
items[numItems] = ob;
numItems++;
}
The pop method needs to remove the top-of-stack item, and return it, as illustrated below.
Note that, in the picture, the value "bbb" is still in items[2]; however, that value is no longer in the stack because numItems is 2 (which means that items[1] is the last item in the stack).
Complete the pop method, using the following header
public Object pop() throws EmptyStackException {
}
The peek method is very similar to the pop method, except that it only returns the top-of-stack value without changing the stack. The size method simply returns the value of numItems, and the empty method returns true iff numItems is zero.
Fill in the following table, using Big-O notation to give the worst and average-case times for each of the stack methods for a stack of size N.
OPERATION | WORST-CASE TIME | AVERAGE-CASE TIME |
constructor | ||
empty | ||
size | ||
push | ||
pop | ||
peek |
Linked-list Implementation
To implement a stack using a linked list, we must first define the Listnode class. The Listnode definition is the same one we used for the linked-list implementation of the List class.
The signatures of the methods of the Stack class are independent of whether the stack is implemented using an array or using a linked list; only the type of the items field needs to change:
private Listnode items; // pointer to the linked list of items in the stack
As discussed above, an important property of stacks is that items are only pushed and popped at one end (the top of the stack). If we implement a stack using a linked list, we can choose which end of the list corresponds to the top of the stack. It is easiest and most efficient to add and remove items at the front of a linked list, therefore, we will choose the front of the list as the top of the stack (i.e., the items field will be a pointer to the node that contains the top-of-stack item). Below is a picture of a stack represented using a linked list; in this case, items have been pushed in alphabetical order, so "cc" is at the top of the stack:
Notice that, in the picture, the top of stack is to the left (at the front of the list), while for the array implementation, the top of stack was to the right (at the end of the array).
Let's consider how to write the pop method. It will need to perform the following steps:
- Check whether the stack is empty; if so, throw an EmptyStackException.
- Remove the first node from the list by setting items = items.next.
- Decrement numItems.
- Return the value that was in the first node in the list.
public Object pop() throws EmptyStackException {
if (empty()) throw new EmptyStackException(); // step 1
Object tmp = items.getData(); // step 2(a)
items = items.getNext(); // step 2(b)
numItems--; // step 3
return tmp; // step 4
}
Now let's consider the push method. Here are before and after pictures, illustrating the effect of a call to push when the stack is implemented using a linked list:
The steps that need to be performed are:
- Create a new node whose data field contains the object to be pushed and whose next field contains a pointer to the first node in the list (or null if the list is empty). Note that the value for the next field of the new node is the value in the Stack's items field.
- Change items to point to the new node.
- Increment numItems.
Complete the push method, using the following header.
public void push(Object ob) {solution
}
The remaining methods (the constructor, peek, size, and empty) are quite straightforward. You should be able to implement them without any major problems.
Fill in the following table, using Big-O notation to give the worst-case times for each of the stack methods for a stack of size N, assuming a linked-list implementation. Look back at the table you filled in for the array implementation. How do the times compare? What are the advantages and disadvantages of using an array vs using a linked list to implement the Stack class?
OPERATION | WORST-CASE TIME |
constructor | |
empty | |
size | |
push | |
pop | |
peek |
Implementing Queues
Array Implementation
public class Queue {We could implement enqueue by adding the new item at the end of the array and implement dequeue by saving the first item in the array, moving all other items one place to the left, and returning the saved value. The problem with this approach is that, although the enqueue operation is efficient, the dequeue operation is not -- it requires time proportional to the number of items in the queue.
// *** fields ***
private static final int INITSIZE = 10; // initial array size
private Object[] items; // the items in the queue
private int numItems; // the number of items in the queue
//*** methods ***
// constructor
public Queue() { ... }
// add items
public void enqueue(Object ob) { ... }
// remove items
public Object dequeue() throws EmptyQueueException { ... }
// other methods
public int size() { ... }
public boolean empty() { ... }
}
public void enqueue(Object ob) {
// check for full array and expand if necessary
if (items.length == numItems) {
// code missing here
}
// use auxiliary method to increment rear index with wraparound
rearIndex = incrementIndex(rearIndex);
// insert new item at rear of queue
items[rearIndex] = ob;
numItems++;
}
private int incrementIndex(int index) {
if (index == items.length-1) return 0;
else return index + 1;
}
To see why we can't simply use expandArray when the array is full, consider the picture shown below.
The steps that need to be carried out when the array is full are:
- Allocate a new array of twice the size.
- Copy the values in the range items[frontIndex] to items[items.length-1] into the new array (starting at position frontIndex in the new array).
- Copy the values in the range items[0] to items[rearIndex] into the new array (starting at position items.length in the new array). Note: if the front of the queue was in items[0], then all of the values were copied by step 2, so this step is not needed.
- Set items to point to the new array.
- Fix the value of rearIndex.
And here's the final code for enqueue:
publc void enqueue(Object ob) {
// check for full array and expand if necessary
if (items.length == numItems) {
Object[] tmp = new Object[items.length*2];
System.arraycopy(items, frontIndex, tmp, frontIndex,
items.length-frontIndex);
if (frontIndex != 0) {
System.arraycopy(items, 0, tmp, items.length, frontIndex);
}
items = tmp;
rearIndex = frontIndex + numItems - 1;
}
// use auxiliary method to increment rear index with wraparound
rearIndex = incrementIndex(rearIndex);
// insert new item at rear of queue
items[rearIndex] = ob;
numItems++;
}
Linked-list Implementation
The class definition is the same as for the array implementation, except for the fields:
// *** fields ***
private Listnode qFront; // pointer to the front of the queue
// (the first node in the list)
private Listnode qRear; // pointer to the rear of the queue
// (the last node in the list)
private int numItems; // the number of items in the queue
Here's a picture of a Queue with three items, aa, bb, cc, with aa at the front of the queue:
Comparison of Array and Linked-List Implementations
- In the linked-list implementation, one pointer must be stored for every item in the stack/queue, while the array stores only the items themselves.
- On the other hand, the space used for a linked list is always proportional to the number of items in the list. This is not necessarily true for the array implementation as described: if a lot of items are added to a stack/queue and then removed, the size of the array can be arbitrarily greater than the number of items in the stack/queue. However, we could fix this problem by modifying the pop/dequeue operations to shrink the array when it becomes too empty.
- For the array implementation, the worst-case times for the push and enqueue methods are O(N), for a stack/queue with N items. This is because when the arary is full, a new array must be allocated and the values must be copied. For the linked-list implementation, push and enqueue are always O(1).
Applications of Stacks and Queues
Queues are useful for many simulations, and are also used for some operations on graphs and trees.
Complete method reverseQ, whose header is given below. Method reverseQ should use a Stack to reverse the order of the items in its Queue parameter.
public static void reverseQ(Queue q) {
// precondition: q contains x1 x2 ... xN (with x1 at the front)
// postcondition: q contains xN ... x2 X1 (with xN at the front)
}
Answers to Self-Study Questions
public Stack() {
items = new Object[INITSIZE];
numItems = 0;
}
public Object pop() throws EmptyStackException {
if (numItems == 0) throw new EmptyStackException();
else {
numItems--;
return items[numItems];
}
}
OPERATION | WORST-CASE TIME | AVERAGE-CASE TIME |
constructor | O(1) | O(1) |
empty | O(1) | O(1) |
size | O(1) | O(1) |
push | O(N) | O(1) |
pop | O(1) | O(1) |
peek | O(1) | O(1) |
public void push(Object ob) {
items = new Listnode(ob, items);
numItems++;
}
OPERATION | WORST-CASE TIME |
constructor | O(1) |
empty | O(1) |
size | O(1) |
push | O(1) |
pop | O(1) |
peek | O(1) |
public static void reverseQ(Queue q) {
// precondition: q contains x1 x2 ... xN (with x1 at the front)
// postcondition: q contains xN ... x2 X1 (with xN at the front)
Stack s = new Stack();
while (!q.empty()) s.push(q.dequeue());
while (!s.empty()) q.enqueue(s.pop());
}
Labels: Stacks and Queues
0 comments:
Post a Comment