The easy way🔗
The winner of a marathon is the Runner who finishes with the fastest time.
How much of our code above can we reuse to solve this new question?
If we try sorting the list of Runners by time, then the winner should simply
be the Runner at the front of the resulting sorted list. So as an intuitive first
guess, we might try marathon.sortBy(new CompareByTime()).first ...but this
will fail to typecheck, and in fact is problematic for two related reasons. First,
the result of sorting an ILoRunner is always another ILoRunner, and the ILoRunner
interface does not have a first field. Second, finding “the winner” of a marathon
is only well-defined if the marathon is not an empty list. So we might try writing the following:
| Runner findWinner(); |
| Runner getFirst(); |
Try implementing these two methods for MtLoRunner and ConsLoRunner.
Pay attention to the differences in the purpose statements.
| Runner findWinner() { |
| throw new RuntimeException("No winner of an empty list of Runners"); |
| } |
| Runner getFirst() { |
| throw new RuntimeException("No first of an empty list of Runners"); |
| } |
| Runner findWinner() { |
| return this.sortBy(new CompareByTime()).getFirst(); |
| } |
| Runner getFirst() { |
| return this.first; |
| } |
Note that the use of getFirst in ConsLoRunner will never fail, because
sorting a non-empty list will result in a non-empty list, so getFirst will be
invoked on some ConsLoRunner object, which indeed has a first field.
14.3.1 The smarter way🔗
In Lecture 27, we’ll learn a bit more precisely what is “extravagant” about this approach.
It seems a bit extravagant that to find the winner of a marathon, we sort an entire list of Runners
and produce an entirely new sorted list, only to take the first item and throw the rest of the list away!
If most of that information is unneeded, could we be cleverer and avoid constructing the entire list?
The key observation here is that to find the fastest runner, we simply need to keep track of the fastest runner seen so far
as we recur over the list; once we’ve processed the entire list, the fastest runner seen so far is therefore the fastest
runner overall — i.e., the winner.
More generally, we might want to find the minimum runner according to any comparison, by keeping track of the minimum seen so far.
| Runner findMin(IRunnerComparator comp); |
In the empty case, as above, we just throw exceptions since there is no minimum of an empty list.
In the non-empty case, finding the winner is just a special case of finding a minimum, as compared by time:
| public Runner findWinner() { return this.findMin(new CompareByTime()); } |
Now we just need to finish designing the findMin method.
We know that findMin only makes sense for non-empty lists. It cannot possibly succeed for empty lists, since
in the empty case, there is no minimum runner, so we have no good answer to return. In that
situation, the only thing we can do is throw an exception:
| public Runner findMin(IRunnerComparator comp) { |
| throw new RuntimeException("No minimum runner available in this list!"); |
| } |
The non-empty case is a bit subtle.
Try to implement this method on the ConsLoRunner class. Where does it get stuck?
If we look at the template for this method, we see
| public Runner findMin(IRunnerComparator comp) { |
| |
| ... |
| } |
We have two problems: first, if we simply call this.rest.findMin(...), eventually we
will reach the MtLoRunner case, and our code above will inevitably throw an exception.
Second, in order to use comp.comesBefore, we need two Runners — but a ConsLoRunner
only has one available.
But wait! Remember our description above: we want to keep track of the minimum Runner seen so far. This
sounds like we need an accumulator parameter, which means we need a helper method:
| Runner findMinHelp(IRunnerComparator comp, Runner acc); |
Implement findMinHelp for MtLoRunner and ConsLoRunner.
In the empty case, there are no Runners in the list, so the minimum must be the accumulator:
| public Runner findMinHelp(IRunnerComparator comp, Runner acc) { return acc; } |
In the non-empty case, we’re now unstuck:
| public Runner findMinHelp(IRunnerComparator comp, Runner acc) { |
| if (comp.compare(acc, this.first) < 0) { |
| return this.rest.findMinHelp(comp, acc); |
| } |
| else { |
| return this.rest.findMinHelp(comp, this.first); |
| } |
| } |
| public Runner findMin(IRunnerComparator comp) { |
| return this.rest.findMinHelp(comp, this.first); |
| } |
Notice that this code has only one potential exception (if we call findMin on an empty list),
and every other code path cannot possibly throw an exception. Much better!
Design a comparator class ReverseComparator that takes an IRunnerComparator
as a parameter, and whose behavior is the reverse of that parameter. I.e., if the given
comparator says one Runner is less than the other, then the ReverseComparator
will say that it is greater.
Design a findMax method. (Hint: There is a very short solution, using the answer to the previous exercise.)