0% found this document useful (0 votes)
37 views

36 ImplementingQueues

The document discusses implementing queue data structures in C++. It describes two common strategies: using an array or using a linked list to store elements. It focuses on implementing the Queue class using these strategies. The key methods for a queue are enqueue to add an element to the end, and dequeue to remove an element from the front. Using a simple array makes enqueue very fast, but dequeue slow, as other elements must be moved. The document will discuss how to fix this inefficient dequeue operation.

Uploaded by

Mary Ann Abud
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
37 views

36 ImplementingQueues

The document discusses implementing queue data structures in C++. It describes two common strategies: using an array or using a linked list to store elements. It focuses on implementing the Queue class using these strategies. The key methods for a queue are enqueue to add an element to the end, and dequeue to remove an element from the front. Using a simple array makes enqueue very fast, but dequeue slow, as other elements must be moved. The document will discuss how to fix this inefficient dequeue operation.

Uploaded by

Mary Ann Abud
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 4

Eric Roberts Handout #36

CS 106B February 13, 2013


Implementing Queues

Outline
•• Chapter 14 presents complete implementations for three of the
Implementing Queues ADTs you’’ve been using since the second assignment: stacks,
queues, and vectors. We described one implementation of the
Stack class in Monday’’s lecture.
•• The reader presents two different strategies to represent the
Stack and Queue classes: one that uses an array to store the
elements and one that uses a linked list. For each of these
strategies, implementing a Stack turns out to be much easier.
•• In today’’s lecture, I am going to focus on implementing the
Queue class, leaving the details of implementing the Stack
Eric Roberts and Vector classes to the text.
CS 106B
February 13, 2013

Methods in the Queue<x> Class The queue.h Interface


/*
* File: queue.h
queue.size() * -------------
Returns the number of values in the queue. * This interface defines a general queue abstraction that uses
* templates so that it can work with any element type.
queue.isEmpty() */
Returns true if the queue is empty.
#ifndef _queue_h
queue.enqueue(value) #define _queue_h
Adds a new value to the end of the queue (which is called its tail). /*
* Template class: Queue<ValueType>
queue.dequeue() * --------------------------------
Removes and returns the value at the front of the queue (which is called its head). * This class template models a queue, which is a linear collection
* of values that resemble a waiting line. Values are added at
queue.peek() * one end of the queue and removed from the other. The fundamental
* operations are enqueue (add to the tail of the queue) and dequeue
Returns the value at the head of the queue without removing it. * (remove from the head of the queue). Because a queue preserves
* the order of the elements, the first value enqueued is the first
queue.clear() * value dequeued. Queues therefore operate in a first-in-first-out
Removes all values from the queue. * (FIFO) order. For maximum generality, the Queue class is defined
* using a template that allows the client to define a queue that
* contains any type of value, as in Queue<string> or Queue<taskT>.
*/

The queue.h Interface The queue.h Interface


template <typename ValueType> /*
class Queue { * Method: size
* Usage: int nElems = queue.size();
public: * ---------------------------------
/* * Returns the number of elements in this queue.
* Constructor: Queue */
* Usage: Queue<ValueType> queue; int size();
* ------------------------------
* Initializes a new empty queue containing the specified value type. /* . . . */
*/
bool isEmpty();
Queue();
/* . . . */
/*
* Destructor: ~Queue void clear();
* ------------------- /* . . . */
* Deallocates any heap storage associated with this queue.
*/ void enqueue(ValueType value);
~Queue(); /* . . . */
ValueType dequeue();
/* . . . */
ValueType peek();
–2–

The queue.h Interface An Overly Simple Strategy


/* Private section */
•• The most straightforward way to represent the elements of a
Private section goes here. queue is to store the elements in an array, exactly as in the
Stack class from Monday.
};

/*
•• Given this representation, the enqueue operation is extremely
* The template feature of C++ works correctly only if the compiler simple to implement. All you need to do is add the element to
* has access to both the interface and the implementation at the
* same time. As a result, the compiler must see the code for the end of the array and increment the element count. That
* the implementation at this point, even though that code is
* not something that the client needs to see in the interface.
operation runs in O(1) time.
* Using the #include facility of the C++ preprocessor allows the
* compiler to have access to the code without forcing the client •• The problem with this simplistic approach is that the dequeue
* to wade through the details.
*/ operation requires removing the element from the beginning
of the array. If you’’re relying on the same strategy you used
Implementation section goes here. in the array-based editor, implementing this operation requires
#endif
moving all the remaining elements to fill the hole left by the
dequeued element. That operation therefore takes O(N) time.

Fixing the O(N) Problem Tracing the Array-Based Queue


•• The key to fixing the problem of having dequeue take O(N) •• Consider what happens if you Queue<char> queue;
time is to eliminate the need for any data motion by keeping execute the operations shown. queue.enqueue('A')
track of two indices: one to mark the head of the queue and queue.enqueue('B')
another to mark the tail. •• Each enqueue operation adds queue.enqueue('C')
queue.dequeue()  'A'
a value at the tail of the queue. queue.enqueue('D')
•• Given these two indices, the enqueue operation stores the new queue.enqueue('E')
element at the position marked by the tail index and then •• Each dequeue operation takes queue.dequeue()  'B'
increments tail so that the next element is enqueued into the its result from the head. queue.enqueue('F')
queue.dequeue()  'C'
next slot. The dequeue operation is symmetric. The next •• If you continue on in this way, queue.dequeue()  'D'
value to be dequeued appears at the array position marked by what happens when you reach queue.enqueue('G')
the head index. Removing it is then simply a matter of the end of the array space? queue.enqueue('H')
 queue.enqueue('I')
incrementing head.
•• Unfortunately, this strategy typically ends up filling the array elements 1000
space even when the queue itself contains very few elements, head 23014 E F G H
as illustrated on the next slide. tail 23456810
0 1 2 3 4 5 6 7
capacity 8

Tracing the Array-Based Queue Implementing the Ring-Buffer Strategy


•• At this point, enqueuing the I Queue<char> queue; •• The data structure described on the preceding slide is called a
would require expanding the queue.enqueue('A') ring buffer because the end of the array is linked back to the
array, even though the queue queue.enqueue('B') beginning.
queue.enqueue('C')
contains only four elements. queue.dequeue()  'A'
queue.enqueue('D')
•• The arithmetic operations necessary to implement the ring
•• The solution to this problem is queue.enqueue('E') buffer strategy are easy to code using modular arithmetic,
to let the elements cycle back queue.dequeue()  'B' which is simply normal arithmetic in which all values are
to the beginning of the array. queue.enqueue('F') reduced to a specific range by dividing each result by some
queue.dequeue()  'C'
queue.dequeue()  'D' constant (in this case, the capacity of the array) and taking the
queue.enqueue('G') remainder. In C++, you can use the % operator to implement
queue.enqueue('H') modular arithmetic.
queue.enqueue('I')
•• When you are using the ring-buffer strategy, it is typically
elements 1000 necessary to expand the queue when there is still one free
head 23014 I E F G H element left in the array. If you don’’t do so, the simple test for
tail 2345610
0 1 2 3 4 5 6 7
an empty queue——whether head is equal to tail——fails
capacity 8 because that would also be true in a completely full queue.
–3–

Structure for the Array-Based Queue Code for the Ring-Buffer Queue
/* /* Implementation section */
* Implementation notes
* -------------------- /*
* The array-based queue stores the elements in a ring buffer, which * Implementation notes: Queue constructor
* consists of a dynamic index and two indices: head and tail. Each * ---------------------------------------
* index wraps to the beginning if necessary. This design requires * The constructor must allocate the array storage for the queue
* that there is always one unused element in the array. If the queue * elements and initialize the fields of the object.
*/
* were allowed to fill completely, the head and tail indices would
* have the same value, and the queue will appear empty. template <typename ValueType>
*/ Queue<ValueType>::Queue() {
capacity = INITIAL_CAPACITY;
private: array = new ValueType[capacity];
head = 0;
static const int INITIAL_CAPACITY = 10; tail = 0;
}
/* Instance variables */
/*
ValueType *array; /* A dynamic array of the elements */ * Implementation notes: ~Queue destructor
int capacity; /* The allocated size of the array */ * ---------------------------------------
int head; /* The index of the head element */ * The destructor frees any memory that is allocated by the implementation.
int tail; /* The index of the tail element */ */
template <typename ValueType>
/* Private method prototypes */
Queue<ValueType>::~Queue() {
void expandCapacity(); delete[] array;
}

Code for the Ring-Buffer Queue Code for the Ring-Buffer Queue
/* Implementation section */
/* /*
* Implementation notes: size * Implementation notes: clear
/** -------------------------- * ---------------------------
** Implementation
The size of thenotes:
queue Queue
can beconstructor
calculated from the head and tail * The clear method need not take account of where in the
** ---------------------------------------
indices by using modular arithmetic. * ring buffer any existing data is stored and can simply
**/The constructor must allocate the array storage for the queue * set the head and tail index back to the beginning.
* elements and initialize the fields of the object. */
template <typename ValueType>
*/
int Queue<ValueType>::size() { template <typename ValueType>
return<typename
template (tail + capacity - head) % capacity;
ValueType> void Queue<ValueType>::clear() {
}
Queue<ValueType>::Queue() { head = tail = 0;
capacity = INITIAL_CAPACITY; }
/* array = new ValueType[capacity];
* head
Implementation
= 0; notes: isEmpty /*
* tail
-----------------------------
= 0; * Implementation notes: enqueue
} * The queue is empty whenever the head and tail indices are * -----------------------------
* equal. Note that this interpretation means that the queue * This method must first check to see whether there is
/** cannot be allowed to fill the capacity entirely and must * enough room for the element and expand the array storage
** Implementation
always leave one unused
notes: space.
~Queue destructor * if necessary.
**/--------------------------------------- */
* The destructor frees any memory that is allocated by the implementation.
template
*/ <typename ValueType> template <typename ValueType>
bool Queue<ValueType>::isEmpty() { void Queue<ValueType>::enqueue(ValueType value) {
return<typename
template head == tail;
ValueType> if (size() == capacity - 1) expandCapacity();
}
Queue<ValueType>::~Queue() { array[tail] = value;
delete[] array; tail = (tail + 1) % capacity;
} }

Code for the Ring-Buffer Queue Code for the Ring-Buffer Queue
/* /*
* Implementation notes: dequeue, peek * Implementation notes: expandCapacity
* ----------------------------------- * ------------------------------------
* These methods must check for an empty queue and report an * This private method doubles the capacity of the elements array
* error if there is no first element. * whenever it runs out of space. To do so, it must allocate a new
*/ * array, copy all the elements from the old array to the new one,
* and free the old storage. Note that this implementation also
template <typename ValueType> * shifts all the elements back to the beginning of the array.
ValueType Queue<ValueType>::dequeue() { */
if (isEmpty()) error("dequeue: Attempting to dequeue an empty queue");
ValueType result = array[head]; template <typename ValueType>
head = (head + 1) % capacity; void Queue<ValueType>::expandCapacity() {
return result; int count = size();
} int oldCapacity = capacity;
capacity *= 2;
template <typename ValueType> ValueType *oldArray = array;
ValueType Queue<ValueType>::peek() { array = new ValueType[capacity];
if (isEmpty()) error("peek: Attempting to peek at an empty queue"); for (int i = 0; i < count; i++) {
return array[head]; array[i] = oldArray[(head + i) % oldCapacity];
} }
head = 0;
tail = count;
delete[] oldArray;
}

#endif
–4–

Implementing a Linked-List Queue Structure for the List-Based Queue


/*
•• In some ways, the linked-list implementation of a queue is * Implementation notes: Queue data structure
easier to understand than the ring-buffer model, even though it * ------------------------------------------
* The list-based queue uses a linked list to store the elements
contains pointers. * of the queue. To ensure that adding a new element to the tail
* of the queue is fast, the data structure maintains a pointer to
•• In the linked-list version, the private data structure for the * the last cell in the queue as well as the first. If the queue is
* empty, both the head pointer and the tail pointer are set to NULL.
Queue class requires two pointers to cells: a head pointer that */
indicates the first cell in the chain, and a tail pointer that private:
indicates the last cell. Because all insertion happens at the tail /* Type for linked list cell */
of the queue, no dummy cell is required.
struct Cell {
•• The private data structure must also keep track of the number ValueType data;
Cell *link;
/* The data value
/* Link to the next cell
*/
*/
of elements so that the size method can run in O(1) time. };

/* Instance variables */

Cell *head; /* Pointer to the cell at the head */


Cell *tail; /* Pointer to the cell at the tail */
int count; /* Number of elements in the queue */

Code for the Linked-List Queue Code for the Linked-List Queue
/* /*
* Implementation notes: Queue constructor * Implementation notes: size, isEmpty, clear
* --------------------------------------- * ------------------------------------------
* The constructor must create an empty linked list and then * These implementations should be self-explanatory.
* initialize the fields of the object. */
*/
template <typename ValueType>
template <typename ValueType> int Queue<ValueType>::size() {
Queue<ValueType>::Queue() { return count;
head = tail = NULL; }
count = 0;
} template <typename ValueType>
bool Queue<ValueType>::isEmpty() {
/* return count == 0;
* Implementation notes: ~Queue destructor }
* ---------------------------------------
* The destructor frees any memory that is allocated by the implementation. template <typename ValueType>
* Freeing this memory guarantees the client that the queue abstraction void Queue<ValueType>::clear() {
* will not "leak memory" in the process of running an application. while (count > 0) {
* Because clear frees each element it processes, this implementation dequeue();
* of the destructor simply calls that method. }
*/ }
template <typename ValueType>
Queue<ValueType>::~Queue() {
clear();
}

Code for the Linked-List Queue Code for the Linked-List Queue
/* /*
* Implementation notes: enqueue * Implementation notes: dequeue, peek
* ----------------------------- * -----------------------------------
* This method allocates a new list cell and chains it in * These methods must check for an empty queue and report an
* at the tail of the queue. If the queue is currently empty, * error if there is no first element. The dequeue method
* the new cell must also become the head pointer in the queue. * must also check for the case in which the queue becomes
*/ * empty and set both the head and tail pointers to NULL.
*/
template <typename ValueType>
void Queue<ValueType>::enqueue(ValueType value) { template <typename ValueType>
Cell *cp = new Cell; ValueType Queue<ValueType>::dequeue() {
cp->data = value; if (isEmpty()) error("dequeue: Attempting to dequeue an empty queue");
cp->link = NULL; Cell *cp = head;
if (head == NULL) { ValueType result = cp->data;
head = cell; head = cp->link;
} else { if (head == NULL) tail = NULL;
tail->link = cell; count--;
} delete cell;
tail = cell; return result;
count++; }
}
template <typename ValueType>
ValueType Queue<ValueType>::peek() {
if (isEmpty()) error("peek: Attempting to peek at an empty queue");
return head->data;
}

You might also like