📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

📚 Interview question

What is a linked list? Compare LinkedList<T> with List<T> in .NET, including big-O and real-world performance, and give a case where a linked list is the right choice.

Singly and doubly linked lists, O(1) node insert/remove vs O(n) search, poor cache locality, LinkedList<T> in .NET, and the LRU cache as a classic use.

Intermediate Theory Intermediate collections data-structures linked-list lru-cache performance

🎯 Key points

  • A linked list stores each element in a separate node holding the value and a pointer to the next node (singly linked) or to both neighbours (doubly linked); nodes are not contiguous in memory
  • Insert or remove is O(1) only when you already hold the node; finding a node by index or value is O(n)
  • LinkedList<T> in .NET is doubly linked and circular internally (the head's Previous field points to the last node), exposing First and Last; it has no indexer
  • Each LinkedListNode<T> is a separate heap object (about 48 bytes for an int on 64-bit), so a linked list allocates per element and adds GC pressure
  • Traversal chases pointers the CPU prefetcher cannot predict, so scans are often several times slower than over a List<T> even though both are O(n)
  • Classic correct use: an LRU cache with Dictionary<TKey, LinkedListNode<T>> for O(1) lookup plus a linked list for O(1) recency moves and eviction
  • Know the interview techniques: in-place reversal, Floyd's fast/slow pointers for cycle detection and finding the middle, and merging sorted lists
  • Real systems often use hybrids that keep locality, such as ConcurrentQueue<T>'s linked array segments and Dictionary's index-based collision chains

⏱ 30-second answer

A linked list stores each element in a node that points to the next node (singly linked) or to both neighbours (doubly linked). Nodes are separate heap objects, not contiguous.

  • Insert or remove when you already hold the node: O(1) (just relink pointers).
  • Find by index or value: O(n) (walk from the head).

.NET's LinkedList<T> is a doubly linked, circular list of LinkedListNode<T> objects. In practice List<T> is faster for most workloads because arrays are cache-friendly and allocate less; linked list nodes are scattered and each costs an object allocation.

Use a linked list when you need frequent O(1) removal or move of known nodes, such as an LRU cache (Dictionary of key → node + linked list for recency order).

🗣 Full answer (senior)

Structure

Singly linked:   head → [A|•] → [B|•] → [C|•] → null

Doubly linked (LinkedList<T>):
         ┌───────────────────────────────┐
         ▼                                 │
      [•|A|•] ⇄ [•|B|•] ⇄ [•|C|•] ──────┘  (circular internally; First/Last exposed)

List<T> versus LinkedList<T>

Operation List<T> LinkedList<T>
Access by index O(1) O(n) (no indexer)
Add at end Amortised O(1) (O(n) on the occasional resize) O(1)
Add/remove at front O(n) O(1)
Insert/remove with a node reference O(n) O(1)
Find value O(n) O(n), slower in practice
Memory per element Element size (+ spare capacity) A node object: header plus list, next and previous references plus the value (about 48 bytes for an int)
Cache locality Excellent Poor
GC pressure One array One object per node

Big-O hides constants: iterating a linked list risks a cache miss per node, so a linear scan can be several times slower than over an array.

When a linked list is the right choice

  • You keep node references (for example in a dictionary) and need O(1) remove, move or insert next to them.
  • You need frequent removal from both ends and from the middle at a known node.
  • Element moves must not invalidate other references (nodes stay the same objects).

Production example

A team built an in-memory recent-items cache with List<string>, calling Remove(key) then Insert(0, key) on every hit. With 50,000 items, each hit was O(n) twice and CPU went to 70% at peak. Rewriting it as an LRU cache with Dictionary<string, LinkedListNode<Entry>> plus LinkedList<Entry> made hits O(1): look up the node, Remove(node), AddFirst(node). Eviction removes Last. CPU dropped below 10%. (For most real caches we would use MemoryCache or HybridCache, but the pattern shows when linked lists shine.)

🔍 What the interviewer is checking

Checks understanding of pointer-based structures and whether the candidate can weigh big-O against real hardware behaviour.

Green flags

  • Explains nodes, singly versus doubly linked.
  • Knows O(1) insert/remove only applies with a node reference.
  • Mentions cache locality and per-node allocation.
  • Gives the LRU cache example and can code reversal or cycle detection.

Red flags

  • Recommends linked lists as generally faster for inserts.
  • Thinks LinkedList<T> supports fast indexing, or that List<T> is a linked list.

📚 Learn in depth

1. Operations on a doubly linked list

Remove B (holding node B):
   A.next = C;  C.prev = A;          O(1), no shifting

   [A] ⇄ [B] ⇄ [C]   ──►   [A] ⇄ [C]

Insert X after A:
   X.prev = A; X.next = A.next; A.next.prev = X; A.next = X;

Compare with List<T>.RemoveAt(1), which copies every later element one slot left.

2. Why it is slower than it looks

Array scan:        [A][B][C][D][E][F][G][H]   one cache line → many elements
Linked list scan:  [A]────────►[B]───►[C]──────────►[D]  each hop likely a cache miss

The CPU's prefetcher cannot predict the next address because it is stored in the current node. That makes linked lists latency-bound on memory. Nodes allocated together may sit close at first, but after the GC compacts and the list is reordered, they scatter.

3. The LinkedList<T> API

  • AddFirst, AddLast, AddBefore(node, value), AddAfter, Remove(node), RemoveFirst, RemoveLast: O(1).
  • Find(value), Contains(value) and Remove(value): O(n), because they walk the list.
  • A node belongs to exactly one list; adding it to another throws InvalidOperationException.
  • Not thread-safe: concurrent writers corrupt the links. Guard it with a lock (as an LRU cache would), or use concurrent collections such as ConcurrentQueue<T> when you only need FIFO.

4. Where linked structures appear

  • LRU caches (recency list + dictionary).
  • Free lists in allocators and object pools.
  • Undo/redo histories with cursor movement.
  • Lock-free queues and stacks: ConcurrentStack<T> uses linked nodes; ConcurrentQueue<T> uses linked segments of arrays, a hybrid that keeps locality.
  • Hash table collision chains: in .NET's Dictionary<TKey, TValue>, chains are indices inside an entries array, not separate node objects.

5. Classic interview operations

Problem Technique Cost
Reverse a singly linked list Three pointers (prev, current, next) O(n) time, O(1) space
Detect a cycle Floyd's tortoise and hare O(n) time, O(1) space
Find the middle Slow pointer moves 1, fast moves 2 O(n) time, O(1) space
Merge two sorted lists Dummy head, compare and relink O(n + m)
Remove nth from end Gap of n between two pointers O(n), one pass

6. Try it yourself

  1. Benchmark summing 1 million ints in List<int> versus LinkedList<int>.
  2. Benchmark 100,000 removals from the front of each.
  3. Implement an LRU cache with capacity 3 and test the eviction order.
  4. Implement singly linked list reversal and cycle detection.

💻 Code

// LRU cache: O(1) get, put and eviction
public sealed class LruCache<TKey, TValue>(int capacity) where TKey : notnull
{
    private readonly Dictionary<TKey, LinkedListNode<(TKey Key, TValue Value)>> _map = new(capacity);
    private readonly LinkedList<(TKey Key, TValue Value)> _order = new(); // First = most recent

    public bool TryGet(TKey key, out TValue value)
    {
        if (_map.TryGetValue(key, out var node))
        {
            _order.Remove(node);      // O(1): we hold the node
            _order.AddFirst(node);    // O(1)
            value = node.Value.Value;
            return true;
        }
        value = default!;
        return false;
    }

    public void Put(TKey key, TValue value)
    {
        if (_map.TryGetValue(key, out var existing))
        {
            _order.Remove(existing);
            _map.Remove(key);
        }
        else if (_map.Count == capacity)
        {
            var lru = _order.Last!;
            _order.RemoveLast();      // evict least recently used
            _map.Remove(lru.Value.Key);
        }
        _map[key] = _order.AddFirst((key, value));
    }
}
// Reverse a singly linked list in place
public sealed class Node(int value) { public int Value = value; public Node? Next; }

public static Node? Reverse(Node? head)
{
    Node? prev = null;
    while (head is not null)
    {
        var next = head.Next;
        head.Next = prev;
        prev = head;
        head = next;
    }
    return prev;
}

⚠ Common mistakes

  • Mistake: Choosing LinkedList<T> because "insert is O(1)". Why it is wrong: Insert is O(1) only if you already hold the node; finding the position is O(n), and the scattered nodes make that walk slow. Instead: Default to List<T> and use a linked list only when you keep node references.

  • Mistake: Calling LinkedList<T>.Remove(value) in a hot path. Why it is wrong: It searches the list from the head, which is O(n). Instead: Store LinkedListNode<T> references (for example in a dictionary) and call Remove(node).

  • Mistake: Ignoring per-node allocation. Why it is wrong: Each node is a heap object, adding GC pressure and several times the memory of an array element. Instead: Measure; for large collections an array-based structure, Queue<T> (a ring buffer) or Deque-style buffer usually wins.

  • Mistake: Sharing a LinkedList<T> between threads without a lock. Why it is wrong: Two threads relinking at once can leave broken Next/Previous pointers and lose nodes. Instead: Protect every operation with one lock, or use a concurrent collection designed for the access pattern.

  • Mistake: Building a custom LRU for production caching. Why it is wrong: It also needs thread safety, size limits, expiry and metrics, which are easy to get wrong. Instead: Use MemoryCache, HybridCache or a proven library, and keep the hand-written version for understanding and interviews.

  • Mistake: Losing the rest of the list when reversing or relinking. Why it is wrong: Overwriting Next before saving it drops the tail. Instead: Store next in a temporary variable before changing pointers.

🏛 Senior insight: trade-offs

Big-O says linked lists win for inserts and removals, but hardware says arrays usually win: contiguous memory, prefetching and fewer allocations beat pointer chasing for most real sizes. So my default is List<T>, and I reach for a linked list when I hold node handles and need O(1) relinking, the LRU pattern being the canonical example.

Workload Best choice Why
Mostly reads and appends List<T> Locality, one allocation
FIFO queue Queue<T> Ring buffer: O(1) both ends without per-item allocation
Move known items to front, evict from back Dictionary + LinkedList<T> O(1) relinking via node handles
Many producers and consumers Channel<T> or ConcurrentQueue<T> Thread safety built in
Sorted with inserts SortedSet<T> or SortedDictionary O(log n) balanced tree

The interesting senior detail is that the best real-world structures are hybrids. ConcurrentQueue<T> uses linked segments of arrays, and Dictionary<TKey, TValue> stores collision chains as indices in arrays. They keep the O(1) relinking idea and still get cache locality. That is the trade-off thinking I want to show.

🔗 Follow-ups the interviewer may ask

  1. When: Candidate is unclear why arrays are faster in practice Why: Checks contiguous memory and List<T> internals
    How to answer

    An array is a fixed-size block of contiguous elements of one type. Element i lives at start + i × elementSize, so indexing is O(1) and iteration is very cache-friendly. Its size cannot change.

    List<T> wraps an internal array plus a Count. When the array is full, Add allocates a new array about twice as large and copies the elements, so Add is amortised O(1) but occasionally O(n). Inserting or removing in the middle is O(n) because elements shift.

    Use an array for fixed-size data and hot paths, List<T> when the size changes, set Capacity when you know the size, and Span<T>/Memory<T> to work on slices without copying.

  2. When: Candidate builds an LRU with a dictionary Why: Probes hash table internals
    How to answer

    A hash table turns a key into an integer hash, maps it to a bucket (hash % bucketCount), and stores the entry there. Lookups compute the same hash, go straight to the bucket, and compare keys with Equals. Average cost is O(1); worst case (everything in one bucket) is O(n).

    .NET's Dictionary<TKey,TValue> keeps an int[] buckets and an Entry[] entries array; colliding entries form a chain via a next index inside the entries array. When full, it resizes to roughly double (a prime size) and rehashes.

    Contract: if a.Equals(b) then a.GetHashCode() == b.GetHashCode(), and a key's hash must not change while it is in the dictionary. Good hashes spread keys evenly.

↩ Usually asked after

More in Data Structures

Last updated 23 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me