Linear Structures
Linked lists
Nodes and pointers: cheap inserts, slow lookups
Give up instant random access, and in return you can splice a new element into the middle of your data by writing just two pointers - no shifting, no copying.
An array is a single unbroken slab of memory: element lives at , so you can leap straight to it - but inserting in the middle means shoving every later element over by one. A makes the opposite trade. Its elements are scattered , each carrying a little arrow - a - to the next one. To insert, you don't move anything. You just re-aim a couple of arrows.
The idea in one line
A node is data plus a pointer
Every node stores two things: a value and a next pointer holding the memory address of the following node. Follow next from the head and you visit the whole list; the last node's next is null, marking the tail. Because there's no formula from index to address, reaching the -th node means traversing - walking hops from the head.
- Singly linked - each node points only forward (
next). Compact; you can only walk one direction. - Doubly linked - each node also holds a
prevpointer, so you can walk backward and delete a node given only itself. The cost is an extra pointer per node to keep consistent.
Rewire it yourself - the playground
Below, boxes are nodes at fixed spots in "memory"; arrows are next pointers. Pick an operation and step through it: the highlighted arrow is the single pointer being written at that step. Watch closely - the boxes never move. Only the arrows change. That is the whole personality of a linked list.
HEAD -> 10 -> 20 -> 30 -> 40 -> null
Singly-linked list in memory. Boxes are nodes; arrows are next-pointers. Pick an operation and step through the rewiring.
The "aha": rewiring, not shifting
Where the costs actually land
A sentinel makes the edges disappear
The code is just two assignments
A node is a two-field record; inserting after a node you already hold is a pair of pointer writes. Order matters: wire the new node's next before you redirect the predecessor, or you'll lose the rest of the list.
class Node:
def __init__(self, value):
self.value = value
self.next = None # pointer to the next Node (or None = tail)
def insert_after(node, value):
fresh = Node(value)
fresh.next = node.next # 1. new node points at the old successor
node.next = fresh # 2. predecessor now points at the new node
# two writes, O(1) - nothing else in the list is touchedArray vs. linked list - the whole tradeoff
Neither wins outright. Pick the structure whose cheap operations match what your program does most.
| Operation | Array (contiguous) | Singly linked list |
|---|---|---|
| Access -th element | O(1) | O(n) |
| Insert / delete at head | O(n) | O(1) |
| Insert / delete after a held node | O(n) | O(1) |
| Search for a value | O(n) | O(n) |
| Memory per element | value only | value + pointer(s) |
| Cache locality | excellent (neighbors adjacent) | poor (nodes scattered) |
The hidden tax: cache locality
Check yourself
To insert a node right after a node you already hold a pointer to, how many pointers change?
Which task is O(1) on an array but O(n) on a singly linked list?
What does inserting into the middle of a linked list cost, and why is it different from an array?
Tie the O(1) splice to the two pointer writes. Try to state it, then check.
Lock it in
- A node is a value plus a next pointer; the list is just those pointers from a head.
- Insert and delete are two pointer writes, O(1) once you hold the spot - no shifting.
- The catch is reaching position k: traversal is O(k), so array and list flip the cost.
- A sentinel node gives the head a predecessor and erases the edge-case null checks.
- Arrays win on random access and cache locality; lists win on splicing at known points.
Primary source
Ask your teacher (where this reappears)
prev-deletion , or how skip lists layer pointers to regain fast search? Ask me. Here's the LLM tie-in, spelled out in memoization and queues to the KV cache in the LLM course: during text generation, an LLM's KV cache grows like an append-only linked list - the keys and values for each past token are computed once and the list is extended by a single node per new token, never recomputed or shifted. Same "extend one node at a time" move you just animated.