User Tools

Site Tools


Sidebar

projects

haas:fall2014:data:projects:dlq0

Corning Community College

CSCS2320 Data Structures

~~TOC~~

Project: DLQ0

Errata

This section will document any updates applied to the project since original release:

  • revision 1: I noticed some typos and omissions that while not causing any problems, could cause confusion and compiler warnings. (20141116)
    • unit-enqueue.c had a bunch of references to “Dequeueing” in its printfs() where it actually meant to say “Enqueueing” (FIXED).
    • the prototype for rmqueue() was missing from the inc/queue.h header. This was causing implicit function declaration warnings during compile. (FIXED).
  • revision 2: unit-mkqueue tweaks and verify-queue.sh enhancements (20141119)
    • While I was debugging some code I noticed that the unit-mkqueue.c logic was not flexible enough in handling implementations where front was pointing to the end of the list. I have added logic to enable proper checking of back/front, regardless of underlying list orientation (FIXED).
    • verify-queue.sh enhanced to show absolute totals, even if improper implementations cause the unit test not to perform the full run.

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) 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:

  • (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 next/prev 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 structures 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 start and end pointers– you can decide which one you want to use for what location).
  • 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 stack (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: First In First Out
  • LILO: Last In Last Out

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:

  • 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).

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 (just as relying on qty in the sll projects was), 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 overrun: 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 underrun: 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/queue.h

1
#ifndef _QUEUE_H
#define _QUEUE_H
 
#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
    int   buffer;                  // maximum queue size (0 is unbounded)
};
typedef struct queue Queue;        // because we deserve nice things
 
Queue *mkqueue(int              ); // create new queue (of max size)
Queue *cpqueue(Queue *          ); // create a copy of an existing queue
Queue *rmqueue(Queue *          ); // clear and de-allocate an existing queue
 
Queue *purge(Queue *            ); // empty a given queue
 
int    enqueue(Queue **, Node * ); // add new node to the back of queue
int    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.

list library

One more enhancement is in store for our venerable display() function, and that is described below:

display() enhancements

You are also to implement an additional feature (resulting in 8 additional modes) to the display() function.

This means you'll need to expand the current modulus divide by 8 to one of 16.

The current modes are as follows:

//       modes: 0 display the list forward, no positional values
//              1 display the list forward, with positional values
//              2 display the list backwards, no positional values
//              3 display the list backwards, with positional values
//              4 display the list forward, no positional values, in ASCII
//              5 display the list forward, with positional values, in ASCII
//              6 display the list backwards, no positional values, in ASCII
//              7 display the list backwards, with positional values, in ASCII

The new feature is to toggle the display of the individual arrows (and the terminating NULL value) and much of the natural spacing we've used so far. It should also ditch, when in ASCII mode, the quotes we have been placing around the ASCII characters.

The idea here is, when combined with the ASCII representation of the list, display() can become a pretty useful function for displaying strings, which could come in handy.

Also, when dealing with numeric values, we could make use of this in applications where we are handling the construction of a numeric value (such as a bignum).

For positioned displays, this would seem to have less utility, but by having a space displayed before the position value, we can salvage the output nicely.

So for each of the above 8 modes, you'll have a WITH_ARROWS mode, and then a WITHOUT_ARROWS mode (leaving us with a total of 16 possible combinations).

For sane implementation purposes, for our current 8 (so modes 0-7), those will be considered to be WITH_ARROWS… whereas the remaining 8 combinations (8-15) will be WITHOUT_ARROWS. All other mode features will map in place (if you recognize the binary pattern I've been following you'll hopefully see how trivial this change (and the previous one) will be).

Also, when not displaying arrows, no NULL values will be displayed at all; this means that in the event we are running display() on a NULL or empty list, nothing but a newline should be displayed.

display() output examples based on mode

Let's say we have a list with the following elements:

start -> 51 -> 49 -> 51 -> 51 -> 55 -> NULL
mode "8": forward, no positions, as numbers, without arrows
5149515155
mode "9": forward, with positions, as numbers, without arrows
 [0]51 [1]49 [2]51 [3]51 [4]55
mode "12": forward, no positions, in ASCII, without arrows
31337
mode "13": forward, with positions, in ASCII, without arrows
 [0]3 [1]1 [2]3 [3]3 [4]7

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.

Reference Implementation

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.

Using the test reference implementation

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).

Reverting back to using your code

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$ 

List Library unit tests

As a result of the required changes to display(), an updated unit test will be released (this will mean a different number of total tests for list than previously).

Be sure to run the various list unit tests and verification scripts to see which functions have fallen out of compliance with the list struct specification changes issued in this project. The verify-list.sh script can be especially useful in getting a big picture view of what work is needed.

Queue library unit tests

In testing/queue/unit/, you will find these files:

  • unit-mkqueue.c - unit test for mkqueue() library function
  • unit-cpqueue.c - unit test for cpqueue() library function
  • unit-rmqueue.c - unit test for rmqueue() library function
  • unit-enqueue.c - unit test for enqueue() library function
  • unit-dequeue.c - unit test for dequeue() 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 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:

  • 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 list and queue libraries should resemble the following (when running the respective verify script):

list library

Here is what you should get for list:

lab46:~/src/data/dlq0$ bin/verify-list.sh 
====================================================
=    Verifying Doubly-Linked List Functionality    =
====================================================
  [mklist] Total:   6, Matches:   6, Mismatches:   0
  [cplist] Total:  30, Matches:  30, Mismatches:   0
  [rmlist] Total:   3, Matches:   3, Mismatches:   0
  [append] Total:  22, Matches:  22, Mismatches:   0
  [insert] Total:  22, Matches:  22, Mismatches:   0
  [obtain] Total:  23, Matches:  23, Mismatches:   0
 [display] Total:  24, Matches:  24, Mismatches:   0
[findnode] Total:  11, Matches:  11, Mismatches:   0
[sortlist] Total:   6, Matches:   6, Mismatches:   0
[swapnode] Total:   7, Matches:   7, Mismatches:   0
====================================================
 [RESULTS] Total: 154, Matches: 154, Mismatches:   0
====================================================
lab46:~/src/data/dlq0$ 

With the added feature to display(), we have 10 additional combinations to test, plus I added in 4 explicit tests of invalid modes, so this will bump up the total number of list tests by 10+4, going from the dll0 total of 102 to the dls0 total of 140 to the dlq0 total of 154.

But aside from this change to display(), your list implementation can remain unchanged.

queue library

Here is what you should get for queue:

lab46:~/src/data/dlq0$ bin/verify-queue.sh 
===================================================
=   Verifying Doubly-Linked Queue Functionality   =
===================================================
[mkqueue] Total:   4, Matches:   4, Mismatches:   0
[cpqueue] Total:   8, Matches:   8, Mismatches:   0
[rmqueue] Total:   3, Matches:   3, Mismatches:   0
  [purge] Total:   3, Matches:   3, Mismatches:   0
[enqueue] Total:  30, Matches:  30, Mismatches:   0
[dequeue] Total:  25, Matches:  25, Mismatches:   0
===================================================
[RESULTS] Total:  73, Matches:  73, Mismatches:   0
===================================================
lab46:~/src/data/dlq0$ 

Submission Criteria

To be successful in this project, the following criteria must be met:

  • Project must be submit on time, by the posted deadline.
    • Late submissions will lose 25% credit per day, with the submission window closing on the 4th day following the deadline.
  • All code must compile cleanly (no warnings or errors)
    • all requested functions must be implemented in the related library
    • all requested functionality must conform to stated requirements (either on this project page or in comment banner in source code files themselves).
  • Executed programs must display in a manner similar to provided output
    • output formatted, where applicable, must match that of project requirements
  • Processing must be correct based on input given and output requested
  • Output, if applicable, must be correct based on values input
  • Code must be nicely and consistently indented (you may use the indent tool)
  • Code must be commented
    • Any “to be implemented” comments MUST be removed
      • these “to be implemented” comments, if still present at evaluation time, will result in points being deducted.
    • Sufficient comments explaining the point of provided logic MUST be present
  • Track/version the source code in a repository
  • Submit a copy of your source code to me using the submit tool (make submit will do this) by the deadline.
haas/fall2014/data/projects/dlq0.txt · Last modified: 2014/11/19 14:43 by wedge