LINEAR DATA STRUCTURES / ALGORITHM BRIEF

Doubly linked list

A doubly linked list gives every node two pointers: next and prev.

IntermediatePhase 02 / Topic 2 of 8Mental modelComplexityEdge cases
01

Overview

A doubly linked list gives every node two pointers: next and prev. You can walk in both directions, and, most importantly, remove any node in O(1) when you hold a reference to it, because the node already knows its predecessor.

This property makes the doubly linked list the backbone of LRU caches, browser history, and deques. Paired with a hash map from key to node, it supports O(1) lookup, O(1) move-to-front, and O(1) eviction of the oldest item.

A train with couplings on both ends

Every carriage is connected to the one in front and the one behind. To remove a carriage from the middle, you connect its neighbors to each other directly; you never need to walk from the engine to find who is in front.

02

When to use it

  • You need O(1) removal of arbitrary nodes that you can locate quickly (usually through a hash map).
  • You need to traverse both forward and backward.
  • You maintain recency or order under frequent moves: LRU / LFU caches.
  • You implement a deque or a text editor buffer.
03

Problem patterns it solves

Hash map + doubly linked list

Recognize it when: O(1) get and put with recency or frequency eviction.

  • 146. LRU Cache
  • 460. LFU Cache
  • 432. All O`one Data Structure
Bidirectional navigation

Recognize it when: back / forward history, move cursor left and right.

  • 1472. Design Browser History
  • 2296. Design a Text Editor
Flatten and reconnect

Recognize it when: nodes with child pointers or multilevel lists.

  • 430. Flatten a Multilevel Doubly Linked List
  • 426. Convert BST to Sorted Doubly Linked List
Deque implementation

Recognize it when: insert and delete at both ends in O(1).

  • 641. Design Circular Deque
  • 707. Design Linked List
04

Where it is used in real software

LRU caches

Redis approximates LRU eviction, and Java's LinkedHashMap with accessOrder = true is a hash map threaded through a doubly linked list, which is exactly an LRU cache.

Browser history

Back and forward buttons move along a doubly linked sequence of visited pages.

Operating system schedulers

The Linux kernel's list_head is a circular doubly linked list used for run queues and many other kernel lists.

Music playlists

Previous and next track navigation, and moving a song within a queue, map naturally to prev and next pointers.

05

Key terms

prev / next
Pointers to the previous and next nodes.
Sentinel head and tail
Dummy nodes at both ends so every real node always has non-null neighbors.
Unlink
node.prev.next = node.next; node.next.prev = node.prev.
Move to front
Unlink a node and insert it right after the head sentinel.
06

LRU cache operations

  1. 1
    Structure

    A Map from key to node, and a doubly linked list with sentinel head (most recent side) and tail (least recent side).

  2. 2
    get(key)

    Look up the node in the map. If found, move it to the front and return its value. O(1).

  3. 3
    put(key, value), existing key

    Update the value and move the node to the front.

  4. 4
    put(key, value), new key

    Create a node, insert it at the front, and add it to the map.

  5. 5
    Evict when over capacity

    Remove the node before the tail sentinel (least recently used) from both the list and the map.

07

LRU cache with capacity 2

Front = most recent, back = least recent

Step 1 / 6
OperationResultList (front to back)Map keys
put(1, A)-1{1}
put(2, B)-2, 1{1, 2}
get(1)A1, 2{1, 2}
put(3, C)evict 23, 1{1, 3}
get(2)-13, 1{1, 3}
put(1, A2)update1, 3{1, 3}

NOWOperation: put(1, A) | Result: - | List (front to back): 1 | Map keys: {1}

Every operation touches a constant number of pointers and one hash map entry, so each is O(1).

08

Implementation

class Node {  constructor(key, value) {    this.key = key;    this.value = value;    this.prev = null;    this.next = null;  }} class LRUCache {  constructor(capacity) {    this.capacity = capacity;    this.map = new Map();    this.head = new Node(); // most recent side    this.tail = new Node(); // least recent side    this.head.next = this.tail;    this.tail.prev = this.head;  }   #unlink(node) {    node.prev.next = node.next;    node.next.prev = node.prev;  }   #addToFront(node) {    node.next = this.head.next;    node.prev = this.head;    this.head.next.prev = node;    this.head.next = node;  }   get(key) {    const node = this.map.get(key);    if (!node) return -1;    this.#unlink(node);    this.#addToFront(node);    return node.value;  }   put(key, value) {    if (this.map.has(key)) {      const node = this.map.get(key);      node.value = value;      this.#unlink(node);      this.#addToFront(node);      return;    }    const node = new Node(key, value);    this.map.set(key, node);    this.#addToFront(node);    if (this.map.size > this.capacity) {      const lru = this.tail.prev;      this.#unlink(lru);      this.map.delete(lru.key);    }  }}
09

Complexity and performance

Remove known nodeO(1)

Both neighbors are directly reachable.

Insert at either endO(1)

With head and tail sentinels.

Search by valueO(n)

Needs a hash map for O(1) lookup.

Memory per node2 pointers

Plus the value (and key for caches).

10

Trade-offs

Extra memory

Two pointers per node instead of one. For millions of small entries, this overhead is noticeable.

More pointers to keep consistent

Every insert and delete updates four pointers; forgetting one silently corrupts the list.

Built-ins

Java's LinkedHashMap already implements the LRU structure; in interviews you are usually asked to build it yourself.

11

Variants and related techniques

LFU cache

One doubly linked list per frequency, plus a map from key to node and a pointer to the minimum frequency.

Circular doubly linked list

A single sentinel whose next is the head and prev is the tail; used in the Linux kernel.

XOR linked list

Stores prev XOR next in one field to save memory; mostly a curiosity.

12

Common mistakes

  • Updating pointers in the wrong order.

    Fix: When inserting, set the new node's prev and next first, then update the neighbors.

  • Forgetting to delete the evicted key from the map.

    Fix: Store the key inside the node so you can remove it from the map during eviction.

  • Null checks everywhere.

    Fix: Use head and tail sentinels so neighbors always exist.

13

Interview questions

Why does an LRU cache need a doubly linked list rather than a singly linked one?

To move or remove an arbitrary node in O(1), you need its predecessor. A singly linked list would require an O(n) walk to find it.

Why not just use an array with timestamps?

Finding the least recently used item would take O(n) per eviction. The linked list keeps items in recency order so eviction is O(1).

14

Practice problems

ProblemDifficultyWhat it trains
707. Design Linked ListMediumFull implementation with sentinels.
146. LRU CacheMediumHash map plus doubly linked list.
1472. Design Browser HistoryMediumBidirectional navigation.
430. Flatten a Multilevel Doubly Linked ListMediumPointer splicing.
460. LFU CacheHardFrequency buckets.