Map #
HashMap<K, V> is the standard key-value collection in Rust — equivalent to dict in Python, Map in JavaScript, or HashMap in Java. It stores key-value pairs with an average access time of O(1). What distinguishes Rust’s HashMap from other languages is its strictness about ownership: when a value is inserted, the HashMap takes over ownership of it. And the way to update an existing value — through the Entry API — is far more elegant and idiomatic than what’s available in many other languages. This article covers HashMap thoroughly, including BTreeMap for sorted-order needs, HashSet as a valueless collection, and a guide to choosing among the three.
Creating a HashMap #
use std::collections::HashMap;
fn main() {
// Empty — the type is inferred from the first insert
let mut skor: HashMap<String, i32> = HashMap::new();
// Insert key-value pairs
skor.insert(String::from("Tim Merah"), 85);
skor.insert(String::from("Tim Biru"), 92);
skor.insert(String::from("Tim Hijau"), 78);
println!("{:?}", skor);
// From two Vecs with zip + collect
let tim = vec!["Merah", "Biru", "Hijau"];
let nilai = vec![85, 92, 78];
let map: HashMap<_, _> = tim.iter().zip(nilai.iter()).collect();
println!("{:?}", map);
// From an array of tuples
let konfigurasi: HashMap<&str, &str> = [
("host", "localhost"),
("port", "8080"),
("mode", "debug"),
].into_iter().collect();
println!("{:?}", konfigurasi);
// With initial capacity to avoid reallocation
let mut cache: HashMap<u64, String> = HashMap::with_capacity(1000);
println!("Initial capacity: {}", cache.capacity());
}
Accessing and Searching #
use std::collections::HashMap;
fn main() {
let mut populasi: HashMap<&str, u64> = HashMap::new();
populasi.insert("Jakarta", 10_500_000);
populasi.insert("Surabaya", 2_900_000);
populasi.insert("Bandung", 2_500_000);
// get() — returns Option<&V>
match populasi.get("Jakarta") {
Some(p) => println!("Jakarta: {} people", p),
None => println!("Not found"),
}
// get() with if let — more concise
if let Some(p) = populasi.get("Surabaya") {
println!("Surabaya: {} people", p);
}
// unwrap_or — a default value if the key is missing
let malang = populasi.get("Malang").copied().unwrap_or(0);
println!("Malang: {} people", malang);
// contains_key() — check existence without fetching the value
println!("Has Jakarta: {}", populasi.contains_key("Jakarta")); // true
println!("Has Malang: {}", populasi.contains_key("Malang")); // false
// Direct access via [] — panics if the key is missing
// println!("{}", populasi["Malang"]); // panic!
// CORRECT: use get() for safe access
let kota = "Bandung";
if let Some(&p) = populasi.get(kota) {
println!("{}: {}", kota, p);
}
// len() and is_empty()
println!("Number of cities: {}", populasi.len());
println!("Empty: {}", populasi.is_empty());
}
The Entry API — Idiomatic Update Patterns #
The Entry API is one of the most elegant features of Rust’s HashMap. It handles three common scenarios — insert new, skip if present, update based on the old value — in a single expression without double lookups:
use std::collections::HashMap;
fn main() {
let mut skor: HashMap<String, i32> = HashMap::new();
// or_insert — insert if absent, return a mutable reference to the value
skor.entry(String::from("Tim A")).or_insert(0);
skor.entry(String::from("Tim A")).or_insert(999); // ignored, Tim A already exists
skor.entry(String::from("Tim B")).or_insert(0);
println!("{:?}", skor); // {"Tim A": 0, "Tim B": 0}
// or_insert_with — insert with a value from a closure (lazy evaluation)
// The closure is only called if the key doesn't exist yet
skor.entry(String::from("Tim C")).or_insert_with(|| {
println!("Computing Tim C's initial score...");
50 // an "expensive" initial value to compute
});
// Modify the value through the mutable reference returned by or_insert
let nilai_a = skor.entry(String::from("Tim A")).or_insert(0);
*nilai_a += 10;
println!("{:?}", skor); // Tim A: 10
// Common pattern: word frequency counting
let teks = "apel mangga apel jeruk mangga apel";
let mut frekuensi: HashMap<&str, u32> = HashMap::new();
for kata in teks.split_whitespace() {
let counter = frekuensi.entry(kata).or_insert(0);
*counter += 1;
}
println!("{:?}", frekuensi); // {"apel": 3, "mangga": 2, "jeruk": 1}
// Common pattern: group elements into Vecs
let data = vec![
("kategori-a", "item-1"),
("kategori-b", "item-2"),
("kategori-a", "item-3"),
("kategori-b", "item-4"),
("kategori-c", "item-5"),
];
let mut kelompok: HashMap<&str, Vec<&str>> = HashMap::new();
for (kategori, item) in data {
kelompok.entry(kategori).or_insert_with(Vec::new).push(item);
}
for (k, v) in &kelompok {
println!("{}: {:?}", k, v);
}
}
Updating and Removing #
use std::collections::HashMap;
fn main() {
let mut harga: HashMap<&str, f64> = HashMap::new();
harga.insert("kopi", 25_000.0);
harga.insert("teh", 15_000.0);
harga.insert("jus", 30_000.0);
// insert() with the same key overwrites the old value
// and returns the old value that was replaced
let harga_lama = harga.insert("kopi", 28_000.0);
println!("Old coffee price: {:?}", harga_lama); // Some(25000.0)
println!("New coffee price: {}", harga["kopi"]); // 28000.0
// remove() — remove and return the removed value
let harga_teh = harga.remove("teh");
println!("Tea removed: {:?}", harga_teh); // Some(15000.0)
println!("After removal: {:?}", harga);
// retain() — keep only the pairs satisfying the condition
let mut inventori: HashMap<&str, u32> = [
("apel", 50), ("mangga", 0), ("jeruk", 30), ("durian", 0),
].into_iter().collect();
inventori.retain(|_, &mut stok| stok > 0);
println!("Available stock: {:?}", inventori);
// clear() — remove all pairs
inventori.clear();
println!("After clear: {:?}", inventori);
}
Iteration #
The iteration order of a HashMap is not guaranteed — this is a consequence of hashing. If you need a specific order, use BTreeMap or sort the results:
use std::collections::HashMap;
fn main() {
let mut kota: HashMap<&str, u64> = [
("Jakarta", 10_500_000u64),
("Surabaya", 2_900_000),
("Bandung", 2_500_000),
("Medan", 2_100_000),
].into_iter().collect();
// Iterate keys and values — order not guaranteed
println!("=== All Cities ===");
for (nama, pop) in &kota {
println!("{}: {}", nama, pop);
}
// Iterate only the keys
let mut nama_kota: Vec<&&str> = kota.keys().collect();
nama_kota.sort();
println!("\nCities (sorted): {:?}", nama_kota);
// Iterate only the values
let total: u64 = kota.values().sum();
println!("Total population: {}", total);
// Iterate with modification via values_mut()
for pop in kota.values_mut() {
*pop = (*pop as f64 * 1.02) as u64; // increase by 2%
}
// Sorted iteration — sort by key
let mut vec_kota: Vec<(&&str, &u64)> = kota.iter().collect();
vec_kota.sort_by_key(|&(k, _)| *k);
println!("\nSorted by name:");
for (nama, pop) in vec_kota {
println!(" {}: {}", nama, pop);
}
// Sort by value
let mut vec_pop: Vec<(&&str, &u64)> = kota.iter().collect();
vec_pop.sort_by(|a, b| b.1.cmp(a.1)); // descending
println!("\nSorted by population (largest first):");
for (nama, pop) in vec_pop {
println!(" {}: {}", nama, pop);
}
}
Ownership in a HashMap #
Types implementing Copy (integers, floats, bools) are copied on insert. Owned types like String are moved:
use std::collections::HashMap;
fn main() {
let kunci = String::from("nama");
let nilai = String::from("Budi");
let mut map = HashMap::new();
map.insert(kunci, nilai); // kunci and nilai are MOVED into the map
// ANTI-PATTERN: using the variables after they've been moved
// println!("{}", kunci); // error: kunci has been moved
// println!("{}", nilai); // error: nilai has been moved
// CORRECT: use references if you still need the original variables
let kunci2 = String::from("usia");
let nilai2 = 30u32; // Copy type
let mut map2: HashMap<&str, u32> = HashMap::new();
map2.insert(&kunci2, nilai2); // &kunci2 = borrow
println!("kunci2 still valid: {}", kunci2); // ✓
println!("nilai2 still valid: {}", nilai2); // ✓ because Copy
// References as keys — need a lifetime valid for as long as the HashMap lives
let teks = String::from("hello world");
let mut frekuensi: HashMap<&str, u32> = HashMap::new();
for kata in teks.split_whitespace() {
*frekuensi.entry(kata).or_insert(0) += 1;
}
println!("{:?}", frekuensi);
// teks must live at least as long as frekuensi
}
Custom Keys #
A type used as a HashMap key must implement Hash and Eq. The easiest way is through #[derive]:
use std::collections::HashMap;
// Derive Hash and Eq — sufficient for common cases
#[derive(Debug, Hash, PartialEq, Eq)]
struct KoordinatGrid {
x: i32,
y: i32,
}
// A more complex custom type
#[derive(Debug, Hash, PartialEq, Eq)]
struct IdProduk {
kategori: String,
kode: u32,
}
fn main() {
// HashMap with a struct key
let mut peta: HashMap<KoordinatGrid, &str> = HashMap::new();
peta.insert(KoordinatGrid { x: 0, y: 0 }, "Origin");
peta.insert(KoordinatGrid { x: 1, y: 0 }, "Right");
peta.insert(KoordinatGrid { x: 0, y: 1 }, "Up");
let posisi = KoordinatGrid { x: 1, y: 0 };
println!("{:?}", peta.get(&posisi)); // Some("Right")
// HashMap with an enum key — derive Hash and Eq on the enum
#[derive(Debug, Hash, PartialEq, Eq)]
enum Mata {
Matematika,
IPA,
Bahasa(String),
}
let mut nilai: HashMap<Mata, u8> = HashMap::new();
nilai.insert(Mata::Matematika, 90);
nilai.insert(Mata::IPA, 85);
nilai.insert(Mata::Bahasa(String::from("Inggris")), 92);
println!("{:?}", nilai.get(&Mata::Matematika)); // Some(90)
}
BTreeMap — A Sorted Map
#
BTreeMap<K, V> stores key-value pairs in a B-tree structure that is always sorted by key. Its operations are O(log n) — slower than HashMap’s O(1), but it guarantees iteration order:
use std::collections::BTreeMap;
fn main() {
let mut harga: BTreeMap<&str, f64> = BTreeMap::new();
harga.insert("pisang", 5_000.0);
harga.insert("apel", 15_000.0);
harga.insert("mangga", 20_000.0);
harga.insert("jeruk", 10_000.0);
// Iteration is always sorted by key (alphanumeric)
println!("=== Price List (Alphabetical) ===");
for (nama, harga) in &harga {
println!(" {:10}: Rp{:.0}", nama, harga);
}
// range() — key range queries
println!("\nFruits between 'j' and 'p':");
for (nama, h) in harga.range("j"..="p") {
println!(" {}: Rp{:.0}", nama, h);
}
// first_key_value() and last_key_value()
println!("\nFirst: {:?}", harga.first_key_value());
println!("Last: {:?}", harga.last_key_value());
// Real case: logs sorted by timestamp
let mut log: BTreeMap<u64, String> = BTreeMap::new();
log.insert(1_700_000_100, String::from("Server started"));
log.insert(1_700_000_050, String::from("Configuration loaded"));
log.insert(1_700_000_200, String::from("Connection accepted"));
println!("\n=== Log (Chronological) ===");
for (ts, pesan) in &log {
println!(" {} | {}", ts, pesan);
}
}
HashSet — A Collection of Unique Keys Without Values
#
HashSet<T> is a simplified HashMap<T, ()> — it stores unique values without extra data. Useful for O(1) membership checks and set operations:
use std::collections::HashSet;
fn main() {
let mut set: HashSet<i32> = HashSet::new();
set.insert(1);
set.insert(2);
set.insert(3);
set.insert(2); // duplicate — ignored
set.insert(1); // duplicate — ignored
println!("{:?}", set); // {1, 2, 3} — order not guaranteed
// contains() — O(1) membership check
println!("Has 2: {}", set.contains(&2)); // true
println!("Has 5: {}", set.contains(&5)); // false
// From a Vec — an easy way to remove duplicates
let angka_duplikat = vec![1, 2, 3, 2, 1, 4, 3, 5];
let unik: HashSet<i32> = angka_duplikat.into_iter().collect();
println!("Unique: {:?}", unik);
// Set operations
let a: HashSet<i32> = [1, 2, 3, 4, 5].into_iter().collect();
let b: HashSet<i32> = [3, 4, 5, 6, 7].into_iter().collect();
// Intersection — in both
let irisan: HashSet<_> = a.intersection(&b).collect();
println!("Intersection: {:?}", irisan); // {3, 4, 5}
// Union — in either or both
let gabungan: HashSet<_> = a.union(&b).collect();
println!("Union: {:?}", gabungan); // {1, 2, 3, 4, 5, 6, 7}
// Difference — in a but not in b
let selisih: HashSet<_> = a.difference(&b).collect();
println!("A \\ B: {:?}", selisih); // {1, 2}
// Subset and superset
let c: HashSet<i32> = [3, 4].into_iter().collect();
println!("c ⊆ a: {}", c.is_subset(&a)); // true
println!("a ⊇ c: {}", a.is_superset(&c)); // true
}
Comparing HashMap, BTreeMap, and HashSet #
| Aspect | HashMap<K,V> | BTreeMap<K,V> | HashSet<T> |
|---|---|---|---|
| Complexity | O(1) average | O(log n) | O(1) average |
| Iteration order | Not guaranteed | Sorted by key | Not guaranteed |
| Key constraints | Hash + Eq | Ord | Hash + Eq |
| Range queries | No | Yes (.range()) | No |
| Memory usage | More efficient | More | More efficient |
| When to use | Default map | When order matters | Unique membership checks |
Use HashMap if:
✓ Standard map needs — lookup, insert, delete
✓ Iteration order doesn't matter
✓ Optimal O(1) performance
Use BTreeMap if:
✓ Need iteration in sorted order
✓ Need key range queries
✓ Keys must always be in a specific order
Use HashSet if:
✓ Only need to know whether an element exists
✓ Need set operations (union, intersection, difference)
✓ Removing duplicates from a collection
Summary #
- The Entry API is the idiomatic way to update —
.entry(k).or_insert(v)handles insert-if-absent,.or_insert_with(||)for expensive-to-compute values, and the returned mutable reference for updates based on the old value.HashMaptakes ownership of keys and values — an insertedStringcan’t be used again afterward. Use&stras a key if you still need the original variable.- Iteration order isn’t guaranteed — if you need sorted iteration, use
BTreeMapor collect into a Vec and sort.- Use
get()instead of[]for safe access —map[k]panics if the key is missing;map.get(k)returns anOption.contains_key()for existence checks without fetching the value — more expressive thanget().is_some().retain()for in-place filtering — more efficient than filter + collect into a new HashMap.- Custom keys need
Hash + Eq— use#[derive(Hash, PartialEq, Eq)]for simple structs or enums.BTreeMapfor guaranteed order — O(log n) slower but supports.range()and iteration is always sorted by key.HashSetfor unique membership — O(1) lookup and supports set operations (union, intersection, difference, subset).