Skip to content

Commit 0cea3d9

Browse files
feat: add custom HashMap implementation with resizing, collision handling, and tests
This PR adds a complete beginner-friendly HashMap implementation from scratch, following The Odin Project’s assignment. It includes a hash function, dynamic resizing, proper collision handling, utility methods (set, get, has, etc.), test cases (test.js), and notes-style documentation (README.md).
2 parents 2c581e2 + 57b42de commit 0cea3d9

File tree

3 files changed

+316
-0
lines changed

3 files changed

+316
-0
lines changed

hashmap/README.md

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# 🧠 HashMap (from scratch)
2+
3+
This project is part of The Odin Project's JavaScript curriculum.
4+
In this, I built a custom `HashMap` class without using JavaScript’s built-in `Map` or object key-value behavior.
5+
6+
---
7+
8+
## 📦 Key Concepts
9+
10+
- A **HashMap** stores key-value pairs.
11+
- Each **key is converted into an index** using a `hash()` function.
12+
- The key-value pair is then stored in a **bucket** (an array at that index).
13+
- **Collisions** are handled using **chaining** (each bucket is an array of pairs).
14+
- **Resizing** happens when the load factor exceeds a threshold (default 0.75).
15+
16+
---
17+
18+
## 🧰 Internal Properties
19+
20+
| Property | Purpose |
21+
|--------------|---------|
22+
| `capacity` | Total number of buckets (initial: 16) |
23+
| `size` | Number of stored key-value pairs |
24+
| `loadFactor` | Max ratio of size/capacity before resizing (default: 0.75) |
25+
| `buckets` | Array of arrays storing key-value pairs |
26+
27+
---
28+
29+
## ⚙️ Hash Function
30+
31+
```js
32+
hash(key) {
33+
let hashCode = 0;
34+
const prime = 31;
35+
36+
for (let i = 0; i < key.length; i++) {
37+
hashCode = (prime * hashCode + key.charCodeAt(i)) % this.capacity;
38+
}
39+
40+
return hashCode;
41+
}
42+
```
43+
44+
---
45+
46+
## ⚙️ Hash Function Details
47+
48+
- ✅ Uses a **prime multiplier** for better distribution.
49+
- ✅ Applies **modulo during the loop** to avoid integer overflow.
50+
- ✅ Assumes **all keys are strings** (as required by the assignment).
51+
52+
---
53+
54+
## ✅ Methods Implemented
55+
56+
### `set(key, value)`
57+
- Adds a new key-value pair.
58+
- If the key already exists, updates the value.
59+
- Triggers a resize if the load factor is exceeded.
60+
61+
### `get(key)`
62+
- Returns the value for a key, or `null` if not found.
63+
64+
### `has(key)`
65+
- Returns `true` if the key exists, else `false`.
66+
67+
### `remove(key)`
68+
- Deletes the key-value pair.
69+
- Returns `true` if deleted, `false` if not found.
70+
71+
### `length()`
72+
- Returns the number of stored key-value pairs.
73+
74+
### `clear()`
75+
- Removes all entries from the map.
76+
77+
### `keys()`
78+
- Returns an array of all keys.
79+
80+
### `values()`
81+
- Returns an array of all values.
82+
83+
### `entries()`
84+
- Returns an array of `[key, value]` pairs.
85+
86+
---
87+
88+
## 🧪 Testing (in `test.js`)
89+
90+
- Inserted 12 values (hit 0.75 load factor)
91+
- Overwrote some keys (length stayed the same)
92+
- Inserted 13th value (`"moon"`) → triggered resize (capacity doubled)
93+
- After resizing, verified that all other methods (`get`, `has`, `remove`, etc.) still worked correctly
94+
95+
---
96+
97+
## 💡 Notes to Self
98+
99+
- Always access **buckets using the hashed index**, never directly with the key.
100+
- For collision handling, always **check if the key exists inside the bucket array**.
101+
- During resizing, rehash and reinsert **every key-value pair**.
102+
- Only increment `size` **when adding a new key**, not when updating an existing one.
103+
104+
---
105+
106+
## 🌱 Possible Improvements
107+
108+
- Support **more data types as keys** (e.g., numbers, symbols).
109+
- Add a `forEach()` method like the native `Map`.
110+
- Implement a **`HashSet`** using similar internal logic.
111+
112+
---
113+
114+
## 📁 File Structure
115+
116+
```
117+
hashmap/
118+
├── hashMap.js # Main class
119+
├── test.js # Manual testing file
120+
└── README.md # You're here!
121+
```
122+
123+
---

hashmap/hashMap.js

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
class HashMap {
2+
constructor() {
3+
this.capacity = 16;
4+
this.size = 0;
5+
this.loadFactor = 0.75;
6+
this.buckets = new Array(this.capacity).fill(null).map(() => []);
7+
}
8+
9+
hash(key) {
10+
let hashCode = 0;
11+
const prime = 31;
12+
13+
for (let i = 0; i < key.length; i++) {
14+
hashCode = (prime * hashCode + key.charCodeAt(i)) % this.capacity;
15+
}
16+
17+
return hashCode;
18+
}
19+
20+
checkIndexBounds(index) {
21+
if (index < 0 || index >= this.buckets.length) {
22+
throw new Error("Trying to access index out of bounds");
23+
}
24+
}
25+
26+
set(key, value) {
27+
const index = this.hash(key);
28+
this.checkIndexBounds(index);
29+
30+
const bucket = this.buckets[index];
31+
32+
for (let i = 0; i < bucket.length; i++) {
33+
if (bucket[i][0] === key) {
34+
bucket[i][1] = value;
35+
return;
36+
}
37+
}
38+
39+
bucket.push([key, value]);
40+
this.size++;
41+
42+
if (this.size / this.capacity > this.loadFactor) {
43+
this.resize();
44+
}
45+
}
46+
47+
get(key) {
48+
const index = this.hash(key);
49+
this.checkIndexBounds(index);
50+
51+
const bucket = this.buckets[index];
52+
53+
for (let i = 0; i < bucket.length; i++) {
54+
if (bucket[i][0] === key) {
55+
return bucket[i][1];
56+
}
57+
}
58+
59+
return null;
60+
}
61+
62+
has(key) {
63+
return this.get(key) !== null;
64+
}
65+
66+
remove(key) {
67+
const index = this.hash(key);
68+
this.checkIndexBounds(index);
69+
70+
const bucket = this.buckets[index];
71+
72+
for (let i = 0; i < bucket.length; i++) {
73+
if (bucket[i][0] === key) {
74+
bucket.splice(i, 1);
75+
this.size--;
76+
return true;
77+
}
78+
}
79+
80+
return false;
81+
}
82+
83+
length() {
84+
return this.size;
85+
}
86+
87+
clear() {
88+
this.buckets = new Array(this.capacity).fill(null).map(() => []);
89+
this.size = 0;
90+
}
91+
92+
keys() {
93+
const keysArray = [];
94+
95+
for (let bucket of this.buckets) {
96+
for (let [key, _] of bucket) {
97+
keysArray.push(key);
98+
}
99+
}
100+
101+
return keysArray;
102+
}
103+
104+
values() {
105+
const valuesArray = [];
106+
107+
for (let bucket of this.buckets) {
108+
for (let [_, value] of bucket) {
109+
valuesArray.push(value);
110+
}
111+
}
112+
113+
return valuesArray;
114+
}
115+
116+
entries() {
117+
const entriesArray = [];
118+
119+
for (let bucket of this.buckets) {
120+
for (let [key, value] of bucket) {
121+
entriesArray.push([key, value]);
122+
}
123+
}
124+
125+
return entriesArray;
126+
}
127+
128+
resize() {
129+
const oldBuckets = this.buckets;
130+
this.capacity *= 2;
131+
this.buckets = new Array(this.capacity).fill(null).map(() => []);
132+
this.size = 0;
133+
134+
for (let bucket of oldBuckets) {
135+
for (let [key, value] of bucket) {
136+
this.set(key, value);
137+
}
138+
}
139+
}
140+
}
141+
142+
module.exports = HashMap;

hashmap/test.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
const HashMap = require('./hashMap');
2+
const map = new HashMap();
3+
4+
// Fill to capacity (0.75 * 16 = 12 items)
5+
map.set('apple', 'red');
6+
map.set('banana', 'yellow');
7+
map.set('carrot', 'orange');
8+
map.set('dog', 'brown');
9+
map.set('elephant', 'gray');
10+
map.set('frog', 'green');
11+
map.set('grape', 'purple');
12+
map.set('hat', 'black');
13+
map.set('ice cream', 'white');
14+
map.set('jacket', 'blue');
15+
map.set('kite', 'pink');
16+
map.set('lion', 'golden');
17+
18+
console.log('Length after 12 inserts:', map.length()); // Expect: 12
19+
console.log('Capacity before resize:', map.capacity); // Expect: 16
20+
21+
// Overwrite some values
22+
map.set('apple', 'dark red');
23+
map.set('banana', 'light yellow');
24+
25+
console.log('Updated apple:', map.get('apple')); // Expect: dark red
26+
console.log('Updated banana:', map.get('banana')); // Expect: light yellow
27+
console.log('Length should still be 12:', map.length()); // No change
28+
29+
// Trigger resize with 13th item
30+
map.set('moon', 'silver');
31+
32+
console.log('Length after resize:', map.length()); // Expect: 13
33+
console.log('Capacity after resize:', map.capacity); // Expect: 32
34+
35+
// Test has()
36+
console.log('Has "moon"?', map.has('moon')); // true
37+
console.log('Has "sun"?', map.has('sun')); // false
38+
39+
// Test remove()
40+
console.log('Remove "dog":', map.remove('dog')); // true
41+
console.log('Remove "sun":', map.remove('sun')); // false
42+
43+
// Test keys, values, entries
44+
console.log('All keys:', map.keys());
45+
console.log('All values:', map.values());
46+
console.log('All entries:', map.entries());
47+
48+
// Clear
49+
map.clear();
50+
console.log('Length after clear:', map.length()); // Expect: 0
51+
console.log('Entries after clear:', map.entries()); // Expect: []

0 commit comments

Comments
 (0)