Lecture 21: High Priority Programming
Last lecture, we talked about a theoretical overview for a new Data Structure called a Heap - with our theoretical foundations firm, let’s take a look at getting it into code:
Heaps - Implementation
We’re going to create a class for a BinaryMaxHeap, a Binary Heap that allows instant access to the highest value/priority integer in the heap!
import java.util.*;
/**
* Binary Max Heap storing Integer values representing
* priorities of any arbitrary complex Node we could store
*/
public class BinaryMaxHeap {
// Fields
// -------------------------------------------------------------------------
private ArrayList<Integer> heap;
// Constructor
// -------------------------------------------------------------------------
public BinaryMaxHeap() {
throw new UnsupportedOperationException();
}
// Methods
// -------------------------------------------------------------------------
/**
* Adds the given int (representing a priority) to the
* proper spot in the heap
*
* @param toAdd Priority we desire to add
*/
public void add(Integer toAdd) {
throw new UnsupportedOperationException();
}
/**
* Removes and returns the highest priority
* item in the heap
* @return the removed item
*/
public int poll() {
throw new UnsupportedOperationException();
}
}The Constructor
Let’s start with the constructor - remember that when we talked about the implementation of a Binary Heap, while our visualization of it looked like a tree, we were able to actually just store it within an ArrayList - in this case, we’ll just use The JCF’s ArrayList to get the job done:
public BinaryMaxHeap() {
this.heap = new ArrayList<>();
}Hey, that was easy! Okay, let’s focus on add:
Adding
public void add(Integer toAdd) {
// Step 1: Maintain a complete binary tree by adding the
// new value at the bottom-most, right-most leaf spot, or
// an append operation into the array
this.heap.add(toAdd); // Appends toAdd to end
// Step 2: Reheapify: bubble-up the value from its current
// index into one where it is <= its parent's
}Step 1 done! Step 2… a little more complicated. If your intuition for Tree algorithms is sharp, you’ve probably figured out that we need some kind of recursive method to effectively reheapify - but before we get there, let’s get ourselves a couple other handy helper methods first.
Reheapification requires access to a parent and its children, and since we’re operating with a list and not a tree, we’ll have to rely on those handy mathematical calculations we relied on last time.
To bubble up, we need to compare some “node” with the value within its parent’s node, so we’ll focus on that first:
/**
* Return the index of the parent of the given child in the
* array representing this heap
*
* @param childIndex Index of child from which to get the parent's
* @return Parent's index of the given child
*/
private int getParentIndex(int childIndex) {
return (childIndex - 1) / 2; // [!] Integer(floor) division
}Okay, great! Now we can reheapify and bubble up:
/**
* Recursively bubbles up the value at the given index until
* its priority is less than or equal to its parent's
*
* @param index Index at which to check for bubbling
*/
private void bubbleUp(int index) {
// Base case:
if (index == 0) {
return;
} // At root, no where else to bubble!
int parentIndex = getParentIndex(index),
parentPri = heap.get(parentIndex),
currentPri = heap.get(index);
// Check if parent's priority is less than the current, if so
// bubble up!
if (parentPri < currentPri) {
// bubble-up == swap operation with parent
// Visit behavior:
heap.set(index, parentPri);
heap.set(parentIndex, currentPri);
// Recursive case:
bubbleUp(parentIndex);
}
}And now that we’ve done that, we can finish off add:
/**
* Adds the given int (representing a priority) to the
* proper spot in the heap
*
* @param toAdd Priority we desire to add
*/
public void add(Integer toAdd) {
// Step 1: Maintain a complete binary tree by adding the
// new value at the bottom-most, right-most leaf spot, or
// an append operation into the array
this.heap.add(toAdd); // Appends toAdd to end
// Step 2: Reheapify: bubble-up the value from its current
// index into one where it is <= its parent's
this.bubbleUp(this.heap.size() - 1);
}Let’s also throw a toString into our Heap so we can see what’s going on inside it:
@Override
public String toString () {
return this.heap.toString();
}Let’s give it a test:
public static void main(String[] args) {
BinaryMaxHeap b = new BinaryMaxHeap();
b.add(25);
b.add(10);
b.add(20);
b.add(8);
System.out.println("BEFORE adding 50:");
System.out.println(b);
b.add(50);
System.out.println("AFTER adding 50:");
System.out.println(b);
}Looks like it’s working! Now, let’s focus on poll:
Removal
When removing a value from a Heap, we generally only care about the highest priority item in the heap - meaning the head of the tree!
Let’s get the base requirement out of the way: returning the highest priority item
/**
* Removes and returns the highest priority
* item in the heap
* @return the removed item
*/
public int poll() {
int polledValue = this.heap.get(0);
// TODO: Remove head and reheapify!
return polledValue;
}The first part of reheapifying is to take the bottom-right value and make it our new head!
/**
* Removes and returns the highest priority
* item in the heap
* @return the removed item
*/
public int poll() {
int polledValue = this.heap.get(0);
int lastValue = this.heap.remove(this.heap.size() - 1);
if (!this.heap.isEmpty()) {
this.heap.set(0, lastValue);
// TODO: Reheapify!
}
return polledValue;
}This removes the last item in the list and assigns it to index 0, overwriting what was previously there.
To reheapify, we’ll need another helper! (big surprise) But first, let’s figure out how to get the children of any given node first:
/**
* Returns the index of the given 'L' or 'R' child given the parent
*
* @param parentIndex Index of the parent in the array
* @param child 'L' or 'R' for desired child
* @return Index corresponding to the parent's 'L' or 'R' child
*/
private int getChildIndex(int parentIndex, char child) {
int result = (parentIndex * 2) + 1; // Index of Left child
if (child == 'R') {
result++;
}
return result;
}And now for trickling our new root node down to where it belongs in the heap:
/**
* Recursively trickles down the value at the given index
* until its priority is greater than or equal to its children's
* @param parentIndex Index at which to check for trickling
*/
private void trickleDown(int parentIndex) {
int leftChildIndex = this.getChildIndex(parentIndex, 'L');
int rightChildIndex = this.getChildIndex(parentIndex, 'R');
int parent = this.heap.get(parentIndex);
int leftChild = leftChildIndex < this.heap.size() ? this.heap.get(leftChildIndex) : -1;
int rightChild = rightChildIndex < this.heap.size() ? this.heap.get(rightChildIndex) : -1;
if (parent < leftChild && leftChild >= rightChild) {
this.heap.set(parentIndex, leftChild);
this.heap.set(leftChildIndex, parent);
this.trickleDown(leftChildIndex);
} else if (parent < rightChild) {
this.heap.set(parentIndex, rightChild);
this.heap.set(rightChildIndex, parent);
this.trickleDown(rightChildIndex);
}
}And now to complete poll
/**
* Removes and returns the highest priority
* item in the heap
* @return the removed item
*/
public int poll() {
int polledValue = this.heap.get(0);
int lastValue = this.heap.remove(this.heap.size() - 1);
if (!this.heap.isEmpty()) {
this.heap.set(0, lastValue);
this.trickleDown(0);
}
return polledValue;
}More tests!
public static void main(String[] args) {
BinaryMaxHeap b = new BinaryMaxHeap();
b.add(50);
b.add(25);
b.add(20);
b.add(8);
b.add(10);
b.add(8);
System.out.println("BEFORE POPPING 50");
System.out.println(b);
b.poll();
System.out.println("AFTER POPPING 50");
System.out.println(b);
}Priority Queues
So we’ve spent all this time talking about Heaps, but it’s incumbent on us to remember why we were doing it -
Priority Queues are a data type that are implemented using the heap data structure to manage collections whose contents specify a definition of priority.
This might mean, e.g.:
- Provide the next patient with the “highest-need” triage score.
- Provide the next closest missile with the “highest-risk” threat score.
- Attempt the next best move for an artificial agent solving a problem.
Priority Queues are useful for pretty much anything to which you can assign some rankable-score, and in which it’s important that we receive the next “highest-priority” immediately while being able to store them effectively, and retrieve the next highest priority quickly as well.
The
PriorityQueueclass is the JCF implementation which is implemented using a min-heap. It has essentially the same interface as a regular FIFO Queue (addto add items andpollto remove the highest priority), but is instead ordered by each item's priority.
Example
Here’s a small example of a Priority Queue of Integer values; for Integers, their priority is the same as their value.
PriorityQueue<Integer> pq = new PriorityQueue<>();
pq.add(20);
pq.add(10);
pq.add(30);
// [?] What gets printed below?
System.out.println(pq.poll());
System.out.println(pq.poll());
System.out.println(pq.poll());Comparable Interface
Note that the Java
PriorityQueueis another Java Generic that can store any reference-type.
Suppose we wished to store Forneymon in a PriorityQueue. What would we have to specify?
How we associate some value of Priority with a Forneymon!
There are couple of different ways that you can specify how objects of custom types are prioritized:
- Have the class implement the
Comparableinterface, and define apublic int compareTo(T other)method. - Construct a PriorityQueue with a
Comparatorobject.
The first method is appropriate when all instances of a class will be prioritized the same way, the second when you might prioritize them differently under different circumstances.
Let’s take a look at using the Comparable interface in this lecture.
Example
Consider a
ForneymonTriageclass that’s meant to prioritize treating the most wounded Forneymon (i.e., lowest health) before others. Implement theComparableinterface in the Forneymon class to enable this.
Step 1: have the class implement
Comparableand stub the methodpublic int compareTo(T other)whereTspecifies what classes the implementing one should be comparable to.
// [!] Note: Comparable is a generic as well, so we must specify what Forneymon are
// comparable to -- in this case, we want it to be comparable to other Forneymon
abstract public class Forneymon implements Comparable<Forneymon> {
// ... Other Forneymon contents elided
public int compareTo (Forneymon other) {
throw new UnsupportedOperationException();
}
// ... Other Forneymon contents elided
}Step 2: implement the
compareTomethod, which returns an integer that is:
0ifthisForneymon and theotherForneymon have equal priority.< 0ifthisForneymon has higher priority than theother(will be nearer to the top of the minheap).> 0ifthisForneymon has lower priority than theother(will be nearer to the bottom of the minheap).
public int compareTo (Forneymon other) {
// [!] Will be < 0 (higher priority) when this Forneymon has less health
// than the other!
return this.health - other.health;
}Finally, testing it, we see that our most wounded Forneymon will be polled first!
public static void main(String[] args) {
PriorityQueue<Forneymon> fmTriage = new PriorityQueue<>();
Burnymon b1 = new Burnymon("b1"),
b2 = new Burnymon("b2"),
b3 = new Burnymon("b3");
b2.takeDamage(10, "Burny");
b3.takeDamage(5, "Burny");
fmTriage.add(b1);
fmTriage.add(b2);
fmTriage.add(b3);
// [?] In what order will the Burnymon be polled below?
while (!fmTriage.isEmpty()) {
System.out.println(fmTriage.poll());
}
}That's it! Internal to the
PriorityQueueimplementation, its contentsthis.compareTo(parent)method is called during the bubble-up process andthis.compareTo(child)during bubble-down to decide if a swap is necessary or not!