skip to content

questions

4

In a singly-linked list, why is appending at the end O(n), and what makes it O(1)?

level: juniorimportance: must knowfreq 72%

answer

  1. Separate rewiring cost from reaching cost
  2. What must you walk to find?
  3. Which end does the handle name?
  4. In a ring, last.next is first
  5. Delete-last still needs the predecessor

basics

~20 s

Appending is O(n) when only a head reference is kept, because the last node is reachable only by walking the whole chain. Storing a tail reference alongside the head makes append O(1) in any of the three variants.

solid answer

~40 s

The insert itself is always constant work — allocate a node and rewire two references. What costs O(n) is *reaching* the insertion point. A list that stores only its head must walk every node to find the last one, so append is O(n). Keep a tail reference and append drops to O(1), for singly and doubly alike. A circular singly-linked list gets a bonus here: store only the last node, and since its `next` is the first node, insert-at-front and insert-at-end are both O(1) from that single reference. The asymmetry shows up on removal — deleting the last node needs its predecessor, which is still O(n) in a singly or circular-singly list even with a tail reference, and O(1) only in a doubly-linked list.

go deeper

for a junior

Recall the two separate costs: rewiring references is constant, but reaching the position may be linear. Know that a stored tail reference is what turns append from O(n) into O(1).

for a middle

Explain the whole grid — append and delete-last for singly, doubly and circular, with and without a tail reference — and why delete-last is the operation where singly and doubly actually diverge.

for a senior

Show that you know the maintenance burden a tail reference adds: every path that changes the final node must update it, and a stale tail fails silently by dropping appended data.

for a principal

Own the framing that a linked list's cost profile is chosen by which references you agree to maintain, and that each stored handle is a correctness obligation on every mutation path, not just a speedup.

## The claim to be careful with "Linked lists have O(1) insertion" is the sentence that gets candidates into trouble. The accurate version is: **insertion is O(1) once you already hold a reference to the node you are inserting next to.** Splicing a node in means writing two or three reference fields, which is constant work regardless of list length. Everything expensive about a linked list is *navigation*, and each variant differs exactly in what it lets you navigate to cheaply. ## The three variants and what they store - **Singly-linked:** each node holds a value and a `next` reference. The final node's `next` is NIL. The list handle usually holds `head`. - **Doubly-linked:** each node holds `prev` as well as `next`. The first node's `prev` and the last node's `next` are NIL. - **Circular:** the last node's `next` points back to the first, so there is no NIL terminator at all. A circular list can be singly or doubly linked (in a circular doubly-linked list the first node's `prev` points at the last). ## Filling in the tail-insert grid A common whiteboard exercise is to fill in the cost of "add a node at the end" for each variant, with and without a stored tail reference: | Variant | Handle stored | Append at end | Delete last node | |---|---|---|---| | Singly | head only | O(n) — walk to the last node | O(n) — need its predecessor | | Singly | head + tail | O(1) | O(n) — tail gives the node, not its predecessor | | Doubly | head only | O(n) — walk to the last node | O(n) — walk to it first | | Doubly | head + tail | O(1) | O(1) — `tail.prev` is right there | | Circular singly | last node only | O(1) — and insert-at-front is O(1) too | O(n) — predecessor unknown | | Circular doubly | any node | O(1) | O(1) | Two results in that grid are the ones interviewers actually probe. **First: a tail reference fixes append but not delete-last in a singly-linked list.** Removing the final node means writing NIL into the *previous* node's `next` field, and the previous node is exactly what a singly-linked list cannot name. You hold the victim, not the node that points at it, so you walk from the head to find it. Candidates who say "we have a tail pointer, so both ends are O(1)" have generalised one operation into two. **Second: a circular singly-linked list gets both ends from one stored reference.** If the handle is the *last* node, then `last.next` is the first node. Push-at-front is: allocate, point the new node at `last.next`, set `last.next` to the new node — done, O(1), and `last` is unchanged. Push-at-back is the same three writes plus advancing `last` to the new node. One field, two constant-time ends. That is why ring buffers of nodes — a turn order for a four-player board game, say, where play advances forever and a player can join between rounds — are often built circular rather than as a list plus a wrap-around index. Storing the *first* node instead would be the worse choice: you would be back to walking the ring to reach the end. ## Why the walk is genuinely linear There is no shortcut to node k. Nodes live wherever they were allocated, and the only way from one to the next is to read a reference field and follow it. Nothing about knowing the *length* helps: storing a count tells you how many hops to make, not how to skip them. This is the structural difference from a contiguous array, where the address of element k is arithmetic on the base address. ## The bookkeeping a tail reference imposes A stored tail is not free in maintenance terms. Every operation that can change which node is last must update it: append, delete-last, deleting from a one-element list (both head and tail become NIL), inserting into an empty list (head and tail become the same node), and clearing. A stale tail reference is one of the classic linked-list bugs, and it fails silently — appends land after a node that is no longer in the list, so the new data simply disappears from every traversal. ## What to say in an interview Separate the two costs out loud: rewiring is O(1), reaching is O(1) or O(n) depending on which references the list keeps. Then state which variant gives you which end cheaply, and flag that delete-last is the operation where singly and doubly genuinely diverge, because it needs a step backwards that only `prev` provides.

  • If we keep a tail reference, is deleting the last node O(1) too?
    Only in a doubly-linked list, where `tail.prev` names the new last node directly. In a singly-linked list the tail reference hands you the victim but not the node whose `next` must be set to NIL, so you still walk from the head to find the predecessor — O(n).
  • A circular singly-linked list stores one reference. Which node should it be, and why?
    The last node. From it you reach the first in one hop through `next`, so insert-at-front and insert-at-end are both O(1). If you store the first node instead, appending means walking the whole ring to find the node that closes it, which puts you back at O(n).
  • What breaks if the tail reference is not maintained correctly?
    Appends silently land on a node that is no longer reachable from the head, so the data vanishes from every traversal without an error. The cases usually missed are inserting into an empty list, deleting the only node, and deleting the last node — each changes which node is last.

A tail reference is a bookmark on the last page: without it, finding the end means turning every page. In a circular list the bookmark does double duty — the page after the last one is the first.

saying these in an interview costs you the question

  • Says all linked-list insertions are O(1) regardless of position
  • Claims a tail reference makes delete-last O(1) in a singly list
  • Thinks appending is O(n) because existing nodes are shifted or copied
  • Cannot say what the last node of a circular list points to
  • Believes storing the length removes the traversal cost

context

open as a page

In a doubly-linked list, why is deleting a node you already hold O(1) but O(n) in a singly-linked list?

level: middleimportance: must knowfreq 78%

basics

~20 s

Unlinking a node means rewriting its predecessor's next reference. A doubly-linked node stores prev, so both neighbours are one hop away — O(1). A singly-linked node cannot look backwards, so its predecessor must be found by walking from the head.

open as a page

In a circular singly-linked list, why does walking until a NIL next reference never terminate?

level: middleimportance: should knowfreq 52%

basics

~20 s

A circular list has no NIL next reference: the last node points back to the first, so the usual end test never fires. Stop by node identity instead — advance first, then halt on returning to the node you started from.

open as a page

Is the prev pointer worth its memory across 10 million nodes, and what do you lose without it?

level: principalimportance: should knowfreq 38%

basics

~20 s

Keep prev only for backward movement or O(1) unlink at externally held nodes. At ten million nodes it costs a machine word each — tens of megabytes. If deletes happen during a traversal you already run, a trailing reference does the same for free.

open as a page