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.
Language Verdict: Pros, Cons & Recommendation
- 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
- 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
- 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
- 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
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.
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.
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.
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.
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.)
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.
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.
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.
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.