Stack - Array Implementation
- Abstract idea of a stack:
The stack is a very common data structure used in programs. By
data structure, we mean something that is meant to hold data
and provides certain operations on that data.
One way to describe how a stack data structure behaves is to look at a physical analogy, a stack of books...
Now, a minimal set of things that we might do with the stack are the following:
Place a book on the top... Take one off the top... See if the stack is empty... NOT Empty, there are books on the stack!
We'll consider other, more complex operations to be inappropriate for our stack. For example, pulling out the 3rd book from the top cannot be done directly because the stack might fall over.
How might you get the 3rd book from the top using only the simple operations?
Abstraction
Now, let's think about a stack in an abstract way. I.e., it doesn't hold any particular kind of thing (like books) and we aren't restricting ourselves to any particular programming language or any particular implementation of a stack.
Stacks hold objects, usually all of the same type. Most stacks support just the simple set of operations we introduced above; and thus, the main property of a stack is that objects go on and come off of the top of the stack.
Here are the minimal operations we'd need for an abstract stack (and their typical names):
Push
: Places an object on the top of the stack.
Pop
: Removes an object from the top of the stack and produces that object.
IsEmpty
: Reports whether the stack is empty or not.
- Order produced by a stack:
Stacks are linear data structures. This means that their contexts are stored in what looks like a line (although vertically). This linear property, however, is not sufficient to discriminate a stack from other linear data structures. For example, an array is a sort of linear data structure. However, you can access any element in an array--not true for a stack, since you can only deal with the element at its top.
One of the distinguishing characteristics of a stack, and the thing that makes it useful, is the order in which elements come out of a stack. Let's see what order that is by looking at a stack of letters...
Suppose we have a stack that can hold letters, call itstack
. What would a particular sequence ofPush
andPop
s do to this stack?
We begin withstack
empty:
----- stack
Push(stack, A)
, giving:
----- | A | <-- top ----- stack
Push(stack, B)
, giving:
----- | B | <-- top ----- | A | ----- stack
letter = Pop(stack)
, giving:
----- ----- | A | <-- top | B | ----- ----- stack letter
Push(stack, C)
, giving:
----- | C | <-- top ----- | A | ----- stack
Now we can see one of the uses of a stack...To reverse the order of a set of objects.
- Implementing a stack with an array:
Let's think about how to implement this stack in the C programming language.
First, if we want to store letters, we can use typechar
. Next, since a stack usually holds a bunch of items with the same type (e.g.,char
), we can use an array to hold the contents of the stack.
Now, consider how we'll use this array of characters, call itcontents
, to hold the contents of the stack. At some point we'll have to decide how big this array is; keep in mind that a normal array has a fixed size.
Let's choose the array to be of size 4 for now. So, an array getting A, then B, will look like:
----------------- | A | B | | | ----------------- 0 1 2 3 contents
Answer: We need to keep track of the top of the stack since not all of the array holds stack elements.
What type of thing will we use to keep track of the top of the stack?
Answer: One choice is to use an integer,top
, which will hold the array index of the element at the top of the stack.
Example:
Again suppose the stack has (A,B) in it already...
stack (made up of 'contents' and 'top') ----------------- ----- | A | B | | | | 1 | ----------------- ----- 0 1 2 3 top contents
Now, suppose we push something on the stack,Push(stack, 'C')
, giving:
stack (made up of 'contents' and 'top') ----------------- ----- | A | B | C | | | 2 | ----------------- ----- 0 1 2 3 top contents
letter = Pop(stack)
stack (made up of 'contents' and 'top') ----------------- ----- ----- | A | B | | | | 1 | | C | ----------------- ----- ----- 0 1 2 3 top letter contents
letter = Pop(stack)
stack (made up of 'contents' and 'top') ----------------- ----- ----- | A | | | | | 0 | | B | ----------------- ----- ----- 0 1 2 3 top letter contents
letter = Pop(stack)
stack (made up of 'contents' and 'top') ----------------- ----- ----- | | | | | | -1| | A | ----------------- ----- ----- 0 1 2 3 top letter contents
-1
.
What happens if we apply the following set of operations?
Push(stack, 'D')
Push(stack, 'E')
Push(stack, 'F')
Push(stack, 'G')
stack (made up of 'contents' and 'top') ----------------- ----- | D | E | F | G | | 3 | ----------------- ----- 0 1 2 3 top contents
Push(stack, 'H')
?
Thus, what is the disadvantage of using an array to implement a stack?
- Dynamically-sized stack:
Now, we will add one more choice to how we'll implement our stack.
We want to be able to decide the maximum size of the stack at run-time (not
compile-time).
Thus, we cannot use a regular array, but must use a pointer to a dynamically-allocated array.
Now, will we need to keep track of any more information besides the contents and top?
Answer: Yes! We'll need to keep the size of this array, i.e., the maximum size of the stack. We'll see why this is necessary as we write the code.
- C code:
Now, let's think about how to actually code this stack of characters
in C.
It is usually convenient to put a data structure in its own module, thus, we'll want to create filesstack.h
andstack.c
.
Now, there are 2 main parts to a C data structure: the data types needed to keep track of a stack and the functions needed to implement stack operations.
- The main data type we need is a type that people can use to declare
new stacks, as in:
type-of-a-stack s1, s2;
- Some of the functions we'll need come directly from the operations needed
for an abstract stack, like:
StackPush(ref-to-s1, 'A'); ch = StackPop(ref-to-s2);
We may need to add a few other operations to help implement a stack. These will become apparent as we start to implement the stack. Remember that we need to put prototypes for each stack function instack.h
and put the function definitions (bodies) instack.c
.
- The main data type we need is a type that people can use to declare
new stacks, as in:
- Data types for a stack:
For the array implementation, we need to keep track of (at least) the array contents and a top index. How could we combine these 2 into a single C construct of type stackT?
Answer: Put them into astruct
.
So, our stack types become:
typedef char stackElementT; /* Give it a generic name--makes */ /* changing the type of things in */ /* the stack easy. */ typedef struct { stackElementT *contents; int top; /* Other fields? */ } stackT;
Are any other fields needed? Well, remember that the maximum size of the array is determined at run-time...We'll probably need to keep that value around so that we can tell when the stack is full... The final type, thus, is:
typedef struct { stackElementT *contents; int top; int maxSize; } stackT;
stack.h
.
- Filling in stack functions:
Now that we've decided on the data types for a stack, let's think about the functions we need...
First, we need the standard stack operations:
StackPush() StackPop() StackIsEmpty()
StackIsEmpty
). That will help us down the line. For example, suppose we use 2 different data structures in a program, both with IsEmpty operations--our naming convention will prevent the 2 different IsEmpty functions from conflicting.
We'll also need 2 extra operations:
StackInit() StackDestroy()
Finally, while the array that holds the contents of the stack will be dynamically-allocated, it still has a maximum size. So, this stack is unlike the abstract stack in that it can get full. We should add something to be able to test for this state:
StackIsFull()
- StackInit():
The first function we'll implement isStackInit()
. It will need to set up astackT
structure so that it represents an empty stack.
Here is what the prototype forStackInit()
looks like...
void StackInit(stackT *stackP, int maxSize);
). It also needs to know what the maximum size of the stack will be (i.e.,stackT * maxSize
).
Now, the body of the function must:
- Allocate space for the contents.
- Store the maximum size (for checking fullness).
- Set up the top.
void StackInit(stackT *stackP, int maxSize) { stackElementT *newContents; /* Allocate a new array to hold the contents. */ newContents = (stackElementT *)malloc(sizeof(stackElementT) * maxSize); if (newContents == NULL) { fprintf(stderr, "Insufficient memory to initialize stack.\n"); exit(1); /* Exit, returning error code. */ } stackP->contents = newContents; stackP->maxSize = maxSize; stackP->top = -1; /* I.e., empty */ }
NULL
). Also, note that if the stack was not passed by reference, we could not have changed its fields.
- StackDestroy():
The next function we'll consider is the one that cleans up a stack when we
are done with it. It should get rid of any dynamically-allocated memory and set
the stack to some reasonable state.
This function only needs the stack to operate on:
void StackDestroy(stackT *stackP);
void StackDestroy(stackT *stackP) { /* Get rid of array. */ free(stackP->contents); stackP->contents = NULL; stackP->maxSize = 0; stackP->top = -1; /* I.e., empty */ }
- StackIsEmpty() and StackIsFull():
Let's look at the functions that determine emptiness and fullness. Now, it's
not necessary to pass a stack by reference to these functions, since they do not
change the stack. So, we could prototype them as:
int StackIsEmpty(stackT stack); int StackIsFull(stackT stack);
StackInit()
, etc.) and some would not. It is more consistent to just pass stacks by reference (with a pointer) all the time. Furthermore, if the structstackT
is large, passing a pointer is more efficient (since it won't have to copy a big struct).
So, our prototypes will be:
int StackIsEmpty(stackT *stackP); int StackIsFull(stackT *stackP);
Emptiness
Now, testing for emptyness is an easy operation. We've said that the top field is -1 when the stack is empty. Here's a simple implementation of the function...int StackIsEmpty(stackT *stackP) { return stackP->top < 0; }
Fullness
Testing for fullness is only slightly more complicated. Let's look at an example stack. Suppose we asked for a stack with a maximum size of 1 and it currently contained 1 element (i.e., it was full)...
stack (made up of 'contents' and 'top') ----- ----- | A | | 0 | ----- ----- 0 top contents
0 = 1 - 1 ), then it is full. Thus, our fullness function is...
int StackIsFull(stackT *stackP) { return stackP->top >= stackP->maxSize - 1; }
maxSize
.
- StackPush():
Now, pushing onto the stack requires the stack itself as well as
something to push. So, its prototype will look like:
void StackPush(stackT *stackP, stackElementT element);
void StackPush(stackT *stackP, stackElementT element) { if (StackIsFull(stackP)) { fprintf(stderr, "Can't push element on stack: stack is full.\n"); exit(1); /* Exit, returning error code. */ } /* Put information in array; update top. */ stackP->contents[++stackP->top] = element; }
++
operator. It increments the top index before it is used as an index in the array (i.e., where to place the new element).
Also note how we just reuse theStackIsFull()
function to test for fullness.
- StackPop():
Finally, popping from a stack only requires a stack parameter, but the value
popped is typically returned. So, its prototype will look like:
stackElementT StackPop(stackT *stackP);
stackElementT StackPop(stackT *stackP) { if (StackIsEmpty(stackP)) { fprintf(stderr, "Can't pop element from stack: stack is empty.\n"); exit(1); /* Exit, returning error code. */ } return stackP->contents[stackP->top--]; }
- Stack module:
Finally, don't forget that we are putting this stack in its own module. The
stack types and function prototypes should go in
stack.h
. The stack function definitions should go instack.c
.
People that need to use the stack must includestack.h
and link their code withstack.c
(really, link their code with its object file,stack.o
).
Finally, since we wrote the types and functions for a stack, we know how to use a stack. For example, when you need stacks, declare stack variables:
stackT s1, s2;
StackInit(&s1, 10); StackPush(&s1, 'Z');
- Testing the implementation:
We've written the complete stack module for you (
stack.h
andstack.c
). Also, here's a sample main programstacktest.c
and aMakefile
.
The program demonstrates one use of the ordering produced by a stack...What is it?
No comments:
Post a Comment