Queues A queue is an ordered collection of data items such that: items can be removed only at one end ( front of the queue) items can be added only at the other end ( back of the queue) Basic operations are: construct: empty: addQ: front: removeQ: Create an empty queue Check if a queue is empty Add a value at the back of the queue Retrieve the value at the front of the queue Remove the value at the front of the queue Queues a queue is a First-In-First-Out (FIFO) structure. Examples: I/O buffers Scheduling queues in a multi-user computer system e.g. Printer queue: When files are submitted to a printer, they are placed in the printer queue. The printer software executes an algorithm something like: for (;;) { while (printerQueue.empty()) sleep 1; printFile = printerQueue.removeQ(); Print(printFile); } Queues Resident queue: On disk, waiting for memory Ready queue: In memory —needs only CPU to run Suspended queue:Waiting for I/O transfer or to be reassigned the CPU Queues vs. Stacks Stacks are a LIFO container store data in the reverse of order received Queues are a FIFO container => store data in the order received Stacks then suggest applications where some sort of reversal or unwinding is desired. Queues suggest applications where service is to be rendered relative to order received. Stacks and Queues can be used in conjunction to compare different orderings of the same data set. From an ordering perspective, then, Queue are the “opposite” of stacks Easy solution to the palindrome problem Easy Palindrome Check #include “Stack.h”; #include “Queue.h”; #include <iostream.h> using namespace std; int main() { Stack S; Queue Q; char ch; cout << “Enter string to be tested as palindrome: “; while (cin >> ch) { S.push(ch); Q.addq(ch); } bool pal = true; while (!Q.empty()) pal = Q.removeq() = = S.pop(); if (pal) cout << “Palindrome!!” << endl; return 0; } Array Implementation Any implementation of a queue requires: storage for the data as well as markers (“pointers”) for the front and for the back of the queue. An array-based implementation would need structures like myArray, an array to store the elements of the queue myFront, an index to track the front queue element myBack, an index to track the position following last queue element Additions to the queue would result in incrementing myBack. Deletions from the queue would result in incrementing myFront. Clearly, we’d run out of space soon! Solutions include: Shifting the elements downward with each deletion (YUCK!!) Viewing array as a circular buffer, i.e. wrapping the end to the front “Circular” Array-Implementation Wraparound keeps the addition/deletion cycle from walking off the edge of the storage array. Say, myArray has QUEUE_CAPACITY elements. When myBack hits the end of myArray, a deletion should wrap myBack around to the first element of myArray, viz: myBack++; if (myBack = = QUEUE_CAPACITY) myBack = 0; //equivalently (preferred, concise) myBack = (myBack + 1) % QUEUE_CAPACITY; Analogous handling of myFront needed. “Circular” Array-Implementation Initially, a queue object is empty. myFront = = 0 myBack = = 0 After many insertions and deletions, the queue is full First element, say, at myArray[i] myFront has value of i. Last element then at myArray[i-1] (i > 0) myBack has value of i. PROBLEM: How to distinguish between empty & full?? Common Solutions: Keep an empty slot between myFront and myBack, i.e. myArray allocated QUEUE_CAPACITY + 1 elements Keep an auxiliary counter to track actual number of elements in queue QUEUE class #ifndef QUEUE #define QUEUE const int QUEUE_CAPACITY = 128; typedef int QueueElement; class Queue { /***** Function Members *****/ public: Queue(); bool empty() const; bool full() const; void addQ(const QueueElement & value); QueueElement front const(); //nondestructive “peek” void removeQ(); /***** Data Members *****/ private: QueueElement myArray[QUEUE_CAPACITY]; int myFront, myBack; }; // end of class declaration #endif QUEUE class implementation Queue::Queue() { myFront = myBack = 0; } bool Queue::empty() { return myFront == myBack; } bool Queue::full() { return myFront == (myBack + 1) % QUEUE_CAPACITY; } QUEUE class implementation void Queue::addQ(const QueueElement& value) { if (myFront != (myBack + 1) % QUEUE_CAPACITY) { myArray[myBack] = value; myBack = (myBack + 1) % QUEUE_CAPACITY; } return; } void Queue::removeQ() { myFront = (myFront + 1) % QUEUE_CAPACITY; } Linked list implementation Linked list implementation is another approach for queue representation. The interface to the class Queue empty, addq, removeq, front, etc. would not change The storage structure would be a linked list. The markers would be pointers now rather than indices into an array. myFront would contain the address of the first node in this list myBack would contain the address of the last node in this list empty would test if myFront was a nonnull pointer, addq would allocate a new node, link it off myBack and update myBack, removeq would remove the first element and update myFront, front would return a copy of the element whose address was in myFront Deque A deque (double-ended queue) is similar to a queue BUT additions and deletions may be performed on either end. Hence, a implementation needs a directive (tag) indicating at what end the operation is to be performed OR multiple methods should be provided. We modify a queue's circular array implementation here. #include <iostream> using namespace std; enum where {front, rear}; /* Deque implemented with same data members myArray myFront myBack Constructors and empty(), full() methods the same */ Deque enqueuing void Deque::addQ(int item, where w) { if ((myBack +1)% QUEUE_CAPACITY == myFront) cout << "FULL, cannot add to queue. Error!! " << endl; else if (w == rear) // regular enqueuing { myArray[myBack] = item; myBack = (myBack+ 1) % QUEUE_CAPACITY; } else // enqueue at front { if (!myFront) myFront = QUEUE_CAPACITY; else myFront++; myArray[myFront] = item; } return; } Deque enqueuing //better to provide two addition methods void Deque::push_front(int item) {if ((myBack +1)% QUEUE_CAPACITY == myFront) cout << "FULL, cannot add to queue." << endl; else // enqueue at front { if (!myFront) myFront = QUEUE_CAPACITY; else myFront--; myArray[myFront] = item; } return; } void Deque::push_back(int item) { if ((myBack +1)% QUEUE_CAPACITY == myFront) cout << "FULL, cannot add to queue." << endl; else // regular enqueuing { myArray[myBack] = item; myBack = (myBack+ 1) % QUEUE_CAPACITY; } return; } Deque dequeuing int Deque::pop_front() // assume non-empty { int item = myArray[myFront]; myFront= (myFront+ 1) % QUEUE_CAPACITY; return item; } void Deque::pop_back() // assume non-empty { if (!myBack) myBack = QUEUE_CAPACITY; else myBack--; int item = myArray[myBack]; // dequeue from rear return item; } deques The deque is a standard container provided by STL. It is easily implemented by the vector type. See chapter 6 for more details. Priority Queues Priority queues are queues in which items are ordered by priority rather than temporally (i.e. order in which received). Any queue implementation then can be taken and modified to acquire a priority queue. The only needed modification is the addq() method. Instead of unconditionally adding to the back, the queue must be scanned for the correct insertion point. An array implementation then would require shifting elements (YUCK). Hence a linked list implementation is preferred. (Develop then the preferred solution after chapter 8) Priority Queues class PriorityQ { public: PriorityQ(); void addq(int); int removeq(); bool empty(); bool full(); int back(); int size(); private: const int PRIORITYQ_CAPACITY = some_value; int myArray[PRIORITYQ_CAPACITY]; int myFront; int myBack; };