Back to Blog
C#

C# LinkedList Usage: When and How to Use LinkedList<T>

c# linkedlist usage: Understand when to use LinkedList<T> in C#. Explore node-based operations, performance tradeoffs, and practical examples for efficient insertion a...

C#LinkedListCollectionsData StructuresPerformance
Diagram showing a C# LinkedList<T> with nodes and pointers illustrating efficient insertion and removal operations.

C# LinkedList usage often confuses developers because LinkedList<T> behaves differently from List<T> in ways that matter for both performance and code structure. The key is that LinkedList<T> is a doubly linked list: each element is a LinkedListNode<T> that holds a value plus references to the previous and next nodes. This design gives you O(1) insertion and removal when you already have a reference to a node, but it costs you O(n) lookup by index or value. If you are coming from a background where arrays and lists dominate, the node-based model can feel awkward, but it is the right tool for specific scenarios.

What LinkedList<T> Solves

A common problem in C# is inserting or removing items in the middle of a collection without paying the cost of shifting elements. With List<T>, adding or removing an element in the middle requires moving every subsequent element one position, which is O(n). If your application frequently inserts or removes at known positions, the list becomes a bottleneck. LinkedList<T> solves this by keeping nodes connected through pointers. When you have a node reference, adding a new node before or after it is just a matter of updating a few references, regardless of how many elements are in the list.

However, this benefit only materializes if you can get to the node in O(1) time. In practice, you often need to find the node first, which is O(n). So the advantage is most useful when you are already iterating through the list and need to perform many insertions or removals along the way, rather than when you are randomly accessing elements.

Core Operations and Syntax

To use LinkedList<T>, you create an instance and work with its methods and the LinkedListNode<T> type. Here is a minimal example that shows the basic operations:

using System; using System.Collections.Generic; var list = new LinkedList<string>(); // Add at the end list.AddLast("first"); list.AddLast("third"); // Add at the beginning list.AddFirst("zero"); // Insert after a specific node LinkedListNode<string> firstNode = list.Find("first"); list.AddAfter(firstNode, "second"); // Remove a node list.Remove("third"); foreach (var item in list) { Console.WriteLine(item); } // Output: zero, first, second

In this example, AddLast and AddFirst are O(1) because they operate on the head or tail of the list. Find is O(n) because it must traverse the list to locate the node. AddAfter is O(1) once you have the node reference. The Remove method also uses a linear search unless you already have the node, in which case you can call Remove(LinkedListNode<T>) directly.

Insertion and Removal Performance

The primary reason to choose LinkedList<T> is the performance of insertion and removal operations when you have a node reference. Consider a scenario where you are processing a list and need to remove certain elements while iterating. With List<T>, removing an element during a foreach loop is problematic because it invalidates the enumerator. With LinkedList<T>, you can iterate using the node objects and remove nodes safely because removal does not shift elements.

var node = list.First; while (node != null) { var next = node.Next; if (ShouldRemove(node.Value)) { list.Remove(node); } node = next; }

This pattern is efficient because each removal is O(1) and you do not need to restart the iteration. In contrast, removing from a List<T> while iterating requires building a new list or using a reverse for loop, which can be more complex and less efficient.

Traversal and Enumeration

LinkedList<T> implements IEnumerable<T>, so you can use foreach, LINQ, and other standard collection features. However, there is no indexer. You cannot access list[3] directly. To get the element at a specific position, you must traverse from the beginning or end, which is O(n). This makes LinkedList<T> a poor choice for scenarios that require frequent random access.

If you need to access elements by index, List<T> is the better option because it provides O(1) index-based access. The lack of an indexer also means that LinkedList<T> does not implement IList<T>, so some APIs that expect IList<T> will not accept it. This is a compatibility consideration that can affect your design.

Memory and Allocation Characteristics

Each element in a LinkedList<T> is stored in a LinkedListNode<T> object, which contains the value plus two references (Previous and Next). This adds memory overhead compared to List<T>, which stores elements in a contiguous array with only the value itself. For large collections, the extra references can significantly increase memory usage. Additionally, each node is a separate object, which may lead to more frequent garbage collection if nodes are created and removed frequently.

List<T> also has some overhead due to array resizing, but it generally uses less memory per element. If memory is a constraint and you do not need the insertion/removal performance, List<T> is usually more memory-efficient.

Choosing Between LinkedList<T> and List<T>

The decision between LinkedList<T> and List<T> should be based on your access patterns. Use LinkedList<T> when you:

  • Frequently insert or remove elements at known positions, especially in the middle of the collection.
  • Need to perform many removals while iterating, without restarting the iteration.
  • Do not require random access by index.
  • Can accept the extra memory overhead per element.

Use List<T> when you:

  • Need fast random access by index.
  • Mostly add elements at the end and rarely remove from the middle.
  • Prefer a more familiar API with an indexer.
  • Want lower memory usage per element.

In many real-world applications, List<T> is the better default because it offers a better balance of performance and simplicity. LinkedList<T> is a specialized tool that shines in specific scenarios, such as implementing a LRU cache, a playlist with frequent insertions, or a custom scheduler where nodes are moved around.

Common Pitfalls and Edge Cases

One common mistake is treating LinkedList<T> like a List<T> and using LINQ methods that require random access. For example, ElementAt(index) will traverse the list from the beginning each time, leading to O(n) operations. If you need to access many elements by index, consider converting to an array or list first.

Another pitfall is modifying the list while using an enumerator. Although LinkedList<T> allows safe removal of nodes during iteration if you use the node pattern, using foreach and calling Remove(value) inside the loop will throw an InvalidOperationException because the enumerator is invalidated. Always use the node-based iteration shown earlier when you need to modify the collection.

Also, be aware that LinkedList<T> is not thread-safe. If multiple threads access the same instance concurrently, you must synchronize access manually. This is true for most collection types, but the node-based structure can make corruption more subtle because multiple references need to be updated atomically.

Maintainability and Code Clarity

LinkedList<T> can make code harder to read because operations like AddAfter and AddBefore require a node reference, which often means you need to track nodes explicitly. This adds complexity compared to the simple Add and Remove methods of List<T>. If your team is not familiar with linked list semantics, the code may be more error-prone. Weigh the performance benefits against the additional cognitive load.

In practice, LinkedList<T> is rarely the first choice. It is a valuable tool when you have measured that insertion/removal in the middle is a bottleneck and you can structure your code around node references. Otherwise, stick with List<T> for clarity and ease of use. If you do use LinkedList<T>, encapsulate the node management within a class to keep the rest of your code clean and avoid leaking node references.

Final Technical Consideration: Node Reuse and Memory Fragmentation

A less obvious aspect of LinkedList<T> is that each node is a separate object, so creating and destroying many nodes can lead to memory fragmentation and increased garbage collection pressure. If your application frequently adds and removes items in a loop, consider reusing nodes by keeping them in a pool. This is an advanced optimization, but it can be relevant for high-throughput systems where allocation costs are significant. However, do not over-engineer unless profiling shows that allocation is a real problem. The built-in LinkedList<T> is not designed for node pooling, so you would need to implement your own structure if that level of control is required.

c# linkedlist usage: Practical Usage and Code Examples | RYUSLOG DEV