BTreeMap and BTreeSet¶
Level: 101 → 201 · working knowledge
One line: The sorted pair: keys live in order, so iterating one is already sorted and collecting into one is a sort you did not write — paid for with Ord instead of Hash, and with lookups that walk a tree instead of jumping straight there.
use std::collections::BTreeMap;
fn main() {
let mut tally: BTreeMap<&str, u32> = BTreeMap::new();
for b in ["Cara", "Ada", "Ben", "Cara"] {
*tally.entry(b).or_insert(0) += 1;
}
println!("{tally:?}"); // {"Ada": 1, "Ben": 1, "Cara": 2}
}
Swap BTreeMap for HashMap and every line still compiles — entry, or_insert, get, insert, len are the same names — but the print comes out in a different order every run.
Collecting into one is the sort¶
use std::collections::{BTreeMap, HashMap};
fn main() {
let scores = [("Cara", 2), ("Ada", 5), ("Ben", 4)];
let sorted: BTreeMap<&str, u32> = scores.into_iter().collect();
let hashed: HashMap<&str, u32> = scores.into_iter().collect();
let mut pairs: Vec<(&str, u32)> = hashed.into_iter().collect();
pairs.sort();
println!("{sorted:?}"); // {"Ada": 5, "Ben": 4, "Cara": 2}
println!("{pairs:?}"); // [("Ada", 5), ("Ben", 4), ("Cara", 2)]
}
Both lines print the same three pairs in the same order. The second one had to leave the map, build a Vec and sort it to get there; the first was in order the whole time. That is the case for reaching past HashMap: not speed, but that the answer is already ordered at every point, including halfway through building it.
What the ordering buys, and what it costs¶
HashMap / HashSet |
BTreeMap / BTreeSet |
|
|---|---|---|
| the key must be | Hash + Eq |
Ord ↗ |
| lookup | O(1) | O(log n) |
| iteration order | arbitrary, and different each run | sorted by key, always |
| first / last | — | first_key_value ↗ · last_key_value ↗ |
| "everything between two keys" | — | range ↗ |
The bottom three rows are not slower on a HashMap — they are unaskable. There is no order for a first, a last or a range to mean anything against.
use std::collections::BTreeMap;
fn main() {
let tally: BTreeMap<&str, u32> =
[("Ada", 3), ("Ben", 2), ("Cara", 3), ("Dan", 1)].into_iter().collect();
let window: Vec<&str> = tally.range("B".."D").map(|(k, _)| *k).collect();
println!("{window:?}"); // ["Ben", "Cara"]
}
range takes the same .., ..= and a..b forms a slice does, and yields borrowed pairs in order. Note what the bound is being compared against: "D" is a string, so "Dan" sorts after it and stays out — and range("B"..="D") does not change that, because the = includes the key "D" itself, which nobody has.
The trap: it sorts by the key, and you wanted the value sorted¶
This is the reason most reaches for BTreeMap disappoint. A vote tally keyed on the candidate is alphabetical, and a leaderboard is what you were after:
use std::collections::BTreeMap;
fn main() {
let tally: BTreeMap<&str, u32> =
[("Ada", 3), ("Ben", 2), ("Cara", 3), ("Dan", 1)].into_iter().collect();
let mut board: Vec<(&str, u32)> = tally.iter().map(|(k, v)| (*k, *v)).collect();
board.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(b.0)));
println!("{board:?}"); // [("Ada", 3), ("Cara", 3), ("Ben", 2), ("Dan", 1)]
}
The other way is to put the count into the key, where the container can see it. A tuple is Ord when its parts are, compared left to right, so a BTreeSet of (Reverse(count), name) pairs is a leaderboard with no sort call in it anywhere — Reverse ↗ being the std wrapper that flips a single comparison. The practice below builds the leaderboard both ways and checks they agree.
BTreeSet is BTreeMap<T, ()>¶
Same relationship as HashSet to HashMap, and the same consequence: collecting into one sorts and deduplicates in one step. Its union, intersection and difference yield their items in order too, so a Vec built from one of those needs no sort afterwards.
The key you cannot use¶
f64 is not Ord. NaN compares false against everything including itself, which leaves floats with PartialOrd only — so a float-keyed map is refused. The refusal is later than you would expect:
use std::collections::BTreeMap;
fn main() {
let scores: BTreeMap<f64, &str> = BTreeMap::new(); // fine — no `Ord` needed to exist
println!("{}", scores.len()); // fine — nor to be measured
// scores.insert(9.1, "Ada"); // E0277 — this is where it bites
}
BTreeMap::new and len carry no K: Ord bound, so the declaration compiles and runs. The error arrives at the first call that has to compare something:
error[E0277]: the trait bound `f64: Ord` is not satisfied
--> float_key.rs:5:12
|
5 | scores.insert(9.1, "Ada");
| ^^^^^^ the trait `Ord` is not implemented for `f64`
|
= help: the following other types implement trait `Ord`:
i128
i16
i32
i64
i8
isize
u128
u16
and 4 others
Scale to an integer — basis points, cents, milliseconds — and the ordering survives intact. The alternative is a wrapper type that promises Ord and rejects NaN when it is built.
If you are coming from another language¶
- Python. There is no sorted dict in the standard library, which is why the habit transfers badly: a Python programmer reaches for
sorted(d.items())at the point of use, and Rust's answer is to have picked the container at the point of construction.dicthas been insertion-ordered since 3.7 — that is a third thing again, and neither Rust map gives it to you (keep aVecalongside, as theHashSetpage does). The closest analogues are outside the language core:sortedcontainers'SortedDicton PyPI, andbisectover a list you keep sorted by hand — which is whatrangereplaces, without the "did I remember to re-sort after inserting" question.collections.Counter(...).most_common()is the leaderboard trap above, already solved for you; Rust makes you say which order you meant. - ABAP. This is the closest correspondence in the language.
TYPES ty TYPE SORTED TABLE OF ... WITH UNIQUE KEY nameis aBTreeMap, andHASHED TABLE ... WITH UNIQUE KEYis aHashMap; both keep entries by a declared key, both refuse duplicates, and the choice is made in the type rather than at the read.READ TABLE it WITH TABLE KEY name = 'Ada'ismap.get("Ada"), andLOOP AT it WHERE name BETWEEN 'B' AND 'D'isrange— with the same performance story, since a sorted table binary-searches the key and aSTANDARD TABLEdoes not. Two differences worth holding. ABAP's sorted table can have a non-unique key and hold several rows per value; aBTreeMapholds exactly one, so the ABAP habit of appending duplicates and looping over them becomesBTreeMap<K, Vec<V>>here. And ABAP lets you declare secondary keys on one table, so the same internal table can be read sorted and hashed; Rust makes you keep two containers, or one and an index. - C++.
std::mapandstd::setare this pair almost exactly — ordered,O(log n), iterable in key order — andstd::unordered_map/std::unordered_setareHashMap/HashSet.lower_bound/upper_boundare whatrangewraps into one call. The difference that bites:std::map::operator[]inserts a default when the key is missing, so a read can silently grow the map; Rust has nooperator[]that does that —map[k]panics on a missing key andentry(k).or_insert(0)is the explicit form of the C++ behaviour. C++'s comparator is a type parameter (std::map<K, V, Cmp>), which is how a C++ programmer keys on a float; Rust's equivalent is a newtype that implementsOrd. - Java.
TreeMap/TreeSetagainstHashMap/HashSet, withSortedMapas the interface.subMap(from, to),firstKey()andlastKey()map ontorange,first_key_valueandlast_key_value. Java'sComparableisOrdandComparatoris the argument tosort_by; the difference is that Java lets aTreeMaptakeDoublekeys and simply behaves oddly aroundNaNat run time, where Rust refuses the type at compile time.
The verified output¶
Verified output of sorted_collections.rs — regenerated by tools/run_examples.py, never hand-typed.
1. Collecting into a BTreeMap is a sort you did not write
BTreeMap {"Ada": 3, "Ben": 2, "Cara": 3, "Dan": 1}
HashMap [("Ada", 3), ("Ben", 2), ("Cara", 3), ("Dan", 1)]
The same four counts. The second line needed a Vec and a sort to be
printable in a fixed order at all; the first was already in one.
2. Ordering buys three questions a HashMap cannot answer
first_key_value() Some(("Ada", 3))
last_key_value() Some(("Dan", 1))
range("B".."D") [("Ben", 2), ("Cara", 3)]
A HashMap has no first, no last and no range: there is no order for
those questions to be asked against.
3. The trap: sorted by KEY, and what you wanted sorted is the value
Ada 3
Ben 2
Cara 3
Dan 1
That is alphabetical, not a leaderboard. Votes-descending still
leaves the map:
[("Ada", 3), ("Cara", 3), ("Ben", 2), ("Dan", 1)]
Ada and Cara tie at 3, and `then` breaks it by name — so the order is
total and the answer is the same on every run.
4. BTreeSet is BTreeMap<T, ()>: collecting into one sorts and dedups
{"Ada", "Ben", "Cara", "Dan"}
eligible but never voted ["Eve"]
The set operations yield their items in order too, so that Vec
needed no sort either.
5. The price is Ord, not Hash
keyed on an integer score {55: "Ben", 72: "Cara", 91: "Ada"}
Integers, char, &str, String, and tuples and Vecs of those are all
Ord. f64 is not — NaN leaves it PartialOrd only — so a BTreeMap keyed
on a float does not compile. Scale to an integer, as above.
Practice¶
Two orders from one tally, and the key you cannot use. Count a list of ballots into a BTreeMap, then produce the roll alphabetically and the leaderboard by votes descending — and say which of the two the container gave you for free. Build the leaderboard twice: once by leaving the map for a Vec, and once by putting the count into the key so that nothing sorts. Both have ties; make the tie-break explicit rather than inherited, and say what would change if you swapped sort_by for sort_unstable_by.
Then ask for the candidates whose names fall in "B".."D", and check whether ..= moves the boundary. Finally, key the same data on each candidate's share of the vote: say why BTreeMap<f64, _> will not compile, scale it to an integer instead — and then count the entries that came out, because that number is the point of the exercise.
Solution
sorted_collections_kata.rs in full — pasted here by tools/run_examples.py from the file CI compiles and runs.
//! Kata solution: two orders from one tally, and the key you cannot use.
//!
//! rustc --edition 2024 sorted_collections_kata.rs -o /tmp/sck && /tmp/sck
use std::cmp::Reverse;
use std::collections::{BTreeMap, BTreeSet};
const BALLOTS: [&str; 11] = [
"Cara", "Ada", "Ben", "Cara", "Dan", "Ada", "Cara", "Ben", "Ada", "Eve", "Dan",
];
/// The tally. Alphabetical order is a property of the container, not of this code.
fn tally() -> BTreeMap<&'static str, u32> {
let mut t = BTreeMap::new();
for b in BALLOTS {
*t.entry(b).or_insert(0) += 1;
}
t
}
fn main() {
let t = tally();
println!("1. The roll, alphabetical — nothing here sorts");
for (name, votes) in &t {
println!(" {name} {votes}");
}
println!();
println!("2. The leaderboard, by leaving the map");
let mut board: Vec<(&str, u32)> = t.iter().map(|(k, v)| (*k, *v)).collect();
board.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(b.0)));
for (name, votes) in &board {
println!(" {name} {votes}");
}
println!(" Two ties — Ada/Cara at 3 and Ben/Dan at 2 — and `then` settles both");
println!(" by name. Without it `sort_by` is still *stable*, so the order would");
println!(" be the map's alphabetical one; but the rule would be implicit, and a");
println!(" switch to sort_unstable_by would silently change the answer.");
println!();
println!("3. The leaderboard, by making the count part of the key");
let ranked: BTreeSet<(Reverse<u32>, &str)> =
t.iter().map(|(k, v)| (Reverse(*v), *k)).collect();
for (Reverse(votes), name) in &ranked {
println!(" {name} {votes}");
}
println!(" Same order, and no sort call anywhere. A tuple is Ord when its parts");
println!(" are, compared left to right, so (Reverse(count), name) orders by");
println!(" count descending and then by name — the tie-break is in the type.");
println!();
println!("4. A question a HashMap cannot be asked");
let window: Vec<&str> = t.range("B".."D").map(|(k, _)| *k).collect();
println!(" candidates in \"B\"..\"D\" {window:?}");
println!(" half-open, so \"D\" is a bound and Dan is excluded; range(\"B\"..=\"D\")");
println!(" would still exclude him, because \"Dan\" > \"D\" as a string.");
let inclusive: Vec<&str> = t.range("B"..="D").map(|(k, _)| *k).collect();
println!(" candidates in \"B\"..=\"D\" {inclusive:?}");
println!();
println!("5. The key you cannot use");
// let mut by_share: BTreeMap<f64, &str> = BTreeMap::new();
// by_share.insert(0.273, "Ada"); // E0277: the trait bound `f64: Ord` is not satisfied
println!(" f64 is PartialOrd but not Ord, because NaN compares false against");
println!(" everything including itself, so a BTreeMap keyed on one does not");
println!(" compile. Scale to an integer and the ordering survives:");
let total: u32 = t.values().sum();
let by_share: BTreeMap<u32, &str> = t.iter().map(|(k, v)| (v * 10_000 / total, *k)).collect();
println!(" share in basis points {by_share:?}");
println!();
println!("6. ...and look at what that just cost");
println!(" Five candidates went in and {} came out.", by_share.len());
println!(" Ada and Cara both hold 2727 basis points, Ben and Dan both 1818, and");
println!(" a map keeps one value per key — so the second insert of each pair");
println!(" overwrote the first. Keying on a DERIVED value drops ties silently:");
println!(" nothing errors, the type is right, and two candidates are gone.");
let by_share_kept: BTreeSet<(u32, &str)> =
t.iter().map(|(k, v)| (v * 10_000 / total, *k)).collect();
println!(" as a set of pairs {by_share_kept:?}");
println!(" The pair is the key, so equal shares no longer collide — and the");
println!(" ordering is still share-then-name, for the same left-to-right reason");
println!(" as the leaderboard in 3.");
}
Verified output of sorted_collections_kata.rs — regenerated by tools/run_examples.py, never hand-typed.
1. The roll, alphabetical — nothing here sorts
Ada 3
Ben 2
Cara 3
Dan 2
Eve 1
2. The leaderboard, by leaving the map
Ada 3
Cara 3
Ben 2
Dan 2
Eve 1
Two ties — Ada/Cara at 3 and Ben/Dan at 2 — and `then` settles both
by name. Without it `sort_by` is still *stable*, so the order would
be the map's alphabetical one; but the rule would be implicit, and a
switch to sort_unstable_by would silently change the answer.
3. The leaderboard, by making the count part of the key
Ada 3
Cara 3
Ben 2
Dan 2
Eve 1
Same order, and no sort call anywhere. A tuple is Ord when its parts
are, compared left to right, so (Reverse(count), name) orders by
count descending and then by name — the tie-break is in the type.
4. A question a HashMap cannot be asked
candidates in "B".."D" ["Ben", "Cara"]
half-open, so "D" is a bound and Dan is excluded; range("B"..="D")
would still exclude him, because "Dan" > "D" as a string.
candidates in "B"..="D" ["Ben", "Cara"]
5. The key you cannot use
f64 is PartialOrd but not Ord, because NaN compares false against
everything including itself, so a BTreeMap keyed on one does not
compile. Scale to an integer and the ordering survives:
share in basis points {909: "Eve", 1818: "Dan", 2727: "Cara"}
6. ...and look at what that just cost
Five candidates went in and 3 came out.
Ada and Cara both hold 2727 basis points, Ben and Dan both 1818, and
a map keeps one value per key — so the second insert of each pair
overwrote the first. Keying on a DERIVED value drops ties silently:
nothing errors, the type is right, and two candidates are gone.
as a set of pairs {(909, "Eve"), (1818, "Ben"), (1818, "Dan"), (2727, "Ada"), (2727, "Cara")}
The pair is the key, so equal shares no longer collide — and the
ordering is still share-then-name, for the same left-to-right reason
as the leaderboard in 3.
See also¶
HashMap— the same API without the order, andentry, which both shareHashSet— the unordered half of the set story, and theVec-beside-the-set trick for insertion orderVec— where a leaderboard ends up, andsort_byagainstsort_unstable_by- Comparison traits — what
Orddemands overPartialOrd, and whyf64has only the second collectandFromIterator— the call that turns any iterator into one of these- Collect the iterator into a
Vec— the prior question: whether to materialize at all
Sources¶
BTreeMap ↗ and BTreeSet ↗; the module page for std::collections ↗ opens with the decision table this page's own comparison is a longer form of. The E0277 transcript is a real compile of the six-line program above it, on rustc 1.98.0.
Po polsku¶
BTreeMap i BTreeSet to para uporządkowana: klucze leżą w kolejności, więc przejście po takiej kolekcji jest już posortowane, a zebranie czegoś do niej jest sortowaniem, którego nie musiałeś pisać. Płaci się za to cechą Ord zamiast Hash oraz wyszukiwaniem, które schodzi po drzewie, zamiast skoczyć prosto pod adres.
Dla polskiego czytelnika jest tu jedna rzecz, której angielski oryginał nie musi zauważać: Ord dla String porównuje punkty kodowe Unicode, a nie porządek alfabetyczny polszczyzny. Skutek jest natychmiastowy i widoczny — ą wypada za z, a nie tuż po a, więc lista nazwisk posortowana przez BTreeMap nie jest listą posortowaną po polsku. To nie jest wada BTreeMap, tylko poprawne zachowanie porównania bajt po bajcie; prawdziwe sortowanie językowe wymaga reguł kolacji (biblioteki spod hasła collation, np. ICU), których biblioteka standardowa nie ma i nie udaje, że ma.
Pułapka, którą strona nazywa wprost, warta powtórzenia: sortuje po kluczu, a ty zwykle chcesz posortować po wartości. Mapa uporządkowana nie pomoże ustawić wyników od największego — do tego zbiera się do Vec i sortuje jawnie. Przydaje się też wiedzieć, że BTreeSet<T> to w istocie BTreeMap<T, ()>, czyli ten sam mechanizm z pustą wartością.
Szukaj po polsku: kolekcje uporządkowane · sortowanie polskich znaków · reguły kolacji · rust BTreeMap ordering · rust sort by value