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 PriorityQueue class is the JCF implementation which is implemented using a min-heap. It has essentially the same interface as a regular FIFO Queue (add to add items and poll to 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 PriorityQueue is another Java Generic that can store any reference-type.

There are couple of different ways that you can specify how objects of custom types are prioritized:

  1. Have the class implement the Comparable interface, and define a public int compareTo(T other) method.
  2. Construct a PriorityQueue with a Comparator object.

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 ForneymonTriage class that’s meant to prioritize treating the most wounded Forneymon (i.e., lowest health) before others. Implement the Comparable interface in the Forneymon class to enable this.

Step 1: have the class implement Comparable and stub the method public int compareTo(T other) where T specifies 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 compareTo method, which returns an integer that is:

  • 0 if this Forneymon and the other Forneymon have equal priority.
  • < 0 if this Forneymon has higher priority than the other (will be nearer to the top of the minheap).
  • > 0 if this Forneymon has lower priority than the other (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 PriorityQueue implementation, its contents this.compareTo(parent) method is called during the bubble-up process and this.compareTo(child) during bubble-down to decide if a swap is necessary or not!