Back to Blog
C#

C# List vs LinkedList: When the Difference Matters

c# list vs linkedlist: Compare C# List<T> and LinkedList<T> on storage, indexing, insertion cost, and memory behavior to pick the right collection for your workload.

List<T>LinkedList<T>Data StructuresPerformance.NET Collections
Flat illustration comparing a contiguous array of squares for List<T> with a chain of linked nodes for LinkedList<T> in C#.

When a developer searches for c# list vs linkedlist, the underlying question is usually whether switching from List<T> to LinkedList<T> will make their code faster. The answer is rarely a simple yes. The two collections solve different access patterns, and choosing the wrong one can make code slower and harder to maintain.

How Each Collection Stores Its Elements

List<T> is backed by a contiguous array. When the array fills, the list allocates a larger array and copies the existing elements into it. Every element sits at a fixed offset from the start of the buffer, which is why indexed access is O(1).

LinkedList<T> is a doubly-linked list. Each element is wrapped in a LinkedListNode<T> object that holds the value plus references to the previous and next nodes. There is no array and no index. To reach the nth element, the runtime must walk the chain from either end.

This structural difference drives every other behavior, so it is worth keeping in mind when comparing the two.

Indexed Access and Iteration

List<T> exposes an indexer, so list[5] is a direct array read. It also implements IList<T>, which means it can be passed to APIs that expect random access.

LinkedList<T> does not implement IList<T> and has no indexer. Calling LINQ's ElementAt(n) on a LinkedList<T> walks the nodes one by one, making it O(n) per call. If code frequently accesses elements by position, LinkedList<T> is the wrong choice.

Iteration is also different. A List<T> iterator reads sequentially from contiguous memory, which the CPU prefetcher handles well. A LinkedList<T> iterator follows object references scattered across the heap, so each step can be a cache miss. For large collections, iterating a LinkedList<T> is measurably slower even though both have O(n) iteration complexity.

Insertion and Removal Costs

The common claim is that LinkedList<T> has O(1) insertion and removal while List<T> is O(n). That claim is only true under specific conditions.

List<T> insertion at the end is amortized O(1) because of capacity doubling. Insertion in the middle requires shifting every subsequent element, which is O(n). Removal in the middle has the same shifting cost.

LinkedList<T> insertion and removal are O(1) only when you already hold the relevant LinkedListNode<T> reference. For example:

var linked = new LinkedList<string>(); var first = linked.AddFirst("first"); var second = linked.AddAfter(first, "second"); linked.AddBefore(second, "between");

Each operation here uses a node reference directly, so no traversal is needed. The same applies to removal:

linked.Remove(second); // O(1) because second is a known node

If you instead pass a value to Remove(value), the list must scan to find the matching node first, which is O(n).

The Practical Trap: Searching Before Inserting

Most real-world middle-insertion scenarios do not start with a node reference. They start with a value or a position. Consider inserting an element after a specific value:

var linked = new LinkedList<int>(); // ... populate the list ... var node = linked.Find(42); // O(n) scan if (node != null) { linked.AddAfter(node, 43); }

The Find call is O(n), so the overall operation is O(n) even though the insertion itself is O(1). The same is true for List<T>: finding the position is O(n), and the shift is O(n). In this scenario, LinkedList<T> does not win.

LinkedList<T> only provides a real advantage when the application keeps node references alive across operations. A classic example is an LRU cache, where you store the node alongside the cached value and move it to the front on every hit:

var node = cacheNodes[key]; linked.Remove(node); linked.AddFirst(node);

Here the node reference is already available, so both operations are O(1). Rebuilding the same behavior with List<T> would require an O(n) removal followed by an O(n) shift.

Memory Footprint and Cache Behavior

List<T> allocates a single array. The memory overhead is the unused capacity at the end of the buffer. If the list is nearly full, that overhead is small.

LinkedList<T> allocates a separate node object for every element. Each node carries the value, two references, and object header overhead. For value types, the value is stored inside the node. The total per-element cost is several times larger than a single array slot. In addition, nodes are allocated individually, so they are scattered across the heap and the garbage collector must track many small objects.

For large collections, the combination of higher memory usage and poor cache locality usually makes LinkedList<T> slower than List<T> for iteration and for most access patterns, even when the asymptotic complexity looks better on paper.

API and Compatibility Differences

LinkedList<T> does not implement IList<T>, so it cannot be used where random access is expected. It also lacks BinarySearch, Sort, and the indexer. If a method signature accepts IList<T>, a LinkedList<T> cannot be passed in.

List<T> supports collection initializers, LINQ's ToArray, and serialization frameworks that expect a list interface. LinkedList<T> works with LINQ through IEnumerable<T>, but operations like ElementAt and Count are O(n) because there is no backing array.

There is also a behavioral difference in enumeration. Modifying a LinkedList<T> while enumerating throws an InvalidOperationException, just as it does for List<T>, but the node-based structure means concurrent modification is detected differently. Neither collection is safe for concurrent writes without external synchronization.

When to Use LinkedList<T> in Real Code

LinkedList<T> is a reasonable choice when the collection is modified frequently at the ends or at known node positions, node references are held by the application and reused across operations, random access by index is never needed, and the collection size is large enough that per-node overhead is acceptable.

The LRU cache pattern above is the most common legitimate use. Another is a playlist or undo history where you navigate between items using node references and insert or remove entries at the current position.

For almost everything else, List<T> is the better default. It uses less memory, iterates faster, supports indexed access, and integrates with the broader .NET collection APIs. If profiling shows that middle insertion is a real bottleneck, the fix is usually a different data structure entirely, such as a Dictionary<TKey, TValue> for lookups or a custom indexed structure, rather than a linked list.

CriterionList<T>LinkedList<T>
Indexed accessO(1)Not supported
Insert at endAmortized O(1)O(1) with tail reference
Insert in middleO(n) shiftO(1) with node reference
Memory per elementArray slotNode object + two references
Cache localityGoodPoor
c# list vs linkedlist: Practical Usage and Code Examples | RYUSLOG DEV