Doubly Linked List
I will skip the basic definition here and focus on implementation.
A doubly linked list is useful because we can add or remove nodes in O(1) time when we already have a reference to the target node.
The downside is that we cannot do random access by index like we can with an array.
This data structure is especially useful in write-heavy scenarios.
Why implementation is tricky
Implementing a doubly linked list is harder than it first appears because there are many edge cases.
You need to update:
- the
prevpointer - the
nextpointer - the
headside - the
tailside
When adding or removing a node, all of these pointers must stay consistent.
Better Approach: Use dummy nodes
Most edge cases come from None handling at the head and tail of the list.
So instead of letting the list start or end with real nodes, we create two dummy nodes:
- a dummy head
- a dummy tail
All operations happen between these two nodes, so the boundary structure never changes.
class Node:
def __init__(self, key: int = 0, value: int = 0):
self.key = key
self.value = value
self.prev = None
self.next = None
class DoublyLinkedList:
def __init__(self):
self.head = Node() # dummy head
self.tail = Node() # dummy tail
self.head.next = self.tail
self.tail.prev = self.head
Add a node to the front
Now we can implement add_first:
class DoublyLinkedList:
def add_first(self, node: Node):
self._insert_between(self.head, node, self.head.next)
def _insert_between(self, prev, target, next):
prev.next = target
target.prev = prev
target.next = next
next.prev = target
Remove a node
Next, implement remove:
class DoublyLinkedList:
def remove(self, node: Node):
prev = node.prev
next = node.next
prev.next = next
next.prev = prev
node.next = None
node.prev = None
Move a node to the front
move_to_front becomes simple:
class DoublyLinkedList:
def move_to_front(self, node: Node):
self.remove(node)
self.add_first(node)
Remove the last real node
Finally, implement remove_last:
class DoublyLinkedList:
def remove_last(self) -> Node:
if self.head.next == self.tail:
raise IndexError
last = self.tail.prev
self.remove(last)
return last
Appendix: Use it for an LRU cache
This structure is a natural fit for an LRU cache.
The dictionary gives O(1) access by key, and the doubly linked list gives O(1) updates to recency order.
class LRUCache:
def __init__(self, capacity: int):
self._capacity = capacity
self._nodes = {}
self._ddl = DoublyLinkedList()
def get(self, key):
node = self._nodes.get(key, None)
if not node:
return -1
self._ddl.move_to_front(node)
return node.value
def put(self, key: int, value: int):
node = self._nodes.get(key, None)
if node:
node.value = value
self._ddl.move_to_front(node)
return
new_node = Node(key, value)
self._ddl.add_first(new_node)
self._nodes[key] = new_node
if len(self._nodes) > self._capacity:
last = self._ddl.remove_last()
del self._nodes[last.key]