liveBook · Manning

Linked list

As explained in Fabulous Adventures in Data Structures and Algorithms

A linked list is a sequence of nodes in which each node refers to the next node in the sequence.

A linked list is either empty or a value followed by another linked list, its tail. In an immutable version, Push places a new value in front of an existing list rather than changing it. Each new head can retain the old list as its tail. section 1.2

Listing 1.1 Example immutable linked list
sealed record LinkedList<T>(T? Value, LinkedList<T>? Tail)
{
    public static LinkedList<T> Empty = new (default, null);#1
    public LinkedList<T> Push(T value) => new (value, this);
    public bool IsEmpty => this == Empty; #2
 public override string ToString()
 {
     var s = "";
     for (var list = this; !list.IsEmpty; list = list.Tail!)
         s += list.Value + " "; #3
     return s;
    }
}

The C# record keeps a value and tail together and gives callers Push and IsEmpty. Its empty list uses null for both fields—a compact choice, but one that still permits invalid combinations of value and tail. section 1.2

The small implementation has several performance surprises:

  • Push takes O(1) time and extra memory; building n items takes O(n) of each. section 1.2.1
  • Record-generated value equality recursively compares lists. Comparing long lists can overflow the call stack; even IsEmpty does more work than a direct tail check. section 1.2.1
  • Repeated string concatenation makes the sample ToString O(n²) in time and extra memory. A StringBuilder would make it O(n). section 1.2.1
public LinkedList<T> Reverse()
{
    var result = Empty;
    for (var list = this; !list.IsEmpty; list = list.Tail!)
        result = result.Push(list.Value!);
    return result;
}

Reverse walks the original list and Pushes each value onto an initially empty result. The original is unchanged, and reversal takes O(n) time and extra memory. section 1.2.2

Reversal without mutation

Push 10, 20, 30, 40, and 50 in that order. The resulting list prints as 50 40 30 20 10; its reversal prints as 10 20 30 40 50, while the first list remains available. section 1.2.2

A linked list can also implement an immutable stack. Start with the required operations—Push, Peek, Pop, and IsEmpty—and their behavior on an empty stack, then choose the representation. A dedicated empty object avoids the first list’s ambiguous null-based state; new stack heads can share an unchanged tail. section 2.2

Listing 2.2 Implementing the immutable stack
class ImStack<T> : IImStack<T>
{
    private class EmptyStack : IImStack<T> #1
    {
        public EmptyStack() { }
        public IImStack<T> Push(T item) => new ImStack<T>(item, this); #2
        public T Peek() => throw new InvalidOperationException();
        public IImStack<T> Pop() => throw new InvalidOperationException();
        public bool IsEmpty => true;
        public IEnumerator<T> GetEnumerator()  
        {
            yield break;
        }
        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); #3
    }
    public static IImStack<T> Empty { get; } = new EmptyStack();  #4

    private readonly T item;
    private readonly IImStack<T> tail;
    private ImStack(T item, IImStack<T> tail) #5
    {
        this.item = item;
        this.tail = tail;
    }
    public IImStack<T> Push(T item) => new ImStack<T>(item, this);
    public T Peek() => item;
    public IImStack<T> Pop() => tail;
    public bool IsEmpty => false;
    public IEnumerator<T> GetEnumerator()
    {
        for (IImStack<T> s = this; !s.IsEmpty; s = s.Pop())
            yield return s.Peek();
    }
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

The stack implementation separates the empty case from a nonempty node. Its empty object permits Push but rejects Peek and Pop; each nonempty node stores an item and a readonly tail. section 2.2

How Fabulous Adventures in Data Structures and Algorithms covers it

How deep it goes
Builds a C# implementation, analyzes operation costs and generated equality, then replaces its error-prone empty representation in a stack.
Examples
Demonstrates reversal with five integers and shows stack branches that share an existing tail.
What it assumes
Assumes familiarity with C# record classes and nullability notation, while defining the linked-list structure from its empty and nonempty cases.
What you can do after
Build it from scratch as an immutable list, reverse it in linear time, and refine the representation for an immutable stack.
How it connects
Connects linked lists to immutable stacks, abstract data types, structural sharing, value equality, and performance analysis.

Also explained in

Compare how 8 books explain linked list →

Related concepts

This explainer was generated by Manning's knowledge graph from the text of Fabulous Adventures in Data Structures and Algorithms. The section links point to the parts of the book it draws on.