CS111, Wellesley College, Fall 2006

Problem Set 7

Due on Friday 10 November at the start of class

Reading

  • IntList Contract
  • StringList Contract
  • String Contract
  • Study the code in the file StringListOps.java in the ps07_programs/Unjumbler folder.
  • Study the code in the file LabOps.java in the ps07_programs/Unjumbler folder.
  • Notes on invoking instance methods vs. class methods.
  • About this Problem Set

    The purpose of this problem set is to give you experience with strings, lists, and iteration (while loops and for loops). In Task 1, you get a clearer picture of how lists and recursive methods on lists work by drawing an invocation tree and box-and-pointer diagrams. In Task 2, you will get a better understanding of lists by writing recursive list methods. You will need to write several methods that manipulate lists of strings as part of implementing an "unjumbling assistant" that will help you solve word jumble problems. In Task 3, you will use iteration to partition a list.

    All code for this assignment is available in the ps07_programs folder in the cs111d download directory on the cs server.

    How to turn in this Problem Set

    You are required to turn in both a hardcopy and a softcopy. For general guidelines on problem set submission, including how to submit a softcopy and how to check if you softcopy submission was successful, click here. Please make sure to keep a copy of your work, either on a zip disk, or in your private directory (or, to play it safe, both).

    Hardcopy Submission

    Your hardcopy packet should consist of:
    1. The cover page;
    2. The paper with your invocation tree/box-and-pointer diagram;
    3. Your modified Unjumbler.java file.
    4. Your iteration table from Task 3a;
    5. Your modified Partition.java file from Tasks 3b, 3c, 3d, and 3e;
    Staple these together, and submit them at the start of class on the due date.

    Softcopy Submission

    You should submit your final version of your ps08_programs folder. In particular,

    Task 1: Box-and-Pointer Diagrams

    In this problem, we first present some notational conventions for using invocation trees together with box-and- pointer diagrams for lists, and then ask you to use these conventions to show the execution of a list manipulation program.

    Notational Conventions

    Recall that an invocation tree is an abbreviation for a collection of Java Execution Model frames. Each node of the tree shows the arguments and the result of a method invocation; such a node abbreviates a frame in ExecutionLand. Also recall that a box-and-pointer diagram is an abbreviation for interconnected linked list nodes in ObjectLand.

    It is possible to combine invocation trees and box-and-pointer diagrams to summarize the execution of a list manipulation program. For instance, consider the following definition of a list reversal method (which is assumed to be in a subclass of IntListOps, so that it can use postpend()):

    public static IntList reverse (IntList L) 
    {
         if (isEmpty(L))
              return L;
         else
              return postpend(reverse(tail(L)), head(L));
              // could also write:
              // return append(reverse(tail(L)), prepend(head(L), empty()));
    }

    Suppose that L1 is a list whose printed representation is [1,2,3]. Then the execution of the invocation reverse(L1) is summarized by the following diagram:

    A boxed element of the form reverse(X):Y is the root node of an invocation tree for the invocation of the reverse() method on list X that returns result Y . In the invocation tree, particular list nodes are denoted by object references, which are letters inside circles. These object references name nodes appearing in the box-and-pointer portion of the diagram. The object references could have instead been denoted by pointers in the diagram, but this would lead to a spaghetti of pointers. We follow the convention that the empty list is always written as a distinct solid circle. (We are not interested in modelling sharing involving the empty list.)

    In the above diagram we treated postpend() as a "primitive" by not showing nodes for each invocation of postpend(). However, the nodes created by the prepend()s performed by the hidden postpend()s are shown in the box-and-pointer diagrams.

    An important feature of the box-and-pointer list diagram portion of the picture is that it accurately depicts sharing. In the case of reverse(), the result returned by one call to reverse does not share any list nodes with the results of any other call. As an example of a diagram with more sharing, consider the following method, which is also assumed to be defined in a subclass of the IntListOps class:

    public static IntList appendTails (IntList L) 
    {
         if (isEmpty(L))
              return L;
         else
              return append(L, appendTails(tail(L)))
    }

    Below is a diagram showing an invocation tree and box-and-pointer list structure for the invocation appendTails(L1), where L1 is defined as above. Note how the final result (the node labelled"f") includes as subparts the results to the recursive calls (the nodes labelled "e" and "d").

    Your Task

    Suppose that the following methods are defined in a subclass of the IntList class:

    public static IntList F (IntList L) 
    {
         return tail(G(0, L));
    }
    
    public static IntList G (int n, IntList L) 
    {
         if (isEmpty(L))
              return prepend(0, L);
         else {
              IntList subResult = G(n + head(L), tail(L));
              return prepend(head(L) + head(subResult),
                             prepend(head(L) * (n + head(subResult)),
                                     tail(subResult)));
         }
    }

    Suppose that L2 is an integer list whose printed representation is [1,2,3,4]. Draw a combined invocation tree/box-and-pointer diagram that illustrates the invocation F(L2). Use the same conventions used in the reverse() and appendTails() examples from above in your diagram.


    Task 2: Word Unjumbler

    Overview

    Word Jumble is a popular game that appears in many newspapers. There is now even an on-line version of Word Jumble. You may want to play this game a few times before continuing with the assignment.

    The game involves "unjumbling" English words whose letters have been reordered. For instance, the jumbled word ytikt can be unjumbled to kitty. The game can be challenging; even relatively short jumbles can be tricky to unjumble. For instance, here are the words that appeared on the April 2, 2004, version of the on-line game; can you unjumble them?:

    catue, hybus, nyflod, degurt

    In this problem, you will create a Java program that acts as an "unjumbling assistant". Given a string, your program will first generate all possible reorderings of the letters in the string. Such reorderings are called permutations. For example, there are six reorderings of the letters in the string tra:

    [tra,rta,rat,tar,atr,art]

    In general, a string with n distinct letters has n! (pronounced "n factorial") permutations. For instance, a 4-letter string has 4! = 24 permutations, a 5-letter string has 5! = 120 permutations, a 6-letter string has 6! = 720 permutations, and so on.

    Next, the assistant will determine which of the permutations is an English word by looking them up in a dictionary. You do not have to worry about how to construct such a dictionary; this has been done for you. Notes on how to use the dictionary as a "black box" can be found later in this problem description. In the case of "tra", filtering out the English words leaves:

    [rat,tar,art]

    When the string you are unjumbling contains duplicate letters, a simple permutations generator will yield some duplicate permutations. For instance, the permutations of "dda" will generate the 3! = 6 permutations:

    [dda,dda,dad,dad,add,add]

    Filtering out the English words yields:

    [dad,dad,add,add]

    In such cases, the unjumbling assistant should also filter out duplicates to yield the final list:

    [dad,add]

    To get a feel for what the unjumbling assistant does, you should experiment with the UnjumblerAnswers.class file in the Unjumbler folder which contains working methods for unjumbling. For this assignment, we have provided this file (instead of a test folder), as an example of how your solution should execute. For example, if you want to see how your program should behave, you can run the unjumbleInteractively() method from the UnjumblerAnswers.class file in either of two different ways:

    You can use this method to help you solve the on-line Word Jumble puzzles!

    Important Note: With its default settings, DrJava is only able to unjumble words of at most 6 characters. Attempts to unjumble words longer than 6 characters will encounter a StackOverflowError, indicating that there is a chain of execution frames that is too long for Java Virtual Machine to handle. Longer words can be handled by modifying the default stack size used by DrJava for program execution. This can be done via the following steps:

    1. In the DrJava window, select Edit>Preferenences to display the Preferences window.
    2. In the Preferences window, select the Miscellaneous category.
    3. In the input line labeled JVM Args for Interactions JVM, type -Xss4000k (no spaces). This tells the Java Virtual Machine (JVM) to use a stack with size 4000 kilobytes (about 4 megabytes), which is sufficient for unjumbling 8-character words.
    4. At the bottom of the Preferences window, press the OK button.
    5. A pop-up window will appear warning you that "Specifying Interactions JVM Args is an advanced option ..." and will ask you if you're sure you want to do this. Press the Yes button.
    6. In the DrJava window, select Tools>Reset Interactions to make the change take effect.

    Your Task

    Your task is to create your own unjumbler program, mostly from scratch. To do this, you need to do is to write definitions for the nine class methods specified below.

    Here are some things you need to know:

    1. public static StringList remove (String s, StringList L)
      Returns a new list in which all occurrences of s in L have been removed. The other strings in the list should have the same relative order in the resulting list as in the given list.

      Examples:
      remove("I", fromString("[I,know,that,I,said,that,I,did]"))
            returns the string list [know,that,said,that,did].

      remove("that", fromString("[I,know,that,I,said,that,I,did]"))
            returns the string list [I,know,I,said,I,did].

      remove("said", fromString("[I,know,that,I,said,that,I,did]"))
            returns the string list [I,know,that,I,that,I,did].

      remove("you", fromString("[I,know,that,I,said,that,I,did]"))
            returns the string list [I,know,that,I,said,that,I,did].

      Note: Use the equals() instance method from the String class to compare two strings. For instance, "cat".equals("cat") returns true but "cat".equals("dog") returns false. You should not use == to compare two strings because it may not return what you expect. For instance, while "cat" == "dog" is guaranteed to return false, "cat" == "cat" and "cat" == ("c" + "at") are not guaranteed to return true. They may return true in some implementations and some circumstances, but you cannot rely on this behavior.

    2. public static StringList removeDuplicates (StringList L)
      Returns a list containing each string in L exactly once. The order of the elements in the returned list should be the relative order of the first occurrence of each element in L.

      Examples:
      removeDuplicates(fromString("[I,know,that,I,said,that,I,did]"))
            returns the string list [I,know,that,said,did].

      removeDuplicates(fromString("[you,say,what,you,mean,and,mean,what,you,say]"))
            returns the string list [you,say,what,mean,and].

      removeDuplicates(fromString("[lists,are,cool]"))
            returns the string list [lists,are,cool].

      Note: The remove method from above is helpful here!

    3. public static StringList mapConcat (String s, StringList L)
      Given a list L with n strings, returns a new list with n strings in which the ith string of the resulting list is the result of concatenating s to the ith element of L.

      Examples:
      mapConcat("com", fromString("[puter,plain,municate,pile]"))
            returns the string list [computer,complain,communicate,compile].

      mapConcat("I ", fromString("[came,saw,conquered]"))
            returns the string list [I came,I saw,I conquered].

    4. public static StringList insertions (String s1, String s2)
      Given two strings s1 and s2, where s2 has n characters, returns a list of n + 1 strings that result from inserting s1 at all possible positions within s2, from left to right.

      Examples:
      insertions("*", "split")
            returns the string list [*split,s*plit,sp*lit,spl*it,spli*t,split*]

      insertions("a", "bcd")
            returns the string list [abcd,bacd,bcad,bcda]

      insertions("com", "pile")
            returns the string list [compile,pcomile,picomle,pilcome,pilecom]

      insertions("abc", "")
            returns the string list [abc]

      Note: The LabOps class contains two helper methods that are useful for defining insertions:

      1. public static String first (String s)
        Returns a string consisting of the first character of s. For example, first("computer") returns the string "c".

      2. public static String butFirst (String s)
        Returns a string consisting of all but the first character of s. For example, butFirst("computer") returns the string "omputer".

    5. public static StringList insertionsList (String s, StringList L)
      Returns a list that contains all the strings that result from inserting s at all possible positions in all the strings of L.

      Examples:
      insertionsList("a", fromString("[bc,cb]"))
            returns the string list [abc,bac,bca,acb,cab,cba]

      insertionsList("*", fromString"[I,am,Sam]"])
            returns the string list [*I,I*,*am,a*m,am*,*Sam,S*am,Sa*m,Sam*]

      insertionsList("abc", fromString("[]"))
            returns the string list []

      Note: The StringListOps class contains a helper method append that is useful for defining insertionsList:

      public static StringList append (StringList L1, StringList L2)
      Returns a new string list containing all the elements of L1 followed by all of the elements of L2. For example,
      • append(fromString("[I,do]"), fromString("[not,like,green,eggs]")) returns [I,do,not,like,green,eggs]
      • append(fromString("[I,do]"), fromString("[]")) returns [I,do]
      • append(fromString("[]"), fromString("[not,like,green,eggs]")) returns [not,like,green,eggs]

    6. public static StringList permutations (String s)
      Returns a list of all permutations of the string s. A permutation of a string s is any string that is formed by reordering the letters in the string s (without duplicating or deleting any letters). For a string with n distinct characters, there are exactly n! (i.e., "n factorial") permutations. If some characters in s are repeated, there are still n! permutations, but the permutations contain duplicates. The elements in the list returned by permutations may be in any order.

      Examples:
      permutations("a") returns the string list [a].

      permutations("ab") returns the string list [ab,ba] or the string list [ba,ab].

      permutations("abc") returns (any permutation of) the string list [abc,bac,bca,acb,cab,cba].

      permutations("abcd") returns (any permutation of) the string list

            [abcd,bacd,bcad,bcda,
             acbd,cabd,cbad,cbda,
             acdb,cadb,cdab,cdba
             abdc,badc,bdac,bdca,
             adbc,dabc,dbac,dbca,
             adcb,dacb,dcab,dcba].

      permutations("121") returns (any permutation of) the string list [121,211,211,112,112,121]. Note that when the given string contains duplicate characters, the permutation list will contain duplicates.

      permutations("1231") returns (any permutation of) the string list
            [1231,2131,2311,2311,
             1321,3121,3211,3211,
             1312,3112,3112,3121,
             1213,2113,2113,2131,
             1123,1123,1213,1231,
             1132,1132,1312,1321].

      Note: There are many ways to define the permutations method, but a particularly elegant way uses the first, butFirst, and insertionsList methods from above. Be very careful in defining your base case!

    7. public static StringList filterWords (StringList L)
      Returns a list of all strings in L that are English words. The resulting strings should be in the same relative order as in L.

      Examples:
      filterWords(fromString("[the,dog,barked,at,the,cat]")) returns the string list [the,dog,barked,at,the,cat].

      filterWords(fromString("[the,dog,barkd,ate,hte,cat]")) returns the string list [the,dog,ate,cat].

      filterWords(fromString("[tra,rta,rat,tar,atr,art]")) returns the string list [rat,tar,art].

      Note: To determine if a string is an English word, you should use the class method isWord() that is already defined for you in the Unjumbler class:

      public static boolean isWord (String s)
      Returns true if s is a word in the default English dictionary, and false otherwise.

      The default dictionary (which can be changed within the Unjumbler class) contains 22641 English words (all lower case, no proper nouns) of up to 8 characters in length. It is not a "perfect" dictionary: there are some perfectly acceptable English words that are not in the dictionary.

      You can change the default dictionary to one that contains 45425 English words without a length restriction, but this takes longer to load. For details on how to do this, see the comments near the end of Unjumbler.java.

    8. public static StringList unjumble (String s)
      Returns a list of all the permutations of s that are English words (as determined by the default dictionary). The order of elements in the resulting list does not matter, but each word in the resulting list should be listed only once.

      Examples:
      unjumble("tra") returns the string list [rat,tar,art].

      unjumble("tras") returns the string list [rats,arts,star]
            (the default dictionary doesn't recognize tars or tsar as words).

      unjumble("argle") returns the string list [glare,large,lager,regal].

      unjumble("sbso") returns the string list [sobs,boss].

      unjumble("xzzy") returns the string list [].

      Note: You should only remove duplicates after performing filterWords. It turns out that performing removeDuplicates on a large lists (such as the output of permutations) can take a very long time. (To understand why this is so, take CS230!)

    9. public static void unjumbleInteractively()
      Launches an interactive session in the Java Console that repeatedly prompts the user for a string and displays the list of unjumbled English words for that string. The interactive session ends when the user enters the empty string.

      Example: Below is a sample session of unjumbleInteractively(). The text typed by the program is in italics, while the text typed by the user is in bold:

      Enter a string to unjumble and press Return (enter the empty string to quit):
      argle
      Constructing dictionary from file dicts/dict8.bin.
      This may take a little while...
      Done! Dictionary constructed with 22641 words.
      [glare,large,lager,regal]
      Enter a string to unjumble and press Return (enter the empty string to quit):

      eshou
      [house]
      Enter a string to unjumble and press Return (enter the empty string to quit):

      sbos
      [boss,sobs]
      Enter a string to unjumble and press Return (enter the empty string to quit):

      Thank you for using the unjumbler!

      Notes:


    Task 3: List Partitioning

    The following partitionIter() method takes an integer named pivot and a list of integers named L and partitions the list into two lists:

    1. All the elements in L less than pivot.
    2. All the elements in L greater than or equal to pivot.

    The two resulting lists are packaged together and returned in a PairOfIntLists object.

        public static PairOfIntLists partitionIter (int pivot, IntList L) 
        {
             return partitionTail(pivot, L, IL.empty(), IL.empty());
        }
    
        public static IntListList partitionTail (int pivot, IntList list, 
                                                 IntList lesses, IntList greaters) 
        {
             if (IL.isEmpty(list)) {
                   return twoLists(lesses, greaters);
              } else if (IL.head(list) < pivot) {
                   return partitionTail(pivot, IL.tail(list), 
                                        IL.prepend(IL.head(list), lesses), greaters);
              } else {
                   return partitionTail(pivot, IL.tail(list), 
                                        lesses, IL.prepend(IL.head(list), greaters));
              }
        }
    

    We will use IL. to prefix IntList operations (and the testing code given to you will use ILL. to prefix IntListList operations, which we will learn about soon).

    We use the PairOfIntLists class to glue two IntLists together. Here is the contract for this class:

    public static PairOfIntLists mkpair (IntList L1, IntList L2);
    Glues L1 and L2 together into a PairOfIntLists object.

    public static IntList first (PairOfIntLists p);
    Returns the first list in PairOfIntLists object.

    public static IntList second (PairOfIntLists p);
    Returns the second list in PairOfIntLists object.

    To make the code more readable, the Partition.java file uses the following helper method, which takes two lists and glues them together into a PairOfIntLists:

    public static PairOfIntLists twoLists (IntList L1, IntList L2) 
    {
         return PairOfIntLists.mkpair(L1, L2);
    }

    Task 3a: Iteration Table

    The partitionTail() method is a tail recursive method that specifies an iteration in four state variables named pivot, list, lesses, and greaters. Any iteration can be characterized by how the values of the state variables change over time. Below is a table with four columns, one for each state variable of the iteration described by partitionTail(). Each row represents the values of the parameters to a particular invocation of partitionTail().

    pivotlist lessesgreaters
         
         
         
         
         
         
         
         
         

    Suppose that the list A has the printed representation [7,2,3,5,8,6,1]. Fill in the above table to show the parameters passed to successive calls to partitionTail() in the computation that begins with the invocation partitionIter(5, A). There are more rows shown here than you need, so you should draw only as many rows as you need to in your table.

    Part 3b: partitionWhile()

    It is possible to express any iteration as a while loop. Flesh out the following code skeleton of a partitionWhile() method that behaves just like the above partitionIter() method except that it uses a while loop rather than tail recursion to express the same iteration as partitionTail().

    public static PairOfIntLists partitionWhile (int pivot, IntList L)
    {
         // Replace this stub:
         return twoLists(IL.empty(), IL.empty());
    }

    Notes:

    Task 3c: partitionFor()

    The iteration expressed by partitionTail() and partitionWhile() can also be expressed via a for loop whose index variable is the list state variable. Flesh out the skeleton of the following method so that it expresses the partitioning iteration with a for loop:

    public static PairOfIntLists partitionFor (int pivot, IntList L)
    {
        // Replace this stub:
        return twoLists(IL.empty(), IL.empty());
    } 

    Recall that a Java for loop has the syntax

    for (init-statement; test-expression; update-statement) {
      body-statements
    }
    

    The above for loop is equivalent to the following while loop:

    init-statement;
    while (test-expression) {
      body-statements;
      update-statement;
    }

    Task 3d: partitionRec()

    In the above partitionIter() method, elements in the two returned lists are in a relative order opposite to their relative order in the original lists. For instance, partitioning the list [1, 4, 8, 3, 6, 7, 5, 2] about the pivot 6 yields the lists [2, 3, 4, 1] and [7, 6, 8].

    Suppose that we want the resulting lists to have the same relative order as in the original list. If we are provided with an IntList reversal method IL.reverse(), we can easily accomplish this by reversing the two lists before gluing them together. That is, we can change the line

    	return twoLists(lesses, greaters);
    
    within partitionTail to be
    	return twoLists(IL.reverse(lesses), IL.reverse(greaters));
    

    An alternative to using IL.reverse() to achieve this behavior is to define a non-tail-recursive version of partitionIter() that we will call partitionRec(). Flesh out the following skeleton of partitionRec(), which partitions the elements of a list about the pivot but maintains the relative order of the elements in the resulting lists:

     
    public static PairOfIntLists partitionRec (int pivot, IntList L)
    { 
        if (IL.isEmpty(L)) { 
            return twoLists(IL.empty(), IL.empty()); 
        } else { 
            PairOfIntLists subresult = partitionRec(pivot, IL.tail(L)); 
            // Replace the following stub:
            return twoLists(IL.empty(), IL.empty());
        }
    }

    Notes:

    Task 3e: quicksort()

    An important use of a partitioning method is a sorting algorithm known as quicksort. Here is the idea behind quicksort:
    To sort the elements of a list, partition the elements of the tail of the list around its head into result lists that we'll call lesses and greaters. Then result of sorting the whole list can be obtained by appending the result of sorting lesses to the result of prepending the head of the list to the result of sorting greaters.

    For example, if the initial list is [5, 2, 8, 3, 6, 7, 1, 4], then partitioning the tail of the list around the head (5) yields:

    	lesses = [4, 1, 3, 2]
    	greaters = [7, 6, 8]
    

    By wishful thinking, sorting lesses will yield [1, 2, 3, 4] and sorting greaters will yield [6, 7, 8]. The result of sorting the original list is the result of appending [1, 2, 3, 4] to the result of prepending 5 to [6, 7, 8].

    Flesh out a method with header public static IntList quicksort (IntList L) that uses this idea to sort the elements of a list. You may use IL.append() to append two lists and any of the above partitioning methods to partition a list. The quicksort() method is tested by the main() method of the Partition class.

    It turns out that quicksort, true to its name, is one of the fastest algorithms for sorting a list. If you take CS230 and CS231, you will learn (among other things) much more about various sorting algorithms and how to compare them in terms of efficiency.