Linear structures arrange elements in a sequence. The category describes access order and position, not one memory layout. Arrays provide direct indexing and good locality. Linked lists trade that locality for edits around a node already in hand, while stacks and queues restrict which end may change.

.NET’s usual choices lean array-backed: T[], List<T>, Stack<T>, and Queue<T> all keep their elements in contiguous storage. LinkedList<T> is the contrast case. It avoids shifting during node-local edits, but pays for a node per element and pointer chasing during traversal.

The Family at a Glance

The choice comes down to access discipline and backing storage. Some structures expose any index, others one end or both. Contiguous storage usually wins on locality and steady-state allocation. Nodes become useful when the target node is already known.

StructureAccess disciplineBackingKey costs.NET
ArrayAny index, O(1)Contiguous, fixed sizeResize = reallocate + copy. Middle insert/remove = O(n) shiftT[]
Dynamic ArrayAny index, O(1). Append amortized O(1)Contiguous, grows ×2Mid-sequence insert/remove O(n)List<T>
LinkedListO(1) at a held node. O(n) to find itDoubly-linked nodesAllocation per node, cache-hostile traversalLinkedList<T>
StackOne end (LIFO)ContiguousResize on growth. No access below the topStack<T>
QueueIn back, out front (FIFO)Ring over an arrayUnbounded growth if producers outpace consumersQueue<T>
DequeBoth ends O(1). Indexed access O(1) (ring)Ring or linked nodesNo built-in .NET typeCustom ring / LinkedList<T>
Circular BufferFIFO, fixed capacity, O(1) worst-caseRing, wraps in place. Zero steady-state allocationFull ⇒ reject or overwrite oldesthand-rolled. Inside Channel<T>
SpanAny index — a view, owns nothingPoints at existing memoryStack-only, can’t cross awaitSpan<T> / Memory<T>

Choosing

Start with the access pattern:

flowchart TD
    A{Access pattern?} -->|Random access or just a sequence| B[List of T, raw array if fixed size]
    A -->|Only ever one end| C{Which discipline?}
    C -->|LIFO: backtracking, undo, DFS| C1[Stack]
    C -->|FIFO: fairness, BFS, pipelines| C2[Queue]
    A -->|Both ends| D[Deque]
    A -->|Fixed capacity, zero steady-state allocation| E[Circular Buffer]
    A -->|Many edits at positions you already hold| F[LinkedList]

Wrap any contiguous sequence in Span when slicing must avoid a copy. Stack and Queue make restricted access part of the contract, so a stray Insert(0, …) cannot violate the intended order. .NET ships no Deque. A ring buffer is usually a better starting point than LinkedList<T>. Circular Buffer fits streams with a fixed capacity or a “last N events” policy. LinkedList earns its cost only for edits at nodes already held. Otherwise, its allocations and pointer chasing lose to contiguous storage (the numbers are in Arrays).

Contiguous storage is the default until measurement shows otherwise. Cache locality is often the deciding constant factor (the measurement boundary separates hardware-cache latency from application-cache latency). And every “O(1) insert” claim for a linked list assumes the target node has already been found.

References

8 items under this folder.