skip to content

Given only a node in a singly linked list and no head, how do you remove it and when does that fail?

level: middleimportance: should knowfreq 58%

answer

  1. you cannot reach backwards, so stop trying
  2. the sequence of payloads is what is observable
  3. borrow from the node in front
  4. which object actually leaves the list?
  5. now ask what the last node borrows from

basics

~20 s

Copy the successor's payload into the node you hold, then splice the successor out. The list shrinks correctly, but the object unlinked is the successor. It fails on the last node, which has no successor to borrow from.

solid answer

~50 s

Since you cannot reach the predecessor, you stop trying to remove *that node* and instead remove *that position*. Copy the successor's payload into the node you hold, then set `node.next = node.next.next`. The list is now one shorter and holds exactly the right sequence of payloads; the object physically unlinked is the successor, not the node you were handed. It fails on the last node: there is no successor to copy from, and without the predecessor you cannot detach it — so the position genuinely cannot be removed with only that reference. It also breaks anyone else holding the successor node, since their reference now names an orphan while its payload lives elsewhere. In a job scheduler where a cancellation handler holds only its own node, the honest fallback for the last-node case is a dead flag on the node that the next traversal reaps.

go deeper

for a junior

Know that a forward-only node cannot reach its predecessor, and that the workaround shifts the next node's payload backwards. Be able to state the two writes it performs.

for a middle

Explain which object is actually spliced out, why the sequence of payloads still comes out right, and why the last node has nothing to borrow from. Volunteer the failure before being asked.

for a senior

Show that you weigh node identity: if nodes are handed out as cancellation handles or held by live cursors, the copy trick corrupts silently, and a dead-flag plus reaping pass is the safer design.

for a principal

Own the tradeoff between an always-O(1) cancellation path and the memory and traversal drag of unreaped dead nodes. Set the reaping policy explicitly rather than letting it emerge from whoever traverses next.

### The constraint A cancellation handler in a scheduler is often handed exactly one thing: the run-list node for the job being cancelled. No head reference, no predecessor, no index. The instinct is that this is impossible in a singly linked list — the removal is a write into the predecessor, and the predecessor is unreachable from a forward-only node. That instinct is correct about the *node* and wrong about the *problem*. ### Reframing: remove the position, not the object A list is a sequence of payloads. Nobody outside the implementation can tell which physical node carries which payload — only the order of payloads is observable. So instead of detaching the node you hold, shift its successor's payload backwards into it and detach the successor: ``` node.payload = node.next.payload node.next = node.next.next ``` Two writes, O(1), no predecessor consulted. The chain is one node shorter and the sequence of payloads is exactly the sequence with the target's payload removed. The node object you were handed is still linked in — it now impersonates its former successor — and the object that actually left the list is the successor. ### Where it breaks: the last node If the node you hold is the last one, `node.next` is nil. There is no payload to shift backwards, and detaching the node itself still requires the predecessor you do not have. This is not a coding oversight to patch; it is an information-theoretic limit of the setup. With a forward-only chain and a single node reference, the last position cannot be removed. Interviewers ask the copy trick almost entirely to get to this follow-up, so lead with it rather than waiting to be caught. The usual attempted patch — setting the node's own forward link to nil — does nothing, because the node was already the last one and its predecessor still points at it. Marking the payload as empty is closer but changes the observable sequence's length only if every reader agrees to skip empties, which is exactly the fallback below made explicit. ### The second failure: node identity The trick assumes no one else cares which physical node holds which payload. That assumption fails whenever node identity is externally visible: - Another cursor parked on the successor is now sitting on a node that is no longer in the list, and it will walk off into whatever the orphan still points at. - A registry mapping job identifiers to nodes — the very thing that let the handler hold a node in the first place — now maps the successor's identifier to a node that has left the list, while the successor's payload lives in a different node. - Any code holding the node as a stable handle for the job sees its payload change underneath it. In a scheduler that hands out node references as cancellation handles, this second problem is usually more damaging than the last-node problem, because it corrupts silently rather than failing loudly. Say this out loud; it separates candidates who memorised the trick from candidates who understand what it costs. ### The honest fallback: mark dead and reap When node identity matters, or the last position must be removable, the production answer is to stop removing eagerly. Give each node a dead flag; cancellation writes the flag, which is unconditionally O(1) at any position, including the last, and never touches a link. Traversals skip flagged nodes, and a pass that is already walking the list — the scheduler's own dispatch sweep — unlinks them properly using the predecessor it is carrying anyway. That choice is not free, and the interviewer will push on it: dead nodes occupy memory and are still visited by every traversal until reaped, so the design needs a policy answering *who reaps and how often*. Without one, a cancel-heavy workload grows the list without bound and turns every dispatch sweep into a walk over mostly-dead entries. The compensating benefit is a genuinely constant-time, always-correct cancellation path, which is usually the latency the scheduler actually cares about. ### Summary of the three options | Approach | Cancel cost | Works on last node | Preserves node identity | | --- | --- | --- | --- | | Payload copy + splice successor | O(1) | No | No | | Search for predecessor from head | O(n) | Yes | Yes | | Mark dead, reap in a later pass | O(1) | Yes | Yes | The copy trick is a genuine, correct technique with a sharply defined domain. Knowing its two failure modes — the last position and observable node identity — is what the question is actually testing.

  • Why is the last node the exception?
    The trick works by shifting a successor's payload backwards, and the last node has no successor to shift from. Detaching it directly needs its predecessor, which a forward-only reference cannot reach. So the limit is informational, not a coding gap: with one node reference in a singly linked chain, that position is unremovable. Interviewers ask the trick mainly to reach this case.
  • Who else can be surprised by the payload copy?
    Anyone holding the successor node. A cursor mid-traversal is now standing on an orphan and will walk off the live list; a registry mapping identifiers to nodes now points that identifier at a detached node while its payload lives elsewhere. The trick is only safe when node identity is invisible outside the list implementation, which is rarely true when nodes are handed out as handles.
  • Does the list's length come out right?
    Yes. Exactly one node is spliced out — the successor — so the length drops by one, and the surviving sequence of payloads is the original sequence minus the target's. Length and ordering are both correct; only the mapping from payloads to physical node objects has shifted, which is precisely the thing external holders can observe.
  • What would you do in a scheduler where cancellation must be O(1) at any position?
    Write a dead flag on the node instead of unlinking it. Marking is a single field write, correct at every position including the last, and it leaves node identity intact. The cost is deferred: dead nodes hold memory and are still walked until some later pass reaps them, so the design must name who reaps and how often or the list grows without bound.

You cannot fire someone whose desk you cannot find in the org chart. So you hand them their neighbour's job and dismiss the neighbour instead: the roster is right, one person left. It works until the person you hold is the last one on the roster.

saying these in an interview costs you the question

  • Claiming any node can be removed in O(1) given the node
  • Presenting the copy trick with no mention of the last node
  • Setting the node's own forward link to nil to remove it
  • Assuming nobody else holds a reference to the successor
  • Believing the node you were handed is the one that leaves

context