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.
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.
| Operation | Time | Why |
|---|---|---|
| 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 |
Notice the bidirectional arrows between nodes. Watch how both next and prev pointers are updated during operations.
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
Node Class Structure
private class Node {
E data;
Node next;
Node prev;
Node(E data) {
this.data = data;
this.next = null;
this.prev = null;
}
}addFirst(value) - O(1)
addLast(value) - O(1)
removeLast() - O(1)
This is O(1) because we can directly access the previous node via tail.prev!
add(1, value) - Insert at index 1
remove(1) - Remove at index 1
| Aspect | Singly Linked | Doubly Linked |
|---|---|---|
| Node size | data + 1 pointer | data + 2 pointers (more memory) |
| removeLast() | O(n) - must traverse | O(1) - use tail.prev |
| Traversal | Forward only | Forward and backward |
| Insert complexity | 2 pointer updates | 4 pointer updates |
| get(index) optimization | Always from head | From closer end |
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
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.