Heap, Stack, and Queue¶
Three core data structures implemented from scratch, the building blocks behind priority scheduling, undo systems, and task queues that show up everywhere in ML infrastructure.
Problem¶
Implement the following data structures:
- Stack, LIFO using a linked list
- Queue, FIFO using two stacks (amortized O(1) dequeue)
- Min-Heap, a priority queue with insert and extract-min
Approach¶
Stack: a singly linked list where push and pop operate on the head. O(1) for both.
Queue via two stacks: enqueue pushes to an inbound stack. Dequeue pops from an outbound stack; when outbound is empty, reverse inbound into outbound. Each element moves at most twice, giving amortized O(1) per operation.
Min-heap: a complete binary tree stored in an array. After insertion, "float" the element up by swapping with its parent while it's smaller. After extraction, move the last element to the root and "sink" it down by swapping with the smaller child.
Implementation¶
Stack (Linked List)¶
class Node:
def __init__(self, data):
self.data = data
self.next = None
class Stack:
def __init__(self):
self.top = None
def push(self, data):
node = Node(data)
node.next = self.top
self.top = node
def pop(self):
if not self.top:
return None
data = self.top.data
self.top = self.top.next
return data
def peek(self):
return self.top.data if self.top else None
Queue (Two Stacks)¶
class Queue:
def __init__(self):
self.inbound = []
self.outbound = []
def enqueue(self, data):
self.inbound.append(data)
def dequeue(self):
if not self.outbound:
while self.inbound:
self.outbound.append(self.inbound.pop())
return self.outbound.pop() if self.outbound else None
Min-Heap¶
class MinHeap:
def __init__(self):
self.heap = [0] # index 0 unused; tree starts at index 1
self.size = 0
def insert(self, item):
self.heap.append(item)
self.size += 1
self._float(self.size)
def pop(self):
if self.size == 0:
return None
root = self.heap[1]
self.heap[1] = self.heap[self.size]
self.size -= 1
self.heap.pop()
if self.size > 0:
self._sink(1)
return root
def _float(self, k):
while k // 2 > 0:
if self.heap[k] < self.heap[k // 2]:
self.heap[k], self.heap[k // 2] = self.heap[k // 2], self.heap[k]
k //= 2
def _sink(self, k):
while k * 2 <= self.size:
mc = self._min_child(k)
if self.heap[k] > self.heap[mc]:
self.heap[k], self.heap[mc] = self.heap[mc], self.heap[k]
else:
break
k = mc
def _min_child(self, k):
if k * 2 + 1 > self.size:
return k * 2
return k * 2 if self.heap[k * 2] < self.heap[k * 2 + 1] else k * 2 + 1
Complexity¶
| Structure | Insert | Remove | Peek |
|---|---|---|---|
| Stack | O(1) | O(1) | O(1) |
| Queue (two stacks) | O(1) | O(1) amortized | , |
| Min-Heap | O(log n) | O(log n) | O(1) |
- Space: O(n) for all three.
Takeaway¶
Heaps power priority queues in beam search, A* pathfinding, and top-k retrieval. The two-stack queue is a common interview question, but it also shows up in real systems where you need FIFO semantics from LIFO primitives.