CPSC 120 Lecture Notes, Friday, Mar 6, 1998

ASSIGNMENTS/ANNOUNCEMENTS

MOTIVATION FOR STACKS

A number of situations arise where you want to handle things in a last-in, first-out (LIFO) manner. For instance, The classic example of a stack is a spring-loaded pile of cafeteria trays.

A stack is a sequence of elements, to which elements can be added and removed -- elements are removed in the reverse order in which they were added, i.e., the last one added is the first one removed (LIFO).

We usually use the word push for adding an element to a stack, and the word pop for removing an element from a stack.

SPECIFYING THE STACK ADT

There are several different formalisms that can be used to specify abstract data types. Remember, we want to be as independent of any particular implementation as possible.

However, people naturally think in terms of state, so a popular way to specify an ADT is with some kind of "abstract, or high-level, implementation", in which we

  1. describe an abstract version of the state, and then
  2. describe the effect of each operation on the abstract state.

For instance, to specify a stack, we could say:

  1. A stack is modeled as a sequence of elements.
  2. Initially the state of the stack is the empty sequence.
  3. The effect of a push(x) operation is to append x to the end of the sequence that represents the state of the stack This operation returns nothing.
  4. The effect of a pop operation is to delete the last element of the sequence that represents the state of the stack. This operation returns the element that was deleted. If the stack is empty, it should return some kind of error indication.

But a purist might complain that this specification is, implicitly, suggesting a particular implementation. To be even more abstract, one can specify an ADT simply in terms of the series of operations that are allowable.

For instance, consider the following sequences:

But it is more involved to give a precise and complete definition of the allowable sequences without reference to an abstract state.

Other operations that you sometimes want to provide:

Java provides a Stack class, in java.util.

USING A STACK -- CHECKING FOR BALANCED PARENTHESES

Our first example of using a stack ADT to solve some other problem is to check whether parentheses are properly balanced: Here is a recursive definition of properly balanced parentheses: According to this definition: What kind of an algorithm can you come up with that will process an input string of parentheses and decide if it is properly balanced? The key observation is: there must be the same number of left parens as right parens, and the number of right parens can never exceed the number of left parens.
numleft = 0
for each char in the string
   if char = ( then numleft++
   if char = ) then numleft--
   if numleft < 0 then unbalanced
endfor
if numleft = 0 then balanced else unbalanced
numleft is being manipulated kind of like a stack. When a ( is encountered, imagine it is being pushed onto the stack. When a ) is encountered, a ( is popped off the stack. We never want to try to pop an empty stack, and at the end the stack should be empty.

Let's see how to do this explicitly with a stack. Using a stack makes it easier to extend this algorithm to more complicated situations.

create an empty stack
for each char in the string
   if char = ( then push ( onto the stack
   if char = ) then pop ( off the stack
   if the pop causes an error then unbalanced
endfor
if stack is empty then balanced else unbalanced

Using java.util.Stack class:

import java.util.*;
class Balance {
    public static void main(String argv[]) {
	char[] parens;
	parens = getInput();	// method that gets input sequence of parens
	Stack S = new Stack();  // create a new empty stack
	try {			// the pop might throw an exception
	    for (int i = 0; i < parens.length; i++) {
		if ( parens[i] == '(' )
		    S.push('(');	// convert char to an object
		else 			// parens[i] == ')'
		    Object o = S.pop();	// don't care about return value --
	    }				//  only possibility is '(' 
	    if (S.empty())
		System.out.println("balanced");
	    else 
		System.out.println("unbalanced");
	} // end try
	catch (EmptyStackException e) {
	    System.out.println("unbalanced");
	}
    }
}	

Now suppose we can have 3 different kinds of parentheses: ( and ), [ and ], { and }. We can easily modify the program: When we encounter a ), we should pop off a (. When we encounter a ], we should pop off a [. When we encounter a }, we should pop off a {.

import java.util.*;
class Balance3 {
    public static void main(String argv[]) {
	char[] parens;
	parens = getInput();	// method that gets input sequence of parens
	Stack S = new Stack();  // create a new empty stack
	try {			// the pop might throw an exception
	    for (int i = 0; i < parens.length; i++) {
		if ( ( parens[i] == '(' ) ||
		     ( parens[i] == '[' ) ||
		     ( parens[i] == '{' ) )
		    S.push(new Character(parens[i])); // convert char to object
		else {			// parens[i] is a right paren
		    char rightp = (( Character) S.pop()).charValue();  
				// cast returned object to Character, then
				// convert to char
		    switch (parens[i]) {
		    case ')' :
			if ( rightp != '(' ) {
			    System.out.println("unbalanced");
			    return;
			}
			break;
		    case ']' :
			if ( rightp != '[' ) {
			    System.out.println("unbalanced");
			    return;
			}
			break;
		    case '}' :
			if ( rightp != '{' ) {
			    System.out.println("unbalanced");
			    return;
			}
			break;
		    }
		}
	    } // end for		

	    // finished processing the input sequence

	    if (S.empty())
		System.out.println("balanced");
	    else 
		System.out.println("unbalanced");
	} // end try
	catch (EmptyStackException e) {
	    System.out.println("unbalanced");
	}
    }
}	
(*If you printed this out before 3/8/98, you got a version containing the postfix section with bugs. See the notes for 3/9/98 for corrected version.*)