CSE143 Notes for Friday, 10/19/12

I mentioned that our next programming assignment will involve String manipulation which is really a CSE142 topic, but I didn't intend the String processing to be the hard part of the assignment. So I said that I wanted to solve a short problem to remind people how basic String manipulation works.

I said that I wanted to write a method called dashes that would take a String as input and that would return a new String with dashes inserted between letters. So given the input "hello", the method would return "h-e-l-l-o".

Someone said that we could build up a temporary String that the method could return. So the basic structure will look like this:

        public static String dashes(String s) {
            String result = ??
            // fill up string
            return result;
        }
Someone said that we could loop over the characters of the string. Remember that the string has a method called length that tells you the number of characters in the string and a method called charAt that gets you the individual characters of the string:

        public static String dashes(String s) {
            String result = ??
            for (int i = 0; i < s.length(); i++) {
                // do something with s.charAt(i)
            }
            return result;
        }
Someone suggested that each time through the loop we want to add to the current result a dash and the next character, so I modifed the loop to be:

        for (int i = 0; i < s.length(); i++) {
            result = result + "-" + s.charAt(i);
        }
The problem with this is that it would add a leading dash that we don't want. And putting the dash after the call on charAt would put an extra dash afterwards. This is a classic fencepost problem that we can solve by processing the first character before the loop.

So we decided to initialize the string to the first character of the word:

        String result = s.charAt(0);
Unfortunately, this isn't going to work. You can't assign a string the value of a character. Someone suggested casting, but that doesn't work either. The usual way to do this in Java is to append the character to an empty string, because you can always concatenate a value to a string:

        String result = "" + s.charAt(0);
But because we processed the first character before the loop, we had to change the loop bounds to start at 1 instead of 0. So our final version became:

        public static String dashes(String s) {
            String result = "" + s.charAt(0);
            for (int i = 1; i < s.length(); i++) {
                result = result + "-" + s.charAt(i);
            }
            return result;
        }
I mentioned one last detail about this method. It fails in one case. Someone said that it doesn't properly handle an empty string, which is correct. We have assumed that there is a character at position 0. That won't work for an empty string. We could add a special case for that, but I said that in this case I would probably add a precondition that makes this clear:

        // pre: s is not an empty string
It's important to consider these cases and document any preconditions, but sometimes assumptions like this are reasonable. For example, in the hangman programming assignment, you will never be asked to deal with an empty string because the program itself guarantees that all words are at least of length 1.

Then we continued our discussion of the sample program to find distances between friends. We left off with the idea that we want to have a map that converts a string into a set of string values. Given the name of a person, we can get a Set with the names of that person's friends. For our sample file:

        "Andrew"	=> maps to => [Christopher, Sarah]
        "Ashley"	=> maps to => [Christopher, Emily, Jessica, Joshua]
        "Bart"	        => maps to => [Lisa, Matthew]
        "Christopher"	=> maps to => [Andrew, Ashley, Jacob, Michael, Sarah]
        "Emily"	        => maps to => [Ashley, Joshua, Sarah]
        "Jacob"	        => maps to => [Christopher, Stuart]
        "Jessica"	=> maps to => [Ashley, Michael]
        "JorEl"	        => maps to => [KalEl, Zod]
        "Joshua"	=> maps to => [Ashley, Emily, Michael]
        "KalEl"	        => maps to => [JorEl]
        "Kyle"	        => maps to => [Lex, Tyler, Zod]
        "Lex"	        => maps to => [Kyle]
        "Lisa"	        => maps to => [Bart, Marge, Matthew]
        "Marge"	        => maps to => [Lisa]
        "Matthew"	=> maps to => [Bart, Lisa, Samantha]
        "Michael"	=> maps to => [Christopher, Jessica, Joshua]
        "Samantha"	=> maps to => [Matthew, Tyler]
        "Sarah"	        => maps to => [Andrew, Christopher, Emily]
        "Stuart"	=> maps to => [Jacob]
        "Tyler"	        => maps to => [Kyle, Samantha]
        "Zod"	        => maps to => [JorEl, Kyle]
We had written this code to construct the structure and read in the lines of input and pull out the individual names:

        Map<String, Set<String>> friends = new TreeMap<String, Set<String>>();
        while (input.hasNextLine()) {
            String line = input.nextLine();
            if (line.contains("--")) {
                Scanner lineData = new Scanner(line);
                String name1 = lineData.next();
                lineData.next();  // this skips the "--" token
                String name2 = lineData.next();
                addTo(friends, name1, name2);
                addTo(friends, name2, name1);
            }
        }

        public static void addTo(Map> friends, String name1, 
                                 String name2) {
            if (!friends.containsKey(name1)) {
                friends.put(name1, new TreeSet<String>());
            }
            friends.get(name1).add(name2);
        }
This code constructs the friends map. The challenge then is to use it to explore friends at various distances. To solve this problem, we will end up using several sets of names. At any given time, we will be exploring a new set of friends that are at the next distance away. We we will continue searching until we either find the target name or run out of people to search. So the overall structure of the method is as follows:

        Set<String> newFriends = new TreeSet<String>();
        newFriends.add(name1);
        int distance = 0;
        while (!newFriends.contains(name2) && !newFriends.isEmpty()) {
            distance++;
            // find friends one further away
        }
Inside the loop, we want to use the current set of newFriends to find the next group of newFriends. We can do so simply by adding all of the friends of these friends to a new set and then replacing newFriends with that new set:

        Set<String> newNewFriends = new TreeSet<String>();
        for (String friend : newFriends) {
            newNewFriends.addAll(friends.get(friend));
        }
        newFriends = newNewFriends;
This provides a pretty good solution to the problem. If we throw in some statements to print out what is happening, we end up with this solution:

        Set<String> newFriends = new TreeSet<String>();
        newFriends.add(name1);
        int distance = 0;
        System.out.println();
        System.out.println("Starting with " + name1);
        while (!newFriends.contains(name2) && !newFriends.isEmpty()) {
            distance++;
            Set<String> newNewFriends = new TreeSet<String>();
            for (String friend : newFriends) {
                newNewFriends.addAll(friends.get(friend));
            }
            newFriends = newNewFriends;
            System.out.println("    " + distance + " away: " + newFriends);
        }
        if (newFriends.contains(name2)) {
            System.out.println("found at a distance of " + distance);
        } else {
            System.out.println("not found");
        }
But notice what happens when we run this version of the program:

        Welcome to the cse143 friend finder.
        starting name? Stuart
        target name? Joshua
        
        Starting with Stuart
            1 away: [Jacob]
            2 away: [Christopher, Stuart]
            3 away: [Andrew, Ashley, Jacob, Michael, Sarah]
            4 away: [Andrew, Christopher, Emily, Jessica, Joshua, Sarah, Stuart]
        found at a distance of 4
It is getting the right answer, but the intermediate answers are not correct. It indicates, for example, that Stuart is 2 away from Stuart. That's because it is including the possibility of going from Stuart to Jacob and then from Jacob back to Stuart. In a similar way, it is saying that Christopher is 2 away and Christopher is 4 away. In this case it came up with the right answer, but allowing this kind of duplication makes the program run more slowly and it leads to an infinite loop when there is no connection between people. That's because when you allow duplicates, it just keeps finding more and more friends when it looks 5 away, 6 away, 7 away, and so on.

The solution is to introduce yet another set to keep track of people who have already been explored. Then when we form a new set of friends to consider, we remove the names of people who have already been explored. And we'll have to add the new people to the set of explored people so that we won't explore them in the future. The code below includes the extra lines of code indicated in bold face:

        Set<String> oldFriends = new TreeSet<String>();
        Set<String> newFriends = new TreeSet<String>();
        newFriends.add(name1);
        int distance = 0;
        System.out.println();
        System.out.println("Starting with " + name1);
        while (!newFriends.contains(name2) && !newFriends.isEmpty()) {
            distance++;
            oldFriends.addAll(newFriends);
            Set<String> newNewFriends = new TreeSet<String>();
            for (String friend : newFriends) {
                newNewFriends.addAll(friends.get(friend));
            }
            newNewFriends.removeAll(oldFriends);
            newFriends = newNewFriends;
            System.out.println("    " + distance + " away: " + newFriends);
        }
        if (newFriends.contains(name2)) {
            System.out.println("found at a distance of " + distance);
        } else {
            System.out.println("not found");
        }
This completes the program.


Stuart Reges
Last modified: Fri Oct 19 16:08:44 PDT 2012