Priority Queues and Lists
Yesterday, we looked at heaps. They are good for accessing the smallest element, but not great as a general representation for sets. If we needed to access the smallest element frequently, heaps could provide a good implementation data structure.
1 Priority Queues (ADT)
addElt: PriorQ, int -> PriorQ // adds element
remMinElt: PriorQ -> PriorQ // remove smallest element
getMinElt: PriorQ -> int // return, but don’t remove, smallest elt
Think for a few moments: what axioms do you expect on priority queues?
if e < getMinElt(PQ), then getMinElt(addElt(PQ,e))=e
if no duplicates in PQ, then NOT hasElt(remMinElt(PQ),peekMinElt(PQ))
Let’s contrast heaps with another possible data structure to implement priority queues: sorted lists. Sorted lists are a data structure, defined as follows:
A sorted list is a list in which the first element is smaller than all elements in the rest of the list and the rest of the list is a sorted list.
We have said that when we implement a data structure, we must build on its core shape. So before we can implement sorted lists, we need to implement plain lists in Java. We will work through the basic implementation of lists in class today. You will build on this to implement sorted lists in lab tomorrow (unless you’d rather implement tree rotation and AVL trees):
2 Lists
Recall what lists looked like in Racket:
; A list-of-number is |
; - empty, or |
; - (cons number list-of-number) |
Let’s convert this data definition to Java following the approach from the first week of the course: we should get an interface for the list-of-number type and classes for each of empty and cons:
interface IListNum {} |
|
class MtListNum implements IListNum { |
MtListNum(){} |
} |
|
class ConsListNum implements IListNum { |
int first; |
IListNum rest; |
|
ConsListNum(int first, IListNum rest) { |
this.first = first; |
this.rest = rest; |
} |
} |
The only substantive different with the migration on trees is that we had to create a class for the empty list, since every datum in Java is an object of a class.
To those with Java experience: we realize this probably isn’t how you learned to do lists. We’ll talk about the approach you’ve seen towards the end of these notes. For now, ask yourself why we might be doing things this way.
As an example of a method on lists, here’s the code for remElt:
interface IListNum { |
IListNum remElt (int elt); |
} |
|
class MtListNum implements IListNum { |
MtListNum(){} |
|
// element not in list, so nothing to remove |
MtListNum remElt (int elt) { |
return this; |
} |
} |
|
class ConsListNum implements IListNum { |
int first; |
IListNum rest; |
|
ConsListNum(int first, IListNum rest) { |
this.first = first; |
this.rest = rest; |
} |
|
// removes one occurrence of given element from list |
IListNum remElt(int elt) { |
if (elt == this.first) |
return this.rest; |
else |
return this.rest.remElt(elt); |
} |
} |
2.1 Removing the Minimum Element: Accumulators
How would we remove the minimum element? First, we have to locate it, then we have to remove it. How do we locate the minimum element?
One common approach is to recur down the list with an extra parameter that holds the smallest element seen so far. When we get to the empty list, we return that minimum value. This extra parameter that holds a value determined over the course of the computation is called an accumulator (it accumulates a value over the computation). We will write a separate method findMinEltAccum to find the smallest element, then have the findMinElt method call this function initialized with the first element in the list. The code follows:
interface IListNum { |
IListNum remElt (int elt); |
IListNum remMinElt (); |
int findMinElt (); |
int findMinEltAccum (int min); |
} |
|
class MtListNum implements IListNum { |
MtListNum(){} |
|
// element not in list, so nothing to remove |
public MtListNum remElt (int elt) { |
return this; |
} |
|
public MtListNum remMinElt () { |
return this; |
} |
|
public int findMinElt () { |
throw new RuntimeException("can't find min in empty list"); |
} |
|
public int findMinEltAccum (int min) { |
return min; |
} |
} |
|
class ConsListNum implements IListNum { |
int first; |
IListNum rest; |
|
ConsListNum(int first, IListNum rest) { |
this.first = first; |
this.rest = rest; |
} |
|
// removes one occurrence of given element from list |
public IListNum remElt(int elt) { |
if (elt == this.first) |
return this.rest; |
else |
return new ConsListNum(this.first, this.rest.remElt(elt)); |
} |
|
public IListNum remMinElt() { |
return this.remElt(this.findMinElt()); |
} |
|
// initialize accumulator to first, find min in rest |
public int findMinElt () { |
return this.rest.findMinEltAccum(this.first); |
} |
|
public int findMinEltAccum (int min) { |
if (this.first < min) |
return this.rest.findMinEltAccum(this.first); |
else |
return this.rest.findMinEltAccum(min); |
} |
} |
|
One potential problem with this code is that it doesn’t limit findMinEltAccum to being used as a helper for findMinElt. We’ll deal with that issue next week.
2.2 Why Do Lists This Way?
If you’ve seen Java before, you almost certainly saw lists done differently, using one of the following two ways:
Using the built-in Java list classes.
Built-in Java lists do not satisfy our ISet interface because the return types are different. The nearest analog to cons (addElt), for example, has the following type for a list of integers:
addFirst : int -> void (where the list arg is implicit)
Where’s the list containing the new element? Whereas our ISet interface returns the new list, the Java list library reuses the existing object and changes its contents to include the new list. Changing the contents of objects can have some nasty consequences. We will spend considerable time discussing the advantages and disadvantages of mutating data structures after Thanksgiving.
Using a single class for both the empty and cons cases.
Roughly speaking, this approach captures a list in a single class:
class ListNode {
int first;
ListNode rest;
...
}
One would set rest to a special value called null to represent the empty list (this passes the type checker in Java).
This solution is awful. Every time you call a method on a list, you have to check whether you have an empty list. You have to clutter up your code with a lot of tests about the "type" of a list. What if you forget to make this test and accidentally try to call a method on a null object? Java gives you a run-time error. If you use a separate subclass for the empty class, Java takes care of calling the proper code automatically. Your code is simply cleaner.
We repeat an earlier mantra: TYPE CHECKS ARE NOT PART OF OO PROGRAMMING. OO uses dispatch, not type checks.
Let’s say this again: null-delimited lists are not OO lists. Even the inventor of null references now gives talks calling this his "billion dollar mistake" (a rough estimated cost of null errors in software over the years). Google advocates against them in production code (see the Google code talk linked to the lectures webpage.)
To wrap up, write out the recurrences for the priority queue operations on lists. Compare to the recurrence for remMinElt on heaps. This motivates preferring sorted lists to regular lists.