CS140 Lecture notes -- Implementation of Stacks

  • Jim Plank and Brad Vander Zanden
  • Directory: ~cs140/www-home/notes/StackImp
  • Lecture notes: http://www.cs.utk.edu/~cs140/notes/StackImp

    Array Implementation of Stacks

    Stacks can be implemented using arrays if the size of the stack is known when the stack is created. The size does not have to be known when you are writing the program, it just has to be known when you make the call to the procedure that creates the stack. An array will store the elements of a stack from left to right in the array, with the bottom most element being placed at location 0.The stack will need to maintain an integer variable called top that points to the top element of the stack. For example, the following diagram shows a stack that has a capacity of 10 and that consists of the five elements 8, 6, 3, 9, and 12:

      0   1   2   3   4    5   6   7   8   9
    ------------------------------------------
    | 8 | 6 | 3 | 9 | 12 | ? | ? | ? | ? | ? |
    ------------------------------------------
    		  ^
    		  |
    		 top = 4
    
    Since top points to the current top element of the stack it must be set initially to -1. Each time a push is performed top is incremented by 1 and each time a pop is performed top is decremented by 1.

    Our stack data structure can be declared as follows:

    typedef struct {
        Jval *stack;    // an array of Jvals
        int top;        // pointer to the top of the stack
        int capacity;   // the maximum number of elements this stack can hold
    } array_stack;
    
    This declaration will be placed in the .c file that defines the implementation of the stack. In the .h file that provides the public declaration of a stack, we will include the statement:
    typedef void * ArrayStack;
    
    By placing this declaration in the .h file and the struct statement in the .c file we hide the implementation of the stack data structure from the user. As a reminder, this technique is called data encapsulation or modularization.


    Stack Creation

    The code to create a stack must take the size of the stack as input, allocate an array of Jvals that is equal to the size of the stack, initialize top to -1, and initialize capacity to the size of the stack:

    ArrayStack new_arraystack(int size) {
        array_stack *new_stack = (array_stack *)malloc(sizeof(array_stack));
        new_stack->stack = (Jval *)malloc(sizeof(Jval) * size);
        new_stack->top = -1;
        new_stack->capacity = size;
        return (ArrayStack) new_stack;
    }
    

    The size, empty, and top commands

    The size procedure returns the value of top incremented by 1. Note that when the stack is empty top will be set to -1 so the size procedure will return 0, which is correct:

    int arraystack_size(ArrayStack s) {
        array_stack *my_stack = (array_stack *)s;
        return my_stack->top + 1;
    }
    
    The empty procedure is equally simple:
    int arraystack_empty(ArrayStack s) {
        return (arraystack_size(s) == 0);
    }
    
    The top procedure needs to ensure that the stack is not empty before returning the top element:
    Jval arraystack_top(ArrayStack s) {
        if (arraystack_empty(s)) {
            fprintf(stderr, "Error: tried to access the top element on an empty stack\n");
            exit(1);
        }
        array_stack *my_stack = (array_stack *)s;
        return my_stack->stack[my_stack->top];
    }
    

    Pushing An Element Onto The Stack

    The push procedure stores a value in the next available position in the stack. It first ensures that the push will not cause the stack to exceed its capacity:

    void arraystack_push(ArrayStack s, Jval v) {
        array_stack *my_stack = (array_stack *)s;
        if (arraystack_size(s) == my_stack->capacity) {
            fprintf(stderr, "Error: arraystack_push called on a full stack\n");
            exit(1);
        }
        my_stack->top++;
        my_stack->stack[my_stack->top] = v;
    }
    

    Popping An Element From The Stack

    The pop procedure removes the top value from the stack by decrementing top and returning the top value to the caller:

    Jval arraystack_pop(ArrayStack s) {
        array_stack *my_stack = (array_stack *)s;
        Jval top_element = arraystack_top(s);
        my_stack->top--;
        return top_element;
    }
    

    Deleting A Stack

    The free_arraystack procedure frees the space allocated for the stack array and then frees the stack header record:

    void free_arraystack(ArrayStack s) {
        array_stack *my_stack = (array_stack *)s;
        free(my_stack->stack);
        free(my_stack);
    }
    

    List Implementation of Stacks

    The implementation of stacks is in the file /home/cs140/spring-2004/src/stack.c. You should study this well and get to know all of the parts. The stack data structure and its use are described in the stacks lecture.

    The typedef of a stack in stack.h sets a stack to be a (void *). In the implementation of stacks, we define what our stack type really is. When we create a stack using new_stack(), we will allocate one of our own stacks, and then pass it to the caller as a (void *). In all of the other stack routines, we receive a (void *) as a parameter. The first thing that we do is cast that to one our true stack data structures, since we assume that the stack was created with new_stack().

    Now, our stack data structure has two parts -- a header, and the items in the stack. The header is defined as follows:

    typedef struct {
      StackNode *top;
      int size;
    } TrueStack;
    
    This is what we'll allocate in new_stack(), and when we get a Stack in the other stack calls, we'll cast it to a (TrueStack *). The first fields are the top of the stack, which is a pointer to the first item in the stack. If the stack is empty, top is NULL. The second field is the number of items in the stack.


    new_stack(), stack_size(), stack_empty()

    Given just this definition of a stack, we can write new_stack(). This allocates a TrueStack, sets top to NULL and size to zero. It then returns a pointer to the TrueStack, cast to a Stack, which is of course a (void *).
    Stack new_stack()
    {
      TrueStack *ts;
    
      ts = (TrueStack *) malloc(sizeof(TrueStack));
      ts->top = NULL;
      ts->size = 0;
      return (Stack) ts;
    }
    
    Also, the implementation of stack_size() is straightforward -- it simply returns the size field:
    int stack_size(Stack s)
    {
      TrueStack *ts;
    
      ts = (TrueStack *) s;
      return ts->size;
    }
    
    And stack_empty() is simple as well -- it returns whether the stack size is zero. Note, I've done this by calling stack_size() rather than checking the size field. The reason for doing this is that if I somehow change the implementation of stacks, I won't have to change stack_empty().
    int stack_empty(Stack s)
    {
      return (stack_size(s) == 0);
    }
    

    stack_push()

    When a stack is not empty, the size field contains the number of items on the stack, and the top field points to a linked list node containing the top item on the stack. This node is a StackNode, defined as follows:
    typedef struct stacknode {
      struct stacknode *link;
      Jval val;
    } StackNode;
    
    Each node contains a value in its val field and a link field that points to the next node on the stack. The last (bottom) node on the stack points to NULL as its next node.

    Thus, suppose you have a stack s created with the following sequence of calls:

      s = new_stack();
      stack_push(s, new_jval_i(34));
      stack_push(s, new_jval_i(-4));
    
    S will be a (void *). When passed to any stack routine, it will be cast to a TrueStack with two fields -- its size is 2, and its top field points to the top node on the stack, which is a StackNode struct. This also has two fields: a value, which is -4, and a link that points to the second node on the stack. This is a StackNode struct with a value of 34 and a link with a value of NULL because there are no more nodes on the stack.

    A high-level picture of the stack is as follows:

    s -----> |---------------|   /--> |---------------| /---> |-------------|
             |    top -----------/    |   link ---------/     | link = NULL |
             |    size = 2   |        |   val.i = -4  |       | val.i = 34  |
             |---------------|        |---------------|       |-------------|
    
    So, stack_push(j) must be written so that it takes a stack with size nodes, and turns it into a stack with size+1 nodes, the first of which is a new node with a val of j.

    Here is the code:

    stack_push(Stack s, Jval val)
    {    
      StackNode *sn;
      TrueStack *ts;
         
      ts = (TrueStack *) s;
    
      /* Create the new node */
      sn = (StackNode *) malloc(sizeof(StackNode));
      sn->val = val;
      sn->link = ts->top;
    
      /* Put the new node at the beginning of the stack */
      ts->top = sn;
      ts->size++;
    }    
    
    Suppose we have called:
      s = new_stack();
      stack_push(s, new_jval_i(34));
    
    and now we make the call
      stack_push(s, new_jval_i(-4));
    
    When we enter stack_push(), s is in the following state:
    s -----> |---------------|   /--> |-------------|
             |    top -----------/    | link = NULL |
             |    size = 1   |        | val.i = 34  |
             |---------------|        |-------------|
    
    The first thing we do is cast s to a TrueStack which we call ts, and then malloc() a StackNode. This gives us a struct with two field, pointed to by sn. I put the question marks for link and val, because we don't know what they are yet.
    s, ts -> |---------------|   /--> |-------------|
             |    top -----------/    | link = NULL |
             |    size = 1   |        | val.i = 34  |
             |---------------|        |-------------|
    
    sn ----> |-------------|
             | link = ?    |
             | val   = ?   |
             |-------------|
    
    Now, we set val to the argument of stack_push(), and link to the current top of the stack:
    s, ts -> |---------------|   /--> |-------------|
             |    top -----------|    | link = NULL |
             |    size = 1   |   |    | val.i = 34  |
             |---------------|   |    |-------------|
                                 |
    sn ----> |-------------|     |
             | link = -----------/
             | val.i = -4  |
             |-------------|
    
    Then, we set ts->top to sn. This has the effect of putting sn at the front of the list. Finally, we increment size. When we're done, it looks like:
    s, ts -> |---------------|                            /--->|-------------|
             |    top -----------\                        |    | link = NULL |
             |    size = 2   |   |                        |    | val.i = 34  |
             |---------------|   |                        |    |-------------|
                                 |                        |
    sn --------------------------+----> |-------------|   | 
                                        | link = ---------/
                                        | val.i = -4  |
                                        |-------------|
    
    When we return from stack_push(), all the user has is s (ts and sn go away, of course), and this is now a pointer to a stack with two nodes:
    s -----> |---------------|                            /--->|-------------|
             |    top -----------\                        |    | link = NULL |
             |    size = 2   |   |                        |    | val.i = 34  |
             |---------------|   |                        |    |-------------|
                                 |                        |
                                 \----> |-------------|   | 
                                        | link = ---------/
                                        | val.i = -4  |
                                        |-------------|
    
    If this is still confusing to you, do one of two things -- copy stack.c and put some printf() statements into it so that you can take a look at memory at each step. If you have gotten to know gdb, you can use gdb to do the same thing.

    Using gdb to step through stack_push()

    First, look at stackex1.c
    #include 
    #include "stack.h"
    
    main()
    {
      Stack s;
     
      s = new_stack();
      stack_push(s, new_jval_i(34));
      stack_push(s, new_jval_i(-4));
    }
    
    We compile it with the -g flag, which means to insert information for a debugger like gdb. Now, we run gdb on stackex1:
    UNIX> gdb stackex1
    GDB is free software and you are welcome to distribute copies of it
     under certain conditions; type "show copying" to see the conditions.
    There is absolutely no warranty for GDB; type "show warranty" for details.
    GDB 4.16 (sparc-sun-solaris2.5.1), 
    Copyright 1996 Free Software Foundation, Inc...
    
    The first thing we do is put a ``breakpoint'' in stack_push(). This tells gdb to stop running the program when it calls stack_push() so that we can examine things:
    (gdb) break stack_push
    Breakpoint 1 at 0x10f8c: file stack.c, line 86.
    
    Now, we run the program. It will stop it when it hits the stack_push(s, new_jval_i(34)) call:
    (gdb) run
    Starting program: /a/hasbro/lymon/homes/cs140/www-home/notes/StackImp/stackex1 
    
    Breakpoint 1, stack_push (s=0x21e58, val={i = 34, l = 34, f = 4.76441478e-44, 
          d = 7.2147925488893527e-313, v = 0x22, 
          s = 0x22 < Address 0x22 out of bounds >, c = 0 '\000', uc = 0 '\000', 
          sh = 0, ush = 0, ui = 34, iarray = {34, 138840}, farray = {
            4.76441478e-44, 1.94556279e-40}, 
          carray = "\000\000\000\"\000\002\036X", 
          ucarray = "\000\000\000\"\000\002\036X"}) at stack.c:86
    86        ts = (TrueStack *) s;
    
    Note, it prints out where it is, along with the arguments to stack_push. Yes, the printing of the jval is yucky, but you can see that it equals the integer 34. It also says that it is about to execute line 86, where we cast s to ts. We can print out s, which is a pointer to 0x21e58:
    (gdb) print s 
    $1 = (void *) 0x21e58
    
    If we print out ts we see that it has a weird value. This is because we haven't actually set it yet:
    (gdb) print ts
    $2 = (TrueStack *) 0x22
    
    We use the step command to execute just one line. Here, we'll step through every line of stack_push(). If we wanted to, we could examine the state at every step. I'm going to wait until the end of stack_push():
    (gdb) step
    87        sn = (StackNode *) malloc(sizeof(StackNode));
    (gdb) step
    88        sn->val = val;
    (gdb) step
    89        sn->link = ts->top;
    (gdb) step
    90        ts->top = sn;
    (gdb) step
    91        ts->size++;
    (gdb) step
    92      }
    
    Ok, now we're at the end of stack_push(). We should have ts pointing to a TrueStack whose size is 1, and whose top points to a new StackNode. Sn should point to this StackNode as well. This node will have a link field of NULL since it is the only node on the stack, and a val whose .i field is 34. Check it out:
    (gdb) print s
    $3 = (void *) 0x21e58
    (gdb) print ts
    $4 = (TrueStack *) 0x21e58
    (gdb) print *ts
    $5 = {top = 0x22260, size = 1}
    (gdb) print sn
    $6 = (StackNode *) 0x22260
    (gdb) print *sn
    $7 = {link = 0x0, val = {i = 34, l = 34, f = 4.76441478e-44, 
        d = 7.2147925488893527e-313, v = 0x22, 
        s = 0x22 < Address 0x22 out of bounds >, c = 0 '\000', uc = 0 '\000', 
        sh = 0, ush = 0, ui = 34, iarray = {34, 138840}, farray = {4.76441478e-44, 
          1.94556279e-40}, carray = "\000\000\000\"\000\002\036X", 
        ucarray = "\000\000\000\"\000\002\036X"}}
    
    There you have it. Memory looks like:
             |--------------------------| 
    s,ts --->| top = 0x22260            | 0x21e58
             | size = 1                 | 0x21e5c
             |                          | 0x21e60
             |                          | 0x21e64
                      ...                           
    sn------>| link = 0  (NULL)         | 0x22260
             |                          | 0x22264
             | val.i = 34               | 0x22268
             |                          | 0x2226c
             |--------------------------|
    
    We type cont, and gdb will execute until the next breakpoint:
    (gdb) cont
    Continuing.
    
    Breakpoint 1, stack_push (s=0x21e58, val={i = -4, l = -4, f = -NaN(0x7ffffc), 
          d = -NaN(0xffffc00022260), v = 0xfffffffc, 
          s = 0xfffffffc < Address 0xfffffffc out of bounds >, c = -1 '', 
          uc = 255 '', sh = -1, ush = 65535, ui = 4294967292, iarray = {-4, 
            139872}, farray = {-NaN(0x7ffffc), 1.96002419e-40}, 
          carray = "\000\002\"`", ucarray = "\000\002\"`"}) at stack.c:86
    86        ts = (TrueStack *) s;
    
    Again, we step to the end and examine the state:
    (gdb) step
    87        sn = (StackNode *) malloc(sizeof(StackNode));
    (gdb) step
    88        sn->val = val;
    (gdb) step
    89        sn->link = ts->top;
    (gdb) step
    90        ts->top = sn;
    (gdb) step
    91        ts->size++;
    (gdb) step
    92      }
    (gdb) print s
    $8 = (void *) 0x21e58
    (gdb) print ts
    $9 = (TrueStack *) 0x21e58
    (gdb) print *ts
    $10 = {top = 0x22278, size = 2}
    (gdb) print sn
    $11 = (StackNode *) 0x22278
    (gdb) print *sn
    $12 = {link = 0x22260, val = {i = -4, l = -4, f = -NaN(0x7ffffc), 
        d = -NaN(0xffffc00022260), v = 0xfffffffc, 
        s = 0xfffffffc < Address 0xfffffffc out of bounds >, c = -1 '', 
        uc = 255 '', sh = -1, ush = 65535, ui = 4294967292, iarray = {-4, 
          139872}, farray = {-NaN(0x7ffffc), 1.96002419e-40}, 
        carray = "\000\002\"`", ucarray = "\000\002\"`"}}
    (gdb) print sn->link 
    $13 = (StackNode *) 0x22260
    (gdb) print *sn->link
    $14 = {link = 0x0, val = {i = 34, l = 34, f = 4.76441478e-44, 
        d = 7.2147925488893527e-313, v = 0x22, 
        s = 0x22 < Address 0x22 out of bounds >, c = 0 '\000', uc = 0 '\000', 
        sh = 0, ush = 0, ui = 34, iarray = {34, 138840}, farray = {4.76441478e-44, 
          1.94556279e-40}, carray = "\000\000\000\"\000\002\036X", 
        ucarray = "\000\000\000\"\000\002\036X"}}
    (gdb) 
    
    You'll note that ts->top is different -- it points to a new node which is now the top of the stack. That node's link field points to the old top of the stack. Memory looks like:
             |--------------------------| 
    s,ts --->| top = 0x22278            | 0x21e58
             | size = 2                 | 0x21e5c
             |                          | 0x21e60
             |                          | 0x21e64
                      ...                           
             | link = 0  (NULL)         | 0x22260
             |                          | 0x22264
             | val.i = 34               | 0x22268
             |                          | 0x2226c
             |                          | 0x22270
             |                          | 0x22274
    sn ----->| link = 0x22260           | 0x22278
             |                          | 0x2227c
             | val.i = -4               | 0x22280
             |                          | 0x22284
             |--------------------------|
    
    Of course, this is really the same as our above drawing:
    s -------> |---------------|   /---->|---------------| /-------->|-------------|
      0x21e58  |    top -----------/     |   link ---------/ 0x22260 | link = NULL |
               |    size = 2   | 0x22278 |   val.i = -4  |           | val.i = 34  |
               |---------------|         |---------------|           |-------------|
    
    You will really help your understanding of this if you go and play with gdb -- step through things and examine state. Gdb is a very powerful tool, and with just the knowledge of a few commands (break, run, step, cont -- learn next too), you can do a lot!

    stack_top()

    Stack_top() is relatively simple. It returns the val field of the top element of the stack. Note that if the stack is empty, there is no top element, and stack_top flags an error. Otherwise, the code is simple:
    Jval stack_top(Stack s)
    { 
      TrueStack *ts;
     
      if (stack_empty(s)) {
        fprintf(stderr, "Error: Stack_top called on an empty stack\n");
        exit(1);
      }
      ts = (TrueStack *) s;
      return ts->top->val;
    }
    

    stack_pop()

    Time to write stack_pop(). This returns the same value as stack_top, but also deletes the top element of the stack. Thus, the basic outline of stack_pop() will be:
    Jval stack_pop(Stack s)
    {
      Jval val;
    
      val = stack_top(s);
    
      /* Delete the top element of the stack */
      ...
    
      return val;
    }
    
    To delete the top element of the stack, we set ts->top to be ts->top->link. Also, we have to do the following: Here's the code:
    Jval stack_pop(Stack s)
    {
      Jval val;
      StackNode *sn;
      TrueStack *ts;
    
      val = stack_top(s);
      ts = (TrueStack *) s;
      sn = ts->top;
      ts->top = sn->link;
      ts->size--;
      free(sn);
      return val;
    }
    
    Let's go over an example. Suppose the user calls:
      Stack s;
      int i;
    
      s = new_stack();
      stack_push(s, new_jval_i(34));
      stack_push(s, new_jval_i(-4));
      i = jval_i(stack_pop(s));
    
    After the second call to stack_push(), s looks like:
    s -----> |---------------|                            /--->|-------------|
             |    top -----------\                        |    | link = NULL |
             |    size = 2   |   |                        |    | val.i = 34  |
             |---------------|   |                        |    |-------------|
                                 |                        |
                                 \----> |-------------|   |
                                        | link = ---------/
                                        | val.i = -4  |
                                        |-------------|
    
    Now, when stack_pop(s) is called, the first things we do is set val to stack_top(s), and cast s to ts.
    val.i = -4
    s,ts---> |---------------|                            /--->|-------------|
             |    top -----------\                        |    | link = NULL |
             |    size = 2   |   |                        |    | val.i = 34  |
             |---------------|   |                        |    |-------------|
                                 |                        |
                                 \----> |-------------|   |
                                        | link = ---------/
                                        | val.i = -4  |
                                        |-------------|
    
    Then, we set sn to ts->top:
    val.i = -4
    s,ts---> |---------------|                            /--->|-------------|
             |    top -----------\                        |    | link = NULL |
             |    size = 2   |   |                        |    | val.i = 34  |
             |---------------|   |                        |    |-------------|
                                 |                        |
    sn --------------------------+----> |-------------|   |
                                        | link = ---------/
                                        | val.i = -4  |
                                        |-------------|
    
    Then, we set ts->top to sn->link. We could have set it to ts->top->link, and it would have done the same thing:
    val.i = -4
    s,ts---> |---------------|            /---------------+--->|-------------|
             |    top --------------------/               |    | link = NULL |
             |    size = 2   |                            |    | val.i = 34  |
             |---------------|                            |    |-------------|
                                                          |
    sn -------------------------------> |-------------|   |
                                        | link = ---------/
                                        | val.i = -4  |
                                        |-------------|
    
    Then we free sn. This basically means that the StackNode pointed to by sn is gone and should not be used again. We now have:
    val.i = -4
    s,ts---> |---------------|            /------------------->|-------------|
             |    top --------------------/                    | link = NULL |
             |    size = 2   |                                 | val.i = 34  |
             |---------------|                                 |-------------|
    sn                                                     
    
    Finally, we decrement ts->size and return val. What we're left with is s as a one-element list, and the proper return value:
    return value.i = -4
    s -----> |---------------|     /------------>|-------------|
             |    top -------------/             | link = NULL |
             |    size = 1   |                   | val.i = 34  |
             |---------------|                   |-------------|
    
    You should be able to go through this with gdb too. Try it out.

    free_stack()

    Finally, free_stack() is what a user calls when he/she is done with a stack. What it does is call stack_pop() until the stack is empty, and then it frees the TrueStack.
    free_stack(Stack s)
    {
      TrueStack *ts;
    
      while (!stack_empty(s)) (void) stack_pop(s);
      ts = (TrueStack *) s;
      free(ts);
    }
    
    You actually do not even need to do the cast here to ts-- you could just call free(s). You'll learn why in CS360. Also, I use the (void) cast to tell the compiler that we do not care about the return value of stack_pop().