September 29, 2025
Arrays are collections of data stored in sequential order. Each element is assigned a numerical index, which allows for quick access. For example:
graph LR
Array["Array"]
Index0["Index: 0"]
Index1["Index: 1"]
Index2["Index: 2"]
Value0["Value: 10"]
Value1["Value: 20"]
Value2["Value: 30"]
Array -->|points to| Index0
Array -->|points to| Index1
Array -->|points to| Index2
Index0 -->|points to| Value0
Index1 -->|points to| Value1
Index2 -->|points to| Value2
HashMaps solve this limitation by allowing data to be stored and accessed using keys instead of indices. For example:
graph TD
HashMap["HashMap"]
Key1["Key: 'Alice'"]
Key2["Key: 'Bob'"]
Key3["Key: 'Charlie'"]
HashFunction["Hash Function"]
Index0["Index: 0"]
Index1["Index: 1"]
Index2["Index: 2"]
Value1["Value: 123456"]
Value2["Value: 789012"]
Value3["Value: 345678"]
HashMap -->|stores| Key1
HashMap -->|stores| Key2
HashMap -->|stores| Key3
Key1 -->|hashed by| HashFunction
Key2 -->|hashed by| HashFunction
Key3 -->|hashed by| HashFunction
HashFunction -->|maps to| Index0
HashFunction -->|maps to| Index1
HashFunction -->|maps to| Index2
Index0 -->|points to| Value1
Index1 -->|points to| Value2
Index2 -->|points to| Value3
At their core, HashMaps are arrays, but they use a hash function to convert keys into indices. Here’s how it works:
fn simple_hash(string: &str) -> u32 {
let mut total = 0;
for c in string.chars() {
total += c as u32;
}
total
}
%) ensures the index falls within the array bounds.A "perfect" hash function should:
However, achieving all these properties simultaneously is often impossible, so trade-offs are made.
Collisions occur when two keys hash to the same index. A common solution is to use a linked list at each index. When a collision happens:
graph LR
Index0["Index: 0"]
KeyValue1["Key: 'Alice', Value: 123456"]
KeyValue2["Key: 'Eve', Value: 567890"]
KeyValue3["Key: 'Grace', Value: 901234"]
None["None"]
Index0 -->|points to| KeyValue1
KeyValue1 -->|next| KeyValue2
KeyValue2 -->|next| KeyValue3
KeyValue3 -->|next| None
We’ll implement four core operations:
HashMap<T, V>.get(key: T)HashMap<T, V>.push(key: T, value: V)HashMap<T, V>.delete(key: T)HashMap<T, V>.clear()cargo init --lib
src/lib.rs with the following:
const DEFAULT_MAX_SIZE: u64 = 256;
pub struct HashMap<T, V> {
curr_size: usize,
arr: [Option<KeyValue<T, V>>; DEFAULT_MAX_SIZE as usize],
}
pub struct KeyValue<T, V> {
key: T,
value: V,
next: Option<Box<KeyValue<T, V>>>,
}
putThe put function inserts a key-value pair into the HashMap:
impl<T: Clone + Hash + PartialEq, V: Copy> HashMap<T, V> {
pub fn put(&mut self, key: T, val: V) -> Option<V> {
let hash_val: u64 = hash_key(key.clone());
let position = hash_val % DEFAULT_MAX_SIZE;
match &self.arr[position as usize] {
Some(_) => self.update_or_link_new_val(key, val, position as usize),
None => {
self.insert_new_value(key, val, position as usize);
None
}
}
}
}
graph LR
HashMap["HashMap"]
Key["Key: 'Dave'"]
HashFunction["Hash Function"]
Index3["Index: 3"]
KeyValue["Key: 'Dave', Value: 456789"]
None["None"]
HashMap -->|inserts| Key
Key -->|hashed by| HashFunction
HashFunction -->|maps to| Index3
Index3 -->|points to| KeyValue
KeyValue -->|next| None
getThe get function retrieves a value by its key:
pub fn get(&self, key: T) -> Option<V> {
let hash_val: u64 = hash_key(key.clone());
let position = hash_val % DEFAULT_MAX_SIZE;
match &self.arr[position as usize] {
Some(_) => self.check_list_for_key(key, position as usize),
None => None,
}
}
graph LR
HashMap["HashMap"]
Key["Key: 'Bob'"]
HashFunction["Hash Function"]
Index1["Index: 1"]
KeyValue["Key: 'Bob', Value: 789012"]
HashMap -->|searches for| Key
Key -->|hashed by| HashFunction
HashFunction -->|maps to| Index1
Index1 -->|points to| KeyValue
removeThe remove function deletes a key-value pair:
pub fn remove(&mut self, key: T) -> Option<V> {
let hash_val: u64 = hash_key(key.clone());
let position: u64 = hash_val % DEFAULT_MAX_SIZE;
match &self.arr[position as usize] {
Some(_) => self.check_item_in_list_and_remove(key, position as usize),
None => None,
}
}
graph LR
HashMap["HashMap"]
Key["Key: 'Charlie'"]
HashFunction["Hash Function"]
Index2["Index: 2"]
KeyValue["Key: 'Charlie', Value: 345678"]
None["None"]
HashMap -->|removes| Key
Key -->|hashed by| HashFunction
HashFunction -->|maps to| Index2
Index2 -->|updates to| None
clearThe clear function resets the HashMap to its initial state:
pub fn clear(&mut self) {
self.curr_size = 0;
self.arr = [Self::INIT; DEFAULT_MAX_SIZE as usize];
}
graph LR
HashMap["HashMap"]
Array["Array"]
EmptyArray["Empty Array"]
HashMap -->|clears| Array
Array -->|becomes| EmptyArray
Congratulations! You’ve implemented a HashMap in Rust from scratch. You now understand:
put, get, remove, and clear.