CS205

Doubly Linked List

A linear collection where each node has references to both the next and previous nodes. Provides O(1) insertion/deletion at both ends.

How Doubly Linked List Works Internally

Node Structure

Each node contains three fields: data, next, and prev. The prev pointer allows backward traversal and enables O(1) deletion from the end.

class Node {
    E data;
    Node next;
    Node prev;
}

Head and Tail

Both head and tail references are maintained. head.prev is null, tail.next is null. This enables efficient operations at both ends of the list.

Bidirectional Traversal

You can traverse forward (using next) or backward (using prev). For get(index), you can optimize by starting from the closer end: if index < size/2, start from head; otherwise start from tail.

More Pointers to Update

Insert and delete operations require updating more pointers than singly-linked. For middle insertions, you update 4 pointers: new.next, new.prev, prev.next, and next.prev.

Time Complexity
OperationTimeWhy
get(index)O(n/2) = O(n)Traverse from closer end (head or tail)
set(index, value)O(n/2) = O(n)Traverse from closer end (head or tail)
addFirst(value)O(1)Update head pointer and new.next
addLast(value)O(1)Update tail pointer and new.prev
add(index, value)O(n)Traverse to index, then O(1) insert
removeFirst()O(1)Update head and new head's prev
removeLast()O(1)Update tail using tail.prev
remove(index)O(n)Traverse to index, then O(1) remove
Interactive Visualizer

Notice the bidirectional arrows between nodes. Watch how both next and prev pointers are updated during operations.

Size: 4
head
prev
[0]10
next
prev
[1]20
next
prev
[2]30
next
prev
[3]40
next
→ null
← tail
Node data Pointer
Speed:

Java Implementation

public void add(int index, E element) {
    if (index < 0 || index > size)
        throw new IndexOutOfBoundsException();
    Node newNode = new Node(element);
    if (size == 0) {
        head = tail = newNode;
    } else if (index == 0) {
        newNode.next = head;
        head.prev = newNode;
        head = newNode;
    } else if (index == size) {
        newNode.prev = tail;
        tail.next = newNode;
        tail = newNode;
    } else {
        Node current = getNode(index);
        newNode.next = current;
        newNode.prev = current.prev;
        current.prev.next = newNode;
        current.prev = newNode;
    }
    size++;
}

Pseudocode

1. Check if index is valid (0 to size)
2. Create a new node with the element
3. If list is empty: set head and tail to new node
4. If adding at index 0:
- Point new node's next to head
- Point head's prev to new node
- Update head to new node
5. If adding at end:
- Point new node's prev to tail
- Point tail's next to new node
- Update tail to new node
6. If adding in middle:
- Traverse to node at index
- Update all four pointers
7. Increment size

Node Class Structure

private class Node {
    E data;
    Node next;
    Node prev;

    Node(E data) {
        this.data = data;
        this.next = null;
        this.prev = null;
    }
}
Pointer Operations Explained

addFirst(value) - O(1)

1. Create new node   2. new.next = head   3. head.prev = new   4. head = new
Before: null ← [A] ⇄ [B] ⇄ [C] → null
After: null ← [NEW] ⇄ [A] ⇄ [B] ⇄ [C] → null

addLast(value) - O(1)

1. Create new node   2. new.prev = tail   3. tail.next = new   4. tail = new
Before: null ← [A] ⇄ [B] ⇄ [C] → null
After: null ← [A] ⇄ [B] ⇄ [C] ⇄ [NEW] → null

removeLast() - O(1)

1. Save tail.data   2. tail = tail.prev   3. tail.next = null   4. Return saved data
Before: null ← [A] ⇄ [B] ⇄ [C] → null
After: null ← [A] ⇄ [B] → null (C removed)

This is O(1) because we can directly access the previous node via tail.prev!

add(1, value) - Insert at index 1

1. Traverse to node at index 1   2. Create new node   3. Update 4 pointers: new.next, new.prev, prev.next, current.prev
Before: null ← [A] ⇄ [B] ⇄ [C] → null
After: null ← [A] ⇄ [NEW] ⇄ [B] ⇄ [C] → null

remove(1) - Remove at index 1

1. Traverse to node at index 1   2. prev.next = current.next   3. next.prev = current.prev   4. Return current.data
Before: null ← [A] ⇄ [B] ⇄ [C] → null
After: null ← [A] ⇄ [C] → null (B removed)
Doubly vs Singly Linked List
AspectSingly LinkedDoubly Linked
Node sizedata + 1 pointerdata + 2 pointers (more memory)
removeLast()O(n) - must traverseO(1) - use tail.prev
TraversalForward onlyForward and backward
Insert complexity2 pointer updates4 pointer updates
get(index) optimizationAlways from headFrom closer end
Edge Cases to Consider

Empty List

  • • head == null AND tail == null
  • • addFirst/addLast: set both head and tail to new node
  • • new node has prev = null and next = null

Single Element

  • • head == tail (same node)
  • • node.prev == null AND node.next == null
  • • Removing: set both head and tail to null

Boundary Pointers

  • • First node: prev must be null
  • • Last node: next must be null
  • • After removeFirst: new head's prev = null
  • • After removeLast: new tail's next = null

Null Checks

  • • Check if node.prev != null before node.prev.next
  • • Check if node.next != null before node.next.prev
  • • Prevents NullPointerException
When to Use Doubly Linked List

Good For

  • ✓ Frequent insertions/deletions at both ends
  • ✓ Implementing deques (double-ended queues)
  • ✓ Navigation (browser history, undo/redo)
  • ✓ LRU cache implementation
  • ✓ When backward traversal is needed

Not Good For

  • ✗ Random access by index
  • ✗ Memory-constrained environments
  • ✗ Simple LIFO stacks (use singly-linked)
  • ✗ When pointer overhead is a concern
  • ✗ Cache-sensitive applications

Real-World Usage

Java's LinkedList class is actually a doubly-linked list! It implements both List and Deque interfaces, allowing it to be used as a list, stack, or queue.