Web Analytics

HashMaps

Intermediate ~30 min read

A HashMap stores a mapping of keys of type K to values of type V. It does this via a hashing function, which determines how it places these keys and values into memory. HashMaps are useful when you need to look up data not by an index, but by using a key that can be of any type.

Rust HashMap Structure

Creating HashMaps

HashMaps are not included in the prelude, so you need to import them:

use std::collections::HashMap;

let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
Import Required: Unlike Vec and String, HashMap is not in the prelude, so you must import it with use std::collections::HashMap;.

Creating HashMaps from Vectors

You can create a HashMap from a vector of tuples using collect():

use std::collections::HashMap;

let teams = vec![
    (String::from("Blue"), 10),
    (String::from("Yellow"), 50),
];
let scores: HashMap<_, _> = teams.into_iter().collect();
Type Inference: The <_, _> tells Rust to infer the key and value types from the vector. You could also write HashMap<String, i32> explicitly.
Output
Click Run to execute your code

Accessing Values

You can get a value out of the HashMap by providing its key to the get method:

use std::collections::HashMap;

let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);

let team_name = String::from("Blue");
let score = scores.get(&team_name);

The get method returns an Option<&V>, so the result is wrapped in Some if the key exists, or None if it doesn't:

match score {
    Some(s) => println!("Score: {}", s),
    None => println!("Team not found"),
}

Updating HashMaps

HashMaps have several ways to update values:

1. Overwriting a Value

If you insert a value with a key that already exists, the value will be overwritten:

scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Blue"), 25);  // Overwrites 10
println!("{:?}", scores);  // {"Blue": 25}

2. Only Inserting If the Key Has No Value

Use the entry API with or_insert:

scores.entry(String::from("Yellow")).or_insert(50);
scores.entry(String::from("Blue")).or_insert(50);  // Won't overwrite

3. Updating a Value Based on the Old Value

The entry API also allows you to update a value based on the old value:

let text = "hello world wonderful world";
let mut map = HashMap::new();

for word in text.split_whitespace() {
    let count = map.entry(word).or_insert(0);
    *count += 1;
}
The Entry API: The entry method returns an Entry enum that represents a value that might or might not exist. The or_insert method returns a mutable reference to the value for the corresponding Entry key if that key exists, and if not, inserts the parameter as the new value.

Iterating Over HashMaps

You can iterate over each key-value pair in a HashMap:

for (key, value) in &scores {
    println!("{}: {}", key, value);
}
Unordered: HashMaps are unordered. If you need ordered key-value pairs, use BTreeMap instead, which stores keys in sorted order.

Ownership

For types that implement the Copy trait, like i32, the values are copied into the HashMap. For owned values like String, the values will be moved and the HashMap will be the owner of those values:

use std::collections::HashMap;

let field_name = String::from("Favorite color");
let field_value = String::from("Blue");

let mut map = HashMap::new();
map.insert(field_name, field_value);
// field_name and field_value are invalid at this point
Ownership Transfer: When you insert owned values (like String) into a HashMap, ownership is transferred. The variables are no longer valid after insertion. If you insert references, the values must be valid for the lifetime of the HashMap.

HashMap Methods

Method Description Example
insert(k, v) Insert or update key-value pair map.insert("key", 10)
get(&k) Get value by key (returns Option) map.get(&"key")
get_mut(&k) Get mutable reference to value map.get_mut(&"key")
entry(k) Get entry for key (Entry API) map.entry("key")
remove(&k) Remove key-value pair map.remove(&"key")
len() Get number of key-value pairs map.len()
contains_key(&k) Check if key exists map.contains_key(&"key")

Common Patterns

1. Word Counting

let text = "hello world wonderful world";
let mut word_count = HashMap::new();

for word in text.split_whitespace() {
    let count = word_count.entry(word).or_insert(0);
    *count += 1;
}

2. Default Values

let score = scores.get(&team_name).copied().unwrap_or(0);

3. Conditional Updates

scores.entry(team_name.clone())
    .and_modify(|e| *e += 10)
    .or_insert(50);

When to Use HashMaps

Use HashMap<K, V> when:

  • You need key-value lookups: When you need to look up values by a key rather than by index
  • You need fast lookups: O(1) average time complexity for get/insert operations
  • You're building caches or indexes: HashMaps are perfect for caching computed values
  • You're counting occurrences: Use HashMap to count frequencies (e.g., word counts, character counts)
  • You're grouping data: When you need to group items by a key (e.g., employees by department)
  • You need associative arrays: When you need to map one set of values to another
  • Order doesn't matter: HashMaps are unordered (use BTreeMap if you need sorted order)

Consider alternatives when:

  • You need ordered keys: Use BTreeMap<K, V> for sorted key-value pairs
  • You only need keys (no values): Use HashSet<T> for unique elements
  • You need indexed access: Use Vec<T> for sequential, indexed data
  • You have a small, fixed set: Consider using arrays or tuples for small datasets
  • Keys don't implement Hash: You'll need to use BTreeMap which only requires Ord
Performance Tip: HashMaps provide O(1) average-case performance for lookups, inserts, and deletes. However, they have higher memory overhead than vectors. For small datasets (< 10 items), the overhead might not be worth it. For large datasets or frequent lookups, HashMaps are ideal.
HashMap vs BTreeMap: Use HashMap when you need fast lookups and order doesn't matter. Use BTreeMap when you need sorted keys, need to iterate in order, or when your keys don't implement Hash (but do implement Ord).

Best Practices

  • Use entry() API: More efficient than checking and inserting separately
  • Prefer get() over indexing: get() returns Option, safer than panicking
  • Consider key types: Keys must implement Eq and Hash traits
  • Use references when possible: Avoid unnecessary clones of keys
  • Consider BTreeMap for ordering: If you need sorted keys
  • Handle missing keys: Always handle the None case from get()
Hash Function: The default hash function is cryptographically secure but not the fastest. For performance-critical code, consider using a different hasher (like FxHashMap from the rustc-hash crate).

Common Mistakes

1. Forgetting to import HashMap

// Wrong - HashMap not in prelude
let mut map = HashMap::new();  // Error: cannot find type `HashMap`

// Correct - Import HashMap
use std::collections::HashMap;
let mut map = HashMap::new();

2. Using owned values when references would work

// Wrong - Unnecessary String allocation
let mut map = HashMap::new();
map.insert(String::from("key"), 10);  // String moved

// Correct - Use &str if keys live long enough
let mut map: HashMap<&str, i32> = HashMap::new();
let key = "key";
map.insert(key, 10);  // key still valid

3. Not handling Option from get()

// Wrong - Can panic if key doesn't exist
let value = map.get(&"key").unwrap();  // Panics if None

// Correct - Handle the Option
match map.get(&"key") {
    Some(value) => println!("{}", value),
    None => println!("Key not found"),
}

// Or use unwrap_or for defaults
let value = map.get(&"key").copied().unwrap_or(0);

4. Trying to use types that don't implement Hash as keys

// Wrong - Vec doesn't implement Hash
let mut map: HashMap<Vec<i32>, i32> = HashMap::new();  // Error!

// Correct - Use types that implement Hash
let mut map: HashMap<String, i32> = HashMap::new();
// Or use a tuple of hashable types
let mut map: HashMap<(i32, i32), i32> = HashMap::new();

Exercise: HashMaps Practice

Task: Implement an employee database using HashMaps.

Requirements:

  • Add employees to departments
  • List all people in a department
  • List all people in company by department (sorted)
  • Use the entry API for efficient updates
Output
Click Run to execute your code
Show Solution
use std::collections::HashMap;

fn add_employee(db: &mut HashMap<String, Vec<String>>, name: String, dept: String) {
    db.entry(dept).or_insert_with(Vec::new).push(name);
}

fn list_department(db: &HashMap<String, Vec<String>>, dept: &str) {
    if let Some(employees) = db.get(dept) {
        println!("Employees in {}:", dept);
        for emp in employees {
            println!("  - {}", emp);
        }
    } else {
        println!("Department '{}' not found", dept);
    }
}

fn list_all(db: &HashMap<String, Vec<String>>) {
    let mut depts: Vec<&String> = db.keys().collect();
    depts.sort();
    
    for dept in depts {
        println!("{}:", dept);
        let mut employees = db[dept].clone();
        employees.sort();
        for emp in employees {
            println!("  - {}", emp);
        }
    }
}

fn main() {
    let mut company: HashMap<String, Vec<String>> = HashMap::new();
    
    add_employee(&mut company, String::from("Sally"), String::from("Engineering"));
    add_employee(&mut company, String::from("Bob"), String::from("Sales"));
    add_employee(&mut company, String::from("Alice"), String::from("Engineering"));
    
    list_department(&company, "Engineering");
    list_all(&company);
}

Summary

  • HashMap<K, V> stores key-value pairs
  • Must import from std::collections
  • Create with HashMap::new() or collect()
  • Access with get(&key) (returns Option<&V>)
  • Insert/update with insert(key, value)
  • Use entry API for conditional updates
  • Keys must implement Eq and Hash
  • Owned values are moved into HashMap
  • HashMaps are unordered
  • Use BTreeMap for sorted key-value pairs

What's Next?

Congratulations! You've completed the Collections module. You now know how to work with vectors, strings, and HashMaps - three of the most commonly used collection types in Rust.

In the next modules, you'll learn about error handling in depth (panic, Result, error propagation), and more advanced Rust features like traits, generics, and lifetimes that build on these foundations.