Start learning free

LESSON 262 OF 414 · HASH TABLE

Hash Maps in Rust: HashMap Basics

Lesson Read the full explanation

Hash Tables, better known as Hash Maps, are just collections of key-value pairs. In other words, they are pieces of data (values) mapped to unique identifiers called properties (keys).

Hash Maps were designed to give us a way of, given a key, associating a value with it for very quick lookups.

In Rust, the Hash Map is implemented as a HashMap from the Standard Library.


Creation:

You create an empty Hash Map by declaring a HashMap<keyType, valueType>, where the keyType and valueType are the data types of the keys and values.

Let's declare a Hash Map that maps text values representing digits to their numerical value (e.g. "two" => 2). We will map strings to integers, so the two types are &str and i32. We will name the map digit_value

use std::collections::HashMap;
let mut digit_value: HashMap<&str, i32> = HashMap::new();

Creating an empty map takes O(1) time.


Initialization:

You can also add some key-value pairs in your map at creation:

let mut digit_value = HashMap::from([
    ("zero", 0),
    ("one", 1),
]);

Accessing values:

A value is retrieved from a map by specifying its corresponding key in square brackets ([]):

let mut digit_value = HashMap::from([
    ("zero", 0),
    ("one", 1),
]);

// Accessing values:
println!("{}", digit_value["zero"]); // prints 0
println!("{}", digit_value["one"]); // prints 1

println!("{:?}", digit_value.get("three")); // prints None

As you can see, if you refer to a key that is not in the map, Rust returns None when using .get(). Accessing a missing key with [] will panic at runtime.

Accessing a value from a map takes O(1) time.


Adding an entry:

Adding an entry to an existing map is simply a matter of calling the insert() method with a new key and value:

let mut digit_value = HashMap::from([
    ("zero", 0),
    ("one", 1),
]);

// Adding new entries:
digit_value.insert("three", 3);

//The new Hash Map:
// {
//	"zero": 0,
//  "one":  1,
//	"three": 3
//}

Adding a new entry to a map takes O(1) time.


Updating an entry:

If you want to update an entry, you can just call insert() again with an existing key and a new value:

let mut digit_value = HashMap::from([
    ("zero", 0),
    ("one", 1),
]);

// Updating entries:
digit_value.insert("zero", 5);

//The new Hash Map:
// {
//	"zero": 5,
//  "one":  1
//}

Updating an entry from a map takes O(1) time.


Deleting entries:

The remove() method allows you to remove a key from a map:

let mut digit_value = HashMap::from([
    ("zero", 0),
    ("one", 1),
]);

// Removing an entry
digit_value.remove("zero");

//The new Hash Map:
// {
//  "one":  1
//}

If you try to delete a key that is not in the map, it won't do anything.

Deleting an entry from a map takes O(1) time.


Checking if a key exists:

You can check if a key exists in a map using the contains_key() method:

let digit_value = HashMap::from([
    ("zero", 0),
    ("one", 1),
]);

println!("{}", digit_value.contains_key("zero")); // prints true since the key exists

println!("{}", digit_value.contains_key("three")); // prints false since the key does not exist in the map

Checking if a key exists in the map is O(1) time.


Iterating over the map keys:

If we want to iterate over keys of the map, we can use the for loop along with the keys() method:

let digit_value = HashMap::from([
    ("zero", 0),
    ("one", 1),
]);

for key in digit_value.keys() {
    println!("{} is {}", key, digit_value[key]);
}

// This will print:
// one is 1
// zero is 0

Notice the order is not the same as initiated. HashMap keeps the data in random order

Iterating over a map takes O(n) time.


Iterating using both the key and the value for simplicity:

If we want to iterate over both keys and values of the map, we can use the for loop along with the & operator as follows:

let digit_value = HashMap::from([
    ("zero", 0),
    ("one", 1),
]);

for (key, val) in &digit_value {
    println!("{} is {}", key, val);
}

// This will print:
// one is 1
// zero is 0

Notice the order is not the same as initiated. HashMap keeps the data in random order

Iterating over a HashMap takes O(n) time.


Assignment
Follow the Coding Tutorial and let's practice with HashMaps.


Hint
Look at the examples above if you get stuck.


STEP 1 OF 7
    Ctrl + Enter to check

    RUST · HASH TABLE

    Hash Map in Rust, explained

    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 an Option: Some(&value) when the key exists and None when 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.

    1. Create the map with HashMap::new() and insert ("apples", 3), ("bananas", 5) and ("grapes", 2). The map is mut because insert changes it. The first insert tells the compiler the key and value types.
    2. Add four apples with entry("apples").or_insert(0) and += 4, using * on the reference. The count becomes 7.
    3. Report a missing fruit. contains_key("pears") is false, so ! flips it and the if prints Out of pears.
    4. Remove the bananas with remove. The returned Option is ignored here.
    5. Print every fruit by looping over fruits.keys(), with the loop variable named fruit, and reading the count as fruits[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. Use get or check contains_key first.
    • Forgetting mut. Calling insert, remove or entry on a map declared without mut is a compile error.
    • Forgetting the * after or_insert when updating a number.
    • Assuming an order. A HashMap does not remember insertion order.
    • Missing the import. Without use std::collections::HashMap; the name HashMap is not found.

    Key takeaways

    • HashMap<K, V> maps unique keys to values; lookup, insert and remove are O(1) on average.
    • map[key] panics for a missing key; get returns an Option.
    • insert both adds and updates; entry(key).or_insert(default) is the idiom for counting.
    • Iteration order is arbitrary, and a full pass costs O(n).
    Keep going with RustAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free