Chapter 7

Heaps & Priority Queues

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.

13
Core Topics
6
Full Implementations
50+
Code Examples
11
Practice Problems
Table of Contents
7.1

Heap Fundamentals

A heap is a complete binary tree that satisfies the heap property, enabling O(log n) insertion and O(1) extraction of extreme values.

Definition & Properties

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.

💡
Why Heaps? They provide O(log n) priority-based access without full sorting. Use heaps for top K, median finding, Dijkstra, and Huffman coding.

Min-Heap vs Max-Heap Visual

Min-Heap Example
Root = 1 (minimum)
Parent always ≤ children
Extract-min is O(log n)
Perfect for priority queues where lower value = higher priority
Max-Heap Example
Root = 99 (maximum)
Parent always ≥ children
Extract-max is O(log n)
Perfect for finding top K largest elements
7.2

Array Representation & Indexing

Store heaps in a 0-indexed array where index relationships encode the tree structure without explicit pointers.

Index Formulas (0-Indexed)

C#
// 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

Visual Array Layout

For heap [10, 20, 30, 40, 50, 60, 70]:

Text
        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 ✓
ℹ️
1-Indexed Alternative: Parent(i) = i/2, Left(i) = 2i, Right(i) = 2i+1. Wastes index 0 but simplifies math. 0-indexed is more common in production code.
7.3

Core Heap Operations

Insert / BubbleUp - O(log n)

Add element at end of array, then bubble up by comparing with parent and swapping until heap property restored.

1
Add new value to end of array and increment size.
2
Compare with parent. If heap property violated, swap.
3
Move to parent position and repeat until root or property satisfied.
C#
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;
    }
}

ExtractMin / BubbleDown - O(log n)

Remove root (min element), move last element to root, then bubble down until heap property restored.

1
Save root value (return this).
2
Move last element to root, decrement size.
3
Compare with children, swap with smaller child if needed.
4
Move to child position and repeat until leaf or property satisfied.
C#
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;
    }
}

Peek - O(1)

Return root without removing it.

C#
public int Peek() {
    if (size == 0) throw new InvalidOperationException("Heap empty");
    return heap[0];
}
7.4

Build Heap from Array - O(n)

Convert unsorted array into heap in linear time using bottom-up heapification instead of n insertions.

Why O(n) Not O(n log n)?

Naively inserting n elements takes O(n log n). Bottom-up approach is faster because:

Algorithm: Start from Last Parent, BubbleDown

C#
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;
    }
}

Example: BuildHeap from [7, 4, 1, 9, 2, 5]

Text
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)
Proof: O(n) Complexity
Let h = log₂(n). Nodes at level k from bottom do O(k) work. Count of nodes at level k = n/2^(k+1).
Time = Σ(k * n/2^(k+1)) for k=0 to h-1 = n * Σ(k/2^(k+1)) = n * 1 = O(n)
Why Start from (n-2)/2?
This is the index of the last non-leaf node. Leaf nodes (indices n/2 to n-1) have no children, so they're already valid heap nodes. No need to bubble down.
7.5

MinHeap Full Implementation - 100+ Lines

C#
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;
    }
}

Usage Example

C#
// 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);
7.6

MaxHeap Implementation

Mirror of MinHeap with comparisons reversed. Root is maximum element. Useful for extracting largest values first.

C#
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;
}
7.7

C# PriorityQueue (.NET 6+)

Built-in generic priority queue with custom priority support. Use this in production instead of manual heap implementation.

Basic API

C#
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)

Common Operations

C#
// 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);

Custom Comparers for Max-Heap

By default, lower numeric priority = higher priority (min-heap). For max-heap behavior, negate or use custom comparer.

C#
// 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);
}

Complex Priority Example: Tasks by Priority + Timestamp

C#
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));
7.8

Heap Sort - O(n log n), In-Place

Sort array using heap as intermediate structure. Not stable but achieves O(n log n) time and O(1) space.

Algorithm: Build Heap, Then Extract All

1
Build max-heap from array - O(n)
2
Swap root with last element, reduce heap size
3
Bubble down new root - O(log n)
4
Repeat n-1 times - O(n log n) total
C#
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;
    }
}

      
C#
// 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

OperationBestAverageWorstSpace
Heap SortO(n log n)O(n log n)O(n log n)O(1)
Build HeapO(n)O(n)O(n)O(1)
Extract Max n timesO(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.9

Top 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).

C#
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

C#
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.10

Two 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

C#
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.11

K-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

C#
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.12

Practice Problems with Full C# Solutions

1
Kth Largest Element in Array (LeetCode 215)
Find the kth largest element in an unsorted array. Expected O(n log k) solution using min-heap.
Heap Top K O(n log k)

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.

C#
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)
2
Top K Frequent Elements (LeetCode 347)
Find k most frequent elements. Count frequencies, use min-heap of size k keyed by frequency.
Heap Hash Table O(n log k)
C#
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;
}
          
3
Last Stone Weight (LeetCode 1046)
Destroy stones pairwise taking the two heaviest. Return weight of last remaining stone or 0.
Max-Heap O(n log n)
C#
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();
}
          
4
Find Median from Data Stream (LeetCode 295)
Design class to find median of data stream. Add element O(log n), find median O(1).
Two Heaps O(log n) add / O(1) find

Already covered in section 7.10 - use max-heap for left half, min-heap for right half.

5
Reorganize String (LeetCode 767)
Rearrange characters so no two adjacent are the same. Return answer or empty if impossible.
Max-Heap Greedy O(n log n)
C#
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();
}
          
6
Task Scheduler (LeetCode 621)
Schedule tasks with cooldown. Use greedy + max-heap to always schedule most frequent remaining task.
Max-Heap Greedy O(n)
C#
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.13

Heap vs BST vs Sorted Array - Comparison

OperationHeapBSTSorted Array
Find Min/MaxO(1)O(log n)O(1)
Extract Min/MaxO(log n)O(log n)O(n)
InsertO(log n)O(log n)O(n)
Find kth ElementO(n)O(log n)*O(1)
Range QueryO(n)O(log n + k)O(log n + k)
Build from ArrayO(n)O(n log n)O(n log n)
Space EfficiencyPerfectExtra pointersArray 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.1
Always Use Built-in PriorityQueue
In interviews, use PriorityQueue<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.2
Handle Edge Cases
Empty heap, single element, duplicates, all same values. Test with k = 1, k = n, negative numbers if applicable.
7.13.3
Explain Time Complexity
For 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.4
Recognize 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.5
Custom Comparers
PriorityQueue uses ascending order by default. Negate values for descending, or pass custom IComparer for complex criteria (e.g., frequency then timestamp).
7.13.6
Mention Stability & In-place
Heap 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.