Lesson 4
Heaps and Array Manipulation in Java
Introduction

Hello there, budding programmer! I hope you're ready because today, we're going to dive deep into high-level data manipulation and increase our understanding of heaps. Heaps are fundamental data structures commonly used in algorithms. We're going to leverage their potential today in an interesting algorithmic problem. Are you ready for the challenge? Let's get started!

Task Statement

We have a task at hand related to array manipulation and the use of heaps. The task is as follows: Given an array of unique integers with elements ranging from 11 to 10610^6 and length between 11 to 10001000, we need to create a Java method prefixMedian(). This method will take the array as input and return a corresponding array, which consists of the medians of all the prefixes of the input array.

Remember that a prefix of an array is a contiguous subsequence that starts from the first element. The median of a sequence of numbers is the middle number when the sequence is sorted. If the length of the sequence is even, the median is the element in the position length / 2 - 1.

For example, consider an input array {1, 9, 2, 8, 3}. The output of your method should be {1, 1, 2, 2, 3}.

Heap and Its Operations

A heap is a useful tool in Java that helps efficiently organize and retrieve data based on their values.

In our context, we use two specific types of heaps: a Min Heap and a Max Heap. The Min Heap is used to store the larger half of the numbers seen so far, while the Max Heap stores the smaller half. In Java, heaps are implemented using the PriorityQueue class.

For our task, we'll be using these principal operations:

  • Adding Elements: You can add a new element to a heap using the add() method. By default, Java's PriorityQueue is a Min Heap. To create a Max Heap, you need to provide a custom comparator like Comparator.reverseOrder(). This ensures that the largest element is always at the top.

    Java
    1PriorityQueue<Integer> minHeap = new PriorityQueue<>(); 2PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Comparator.reverseOrder());
  • Removing Elements: The poll() method removes the top element from the heap, which is the smallest element in the Min Heap or the largest element in the Max Heap.

  • Accessing Minimum Element: The peek() method is used to inspect the root element of the heap without removing it. For the Min Heap, peek() will return the smallest element.

These operations ensure that the root element can always be gathered quickly, in constant time O(1)O(1), and new elements can be added while maintaining the heap structure in logarithmic time, O(logn)O(log n).

Solution Building: Step 1

Alright, let's break our approach down into manageable steps. To begin with, we're going to need two heaps: a Min Heap to store the larger half of the numbers seen so far and a Max Heap to store the smaller half. We'll also need an array to store the median for each prefix. Now, let's initialize these.

Java
1import java.util.*; 2 3public class PrefixMedian { 4 public static int[] prefixMedian(int[] arr) { 5 PriorityQueue<Integer> minHeap = new PriorityQueue<>(); 6 PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Comparator.reverseOrder()); 7 int[] medians = new int[arr.length]; 8 } 9}
Solution Building: Step 2

As the next step, we will sequentially take each number from the array and, depending on its value, push it into the minHeap or the maxHeap. If it is smaller than the maximum of the lower half, it will go into the maxHeap. Otherwise, it will go into the minHeap.

Java
1import java.util.*; 2 3public class PrefixMedian { 4 public static int[] prefixMedian(int[] arr) { 5 PriorityQueue<Integer> minHeap = new PriorityQueue<>(); 6 PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Comparator.reverseOrder()); 7 int[] medians = new int[arr.length]; 8 9 for (int num : arr) { 10 if (!maxHeap.isEmpty() && num < maxHeap.peek()) { 11 maxHeap.add(num); 12 } else { 13 minHeap.add(num); 14 } 15 } 16 } 17}
Solution Building: Step 3

Next, we need to balance the two heaps to ensure that the difference between their sizes is never more than one. This way, we can always have quick access to the median. If the maxHeap size becomes larger than the minHeap, we poll the maxHeap's top element and add it to the minHeap. If the minHeap becomes more than one element larger than the maxHeap, we do the reverse.

Java
1import java.util.*; 2 3public class PrefixMedian { 4 public static int[] prefixMedian(int[] arr) { 5 PriorityQueue<Integer> minHeap = new PriorityQueue<>(); 6 PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Comparator.reverseOrder()); 7 int[] medians = new int[arr.length]; 8 9 for (int num : arr) { 10 if (!maxHeap.isEmpty() && num < maxHeap.peek()) { 11 maxHeap.add(num); 12 } else { 13 minHeap.add(num); 14 } 15 16 if (maxHeap.size() > minHeap.size()) { 17 minHeap.add(maxHeap.poll()); 18 } else if (minHeap.size() > maxHeap.size() + 1) { 19 maxHeap.add(minHeap.poll()); 20 } 21 } 22 } 23}
Solution Building: Step 4

Having balanced the heaps, we've set ourselves up for the effortless retrieval of the median. We compute the median based on the elements at the top of the maxHeap and minHeap, and then append it to our array of medians.

Java
1import java.util.*; 2 3public class PrefixMedian { 4 public static int[] prefixMedian(int[] arr) { 5 PriorityQueue<Integer> minHeap = new PriorityQueue<>(); 6 PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Comparator.reverseOrder()); 7 int[] medians = new int[arr.length]; 8 9 for (int i = 0; i < arr.length; i++) { 10 int num = arr[i]; 11 if (!maxHeap.isEmpty() && num < maxHeap.peek()) { 12 maxHeap.add(num); 13 } else { 14 minHeap.add(num); 15 } 16 17 if (maxHeap.size() > minHeap.size()) { 18 minHeap.add(maxHeap.poll()); 19 } else if (minHeap.size() > maxHeap.size() + 1) { 20 maxHeap.add(minHeap.poll()); 21 } 22 23 if (minHeap.size() == maxHeap.size()) { 24 medians[i] = maxHeap.peek(); 25 } else { 26 medians[i] = minHeap.peek(); 27 } 28 } 29 30 return medians; 31 } 32 33 public static void main(String[] args) { 34 int[] arr = {1, 9, 2, 8, 3}; 35 int[] medians = prefixMedian(arr); 36 37 System.out.println("Final Medians Array: " + Arrays.toString(medians)); 38 } 39}
Lesson Summary

Congratulations! You've successfully tackled an interesting algorithmic problem that required the use of heaps for array manipulation in Java. The solution you've created not only uses heaps but also demonstrates your understanding of array hierarchies and the meaningful interpretation of numerical values.

In the next session, you'll be given more similar problems to solve. This will encourage you to use heaps and array manipulations fluently, helping you consolidate your understanding of today's lesson. Keep practicing, and remember — practice makes perfect. Happy coding!

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.