📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

📚 Interview question

What is a heap data structure, and how does PriorityQueue<TElement,TPriority> work in .NET? Show how you would find the top K items from a large stream efficiently.

Binary and d-ary heaps stored in arrays, heap operations and costs, PriorityQueue in .NET (min-heap, no stability, Remove in .NET 9), and classic uses: top-K, scheduling, merging sorted streams and Dijkstra.

Mid-Level Coding Intermediate algorithms data-structures heap priority-queue top-k

🎯 Key points

  • A heap is a complete tree stored in an array with the heap property; in a min-heap every parent ≤ its children, so the minimum is always at index 0
  • Peek is O(1); insert (sift up) and remove-min (sift down) are O(log n); building a heap from n items is O(n); finding an arbitrary item is O(n)
  • Parent and child positions come from index arithmetic, so heaps need no pointers and are compact and cache-friendly
  • PriorityQueue<TElement, TPriority> is an array-backed 4-ary min-heap: shallower than a binary heap (log₄ n levels) with children in adjacent memory, at the cost of more comparisons per level
  • A max-heap is the same class with a reversed IComparer<TPriority>; it is not stable, has no decrease-key (Remove exists from .NET 9) and is not thread-safe
  • Top-K largest from a stream uses a size-K min-heap with EnqueueDequeue: O(n log K) time and O(K) memory versus O(n log n) and O(n) for a full sort
  • Workarounds: add a sequence-number tie-breaker for stability, use lazy deletion for priority updates, and guard shared queues with a lock or a single consumer
  • Uses: schedulers and timers, top-K dashboards, merging K sorted streams, running median with two heaps, and Dijkstra's shortest paths

⏱ 30-second answer

A heap is a complete tree stored in an array that keeps the heap property: in a min-heap every parent ≤ its children, so the smallest item is always at the root.

  • Peek min: O(1). Insert (sift up) and remove min (sift down): O(log n). Build from n items: O(n).
  • Not sorted overall, and not searchable quickly (finding an arbitrary item is O(n)).

.NET's PriorityQueue<TElement,TPriority> (.NET 6+) is an array-backed min-heap (4-ary internally). Enqueue, Dequeue, TryPeek, EnqueueDequeue; not stable for equal priorities; no decrease-key (Remove was added in .NET 9). For a max-heap, pass a reversed comparer.

Top K from a stream: keep a min-heap of size K; for each item, if it beats the heap's minimum, replace it. That is O(n log K) time and O(K) memory.

(Not to be confused with the managed heap, which is memory.)

🗣 Full answer (senior)

Array layout

Min-heap as a tree:              Same heap as an array (binary heap):
            [2]                  index:  0   1   2   3   4   5   6
          /     \                       [2] [5] [3] [9] [6] [8] [4]
       [5]       [3]
      /   \     /   \            parent(i) = (i - 1) / 2
    [9]  [6]  [8]  [4]           children(i) = 2i + 1, 2i + 2

No pointers: navigation is index arithmetic, so heaps are compact and cache-friendly.

Operations

Operation How Cost
Peek Read index 0 O(1)
Insert Append at end, sift up while smaller than parent O(log n)
Remove min Move last to root, sift down swapping with the smaller child O(log n)
Build heap Sift down from the last parent to the root O(n)
Find arbitrary item Scan O(n)

PriorityQueue in .NET

Feature Detail
Order Min-priority first (use a custom IComparer<TPriority> for max)
Stability Not stable: equal priorities come out in no guaranteed order
Update priority No decrease-key; .NET 9 added Remove; otherwise enqueue again and skip stale entries
Thread safety None
Helpers EnqueueRange, EnqueueDequeue (push then pop, efficient for top-K), DequeueEnqueue, TryDequeue

Production example

An analytics endpoint returned the 20 slowest requests out of ~5 million log records per hour. It used records.OrderByDescending(r => r.Duration).Take(20), which in practice sorted or buffered everything and allocated heavily. A size-20 min-heap streamed the records in O(n log 20) with almost no memory, and the endpoint went from 9 seconds to 600 ms.

🔍 What the interviewer is checking

Checks heap fundamentals and the ability to apply them to a common coding problem.

Green flags

  • Explains the array layout and heap property.
  • Gives correct costs for peek, insert, remove and build.
  • Implements top-K with a size-K min-heap and explains O(n log K).
  • Knows PriorityQueue limitations and workarounds.

Red flags

  • Sorts everything for top-K.
  • Thinks a heap is fully sorted.

📚 Learn in depth

1. Sift up and sift down

Insert 1 into [2,5,3,9,6,8,4]:
  append → [2,5,3,9,6,8,4,1]   1 at index 7, parent index 3 (9) → swap
         → [2,5,3,1,6,8,4,9]   parent index 1 (5) → swap
         → [2,1,3,5,6,8,4,9]   parent index 0 (2) → swap
         → [1,2,3,5,6,8,4,9]   done (at most log n swaps)

Remove min from [2,5,3,9,6,8,4]:
  take 2 from the root; move the last item (4) to the root → [4,5,3,9,6,8]
  sift down: children are 5 and 3, smaller is 3 → swap → [3,5,4,9,6,8]
  4 is now at index 2; its only child is 8 → 4 ≤ 8, done

2. Why .NET uses a 4-ary heap

Each node has four children, so the tree is shallower (log₄ n) and children sit next to each other in memory. Sift-down compares more children per level but touches fewer levels and cache lines; in practice it is faster for typical workloads.

3. Top-K patterns

Problem Heap Cost
K largest from a stream Min-heap of size K O(n log K) time, O(K) space
K smallest Max-heap of size K O(n log K)
Merge K sorted streams Min-heap of current heads O(n log K)
Running median Max-heap (lower half) + min-heap (upper half) O(log n) per item
Next timer / scheduled job Min-heap by due time O(log n)
Shortest paths (Dijkstra) Min-heap by distance O((V + E) log V)

Compare: full sort is O(n log n) time and O(n) memory, which loses badly when K is small and n is large or unbounded.

4. Handling priority updates

Without decrease-key, use lazy deletion: enqueue the item again with the new priority and keep a dictionary of current priorities; when you dequeue an entry whose priority is stale, skip it. This is the standard way to write Dijkstra with PriorityQueue.

5. Heapsort

Build a heap in O(n), then repeatedly remove the max: O(n log n) time, O(1) extra space, not stable. .NET's Array.Sort uses introsort, which falls back to heapsort when quicksort recursion gets too deep.

6. Try it yourself

  1. Implement top-K with PriorityQueue and compare with OrderByDescending().Take(K) on 10 million random ints using BenchmarkDotNet (time and allocations).
  2. Merge 5 sorted arrays into one sorted output using a heap of (arrayIndex, position).
  3. Implement a running median with two heaps.

💻 Code

// Top K largest items from a large or unbounded stream: O(n log K) time, O(K) memory
public static List<RequestLog> SlowestK(IEnumerable<RequestLog> logs, int k)
{
    var heap = new PriorityQueue<RequestLog, TimeSpan>(k);   // min-heap by duration
    foreach (var log in logs)
    {
        if (heap.Count < k)
        {
            heap.Enqueue(log, log.Duration);
        }
        else if (heap.TryPeek(out _, out var smallest) && log.Duration > smallest)
        {
            heap.EnqueueDequeue(log, log.Duration);           // push then pop the smallest, one sift
        }
    }

    var result = new List<RequestLog>(heap.Count);
    while (heap.TryDequeue(out var item, out _)) result.Add(item);
    result.Reverse();                                          // slowest first
    return result;
}
// Max-heap via a reversed comparer
var maxHeap = new PriorityQueue<string, int>(Comparer<int>.Create((a, b) => b.CompareTo(a)));
maxHeap.Enqueue("low", 1);
maxHeap.Enqueue("high", 10);
Console.WriteLine(maxHeap.Dequeue()); // high
// Merge K sorted sequences
public static IEnumerable<int> MergeSorted(IReadOnlyList<IEnumerator<int>> sources)
{
    var heap = new PriorityQueue<int, int>();   // element = source index, priority = current value
    for (int i = 0; i < sources.Count; i++)
        if (sources[i].MoveNext()) heap.Enqueue(i, sources[i].Current);

    while (heap.TryDequeue(out var i, out var value))
    {
        yield return value;
        if (sources[i].MoveNext()) heap.Enqueue(i, sources[i].Current);
    }
}

⚠ Common mistakes

  • Mistake: Sorting everything to get the top K. Why it is wrong: A sort is O(n log n) and needs every item in memory, which fails for large or unbounded streams. Instead: Keep a size-K min-heap for O(n log K) time and O(K) memory.

  • Mistake: Keeping a List<T> sorted with Insert (or re-sorting after each add) as a priority queue. Why it is wrong: Each insert shifts elements (O(n)) or re-sorts (O(n log n)), so a busy scheduler degrades quadratically. Instead: Use PriorityQueue<TElement, TPriority>, where each insert and removal is O(log n).

  • Mistake: Expecting PriorityQueue to be stable. Why it is wrong: Items with equal priority come out in no guaranteed order, so FIFO fairness between equal jobs is lost. Instead: Add a tie-breaker to the priority, for example (priority, sequenceNumber) with a tuple comparer.

  • Mistake: Trying to update an item's priority in place. Why it is wrong: There is no decrease-key, and changing a mutable priority object breaks the heap property silently. Instead: Enqueue again and skip stale entries when dequeued (lazy deletion), or call Remove (.NET 9 and later) when you have the element.

  • Mistake: Sharing one PriorityQueue between threads without a lock. Why it is wrong: Concurrent sift operations corrupt the array, losing items or returning the wrong minimum. Instead: Use a lock around every operation, a single consumer task fed by a Channel<T>, or a broker with priorities for distributed work.

  • Mistake: Thinking a heap is sorted. Why it is wrong: Only the root is guaranteed to be the minimum; UnorderedItems is in arbitrary order. Instead: Dequeue repeatedly if you need sorted output.

🏛 Senior insight: trade-offs

Heaps are the go-to structure whenever I need "the next most important thing" repeatedly: schedulers, timers, top-K dashboards, merging sorted streams and shortest paths. The key insight is that you rarely need a full sort: a size-K heap turns an O(n log n), O(n)-memory problem into O(n log K) with constant memory, which matters for streams and big data.

Heap shape Levels for n = 1,000,000 Comparisons per sift-down level Cache behaviour Fits best
Binary (2 children) ~20 2 Children may span cache lines deeper down Simple implementations, textbooks
4-ary (.NET PriorityQueue) ~10 4 Four children usually share a cache line Insert-heavy and general workloads
Larger d-ary Fewer d Wider scans per level Insert-heavy, rare removals
Concurrency need Approach
One thread owns the queue Plain PriorityQueue
Many producers, one scheduler Producers write to a Channel<T>; the scheduler task owns the heap
Several consumers Lock around enqueue/dequeue, or partition by key
Across processes Broker or database ordering (for example Service Bus scheduled messages, an indexed DueAt column)

In .NET I know PriorityQueue's limits (no stability, no decrease-key, not thread-safe) and the standard workarounds: tie-breakers, lazy deletion, and single ownership or a lock.

🔗 Follow-ups the interviewer may ask

  1. When: Candidate is unsure what a complete binary tree is Why: Checks tree fundamentals
    How to answer

    A tree is a hierarchy of nodes with one root, where each node has children and no cycles. A binary tree limits each node to two children (left, right). A binary search tree (BST) adds an ordering rule: left subtree < node < right subtree, so search, insert and delete take O(height).

    If the tree is balanced, height is O(log n); if you insert sorted data into a plain BST it degenerates into a linked list with O(n) height. Self-balancing trees (red-black, AVL) keep O(log n); .NET's SortedSet and SortedDictionary are red-black trees.

    Traversals: in-order (sorted output for a BST), pre-order (copy/serialise), post-order (delete/evaluate), level-order (BFS).

    Trees are everywhere: database B+ tree indexes, file systems, JSON/XML/DOM, Roslyn syntax trees, expression trees, org charts.

  2. When: Candidate compares PriorityQueue with Queue Why: Checks queue fundamentals
    How to answer
    • A stack is LIFO (last in, first out): Push, Pop, Peek, all O(1). Used for the call stack, undo, expression parsing, DFS and backtracking.
    • A queue is FIFO (first in, first out): Enqueue, Dequeue, Peek, all O(1). Used for work queues, BFS, buffering and rate limiting.

    In .NET, Stack<T> is an array that grows at the end. Queue<T> is a circular buffer: head and tail indexes wrap around the array, so dequeuing does not shift elements.

    Neither is thread-safe. For concurrency use ConcurrentQueue<T>/ConcurrentStack<T>, or better for producer/consumer pipelines, a bounded Channel<T> which adds async waiting and back-pressure.

🛠 Real-world problems: how you handle them

Slowest-requests endpoint from 9 seconds to 600 ms
Situation
[Ops dashboard] on .NET [10] showing the [20] slowest requests from [~5 million] log records per hour.
Signals
Endpoint took [9 s], allocated [hundreds of MB] per call and triggered gen2 GCs on the API.
Investigation
Profiler showed `OrderByDescending(...).Take(20)` buffering and sorting all records.
Resolution
Replaced with a size-20 `PriorityQueue` min-heap using `EnqueueDequeue` while streaming records.
Prevention
Guideline to use heaps for top-K; allocation budget in endpoint benchmarks.

How to tell it (STAR)

**S:** Our [ops dashboard] took [9 seconds] to show the 20 slowest requests and caused GC spikes. **T:** I wanted it fast enough to auto-refresh. **A:** The code sorted [5 million] records to take 20. I replaced it with a min-heap of size 20 that streams the records and keeps only the current top 20. **R:** It dropped to [600 ms] with almost no allocations, and the GC spikes on the API disappeared.

↩ Usually asked after

More in Data Structures

Last updated 23 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me