List vs LinkedList in C#: When to Use Each
Use List<T> by default in C#, and choose LinkedList<T> only when you hold references to nodes and insert or remove at those nodes often. List<T> is a resizable array: index access is O(1), adding at the end is O(1) on average, and inserting or removing in the middle is O(n) because later elements shift. LinkedList<T> is a doubly linked list. Each element is a node with a Next and a Previous reference, so inserting or removing at a node you already hold is O(1). There is no indexer, and reaching a position means walking from one end, which is O(n).
In practice that makes List<T> the right choice in almost every case, including most cases that sound like linked list work. The exceptions are specific, and they are listed below.
How List<T> works
List<T> keeps its elements in one array and tracks how many slots are in use. When the array is full, Add allocates a new array of double the capacity and copies the elements across. That copy is why Add is O(1) only on average, which is called amortized constant time.
Because the elements sit next to each other in memory, list[i] is a single address calculation. Iterating a List<T> is also fast, since the processor cache loads neighboring elements together.
var numbers = new List<int> { 1, 2, 3, 4, 5 }; Console.WriteLine(numbers[2]); // 3, O(1) numbers.Add(6); // O(1) amortized numbers.Insert(0, 0); // O(n): shifts six elements right numbers.RemoveAt(3); // O(n): shifts the tail left
How LinkedList<T> works
LinkedList<T> stores each element in a LinkedListNode<T> object. A node holds the value, a reference to the next node, a reference to the previous node, and a reference to its list. The list itself holds only First, Last and Count.
There is no list[i]. LinkedList<T> implements ICollection<T> but not IList<T>, and the LINQ method ElementAt(i) walks i steps to get there. What you get instead is O(1) insertion and removal at any node whose reference you already hold.
var names = new LinkedList<string>(); names.AddLast("b"); names.AddLast("d"); names.AddFirst("a"); // a, b, d LinkedListNode<string> node = names.Find("b"); // O(n) search names.AddAfter(node, "c"); // O(1): a, b, c, d names.Remove("d"); // O(n): searches by value names.RemoveFirst(); // O(1): b, c for (var n = names.First; n != null; n = n.Next) Console.WriteLine(n.Value);
Remove("d") searches, while Remove(node) with a node reference is O(1). The speed of a linked list exists only when you skip the search.
List vs LinkedList: side by side
| Operation | List<T> | LinkedList<T> |
|---|---|---|
| Access by index | O(1) | Not supported; ElementAt is O(n) |
| Add at end | O(1) amortized | O(1) |
| Add or remove at front | O(n) | O(1) |
| Insert or remove in the middle | O(n), shifts elements | O(1) with a node reference, O(n) to find one |
| Search by value | O(n) | O(n) |
| Memory per element | The value, plus unused capacity | The value plus three references and an object header |
| Iteration | Fast, contiguous memory | Slower, each node is a separate object |
| Interfaces | IList<T> | ICollection<T> only |
On a 64-bit runtime a LinkedListNode<int> costs about 48 bytes, against 4 bytes per int inside a List<int>. For a million elements that is about 48 MB against 4 to 8 MB, depending on spare capacity, plus the time to allocate a million small objects.
When LinkedList<T> is the right choice
You keep node references and edit at them. The clearest example is an LRU cache, which is a cache that evicts the least recently used entry when it is full. A Dictionary<TKey, LinkedListNode<TValue>> finds the node in O(1), and Remove(node) followed by AddFirst(node) moves it to the front in O(1). A List<T> would spend O(n) on every move.
You add and remove at both ends and also from the middle. A scheduler that cancels arbitrary pending items by node reference is one case. If you only need both ends, Queue<T> and Stack<T> are array based and faster.
References must stay valid across insertions. A List<T> index becomes wrong as soon as something is inserted before it. A LinkedListNode<T> stays correct.
When List<T> is the right choice
Every other case. Indexing, sorting, binary search, slicing with GetRange, and LINQ over a large collection all run faster on an array. Even inserting in the middle is often faster on a List<T> when the collection is small. Copying a few hundred elements costs less than walking a few hundred nodes to find the position.
If you are unsure, use List<T>, measure, and switch only when a profiler shows the shifting cost.
One note for readers who know Java. Java's LinkedList implements both List and Deque, so it has get(i) and can serve as a queue. The C# type has no indexer and does not implement IList<T>, so the two are not interchangeable in a design discussion.
Key Takeaways
- Default to
List<T>. Index access, iteration and memory all favor the array. LinkedList<T>is faster only when you already hold the node. Without the reference, the O(n) search cancels the O(1) edit.- The LRU cache is the standard example. A dictionary of nodes plus a linked list gives O(1) get, put and eviction.
- Learn both structures with their costs. Grokking Data Structures for Coding Interviews covers arrays and linked lists with worked problems.
- Practice the linked list questions. Grokking the Coding Interview has a full pattern on in-place reversal of a linked list, and a mock interview tests whether you can state the trade-off in one sentence.

GET YOUR FREE
Coding Questions Catalog

$99

$197

$72