Master hierarchical data structures for efficient sorting and priority-based access. Learn min/max heaps, array representation, heap operations, patterns for top K problems, median finding, and task scheduling.
A heap is a complete binary tree that satisfies the heap property, enabling O(log n) insertion and O(1) extraction of extreme values.
A complete binary tree fills all levels completely except possibly the last, which fills left-to-right. This shape guarantees height = O(log n). The heap property defines the ordering:
Heaps are not sorted globally (no O(n log n) guarantee between all elements), only locally along parent-child paths. This allows O(n) construction and O(log n) operations.
Store heaps in a 0-indexed array where index relationships encode the tree structure without explicit pointers.
// For node at index i: private int Parent(int i) => (i - 1) / 2; // Integer division private int LeftChild(int i) => 2 * i + 1; private int RightChild(int i) => 2 * i + 2; // Example: Node at index 5 // Parent: (5-1)/2 = 2 // Left child: 2*5+1 = 11 // Right child: 2*5+2 = 12
For heap [10, 20, 30, 40, 50, 60, 70]:
10 (idx 0)
/ \
20 30 (idx 1,2)
/ \ / \
40 50 60 70 (idx 3,4,5,6)
Array: [10, 20, 30, 40, 50, 60, 70]
Index: 0 1 2 3 4 5 6
For idx 1 (20):
Parent: (1-1)/2 = 0 → 10 ✓
Left: 2*1+1 = 3 → 40 ✓
Right: 2*1+2 = 4 → 50 ✓
Add element at end of array, then bubble up by comparing with parent and swapping until heap property restored.
public void Insert(int value) { if (size == heap.Length) Resize(); heap[size] = value; BubbleUp(size); size++; } private void BubbleUp(int idx) { while (idx > 0) { int parentIdx = Parent(idx); if (heap[parentIdx] <= heap[idx]) break; // Min-heap property satisfied Swap(idx, parentIdx); idx = parentIdx; } }
Remove root (min element), move last element to root, then bubble down until heap property restored.
public int ExtractMin() { if (size == 0) throw new InvalidOperationException("Heap empty"); int min = heap[0]; heap[0] = heap[--size]; BubbleDown(0); return min; } private void BubbleDown(int idx) { while (LeftChild(idx) < size) { int smallest = idx; int left = LeftChild(idx); int right = RightChild(idx); if (heap[left] < heap[smallest]) smallest = left; if (right < size && heap[right] < heap[smallest]) smallest = right; if (smallest == idx) break; // Heap property satisfied Swap(idx, smallest); idx = smallest; } }
Return root without removing it.
public int Peek() { if (size == 0) throw new InvalidOperationException("Heap empty"); return heap[0]; }
Convert unsorted array into heap in linear time using bottom-up heapification instead of n insertions.
Naively inserting n elements takes O(n log n). Bottom-up approach is faster because:
public static void BuildHeap(int[] arr) { int n = arr.Length; // Start from last non-leaf node: (n-2)/2 for (int i = (n - 2) / 2; i >= 0; i--) { HeapifyDown(arr, i, n); } } private static void HeapifyDown(int[] arr, int idx, int n) { while (2 * idx + 1 < n) { int left = 2 * idx + 1; int right = 2 * idx + 2; int smallest = idx; if (arr[left] < arr[smallest]) smallest = left; if (right < n && arr[right] < arr[smallest]) smallest = right; if (smallest == idx) break; (arr[idx], arr[smallest]) = (arr[smallest], arr[idx]); idx = smallest; } }
Original: [7, 4, 1, 9, 2, 5]
Tree: 7
/ \
4 1
/ \ /
9 2 5
Start from i = (6-2)/2 = 2 (node with value 1)
→ Already min-heap at 1 since no valid children in tree position
i = 1 (node with value 4)
→ Left child = 9, Right = 2
→ Swap with 2 (smaller): [7, 2, 1, 9, 4, 5]
→ 4 now has no more children below
i = 0 (node with value 7)
→ Left = 2, Right = 1
→ Swap with 1 (smaller): [1, 2, 7, 9, 4, 5]
→ Move to position 2 (value 7)
→ Children are 4 and 5, swap with 4: [1, 2, 4, 9, 7, 5]
Result: [1, 2, 4, 9, 7, 5] (valid min-heap)
public class MinHeap { private int[] heap; private int size; private const int DEFAULT_CAPACITY = 10; /// <summary>Initialize empty heap with default capacity</summary> public MinHeap() { heap = new int[DEFAULT_CAPACITY]; size = 0; } /// <summary>Initialize with specific capacity</summary> public MinHeap(int capacity) { heap = new int[capacity]; size = 0; } /// <summary>Build heap from existing array - O(n)</summary> public MinHeap(int[] arr) { heap = new int[arr.Length]; Array.Copy(arr, heap, arr.Length); size = arr.Length; BuildHeap(); } // ===== Query Operations ===== /// <summary>Get minimum without removing - O(1)</summary> public int Peek() { if (size == 0) throw new InvalidOperationException("Heap is empty"); return heap[0]; } /// <summary>Check if heap is empty</summary> public bool IsEmpty() => size == 0; /// <summary>Get current size</summary> public int Size() => size; // ===== Modification Operations ===== /// <summary>Add element to heap - O(log n)</summary> public void Insert(int value) { if (size == heap.Length) { Resize(); } heap[size] = value; BubbleUp(size); size++; } /// <summary>Remove and return minimum - O(log n)</summary> public int ExtractMin() { if (size == 0) throw new InvalidOperationException("Heap is empty"); int min = heap[0]; heap[0] = heap[--size]; if (size > 0) { BubbleDown(0); } return min; } // ===== Internal Helper Methods ===== /// <summary>Restore heap property from index upward</summary> private void BubbleUp(int idx) { while (idx > 0) { int parentIdx = Parent(idx); if (heap[parentIdx] <= heap[idx]) break; Swap(idx, parentIdx); idx = parentIdx; } } /// <summary>Restore heap property from index downward</summary> private void BubbleDown(int idx) { while (LeftChild(idx) < size) { int smallest = idx; int left = LeftChild(idx); int right = RightChild(idx); if (heap[left] < heap[smallest]) smallest = left; if (right < size && heap[right] < heap[smallest]) smallest = right; if (smallest == idx) break; Swap(idx, smallest); idx = smallest; } } /// <summary>Build heap from existing array - O(n)</summary> private void BuildHeap() { // Start from last non-leaf and work backwards for (int i = (size - 2) / 2; i >= 0; i--) { BubbleDown(i); } } /// <summary>Get parent index of element at position i</summary> private int Parent(int i) => (i - 1) / 2; /// <summary>Get left child index of element at position i</summary> private int LeftChild(int i) => 2 * i + 1; /// <summary>Get right child index of element at position i</summary> private int RightChild(int i) => 2 * i + 2; /// <summary>Swap elements at two indices</summary> private void Swap(int i, int j) { (heap[i], heap[j]) = (heap[j], heap[i]); } /// <summary>Double array capacity when full</summary> private void Resize() { int[] newHeap = new int[heap.Length * 2]; Array.Copy(heap, newHeap, heap.Length); heap = newHeap; } /// <summary>Get array representation (for debugging)</summary> public int[] ToArray() { int[] result = new int[size]; Array.Copy(heap, result, size); return result; } }
// Create empty heap and insert var heap = new MinHeap(); heap.Insert(5); heap.Insert(3); heap.Insert(7); heap.Insert(1); // Extract in sorted order while (!heap.IsEmpty()) { Console.WriteLine(heap.ExtractMin()); // 1, 3, 5, 7 } // Build heap from array - O(n) int[] arr = { 7, 3, 9, 1 }; var heap2 = new MinHeap(arr);
Mirror of MinHeap with comparisons reversed. Root is maximum element. Useful for extracting largest values first.
public class MaxHeap { private int[] heap; private int size; public MaxHeap() => heap = new int[10]; public MaxHeap(int[] arr) { heap = new int[arr.Length]; Array.Copy(arr, heap, arr.Length); size = arr.Length; BuildHeap(); } public int Peek() { if (size == 0) throw new InvalidOperationException("Heap empty"); return heap[0]; } public void Insert(int value) { if (size == heap.Length) Resize(); heap[size] = value; BubbleUp(size); size++; } public int ExtractMax() { if (size == 0) throw new InvalidOperationException("Heap empty"); int max = heap[0]; heap[0] = heap[--size]; if (size > 0) BubbleDown(0); return max; } // Only difference from MinHeap: > instead of < in comparisons private void BubbleUp(int idx) { while (idx > 0) { int parentIdx = (idx - 1) / 2; if (heap[parentIdx] >= heap[idx]) break; // Reverse for max Swap(idx, parentIdx); idx = parentIdx; } } private void BubbleDown(int idx) { while (2 * idx + 1 < size) { int largest = idx; int left = 2 * idx + 1; int right = 2 * idx + 2; if (heap[left] > heap[largest]) largest = left; if (right < size && heap[right] > heap[largest]) largest = right; if (largest == idx) break; Swap(idx, largest); idx = largest; } } private void BuildHeap() { for (int i = (size - 2) / 2; i >= 0; i--) BubbleDown(i); } private void Swap(int i, int j) => (heap[i], heap[j]) = (heap[j], heap[i]); private void Resize() { int[] newHeap = new int[heap.Length * 2]; Array.Copy(heap, newHeap, heap.Length); heap = newHeap; } public bool IsEmpty() => size == 0; public int Size() => size; }
Built-in generic priority queue with custom priority support. Use this in production instead of manual heap implementation.
using System.Collections.Generic; // Min-heap by default (lower priority value = higher priority) var pq = new PriorityQueue<string, int>(); // Enqueue: Add element with priority pq.Enqueue("task-high", 1); // Priority 1 pq.Enqueue("task-low", 5); // Priority 5 pq.Enqueue("task-med", 3); // Priority 3 // Dequeue: Remove and return highest-priority element while (pq.Count > 0) { string task = pq.Dequeue(); Console.WriteLine(task); } // Output: task-high, task-med, task-low (ascending priority)
// Peek without removing var (element, priority) = pq.Peek(); // Try operations (safe, return bool) if (pq.TryDequeue(out string elem, out int pri)) { Console.WriteLine($"Got: {elem} (priority: {pri})"); } // Get count int remaining = pq.Count; // EnqueueRange: Add multiple elements at once var items = new[] { ("a", 1), ("b", 2) }; pq.EnqueueRange(items);
By default, lower numeric priority = higher priority (min-heap). For max-heap behavior, negate or use custom comparer.
// Option 1: Negate values var maxHeap = new PriorityQueue<int, int>(); maxHeap.Enqueue(5, -5); // Negate for max-heap maxHeap.Enqueue(3, -3); Console.WriteLine(maxHeap.Dequeue()); // 5 // Option 2: Custom comparer var reverseComparer = new ReverseComparer<int>(); var maxHeap2 = new PriorityQueue<int, int>(reverseComparer); maxHeap2.Enqueue(5, 5); maxHeap2.Enqueue(3, 3); Console.WriteLine(maxHeap2.Dequeue()); // 5 // Simple reverse comparer public class ReverseComparer<T> : IComparer<T> where T : IComparable<T> { public int Compare(T a, T b) => b.CompareTo(a); }
public record Task(string Name, int Priority, long Timestamp); public record TaskPriority(int Priority, long Timestamp) : IComparable<TaskPriority> { public int CompareTo(TaskPriority other) { // Higher priority first int pri = other.Priority.CompareTo(this.Priority); if (pri != 0) return pri; // Then earlier timestamp (FIFO for same priority) return this.Timestamp.CompareTo(other.Timestamp); } } var pq = new PriorityQueue<Task, TaskPriority>(); pq.Enqueue(new Task("A", 1, 100), new TaskPriority(1, 100)); pq.Enqueue(new Task("B", 1, 50), new TaskPriority(1, 50));
Sort array using heap as intermediate structure. Not stable but achieves O(n log n) time and O(1) space.
public static void HeapSort(int[] arr) { int n = arr.Length; // Step 1: Build max-heap - O(n) for (int i = (n - 2) / 2; i >= 0; i--) { HeapifyDown(arr, i, n); } // Step 2: Extract max n-1 times - O(n log n) for (int i = n - 1; i > 0; i--) { // Move current max (root) to end (arr[0], arr[i]) = (arr[i], arr[0]); // Restore heap property in reduced array HeapifyDown(arr, 0, i); } } private static void HeapifyDown(int[] arr, int i, int n) { while (2 * i + 1 < n) { int left = 2 * i + 1; int right = 2 * i + 2; int largest = i; if (arr[left] > arr[largest]) largest = left; if (right < n && arr[right] > arr[largest]) largest = right; if (largest == i) break; (arr[i], arr[largest]) = (arr[largest], arr[i]); i = largest; } }// Example: Sort [3, 1, 4, 1, 5, 9, 2] int[] arr = { 3, 1, 4, 1, 5, 9, 2 }; HeapSort(arr); // Result: [1, 1, 2, 3, 4, 5, 9]Complexity Analysis
Operation Best Average Worst Space Heap Sort O(n log n) O(n log n) O(n log n) O(1) Build Heap O(n) O(n) O(n) O(1) Extract Max n times O(n log n) O(n log n) O(n log n) O(1) Not Stable: Heap sort doesn't preserve relative order of equal elements (unstable). Example: [1a, 2, 1b] may become [1b, 1a, 2]. For stable sort, use merge sort. 7.9Top K Pattern - Find K Largest/Smallest Efficiently
Use min-heap for top K largest (size K, discard mins), max-heap for top K smallest (size K, discard maxes).
Top K Largest - Min-Heap Template
Idea: Maintain min-heap of size K containing K largest elements. When new element > min, remove min and add new element. Time: O(n log K).
public static List<int> TopKLargest(int[] nums, int k) { // Min-heap: smallest of K largest elements at root var minHeap = new PriorityQueue<int, int>(); foreach (int num in nums) { minHeap.Enqueue(num, num); // Keep heap size at most K if (minHeap.Count > k) { minHeap.Dequeue(); } } var result = new List<int>(); while (minHeap.Count > 0) { result.Add(minHeap.Dequeue()); } result.Reverse(); // Reverse to get descending order return result; } // Example int[] nums = { 3, 2, 1, 5, 6, 4 }; var topK = TopKLargest(nums, 2); // Result: [6, 5]Top K Smallest - Max-Heap Template
public static List<int> TopKSmallest(int[] nums, int k) { // Max-heap: largest of K smallest elements at root var maxHeap = new PriorityQueue<int, int>( new ReverseComparer<int>()); foreach (int num in nums) { maxHeap.Enqueue(num, num); // Keep heap size at most K if (maxHeap.Count > k) { maxHeap.Dequeue(); } } var result = new List<int>(); while (maxHeap.Count > 0) { result.Add(maxHeap.Dequeue()); } result.Reverse(); // Reverse to get ascending order return result; } 7.10Two Heaps Pattern - Median from Data Stream
Use max-heap for left half (smaller values) and min-heap for right half (larger values) to find median in O(log n).
Algorithm
- Max-heap stores values ≤ median. Min-heap stores values ≥ median.
- Keep |maxHeap.size - minHeap.size| ≤ 1 for balance.
- Add: O(log n) for insertion + balancing. Median: O(1) peek.
public class MedianFinder { private PriorityQueue<int, int> maxHeap; // Left half private PriorityQueue<int, int> minHeap; // Right half public MedianFinder() { maxHeap = new PriorityQueue<int, int>( new ReverseComparer<int>()); minHeap = new PriorityQueue<int, int>(); } public void AddNum(int num) { // Add to max-heap first if (maxHeap.Count == 0 || num <= maxHeap.Peek()) { maxHeap.Enqueue(num, num); } else { minHeap.Enqueue(num, num); } // Balance heaps: maxHeap can have at most 1 more element if (maxHeap.Count > minHeap.Count + 1) { minHeap.Enqueue(maxHeap.Dequeue(), maxHeap.Peek()); } if (minHeap.Count > maxHeap.Count) { maxHeap.Enqueue(minHeap.Dequeue(), minHeap.Peek()); } } public double FindMedian() { int total = maxHeap.Count + minHeap.Count; if (total % 2 == 1) { // Odd: return max of left half return maxHeap.Peek(); } else { // Even: return average of both roots return (maxHeap.Peek() + minHeap.Peek()) / 2.0; } } } // Example var mf = new MedianFinder(); mf.AddNum(1); Console.WriteLine(mf.FindMedian()); // 1.0 mf.AddNum(2); Console.WriteLine(mf.FindMedian()); // 1.5 mf.AddNum(3); Console.WriteLine(mf.FindMedian()); // 2.0 7.11K-way Merge Pattern - Merge K Sorted Lists
Use min-heap to efficiently merge K sorted lists. Store (value, listIndex, elementIndex) to track source.
Algorithm: Track Source with Tuples
public static List<int> MergeKSortedLists(List<List<int>> lists) { var result = new List<int>(); // Min-heap: (value, listIndex, elementIndex) var minHeap = new PriorityQueue<(int val, int li, int ei), int>(); // Add first element from each list for (int i = 0; i < lists.Count; i++) { if (lists[i].Count > 0) { minHeap.Enqueue((lists[i][0], i, 0), lists[i][0]); } } // Extract min, add next from same list while (minHeap.Count > 0) { var (val, li, ei) = minHeap.Dequeue(); result.Add(val); // Add next element from same list if (ei + 1 < lists[li].Count) { int nextVal = lists[li][ei + 1]; minHeap.Enqueue((nextVal, li, ei + 1), nextVal); } } return result; } // Example: Merge [[1,4,5], [1,3,4], [2,6]] var lists = new List<List<int>> { new() { 1, 4, 5 }, new() { 1, 3, 4 }, new() { 2, 6 } }; var merged = MergeKSortedLists(lists); // Result: [1, 1, 2, 3, 4, 4, 5, 6] 7.12Practice Problems with Full C# Solutions
1Kth Largest Element in Array (LeetCode 215)Find the kth largest element in an unsorted array. Expected O(n log k) solution using min-heap.Approach
Maintain min-heap of size k. When heap has k elements and new element is larger than root, remove root and add new element. Final root is kth largest.
public int FindKthLargest(int[] nums, int k) { var minHeap = new PriorityQueue<int, int>(); foreach (int num in nums) { minHeap.Enqueue(num, num); if (minHeap.Count > k) { minHeap.Dequeue(); } } return minHeap.Peek(); } // Time: O(n log k) | Space: O(k)2Top K Frequent Elements (LeetCode 347)Find k most frequent elements. Count frequencies, use min-heap of size k keyed by frequency.public int[] TopKFrequent(int[] nums, int k) { // Count frequencies var freq = new Dictionary<int, int>(); foreach (int num in nums) { freq[num] = freq.GetValueOrDefault(num) + 1; } // Min-heap by frequency var minHeap = new PriorityQueue<int, int>(); foreach (var (num, count) in freq) { minHeap.Enqueue(num, count); if (minHeap.Count > k) { minHeap.Dequeue(); } } var result = new int[k]; int idx = 0; while (minHeap.Count > 0) { result[idx++] = minHeap.Dequeue(); } return result; }3Last Stone Weight (LeetCode 1046)Destroy stones pairwise taking the two heaviest. Return weight of last remaining stone or 0.public int LastStoneWeight(int[] stones) { // Max-heap of stone weights var maxHeap = new PriorityQueue<int, int>( new ReverseComparer<int>()); foreach (int stone in stones) { maxHeap.Enqueue(stone, stone); } while (maxHeap.Count > 1) { int first = maxHeap.Dequeue(); int second = maxHeap.Dequeue(); if (first != second) { int diff = first - second; maxHeap.Enqueue(diff, diff); } } return maxHeap.Count == 0 ? 0 : maxHeap.Dequeue(); }4Find Median from Data Stream (LeetCode 295)Design class to find median of data stream. Add element O(log n), find median O(1).Already covered in section 7.10 - use max-heap for left half, min-heap for right half.
5Reorganize String (LeetCode 767)Rearrange characters so no two adjacent are the same. Return answer or empty if impossible.public string ReorganizeString(string s) { // Count character frequencies var freq = new Dictionary<char, int>(); foreach (char c in s) { freq[c] = freq.GetValueOrDefault(c) + 1; } // If any char appears more than (n+1)/2 times, impossible int maxFreq = freq.Values.Max(); if (2 * maxFreq > s.Length + 1) return ""; // Max-heap by frequency var maxHeap = new PriorityQueue<(char, int), int>( new ReverseComparer<int>()); foreach (var (c, count) in freq) { maxHeap.Enqueue((c, count), count); } var sb = new StringBuilder(); while (maxHeap.Count > 0) { var (c1, cnt1) = maxHeap.Dequeue(); sb.Append(c1); if (maxHeap.Count > 0) { var (c2, cnt2) = maxHeap.Dequeue(); sb.Append(c2); // Re-add if count remains if (cnt1 > 1) maxHeap.Enqueue((c1, cnt1 - 1), cnt1 - 1); if (cnt2 > 1) maxHeap.Enqueue((c2, cnt2 - 1), cnt2 - 1); } } return sb.ToString(); }6Task Scheduler (LeetCode 621)Schedule tasks with cooldown. Use greedy + max-heap to always schedule most frequent remaining task.public int LeastInterval(char[] tasks, int n) { // Count task frequencies var freq = new Dictionary<char, int>(); foreach (char t in tasks) { freq[t] = freq.GetValueOrDefault(t) + 1; } // Max-heap by frequency var maxHeap = new PriorityQueue<int, int>( new ReverseComparer<int>()); foreach (int count in freq.Values) { maxHeap.Enqueue(count, count); } int intervals = 0; while (maxHeap.Count > 0) { int cycles = n + 1; int taskCount = 0; // Execute tasks greedily (pick n+1 most frequent) var temp = new List<int>(); while (taskCount < cycles && maxHeap.Count > 0) { int count = maxHeap.Dequeue(); if (count > 1) { temp.Add(count - 1); } taskCount++; } // Re-add remaining tasks foreach (int count in temp) { maxHeap.Enqueue(count, count); } // Add intervals (either n+1 or remaining tasks if last cycle) intervals += maxHeap.Count == 0 ? taskCount : cycles; } return intervals; } 7.13Heap vs BST vs Sorted Array - Comparison
Operation Heap BST Sorted Array Find Min/Max O(1) O(log n) O(1) Extract Min/Max O(log n) O(log n) O(n) Insert O(log n) O(log n) O(n) Find kth Element O(n) O(log n)* O(1) Range Query O(n) O(log n + k) O(log n + k) Build from Array O(n) O(n log n) O(n log n) Space Efficiency Perfect Extra pointers Array size When to Use Heap: Priority queues, top K problems, median finding, Dijkstra, Huffman. Fast extraction of extremes.When to Use BST: Balanced access to all elements, range queries, successor/predecessor, sorted iteration. O(log n) for all ops.Interview Tips
7.13.1Always Use Built-in PriorityQueueIn interviews, usePriorityQueue<T, TPriority>(.NET 6+) instead of manual heap. It's tested, fast, and saves time. Only implement manually if explicitly asked to "understand heap".7.13.2Handle Edge CasesEmpty heap, single element, duplicates, all same values. Test with k = 1, k = n, negative numbers if applicable.7.13.3Explain Time ComplexityFor top K: O(n log k) beats O(n log n) sorting for small K. Mention space trade-off: O(k) vs O(n). Show why BuildHeap is O(n) not O(n log n).7.13.4Recognize Heap Patterns"Top K" → min-heap of size K. "Median from stream" → two heaps. "Merge K lists" → heap with source tracking. "Frequent" → heap by frequency not value.7.13.5Custom ComparersPriorityQueue uses ascending order by default. Negate values for descending, or pass custom IComparer for complex criteria (e.g., frequency then timestamp).7.13.6Mention Stability & In-placeHeap sort is not stable (may reorder equal elements) but is in-place. Mention tradeoffs: QuickSort unstable/in-place, MergeSort stable/O(n) space, HeapSort unstable/in-place.Common Mistakes to Avoid
Mistake 1: Using min-heap when you need max-heap (or vice versa). Always clarify: "Find K largest" = min-heap, "Find K smallest" = max-heap.Mistake 2: Forgetting to balance two heaps properly in median finding. maxHeap size should be ≥ minHeap size, differ by at most 1.Mistake 3: Implementing naive bubble-up/down with wrong termination. Check parent index ≥ 0 for bubble-up, left child < size for bubble-down.