List #
Ketika developer dari bahasa lain mencari “list” di Rust, jawabannya hampir selalu Vec<T>. Bukan LinkedList — meski namanya terdengar seperti apa yang dicari. Rust adalah bahasa yang sangat peduli dengan locality of reference: data yang tersimpan bersebelahan di memori diakses jauh lebih cepat daripada data yang terhubung via pointer, bahkan untuk operasi penyisipan di tengah. Artikel ini membahas Vec<T> secara mendalam — cara kerjanya di memori, operasi-operasi yang tersedia, dan teknik yang membuat kode Vec idiomatis. Kemudian membahas VecDeque untuk kasus antrian dua arah, dan terakhir LinkedList beserta penjelasan jujur kapan (dan kapan tidak) menggunakannya.
Vec<T> — Pilihan Utama untuk List
#
Vec<T> adalah array dinamis yang dialokasikan di heap. Tiga angka mendefinisikan state-nya di memori: pointer ke data, panjang (jumlah elemen aktual), dan kapasitas (jumlah elemen yang bisa ditampung sebelum realokasi):
flowchart LR
subgraph Stack
V["Vec\nptr → ...\nlen: 3\ncap: 4"]
end
subgraph Heap
D["[10 | 20 | 30 | _ ]"]
end
V -- ptr --> DSaat push dilakukan dan len == cap, Vec mengalokasikan blok baru (biasanya dua kali lipat kapasitas sebelumnya), menyalin semua elemen, dan membebaskan memori lama. Proses ini mahal, tapi jarang terjadi — amortized cost per push tetap O(1).
Membuat Vec #
fn main() {
// Kosong — tipe harus eksplisit karena belum ada elemen
let mut v1: Vec<i32> = Vec::new();
// Macro vec! — cara paling umum
let v2 = vec![10, 20, 30, 40, 50];
// Diisi nilai yang sama
let v3 = vec![0u8; 1024]; // 1024 byte, semua nol
// Dari iterator — paling fleksibel
let v4: Vec<i32> = (1..=10).collect();
let v5: Vec<i32> = (1..=10).filter(|n| n % 2 == 0).collect();
// Dengan kapasitas awal — hindari realokasi jika jumlah elemen diketahui
let mut v6: Vec<String> = Vec::with_capacity(100);
println!("{:?}", v2);
println!("{:?}", v4);
println!("{:?}", v5);
println!("Kapasitas v6: {}", v6.capacity()); // 100
}
Menambah dan Menghapus Elemen #
fn main() {
let mut v = vec![1, 2, 3];
// Tambah di akhir — O(1) amortized
v.push(4);
v.push(5);
println!("{:?}", v); // [1, 2, 3, 4, 5]
// Hapus dari akhir — O(1)
let terakhir = v.pop();
println!("Pop: {:?}", terakhir); // Some(5)
println!("{:?}", v); // [1, 2, 3, 4]
// Sisipkan di posisi tertentu — O(n) karena perlu geser elemen
v.insert(1, 99);
println!("{:?}", v); // [1, 99, 2, 3, 4]
// Hapus di posisi tertentu — O(n) karena perlu geser elemen
let dihapus = v.remove(1);
println!("Remove: {}", dihapus); // 99
println!("{:?}", v); // [1, 2, 3, 4]
// swap_remove — O(1) alternatif remove, tapi tidak jaga urutan
let mut v2 = vec!["a", "b", "c", "d", "e"];
v2.swap_remove(1); // hapus "b", ganti dengan "e"
println!("{:?}", v2); // ["a", "e", "c", "d"]
// Hapus semua elemen — tidak bebaskan kapasitas
v.clear();
println!("Setelah clear: {:?} (len={}, cap={})", v, v.len(), v.capacity());
// Gabungkan dua Vec
let mut a = vec![1, 2, 3];
let b = vec![4, 5, 6];
a.extend(b.iter());
println!("{:?}", a); // [1, 2, 3, 4, 5, 6]
// Atau dengan append — memindahkan semua elemen dari b ke a, b jadi kosong
let mut c = vec![1, 2, 3];
let mut d = vec![4, 5, 6];
c.append(&mut d);
println!("c: {:?}, d: {:?}", c, d); // c: [1..6], d: []
}
remove(i)menggeser semua elemen setelahisatu posisi ke kiri — O(n). Jika urutan tidak penting, gunakanswap_remove(i)yang O(1): ia menukar elemen yang dihapus dengan elemen terakhir lalu memperpendek Vec.
Mengakses Elemen #
fn main() {
let v = vec![10, 20, 30, 40, 50];
// Akses via indeks — panic jika out of bounds
println!("Pertama: {}", v[0]);
println!("Terakhir: {}", v[v.len() - 1]);
// ANTI-PATTERN: akses tanpa validasi di kode produksi
// let x = v[99]; // panic: index out of bounds
// BENAR: gunakan .get() yang mengembalikan Option
match v.get(2) {
Some(&val) => println!("Indeks 2: {}", val),
None => println!("Indeks 2 tidak ada"),
}
println!("{:?}", v.get(10)); // None — tidak panic
// first() dan last()
println!("Pertama: {:?}", v.first()); // Some(10)
println!("Terakhir: {:?}", v.last()); // Some(50)
// contains() — cek keberadaan elemen
println!("Ada 30: {}", v.contains(&30)); // true
println!("Ada 99: {}", v.contains(&99)); // false
// position() — cari indeks elemen pertama
println!("Posisi 30: {:?}", v.iter().position(|&x| x == 30)); // Some(2)
// binary_search() — O(log n) tapi butuh Vec sudah terurut
let terurut = vec![1, 3, 5, 7, 9, 11];
println!("{:?}", terurut.binary_search(&7)); // Ok(3)
println!("{:?}", terurut.binary_search(&6)); // Err(3) — posisi jika diinsert
}
Pengurutan dan Dedup #
fn main() {
let mut v = vec![3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5];
// Urutkan — O(n log n)
v.sort();
println!("Terurut: {:?}", v);
// Hapus duplikat berurutan — hanya bekerja setelah sort
v.dedup();
println!("Dedup: {:?}", v); // [1, 2, 3, 4, 5, 6, 9]
// sort_by — urutan kustom
let mut kata = vec!["pisang", "apel", "mangga", "jeruk"];
kata.sort_by(|a, b| a.len().cmp(&b.len()));
println!("Urut panjang: {:?}", kata);
// sort_by_key — lebih ringkas
kata.sort_by_key(|s| s.len());
println!("Urut panjang: {:?}", kata);
// sort_unstable — lebih cepat dari sort, tidak jaga urutan elemen sama
let mut angka = vec![5, 2, 8, 1, 9, 3];
angka.sort_unstable();
println!("Unstable sort: {:?}", angka);
// Balik urutan
angka.reverse();
println!("Reversed: {:?}", angka);
}
Slice — Pandangan ke Sebagian Vec #
Slice (&[T] atau &mut [T]) adalah pandangan ke bagian dari Vec tanpa menyalin data. Hampir semua fungsi yang menerima data sekuensial sebaiknya menerima slice, bukan &Vec<T>, karena lebih fleksibel:
// ANTI-PATTERN: parameter &Vec<T> terlalu spesifik
fn jumlahkan_vec(v: &Vec<i32>) -> i32 {
v.iter().sum()
}
// BENAR: &[T] lebih generik — menerima Vec, array, dan slice
fn jumlahkan(data: &[i32]) -> i32 {
data.iter().sum()
}
fn main() {
let v = vec![1, 2, 3, 4, 5];
let arr = [10, 20, 30];
println!("{}", jumlahkan(&v)); // dari Vec
println!("{}", jumlahkan(&arr)); // dari array
println!("{}", jumlahkan(&v[1..4])); // slice [2, 3, 4]
// windows() — iterasi jendela bergeser ukuran n
let angka = vec![1, 2, 3, 4, 5];
for window in angka.windows(3) {
println!("{:?}", window); // [1,2,3], [2,3,4], [3,4,5]
}
// chunks() — pecah menjadi potongan ukuran n
for chunk in angka.chunks(2) {
println!("{:?}", chunk); // [1,2], [3,4], [5]
}
// split_at() — bagi menjadi dua slice
let (kiri, kanan) = angka.split_at(2);
println!("Kiri: {:?}, Kanan: {:?}", kiri, kanan); // [1,2], [3,4,5]
}
Retain — Filter In-Place #
fn main() {
let mut v = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Pertahankan hanya elemen yang memenuhi kondisi
v.retain(|&x| x % 2 == 0);
println!("{:?}", v); // [2, 4, 6, 8, 10]
let mut nama = vec![
String::from("Budi"),
String::from(""),
String::from("Sari"),
String::from(""),
String::from("Joko"),
];
// Hapus string kosong
nama.retain(|s| !s.is_empty());
println!("{:?}", nama); // ["Budi", "Sari", "Joko"]
}
Kapasitas dan Manajemen Memori #
fn main() {
let mut v: Vec<i32> = Vec::new();
// Pantau kapasitas saat elemen ditambahkan
for i in 0..20 {
v.push(i);
// kapasitas bertambah dua kali lipat saat habis: 0 → 1 → 2 → 4 → 8 → 16 → 32
}
println!("Len: {}, Cap: {}", v.len(), v.capacity());
// with_capacity — alokasi di awal jika jumlah elemen diketahui
let mut v2: Vec<i32> = Vec::with_capacity(100);
for i in 0..100 {
v2.push(i); // tidak ada realokasi
}
println!("Len: {}, Cap: {}", v2.len(), v2.capacity()); // 100, 100
// shrink_to_fit — bebaskan kapasitas berlebih
v2.truncate(10); // potong ke 10 elemen
println!("Setelah truncate — Len: {}, Cap: {}", v2.len(), v2.capacity());
v2.shrink_to_fit();
println!("Setelah shrink — Len: {}, Cap: {}", v2.len(), v2.capacity());
}
VecDeque<T> — Antrian Dua Arah
#
VecDeque<T> (double-ended queue) adalah koleksi yang dioptimalkan untuk penambahan dan penghapusan di kedua ujung secara efisien — keduanya O(1). Ini pilihan yang tepat untuk mengimplementasikan antrian (queue) atau deque:
use std::collections::VecDeque;
fn main() {
let mut deque: VecDeque<i32> = VecDeque::new();
// Tambah di belakang — push_back = push pada Vec
deque.push_back(1);
deque.push_back(2);
deque.push_back(3);
// Tambah di depan — O(1), lebih efisien dari Vec::insert(0, ...)
deque.push_front(0);
deque.push_front(-1);
println!("{:?}", deque); // [-1, 0, 1, 2, 3]
// Hapus dari depan — O(1), ini yang membuat VecDeque cocok untuk queue
let depan = deque.pop_front();
println!("Pop front: {:?}", depan); // Some(-1)
// Hapus dari belakang — O(1)
let belakang = deque.pop_back();
println!("Pop back: {:?}", belakang); // Some(3)
println!("{:?}", deque); // [0, 1, 2]
// Simulasi queue FIFO
let mut antrian: VecDeque<String> = VecDeque::new();
antrian.push_back(String::from("tugas-1"));
antrian.push_back(String::from("tugas-2"));
antrian.push_back(String::from("tugas-3"));
while let Some(tugas) = antrian.pop_front() {
println!("Proses: {}", tugas);
}
// Konversi dari Vec
let v = vec![1, 2, 3, 4, 5];
let mut deque2: VecDeque<i32> = v.into();
deque2.push_front(0);
println!("{:?}", deque2); // [0, 1, 2, 3, 4, 5]
}
LinkedList<T> — Dan Mengapa Jarang Digunakan
#
LinkedList<T> adalah doubly-linked list di standard library. Secara teoritis, ia punya O(1) untuk insert/delete di posisi yang sudah diketahui. Tapi dalam praktik di Rust — dan bahasa modern umumnya — LinkedList hampir selalu lebih lambat dari Vec, bahkan untuk kasus yang secara teoritis menguntungkan linked list:
use std::collections::LinkedList;
fn main() {
let mut list: LinkedList<i32> = LinkedList::new();
// Tambah di belakang
list.push_back(1);
list.push_back(2);
list.push_back(3);
// Tambah di depan
list.push_front(0);
println!("{:?}", list); // [0, 1, 2, 3]
// Hapus dari depan dan belakang
list.pop_front();
list.pop_back();
println!("{:?}", list); // [1, 2]
// Iterasi
for val in &list {
print!("{} ", val);
}
println!();
// Gabungkan dua LinkedList
let mut a: LinkedList<i32> = (1..=3).collect();
let mut b: LinkedList<i32> = (4..=6).collect();
a.append(&mut b);
println!("{:?}", a); // [1, 2, 3, 4, 5, 6]
println!("b setelah append: {:?}", b); // [] — b dikosongkan
}
Mengapa Vec Biasanya Lebih Cepat dari LinkedList
#
Alasannya adalah cache locality. Semua elemen Vec berada dalam blok memori yang bersebelahan, sehingga cache CPU bisa memuat banyak elemen sekaligus. LinkedList mengalokasikan setiap node secara terpisah di heap — mengakses elemen berikutnya berarti mem-fetch dari alamat memori yang tidak bersebelahan, menyebabkan banyak cache miss.
Vec: [1][2][3][4][5] ← satu blok memori bersebelahan
LinkedList: [1]→heap₁ [2]→heap₂ [3]→heap₃ ← pointer tersebar di memori
Benchmark nyata menunjukkan Vec lebih cepat untuk: iterasi, push, dan bahkan insert di tengah (untuk n yang wajar), karena overhead alokasi per-node dan cache miss dari LinkedList jauh melebihi O(n) geser elemen Vec.
Perbandingan Pilihan Koleksi List #
| Koleksi | Akses acak | Push/pop belakang | Push/pop depan | Insert tengah | Kapan digunakan |
|---|---|---|---|---|---|
Vec<T> | O(1) | O(1) amortized | O(n) | O(n) | Default — hampir semua kasus |
VecDeque<T> | O(1) | O(1) | O(1) | O(n) | Queue, buffer sliding window |
LinkedList<T> | O(n) | O(1) | O(1) | O(1)* | Hampir tidak pernah |
*O(1) insert di tengah LinkedList hanya jika sudah punya iterator ke posisinya — mendapat iterator-nya sendiri butuh O(n).
Gunakan Vec jika:
✓ Default untuk semua kebutuhan list
✓ Perlu akses acak dengan indeks
✓ Ukuran koleksi tidak diketahui saat kompilasi
✓ Perlu sort, dedup, atau operasi slice
Gunakan VecDeque jika:
✓ Perlu push/pop efisien di kedua ujung
✓ Implementasi queue (FIFO) atau deque
✓ Sliding window buffer dengan ukuran tetap
Gunakan LinkedList jika:
✗ Hampir tidak ada kasus nyata yang membutuhkannya di Rust
✓ Satu-satunya kasus: perlu split/merge list dalam O(1) menggunakan cursor API
Ringkasan #
Vec<T>adalah pilihan default untuk semua kebutuhan list — lebih cepat dariLinkedListdalam hampir semua kasus nyata karena cache locality.- Alokasi kapasitas awal dengan
Vec::with_capacity(n)jika jumlah elemen diperkirakan — menghindari realokasi berulang yang mahal.- Gunakan
.get(i)bukanv[i]untuk akses yang aman —.get()mengembalikanOption,v[i]panic jika out of bounds.swap_removelebih cepat dariremovejika urutan tidak penting —removeO(n) karena menggeser elemen;swap_removeO(1) karena menukar dengan elemen terakhir.- Parameter fungsi sebaiknya
&[T]bukan&Vec<T>— slice lebih generik, menerima Vec, array, dan slice sekaligus tanpa konversi.windows(n)untuk analisis urutan tetangga,chunks(n)untuk memproses dalam batch — keduanya menghasilkan slice tanpa alokasi.retainuntuk filter in-place — lebih efisien dari filter + collect karena tidak mengalokasikan Vec baru.VecDeque<T>untuk queue FIFO —push_back+pop_frontkeduanya O(1), berbeda dariVecyanginsert(0, x)adalah O(n).LinkedListhampir tidak pernah tepat di Rust — cache miss dari alokasi per-node biasanya jauh lebih mahal dari O(n) geser elemen Vec.