Intermediate Projects
Custom Stack and Queue
Building a custom MinStack<T> and a custom circular-buffer RingQueue<T> from scratch demonstrates how to maintain auxiliary $O(1)$ aggregate invariants alongside LIFO/FIFO operations and how to manage uninitialized circular buffer slots safely with MaybeUninit<T>.
1. `MinStack<T>` (O(1) `push`, `pop`, `top`, AND `min`!):
Each entry stores `(value, current_stack_minimum)`:
Push 30 -> [ (30, min: 30) ]
Push 10 -> [ (30, min: 30), (10, min: 10) ]
Push 20 -> [ (30, min: 30), (10, min: 10), (20, min: 10) ]
2. `RingQueue<T, CAP>` (Fixed-Capacity Circular Queue with `head`, `tail`, `len`):
Index: 0 1 2 3
+------+------+------+------+
| B | C | . | A | `head = 3`, `len = 3`
+------+------+------+------+ `tail = (head + len) % CAP = 2`
1. $O(1)$ Minimum-Tracking Stack (MinStack<T>)
A standard stack finds its minimum element in $O(n)$ time by scanning all elements. By storing each element alongside the minimum value at or below that stack depth, MinStack<T> answers .min() in $O(1)$ time even as elements are pushed and popped!
2. Fixed-Capacity Circular Queue (RingQueue<T, CAP>)
Using modular index arithmetic (self.head + self.len) % CAP inside a fixed-capacity array [Option<T>; CAP] implements a zero-heap-allocation FIFO queue with $O(1)$ enqueue and dequeue:
/// 1. LIFO Stack that answers `.min()` in O(1) worst-case time!
#[derive(Debug, Default)]
pub struct MinStack<T> {
// Stores `(element, min_up_to_this_depth)`
entries: Vec<(T, T)>,
}
impl<T: Ord + Clone> MinStack<T> {
pub const fn new() -> Self {
Self {
entries: Vec::new(),
}
}
pub fn push(&mut self, value: T) {
let new_min = match self.entries.last() {
Some((_, cur_min)) if *cur_min < value => cur_min.clone(),
_ => value.clone(),
};
self.entries.push((value, new_min));
}
pub fn pop(&mut self) -> Option<T> {
self.entries.pop().map(|(val, _)| val)
}
pub fn top(&self) -> Option<&T> {
self.entries.last().map(|(val, _)| val)
}
/// Returns the smallest element currently in the stack in O(1) time!
pub fn min(&self) -> Option<&T> {
self.entries.last().map(|(_, cur_min)| cur_min)
}
}
/// 2. Fixed-capacity circular FIFO Queue backed by an inline array (`0` heap allocations!).
pub struct RingQueue<T, const CAP: usize> {
slots: [Option<T>; CAP],
head: usize,
len: usize,
}
impl<T, const CAP: usize> RingQueue<T, CAP> {
pub const fn new() -> Self {
const {
assert!(CAP > 0, "RingQueue capacity must be > 0");
}
Self {
slots: [const { None }; CAP],
head: 0,
len: 0,
}
}
pub fn enqueue(&mut self, item: T) -> Result<(), T> {
if self.len == CAP {
return Err(item); // Queue is full; return ownership of `item` to caller
}
let tail = (self.head + self.len) % CAP;
self.slots[tail] = Some(item);
self.len += 1;
Ok(())
}
pub fn dequeue(&mut self) -> Option<T> {
if self.len == 0 {
return None;
}
let item = self.slots[self.head].take();
self.head = (self.head + 1) % CAP;
self.len -= 1;
item
}
pub fn peek(&self) -> Option<&T> {
if self.len == 0 {
None
} else {
self.slots[self.head].as_ref()
}
}
pub const fn len(&self) -> usize {
self.len
}
pub const fn is_empty(&self) -> bool {
self.len == 0
}
}
fn main() {
// 1. Verify O(1) MinStack:
let mut stack = MinStack::new();
stack.push(30);
stack.push(10);
stack.push(20);
stack.push(5);
assert_eq!(stack.min(), Some(&5));
assert_eq!(stack.pop(), Some(5));
assert_eq!(stack.min(), Some(&10));
println!("MinStack after popping 5: top={:?}, min={:?}", stack.top(), stack.min());
// 2. Verify wrap-around in RingQueue<_, 3>:
let mut queue = RingQueue::<&str, 3>::new();
queue.enqueue("job_1").unwrap();
queue.enqueue("job_2").unwrap();
queue.enqueue("job_3").unwrap();
assert_eq!(queue.enqueue("overflow"), Err("overflow"));
assert_eq!(queue.dequeue(), Some("job_1"));
// Enqueueing "job_4" wraps around to physical slot 0!
queue.enqueue("job_4").unwrap();
let drained = [queue.dequeue(), queue.dequeue(), queue.dequeue()];
assert_eq!(drained, [Some("job_2"), Some("job_3"), Some("job_4")]);
println!("RingQueue wrapped and drained in FIFO order: {drained:?}");
}
MinStack after popping 5: top=Some(20), min=Some(10)
RingQueue wrapped and drained in FIFO order: [Some("job_2"), Some("job_3"), Some("job_4")]