C# Linked Lists

A linked list is a collection where each item is stored in a node, and each node points to the next node in the sequence. Unlike an array or List<T>, a linked list does not require all items to sit beside each other in one contiguous block of memory. Linked lists matter because they teach an important data-structure idea: fast insertion and removal can be more valuable than fast indexing, depending on the problem.

Overview: How C# Linked Lists Work

C# provides LinkedList<T> in System.Collections.Generic. It is a generic doubly linked list, which means every stored value lives inside a LinkedListNode<T>, and each node has references to both the previous node and the next node. The list itself keeps references to the first node, the last node, and the current Count.

This is very different from List<T>. A List<T> stores values in an internal array, so reading items[3] is fast because the runtime can jump directly to the fourth slot. A linked list has no direct slot number. To reach the fourth value, you start at First and follow Next references three times. That makes random access slow: finding an item by position is O(n), not O(1).

The advantage is node-based insertion and removal. If you already have a reference to the node, AddBefore, AddAfter, and Remove can update a few references without shifting thousands of later elements. In an array-backed list, inserting near the beginning requires moving many items right. In a linked list, the new node is linked between existing nodes by changing neighboring Next and Previous references.

Each node has overhead. Besides the value, it stores references for list ownership, previous node, and next node. Those extra objects and references use more memory and can be less cache-friendly than arrays. This is why linked lists are not automatically faster. They are best when your algorithm naturally works with nodes and frequently inserts or removes near known positions.

The built-in LinkedList<T> is not circular, and it does not allow one node to belong to two lists at the same time. A LinkedListNode<T> tracks which list owns it. Removing a node detaches it, after which it can be inserted again.

Syntax

LinkedList<type> name = new LinkedList<type>();
name.AddFirst(value);
name.AddLast(value);
LinkedListNode<type>? node = name.Find(value);
name.AddBefore(node, value);
name.AddAfter(node, value);
name.Remove(value);
name.Remove(node);
Part Meaning
LinkedList<T> A generic doubly linked list storing values of type T.
LinkedListNode<T> The node object that wraps a value and links to neighboring nodes.
First and Last References to the first and last nodes, or null when the list is empty.
AddFirst and AddLast Insert at the beginning or end.
AddBefore and AddAfter Insert relative to an existing node.
Find Searches from the beginning and returns the first matching node, or null.
Remove Removes by value or by node.

Examples

Creating and Traversing a LinkedList

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        LinkedList<string> names = new LinkedList<string>();

        names.AddLast("Ada");
        names.AddLast("Grace");
        names.AddFirst("Maya");

        Console.WriteLine($"Count: {names.Count}");
        Console.WriteLine($"First: {names.First!.Value}");
        Console.WriteLine($"Last: {names.Last!.Value}");

        foreach (string name in names)
        {
            Console.WriteLine(name);
        }
    }
}

Output:

Count: 3
First: Maya
Last: Grace
Maya
Ada
Grace

AddLast appends nodes to the tail. AddFirst creates a new head node and links the old first node after it. The foreach loop starts at First and follows each node’s Next reference until the end.

Inserting After a Known Node

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        LinkedList<string> route = new LinkedList<string>();
        route.AddLast("Home");
        route.AddLast("Library");
        route.AddLast("Office");

        LinkedListNode<string>? library = route.Find("Library");
        if (library != null)
        {
            route.AddAfter(library, "Coffee Shop");
        }

        Console.WriteLine(string.Join(" -> ", route));
    }
}

Output:

Home -> Library -> Coffee Shop -> Office

Find performs a linear search because the list cannot jump by index. Once the Library node is found, insertion after it is cheap: the new node’s links are placed between Library and Office.

Using a Linked List as a Small Deque

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        LinkedList<string> tabs = new LinkedList<string>();

        tabs.AddLast("Docs");
        tabs.AddLast("Editor");
        tabs.AddLast("Preview");

        string closed = tabs.Last!.Value;
        tabs.RemoveLast();
        tabs.AddFirst(closed);

        Console.WriteLine("Active order:");
        foreach (string tab in tabs)
        {
            Console.WriteLine(tab);
        }
    }
}

Output:

Active order:
Preview
Docs
Editor

A deque is a double-ended queue: you add or remove from both ends. LinkedList<T> can support that pattern with AddFirst, AddLast, RemoveFirst, and RemoveLast. For heavy queue workloads, also compare Queue<T> and Deque<T>-style libraries, but this shows why linked ends are useful.

Building a Simple Singly Linked List

using System;

class Program
{
    static void Main()
    {
        SimpleLinkedList list = new SimpleLinkedList();
        list.AddLast(10);
        list.AddLast(20);
        list.AddLast(30);

        Console.WriteLine(list.Contains(20));
        Console.WriteLine(list.RemoveFirst());
        Console.WriteLine(list.ToDisplayString());
    }
}

class SimpleLinkedList
{
    private Node? head;

    public void AddLast(int value)
    {
        Node node = new Node(value);
        if (head == null)
        {
            head = node;
            return;
        }

        Node current = head;
        while (current.Next != null)
        {
            current = current.Next;
        }

        current.Next = node;
    }

    public bool Contains(int value)
    {
        Node? current = head;
        while (current != null)
        {
            if (current.Value == value)
            {
                return true;
            }

            current = current.Next;
        }

        return false;
    }

    public int RemoveFirst()
    {
        if (head == null)
        {
            throw new InvalidOperationException("The list is empty.");
        }

        int value = head.Value;
        head = head.Next;
        return value;
    }

    public string ToDisplayString()
    {
        if (head == null)
        {
            return "(empty)";
        }

        string result = "";
        Node? current = head;
        while (current != null)
        {
            result += current.Value;
            current = current.Next;
            if (current != null)
            {
                result += " -> ";
            }
        }

        return result;
    }

    private class Node
    {
        public Node(int value)
        {
            Value = value;
        }

        public int Value { get; }
        public Node? Next { get; set; }
    }
}

Output:

True
10
20 -> 30

This custom example shows the core algorithm without the built-in collection. The list stores only a head reference. Each node stores a value and a Next reference. Adding at the end must walk from the head to the last node because this simple version does not store a tail pointer.

How It Works Step by Step

  1. Creating a LinkedList<T> creates a list object with no nodes. First and Last are null.
  2. AddFirst or AddLast creates a LinkedListNode<T> for the value and links it as the new head or tail.
  3. In a doubly linked list, each middle node has a Previous reference and a Next reference.
  4. Traversal follows references one node at a time. This is why enumeration is natural, but index lookup is not.
  5. Insertion beside a known node rewires a small number of references, then increments Count.
  6. Removal rewires the neighboring nodes so they skip the removed node, then clears the removed node’s ownership and neighbor references.
  7. The garbage collector later reclaims detached nodes when no live variable references them.

Complexity depends on what you already know. AddFirst, AddLast, RemoveFirst, and RemoveLast are O(1). Removing a known node is O(1). Finding a node by value is O(n). Accessing the item at position 500 is also O(n) because you must walk there.

Common Mistakes

Treating a Linked List Like an Array

LinkedList<string> names = new LinkedList<string>();
names.AddLast("Ada");
Console.WriteLine(names[0]);

LinkedList<T> has no indexer. That is intentional: indexing would hide a linear traversal behind array-like syntax. Use First, Last, Find, or a loop.

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        LinkedList<string> names = new LinkedList<string>();
        names.AddLast("Ada");
        names.AddLast("Grace");

        int index = 0;
        foreach (string name in names)
        {
            Console.WriteLine($"{index}: {name}");
            index++;
        }
    }
}

Output:

0: Ada
1: Grace

Forgetting That First Can Be Null

On an empty linked list, First and Last are null. Reading First.Value without checking can throw NullReferenceException. Check Count or compare the node with null.

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        LinkedList<int> numbers = new LinkedList<int>();

        if (numbers.First == null)
        {
            Console.WriteLine("The list is empty.");
        }
        else
        {
            Console.WriteLine(numbers.First.Value);
        }
    }
}

Output:

The list is empty.

Changing the List During foreach

foreach (string value in names)
{
    if (value.StartsWith("temp"))
    {
        names.Remove(value);
    }
}

Modifying a linked list during foreach invalidates the enumerator. When you need to remove while walking nodes, control the traversal yourself and save Next before removal.

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        LinkedList<string> names = new LinkedList<string>();
        names.AddLast("temp-a");
        names.AddLast("keep");
        names.AddLast("temp-b");

        LinkedListNode<string>? node = names.First;
        while (node != null)
        {
            LinkedListNode<string>? next = node.Next;
            if (node.Value.StartsWith("temp"))
            {
                names.Remove(node);
            }

            node = next;
        }

        Console.WriteLine(string.Join(", ", names));
    }
}

Output:

keep

Best Practices

  • Use LinkedList<T> when you frequently insert or remove near known nodes or at both ends.
  • Use List<T> or arrays when you need fast indexing, compact memory layout, sorting, or frequent random access.
  • Keep node references only when they remain valid and belong to the same list you are modifying.
  • Always check First, Last, or Find results for null before reading Value.
  • Use foreach for read-only traversal, and use explicit node traversal when removing while walking.
  • Do not assume linked lists are faster because insertion is cheap. Measure when performance matters.
  • Prefer the built-in LinkedList<T> for production code unless implementing a list is the exercise.

Practice Exercises

  1. Create a LinkedList<string> representing a music playlist. Add songs to the beginning and end, then print the first song, last song, and full order.
  2. Write a program that inserts "Review" before the node containing "Submit". Hint: use Find and check for null.
  3. Modify the SimpleLinkedList example to store both head and tail, so AddLast becomes O(1).

Summary

  • A linked list stores values in nodes connected by references.
  • C# LinkedList<T> is a doubly linked list with First, Last, Previous, and Next.
  • Linked lists are good for insertion and removal near known nodes, but poor for random indexing.
  • Find and positional traversal are O(n); adding or removing at known ends or known nodes is O(1).
  • Nodes can be removed and reinserted, but a node cannot belong to two lists at once.
  • Choose linked lists for the access pattern, not because they sound more advanced than arrays or lists.