Introduction to HashMap in Rust
HashMap is a collection type that stores key-value pairs, allowing fast insertion, lookup, and deletion based on keys. It's one of the most commonly used data structures in Rust, providing average constant-time complexity for basic operations. HashMaps are part of the standard library's collections module and are implemented using a hash table.
Key Concepts
- Key-Value Storage: Each value is associated with a unique key
- Hashing: Keys are hashed to determine storage location
- Average O(1) Operations: Fast lookup, insertion, and deletion
- No Guaranteed Order: Iteration order is arbitrary
- Ownership: HashMap owns its keys and values
- Type Parameters:
HashMap<K, V>where K is key type, V is value type
1. Creating and Initializing HashMaps
Basic Creation
use std::collections::HashMap; fn main() { // Creating an empty HashMap let mut scores: HashMap<String, i32> = HashMap::new(); // Creating with capacity let mut with_capacity: HashMap<String, i32> = HashMap::with_capacity(10); println!("Capacity: {}", with_capacity.capacity()); // From arrays/tuples let teams = vec![ String::from("Blue"), String::from("Yellow"), ]; let initial_scores = vec![10, 50]; let scores: HashMap<_, _> = teams.iter() .zip(initial_scores.iter()) .map(|(k, v)| (k.clone(), *v)) .collect(); println!("Scores from zip: {:?}", scores); // Using from_iter with tuples let data = vec![ (String::from("Red"), 30), (String::from("Green"), 40), (String::from("Blue"), 50), ]; let map: HashMap<_, _> = data.into_iter().collect(); println!("From vec of tuples: {:?}", map); // Using collect with iterator let map: HashMap<_, _> = (1..=5) .map(|i| (format!("key_{}", i), i * 10)) .collect(); println!("From range: {:?}", map); } From Iterator Patterns
use std::collections::HashMap; fn main() { // From a vector of tuples let pairs = vec![("a", 1), ("b", 2), ("c", 3)]; let map: HashMap<&str, i32> = pairs.into_iter().collect(); println!("From pairs: {:?}", map); // Transforming while collecting let words = vec!["apple", "banana", "cherry", "date"]; let word_lengths: HashMap<&str, usize> = words .into_iter() .map(|word| (word, word.len())) .collect(); println!("Word lengths: {:?}", word_lengths); // Filtering while collecting let numbers = vec![1, 2, 3, 4, 5, 6]; let even_squares: HashMap<i32, i32> = numbers .into_iter() .filter(|&n| n % 2 == 0) .map(|n| (n, n * n)) .collect(); println!("Even squares: {:?}", even_squares); // From a string (counting characters) let text = "hello world hello"; let char_counts: HashMap<char, i32> = text .chars() .filter(|c| !c.is_whitespace()) .fold(HashMap::new(), |mut acc, c| { *acc.entry(c).or_insert(0) += 1; acc }); println!("Character counts: {:?}", char_counts); } 2. Basic Operations
Inserting and Updating
use std::collections::HashMap; fn main() { let mut scores = HashMap::new(); // Inserting values scores.insert(String::from("Blue"), 10); scores.insert(String::from("Yellow"), 50); scores.insert(String::from("Red"), 30); println!("After inserts: {:?}", scores); // Insert returns Option of old value let old_value = scores.insert(String::from("Blue"), 25); println!("Old value for Blue: {:?}", old_value); // Some(10) // Insert if key doesn't exist scores.entry(String::from("Green")).or_insert(40); scores.entry(String::from("Blue")).or_insert(100); // Won't change println!("After or_insert: {:?}", scores); // Update based on existing value let text = "hello world wonderful world"; let mut word_counts = HashMap::new(); for word in text.split_whitespace() { let count = word_counts.entry(word).or_insert(0); *count += 1; } println!("Word counts: {:?}", word_counts); // Complex update let mut map: HashMap<&str, Vec<i32>> = HashMap::new(); map.entry("numbers").or_insert_with(Vec::new).push(1); map.entry("numbers").or_insert_with(Vec::new).push(2); map.entry("numbers").or_insert_with(Vec::new).push(3); println!("Numbers: {:?}", map); } Accessing Values
use std::collections::HashMap; fn main() { let mut scores = HashMap::new(); scores.insert(String::from("Blue"), 10); scores.insert(String::from("Yellow"), 50); // Using get (returns Option<&V>) let team_name = String::from("Blue"); if let Some(score) = scores.get(&team_name) { println!("Blue score: {}", score); } // Using get with default let score = scores.get(&String::from("Red")).unwrap_or(&0); println!("Red score (default): {}", score); // Direct indexing (panics if key not found) // let score = scores[&String::from("Red")]; // Panics! // Safe access with match match scores.get(&String::from("Yellow")) { Some(score) => println!("Yellow: {}", score), None => println!("Yellow not found"), } // Check if contains key if scores.contains_key(&String::from("Blue")) { println!("Blue exists"); } // Getting multiple values let keys_to_check = vec!["Blue", "Red", "Yellow"]; for key in keys_to_check { match scores.get(&String::from(key)) { Some(score) => println!("{}: {}", key, score), None => println!("{} not found", key), } } } Removing Elements
use std::collections::HashMap; fn main() { let mut scores = HashMap::new(); scores.insert(String::from("Blue"), 10); scores.insert(String::from("Yellow"), 50); scores.insert(String::from("Red"), 30); scores.insert(String::from("Green"), 40); println!("Before removal: {:?}", scores); // Remove by key (returns Option<V>) let removed = scores.remove(&String::from("Blue")); println!("Removed: {:?}", removed); // Remove entry if it matches a value if let Some((key, value)) = scores.remove_entry(&String::from("Yellow")) { println!("Removed entry: {} => {}", key, value); } // Remove with condition (drain_filter is unstable) let mut to_remove = Vec::new(); for (key, &value) in &scores { if value < 35 { to_remove.push(key.clone()); } } for key in to_remove { scores.remove(&key); } println!("After conditional removal: {:?}", scores); // Clear all entries scores.clear(); println!("After clear: {:?}", scores); println!("Is empty: {}", scores.is_empty()); } 3. Iteration
Basic Iteration
use std::collections::HashMap; fn main() { let mut scores = HashMap::new(); scores.insert("Blue".to_string(), 10); scores.insert("Yellow".to_string(), 50); scores.insert("Red".to_string(), 30); scores.insert("Green".to_string(), 40); // Iterate over key-value pairs println!("All entries:"); for (key, value) in &scores { println!(" {}: {}", key, value); } // Iterate over keys println!("Keys:"); for key in scores.keys() { println!(" {}", key); } // Iterate over values println!("Values:"); for value in scores.values() { println!(" {}", value); } // Mutable iteration over values println!("Doubling all scores:"); for value in scores.values_mut() { *value *= 2; } for (key, value) in &scores { println!(" {}: {}", key, value); } // Consuming iteration (moves ownership) let scores_clone = scores.clone(); for (key, value) in scores_clone { println!("Consumed: {} => {}", key, value); } // scores_clone is no longer valid // Using iterators with adapters let high_scores: Vec<_> = scores .iter() .filter(|(_, &score)| score > 50) .map(|(key, _)| key) .collect(); println!("High score keys: {:?}", high_scores); } Advanced Iteration Patterns
use std::collections::HashMap; fn main() { let mut scores = HashMap::new(); scores.insert("Blue".to_string(), 10); scores.insert("Yellow".to_string(), 50); scores.insert("Red".to_string(), 30); scores.insert("Green".to_string(), 40); // Finding min/max by value let min_score = scores.iter().min_by_key(|(_, &v)| v); println!("Min score: {:?}", min_score); let max_score = scores.iter().max_by_key(|(_, &v)| v); println!("Max score: {:?}", max_score); // Sum of values let total: i32 = scores.values().sum(); println!("Total scores: {}", total); // Transform values while iterating let doubled: HashMap<_, _> = scores .iter() .map(|(k, v)| (k.clone(), v * 2)) .collect(); println!("Doubled: {:?}", doubled); // Conditional collection let filtered: HashMap<_, _> = scores .into_iter() .filter(|(_, v)| *v > 30) .collect(); println!("Filtered: {:?}", filtered); // Fold over entries let mut map = HashMap::new(); map.insert("a", 1); map.insert("b", 2); map.insert("c", 3); let sum: i32 = map.iter().fold(0, |acc, (_, &v)| acc + v); println!("Sum using fold: {}", sum); // Partition by predicate let mut map = HashMap::new(); map.insert(1, "odd"); map.insert(2, "even"); map.insert(3, "odd"); map.insert(4, "even"); let (odds, evens): (Vec<_>, Vec<_>) = map .into_iter() .partition(|(k, _)| k % 2 == 1); println!("Odd keys: {:?}", odds); println!("Even keys: {:?}", evens); } 4. Entry API
Using Entry for Efficient Operations
use std::collections::hash_map::Entry; use std::collections::HashMap; fn main() { let mut map = HashMap::new(); // Entry pattern - insert if absent match map.entry("key1") { Entry::Vacant(entry) => { entry.insert(42); println!("Inserted new value"); } Entry::Occupied(mut entry) => { *entry.get_mut() += 1; println!("Updated existing value"); } } // More concise with or_insert map.entry("key2").or_insert(100); map.entry("key2").or_insert(200); // Won't change println!("Map: {:?}", map); // or_insert_with for lazy initialization let expensive_computation = || { println!("Performing expensive computation..."); 42 * 100 }; map.entry("key3").or_insert_with(expensive_computation); map.entry("key3").or_insert_with(expensive_computation); // Won't call // or_insert_with_key (uses the key) map.entry("key4").or_insert_with_key(|k| k.len()); println!("After lazy init: {:?}", map); // Modify in place let text = "hello world hello universe hello"; let mut word_counts = HashMap::new(); for word in text.split_whitespace() { *word_counts.entry(word).or_insert(0) += 1; } println!("Word counts: {:?}", word_counts); // Complex modifications let mut map: HashMap<&str, Vec<i32>> = HashMap::new(); map.entry("numbers") .or_insert_with(Vec::new) .push(1); map.entry("numbers") .and_modify(|v| v.push(2)) .or_insert_with(|| vec![2]); map.entry("letters") .and_modify(|v| v.push(3)) .or_insert_with(|| vec![1, 2, 3]); println!("Modified map: {:?}", map); // Entry with chaining let mut map = HashMap::new(); map.entry("count") .and_modify(|c| *c += 1) .or_insert(1); map.entry("count") .and_modify(|c| *c += 1) .or_insert(1); println!("Count: {}", map["count"]); // 2 } Advanced Entry Patterns
use std::collections::HashMap; fn main() { // Grouping items by key let items = vec![ ("fruit", "apple"), ("fruit", "banana"), ("fruit", "orange"), ("vegetable", "carrot"), ("vegetable", "broccoli"), ]; let mut grouped: HashMap<&str, Vec<&str>> = HashMap::new(); for (category, item) in items { grouped.entry(category) .or_insert_with(Vec::new) .push(item); } println!("Grouped items: {:?}", grouped); // Counting occurrences with enum enum Event { Click, KeyPress(char), Resize { width: u32, height: u32 }, } let events = vec![ Event::Click, Event::KeyPress('a'), Event::Resize { width: 100, height: 100 }, Event::Click, Event::KeyPress('b'), Event::Click, ]; let mut event_counts: HashMap<&str, u32> = HashMap::new(); for event in &events { let key = match event { Event::Click => "click", Event::KeyPress(_) => "keypress", Event::Resize { .. } => "resize", }; *event_counts.entry(key).or_insert(0) += 1; } println!("Event counts: {:?}", event_counts); // Cache pattern struct ExpensiveComputation; impl ExpensiveComputation { fn compute(&self, input: &str) -> usize { println!("Computing for: {}", input); input.len() } } let mut cache: HashMap<String, usize> = HashMap::new(); let computer = ExpensiveComputation; for input in &["hello", "world", "hello", "rust", "world"] { let result = cache.entry((*input).to_string()) .or_insert_with(|| computer.compute(input)); println!("Result for {}: {}", input, result); } } 5. Performance and Capacity
Capacity Management
use std::collections::HashMap; fn main() { // Creating with specific capacity let mut map: HashMap<i32, i32> = HashMap::with_capacity(1000); println!("Initial capacity: {}", map.capacity()); // Adding elements for i in 0..100 { map.insert(i, i * 10); } println!("After 100 inserts, capacity: {}", map.capacity()); // Shrink to fit map.shrink_to_fit(); println!("After shrink_to_fit: {}", map.capacity()); // Reserve additional capacity map.reserve(200); println!("After reserving 200: {}", map.capacity()); // Try reserve (may fail) match map.try_reserve(1000) { Ok(()) => println!("Successfully reserved more space"), Err(e) => println!("Failed to reserve: {}", e), } // Check current metrics println!("Length: {}", map.len()); println!("Capacity: {}", map.capacity()); println!("Load factor: {:.2}", map.len() as f64 / map.capacity() as f64); // Clear and reuse capacity map.clear(); println!("After clear, capacity: {}", map.capacity()); // Capacity retained map.shrink_to_fit(); println!("After shrink_to_fit: {}", map.capacity()); } Performance Benchmarking
use std::collections::HashMap; use std::time::Instant; fn main() { // Compare insertion performance with different capacities let sizes = [1000, 10000, 100000]; for &size in &sizes { // Without pre-allocation let start = Instant::now(); let mut map = HashMap::new(); for i in 0..size { map.insert(i, i * 2); } let time_without = start.elapsed(); // With pre-allocation let start = Instant::now(); let mut map = HashMap::with_capacity(size); for i in 0..size { map.insert(i, i * 2); } let time_with = start.elapsed(); println!("Size {}: Without={:?}, With={:?} (Ratio: {:.2})", size, time_without, time_with, time_without.as_secs_f64() / time_with.as_secs_f64()); } // Lookup performance let size = 100000; let mut map = HashMap::with_capacity(size); for i in 0..size { map.insert(i, i * 2); } let start = Instant::now(); for i in 0..size { let _ = map.get(&i); } let lookup_time = start.elapsed(); println!("Lookup time for {} items: {:?}", size, lookup_time); // Compare hash function performance use std::collections::hash_map::RandomState; use std::hash::{BuildHasher, Hasher}; let mut map = HashMap::with_capacity_and_hasher(size, RandomState::new()); let start = Instant::now(); for i in 0..size { map.insert(i, i * 2); } let time_custom = start.elapsed(); println!("Custom hasher time: {:?}", time_custom); } 6. Custom Key Types
Using Custom Types as Keys
use std::collections::HashMap; use std::hash::{Hash, Hasher}; #[derive(Debug, Eq, PartialEq, Hash)] struct Person { name: String, age: u32, } impl Person { fn new(name: &str, age: u32) -> Self { Person { name: name.to_string(), age, } } } #[derive(Debug)] struct Record { value: i32, timestamp: u64, } // Custom implementation of Hash for more control struct CaseInsensitiveString(String); impl PartialEq for CaseInsensitiveString { fn eq(&self, other: &Self) -> bool { self.0.to_lowercase() == other.0.to_lowercase() } } impl Eq for CaseInsensitiveString {} impl Hash for CaseInsensitiveString { fn hash<H: Hasher>(&self, state: &mut H) { self.0.to_lowercase().hash(state); } } fn main() { // Using custom struct as key let mut person_records: HashMap<Person, Record> = HashMap::new(); person_records.insert( Person::new("Alice", 30), Record { value: 100, timestamp: 1234567890 }, ); person_records.insert( Person::new("Bob", 25), Record { value: 200, timestamp: 1234567891 }, ); // Lookup by Person let alice = Person::new("Alice", 30); if let Some(record) = person_records.get(&alice) { println!("Alice's record: {:?}", record); } // Case-insensitive keys let mut map: HashMap<CaseInsensitiveString, i32> = HashMap::new(); map.insert(CaseInsensitiveString("Hello".to_string()), 1); map.insert(CaseInsensitiveString("WORLD".to_string()), 2); println!("Get 'hello': {:?}", map.get(&CaseInsensitiveString("hello".to_string()))); println!("Get 'world': {:?}", map.get(&CaseInsensitiveString("world".to_string()))); // Tuple as key let mut coordinate_map: HashMap<(i32, i32), String> = HashMap::new(); coordinate_map.insert((0, 0), "origin".to_string()); coordinate_map.insert((1, 0), "east".to_string()); coordinate_map.insert((0, 1), "north".to_string()); println!("At (1,0): {:?}", coordinate_map.get(&(1, 0))); // Array as key let mut array_map: HashMap<[i32; 2], String> = HashMap::new(); array_map.insert([0, 0], "origin".to_string()); array_map.insert([1, 0], "right".to_string()); println!("Array key: {:?}", array_map.get(&[0, 0])); } 7. Nested HashMaps
Multi-level Maps
use std::collections::HashMap; fn main() { // HashMap of HashMaps let mut users: HashMap<String, HashMap<String, i32>> = HashMap::new(); // Adding user Alice with her scores let mut alice_scores = HashMap::new(); alice_scores.insert("math".to_string(), 95); alice_scores.insert("physics".to_string(), 87); users.insert("alice".to_string(), alice_scores); // Adding user Bob with his scores let mut bob_scores = HashMap::new(); bob_scores.insert("math".to_string(), 78); bob_scores.insert("chemistry".to_string(), 92); users.insert("bob".to_string(), bob_scores); // Access nested values if let Some(scores) = users.get("alice") { if let Some(math_score) = scores.get("math") { println!("Alice's math score: {}", math_score); } } // Update nested value using entry users.entry("charlie".to_string()) .or_insert_with(HashMap::new) .insert("physics".to_string(), 88); // Iterate over nested structure for (user, scores) in &users { println!("User: {}", user); for (subject, score) in scores { println!(" {}: {}", subject, score); } } // Count occurrences with nested structure let data = vec![ ("fruit", "apple"), ("fruit", "banana"), ("fruit", "apple"), ("vegetable", "carrot"), ("vegetable", "broccoli"), ("fruit", "banana"), ]; let mut counts: HashMap<&str, HashMap<&str, i32>> = HashMap::new(); for (category, item) in data { *counts .entry(category) .or_insert_with(HashMap::new) .entry(item) .or_insert(0) += 1; } println!("Counts: {:?}", counts); } 8. Serialization and Deserialization
Using serde with HashMap
use serde::{Serialize, Deserialize}; use serde_json; use std::collections::HashMap; #[derive(Debug, Serialize, Deserialize)] struct Config { settings: HashMap<String, String>, enabled: bool, version: u32, } fn main() -> Result<(), Box<dyn std::error::Error>> { // Create a HashMap let mut settings = HashMap::new(); settings.insert("host".to_string(), "localhost".to_string()); settings.insert("port".to_string(), "8080".to_string()); settings.insert("timeout".to_string(), "30".to_string()); let config = Config { settings, enabled: true, version: 1, }; // Serialize to JSON let json = serde_json::to_string_pretty(&config)?; println!("Serialized config:\n{}", json); // Deserialize from JSON let deserialized: Config = serde_json::from_str(&json)?; println!("Deserialized: {:?}", deserialized); // HashMap-specific serialization let mut map = HashMap::new(); map.insert("key1", "value1"); map.insert("key2", "value2"); map.insert("key3", "value3"); let json_map = serde_json::to_string(&map)?; println!("Map as JSON: {}", json_map); // Parse JSON into HashMap let json_data = r#"{"name":"Alice","age":"30","city":"New York"}"#; let parsed: HashMap<String, String> = serde_json::from_str(json_data)?; println!("Parsed JSON: {:?}", parsed); Ok(()) } 9. Common Patterns and Use Cases
Caching and Memoization
use std::collections::HashMap; use std::time::{Duration, Instant}; struct Cache<T> { data: HashMap<String, (T, Instant)>, ttl: Duration, } impl<T: Clone> Cache<T> { fn new(ttl: Duration) -> Self { Cache { data: HashMap::new(), ttl, } } fn get(&self, key: &str) -> Option<&T> { self.data.get(key).and_then(|(value, time)| { if time.elapsed() < self.ttl { Some(value) } else { None } }) } fn insert(&mut self, key: String, value: T) { self.data.insert(key, (value, Instant::now())); } fn cleanup(&mut self) { let now = Instant::now(); self.data.retain(|_, (_, time)| now.duration_since(*time) < self.ttl); } } fn main() { let mut cache: Cache<String> = Cache::new(Duration::from_secs(5)); cache.insert("user_1".to_string(), "Alice".to_string()); cache.insert("user_2".to_string(), "Bob".to_string()); println!("Cached user_1: {:?}", cache.get("user_1")); println!("Cached user_3: {:?}", cache.get("user_3")); std::thread::sleep(Duration::from_secs(6)); cache.cleanup(); println!("After TTL, user_1: {:?}", cache.get("user_1")); } Index and Counter
use std::collections::HashMap; struct Indexer { word_to_id: HashMap<String, usize>, id_to_word: HashMap<usize, String>, next_id: usize, } impl Indexer { fn new() -> Self { Indexer { word_to_id: HashMap::new(), id_to_word: HashMap::new(), next_id: 0, } } fn get_or_create_id(&mut self, word: &str) -> usize { if let Some(&id) = self.word_to_id.get(word) { id } else { let id = self.next_id; self.word_to_id.insert(word.to_string(), id); self.id_to_word.insert(id, word.to_string()); self.next_id += 1; id } } fn get_word(&self, id: usize) -> Option<&String> { self.id_to_word.get(&id) } fn get_id(&self, word: &str) -> Option<&usize> { self.word_to_id.get(word) } fn size(&self) -> usize { self.word_to_id.len() } } fn main() { let mut indexer = Indexer::new(); let words = vec!["hello", "world", "hello", "rust", "world", "programming"]; for word in words { let id = indexer.get_or_create_id(word); println!("Word '{}' has id {}", word, id); } println!("\nReverse lookup:"); for id in 0..indexer.size() { if let Some(word) = indexer.get_word(id) { println!("ID {}: '{}'", id, word); } } } Frequency Counter
use std::collections::HashMap; use std::hash::Hash; struct FrequencyCounter<T> { counts: HashMap<T, usize>, } impl<T: Eq + Hash> FrequencyCounter<T> { fn new() -> Self { FrequencyCounter { counts: HashMap::new(), } } fn add(&mut self, item: T) { *self.counts.entry(item).or_insert(0) += 1; } fn add_many(&mut self, items: Vec<T>) { for item in items { self.add(item); } } fn frequency(&self, item: &T) -> usize { self.counts.get(item).copied().unwrap_or(0) } fn most_frequent(&self) -> Option<(&T, &usize)> { self.counts.iter().max_by_key(|(_, &count)| count) } fn least_frequent(&self) -> Option<(&T, &usize)> { self.counts.iter().min_by_key(|(_, &count)| count) } fn total_count(&self) -> usize { self.counts.values().sum() } fn unique_count(&self) -> usize { self.counts.len() } fn frequencies(&self) -> &HashMap<T, usize> { &self.counts } fn clear(&mut self) { self.counts.clear(); } } fn main() { let mut counter = FrequencyCounter::new(); let data = vec![ "apple", "banana", "apple", "orange", "banana", "apple", "grape", "orange", ]; counter.add_many(data); println!("Frequency of 'apple': {}", counter.frequency(&"apple")); println!("Frequency of 'banana': {}", counter.frequency(&"banana")); println!("Most frequent: {:?}", counter.most_frequent()); println!("Least frequent: {:?}", counter.least_frequent()); println!("Total count: {}", counter.total_count()); println!("Unique items: {}", counter.unique_count()); println!("All frequencies: {:?}", counter.frequencies()); } Graph Representation
use std::collections::{HashMap, HashSet, VecDeque}; struct Graph<T> { adjacency_list: HashMap<T, HashSet<T>>, } impl<T: Eq + Hash + Clone> Graph<T> { fn new() -> Self { Graph { adjacency_list: HashMap::new(), } } fn add_vertex(&mut self, vertex: T) { self.adjacency_list.entry(vertex).or_insert_with(HashSet::new); } fn add_edge(&mut self, from: T, to: T) { self.adjacency_list .entry(from.clone()) .or_insert_with(HashSet::new) .insert(to.clone()); self.adjacency_list.entry(to).or_insert_with(HashSet::new); } fn add_bidirectional_edge(&mut self, a: T, b: T) { self.add_edge(a.clone(), b.clone()); self.add_edge(b, a); } fn neighbors(&self, vertex: &T) -> Option<&HashSet<T>> { self.adjacency_list.get(vertex) } fn bfs(&self, start: T) -> Vec<T> { let mut visited = HashSet::new(); let mut queue = VecDeque::new(); let mut result = Vec::new(); visited.insert(start.clone()); queue.push_back(start); while let Some(vertex) = queue.pop_front() { result.push(vertex.clone()); if let Some(neighbors) = self.adjacency_list.get(&vertex) { for neighbor in neighbors { if !visited.contains(neighbor) { visited.insert(neighbor.clone()); queue.push_back(neighbor.clone()); } } } } result } fn has_cycle(&self) -> bool { let mut visited = HashSet::new(); let mut recursion_stack = HashSet::new(); for vertex in self.adjacency_list.keys() { if !visited.contains(vertex) { if self.has_cycle_helper(vertex, &mut visited, &mut recursion_stack) { return true; } } } false } fn has_cycle_helper(&self, vertex: &T, visited: &mut HashSet<T>, stack: &mut HashSet<T>) -> bool { visited.insert(vertex.clone()); stack.insert(vertex.clone()); if let Some(neighbors) = self.adjacency_list.get(vertex) { for neighbor in neighbors { if !visited.contains(neighbor) { if self.has_cycle_helper(neighbor, visited, stack) { return true; } } else if stack.contains(neighbor) { return true; } } } stack.remove(vertex); false } fn shortest_path(&self, start: T, end: T) -> Option<Vec<T>> { let mut visited = HashSet::new(); let mut queue = VecDeque::new(); let mut parent = HashMap::new(); visited.insert(start.clone()); queue.push_back(start.clone()); while let Some(vertex) = queue.pop_front() { if vertex == end { return Some(self.reconstruct_path(parent, start, end)); } if let Some(neighbors) = self.adjacency_list.get(&vertex) { for neighbor in neighbors { if !visited.contains(neighbor) { visited.insert(neighbor.clone()); parent.insert(neighbor.clone(), vertex.clone()); queue.push_back(neighbor.clone()); } } } } None } fn reconstruct_path(&self, parent: HashMap<T, T>, start: T, end: T) -> Vec<T> { let mut path = Vec::new(); let mut current = end; path.push(current.clone()); while current != start { if let Some(prev) = parent.get(¤t) { path.push(prev.clone()); current = prev.clone(); } else { break; } } path.reverse(); path } } fn main() { let mut graph = Graph::new(); graph.add_bidirectional_edge(1, 2); graph.add_bidirectional_edge(1, 3); graph.add_bidirectional_edge(2, 4); graph.add_bidirectional_edge(3, 4); graph.add_bidirectional_edge(4, 5); println!("BFS from 1: {:?}", graph.bfs(1)); println!("Has cycle: {}", graph.has_cycle()); if let Some(path) = graph.shortest_path(1, 5) { println!("Shortest path: {:?}", path); } // Directed graph with cycle let mut directed = Graph::new(); directed.add_edge(1, 2); directed.add_edge(2, 3); directed.add_edge(3, 1); println!("Directed has cycle: {}", directed.has_cycle()); } 10. Concurrency with HashMap
Thread-safe HashMap
use std::collections::HashMap; use std::sync::{Arc, Mutex}; use std::thread; fn main() { // Shared HashMap with Mutex let shared_map = Arc::new(Mutex::new(HashMap::new())); let mut handles = vec![]; for i in 0..10 { let map_clone = shared_map.clone(); handles.push(thread::spawn(move || { let mut map = map_clone.lock().unwrap(); map.insert(i, format!("Value {}", i)); })); } for handle in handles { handle.join().unwrap(); } println!("Final map: {:?}", shared_map.lock().unwrap()); // Multiple readers with RwLock use std::sync::RwLock; let shared_map = Arc::new(RwLock::new(HashMap::new())); // Writers let mut write_handles = vec![]; for i in 0..5 { let map_clone = shared_map.clone(); write_handles.push(thread::spawn(move || { let mut map = map_clone.write().unwrap(); map.insert(i, i * 10); })); } // Readers let mut read_handles = vec![]; for _ in 0..5 { let map_clone = shared_map.clone(); read_handles.push(thread::spawn(move || { let map = map_clone.read().unwrap(); println!("Map size: {}", map.len()); })); } for handle in write_handles { handle.join().unwrap(); } for handle in read_handles { handle.join().unwrap(); } // Concurrent HashMap with DashMap (external crate) // use dashmap::DashMap; // let map = DashMap::new(); // rayon::scope(|s| { // for i in 0..1000 { // s.spawn(|_| { // map.insert(i, i * 2); // }); // } // }); } 11. Advanced HashMap Features
Custom Hashers
use std::collections::HashMap; use std::hash::{BuildHasherDefault, Hasher}; // Simple identity hasher (for integers only - not safe for real use) struct IdentityHasher { value: u64, } impl Hasher for IdentityHasher { fn finish(&self) -> u64 { self.value } fn write(&mut self, bytes: &[u8]) { // Simplified for example - not correct for general use if bytes.len() == 8 { self.value = u64::from_le_bytes(bytes.try_into().unwrap()); } } } type IdentityBuildHasher = BuildHasherDefault<IdentityHasher>; // FNV hasher (often faster for small keys) use fnv::FnvHashMap; // External crate fn main() { // Using custom hasher let mut map: HashMap<i32, String, IdentityBuildHasher> = HashMap::with_hasher(IdentityBuildHasher::default()); map.insert(1, "one".to_string()); map.insert(2, "two".to_string()); println!("Custom hasher map: {:?}", map); // Using FNV (faster for small keys) let mut fnv_map: HashMap<i32, String> = HashMap::default(); fnv_map.insert(1, "one".to_string()); fnv_map.insert(2, "two".to_string()); // Alternative using fnv crate // use fnv::FnvHashMap; // let mut map = FnvHashMap::default(); // map.insert(1, "one"); } Performance Optimization Tips
use std::collections::HashMap; fn main() { // 1. Pre-allocate capacity when size is known let expected_size = 1000; let mut map = HashMap::with_capacity(expected_size); // 2. Use entry API for conditional inserts/updates let mut map = HashMap::new(); // Efficient - single lookup *map.entry("key").or_insert(0) += 1; // Inefficient - two lookups // if !map.contains_key("key") { // map.insert("key", 0); // } // *map.get_mut("key").unwrap() += 1; // 3. Choose appropriate key type // &str vs String - &str can't be owned by HashMap let mut map1: HashMap<&str, i32> = HashMap::new(); map1.insert("key", 1); // Key is borrowed let mut map2: HashMap<String, i32> = HashMap::new(); map2.insert("key".to_string(), 1); // Key is owned // 4. Consider using `BTreeMap` if ordering needed // BTreeMap has slower insertion but maintains order // 5. Use `shrink_to_fit` to free memory let mut map = HashMap::with_capacity(1000); for i in 0..100 { map.insert(i, i); } map.shrink_to_fit(); // 6. Clone vs retain let mut map = HashMap::new(); for i in 0..100 { map.insert(i, i * 10); } // Filter and create new let filtered: HashMap<_, _> = map .into_iter() .filter(|(k, _)| k % 2 == 0) .collect(); println!("Filtered: {:?}", filtered); } Conclusion
HashMap is one of the most versatile and commonly used data structures in Rust:
Key Takeaways
- Fast Operations: Average O(1) for insert, get, remove
- Flexible Keys: Any type implementing
Eq + Hash - Entry API: Efficient conditional operations
- Ownership: HashMap owns its keys and values
- No Order: Iteration order is not guaranteed
- Customizable: Can use custom hashers for specific needs
Best Practices
- Use entry API for conditional inserts and updates
- Pre-allocate capacity when size is known
- Choose appropriate key types (borrowed vs owned)
- Consider BTreeMap if ordering is needed
- Handle missing keys with
getorentryinstead of direct indexing - Use
shrink_to_fitafter removing many elements - Consider performance implications of different hashers
Common Use Cases
- Caching and memoization
- Counting frequencies
- Building indexes
- Representing graphs
- Configuration storage
- Lookup tables
- Grouping data
HashMap is an essential tool in Rust programming, providing fast and flexible key-value storage with strong type safety guarantees.