Sebagian besar pertanyaan tentang data bukanlah “berapa nilainya”, melainkan “apakah nilai ini sudah pernah muncul?”. Panitia seminar harus tahu nama mana yang belum terdaftar, gerbang stadion harus tahu tiket mana yang sudah dipakai masuk, dan platform berita harus tahu tag mana yang sudah menempel pada artikel. Semua pertanyaan itu berbicara tentang keunikan, dan koleksi yang lahir untuk menjawabnya adalah HashSet<T> — himpunan nilai yang menolak duplikat dengan otomatis. Chapter ini membahas cara memasukkan dan memeriksa anggota, memadatkan daftar penuh duplikasi, serta operasi himpunan klasik seperti gabungan (union) dan irisan (intersection).

HashSet Menolak Duplikat #

Method insert pada HashSet memiliki satu perilaku khas yang tidak dimiliki HashMap: nilai kembaliannya adalah bool yang melaporkan apakah nilai yang dikirim benar-benar baru. Program berikut mengelola daftar hadir rapat dan memanfaatkan laporan tersebut:

rust
use std::collections::HashSet;

fn main() {
    let mut attendees = HashSet::new();

    let first_add = attendees.insert("Budi");
    let second_add = attendees.insert("Budi");
    attendees.insert("Siti");
    attendees.insert("Andi");

    println!("Penambahan 'Budi' pertama: {}", first_add);
    println!("Penambahan 'Budi' kedua : {}", second_add);
    println!("Jumlah peserta unik: {}", attendees.len());

    println!("Apakah 'Siti' hadir? {}", attendees.contains("Siti"));
    println!("Apakah 'Rina' hadir? {}", attendees.contains("Rina"));

    let removed = attendees.remove("Andi");
    println!("Menghapus 'Andi' berhasil: {}", removed);
    println!("Jumlah peserta akhir: {}", attendees.len());
}

Output yang Dihasilkan:

text
Penambahan 'Budi' pertama: true
Penambahan 'Budi' kedua : false
Jumlah peserta unik: 3
Apakah 'Siti' hadir? true
Apakah 'Rina' hadir? false
Menghapus 'Andi' berhasil: true
Jumlah peserta akhir: 2

Mari kita bedah kode di atas baris per baris:

  1. Baris 4 (let mut attendees = HashSet::new();): himpunan kosong bertipe HashSet<&str> — tipe elemennya disimpulkan dari isi pertama yang masuk.
  2. Baris 6-7 (let first_add = ...; let second_add = ...;): insert pertama menemukan keadaan kosong, memasukkan "Budi", dan mengembalikan true karena keanggotaan baru tercipta. insert kedua dengan nilai yang sama mengembalikan false — set menolak duplikat dengan diam-diam dan tidak mengubah isinya sama sekali.
  3. Baris 13 (attendees.len()): meskipun "Budi" dikirim dua kali, jumlah anggota hanya 3 — satu Budi, satu Siti, satu Andi.
  4. Baris 15-16 (attendees.contains(...)): method contains memeriksa keanggotaan dan mengembalikan bool. "Rina" tidak pernah dimasukkan, jadi jawabannya false.
  5. Baris 18-19 (attendees.remove("Andi")): remove menghapus anggota dan melaporkan true; menghapus anggota yang tidak ada akan melaporkan false — pola Option dari HashMap di sini diringkas menjadi bool karena nilai yang dihapus tidak dikembalikan.

Merapatkan Daftar yang Berulang #

Sumber data mentah sering datang penuh duplikasi: pemindai tiket mengirim deretan kode, dan tugas kita adalah menghitung tiket valid yang berbeda. Perjalanan dari Vec ke HashSet cukup satu loop insert:

rust
use std::collections::HashSet;

fn main() {
    let scanned_tickets = vec!["T-001", "T-002", "T-001", "T-003"];
    let mut valid_tickets = HashSet::new();

    for ticket in scanned_tickets {
        valid_tickets.insert(ticket);
    }

    println!("Tiket yang discan: 4");
    println!("Tiket unik: {}", valid_tickets.len());
}

Output yang Dihasilkan:

text
Tiket yang discan: 4
Tiket unik: 3

Empat pemindaian menghasilkan tiga tiket unik — T-001 terpindai dua kali dan set hanya menghitungnya sekali. Mari kita bedah kode di atas baris per baris:

  1. Baris 4 (let scanned_tickets = vec![...]): empat hasil pemindaian tersimpan dalam Vec<&str> dengan satu duplikasi — kode "T-001" muncul dua kali.
  2. Baris 5 (let mut valid_tickets = HashSet::new();): himpunan penampung dimulai kosong.
  3. Baris 7-9 (for ticket in scanned_tickets): loop memindahkan setiap kode tiket dari vector ke set. Insert pertama "T-001" mengembalikan true, insert keduanya mengembalikan false — nilai kembalian dibiarkan karena penolakan duplikat memang perilaku yang kita inginkan.
  4. Baris 12 (valid_tickets.len()): himpunan melaporkan 3 anggota unik meskipun sumbernya memuat 4 entri. Pola ini bekerja untuk sumber duplikasi apa pun — daftar belanja bertuliskan barang yang sama, daftar undangan dengan nama kembar, atau hasil penggabungan banyak file.

Mekanisme: Sebuah HashMap Tanpa Value #

HashSet di dalamnya benar-benar dibangun di atas mesin HashMap: setiap nilai di-hash, hash menentukan bucket penyimpanan, dan keanggotaan diperiksa lewat perbandingan kesetaraan. Bedanya, set hanya menyimpan kunci tanpa value yang menempel — seperti papan skor yang hanya mencatat nama tim tanpa angka. Konsekuensinya menular: pencarian contains selesai dalam waktu rata-rata konstan, nilai yang bisa di-hash dan dibandingkan kesetaraannya bisa menjadi anggota, dan urutan iterasi tidak dijamin karena biji acak hash berbeda di setiap eksekusi — sifat yang sama dengan HashMap.

Urutan acak tersebut tidak mengganggu contoh-contoh di atas karena insert, contains, dan len semuanya bekerja pada satu nilai. Begitu kita mencetak seluruh anggota atau menggabungkan dua set, kita kembali memakai resep “kumpulkan lalu sort” dari chapter HashMap: Iterasi dan Update.

Operasi Himpunan: Intersection, Union, dan Difference #

Kekuatan sesungguhnya HashSet muncul ketika dua himpunan bertemu. Ekstrakurikuler futsal punya anggota sendiri, ekstrakurikuler basket punya anggota sendiri, dan keputusan penjadwalan butuh tahu siapa yang terjebak di dua kegiatan sekaligus:

rust
use std::collections::HashSet;

fn main() {
    let mut futsal_club = HashSet::new();
    futsal_club.insert("Andi");
    futsal_club.insert("Budi");
    futsal_club.insert("Citra");

    let mut basket_club = HashSet::new();
    basket_club.insert("Budi");
    basket_club.insert("Citra");
    basket_club.insert("Dewi");

    let mut both_clubs = Vec::new();
    for student in futsal_club.intersection(&basket_club) {
        both_clubs.push(student);
    }
    both_clubs.sort();
    println!("Mengikuti dua klub: {:?}", both_clubs);

    let mut all_students = Vec::new();
    for student in futsal_club.union(&basket_club) {
        all_students.push(student);
    }
    all_students.sort();
    println!("Semua siswa unik: {:?}", all_students);

    let mut futsal_only = Vec::new();
    for student in futsal_club.difference(&basket_club) {
        futsal_only.push(student);
    }
    futsal_only.sort();
    println!("Hanya futsal: {:?}", futsal_only);

    let mut core_members = HashSet::new();
    core_members.insert("Budi");
    println!("Inti futsal bagian dari klub: {}", core_members.is_subset(&futsal_club));
}

Output yang Dihasilkan:

text
Mengikuti dua klub: ["Budi", "Citra"]
Semua siswa unik: ["Andi", "Budi", "Citra", "Dewi"]
Hanya futsal: ["Andi"]
Inti futsal bagian dari klub: true

Output stabil karena setiap operasi hasilnya ditampung ke Vec lalu di-sort sebelum dicetak. Mari kita bedah logikanya:

  1. Baris 14-19 (intersection(&basket_club)): intersection menghasilkan penelusuran anggota-anggota yang hadir di kedua himpunan — Budi dan Citra satu-satunya nama yang muncul di dua daftar klub. Loop for menelusuri hasil intersection dan memindahkan setiap anggota ke both_clubs, lalu sort memberi keterurutan abjad.
  2. Baris 21-26 (union(&basket_club)): union menggabungkan keanggotaan kedua himpunan tanpa duplikasi — empat nama unik meski Budi dan Citra tercatat dua kali di sumbernya.
  3. Baris 28-33 (difference(&basket_club)): difference menghasilkan anggota futsal yang tidak ada di basket — hanya Andi. Arahnya penting: basket_club.difference(&futsal_club) akan menghasilkan Dewi.
  4. Baris 35-37 (is_subset(&futsal_club)): method relasi is_subset menjawab pertanyaan “apakah setiap anggota himpunan pertama juga anggota himpunan kedua” — core_members beranggotakan Budi yang memang anggota futsal, jadi jawabannya true.

Keluarga method relasi lainnya mengikuti logika yang sama: is_superset membalik arah pertanyaan, dan is_disjoint memeriksa dua himpunan yang tidak memiliki anggota bersama sama sekali.

Ringkasan #

HashSet menyimpan himpunan nilai unik: insert menolak duplikat dan melaporkan kebaruan lewat bool, contains memeriksa keanggotaan, dan remove menghapus dengan laporan berhasil atau tidak. Mekanisme internalnya sama dengan HashMap — hash menentukan bucket, waktu pencarian rata-rata konstan, dan urutan iterasi acak — sehingga output terurut memerlukan pola kumpulkan-lalu-sort. Operasi himpunan intersection, union, dan difference menjawab pertanyaan silang antar dua kumpulan data, ditemani method relasi seperti is_subset. Berikutnya kita bertemu koleksi yang mengambil jalan sepenuhnya berbeda dari hash: BTreeMap dan keterurutan berbasis pohon.