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: stacks.
Also, update pertinent list library functions to conform to expanded specifications:
It is recommended that you tend to the list enhancements before starting on the stack, as you'll need that functionality online in order to get a fully operational stack.
A stack is considered one of the most important data structures, along with queues (next 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 “stack” is defined as:
Additionally, when viewing it as a verb (an action), we also find some positive computing application (bolded) in a less reputable cardplaying usage:
Or, to distill it out:
Combining with our previous definitions, we have:
So, how does all this list and node stuff play into our stack implementation?
Well, we're going to build the stack ON TOP OF lists (which are composed of nodes).
Therefore, a stack 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 stack as a vertical object, much like a pile of papers that need to be processed (or a pile of anything we need to work with).
Although we've commonly viewed lists horizontally (from left to right), there is absolutely nothing requiring this positional orientation.
Similarly, stacks possess no mandatory orientation, but we do usually visualize them as vertical entities, largely because that's how the piles of paper that accumulate on our desks tend to grow.
The stack data structure presents certain advantages that encourages its use in solving problems (why do we stack a bunch of papers all in the same place to create piles? Why is that more advantageous than giving each one its own unique desk space?), and we accomplish that by its compositional definition:
These qualities cause the stack to be described as a LIFO (or FILO) structure:
And that describes what is conceptually going on– if we can ONLY access our data through one location (the top), the data most immediately available to us is that which we most recently placed there (hence the last one we pushed in would be the first one we get back when popping 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 top, along with the core push() and pop() functions defines the minimal necessary requiments to interface with a stack. Sometimes we'll see additional actions sneak in. While these may be commonly associated with stacks, they should not be confused as core requiments of a stack:
While we may be implementing these supplemental functions, it should be noted that not only are they in no way necessary for using a stack, 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.
With a stack, 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 stack (ie the underlying list). Therefore, qty makes an appearance once again in the list struct.
Additionally, the stack will have a configured maximum size- if the quantity of nodes in the list exceeds the configured size of the stack, we should prevent any additional pushes.
It should also be pointed out that in other applications, a stack need not have a maximum 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 stack can experience:
For this project, we're going to be implementing the stack data structure atop of our recently re-implemented linked list (the doubly linked list).
Should you be having any lingering issues with your doubly-linked list implementation, remember that the test reference implementation is (and has been) available. With this, you don't have to worry about all the supporting node and list functions that aren't the focus of the project (although, because you ARE changing some list functions in this project, those particular ones are not included).
With the introduction of the status codes in previous projects (and stack introducing its own), I decided to break out that information into its own header file. All other header files now include data.h, so nothing is lost or inaccessible.
Here's what's in the file:
#ifndef _DATA_H #define _DATA_H // Status codes for the doubly linked list implementation // #define DLL_SUCCESS 0 #define DLL_MALLOC_FAIL 1 #define DLL_ALREADY_ALLOC 2 #define DLL_NULL 4 #define DLL_EMPTY 8 #define DLL_DEFAULT_FAIL 64 #define DLL_FAIL 128 // Status codes for the stack implementation // #define DLS_SUCCESS 256 #define DLS_CREATE_FAIL 512 #define DLS_NULL 1024 #define DLS_EMPTY 2048 #define DLS_OVERFLOW 4096 #define DLS_UNDERFLOW 8192 #define DLS_DEFAULT_FAIL 16384 #define DLS_FAIL 32768 // Status codes for the list's display() function modes // #define DISPLAY_FORWARD 0 // display list forward #define DISPLAY_POSLESS 0 // display list without position indices #define DISPLAY_ASVALUE 0 // display list data in numeric form #define DISPLAY_WITHPOS 1 // display list with position indices #define DISPLAY_REVERSE 2 // display list in reverse order #define DISPLAY_INASCII 4 // display list data in ASCII form // custom types (mostly for shortening typing) // typedef unsigned short int code_t; // status code data type typedef unsigned long int uli; // shorter than typing 'unsigned long int' #endif
In addition to the status codes (for list and stack), we also see new defines for display(), better visualizing the numeric modes.
Finally, there's two new typedefs to create shortcut types (to save on long typing, throwing off alignment of code):
There is one change (an addition) that has been made to list.h, and that is the return of the qty element:
struct list { Node *first; // pointer to start of list Node *last; // pointer to end of list uli qty; // count of nodes in list }; typedef struct list List; // because we deserve nice things
We're also taking advantage of our new “uli” typedef, as you can see qty is defined as a uli type.
It should be noted (and stressed) that the utilization of qty should JUST be in maintaining a count. You need not re-implement or change the logic of any of your list functionality to base its operations or to rely on qty in any way (just as before, if you do you'll lose credit).
From the list's point of view, its sole purpose is for show.
With that said, you likely only have to make changes to 4 functions in order to appropriately accommodate qty– any low-level list operation that is responsible for initializing, adding, or taking nodes out of the list (creating, inserting, appending, obtaining).
You may make use of the qty element to streamline your display() function's reverse list with index option.
#ifndef _STACK_H #define _STACK_H #include "data.h" // helpful #defines #include "list.h" // stack relies on list to work // (which relies on node) struct stack { List *data; // pointer to list containing data Node *top; // pointer to node at top of stack uli size; // maximum stack size (0 implies }; // an unbounded stack) typedef struct stack Stack; // because we deserve nice things code_t mkstack(Stack **, uli); // create new stack (of max size) code_t cpstack(Stack *, Stack **); // create a copy of an existing stack code_t rmstack(Stack **); // clear and de-allocate an existing stack code_t push(Stack **, Node * ); // put new node on top of stack code_t pop (Stack **, Node **); // get (and disconnect) top node off stack code_t peek(Stack *, Node **); // get (don't disconnect) top node code_t isempty(Stack *); // determine if the stack is empty or not #endif
For our stack implementation, just as with our doubly-linked list implementation, we will make increased used of the double pointer in order to achieve passing parameters by address.
This is necessary so that we can free up the return value of push() and pop() to be used for status (ie look out for stack overflows and underflows). You'll note the extensive deployment of the code_t type as the return value for all the stack functions (instead of lengthily having to type out “unsigned short int” in each case).
peek() and isempty() are being implemented as an exercise to aid in your understanding of stacks. Again, avoid their use except is a means of convenience (or to further optimize your code). The general rule of thumb is that the use of peek() and isempty() should result in shortening your code in a clear or clever way.
If you cannot think of how to solve a problem without the use of peek()/isempty(), that is a strong clue that you shouldn't be using them.
Also, while nothing is stopping you from doing so, the idea here is that things like size and the underlying list qty in stack transactions will NOT be accessed outside of the push() and pop() functions. Just like my warnings about using qty in your list solutions– do not consider size as a variable for your general use (push() will probably be the only place it is used).
In object-oriented programming, both size and qty would be private member variables of their respective classes, unable to be used by anything other than their respective member functions.
Againt, in src/list/, you are to add support for qty so that, just as the list's first and last maintain an accurate positioning of their respective aspects of the list, qty maintains a count of the total number of nodes still in the list.
You are also to implement 4 additional modes to the display() function.
This means you'll need to expand the current modulus divide by 4 to one of 8.
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
The new modes are (you may want to add these comments to your display.c file):
// 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
What has changed? In modes 4-7, instead of displaying the numeric value contained in the node's data element, you are instead to represent it as its ASCII character.
For example, if there was a numeric 65 stored in the node , in the mode 4-7 representation, an 'A' should instead be displayed.
Let's say we have a list with the following elements:
first -> 51 -> 49 -> 51 -> 51 -> 55 -> NULL
51 -> 49 -> 51 -> 51 -> 55 -> NULL
[0] 51 -> [1] 49 -> [2] 51 -> [3] 51 -> [4] 55 -> NULL
'3' -> '1' -> '3' -> '3' -> '7' -> NULL
[0] '3' -> [1] '1' -> [2] '3' -> [3] '3' -> [4] '7' -> NULL
As a result of the required changes to display() and the incorporation of qty, pertinent list unit tests will be enhanced, so you can make use of them to ensure implementation compliance.
The pertinent list unit tests have been updated. You only need to focus on the following for the list library (remaining list functions need no changes):
The verify-list.sh script has been updated to display just these updated unit tests.
In src/stack/, 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 stack is to utilize the stack for its underlying data storage operations. This is what the stack's data list pointer is to be used for.
You'll notice the presence of a set of stack-related #define's in the data.h header file. These are intended to be used to report on various states of stack status after performing various operations.
They are not exclusive- in some cases, multiple states can be applied. The intent is that you will OR together all pertinent states and return that from the function.
For example, in the case of “DLS_CREATE_FAIL”, there are actually a total of three states raised:
ALL THREE states must be returned from the function in question should such an occurrence take place.
It is also intended that when handling stack status codes, any list status codes will also be present (that is why code_t is a short int, whereas the list status code was a char– we have twice the amount of bytes for status code storage now– lower 8-bits for list, upper 8-bits for stack).
In testing/stack/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 stack 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:
Now that we've completed our stack functionality, we can use these individual functions to piece together solutions to various everyday problems where a stack could be effective (and even compare approaches to when we didn't have the benefit of a stack in solving the problem). After all, that's a big aspect to learning data structures- they open doors to new algorithms and problem solving capabilities.
Our task (once again) will be that of palindromes (ie words/phrases that, when reversed, spell the same thing).
This implementation will be considered an extra credit opportunity, so as to offer those who have fallen behind (but working to get caught up) a reprieve on some of the credit they've lost.
It is also highly recommended to undertake as it will give you further experience working with these concepts.
Note this is a DIFFERENT approach than you would have taken in the program with sll2- you're to use stack functionality to aid you with the heavy lifting. While you will still make use of list functionality for grabbing the initial input, the actual palindrome comparison processing needs to heavily involve stacks.
To assist you in verifying a correct implementation, a fully working implementation of the node, list, and stack libraries should resemble the following (when running the respective verify script):
Here is what you should get for node:
lab46:~/src/data/dls0$ bin/verify-node.sh ==================================================== = Verifying Doubly-Linked Node Functionality = ==================================================== [mknode] Total: 5, Matches: 5, Mismatches: 0 [cpnode] Total: 6, Matches: 6, Mismatches: 0 [rmnode] Total: 2, Matches: 2, Mismatches: 0 ==================================================== [RESULTS] Total: 13, Matches: 13, Mismatches: 0 ==================================================== lab46:~/src/data/dls0$
As no coding changes were needed in the node library, these results should be identical to that of a fully functioning node implementation from the dll0 project.
Here is what you should get for list:
lab46:~/src/data/dls0$ bin/verify-list.sh ==================================================== = Verifying Doubly-Linked List Functionality = ==================================================== [mklist] Total: 15, Matches: 15, Mismatches: 0 [append] Total: 40, Matches: 40, Mismatches: 0 [insert] Total: 41, Matches: 41, Mismatches: 0 [obtain] Total: 70, Matches: 70, Mismatches: 0 [display] Total: 28, Matches: 28, Mismatches: 0 ==================================================== [RESULTS] Total: 194, Matches: 194, Mismatches: 0 ==================================================== lab46:~/src/data/dls0$
Due to the re-introduction of qty into list (impacting actions performed by mklist(), append(), insert(), and obtain()), along with feature additions to display(), those functions saw additional tests performed, so our original max total of 102 from dll0 has now changed to 140 (ie various qty and display()-related tests adding to the previous total).
Remember though- aside from the minor change of adding qty and enhancing display(), all remaining list functions need no change (and mklist()/append()/insert()/obtain() remained largely unchanged).
Here is what you should get for stack:
lab46:~/src/data/dls0$ bin/verify-stack.sh =================================================== = Verifying Doubly-Linked Stack Functionality = =================================================== [mkstack] Total: 6, Matches: 6, Mismatches: 0 [cpstack] Total: 12, Matches: 12, Mismatches: 0 [rmstack] Total: 6, Matches: 6, Mismatches: 0 [push] Total: 30, Matches: 30, Mismatches: 0 [pop] Total: 25, Matches: 25, Mismatches: 0 [peek] Total: 37, Matches: 37, Mismatches: 0 [isempty] Total: 13, Matches: 13, Mismatches: 0 =================================================== [RESULTS] Total: 129, Matches: 129, Mismatches: 0 =================================================== lab46:~/src/data/dls0$
To be successful in this project, the following criteria must be met: