DSA Notes
A complete guide to doubly linked lists — node structure with prev and next pointers, all operations with O(1) and O(n) complexities, comparison with singly linked lists, LRU Cache implementation using doubly linked list and hash map, and interview problems.
Introduction
A doubly linked list is a linked list where each node has two pointers — next (pointing forward) and prev (pointing backward). This bidirectionality removes one of the major limitations of singly linked lists: the inability to efficiently delete a node given only its reference.
In a singly linked list, to delete a node, you need to find the *previous* node first (O(n) traversal). In a doubly linked list, every node already knows its previous node — deletion takes O(1).
This property makes doubly linked lists the foundation of one of the most important real-world data structures: the LRU Cache.
Complete Doubly Linked List Implementation
class DoublyLinkedList:
def __init__(self):
self.head = None
self.tail = None
self._size = 0
def __len__(self):
return self._size
def is_empty(self):
return self.head is None
def __repr__(self):
if self.head is None:
return "None"
nodes = []
cur = self.head
while cur:
nodes.append(str(cur.data))
cur = cur.next
return " ↔ ".join(nodes)Insertion Operations
Insert at Head — O(1)
def insert_at_head(self, data):
new_node = DoublyNode(data)
if self.head is None:
self.head = self.tail = new_node
else:
new_node.next = self.head # New node → old head
self.head.prev = new_node # Old head ← new node
self.head = new_node # Update head
self._size += 1Visualization:
Insert at Tail — O(1)
def insert_at_tail(self, data):
new_node = DoublyNode(data)
if self.tail is None:
self.head = self.tail = new_node
else:
new_node.prev = self.tail # New node ← old tail
self.tail.next = new_node # Old tail → new node
self.tail = new_node # Update tail
self._size += 1Insert After a Given Node — O(1)
def insert_after(self, node, data):
"""
Insert a new node immediately after the given node.
O(1) — no traversal needed.
This is the key advantage of doubly linked lists.
"""
if node is None:
raise ValueError("Node cannot be None")
new_node = DoublyNode(data)
new_node.prev = node # new ← node
new_node.next = node.next # new → node.next
if node.next is not None:
node.next.prev = new_node # node.next ← new
else:
self.tail = new_node # new_node is the new tail
node.next = new_node # node → new
self._size += 1Insert at Position — O(n)
def insert_at_position(self, data, position):
if position < 0 or position > self._size:
raise IndexError(f"Position {position} out of range")
if position == 0:
self.insert_at_head(data)
return
if position == self._size:
self.insert_at_tail(data)
return
# Optimization: if position is in second half, traverse from tail
if position <= self._size // 2:
current = self.head
for _ in range(position - 1):
current = current.next
else:
current = self.tail
for _ in range(self._size - position - 1):
current = current.prev
self.insert_after(current, data)Deletion Operations
Delete a Given Node — O(1)
This is the primary advantage over singly linked lists.
def delete_node(self, node):
"""
Delete a specific node in O(1).
In a singly linked list, this would require O(n) to find the previous node.
Here, node.prev gives us the previous node directly.
"""
if node is None:
return
# Update the previous node's next pointer
if node.prev is not None:
node.prev.next = node.next
else:
self.head = node.next # Deleting head
# Update the next node's prev pointer
if node.next is not None:
node.next.prev = node.prev
else:
self.tail = node.prev # Deleting tail
# Clean up the deleted node's pointers
node.prev = None
node.next = None
self._size -= 1Visualization:
Delete Head — O(1)
def delete_head(self):
if self.head is None:
raise ValueError("List is empty")
removed = self.head.data
self.delete_node(self.head)
return removedDelete Tail — O(1)
def delete_tail(self):
"""
O(1) — tail pointer + prev pointer give direct access.
Compare: singly linked list delete tail is O(n).
"""
if self.tail is None:
raise ValueError("List is empty")
removed = self.tail.data
self.delete_node(self.tail)
return removedTraversal — Both Directions
def traverse_forward(self):
"""Print list from head to tail."""
result = []
current = self.head
while current:
result.append(str(current.data))
current = current.next
print(" → ".join(result))
def traverse_backward(self):
"""Print list from tail to head."""
result = []
current = self.tail
while current:
result.append(str(current.data))
current = current.prev
print(" → ".join(result))Singly vs Doubly — Direct Comparison
| Operation | Singly Linked | Doubly Linked | Notes |
|---|---|---|---|
| Insert at head | O(1) | O(1) | Same |
| Insert at tail (with tail ptr) | O(1) | O(1) | Same |
| Insert after given node | O(1) | O(1) | Same |
| Insert before given node | O(n) | O(1) | DLL has prev pointer |
| Delete head | O(1) | O(1) | Same |
| Delete tail | O(n) | O(1) | DLL has prev pointer |
| Delete given node | O(n) | O(1) | DLL's biggest advantage |
| Traverse forward | O(n) | O(n) | Same |
| Traverse backward | O(n) | O(n) | Both O(n), but DLL is simpler |
| Memory per node | data + 1 ptr | data + 2 ptrs | SLL uses less memory |
| Reverse traversal | O(n) complex | O(n) simple | DLL has prev ptr |
LRU Cache — The Classic Application
LRU (Least Recently Used) Cache is one of the most important data structures in systems programming. It evicts the least recently used item when the cache is full.
Requirements:
get(key)→ O(1) averageput(key, value)→ O(1) average- When cache is full, remove the least recently used item
Solution: Doubly linked list (for O(1) move-to-front and O(1) delete) + Hash Map (for O(1) lookup).
Testing:
lru = LRUCache(capacity=3)
lru.put(1, "one") # Cache: {1:one}
lru.put(2, "two") # Cache: {1:one, 2:two}
lru.put(3, "three") # Cache: {1:one, 2:two, 3:three}
print(lru.get(1)) # "one" → moves 1 to MRU front
lru.put(4, "four") # Evicts 2 (LRU) → Cache: {1:one, 3:three, 4:four}
print(lru.get(2)) # -1 (evicted)
print(lru.get(3)) # "three"Summary
The doubly linked list's key advantages over singly linked list:
- O(1) delete of any node given its reference — via
prevpointer - O(1) insert before a node — via
prevpointer - O(1) delete tail — via
tail.prevpointer - Bidirectional traversal — both forward and backward
Cost: double the pointer memory per node.
The doubly linked list is the backbone of the LRU Cache, which combines it with a hash map to achieve O(1) for both get and put. This data structure is fundamental to OS memory management, database buffer pools, and web browser caches.
*Next Lesson: Circular Linked List — Circular structure for round-robin scheduling and cyclic sequences.*
Exam Focus
Revise definitions, diagrams, examples, and short-answer points for Doubly Linked List — Complete Implementation with Bidirectional Operations.
Interview Use
Prepare one clear explanation, one practical example, and one common mistake for this Data Structures & Algorithms topic.
Search Terms
data-structures-algorithms, data structures & algorithms, data, structures, algorithms, linked, lists, doubly
Related Data Structures & Algorithms Topics