Module 1: Supplemental Material


Unidimensional array of integers

The example below reviews the array-of-integers example from Module 1. In addition, there are two variations (example1() and example2()) both of which have a subtle bug.

public class OddNumbers {

    public static void main (String[] argv)
    {
	// Read this example:
	example1 ();

	// Debug this one:
	example2 ();

	// Debug this one:
	example3 ();
    }


    static void example1 ()
    {
	int[] oddIntegers = makeOddArray (10);
	printWithWhile (oddIntegers);
	printWithDo (oddIntegers);
    }


    static void example2 ()
    {
	int[] A = {};
	printWithWhile (A);
	printWithDo (A);
    }

    static void example3 ()
    {
	int[] A = {1, 3, 5};
	printWithWhile2 (A);
    }


    static int[] makeOddArray (int size)
    {
	int[] oddGuys = new int [size];
	for (int i=0; i < oddGuys.length; i++) {
	    oddGuys[i] = 2*i + 1;
	}
	return oddGuys;
    }


    static void printWithWhile (int[] A)
    {
	System.out.print ("  Array elements: ");
	int i=0;
	while (i < A.length) {
	    System.out.print ("  " + A[i]);
	    i ++;
	}
	System.out.println ();
    }


    static void printWithDo (int[] A)
    {
	System.out.print ("  Array elements: ");
	int i=0;
	do {
	    System.out.print ("  " + A[i]);
	    i ++;
	} while (i < A.length);
	System.out.println ();
    }


    static void printWithWhile2 (int[] A)
    {
	System.out.print ("  Array elements: ");
	int i=0;
	while (i < A.length) 
	    System.out.print ("  " + A[i]);
	    i ++;
	System.out.println ();
    }

}


Unidimensional array of characters

Here's a reading exercise. The example below shows how to work with char arrays:

Here's the program:
public class CaesarCipher {

    public static void main (String[] argv)
    {
	// We'll test with this string.
	String s = "Top Secret: Col.Mustard, lead pipe, kitchen";
	char[] letters = s.toCharArray ();
	System.out.println (letters);

	// Encrypt:
	char[] codedLetters = caesarShift (letters, 3);
	System.out.println (codedLetters);

	// Decrypt:
	char[] decodedLetters = caesarShift (codedLetters, -3);
	System.out.println (decodedLetters);
    }
  

    static char[] caesarShift (char[] letters, int shift)
    {
	// Make an array to hold shifted letters.
	char[] shiftedLetters = new char [letters.length];

	// Now compute shifted letters.
	for (int i=0; i < letters.length; i++) {
	    // Note conversion char-to-int and int-to-char.
	    int intValueOfChar = (int) letters[i];
	    int shiftedIntValue = intValueOfChar + shift;
	    char charValueOfInt = (char) shiftedIntValue;
	    shiftedLetters[i] = charValueOfInt;
	}
	return shiftedLetters;
    }

}
Note:


Solving a problem step by step

Consider the problem of determining if two strings are anagrams:

Let's start with the general idea: we have to check that every letter in the first word is found in the second word, and vice-versa.

Now let's try to refine this idea into an algorithm:

   1.  for each character ch in firstWord
   2.    Make sure ch is in the secondWord
   3.  endfor
  

Next, a little more detail:

   1.  for i=0 to firstWord.length
   2.    char ch = firstWord[i]      // This is the i-th letter
   3.    // Somehow check whether it's in the secondWord
   4.    If not in the secondWord
   5.      // These are not anagrams - take appropriate action
   6.  endfor
   7.  // We reach here if all letters were found.
   8.  // They are anagrams.
  

Note:

  • At this stage, we don't have to have everything solved in detail. For example, we left out the problem of looking for a particular char in the second word.

  • At this stage, we should not be focused on writing code. That way we don't tax ourselves over semi-colons and language syntax.

  • Observe that we use careful indentation in describing the algorithm. This is a really important visual cue: don't be sloppy in indentation.

  • Notice that we didn't address the problem of checking whether the second word's letters appear in the first. Is that automatically taken care of?

  • Generally, at this stage, it's best to "stare" at the algorithm to see if it seems right before rushing off to write code.

Next, let's write the program's "skeleton", along with some test code:

public class Anagram {

    public static void main (String[] argv)
    {
        // A test.
	String s = "conversation";
	String s2 = "conservation";
	boolean yes = areAnagrams (s.toCharArray(), s2.toCharArray());
	System.out.println ("Are " + s + " and " + s2 + " anagrams? " + yes);

        // A trickier test.
	s = "slop";
	s2 = "sloop";
	yes = areAnagrams (s.toCharArray(), s2.toCharArray());
	System.out.println ("Are " + s + " and " + s2 + " anagrams? " + yes);
    }



    // The real work is done here.

    static boolean areAnagrams (char[] A, char[] B)
    {
    }

}
Note:
  • The test code has two cases, both in main.

  • We've decided on what the method looks like, but not the details:
        static boolean areAnagrams (char[] A, char[] B)
        {
        }
        

  • At this point, you should go ahead an compile to remove any errors in the test code. Of course you'll get a compilation error for the method areAnagrams(), which we'll ignore for now.

Next, let's develop the code based on the algorithm so far:

public class Anagram {

    public static void main (String[] argv)
    {
        // A test.
	String s = "conversation";
	String s2 = "conservation";
	boolean yes = areAnagrams (s.toCharArray(), s2.toCharArray());
	System.out.println ("Are " + s + " and " + s2 + " anagrams? " + yes);

        // A trickier test.
	s = "slop";
	s2 = "sloop";
	yes = areAnagrams (s.toCharArray(), s2.toCharArray());
	System.out.println ("Are " + s + " and " + s2 + " anagrams? " + yes);
    }



    // The real work is done here.

    static boolean areAnagrams (char[] A, char[] B)
    {
        // Check each letter in A to see if it occurs in B
	for (int i=0; i < A.length; i++) {

	    boolean found = false;

	    // Somehow search for A[i] in B.

	    if (! found) {
                // If a letter's not found we know they are not anagrams.
		return false;
	    }

	} // end-for

	return true;
    }

}

Now we have only one detail left: given a single letter of the first word, A[i], how do we look for it in B?
    ⇒ by scanning B.

So, here is our first complete attempt:

public class Anagram {

    public static void main (String[] argv)
    {
        // A test.
	String s = "conversation";
	String s2 = "conservation";
	boolean yes = areAnagrams (s.toCharArray(), s2.toCharArray());
	System.out.println ("Are " + s + " and " + s2 + " anagrams? " + yes);

        // A trickier test.
	s = "slop";
	s2 = "sloop";
	yes = areAnagrams (s.toCharArray(), s2.toCharArray());
	System.out.println ("Are " + s + " and " + s2 + " anagrams? " + yes);
    }



    // The real work is done here.

    static boolean areAnagrams (char[] A, char[] B)
    {
        // Check each letter in A to see if it occurs in B
	for (int i=0; i < A.length; i++) {

	    // Search for A[i] in B.
	    boolean found = false;
	    for (int j=0; j < B.length; j++) {
		if (A[i] == B[j]) {
		    found = true;
		}
	    }

	    if (! found) {
                // If a letter's not found we know they are not anagrams.
		return false;
	    }

	} // end-for

	return true;
    }

}
Does it work? The output is:
Are conversation and conservation anagrams? true
Are slop and sloop anagrams? true
So, it does NOT work.

Before reading further, can you see why?

Here's the solution to the above problem, with one more test added:

public class Anagram2 {

    public static void main (String[] argv)
    {
	String s = "conversation";
	String s2 = "conservation";
	boolean yes = areAnagrams (s.toCharArray(), s2.toCharArray());
	System.out.println ("Are " + s + " and " + s2 + " anagrams? " + yes);

	s = "slop";
	s2 = "sloop";
	yes = areAnagrams (s.toCharArray(), s2.toCharArray());
	System.out.println ("Are " + s + " and " + s2 + " anagrams? " + yes);

        // A third test.
	s = "sloop";
	s2 = "polls";
	yes = areAnagrams (s.toCharArray(), s2.toCharArray());
	System.out.println ("Are " + s + " and " + s2 + " anagrams? " + yes);
    }


    static boolean areAnagrams (char[] A, char[] B)
    {
        // A simple test: check that the lengths are the same.
	if (A.length != B.length) {
	    return false;
	}

	for (int i=0; i < A.length; i++) {

	    // Search for A[i] in B.
	    boolean found = false;
	    for (int j=0; j < B.length; j++) {
		if (A[i] == B[j]) {
		    found = true;
		}
	    }

	    if (! found) {
		return false;
	    }

	}

	return true;
    }

}
The output from this program is:
Are conversation and conservation anagrams? true
Are slop and sloop anagrams? false
Are sloop and polls anagrams? true
So, this doesn't work either. The problem is, "sloop" and "polls" are of the same length, and every letter in one is found in the other.

To solve this problem:

  • We want to match every letter in "sloop" with a corresponding one in "polls" while accounting for duplicates.
  • To do this, we'll "mark" the letters found by "scratching" them out.
Here's the program:
public class Anagram3 {

    public static void main (String[] argv)
    {
	String s = "conversation";
	String s2 = "conservation";
	boolean yes = areAnagrams (s.toCharArray(), s2.toCharArray());
	System.out.println ("Are " + s + " and " + s2 + " anagrams? " + yes);

	s = "slop";
	s2 = "sloop";
	yes = areAnagrams (s.toCharArray(), s2.toCharArray());
	System.out.println ("Are " + s + " and " + s2 + " anagrams? " + yes);

	s = "sloop";
	s2 = "polls";
	yes = areAnagrams (s.toCharArray(), s2.toCharArray());
	System.out.println ("Are " + s + " and " + s2 + " anagrams? " + yes);

        // Fourth test.
	s = "sloop";
	s2 = "pools";
	yes = areAnagrams (s.toCharArray(), s2.toCharArray());
	System.out.println ("Are " + s + " and " + s2 + " anagrams? " + yes);
    }


    static boolean areAnagrams (char[] A, char[] B)
    {
	if (A.length != B.length) {
	    return false;
	}

	for (int i=0; i < A.length; i++) {

	    // Search for A[i] in B.
	    boolean found = false;
	    for (int j=0; j < B.length; j++) {
		if (A[i] == B[j]) {
		    found = true;
                    // Now scratch out the letter found.
		    B[j] = '#';   
		}
	    }

	    if (! found) {
		return false;
	    }

	}

	return true;
    }

}
Note:
  • To "scratch out" or "mark", we overwrite the char with a "#". We are assuming of course, that "#" doesn't itself occur in words.

  • The idea is, once we've found a letter, we don't want that letter to be found in another search and so we scratch it out.

  • We've now added a fourth test: "sloop" and "pools"

  • The output of this program is:
    Are conversation and conservation anagrams? false
    Are slop and sloop anagrams? false
    Are sloop and polls anagrams? false
    Are sloop and pools anagrams? false
        
    Ouch. What went wrong here?

Before we point out what went wrong, let's do some debugging by printing out stuff:

import java.util.*;

public class Anagram3Debug {

    public static void main (String[] argv)
    {
	String s = "conversation";
	String s2 = "conservation";
	boolean yes = areAnagrams (s.toCharArray(), s2.toCharArray());
	System.out.println ("Are " + s + " and " + s2 + " anagrams? " + yes);

	s = "slop";
	s2 = "sloop";
	yes = areAnagrams (s.toCharArray(), s2.toCharArray());
	System.out.println ("Are " + s + " and " + s2 + " anagrams? " + yes);

	s = "sloop";
	s2 = "polls";
	yes = areAnagrams (s.toCharArray(), s2.toCharArray());
	System.out.println ("Are " + s + " and " + s2 + " anagrams? " + yes);

        // Fourth test.
	s = "sloop";
	s2 = "pools";
	yes = areAnagrams (s.toCharArray(), s2.toCharArray());
	System.out.println ("Are " + s + " and " + s2 + " anagrams? " + yes);
    }


    static boolean areAnagrams (char[] A, char[] B)
    {
        // Always mark the start and the data that's come in.
        System.out.println ("areAnagrams(): Start");
        System.out.println ("  A=" + Arrays.toString(A) + "  B=" + Arrays.toString(B));
        
	if (A.length != B.length) {
            // Second debug statement.
            System.out.println ("  >> unequal lengths: not anagrams");
	    return false;
	}

	for (int i=0; i < A.length; i++) {

	    // Search for A[i] in B.

            // Third: in the outer loop.
            System.out.println ("  >> i=" + i + " searching for A[i]=" + A[i] + " in B");

	    boolean found = false;
	    for (int j=0; j < B.length; j++) {
		if (A[i] == B[j]) {
                    // Inner-loop, inside the if
                    System.out.println ("  >>>> found j=" + j + " B[j]=" + B[j]);
		    found = true;
                    // Now scratch out the letter found.
		    B[j] = '#';   
		}
	    }
            // Outer loop, after the if-statement, to see what happened.
            System.out.println ("  >> after-inner-for: A=" + Arrays.toString(A) + " B=" + Arrays.toString(B)); 

            if (! found) {
                // Just before exiting.
                System.out.println ("  >> A[i]=" + A[i] + " not found => not anagrams");
		return false;
	    }

        } //end-for
        

        // Just before exiting, so we know how we exited.
        System.out.println ("  >> A and B are anagrams");
	return true;
    }

}
Note:
  • To get a true sense of what the code is doing, we need quite a few println's.

  • Notice the indentation in the output to distinguish inner loops (">>>>") from outer loops (">>").

  • Here's the first part of the output:
    areAnagrams(): Start
      A=[c, o, n, v, e, r, s, a, t, i, o, n]  B=[c, o, n, s, e, r, v, a, t, i, o, n]
      >> i=0 searching for A[i]=c in B
      >>>> found j=0 B[j]=c
      >> after-inner-for: A=[c, o, n, v, e, r, s, a, t, i, o, n] B=[#, o, n, s, e, r, v, a, t, i, o, n]
      >> i=1 searching for A[i]=o in B
      >>>> found j=1 B[j]=o
      >>>> found j=10 B[j]=o
      >> after-inner-for: A=[c, o, n, v, e, r, s, a, t, i, o, n] B=[#, #, n, s, e, r, v, a, t, i, #, n]
    
        ...
    
        
    Right away, we see the problem
    • First, we see that the 'c' has been properly written out in B.

    • However, when searching for 'o', the search worked, but we overwrote both occurences of 'o' in B - an error!

To solve the problem we need to make sure we write only once. We can do this by "breaking" out of the loop:

public class Anagram4 {

    public static void main (String[] argv)
    {
        // ... same as in Anagram3.java ...
    }


    static boolean areAnagrams (char[] A, char[] B)
    {
	if (A.length != B.length) {
	    return false;
	}

	for (int i=0; i < A.length; i++) {

	    // Search for A[i] in B.
	    boolean found = false;
	    for (int j=0; j < B.length; j++) {
		if (A[i] == B[j]) {
		    found = true;
		    B[j] = '#';   
                    // Break out of the inner loop once we've marked one occurence.
		    break;
		}
	    }

	    if (! found) {
		return false;
	    }

	}

	return true;
    }

}
The output is:
Are conversation and conservation anagrams? true
Are slop and sloop anagrams? false
Are sloop and polls anagrams? false
Are sloop and pools anagrams? true
So, it works for our tests.

Even thought this works, it's not a good program because we end up destroying B by writing into it.

We should instead make a copy of B and write into the copy:

public class Anagram5 {

    public static void main (String[] argv)
    {
        // ... same as before ...
    }


    static boolean areAnagrams (char[] A, char[] B)
    {
	if (A.length != B.length) {
	    return false;
	}

        // Make a copy.
	char[] C = new char [B.length];
	for (int i=0; i< B.length; i++) {
	    C[i] = B[i];

	}

        // Use the copy.
	for (int i=0; i < A.length; i++) {

	    // Search for A[i] in C.
	    boolean found = false;
	    for (int j=0; j < C.length; j++) {
		if (A[i] == C[j]) {
		    found = true;
                    // Write into the copy.
		    C[j] = '#';   
		    break;
		}
	    }

	    if (! found) {
		return false;
	    }

	} //end-for

	return true;
    }

}

There is actually a much nicer and faster way of checking that two words are anagrams. It involves sorting. Can you guess what this approach is?


Arrays of strings

Let's modify the above anagram code to find all anagrams in a dictionary:

public class FindAnagrams {

    public static void main (String[] argv)
    {
        // Get dictionary.
        String[] words = WordTool.getDictionary ();

        // We'll count the anagrams found.
        int count = 0;

        // Compare each pair of words.
        for (int i=0; i < words.length; i++) {
            for (int j=0; j < words.length; j++) {

                // Make sure a word is not compared to itself.
                if (i != j) {
                    if ( areAnagrams (words[i].toCharArray(), words[j].toCharArray()) ) {
                        System.out.println (words[i] + " and " + words[j] + " are anagrams");
                        count ++;
                    }
                }

            }
        }
        
        System.out.println (count + " anagrams found");
    }



    static boolean areAnagrams (char[] A, char[] B)
    {
        // ... same as in Anagram5.java above ...
    }

}
Note:
  • The algorithm is simple: merely compare every pair of words.

  • We had to be careful not to compare a word with itself.

Exercise 1: Download FindAnagrams.java, WordTool.java and words (the dictionary). Run and discover how many anagrams there are. Next, modify the code to print out how many comparisons are made: how many letter-comparisons are made? Also modify the code to find and print the largest anagrams.


Random arrays

As another example of using random arrays, let's solve the following problem:

  • The Argentine writer Jorge Luis Borges is often credited with the now-famous assertion "A half-dozen monkeys provided with typewriters would, in a few eternities, produce all the books in the British Museum". To which he added "Strictly speaking, one immortal monkey would suffice".

  • What he meant was that a monkey hitting keys at random would eventually produce proper words, would eventually produce proper sentences, and ... given enough time, would produce a work of Shakespeare.

  • It's estimated (yes, this can be calculated approximately) that the time needed to produce even a modest-sized text, counting one nanosecond per keystroke dwarfs the known age of the universe.

Let's do a small version of this problem:

  • We will generate a single string randomly.

  • We'll check to see if the string is in our dictionary.

  • We'll count how many trials it takes to generate a valid word.
Here's the program:
public class RandomWord {

    static String[] words;

    public static void main (String[] argv)
    {
        // This is the dictionary.
        words = WordTool.getDictionary ();

        // We'll try generating words of length 3 for now.
        int size = 3;
        int numTrials = generateWordRandomly (size);
        System.out.println ("It took " + numTrials + " trials to generate a word of length " + size);
    }

    static int generateWordRandomly (int size) 
    {
        // We'll want to count the number of trials it took.
        int numTrials = 0;

        boolean done = false;
        while (! done) {

            // Generate a random word.
            String randomStr = generate (size);

            numTrials ++;
            
            // Check if it's in the dictionary.
            for (int i=0; i < words.length; i++) {
                if ( words[i].equalsIgnoreCase (randomStr) ) {
                    // Found!
                    System.out.println ("Randomly generated word: " + randomStr);
                    done = true;
                    break;
                }
            }

        } // end-while
        
        return numTrials;
    }
    
    
    static String generate (int size)
    {
        // Make a char array of desired length.
        char[] letters = new char [size];
        for (int i=0; i < letters.length; i++) {
            // To generate a letter from 'a' to 'z', first
            // generate an integer and then add to 'a'.
            int k = UniformRandom.uniform (0, 25);
            letters[i] = (char) ( (int)'a' + k);
        }

        // Make a String out of it and return that.
        String str = new String (letters);
        return str;
    }
    
}

Exercise 2: Download RandomWord.java, WordTool.java and words (the dictionary). Run and discover how long it takes to randomly generate a valid word of length 3. Next, try length 4.

Let's point out some programming issues:

  • The structure of the main part of the code is:
           boolean done = false
           while (! done) {
               // Make an attempt
               // If attempt successful, set done=true
           }
        
    This is a common problem-solving technique.

  • Note that we could return inside the loop itself.
            while (true) {
    
                // Generate a random word.
                String randomStr = generate (size);
    
                numTrials ++;
                
                // Check if it's in the dictionary.
                for (int i=0; i < words.length; i++) {
                    if ( words[i].equalsIgnoreCase (randomStr) ) {
                        // Found!
                        System.out.println ("Randomly generated word: " + randomStr);
                        return numTrials;
                    }
                }
    
            } // end-while
        
    Here, we don't need the boolean flag done because the loop ends only when we've found what we're looking for.

  • The above problem is a good example of a loop where a for-loop won't work.

  • for-loops work when the loop termination is known ahead of time, or at least the maximum number of iterations are known.

  • For example, if we wanted to limit the number of trials:
    
            for (int numTrials=0; numTrials < 100000; numTrials++) {
    
                // Generate a random word.
                String randomStr = generate (size);
    
                // Check if it's in the dictionary.
                for (int i=0; i < words.length; i++) {
                    if ( words[i].equalsIgnoreCase (randomStr) ) {
                        // Found!
                        System.out.println ("Randomly generated word: " + randomStr);
                        return numTrials;
                    }
                }
    
            } // end-while
            
            // We need to signal failure in some way:
            return -1;
        }
        

Exercise 3: Instead of expecting a random string to be a word, it might be more reasonable to expect it to be an anagram of some word in the dictionary. Modify the above program to check whether a randomly generated word is an anagram of some word in the dictionary. Do trials for size=3 and size=4: they should take less time than for a straight word search.


More practice with GUI's

Let's now create a GUI to allow a user to enter two strings and check if they are anagrams.

We want the GUI to look like this:

Here's the program:

import java.util.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;

public class AnagramGUI extends JFrame {

    JTextArea textArea;
    JTextField stringField;
    JScrollPane scrollPane;

    public AnagramGUI ()
    {
        // Set some parameters of the frame (window) that's brought up.
        this.setSize (600, 600);
        this.setTitle ("Anagram Tester");
        this.setResizable (true);

        // This is how stuff is put into a frame.
	Container cPane = this.getContentPane();
        textArea = new JTextArea ();
	scrollPane = new JScrollPane (textArea, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
        cPane.add (scrollPane, BorderLayout.CENTER);
        
        // Make the controls.
        JPanel panel = new JPanel ();
        JLabel label = new JLabel ("Enter two strings: ");
        panel.add (label);
        stringField = new JTextField (30);
        panel.add (stringField);
        JButton button = new JButton ("Go");
	button.addActionListener (
	  new ActionListener () {
	      public void actionPerformed (ActionEvent a)
	      {
		  handleButtonClick();
	      }
	  }
        );
        panel.add (button);
        cPane.add (panel, BorderLayout.SOUTH);
        
        this.setVisible (true);
    }
    

    String inputStr;


    // When the user clicks the button, this method gets called.
    // It's where we need to respond.

    void handleButtonClick ()
    {
        // Extract the string from the textfield where the user typed the strings.
        inputStr = stringField.getText ();

        // Break into two strings, separating by space.
	String[] strings = inputStr.split (" ");

        // Check if anagrams.
	boolean yes = areAnagrams (strings[0].toCharArray(), strings[1].toCharArray());

        // Prepare output.
	String outputStr = strings[0] + " and " + strings[1] + " are not anagrams";
	if (yes) {
	    outputStr = strings[0] + " and " + strings[1] + " are anagrams";
	}

        // Put the output string in the text box.
	String text = textArea.getText ();
	text += outputStr + "\n";
	textArea.setText (text);
    }


    static boolean areAnagrams (char[] A, char[] B)
    {
        // ... same as in Anagram5.java above ...
    }



    public static void main (String[] argv)
    {
	AnagramGUI a = new AnagramGUI ();
    }

}

Exercise 4: Download and try out the program.

Observe the general structure of the program:

// ... Some import statements ...

public class AnagramGUI extends JFrame {

    // ... Variables needed for the GUI ...

    public AnagramGUI ()
    {
        // ... Build the GUI ...
    }
    

    String inputStr;


    // When the user clicks the button, this method gets called.
    // It's where we need to respond.

    void handleButtonClick ()
    {
        // ... This is where we do the computation ...
    }


    public static void main (String[] argv)
    {
        // Throw up the GUI.
	AnagramGUI a = new AnagramGUI ();
    }

}
Note:
  • To work this kind of GUI, you don't really need to understand how it's put together.

  • All you need to know is: (1) where to write code to respond to a button-click (in the method handleButtonClick() and (2) how to write stuff to the GUI textbox.

Exercise 5: Simplify the above program to simply echo to the textbox whatever the user types in.


© 2006-2020, Rahul Simha & James Taylor (revised 2020)