Priority

Per usual, with a new data structure, let's think about some motivations for new problems that our current tools don't adequately address...


Motivation


How about a nice motivating example to get us started?

You are managing a massive, worldwide delivery service, and need to assess some Priorities of deliveries so that the highest Priority items are delivered before the others.

Let's think about some challenges in this scenario:

What might be some challenges associated with maintaining this collection of orders as a List or a BinarySearchTree?

Some issues, among others:

  • Since priorities might not be unique, a BST can be challenging to use, and even if they were unique, searching for the highest priority would either mean having an unbalanced tree or one that still takes O(log(n)) to find.

  • Searching for the highest-priority order would be an O(n) operation on an unsorted list, or O(log(n)) on a sorted one.

  • Maintaining an ArrayList means that as orders with different priorities come in, much shifting might be necessary to insert the new orders.

  • Maintaining a LinkdList is immediately infeasible since its search would be O(n) at best, unable to take advantage of binary search on an ArrayList.

As such, it seems that all of our existing techniques lack *some* desire in this example, and we should make it a *priority* to find some solutions to the following:

Our objective(s) will be to find a data structure that:

  • Returns the highest "need / priority" item in some collection as quickly as possible.

  • Can efficiently handle insertions / removals of items while still guaranteeing this first property.

  • Can even be used to find the n-greatest need items from a larger collection.


Basics


A Priority Queue is a generalized version of a queue in which items can be retrieved according to the ranking / ordering of some arbitrary criteria.

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.

By what definition of "priority" would a standard FIFO Queue be considered a special case of a Priority Queue?

Order of insertion! Standard Queue items are simply removed in the FIFO order of their insertion; we could mimic this behavior with a priority queue by simply logging the order of insertion and always removing the one with the smallest order counter first.



Heaps

To implement a Priority Queue, let's talk about a new tree-based data structure. The heap!

A heap (generally referring to a max heap) is a special type of tree where *all* subtrees of a given Node have values less than or equal to that Node's.

WARNING: The heap data structure is on no way related to the heap where objects are stored!

Such unfortunate naming schemas in computer science... it's not like humans have infinite generative language capacities or anything...

Heaps are typically used when we need immediate access (O(1)) to the largest / smallest "priority" element in a collection, and can quickly reorder the collection to maintain this property at all times.


Here's a fun comic that illustrates how they work:


Examining this amusing XKCD, we do notice something interesting about the heap of presents: where is the largest present located, and why is this interesting / useful?

The largest is located at the root! This means we could access it in constant time, since we always know where the root of a tree is.

In the above comic, we see that the heap of presents at the base has the largest at the top with children decreasing in size with greater depth.

Our discussion, however, will be restricted to binary heaps, which have some additional restrictions.

A binary heap is a complete binary tree where all subtrees of a given Node have values less than or equal to that Node's.


So we're restricting the number of children any Node can have to 2, AND requiring the binary tree be complete.

A complete binary tree is a binary tree with all full levels, except for possibly the last level, in which case it is required that the Nodes be filled from left to right without any spaces.


Is the following binary tree complete? Is it a heap?


Is the following binary tree complete? Is it a heap?


Is the following binary tree complete? Is it a heap?


Is the following binary tree complete? Is it a heap?


Is a single-node tree a heap?

Yes! It must be complete because the last (and only) level is full, and its value must be larger than its subtrees because it has no subtrees!


How many unique structural configurations (ignoring node values) are there for a complete binary tree with N nodes?

Just 1! If you were to change the position of any node in the structure, it would cease to be complete.


Heap Properties


Heaps have some nice properties, for example:

The largest value of a (max)heap is always at the root, and therefore accessible with constant time, O(1)!

Because heaps are complete trees, we can represent them as arrays to enable random access, with indexing starting at the root and numbering nodes level-by-level from left-to-right.

What performance benefit does representing a complete tree as an array have over a Node-with-pointers implementation?

Arrays are indexed, meaning we can access any element with constant time, O(1).



"Nice colors in that graph, Andrew, do you also design children's play equipment?"

Shhh! It was illustrative!


We know that in the array representation of a heap, the root must always be at index 0.


But how do we determine the children and parent of any given index in an array?

Well, since it's a complete tree, we have a simple computational way of finding these indexes...

Given the index n of a "Node" in our conceptual tree structure, how can I get the index of the parent?

Index of parent: (n - 1) / 2; (NOTE: this is integer division, so we ignore the decimal of the quotient)


So, we can define a simple functional mapping, pretending that we have a Heap class (to be done later):

  private int getParent (int index) {
      return (index - 1) / 2;
  }

Getting the left and right children is a similar mechanical exercise:

  // Child is either 'L' or 'R'
  private int getChild (int index, char child) {
      int result = (index * 2) + 1;
      if (child == 'R') {
          result++;
      }
      return result;
  }

Draw the binary tree representation of the following heap:


Now that we have the basic tools down, we're ready to go over some heap algorithms!



Heap Operations

The key with any heap operation is that it leaves the resulting data in heap format.

So, whenever we modify the contents of a heap, we need to make sure that we "reheapify" its elements afterwards.

This maintains the constant time access of the largest element at the root.

After every operation on a heap, we have some general steps: perform the operation and then "reheapify" the binary tree.

Insertion


For insertion, that looks like this:

  1. Create a complete binary tree with the newly inserted node in its proper location as the left-most vacant leaf position on the last level of the tree (i.e., the only place we could insert a leaf and still have the tree be complete).

  2. Starting at that newly inserted node, bubble upwards ensuring that each node is less than its parent.

  3. As soon as a parent is greater than the node we're bubbling, stop--your array is now reheapified! (assuming it was a heap before the insertion).


Why is it sufficient to conclude that in step 3, when the parent is greater than the node we're bubbling, that we may stop? Why must we not also continue checking farther up the heap to verify its heapiness?

By assumption, we are inserting into a structure that is already a heap. This means that all nodes are already abiding by the definition of a heap. So, when we insert a new node, and stop bubbling up whenever the parent is larger than what we're bubbling, we can trust that all nodes updwards are already heap-compliant.

Using the above steps, insert the node with value 50 into the heap below:





Got that?

Let's make a simple BinaryMaxHeap class with this algorithm!

I'll start you off...

Complete the insert function and its helper, bubbleUp, in our BinaryMaxHeap below:


  package tree.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 () {
          this.heap = new ArrayList<>();
      }
      
      // 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) {
          // 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);
      }
      
      // Helper Methods
      // -------------------------------------------------------------------------
      
      /**
       * 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);
          }
      }
      
      /**
       * 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 division
      }
      
      /**
       * 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;
      }
      
      @Override
      public String toString () {
          return this.heap.toString();
      }
      
      // Empirical Tests
      // -------------------------------------------------------------------------
      
      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);
      }
  
  }

Great! On to deletion.


Removal


Nothing particularly surprising about deletion; it's essentially the same algorithm as insertion, in reverse!

Just like we might pop an item off the top of a Stack, we also typically remove the root of a heap to get the next largest / smallest elemetn.

That said, the algorithm for removing items from a heap is generalized to removing an item at any index (thus the beauty of recursive data structures).

  1. Remove the target node from the heap

  2. Promote the bottom-right-most leaf of the last level to the position of the removed node

  3. Trickle down the promoted item such that if either of its two children are greater than it, then it trades places with the greatest of its children.

  4. Continue trickling down until no child is greater than the trickling node.


Let's try removing the 50 node at the root from the heap below:





I shall leave it as an exercise to implement the deletion function into our BinaryMaxHeap class!


What is the time complexity of insertion? Of deletion?

Both are O(log(n)), for the same reason that binary search was. At each step of bubbling, we're ignoring 1/2 of the other nodes in the heap.

To find an element in a heap, can we perform the same binary search algorithm on heaps that we can on binary search trees?

No, we do not have the same guarantee that an item must be found in one subtree or another; it could be at either if it is less than a given Node N.



Priority Queues

Priority Queues are a data type that are implemented using the heap data structure to manage collections whose contents specify a definition of priority.

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.

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.

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:

  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.


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!

  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!



  PDF / Print