Range Queries
One of the defining advantages of BTreeMap<K, V> and BTreeSet<T> over hash-based collections is the ability to perform ordered range queries in $O(\log n + k)$ time (where $k$ is the number of matching elements in the range), jumping directly to the lower bound in the B-Tree and walking leaf/internal nodes sequentially until the upper bound.
Expressing Interval Endpoints with std::ops::RangeBounds
Range queries accept any type implementing std::ops::RangeBounds<Q>, which includes all built-in range syntaxes as well as custom (Bound<Q>, Bound<Q>) pairs:
pub enum Bound<T> {
Included(T),
Excluded(T),
Unbounded,
}
| Range Syntax | Lower Bound |
Upper Bound |
Mathematical Interval |
|---|---|---|---|
a..b |
Included(&a) |
Excluded(&b) |
$[a, b)$ |
a..=b |
Included(&a) |
Included(&b) |
$[a, b]$ |
a.. |
Included(&a) |
Unbounded |
$[a, +\infty)$ |
..b |
Unbounded |
Excluded(&b) |
$(-\infty, b)$ |
..=b |
Unbounded |
Included(&b) |
$(-\infty, b]$ |
.. |
Unbounded |
Unbounded |
$(-\infty, +\infty)$ |
(Bound::Excluded(a), Bound::Included(b)) |
Excluded(a) |
Included(b) |
$(a, b]$ (open lower bound!) |
Querying Half-Open, Closed, and Open-Lower Intervals
Because Rust does not have a dedicated built-in syntax for an exclusive lower bound $(a, b]$, passing a 2-tuple (Bound::Excluded(a), Bound::Included(b)) lets you express any combination of open and closed endpoints:
use std::collections::BTreeSet;
use std::ops::Bound::{Excluded, Included};
fn main() {
let scores = BTreeSet::from([10, 20, 30, 40, 50]);
// 1. Closed interval [20, 40] (`20..=40`):
let closed: Vec<i32> = scores.range(20..=40).copied().collect();
// 2. Half-open interval (20, 40] (strictly > 20 and <= 40):
let open_left: Vec<i32> = scores
.range((Excluded(&20), Included(&40)))
.copied()
.collect();
println!("[20, 40]: {closed:?}");
println!("(20, 40]: {open_left:?}");
}
[20, 40]: [20, 30, 40]
(20, 40]: [30, 40]
Predecessor (<= x) and Successor (>= x) Lookups in $O(\log n)$
Combining .range() with .next() or .next_back() implements C++-style lower_bound, upper_bound, floor, and ceiling queries in $O(\log n)$ time:
| Query | Idiomatic BTreeMap / BTreeSet Expression |
|---|---|
Smallest key $\ge x$ (ceiling / lower_bound) |
map.range(x..).next() |
Smallest key $> x$ (strict successor / upper_bound) |
map.range((Excluded(x), Unbounded)).next() |
| Largest key $\le x$ (floor) | map.range(..=x).next_back() |
| Largest key $< x$ (strict predecessor) | map.range(..x).next_back() |
use std::collections::BTreeMap;
fn main() {
let versions = BTreeMap::from([(100, "v1.0"), (200, "v2.0"), (300, "v3.0")]);
let floor_250 = versions.range(..=250).next_back();
let ceil_250 = versions.range(250..).next();
println!("Floor of 250 (<= 250): {floor_250:?}");
println!("Ceiling of 250 (>= 250): {ceil_250:?}");
}
Floor of 250 (<= 250): Some((&200, &"v2.0"))
Ceiling of 250 (>= 250): Some((&300, &"v3.0"))