Speed vs. complexity.
Let us define a static nested class Time
under PriorityQueueTest
that stores runtimes for the add()
and remove()
methods in any PQ:
What is the advantage of defining static nested class over non-static nested class?
Let us define addRuntime()
that estimates the runtime for add()
and remove()
for a particular PQ:
L5-8
: estimates the runtime for add()
:
L5
: gets the snapshot of the starting time using currentTimeMillis()
.
L7
: gets the snapshot of the ending time using currentTimeMillis()
.
L8
: accumulates the runtime to times.add
.
L11-14
: estimates the runtime for remove()
.
Is the approach using
currentTimeMillis()
a proper way of benchmarking the speed?
We then define benchmark()
that estimates the speeds of multiple PQs using the same lists of keys by calling addRuntime()
:
L1
: method declaration:
AbstractPriorityQueue[]
: It is unusual to declare an array of objects involving generics in Java (you will see the unique use case of it in the testSpeedAux()
method).
Supplier
: passes a function as a parameter that returns a key of the generic type T
.
L2
: generates an array of Time
using several stream operations. The dimension of times[]
is the same as the dimension of queues
such that times[i]
accumulates the speeds taken by the queue[i]
, respectively.
generate()
: creates a stream specified by the supplier.
limit()
: creates a stream with a specific size.
toArray()
: returns an array of the specific type.
L4
: benchmarks the PQs multiple times as specified by iter
.
L5
: generates the list of keys specified by the supplier sup
.
L6-7
: estimate the runtimes by using the same list of keys for each PQ.
Why do we need to benchmark the priority queues multiple times?
Let us define testSpeedAux()
that takes multiple PQs and prints out the benchmarking results:
L1
: annotates safeVarargs
indicating that this method takes vararg
parameters.
L2
: declares a final method that cannot be overridden by its subclasses.
It can take a 0-to-many number of abstract PQs.
The type of queues
is AbstractPriorityQueue<Integer>[]
such that it can be directly passed to the benchmark()
method.
L5
: creates a random generator.
L8
: benchmarks different sizes of PQs.
L10
: warms up the Java Virtual Machine (JVM) before the actual benchmarking.
rand::nextnt
that is equivalent to the lambda expression of () -> rand.nextInt()
.
L12
: benchmarks add()
and remove()
in the PQs.
L14-18
: prints the benchmark results.
L14
: uses StringJoiner
to concatenate strings with the specific delimiter.
L16
: exercise.
Why is it recommended to declare the method
final
if it takesvararg
parameters? Why does JVM need to be warmed-up before the actual benchmarking? Why should you useStringJoiner
instead of concatenating strings with the+
operator?
The followings show runtime comparisons between LazyPQ
, EagerPQ
, and BinaryHeap
:
This chapter discusses different types of priority queues and benchmarks their performance in terms of the worst-case complexities.
Lazy and eager priority queues.
A priority queue (PQ) is a data structure that supports the following two operations:
add()
: adds a comparable key to the PQ.
remove()
: removes the key with the highest (or lowest) priority in the PQ.
A PQ that removes the key with the highest priority is called a maximum PQ (max-PQ), and with the lowest priority is called a minimum PQ (min-PQ).
Does a priority queue need to be sorted at all time to support those two operations? What are the use cases of priority queues?
Let us define AbstractPriorityQueue
that is an abstract class to be inherited by all priority queues:
L1
: declares the generic type T
that is the type of input keys to be stored in this PQ.
T
must be comparable by its priority.
L2
: is a comparator that can compare keys of the generic type T
.
final
: must be initialized in every constructor.
Comparators: naturalOrder()
, reverseOrder()
.
L6
: the javadoc {@link}
hyperlinks to the specified methods.
What are comparable data types in Java? Can you define your own comparator?
Let us define three abstract methods, add()
, remove()
, and size()
in AbstractPriorityQueue
:
Given the abstract methods, we can define the regular method isEmpty()
:
Let us define LazyPriorityQueue
whose core methods satisfy the following conditions:
In other words, all the hard work is done at the last minute when it needs to remove the key.
L1
: declares T
and passes it to its super class, AbstractPriorityQueue
.
L2
: defines a list to store input keys.
L17-19
: overrides the size()
method.
Can you add keys to the member field
keys
when it is declared as final (a constant)? Why does all constructors inLazyPriorityQueue
need to call the super constructor?
We then override the core methods, add()
and remove()
:
L16
: edge case handling.
Let us define EagerPriorityQueue
whose core methods satisfy the following conditions:
In other words, all the hard work is done as soon as a key is added.
What are the situations that
LazyPQ
is preferred overEagerPQ
and vice versa?
The implementations of the two constructors and the size()
method are identical to the ones in LazyPriorityQueue
.
Should we create an abstract class that implements the above code and make it as a super class of
LazyPQ
andEagerPQ
? What level of abstraction is appropriate in object-oriented programming?
We then override the core methods, add()
and remove()
:
L10
: reverse engineers the return value of Collections.binarySearch()
.
What are the worst-case complexities of
add()
andremove()
inLazyPQ
andEagerPQ
in terms of assignments and comparison?
Operations: swim and sink.
A binary heap is a PQ that takesfor both add()
and remove()
, where keys are stored in a balanced binary tree in which every node has a higher priority than its children (when used as a max-PQ).
When should we use a binary heap over a lazy PQ or an eager PQ?
A balanced binary tree can be implemented by a list such that given the 'th key in the list:
The index of its parent:
The index of its left child: .
The index of its right child: .
e.g., given the list [1, 2, 3, 4, 5]
, 1
is the root, 2
and 3
are the left and right children of 1
, and 4
and 5
are the left and right children of 2
, respectively.
Is the tree represented by the list
[1, 2, 3, 4, 5]
balanced?
Let us create the BinaryHeap
class inheriting AbstractPriorityQueue
:
L11
: adding null
as the first item makes it easier to calculate the indices of parents and children.
L16
: keys
has the extra item of null
from L11
that is not an input key.
How does adding
null
make the calculation of the indices easier?
Additionally, let us define the helper method compare()
that compares two keys in the list:
L8
: calls the compare()
method in the member field priority
declared in the super class.
Add()
with SwimThe add()
method in a heap uses the operation called swim as follows:
L3
: appends the new key to the list.
L7-10
: keeps swimming until the list becomes a heap:
L8
: compares the new key to its parent if exists.
L9
: if the new key has a higher priority than its parent, Collections.swap()
them.
How many keys are compared at most for the `add operation?
Remove()
with SinkThe remove()
method in a heap uses the operation called sink as follows:
L3
: checks if the heap is empty.
L4
: swaps the root and the last key in the list.
L3
: removes the (swapped) last key with the highest priority in this heap.
L10-16
: keeps sinking until the list becomes a heap:
L12
: finds the child with a higher priority.
L13
: breaks if the parent has a higher priority than the child.
L14
: swaps if the child has a higher priority than the parent.
How many keys are compared at most for the
remove
operation?
Quiz 2: Priority Queues
Create a class called under the main package that extends the abstract class .
Each node in TernaryHeapQuiz
takes up to 3 children, so it becomes a ternary instead of a binary tree.
Override the required abstract methods, , , and , such that both add()
and remove()
give .
Feel free to use the code in .
Create the class under the test package.
Test the correctness of your TernaryHeapQuiz
using the method.
Add more tests for a more thorough assessment if necessary.
Compare runtime speeds between BinaryHeap
and TernaryHeapQuiz
for add()
and remove()
using the method.
Create a PDF file quiz2.pdf and write a report that includes the following:
A table and a chart to compare speeds between the two priority queues for those two methods, add()
and remove()
, with respect to different input sizes.
A brief explanation of why a certain PQ is faster than the other PQ with respect to different input sizes.
1. Commit and push everything under the following packages to your GitHub repository:
2. Submit quiz2.pdf to Canvas.
Robustness tests.
Let us create and define testRobustnessAux()
that checks the robustness of any PQ inheriting AbstractPriorityQueue
:
L6
: the generic type T
is defined specifically for this method such that the priority queue pq
, the list keys
, and the comparator comp
take the same type T
that is comparable.
L7
: any collection that inherits the interface has the member method that takes a and applies each key in the collection to the consumer.
L8
: any collection has the member method that returns the of the collection.
: creates a stream of the collection whose keys are sorted
: creates a collection specified by the .
: returns a collector that transforms the stream into a list.
L9
: iterates each of the sorted keys and compares it to the returned value of pq.remove()
.
What are the advantages of defining a method-level generic type?
The following code shows a traditional way of iterating over a list (that is equivalent to L7
above):
Java 5 introduced an enhanced for loop that simplified the traditional for loop:
The syntax of the above code can be simplified as follows (as shown in L7
):
What are the main differences between object-oriented programming and functional programming?
Let us define the testRobustness()
method that calls the auxiliary method testRobustnessAux()
:
L7-9
: tests different types of max-PQs. The keys need to be sorted in reverse order for it to test the remove()
method.
L11-13
: tests different types of min-PQs. The keys need to be sorted in the natural order for it to test the remove()
method.
The generic types of the PQs in
L4-5
are explicitly coded as<Integer>
, whereas they are simplified to<>
inL7-13
. When do generic types need to be explicitly coded?
The testRobustness()
method illustrates the benefit of defining AbstractPriorityQueue
such that any type of PQ can be passed to testRobustnessAux()
for unit-testing.
add()
: takesto add a key to the PQ.
remove()
: takes to remove the key with the highest/lowest priority from the PQ.
L6-8
: appends a key to the list in .
L15-20
: removes a key to the list in .
L17
: finds the max-key in the list using Collections.max()
in .
L18
: removes a key from the list in .
Is ArrayList
the best implementation of List
for LazyPriorityQueue
?
Why does remove()
in L18
cost ?
add()
: takesto add a key to the PQ.
remove()
: takes to remove the key with the highest/lowest priority from the PQ.
L6-12
: inserts a key to the list in .
L8
: finds the index of the key to be inserted in the list using binary search in .
L11
: inserts the key at the index
position in .
L19-21
: removes a key from the list in .
Main:
Test:
Java 8 introduced that enabled functional programming in Java. The following code takes each key
(as a variable) in keys
and applies it to pq.add()
:
Since Java 8, higher-order methods can be defined by parameterizing types of interfaces in the package.