Linked List
Intuition
A linked list is a chain of small boxes, each holding one value and an
arrow pointing to the next box. The list itself is just a pointer
called head aimed at the first box. To find anything, you start at
head and follow arrows until you arrive — or run off the end.
This is the opposite of an array. An array stores everything in one
contiguous block: random access is O(1), but inserting in the
middle has to shift everything that comes after. A linked list pays
for the chain: random access is O(n), but inserting anywhere costs
only an arrow rewiring once you're standing at the right spot. The
right pick depends entirely on what you do more often: read by index,
or splice in the middle.
How It Works
The displayed code defines two classes:
Node— holds avalueand anextpointer.LinkedList— holds theheadpointer (the entry point), asizecounter, and amaxcapacity for the visualization.
The five operations the visualization walks:
insert_head(v)— allocate a new node whosenextis the old head, then pointheadat the new node.O(1).insert_tail(v)— ifheadisNone, the new node is the head. Otherwise walkcurr.nextto the last node, then attach.O(n).insert_at(k, v)— splice at indexk. Walkk − 1steps to the predecessor, thenpred.next = Node(v, pred.next).O(k).delete(v)— find the first node holdingv, then unlink it:pred.next = node.next. Special-caseheadif the match is at position 0.O(n).search(v)— walk fromhead, return the index of the first match (or−1).O(n).
The single concept every operation reduces to: splice an arrow.
Insertion writes one or two .next pointers. Deletion writes one. The
visualization deliberately shows these writes as separate beats
(makeRoom, setNewNext, setPrev) so the order — new node's next
first, then predecessor's next — is unambiguous.
Step By Step
Starting with an empty list:
insert_head(5)→ head isNone, so the new node5becomes the head. List:5 → ∅.insert_head(8)→ new node8'snextpoints at5, thenhead = 8. List:8 → 5 → ∅. Two writes, two beats.insert_tail(3)→ walkhead→8, then8.next→5, confirm5.nextisNone→ attach. List:8 → 5 → 3 → ∅.insert_at(1, 7)→ walkk − 1 = 0steps (stay athead = 8). New node7'snextpoints at8.next = 5. Then8.next = 7. List:8 → 7 → 5 → 3 → ∅.search(5)→ walk from head counting indices:8(0),7(1),5(2). Return2.delete(5)→ walk, find predecessor7. Rewire7.next = 3. List:8 → 7 → 3 → ∅.
In the visualization, each node is a labeled box; the next pointer
is a dashed arrow. A traversal cursor (curr) descends along the
chain. When a splice fires, the new arrow draws in, the old arrow
fades out, and the size header updates atomically.
Complexity
| Operation | Singly linked | Array (for comparison) |
|---|---|---|
| Access by index | O(n) | O(1) |
| Insert at head | O(1) | O(n) |
| Insert at tail | O(n)* | O(1) amortized |
Insert at index k | O(k) | O(n − k) |
| Delete by value | O(n) | O(n) |
| Search | O(n) | O(n) (unsorted) |
*Tail insertion is O(1) if the list also keeps a tail pointer.
The displayed implementation does not — it walks from head every
time. That's a deliberate simplification.
Space: O(n) for the nodes, plus one pointer per node — roughly 2×
the memory of an equivalent array.
Edge Cases
- Empty list, then
insert_head/insert_tail— the new node becomes the head. Both operations collapse to the same write. - Empty list,
delete/search— both return immediately with "not found" (False/−1). - Single-node list —
deleteof the head's value setshead = head.next = None. Size drops to 0. - Delete a value not in the list —
curr.nextwalks off the end andcurr.nextbecomesNone. Guard returnsFalse. insert_at(k, v)withk = 0— the displayed code delegates toinsert_headexplicitly.insert_at(k, v)withk > size— the displayed code walksk − 1steps even if that runs off the end. Real code should bounds-check first.
Common Mistakes
- Rewiring in the wrong order. When inserting between
predandsucc, you must setnewNode.next = succbeforepred.next = newNode. The reverse order writespred.next = newNodefirst, then asks "what was the oldpred.next?" — but that value is now lost. You'd have to stash it in a temp variable, which is what most languages without tuple assignment effectively do. - Losing the head. Writing
head = head.nextto delete the head is correct only if the old head isn't referenced elsewhere. In GC'd languages this is fine; in C / C++ you'd leak the node. - Walking off the end without checking.
while curr.next:stops one node early — leavingcurrat the last node, which is what insert-at-tail wants.while curr:walks one too far and crashes onNone.next. Pick the one that matches your intent. - Confusing singly and doubly linked lists. A doubly linked list
has a
prevpointer in addition tonext. That doubles the bookkeeping on every splice (four writes instead of two) but lets you delete a known node inO(1)without walking to find its predecessor. The displayed code is singly linked. - Using a linked list when you really want an array. Modern
CPUs are extremely fast on contiguous memory; pointer-chasing
through scattered allocations is a cache disaster. Reach for a
linked list when you genuinely need
O(1)middle-splices and rarely access by index. Otherwise an array (orArrayList/vector/ Pythonlist) almost always wins on real hardware.