Programming Assignment 1
CS 51510
Algorithms
Fall, 2024

This assignment makes use of the files contained in this zip file. This assignment is due Friday, December 6.

This assignment is an extension of the generic sorting examples from class.

Here is a programming problem that helps explain this assignment. Suppose that we want to sort Triple objects using the sum of the first two components of each Triple, but we want to be able to switch between ascending and descending sorts. We could write two comparators.

   class CmpTriples_A implements Comparator<Triple>
   {
      public int compare(Triple t1, Triple t2)
      {
         final int s1 = t1.first() + t1.second();
         final int s2 = t2.first() + t2.second();
         if (s1 < s2) return -1;
         if (s2 < s1) return  1;
         return 0;
      }
   }

   class CmpTriples_D implements Comparator<Triple>
   {
      public int compare(Triple t1, Triple t2)
      {
         final int s1 = t1.first() + t1.second();
         final int s2 = t2.first() + t2.second();
         if (s1 > s2) return -1;
         if (s2 > s1) return  1;
         return 0;
      }
   }

But notice that these two comparators are almost exactly alike. They differ in only one character that is used twice. One comparator uses the less-than operator and the other comparator uses the greater-than operator. This kind of code repetition is not good.

We should be able to "factor out" the inequality operator and write the comparator just once using a parameter that represents the inequality operator. Java allows us to this by using what are called functional interfaces. In this example we use a BiPredicate functional to represent the less-than or greater-than operators.

   class CmpTriples implements Comparator<Triple>
   {
      private final BiPredicate<Integer, Integer> cmp;

      CmpTriples(BiPredicate<Integer, Integer> cmp)
      {
         this.cmp = cmp;
      }

      public int compare(Triple t1, Triple t2)
      {
         final int s1 = t1.first() + t1.second();
         final int s2 = t2.first() + t2.second();
         if (cmp.test(s1, s2)) return -1;
         if (cmp.test(s2, s1)) return  1;
         return 0;
      }
   }

If we have a List of Triple objects, then we can sort it two ways using this single comparator.

      final BiPredicate<Integer, Integer> lt = (x,y) -> x < y;
      final BiPredicate<Integer, Integer> gt = (x,y) -> x > y;
      triples.sort( new CmpTriples(lt) ); // ascending
      triples.sort( new CmpTriples(gt) ); // descending

Here is how you should think about this last bit of code. We want to sort a list of triples, so we call the triples.sort method. But the sort method needs to be told how to compare two triples. So we pass it a comparator object that encodes "how" to compare two triple objects, triples.sort( new CmpTriples() ). But the comparator needs to be told an extra piece of detail, the less-than or greater-than operator. So we pass the comparator constructor an object that encodes the comparison operator, triples.sort( new CmpTriples( (x,y) -> x < y ) ). What we have is a sort method "parameterized" by a comparator object, and a comparator object "parameterized" by a functional object.

Now suppose that we wanted to sort by the product of the first two components of each Triple (instead of the sum). We could write two more "simple" comparators, one for ascending sorts and one for descending sorts. Or, we could give our parameterized comparator one more functional parameter. We can give the comparator a functional parameter that can represent the sum or product operators. We use a IntBinaryOperator functional to represent these operators.

   class CmpTriples implements Comparator<Triple>
   {
      private final BiPredicate<Integer, Integer> cmp;
      private final IntBinaryOperator reduce;

      CmpTriples(BiPredicate<Integer, Integer> cmp, IntBinaryOperator reduce)
      {
         this.cmp = cmp;
         this.reduce = reduce;
      }

      public int compare(Triple t1, Triple t2)
      {
         final int r1 = reduce.applyAsInt(t1.first(), t1.second());
         final int r2 = reduce.applyAsInt(t2.first(), t2.second());
         if (cmp.test(r1, r2)) return -1;
         if (cmp.test(r2, r1)) return  1;
         return 0;
      }
   }

Here is how we sort our list of triples in descending order using the product operator.

   triples.sort( new CmpTriples( (x,y) -> x > y, (x,y) -> x * y ) );

In the zip file, see the program TestSimpleExample.java which has the above example written out so that you can compile and run it.

Here are a few well written tutorials on comparators and lambda expressions.

The idea of this assignment is roughly this. Given a list of integer triples, like this,

     (5 8 3)
     (5 6 1)
     (8 6 7)
     (6 1 1)
     (1 9 6)
     (4 3 2)
     (8 4 6)
     (7 9 3)
     (8 8 5)
     (4 1 5)

write code that sorts the list in each of the following ways.

  1. Sort in ascending order on sum of the first two elements, use third element as a tie breaker.
  2. Sort in descending order on sum of the first two elements, use third element as a tie breaker.
  3. Sort in ascending order on sum of the first and third elements, use second element as a tie breaker.
  4. Sort in ascending order on maximum of the second and third elements, use first element as a tie breaker.
  5. Sort in descending order on maximum of the second and third elements, use first element as a tie breaker.
  6. Sort in ascending order on minimum of the first and second elements, use third element as a tie breaker.
  7. Sort in ascending order on first element, use second element as a tie breaker.
  8. Sort in descending order on third element, use first element as a tie breaker.
  9. Sort in ascending order on first element, use second element, in descending order, as a tie breaker.
  10. Sort in ascending order on product of the first two elements, use third element as a tie breaker.
  11. Sort in descending order on minimum of the first and second elements, use the sum of all three elements as a tie breaker.

The real goal of the assignment is to implement these sorts in as little code as possible by using a carefully parameterized comparator. There are purposely a large number of sorts to force you to emphasize code reuse. (One obvious way to do this problem is to write ten different comparators, one for each sorting problem, but that will lead to a lot of redundant code and very little credit for this assignment.)

If you run the eleven different sorts on the example triples given above, the results should look like this.

[(5 8 3), (5 6 1), (8 6 7), (6 1 1), (1 9 6), (4 3 2), (8 4 6), (7 9 3), (8 8 5), (4 1 5)]

[(4 1 5), (6 1 1), (4 3 2), (1 9 6), (5 6 1), (8 4 6), (5 8 3), (8 6 7), (7 9 3), (8 8 5)]
[(8 8 5), (7 9 3), (8 6 7), (5 8 3), (8 4 6), (5 6 1), (1 9 6), (4 3 2), (6 1 1), (4 1 5)]
[(4 3 2), (5 6 1), (6 1 1), (1 9 6), (5 8 3), (4 1 5), (7 9 3), (8 8 5), (8 4 6), (8 6 7)]
[(6 1 1), (4 3 2), (4 1 5), (5 6 1), (8 4 6), (8 6 7), (5 8 3), (8 8 5), (1 9 6), (7 9 3)]
[(7 9 3), (1 9 6), (8 8 5), (5 8 3), (8 6 7), (8 4 6), (5 6 1), (4 1 5), (4 3 2), (6 1 1)]
[(6 1 1), (4 1 5), (1 9 6), (4 3 2), (8 4 6), (5 6 1), (5 8 3), (8 6 7), (7 9 3), (8 8 5)]
[(1 9 6), (4 1 5), (4 3 2), (5 6 1), (5 8 3), (6 1 1), (7 9 3), (8 4 6), (8 6 7), (8 8 5)]
[(8 6 7), (8 4 6), (1 9 6), (8 8 5), (4 1 5), (7 9 3), (5 8 3), (4 3 2), (6 1 1), (5 6 1)]
[(1 9 6), (4 3 2), (4 1 5), (5 8 3), (5 6 1), (6 1 1), (7 9 3), (8 8 5), (8 6 7), (8 4 6)]
[(4 1 5), (6 1 1), (1 9 6), (4 3 2), (5 6 1), (8 4 6), (5 8 3), (8 6 7), (7 9 3), (8 8 5)]
[(8 8 5), (7 9 3), (8 6 7), (5 8 3), (5 6 1), (8 4 6), (4 3 2), (1 9 6), (4 1 5), (6 1 1)]

In the zip file there is some code to help you get started. The file Triple.java defines the Triple class and also contains a static method to generate lists of random triples (do not modify this class). The file CmpTriples.java outlines how you should define the "comparator factory" that can instantiate all eleven of the needed comparators. The file TestTriples.java outlines a main() method that should test your CmpTriples class by sorting a list of triples eleven ways by instantiating eleven different comparators. The file TestTriples_Random.java does the same thing, but on a randomly generated list of triples instead of on a single fixed list. Randomly generated lists of triples give you a more robust test of your comparators, but the fixed list comes with the expected output.

You do not need to write a sorting function. Use either the Collections.sort() static method or the List.sort() instance method.

Here are a few functional interfaces that might be useful in your comparator constructor.

Turn in a zip file called CS51510Hw1Surname.zip (where Surname is your last name) containing your versions of CmpTriples.java, TestTriples.java, and TestTriples_Random.java.

This assignment is due Friday, December 6.