Corning Community College
CSCS2320 Data Structures
~~TOC~~
This section will document any updates applied to the project since original release:
In this project, resume our conceptual journey and explore another data structure: queues.
A queue is considered one of the most important data structures, along with stack (last week's project) and trees. And it is largely because of how often we find them playing out in nature or our day-to-day lives.
The word “queue” is defined as:
So, how does all this list and node stuff play into our queue implementation?
Well, like stacks, we're going to build the queue ON TOP OF lists (which are composed of nodes).
Therefore, a queue is a data structure that stores its data in a list (which consists of nodes), and we apply various rules/restrictions on our access of that list data.
The concept of restricting access is a very important one- which we did with our list as well (limiting our access to the list through the use of append(), insert(), and obtain() versus manipulating the next/prev pointers manually all the time). By limiting how we access the data, we give ourselves certain algorithmic advantages:
It is common to think of a queue as a horizontal object, much like a line of people that need to be services (such as a checkout line at the grocery store, or a line at the bank).
Although we've commonly viewed lists horizontally (from left to right), there is absolutely nothing requiring this positional orientation.
Similarly, queues possess no mandatory orientation, but we do usually visualize them as horizontal entities, largely because that's how we commonly find ourselves entangled in this data structures in nature.
The queue data structure presents certain advantages that encourages its use in solving problems (why is the notion of forming lines important? What problems does that solve? How are resources more efficiently utilized by this act?), and we accomplish that by its compositional definition:
These qualities cause the queue to be described as a FIFO (or LILO) structure:
And that describes what is conceptually going on– if we can ONLY put our data on at one end (the back), and grab our data from the other (the front), the data most immediately available to us is that which we placed there first (hence the first one we pushed in would be the first one we get back when dequeueing it).
This concept is very important, and being aware of it can be of significant strategic importance when going about solving problems (and seeing its pattern proliferate in nature).
With that said, the existence of front, back, along with the core enqueue() and dequeue() functions defines the minimal necessary requiments to interface with a queue. Sometimes we'll see additional actions sneak in. While these may be commonly associated with queue, they should not be confused as core requiments of a queue:
While we may be implementing these supplemental functions, it should be noted that not only are they in no way necessary for using a queue, they could be detrimental, as one could rely on them as a crutch.
Their inclusion should ONLY be viewed as a means of convenience (in certain scenarios they may result in less code needing to be written), but NOT as something you should routinely make use of.
With a queue, there sometimes exists a need to cap its total size (especially in applications on the computer, we may have only allocated a fixed amount of space and cannot exceed it). For this reason, we will need to maintain a count of nodes in the queue (ie the underlying list). For this reason, we continue to make use of the list's qty element.
Additionally, the queue will have a configured maximum buffer- if the quantity of nodes in the list exceeds the configured buffer of the queue, we should prevent any additional enqueues.
It should also be pointed out that in other applications, a queue need not have a maximum buffer size.. in which case it can theoretically grow an indefinite amount. We will explore both conditions (unbounded and bounded) in this project.
There are two very important operational error conditions a queue can experience:
For this project, we're going to be implementing the queue data structure atop of our recently re-implemented linked list (the doubly linked list).
Building on the data.h header file introduced in dls0, a section of status codes has been added for queues:
// Status codes for the queue implementation // #define DLQ_SUCCESS 256 #define DLQ_CREATE_FAIL 512 #define DLQ_NULL 1024 #define DLQ_EMPTY 2048 #define DLQ_OVERRUN 4096 #define DLQ_UNDERRUN 8192 #define DLQ_DEFAULT_FAIL 16384 #define DLQ_FAIL 32768
You may notice that they equate to the same numerical values as the stack; that is because, for the purposes of our stack and queue implementations, there will be no overlap in functionality (stacks won't be accessing queue operations, and queues will not be accessing stack operations).
#ifndef _QUEUE_H #define _QUEUE_H #include "data.h" // helpful #defines #include "list.h" // queue relies on list to work // (which relies on node) struct queue { List *data; // pointer to list containing data Node *front; // pointer to node at front of queue Node *back; // pointer to node at back of queue uli buffer; // maximum queue size (0 is unbounded) }; typedef struct queue Queue; // because we deserve nice things code_t mkqueue(Queue **, uli ); // create new queue (of max size) code_t cpqueue(Queue *, Queue **); // create a copy of an existing queue code_t rmqueue(Queue ** ); // clear and de-allocate a queue code_t purge (Queue ** ); // clear and de-allocate an existing queue code_t enqueue(Queue **, Node * ); // add new node to the back of queue code_t dequeue(Queue **, Node ** ); // take off node at front of queue #endif
For our queue implementation, we will continue to utilize the double pointer, in order to achieve passing parameters by address.
This is necessary so that we can free up the return value of enqueue() and dequeue() to be used for status (ie look out for buffer overruns and underruns).
Also, while nothing is stopping you from doing so, the idea here is that things like buffer and the underlying list qty in queue transactions will NOT be accessed outside of the enqueue() and dequeue() functions. Just like my warnings about using qty in your list solutions (save for display() when showing position values in a backwards orientation)– do not consider buffer as a variable for your general use.
In object-oriented programming, both buffer and qty would be private member variables of their respective classes, unable to be used by anything other than their respective member functions.
In src/queue/, you will find skeletons of the above prototyped functions, hollowed out in anticipation of being made operational.
Figure out what is going on, the connections, and make sure you understand it.
Again, your queue is to utilize the list for its underlying data storage operations. This is what the queue's data list pointer is to be used for.
As the layers and complexities rise, narrowing down the source of errors becomes increasingly important.
If your stack push() isn't working, is it because of a problem in push()? Or might it be in an underlying list operation it relies upon? Or perhaps even the lowest-level node functions..
To aid you in your development efforts, you now have the ability to import a functioning node and list implementation into your project for the purposes of testing your stack functionality.
This also provides another opportunity for those who have fallen behind to stay current, as they can work on this project without having needed to successfully get full list functionality.
You'll notice that, upon running make help in the base-level Makefile, the following new options appear (about halfway in the middle):
** ** ** make use-test-reference - use working implementation object files ** ** make use-your-own-code - use your node/list implementation code ** ** **
In order to make use of it, you'll need to run make use-test-reference from the base of your dlq0 project directory, as follows:
lab46:~/src/data/dlq0$ make use-test-reference ... NODE and LIST reference implementation in place, run 'make' to build everything. lab46:~/src/data/dlq0$
You'll see that final message indicating everything is in place (it automatically runs a make clean for you), and then you can go ahead and build everything with it:
lab46:~/src/data/dlq0$ make ...
Debugging: When using the test reference implementation, you will not be able to debug the contents of the node and list functions (the files provided do not have debugging symbols added), so you'll need to take care not to step into these functions (it would be just like stepping into printf(). You can still compile the project with debugging support and debug (as usual) those compiled functions (ie the stack functions).
If you were trying out the reference implementation to verify queue functionality, and wanted to revert back to your own code, it is as simple as:
lab46:~/src/data/dlq0$ make use-your-own-code Local node/list implementation restored, run 'make clean; make' to build everything. lab46:~/src/data/dlq0$
In testing/queue/unit/, you will find these files:
There are also corresponding verify-FUNCTION.sh scripts that will output a “MATCH”/“MISMATCH” to confirm overall conformance with the pertinent stack functionality.
These are complete runnable programs (when compiled, and linked against the queue library, which is all handled for you by the Makefile system in place).
Of particular importance, I want you to take a close look at:
To assist you in verifying a correct implementation, a fully working implementation of the queue library should resemble the following (when running the respective verify script):
Here is what you should get for queue:
lab46:~/src/data/dlq0$ bin/verify-queue.sh coming soon... lab46:~/src/data/dlq0$
To be successful in this project, the following criteria must be met: