FundamentalsData structureaka list, chain, node list

Linked List

A sequence of nodes where each node stores a value and a pointer to the next, giving O(1) insertion/deletion at a known position but O(n) access by index.

▶ VisualizePattern: Two PointersPractice (6)
Progress

Definition

A linked list stores elements in separate nodes scattered through memory; each node holds a value and a reference (next) to the following node. The list is identified by its head pointer, and the last node points to null. Variants add a prev pointer (Doubly Linked List) or connect the tail back to the head (Circular Linked List).

Its trade-off is the mirror image of an Array: no random access (reaching index i requires walking i pointers), but once you hold a pointer to a node, splicing a node in or out next to it is O(1) with no shifting. That property is what makes it the backbone of LRU Cache, hash-table Separate Chaining, and adjacency lists.

In interviews the linked list matters less as a container and more as a pointer-manipulation exercise: reversal, cycle detection with Fast & Slow Pointers, merging sorted lists, and finding the middle or the k-th node from the end without knowing the length.

pointersnodesO(1) insertsequential accessdummy headtwo pointers

Intuition

A mental model before the formal terms.

Think of a scavenger hunt. Each clue tells you where the next clue is; you cannot jump to clue 7 without reading clues 1 through 6. But inserting a new clue is trivial: write the new clue pointing to the old "next" location, then edit the previous clue to point to the new one. Nobody else has to move.

An array is a bookshelf where every book has a fixed slot; a linked list is a set of books each hiding a note saying where the next book is. Reading in order costs the same; jumping to the middle does not.

How it works

  1. Node: { value, next }. List: head (and often tail and size).
  2. pushFront(v): node.next = head; head = node. O(1).
  3. pushBack(v): with a tail pointer, tail.next = node; tail = node in O(1); without one, walk to the end in O(n).
  4. insertAfter(node, v): new.next = node.next; node.next = new. Two pointer writes, O(1).
  5. deleteAfter(node): node.next = node.next.next. O(1) — deleting a node requires its predecessor, which is why singly linked deletion by node reference is O(n) unless you copy the next node's value into it.
  6. get(i) / search(v): start at head, follow next until index i or the value is found. O(n).
  7. A dummy (sentinel) head node whose next is the real head removes special cases for inserting or deleting at position 0.

Why it works

Because order is encoded in pointers rather than addresses, changing the sequence only requires rewriting the pointers adjacent to the change — a constant number of writes. Arrays must physically move elements to preserve the address formula.

The absence of an address formula is exactly why access is linear: the only way to find node i is to follow i links.

Reversal works by iterating with three pointers (prev, cur, next): each step flips one link, and the invariant "everything before cur is already reversed and prev is its head" holds until cur is null.

Floyd's cycle detection works because if a cycle of length L exists, a pointer moving 2 steps gains one step per iteration on a pointer moving 1 step, so they meet within L iterations after both enter the cycle.

Operations

OperationDescriptionCost
pushFront(v)New node becomes head.O(1)
pushBack(v)Append at tail; O(1) with a tail pointer, O(n) without.O(1)
popFront()Advance head; return old value.O(1)
insertAfter(node, v)Rewire two pointers around an existing node.O(1)
deleteAfter(node)Skip the next node by pointer rewrite.O(1)
get(i)Walk i links from the head.O(n)
search(v)Linear scan following next.O(n)
reverse()Flip every next pointer in one pass with three pointers.O(n)

Recognition

How to tell a problem wants this.

  • The input is explicitly given as ListNode / head and the task is to reverse, merge, reorder, remove or detect a cycle.
  • "In O(1) extra space" for a sequence problem — you must rewire pointers instead of copying to an array.
  • You need O(1) insert/delete at an arbitrary known position, or an O(1) move-to-front (caches, MRU lists).
  • Unknown or unbounded stream length where you never need random access.

Interactive demo

Play, step, change the input. ← → and space work too.

Showing the closely related Singly Linked List visualization.

empty list
1/30Start with an empty list: head = null. Each node stores a value and a single `next` pointer, so traversal is one-directional.
VisitingInsertedRemovedMatch
1append(v): walk to the tail; tail.next = Node(v)
2prepend(v): node = Node(v); node.next = head; head = node
3insert(v, i): walk to node i-1; node.next = prev.next; prev.next = node
4delete(v): walk until curr.value == v; prev.next = curr.next
5reverse(): prev = null; curr = head
6 while curr: next = curr.next; curr.next = prev; prev = curr; curr = next
7 head = prev
Variables
size0
Complexity
access O(n)
search O(n)
insert O(1)
delete O(1)
Speed

Pseudocode

1class Node: value, next = null
2class LinkedList: head = null, tail = null, size = 0
3pushFront(v): n = Node(v); n.next = head; head = n; if tail == null: tail = n; size++
4pushBack(v): n = Node(v); if tail: tail.next = n else head = n; tail = n; size++
5insertAfter(node, v): n = Node(v); n.next = node.next; node.next = n; if node == tail: tail = n; size++
6deleteAfter(node): gone = node.next; node.next = gone.next; if gone == tail: tail = node; size--
7search(v): cur = head; while cur: if cur.value == v return cur; cur = cur.next; return null
8reverse(): prev = null; cur = head; while cur: nxt = cur.next; cur.next = prev; prev = cur; cur = nxt; head = prev

Implementation

1from typing import Generic, Optional, TypeVar
2
3T = TypeVar("T")
4
5
61 · Node and list state
7class ListNode(Generic[T]):
8 __slots__ = ("value", "next")
9
10 def __init__(self, value: T, next: "Optional[ListNode[T]]" = None) -> None:
11 self.value = value
12 self.next = next
13
14
15class LinkedList(Generic[T]):
16 def __init__(self) -> None:
17 self.head: Optional[ListNode[T]] = None
18 self.tail: Optional[ListNode[T]] = None
19 self._n = 0
20
21 def __len__(self) -> int:
22 return self._n
23
242 · Push at either end
25 def push_front(self, v: T) -> None:
26 node = ListNode(v, self.head)
27 self.head = node
28 if self.tail is None:
29 self.tail = node
30 self._n += 1
31
32 def push_back(self, v: T) -> None:
33 node = ListNode(v)
34 if self.tail is not None:
35 self.tail.next = node
36 else:
37 self.head = node
38 self.tail = node
39 self._n += 1
40
413 · Insert / delete after a known node
42 def insert_after(self, node: ListNode[T], v: T) -> None:
43 fresh = ListNode(v, node.next)
44 node.next = fresh
45 if node is self.tail:
46 self.tail = fresh
47 self._n += 1
48
49 def delete_after(self, node: ListNode[T]) -> T:
50 gone = node.next
51 if gone is None:
52 raise ValueError("nothing after node")
53 node.next = gone.next
54 if gone is self.tail:
55 self.tail = node
56 self._n -= 1
57 return gone.value
58
594 · Search by value
60 def search(self, v: T) -> Optional[ListNode[T]]:
61 cur = self.head
62 while cur is not None:
63 if cur.value == v:
64 return cur
65 cur = cur.next
66 return None
67
685 · Reverse in place
69 def reverse(self) -> None:
70 prev: Optional[ListNode[T]] = None
71 cur = self.head
72 self.tail = self.head
73 while cur is not None:
74 nxt = cur.next
75 cur.next = prev
76 prev = cur
77 cur = nxt
78 self.head = prev
Walkthrough
  1. __slots__ on ListNode removes the per-instance __dict__, shrinking each node and speeding attribute access.
  2. push_front builds the node with self.head as its next; push_back appends through tail.
  3. insert_after/delete_after splice around a node the caller holds, using is to compare with tail (identity, not equality).
  4. search compares with ==, so values that define __eq__ match structurally.
  5. reverse performs the classic three-variable walk; Python tuple assignment could compress it to one line.
Complexity (this implementation)
time O(1) push/insert_after/delete_after, O(n) search/reverse · space O(n)

Every node is a full Python object (~56 bytes with slots); a list of the same values is far smaller and faster to traverse.

Language notes
  • Python has no linked list in the standard library; collections.deque is a doubly linked list of blocks and covers most needs.
  • Use is None / is not None for null checks — a node whose value is falsy would break if node.
  • Reference counting frees nodes as soon as they are unlinked; a cycle needs the garbage collector.
Common mistakes in this language
  • Using while cur: where a node class defines __len__ or __bool__ — it may be falsy.
  • Dropping the rest of the list by writing cur.next = prev before saving cur.next.
  • Recursing over a long list — Python's default recursion limit is 1000.
Language differences that matter here
  • Memory: C++ needs explicit delete (or smart pointers) for nodes; JS and Python free unreachable nodes automatically.
  • Null: nullptr in C++, null in JS/TS (with strictNullChecks enforcing guards), None in Python (compare with is).
  • Standard library: C++ ships std::forward_list/std::list; JS and Python have no linked list — Python's deque is the closest.
  • Recursion on lists: Python's 1000-frame limit and JS's ~10k frames make iterative traversal mandatory for long lists; C++ stacks are larger but still finite.

Complexity

OperationAverageWorstNote
AccessO(n)O(n)O(1) for head; tail too with a tail pointer.
SearchO(n)O(n)
InsertO(1)O(1)Given a pointer to the neighbour; O(n) to find a position by index.
DeleteO(1)O(1)Given the predecessor; O(n) otherwise in a singly linked list.
UpdateO(n)O(n)O(1) once the node is located.
Push front / Pop frontO(1)O(1)
Push backO(1)O(1)Requires a tail pointer.
ReverseO(n)O(n)
SpaceO(n)One pointer (8 bytes) of overhead per node; two for doubly linked.

Advantages & disadvantages

Advantages
  • O(1) insertion and deletion at any position for which you already hold a node pointer.
  • No resizing or shifting; grows one node at a time, so no wasted capacity and no O(n) copy spikes.
  • Nodes can be spliced between lists in O(1) — the basis of LRU Cache and deque implementations.
  • Works naturally as a stack or queue (with a tail pointer).
Disadvantages
  • O(n) access by index and O(n) search — no Binary Search.
  • Extra memory per node for the pointer(s), often doubling the footprint for small values.
  • Poor cache locality: nodes are scattered across the heap, so a full traversal is several times slower than an array scan of equal length.
  • Singly linked lists cannot walk backwards; deleting a node by reference needs its predecessor.

Use cases

  • Bucket chains in Hash Table collision handling (Separate Chaining).
  • Doubly linked list + hash map for LRU Cache / LFU Cache with O(1) move-to-front.
  • Queues and deques (Queue, Deque) with O(1) operations at both ends.
  • Adjacency lists in graph implementations; free lists in memory allocators.
  • Undo/redo histories, playlists, and any sequence with frequent middle insertions.
Use it when
  • Frequent insertions/deletions at known positions (you hold the node), especially at the front.
  • You need to splice nodes between structures in O(1) — caches, schedulers, free lists.
  • Interview problems that hand you a ListNode and forbid extra space.
Avoid it when
  • You need random access or binary search — use an Array / Dynamic Array.
  • Memory or cache performance matters and the elements are small — pointer overhead and scattered nodes dominate.
  • You only ever append and iterate — a Dynamic Array is simpler and faster.

Alternatives

Common mistakes

  • Losing the rest of the list by overwriting cur.next before saving it in a temporary (nxt = cur.next).
  • Forgetting to update tail (or head) after inserting/deleting at the ends, leaving a stale pointer.
  • Null-pointer errors on empty or single-node lists; fix with a dummy head and explicit checks.
  • Deleting a node by reference in a singly linked list without its predecessor — impossible except by copying the next node's value.
  • Off-by-one in fast/slow pointer loops (fast && fast.next vs fast.next && fast.next.next) when finding the middle.
  • Accidentally creating a cycle when reordering nodes and never terminating the list with null.

Interview patterns

  • Iterative and recursive reversal, including reverse in groups of k and reverse between positions.
  • Fast/slow pointers to find the middle, detect a cycle, and locate the cycle start.
  • Two-pointer gap of k to remove the k-th node from the end in one pass.
  • Merge two sorted lists with a dummy head; merge k lists with a Min-Heap.
  • Dummy head sentinel to unify insert/delete at position 0 with the general case.
  • Splitting and re-merging lists (reorder list, odd-even list, sort list with merge sort).

Interview problems