A deque (double-ended queue) is a linear collection that supports insertion and removal at both Ends. It generalises both stacks (LIFO) and queues (FIFO).
Operation Description Array-backed Linked-list push_front(x)Insert at front O ( 1 ) O(1) O ( 1 ) O ( 1 ) O(1) O ( 1 ) push_back(x)Insert at back O ( 1 ) O(1) O ( 1 ) O ( 1 ) O(1) O ( 1 ) pop_front()Remove from front O ( 1 ) O(1) O ( 1 ) O ( 1 ) O(1) O ( 1 ) pop_back()Remove from back O ( 1 ) O(1) O ( 1 ) O ( 1 ) O(1) O ( 1 ) front()Access front element O ( 1 ) O(1) O ( 1 ) O ( 1 ) O(1) O ( 1 ) back()Access back element O ( 1 ) O(1) O ( 1 ) O ( 1 ) O(1) O ( 1 ) is_empty()Check if empty O ( 1 ) O(1) O ( 1 ) O ( 1 ) O(1) O ( 1 ) size()Number of elements O ( 1 ) O(1) O ( 1 ) O ( 1 ) O(1) O ( 1 )
A circular buffer (ring buffer) implements a deque using a fixed-size array with two indices (head And tail) that wrap around.
class CircularBufferDeque :
Deque using a circular buffer (dynamic array).
Time: O(1) amortised for all operations
def __init__ ( self , capacity = 16 ):
self .data = [ None ] * capacity
def push_front ( self , value ):
if self .size == self .capacity:
self .head = ( self .head - 1 ) % self .capacity
self .data[ self .head] = value
def push_back ( self , value ):
if self .size == self .capacity:
self .data[ self .tail] = value
self .tail = ( self .tail + 1 ) % self .capacity
raise IndexError ( " pop from empty deque " )
value = self .data[ self .head]
self .head = ( self .head + 1 ) % self .capacity
raise IndexError ( " pop from empty deque " )
self .tail = ( self .tail - 1 ) % self .capacity
value = self .data[ self .tail]
raise IndexError ( " front of empty deque " )
return self .data[ self .head]
raise IndexError ( " back of empty deque " )
return self .data[( self .tail - 1 ) % self .capacity]
new_data = [ None ] * ( self .capacity * 2 )
for i in range ( self .size):
new_data[i] = self .data[( self .head + i) % self .capacity]
graph LR
subgraph Circular Buffer
direction LR
A[5] --> B[12] --> C[7] --> D[3] --> E[.] --> F[.] --> G[.] --> H[.]
end
HEAD["head=2"] -.-> C
TAIL["tail=5"] -.-> E __slots__ = ( " val', 'prev', 'next')
Deque using a doubly-linked list with sentinel nodes.
Time: O(1) for all operations
Space: O(n) — one node per element plus two sentinels
self .sentinel = DequeNode( None )
self .sentinel.prev = self .sentinel
self .sentinel.next = self .sentinel
def push_front( self , value):
node.next = self .sentinel.next
node.prev = self .sentinel
self .sentinel.next.prev = node
self .sentinel.next = node
def push_back( self , value):
node.prev = self .sentinel.prev
node.next = self .sentinel
self .sentinel.prev.next = node
self .sentinel.prev = node
raise IndexError ( " pop from empty deque " )
node = self .sentinel.next
node.next.prev = self .sentinel
self .sentinel.next = node.next
raise IndexError ( " pop from empty deque " )
node = self .sentinel.prev
node.prev.next = self .sentinel
self .sentinel.prev = node.prev
Language Type Implementation Notes Python collections.dequeCircular buffer O ( 1 ) O(1) O ( 1 ) all operationsC++ std::dequeSegmented array O ( 1 ) O(1) O ( 1 ) all operationsJava ArrayDequeCircular buffer O ( 1 ) O(1) O ( 1 ) all operationsRust VecDequeRing buffer O ( 1 ) O(1) O ( 1 ) all operationsGo None (use slice) N/A Manual implementation needed
Block size is 64 elements). This gives $O(1)$ amortised operations with good cache locality — much Better than a naive linked list but slightly worse than a pure circular buffer for sequential Access.A priority queue supports inserting elements with associated priorities and extracting the element With the highest (or lowest) priority.
Operation Description Binary Heap Binomial Heap Fibonacci Heap insert(x)Insert element with priority O ( log n ) O(\log n) O ( log n ) O ( log n ) O(\log n) O ( log n ) O ( 1 ) O(1) O ( 1 ) amort.extract_min()Remove and return minimum element O ( log n ) O(\log n) O ( log n ) O ( log n ) O(\log n) O ( log n ) O ( log n ) O(\log n) O ( log n ) amort.decrease_key(x, k)Decrease priority of element x O ( log n ) O(\log n) O ( log n ) O ( log n ) O(\log n) O ( log n ) O ( 1 ) O(1) O ( 1 ) amort.find_min()Return minimum without removing O ( 1 ) O(1) O ( 1 ) O ( 1 ) O(1) O ( 1 ) O ( 1 ) O(1) O ( 1 ) delete(x)Remove arbitrary element O ( log n ) O(\log n) O ( log n ) O ( log n ) O(\log n) O ( log n ) O ( log n ) O(\log n) O ( log n ) amort.merge(q1, q2)Merge two priority queues O ( n ) O(n) O ( n ) O ( log n ) O(\log n) O ( log n ) O ( 1 ) O(1) O ( 1 ) amort.
A binary heap is a complete binary tree that satisfies the heap property: each node is smaller than (min-heap) or greater than (max-heap) its children.
For a node at index i i i (0-based):
Relationship Formula Parent ⌊ ( i − 1 ) / 2 ⌋ \lfloor (i - 1) / 2 \rfloor ⌊( i − 1 ) /2 ⌋ Left child 2 i + 1 2i + 1 2 i + 1 Right child 2 i + 2 2i + 2 2 i + 2
graph TD
H[1] --> HL[3]
H --> HR[5]
HL --> HLL[4]
HL --> HLR[8]
HR --> HRL[7]
HR --> HRR[9]
HRL --> HRLL[10]
style H fill:#e74c3c,color:#fff
style HL fill:#e67e22,color:#fff
style HR fill:#e67e22,color:#fff Array: [1, 3, 5, 4, 8, 7, 9, 10]
Given an arbitrary array, rearrange it into a valid heap.
Build a max-heap from an unsorted array.
Time: O(n) — NOT O(n log n). See proof below.
if left < size and arr[left] > arr[largest]:
if right < size and arr[right] > arr[largest]:
arr[i], arr[largest] = arr[largest], arr[i]
# Start from the last non-leaf node and work up
for i in range (n // 2 - 1 , - 1 , - 1 ):
$O(h)$. There are at most $\lceil n / 2^{h+1} \rceil$ nodes at height $h$. The total cost is $\sum_{h=0}^{\lfloor \log n \rfloor} \lceil n / 2^{h+1} \rceil \cdot O(h) = O(n \sum_{h=0}^{\infty} h / 2^{h}) = O(n)$. The key insight is that most nodes are near the bottom of the tree and require little or no sifting. Min-heap with insert, extract-min, and decrease-key.
Time: O(log n) per insert/extract, O(1) for peek.
raise IndexError ( " peek from empty heap " )
self ._sift_up( len ( self .heap) - 1 )
raise IndexError ( " extract from empty heap " )
self .heap[ 0 ] = self .heap.pop()
def decrease_key ( self , index , new_value ):
if new_value > self .heap[index]:
raise ValueError ( " new value must be smaller " )
self .heap[index] = new_value
if self .heap[i] >= self .heap[parent]:
self .heap[i], self .heap[parent] = self .heap[parent], self .heap[i]
if left < n and self .heap[left] < self .heap[smallest]:
if right < n and self .heap[right] < self .heap[smallest]:
self .heap[i], self .heap[smallest] = self .heap[smallest], self .heap[i]
Time: O(n log n) worst case
if left < size and arr[left] > arr[largest]:
if right < size and arr[right] > arr[largest]:
arr[i], arr[largest] = arr[largest], arr[i]
for i in range (n - 1 , 0 , - 1 ):
arr[ 0 ], arr[i] = arr[i], arr[ 0 ]
A d-ary heap is a generalisation where each node has up to d d d children. For a node at index i i i (0-based):
Relationship Formula Parent ⌊ ( i − 1 ) / d ⌋ \lfloor (i - 1) / d \rfloor ⌊( i − 1 ) / d ⌋ Child j j j d ⋅ i + j + 1 d \cdot i + j + 1 d ⋅ i + j + 1 for 0 ≤ j < d 0 \le j \lt d 0 ≤ j < d
Metric Binary (d = 2 d=2 d = 2 ) 4-ary (d = 4 d=4 d = 4 ) Choice of d d d Height O ( log 2 n ) O(\log_2 n) O ( log 2 n ) O ( log 4 n ) O(\log_4 n) O ( log 4 n ) d = E / V + 1 d = E/V + 1 d = E / V + 1 optimises DijkstraSift-down cost O ( d log d n ) O(d \log_d n) O ( d log d n ) O ( d log d n ) O(d \log_d n) O ( d log d n ) Larger d d d = fewer levels but more compares Sift-up cost O ( log d n ) O(\log_d n) O ( log d n ) O ( log d n ) O(\log_d n) O ( log d n ) Smaller d d d = cheaper sift-up
For Dijkstra’s algorithm on sparse graphs (E = O ( V ) E = O(V) E = O ( V ) ), d = 2 d = 2 d = 2 is optimal. For dense graphs (E = O ( V 2 ) E = O(V^2) E = O ( V 2 ) ), d ≈ V / 2 d \approx V/2 d ≈ V /2 gives the best performance because sift-down is called more often Than sift-up.
A binomial heap is a collection of binomial trees that supports efficient merge. It is the basis for The Fibonacci heap.
A binomial tree B k B_k B k is defined recursively:
B 0 B_0 B 0 is a single nodeB k B_k B k is formed by linking two B k − 1 B_{k-1} B k − 1 trees: one becomes the leftmost child of the other’s rootProperties of B k B_k B k :
Property Value Number of nodes 2 k 2^k 2 k Height k k k Root degree k k k Children of root B k − 1 , B k − 2 , … , B 0 B_{k-1}, B_{k-2}, \ldots, B_0 B k − 1 , B k − 2 , … , B 0
Binomial heap: merge-able priority queue.
Insert: O(log n) worst case
Extract-min: O(log n) worst case
Merge: O(log n) worst case
def _merge_roots ( self , h1 , h2 ):
"""Merge two root lists sorted by degree. O(log n)."""
if h1.degree <= h2.degree:
if h1.degree <= h2.degree:
tail.sibling = h1 if h1 else h2
"""Make y a child of z. y.key >= z.key."""
"""Merge and consolidate. O(log n)."""
self .head = self ._merge_roots( self .head, h.head)
if (curr.degree != next_node.degree) or \
(next_node.sibling and next_node.sibling.degree == curr.degree):
elif curr.key <= next_node.key:
curr.sibling = next_node.sibling
self ._link(next_node, curr)
self ._link(curr, next_node)
new_heap = BinomialHeap()
new_heap.head = BinomialNode(key)
if curr.key < min_node.key:
min_prev.sibling = min_node.sibling
self .head = min_node.sibling
child_heap = BinomialHeap()
next_child = child.sibling
child.sibling = reversed_list
child_heap.head = reversed_list
A Fibonacci heap is a collection of min-heap-ordered trees that supports amortised O ( 1 ) O(1) O ( 1 ) insert and Decrease-key, making it asymptotically optimal for algorithms like Dijkstra’s and Prim’s.
Unlike binomial heaps, Fibonacci heaps do not consolidate trees on every operation. Instead, they Perform consolidation only during extract_min. This laziness is what gives amortised O ( 1 ) O(1) O ( 1 ) for insert and decrease_key.
Operation Amortised Time Worst Case Mechanism Insert O ( 1 ) O(1) O ( 1 ) O ( 1 ) O(1) O ( 1 ) Add tree to root list Find-min O ( 1 ) O(1) O ( 1 ) O ( 1 ) O(1) O ( 1 ) Pointer to minimum root Extract-min O ( log n ) O(\log n) O ( log n ) O ( n ) O(n) O ( n ) Consolidate trees during extraction Decrease-key O ( 1 ) O(1) O ( 1 ) O ( log n ) O(\log n) O ( log n ) Cut and cascade if parent marked Delete O ( log n ) O(\log n) O ( log n ) O ( n ) O(n) O ( n ) Decrease-key to − ∞ -\infty − ∞ then extract-min Merge O ( 1 ) O(1) O ( 1 ) O ( 1 ) O(1) O ( 1 ) Concatenate root lists
When a node loses its first child, it is marked. When it loses a second child, it is cut from its Parent and added to the root list. This cascading ensures the tree structure does not degrade too Badly — the degree of any node is bounded by O ( log ϕ n ) O(\log_\phi n) O ( log ϕ n ) where ϕ = ( 1 + 5 ) / 2 \phi = (1 + \sqrt{5}) / 2 ϕ = ( 1 + 5 ) /2 .
Fibonacci heap with amortised O(1) insert and decrease-key.
Extract-min: O(log n) amortised
Decrease-key: O(1) amortised
if self .min_node is None :
self ._add_to_root_list(node)
if node.key < self .min_node.key:
def _add_to_root_list ( self , node ):
node.left = self .min_node
node.right = self .min_node.right
self .min_node.right.left = node
self .min_node.right = node
self ._add_to_root_list(child)
return z.key if z else None
max_degree = int (math.log( self .total_nodes) / math.log(( 1 + math.sqrt( 5 )) / 2 )) + 1
degree_to_tree = [ None ] * (max_degree + 1 )
for tree in degree_to_tree:
if self .min_node is None or tree.key < self .min_node.key:
def decrease_key ( self , node , new_key ):
raise ValueError ( " new key is greater than current key " )
if parent and node.key < parent.key:
self ._cascading_cut(parent)
if node.key < self .min_node.key:
self ._add_to_root_list(x)
def _cascading_cut ( self , y ):
Fibonacci heaps have high constant factors (due to the complex pointer manipulation and lazy Structure). They are asymptotically better than binary heaps only when decrease_key is called many Times relative to extract_min. In practice:
Algorithm Binary Heap Time Fibonacci Heap Time Practical Winner Dijkstra (sparse) O ( ( V + E ) log V ) O((V+E) \log V) O (( V + E ) log V ) O ( V log V + E ) O(V \log V + E) O ( V log V + E ) Binary heap Dijkstra (dense) O ( V 2 log V ) O(V^2 \log V) O ( V 2 log V ) O ( V 2 ) O(V^2) O ( V 2 ) Fibonacci heap Prim (sparse) O ( ( V + E ) log V ) O((V+E) \log V) O (( V + E ) log V ) O ( V log V + E ) O(V \log V + E) O ( V log V + E ) Binary heap Prim (dense) O ( V 2 log V ) O(V^2 \log V) O ( V 2 log V ) O ( V 2 ) O(V^2) O ( V 2 ) Fibonacci heap
Heaps (or 4-ary heaps) are almost always faster in practice. Pairing heaps are a simpler alternative That achieves the same amortised bounds for most operations.A pairing heap is a simplified alternative to Fibonacci heaps. It is a self-adjusting heap that Supports all operations in amortised O ( log n ) O(\log n) O ( log n ) except insert and find-min which are O ( 1 ) O(1) O ( 1 ) . The Decrease-key operation is conjectured to be O ( 1 ) O(1) O ( 1 ) amortised, but this has only been proven for Special cases.
Pairing heap — simpler Fibonacci heap alternative.
Extract-min: O(log n) amortised
Decrease-key: O(log n) amortised (O(1) conjectured)
self .root = self ._merge( self .root, node)
raise IndexError ( " empty heap " )
def _merge ( self , h1 , h2 ):
raise IndexError ( " empty heap " )
self .root = self ._two_pass_merge(child)
def _two_pass_merge ( self , first ):
if first is None or first.sibling is None :
while first and first.sibling:
next_pair = first.sibling.sibling
pairs.append( self ._merge(first, first.sibling))
for i in range ( len (pairs) - 2 , - 1 , - 1 ):
result = self ._merge(result, pairs[i])
self .root = self ._merge( self .root, other.root)
def dijkstra_pq ( graph , source ):
Dijkstra using binary heap priority queue.
dist = {v: float ( ' inf ' ) for v in graph}
prev = {v: None for v in graph}
heapq.heappush(pq, (new_dist, v))
from collections import Counter
Build Huffman codes from character frequencies.
Time: O(n log n) where n = number of unique characters
heap = [(count, i, char) for i, (char, count) in enumerate (freq.items())]
left_count, _, left_node = heapq.heappop(heap)
right_count, _, right_node = heapq.heappop(heap)
merged = (left_count + right_count, counter)
parent[left_node] = merged
parent[right_node] = merged
heapq.heappush(heap, (merged[ 0 ], merged[ 1 ], merged))
code.append( ' 0 ' if parent[node][ 0 ] == merged[ 0 ] and \
isinstance (node, str ) and left_count else ' 1 ' )
codes[char] = '' .join( reversed (code)) if code else ' 0 '
def merge_k_sorted ( lists ):
Merge k sorted lists using a min-heap.
Time: O(N log k) where N = total elements, k = number of lists
for i, lst in enumerate (lists):
heapq.heappush(heap, (lst[ 0 ], i, 0 ))
val, list_idx, elem_idx = heapq.heappop(heap)
if elem_idx + 1 < len (lists[list_idx]):
next_val = lists[list_idx][elem_idx + 1 ]
heapq.heappush(heap, (next_val, list_idx, elem_idx + 1 ))
Discrete event simulation using a priority queue.
Time: O(E log E) where E = number of events
def schedule ( self , time_delta , event_fn , * args ):
event_time = self .clock + time_delta
heapq.heappush( self .events, (event_time, event_fn, args))
event_time, event_fn, args = heapq.heappop( self .events)
Python heapq:
min_val = heapq.heappop(heap) # 2
merged = heapq.merge([ 1 , 3 , 5 ], [ 2 , 4 , 6 ])
C++ std::priority_queue:
std :: priority_queue <int> max_pq;
int top = max_pq. top (); // 5
std :: priority_queue <int , std :: vector <int> , std :: greater <int>> min_pq;
auto cmp = []( const pair < int , int > & a , const pair < int , int > & b ) {
return a.second > b.second;
std :: priority_queue < pair < int , int >, vector < pair < int , int >>, decltype (cmp)> pq ( cmp );
`(-neg_x, x)`. C++ `std::priority_queue` is a **max-heap** by default; use `std::greater` for a Min-heap.Python’s heapq is a min-heap, while C++ std::priority_queue is a max-heap by default. Forgetting This leads to extracting the wrong element. Always verify the heap property before using it in an Algorithm.
Binary heaps do not support efficient decrease_key in their standard library implementations. Modifying an element’s priority in-place and calling heapify is O ( n ) O(n) O ( n ) . If you need efficient decrease_keyUse a Fibonacci heap or a custom heap with a position map.
If priorities are computed as sums or products of other values, they can overflow 32-bit integers. In competitive programming and systems code, use 64-bit integers. In Python, this is not an issue.
Dijkstra’s algorithm requires non-negative edge weights. If the graph has negative weights, use Bellman-Ford (O ( V E ) O(VE) O ( V E ) ) instead. A common mistake is to shift all weights to be positive (adding a Constant to each edge), which changes the shortest paths.
When two elements have the same priority, the order of extraction depends on the tie-breaking rule. In Python, if the second element of the heap tuple is not comparable, you get a TypeError. Always Include a unique identifier as a tiebreaker: (priority, counter, data).
heapq.nlargest(k, iterable) is O ( n log k ) O(n \log k) O ( n log k ) But for small k k k (e.g., k = 1 k = 1 k = 1 ), it is faster than Sorting (O ( n log n ) O(n \log n) O ( n log n ) ). However, if k k k is close to n n n Sorting is faster. The threshold is Approximately k = n / 1000 k = n / 1000 k = n /1000 .
When implementing a circular buffer, the most common bugs are: (1) confusing full and empty states (both occur when head == tail), (2) incorrect modular arithmetic when resizing, and (3) forgetting To handle the wrap-around when iterating. Using a separate size counter (rather than inferring it From head and tail) avoids the full/empty ambiguity.
The degree bound D ( n ) = O ( log ϕ n ) D(n) = O(\log_\phi n) D ( n ) = O ( log ϕ n ) for Fibonacci heaps depends on the cascading cut mechanism Working correctly. If you forget to mark a node when cutting its child, the degree can grow Unbounded, and the amortised bounds break down.
This topic covers the core concepts of deques and priority queues, including underlying theory, practical implementation, and key applications.
Key concepts include:
Big O notation and complexity analysis searching algorithms (binary, linear) sorting algorithms (bubble, merge, quick) graph algorithms (Dijkstra, BFS, DFS) dynamic programming Understanding these concepts thoroughly is essential for both examinations and practical programming, and requires both theoretical knowledge and hands-on practice.
Worked examples demonstrating the application of key concepts are covered in the detailed sub-pages linked above.
Linked Lists — Deques can be implemented using doubly-linked lists for O(1) operations at both ends.Stacks and Queues — Deques generalise both stacks and queues, supporting LIFO and FIFO operations.Binary Search Trees — Priority queues are used in tree balancing algorithms and heap operations.Graph Algorithms — Priority queues are essential for Dijkstra’s and Prim’s algorithms.A deque (double-ended queue) generalizes both stacks and queues by allowing insertion and removal at both ends in O(1) time. The classic implementation uses a circular buffer — an array with head and tail pointers that wrap around using modular arithmetic. This gives excellent cache performance compared to linked lists because elements are contiguous in memory. Deques are the underlying data structure for sliding window algorithms (like finding the maximum in every window of size k) and for work-stealing thread pools where tasks are dequeued from one end and pushed to the other.
Heaps are the simplest priority queue: a complete binary tree stored as an array, where the parent is always smaller (min-heap) or larger (max-heap) than its children. The key insight is that this array representation is extremely cache-friendly, and the tree structure guarantees O(log n) insert and extract-min. Building a heap from scratch is O(n), not O(n log n), because most nodes are near the bottom and need little sifting. Binary heaps are the default in practice (Python’s heapq, Java’s PriorityQueue) because their low constant factors beat more sophisticated heaps.
Fibonacci heaps achieve theoretically optimal O(1) amortized insert and decrease-key by deferring consolidation until extract-min. This laziness is brilliant for algorithms like Dijkstra’s, where decrease-key is called much more often than extract-min. However, the complex pointer manipulation and poor cache behavior mean binary heaps are almost always faster in practice for sparse graphs. Pairing heaps offer a simpler alternative with similar amortized bounds. The lesson: theoretical asymptotic complexity is only part of the story — constant factors, cache behavior, and implementation complexity matter enormously in real code.