Corning Community College CSCS2320 Data Structures ~~TOC~~ ======Project: DLQ0====== =====Errata===== This section will document any updates applied to the project since original release: * __revision #__: (DATESTRING) =====Objective===== In this project, resume our conceptual journey and explore another data structure: queues. =====Background===== A **queue** is considered one of the most important data structures, along with **stack** (last week's project), trees, and hash tables. And it is largely because of how often we find them playing out in nature or in our day-to-day lives. The word "**queue**" is [[https://www.google.com/search?&q=define%3Aqueue&ie=utf-8&oe=utf-8|defined]] as: * (generically): a line or sequence of items awaiting their turn to be attended to or to proceed * (computing): a list of data items, commands, etc., stored so as to be retrievable in a definite order, usually the order of insertion ====Lists and Nodes==== 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 after/prior pointers manually all the time). By limiting how we access the data, we give ourselves certain algorithmic advantages: * __error reduction__: if have a small set of operations that can do one thing, and do their one thing extremely well (**insert()**, **append()**, and **obtain()** again, for instance), we can then rely on them to do the low-level grunt work, freeing us up to accomplish higher level tasks (such as **sorting** or **swapping**), or even things like determining if a word is a **palindrome**, or just preserving order of items during storage. * __performance__: by restricting our available choices, the edge cases we have to check for are reduced, and in ideal situations, the average case moves closer to the best case. ====conceptualizing a queue==== 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 structure in nature. ====the queue==== 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: * a queue has a **back** and a **front**, basically node pointers that constantly point to the back and front node in the queue (equivalent to the underlying list's last and first pointers, respectively). * to put an item on the queue, we **enqueue** it (place it at the back of the queue). So one of the functions we'll be implementing is **enqueue()**, which will take the node we wish to place on the given queue, and enqueue will handle all the necessary coordination with its underlying list. * to get an item off of the queue, we **dequeue** it. In our **dequeue()** function, we grab the **front** node off the queue (this also translates into a set of list-level transactions that our **dequeue()** function will handle for us). These qualities cause the queue to be described as a FIFO (or LILO) structure: * **FIFO**: **F**irst **I**n **F**irst **O**ut * **LILO**: **L**ast **I**n **L**ast **O**ut 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 enqueued would be the first one we get back when we dequeue 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 requirements 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 requirements of a queue: * **purge**: a way to quickly empty out a queue (evacuate its contents-- note this is partially similar in nature to what our **rmqueue()** function will do; only we won't take it the extra step of de-allocating and NULLifying the queue pointer). * this will be similar in nature to the list's **empty()** function, which properly clears a list to an empty state; only, **purge()** is operating at the queue level. 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. ====buffer size can matter==== 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. ====queue error conditions==== There are two very important operational error conditions a queue can experience: * __buffer **over**run__: this is the situation where the quantity of the list is equal to the configured queue buffer, and we try to enqueue another node onto the queue. * __buffer **under**run__: this is the situation where the queue is empty, yet we still try to dequeue a value from it. =====Project Overview===== 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). ====In inc/data.h==== Building on the **data.h** header file introduced in dls0, a section of status codes has been added for queues: ////////////////////////////////////////////////////////////////////// // // Status codes for the doubly linked queue implementation // #define DLQ_SUCCESS 0x0000000100000000 #define DLQ_CREATE_FAIL 0x0000000200000000 #define DLQ_NULL 0x0000000400000000 #define DLQ_EMPTY 0x0000000800000000 #define DLQ_OVERRUN 0x0000001000000000 #define DLQ_UNDERRUN 0x0000002000000000 #define DLQ_ERROR 0x0000004000000000 #define DLQ_INVALID 0x0000008000000000 #define DLQ_DEFAULT_FAIL 0x0000000000808000 ====In inc/queue.h==== #ifndef _QUEUE_H #define _QUEUE_H ////////////////////////////////////////////////////////////////////// // // Queue relies on list (which relies on node) to work. // #include "list.h" ////////////////////////////////////////////////////////////////////// // // Define the queue struct // struct queue { Node *front; // pointer to front of queue Node *back; // pointer to back of queue List *data; // pointer to queue data ulli buffer; // queue length (0- unbounded) }; code_t mkqueue(Queue **, ulli ); // create new queue (of length) code_t cpqueue(Queue *, Queue **); // create a copy of a queue code_t rmqueue(Queue ** ); // de-allocate a queue code_t purge (Queue ** ); // clear an existing queue code_t enqueue(Queue **, Node * ); // add node to back of queue code_t dequeue(Queue **, Node ** ); // grab node at front of queue #endif For our queue implementation, we will continue to utilize the double pointer, in order to practice 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 codes (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. ====queue library==== 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. ====Queue library unit tests==== In **unit/queue/**, you will find these files: * **unit-mkqueue.c** - unit test for **mkqueue()** library function * **unit-enqueue.c** - unit test for **enqueue()** library function * **unit-dequeue.c** - unit test for **dequeue()** library function * **unit-cpqueue.c** - unit test for **cpqueue()** library function * **unit-rmqueue.c** - unit test for **rmqueue()** library function * **unit-purge.c** - unit test for **purge()** library function There are also corresponding **verify-FUNCTION.sh** scripts that will output a "MATCH"/"MISMATCH" to confirm overall conformance with the pertinent queue 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: * the source code to each of these unit tests * the purpose of these programs is to validate the correct functionality of the respective library functions * follow the logic * make sure you understand what is going on * ask questions to get clarification! * the output from these programs once compiled and ran * analyze the output * make sure you understand what is going on * ask questions to get clarification! =====Expected Results===== 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): ====queue library==== Here is what you should get for queue: lab46:~/src/data/dlq0$ make check ====================================================== = Verifying Doubly-Linked Queue Functionality = ====================================================== [mkqueue] Total: 6, Matches: 6, Mismatches: 0 [enqueue] Total: 18, Matches: 18, Mismatches: 0 [dequeue] Total: 19, Matches: 19, Mismatches: 0 [cpqueue] Total: 17, Matches: 17, Mismatches: 0 [purge] Total: 7, Matches: 7, Mismatches: 0 [rmqueue] Total: 10, Matches: 10, Mismatches: 0 ====================================================== [RESULTS] Total: 77, Matches: 77, Mismatches: 0 ====================================================== lab46:~/src/data/dlq0$ =====Submission===== {{page>haas:fall2015:common:submitblurb#DATA&noheader&nofooter}}