Skip to content

Linked lists

coreCommon atAmazonMicrosoftMetaBloombergHRTRead firstArraysPractise in the Sandbox →

A linked list stores each item in a node that points to the next one (and, in a doubly linked list, the previous one). There’s no indexing, so finding position i is O(n), but once you hold a node, inserting or removing next to it is O(1): just rewire a few pointers. Sentinel nodes at both ends remove every “empty list / first node / last node” special case.

A treasure hunt: each clue tells you where the next clue is. To reach clue 50 you must follow 49 clues; there’s no shortcut. But inserting a new clue between 12 and 13 is trivial: write the new clue to point at 13, and change clue 12 to point at the new one. Nothing else moves.

That’s the trade against an array:

Array Linked list
reach position i O(1) O(i)
insert/remove at a known spot O(n) (shift) O(1) (rewire)
memory one block, cache friendly one allocation per node, plus pointers, scattered

A doubly linked list adds a prev pointer to each node. That’s what makes removing a node O(1) when all you have is the node itself: you can reach both neighbours without walking from the head.

Sentinels (also called dummy nodes) are permanent fake nodes at the front and back. With them, every real node always has a real prev and a real next, so insert and remove are the same four or two lines every time.

Push to the front and back, click a node to select it, insert after it or remove it. Each operation plays the exact pointer writes in order. Note that none of them branch on “is this the first node?”.

Doubly linked list with sentinels
sandbox
head379tail
node being linked / selectednode being unlinked
dashed boxes = sentinels; solid arrow = next, dashed arrow = prev; the highlighted arrow is the pointer written in this step
Sentinels (dashed) are dummy nodes that never hold data. Click a real node to select it, then insert after it or remove it.
1 / 1
log (newest first)
Nothing yet. Try an operation.
The problem says… Why a linked list fits
“remove / move this item in O(1)” and you can keep a reference to it Doubly linked list + a hash map from key to node: O(1) find, O(1) unlink and relink. See caching.
“maintain an order that changes constantly” (recency, insertion order with deletes) Splicing is O(1); an array would shift.
the input is a linked list Reverse, merge, split, find middle, detect cycles: pointer manipulation in O(1) space.
“insert many items in the middle while iterating” Insert next to the cursor without shifting.
chains inside a hash table bucket Each bucket’s collisions form a short list. See hash maps.

If none of these apply, prefer an array. It’s faster in practice for almost everything else.

  1. prev = null, curr = head.
  2. Save nxt = curr.next before touching anything; it’s the only path to the rest of the list.
  3. Flip the link: curr.next = prev.
  4. Advance: prev = curr, curr = nxt. When curr is null, prev is the new head.
Reverse a singly linked list in place
step through
nullnull1curr2345
prevnull
curr1
nxt—
list from prevnull
prev = null, curr = head. Invariant: everything before curr is already reversed and starts at prev.
1 / 22
reverse.py
from __future__ import annotations
from dataclasses import dataclass
@dataclass
class Node:
val: int
next: Node | None = None
def reverse(head: Node | None) -> Node | None:
prev: Node | None = None
curr = head
while curr:
nxt = curr.next # 1. save the rest before we cut the link
curr.next = prev # 2. flip one link
prev = curr # 3. the reversed part now starts at curr
curr = nxt # 4. move into the untouched part
return prev # prev is the new head
def build(vals: list[int]) -> Node | None:
dummy = Node(0) # a dummy head avoids special-casing the first append
tail = dummy
for v in vals:
tail.next = Node(v)
tail = tail.next
return dummy.next
def to_list(head: Node | None) -> list[int]:
out: list[int] = []
while head:
out.append(head.val)
head = head.next
return out
if __name__ == "__main__":
assert to_list(reverse(build([1, 2, 3, 4, 5]))) == [5, 4, 3, 2, 1]
assert to_list(reverse(build([7]))) == [7]
assert reverse(None) is None
print("ok")
  1. Create two sentinels, head and tail, linked to each other. The real nodes live between them.
  2. Insert node after a: with b = a.next, first set the new node’s own pointers (node.prev = a, node.next = b), then the neighbours’ (a.next = node, b.prev = node).
  3. Unlink node: node.prev.next = node.next and node.next.prev = node.prev.
  4. Optionally reset the removed node’s pointers so a stale reference can’t walk back into the list.
  5. Front is “insert after head”, back is “insert after tail.prev”; empty means head.next == tail.
insert_after / unlink.py
from __future__ import annotations
class DNode:
__slots__ = ("val", "prev", "next")
def __init__(self, val: int = 0) -> None:
self.val = val
self.prev: DNode = self # sentinels start pointing at themselves
self.next: DNode = self
def make_list() -> tuple[DNode, DNode]:
head, tail = DNode(), DNode() # 1. two sentinels that never hold data
head.next, tail.prev = tail, head
return head, tail
def insert_after(a: DNode, node: DNode) -> None:
b = a.next
node.prev, node.next = a, b # 2. set the new node's own links first
a.next = b.prev = node # 3. then point both neighbours at it
def unlink(node: DNode) -> None:
node.prev.next = node.next # 4. neighbours skip over the node
node.next.prev = node.prev
node.prev = node.next = node # 5. detach, so stale references can't wander back in
def values(head: DNode, tail: DNode) -> list[int]:
out, n = [], head.next
while n is not tail:
out.append(n.val)
n = n.next
return out
if __name__ == "__main__":
head, tail = make_list()
a, b, c = DNode(1), DNode(2), DNode(3)
insert_after(head, a) # front
insert_after(a, c)
insert_after(a, b) # middle
assert values(head, tail) == [1, 2, 3]
unlink(b) # O(1): no search, we hold the node
assert values(head, tail) == [1, 3]
insert_after(tail.prev, b) # back
unlink(a)
unlink(c)
unlink(b)
assert values(head, tail) == [] and head.next is tail and tail.prev is head
print("ok")
Python TypeScript C#
built in none (deque is not a node list you can splice) none LinkedList<T> (doubly linked) with LinkedListNode<T>
O(1) ops on a node write your own Node class write your own AddAfter(node, x), AddBefore, Remove(node), AddFirst, RemoveLast
node class tips __slots__ or @dataclass(slots=True) to cut memory class with next: Node | null class (reference type); a record gives value equality you don’t want here
null-safety Optional[Node] / Node | None strict null checks, ! only when proven nullable reference types, ?.

In interviews you nearly always write your own node class; it’s three lines and shows you know the mechanics.

Operation Singly linked Doubly linked Notes
access / search O(n) O(n) Walk from the head.
insert at head O(1) O(1)
insert at tail O(1) with a tail pointer O(1)
insert after a held node O(1) O(1)
remove a held node O(n) (need its predecessor) O(1) The reason prev exists.
remove head O(1) O(1)
memory per item value + 1 pointer value + 2 pointers Plus allocator overhead per node.

Big-O hides the constant: following pointers means a likely cache miss per node, while an array scan streams through memory. A linked list that’s “O(n), same as an array” can be several times slower to traverse.

  • Fast and slow pointers. Move one pointer 2 steps, another 1 step. They meet if there’s a cycle (Floyd’s algorithm); when fast hits the end, slow is at the middle. See two pointers.
  • Dummy head for building. Start a result list at a dummy node and tail.next = … repeatedly; return dummy.next. Removes the “first element” special case when merging or filtering.
  • Circular lists. The last node points back to the first. A single sentinel that is both head and tail gives a circular doubly linked list (that’s how .NET’s LinkedList<T> works inside).
  • Hash map + doubly linked list. O(1) lookup plus O(1) reorder: the standard shape behind recency-ordered caches and ordered dictionaries.
  • Skip lists. Linked lists with express lanes give O(log n) search; Redis sorted sets use them. See sorted containers.
  • Intrusive / index-based lists. Store next/prev as integer indices into arrays (no per-node allocation). Common in performance-sensitive code and in hash table internals.
  • Losing the rest of the list by overwriting next before saving it.
  • Null dereferences at the ends: node.next.next when node.next is null. Sentinels fix most of these; for singly linked code, check fast and fast.next.
  • Forgetting the back pointer. Updating next but not prev (or vice versa) leaves the list consistent in one direction only. Test by walking both ways.
  • Off-by-one in “k-th from the end”. Draw a 3-node example and count.
  • Recursion on long lists. Recursive reverse or traversal uses O(n) call stack; Python fails around 1,000 nodes by default.
  • Comparing nodes by value when you mean identity. Use is (Python), === (TS), ReferenceEquals or a class without overridden equality (C#).
  • Draw it. Three boxes and arrows on the whiteboard prevent most pointer bugs.
  • Say whether you use sentinels and why: “Dummy head and tail so insert and remove never branch on empty or end-of-list.”
  • Narrate each write in order: “Save next, flip, advance.” Interviewers watch the order.
  • Test the edges explicitly: empty list, one node, two nodes, operation at the head, operation at the tail.
  • Follow-ups: “do it in O(1) space” (no auxiliary array), “do it without recursion”, “make it thread-safe” (a lock around the splice; the four writes must appear atomic).
Check yourself
pick one
You hold a reference to a node in a singly linked list and must delete it in O(1). The node is not the tail. What's the classic trick?
Check yourself
pick one
In a doubly linked list with head and tail sentinels, how many pointer writes does removing a node take (excluding optional cleanup)?
Check yourself
pick one
A cache keeps its entries ordered by last use, and a hash map hands you any entry's node. Why is that list doubly linked rather than singly linked?

Classic textbook exercises, not gym questions:

  • Detect a cycle and return the node where it starts (Floyd).
  • Remove the k-th node from the end in one pass.
  • Merge two sorted linked lists using a dummy head.
  • Reverse the nodes between positions m and n in one pass.
  • CLRS, §10.2 “Linked lists”: sentinels and the LIST-DELETE / LIST-INSERT procedures.
  • Sedgewick and Wayne, Algorithms, §1.3 “Linked lists”.
  • Microsoft Learn, LinkedList<T> (O(1) node operations, circular implementation).
  • Bjarne Stroustrup, “Why you should avoid linked lists” (GoingNative 2012 keynote): why arrays usually win in practice.