Map #
HashMap<K, V> adalah koleksi kunci-nilai standar di Rust — setara dengan dict di Python, Map di JavaScript, atau HashMap di Java. Ia menyimpan pasangan kunci-nilai dengan waktu akses rata-rata O(1). Yang membedakan HashMap Rust dari bahasa lain adalah ketegasannya soal ownership: ketika sebuah nilai di-insert, HashMap mengambil alih ownership-nya. Dan cara mengupdate nilai yang sudah ada — lewat Entry API — jauh lebih elegan dan idiomatis dari yang tersedia di banyak bahasa lain. Artikel ini membahas HashMap secara menyeluruh, termasuk BTreeMap untuk kebutuhan urutan terurut, HashSet sebagai koleksi tanpa nilai, dan panduan memilih di antara ketiganya.
Membuat HashMap #
use std::collections::HashMap;
fn main() {
// Kosong — tipe diinfer dari insert pertama
let mut skor: HashMap<String, i32> = HashMap::new();
// Insert pasangan kunci-nilai
skor.insert(String::from("Tim Merah"), 85);
skor.insert(String::from("Tim Biru"), 92);
skor.insert(String::from("Tim Hijau"), 78);
println!("{:?}", skor);
// Dari dua Vec dengan 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);
// Dari array of tuple
let konfigurasi: HashMap<&str, &str> = [
("host", "localhost"),
("port", "8080"),
("mode", "debug"),
].into_iter().collect();
println!("{:?}", konfigurasi);
// Dengan kapasitas awal untuk menghindari realokasi
let mut cache: HashMap<u64, String> = HashMap::with_capacity(1000);
println!("Kapasitas awal: {}", cache.capacity());
}
Mengakses dan Mencari #
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() — mengembalikan Option<&V>
match populasi.get("Jakarta") {
Some(p) => println!("Jakarta: {} jiwa", p),
None => println!("Tidak ditemukan"),
}
// get() dengan if let — lebih ringkas
if let Some(p) = populasi.get("Surabaya") {
println!("Surabaya: {} jiwa", p);
}
// unwrap_or — nilai default jika kunci tidak ada
let malang = populasi.get("Malang").copied().unwrap_or(0);
println!("Malang: {} jiwa", malang);
// contains_key() — cek keberadaan tanpa ambil nilainya
println!("Ada Jakarta: {}", populasi.contains_key("Jakarta")); // true
println!("Ada Malang: {}", populasi.contains_key("Malang")); // false
// Akses langsung via [] — panic jika kunci tidak ada
// println!("{}", populasi["Malang"]); // panic!
// BENAR: gunakan get() untuk akses aman
let kota = "Bandung";
if let Some(&p) = populasi.get(kota) {
println!("{}: {}", kota, p);
}
// len() dan is_empty()
println!("Jumlah kota: {}", populasi.len());
println!("Kosong: {}", populasi.is_empty());
}
Entry API — Pola Idiomatis untuk Update #
Entry API adalah salah satu fitur paling elegan di HashMap Rust. Ia menangani tiga skenario umum — insert baru, skip jika ada, update berdasarkan nilai lama — dalam satu ekspresi tanpa lookup ganda:
use std::collections::HashMap;
fn main() {
let mut skor: HashMap<String, i32> = HashMap::new();
// or_insert — insert jika belum ada, kembalikan referensi mutable ke nilai
skor.entry(String::from("Tim A")).or_insert(0);
skor.entry(String::from("Tim A")).or_insert(999); // diabaikan, Tim A sudah ada
skor.entry(String::from("Tim B")).or_insert(0);
println!("{:?}", skor); // {"Tim A": 0, "Tim B": 0}
// or_insert_with — insert dengan nilai dari closure (lazy evaluation)
// Closure hanya dipanggil jika kunci belum ada
skor.entry(String::from("Tim C")).or_insert_with(|| {
println!("Menghitung skor awal Tim C...");
50 // nilai awal yang "mahal" dihitung
});
// Modifikasi nilai via referensi mutable yang dikembalikan or_insert
let nilai_a = skor.entry(String::from("Tim A")).or_insert(0);
*nilai_a += 10;
println!("{:?}", skor); // Tim A: 10
// Pola umum: hitung frekuensi kata
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}
// Pola umum: kelompokkan elemen ke dalam Vec
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);
}
}
Memperbarui dan Menghapus #
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() dengan kunci yang sama menimpa nilai lama
// dan mengembalikan nilai lama yang ditimpa
let harga_lama = harga.insert("kopi", 28_000.0);
println!("Harga kopi lama: {:?}", harga_lama); // Some(25000.0)
println!("Harga kopi baru: {}", harga["kopi"]); // 28000.0
// remove() — hapus dan kembalikan nilai yang dihapus
let harga_teh = harga.remove("teh");
println!("Teh dihapus: {:?}", harga_teh); // Some(15000.0)
println!("Setelah hapus: {:?}", harga);
// retain() — pertahankan hanya pasangan yang memenuhi kondisi
let mut inventori: HashMap<&str, u32> = [
("apel", 50), ("mangga", 0), ("jeruk", 30), ("durian", 0),
].into_iter().collect();
inventori.retain(|_, &mut stok| stok > 0);
println!("Stok tersedia: {:?}", inventori);
// clear() — hapus semua pasangan
inventori.clear();
println!("Setelah clear: {:?}", inventori);
}
Iterasi #
Urutan iterasi HashMap tidak terjamin — ini adalah konsekuensi dari hashing. Jika butuh urutan tertentu, gunakan BTreeMap atau sort hasilnya:
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();
// Iterasi kunci-nilai — urutan tidak terjamin
println!("=== Semua Kota ===");
for (nama, pop) in &kota {
println!("{}: {}", nama, pop);
}
// Iterasi hanya kunci
let mut nama_kota: Vec<&&str> = kota.keys().collect();
nama_kota.sort();
println!("\nKota (terurut): {:?}", nama_kota);
// Iterasi hanya nilai
let total: u64 = kota.values().sum();
println!("Total populasi: {}", total);
// Iterasi dengan modifikasi via values_mut()
for pop in kota.values_mut() {
*pop = (*pop as f64 * 1.02) as u64; // naik 2%
}
// Iterasi terurut — sort by key
let mut vec_kota: Vec<(&&str, &u64)> = kota.iter().collect();
vec_kota.sort_by_key(|&(k, _)| *k);
println!("\nUrut nama:");
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!("\nUrut populasi (besar ke kecil):");
for (nama, pop) in vec_pop {
println!(" {}: {}", nama, pop);
}
}
Ownership di HashMap #
Tipe yang mengimplementasikan Copy (integer, float, bool) di-copy saat di-insert. Tipe owned seperti String di-move:
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 dan nilai di-MOVE ke map
// ANTI-PATTERN: menggunakan variabel setelah di-move
// println!("{}", kunci); // error: kunci sudah di-move
// println!("{}", nilai); // error: nilai sudah di-move
// BENAR: gunakan referensi jika masih butuh variabel asli
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 masih valid: {}", kunci2); // ✓
println!("nilai2 masih valid: {}", nilai2); // ✓ karena Copy
// Referensi sebagai kunci — perlu lifetime yang valid selama HashMap hidup
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 harus hidup setidaknya selama frekuensi
}
Kunci Kustom #
Tipe yang digunakan sebagai kunci HashMap harus mengimplementasikan Hash dan Eq. Cara termudah adalah lewat #[derive]:
use std::collections::HashMap;
// Derive Hash dan Eq — cukup untuk kasus umum
#[derive(Debug, Hash, PartialEq, Eq)]
struct KoordinatGrid {
x: i32,
y: i32,
}
// Tipe kustom yang lebih kompleks
#[derive(Debug, Hash, PartialEq, Eq)]
struct IdProduk {
kategori: String,
kode: u32,
}
fn main() {
// HashMap dengan kunci struct
let mut peta: HashMap<KoordinatGrid, &str> = HashMap::new();
peta.insert(KoordinatGrid { x: 0, y: 0 }, "Titik asal");
peta.insert(KoordinatGrid { x: 1, y: 0 }, "Kanan");
peta.insert(KoordinatGrid { x: 0, y: 1 }, "Atas");
let posisi = KoordinatGrid { x: 1, y: 0 };
println!("{:?}", peta.get(&posisi)); // Some("Kanan")
// HashMap dengan kunci enum — derive Hash dan Eq pada 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 — Map Terurut
#
BTreeMap<K, V> menyimpan pasangan kunci-nilai dalam struktur B-tree yang selalu terurut berdasarkan kunci. Operasinya O(log n) — lebih lambat dari HashMap yang O(1), tapi menjamin urutan iterasi:
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);
// Iterasi selalu terurut berdasarkan kunci (alphanumerik)
println!("=== Daftar Harga (Terurut Abjad) ===");
for (nama, harga) in &harga {
println!(" {:10}: Rp{:.0}", nama, harga);
}
// range() — query rentang kunci
println!("\nBuah antara 'j' dan 'p':");
for (nama, h) in harga.range("j"..="p") {
println!(" {}: Rp{:.0}", nama, h);
}
// first_key_value() dan last_key_value()
println!("\nPertama: {:?}", harga.first_key_value());
println!("Terakhir: {:?}", harga.last_key_value());
// Kasus nyata: log terurut berdasarkan timestamp
let mut log: BTreeMap<u64, String> = BTreeMap::new();
log.insert(1_700_000_100, String::from("Server dimulai"));
log.insert(1_700_000_050, String::from("Konfigurasi dimuat"));
log.insert(1_700_000_200, String::from("Koneksi diterima"));
println!("\n=== Log (Terurut Waktu) ===");
for (ts, pesan) in &log {
println!(" {} | {}", ts, pesan);
}
}
HashSet — Koleksi Kunci Unik Tanpa Nilai
#
HashSet<T> adalah HashMap<T, ()> yang disederhanakan — menyimpan nilai unik tanpa data tambahan. Berguna untuk cek keberadaan O(1) dan operasi himpunan:
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); // duplikat — diabaikan
set.insert(1); // duplikat — diabaikan
println!("{:?}", set); // {1, 2, 3} — urutan tidak terjamin
// contains() — O(1) cek keberadaan
println!("Ada 2: {}", set.contains(&2)); // true
println!("Ada 5: {}", set.contains(&5)); // false
// Dari Vec — cara mudah menghapus duplikat
let angka_duplikat = vec![1, 2, 3, 2, 1, 4, 3, 5];
let unik: HashSet<i32> = angka_duplikat.into_iter().collect();
println!("Unik: {:?}", unik);
// Operasi himpunan
let a: HashSet<i32> = [1, 2, 3, 4, 5].into_iter().collect();
let b: HashSet<i32> = [3, 4, 5, 6, 7].into_iter().collect();
// Irisan (intersection) — ada di keduanya
let irisan: HashSet<_> = a.intersection(&b).collect();
println!("Irisan: {:?}", irisan); // {3, 4, 5}
// Gabungan (union) — ada di salah satu atau keduanya
let gabungan: HashSet<_> = a.union(&b).collect();
println!("Gabungan: {:?}", gabungan); // {1, 2, 3, 4, 5, 6, 7}
// Selisih (difference) — ada di a tapi tidak di b
let selisih: HashSet<_> = a.difference(&b).collect();
println!("A \\ B: {:?}", selisih); // {1, 2}
// Subset dan 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
}
Perbandingan HashMap, BTreeMap, dan HashSet #
| Aspek | HashMap<K,V> | BTreeMap<K,V> | HashSet<T> |
|---|---|---|---|
| Kompleksitas | O(1) rata-rata | O(log n) | O(1) rata-rata |
| Urutan iterasi | Tidak terjamin | Terurut by key | Tidak terjamin |
| Constraint kunci | Hash + Eq | Ord | Hash + Eq |
| Query rentang | Tidak | Ya (.range()) | Tidak |
| Penggunaan memori | Lebih efisien | Lebih banyak | Lebih efisien |
| Kapan digunakan | Default map | Map urutan penting | Cek keanggotaan unik |
Gunakan HashMap jika:
✓ Kebutuhan map standar — lookup, insert, delete
✓ Urutan iterasi tidak penting
✓ Performa O(1) yang optimal
Gunakan BTreeMap jika:
✓ Perlu iterasi dalam urutan terurut
✓ Perlu query rentang kunci (range queries)
✓ Kunci harus selalu dalam urutan tertentu
Gunakan HashSet jika:
✓ Hanya perlu tahu apakah elemen ada atau tidak
✓ Perlu operasi himpunan (union, intersection, difference)
✓ Hapus duplikat dari koleksi
Ringkasan #
- Entry API adalah cara idiomatis untuk update —
.entry(k).or_insert(v)menangani insert-jika-tidak-ada,.or_insert_with(||)untuk nilai yang mahal dihitung, dan mutable ref yang dikembalikan untuk update berdasarkan nilai lama.HashMapmengambil ownership kunci dan nilai —Stringyang di-insert tidak bisa digunakan lagi setelah itu. Gunakan&strsebagai kunci jika variabel asli masih dibutuhkan.- Urutan iterasi tidak terjamin — jika butuh iterasi terurut, gunakan
BTreeMapatau collect ke Vec dan sort.- Gunakan
get()bukan[]untuk akses aman —map[k]panic jika kunci tidak ada;map.get(k)mengembalikanOption.contains_key()untuk cek keberadaan tanpa mengambil nilai — lebih ekspresif dariget().is_some().retain()untuk filter in-place — lebih efisien dari filter + collect ke HashMap baru.- Kunci kustom butuh
Hash + Eq— gunakan#[derive(Hash, PartialEq, Eq)]untuk struct atau enum sederhana.BTreeMapuntuk urutan terjamin — O(log n) lebih lambat tapi mendukung.range()dan iterasi selalu terurut by key.HashSetuntuk keanggotaan unik — O(1) lookup dan mendukung operasi himpunan (union, intersection, difference, subset).