A singly linked list is like a treasure hunt where each clue only tells you where to go next. A doubly linked list is like a treasure hunt where each clue also tells you how to go back to where you came from.
Every node now carries three things: a value, a next pointer pointing to the node ahead, and a prev pointer pointing to the node behind. The list has both a head (front) and a tail (back), and you can traverse it in either direction.
What changes compared to singly linked?
The extra prev pointer unlocks two key improvements:
\(O(1)\) insert at tail — since you always know where the tail is, inserting there is just pointer updates, not a full traversal.
\(O(1)\) delete given the node — if you have a direct reference to a node, you can unlink it instantly. In a singly linked list, you'd need to find its predecessor first (\(O(n)\)) because you can't walk backwards.
Both of these come from the same root cause: you can now move backwards through the chain, so you never need to re-traverse from the head just to find a predecessor.
◈ Explore doubly linked list
← → arrow keys to step · click elements to interact
Doubly Linked List Operations
classNode:
def__init__(self, val):
self.val = val
self.prev = self.next=None
classDoublyLinkedList:
def__init__(self): self.head = self.tail =None
defpush_front(self, v):
n = Node(v)
ifnot self.head: self.head = self.tail = n;return
n.next= self.head; self.head.prev = n; self.head = n
defpush_back(self, v):
n = Node(v)
ifnot self.tail: self.head = self.tail = n;return
n.prev = self.tail; self.tail.next= n; self.tail = n
Note: The price for bidirectional traversal is memory: every node now stores two pointers instead of one. For n nodes, that's n extra pointer slots. Worth it when you need to insert or delete at both ends frequently — like in a Deque or an LRU Cache.
Insert at head (\(O(1)\))
To add a new node at the front:
Set the new node's next to the current head.
Set the current head's prev to the new node.
Move the head pointer to the new node.
Three pointer changes. Done.
Insert at tail (\(O(1)\))
Because we track the tail explicitly, inserting at the back is equally simple:
Set the current tail's next to the new node.
Set the new node's prev to the current tail.
Move the tail pointer to the new node.
This is something a singly linked list cannot do in \(O(1)\) without a tail pointer — and even with a tail pointer, deletion from the tail still requires \(O(n)\) to find the new tail. With a doubly linked list, the new tail is just old_tail.prev.
Delete given a node reference (\(O(1)\))
If you already have a pointer to the node you want to delete:
Set node.prev.next = node.next
Set node.next.prev = node.prev
The node is now unlinked from both directions. No traversal needed.
Real-world uses
Doubly linked lists are the backbone of several important data structures:
LRU Cache — move recently used items to the front in \(O(1)\)
Deque (double-ended queue) — push and pop from both ends in \(O(1)\)
Browser history — navigate back and forward
Undo/Redo — move through command history in both directions
Complexity
Access by positionStill must traverse from head or tail
Slow\(O(n)\)
Insert at headThree pointer updates
Instant\(O(1)\)
Insert at tailTail pointer makes this instant
Instant\(O(1)\)
Delete with node referenceprev pointer removes need to re-traverse