Skip to content
Lesson 11 of 15

Step 1 of 5 · Reading · ~3 min

Learn

Collections

HashMap

A HashMap<K, V> stores values under keys: give it a key, get back the value filed under it. By the end of this lesson you can count how often things occur with the entry idiom, look counts up safely, and avoid the ordering trap that makes HashMap output change from run to run.

Creating and filling a map

HashMap is not imported automatically. It lives in std::collections, so the first line of any program that uses it is use std::collections::HashMap;. HashMap<&str, i64> means keys are &str and values are i64.

let mut stock: HashMap<&str, i64> = HashMap::new();
stock.insert("bolt", 40);
stock.insert("nut", 15);
stock.insert("bolt", 35);        // same key: the old 40 is replaced
println!("{}", stock.len());     // 2

A key appears at most once. Inserting an existing key overwrites its value; it does not add a second entry.

Looking a key up

stock["nut"] gives 15, but indexing a key that is not there panics. The careful door is get, which returns Some(&value) when the key exists and None when it does not. You handle both with the match you learned in the if-and-match lesson:

match stock.get("washer") {
    Some(n) => println!("washer: {}", n),
    None => println!("washer: none"),
}
// washer: none

Counting with entry

Counting is the job HashMap does most. The difficulty is that the first time you see a key it has no count yet. entry(key) gives you the slot for that key whether it exists or not, .or_insert(0) puts 0 in the slot if it was empty, and either way hands back a mutable reference to the value. The * from the Vec lesson follows that reference so += 1 changes the stored number:

let votes = vec!["kim", "raj", "kim", "lee", "raj", "kim"];
let mut tally: HashMap<&str, i64> = HashMap::new();
for v in &votes {
    *tally.entry(*v).or_insert(0) += 1;
}

v is a &&str because you borrowed the Vec, so *v hands the plain &str to entry. Trace it: kim is new, slot becomes 0, then 1. raj new, 1. kim exists at 1, now 2. lee new, 1. raj 2. kim 3. Final: kim 3, raj 2, lee 1.

The trap is the starting value. or_insert(1) looks reasonable, but the += 1 still runs on the very first sighting, so every count comes out one too high: kim 4, raj 3, lee 2.

Iteration order is not yours

for (name, n) in &tally visits every pair, but in an order HashMap chooses, and that order can differ between two runs of the same program on the same input. When the order matters, walk something whose order you control, such as the original Vec or a list of names, and look each key up:

let roster = vec!["lee", "ann", "kim"];
for name in &roster {
    match tally.get(name) {
        Some(n) => println!("{} {}", name, n),
        None => println!("{} 0", name),
    }
}
// lee 1
// ann 0
// kim 3

Common mistakes

  • or_insert(1) instead of or_insert(0). Every count is one too high, so a line where each word appears once reports a count of 2.
  • Picking the answer by looping over the map. With a tie, the winner depends on HashMap's order, so the program can print a different word on a different run. Walk the words in input order instead.
  • Using >= when comparing counts. A tie then replaces the earlier word with the later one, and x y x y reports y instead of x.
  • Treating Dog and dog as the same key. Keys are compared exactly, byte for byte; they are two different words.

Your exercise

Print the most frequent word in a line and how many times it appears. The starter already splits the line into words and prints best_word and best_count; you fill counts with the entry idiom and then choose the winner. The mistake to avoid is letting a tie go to any word but the one that appears first.

Up nextIteratorsCollections

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…