Advanced Data Structures: Heaps and Priority Queues in Java

Understanding and implementing Heaps (Min-Heap, Max-Heap) and Priority Queues in Java, and their applications in Dijkstra's algorithm and other scenarios.


PriorityQueue in Java for Competitive Programming

What is a PriorityQueue?

A PriorityQueue is a data structure that implements a priority queue abstract data type. Unlike a regular queue, elements in a priority queue are associated with a "priority." Elements with higher priority are dequeued before elements with lower priority. In Java, the PriorityQueue class provides an efficient implementation of this data structure using a heap. By default, the PriorityQueue orders elements in ascending order (min-heap).

PriorityQueue Class in Java

The java.util.PriorityQueue class provides the following key features:

  • It is based on the heap data structure (usually a binary heap).
  • It does not allow null elements.
  • It is not thread-safe. If concurrent access is needed, use java.util.concurrent.PriorityBlockingQueue.
  • The head of the queue is the least element according to the specified ordering. If multiple elements are tied for least value, the head is one of those elements.
  • The offer, poll, remove(), and add methods provide basic queue operations.

Using the Built-in PriorityQueue Class

Basic Operations

Here's how to create and use a basic PriorityQueue:

 import java.util.PriorityQueue;

public class PriorityQueueExample {
    public static void main(String[] args) {
        // Create a PriorityQueue (min-heap by default)
        PriorityQueue<Integer> pq = new PriorityQueue<>();

        // Add elements
        pq.offer(10);
        pq.offer(2);
        pq.offer(8);
        pq.offer(1);
        pq.offer(5);

        System.out.println("PriorityQueue: " + pq); // Output will vary due to heap internal structure, but the smallest will be at the top

        // Get the head (smallest element) without removing
        System.out.println("Peek: " + pq.peek());  // Output: 1

        // Remove the head (smallest element)
        System.out.println("Poll: " + pq.poll());  // Output: 1

        System.out.println("PriorityQueue after poll: " + pq); // Output will vary

        // Check if the queue is empty
        System.out.println("Is empty: " + pq.isEmpty()); // Output: false

        // Get the size
        System.out.println("Size: " + pq.size());   // Output: 4
    }
} 

Customizing Priority using Comparators

To customize the priority (e.g., create a max-heap or order based on a different criteria), you can use a Comparator.

 import java.util.PriorityQueue;
import java.util.Comparator;

public class PriorityQueueComparatorExample {
    public static void main(String[] args) {
        // Create a PriorityQueue with a custom Comparator (max-heap)
        PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Comparator.reverseOrder());

        maxHeap.offer(10);
        maxHeap.offer(2);
        maxHeap.offer(8);
        maxHeap.offer(1);
        maxHeap.offer(5);

        System.out.println("Max Heap PriorityQueue: " + maxHeap); // Output will vary, but the largest will be at the top
        System.out.println("Peek (Max): " + maxHeap.peek());   // Output: 10
        System.out.println("Poll (Max): " + maxHeap.poll());   // Output: 10
        System.out.println("Max Heap PriorityQueue after poll: " + maxHeap); // Output will vary


        // Custom Comparator for objects based on a specific attribute (example: sorting Person objects by age)
        class Person {
            String name;
            int age;

            public Person(String name, int age) {
                this.name = name;
                this.age = age;
            }

            @Override
            public String toString() {
                return name + "(" + age + ")";
            }
        }

        PriorityQueue<Person> peopleQueue = new PriorityQueue<>(Comparator.comparingInt(p -> p.age)); // Sort by age (ascending)

        peopleQueue.offer(new Person("Alice", 30));
        peopleQueue.offer(new Person("Bob", 25));
        peopleQueue.offer(new Person("Charlie", 35));

        System.out.println("People Queue (sorted by age): " + peopleQueue); // Output will vary, but Bob will be at the top
        System.out.println("Oldest Person: " + peopleQueue.poll());

    }
} 

PriorityQueue in Competitive Programming

PriorityQueue is incredibly useful in competitive programming for problems involving:

  • Finding the Kth smallest/largest element: Efficiently keep track of the k smallest/largest elements seen so far.
  • Dijkstra's Algorithm: Used for finding the shortest path in a graph.
  • Heap Sort: Although not typically used directly, it's the underlying algorithm for PriorityQueue.
  • Merging K sorted arrays: Efficiently merge k sorted arrays into a single sorted array.
  • Scheduling problems: Optimal scheduling based on priority.

Example Problems and Solutions

Problem 1: Kth Largest Element in an Array

Given an array of integers nums and an integer k, return the kth largest element in the array. Note that it is the kth largest element in the sorted order, not the kth distinct element.

 // Input: nums = [3,2,1,5,6,4], k = 2
// Output: 5 
 import java.util.PriorityQueue;

class Solution {
    public int findKthLargest(int[] nums, int k) {
        PriorityQueue<Integer> minHeap = new PriorityQueue<>(); // Min-heap to store k largest elements

        for (int num : nums) {
            minHeap.offer(num);
            if (minHeap.size() > k) {
                minHeap.poll(); // Remove the smallest element if the heap size exceeds k
            }
        }

        return minHeap.peek(); // The smallest element in the heap is the kth largest overall
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int[] nums = {3, 2, 1, 5, 6, 4};
        int k = 2;
        int result = sol.findKthLargest(nums, k);
        System.out.println("Kth largest element: " + result); // Output: 5
    }
} 

Problem 2: Merge K Sorted Lists

You are given an array of k linked-lists lists, each linked-list is sorted in ascending order. Merge all the linked-lists into one sorted linked-list and return it.

 // Input: lists = [[1,4,5],[1,3,4],[2,6]]
// Output: [1,1,2,3,4,4,5,6] 
 import java.util.PriorityQueue;
import java.util.List;
import java.util.ArrayList;


class ListNode {
      int val;
      ListNode next;
      ListNode() {}
      ListNode(int val) { this.val = val; }
      ListNode(int val, ListNode next) { this.val = val; this.next = next; }
  }

class SolutionMergeKLists {
    public ListNode mergeKLists(ListNode[] lists) {
        PriorityQueue<ListNode> pq = new PriorityQueue<>( (a,b) -> a.val - b.val);

        for (ListNode head : lists){
            if (head != null) {
                pq.add(head);
            }
        }


        ListNode dummy = new ListNode(-1);
        ListNode tail = dummy;

        while(!pq.isEmpty()){
            ListNode curr = pq.poll();
            tail.next = curr;
            tail = curr;

            if (curr.next != null){
                pq.add(curr.next);
            }
        }

        return dummy.next;

    }

    public static void main(String[] args) {
        SolutionMergeKLists sol = new SolutionMergeKLists();
        // Create some sample linked lists for testing
        ListNode list1 = new ListNode(1, new ListNode(4, new ListNode(5)));
        ListNode list2 = new ListNode(1, new ListNode(3, new ListNode(4)));
        ListNode list3 = new ListNode(2, new ListNode(6));

        ListNode[] lists = {list1, list2, list3};

        ListNode mergedList = sol.mergeKLists(lists);

        // Print the merged list (optional)
        ListNode current = mergedList;
        System.out.print("[");
        while (current != null) {
            System.out.print(current.val);
            if (current.next != null) {
                System.out.print(", ");
            }
            current = current.next;
        }
        System.out.println("]"); // Output: [1, 1, 2, 3, 4, 4, 5, 6]
    }
}