Module 8: Supplemental Material


Balancing parentheses

Let's modify the paren-checking code to identify the position in the string where the mismatch occurs:

Here's the program:
import java.util.*;

public class ParenBalancing5 {

    public static void main (String[] argv)
    {
        // Test 1.
	String s = "((()))";
	int index = checkParens (s);
        if (index >= 0) {
            System.out.println ("Unbalanced: mismatch at position " + index + " in string " + s);
        }
        else {
            System.out.println ("String " + s + " has balanced parens");
        }

        // ... other tests ...
    }


    static int checkParens (String inputStr)
    {
	char[] letters = inputStr.toCharArray();
	Stack<Character> stack = new Stack<Character> ();

	for (int i=0; i<letters.length; i++) {
	    if (letters[i] == '(') {
		stack.push (letters[i]);
	    }
	    else if (letters[i] == ')') {
		// Look for a match on the stack.
		char ch = ')';
		if (! stack.isEmpty() ) {
		    ch = stack.pop ();
		}
		if (ch != '(') {
                    // Return the position where the mismatch occured.
		    return i;
		}
	    }
	}
	
        // If we've reached here, either there's no mismatch, or
        // the mismatch is because of unmatched parens still on the stack.
	if ( ! stack.isEmpty() )  {
	    return letters.length;
	}
	else {
            return -1;
        }
    }

}


Exceptions

Recall the pop() method from our first implementation of a stack in OurStack.java:

public class OurStack {

    char[] letters;         // Store the chars in here.
    int top;                // letters[top]: next available space.

    // ... other methods ...

    public char pop ()
    {
        // Test for empty stack.
	if (top <= 0) {
            System.out.println ("ERROR in OurStack.pop(): stack empty");
            // Still need to have a return statement, so we return some "junk letter".
            return '@';
	}

        top --;
        return letters[top];
    }

}
Note:

Exceptions:

The first step is to define an exception:

public class OurStackException extends Exception {

    public String toString ()
    {
        return "Stack is empty: you can't pop an empty stack";
    }

}
Note:

The next step is to re-write the pop() method to use it:

public class OurStack4 {

    char[] letters;
    int top;

    // ... other methods ...


    // The new pop() method that uses the newly defined exception.

    public char pop ()
        throws OurStackException
    {
	if (top > 0) {
	    top --;
	    return letters[top];
	}
	else {
	    // Error: throw an exception
            throw new OurStackException ();

	    // We don't need this: return '@';
	}
    }

}
Note:

Finally, the caller needs to be re-written so that the call to pop() is within a try-catch statement:

public class OurStackExample4 {

    public static void main (String[] argv)
    {
        try {
            OurStack4 stack = new OurStack4 ();
            stack.pop ();
        }
        catch (OurStackException e) {
            e.printStackTrace ();
        }
    }

}
Note:

Exercise 1: Replace the line e.printStackTrace(); with System.out.println(e);. What do you notice is the difference? Suppose we were to add the line System.exit(0); inside the catch clause. What happens? Is it a good idea in general to add the line System.exit(0); to the catch clause of an exception?


Which queue to join?

Suppose there are two checkout lanes at the grocery store:

To simulate the coin-flip strategy:

  • We will have scores of customers arrive at the checkout lanes.

  • Each customer will flip a coin to decide which of the two "servers" (checkout lanes) to pick.

  • Once a queue is selected, a customer stays until checkout is complete.

Some details:

  • We will simplify the simulation so that at each time-step, a new customer arrives.

  • At each time-step, each server might or might not be done with the customer they are serving. We'll use a probability to decide this.

  • Thus, a server with probability 0.8 (a fast server) will have a higher chance of finishing with a customer than a server with probability 0.4.

  • How do we simulate a random event with probability like 0.8?
    • Generate a random number between 0 and 1.
    • If the random number is less than 0.8, we declare the event to occur.
    • Otherwise, we say that the event did not occur.

Exercise 2: Why does the above method work? Another way of thinking about this:

  • Mark the numbers 0.1, 0.2, ... 1.0 on a wheel.
  • Spin the wheel (like in Wheel of Fortune) to see which number appears under the "arrow". If it's a number between 0 and 0.8, we say the event has occurred, else we say it didn't occur.
  • Why is this the same as the random-number method above?

Next, let's look at the code for the random-queue simulation:

import java.util.*;

public class ShortestQueue {

    public static void main (String[] argv)
    {
        // Four experiments with random-Queue using 100,1000,10000 and 100,000 customers.
	randomQueue (100, 1, 0.6, 0.6);
	randomQueue (1000, 1, 0.6, 0.6);
	randomQueue (10000, 1, 0.6, 0.6);
	randomQueue (100000, 1, 0.6, 0.6);

        // Four experiments with shortest-Queue using 100,1000,10000 and 100,000 customers.
	shortestQueue (100, 1, 0.6, 0.6);
	shortestQueue (1000, 1, 0.6, 0.6);
	shortestQueue (10000, 1, 0.6, 0.6);
	shortestQueue (100000, 1, 0.6, 0.6);
    }



    static void randomQueue (int numTimeSteps, double arrivalRate, double server1Rate, double server2Rate)
    {
        // Create two queues.
	LinkedList<Integer> queue1 = new LinkedList<Integer>();
	LinkedList<Integer> queue2 = new LinkedList<Integer>();

        // Statistics that we'll track.
	double sumOfWaitTimes = 0;
	int numCustServed = 0;

        // Repeat for numTimeSteps steps.

	for (int n=0; n<numTimeSteps; n++) {

	    // See if arrival occurs and if so, which queue it should join.
	    if (UniformRandom.uniform() < arrivalRate) {
		// Now choose a queue randomly (flip a coin).
		if (UniformRandom.uniform() > 0.5) {
                    // We'll store the time-stamp for each customer.
		    queue1.add (n); 
		}
		else {
		    queue2.add (n);
		}
	    }

	    // See if anyone completes in queue 1.
	    if ( (! queue1.isEmpty()) && 
		 (UniformRandom.uniform() < server1Rate) ) {
		int arrivalTime = queue1.remove();
		sumOfWaitTimes += (n - arrivalTime);
		numCustServed ++;
	    }	    

	    // See if anyone completes in queue 2.
	    if ( (! queue2.isEmpty()) && 
		 (UniformRandom.uniform() < server2Rate) ) {
		int arrivalTime = queue2.remove();
		sumOfWaitTimes += (n - arrivalTime);
		numCustServed ++;
	    }	    

	} //end-for

	double avgWaitTime = sumOfWaitTimes / numCustServed;
	System.out.println ("Random queue stats:#time steps=" + numTimeSteps);
	System.out.println ("  Average wait time: " + avgWaitTime);
	System.out.println ("  Num left in system: " + (queue1.size() + queue2.size()) );
    }



    static void shortestQueue (int numTimeSteps, double arrivalRate, double server1Rate, double server2Rate)
    {
        // ... we'll look at this later ...
    }

}
Explanation:
  • First, let's understand the overall structure of the simulation:
            // Statistics that we'll track.
    	double sumOfWaitTimes = 0;
    	int numCustServed = 0;
    
            // Repeat for numTimeSteps steps.
    
    	for (int n=0; n<numTimeSteps; n++) {
    
                // ... simulation details ...
    
    	} //end-for
    
    	double avgWaitTime = sumOfWaitTimes / numCustServed;
    
         
    • The simulation is run for numTimeSteps steps.
    • We'll track, for each customer, the time from arrival to the chosen queue to departure after checkout.
    • These "wait" times (as in "waiting to get done") are summed up over all customers.
    • The average wait time, then, is this sum divided by the number of customers (numCustServed) we are collecting statistics (wait times) about.

  • Next, let's observe how arrivals (customers, really) are generated:
    	    // See if arrival occurs and if so, which queue it should join.
    	    if (UniformRandom.uniform() < arrivalRate) {
    
    		// Now choose a queue randomly (flip a coin).
    
                }
         
    • At each time step, we spin a wheel (generate a random number between 0 and arrivalRate) to see if a customer arrives at the checkout lanes.
    • We haven't shown the code above (inside the if statement) where the customer chooses which queue to go to.

  • Next, how do we make the customer choose a queue?
    		// Now choose a queue randomly (flip a coin).
    		if (UniformRandom.uniform() > 0.5) {
                        // We'll store the time-stamp for each customer.
    		    queue1.add (n); 
    		}
    		else {
    		    queue2.add (n);
    		}
         
    • We flip a coin (alternatively, spin the wheel).
    • With probability 0.5, the customer chooses Queue 1; with probability 0.5, the customer chooses Queue 2.

  • Notice one important detail:
    		    queue1.add (n); 
         
    • We store the arrival time-step (n) in the queue.
    • This is all we're going to need when this customer departs.

  • Each "server" has a rate of service: the higher the rate, the higher the speed of service. Once again, this is determined randomly.

  • At each time step, we spin the wheel to see if a server is done "serving":
    	    if ( (! queue1.isEmpty()) && 
    		 (UniformRandom.uniform() < server1Rate) ) {
                     // ...
    	    }	    
         
    Of course, we need to do this only for queues that aren't empty.

  • Notice the actions taken when service completes:
    	    if ( (! queue1.isEmpty()) && 
    		 (UniformRandom.uniform() < server1Rate) ) {
    		int arrivalTime = queue1.remove();
    		sumOfWaitTimes += (n - arrivalTime);
    		numCustServed ++;
    	    }	    
         
    • The remove() operation extracts the integer (arrival time) that we stored earlier.
    • This customer's wait time is the current time (n) minus the arrival time.
    • We accumulate these wait times in the sum sumOfWaitTimes.

Finally, let's look at the equivalent code for the shortest-queue simulation:

    static void shortestQueue (int numTimeSteps, double arrivalRate, double server1Rate, double server2Rate)
    {
        // ... initialization, create two queues etc ... same as above.


        // Repeat for given number of time steps.
	for (int n=0; n<numTimeSteps; n++) {

	    // See if arrival occurs.
	    if (UniformRandom.uniform() < arrivalRate) {
		// Now choose shortest queue.
		if (queue1.size() > queue2.size()) {
		    queue2.add (n);
		}
		else {
		    queue1.add (n);
		}
	    }

            // ... the rest of the for-loop is the same ...

	} //end-for

        // ... same stats ...

    }
Note:
  • Obviously, the only difference is that, instead of choosing a random queue, an arriving customer chooses the shorter queue.

Exercise 3: Download, compiler and run the above program, ShortestQueue.java:

  • Identify where in the code we used queue operations.
  • What is the difference between the two queue-joining strategies?
  • Next, change the service rates to 0.8 and 0.4, and see what results. What do you observe? Can you explain what you observe?
  • For cases where the service rates are very different, is it always best to join the shortest queue? Can you think of a better strategy?



© 2006, Rahul Simha (revised 2017)