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 rearNode 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 = NoneComplete 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._sizeUsing the Deque
Basic Operations
dq = Deque()
dq.insert_rear(10)
dq.insert_rear(20)
dq.insert_rear(30)
print("After rear insertions:", dq)
dq.insert_front(5)
print("After front insertion:", dq)
print(f"Front: {dq.peek_front()}, Rear: {dq.peek_rear()}")
removed = dq.delete_front()
print(f"Removed from front: {removed}")
print("After front deletion:", dq)
removed = dq.delete_rear()
print(f"Removed from rear: {removed}")
print("After rear deletion:", dq)
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: FalseStep-by-Step Pointer Visualization
Here is how the internal structure changes with each operation:
dq = Deque()
dq.insert_rear(10)
dq.insert_rear(20)
dq.insert_front(5)
dq.delete_front() # removes 5
dq.delete_rear() # removes 20Using the Deque as a Stack or Queue
A deque can function as both a stack (LIFO) and a queue (FIFO):
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
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 → 2Output:
Stack pop: 3
Stack pop: 2
Queue dequeue: 1
Queue dequeue: 2Common 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]
value = dq.delete_front()
print(f"Removed: {value}")
print(f"Front: {dq.front}") # None
print(f"Rear: {dq.rear}") # None: correctly updatedOutput:
Removed: 42
Front: None
Rear: NoneError 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 dequeTime 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.