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 - the last data element stored on a stack is the first data element retrieved from it. 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 dishwasher (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).

An illustration of a stack as a pile of elements. Programs push new elements on the top of the stack and remove them by popping them off the top of the stack.
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 important data structures in their own right. Programmers can implement stacks in many ways, including arrays, which we do as an array example. The bottom of the stack is the first array element (i.e., the element at index location 0). The stack top is always changing as elements are pushed on and popped off the stack. So, we'll use a second variable, called a stack pointer, to keep track of the stack top. The stack pointer is just an index into the array that represents the stack.

A picture of a stack implemented as an open-ended array. The array is drawn so that element 0 is at the bottom and the index increases upward. An integer variable named sp (for stack pointer) is initialized to 0, which is where the program puts the first element pushed on the stack.
Implementing a stack as an array. The array is drawn vertically rather than horizontally to better match the typical depiction of a stack, with the beginning of the array at the bottom and the other end left open. The array index numbers are on the left side of the array. Initially, the stack is empty, denoted by setting sp to 0.

Implementing a stack as an array has two problems. First, the size of the stack (i.e., the maximum number of elements that the stack can hold) must be a compile-time constant. Second, the type of data stored in the stack (i.e., the array type) is specified when the array is defined - when we write the code. We'll fix these problems as we refine our initial implementation in subsequent chapters, but we won't see the final, clean result until we cover templates near the end of the text.

The following discussion describes how stacks work generally and as implemented with arrays specifically. 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 symbolic constant implemented with 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 onto it.

Operation Picture Execution
Stack is empty The stack pointer, sp, indexes into the array st and logically determines how many elements are on the stack. The stack st is empty and sp is 0.  
push('A');
The letter 'A' is pushed on the stack and is stored at st[0]. sp is incremented to 1.
st[0] = 'A';
sp = 0 + 1;
push('B');
The Letter 'B' is pushed on the stack and is stored at st[1]; sp is incremented to 2.
st[1] = 'B';
sp = 1 + 1;
push('C');
The Letter 'C' is pushed on the stack and is stored at st[2]; sp is incremented to 3.
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 is decremented to 2 and the element at sp[2], 'C,' is popped off the statck. sp[2] still stores a 'C,' but that index location is now logically empty - there is no need to physically remove the value from the array.
sp = 3 - 1;
return sp[2];
data = pop();
sp is decremented to 1 and the element at sp[1], 'B,' is popped off the statck.
sp = 2 - 1;
return sp[2];
data = pop();
sp is decremented to 0 and the element at sp[0], 'A,' is popped off the statck. Although the letters A, B, and C are still physically in the stack array st, the stack is logically empty because the stack pointer, sp, is 0.
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. We base the first implementation on automatic or local variables and the second on dynamic or heap variables. More elegant solutions based on classes and templates will follow in subsequent chapters.