Queues, circular buffers and deques
First in, first out: why a List makes a slow queue, how a circular buffer gives O(1) at both ends, using Queue<T>, and when you need a deque.
| Operation | Meaning | Cost |
|---|---|---|
Enqueue(x) | O(1) amortised | |
Dequeue() | O(1) | |
Peek() | O(1) | |
Count | O(1) |
The slow way: a List
list.Add(x) to enqueue and list.RemoveAt(0) to dequeue. But removing index 0 shifts every other item one slot left, so each dequeue is O(n), and processing n items costs O(n²).
The fast way: a circular buffer
head (where the next item will be taken) and tail (where the next item will be written). Both only move forward, and when they reach the end of the array they wrap around to index 0 using % capacity. The array behaves like a ring.
public class RingQueue<T>{ private T[] _items; private int _head; // index of the front item private int _tail; // index where the next item goes private int _count; public RingQueue(int capacity = 4) => _items = new T[capacity]; public int Count => _count; public void Enqueue(T item) { if (_count == _items.Length) Grow(); _items[_tail] = item; _tail = (_tail + 1) % _items.Length; // wrap around _count++; } public T Dequeue() { if (_count == 0) throw new InvalidOperationException("Queue is empty"); T item = _items[_head]; _items[_head] = default!; _head = (_head + 1) % _items.Length; // wrap around _count--; return item; } private void Grow() { var bigger = new T[_items.Length * 2]; for (int i = 0; i < _count; i++) // copy in queue order, front first bigger[i] = _items[(_head + i) % _items.Length]; _items = bigger; _head = 0; _tail = _count; }}Queue<T> in .NET
Queue<T> is a growable circular buffer, just like the one above.
var jobs = new Queue<string>();jobs.Enqueue("Export IFC");jobs.Enqueue("Generate drawings");jobs.Enqueue("Send report"); Console.WriteLine($"Next up: {jobs.Peek()}"); while (jobs.TryDequeue(out var job)) Console.WriteLine($"Running {job} ({jobs.Count} left)");Next up: Export IFC Running Export IFC (2 left) Running Generate drawings (1 left) Running Send report (0 left)
Deque: a double-ended queue
head backwards is (head - 1 + capacity) % capacity.
Deque<T>. Use LinkedList<T> (AddFirst, AddLast, RemoveFirst, RemoveLast are all O(1)), or write a circular-buffer deque when performance matters.
Other queues you will meet
| Type | Order | Where |
|---|---|---|
Queue<T> | ||
PriorityQueue<TElement, TPriority> | ||
ConcurrentQueue<T> | ||
Channel<T> |
RecentCounter class. Ping(int t) records a request at time t milliseconds (times only increase) and returns how many requests happened in the last 3000 ms, including this one (the range [t − 3000, t]). Pings at 1, 100, 3001, 3002 should return 1, 2, 3, 3.
Show a hint
Queue<int>. On each ping, enqueue t, then dequeue from the front while the oldest time is less than t − 3000.
Show the solution
var counter = new RecentCounter();foreach (int t in new[] { 1, 100, 3001, 3002 }) Console.Write(counter.Ping(t) + " "); public class RecentCounter{ private readonly Queue<int> _times = new(); public int Ping(int t) { _times.Enqueue(t); while (_times.Peek() < t - 3000) _times.Dequeue(); // too old, never needed again return _times.Count; }}1 2 3 3
Remember
A queue is FIFO: enqueue at the back, dequeue from the front, both O(1). A List with RemoveAt(0)is an O(n) dequeue; a circular buffer moves indexes instead of items.Head and tail wrap around with % capacity; when growing, copy items in queue order.A deque allows O(1) at both ends; in .NET use LinkedList<T>or a circular buffer.