Sparround

Doubly and Circular Linked List

A doubly linked list stores a prev reference in each node in addition to next. For the price of one extra pointer you gain two important capabilities:

  • Bidirectional traversal — forwards and backwards; reading the list in reverse is O(n) and needs no extra structure
  • `O(1)` deletion when you hold the node — since prev is known, node.prev.next = node.next; node.next.prev = node.prev is all it takes. In a singly list, finding the previous node was O(n).

The cost: an extra pointer per node (memory) and keeping two references in sync on every operation (easier to get wrong).

Doubly linked lists almost always keep both a head and a tail, which means O(1) insertion and removal at both ends — so the structure naturally behaves as a deque.

A circular linked list is the variant where the last node points back to the head instead of null. In a doubly circular list head.prev also points at the tail — so "start" and "end" become purely conventional.

Advantages:

  • Endless cycling comes for free — round-robin, carousels, a playlist's repeat mode
  • Fewer edge casesnull checks nearly disappear, since every node always has a next and a prev
  • A single current pointer lets you work at both ends

The main hazard: traversal never stops. A naive while (cur != null) becomes an infinite loop. The stop condition must always be "have we come back to the start?": do { ... } while (cur !== start).

A widely used companion technique is the sentinel (dummy) node: one node holding no real data plays both head and tail. An empty list is then simply sentinel.next === sentinel, so all the null special cases vanish. Most LRU cache implementations are built exactly this way.

OperationSinglyDoublyComment
Insert/remove at headO(1)O(1)Cheap in both
Insert at tail (with a tail pointer)O(1)O(1)
Remove from tailO(n)O(1)Doubly wins because it has `prev`
Delete a given nodeO(n)O(1)This is the whole basis of an LRU cache
Backward traversalNot possible (or O(n) extra work)O(n)Essential for browser history
Memory (per node)1 pointer2 pointersDoubly costs roughly 1.5-2x metadata
Risk of bugsLowerHigher — two references must stay in syncForgetting `prev` on deletion is the classic bug

Real-world uses — knowing where these structures actually run counts for more in an interview than theory.

  • LRU cache — the canonical example. A HashMap maps a key straight to a node (O(1) lookup) while a doubly linked list keeps the usage order: on every access the node is unlinked in O(1) and moved to the front, and when capacity is full the last node is evicted. Every operation is O(1), and you cannot do this with an array.
  • Browser history / undo-redo — moving back and forward is exactly prev/next. Opening a new page truncates the chain after current.
  • Round-robin schedulingcurrent = current.next over a circular list; switching to the next process is O(1) and there is no notion of an end.
  • Music/media playlists — the next/previous buttons and repeat mode.
  • Editor cursors, turn order in games, Josephus-style problems.

Note: most of these come ready-made in standard libraries (Java LinkedList/LinkedHashMap, Kotlin ArrayDeque, Dart Queue/LinkedList), but in an interview you are expected to write them by hand.

Interview tip. "Implement an LRU cache" is the most common design question at mid-level interviews, and the right answer is always the same: a hash map plus a doubly linked list. Structure it like this: (1) the requirement — get and put must be O(1); (2) why one structure isn't enough — a map keeps no order, a list gives no lookup; (3) the combination — the map points at nodes, the list keeps the order; (4) sentinel nodes to remove the edge cases.

Another classic: "What does a doubly linked list buy you over a singly one, and what does it cost?" — always give both halves: you gain O(1) deletion and backward traversal, you pay an extra pointer per node and the obligation to keep two references in sync.

The most common mistakes: updating only next on deletion and forgetting prev (the list stays correct in one direction and corrupt in the other — one of the hardest bugs to find); writing a circular traversal whose stop condition tests for null, producing an infinite loop; and counting the sentinel as an element so size is off by one.

📚 Sources and documentation

  • java.util.LinkedListofficialdocs.oracle.com

    The standard library doubly-linked list, with its documented operation costs.