Rust and Data Structures

Binary Search Trees

While textbook Binary Search Trees (BSTs) store 1 key per heap-allocated node with left and right child pointers, pointer-per-node binary trees perform poorly on modern CPUs because every step down the tree chases a pointer to a separate cache line. Consequently, Rust's standard library provides BTreeMap<K, V> and BTreeSet<T>—cache-optimized B-Trees that store $B - 1$ to $2B - 1$ sorted keys contiguously inside each node (where $B = 6$, i.e., up to 11 keys per node).

1. Pointer-Based Binary Search Tree (1 Key per Node -> High Cache Miss Rate):
         [ 50 ]
        /      \        (Every comparison chases an 8-byte pointer to a random heap address)
    [ 20 ]    [ 80 ]

2. Rust `std::collections::BTreeMap` (B-Tree with up to 11 Keys per Contiguous Node):
         [ 20 | 35 | 50 | 65 | 80 | ... ]  (Fits in 1-2 CPU Cache Lines!)
        /     |    |    |    |     \
     [...]  [...] [...] [...] [...] [...]  (Shallow tree depth; $O(\log n)$ with tiny constant!)

When to Choose BTreeMap Over HashMap

Requirement HashMap<K, V> BTreeMap<K, V>
Required Key Trait Eq + Hash Ord (Total ordering)
Point Lookup / Insert $O(1)$ expected $O(\log n)$
Sorted Iteration No (Randomized order) Yes ($O(n)$ in-order traversal from smallest to largest key!)
Minimum / Maximum Key $O(n)$ full scan $O(\log n)$ (first_key_value, last_key_value, pop_first, pop_last)
Range Queries (a..=b) $O(n)$ full scan $O(\log n + k)$ (map.range(start..=end) yields only keys in range!)
Prefix / Floor / Ceiling Lookup Not supported map.range(..=target).next_back() finds largest key $\le \text{target}$ in $O(\log n)$!

Range Queries and Floor/Ceiling Lookups with BTreeMap::range

One of the most powerful capabilities of BTreeMap and BTreeSet is double-ended range iteration (map.range(bounds)). Because the returned Range iterator implements DoubleEndedIterator, you can find both the ceiling (smallest key $\ge x$) and floor (largest key $\le x$) in $O(\log n)$ time:

use std::collections::BTreeMap;
use std::ops::Bound::{Included, Excluded};

pub struct TimeSeriesIndex {
    events: BTreeMap<u64, &'static str>,
}

impl TimeSeriesIndex {
    pub fn new() -> Self {
        Self {
            events: BTreeMap::new(),
        }
    }

    pub fn insert(&mut self, timestamp: u64, label: &'static str) {
        self.events.insert(timestamp, label);
    }

    /// Returns all events in the inclusive window `[start_ts, end_ts]` in O(log n + k) time.
    pub fn query_window(&self, start_ts: u64, end_ts: u64) -> Vec<(u64, &'static str)> {
        self.events
            .range((Included(start_ts), Included(end_ts)))
            .map(|(&ts, &label)| (ts, label))
            .collect()
    }

    /// Finds the most recent event at or before `timestamp` (Floor query) in O(log n) time!
    pub fn floor_event(&self, timestamp: u64) -> Option<(u64, &'static str)> {
        self.events
            .range(..=timestamp)
            .next_back()
            .map(|(&ts, &label)| (ts, label))
    }

    /// Finds the next event strictly after `timestamp` (Strict Ceiling query) in O(log n) time!
    pub fn next_event_after(&self, timestamp: u64) -> Option<(u64, &'static str)> {
        self.events
            .range((Excluded(timestamp), std::ops::Bound::Unbounded))
            .next()
            .map(|(&ts, &label)| (ts, label))
    }
}

fn main() {
    let mut index = TimeSeriesIndex::new();
    index.insert(100, "boot");
    index.insert(250, "connected");
    index.insert(400, "checkpoint");
    index.insert(600, "shutdown");

    let window = index.query_window(200, 450);
    assert_eq!(window, vec![(250, "connected"), (400, "checkpoint")]);

    // Floor lookup for timestamp 300 finds (250, "connected"):
    assert_eq!(index.floor_event(300), Some((250, "connected")));
    // Strict ceiling lookup after 250 finds (400, "checkpoint"):
    assert_eq!(index.next_event_after(250), Some((400, "checkpoint")));

    println!("Window [200..=450]: {window:?}");
    println!("Floor at ts=300:    {:?}", index.floor_event(300));
}
Window [200..=450]: [(250, "connected"), (400, "checkpoint")]
Floor at ts=300:    Some((250, "connected"))