CS140 Lecture notes -- Stacks

  • Jim Plank
  • Directory: /home/plank/cs140/Notes/Stacks
  • Lecture notes: http://www.cs.utk.edu/~plank/plank/classes/cs140/Notes/Stacks/
  • Tue Sep 18 17:29:20 EDT 2007

    Stacks

    A stack is a primitive data structure, having a property called LIFO, which means ``last in, first out.'' Basically, a stack has three main operations: That's it. We have a stack header file in /home/plank/cs140/include/stack.h, which defines some extra operations which we will discuss later in the lecture. Of course, Stack is a (void *). We will discuss the implementation later, but it is in /home/plank/cs140/Fall-2006/objs/stack.o, and its source code is in /home/plank/cs140/Notes/Stacks/stack.c.

    You will need to link stack.o with libfdr.a, as is done in this directory's makefile.

    Stacksimp.c shows a very simple example of using a stack. First, we push three integers on the stack -- 1, 2 and 3. Then we call stack_pop() twice, and print out the values. Finally, we push 4 onto the stack, and call stack_pop() twice more, printing out the values.

    Here's the code:

    #include <stdio.h>
    #include "jval.h"
    #include "stack.h"
    
    main()
    {
      Jval jv;
      Stack s;
    
      s = new_stack();
    
      stack_push(s, new_jval_i(1));
      stack_push(s, new_jval_i(2));
      stack_push(s, new_jval_i(3));
    
      jv = stack_pop(s);
      printf("First pop: %d\n", jv.i);
      jv = stack_pop(s);
      printf("Second pop: %d\n", jv.i);
      
      stack_push(s, new_jval_i(4));
    
      jv = stack_pop(s);
      printf("Third pop: %d\n", jv.i);
      jv = stack_pop(s);
      printf("Fourth pop: %d\n", jv.i);
    }
    

    (Note you can do this without declaring jv -- see simp2.c).

    And here's its output:

    UNIX> stacksimp
    First pop: 3
    Second pop: 2
    Third pop: 4
    Fourth pop: 1
    UNIX>
    
    So, let's look at this in detail. You can envision a stack as a list of items, where items are added to (pushed) and taken from (popped) the same end of the list.

    Thus, after the first call to stack_push(), the stack s is the list (1). After the second call, s is (1, 2), and after the third call, s is (1, 2, 3).

    Now, the first call to stack_pop() removes the last element in s: 3. Then the s is (1, 2). The second call removes the element 2, and the list becomes (1). Thus, to this point, the output is:

    UNIX> stacksimp
    First pop: 3
    Second pop: 2
    
    Now, we call stack_push(s, new_jval_i(4)), which puts 4 at the end of s. S is now (1, 4). The third call to stack_pop() removes the 4, and s is once again (1). Finally, the last call to stack_pop() removes and returns the 1, and the stack is empty. Thus, the output following the third call to stack_pop() is:
    Third pop: 4
    Fourth pop: 1
    UNIX>
    

    The full stack interface

    Here are all of the procedures defined in stack.h: If you call stack_pop() or stack_top() on an empty stack, your program will exit with an error.

    Two more examples of using the stack data structure

    The first such example is reversing the lines of standard input. A stack is also a natural data structure for this. We'll read lines in from standard input and push them onto a stack. Then when standard input is done, we'll pop lines off the stack and print them. This will print them in reverse order.

    Here's the code (in stackrev.c). Note how we use the jval to hold a string. Also note how convenient new_jval_s() and jval_s() are.

    #include <stdio.h>
    #include <string.h>
    #include "jval.h"
    #include "stack.h"
    #include "fields.h"
    
    main()
    {
      IS is;
      Stack *s;
    
      s = new_stack();
      is = new_inputstruct(NULL);
    
      while (get_line(is) >= 0) {
        stack_push(s, new_jval_s(strdup(is->text1)));
      }
    
      while (!stack_empty(s)) {
        printf("%s", jval_s(stack_pop(s)));
      }
    }
    
    

    This runs just like we think it should:

    UNIX> cat input
    Give
    Him
    Six!
    UNIX> stackrev < input
    Six!
    Him
    Give
    UNIX> 
    
    The first three calls to stack_push() create s: ( "Give\n", "Him\n", "Six!\n" ). The first call to stack_pop removes the "Six!\n" and turns s into: ( "Give\n", "Him\n" ). The second call removes "Him\n", and the third removes "Give\n". At that point, s is empty, and therefore stack_empty(s) returns 1 and the while loop is exited.

    The second example is implementing tail with a stack. What we do here is again read all of standard input into a stack. Then we call stack_size() to see how many lines are in the stack. We want to print out the smaller of the number of lines in the stack, and the first command line argument to tail. In other words, if we call ``tail 10'' and there are 5 lines in standard input, then we want to print out 5 lines. If there are 15 lines in standard input, then we want to print out 10 lines.

    Once we know how many lines we want to print (call it nl), we call stack_pop() nl times. This will get the last nl lines of standard input. The problem is that it gets them backwards. We can't just call stack_pop and then printf(), because that would give us the last nl lines in reverse order. Therefore, we must pop them and put them into an array, and then print them by traversing the array in the correct order.

    This is done in stacktail.c:

    #include <stdio.h>
    #include <string.h>
    #include "jval.h"
    #include "stack.h"
    #include "fields.h"
    
    main(int argc, char **argv)
    {
      IS is;
      Stack *s;
      Jval j;
      char **lines;
      int nl, i;
    
      /* Get the number of lines */
    
      if (argc != 2) {
        fprintf(stderr, "usage: stacktail n\n");
        exit(1);
      }
      nl = atoi(argv[1]);
      if (nl <= 0) exit(0);
    
      /* Intialize stack and input */
    
      s = new_stack();
      is = new_inputstruct(NULL);
    
      /* Push each line onto the stack */
    
      while (get_line(is) >= 0) {
        stack_push(s, new_jval_s(strdup(is->text1)));
      }
    
      /* Set nl to be the min of the given nl and 
         the number of lines in standard input. */
    
      if (stack_size(s) < nl) nl = stack_size(s);
      if (nl == 0) exit(0);
     
      /* Allocate an array to hold nl lines, and pop the last nl lines 
        off the stack into that array.  We put them into the array in 
        reverse order so that the array is in the right order.  */
    
      lines = (char **) malloc(sizeof(char *)*nl);
      if (lines == NULL) { perror("malloc lines"); exit(1); }
    
      for (i = nl-1; i >= 0; i--) {
        lines[i] = jval_s(stack_pop(s));
      }
    
      /* Print out the lines by traversing the array forwards */
    
      for (i = 0; i < nl; i++) {
        printf("%s", lines[i]);
      }
    }
    
    

    And of course, it works like you think it would:

    UNIX> stacktail 2 < input
    Him
    Six!
    UNIX> stacktail 5 < input
    Give
    Him
    Six!
    UNIX> stacktail 5 < stacktail.c
      for (i = 0; i < nl; i++) {
        printf("%s", lines[i]);
      }
    }
    
    UNIX> 
    
    Note, instead of an array, we could have used a second stack: (in stacktail2.c):

    #include <stdio.h>
    #include <string.h>
    #include "jval.h"
    #include "stack.h"
    #include "fields.h"
    
    main(int argc, char **argv)
    {
      IS is;
      Stack *s, *s2;
      Jval j;
      int nl, i;
    
      /* Get the number of lines */
    
      if (argc != 2) {
        fprintf(stderr, "usage: stacktail n\n");
        exit(1);
      }
      nl = atoi(argv[1]);
      if (nl <= 0) exit(0);
    
      /* Intialize stack and input */
    
      s = new_stack();
      is = new_inputstruct(NULL);
    
      /* Push each line onto the stack */
    
      while (get_line(is) >= 0) {
        stack_push(s, new_jval_s(strdup(is->text1)));
      }
    
      /* Set nl to be the min of the given nl and 
         the number of lines in standard input. */
    
      if (stack_size(s) < nl) nl = stack_size(s);
      if (nl == 0) exit(0);
     
      /* Allocate a second stack, and push on the last nl elements */
    
      s2 = new_stack();
    
      for (i = 0; i < nl; i++) {
        stack_push(s2, stack_pop(s));
      }
    
      /* Print out the lines by popping them off the second stack */
    
      while (!stack_empty(s2)) {
        j = stack_pop(s2);
        printf("%s", j.s);
      }
    }
    


    Some commentary

    Now, all the above is well and good. However, implementing tail in this way is, in my opinion, convoluted. While you can do it with a stack, it is not natural, since you have to reverse the elements. A queue (next lecture) is much more natural, as is a doubly linked list (later). I just used the above stacktail programs to illustrate the use of a stack.

    Implementing a stack

    This code is in stack.c.

    To implement a stack, you use two structs -- one which will be the main struct for the stack, and one for each node in the stack. Here they are (in reverse order).

    typedef struct stacknode {
      struct stacknode *next;
      Jval val;
    } Stack_Node;
    
    typedef struct {
      int size;
      Stack_Node *top;
    } True_Stack;
    
    The top of a stack points to NULL if the stack is empty, and it points to the top Stack_Node on the stack if the stack is non-empty. Each Stack_Node points to the next Stack_Node in the stack with its next pointer, unless it is the last Stack_Node, in which its next pointer is NULL.

    Here are three stack examples:

    Most of the code is straightforward. I won't go over new_stack(), stack_top(), stack_empty() or stack_size(). Look over their implementations in stack.c.

    The two subtle pieces of code are stack_push() and stack_pop(). Here is stack_push():

    void stack_push(Stack s, Jval jv)  /* Push an element onto the stack */
    {
      True_Stack *ts;
      Stack_Node *n;
    
      ts = (True_Stack *) s;
      n = (Stack_Node *) malloc(sizeof(Stack_Node));
      if (n == NULL) { perror("stack_push"); exit(1); }
      n->next = ts->top;
      n->val = jv;
      ts->top = n;
      ts->size++;
    }
    
    It allocates a new node, and then puts it in front of the previous top of the stack. Here are pictures of it working:

    When it's done, it returns, and we can redraw the stack as follows:

    Stack_pop() works as follows. You save the top of the stack and its val. Then you move the top of the stack to the next element, free the old top of the stack, and return the stored val:

    Jval stack_pop(Stack s)  
    {
      True_Stack *ts;
      Jval jv;
      Stack_Node *n;
    
      ts = (True_Stack *) s;
      if (ts->size == 0) {
        fprintf(stderr, "stack_pop -- popping empty stack\n"); 
        exit(1);
      }
      n = (ts->top);
      ts->top = n->next;
      jv = n->val;
      free(n);
      ts->size--;
      return jv;
    }
    
    Here it is working:

    When it's done, it returns jv, and we can redraw the state of the program as follows: