Module: Collections

HashMaps

Rust Programming: HashMaps

HashMaps are a fundamental data structure in many programming languages, and Rust is no exception. They provide a way to store key-value pairs, allowing for efficient lookup of values based on their associated keys. This document will cover HashMaps in Rust, including creation, manipulation, iteration, and common use cases.

What is a HashMap?

A HashMap (short for Hash Map) is a collection that stores data in key-value pairs.

  • Keys: Must be unique and implement the Eq and Hash traits. Common key types include integers, strings, and custom structs that implement these traits.
  • Values: Can be of any type. There are no restrictions on the value type.

HashMaps offer average O(1) time complexity for insertion, deletion, and lookup, making them very efficient for scenarios where you need to quickly access data based on a key. However, in the worst case (hash collisions), performance can degrade to O(n).

Importing HashMaps

To use HashMaps, you need to bring them into scope using the use keyword:

use std::collections::HashMap;

Creating a HashMap

There are several ways to create a HashMap:

  1. Empty HashMap:

    let mut scores: HashMap<String, i32> = HashMap::new();
    

    This creates an empty HashMap where keys are Strings and values are i32s. The mut keyword is essential if you intend to modify the HashMap later.

  2. Initializing with Values:

    let mut scores = HashMap::from([
        ("Blue".to_string(), 10),
        ("Yellow".to_string(), 50),
    ]);
    

    This creates a HashMap and initializes it with the provided key-value pairs. Note the use of to_string() to convert string literals to String objects, as HashMaps require String keys in this example.

  3. Collecting from an Iterator:

    let teams = vec![("Red".to_string(), 20), ("Green".to_string(), 30)];
    let scores: HashMap<String, i32> = teams.into_iter().collect();
    

    This converts a vector of tuples into a HashMap. into_iter() consumes the vector, and collect() gathers the elements into a HashMap.

Inserting and Updating Values

  • insert(key, value): Inserts a new key-value pair into the HashMap. If the key already exists, the old value is overwritten with the new value. Returns Option<V>, where Some(old_value) is returned if the key existed, and None if it was a new key.

    let mut scores = HashMap::new();
    scores.insert("Blue".to_string(), 10);
    scores.insert("Yellow".to_string(), 50);
    
    if let Some(old_score) = scores.insert("Blue".to_string(), 25) {
        println!("Previous score for Blue was: {}", old_score); // Output: Previous score for Blue was: 10
    }
    
  • entry(key).or_insert(value): Inserts a value if the key is not already present. If the key exists, it returns a mutable reference to the existing value. This is useful for updating values based on their current state.

    let mut counts = HashMap::new();
    counts.entry("apple".to_string()).or_insert(0); // Inserts 0 if "apple" doesn't exist
    *counts.entry("apple".to_string()).or_insert(0) += 1; // Increments the count for "apple"
    *counts.entry("banana".to_string()).or_insert(0) += 1; // Inserts 0 and increments for "banana"
    
    println!("{:?}", counts); // Output: {"apple": 1, "banana": 1}
    

Accessing Values

  • get(key): Returns an Option<&V> containing a reference to the value associated with the key, or None if the key is not found.

    let scores = HashMap::from([
        ("Blue".to_string(), 10),
        ("Yellow".to_string(), 50),
    ]);
    
    match scores.get("Blue") {
        Some(score) => println!("Blue's score is: {}", score), // Output: Blue's score is: 10
        None => println!("Blue not found"),
    }
    
    match scores.get("Red") {
        Some(score) => println!("Red's score is: {}", score),
        None => println!("Red not found"), // Output: Red not found
    }
    
  • get_mut(key): Returns an Option<&mut V> containing a mutable reference to the value associated with the key, or None if the key is not found. This allows you to modify the value in place.

    let mut scores = HashMap::from([
        ("Blue".to_string(), 10),
        ("Yellow".to_string(), 50),
    ]);
    
    if let Some(score) = scores.get_mut("Yellow") {
        *score += 5; // Increment Yellow's score
    }
    
    println!("{:?}", scores); // Output: {"Blue": 10, "Yellow": 55}
    

Removing Values

  • remove(key): Removes the key-value pair associated with the key and returns an Option<V> containing the removed value, or None if the key was not found.

    let mut scores = HashMap::from([
        ("Blue".to_string(), 10),
        ("Yellow".to_string(), 50),
    ]);
    
    if let Some(removed_score) = scores.remove("Blue") {
        println!("Removed Blue's score: {}", removed_score); // Output: Removed Blue's score: 10
    }
    
    println!("{:?}", scores); // Output: {"Yellow": 50}
    
  • clear(): Removes all key-value pairs from the HashMap.

    let mut scores = HashMap::from([
        ("Blue".to_string(), 10),
        ("Yellow".to_string(), 50),
    ]);
    
    scores.clear();
    println!("{:?}", scores); // Output: {}
    

Iterating over a HashMap

You can iterate over a HashMap in several ways:

  • Iterating over Keys:

    let scores = HashMap::from([
        ("Blue".to_string(), 10),
        ("Yellow".to_string(), 50),
    ]);
    
    for key in scores.keys() {
        println!("Key: {}", key);
    }
    
  • Iterating over Values:

    let scores = HashMap::from([
        ("Blue".to_string(), 10),
        ("Yellow".to_string(), 50),
    ]);
    
    for value in scores.values() {
        println!("Value: {}", value);
    }
    
  • Iterating over Key-Value Pairs:

    let scores = HashMap::from([
        ("Blue".to_string(), 10),
        ("Yellow".to_string(), 50),
    ]);
    
    for (key, value) in &scores { // Use &scores to borrow the HashMap
        println!("Key: {}, Value: {}", key, value);
    }
    

HashMap Properties

  • len(): Returns the number of key-value pairs in the HashMap.
  • is_empty(): Returns true if the HashMap is empty, false otherwise.
  • contains_key(key): Returns true if the HashMap contains the specified key, false otherwise.

Example: Word Counting

Here's a practical example of using a HashMap to count the frequency of words in a string:

use std::collections::HashMap;

fn main() {
    let text = "this is a test this is only a test";
    let mut word_counts: HashMap<String, i32> = HashMap::new();

    for word in text.split_whitespace() {
        let cleaned_word = word.to_lowercase(); // Convert to lowercase for case-insensitive counting
        *word_counts.entry(cleaned_word).or_insert(0) += 1;
    }

    println!("{:?}", word_counts);
    // Output: {"this": 2, "is": 2, "a": 2, "test": 2, "only": 1}
}

Key Considerations

  • Hashing Performance: The performance of a HashMap relies on a