Module 8: Supplemental Material
Balancing parentheses
Let's modify the paren-checking code to identify the position in
the string where the mismatch occurs:
- We will return the mismatch position from the
checkParens() method.
- If no mismatch occurs, we'll return -1.
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:
- Even though there was an error, we had to return some "fake" letter.
- We also assume that the caller (whoever called
pop()) is ready to deal with the '@' letter.
Exceptions:
- Exceptions are a much better way of handling such errors.
- We'll first modify the code to use exceptions, and then explain.
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:
- There are actually many ways to create our own exceptions,
but the most common is to extend the class Exception that
is the Java library.
- The very minimum one should do is to print something useful
in the toString(), as above.
- More complicated things could be done, depending on the application.
⇒
For example, we could store some info to pass back to the caller.
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:
- We've used two new Java reserved words: throws and throw.
- Notice how the method signature is now written:
public char pop ()
throws OurStackException
{
// ... body of method ...
}
This is necessary because the caller needs to be ready to catch
the exception, if thrown.
- Inside the method, when something goes wrong, we
throw the exception:
public char pop ()
throws OurStackException
{
if (top > 0) {
// ...
}
else {
throw new OurStackException ();
}
}
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:
- The exception that must be caught is of course the same as
the one thrown by pop().
- When nothing goes wrong, and pop() does not throw an
exception, all the execution stays withing the try clause.
- When an exception is thrown, control immediately goes to
to the catch clause.
- In this case, the code in the catch clause has
only one line of code.
- In general, we could write any code we like in the
catch clause, for example:
public static void main (String[] argv)
{
try {
OurStack4 stack = new OurStack4 ();
stack.pop ();
}
catch (OurStackException e) {
System.out.println ("Something's very, very, very wrong here ...");
System.exit(0);
}
}
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:
- We often join the shorter queue.
- Suppose instead we flipped a coin to decide which queue to
join.
⇒
Intuition suggests this will not be as good.
⇒
Let's see if we can quantify that through a simulation.
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)