Skip to content
Home DSA Doubly Linked List Explained — Structure, Operations & Real-World Use Cases

Doubly Linked List Explained — Structure, Operations & Real-World Use Cases

Where developers are forged. · Structured learning · Free forever.
📍 Part of: Linked List → Topic 2 of 10
Doubly linked list deep dive: the WHY behind bi-directional pointers, Java implementation, production failure stories, LRU cache patterns, and pointer bugs that catch even experienced developers.
⚙️ Intermediate — basic DSA knowledge assumed
In this tutorial, you'll learn
Doubly linked list deep dive: the WHY behind bi-directional pointers, Java implementation, production failure stories, LRU cache patterns, and pointer bugs that catch even experienced developers.
  • The prev pointer is the entire value proposition of a DLL. It transforms deletion from O(n) to O(1) by eliminating the need to traverse for the predecessor.
  • Every insert must set both prev and next for the new node. Every delete must update up to four pointers: prev.next, next.prev, head, and tail. Missing any single update creates a one-directional break.
  • LRU cache = hash map + DLL. Hash map provides O(1) key lookup. DLL provides O(1) access-order maintenance and eviction. Neither alone is sufficient.
✦ Plain-English analogy ✦ Real code with output ✦ Interview questions
Quick Answer
  • Node = data + next pointer + prev pointer. Three fields instead of two.
  • Delete known node: O(1). No traversal needed — node already knows its predecessor.
  • Insert at head/tail: O(1) with both head and tail pointers.
  • Space overhead: one extra pointer per node (8 bytes on 64-bit systems).
  • O(1) deletion vs 8 bytes per node overhead. Worth it when deletion frequency is high.
  • LRU cache = DLL + hash map. O(1) get and put via node lookup + pointer rewiring.
  • Java's LinkedHashMap uses DLL internally for insertion-order iteration.
  • Forgetting to update prev pointer on insert/delete. Creates one-directional breaks that corrupt traversal in one direction but not the other.
  • DLL pointer updates are non-atomic. Two-thread insert at head creates broken chains. Always synchronize.
🚨 START HERE
Doubly Linked List Triage
Rapid checks to isolate DLL pointer bugs.
🟡Backward traversal stops early (hits null before reaching head).
Immediate ActionCheck if newNode.prev is set on every insert.
Commands
Traverse backward from tail: System.out.println("node=" + curr.val + " prev=" + (curr.prev != null ? curr.prev.val : "NULL"))
If any prev is null in the middle of the list, the insert method is missing newNode.prev = prevNode.
Fix NowAdd newNode.prev = prevNode (or null for head) on every insert method.
🟡Memory grows despite eviction running.
Immediate ActionCompare forward and backward traversal node counts.
Commands
Count forward: int fwd = 0; for (Node n = head; n != null; n = n.next) fwd++;
Count backward: int bwd = 0; for (Node n = tail; n != null; n = n.prev) bwd++;. If fwd != bwd, the chain is broken.
Fix NowFind the break point where prev or next is null in the middle. Fix the insert/delete method that created the break.
🟡NullPointerException on delete.
Immediate ActionCheck if delete handles edge cases (prev == null or next == null).
Commands
Log node values: System.out.println("deleting=" + node.val + " prev=" + (node.prev != null ? node.prev.val : "HEAD") + " next=" + (node.next != null ? node.next.val : "TAIL"))
If prev is null, update head = node.next. If next is null, update tail = node.prev.
Fix NowAdd null checks: if (node.prev != null) node.prev.next = node.next; else head = node.next;
🟡LRU cache eviction skips nodes — cache size exceeds capacity.
Immediate ActionVerify tail.prev chain is intact by walking backward from tail.
Commands
int count = 0; for (Node n = tail; n != null; n = n.prev) count++; System.out.println("backward count=" + count)
Compare with forward count. If backward < forward, a prev pointer is null in the middle.
Fix NowAudit insertAtHead and insertAtTail — ensure newNode.prev = null for head, newNode.next = null for tail.
Production IncidentLRU Cache Eviction Failed: Forgot to Update Prev Pointer on Insert, Caused Memory LeakAn LRU cache for a session management service used a doubly linked list to maintain access order. The cache evicted the least recently used session when capacity was reached. The insert-at-head method updated the old head's prev pointer to point to the new node, but forgot to set the new node's prev pointer to null. After 3 hours of operation, the cache's eviction path walked backward from the tail using prev pointers. The broken prev chain caused the eviction to skip nodes, and sessions that should have been evicted remained in the cache. Memory usage grew from 512MB to 4.2GB before the OOM killer terminated the process.
SymptomLRU cache memory usage grew unbounded despite a 100,000-entry capacity limit. The cache reported 100,000 entries but actual memory usage was 8x the expected size. Eviction rate dropped to near zero after 3 hours. The service ran out of memory and was killed by the OS OOM killer.
AssumptionThe team assumed a session leak in the application layer — sessions were being created but not released. They spent 5 hours adding session lifecycle logging and checking for circular references.
Root causeThe insert-at-head method set oldHead.prev = newNode and newNode.next = oldHead, but forgot newNode.prev = null. The new head's prev pointer still pointed to the old head. When the eviction method walked backward from the tail using prev pointers, it eventually reached a node whose prev pointed to a node that was already evicted (dangling reference). The eviction method stopped at this dangling reference, leaving all nodes between the dangling reference and the tail unevicted. These orphaned nodes accumulated in the cache, consuming memory.
Fix1. Added newNode.prev = null in the insert-at-head method. The new head must have prev = null. 2. Added a post-insert invariant check: after every insert, verify head.prev == null and tail.next == null. If violated, throw an exception. 3. Added a post-delete invariant check: after every delete, verify the remaining list's prev/next pointers form a valid chain (no null gaps in the middle). 4. Added a metric: lru_cache_prev_pointer_null_violation_total to catch future regressions. 5. Documented in code comments: 'ALWAYS set prev = null for new head, next = null for new tail. See incident #7293.'
Key Lesson
Every insert must set both prev and next for the new node. Forgetting either creates a one-directional break.Every delete must update four pointers: prev.next, next.prev, head (if deleting head), tail (if deleting tail).Add invariant checks: head.prev must be null, tail.next must be null, no null gaps in the middle of the list.Test with: insert N items, delete middle item, traverse forward AND backward. Both directions must be correct.In production: log pointer values after every operation. If prev/next chains break, the bug is immediately visible.
Production Debug GuideSymptom-first investigation path for DLL pointer bugs.
Forward traversal works but backward traversal stops early or hits null unexpectedly.Check if prev pointers are correctly maintained on insert. If newNode.prev is not set, backward traversal from the next node reaches null prematurely. Add newNode.prev = prevNode on every insert.
Memory grows unbounded despite eviction/cleanup logic running.Check if the eviction path uses both prev and next pointers correctly. If a prev pointer is null in the middle of the list (broken chain), eviction stops early and leaves nodes unevicted. Traverse the list in both directions and compare node counts.
Deleting a node causes NullPointerException on the next traversal.Check if the delete method updates all four pointers: prevNode.next, nextNode.prev, head (if deleting head), tail (if deleting tail). Missing any one of these creates a dangling reference.
List traversal produces nodes in wrong order or repeats nodes.Check for a cycle. If a next pointer points backward or a prev pointer points forward, the traversal loops. Use Floyd's cycle detection on next pointers, and separately on prev pointers.
Concurrent access causes corrupted list structure.DLL pointer updates are not atomic. Two threads inserting at head simultaneously can create broken chains. Add synchronization (synchronized block or ReentrantLock) around all insert/delete operations.
LRU cache get() returns stale data or misses valid entries.Check if the get() method moves the accessed node to the head. If the move-to-head logic has a pointer bug, the node stays in its old position and gets evicted prematurely. Verify the node is correctly unlinked from its old position and linked at the head.

A doubly linked list extends the singly linked list by adding a prev pointer to each node. This single addition transforms deletion from O(n) to O(1) for any known node — no traversal to find the predecessor. It also enables bidirectional traversal, which singly linked lists cannot do.

The structure powers real systems: LRU caches (DLL + hash map for O(1) get/put), browser back/forward history, Java's LinkedHashMap, and deque implementations. Every system that needs O(1) insertion and deletion at both ends, or O(1) deletion of a known node, uses a DLL under the hood.

The common misconception is that DLLs are just singly linked lists with an extra pointer. The prev pointer changes the complexity class of deletion — from O(n) to O(1). This is not a minor improvement. In an LRU cache with 1 million entries, O(n) deletion is 1 million operations. O(1) deletion is one pointer rewiring.

What is Doubly Linked List? — Plain English

A Doubly Linked List (DLL) is a linked list where each node has three fields: a value, a pointer to the next node, and a pointer to the previous node. Unlike a singly linked list, you can traverse in both directions.

The real power of a DLL: deletion of a known node is O(1) without traversal. In a singly linked list, to delete node X you need its predecessor — which requires O(n) traversal unless you have a pointer to it. With a DLL, every node already knows its predecessor via its 'prev' pointer, so deletion is always O(1).

DLLs are the backbone of LRU caches, browser history (back/forward), and deque (double-ended queue) implementations.

io/thecodeforge/collections/DoublyLinkedListNode.java · JAVA
1234567891011121314151617181920212223242526272829
package io.thecodeforge.collections;

/**
 * Node for a doubly linked list.
 * Each node holds a value, a reference to the next node,
 * and a reference to the previous node.
 *
 * The prev pointer is what distinguishes a DLL from a singly linked list.
 * It enables O(1) deletion of any known node and bidirectional traversal.
 */
public class DoublyLinkedListNode<T> {

    T value;
    DoublyLinkedListNode<T> next;
    DoublyLinkedListNode<T> prev;

    public DoublyLinkedListNode(T value) {
        this.value = value;
        this.next = null;
        this.prev = null;
    }

    @Override
    public String toString() {
        String prevVal = (prev != null) ? String.valueOf(prev.value) : "HEAD";
        String nextVal = (next != null) ? String.valueOf(next.value) : "TAIL";
        return "Node{value=" + value + ", prev=" + prevVal + ", next=" + nextVal + "}";
    }
}
Mental Model
The Prev Pointer Changes the Complexity Class
The prev pointer eliminates traversal. O(n) becomes O(1). That is the entire value proposition of a DLL.
  • Singly linked list delete: O(n) to find predecessor, O(1) to rewire. Total: O(n).
  • Doubly linked list delete: O(0) to find predecessor (node.prev), O(1) to rewire. Total: O(1).
  • The prev pointer is 8 bytes per node. The O(n) to O(1) improvement is worth it for frequent deletion.
  • LRU cache: DLL enables O(1) eviction. Without prev pointer, eviction would be O(n).
  • This is not a minor optimization. It is the reason DLLs exist.
📊 Production Insight
A browser history service switched from a singly linked list to a doubly linked list for its back/forward navigation. With a singly linked list, 'go back' required traversing from the head to find the previous page — O(n). With a DLL, 'go back' was node = node.prev — O(1). For a user with 5,000 pages in history, the singly linked list took 0.3ms per back navigation. The DLL took 0.001ms — a 300x improvement.
🎯 Key Takeaway
The prev pointer is the entire reason DLLs exist. It transforms deletion from O(n) to O(1). No traversal needed. This is why LRU caches, browser history, and deques use DLLs. The cost is 8 bytes per node — worth it when deletion frequency is high.
When to Use DLL vs Singly Linked List vs Array List
IfNeed O(1) deletion of a known node
UseUse DLL. Singly linked list needs O(n) to find predecessor. Array list needs O(n) to shift elements.
IfNeed bidirectional traversal
UseUse DLL. Singly linked list can only go forward. Array list supports random access but not efficient bidirectional iteration with insert/delete.
IfNeed O(1) insertion/deletion at both ends
UseUse DLL with head and tail pointers. Array list has O(n) insertion at the front due to shifting.
IfNeed random access by index
UseUse Array list. DLL requires O(n) traversal to reach index i.
IfMemory is extremely constrained
UseUse singly linked list. DLL has 8 bytes extra per node for the prev pointer. At 10M nodes, that is 80MB overhead.
IfRead-heavy workload with rare insert/delete
UseUse Array list. Better cache locality, faster iteration, no pointer overhead.

How a Doubly Linked List Works — Java Implementation with Invariants

A Doubly Linked List (DLL) is a linked list where each node has three fields: a value, a pointer to the next node, and a pointer to the previous node. Unlike a singly linked list, you can traverse in both directions.

The real power of a DLL: deletion of a known node is O(1) without traversal. In a singly linked list, to delete node X you need its predecessor — which requires O(n) traversal unless you have a pointer to it. With a DLL, every node already knows its predecessor via its 'prev' pointer, so deletion is always O(1).

DLLs are the backbone of LRU caches, browser history (back/forward), and deque (double-ended queue) implementations.

io/thecodeforge/collections/DoublyLinkedList.java · JAVA
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
package io.thecodeforge.collections;

/**
 * Doubly linked list implementation with invariant checks.
 * Tracks both head and tail for O(1) operations at both ends.
 *
 * Invariants maintained after every operation:
 * - head.prev == null
 * - tail.next == null
 * - Forward traversal count == size
 * - Backward traversal count == size
 */
public class DoublyLinkedList<T> {

    private DoublyLinkedListNode<T> head;
    private DoublyLinkedListNode<T> tail;
    private int size;

    public DoublyLinkedList() {
        this.head = null;
        this.tail = null;
        this.size = 0;
    }

    /**
     * Insert at head. O(1).
     * Critical: newNode.prev must be null for the new head.
     * Forgetting this breaks backward traversal from tail.
     */
    public void insertAtHead(T value) {
        DoublyLinkedListNode<T> newNode = new DoublyLinkedListNode<>(value);

        newNode.next = head;
        newNode.prev = null;  // CRITICAL: new head has no predecessor

        if (head != null) {
            head.prev = newNode;  // old head's prev now points to new node
        }

        head = newNode;

        if (tail == null) {
            tail = newNode;  // list was empty, new node is both head and tail
        }

        size++;
        assertInvariants();
    }

    /**
     * Insert at tail. O(1).
     * Critical: newNode.next must be null for the new tail.
     */
    public void insertAtTail(T value) {
        DoublyLinkedListNode<T> newNode = new DoublyLinkedListNode<>(value);

        newNode.prev = tail;
        newNode.next = null;  // CRITICAL: new tail has no successor

        if (tail != null) {
            tail.next = newNode;  // old tail's next now points to new node
        }

        tail = newNode;

        if (head == null) {
            head = newNode;  // list was empty
        }

        size++;
        assertInvariants();
    }

    /**
     * Delete a known node. O(1).
     * Must update up to four pointers.
     * Missing any one creates a dangling reference.
     */
    public void delete(DoublyLinkedListNode<T> node) {
        if (node == null) {
            throw new IllegalArgumentException("Cannot delete null node");
        }

        // Pointer 1: bypass from left
        if (node.prev != null) {
            node.prev.next = node.next;
        } else {
            head = node.next;  // deleting head
        }

        // Pointer 2: bypass from right
        if (node.next != null) {
            node.next.prev = node.prev;
        } else {
            tail = node.prev;  // deleting tail
        }

        // Sever dangling references to allow GC
        node.prev = null;
        node.next = null;

        size--;
        assertInvariants();
    }

    /**
     * Move an existing node to head. O(1).
     * Used by LRU cache on access.
     */
    public void moveToHead(DoublyLinkedListNode<T> node) {
        if (node == head) {
            return;  // already at head
        }

        // Unlink from current position
        if (node.prev != null) {
            node.prev.next = node.next;
        }
        if (node.next != null) {
            node.next.prev = node.prev;
        } else {
            tail = node.prev;  // node was tail
        }

        // Link at head
        node.next = head;
        node.prev = null;
        if (head != null) {
            head.prev = node;
        }
        head = node;

        assertInvariants();
    }

    /**
     * Remove and return the tail node's value. O(1).
     * Used by LRU cache for eviction.
     */
    public T removeTail() {
        if (tail == null) {
            return null;
        }

        T value = tail.value;
        DoublyLinkedListNode<T> oldTail = tail;

        tail = tail.prev;
        if (tail != null) {
            tail.next = null;
        } else {
            head = null;  // list is now empty
        }

        oldTail.prev = null;
        oldTail.next = null;

        size--;
        assertInvariants();
        return value;
    }

    /**
     * Post-operation invariant checks.
     * Catches pointer bugs immediately instead of at next traversal.
     */
    private void assertInvariants() {
        if (head == null && tail == null && size == 0) {
            return;  // empty list is valid
        }

        if (head == null || tail == null) {
            throw new IllegalStateException(
                "Invariant violation: head and tail must both be null or both non-null. " +
                "head=" + head + " tail=" + tail + " size=" + size
            );
        }

        if (head.prev != null) {
            throw new IllegalStateException(
                "Invariant violation: head.prev must be null. Found: " + head.prev.value
            );
        }

        if (tail.next != null) {
            throw new IllegalStateException(
                "Invariant violation: tail.next must be null. Found: " + tail.next.value
            );
        }

        // Verify forward chain length matches size
        int forwardCount = 0;
        DoublyLinkedListNode<T> curr = head;
        while (curr != null) {
            forwardCount++;
            if (curr.prev != null && curr.prev.next != curr) {
                throw new IllegalStateException(
                    "Invariant violation: prev.next broken at node " + curr.value
                );
            }
            curr = curr.next;
        }

        if (forwardCount != size) {
            throw new IllegalStateException(
                "Invariant violation: forward chain length " + forwardCount +
                " != size " + size
            );
        }
    }

    public DoublyLinkedListNode<T> getHead() { return head; }
    public DoublyLinkedListNode<T> getTail() { return tail; }
    public int getSize() { return size; }
    public boolean isEmpty() { return size == 0; }
}
Mental Model
The Four-Pointer Rule for Deletion
Delete is not one pointer change. It is up to four. Think of it as a checklist: did I update prev's next? Did I update next's prev? Was it the head? Was it the tail?
  • prevNode.next = nextNode (bypass the deleted node from the left).
  • nextNode.prev = prevNode (bypass the deleted node from the right).
  • If deleting head: head = nextNode (or null if list becomes empty).
  • If deleting tail: tail = prevNode (or null if list becomes empty).
  • All four checks are mandatory. Missing any one creates a bug.
📊 Production Insight
A connection pool manager used a DLL to track idle connections in LRU order. The delete method updated prev.next and next.prev but forgot to update the tail pointer when deleting the tail node. After deleting the tail, tail still pointed to the deleted node. The next eviction call returned a disconnected node with null next/prev pointers. The connection was never returned to the pool, and the pool gradually drained. After 6 hours, the pool had zero available connections and all new requests timed out.
Cause: delete method did not update tail when deleting the tail node. Effect: evicted connections were disconnected from the list but tail still referenced them. Impact: connection pool drained to zero over 6 hours, causing cascading timeouts. Action: add the fourth pointer check — if (node.next == null) tail = node.prev.
🎯 Key Takeaway
Every insert sets two pointers (newNode.prev and newNode.next) and may update one neighbor. Every delete updates up to four pointers. Missing any single pointer update creates a one-directional break — forward traversal works, backward traversal fails. Add invariant checks after every operation.
Insert/Delete Operation Selection
IfNeed to add item to front of list
UseUse insertAtHead. O(1). Set newNode.prev = null explicitly.
IfNeed to add item to end of list
UseUse insertAtTail. O(1). Set newNode.next = null explicitly.
IfNeed to remove a specific node you have a reference to
UseUse delete(node). O(1). Update all four pointers. Sever node.prev and node.next after.
IfNeed to remove the oldest/least-recently-used item
UseUse removeTail. O(1). This is the eviction path in LRU caches.
IfNeed to promote a node to the front (access = refresh)
UseUse moveToHead. O(1). Unlink from current position, link at head. This is the get() path in LRU caches.
IfNeed to insert in sorted order
UseMust traverse to find position — O(n). DLL does not help with sorted insertion. Consider a skip list or balanced tree.

LRU Cache: The Killer App for Doubly Linked Lists

An LRU (Least Recently Used) cache is a hash map + doubly linked list working together. The hash map provides O(1) key-to-node lookup. The DLL provides O(1) access-order maintenance and O(1) eviction.

On get(key): 1. Look up node in hash map — O(1). 2. Move node to head of DLL — O(1) pointer rewiring. 3. Return value.

On put(key, value): 1. If key exists: update value, move to head — O(1). 2. If key does not exist: create node, insert at head, add to hash map — O(1). 3. If capacity exceeded: remove tail from DLL and hash map — O(1).

Why DLL and not singly linked list: eviction removes the tail node. In a singly linked list, removing the tail requires O(n) traversal to find the second-to-last node. In a DLL, tail.prev is the second-to-last node — O(1).

Java's LinkedHashMap with accessOrder=true is a built-in LRU cache implementation using this exact pattern.

io/thecodeforge/cache/LRUCache.java · JAVA
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
package io.thecodeforge.cache;

import java.util.HashMap;
import java.util.Map;

/**
 * LRU Cache backed by a hash map + doubly linked list.
 * O(1) get and put. O(1) eviction.
 *
 * The DLL maintains access order: most recently used at head,
 * least recently used at tail. The hash map provides O(1) key-to-node
 * lookup so we can move a node to head without traversing the DLL.
 */
public class LRUCache<K, V> {

    private final int capacity;
    private final Map<K, Node<K, V>> cache;
    private Node<K, V> head;
    private Node<K, V> tail;

    private static class Node<K, V> {
        K key;
        V value;
        Node<K, V> prev;
        Node<K, V> next;

        Node(K key, V value) {
            this.key = key;
            this.value = value;
        }
    }

    public LRUCache(int capacity) {
        this.capacity = capacity;
        this.cache = new HashMap<>(capacity);
    }

    /**
     * Get value by key. O(1).
     * Moves accessed node to head (most recently used).
     */
    public V get(K key) {
        Node<K, V> node = cache.get(key);
        if (node == null) {
            return null;
        }
        moveToHead(node);
        return node.value;
    }

    /**
     * Put key-value pair. O(1).
     * If key exists: update and move to head.
     * If new: insert at head. If over capacity: evict tail.
     */
    public void put(K key, V value) {
        Node<K, V> node = cache.get(key);

        if (node != null) {
            node.value = value;
            moveToHead(node);
        } else {
            Node<K, V> newNode = new Node<>(key, value);
            cache.put(key, newNode);
            addToHead(newNode);

            if (cache.size() > capacity) {
                Node<K, V> evicted = removeTail();
                cache.remove(evicted.key);
            }
        }
    }

    /**
     * Move node to head. O(1).
     * Step 1: unlink from current position.
     * Step 2: link at head.
     */
    private void moveToHead(Node<K, V> node) {
        removeNode(node);
        addToHead(node);
    }

    /**
     * Add node right after head sentinel (or as head if no sentinel).
     * CRITICAL: newNode.prev must be set to null (or head sentinel).
     */
    private void addToHead(Node<K, V> node) {
        node.prev = null;   // CRITICAL: see incident #7293
        node.next = head;

        if (head != null) {
            head.prev = node;
        }
        head = node;

        if (tail == null) {
            tail = node;
        }
    }

    /**
     * Remove node from its current position. O(1).
     * Updates all four pointers.
     */
    private void removeNode(Node<K, V> node) {
        if (node.prev != null) {
            node.prev.next = node.next;
        } else {
            head = node.next;  // node was head
        }

        if (node.next != null) {
            node.next.prev = node.prev;
        } else {
            tail = node.prev;  // node was tail
        }

        node.prev = null;
        node.next = null;
    }

    /**
     * Remove and return tail node. O(1).
     */
    private Node<K, V> removeTail() {
        if (tail == null) {
            return null;
        }
        Node<K, V> oldTail = tail;
        removeNode(tail);
        return oldTail;
    }

    public int size() {
        return cache.size();
    }
}
Mental Model
Why LRU Cache Needs Both Hash Map AND Doubly Linked List
Hash map alone: O(1) lookup but no ordering — eviction requires scanning all entries (O(n)). DLL alone: O(1) ordering and eviction but O(n) lookup by key. Together: O(1) everything.
  • Hash map: key -> DLL node. O(1) lookup to find the node for a given key.
  • DLL: maintains access order. Head = most recent, tail = least recent.
  • get(key): hash map finds node in O(1), DLL moves it to head in O(1). Total: O(1).
  • put(key): hash map inserts in O(1), DLL inserts at head in O(1). If over capacity, DLL removes tail in O(1). Total: O(1).
  • This is exactly how Java's LinkedHashMap(accessOrder=true) works internally.
📊 Production Insight
A session management service used a HashMap for session lookup and a separate TTL-based cleanup thread for eviction. The cleanup thread scanned all sessions every 60 seconds — O(n) per scan. With 2 million active sessions, each scan took 4.2 seconds, consuming 7% of CPU. Replacing with an LRU cache (HashMap + DLL) eliminated the scan entirely. Eviction became O(1) — triggered on put() when capacity was exceeded. CPU usage dropped 7%, and session access latency improved from 12ms to 0.3ms because the hash map lookup replaced a full scan.
Cause: O(n) scan-based eviction on 2M sessions. Effect: 4.2-second scan every 60 seconds, 7% CPU, 12ms access latency. Impact: session service was the bottleneck for the entire auth pipeline. Action: replace with LRU cache (HashMap + DLL). Eviction: O(1). Access: O(1). CPU: -7%. Latency: 12ms to 0.3ms.
🎯 Key Takeaway
LRU cache = hash map + doubly linked list. Hash map gives O(1) key lookup. DLL gives O(1) access-order maintenance and eviction. Neither alone is sufficient. Java's LinkedHashMap with accessOrder=true implements this pattern natively — use it instead of rolling your own unless you need custom eviction policies.
When to Use LRU Cache vs Alternatives
IfNeed eviction of least-recently-used items with O(1) access
UseUse LRU cache (DLL + hash map). This is the standard pattern.
IfNeed eviction of least-frequently-used items
UseUse LFU cache (requires frequency tracking — hash map + min-heap or frequency buckets). More complex than LRU.
IfNeed time-based eviction (expire after N seconds)
UseUse TTL cache with a priority queue or scheduled cleanup. LRU is access-order, not time-order.
IfCache fits entirely in memory and keys are small
UseUse LinkedHashMap(accessOrder=true) — built-in LRU cache in Java. No need to implement DLL manually.
IfNeed thread-safe LRU cache
UseSynchronize all operations with a ReentrantLock, or use ConcurrentHashMap for the map + synchronized DLL operations. The hash map and DLL must be updated atomically.
IfCache size is fixed and small (< 1000 entries)
UseA simple LinkedHashMap is sufficient. The DLL overhead is negligible at small sizes.

Thread Safety and Concurrent Doubly Linked Lists

DLL pointer updates are not atomic. Each pointer assignment (node.next = X, node.prev = Y) is a separate memory operation. Two threads operating on the same DLL simultaneously can interleave pointer updates, creating broken chains.

Common concurrency bugs: 1. Two threads insert at head simultaneously: both set newNode.next = head, but only one head.prev = newNode survives. The other's prev pointer is lost. 2. One thread deletes a node while another traverses through it: the traversing thread reads a node that is being unlinked, following a next pointer to a node whose prev no longer points back. 3. One thread moveToHead while another removeTail: both modify the same node's pointers, creating an inconsistent state.

Solutions
  • Coarse-grained: synchronized block or ReentrantLock around all operations. Simple but serializes all access.
  • Fine-grained: lock only the nodes being modified and their neighbors. Complex but allows concurrent reads and non-conflicting writes.
  • Lock-free: not practical for DLLs. The multi-pointer update requirement makes CAS-based approaches extremely complex.
  • Copy-on-write: snapshot the list, modify the snapshot, swap reference. Works for read-heavy workloads.

Java's ConcurrentLinkedDeque uses a lock-free approach for deque operations but it is significantly more complex than a basic DLL.

io/thecodeforge/collections/ConcurrentDoublyLinkedList.java · JAVA
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
package io.thecodeforge.collections;

import java.util.concurrent.locks.ReentrantLock;

/**
 * Thread-safe doubly linked list using coarse-grained locking.
 * All operations acquire a single lock.
 * Simple, correct, but serializes all access.
 *
 * For high-concurrency scenarios, consider fine-grained locking
 * or Java's ConcurrentLinkedDeque.
 */
public class ConcurrentDoublyLinkedList<T> {

    private DoublyLinkedListNode<T> head;
    private DoublyLinkedListNode<T> tail;
    private int size;
    private final ReentrantLock lock = new ReentrantLock();

    public void insertAtHead(T value) {
        lock.lock();
        try {
            DoublyLinkedListNode<T> newNode = new DoublyLinkedListNode<>(value);
            newNode.next = head;
            newNode.prev = null;

            if (head != null) {
                head.prev = newNode;
            }
            head = newNode;

            if (tail == null) {
                tail = newNode;
            }
            size++;
        } finally {
            lock.unlock();
        }
    }

    public void insertAtTail(T value) {
        lock.lock();
        try {
            DoublyLinkedListNode<T> newNode = new DoublyLinkedListNode<>(value);
            newNode.prev = tail;
            newNode.next = null;

            if (tail != null) {
                tail.next = newNode;
            }
            tail = newNode;

            if (head == null) {
                head = newNode;
            }
            size++;
        } finally {
            lock.unlock();
        }
    }

    public void delete(DoublyLinkedListNode<T> node) {
        lock.lock();
        try {
            if (node.prev != null) {
                node.prev.next = node.next;
            } else {
                head = node.next;
            }

            if (node.next != null) {
                node.next.prev = node.prev;
            } else {
                tail = node.prev;
            }

            node.prev = null;
            node.next = null;
            size--;
        } finally {
            lock.unlock();
        }
    }

    /**
     * Traverse the list with a lock held.
     * For read-heavy workloads, consider a ReadWriteLock.
     */
    public void traverse(java.util.function.Consumer<T> action) {
        lock.lock();
        try {
            DoublyLinkedListNode<T> curr = head;
            while (curr != null) {
                action.accept(curr.value);
                curr = curr.next;
            }
        } finally {
            lock.unlock();
        }
    }

    public int getSize() {
        lock.lock();
        try {
            return size;
        } finally {
            lock.unlock();
        }
    }
}
Mental Model
Why DLLs Are Hard to Make Lock-Free
Singly linked list delete modifies one pointer (prev.next) — potentially lock-free with CAS. DLL delete modifies up to four — practically requires a lock.
  • Singly linked list delete: one pointer change. Can use CAS on prev.next.
  • DLL delete: four pointer changes. CAS on each individually does not guarantee atomicity.
  • If thread A CAS's prev.next but thread B CAS's next.prev before A completes, the chain is inconsistent.
  • Solution: use a lock, or use a lock-free deque implementation (ConcurrentLinkedDeque) which is 500+ lines of complex CAS logic.
  • For most applications, coarse-grained locking is correct and fast enough. Do not over-engineer.
📊 Production Insight
A real-time leaderboard service used a DLL to maintain sorted player scores. The service was single-threaded initially. When scaled to 4 threads, the DLL produced corrupted rankings — players appeared twice, scores were wrong, and occasionally a NullPointerException crashed the worker thread. Root cause: no synchronization on DLL operations. Two threads inserting at head simultaneously created a broken prev chain. The fix was adding a ReentrantLock around all insert/delete operations. Throughput dropped 15% due to lock contention, but correctness was restored. A later optimization replaced the DLL with a ConcurrentSkipListMap, which provided both safety and higher concurrency.
🎯 Key Takeaway
DLLs are not thread-safe. Concurrent access requires explicit synchronization. Coarse-grained locking is simple and correct for most use cases. For high-concurrency deque operations, use ConcurrentLinkedDeque. For arbitrary insert/delete, consider ConcurrentSkipListMap instead.
Concurrency Strategy Selection
IfSingle-threaded access only
UseUse non-synchronized DLL. No locking overhead.
IfLow to moderate concurrency, correctness is critical
UseUse coarse-grained locking (synchronized or ReentrantLock). Simple and correct.
IfRead-heavy, write-rare workload
UseUse ReadWriteLock — allows concurrent reads, exclusive writes.
IfHigh concurrency, need a deque (head/tail only access)
UseUse ConcurrentLinkedDeque — lock-free, high throughput, but limited to deque operations.
IfHigh concurrency, need arbitrary insert/delete by value
UseConsider ConcurrentSkipListMap instead of DLL. O(log n) operations but thread-safe and highly concurrent.
IfNeed an LRU cache with high concurrency
UseUse ConcurrentHashMap for the map + synchronized DLL, or use Caffeine cache which provides concurrent LRU.

Edge Cases and Boundary Conditions in Doubly Linked Lists

DLL bugs almost always occur at the boundaries: empty list, single element, head, tail, and interleaved operations. The happy path (insert into middle of large list, delete from middle) rarely fails. The boundaries fail constantly.

Common boundary failures
  • Insert into empty list: head and tail must both point to the new node.
  • Delete the only element: head and tail must both become null.
  • Insert at head: new head's prev must be null. Old head's prev must point to new head.
  • Delete head: new head's prev must be null. Old head must be severed.
  • Delete tail: new tail's next must be null. Old tail must be severed.
  • Move to head when node is already head: no-op.
  • Move to head when node is tail: tail must update to the new tail (node.prev).

Test every operation against all five boundary conditions. Run invariant checks after every operation to catch boundary bugs immediately.

io/thecodeforge/collections/DoublyLinkedListTest.java · JAVA
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
package io.thecodeforge.collections;

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

/**
 * Boundary condition tests for doubly linked list.
 * Each test verifies invariants after every operation.
 */
public class DoublyLinkedListTest {

    @Test
    public void testInsertIntoEmptyList() {
        DoublyLinkedList<String> list = new DoublyLinkedList<>();
        list.insertAtHead("first");
        
        assertNotNull(list.getHead());
        assertNotNull(list.getTail());
        assertSame(list.getHead(), list.getTail());
        assertEquals(1, list.getSize());
        assertNull(list.getHead().prev);
        assertNull(list.getTail().next);
    }

    @Test
    public void testDeleteOnlyElement() {
        DoublyLinkedList<String> list = new DoublyLinkedList<>();
        list.insertAtHead("only");
        DoublyLinkedListNode<String> node = list.getHead();
        
        list.delete(node);
        
        assertNull(list.getHead());
        assertNull(list.getTail());
        assertEquals(0, list.getSize());
        assertNull(node.prev);
        assertNull(node.next);
    }

    @Test
    public void testInsertAtHeadBoundary() {
        DoublyLinkedList<String> list = new DoublyLinkedList<>();
        list.insertAtHead("second");
        list.insertAtHead("first");
        
        assertEquals("first", list.getHead().value);
        assertNull(list.getHead().prev);
        assertEquals("second", list.getHead().next.value);
    }

    @Test
    public void testDeleteHead() {
        DoublyLinkedList<String> list = new DoublyLinkedList<>();
        list.insertAtHead("third");
        list.insertAtHead("second");
        list.insertAtHead("first");
        
        list.delete(list.getHead());
        
        assertEquals("second", list.getHead().value);
        assertNull(list.getHead().prev);
        assertEquals(2, list.getSize());
    }

    @Test
    public void testDeleteTail() {
        DoublyLinkedList<String> list = new DoublyLinkedList<>();
        list.insertAtHead("third");
        list.insertAtHead("second");
        list.insertAtHead("first");
        
        list.delete(list.getTail());
        
        assertEquals("second", list.getTail().value);
        assertNull(list.getTail().next);
        assertEquals(2, list.getSize());
    }

    @Test
    public void testMoveToHeadWhenAlreadyHead() {
        DoublyLinkedList<String> list = new DoublyLinkedList<>();
        list.insertAtHead("first");
        list.insertAtHead("second");
        DoublyLinkedListNode<String> head = list.getHead();
        
        list.moveToHead(head);
        
        assertSame(head, list.getHead());
        assertEquals(2, list.getSize());
    }

    @Test
    public void testMoveTailToHead() {
        DoublyLinkedList<String> list = new DoublyLinkedList<>();
        list.insertAtHead("first");
        list.insertAtHead("second");
        list.insertAtHead("third");
        DoublyLinkedListNode<String> tail = list.getTail();
        
        list.moveToHead(tail);
        
        assertSame(tail, list.getHead());
        assertEquals(3, list.getSize());
        assertNull(tail.prev);
        assertEquals("third", tail.next.value);
        assertNotSame(tail, list.getTail());
    }

    @Test
    public void testForwardAndBackwardTraversalMatch() {
        DoublyLinkedList<Integer> list = new DoublyLinkedList<>();
        for (int i = 1; i <= 10; i++) {
            list.insertAtTail(i);
        }
        
        // Forward traversal
        int forwardCount = 0;
        DoublyLinkedListNode<Integer> curr = list.getHead();
        while (curr != null) {
            forwardCount++;
            curr = curr.next;
        }
        
        // Backward traversal
        int backwardCount = 0;
        curr = list.getTail();
        while (curr != null) {
            backwardCount++;
            curr = curr.prev;
        }
        
        assertEquals(forwardCount, backwardCount);
        assertEquals(list.getSize(), forwardCount);
    }

    @Test
    public void testInterleavedInsertDelete() {
        DoublyLinkedList<Integer> list = new DoublyLinkedList<>();
        
        list.insertAtHead(1);
        list.insertAtTail(2);
        list.insertAtHead(0);
        list.insertAtTail(3);
        
        assertEquals(4, list.getSize());
        
        list.delete(list.getHead());
        list.delete(list.getTail());
        
        assertEquals(2, list.getSize());
        assertEquals(Integer.valueOf(1), list.getHead().value);
        assertEquals(Integer.valueOf(2), list.getTail().value);
        assertNull(list.getHead().prev);
        assertNull(list.getTail().next);
    }
}
Mental Model
The Edge Case Matrix
The bugs are not in the happy path. They are in the boundaries: empty list, single element, head operations, tail operations, and interleaved operations.
  • Empty list insert: new node is both head and tail. Both pointers must be set.
  • Single element delete: both head and tail become null. Size becomes 0.
  • Head delete: new head.prev must be null. Old head must be severed.
  • Tail delete: new tail.next must be null. Old tail must be severed.
  • Interleaved operations: each operation must handle the case where it modifies head or tail.
📊 Production Insight
A distributed task queue used a DLL to maintain task priority order. A developer added a 'requeue' feature that removed a failed task from its current position and re-inserted it at the tail. The requeue method called delete(node) then insertAtTail(node.value) — but reused the same node object without clearing its prev/next pointers from the delete. The node's prev still pointed to its old neighbor. insertAtTail set node.prev = tail and node.next = null, but the old neighbor's next still pointed to this node. Result: a cycle. Forward traversal from head reached the requeued node, then followed the stale next pointer back to an earlier node, looping forever. The queue worker thread hung in an infinite loop, processing no tasks.
Cause: requeue reused a node object without clearing stale pointers from delete. Effect: cycle in the DLL, infinite traversal loop. Impact: task queue worker hung for 2 hours before watchdog restart. Action: always set node.prev = null and node.next = null after delete, before any re-insertion. Better: create a new node for re-insertion.
🎯 Key Takeaway
DLL bugs live at the boundaries: empty list, single element, head/tail operations, and interleaved operations. Test every operation against all five boundary conditions. Add invariant checks that verify head.prev==null, tail.next==null, and forward/backward traversal counts match size. Catch bugs at write time, not at read time.
Edge Case Testing Strategy
IfImplementing insertAtHead
UseTest: insert into empty list (head and tail both set), insert when list has one element, insert when list has many elements. Verify head.prev is always null.
IfImplementing insertAtTail
UseTest: insert into empty list, insert when list has one element, insert when list has many elements. Verify tail.next is always null.
IfImplementing delete
UseTest: delete head, delete tail, delete middle, delete only element, delete from two-element list. Verify all four pointers and head/tail state.
IfImplementing moveToHead
UseTest: move head to head (no-op), move tail to head, move middle to head. Verify old position's neighbors are correctly linked.
IfAny operation
UseRun invariant check after every operation: head.prev==null, tail.next==null, forward count==size, backward count==size.
🗂 Doubly Linked List vs Singly Linked List vs Array List vs LinkedBlockingDeque
Comparison of list/deque data structures. Operation complexities assume head and tail pointers for linked lists.
Feature / OperationDoubly Linked ListSingly Linked ListArray List (dynamic array)LinkedBlockingDeque
Node structuredata + next + prev (3 fields)data + next (2 fields)contiguous array of referencesdata + next + prev (3 fields) + lock
Insert at headO(1)O(1)O(n) — shift all elementsO(1) — with lock
Insert at tailO(1) with tail pointerO(n) without tail pointer, O(1) withO(1) amortized — appendO(1) — with lock
Delete known nodeO(1) — node.prev gives predecessorO(n) — must traverse to find predecessorO(n) — shift elements after indexO(1) — with lock
Delete headO(1)O(1)O(n) — shift all elementsO(1) — with lock
Delete tailO(1) with tail pointerO(n) — must find second-to-lastO(1) — just decrement sizeO(1) — with lock
Access by indexO(n) — must traverseO(n) — must traverseO(1) — direct array indexO(n) — must traverse
Search by valueO(n) — linear scanO(n) — linear scanO(n) — linear scan (unsorted)O(n) — linear scan
Bidirectional traversalYes — both directionsNo — forward onlyYes — by indexYes — both directions
Memory per element24 bytes (data + 2 pointers on 64-bit)16 bytes (data + 1 pointer)8 bytes (reference) + array overhead24 bytes + lock overhead
Cache localityPoor — nodes scattered in heapPoor — nodes scattered in heapExcellent — contiguous memoryPoor — nodes scattered in heap
Thread safetyNot thread-safeNot thread-safeNot thread-safeThread-safe — blocking operations
Primary use caseLRU cache, browser history, dequeStack, simple queueRandom access, iteration-heavyProducer-consumer queue, work stealing

🎯 Key Takeaways

  • The prev pointer is the entire value proposition of a DLL. It transforms deletion from O(n) to O(1) by eliminating the need to traverse for the predecessor.
  • Every insert must set both prev and next for the new node. Every delete must update up to four pointers: prev.next, next.prev, head, and tail. Missing any single update creates a one-directional break.
  • LRU cache = hash map + DLL. Hash map provides O(1) key lookup. DLL provides O(1) access-order maintenance and eviction. Neither alone is sufficient.
  • DLL pointer updates are non-atomic. Multi-threaded access without synchronization corrupts the list structure. Use ReentrantLock for correctness, or replace with ConcurrentSkipListMap for high contention.
  • Test every DLL operation against five boundary conditions: empty list, single element, head operations, tail operations, and interleaved operations. Add invariant checks after every operation.
  • Choose DLL when you need O(1) deletion of known nodes or bidirectional traversal. Choose array list when you need random access or read-heavy iteration. The 8-byte-per-node overhead and poor cache locality of DLL are significant costs.

⚠ Common Mistakes to Avoid

    Forgetting to set newNode.prev = null on insert at head — The new head must have prev = null. If prev still points to the old head, backward traversal from tail reaches a node before the actual head, breaking the chain. Fix: always set newNode.prev = null explicitly in insertAtHead.
    Fix

    always set newNode.prev = null explicitly in insertAtHead.

    Forgetting to set newNode.next = null on insert at tail — The new tail must have next = null. If next still points to the old tail, forward traversal from head reaches a node after the actual tail, creating a cycle. Fix: always set newNode.next = null explicitly in insertAtTail.
    Fix

    always set newNode.next = null explicitly in insertAtTail.

    Not updating head or tail when deleting the head or tail node — The delete method must check if the deleted node is head or tail and update accordingly. Missing either leaves a stale pointer to a deleted node. Fix: add explicit checks — if (node.prev == null) head = node.next; if (node.next == null) tail = node.prev.
    Fix

    add explicit checks — if (node.prev == null) head = node.next; if (node.next == null) tail = node.prev.

    Not severing deleted node's pointers — After delete, set node.prev = null and node.next = null. Otherwise the deleted node still references the list, preventing garbage collection and creating potential dangling references. Fix: always null out both pointers on the deleted node.
    Fix

    always null out both pointers on the deleted node.

    Reusing a deleted node without clearing pointers — If you delete a node and re-insert it elsewhere without clearing prev/next, stale pointers create cycles or broken chains. Fix: always clear both pointers before re-insertion, or create a new node.
    Fix

    always clear both pointers before re-insertion, or create a new node.

    Not handling single-element list — Insert one node, then delete it. Both head and tail must become null. If delete only updates head, tail still points to the deleted node. Fix: test single-element insert-then-delete explicitly.
    Fix

    test single-element insert-then-delete explicitly.

    Concurrent access without synchronization — Two threads inserting at head simultaneously can interleave pointer updates, creating broken chains. Fix: use synchronized block or ReentrantLock around all operations.
    Fix

    use synchronized block or ReentrantLock around all operations.

    Using DLL where array list is better — If you need random access by index or read-heavy iteration, array list is 10-100x faster due to cache locality. DLL is optimal for frequent insert/delete at known positions. Fix: choose data structure based on access pattern, not theoretical elegance.
    Fix

    choose data structure based on access pattern, not theoretical elegance.

    Not adding invariant checks — Without post-operation assertions, pointer bugs are discovered at next traversal, which may be much later in a different code path. Fix: add assertInvariants() after every operation in development and testing.
    Fix

    add assertInvariants() after every operation in development and testing.

    Confusing LRU cache implementation — Using only a hash map (no ordering, O(n) eviction) or only a DLL (O(n) lookup by key). Both are required for O(1) operations. Fix: always pair hash map with DLL for LRU cache.
    Fix

    always pair hash map with DLL for LRU cache.

Interview Questions on This Topic

  • QExplain why deletion of a known node is O(1) in a doubly linked list but O(n) in a singly linked list. What role does the prev pointer play?
  • QYou are implementing delete(node) for a DLL. How many pointers must you update? Walk through each one and explain what happens if you skip it.
  • QAn LRU cache uses a hash map + doubly linked list. Why can't you implement an LRU cache with just a hash map? Or just a DLL?
  • QA DLL's backward traversal stops early — hits null before reaching the head. What is the most likely cause, and how would you fix it?
  • QTwo threads insert at head simultaneously on an unsynchronized DLL. What specific corruption can occur? Draw the pointer state after both inserts complete.
  • QYou need to implement a thread-safe DLL. What are your synchronization options, and what are the trade-offs of each?
  • QWalk through the edge case of deleting the only element in a DLL. What must happen to head, tail, and the deleted node's pointers?
  • QCompare the memory overhead of a DLL vs an array list for 1 million integer elements. Which uses more memory, and by how much?
  • QJava's LinkedHashMap uses a DLL internally. Explain how accessOrder=true makes it an LRU cache, and what happens on get() and put().
  • QYou find a production bug: forward traversal count does not match backward traversal count in a DLL. What does this tell you, and how would you find the break point?

Frequently Asked Questions

What is the main advantage of a doubly linked list over a singly linked list?

The main advantage is O(1) deletion of any known node. In a singly linked list, deleting node X requires finding X's predecessor, which is O(n) traversal. In a DLL, every node has a prev pointer that directly references its predecessor, making deletion O(1). DLL also supports bidirectional traversal, which singly linked lists cannot do.

How many pointers must be updated when deleting a node from a DLL?

Up to four pointers: (1) prevNode.next = nextNode, (2) nextNode.prev = prevNode, (3) head = nextNode if deleting the head, (4) tail = prevNode if deleting the tail. Additionally, the deleted node's prev and next should be set to null to prevent dangling references.

Why does an LRU cache need both a hash map and a doubly linked list?

The hash map provides O(1) key-to-node lookup so you can find any cached item without traversing the list. The DLL provides O(1) access-order maintenance (move to head on access) and O(1) eviction (remove tail). Without the hash map, lookup is O(n). Without the DLL, eviction is O(n). Together they give O(1) for all operations.

Is a doubly linked list thread-safe?

No. DLL pointer updates are not atomic — each pointer assignment (node.next = X, node.prev = Y) is a separate memory operation. Two threads operating on the same DLL can interleave pointer updates and create broken chains. For thread safety, use synchronized blocks, ReentrantLock, or replace with ConcurrentLinkedDeque/ConcurrentSkipListMap.

When should I use a DLL instead of an array list?

Use a DLL when you need O(1) insertion/deletion at both ends or O(1) deletion of known nodes. Use an array list when you need O(1) random access by index or read-heavy iteration. Array list has better cache locality (contiguous memory) and is 10-100x faster for iteration. DLL has poor cache locality (nodes scattered in heap) but excels at pointer-based operations.

What is the space overhead of a DLL compared to a singly linked list?

A DLL adds one extra pointer (prev) per node. On a 64-bit JVM, this is 8 bytes per node (plus object header overhead). For 1 million nodes, that is approximately 8MB extra. The trade-off: 8 bytes per node buys O(1) deletion vs O(n) deletion.

How does Java's LinkedHashMap use a doubly linked list?

LinkedHashMap maintains a DLL through all entries in insertion order (default) or access order (accessOrder=true). When accessOrder=true, every get() or put() moves the accessed entry to the tail of the DLL. Combined with a capacity limit and removeEldestEntry() override, this makes LinkedHashMap an LRU cache — the eldest entry (head) is removed when capacity is exceeded.

What is the most common bug in DLL implementations?

Forgetting to update the prev pointer on insert or the head/tail pointer on delete. These create one-directional breaks: forward traversal works correctly but backward traversal hits null prematurely, or vice versa. The fix: add invariant checks (head.prev == null, tail.next == null, forward count == backward count) after every operation.

🔥
Naren Founder & Author

Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.

← PreviousSingly Linked ListNext →Circular Linked List
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged