BTreeMap and BTreeSet: the APIs that matter
Published 13 October 2026
What they are¶
BTreeMap<K, V> is an ordered map. BTreeSet<T> is the same thing without values
(internally it’s a BTreeMap<T, SetValZST>). Both live in std::collections.
- Keys need
Ord, notHash + Eq. - Iteration is always in sorted key order.
- Lookups, inserts and removes are \(O(\log n)\).
Under the hood it’s a B-tree, not a binary tree: each node holds up to 11 keys in a
contiguous array, so a lookup does a short linear scan per node. Fewer allocations and
better cache behaviour than a red-black tree like C++’s std::map.
BTreeMap or HashMap?¶
| You need | Use |
|---|---|
| Just get/insert/remove, fastest possible | HashMap |
| Sorted iteration, or deterministic output | BTreeMap |
| min / max / “next key after x” / range queries | BTreeMap |
Keys that implement Ord but not Hash |
BTreeMap |
The deterministic-order point matters more than it seems: printing a HashMap gives a
different order on every run, which makes snapshot tests and debug output flaky.
The basics¶
use std::collections::BTreeMap;
let mut m = BTreeMap::new();
assert_eq!(m.insert("b", 2), None); // new key
assert_eq!(m.insert("b", 20), Some(2)); // returns the old value
m.insert("a", 1);
m.insert("c", 3);
assert_eq!(m.get("a"), Some(&1));
if let Some(v) = m.get_mut("c") { *v += 10; }
assert_eq!(m["c"], 13); // Index panics if missing
assert!(m.contains_key("b"));
assert_eq!(m.remove("b"), Some(20));
assert_eq!(m.remove_entry("a"), Some(("a", 1))); // key back too
assert_eq!(m.len(), 1);
Building one from literals or an iterator:
let m = BTreeMap::from([(3, "c"), (1, "a"), (2, "b")]);
let squares: BTreeMap<u32, u32> = (1..=5).map(|k| (k, k * k)).collect();
Looking up String keys with &str¶
get, remove, contains_key and range take &Q where K: Borrow<Q>, so you don’t
have to allocate a String just to look something up:
let mut scores: BTreeMap<String, u32> = BTreeMap::new();
scores.insert("alice".to_string(), 90);
assert_eq!(scores.get("alice"), Some(&90)); // &str, no allocation
The Entry API¶
Same as HashMap. One lookup instead of a contains_key followed by an insert.
// counting
let mut counts: BTreeMap<char, usize> = BTreeMap::new();
for c in "hello".chars() {
*counts.entry(c).or_insert(0) += 1;
}
// grouping: or_default() needs V: Default
let mut by_len: BTreeMap<usize, Vec<&str>> = BTreeMap::new();
for w in ["hi", "yo", "rust"] {
by_len.entry(w.len()).or_default().push(w);
}
assert_eq!(by_len[&2], ["hi", "yo"]);
// update if present, insert otherwise
counts.entry('h').and_modify(|n| *n += 100).or_insert(1);
What entry actually is¶
map.entry(k) means: search the tree once for where k is, or would go, and hand
back that slot. It’s an enum:
enum Entry<'a, K, V> {
Occupied(OccupiedEntry<'a, K, V>), // k is there: read, modify or remove its value
Vacant(VacantEntry<'a, K, V>), // k isn't: insert at the slot already found
}
The chained methods are shortcuts for matching on it. or_insert is roughly:
match map.entry(k) {
Entry::Occupied(e) => e.into_mut(), // &mut V to the existing value
Entry::Vacant(e) => e.insert(v), // insert, then &mut V to the new value
}
So *map.entry(c).or_insert(0) += 1 reads as “find c’s slot, put a 0 there if it’s
empty, give me the value, add 1”. Think of it as a cursor to where k belongs, not a
fancier get.
Why it exists¶
1. Searching once instead of twice. The obvious version walks the tree two or three times:
entry reuses the slot from the first search. Other languages have the same thing for
the same reason: Java’s merge / computeIfAbsent, Python’s setdefault / defaultdict.
2. The borrow checker. Updating in place without entry compiles:
But returning the reference is rejected, even though the logic is correct:
fn get_or_insert(m: &mut BTreeMap<String, u32>, k: String) -> &mut u32 {
if let Some(v) = m.get_mut(&k) { return v; } // this borrow may be returned...
m.insert(k.clone(), 0); // E0499: cannot borrow `*m` as mutable more than once
m.get_mut(&k).unwrap()
}
The current borrow checker can’t see that the first borrow is dead when the if fails
(a known limitation that Polonius, the next-generation borrow checker, is meant to fix).
entry sidesteps it: there’s one &mut borrow of the map, from the search to the result.
Why not map[k] += 1 like C++? C++’s operator[] quietly inserts a default when
the key is missing. In Rust, indexing never mutates the map: map[&k] panics on a
missing key and can’t insert. entry is the explicit, opt-in version.
entry takes the key by value
With String keys, map.entry(name.to_string()) allocates even when the key already
exists, and the new String is thrown away. In a hot loop where the key is usually
present, try get_mut first:
or_insert vs or_insert_with¶
Rust evaluates arguments before the call, so
runs build() every time, even when k is already there. or_insert sees the key
exists and drops the value unused. Passing a closure instead makes it lazy:
Side by side, inside:
fn or_insert(self, default: V) -> &mut V {
match self {
Occupied(e) => e.into_mut(), // `default` was already built, now dropped
Vacant(e) => e.insert(default),
}
}
fn or_insert_with<F: FnOnce() -> V>(self, f: F) -> &mut V {
match self {
Occupied(e) => e.into_mut(), // f never called
Vacant(e) => e.insert(f()),
}
}
| Method | Use for |
|---|---|
or_insert(0), or_insert(false) |
Cheap or already-computed values |
or_insert_with(|| expensive()) |
Anything that allocates, computes, or does I/O |
or_default() |
or_insert_with(Default::default): Vec, String, counters |
or_insert_with_key(|k| f(k)) |
The default depends on the key (it’s moved into the entry already) |
You can pass a function path instead of a closure: or_insert_with(build).
Clippy catches two versions of this:
or_insert(Vec::new())warns by default (clippy::unwrap_or_default) and suggestsor_default().Vec::new()doesn’t allocate, so this is about style, not cost.or_insert(build())oror_insert(String::from("x"))isclippy::or_fun_call, which is in the nursery group and off by default. Turn it on with#![warn(clippy::or_fun_call)].
It’s the same eager-vs-lazy split as unwrap_or / unwrap_or_else and ok_or /
ok_or_else.
Matching on Entry directly¶
When you need to branch, match on the Entry directly:
use std::collections::btree_map::Entry;
match counts.entry('z') {
Entry::Vacant(e) => { e.insert(1); }
Entry::Occupied(mut e) => { *e.get_mut() += 1; }
}
// remove the key when its count hits a value, in one lookup
if let Entry::Occupied(e) = counts.entry('l') {
if *e.get() == 2 { e.remove(); }
}
Ordered iteration¶
let m = BTreeMap::from([(3, "c"), (1, "a"), (2, "b")]);
for (k, v) in &m { /* (1, "a"), (2, "b"), (3, "c") */ }
let keys: Vec<_> = m.keys().rev().copied().collect();
assert_eq!(keys, [3, 2, 1]);
| Method | Yields |
|---|---|
iter() |
(&K, &V) |
iter_mut() |
(&K, &mut V) |
keys() |
&K |
values() |
&V |
values_mut() |
&mut V |
into_iter() |
(K, V) |
into_keys() |
K |
into_values() |
V |
All of them are DoubleEndedIterator, so .rev() and .next_back() are free.
There is no keys_mut
Keys are never handed out mutably. Changing a key in place could break the sort order
the tree depends on. To “rename” a key, remove it and insert again.
Min and max¶
let mut m = BTreeMap::from([(10, "x"), (20, "y"), (30, "z")]);
assert_eq!(m.first_key_value(), Some((&10, &"x")));
assert_eq!(m.last_key_value(), Some((&30, &"z")));
assert_eq!(m.pop_first(), Some((10, "x"))); // remove and return
assert_eq!(m.pop_last(), Some((30, "z")));
// an OccupiedEntry for the smallest key, to read/modify/remove it
if let Some(mut e) = m.first_entry() {
*e.get_mut() = "Y";
}
pop_first / pop_last make a BTreeMap a priority queue you can pop from both
ends, which BinaryHeap can’t do. They’re \(O(\log n)\), stable since Rust 1.66.
range: the reason to pick a BTreeMap¶
range takes anything that implements RangeBounds and returns a double-ended iterator
over just that slice of keys, in \(O(\log n)\) to find the start.
let m = BTreeMap::from([(1, "a"), (5, "e"), (9, "i"), (12, "l")]);
let mid: Vec<_> = m.range(5..10).map(|(k, _)| *k).collect();
assert_eq!(mid, [5, 9]);
Floor, ceiling, predecessor, successor¶
These are the queries C++ gets from lower_bound / upper_bound and Java from
TreeMap.floorKey / ceilingKey. In Rust they’re all range plus next or next_back:
use std::ops::Bound::{Excluded, Unbounded};
// largest key <= 7
assert_eq!(m.range(..=7).next_back(), Some((&5, &"e")));
// smallest key >= 7
assert_eq!(m.range(7..).next(), Some((&9, &"i")));
// largest key < 5
assert_eq!(m.range(..5).next_back(), Some((&1, &"a")));
// smallest key > 5: there's no `5<..` syntax, so use Bound
assert_eq!(m.range((Excluded(5), Unbounded)).next(), Some((&9, &"i")));
| Query | Java | Rust |
|---|---|---|
largest <= x |
floorKey(x) |
range(..=x).next_back() |
largest < x |
lowerKey(x) |
range(..x).next_back() |
smallest >= x |
ceilingKey(x) |
range(x..).next() |
smallest > x |
higherKey(x) |
range((Excluded(x), Unbounded)).next() |
range_mut is the same thing with &mut V:
Prefix search on string keys¶
With String keys you can range with &str, but you have to spell the type out and use
Bound explicitly ("ap".. won’t type-check against String keys as str):
use std::ops::Bound::{Included, Unbounded};
let words = BTreeMap::from([
("apple".to_string(), 1),
("apricot".to_string(), 2),
("banana".to_string(), 3),
]);
let ap: Vec<&str> = words
.range::<str, _>((Included("ap"), Unbounded))
.take_while(|(k, _)| k.starts_with("ap"))
.map(|(k, _)| k.as_str())
.collect();
assert_eq!(ap, ["apple", "apricot"]);
range panics on bad bounds
Both of these panic at runtime rather than returning an empty iterator:
m.range(5..2) // range start is greater than range end in BTreeMap
m.range((Excluded(3), Excluded(3))) // range start and end are equal and excluded in BTreeMap
If the bounds come from user input, check start <= end first.
Splitting, merging, filtering¶
let mut a = BTreeMap::from([(1, 'a'), (2, 'b'), (3, 'c'), (4, 'd')]);
let mut b = a.split_off(&3); // a = {1, 2}, b = {3, 4}; key 3 goes to the right half
a.append(&mut b); // moves everything back; b is now empty
retain keeps entries matching a predicate. extract_if (stable since Rust 1.91)
removes the matching ones and gives them back, over a key range:
let mut m: BTreeMap<u32, u32> = (1..=10).map(|k| (k, k * k)).collect();
m.retain(|k, _| k % 2 == 0); // {2, 4, 6, 8, 10}
let big: BTreeMap<u32, u32> = m.extract_if(.., |_, v| *v > 30).collect();
// big = {6: 36, 8: 64, 10: 100}, m = {2: 4, 4: 16}
The iterator is lazy: entries are only removed as you pull them, so drop it early and the rest stay.
BTreeSet¶
Everything above carries over, minus the values. insert returns a bool instead of
the old value.
use std::collections::BTreeSet;
let mut s = BTreeSet::new();
assert!(s.insert(5)); // true: newly added
assert!(!s.insert(5)); // false: already there
s.extend([3, 8, 1]);
assert!(s.contains(&3));
assert!(s.remove(&3));
assert_eq!(s.first(), Some(&1));
assert_eq!(s.last(), Some(&8));
assert_eq!(s.iter().copied().collect::<Vec<_>>(), [1, 5, 8]); // sorted
assert_eq!(s.range(2..).next(), Some(&5)); // ceiling(2)
assert_eq!(s.range(..5).next_back(), Some(&1)); // lower(5)
assert_eq!(s.pop_first(), Some(1));
assert_eq!(s.pop_last(), Some(8));
A few set-only methods:
get(&q)returns the stored element itself, useful when equal-by-Ordvalues carry extra data.take(&q)removes and returns the stored element.replace(v)insertsvand returns the element it displaced, if any.split_off,append,retain,extract_ifwork like the map versions.
Set operations¶
The methods return lazy iterators that walk both sets in order. The operators on
references build a new BTreeSet (and need T: Clone).
let a = BTreeSet::from([1, 2, 3, 4]);
let b = BTreeSet::from([3, 4, 5]);
a.union(&b); // 1 2 3 4 5
a.intersection(&b); // 3 4
a.difference(&b); // 1 2
a.symmetric_difference(&b); // 1 2 5
assert_eq!(&a & &b, BTreeSet::from([3, 4]));
assert_eq!(&a | &b, BTreeSet::from([1, 2, 3, 4, 5]));
assert_eq!(&a - &b, BTreeSet::from([1, 2]));
assert_eq!(&a ^ &b, BTreeSet::from([1, 2, 5]));
assert!(BTreeSet::from([3, 4]).is_subset(&a));
assert!(a.is_superset(&BTreeSet::from([1])));
assert!(a.is_disjoint(&BTreeSet::from([9])));
Patterns that come up¶
Multiset (sorted bag with duplicates)¶
Rust has no BTreeMultiSet. Use a map from value to count, and delete the key when the
count hits zero so first_key_value / last_key_value stay correct:
fn add(ms: &mut BTreeMap<i32, usize>, x: i32) {
*ms.entry(x).or_insert(0) += 1;
}
fn remove_one(ms: &mut BTreeMap<i32, usize>, x: i32) {
if let Entry::Occupied(mut e) = ms.entry(x) {
*e.get_mut() -= 1;
if *e.get() == 0 { e.remove(); }
}
}
This is the standard tool for “sliding window max/min” when you also need to remove arbitrary elements.
Sort descending, break ties another way¶
Put a tuple in a set and wrap the field you want reversed in std::cmp::Reverse:
use std::cmp::Reverse;
let mut board: BTreeSet<(Reverse<u32>, &str)> = BTreeSet::new();
board.insert((Reverse(90), "amy"));
board.insert((Reverse(95), "bob"));
board.insert((Reverse(90), "cat"));
let order: Vec<_> = board.iter().map(|(_, n)| *n).collect();
assert_eq!(order, ["bob", "amy", "cat"]); // score desc, then name asc
To update a score, remove the old tuple and insert the new one.
Interval booking¶
Store start -> end. A new [s, e) clashes only if the booking that starts closest
before e hasn’t ended by s:
fn book(booked: &mut BTreeMap<u32, u32>, s: u32, e: u32) -> bool {
if booked.range(..e).next_back().is_some_and(|(_, &end)| end > s) {
return false;
}
booked.insert(s, e);
true
}
(is_some_and is covered in its own post.)
Gotchas¶
- No indexing by position.
iter().nth(k)is \(O(k)\). There’s no order-statistic “k-th smallest in \(O(\log n)\)” in std. - No cursors on stable.
lower_bound/upper_boundreturning a movable cursor exist behind#![feature(btree_cursors)]; on stable,rangecovers the same queries. - Don’t mutate a key’s ordering through interior mutability (
Cell,RefCell) while it’s in the tree. It won’t be UB, but lookups and iteration will give wrong answers. Ordmust agree withEq. Two keys that compareEqualare the same key. If yourOrdonly looks at one field, inserting a second item with the same field replaces the first (map) or is rejected (set).- Floats aren’t
Ord. Wrap them (ordered_float::OrderedFloat) or usef64::total_cmpin your own newtype’sOrdimpl.
Cheat sheet¶
| Operation | Complexity |
|---|---|
get, insert, remove, entry |
\(O(\log n)\) |
first_*, last_*, pop_* |
\(O(\log n)\) |
range(..) to first item |
\(O(\log n)\) |
| each further item in an iterator | amortised \(O(1)\) |
append |
\(O(n + m)\) |
len, is_empty |
\(O(1)\) |