Rust
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
78: Implementing a Hash Table in Rust
I've always felt that the best way to truly understand how a HashMap works under the hood is to build a stripped-down version of one. We aren't going to beat the standard library's performance—std::collections::HashMap uses a sophisticated algorithm called Swiss Tables—but for our purposes, we'll build a classic hash table using separate chaining. This is the "textbook" approach where each slot in our array (a bucket) holds a list of entries that hashed to the same index.
Setting up the bucket array
First, we need a structure. I want this to be generic so it can handle different types of keys and values, but we have to ensure the key can be hashed. In Rust, that means the key must implement Hash and Eq. I'll use a Vec of Vecs for my buckets. Each inner Vec will store tuples of (K, V).
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
struct SimpleHash<K, V> {
buckets: Vec<Vec<(K, V)>>,
capacity: usize,
}
impl<K: Hash + Eq, V> SimpleHash<K, V> {
fn new(capacity: usize) -> Self {
let mut buckets = Vec::with_capacity(capacity);
for _ in 0..capacity {
buckets.push(Vec::new());
}
SimpleHash { buckets, capacity }
}
}
Mapping keys to indices
Now we need a way to turn a key into an index within our buckets vector. I'll create a private helper method for this. I'm using DefaultHasher here because it's built-in and reliable. I'll feed the key into the hasher and then use the modulo operator to keep the result within the bounds of our capacity.
impl<K: Hash + Eq, V> SimpleHash<K, V> {
// ... new() method ...
fn calculate_hash(&self, key: &K) -> usize {
let mut s = DefaultHasher::new();
key.hash(&mut s);
(s.finish() as usize) % self.capacity
}
}
The mistake: Ignoring existing keys
When I first sketched out the insert method, I did something lazy. I just calculated the index and pushed the key-value pair into the bucket. It felt efficient at the time, but here is what that looked like:
// WRONG VERSION - Don't do this!
fn insert(&mut self, key: K, value: V) {
let index = self.calculate_hash(&key);
self.buckets[index].push((key, value));
}
I quickly realized the problem when I tried to update a value. If I inserted ("user_1", "Alice") and then inserted ("user_1", "Bob"), my bucket now contains both. When I call get, I'll likely find "Alice" first, and "Bob" will just sit there taking up space. That's not a map; that's just a weirdly organized list. I need to check if the key already exists and update it in place.
Fixing the insertion logic
To fix this, I'll iterate through the bucket. If I find the key, I'll update the value and return early. If I reach the end of the bucket without finding it, only then will I push a new pair. This keeps our data clean.
fn insert(&mut self, key: K, value: V) {
let index = self.calculate_hash(&key);
let bucket = &mut self.buckets[index];
for pair in bucket.iter_mut() {
if pair.0 == key {
pair.1 = value;
return;
}
}
bucket.push((key, value));
}
Retrieving values with references
Finally, we need a get method. Since we don't want to take ownership of the value (which would remove it from the table), we return an Option<&V>. This is the standard Rust pattern for lookups.
fn get(&self, key: &K) -> Option<&V> {
let index = self.calculate_hash(key);
let bucket = &self.buckets[index];
for (k, v) in bucket {
if k == key {
return Some(v);
}
}
None
}
And there we have it. It's not the fastest implementation in the world, but it demonstrates the core cycle of hashing: Key → Hash → Index → Bucket Search. By using Vec<Vec<(K, V)>>, we've handled collisions gracefully via separate chaining.
📋 Practical Task
Implementing a Remove Method for SimpleHash
Now it's your turn. The SimpleHash implementation we built is missing a crucial piece of functionality: the ability to delete a key-value pair.
Your task is to add a remove method to the SimpleHash struct. The method should:
- Take a reference to a key
&Kas an argument. - Calculate the correct bucket index.
- Find the element with the matching key within that bucket.
- Remove the element from the
Vecand return the valueVwrapped inSome(). - Return
Noneif the key was not found.
Hint: Look into the Vec::retain method or use position() combined with swap_remove() or remove() for efficient deletion from a vector.
There are no comments for now.