Why this matters
A list answers "what is at position 3?". Many questions are instead "what is the value for this key?": the phone number for a name, the count for a word, the configuration value for a setting. A map stores key-value pairs and finds a value by its key quickly. Rust's standard library has two: HashMap, the fast general-purpose choice, and BTreeMap, which keeps keys sorted. Word counting, grouping, caching and indexing are all a few lines with them.
HashMap basics
use std::collections::HashMap;
fn main() {
let mut ages: HashMap<String, u32> = HashMap::new();
ages.insert(String::from("Ada"), 36);
ages.insert(String::from("Alan"), 41);
ages.insert(String::from("Ada"), 37);
println!("{:?}", ages.get("Ada"));
println!("{:?}", ages.get("Grace"));
println!("{}", ages.contains_key("Alan"));
println!("{}", ages.len());
if let Some(a) = ages.get_mut("Alan") {
*a += 1;
}
println!("{}", ages["Alan"]);
let removed = ages.remove("Ada");
println!("{:?} {}", removed, ages.len());
}
Some(37)
None
true
2
42
Some(37) 1
HashMapis not in the prelude:use std::collections::HashMap;.insert(k, v)adds or replaces (it returns the old value as anOption).get(&k)returnsOption<&V>;get_mutreturnsOption<&mut V>;map[&k]panics if the key is missing.- With
Stringkeys you can look up with a&str(get("Ada")): the map accepts any borrowed form of the key.
How a hash map works
A HashMap is an array of buckets. To store a key, it computes a hash, a number derived from the key's bytes, and uses it to pick a bucket:
hash("Ada") = 0x9f3a... -> bucket 5
hash("Alan") = 0x11c0... -> bucket 2
buckets: 0 1 2 3 4 5 6 7
[ ] [ ] [Alan:41] [ ] [ ] [Ada:37] [ ] [ ]
Lookup computes the same hash and goes straight to the bucket, so it takes about the same time whether the map has ten entries or ten million ("O(1) on average"). Collisions (two keys in one bucket) are handled internally; when the map gets too full it allocates a bigger array and redistributes everything, like a Vec growing.
Two consequences:
- Keys must be hashable and comparable: they implement the
HashandEqtraits. Strings, integers,char,booland tuples of these do;f64does not (NaN breaks equality). You can derive them for your own types (module 10). - Iteration order is unspecified. It depends on hashes, and Rust randomizes the hash function per process (to defend against attackers who craft colliding keys). The same program can print a HashMap in a different order on every run. Never rely on HashMap order in output: sort first, or use a BTreeMap.
Ownership: the map owns its keys and values
insert moves the key and value into the map:
use std::collections::HashMap;
fn main() {
let name = String::from("Ada");
let mut m = HashMap::new();
m.insert(name, 1);
println!("{name}");
}
error[E0382]: borrow of moved value: `name`
--> main.rs:7:15
|
4 | let name = String::from("Ada");
| ---- move occurs because `name` has type `String`, which does not implement the `Copy` trait
5 | let mut m = HashMap::new();
6 | m.insert(name, 1);
| ---- value moved here
7 | println!("{name}");
| ^^^^^^ value borrowed here after move
|
help: consider cloning the value if the performance cost is acceptable
|
6 | m.insert(name.clone(), 1);
| ++++++++
Copy types like integers are copied as usual. References can be stored too (HashMap<&str, u32>), but then the map borrows from whatever owns those strings and cannot outlive it; owned String keys are the simple choice.
The entry API
The most common map operation is "update the value for this key, creating it if missing". The entry API does it with one lookup:
use std::collections::HashMap;
fn main() {
let text = "one fish two fish red fish blue fish";
let mut counts: HashMap<&str, u32> = HashMap::new();
for word in text.split_whitespace() {
*counts.entry(word).or_insert(0) += 1;
}
let mut sorted: Vec<_> = counts.iter().collect();
sorted.sort();
println!("{:?}", sorted);
let mut by_len: HashMap<usize, Vec<&str>> = HashMap::new();
for word in text.split_whitespace() {
by_len.entry(word.len()).or_default().push(word);
}
println!("{:?}", by_len.get(&4));
}
[("blue", 1), ("fish", 4), ("one", 1), ("red", 1), ("two", 1)]
Some(["fish", "fish", "fish", "blue", "fish"])
map.entry(key) returns an Entry, a view of the slot for that key, occupied or vacant. or_insert(v) inserts v if vacant and returns a &mut V to the value either way, so *... += 1 updates it in place. or_default() inserts the type's default (0, empty Vec, empty String); or_insert_with(|| ...) computes the initial value only when needed. This is the idiom for counting and grouping.
BTreeMap: sorted keys
BTreeMap has nearly the same API but keeps entries sorted by key (keys implement Ord). Lookups take O(log n), slightly slower than hashing, and iteration is in key order, which makes output deterministic:
use std::collections::BTreeMap;
fn main() {
let mut stock = BTreeMap::new();
stock.insert("pear", 4);
stock.insert("apple", 12);
stock.insert("fig", 0);
stock.insert("banana", 7);
for (fruit, n) in &stock {
println!("{fruit:<8}{n:>3}");
}
println!("{:?}", stock.first_key_value());
let some: Vec<_> = stock.range("b".."g").collect();
println!("{:?}", some);
}
apple 12
banana 7
fig 0
pear 4
Some(("apple", 12))
[("banana", 7), ("fig", 0)]
It also supports range queries (range(a..b)), first and last entries, and ordered traversal, which a HashMap cannot do.
Iterating a map
use std::collections::BTreeMap;
fn main() {
let mut m = BTreeMap::from([("a", 1), ("b", 2), ("c", 3)]);
for (k, v) in &m {
print!("{k}={v} ");
}
println!();
for v in m.values_mut() {
*v *= 10;
}
let keys: Vec<_> = m.keys().collect();
println!("{:?} {:?}", keys, m);
let total: i32 = m.values().sum();
println!("{total}");
}
a=1 b=2 c=3
["a", "b", "c"] {"a": 10, "b": 20, "c": 30}
60
&map yields (&K, &V) pairs, &mut map yields (&K, &mut V) (keys can never be changed in place, since that would break the map's organization), and map by value yields owned (K, V) pairs. keys(), values() and values_mut() iterate one side.
Choosing
- HashMap: default for lookups by key; fastest; unordered.
- BTreeMap: when you need sorted output, range queries, or deterministic iteration; also fine for small maps.
Common mistakes
- Printing a HashMap directly in a program whose output matters: order varies between runs. Sort or use BTreeMap.
map[&key]on a possibly missing key: panics. Useget.- Two lookups for one update (
if map.contains_key(k) { ... } else { map.insert(...) }): useentry. - Float keys: E0277,
f64does not implementEq/Hash/Ord. Convert to an integer representation (cents, bits) or use an ordered wrapper.
Recap
HashMap<K, V>: average O(1) insert/lookup by hashing; keys needHash + Eq; unspecified, randomized order.BTreeMap<K, V>: sorted by key, O(log n), range queries; keys needOrd.insertmoves keys and values in;getreturnsOption<&V>;map[&k]panics on missing keys.entry(k).or_insert(v)/or_default()is the idiom for counting and grouping.- For deterministic output, sort the pairs or use a BTreeMap.
Going further: read the docs for std::collections (the module page), which compares all the collections and their costs in a table.