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

📚 Interview question

How do Queue<T> and Stack<T> work in C#? Explain their internal storage, Enqueue/Dequeue and Push/Pop, the Try methods, the enumeration order, and when you would use PriorityQueue<TElement,TPriority> or a concurrent queue instead.

The .NET `Queue<T>` and `Stack<T>` APIs, their circular-buffer and array internals, and the priority and concurrent alternatives.

Junior Theory Beginner collections fifo-lifo priorityqueue queue stack

🎯 Key points

  • `Queue<T>` is FIFO; `Stack<T>` is LIFO; both are array-backed with amortised O(1) operations
  • `Queue<T>` uses a circular buffer, so `Dequeue` never shifts elements
  • `Dequeue`, `Pop` and `Peek` throw on empty; `TryDequeue`, `TryPop`, `TryPeek` return `false`
  • A stack enumerates top to bottom, the reverse of push order
  • `PriorityQueue<TElement,TPriority>` is a min-heap: lowest priority first, not stable
  • Use an explicit `Stack<T>` to replace deep recursion and avoid `StackOverflowException`
  • None are thread-safe; use `ConcurrentQueue<T>`, `ConcurrentStack<T>` or `Channel<T>`
  • `List<T>.RemoveAt(0)` as a queue is O(n) per call

⏱ 30-second answer

Queue<T> is FIFO (first in, first out) and Stack<T> is LIFO (last in, first out). Both are backed by an array and give O(1) Enqueue/Dequeue and Push/Pop (amortised, since the array grows). Queue<T> uses a circular buffer with head and tail indexes so dequeuing never shifts elements. Dequeue, Pop and Peek throw InvalidOperationException when empty; TryDequeue, TryPop and TryPeek return false instead. A stack enumerates from the top down. Use PriorityQueue<TElement,TPriority> (.NET 6+) when the next item depends on priority, and ConcurrentQueue<T> or Channel<T> when several threads produce or consume.

🗣 Full answer (senior)

How I would answer this in an interview

Queue<T> is FIFO (first in, first out) and Stack<T> is LIFO (last in, first out). Both are backed by an array, give O(1) amortised add/remove at their ends, and are not thread-safe. I'd describe the storage, the API including the Try methods, enumeration order, and then when to switch to PriorityQueue<TElement,TPriority>, ConcurrentQueue<T> or Channel<T>.

1. Internal storage

  • Stack<T>: an array plus a count. Push writes at array[count++]; Pop reads array[--count]. Growth doubles the array (like List<T>).
  • Queue<T>: a circular buffer — an array with _head, _tail and _size. Enqueue writes at _tail and advances it (wrapping to 0 at the end); Dequeue reads at _head and advances it. When full, it allocates a larger array and copies the elements in order (unwrapping the ring).

Because both use arrays, they are compact and cache-friendly; there is no per-element node allocation (unlike a linked list).

2. API

Operation Queue<T> Stack<T> Cost
Add Enqueue(x) Push(x) Amortised O(1)
Remove + return Dequeue() Pop() O(1); throws InvalidOperationException if empty
Look without removing Peek() Peek() O(1); throws if empty
Safe versions TryDequeue(out x), TryPeek(out x) TryPop(out x), TryPeek(out x) O(1), return bool (.NET Core 2.0+)
Count Count Count O(1)
Search Contains(x) Contains(x) O(n)
Capacity EnsureCapacity, TrimExcess same

Prefer the Try methods when emptiness is a normal condition; using Count > 0 + Dequeue is fine single-threaded but racy if anything else touches the collection.

3. Enumeration order

  • foreach over a Queue<T> yields items in dequeue order (oldest first) without removing them.
  • foreach over a Stack<T> yields items in pop order (most recent first).
  • ToArray() follows the same order. Note: new Stack<T>(otherStack) reverses the order, because it pushes items in the enumeration order of the source — a common gotcha when copying stacks.
  • Both use version checks: modifying during enumeration throws.

4. Typical uses

  • Queue: breadth-first search, processing work in arrival order, buffering events, rate-limited sending, round-robin scheduling.
  • Stack: depth-first search without recursion (avoids StackOverflowException on deep data), undo/redo, expression parsing (shunting-yard), matching brackets, backtracking.

5. PriorityQueue<TElement,TPriority> (.NET 6)

When the next item to process should be the most important, not the oldest:

var queue = new PriorityQueue<Job, int>();     // min-priority: smallest priority dequeued first
queue.Enqueue(new Job("refund"), priority: 1);
queue.Enqueue(new Job("report"), priority: 5);
while (queue.TryDequeue(out var job, out var priority)) Run(job);
  • Implemented as an array-backed 4-ary min-heap: Enqueue and Dequeue are O(log n), Peek is O(1).
  • Not stable: items with equal priority come out in an unspecified order. Add a sequence number to the priority ((priority, sequence) tuple) if FIFO among equals matters.
  • No Contains, no update-priority operation; enumeration via UnorderedItems is unordered.
  • For a max-priority queue, pass a reversed comparer: new PriorityQueue<T, int>(Comparer<int>.Create((a, b) => b.CompareTo(a))).
  • Use cases: Dijkstra/A* path finding, scheduling jobs by deadline, merging sorted streams (k-way merge), top-N tracking.

6. Concurrent options

Queue<T> and Stack<T> are not thread-safe. For producer/consumer scenarios:

  • ConcurrentQueue<T> / ConcurrentStack<T>: lock-free, use TryDequeue/TryPop.
  • Channel<T> (System.Threading.Channels): async producer/consumer with backpressure (bounded channels), await reader.ReadAsync(), completion signalling — the modern default for in-process pipelines and BackgroundService work queues.
  • For work that must survive restarts or scale across instances, use a durable broker (Azure Service Bus, Storage Queues), not in-memory queues.

7. A production example

A background export processor used a static Queue<ExportRequest> filled by API requests and drained by a hosted service with if (queue.Count > 0) queue.Dequeue(). Under load, requests were occasionally lost or processed twice, and once an InvalidOperationException ("Queue empty") crashed the hosted service, silently stopping all exports. We replaced it with a bounded Channel<ExportRequest> (BoundedChannelFullMode.Wait for backpressure), a BackgroundService reading with await foreach (var r in reader.ReadAllAsync(ct)), and error handling per item. Because exports were business-critical, we later moved to Azure Service Bus so pending work survived deployments.

🔍 What the interviewer is checking

Checks basic collection knowledge and whether the candidate thinks about empty cases and ordering.

  • Green flags: explains FIFO/LIFO, circular buffer, O(1) operations, Try methods, stack enumeration order, and names PriorityQueue and concurrent options.
  • Red flags: uses lists as queues, ignores empty handling, or thinks Queue<T> is thread-safe.

📚 Learn in depth

1. The circular buffer in Queue<T>

capacity 8, after enqueuing A..F and dequeuing A, B:
index:  0   1   2   3   4   5   6   7
       [ ] [ ] [C] [D] [E] [F] [ ] [ ]
                ^head           ^tail

enqueue G, H, I (I wraps to index 0):
       [I] [ ] [C] [D] [E] [F] [G] [H]
            ^tail ^head
  • Enqueue: _array[_tail] = item; MoveNext(ref _tail); _size++, where MoveNext wraps to 0 at the end (implemented with a compare rather than % for speed).
  • Dequeue: read _array[_head], clear the slot if T contains references (so the GC can collect the object), advance _head.
  • Growth: when _size == _array.Length, allocate a larger array (2×, minimum +4) and copy the two segments (_head..end and 0.._tail) so the new array starts at index 0.

Without the ring buffer, dequeuing from the front of an array would require shifting (O(n)) as List<T>.RemoveAt(0) does.

2. Why Stack<T> and Queue<T> clear slots

When T is a reference type (or a struct containing references), popped/dequeued slots are set to default. Otherwise the backing array would keep removed objects reachable and prevent garbage collection — a subtle memory leak in long-lived queues. For unmanaged T, the runtime skips clearing for speed (RuntimeHelpers.IsReferenceOrContainsReferences<T>()).

3. Binary and d-ary heaps

PriorityQueue stores a complete tree in an array. For a 4-ary heap, the children of node i are at 4i + 1 .. 4i + 4, and the parent at (i − 1) / 4.

  • Enqueue: append at the end, then sift up (swap with parent while smaller) — O(log₄ n).
  • Dequeue: take the root, move the last element to the root, then sift down (swap with the smallest child) — O(log₄ n) levels, each comparing up to 4 children.

A 4-ary heap is shallower than a binary heap (fewer levels, fewer cache misses) at the cost of more comparisons per level; .NET chose arity 4 after benchmarking. EnqueueRange and the constructor from a collection use heapify (Floyd's algorithm), O(n), which is faster than n enqueues.

EnqueueDequeue and DequeueEnqueue combine operations efficiently — useful for maintaining a bounded top-N heap.

4. Top-N with a bounded heap

// Keep the 10 largest orders seen in a stream
var heap = new PriorityQueue<Order, decimal>();      // min-heap by total
foreach (var o in orders)
{
    if (heap.Count < 10) heap.Enqueue(o, o.Total);
    else if (heap.TryPeek(out _, out var smallest) && o.Total > smallest) heap.DequeueEnqueue(o, o.Total);
}

O(n log 10) time and O(10) memory, versus sorting everything (O(n log n), O(n) memory).

5. Iterative DFS/BFS

static IEnumerable<Node> Bfs(Node root)
{
    var queue = new Queue<Node>([root]);
    var seen = new HashSet<Node>([root]);
    while (queue.TryDequeue(out var node))
    {
        yield return node;
        foreach (var child in node.Children)
            if (seen.Add(child)) queue.Enqueue(child);
    }
}

Replacing the Queue with a Stack turns it into depth-first traversal. Using an explicit stack avoids recursion depth limits — important because StackOverflowException cannot be caught and terminates the process.

6. Concurrent queues and channels internally

  • ConcurrentQueue<T> uses linked segments (arrays) with lock-free head/tail indexes using Interlocked operations; it scales well under contention and never blocks.
  • Channel<T> builds on similar structures plus async waiters: ReadAsync completes immediately if data is available, otherwise returns a pending ValueTask completed by the next writer. Bounded channels make writers wait (or drop items, depending on BoundedChannelFullMode), giving backpressure so a fast producer can't exhaust memory.

Try it yourself

  1. Implement BFS and DFS over a tree with Queue<T> and Stack<T> and compare visit order.
  2. Copy a stack with new Stack<T>(stack) and compare the enumeration order.
  3. Enqueue items with equal priority into a PriorityQueue and observe the dequeue order; fix it with a (priority, sequence) tuple.
  4. Build a bounded Channel<int> with a slow consumer and watch the producer wait.

💻 Code

// FIFO work list
var work = new Queue<string>();
work.Enqueue("a.csv");
work.Enqueue("b.csv");
while (work.TryDequeue(out var file))
    Console.WriteLine($"Processing {file}");   // a.csv, then b.csv

// LIFO: iterative depth-first walk, no recursion
var pending = new Stack<string>();
pending.Push(rootFolder);
while (pending.TryPop(out var dir))
{
    foreach (var sub in Directory.EnumerateDirectories(dir))
        pending.Push(sub);
}

// Constructor order surprise
var s = new Stack<int>([1, 2, 3]);
Console.WriteLine(s.Pop());                    // 3
Console.WriteLine(string.Join(",", s));       // 2,1

// Priority: lowest value comes out first
var jobs = new PriorityQueue<string, int>();
jobs.Enqueue("email", 3);
jobs.Enqueue("payment", 1);
jobs.Enqueue("report", 2);
while (jobs.TryDequeue(out var job, out var priority))
    Console.WriteLine($"{priority}: {job}");  // payment, report, email

⚠ Common mistakes

1. Calling Dequeue/Pop on an empty collection

What happens: queue.Dequeue() without checking. Why it is wrong: throws InvalidOperationException, which can crash a background service. Do instead: use TryDequeue/TryPop, or check Count in single-threaded code.

2. Using Queue<T> across threads

What happens: a static queue shared between API requests and a background worker. Why it is wrong: not thread-safe — lost items, duplicates, corrupted state and race conditions between Count and Dequeue. Do instead: use Channel<T> or ConcurrentQueue<T>.

3. Using List<T>.RemoveAt(0) as a queue

What happens: a list treated as FIFO. Why it is wrong: each removal shifts all elements — O(n) per dequeue. Do instead: use Queue<T>.

4. Expecting PriorityQueue to be FIFO for equal priorities

What happens: jobs with the same priority are processed out of arrival order. Why it is wrong: the heap is not stable. Do instead: include a monotonically increasing sequence number in the priority.

5. Trying to update priorities in PriorityQueue

What happens: code searches the queue to change an element's priority. Why it is wrong: there's no update operation; enumeration is unordered and modification isn't supported. Do instead: enqueue the item again with the new priority and ignore stale entries when dequeued (lazy deletion), or use a different structure.

6. Unbounded in-memory queues

What happens: producers enqueue faster than consumers process. Why it is wrong: memory grows until the process is OOM-killed, and all pending work is lost on restart. Do instead: use bounded channels for backpressure, and durable queues (Service Bus) for important work.

7. Copying a stack and expecting the same order

What happens: var copy = new Stack<T>(original);. Why it is wrong: the copy is reversed because the constructor pushes in enumeration (pop) order. Do instead: new Stack<T>(original.Reverse()) or copy via ToArray() and reverse.

8. Recursion for deep traversals

What happens: recursive DFS over large or user-supplied graphs. Why it is wrong: deep inputs cause StackOverflowException, which cannot be caught. Do instead: use an explicit Stack<T> and a visited set.

9. Using Contains on queues frequently

What happens: checking for duplicates with queue.Contains(item) before enqueueing. Why it is wrong: O(n) per check. Do instead: maintain a companion HashSet<T> of queued items.

10. Modifying a queue or stack during foreach

What happens: dequeuing inside a foreach over the same queue. Why it is wrong: throws InvalidOperationException due to version checks. Do instead: use while (queue.TryDequeue(out var item)).

🏛 Senior insight: trade-offs

Seniors use these types to remove recursion and to make processing order explicit, and they know the limits.

Problem Tool Why
Deep recursion risk Stack<T> loop Heap memory, no StackOverflowException
Level-order processing Queue<T> BFS order
Scheduling by urgency PriorityQueue O(log n) dequeue of minimum
Ties must be FIFO priority (level, seq) Heap is unstable
Cross-thread hand-off Channel<T> Async, bounded, back-pressure

They also mention that StackOverflowException cannot be caught in .NET and kills the process, which is a strong reason to make deep traversals iterative.

🔗 Follow-ups the interviewer may ask

  1. When: Candidate is asked for stack and queue theory Why: Checks data structure 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.

  2. When: Candidate mentions PriorityQueue Why: Probes heap internals and ordering guarantees
    How to 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.)

  3. When: Candidate needs a queue shared between threads Why: Checks ConcurrentQueue and Channel knowledge
    How to answer

    System.Collections.Concurrent types are safe for many threads at once. ConcurrentDictionary uses lock-free reads and fine-grained (striped) locks for writes. Each single method is atomic, but check-then-act across two calls is not, so use GetOrAdd, AddOrUpdate, TryUpdate or TryRemove. Caveat: the value factory in GetOrAdd/AddOrUpdate can run more than once under contention (only one result is stored), so wrap expensive or side-effecting work in Lazy<T>. Count, IsEmpty and ToArray take every lock, so avoid them in hot paths. ConcurrentQueue/ConcurrentStack are lock-free FIFO/LIFO. ConcurrentBag is unordered, optimised for the same thread adding and taking. BlockingCollection adds bounding and blocking Take but blocks threads; for async producer/consumer use Channel<T>.

↩ Usually asked after

More in Collections

Last updated 30 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me