Stacks

[books-stack.jpg]

A stack is a data structure that allows deletion of elements and insertion of new elements.  More specifically, a  stack  is a structure subject to the following rule: every deletion operation deletes the youngest element of the stack, i.e., the element that has been in the structure for less time.

In other words, the first object to be inserted in the stack is the last one to be deleted. This policy is known by the acronym LIFO (= Last In First Out).

Table of contents:

Implementation in an array

Let's assume that our stack is stored in an array  stk[0..N-1].  (The nature of the elements of the array is irrelevant: they may be integers, bytes, pointers, etc.)  Let's say that the part of the array occupied by the stack is

stk[0..t-1] .

The index t indicates the first free position in the array and t-1 is the top of the stack.  The stack is empty if t is 0 and full if t is N.  In the figure below, the characters A, B, … , H were inserted into the stack in this order:

0               t     N-1
A B C D E F G H        

To delete an element from the stack — this operation is known as a pop — do

   x = stk[--t];

This is equivalent to the pair of instructions  t -= 1;  x = stk[t];.  Of course you should never attempt to pop an empty stack.

To insert an object y into the stack — the operation is kown as a push — do

   stk[t++] = y;  

This is equivalent to the pair of instructions  stk[t] = y;  t += 1;.  Of course you shoud never attempt to push onto a full stack.

To make the code easier to read, it is convenient to wrap these operations into two small functions. If the elements of the stack are of type char, these functions are

char pop (void) {
   return stk[--t];
}

void push (char y) {
   stk[t++] = y;
}

(These two functions have the additional benefit of isolating the user of the stack from the particular names of variables and design decisions, like stk[0..t-1] instead of stk[0..t], for example.)

We are assuming here that the variables stk and t are global, that is, they were defined outside the code of the functions.  To complete the package, we would need three more functions: one to create a stack, one to check whether the stack is empty, and one to check whether the stack is full. (See the exercise below.)

Exercises 1

  1. Stack implementation module (version 1).  Write a module stackofchars.c to implement a stack of ASCII characters. The module must contain the functions  createstackpushpopstackisemptystackisfull.  Treat the parameters of the stack (the array stk and the index t) as global variables in the module.  Also write an interface stackofchars.h for the module. [Solution: ./solutions/pilha3.html]
  2. Make a different design decision from the one above: suppose that the part of the array occupied by the stack is stk[1..t]. Write the code of the functions push, pop, stackisempty, and stackisfull.
  3. Write an algorithm that uses a stack to invert the order of the letters of each word of an ASCII string, preserving the order of the words. For example, for the string  STRING OF ASCII CHARACTERS  the result will be  GNIRTS FO IICSA SRETCARAHC.
  4. Permutations produced by popping.  Suppose that objects  1, 2, 3, 4  are pushed, in this order, onto an initially empty stack. After pushing an object, you may pop zero or more elements of the stack. Each popped element is printed. For example, the sequence of operations

    push 1, push 2, pop, push 3, pop, pop, push 4, pop,

    prints the sequence 2, 3, 1, 4. Which of the 24 permutations of 1, 2, 3, 4 can be obtained in this manner?

  5. [Sedgewick]  The fragment of program below manipulates a stack of objects of the char type.  (The function peek returns the element at the top of the stack, but does not pop that element from the stack.)  Say, in English, what is it that the fragment does. Write an equivalent fragment that is simpler and shorter.
    if (stackisempty ())  push ('B');
    else {
       if (peek () != 'A')  push ('B');
       else {
          while (!stackisempty () && peek () == 'A') 
             pop ();
          push ('B'); } }
    

Application: parentheses and brackets

Consider the problem of deciding whether a given sequence of parentheses and brackets is well formed (that is, whether the parentheses and brackets are closed in the order contrary to that in which they were opened).  For example, the sequence

( ( ) [ ( ) ] )

is well formed, while   ( [ ) ]   is malformed.  Suppose that the sequence of parentheses and brackets is stored in an ASCII string  s.  (As usual, the last character of the string is \0.)

We use a stack to solve the problem.  The algorithm is simple: examine the string from left to right and push the left parentheses and left brackets onto the stack where they will wait for the corresponding right parentheses and right brackets. (See pseudocode.)

To simplify, the variables stk and t will be global.  We also assume that the size N of the array is greater than the size of the string and therefore the stack will never overflow.

#define N 100
char stk[N];
int t;

// This function returns 1 if the ASCII
// string s contains a well-formed sequence
// of parentheses and brackets; it returns
// 0 if the sequence is malformed.

int wellformed (char s[]) 
{
   createstack ();
   for (int i = 0; s[i] != '\0'; ++i) {
      char c;
      switch (s[i]) {
         case ')': if (stackisempty ()) return 0;
                   c = pop ();
                   if (c != '(') return 0;
                   break;
         case ']': if (stackisempty ()) return 0;
                   c = pop ();
                   if (c != '[') return 0;
                   break;
         default:  push (s[i]);
      }
   }
   return stackisempty ();
}

void createstack (void) {
   t = 0;
}

void push (char y) {
   stk[t++] = y;
}

char pop (void) {
   return stk[--t];
} 

int stackisempty (void) {
   return t <= 0;
}

(We could embed the code of the stack manipulation functions in the code of wellformed. The result would be shorter and more compact, but a little more difficult to read.)

Exercises 2

  1. Give a formal definition of well-formed sequence of parentheses and brackets. (Hint: make the definition recursive.)
  2. Does the function wellformed work correctly if the sequence s has only two elements? only one? none?
  3. Show that, in the beginning of each iteration in the code of function wellformed, s is well formed if and only if the sequence  stk[0..t-1] s[i...]  is well formed.
  4. Write a version of the function wellformed that allocates the stack dynamically.
  5. Let's say that our alphabet contains only the letters a, b, and c. Consider the following set of strings:   cacabcbabcbabacabaacaabbcbb, …   Any string of this set has the form WcM, where W is a sequence of letters that contains only a and b while M is the reverse of W  (that is, M is W read backwards). Write a program that will decide whether a string x belongs to our set or not.

Another application: Polish notation

The usual notation of arithmetic expressions is infix, that is, the operators are written between the operands.  In the postfix (also known as Polish) notation, the operators are written after the operands.  Here are some examples of infix and the corresponding postfix expressions:

infix postfix
(A+B*C) ABC*+
(A*(B+C)/D-E) ABC+*D/E-
(A+B*(C-D*(E-F)-G*H)-I*3) ABCDEF-*-GH*-*+I3*-
(A+B*C/D*E-F) ABC*D/E*+F-
(A+B+C*D-E*F*G) AB+CD*+EF*G*-
(A+(B-(C+(D-(E+F))))) ABCDEF+-+-+
(A*(B+(C*(D+(E*(F+G))))))   ABCDEFG+*+*+*

Notice that the operands (A, B, C, etc.) appear in the same order in the infix and the corresponding postfix expressions.  Also notice that the postfix notation does not need parentheses and precedence rules between operators (like the precedence of * over + for example), both of which are indispensable in the infix notation.

Our problem:  translate into postfix notation the infix expression stored in a string inf.  To simplify, we shall assume that

The algorithm reads the expression inf one character at a time and uses a stack to do the translation.  Every left parenthesis is pushed onto the stack. When a right parenthesis is found, the algorithm pops characters from the stack until it finds a left parenthesis and pops that too. When a + or a - is found, the algorithm pops characters from the stack until it finds a left parenthesis, which is not popped. When a * or a / is found, the algorithm pops the stack until it finds a left parenthesis or a + or a -.  Constants and variables are transferred directly from inf to the postfix expression.  (See a draft in pseudocode.)

The variables stk and t are global.  We assume that N is greater than the size of the string inf, and therefore we need not worry about the stack becoming full. Since the expression inf is wrapped in parentheses, we need not worry about an empty stack.

#define N 100
char stk[N];
int t;

// This function receives an infix expression inf
// and returns the corresponding postfix expression.

char *infixToPostfix (char *inf) {
   int n = strlen (inf);
   char *post; 
   post = malloc ((n+1) * sizeof (char));
   createstack ();
   push (inf[0]);       // push '('

   int j = 0;
   for (int i = 1; inf[i] != '\0'; ++i) {
      switch (inf[i]) {
         char x;
         case '(': push (inf[i]);
                   break;
         case ')': x = pop ();
                   while (x != '(') {
                      post[j++] = x;
                      x = pop ();
                   }
                   break;
         case '+': 
         case '-': x = pop ();
                   while (x != '(') {
                      post[j++] = x;
                      x = pop ();
                   }
                   push (x);
                   push (inf[i])
                   break;
         case '*':
         case '/': x = pop ();
                   while (x != '(' && x != '+' && x != '-') {
                      post[j++] = x;
                      x = pop ();
                   }
                   push (x);
                   push (inf[i]);
                   break;
         default:  post[j++] = inf[i];
      }
   }
   post[j] = '\0';      
   return post;
}  

(We could operate the stack directly, by inserting the code of the stack manipulation functions into the code of the calling function. The result would be shorter and more compact, but somewhat less readable.)

Here is the result of the application of function infixToPostfix to the infix expression  (A*(B*C+D)). The table shows the values of the variables at the beginning of each iteration:

inf[0..i-1] stk[0..t-1] post[0..j-1]
( (  
(A ( A
(A* (* A
(A*( (*( A
(A*(B (*( AB
(A*(B* (*(* AB
(A*(B*C (*(* ABC
(A*(B*C+ (*(+ ABC*
(A*(B*C+D (*(+ ABC*D
(A*(B*C+D) (* ABC*D+
(A*(B*C+D))     ABC*D+*

Exercises 3

  1. Use the infixToPostfix function to convert the expression  (A+B)*D+E/(F+A*D)+C  to postfix.
  2. Stack size.  Along the execution of the function infixToPostfix, what is the size of stk, in terms of n, in the worst case?  In other words, what is the greatest value of the variable t in the worst case? What happens if we put a bound (less than 10, for example) on the number of left parentheses in the expression?
  3. Rewrite the function infixToPostfix after dropping the assumption that the infix expression is wrapped in parentheses.
  4. Dynamic allocation.  Rewrite the function infixToPostfix after implementing the stack in a dynamically allocated array.
  5. Brackets and parentheses.  Rewrite the function infixToPostfix assuming that the infix expression may have brackets as well as parentheses.
  6. Rewrite the function infixToPostfix assuming that the expression may not be valid.
  7. Value of a Polish expression.  Let post be a nonempty ASCII string that stores an arithmetic expression in postfix notation. Suppose that post contains only the operators  +-*,  and  /  (all require two operands). Suppose also that the expression has no constants and that all the names of all variables are uppercase single letters. Suppose moreover that we have an array val that attributes integer values to all the variables:

    val[0] is the value of variable A,
    val[1] is the value of variable B, etc.

    Write a function to compute the value of post. Be careful with divisions by zero!

Resizing array implementation

It is not always possible the predict the amount of space a stack will need. If the stack is implemented in an array, we could resize the array every time the stack becomes full, as we already did when implementing a queue.

Exercises 4

  1. Stack implementation module (version 2).  Write a module stackofchars.c implementing a stack of ASCII characters in an array with resizing.  The module must contain the functions  createstackpushpopstackisempty,  and  freestack.  Treat the parameters of the stack as global variables.  Also write an interface stackofchars.h for the module.

Linked list implementation of a stack

How to implement a stack in a linked list?  Let's say that the elements of the stack are ASCII characters and the cells of the list are of type cell (see The structure of a linked list):

typedef struct cell {
   char         contents; 
   struct cell *next;
} cell;

We make the following design decisions. 1. Our list will have a head cell (whence the first cell of the list will not be part of the stack).  2. The top of the stack will stay in the second cell rather than the last one (why?).  3. A global variable st will point to the head of the list.

The functions that manipulate the stack can then be written like this:

cell *st;

void createstack (void) {
   st = malloc (sizeof (cell)); // head
   st->next = NULL; 
}

void push (char y) { 
   cell *new;
   new = malloc (sizeof (cell));
   new->contents = y;
   new->next  = st->next;
   st->next = new; 
}

char pop (void) {
   cell *p;
   p = st->next;
   char x = p->contents;
   st->next = p->next;
   free (p);
   return x; 
}

(As usual, we assume that the function pop will never be called on an empty stack.)

Exercises 5

  1. Implement a stack in a linked list without a head cell. The stack will be specified by the address of the first cell of the list (where the top of the stack is).
  2. Rewrite the functions wellformed and infixToPostfix, this time using a linked list implementation of the stack.

Appendix: The execution stack of a program

Every C program consists of one or more functions (and main is the first function to be executed). To manage the calls to the various functions, the operating system of the computer uses an execution stack.  (See the Call stack entry in Wikipedia.)  The operation can be described conceptually as follows.

When it encounters a function call, the computer creates a new workspace that contains all the parameters and all the local variables of the function.  This workspace is pushed onto the execution stack (on top of the workspace that called the function) and the execution of the function begins (confined to its workspace).  When the execution of the function ends, its workspace is popped from the stack and discarded.  The workspace that is now at the top of the stack is reactivated and the execution is restarted from the point at which it had been interrupted.  (See What's the stack? in Julia's drawings.)

Consider the following example:

   int G (int a, int b) {
      int x;
      x = a + b;
      return x;
   }

   int F (int i, j, k) {
      int x; 
      x = /*2*/ G (i, j) /*3*/;
      x = x + k;
      return x;
   }

   int main (void) {
      int i, j, k, y;
      i = 111; j = 222; k = 444;
      y = /*1*/ F (i, j, k) /*4*/;
      printf ("%d\n", y);
      return EXIT_SUCCESS;
   }

The execution of the program proceeds as follows:

In our example, F and G are distinct functions. But all would work the same way if F and G were identical, that is, if F were a recursive function.

Exercises 6

  1. Write an iterative function to simulate the behavior of the following recursive function. Use a stack.
    int TTT (int x[], int n) {
       if (n == 0) return 0;
       if (x[n] > 0) return x[n] + TTT (x, n-1);
       else return TTT (x, n-1);
    }
    
  2. Execution stack of a program.  (This exercise simulates the workings of the execution stack as well as the processing of the directives #include and #define of the preprocessor of the C compiler.)  Write a program that receives an ASCII text file and saves another ASCII text file as specified next. Each line of the input file contains words separated by spaces; the words that begin with  #  are special and the remaining ones are normal. The output file will contain the normal words, on a single line, in a certain order. Suppose, for example, that the input file contains the following lines:
    0   #4 aaa #2
    1   bbb
    2   CC #4 DDD #1 ee
    3   FF #2 #4
    4   GG hhh
    

    (The numbers on the left margin serve only as a reference and are not part of the file.)  Then the output file will contain

    GG hhh aaa CC GG hhh DDD bbb ee
    

    As the example suggests, the special words are replaced by the line of the input file whose number is given after #, and this is done recursively. To make the exercise more interesting, do not write recursive functions.