CS140 Lecture notes -- Stacks

Motivation for Stacks

There are some problems in which we want to store information, and we want to access the more recently stored information first. A data structure that allows us to access information in this manner is called a stack. It is so-named because it resembles a stack of dishes. When you want a dish, you take it off the stack of dishes. When you clean a dish, you put it back on top of the stack of dishes. The same holds with a stack of data. When you store a new data item, you store it on top of the stack. When you want to retrieve a data item, you retrieve the top item from the stack. Note that this data item will be the one most recently stored on the stack.

A couple examples where you want to access information in this manner in the real world are as follows:

  1. reversing the lines in a file: If you want to reverse the lines in a file, an obvious technique is to store them in a doubly linked list and then traverse the list in reverse. Another way to do so is to store the lines in a stack, and then remove the lines from the stack once the entire file is read. The last lines read will be the top lines on the stack, so removing the lines one at a time from the stack will have the effect of reversing the file.

  2. call stacks: Whenever your C program calls a function, the C compiler creates a so-called stack record that contains important information about that function, such as storage for its parameters, its return value, and its local variables. The C compiler pushes this stack record onto something called a call stack. The call stack is organized so that the stack record of the most recently called function is on top of the stack, and the stack record of the least recently called function, which in the case of C is main, is at the bottom of the stack. For example, if main calls A, which in turn calls B, then the call stack would be:
    By convention stacks are shown growing upwards. While a function is executing, its stack record will be on top of the stack, and hence its information will be readily available. In the above case, B is currently executing and so we can readily access its parameters and local variables. If B calls a new function C, then a stack record for C is created and pushed onto the call stack. C now has access to its local variables and parameters. When C returns, we want B to resume executing, and thus we want access to B's stack record. By simply popping C's stack record off the call stack, we again have access to B's stack record. Hence a stack is the perfect data structure for implementing a call stack, because we always want access to the most recently called function, and when it returns, we want access to the function that called it.

  3. Balancing parantheses: In many editors, it is common to want to know which left paranthesis (or brace or bracket, etc) will be matched when we type a right paranthesis. Clearly we want the most recent left parenthesis. Therefore, the editor can keep track of parentheses by pushing them onto a stack, and then popping them off as each right parenthesis is encountered.

Interface for Stacks

Our interface for a stack will be as follows:

Notice that in keeping with our policy of information hiding, we return a "void *" handle to the user. We will declare the structs that implement our stack within the .c file that implements the stack.

There is an argument that could be made for allowing the user to specify either a default initial size for the stack, or alternatively, a maximum size of the stack. I have chosen not to do so for simplicity sake. A full strength commercial version of a stack would probably give the user both options.

Stack Implementation

Stacks can be implemented using either arrays or lists. With an array we add items to the end of the array and with a list we add items to the end of the list. An array uses less space, because it does not require a next pointer, and it tends to be more efficient because the stack items are kept physically continguous and because the bookkeeping is slightly simpler. However, arrays are fixed size and hence the user must either specify a maximum bound on the size of the array or we must be prepared to re-size the array if the array overflows. Lists are more flexible because they can grow arbitrarily large without having to worry about overflow. Of course if they get too large, malloc could run out of memory, but lists typically do not become that large.

Array Implementation

The array implementation needs to maintain three pieces of information:

  1. a pointer to the array of data values
  2. the index of the top item in the array
  3. the current capacity of the array: If we try to push an element onto the array and the array is filled to capacity, we will either need to raise an error condition, or better yet, resize the array. In these notes we resize the array using re-alloc.

Here is the struct used to implement the array. Note that is a container object for the array. The "separate" element that we use to hold the stack is an array:

#define INIT_SIZE 100

typedef struct
	void **values; 	// The array of values.
	int top_of_stack;  // The top of the stack.
	int size;	// Current capacity of the stack.
} Stack;
Note that each entry in the array is a "void *" pointer, and hence our values array is a "void **". The INIT_SIZE is the initial size of our stack and the RESIZE_FACTOR is the amount by which we increase the stack capacity if the array becomes full. In this case we double the array's size each time it becomes full. If the user were allowed to specify a default initial size for the stack, or a maximum bound for the stack, then we would omit the INIT_SIZE constant and instead malloc an array of the size specified by the user.

The code for the array implementation of the stack can be found here.

List Implementation

The book uses a typedef to equate a stack with a list. I do not like this implementation because it requires that a stack be a list. I prefer instead to create a container object for a stack that contains a pointer to either a singly or doubly-linked list:

typedef struct
	Dllist *list;
} Stack;
Push will prepend items to the front of the list and pop will remove the first item from the list. Hence both pop and top can use dll_first to find the top element of the list. You will be coding the list implementation for stacks in one of your labs.