7.10. Stacks And Stack Operations

Time: 00:03:51 | Download: Large Small | Streaming | Slides (PDF)

A stack is a simple last-in, first-out (LIFO) data structure. That is, the last data element stored in a stack is the first data element retrieved from the stack. The common analogy is a stack of plates in a cafeteria: when you go through the line, you pop the top plate off the stack; the dish washer (stepping away from reality a bit), pushes a single clean plate on top of the stack. So a stack supports two basic operations: push and pop. Some stacks also provide additional operations: size (the number of data elements currently on the stack) and peek (look at the top element without removing it).

The primary stack operations. A new data element is stored by pushing it on the top of the stack. A data element is retrieved by popping the top element off the top of the stack and returning it.

Stacks are an important data structure in their own right and they may be implemented in several ways. We will use one of the ways of building a stack as an example of how to create and use arrays. The bottom of the stack is the first element of the array (i.e., the element at index location 0). But how do we know the location of the element at the top of the stack? Keeping track of the top element requires a second bit of information, an integer called the stack pointer that denotes the current top of the stack. The stack pointer is just an index into the array that represents the stack.

Implementing a stack as an array. To better match the typical depiction of a stack, the array is drawn vertically rather than horizontally with the beginning of the array at the bottom and the other end left open. The array index numbers are shown on the left side of the array. Initially, the stack is empty, which is denoted by setting sp to 0.

We encounter two problems when we implement a stack as an array. Both the size of the stack (i.e., the maximum number of elements that the stack can hold) and the type of data that can be stored in the stack are limited by the requirement that the size and type of the array must be specified when the array is defined (i.e., when we write the code). We will return to and refine this initial implementation of a stack in subsequent chapters. Each refinement will get us closer to removing these (and other) problems but we won't see the final, clean result until we cover templates near the end of the text.

The following discussion focuses on how stacks work or behave and how that behavior can be achieved with arrays. So for now, we "solve" the two problems presented above by simply creating a stack that can only store characters implemented as a char array whose size is left ambiguous (specified as a macro, enum, or const).

char	st[SIZE];
int	sp = 0;

Stack Behavior

The various stack operations are easy to implement, but notice that push and pop use post-increment and pre-decrement respectively (this is crucial for the algorithm to work).

push
st[sp++] = data (sp must be < SIZE)
pop
return st[--sp] (sp must be > 0)
size
return sp;
peek
return st[sp-1]

Based on these operations, the snapshots shown in Figure 3 illustrate the appearance of a stack as data (characters) are stored in or pushed on to it.

Operation Picture Execution
Stack is empty  
push('A');
st[0] = 'A';
sp = 0 + 1;
push('B');
st[1] = 'B';
sp = 1 + 1;
push('C');
st[2] = 'C';
sp = 2 + 1;
The push operation illustrated. Each call to the push function (left column) pushes a data element on to the stack. The main instruction in the push function is st[sp++] = data, where "data" is the function argument. The middle column abstractly illustrates how the stack (the array and the stack pointer) appear after each call to the push function. The right column breaks the behavior of the push function in to two steps.

Similarly, the data (characters) stored in a stack can be retrieved from or popped off of it.

Operation Picture Execution
data = pop();
sp = 3 - 1;
return sp[2];
data = pop();
sp = 2 - 1;
return sp[2];
data = pop();
sp = 1 - 1;
return sp[2];
The pop operation illustrated. Each call to the pop function (middle column) removes a data element from the top of the stack and returns it. The main instruction in the pop function is return st[--sp]. The middle column abstractly illustrates how the stack (the array and the stack pointer) appear after each call to the pop function. The right column breaks the behavior of the pop function in to two steps. Notice that the pop operations doesn't actually remove a character from the stack array, but the slots at and above the stack pointer are treated as empty. The next push operation will overwrite the data currently stored at the stack pointer. The stack appearing at the bottom of the table is logically empty.

Maintaining a stack as two discrete variables (an array and a stack pointer) is cumbersome, error-prone, and makes it difficult to support multiple stacks in a program. Fortunately, we can solve these problems (if somewhat inelegantly) if we implement a stack as a struct. But even after settling on a structure-based solution, there are still two possible paths that we can take: the first implementation is based on automatic variables while the second is based on dynamic variables. More elegant solutions based on classes and templates will follow in subsequent chapters.