How to Implement a Deque Using a Doubly Linked List in Python
A deque (double-ended queue) is a versatile data structure that supports insertion and deletion at both ends in constant time. While Python's collections.deque provides a built-in implementation, building one from scratch using a doubly linked list deepens your understanding of pointer manipulation and data structure design. This guide walks through a complete implementation with all standard deque operations.
Why Use a Doubly Linked List?
A doubly linked list is the ideal backing structure for a deque because:
- Each node has pointers to both the next and previous nodes
- Insertions and deletions at either end take O(1) time
- No element shifting is required (unlike array-based implementations)
- Memory is allocated per node, so no resizing overhead
None ← [10] ⇄ [20] ⇄ [30] → None
↑ ↑
front rear
Node Structure
Each node in the doubly linked list stores a value and two pointers:
class Node:
"""A node in a doubly linked list."""
def __init__(self, value):
self.value = value
self.next = None
self.prev = None
Complete Deque Implementation
The deque class maintains references to the front and rear nodes, along with a size counter for O(1) size queries:
class Node:
def __init__(self, value):
self.value = value
self.next = None
self.prev = None
class Deque:
"""Deque implementation using a doubly linked list."""
def __init__(self):
self.front = None
self.rear = None
self._size = 0
def is_empty(self):
"""Return True if the deque is empty."""
return self._size == 0
def size(self):
"""Return the number of elements."""
return self._size
def insert_front(self, value):
"""Insert an element at the front."""
new_node = Node(value)
if self.is_empty():
self.front = self.rear = new_node
else:
new_node.next = self.front
self.front.prev = new_node
self.front = new_node
self._size += 1
def insert_rear(self, value):
"""Insert an element at the rear."""
new_node = Node(value)
if self.is_empty():
self.front = self.rear = new_node
else:
new_node.prev = self.rear
self.rear.next = new_node
self.rear = new_node
self._size += 1
def delete_front(self):
"""Remove and return the front element."""
if self.is_empty():
raise IndexError("Delete from an empty deque")
value = self.front.value
self.front = self.front.next
if self.front is None:
self.rear = None
else:
self.front.prev = None
self._size -= 1
return value
def delete_rear(self):
"""Remove and return the rear element."""
if self.is_empty():
raise IndexError("Delete from an empty deque")
value = self.rear.value
self.rear = self.rear.prev
if self.rear is None:
self.front = None
else:
self.rear.next = None
self._size -= 1
return value
def peek_front(self):
"""Return the front element without removing it."""
if self.is_empty():
raise IndexError("Peek from an empty deque")
return self.front.value
def peek_rear(self):
"""Return the rear element without removing it."""
if self.is_empty():
raise IndexError("Peek from an empty deque")
return self.rear.value
def display(self):
"""Return all elements as a list (front to rear)."""
elements = []
current = self.front
while current is not None:
elements.append(current.value)
current = current.next
return elements
def __repr__(self):
return f"Deque({self.display()})"
def __len__(self):
return self._size
Using the Deque
Basic Operations
dq = Deque()
# Insert at rear
dq.insert_rear(10)
dq.insert_rear(20)
dq.insert_rear(30)
print("After rear insertions:", dq)
# Insert at front
dq.insert_front(5)
print("After front insertion:", dq)
# Peek at both ends
print(f"Front: {dq.peek_front()}, Rear: {dq.peek_rear()}")
# Delete from front
removed = dq.delete_front()
print(f"Removed from front: {removed}")
print("After front deletion:", dq)
# Delete from rear
removed = dq.delete_rear()
print(f"Removed from rear: {removed}")
print("After rear deletion:", dq)
# Size and empty check
print(f"Size: {dq.size()}, Empty: {dq.is_empty()}")
Output:
After rear insertions: Deque([10, 20, 30])
After front insertion: Deque([5, 10, 20, 30])
Front: 5, Rear: 30
Removed from front: 5
After front deletion: Deque([10, 20, 30])
Removed from rear: 30
After rear deletion: Deque([10, 20])
Size: 2, Empty: False
Step-by-Step Pointer Visualization
Here is how the internal structure changes with each operation:
dq = Deque()
dq.insert_rear(10)
# front → [10] ← rear
# prev: None, next: None
dq.insert_rear(20)
# front → [10] ⇄ [20] ← rear
dq.insert_front(5)
# front → [5] ⇄ [10] ⇄ [20] ← rear
dq.delete_front() # removes 5
# front → [10] ⇄ [20] ← rear
dq.delete_rear() # removes 20
# front → [10] ← rear
Using the Deque as a Stack or Queue
A deque can function as both a stack (LIFO) and a queue (FIFO):
# As a STACK (Last-In, First-Out)
stack = Deque()
stack.insert_rear(1) # push
stack.insert_rear(2) # push
stack.insert_rear(3) # push
print("Stack pop:", stack.delete_rear()) # pop → 3
print("Stack pop:", stack.delete_rear()) # pop → 2
# As a QUEUE (First-In, First-Out)
queue = Deque()
queue.insert_rear(1) # enqueue
queue.insert_rear(2) # enqueue
queue.insert_rear(3) # enqueue
print("Queue dequeue:", queue.delete_front()) # dequeue → 1
print("Queue dequeue:", queue.delete_front()) # dequeue → 2
Output:
Stack pop: 3
Stack pop: 2
Queue dequeue: 1
Queue dequeue: 2
Common Mistake: Not Handling the Single-Element Case
When the deque has only one element, both front and rear point to the same node. Deleting it requires updating both pointers to None:
dq = Deque()
dq.insert_rear(42) # front and rear both point to [42]
# WRONG implementation (if delete_front only updates front):
# front = front.next → None ✓
# But rear still points to the deleted node! → Memory leak / invalid state
# CORRECT: check if front becomes None after deletion
value = dq.delete_front()
print(f"Removed: {value}")
print(f"Front: {dq.front}") # None
print(f"Rear: {dq.rear}") # None: correctly updated
Output:
Removed: 42
Front: None
Rear: None
When deleting the last element from the deque, always set both front and rear to None. Failing to do so leaves a dangling pointer that causes incorrect behavior on subsequent operations.
Error Handling for Empty Deque Operations
Attempting to delete or peek from an empty deque should raise a clear error:
dq = Deque()
try:
dq.delete_front()
except IndexError as e:
print(f"Error: {e}")
try:
dq.peek_rear()
except IndexError as e:
print(f"Error: {e}")
Output:
Error: Delete from an empty deque
Error: Peek from an empty deque
Always check is_empty() before calling delete_front(), delete_rear(), peek_front(), or peek_rear(), or handle the IndexError exception in your code.
Time and Space Complexity
| Operation | Time Complexity | Description |
|---|---|---|
insert_front() | O(1) | Create node, update 2 pointers |
insert_rear() | O(1) | Create node, update 2 pointers |
delete_front() | O(1) | Update front pointer |
delete_rear() | O(1) | Update rear pointer |
peek_front() | O(1) | Return front value |
peek_rear() | O(1) | Return rear value |
size() | O(1) | Return stored counter |
is_empty() | O(1) | Check counter |
display() | O(n) | Traverse all nodes |
Space Complexity: O(n) where n is the number of elements stored.
All core deque operations run in constant time O(1), which is the primary advantage of the doubly linked list implementation over array-based approaches that may require O(n) shifts for front insertions or deletions.