What a hash map is
A hash map stores key-value pairs. You give it a key, and it gives back the value stored for that key, without scanning through the other entries. Lookups, insertions and removals take O(1) time on average, which is why hash maps are the first tool to reach for when a problem asks "how many times have I seen this?" or "have I seen this before?".
Rust's hash map is HashMap<K, V> from std::collections. Keys must be unique: inserting a key that already exists replaces its value. Every key has one type K and every value one type V, and the compiler checks them.
Creating a map
Bring the type into scope with use, then create an empty map with HashMap::new(). The map must be declared mut if you plan to change it.
use std::collections::HashMap;
let mut digit_value: HashMap<&str, i32> = HashMap::new();
You rarely need to write the types out. If you insert a pair right after, the compiler infers K and V from it. To start with some entries, build the map from an array of tuples:
let digit_value = HashMap::from([
("zero", 0),
("one", 1),
]);
Reading values
There are two ways to read, and they differ in what happens for a missing key.
map[key]returns the value directly, but panics if the key is absent.map.get(key)returns anOption:Some(&value)when the key exists andNonewhen it doesn't.
With the map from above, digit_value["one"] is 1 and digit_value.get("three") is None. Use indexing when the key is certainly there (for example, you just checked it) and get when it may not be.
Adding, updating and removing
insert(key, value) adds a pair. If the key was already present, it replaces the value and returns the old one wrapped in Some; for a new key it returns None. So the same call both adds and updates.
remove(key) deletes a pair and returns the removed value as an Option. Removing a key that isn't there does nothing and returns None. contains_key(key) answers whether a key exists.
let mut stock = HashMap::new();
stock.insert("apples", 3);
println!("{:?}", stock.insert("apples", 7)); // Some(3)
println!("{:?}", stock.remove("apples")); // Some(7)
println!("{:?}", stock.remove("apples")); // None
This prints Some(3), Some(7) and None, one per line.
The entry API
A very common task is "change the value if the key exists, create it if not", such as counting. Reading, checking and writing back by hand takes several lines. The entry method does it in one: map.entry(key).or_insert(0) inserts 0 when the key is missing, and either way returns a mutable reference to the value in the map. Dereference it with * to change it in place:
*stock.entry("figs").or_insert(0) += 1;
Without the *, the expression would add to the reference itself, and the code would not compile.
Iterating
You can loop over map.keys() and look each value up, or loop over the pairs directly with for (key, val) in &map. Borrowing the map with & keeps it usable after the loop; looping over map itself would move it. Looping visits all n entries, so it takes O(n) time.
The order is arbitrary. Do not rely on the order matching the order of insertion, and expect it to differ from one run of the program to the next. When you need a stable order, collect the keys into a Vec and sort it first.
The exercise, step by step
The lesson tracks a fruit stock in a map named fruits.
- Create the map with
HashMap::new()and insert("apples", 3),("bananas", 5)and("grapes", 2). The map ismutbecauseinsertchanges it. The first insert tells the compiler the key and value types. - Add four apples with
entry("apples").or_insert(0)and+= 4, using*on the reference. The count becomes 7. - Report a missing fruit.
contains_key("pears")isfalse, so!flips it and theifprintsOut of pears. - Remove the bananas with
remove. The returnedOptionis ignored here. - Print every fruit by looping over
fruits.keys(), with the loop variable namedfruit, and reading the count asfruits[fruit].
let mut fruits = HashMap::new();
fruits.insert("apples", 3);
fruits.insert("bananas", 5);
fruits.insert("grapes", 2);
*fruits.entry("apples").or_insert(0) += 4;
if !fruits.contains_key("pears") {
println!("Out of pears");
}
fruits.remove("bananas");
for fruit in fruits.keys() {
println!("We have {} {}.", fruits[fruit], fruit);
}
The program prints Out of pears, then We have 7 apples. and We have 2 grapes.. The last two lines can come out in either order, because of the arbitrary iteration order.
Common mistakes
- Indexing a missing key.
fruits["pears"]panics. Usegetor checkcontains_keyfirst. - Forgetting
mut. Callinginsert,removeorentryon a map declared withoutmutis a compile error. - Forgetting the
*afteror_insertwhen updating a number. - Assuming an order. A
HashMapdoes not remember insertion order. - Missing the import. Without
use std::collections::HashMap;the nameHashMapis not found.
Key takeaways
HashMap<K, V>maps unique keys to values; lookup, insert and remove areO(1)on average.map[key]panics for a missing key;getreturns anOption.insertboth adds and updates;entry(key).or_insert(default)is the idiom for counting.- Iteration order is arbitrary, and a full pass costs
O(n).
