Implementing a Basic HashMap in Rust
This challenge asks you to implement a simplified HashMap data structure in Rust. HashMaps are fundamental for efficient key-value storage and retrieval, and understanding their underlying mechanics is crucial for any serious programmer. This exercise will solidify your understanding of Rust's ownership, borrowing, and hashing concepts.
Problem Description
You are to implement a basic HashMap struct in Rust. This HashMap should support the following operations:
insert(key: K, value: V): Inserts a key-value pair into the HashMap. If the key already exists, the old value should be overwritten.get(key: &K): Retrieves the value associated with the given key. ReturnsSome(&V)if the key exists, andNoneotherwise.remove(key: &K): Removes the key-value pair associated with the given key. ReturnsSome(V)if the key existed and was removed, andNoneotherwise.len(): Returns the number of key-value pairs currently stored in the HashMap.
The HashMap should use a fixed-size array as its underlying storage (a "bucket array"). A simple modulo hashing function should be used to determine the bucket index for a given key. Handle collisions using separate chaining (each bucket will be a Vec of key-value pairs).
Key Requirements:
- The
HashMapshould be generic over key typeKand value typeV. - The key type
Kmust implement theEqandHashtraits. - The bucket array size should be a compile-time constant.
- Implement the
Droptrait to ensure all allocated memory is freed when theHashMapgoes out of scope. - Handle potential panics gracefully (e.g., when the bucket array is full).
Expected Behavior:
The HashMap should behave as expected for standard insertion, retrieval, and deletion operations. It should correctly handle collisions and overwrite existing values when inserting duplicate keys. The len() function should accurately reflect the number of elements stored.
Edge Cases to Consider:
- Empty HashMap: Ensure all operations work correctly when the HashMap is empty.
- Duplicate Keys: Insertion of duplicate keys should overwrite the existing value.
- Key Not Found:
get()andremove()should returnNonewhen the key is not present. - Hashing Collisions: The separate chaining strategy should correctly handle multiple keys hashing to the same bucket.
- Memory Management: Ensure that memory is properly allocated and deallocated to prevent leaks.
Examples
Example 1:
Input:
let mut map: HashMap<String, i32> = HashMap::new();
map.insert("one".to_string(), 1);
map.insert("two".to_string(), 2);
map.insert("three".to_string(), 3);
let value1 = map.get("two");
let value2 = map.get("four");
Output:
value1: Some(&2)
value2: None
Explanation: The HashMap is initialized and populated with three key-value pairs. Retrieving the value associated with "two" returns Some(&2), while retrieving the value associated with "four" (which doesn't exist) returns None.
Example 2:
Input:
let mut map: HashMap<i32, String> = HashMap::new();
map.insert(1, "one".to_string());
map.insert(2, "two".to_string());
map.insert(1, "new_one".to_string()); // Overwrite existing key
let value1 = map.get(&1);
let value2 = map.get(&3);
Output:
value1: Some(&"new_one")
value2: None
Explanation: Inserting a duplicate key (1) overwrites the existing value. Retrieving the value associated with key 1 now returns Some(&"new_one".
Example 3:
Input:
let mut map: HashMap<String, i32> = HashMap::new();
map.insert("a".to_string(), 10);
let removed_value = map.remove("a");
let value = map.get("a");
Output:
removed_value: Some(10)
value: None
Explanation: Removing the key "a" returns the associated value (10), and subsequent attempts to retrieve the value associated with "a" return None.
Constraints
- Bucket Array Size: The bucket array size must be a compile-time constant, e.g.,
const BUCKET_SIZE: usize = 16;. - Key Type: The key type
Kmust implementEqandHash. - Value Type: The value type
Vcan be any type. - No External Crates: You are not allowed to use external crates (e.g.,
std::collections::HashMap). You must implement the HashMap from scratch. - Performance: While not a primary focus, strive for reasonable performance. Avoid unnecessary allocations or copies. The hash function should be reasonably efficient.
Notes
- Start by defining the
HashMapstruct and the bucket array. - Implement the
Hashtrait for your key type if it's not already implemented. A simple identity hash function is sufficient for this exercise. - Consider using a
Vecto represent each bucket in the bucket array. - Pay close attention to ownership and borrowing rules when inserting, retrieving, and removing elements.
- The
Droptrait implementation is crucial for preventing memory leaks. Make sure to iterate through the bucket array and drop any remaining values. - Test your implementation thoroughly with various scenarios, including edge cases.
- Remember to handle collisions using separate chaining.