HashMaps Exercises

Overview

These exercises cover HashMap<K, V> — Rust’s key-value store with the powerful Entry API.


hashmaps1 - Creating and Inserting

Concept: Basic HashMap operations

use std::collections::HashMap;
 
fn fruit_basket() -> HashMap<String, u32> {
    let mut basket = HashMap::new();
 
    basket.insert(String::from("banana"), 2);
    basket.insert(String::from("apple"), 3);
    basket.insert(String::from("mango"), 1);
 
    basket
}

Key Takeaways:

  • Import with use std::collections::HashMap
  • Create with HashMap::new()
  • Insert with .insert(key, value)
  • Keys must implement Hash + Eq

hashmaps2 - Entry API: Insert If Not Exists

Concept: Using entry().or_insert() to conditionally insert

fn fruit_basket(basket: &mut HashMap<Fruit, u32>) {
    let fruit_kinds = [Fruit::Apple, Fruit::Banana, Fruit::Mango, Fruit::Lychee, Fruit::Pineapple];
 
    for fruit in fruit_kinds {
        // Only insert if the key doesn't exist
        basket.entry(fruit).or_insert(1);
    }
}

Why use Entry API instead of contains_key?

// ❌ Two lookups
if !basket.contains_key(&fruit) {
    basket.insert(fruit, 1);
}
 
// ✅ One lookup
basket.entry(fruit).or_insert(1);

Key Takeaways:

  • entry(key) returns an Entry enum (either Occupied or Vacant)
  • .or_insert(value) inserts only if vacant, returns &mut V
  • More efficient than checking + inserting separately

hashmaps3 - Entry API: Update Existing Values

Concept: Using entry().or_default() with struct values

The Problem

Parse soccer match results and build a scores table:

  • Input: "England,France,4,2" (England scored 4, France scored 2)
  • Track: goals_scored and goals_conceded for each team

The Solution

#[derive(Default)]
struct TeamScores {
    goals_scored: u8,
    goals_conceded: u8,
}
 
fn build_scores_table(results: &str) -> HashMap<&str, TeamScores> {
    let mut scores = HashMap::new();
 
    for line in results.lines() {
        let mut split_iterator = line.split(',');
        let team_1_name = split_iterator.next().unwrap();
        let team_2_name = split_iterator.next().unwrap();
        let team_1_score: u8 = split_iterator.next().unwrap().parse().unwrap();
        let team_2_score: u8 = split_iterator.next().unwrap().parse().unwrap();
 
        // Get or create team 1's entry, then update
        let team_1 = scores.entry(team_1_name).or_default();
        team_1.goals_scored += team_1_score;
        team_1.goals_conceded += team_2_score;
 
        // Get or create team 2's entry, then update
        let team_2 = scores.entry(team_2_name).or_default();
        team_2.goals_scored += team_2_score;
        team_2.goals_conceded += team_1_score;
    }
 
    scores
}

Step-by-Step Execution

Let’s trace through the input data:

England,France,4,2
France,Italy,3,1
Poland,Spain,2,0
Germany,England,2,1
England,Spain,1,0

Match 1: “England,France,4,2”

Parsing:

team_1_name = "England"
team_2_name = "France"
team_1_score = 4
team_2_score = 2

Step 1: scores.entry("England").or_default()

  • HashMap is empty → Entry::Vacant
  • or_default() inserts TeamScores { goals_scored: 0, goals_conceded: 0 }
  • Returns &mut TeamScores

Step 2: Update England

team_1.goals_scored += 4;   // 0 + 4 = 4
team_1.goals_conceded += 2; // 0 + 2 = 2

Step 3: scores.entry("France").or_default()

  • “France” not in HashMap → Entry::Vacant
  • or_default() inserts default TeamScores
  • Returns &mut TeamScores

Step 4: Update France

team_2.goals_scored += 2;   // 0 + 2 = 2
team_2.goals_conceded += 4; // 0 + 4 = 4

HashMap after Match 1:

{
  "England": { goals_scored: 4, goals_conceded: 2 },
  "France":  { goals_scored: 2, goals_conceded: 4 }
}

Match 2: “France,Italy,3,1”

Parsing:

team_1_name = "France"
team_2_name = "Italy"
team_1_score = 3
team_2_score = 1

Step 1: scores.entry("France").or_default()

  • “France” already exists → Entry::Occupied
  • or_default() does nothing, returns existing &mut TeamScores

Step 2: Update France (adds to existing values!)

team_1.goals_scored += 3;   // 2 + 3 = 5
team_1.goals_conceded += 1; // 4 + 1 = 5

Step 3: scores.entry("Italy").or_default() → creates new entry

Step 4: Update Italy

team_2.goals_scored += 1;   // 0 + 1 = 1
team_2.goals_conceded += 3; // 0 + 3 = 3

HashMap after Match 2:

{
  "England": { goals_scored: 4, goals_conceded: 2 },
  "France":  { goals_scored: 5, goals_conceded: 5 },
  "Italy":   { goals_scored: 1, goals_conceded: 3 }
}

Final State (after all 5 matches)

{
  "England": { goals_scored: 6, goals_conceded: 4 },
  "France":  { goals_scored: 5, goals_conceded: 5 },
  "Italy":   { goals_scored: 1, goals_conceded: 3 },
  "Poland":  { goals_scored: 2, goals_conceded: 0 },
  "Spain":   { goals_scored: 0, goals_conceded: 3 },
  "Germany": { goals_scored: 2, goals_conceded: 1 }
}

Key Concepts

ConceptExplanation
#[derive(Default)]Allows or_default() to create TeamScores { 0, 0 }
entry(key)Returns Entry::Occupied or Entry::Vacant
or_default()If vacant, insert Default::default(); returns &mut V
&mut VMutable reference lets you update the value in place

Why This Works

  1. First lookup: If team doesn’t exist, create with zeros
  2. Same lookup: Returns mutable reference to update
  3. Accumulation: += adds to existing values, building totals across matches

Entry API Summary

MethodWhen to Use
.or_insert(value)Insert specific value if vacant
.or_insert_with(|| ...)Insert computed value if vacant (lazy)
.or_default()Insert Default::default() if vacant
.and_modify(|v| ...)Modify if occupied

Rust Book Reference