Lesson 4
Efficient Array Manipulation with Heaps in C#
Introduction

Hello there, aspiring C# developer! I hope you're ready because today we're going to delve into high-level data manipulation and increase our understanding of heaps in C#. Heaps are fundamental data structures that play a significant role in various algorithms. Today, we'll unlock their potential in an intriguing algorithmic problem. Are you ready for the challenge? Let's get started!

Task Statement

We have a task related to array manipulation and the use of heaps. The task is as follows: Given an array of unique integers with elements ranging from 1 to (10^6) and a length between 1 and 1000, we need to create a C# 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

In C#, a heap is a sophisticated, binary tree-based data structure designed with the heap property in mind: for a Min Heap, every parent node’s value is less than or equal to its children, while for a Max Heap, each parent node's value is greater than or equal to its children. These properties make heaps ideal for efficiently finding and removing the minimum or maximum elements.

Heaps are often implemented using arrays, where the tree structure is mapped as follows:

  • For any node located at index i, its left child is at index 2 * i + 1
  • The right child is at 2 * i + 2
  • Its parent is located at Math.Floor((i - 1) / 2)

In this context, we use two types of heaps:

  • Min Heap: Holds the larger half of the numbers seen so far.
  • Max Heap: Holds the smaller half.

C# doesn't come with built-in heap functionality, so we implemented one using lists.

Min Heap and Max Heap Implementation

Here are custom implementations for MinHeap and MaxHeap using lists:

C#
1public class MinHeap { 2 private List<int> heap; 3 4 public MinHeap() { 5 heap = new List<int>(); 6 } 7 8 public void Add(int value) { 9 heap.Add(value); 10 HeapifyUp(heap.Count - 1); 11 } 12 13 public int Poll() { 14 if (heap.Count == 0) return -1; 15 if (heap.Count == 1) { 16 int root = heap[0]; 17 heap.RemoveAt(0); 18 return root; 19 } 20 int result = heap[0]; 21 heap[0] = heap[heap.Count - 1]; 22 heap.RemoveAt(heap.Count - 1); 23 HeapifyDown(0); 24 return result; 25 } 26 27 public int Peek() { 28 return heap[0]; 29 } 30 31 public int Size() { 32 return heap.Count; 33 } 34 35 private void HeapifyUp(int index) { 36 while (index > 0 && heap[index] < heap[(index - 1) / 2]) { 37 int parentIndex = (index - 1) / 2; 38 Swap(index, parentIndex); 39 index = parentIndex; 40 } 41 } 42 43 private void HeapifyDown(int index) { 44 int leftChild, rightChild, smallestChild; 45 46 while (index * 2 + 1 < heap.Count) { 47 leftChild = index * 2 + 1; 48 rightChild = index * 2 + 2; 49 smallestChild = leftChild; 50 51 if (rightChild < heap.Count && heap[rightChild] < heap[leftChild]) { 52 smallestChild = rightChild; 53 } 54 55 if (heap[index] <= heap[smallestChild]) { 56 break; 57 } 58 59 Swap(index, smallestChild); 60 index = smallestChild; 61 } 62 } 63 64 private void Swap(int i, int j) { 65 int temp = heap[i]; 66 heap[i] = heap[j]; 67 heap[j] = temp; 68 } 69} 70 71public class MaxHeap : MinHeap { 72 public new void Add(int value) { 73 base.Add(-value); 74 } 75 76 public new int Poll() { 77 return -base.Poll(); 78 } 79 80 public new int Peek() { 81 return -base.Peek(); 82 } 83}

The MinHeap class offers operations like Add (insert element), Poll (remove and return the smallest element), Peek (look at the smallest element), and Size (get heap size). The HeapifyUp and HeapifyDown methods ensure the heap property is maintained. The MaxHeap class relies on MinHeap, but manipulates negated values to handle max-heap operations by overriding methods to negate values, converting its behavior.

Initializing Heaps and Median Array

We first set up our maxHeap and minHeap for storing smaller and larger halves, respectively, and an array to store medians:

C#
1public class Solution { 2 public static int[] PrefixMedian(int[] arr) { 3 MinHeap minHeap = new MinHeap(); 4 MaxHeap maxHeap = new MaxHeap(); 5 int[] medians = new int[arr.Length]; 6 7 return medians; 8 } 9}
Distributing Elements into Heaps

Next, as we iterate through the array, we decide whether to insert each element into the maxHeap or minHeap based on its value:

C#
1public class Solution { 2 public static int[] PrefixMedian(int[] arr) { 3 MinHeap minHeap = new MinHeap(); 4 MaxHeap maxHeap = new MaxHeap(); 5 int[] medians = new int[arr.Length]; 6 7 foreach (int num in arr) { 8 if (maxHeap.Size() > 0 && num < maxHeap.Peek()) { 9 maxHeap.Add(num); 10 } else { 11 minHeap.Add(num); 12 } 13 } 14 15 return medians; 16 } 17}
Balancing Heaps

To ensure we can access medians efficiently, we need to balance the heaps. If maxHeap becomes larger, we transfer elements, and if minHeap is larger by more than one, we transfer elements back:

C#
1public class Solution { 2 public static int[] PrefixMedian(int[] arr) { 3 MinHeap minHeap = new MinHeap(); 4 MaxHeap maxHeap = new MaxHeap(); 5 int[] medians = new int[arr.Length]; 6 7 for (int i = 0; i < arr.Length; i++) { 8 int num = arr[i]; 9 if (maxHeap.Size() > 0 && num < maxHeap.Peek()) { 10 maxHeap.Add(num); 11 } else { 12 minHeap.Add(num); 13 } 14 15 if (maxHeap.Size() > minHeap.Size()) { 16 minHeap.Add(maxHeap.Poll()); 17 } else if (minHeap.Size() > maxHeap.Size() + 1) { 18 maxHeap.Add(minHeap.Poll()); 19 } 20 } 21 22 return medians; 23 } 24}
Extracting Prefix Medians

Finally, the medians are extracted, where equality of heap sizes leads to picking from maxHeap's root, and otherwise from minHeap:

C#
1public class Solution { 2 public static int[] PrefixMedian(int[] arr) { 3 MinHeap minHeap = new MinHeap(); 4 MaxHeap maxHeap = new MaxHeap(); 5 int[] medians = new int[arr.Length]; 6 7 for (int i = 0; i < arr.Length; i++) { 8 int num = arr[i]; 9 if (maxHeap.Size() > 0 && num < maxHeap.Peek()) { 10 maxHeap.Add(num); 11 } else { 12 minHeap.Add(num); 13 } 14 15 if (maxHeap.Size() > minHeap.Size()) { 16 minHeap.Add(maxHeap.Poll()); 17 } else if (minHeap.Size() > maxHeap.Size() + 1) { 18 maxHeap.Add(minHeap.Poll()); 19 } 20 21 medians[i] = (minHeap.Size() == maxHeap.Size()) ? maxHeap.Peek() : minHeap.Peek(); 22 } 23 24 return medians; 25 } 26 27 public static void Main() { 28 int[] arr = {1, 9, 2, 8, 3}; 29 int[] medians = PrefixMedian(arr); 30 Console.WriteLine("Final Medians Array: "); 31 foreach (int median in medians) { 32 Console.Write(median + " "); 33 } 34 } 35}

This C# implementation balances the heaps and extracts prefixes' medians effectively, building an array of medians that is printed in the Main method.

Lesson Summary

Congratulations! You've successfully tackled an engaging algorithmic problem that required the use of heaps for array manipulation in C#. By creating and managing your heap data structure, you reinforced your understanding of both C# data management and algorithmic problem-solving. Keep practicing with similar problems to strengthen your grasp of heaps and array manipulations. Remember — consistency and practice lead to mastery. Happy coding!

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