7. Linked Lists

Hand-rolling node classes and pointer-rewiring idioms, plus the identity-comparison and recursion-depth pitfalls that trip up traversal.

A linked list is just a chain of manually-wired object references — the interview value lives entirely in how precisely you declare nodes, rewire pointers, and guard traversal loops, not in the concept of a list itself. This section drills the exact node idioms, sentinel patterns, and loop-guard syntax each language expects, assuming you already know why the classic techniques (reversal, fast/slow pointers, cycle detection) work.

See roadmap: Linked List

Language Verdict: Pros, Cons & Recommendation

Python

3/5
  • Tuple assignment (prev, curr = curr, nxt) makes the reversal pointer dance a single, safe statement — no risk of ordering bugs
  • is gives an unambiguous, fast identity check for cycle/'same node' detection, distinct from value-based ==
  • collections.deque is available if a problem needs O(1) both-ends operations on a node sequence, not just node-by-node mutation
  • Shallowest default recursion limit (~1000 frames) of the four — recursive traversal/reversal on a long list is most likely to blow up first
  • @dataclass-based nodes generate value-based __eq__ by default, a trap if you then rely on default equality for identity-style checks
  • No stdlib singly-linked, node-mutable list type — every interview problem still means hand-rolling ListNode from scratch

Java

4/5
  • No implicit truthiness for object references — fast != null && fast.next != null can't silently misfire the way a truthy check could
  • == on references is unambiguous identity comparison, with no operator-overloading or coercion surprises for cycle checks
  • Real LinkedList/Deque implementations exist in the stdlib as a reference, even though interview code still hand-rolls raw nodes
  • Deep, memory-bounded call stack headroom makes recursive traversal safe well past typical interview-sized lists
  • No tuple/multi-assignment sugar — the reversal pointer dance needs two ordered statements (prev = curr; then curr = nxt;), and getting the order wrong is the most common reversal bug
  • record-based nodes are immutable, so they don't actually fit mutable pointer-rewiring — you're stuck with a verbose plain class

Go

4/5
  • A two-field struct plus pointers is the natural encoding — no Optional, no extra class ceremony
  • Multiple assignment (prev, curr = curr, nxt) makes the reversal dance as safe as Python's tuple swap
  • container/list exists if you need a real stdlib deque of nodes, though interviews still roll ListNode
  • No ~1000-frame recursion ceiling — recursive traversal is fine on typical interview lists
  • Nil pointer dereference panics with no exceptions to catch — every curr.Next needs a nil check first
  • container/list uses untyped any values and *list.Element — awkward for LeetCode-style ListNode problems
  • No truthiness on pointers: if curr does not compile; you must write if curr != nil

JavaScript

3/5
  • No class ceremony required — a plain object literal ({ val, next: null }) works identically to a class for mechanical purposes
  • Default parameters (next = null) and reference-rebinding semantics need zero extra syntax to get right
  • Deep, memory-bounded call stack (comparable to Java) makes recursive traversal safe on realistic interview-sized lists
  • No tuple-assignment sugar — the same two-statement ordering risk in the reversal dance as Java
  • fast && fast.next truthy-guard is easy to confuse with fast?.next, which returns undefined instead of stopping the loop — a subtly different bug
  • No stdlib deque/linked structure at all to fall back on for node-sequence work beyond the plain Array
Recommendation: The mechanics are structurally identical across all four, so this comes down to discipline, not tooling: watch Python's shallow recursion limit on long lists; in Java/JS, don't let missing tuple assignment scramble pointer-dance ordering (Go *does* have multiple assignment). Python remains the default DSA language; Go is a comfortable second for pointer-rewiring because nil checks are explicit and there is no recursion-limit surprise.

Coding Mechanics, Side by Side

Node Class Idiom

Must-know
class ListNode: def __init__(self, val, next=None): self.val = val self.next = next # @dataclass alternative — less common for mutable list nodes from dataclasses import dataclass @dataclass class Node: val: int next: 'Node | None' = None

The plain __init__ form is the interview convention (it matches what LeetCode and most judges pre-define) — public, mutable attributes, no __slots__ or property ceremony. @dataclass autogenerates __init__/__repr__/__eq__, but the generated __eq__ compares by value, not identity — a subtle trap if you later rely on default equality for an identity-style check.

Dummy-Head (Sentinel) Pattern

Must-know

The sentinel node turns "delete the head" and "delete a middle node" into the exact same code path — no if node is head branch needed. This idiom is identical in structure across all four languages.

dummy = ListNode(0, head) prev = dummy while prev.next: if prev.next.val == target: prev.next = prev.next.next # unlink, no special-case for head else: prev = prev.next return dummy.next

Returning dummy.next (not dummy) at the end is the detail people forget. dummy's own val is never read; any placeholder works.

Reference Semantics When Relinking

Must-know
a.next = b # rebinds a's reference; does not copy b b.next = c # a -> b -> c now share the actual node objects, not copies

Assigning .next never deep-copies — it just rebinds a pointer, exactly like reassigning any variable. This is a direct consequence of Python's pass-by-object-reference model from Language Fundamentals: .next fields hold references to heap objects, not the objects themselves.

In-Place Reversal: The Pointer Dance (Mechanics Only)

Recommended
prev, curr = None, head while curr: nxt = curr.next # save before overwriting curr.next = prev prev, curr = curr, nxt head = prev

The three-variable dance (prev/curr/nxt) exists purely to avoid losing the rest of the list the instant curr.next is overwritten — always save nxt first. Python's tuple assignment (prev, curr = curr, nxt) lets both reassignments happen in one statement.

GC of Unlinked / Removed Nodes

Optional

None of the four require a manual free() call — the runtime always reclaims an unreachable node eventually. For an interview-sized list, the timing difference below is a non-issue; it only matters at scale.

prev.next = curr.next # curr is now unreachable from head # no explicit free/del needed

CPython uses reference counting — once curr's refcount drops to zero, it's deallocated immediately and deterministically, no GC pause required. (Cyclic garbage, e.g. from a hand-built circular list, needs the separate cyclic collector instead of refcounting alone.)

Iterative vs. Recursive Traversal: Recursion-Limit Risk

Must-know
def reverse_recursive(node, prev=None): if not node: return prev nxt = node.next node.next = prev return reverse_recursive(nxt, node)

Python's default recursion limit is ~1000 frames (sys.getrecursionlimit()); a recursive traversal/reversal over a few thousand nodes raises RecursionError well before Java or JS hit their limits. Default to the iterative version for linked-list problems in Python specifically — see Trees & Recursion Mechanics for the full recursion-limit discussion.

Fast/Slow Pointer Loop-Guard Syntax

Must-know
slow, fast = head, head while fast and fast.next: slow = slow.next fast = fast.next.next

fast and fast.next relies on Python truthiness — None is falsy, so the short-circuit and naturally guards fast.next from ever being accessed on a None fast. No is not None check needed here.

Node Identity vs. Value Comparison

Recommended
if slow is fast: # identity check — same object in memory return True # cycle detected

is compares identity; == compares value (calling __eq__, which for a plain ListNode without an override falls back to identity anyway). Use is explicitly for 'same node' checks like cycle detection, so the intent stays correct even if someone later adds __eq__ to the class.

Doubly Linked List: Double the Pointer Bookkeeping

Recommended

A doubly linked node just adds a prev field alongside next — structurally identical across Python, Java, Go, and JS (only the class/struct syntax differs, already shown above). The real mechanical difference shows up at insert/delete time: a singly linked list needs 2 pointer updates, a doubly linked one needs 4.

Inserting node between a and b (a -> b becomes a -> node -> b):

node.prev, node.next = a, b # 1, 2 — the new node's own pointers a.next = node # 3 — forward link from the left neighbor b.prev = node # 4 — back link from the right neighbor
node.Prev, node.Next = a, b // 1, 2 — Go multiple assignment works here too a.Next = node // 3 b.Prev = node // 4

Forgetting one of the four — most often the new node's own prev/next, or the far neighbor's back-pointer — is the most common doubly-linked-list bug, and it's identical in nature no matter which of the four languages you're writing it in. This is structural, not a language quirk.

Further Reading

  • dataclasses — Data ClassesPython official docs

    The @dataclass alternative to a hand-written __init__ for node classes, including how the generated __eq__ compares by value rather than identity.

  • 6.10. Comparisons (is vs ==)Python official docs

    The formal definition of `is` as identity comparison versus `==` as value comparison — the exact distinction you rely on for cycle/identity checks on nodes.

  • 15.21. Equality OperatorsOracle Java Language Specification

    The formal spec for == on reference types, confirming it's identity comparison for plain objects like list nodes that don't override equals().

  • Reference for the class syntax used for node definitions, including default parameter values (e.g. next = null) in constructors.

  • Confirms === on objects is reference comparison with no coercion — the operator to use for 'same node' checks like cycle detection.

  • The mechanism behind Python's ~1000-frame default recursion limit, which makes recursive linked-list traversal riskier in Python than in Java or JS.

  • container/list — doubly linked listGo official docs (pkg.go.dev)

    Stdlib doubly-linked list. Interview problems still define `type ListNode struct { Val int; Next *ListNode }` rather than using this package.